1 /**
2  * HibernateD - Object-Relation Mapping for D programming language, with interface similar to Hibernate. 
3  * 
4  * Hibernate documentation can be found here:
5  * $(LINK http://hibernate.org/docs)$(BR)
6  * 
7  * Source file hibernated/session.d.
8  *
9  * This module contains implementation of Hibernated SessionFactory and Session classes.
10  * 
11  * Copyright: Copyright 2013
12  * License:   $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
13  * Author:   Vadim Lopatin
14  */
15 module hibernated.session;
16 
17 import std.algorithm;
18 import std.conv;
19 import std.stdio;
20 import std.exception;
21 import std.variant;
22 
23 import ddbc.core;
24 import ddbc.common;
25 
26 import hibernated.type;
27 import hibernated.dialect;
28 import hibernated.core;
29 import hibernated.metadata;
30 import hibernated.query;
31 
32 const TRACE_REFS = false;
33 
34 /// Factory to create HibernateD Sessions - similar to org.hibernate.SessionFactory
35 interface SessionFactory {
36     /// close all active sessions
37 	void close();
38     /// check if session factory is closed
39 	bool isClosed();
40     /// creates new session
41 	Session openSession();
42     /// retrieve information about tables and indexes for schema
43     DBInfo getDBMetaData();
44 }
45 
46 /// Session - main interface to load and persist entities -- similar to org.hibernate.Session
47 abstract class Session
48 {
49     /// returns metadata
50     EntityMetaData getMetaData();
51 
52 	/// not supported in current implementation
53 	Transaction beginTransaction();
54 	/// not supported in current implementation
55 	void cancelQuery();
56 	/// not supported in current implementation
57 	void clear();
58 
59 	/// closes session
60 	Connection close();
61 
62     ///Does this session contain any changes which must be synchronized with the database? In other words, would any DML operations be executed if we flushed this session?
63     bool isDirty();
64     /// Check if the session is still open.
65     bool isOpen();
66     /// Check if the session is currently connected.
67     bool isConnected();
68     /// Check if this instance is associated with this Session.
69     bool contains(Object object);
70     /// Retrieve session factory used to create this session
71 	SessionFactory getSessionFactory();
72     /// Lookup metadata to find entity name for object.
73 	string getEntityName(Object object);
74     /// Lookup metadata to find entity name for class type info.
75     string getEntityName(TypeInfo_Class type);
76 
77     /// Return the persistent instance of the given named entity with the given identifier, or null if there is no such persistent instance.
78     T get(T : Object, ID)(ID id) {
79         Variant v = id;
80         return cast(T)getObject(getEntityName(T.classinfo), v);
81     }
82     
83     /// Read the persistent state associated with the given identifier into the given transient instance.
84     T load(T : Object, ID)(ID id) {
85         Variant v = id;
86         return cast(T)loadObject(getEntityName(T.classinfo), v);
87     }
88 
89     /// Read the persistent state associated with the given identifier into the given transient instance.
90     void load(T : Object, ID)(T obj, ID id) {
91         Variant v = id;
92         loadObject(obj, v);
93     }
94     
95     /// Return the persistent instance of the given named entity with the given identifier, or null if there is no such persistent instance.
96 	Object getObject(string entityName, Variant id);
97 
98     /// Read the persistent state associated with the given identifier into the given transient instance.
99     Object loadObject(string entityName, Variant id);
100 
101     /// Read the persistent state associated with the given identifier into the given transient instance
102     void loadObject(Object obj, Variant id);
103 
104     /// Re-read the state of the given instance from the underlying database.
105 	void refresh(Object obj);
106 
107     /// Persist the given transient instance, first assigning a generated identifier.
108 	Variant save(Object obj);
109 
110 	/// Persist the given transient instance.
111 	void persist(Object obj);
112 
113 	/// Update the persistent instance with the identifier of the given detached instance.
114 	void update(Object object);
115 
116     /// remove object from DB (renamed from original Session.delete - it's keyword in D)
117     void remove(Object object);
118 
119 	/// Create a new instance of Query for the given HQL query string
120 	Query createQuery(string queryString);
121 }
122 
123 /// Transaction interface: TODO
124 interface Transaction {
125 }
126 
127 /// Interface for usage of HQL queries.
128 abstract class Query
129 {
130 	///Get the query string.
131 	string 	getQueryString();
132 	/// Convenience method to return a single instance that matches the query, or null if the query returns no results.
133 	Object 	uniqueObject();
134     /// Convenience method to return a single instance that matches the query, or null if the query returns no results. Reusing existing buffer.
135     Object  uniqueObject(Object obj);
136     /// Convenience method to return a single instance that matches the query, or null if the query returns no results.
137     T uniqueResult(T : Object)() {
138         return cast(T)uniqueObject();
139     }
140     /// Convenience method to return a single instance that matches the query, or null if the query returns no results. Reusing existing buffer.
141     T uniqueResult(T : Object)(T obj) {
142         return cast(T)uniqueObject(obj);
143     }
144 
145     /// Convenience method to return a single instance that matches the query, or null if the query returns no results.
146 	Variant[] uniqueRow();
147 	/// Return the query results as a List of entity objects
148 	Object[] listObjects();
149     /// Return the query results as a List of entity objects
150     T[] list(T : Object)() {
151         return cast(T[])listObjects();
152     }
153     /// Return the query results as a List which each row as Variant array
154 	Variant[][] listRows();
155 	
156 	/// Bind a value to a named query parameter (all :parameters used in query should be bound before executing query).
157 	protected Query setParameterVariant(string name, Variant val);
158 
159     /// Bind a value to a named query parameter (all :parameters used in query should be bound before executing query).
160     Query setParameter(T)(string name, T val) {
161         static if (is(T == Variant)) {
162             return setParameterVariant(name, val);
163         } else {
164             return setParameterVariant(name, Variant(val));
165         }
166     }
167 }
168 
169 
170 /// Allows reaction to basic SessionFactory occurrences
171 interface SessionFactoryObserver {
172     ///Callback to indicate that the given factory has been closed.
173     void sessionFactoryClosed(SessionFactory factory);
174     ///Callback to indicate that the given factory has been created and is now ready for use.
175     void sessionFactoryCreated(SessionFactory factory);
176 }
177 
178 interface EventListeners {
179     // TODO:
180 }
181 
182 interface ConnectionProvider {
183 }
184 
185 interface Settings {
186     Dialect getDialect();
187     ConnectionProvider getConnectionProvider();
188     bool isAutoCreateSchema();
189 }
190 
191 interface Mapping {
192     string getIdentifierPropertyName(string className);
193     Type getIdentifierType(string className);
194     Type getReferencedPropertyType(string className, string propertyName);
195 }
196 
197 class Configuration {
198     bool dummy;
199 }
200 
201 class EntityCache {
202     const string name;
203     const EntityInfo entity;
204     Object[Variant] items;
205     this(const EntityInfo entity) {
206         this.entity = entity;
207         this.name = entity.name;
208     }
209     bool hasKey(Variant key) {
210         return ((key in items) !is null);
211     }
212     Object peek(Variant key) {
213         if ((key in items) is null)
214             return null;
215         return items[key];
216     }
217     Object get(Variant key) {
218         enforceEx!CacheException((key in items) !is null, "entity " ~ name ~ " with key " ~ key.toString() ~ " not found in cache");
219         return items[key];
220     }
221     void put(Variant key, Object obj) {
222         items[key] = obj;
223     }
224     Object[] lookup(Variant[] keys, out Variant[] unknownKeys) {
225         Variant[] unknown;
226         Object[] res;
227         foreach(key; keys) {
228             Object obj = peek(normalize(key));
229             if (obj !is null) {
230                 res ~= obj;
231             } else {
232                 unknown ~= normalize(key);
233             }
234         }
235         unknownKeys = unknown;
236         return res;
237     }
238 }
239 
240 /// helper class to disconnect Lazy loaders from closed session.
241 class SessionAccessor {
242     private SessionImpl _session;
243 
244     this(SessionImpl session) {
245         _session = session;
246     }
247     /// returns session, with session state check - throws LazyInitializationException if attempting to get unfetched lazy data while session is closed
248     SessionImpl get() {
249         enforceEx!LazyInitializationException(_session !is null, "Cannot read from closed session");
250         return _session;
251     }
252     /// nulls session reference
253     void onSessionClosed() {
254         _session = null;
255     }
256 }
257 
258 /// Implementation of HibernateD session
259 class SessionImpl : Session {
260 
261     private bool closed;
262     SessionFactoryImpl sessionFactory;
263     private EntityMetaData metaData;
264     Dialect dialect;
265     DataSource connectionPool;
266     Connection conn;
267 
268     EntityCache[string] cache;
269 
270     private SessionAccessor _accessor;
271     @property SessionAccessor accessor() {
272         return _accessor;
273     }
274 
275     package EntityCache getCache(string entityName) {
276         EntityCache res;
277         if ((entityName in cache) is null) {
278             res = new EntityCache(metaData[entityName]);
279             cache[entityName] = res;
280         } else {
281             res = cache[entityName];
282         }
283         return res;
284     }
285 
286     package bool keyInCache(string entityName, Variant key) {
287         return getCache(entityName).hasKey(normalize(key));
288     }
289     
290     package Object peekFromCache(string entityName, Variant key) {
291         return getCache(entityName).peek(normalize(key));
292     }
293 
294     package Object getFromCache(string entityName, Variant key) {
295         return getCache(entityName).get(normalize(key));
296     }
297 
298     package void putToCache(string entityName, Variant key, Object value) {
299         return getCache(entityName).put(normalize(key), value);
300     }
301 
302     package Object[] lookupCache(string entityName, Variant[] keys, out Variant[] unknownKeys) {
303         return getCache(entityName).lookup(keys, unknownKeys);
304     }
305     
306     override EntityMetaData getMetaData() {
307         return metaData;
308     }
309 
310     private void checkClosed() {
311         enforceEx!SessionException(!closed, "Session is closed");
312     }
313 
314     this(SessionFactoryImpl sessionFactory, EntityMetaData metaData, Dialect dialect, DataSource connectionPool) {
315         //writeln("Creating session");
316         this.sessionFactory = sessionFactory;
317         this.metaData = metaData;
318         this.dialect = dialect;
319         this.connectionPool = connectionPool;
320         this.conn = connectionPool.getConnection();
321         this._accessor = new SessionAccessor(this);
322     }
323 
324     override Transaction beginTransaction() {
325         throw new HibernatedException("Method not implemented");
326     }
327     override void cancelQuery() {
328         throw new HibernatedException("Method not implemented");
329     }
330     override void clear() {
331         throw new HibernatedException("Method not implemented");
332     }
333     ///Does this session contain any changes which must be synchronized with the database? In other words, would any DML operations be executed if we flushed this session?
334     override bool isDirty() {
335         throw new HibernatedException("Method not implemented");
336     }
337     /// Check if the session is still open.
338     override bool isOpen() {
339         return !closed;
340     }
341     /// Check if the session is currently connected.
342     override bool isConnected() {
343         return !closed;
344     }
345     /// End the session by releasing the JDBC connection and cleaning up.
346     override Connection close() {
347         checkClosed();
348         _accessor.onSessionClosed();
349         closed = true;
350         sessionFactory.sessionClosed(this);
351         //writeln("closing connection");
352         assert(conn !is null);
353         conn.close();
354         return null;
355     }
356     ///Check if this instance is associated with this Session
357     override bool contains(Object object) {
358         throw new HibernatedException("Method not implemented");
359     }
360     override SessionFactory getSessionFactory() {
361         checkClosed();
362         return sessionFactory;
363     }
364 
365     override string getEntityName(Object object) {
366         checkClosed();
367         return metaData.findEntityForObject(object).name;
368     }
369 
370     override string getEntityName(TypeInfo_Class type) {
371         checkClosed();
372         return metaData.getEntityName(type);
373     }
374 
375     override Object getObject(string entityName, Variant id) {
376         auto info = metaData.findEntity(entityName);
377         return getObject(info, null, id);
378     }
379 
380     /// Read the persistent state associated with the given identifier into the given transient instance.
381     override Object loadObject(string entityName, Variant id) {
382         Object obj = getObject(entityName, id);
383         enforceEx!ObjectNotFoundException(obj !is null, "Entity " ~ entityName ~ " with id " ~ to!string(id) ~ " not found");
384         return obj;
385     }
386 
387     /// Read the persistent state associated with the given identifier into the given transient instance
388     override void loadObject(Object obj, Variant id) {
389         auto info = metaData.findEntityForObject(obj);
390         Object found = getObject(info, obj, id);
391         enforceEx!ObjectNotFoundException(found !is null, "Entity " ~ info.name ~ " with id " ~ to!string(id) ~ " not found");
392     }
393 
394     /// Read the persistent state associated with the given identifier into the given transient instance
395     Object getObject(const EntityInfo info, Object obj, Variant id) {
396         string hql = "FROM " ~ info.name ~ " WHERE " ~ info.getKeyProperty().propertyName ~ "=:Id";
397         Query q = createQuery(hql).setParameter("Id", id);
398         Object res = q.uniqueResult(obj);
399         return res;
400     }
401     
402     /// Read entities referenced by property 
403     Object[] loadReferencedObjects(const EntityInfo info, string referencePropertyName, Variant fk) {
404         string hql = "SELECT a2 FROM " ~ info.name ~ " AS a1 JOIN a1." ~ referencePropertyName ~ " AS a2 WHERE a1." ~ info.getKeyProperty().propertyName ~ "=:Fk";
405         Query q = createQuery(hql).setParameter("Fk", fk);
406         Object[] res = q.listObjects();
407         return res;
408     }
409 
410     /// Re-read the state of the given instance from the underlying database.
411     override void refresh(Object obj) {
412         auto info = metaData.findEntityForObject(obj);
413         string query = metaData.generateFindByPkForEntity(dialect, info);
414         enforceEx!TransientObjectException(info.isKeySet(obj), "Cannot refresh entity " ~ info.name ~ ": no Id specified");
415         Variant id = info.getKey(obj);
416         //writeln("Finder query: " ~ query);
417         PreparedStatement stmt = conn.prepareStatement(query);
418         scope(exit) stmt.close();
419         stmt.setVariant(1, id);
420         ResultSet rs = stmt.executeQuery();
421         //writeln("returned rows: " ~ to!string(rs.getFetchSize()));
422         scope(exit) rs.close();
423         if (rs.next()) {
424             //writeln("reading columns");
425             metaData.readAllColumns(obj, rs, 1);
426             //writeln("value: " ~ obj.toString);
427         } else {
428             // not found!
429             enforceEx!ObjectNotFoundException(false, "Entity " ~ info.name ~ " with id " ~ to!string(id) ~ " not found");
430         }
431     }
432 
433     private void saveRelations(const EntityInfo ei, Object obj) {
434         foreach(p; ei) {
435             if (p.manyToMany) {
436                 saveRelations(p, obj);
437             }
438         }
439     }
440 
441     private void saveRelations(const PropertyInfo p, Object obj) {
442         Object[] relations = p.getCollectionFunc(obj);
443         Variant thisId = p.entity.getKey(obj);
444         if (relations !is null && relations.length > 0) {
445             string sql = p.joinTable.getInsertSQL(dialect);
446             string list;
447             foreach(r; relations) {
448                 Variant otherId = p.referencedEntity.getKey(r);
449                 if (list.length != 0)
450                     list ~= ", ";
451                 list ~= createKeyPairSQL(thisId, otherId);
452             }
453             sql ~= list;
454             Statement stmt = conn.createStatement();
455             scope(exit) stmt.close();
456             //writeln("sql: " ~ sql);
457             stmt.executeUpdate(sql);
458         }
459     }
460     
461     private void updateRelations(const EntityInfo ei, Object obj) {
462         foreach(p; ei) {
463             if (p.manyToMany) {
464                 updateRelations(p, obj);
465             }
466         }
467     }
468 
469     private void deleteRelations(const EntityInfo ei, Object obj) {
470         foreach(p; ei) {
471             if (p.manyToMany) {
472                 deleteRelations(p, obj);
473             }
474         }
475     }
476     
477     private Variant[] readRelationIds(const PropertyInfo p, Variant thisId) {
478         Variant[] res;
479         string q = p.joinTable.getOtherKeySelectSQL(dialect, createKeySQL(thisId));
480         Statement stmt = conn.createStatement();
481         scope(exit) stmt.close();
482         ResultSet rs = stmt.executeQuery(q);
483         scope(exit) rs.close();
484         while (rs.next()) {
485             res ~= rs.getVariant(1);
486         }
487         return res;
488     }
489 
490     private void updateRelations(const PropertyInfo p, Object obj) {
491         Variant thisId = p.entity.getKey(obj);
492         Variant[] oldRelIds = readRelationIds(p, thisId);
493         Variant[] newRelIds = p.getCollectionIds(obj);
494         bool[string] oldmap;
495         foreach(v; oldRelIds)
496             oldmap[createKeySQL(v)] = true;
497         bool[string] newmap;
498         foreach(v; newRelIds)
499             newmap[createKeySQL(v)] = true;
500         string[] keysToDelete;
501         string[] keysToAdd;
502         foreach(v; newmap.keys)
503             if ((v in oldmap) is null)
504                 keysToAdd ~= v;
505         foreach(v; oldmap.keys)
506             if ((v in newmap) is null)
507                 keysToDelete ~= v;
508         if (keysToAdd.length > 0) {
509             Statement stmt = conn.createStatement();
510             scope(exit) stmt.close();
511             stmt.executeUpdate(p.joinTable.getInsertSQL(dialect, createKeySQL(thisId), keysToAdd));
512         }
513         if (keysToDelete.length > 0) {
514             Statement stmt = conn.createStatement();
515             scope(exit) stmt.close();
516             stmt.executeUpdate(p.joinTable.getDeleteSQL(dialect, createKeySQL(thisId), keysToDelete));
517         }
518     }
519     
520     private void deleteRelations(const PropertyInfo p, Object obj) {
521         Variant thisId = p.entity.getKey(obj);
522         Variant[] oldRelIds = readRelationIds(p, thisId);
523         string[] ids;
524         foreach(v; oldRelIds)
525             ids ~= createKeySQL(v);
526         if (ids.length > 0) {
527             Statement stmt = conn.createStatement();
528             scope(exit) stmt.close();
529             stmt.executeUpdate(p.joinTable.getDeleteSQL(dialect, createKeySQL(thisId), ids));
530         }
531     }
532     
533 
534     /// Persist the given transient instance, first assigning a generated identifier if not assigned; returns generated value
535     override Variant save(Object obj) {
536         auto info = metaData.findEntityForObject(obj);
537         if (!info.isKeySet(obj)) {
538             if (info.getKeyProperty().generated) {
539                 // autogenerated on DB side
540                 string query = dialect.appendInsertToFetchGeneratedKey(metaData.generateInsertNoKeyForEntity(dialect, info), info);
541 				PreparedStatement stmt = conn.prepareStatement(query);
542 				scope(exit) stmt.close();
543 				metaData.writeAllColumns(obj, stmt, 1, true);
544 				Variant generatedKey;
545 				stmt.executeUpdate(generatedKey);
546 			    info.setKey(obj, generatedKey);
547             } else if (info.getKeyProperty().generatorFunc !is null) {
548                 // has generator function
549                 Variant generatedKey = info.getKeyProperty().generatorFunc(conn, info.getKeyProperty());
550                 info.setKey(obj, generatedKey);
551                 string query = metaData.generateInsertAllFieldsForEntity(dialect, info);
552                 PreparedStatement stmt = conn.prepareStatement(query);
553                 scope(exit) stmt.close();
554                 metaData.writeAllColumns(obj, stmt, 1);
555                 stmt.executeUpdate();
556             } else {
557                 throw new PropertyValueException("Key is not set and no generator is specified");
558             }
559         } else {
560 			string query = metaData.generateInsertAllFieldsForEntity(dialect, info);
561 			PreparedStatement stmt = conn.prepareStatement(query);
562 			scope(exit) stmt.close();
563 			metaData.writeAllColumns(obj, stmt, 1);
564 			stmt.executeUpdate();
565         }
566         Variant key = info.getKey(obj);
567         putToCache(info.name, key, obj);
568         saveRelations(info, obj);
569         return key;
570     }
571 
572 	/// Persist the given transient instance.
573 	override void persist(Object obj) {
574         auto info = metaData.findEntityForObject(obj);
575         enforceEx!TransientObjectException(info.isKeySet(obj), "Cannot persist entity w/o key assigned");
576 		string query = metaData.generateInsertAllFieldsForEntity(dialect, info);
577 		PreparedStatement stmt = conn.prepareStatement(query);
578 		scope(exit) stmt.close();
579 		metaData.writeAllColumns(obj, stmt, 1);
580 		stmt.executeUpdate();
581         Variant key = info.getKey(obj);
582         putToCache(info.name, key, obj);
583         saveRelations(info, obj);
584     }
585 
586     override void update(Object obj) {
587         auto info = metaData.findEntityForObject(obj);
588         enforceEx!TransientObjectException(info.isKeySet(obj), "Cannot persist entity w/o key assigned");
589 		string query = metaData.generateUpdateForEntity(dialect, info);
590 		//writeln("Query: " ~ query);
591         {
592     		PreparedStatement stmt = conn.prepareStatement(query);
593     		scope(exit) stmt.close();
594     		int columnCount = metaData.writeAllColumns(obj, stmt, 1, true);
595     		info.keyProperty.writeFunc(obj, stmt, columnCount + 1);
596     		stmt.executeUpdate();
597         }
598         updateRelations(info, obj);
599 	}
600 
601     // renamed from Session.delete since delete is D keyword
602     override void remove(Object obj) {
603         auto info = metaData.findEntityForObject(obj);
604         deleteRelations(info, obj);
605         string query = "DELETE FROM " ~ dialect.quoteIfNeeded(info.tableName) ~ " WHERE " ~ dialect.quoteIfNeeded(info.getKeyProperty().columnName) ~ "=?";
606 		PreparedStatement stmt = conn.prepareStatement(query);
607 		info.getKeyProperty().writeFunc(obj, stmt, 1);
608 		stmt.executeUpdate();
609 	}
610 
611 	/// Create a new instance of Query for the given HQL query string
612 	override Query createQuery(string queryString) {
613 		return new QueryImpl(this, queryString);
614 	}
615 }
616 
617 /// Implementation of HibernateD SessionFactory
618 class SessionFactoryImpl : SessionFactory {
619 //    Configuration cfg;
620 //    Mapping mapping;
621 //    Settings settings;
622 //    EventListeners listeners;
623 //    SessionFactoryObserver observer;
624     private bool closed;
625     private EntityMetaData metaData;
626     Dialect dialect;
627     DataSource connectionPool;
628 
629     SessionImpl[] activeSessions;
630 
631 
632     DBInfo _dbInfo;
633     override public DBInfo getDBMetaData() {
634         if (_dbInfo is null)
635             _dbInfo = new DBInfo(dialect, metaData);
636         return _dbInfo;
637     }
638     
639 
640     void sessionClosed(SessionImpl session) {
641         foreach(i, item; activeSessions) {
642             if (item == session) {
643                 remove(activeSessions, i);
644             }
645         }
646     }
647 
648     this(EntityMetaData metaData, Dialect dialect, DataSource connectionPool) {
649         //writeln("Creating session factory");
650         this.metaData = metaData;
651         this.dialect = dialect;
652         this.connectionPool = connectionPool;
653     }
654 
655 //    this(Configuration cfg, Mapping mapping, Settings settings, EventListeners listeners, SessionFactoryObserver observer) {
656 //        this.cfg = cfg;
657 //        this.mapping = mapping;
658 //        this.settings = settings;
659 //        this.listeners = listeners;
660 //        this.observer = observer;
661 //        if (observer !is null)
662 //            observer.sessionFactoryCreated(this);
663 //    }
664     private void checkClosed() {
665         enforceEx!SessionException(!closed, "Session factory is closed");
666     }
667 
668 	override void close() {
669         //writeln("Closing session factory");
670         checkClosed();
671         closed = true;
672 //        if (observer !is null)
673 //            observer.sessionFactoryClosed(this);
674         // TODO:
675     }
676 
677 	bool isClosed() {
678         return closed;
679     }
680 
681 	Session openSession() {
682         checkClosed();
683         SessionImpl session = new SessionImpl(this, metaData, dialect, connectionPool);
684         activeSessions ~= session;
685         return session;
686     }
687 }
688 
689 struct ObjectList {
690     Object[] list;
691     void add(Object obj) {
692         foreach(v; list) {
693             if (v == obj) {
694                 return; // avoid duplicates
695             }
696         }
697         list ~= obj;
698     }
699     @property int length() { return cast(int)list.length; }
700     ref Object opIndex(size_t index) {
701         return list[index];
702     }
703 }
704 
705 Variant normalize(Variant v) {
706     // variants of different type are not equal, normalize to make sure that keys Variant(1) and Variant(1L) are the same
707     if (v.convertsTo!long)
708         return Variant(v.get!long);
709     else if (v.convertsTo!ulong)
710         return Variant(v.get!ulong);
711     return Variant(v.toString());
712 }
713 
714 /// task to load reference entity
715 class PropertyLoadItem {
716     const PropertyInfo property;
717     private ObjectList[Variant] _map; // ID to object list
718     this(const PropertyInfo property) {
719         this.property = property;
720     }
721     @property ref ObjectList[Variant] map() { return _map; }
722     @property Variant[] keys() { return _map.keys; }
723     @property int length() { return cast(int)_map.length; }
724     ObjectList * opIndex(Variant key) {
725         Variant id = normalize(key);
726         if ((id in _map) is null) {
727             _map[id] = ObjectList();
728         }
729         //assert(length > 0);
730         return &_map[id];
731     }
732     void add(ref Variant id, Object obj) {
733         auto item = opIndex(id);
734         item.add(obj);
735         //assert(item.length == opIndex(id).length);
736     }
737     string createCommaSeparatedKeyList() {
738         assert(map.length > 0);
739         return .createCommaSeparatedKeyList(map.keys);
740     }
741 }
742 
743 string createKeySQL(Variant id) {
744     if (id.convertsTo!long || id.convertsTo!ulong) {
745         return id.toString();
746     } else {
747         return "'" ~ id.toString() ~ "'";
748     }
749 }
750 
751 string createKeyPairSQL(Variant id1, Variant id2) {
752     return "(" ~ createKeySQL(id1) ~ ", " ~ createKeySQL(id2) ~ ")";
753 }
754 
755 string createCommaSeparatedKeyList(Variant[] list) {
756     assert(list.length > 0);
757     string res;
758     foreach(v; list) {
759         if (res.length > 0)
760             res ~= ", ";
761         res ~= createKeySQL(v);
762     }
763     return res;
764 }
765 
766 /// task to load reference entity
767 class EntityCollections {
768     private ObjectList[Variant] _map; // ID to object list
769     @property ref ObjectList[Variant] map() { return _map; }
770     @property Variant[] keys() { return _map.keys; }
771     @property int length() { return cast(int)_map.length; }
772     ref Object[] opIndex(Variant key) {
773         //writeln("searching for key " ~ key.toString);
774         Variant id = normalize(key);
775         if ((id in _map) is null) {
776             //writeln("creating new item");
777             _map[id] = ObjectList();
778         }
779         //assert(length > 0);
780         //writeln("returning item");
781         return _map[id].list;
782     }
783     void add(ref Variant id, Object obj) {
784         auto item = opIndex(id);
785         //writeln("item count = " ~ to!string(item.length));
786         item ~= obj;
787     }
788 }
789 
790 class PropertyLoadMap {
791     private PropertyLoadItem[const PropertyInfo] _map;
792     PropertyLoadItem opIndex(const PropertyInfo prop) {
793         if ((prop in _map) is null) {
794             //writeln("creating new PropertyLoadItem for " ~ prop.propertyName);
795             _map[prop] = new PropertyLoadItem(prop);
796         }
797         assert(_map.length > 0);
798         return _map[prop];
799     }
800     PropertyLoadItem remove(const PropertyInfo pi) {
801         PropertyLoadItem item = _map[pi];
802         _map.remove(pi);
803         return item;
804     }
805     @property ref PropertyLoadItem[const PropertyInfo] map() { return _map; }
806     @property int length() { return cast(int)_map.length; }
807     @property const (PropertyInfo)[] keys() { return _map.keys; }
808     void add(const PropertyInfo property, Variant id, Object obj) {
809         auto item = opIndex(property);
810         item.add(id, obj);
811         //assert(item.length > 0);
812     }
813 }
814 
815 /// Implementation of HibernateD Query
816 class QueryImpl : Query
817 {
818 	SessionImpl sess;
819 	ParsedQuery query;
820 	ParameterValues params;
821 	this(SessionImpl sess, string queryString) {
822 		this.sess = sess;
823         //writeln("QueryImpl(): HQL: " ~ queryString);
824         QueryParser parser = new QueryParser(sess.metaData, queryString);
825         //writeln("parsing");
826 		this.query = parser.makeSQL(sess.dialect);
827         //writeln("SQL: " ~ this.query.sql);
828         params = query.createParams();
829         //writeln("exiting QueryImpl()");
830     }
831 
832 	///Get the query string.
833 	override string getQueryString() {
834 		return query.hql;
835 	}
836 
837 	/// Convenience method to return a single instance that matches the query, or null if the query returns no results.
838 	override Object uniqueObject() {
839         return uniqueResult(cast(Object)null);
840 	}
841 
842     /// Convenience method to return a single instance that matches the query, or null if the query returns no results. Reusing existing buffer.
843     override Object uniqueObject(Object obj) {
844         Object[] rows = listObjects(obj);
845         if (rows == null)
846             return null;
847         enforceEx!NonUniqueResultException(rows.length == 1, "Query returned more than one object: " ~ getQueryString());
848         return rows[0];
849     }
850 
851 	/// Convenience method to return a single instance that matches the query, or null if the query returns no results.
852 	override Variant[] uniqueRow() {
853 		Variant[][] rows = listRows();
854 		if (rows == null)
855 			return null;
856         enforceEx!NonUniqueResultException(rows.length == 1, "Query returned more than one row: " ~ getQueryString());
857 		return rows[0];
858 	}
859 
860     private FromClauseItem findRelation(FromClauseItem from, const PropertyInfo prop) {
861         for (int i=0; i<query.from.length; i++) {
862             FromClauseItem f = query.from[i];
863             if (f.base == from && f.baseProperty == prop)
864                 return f;
865             if (f.entity == prop.referencedEntity && from.base == f)
866                 return f;
867         }
868         return null;
869     }
870 
871     private Object readRelations(Object objectContainer, DataSetReader r, PropertyLoadMap loadMap) {
872         Object[] relations = new Object[query.select.length];
873         //writeln("select clause len = " ~ to!string(query.select.length));
874         // read all related objects from DB row
875         for (int i = 0; i < query.select.length; i++) {
876             FromClauseItem from = query.select[i].from;
877             //writeln("reading " ~ from.entityName);
878             Object row;
879             if (!from.entity.isKeyNull(r, from.startColumn)) {
880                 //writeln("key is not null");
881                 Variant key = from.entity.getKey(r, from.startColumn);
882                 //writeln("key is " ~ key.toString);
883                 row = sess.peekFromCache(from.entity.name, key);
884                 if (row is null) {
885                     //writeln("row not found in cache");
886                     row = (objectContainer !is null && i == 0) ? objectContainer : from.entity.createEntity();
887                     //writeln("reading all columns");
888                     sess.metaData.readAllColumns(row, r, from.startColumn);
889                     sess.putToCache(from.entity.name, key, row);
890                 } else if (objectContainer !is null) {
891                     //writeln("copying all properties to existing container");
892                     from.entity.copyAllProperties(objectContainer, row);
893                 }
894             }
895             relations[i] = row;
896         }
897         //writeln("fill relations...");
898         // fill relations
899         for (int i = 0; i < query.select.length; i++) {
900             if (relations[i] is null)
901                 continue;
902             FromClauseItem from = query.select[i].from;
903             auto ei = from.entity;
904             for (int j=0; j<ei.length; j++) {
905                 auto pi = ei[j];
906                 if (pi.oneToOne || pi.manyToOne) {
907                     static if (TRACE_REFS) writeln("updating relations for " ~ from.pathString ~ "." ~ pi.propertyName);
908                     FromClauseItem rfrom = findRelation(from, pi);
909                     if (rfrom !is null && rfrom.selectIndex >= 0) {
910                         Object rel = relations[rfrom.selectIndex];
911                         pi.setObjectFunc(relations[i], rel);
912                     } else {
913                         if (pi.columnName !is null) {
914                             static if (TRACE_REFS) writeln("relation " ~ pi.propertyName ~ " has column name");
915                             if (r.isNull(from.startColumn + pi.columnOffset)) {
916                                 // FK is null, set NULL to field
917                                 pi.setObjectFunc(relations[i], null);
918                                 static if (TRACE_REFS) writeln("relation " ~ pi.propertyName ~ " has null FK");
919                             } else {
920                                 Variant id = r.getVariant(from.startColumn + pi.columnOffset);
921                                 Object existing = sess.peekFromCache(pi.referencedEntity.name, id);
922                                 if (existing !is null) {
923                                     static if (TRACE_REFS) writeln("existing relation found in cache");
924                                     pi.setObjectFunc(relations[i], existing);
925                                 } else {
926                                     // FK is not null
927                                     if (pi.lazyLoad) {
928                                         // lazy load
929                                         static if (TRACE_REFS) writeln("scheduling lazy load for " ~ from.pathString ~ "." ~ pi.propertyName ~ " with FK " ~ id.toString);
930                                         LazyObjectLoader loader = new LazyObjectLoader(sess.accessor, pi, id);
931                                         pi.setObjectDelegateFunc(relations[i], &loader.load);
932                                     } else {
933                                         // delayed load
934                                         static if (TRACE_REFS) writeln("relation " ~ pi.propertyName ~ " with FK " ~ id.toString() ~ " will be loaded later");
935                                         loadMap.add(pi, id, relations[i]); // to load later
936                                     }
937                                 }
938                             }
939                         } else {
940                             // TODO:
941                             assert(false, "relation " ~ pi.propertyName ~ " has no column name. To be implemented.");
942                         }
943                     }
944                 } else if (pi.oneToMany || pi.manyToMany) {
945                     Variant id = ei.getKey(relations[i]);
946                     if (pi.lazyLoad) {
947                         // lazy load
948                         static if (TRACE_REFS) writeln("creating lazy loader for " ~ from.pathString ~ "." ~ pi.propertyName ~ " by FK " ~ id.toString);
949                         LazyCollectionLoader loader = new LazyCollectionLoader(sess.accessor, pi, id);
950                         pi.setCollectionDelegateFunc(relations[i], &loader.load);
951                     } else {
952                         // delayed load
953                         static if (TRACE_REFS) writeln("Relation " ~ from.pathString ~ "." ~ pi.propertyName ~ " will be loaded later by FK " ~ id.toString);
954                         loadMap.add(pi, id, relations[i]); // to load later
955                     }
956                 }
957             }
958         }
959         return relations[0];
960     }
961 
962 	/// Return the query results as a List of entity objects
963 	override Object[] listObjects() {
964         return listObjects(null);
965 	}
966 
967     private void delayedLoadRelations(PropertyLoadMap loadMap) {
968         auto types = loadMap.keys;
969         static if (TRACE_REFS) writeln("delayedLoadRelations " ~ to!string(loadMap.length));
970         foreach(pi; types) {
971             static if (TRACE_REFS) writeln("delayedLoadRelations " ~ pi.entity.name ~ "." ~ pi.propertyName);
972             assert(pi.referencedEntity !is null);
973             auto map = loadMap.remove(pi);
974             if (map.length == 0)
975                 continue;
976             //writeln("delayedLoadRelations " ~ pi.entity.name ~ "." ~ pi.propertyName);
977             string keys = map.createCommaSeparatedKeyList();
978             if (pi.oneToOne || pi.manyToOne) {
979                 if (pi.columnName !is null) {
980                     Variant[] unknownKeys;
981                     Object[] list = sess.lookupCache(pi.referencedEntity.name, map.keys, unknownKeys);
982                     if (unknownKeys.length > 0) {
983                         string hql = "FROM " ~ pi.referencedEntity.name ~ " WHERE " ~ pi.referencedEntity.keyProperty.propertyName ~ " IN (" ~ createCommaSeparatedKeyList(unknownKeys) ~ ")";
984                         static if (TRACE_REFS) writeln("delayedLoadRelations: loading " ~ pi.propertyName ~ " HQL: " ~ hql);
985                         QueryImpl q = cast(QueryImpl)sess.createQuery(hql);
986                         Object[] fromDB = q.listObjects(null, loadMap);
987                         list ~= fromDB;
988                         static if (TRACE_REFS) writeln("delayedLoadRelations: objects loaded " ~ to!string(fromDB.length));
989                     } else {
990                         static if (TRACE_REFS) writeln("all objects found in cache");
991                     }
992                     static if (TRACE_REFS) writeln("delayedLoadRelations: updating");
993                     foreach(rel; list) {
994                         static if (TRACE_REFS) writeln("delayedLoadRelations: reading key from " ~ pi.referencedEntity.name);
995                         Variant key = pi.referencedEntity.getKey(rel);
996                         //writeln("map length before: " ~ to!string(map.length));
997                         auto objectsToUpdate = map[key].list;
998                         //writeln("map length after: " ~ to!string(map.length));
999                         //writeln("updating relations with key " ~ key.toString() ~ " (" ~ to!string(objectsToUpdate.length) ~ ")");
1000                         foreach(obj; objectsToUpdate) {
1001                             pi.setObjectFunc(obj, rel);
1002                         }
1003                     }
1004                 } else {
1005                     assert(false, "Delayed loader for non-join column is not yet implemented for OneToOne and ManyToOne");
1006                 }
1007             } else if (pi.oneToMany || pi.manyToMany) {
1008                 string hql = "FROM " ~ pi.referencedEntity.name ~ " WHERE " ~ pi.referencedPropertyName ~ "." ~ pi.referencedEntity.keyProperty.propertyName ~ " IN (" ~ keys ~ ")";
1009                 static if (TRACE_REFS) writeln("delayedLoadRelations: loading " ~ pi.propertyName ~ " HQL: " ~ hql);
1010                 QueryImpl q = cast(QueryImpl)sess.createQuery(hql);
1011                 assert(q !is null);
1012                 Object[] list = q.listObjects(null, loadMap);
1013                 static if (TRACE_REFS) writeln("delayedLoadRelations oneToMany/manyToMany: objects loaded " ~ to!string(list.length));
1014                 EntityCollections collections = new EntityCollections();
1015                 // group by referenced PK
1016                 foreach(rel; list) {
1017                     static if (TRACE_REFS) writeln("delayedLoadRelations oneToMany/manyToMany: reading reference from " ~ pi.referencedEntity.name ~ "." ~ pi.referencedProperty.propertyName ~ " joinColumn=" ~ pi.referencedProperty.columnName);
1018                     assert(pi.referencedProperty.manyToOne, "property referenced from OneToMany should be ManyToOne");
1019                     assert(pi.referencedProperty.getObjectFunc !is null);
1020                     assert(rel !is null);
1021                     //writeln("delayedLoadRelations oneToMany: reading object " ~ rel.classinfo.toString);
1022                     Object obj = pi.referencedProperty.getObjectFunc(rel);
1023                     //writeln("delayedLoadRelations oneToMany: object is read");
1024                     if (obj !is null) {
1025                         //writeln("delayedLoadRelations oneToMany: object is not null");
1026                         //writeln("pi.entity.name=" ~ pi.entity.name ~ ", obj is " ~ obj.classinfo.toString);
1027                         //writeln("obj = " ~ obj.toString);
1028                         //writeln("pi.entity.keyProperty=" ~ pi.entity.keyProperty.propertyName);
1029                         //assert(pi.entity.keyProperty.getFunc !is null);
1030                         //Variant k = pi.entity.keyProperty.getFunc(obj);
1031                         //writeln("key=" ~ k.toString);
1032                         Variant key = pi.entity.getKey(obj);
1033                         collections[key] ~= rel;
1034                         //collections.add(k, rel);
1035                     }
1036                 }
1037                 // update objects
1038                 foreach(key; collections.keys) {
1039                     auto objectsToUpdate = map[key].list;
1040                     foreach(obj; objectsToUpdate) {
1041                         pi.setCollectionFunc(obj, collections[key]);
1042                     }
1043                 }
1044             }
1045         }
1046     }
1047 
1048     /// Return the query results as a List of entity objects
1049     Object[] listObjects(Object placeFirstObjectHere) {
1050         PropertyLoadMap loadMap = new PropertyLoadMap();
1051         return listObjects(placeFirstObjectHere, loadMap);
1052     }
1053 
1054     /// Return the query results as a List of entity objects
1055     Object[] listObjects(Object placeFirstObjectHere, PropertyLoadMap loadMap) {
1056         static if (TRACE_REFS) writeln("Entering listObjects " ~ query.hql);
1057         auto ei = query.entity;
1058         enforceEx!SessionException(ei !is null, "No entity expected in result of query " ~ getQueryString());
1059         params.checkAllParametersSet();
1060         sess.checkClosed();
1061         
1062         Object[] res;
1063 
1064 
1065         //writeln("SQL: " ~ query.sql);
1066         PreparedStatement stmt = sess.conn.prepareStatement(query.sql);
1067         scope(exit) stmt.close();
1068         params.applyParams(stmt);
1069         ResultSet rs = stmt.executeQuery();
1070         assert(query.select !is null && query.select.length > 0);
1071         int startColumn = query.select[0].from.startColumn;
1072         {
1073             scope(exit) rs.close();
1074             while(rs.next()) {
1075                 //writeln("read relations...");
1076                 Object row = readRelations(res.length > 0 ? null : placeFirstObjectHere, rs, loadMap);
1077                 if (row !is null)
1078                     res ~= row;
1079             }
1080         }
1081         if (loadMap.length > 0) {
1082             static if (TRACE_REFS) writeln("relation properties scheduled for load: loadMap.length == " ~ to!string(loadMap.length));
1083             delayedLoadRelations(loadMap);
1084         }
1085         static if (TRACE_REFS) writeln("Exiting listObjects " ~ query.hql);
1086         return res.length > 0 ? res : null;
1087     }
1088     
1089     /// Return the query results as a List which each row as Variant array
1090 	override Variant[][] listRows() {
1091 		params.checkAllParametersSet();
1092 		sess.checkClosed();
1093 		
1094 		Variant[][] res;
1095 		
1096 		//writeln("SQL: " ~ query.sql);
1097 		PreparedStatement stmt = sess.conn.prepareStatement(query.sql);
1098 		scope(exit) stmt.close();
1099 		params.applyParams(stmt);
1100 		ResultSet rs = stmt.executeQuery();
1101 		scope(exit) rs.close();
1102 		while(rs.next()) {
1103 			Variant[] row = new Variant[query.colCount];
1104 			for (int i = 1; i<=query.colCount; i++)
1105 				row[i - 1] = rs.getVariant(i);
1106 			res ~= row;
1107 		}
1108 		return res.length > 0 ? res : null;
1109 	}
1110 	
1111 	/// Bind a value to a named query parameter.
1112 	override protected Query setParameterVariant(string name, Variant val) {
1113 		params.setParameter(name, val);
1114 		return this;
1115 	}
1116 }
1117 
1118 class LazyObjectLoader {
1119     const PropertyInfo pi;
1120     Variant id;
1121     SessionAccessor sess;
1122     this(SessionAccessor sess, const PropertyInfo pi, Variant id) {
1123         static if (TRACE_REFS) writeln("Created lazy loader for " ~ pi.referencedEntityName ~ " with id " ~ id.toString);
1124         this.pi = pi;
1125         this.id = id;
1126         this.sess = sess;
1127     }
1128     Object load() {
1129         static if (TRACE_REFS) writeln("LazyObjectLoader.load()");
1130         static if (TRACE_REFS) writeln("lazy loading of " ~ pi.referencedEntityName ~ " with id " ~ id.toString);
1131         return sess.get().loadObject(pi.referencedEntityName, id);
1132     }
1133 }
1134 
1135 class LazyCollectionLoader {
1136     const PropertyInfo pi;
1137     Variant fk;
1138     SessionAccessor sess;
1139     this(SessionAccessor sess, const PropertyInfo pi, Variant fk) {
1140         assert(!pi.oneToMany || (pi.referencedEntity !is null && pi.referencedProperty !is null), "LazyCollectionLoader: No referenced property specified for OneToMany foreign key column");
1141         static if (TRACE_REFS) writeln("Created lazy loader for collection for references " ~ pi.entity.name ~ "." ~ pi.propertyName ~ " by id " ~ fk.toString);
1142         this.pi = pi;
1143         this.fk = fk;
1144         this.sess = sess;
1145     }
1146     Object[] load() {
1147         static if (TRACE_REFS) writeln("LazyObjectLoader.load()");
1148         static if (TRACE_REFS) writeln("lazy loading of references " ~ pi.entity.name ~ "." ~ pi.propertyName ~ " by id " ~ fk.toString);
1149         Object[] res = sess.get().loadReferencedObjects(pi.entity, pi.propertyName, fk);
1150         return res;
1151     }
1152 }
1153