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 
810     this() {}
811 
812     this(PropertyLoadMap plm) {
813         foreach(k; plm.keys) {
814             auto pli = plm[k];
815             foreach(plik; pli.map.keys) {
816                 foreach(obj; pli.map[plik].list.dup) {
817                     add(k, plik, obj);
818                 }
819             }
820         }
821     }
822 
823     PropertyLoadItem remove(const PropertyInfo pi) {
824         PropertyLoadItem item = _map[pi];
825         _map.remove(pi);
826         return item;
827     }
828     @property ref PropertyLoadItem[const PropertyInfo] map() { return _map; }
829     @property int length() { return cast(int)_map.length; }
830     @property const (PropertyInfo)[] keys() { return _map.keys; }
831     void add(const PropertyInfo property, Variant id, Object obj) {
832         auto item = opIndex(property);
833         item.add(id, obj);
834         //assert(item.length > 0);
835     }
836 }
837 
838 /// Implementation of HibernateD Query
839 class QueryImpl : Query
840 {
841 	SessionImpl sess;
842 	ParsedQuery query;
843 	ParameterValues params;
844 	this(SessionImpl sess, string queryString) {
845 		this.sess = sess;
846         //writeln("QueryImpl(): HQL: " ~ queryString);
847         QueryParser parser = new QueryParser(sess.metaData, queryString);
848         //writeln("parsing");
849 		this.query = parser.makeSQL(sess.dialect);
850         //writeln("SQL: " ~ this.query.sql);
851         params = query.createParams();
852         //writeln("exiting QueryImpl()");
853     }
854 
855 	///Get the query string.
856 	override string getQueryString() {
857 		return query.hql;
858 	}
859 
860 	/// Convenience method to return a single instance that matches the query, or null if the query returns no results.
861 	override Object uniqueObject() {
862         return uniqueResult(cast(Object)null);
863 	}
864 
865     /// Convenience method to return a single instance that matches the query, or null if the query returns no results. Reusing existing buffer.
866     override Object uniqueObject(Object obj) {
867         Object[] rows = listObjects(obj);
868         if (rows == null)
869             return null;
870         enforceHelper!NonUniqueResultException(rows.length == 1, "Query returned more than one object: " ~ getQueryString());
871         return rows[0];
872     }
873 
874 	/// Convenience method to return a single instance that matches the query, or null if the query returns no results.
875 	override Variant[] uniqueRow() {
876 		Variant[][] rows = listRows();
877 		if (rows == null)
878 			return null;
879         enforceHelper!NonUniqueResultException(rows.length == 1, "Query returned more than one row: " ~ getQueryString());
880 		return rows[0];
881 	}
882 
883     private FromClauseItem findRelation(FromClauseItem from, const PropertyInfo prop) {
884         for (int i=0; i<query.from.length; i++) {
885             FromClauseItem f = query.from[i];
886             if (f.base == from && f.baseProperty == prop)
887                 return f;
888             if (f.entity == prop.referencedEntity && from.base == f)
889                 return f;
890         }
891         return null;
892     }
893 
894     private Object readRelations(Object objectContainer, DataSetReader r, PropertyLoadMap loadMap) {
895         Object[] relations = new Object[query.select.length];
896         //writeln("select clause len = " ~ to!string(query.select.length));
897         // read all related objects from DB row
898         for (int i = 0; i < query.select.length; i++) {
899             FromClauseItem from = query.select[i].from;
900             //writeln("reading " ~ from.entityName);
901             Object row;
902             if (!from.entity.isKeyNull(r, from.startColumn)) {
903                 //writeln("key is not null");
904                 Variant key = from.entity.getKey(r, from.startColumn);
905                 //writeln("key is " ~ key.toString);
906                 row = sess.peekFromCache(from.entity.name, key);
907                 if (row is null) {
908                     //writeln("row not found in cache");
909                     row = (objectContainer !is null && i == 0) ? objectContainer : from.entity.createEntity();
910                     //writeln("reading all columns");
911                     sess.metaData.readAllColumns(row, r, from.startColumn);
912                     sess.putToCache(from.entity.name, key, row);
913                 } else if (objectContainer !is null) {
914                     //writeln("copying all properties to existing container");
915                     from.entity.copyAllProperties(objectContainer, row);
916                 }
917             }
918             relations[i] = row;
919         }
920         //writeln("fill relations...");
921         // fill relations
922         for (int i = 0; i < query.select.length; i++) {
923             if (relations[i] is null)
924                 continue;
925             FromClauseItem from = query.select[i].from;
926             auto ei = from.entity;
927             for (int j=0; j<ei.length; j++) {
928                 auto pi = ei[j];
929                 if (pi.oneToOne || pi.manyToOne) {
930                     static if (TRACE_REFS) writeln("updating relations for " ~ from.pathString ~ "." ~ pi.propertyName);
931                     FromClauseItem rfrom = findRelation(from, pi);
932                     if (rfrom !is null && rfrom.selectIndex >= 0) {
933                         Object rel = relations[rfrom.selectIndex];
934                         pi.setObjectFunc(relations[i], rel);
935                     } else {
936                         if (pi.columnName !is null) {
937                             static if (TRACE_REFS) writeln("relation " ~ pi.propertyName ~ " has column name");
938                             if (r.isNull(from.startColumn + pi.columnOffset)) {
939                                 // FK is null, set NULL to field
940                                 pi.setObjectFunc(relations[i], null);
941                                 static if (TRACE_REFS) writeln("relation " ~ pi.propertyName ~ " has null FK");
942                             } else {
943                                 Variant id = r.getVariant(from.startColumn + pi.columnOffset);
944                                 Object existing = sess.peekFromCache(pi.referencedEntity.name, id);
945                                 if (existing !is null) {
946                                     static if (TRACE_REFS) writeln("existing relation found in cache");
947                                     pi.setObjectFunc(relations[i], existing);
948                                 } else {
949                                     // FK is not null
950                                     if (pi.lazyLoad) {
951                                         // lazy load
952                                         static if (TRACE_REFS) writeln("scheduling lazy load for " ~ from.pathString ~ "." ~ pi.propertyName ~ " with FK " ~ id.toString);
953                                         LazyObjectLoader loader = new LazyObjectLoader(sess.accessor, pi, id);
954                                         pi.setObjectDelegateFunc(relations[i], &loader.load);
955                                     } else {
956                                         // delayed load
957                                         static if (TRACE_REFS) writeln("relation " ~ pi.propertyName ~ " with FK " ~ id.toString() ~ " will be loaded later");
958                                         loadMap.add(pi, id, relations[i]); // to load later
959                                     }
960                                 }
961                             }
962                         } else {
963                             // TODO:
964                             assert(false, "relation " ~ pi.propertyName ~ " has no column name. To be implemented.");
965                         }
966                     }
967                 } else if (pi.oneToMany || pi.manyToMany) {
968                     Variant id = ei.getKey(relations[i]);
969                     if (pi.lazyLoad) {
970                         // lazy load
971                         static if (TRACE_REFS) writeln("creating lazy loader for " ~ from.pathString ~ "." ~ pi.propertyName ~ " by FK " ~ id.toString);
972                         LazyCollectionLoader loader = new LazyCollectionLoader(sess.accessor, pi, id);
973                         pi.setCollectionDelegateFunc(relations[i], &loader.load);
974                     } else {
975                         // delayed load
976                         static if (TRACE_REFS) writeln("Relation " ~ from.pathString ~ "." ~ pi.propertyName ~ " will be loaded later by FK " ~ id.toString);
977                         loadMap.add(pi, id, relations[i]); // to load later
978                     }
979                 }
980             }
981         }
982         return relations[0];
983     }
984 
985 	/// Return the query results as a List of entity objects
986 	override Object[] listObjects() {
987         return listObjects(null);
988 	}
989 
990     private void delayedLoadRelations(PropertyLoadMap loadMap) {
991         loadMap = new PropertyLoadMap(loadMap);
992 
993         auto types = loadMap.keys;
994         static if (TRACE_REFS) writeln("delayedLoadRelations " ~ to!string(loadMap.length));
995 
996         foreach(pi; types) {
997             static if (TRACE_REFS) writeln("delayedLoadRelations " ~ pi.entity.name ~ "." ~ pi.propertyName);
998             assert(pi.referencedEntity !is null);
999             auto map = loadMap.remove(pi);
1000             if (map.length == 0)
1001                 continue;
1002             //writeln("delayedLoadRelations " ~ pi.entity.name ~ "." ~ pi.propertyName);
1003             string keys = map.createCommaSeparatedKeyList();
1004             if (pi.oneToOne || pi.manyToOne) {
1005                 if (pi.columnName !is null) {
1006                     Variant[] unknownKeys;
1007                     Object[] list = sess.lookupCache(pi.referencedEntity.name, map.keys, unknownKeys);
1008                     if (unknownKeys.length > 0) {
1009                         string hql = "FROM " ~ pi.referencedEntity.name ~ " WHERE " ~ pi.referencedEntity.keyProperty.propertyName ~ " IN (" ~ createCommaSeparatedKeyList(unknownKeys) ~ ")";
1010                         static if (TRACE_REFS) writeln("delayedLoadRelations: loading " ~ pi.propertyName ~ " HQL: " ~ hql);
1011                         QueryImpl q = cast(QueryImpl)sess.createQuery(hql);
1012                         Object[] fromDB = q.listObjects(null, loadMap);
1013                         list ~= fromDB;
1014                         static if (TRACE_REFS) writeln("delayedLoadRelations: objects loaded " ~ to!string(fromDB.length));
1015                     } else {
1016                         static if (TRACE_REFS) writeln("all objects found in cache");
1017                     }
1018                     static if (TRACE_REFS) writeln("delayedLoadRelations: updating");
1019                     foreach(rel; list) {
1020                         static if (TRACE_REFS) writeln("delayedLoadRelations: reading key from " ~ pi.referencedEntity.name);
1021                         Variant key = pi.referencedEntity.getKey(rel);
1022                         //writeln("map length before: " ~ to!string(map.length));
1023                         auto objectsToUpdate = map[key].list;
1024                         //writeln("map length after: " ~ to!string(map.length));
1025                         //writeln("updating relations with key " ~ key.toString() ~ " (" ~ to!string(objectsToUpdate.length) ~ ")");
1026                         foreach(obj; objectsToUpdate) {
1027                             pi.setObjectFunc(obj, rel);
1028                         }
1029                     }
1030                 } else {
1031                     assert(false, "Delayed loader for non-join column is not yet implemented for OneToOne and ManyToOne");
1032                 }
1033             } else if (pi.oneToMany || pi.manyToMany) {
1034                 string hql = "FROM " ~ pi.referencedEntity.name ~ " WHERE " ~ pi.referencedPropertyName ~ "." ~ pi.referencedEntity.keyProperty.propertyName ~ " IN (" ~ keys ~ ")";
1035                 static if (TRACE_REFS) writeln("delayedLoadRelations: loading " ~ pi.propertyName ~ " HQL: " ~ hql);
1036                 QueryImpl q = cast(QueryImpl)sess.createQuery(hql);
1037                 assert(q !is null);
1038                 Object[] list = q.listObjects(null, loadMap);
1039                 static if (TRACE_REFS) writeln("delayedLoadRelations oneToMany/manyToMany: objects loaded " ~ to!string(list.length));
1040                 EntityCollections collections = new EntityCollections();
1041                 // group by referenced PK
1042                 foreach(rel; list) {
1043                     static if (TRACE_REFS) writeln("delayedLoadRelations oneToMany/manyToMany: reading reference from " ~ pi.referencedEntity.name ~ "." ~ pi.referencedProperty.propertyName ~ " joinColumn=" ~ pi.referencedProperty.columnName);
1044                     assert(pi.referencedProperty.manyToOne, "property referenced from OneToMany should be ManyToOne");
1045                     assert(pi.referencedProperty.getObjectFunc !is null);
1046                     assert(rel !is null);
1047                     //writeln("delayedLoadRelations oneToMany: reading object " ~ rel.classinfo.toString);
1048                     Object obj = pi.referencedProperty.getObjectFunc(rel);
1049                     //writeln("delayedLoadRelations oneToMany: object is read");
1050                     if (obj !is null) {
1051                         //writeln("delayedLoadRelations oneToMany: object is not null");
1052                         //writeln("pi.entity.name=" ~ pi.entity.name ~ ", obj is " ~ obj.classinfo.toString);
1053                         //writeln("obj = " ~ obj.toString);
1054                         //writeln("pi.entity.keyProperty=" ~ pi.entity.keyProperty.propertyName);
1055                         //assert(pi.entity.keyProperty.getFunc !is null);
1056                         //Variant k = pi.entity.keyProperty.getFunc(obj);
1057                         //writeln("key=" ~ k.toString);
1058                         Variant key = pi.entity.getKey(obj);
1059                         collections[key] ~= rel;
1060                         //collections.add(k, rel);
1061                     }
1062                 }
1063                 // update objects
1064                 foreach(key; collections.keys) {
1065                     auto objectsToUpdate = map[key].list;
1066                     foreach(obj; objectsToUpdate) {
1067                         pi.setCollectionFunc(obj, collections[key]);
1068                     }
1069                 }
1070             }
1071         }
1072     }
1073 
1074     /// Return the query results as a List of entity objects
1075     Object[] listObjects(Object placeFirstObjectHere) {
1076         PropertyLoadMap loadMap = new PropertyLoadMap();
1077         return listObjects(placeFirstObjectHere, loadMap);
1078     }
1079 
1080     /// Return the query results as a List of entity objects
1081     Object[] listObjects(Object placeFirstObjectHere, PropertyLoadMap loadMap) {
1082         static if (TRACE_REFS) writeln("Entering listObjects " ~ query.hql);
1083         auto ei = query.entity;
1084         enforceHelper!SessionException(ei !is null, "No entity expected in result of query " ~ getQueryString());
1085         params.checkAllParametersSet();
1086         sess.checkClosed();
1087         
1088         Object[] res;
1089 
1090 
1091         //writeln("SQL: " ~ query.sql);
1092         PreparedStatement stmt = sess.conn.prepareStatement(query.sql);
1093         scope(exit) stmt.close();
1094         params.applyParams(stmt);
1095         ResultSet rs = stmt.executeQuery();
1096         assert(query.select !is null && query.select.length > 0);
1097         int startColumn = query.select[0].from.startColumn;
1098         {
1099             scope(exit) rs.close();
1100             while(rs.next()) {
1101                 //writeln("read relations...");
1102                 Object row = readRelations(res.length > 0 ? null : placeFirstObjectHere, rs, loadMap);
1103                 if (row !is null)
1104                     res ~= row;
1105             }
1106         }
1107         if (loadMap.length > 0) {
1108             static if (TRACE_REFS) writeln("relation properties scheduled for load: loadMap.length == " ~ to!string(loadMap.length));
1109             delayedLoadRelations(loadMap);
1110         }
1111         static if (TRACE_REFS) writeln("Exiting listObjects " ~ query.hql);
1112         return res.length > 0 ? res : null;
1113     }
1114     
1115     /// Return the query results as a List which each row as Variant array
1116 	override Variant[][] listRows() {
1117 		params.checkAllParametersSet();
1118 		sess.checkClosed();
1119 		
1120 		Variant[][] res;
1121 		
1122 		//writeln("SQL: " ~ query.sql);
1123 		PreparedStatement stmt = sess.conn.prepareStatement(query.sql);
1124 		scope(exit) stmt.close();
1125 		params.applyParams(stmt);
1126 		ResultSet rs = stmt.executeQuery();
1127 		scope(exit) rs.close();
1128 		while(rs.next()) {
1129 			Variant[] row = new Variant[query.colCount];
1130 			for (int i = 1; i<=query.colCount; i++)
1131 				row[i - 1] = rs.getVariant(i);
1132 			res ~= row;
1133 		}
1134 		return res.length > 0 ? res : null;
1135 	}
1136 	
1137 	/// Bind a value to a named query parameter.
1138 	override protected Query setParameterVariant(string name, Variant val) {
1139 		params.setParameter(name, val);
1140 		return this;
1141 	}
1142 }
1143 
1144 class LazyObjectLoader {
1145     const PropertyInfo pi;
1146     Variant id;
1147     SessionAccessor sess;
1148     this(SessionAccessor sess, const PropertyInfo pi, Variant id) {
1149         static if (TRACE_REFS) writeln("Created lazy loader for " ~ pi.referencedEntityName ~ " with id " ~ id.toString);
1150         this.pi = pi;
1151         this.id = id;
1152         this.sess = sess;
1153     }
1154     Object load() {
1155         static if (TRACE_REFS) writeln("LazyObjectLoader.load()");
1156         static if (TRACE_REFS) writeln("lazy loading of " ~ pi.referencedEntityName ~ " with id " ~ id.toString);
1157         return sess.get().loadObject(pi.referencedEntityName, id);
1158     }
1159 }
1160 
1161 class LazyCollectionLoader {
1162     const PropertyInfo pi;
1163     Variant fk;
1164     SessionAccessor sess;
1165     this(SessionAccessor sess, const PropertyInfo pi, Variant fk) {
1166         assert(!pi.oneToMany || (pi.referencedEntity !is null && pi.referencedProperty !is null), "LazyCollectionLoader: No referenced property specified for OneToMany foreign key column");
1167         static if (TRACE_REFS) writeln("Created lazy loader for collection for references " ~ pi.entity.name ~ "." ~ pi.propertyName ~ " by id " ~ fk.toString);
1168         this.pi = pi;
1169         this.fk = fk;
1170         this.sess = sess;
1171     }
1172     Object[] load() {
1173         static if (TRACE_REFS) writeln("LazyObjectLoader.load()");
1174         static if (TRACE_REFS) writeln("lazy loading of references " ~ pi.entity.name ~ "." ~ pi.propertyName ~ " by id " ~ fk.toString);
1175         Object[] res = sess.get().loadReferencedObjects(pi.entity, pi.propertyName, fk);
1176         return res;
1177     }
1178 }
1179