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