1 /++
2     Object-Relational Mapping (ORM)
3  +/
4 module oceandrift.db.orm;
5 
6 import std.meta;
7 import std.string;
8 import std.traits;
9 
10 import oceandrift.db.dbal.driver;
11 import oceandrift.db.dbal.v4;
12 
13 ///
14 public import oceandrift.db.dbal.v4 : asc, desc, distinct, where, whereNot, whereParentheses;
15 
16 @safe:
17 
18 /++
19     Determines whether a $(I Database Driver) is compatible with the ORM
20  +/
21 enum bool isORMCompatible(Driver) =
22     isDatabaseDriver!Driver
23     && isQueryCompiler!Driver;
24 
25 /++
26     Determines whether a type is an entity type suitable for ORM use
27  +/
28 enum bool isEntityType(TEntity) = (
29         (
30             is(TEntity == struct)
31             || is(TEntity == class)
32     )
33     && is(ReturnType!((TEntity t) => t.id) == ulong)
34     && __traits(compiles, delegate(TEntity t) @safe { t.id = ulong(0); })
35     );
36 
37 /++
38     Determines the associated table name for an entity type
39  +/
40 enum string tableName(alias TEntity) =
41     __traits(identifier, TEntity).toLower;
42 
43 /++
44     Determines the associated join table name for two entity types
45  +/
46 enum string joinTableName(alias TEntity1, alias TEntity2) =
47     joinTableNameImpl!(TEntity1, TEntity2)();
48 
49 private string joinTableNameImpl(alias TEntity1, alias TEntity2)()
50 {
51     import std.string : cmp;
52 
53     string name1 = tableName!TEntity1;
54     string name2 = tableName!TEntity2;
55 
56     int x = cmp(name1, name2);
57 
58     // dfmt off
59     return (x < 0)
60         ? name1 ~ '_' ~ name2
61         : name2 ~ '_' ~ name1
62     ;
63     // dfmt on
64 }
65 
66 /++
67     Determines the associated column name for a field of an entity type
68  +/
69 enum columnName(string fieldName) = columnNameImpl(fieldName);
70 
71 ///
72 unittest
73 {
74     assert(columnName!"name" == "name");
75     assert(columnName!"user_id" == "user_id");
76 
77     assert(columnName!"firstName" == "first_name");
78     assert(columnName!"LastName" == "last_name");
79 
80     assert(columnName!"userID" == "user_id");
81     assert(columnName!"canEditPosts" == "can_edit_posts");
82     assert(columnName!"isHTTPS" == "is_https");
83     assert(columnName!"isHTTPS_URL" == "is_https_url");
84 }
85 
86 private string columnNameImpl(string fieldName)
87 {
88     import std.ascii : isUpper, toLower;
89 
90     string output = "";
91     bool previousWasUpper = false;
92 
93     foreach (size_t idx, char c; fieldName)
94     {
95         if (c.isUpper)
96         {
97             if (!previousWasUpper && (idx > 0) && (output[$ - 1] != '_'))
98                 output ~= '_';
99 
100             output ~= c.toLower;
101             previousWasUpper = true;
102             continue;
103         }
104 
105         previousWasUpper = false;
106         output ~= c;
107     }
108 
109     return output[];
110 }
111 
112 /++
113     Returns:
114         The column names associated with the provided entity type
115  +/
116 enum auto columnNames(alias TEntity) =
117     aliasSeqOf!(columnNamesImpl!TEntity());
118 
119 /++
120     Returns:
121         The column names associated with the provided entity type
122         except for the ID column
123  +/
124 enum auto columnNamesNoID(alias TEntity) =
125     aliasSeqOf!(columnNamesImpl!(TEntity, false)());
126 
127 enum fieldNames(alias TEntity) =
128     aliasSeqOf!(columnNamesImpl!(TEntity, true, false)());
129 
130 enum fieldNamesNoID(alias TEntity) =
131     aliasSeqOf!(columnNamesImpl!(TEntity, false, false)());
132 
133 private auto columnNamesImpl(TEntity, bool includeID = true, bool toColumnNames = true)()
134 {
135     string[] columnNames = [];
136 
137     static foreach (idx, field; FieldNameTuple!TEntity)
138     {
139         static assert(isDBValueCompatible!(Fields!TEntity[idx]), "Column not serializable to DBValue");
140 
141         static if (includeID || (field.toLower != "id"))
142         {
143             static if (toColumnNames)
144                 columnNames ~= columnName!field;
145             else
146                 columnNames ~= field;
147         }
148     }
149 
150     return columnNames;
151 }
152 
153 /++
154     Transforms a result row into an instance of the specified entity type
155  +/
156 TEntity toEntity(TEntity)(Row row)
157 {
158     static if (is(TEntity == class))
159         TEntity e = new TEntity();
160     else static if (is(TEntity == struct))
161         TEntity e = TEntity();
162     else
163         static assert(0, "faulty template constraint implementation.");
164 
165     static foreach (idx, name; fieldNames!TEntity)
166         mixin("e." ~ name) = row[idx].getAs!(typeof(mixin("e." ~ name)));
167 
168     return e;
169 }
170 
171 /++
172     Transforms a statement’s result rows into a collection of entities
173  +/
174 EntityCollection!TEntity toEntities(TEntity)(Statement stmt) pure nothrow @nogc
175 {
176     return EntityCollection!TEntity(stmt);
177 }
178 
179 /++
180     Collection of entities (retrieved through a query)
181 
182     Lazy $(I Input Range) implementation.
183  +/
184 struct EntityCollection(TEntity) if (isEntityType!TEntity)
185 {
186 @safe:
187 
188     private
189     {
190         Statement _stmt;
191     }
192 
193     private this(Statement stmt) pure nothrow @nogc
194     {
195         _stmt = stmt;
196     }
197 
198     ///
199     bool empty()
200     {
201         return _stmt.empty;
202     }
203 
204     ///
205     TEntity front()
206     {
207         return _stmt.front.toEntity!TEntity();
208     }
209 
210     ///
211     void popFront()
212     {
213         _stmt.popFront();
214     }
215 }
216 
217 /++
218     ORM Query Builder
219 
220     ---
221     // pre-compiled query (CTFE)
222     enum bqMountains = em.find!Mountain()
223         .orderBy("height")
224         .limit(25)
225         .select();
226 
227     // execute (at runtime)
228     auto mountains = bqMountains.via(db);
229     foreach (mt; mountains) {
230         // […]
231     }
232     ---
233 
234     For each member function returning a [BuiltPreCollection],
235     there’s also a shorthand “via” function.
236     The “via” variant executes the built query via the provided database connection.
237 
238     ---
239     auto mountains = em.find!Mountain()
240         .orderBy("height")
241         .limit(25)
242         .selectVia(db);
243 
244     foreach (mt; mountains) {
245         // […]
246     }
247     ---
248  +/
249 struct PreCollection(TEntity, DatabaseDriver)
250         if (isEntityType!TEntity && isORMCompatible!DatabaseDriver)
251 {
252 @safe:
253 
254     /++
255         SELECTs the requested entities from the database
256 
257         ---
258         auto mountains = em.find!Mountain()
259             .orderBy("height")
260             .limit(25)
261             .selectVia(db);
262 
263         foreach (mt; mountains) {
264             // […]
265         }
266         ---
267      +/
268     BuiltPreCollection!TEntity select()
269     {
270         BuiltQuery bq = _query
271             .select(columnNames!TEntity)
272             .build!DatabaseDriver();
273         return BuiltPreCollection!TEntity(bq);
274     }
275 
276     /// ditto
277     EntityCollection!TEntity selectVia(DatabaseDriver db)
278     {
279         return this.select().via(db);
280     }
281 
282     /++
283         COUNTs the number of requested entities from the database
284 
285         ---
286         ulong nMountains = em.find!Mountain().countVia(db);
287         ---
288      +/
289     BuiltQuery count()
290     {
291         BuiltQuery bq = _query
292             .select(oceandrift.db.dbal.v4.count("*"))
293             .build!DatabaseDriver();
294         return bq;
295     }
296 
297     /// ditto
298     ulong countVia(DatabaseDriver db)
299     {
300         BuiltQuery bq = this.count();
301 
302         Statement stmt = db.prepareBuiltQuery(bq);
303         stmt.execute();
304 
305         debug assert(!stmt.empty);
306         return stmt.front[0].getAs!ulong();
307     }
308 
309     /++
310         SELECTs aggregate data for the requested entities from the database
311 
312         ---
313         DBValue maxHeight = em.find!Mountain().aggregateVia(AggregateFunction.max, "height", db);
314         ---
315      +/
316     BuiltQuery aggregate(Distinct distinct = Distinct.no)(AggregateFunction aggr, string column)
317     {
318         BuiltQuery bq = _query
319             .select(SelectExpression(col(column), aggr, distinct))
320             .build!DatabaseDriver();
321         return bq;
322     }
323 
324     /// ditto
325     DBValue aggregateVia(Distinct distinct = Distinct.no)(AggregateFunction aggr, string column, DatabaseDriver db)
326     {
327         BuiltQuery bq = this.aggregate!(distinct)(aggr, column);
328 
329         Statement stmt = db.prepareBuiltQuery(bq);
330         stmt.execute();
331 
332         debug assert(!stmt.empty);
333         debug assert(stmt.front.length == 1);
334         return stmt.front[0];
335     }
336 
337     /++
338         DELETEs the requested entities from the database
339 
340         ---
341         em.find!Mountain()
342             .where("height", '<', 3000)
343             .deleteVia(db);
344         ---
345      +/
346     BuiltQuery delete_()
347     {
348         BuiltQuery bq = _query
349             .delete_()
350             .build!DatabaseDriver();
351         return bq;
352     }
353 
354     /// ditto
355     void deleteVia(DatabaseDriver db)
356     {
357         BuiltQuery bq = this.delete_();
358 
359         Statement stmt = db.prepareBuiltQuery(bq);
360         stmt.execute();
361         stmt.close();
362     }
363 
364     /++
365         Specifies the filter criteria for the requested data
366 
367         Adds a WHERE clause to the query.
368 
369         ---
370         auto children = em.find!Person()
371             .where("age", ComparisonOperator.lessThan, 18)
372             .selectVia(db);
373         ---
374      +/
375     PreCollection!(TEntity, DatabaseDriver) where(LogicalOperator logicalJunction = and, TComparisonOperator)(
376         string column, TComparisonOperator op, const DBValue value)
377             if (isComparisonOperator!TComparisonOperator)
378     {
379         return typeof(this)(_query.where!logicalJunction(column, op, value));
380     }
381 
382     /// ditto
383     PreCollection!(TEntity, DatabaseDriver) where(LogicalOperator logicalJunction = and, TComparisonOperator, T)(
384         string column, TComparisonOperator op, const T value)
385             if (isComparisonOperator!TComparisonOperator && isDBValueCompatible!T)
386     {
387         return typeof(this)(_query.where!logicalJunction(column, op, value));
388     }
389 
390     /// ditto
391     PreCollection!(TEntity, DatabaseDriver) where(LogicalOperator logicalJunction = and, TComparisonOperator)(
392         string column, TComparisonOperator op)
393             if (isComparisonOperator!TComparisonOperator)
394     {
395         return typeof(this)(_query.where!logicalJunction(column, op, null));
396     }
397 
398     /// ditto
399     PreCollection!(TEntity, DatabaseDriver) whereParentheses(LogicalOperator logicalJunction = and)(
400         Query delegate(Query q) @safe pure conditions)
401     {
402         return typeof(this)(_query.whereParentheses!logicalJunction(conditions));
403     }
404 
405     /++
406         Specifies a sorting criteria for the requested data
407      +/
408     PreCollection!(TEntity, DatabaseDriver) orderBy(string column, OrderingSequence orderingSequence = asc)
409     {
410         return typeof(this)(_query.orderBy(column, orderingSequence));
411     }
412 
413     /++
414         LIMITs the number of entities to retrieve
415      +/
416     PreCollection!(TEntity, DatabaseDriver) limit(ulong limit)
417     {
418         return typeof(this)(_query.limit(limit));
419     }
420 
421     /// ditto
422     PreCollection!(TEntity, DatabaseDriver) limit(ulong limit, int offset)
423     {
424         return typeof(this)(_query.limit(limit, offset));
425     }
426 
427 private:
428     Query _query;
429 }
430 
431 /++
432     Compiled query statement to use with the ORM
433  +/
434 struct BuiltPreCollection(TEntity) if (isEntityType!TEntity)
435 {
436 @safe pure nothrow @nogc:
437 
438     ///
439     string sql()
440     {
441         return _query.sql;
442     }
443 
444 private:
445     BuiltQuery _query;
446 }
447 
448 ///
449 struct PreparedCollection(TEntity) if (isEntityType!TEntity)
450 {
451 @safe:
452 
453     ///
454     void bind(T)(int index, const T value)
455     {
456         _statement.bind(index, value);
457     }
458 
459     ///
460     EntityCollection!TEntity execute()
461     {
462         _statement.execute();
463         return EntityCollection!TEntity(_statement);
464     }
465 
466 private:
467     Statement _statement;
468 }
469 
470 PreparedCollection!TEntity prepareCollection(TEntity, DatabaseDriver)(
471     BuiltPreCollection!TEntity builtPreCollection, DatabaseDriver db)
472         if (isDatabaseDriver!DatabaseDriver && isEntityType!TEntity)
473 {
474     return PreparedCollection!TEntity(db.prepareBuiltQuery(builtPreCollection._query));
475 }
476 
477 /++
478     Retrieves and maps data to the corresponding entity type
479 
480     See_Also: [via]
481  +/
482 EntityCollection!TEntity map(TEntity, DatabaseDriver)(DatabaseDriver db, BuiltPreCollection!TEntity builtPreCollection)
483         if (isDatabaseDriver!DatabaseDriver && isEntityType!TEntity)
484 {
485     pragma(inline, true);
486     return via(builtPreCollection, db);
487 }
488 
489 /++
490     Executes a built query via the provided database connection
491  +/
492 EntityCollection!TEntity via(TEntity, DatabaseDriver)(
493     BuiltPreCollection!TEntity builtPreCollection, DatabaseDriver db)
494         if (isDatabaseDriver!DatabaseDriver && isEntityType!TEntity)
495 {
496     Statement stmt = db.prepareBuiltQuery(builtPreCollection._query);
497     stmt.execute();
498     return EntityCollection!TEntity(stmt);
499 }
500 
501 /++
502     Loads an entity from the database
503  +/
504 bool get(TEntity, DatabaseDriver)(DatabaseDriver db, ulong id, out TEntity output)
505         if (isEntityType!TEntity)
506 {
507     enum BuiltQuery query = table(tableName!TEntity).qb
508             .where("id", '=')
509             .select(columnNames!TEntity)
510             .build!DatabaseDriver();
511 
512     Statement stmt = db.prepareBuiltQuery(query);
513     stmt.bind(0, id);
514 
515     stmt.execute();
516     if (stmt.empty)
517         return false;
518 
519     output = stmt.front.toEntity!TEntity;
520     return true;
521 }
522 
523 /++
524     Entity Manager
525 
526     Primary object of the ORM
527  +/
528 struct EntityManager(DatabaseDriver) if (isORMCompatible!DatabaseDriver)
529 {
530 @safe:
531 
532     private
533     {
534         DatabaseDriver _db;
535     }
536 
537     ///
538     this(DatabaseDriver db)
539     {
540         _db = db;
541     }
542 
543     /++
544         Loads the requested entity (#ID) from the database
545 
546         ---
547         Mountain mt;
548         bool found = em.get!Mountain(4, mt);
549         ---
550      +/
551     bool get(TEntity)(ulong id, out TEntity output) if (isEntityType!TEntity)
552     {
553         enum BuiltQuery query = table(tableName!TEntity).qb
554                 .where("id", '=')
555                 .select(columnNames!TEntity)
556                 .build!DatabaseDriver();
557 
558         Statement stmt = _db.prepareBuiltQuery(query);
559         stmt.bind(0, id);
560 
561         stmt.execute();
562         if (stmt.empty)
563             return false;
564 
565         output = stmt.front.toEntity!TEntity;
566         return true;
567     }
568 
569     /++
570         Updates or stores the provided entity in the database
571      +/
572     void save(TEntity)(ref TEntity entity) if (isEntityType!TEntity)
573     {
574         if (entity.id == 0)
575             entity.id = this.store(entity);
576         else
577             this.update(entity);
578     }
579 
580     /++
581         Stores the provided entity in the database
582 
583         Does not set the entity ID. Returns it instead.
584         This allows you to use the entity as a template.
585 
586         See_Also:
587             [EntityManager.save|save]
588      +/
589     ulong store(TEntity)(const TEntity entity) if (isEntityType!TEntity)
590     {
591         enum BuiltQuery query = table(tableName!TEntity)
592                 .insert(columnNamesNoID!TEntity)
593                 .build!DatabaseDriver();
594 
595         Statement stmt = _db.prepareBuiltQuery(query);
596 
597         static foreach (int idx, column; fieldNamesNoID!TEntity)
598             stmt.bind(idx, mixin("cast(const) entity." ~ column));
599 
600         stmt.execute();
601 
602         return _db.lastInsertID().getAs!ulong;
603     }
604 
605     /++
606         Updates the provided entity in the database
607 
608         See_Also:
609             [EntityManager.save|save]
610      +/
611     void update(TEntity)(const TEntity entity) if (isEntityType!TEntity)
612     in (entity.id != 0)
613     {
614         enum BuiltQuery query = table(tableName!TEntity).qb
615                 .where("id", '=')
616                 .update(columnNamesNoID!TEntity)
617                 .build!DatabaseDriver();
618 
619         Statement stmt = _db.prepareBuiltQuery(query);
620 
621         static foreach (int idx, column; columnNamesNoID!TEntity)
622             mixin("stmt.bind(idx, cast(const) entity." ~ column ~ ");");
623 
624         enum int idParamN = columnNamesNoID!TEntity.length;
625         stmt.bind(idParamN, entity.id);
626 
627         stmt.execute();
628     }
629 
630     /++
631         Removes (deletes) the provided entity from the database
632      +/
633     void remove(TEntity)(ulong id) if (isEntityType!TEntity)
634     {
635         enum BuiltQuery query = table(tableName!TEntity).qb
636                 .where("id", '=')
637                 .delete_()
638                 .build!DatabaseDriver();
639 
640         Statement stmt = _db.prepareBuiltQuery(query);
641         stmt.bind(0, id);
642         stmt.execute();
643     }
644 
645     /// ditto
646     void remove(TEntity)(TEntity entity) if (isEntityType!TEntity)
647     {
648         return this.remove!TEntity(entity.id);
649     }
650 
651     deprecated EntityCollection!TEntity find(TEntity)(Query delegate(Query) @safe buildQuery)
652             if (isEntityType!TEntity)
653     in (buildQuery !is null)
654     {
655         Query q = table(tableName!TEntity).qb;
656         q = buildQuery(q);
657         BuiltQuery query = q
658             .select(columnNames!TEntity)
659             .build!DatabaseDriver();
660 
661         Statement stmt = _db.prepareBuiltQuery(query);
662         stmt.execute();
663 
664         return EntityCollection!TEntity(stmt);
665     }
666 
667     deprecated EntityCollection!TEntity find(TEntity, Query function(Query) @safe pure buildQuery)(
668         void delegate(Statement) @safe bindValues = null)
669             if (isEntityType!TEntity && (buildQuery !is null))
670     {
671         enum Query q = buildQuery(table(tableName!TEntity).qb);
672         enum BuiltQuery query = q
673                 .select(columnNames!TEntity)
674                 .build!DatabaseDriver();
675 
676         Statement stmt = _db.prepareBuiltQuery(query);
677 
678         if (bindValues !is null)
679             bindValues(stmt);
680 
681         stmt.execute();
682 
683         return EntityCollection!TEntity(stmt);
684     }
685 
686     /++
687         Finds entites from the database
688 
689         Query building starting point.
690      +/
691     static PreCollection!(TEntity, DatabaseDriver) find(TEntity)()
692             if (isEntityType!TEntity)
693     {
694         enum Query q = table(tableName!TEntity).qb;
695         enum pc = PreCollection!(TEntity, DatabaseDriver)(q);
696         return pc;
697     }
698 
699     ///
700     bool manyToOne(TEntityOne, TEntityMany)(TEntityMany many, out TEntityOne output)
701             if (isEntityType!TEntityOne && isEntityType!TEntityMany)
702     {
703         enum BuiltQuery bq = table(tableName!TEntityOne).qb
704                 .where("id", '=')
705                 .select(columnNames!TEntityOne)
706                 .build!DatabaseDriver();
707 
708         Statement stmt = _db.prepareBuiltQuery(bq);
709         immutable ulong oneID = mixin("many." ~ tableName!TEntityOne ~ "ID");
710         stmt.bind(0, oneID);
711         stmt.execute();
712 
713         if (stmt.empty)
714             return false;
715 
716         output = stmt.front.toEntity!TEntityOne();
717         return true;
718     }
719 
720     ///
721     bool oneToOne(TEntityTarget, TEntitySource)(TEntitySource source, out TEntityTarget toOne)
722             if (isEntityType!TEntityTarget && isEntityType!TEntitySource)
723     {
724         pragma(inline, true);
725         return manyToOne(source, toOne);
726     }
727 
728     ///
729     static PreCollection!(TEntityTarget, DatabaseDriver) manyToMany(
730         TEntityTarget,
731         TEntitySource,
732         string joinTableName_ = joinTableName!(TEntitySource, TEntityTarget)
733     )(TEntitySource source)
734             if (isEntityType!TEntityTarget && isEntityType!TEntitySource)
735     {
736         enum Table joinTable = table(joinTableName_);
737 
738         enum string targetName = tableName!TEntityTarget;
739         enum Column columnForeignKeyTarget = col(joinTable, targetName ~ "_id");
740 
741         enum string sourceName = tableName!TEntitySource;
742         enum Column columnForeignKeySource = col(joinTable, sourceName ~ "_id");
743 
744         enum Column columnPrimaryKeyTarget = col(table(targetName), "id");
745 
746         enum Query q =
747             joinTable.qb
748                 .join(
749                     columnPrimaryKeyTarget,
750                     columnForeignKeyTarget
751                 )
752                 .where(columnForeignKeySource, '=');
753         enum pcT = PreCollection!(TEntityTarget, DatabaseDriver)(q);
754 
755         auto pc = pcT;
756         pc._query.updatePreSetWhereValue(0, DBValue(source.id));
757         return pc;
758     }
759 
760     /++
761         Assigns two entities with a many-to-many relation to each other
762      +/
763     void manyToManyAssign(
764         TEntity1,
765         TEntity2,
766         string joinTableName_ = joinTableName!(TEntity1, TEntity2),
767     )(TEntity1 e1, TEntity2 e2) if (isEntityType!TEntity1 && isEntityType!TEntity2)
768     {
769         enum Table joinTable = table(joinTableName_);
770         enum string e2Col = tableName!TEntity2 ~ "_id";
771         enum string e1Col = tableName!TEntity1 ~ "_id";
772 
773         enum BuiltQuery bq = joinTable.insert(e1Col, e2Col).build!DatabaseDriver();
774 
775         Statement stmt = _db.prepareBuiltQuery(bq);
776         stmt.bind(0, e1.id);
777         stmt.bind(1, e2.id);
778         stmt.execute();
779     }
780 
781     /++
782         Deletes the association from each other of two entities with a many-to-many relation
783      +/
784     void manyToManyUnassign(
785         TEntity1,
786         TEntity2,
787         string joinTableName_ = joinTableName!(TEntity1, TEntity2),
788     )(TEntity1 e1, TEntity2 e2) if (isEntityType!TEntity1 && isEntityType!TEntity2)
789     {
790         enum Table joinTable = table(joinTableName!(TEntity1, TEntity2));
791         enum string e2Col = tableName!TEntity2 ~ "_id";
792         enum string e1Col = tableName!TEntity1 ~ "_id";
793 
794         enum BuiltQuery bq = joinTable.qb
795                 .where(e1Col, '=')
796                 .where(e2Col, '=')
797                 .delete_()
798                 .build!DatabaseDriver();
799 
800         Statement stmt = _db.prepareBuiltQuery(bq);
801         stmt.bind(0, e1.id);
802         stmt.bind(1, e2.id);
803         stmt.execute();
804     }
805 
806     ///
807     static PreCollection!(TEntityMany, DatabaseDriver) oneToMany(
808         TEntityMany,
809         TEntityOne,
810     )(TEntityOne source) if (isEntityType!TEntityMany && isEntityType!TEntityOne)
811     {
812         enum string foreignKeyColumn = tableName!TEntityOne ~ "_id";
813 
814         enum Query q = table(tableName!TEntityMany).qb.where(foreignKeyColumn, '=');
815         enum pcT = PreCollection!(TEntityMany, DatabaseDriver)(q);
816 
817         auto pc = pcT;
818         pc._query.updatePreSetWhereValue(0, DBValue(source.id));
819         return pc;
820     }
821 
822     // debugging helper
823     static void _pragma(TEntity)()
824     {
825         pragma(msg, "==== EntityManager._pragma!(" ~ TEntity.stringof ~ "):");
826 
827         static if (!isEntityType!TEntity)
828         {
829             pragma(msg, "- ERROR: Not a compatible type");
830         }
831         else
832         {
833             pragma(msg, "- Table Name:\n" ~ tableName!TEntity);
834             pragma(msg, "- Column Names:");
835             pragma(msg, columnNames!TEntity);
836             pragma(msg, "- Column Names (no ID):");
837             pragma(msg, columnNamesNoID!TEntity);
838         }
839 
840         pragma(msg, "/====");
841     }
842 }
843 
844 /++
845     Mixin template to add the entity ID member to a type
846 
847     ---
848     struct MyEntity {
849         mixin EntityID;
850     }
851     ---
852 
853     $(TIP
854         You don’t have to use this, but it might help with reduction of boilerplate code.
855     )
856  +/
857 mixin template EntityID()
858 {
859     ulong id = 0;
860 }