1 /++
2     SQLite3 Database Driver
3 
4     ---
5     DatabaseDriver db = new SQLite3("my-database-file.sqlite3");
6 
7     db.connect(); // establish database connection
8     scope(exit) db.close(); // scope guard, to close the database connection when exiting the current scope
9     ---
10 +/
11 module oceandrift.db.sqlite3;
12 
13 import etc.c.sqlite3;
14 import std.array : appender, Appender;
15 import std.string : fromStringz, toStringz;
16 import std.format : format;
17 
18 import oceandrift.db.dbal.driver;
19 import oceandrift.db.dbal.v4;
20 
21 @safe:
22 
23 private enum
24 {
25     formatDate = "%04d-%02u-%02u",
26     formatTime = "%02u:%02u:%02u",
27     formatDateTime = formatDate ~ ' ' ~ formatTime,
28 }
29 
30 private void enforce(
31     const int actual,
32     lazy string msg,
33     lazy uint extendedResultCode = -1,
34     ResultCode expected = ResultCode.ok,
35     string file = __FILE__,
36     size_t line = __LINE__) pure
37 {
38     if (actual != expected)
39         throw new SQLiteX(cast(ResultCode) actual, msg, extendedResultCode, file, line);
40 }
41 
42 class SQLiteX : Exception
43 {
44 @safe pure:
45 
46     public
47     {
48         ResultCode code;
49         uint extendedCode;
50     }
51 
52     this(ResultCode code, string msg, uint extendedCode, string file = __FILE__, size_t line = __LINE__)
53     {
54         super(msg, file, line);
55         this.code = code;
56         this.extendedCode = extendedCode;
57     }
58 }
59 
60 /++
61     Modes to open SQLite3 databases in
62  +/
63 enum OpenMode
64 {
65     /// read-only
66     ro = SQLITE_OPEN_READONLY,
67 
68     /// reading & writing
69     rw = SQLITE_OPEN_READWRITE,
70 
71     /// reading & writing, create if not exists
72     create = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
73 
74     /// in-memory
75     memory = OpenMode.create | SQLITE_OPEN_MEMORY,
76 
77     /// “multi-thread” mode, separate threads threads may not share database connections
78     multiThreadindMultiDB = SQLITE_OPEN_NOMUTEX,
79 
80     /// “serialized” mode, threads can share database connections
81     multiThreadingSerialized = SQLITE_OPEN_FULLMUTEX,
82 
83     /// database filename cannot be a symbolic link
84     noSymLink = SQLITE_OPEN_NOFOLLOW,
85 }
86 
87 /++
88     SQLite3 function result code
89 
90     Named Enum Wrapper for sqlite’s C API result codes
91  +/
92 enum ResultCode
93 {
94     /// Successful result
95     ok = SQLITE_OK,
96 
97     /// Generic error
98     error = SQLITE_ERROR,
99 
100     /// Internal logic error in SQLite
101     internalDatabaseError = SQLITE_INTERNAL,
102 
103     /// Access permission denied
104     permission = SQLITE_PERM,
105 
106     /// Callback routine requested an abort
107     abort = SQLITE_ABORT,
108 
109     /// The database file is locked
110     busy = SQLITE_BUSY,
111 
112     /// A table in the database is locked
113     locked = SQLITE_LOCKED,
114 
115     /// A malloc() failed
116     noMem = SQLITE_NOMEM,
117 
118     /// Attempt to write a readonly database
119     readOnly = SQLITE_READONLY,
120 
121     /// Operation terminated by sqlite3_interrupt()
122     interrupt = SQLITE_INTERRUPT,
123 
124     /// Some kind of disk I/O error occurred
125     ioError = SQLITE_IOERR,
126 
127     /// The database disk image is malformed
128     corruptDiskImage = SQLITE_CORRUPT,
129 
130     /// Unknown opcode in sqlite3_file_control()
131     opCodeNotFound = SQLITE_NOTFOUND,
132 
133     /// Insertion failed because database is full
134     dbFull = SQLITE_FULL,
135 
136     /// Unable to open the database file
137     cantOpen = SQLITE_CANTOPEN,
138 
139     /// Database lock protocol error
140     protocolError = SQLITE_PROTOCOL,
141 
142     /// Internal use only
143     empty = SQLITE_EMPTY,
144 
145     /// The database schema changed
146     schemaChanged = SQLITE_SCHEMA,
147 
148     /// String or BLOB exceeds size limit
149     tooBig = SQLITE_TOOBIG,
150 
151     /// Abort due to constraint violation
152     constraintViolation = SQLITE_CONSTRAINT,
153 
154     /// Data type mismatch
155     typeMismatch = SQLITE_MISMATCH,
156 
157     /// Library used incorrectly
158     libraryMisuse = SQLITE_MISUSE,
159 
160     /// Uses OS features not supported on host
161     noLFS = SQLITE_NOLFS,
162 
163     /// Authorization denied
164     authDenied = SQLITE_AUTH,
165 
166     /// Not used
167     format = SQLITE_FORMAT,
168 
169     /// 2nd parameter to sqlite3_bind out of range
170     outOfRange = SQLITE_RANGE,
171 
172     /// File opened that is not a database file
173     notADatabase = SQLITE_NOTADB,
174 
175     /// sqlite3_step() has finished executing
176     done = SQLITE_DONE,
177 
178     /// sqlite3_step() has another row ready
179     row = SQLITE_ROW,
180 
181     ///
182     notice = SQLITE_NOTICE,
183 
184     ///
185     warning = SQLITE_WARNING,
186 }
187 
188 /++
189     SQLite3 oceandrift database driver
190 
191     Built upon the sqlite3 C library
192  +/
193 final class SQLite3 : DatabaseDriverSpec
194 {
195 @safe:
196     private
197     {
198         sqlite3* _handle;
199         OpenMode _mode;
200         string _filename;
201     }
202 
203     public this(string filename = ":memory:", OpenMode mode = OpenMode.create)
204     {
205         _filename = filename;
206         _mode = mode;
207     }
208 
209     OpenMode mode() pure nothrow @nogc
210     {
211         return _mode;
212     }
213 
214     public
215     {
216         void connect()
217         {
218             connectImpl();
219         }
220 
221         bool connected() nothrow
222         {
223             return (_handle !is null);
224         }
225 
226         void close()
227         {
228             if (connected())
229                 closeImpl();
230         }
231     }
232 
233     public
234     {
235         bool autoCommit() @trusted
236         {
237             return (_handle.sqlite3_get_autocommit != 0);
238         }
239 
240         void autoCommit(bool enable)
241         {
242             assert(0); // no
243         }
244 
245         void transactionStart()
246         {
247             exec("BEGIN TRANSACTION");
248         }
249 
250         void transactionCommit()
251         {
252             exec("COMMIT");
253         }
254 
255         void transactionRollback()
256         {
257             exec("ROLLBACK");
258         }
259     }
260 
261     public
262     {
263         void execute(string sql)
264         {
265             exec(sql);
266         }
267 
268         Statement prepare(string sql)
269         {
270             return new SQLite3Statement(_handle, sql);
271         }
272 
273         DBValue lastInsertID() @trusted
274         {
275             return DBValue(_handle.sqlite3_last_insert_rowid());
276         }
277     }
278 
279     public
280     {
281         ///
282         sqlite3* getConnection()
283         {
284             return _handle;
285         }
286     }
287 
288     public pure  // Query Compiler
289     {
290         static BuiltQuery build(const Select select)
291         {
292             auto sql = appender!string("SELECT");
293 
294             foreach (idx, se; select.columns)
295             {
296                 if (idx > 0)
297                     sql ~= ',';
298 
299                 se.toSQL(sql);
300             }
301 
302             sql ~= ` FROM "`;
303             sql ~= select.query.table.name.escapeIdentifier();
304             sql ~= '"';
305 
306             const query = CompilerQuery(select.query);
307             query.join.joinToSQL(sql);
308             query.where.whereToSQL(sql);
309             query.orderByToSQL(sql);
310             query.limitToSQL(sql);
311 
312             return BuiltQuery(
313                 sql.data,
314                 PlaceholdersMeta(query.where.placeholders),
315                 PreSets(query.where.preSet, query.limit.preSet, query.limit.offsetPreSet)
316             );
317         }
318 
319         static BuiltQuery build(const Update update)
320         in (update.columns.length >= 1)
321         in (CompilerQuery(update.query).join.length == 0)
322         {
323             auto sql = appender!string("UPDATE");
324             sql ~= ` "`;
325             sql ~= update.query.table.name.escapeIdentifier();
326             sql ~= `" SET`;
327 
328             foreach (idx, value; update.columns)
329             {
330                 if (idx > 0)
331                     sql ~= ',';
332 
333                 sql ~= ` "`;
334                 sql ~= value.escapeIdentifier;
335                 sql ~= `" = ?`;
336             }
337 
338             const query = CompilerQuery(update.query);
339             query.where.whereToSQL(sql);
340             query.orderByToSQL(sql);
341             query.limitToSQL(sql);
342 
343             return BuiltQuery(
344                 sql.data,
345                 PlaceholdersMeta(query.where.placeholders),
346                 PreSets(query.where.preSet, query.limit.preSet, query.limit.offsetPreSet)
347             );
348         }
349 
350         static BuiltQuery build(const Insert query)
351         in (
352             (query.columns.length > 1)
353             || (query.rowCount == 1)
354         )
355         {
356             auto sql = appender!string(`INSERT INTO "`);
357             sql ~= escapeIdentifier(query.table.name);
358 
359             if (query.columns.length == 0)
360             {
361                 sql ~= `" DEFAULT VALUES`;
362             }
363             else
364             {
365                 sql ~= `" (`;
366 
367                 foreach (idx, column; query.columns)
368                 {
369                     if (idx > 0)
370                         sql ~= ", ";
371 
372                     sql ~= '"';
373                     sql ~= escapeIdentifier(column);
374                     sql ~= '"';
375                 }
376 
377                 sql ~= ") VALUES";
378 
379                 for (uint n = 0; n < query.rowCount; ++n)
380                 {
381                     if (n > 0)
382                         sql ~= ",";
383 
384                     sql ~= " (";
385                     if (query.columns.length > 0)
386                     {
387                         sql ~= '?';
388 
389                         if (query.columns.length > 1)
390                             for (size_t i = 1; i < query.columns.length; ++i)
391                                 sql ~= ",?";
392                     }
393                     sql ~= ')';
394                 }
395             }
396 
397             return BuiltQuery(sql.data);
398         }
399 
400         static BuiltQuery build(const Delete delete_)
401         in (CompilerQuery(delete_.query).join.length == 0)
402         {
403             auto sql = appender!string(`DELETE FROM "`);
404             sql ~= delete_.query.table.name.escapeIdentifier();
405             sql ~= '"';
406 
407             const query = CompilerQuery(delete_.query);
408 
409             query.where.whereToSQL(sql);
410             query.orderByToSQL(sql);
411             query.limitToSQL(sql);
412 
413             return BuiltQuery(
414                 sql.data,
415                 PlaceholdersMeta(query.where.placeholders),
416                 PreSets(query.where.preSet, query.limit.preSet, query.limit.offsetPreSet)
417             );
418         }
419     }
420 
421     private
422     {
423         void connectImpl() @trusted
424         {
425             immutable connected = sqlite3_open_v2(_filename.toStringz, &_handle, int(_mode), null);
426             enforce(connected, "Connection failed");
427             enforce(_handle.sqlite3_extended_result_codes(1), "Enabling SQLite's extended result codes failed");
428             _mode = mode;
429         }
430 
431         void closeImpl() @trusted
432         {
433             immutable closed = _handle.sqlite3_close_v2();
434             enforce(closed, "Failed to close connection");
435             _handle = null;
436         }
437 
438         void exec(string sql) @trusted
439         {
440             char* errorMsg;
441             immutable status = sqlite3_exec(_handle, sql.toStringz, null, null, &errorMsg);
442             try
443             {
444                 enforce(
445                     status,
446                     errorMsg.fromStringz.idup,
447                     _handle.sqlite3_extended_errcode(),
448                     ResultCode.ok,
449                 );
450             }
451             finally
452             {
453                 if (errorMsg !is null)
454                     sqlite3_free(errorMsg);
455             }
456         }
457     }
458 }
459 
460 // undocumented on purpose
461 final class SQLite3Statement : Statement
462 {
463 @safe:
464 
465     private
466     {
467         sqlite3* _dbHandle;
468         sqlite3_stmt* _stmtHandle;
469         int _status;
470         Row _front;
471 
472     }
473 
474     private this(sqlite3* dbHandle, string sql) @trusted
475     {
476         _dbHandle = dbHandle;
477 
478         // If nByte is positive, then it is the number of bytes read from zSql.
479         // No zero-terminated required.
480         immutable prepared = _dbHandle.sqlite3_prepare_v2(sql.ptr, cast(int) sql.length, &_stmtHandle, null);
481 
482         enforce(
483             prepared,
484             "Preparation failed:\n" ~ _dbHandle.sqlite3_errmsg()
485                 .fromStringz.idup, // “The application does not need to worry about freeing the result.”
486                 _dbHandle.sqlite3_extended_errcode(),
487         );
488     }
489 
490     public
491     {
492         void execute() @trusted
493         {
494             this.popFront();
495         }
496 
497         void close() @trusted
498         {
499             // Invoking sqlite3_finalize() on a NULL pointer is a harmless no-op.
500             _stmtHandle.sqlite3_finalize();
501             _stmtHandle = null;
502         }
503     }
504 
505     public
506     {
507         bool empty() pure nothrow @nogc
508         {
509             return (this._status != SQLITE_ROW);
510         }
511 
512         void popFront() @trusted
513         {
514             _status = _stmtHandle.sqlite3_step();
515 
516             if ((_status != SQLITE_ROW) && (_status != SQLITE_DONE))
517             {
518                 enforce(
519                     _status,
520                     cast(immutable) _dbHandle.sqlite3_errmsg.fromStringz,
521                     ResultCode.row,
522                 );
523             }
524 
525             this.populateRow();
526         }
527 
528         Row front() pure nothrow @nogc
529         {
530             return _front;
531         }
532     }
533 
534     public
535     {
536         void bind(int index, const bool value)
537         {
538             return this.bind(index, int(value));
539         }
540 
541         void bind(int index, const byte value)
542         {
543             return this.bind(index, int(value));
544         }
545 
546         void bind(int index, const ubyte value)
547         {
548             return this.bind(index, int(value));
549         }
550 
551         void bind(int index, const short value)
552         {
553             return this.bind(index, int(value));
554         }
555 
556         void bind(int index, const ushort value)
557         {
558             return this.bind(index, int(value));
559         }
560 
561         void bind(int index, const int value) @trusted
562         {
563             this.resetIfNeeded();
564 
565             immutable x = _stmtHandle.sqlite3_bind_int(index + 1, value);
566             enforce(x, "Parameter binding failed");
567         }
568 
569         void bind(int index, const uint value) @trusted
570         {
571             return this.bind(index, long(value));
572         }
573 
574         void bind(int index, const long value) @trusted
575         {
576             this.resetIfNeeded();
577 
578             immutable x = _stmtHandle.sqlite3_bind_int64(index + 1, value);
579             enforce(x, "Parameter binding failed");
580         }
581 
582         // WARNING: will store value as if it were signed (→ long)
583         void bind(int index, const ulong value)
584         {
585             return this.bind(index, long(value));
586         }
587 
588         void bind(int index, const double value) @trusted
589         {
590             this.resetIfNeeded();
591 
592             immutable x = _stmtHandle.sqlite3_bind_double(index + 1, value);
593             enforce(x, "Parameter binding failed");
594         }
595 
596         void bind(int index, const const(ubyte)[] value) @trusted
597         {
598             this.resetIfNeeded();
599 
600             immutable x = _stmtHandle.sqlite3_bind_blob64(index + 1, cast(void*) value.ptr, value.length, SQLITE_STATIC);
601             enforce(x, "Parameter binding failed");
602         }
603 
604         void bind(int index, const string value) @trusted
605         {
606             this.resetIfNeeded();
607 
608             immutable x = _stmtHandle.sqlite3_bind_text64(index + 1, value.ptr, value.length, SQLITE_STATIC, SQLITE_UTF8);
609             enforce(x, "Parameter binding failed");
610         }
611 
612         void bind(int index, const DateTime value)
613         {
614             return this.bind(
615                 index,
616                 format!formatDateTime(
617                     value.year, value.month, value.day,
618                     value.hour, value.minute, value.second),
619             );
620         }
621 
622         void bind(int index, const TimeOfDay value)
623         {
624             return this.bind(
625                 index,
626                 format!formatTime(value.hour, value.minute, value.second)
627             );
628         }
629 
630         void bind(int index, const Date value)
631         {
632             return this.bind(
633                 index,
634                 format!formatDate(value.year, value.month, value.day)
635             );
636         }
637 
638         void bind(int index, const typeof(null)) @trusted
639         {
640             this.resetIfNeeded();
641             immutable x = _stmtHandle.sqlite3_bind_null(index + 1);
642             enforce(x, "Parameter binding failed");
643         }
644     }
645 
646     private
647     {
648         void resetIfNeeded() @trusted
649         {
650             if (_status == 0)
651                 return;
652 
653             immutable r = _stmtHandle.sqlite3_reset();
654             enforce(r, "Reset failed");
655             _status = 0;
656         }
657 
658         void populateRow() @trusted
659         {
660             int cntColumns = _stmtHandle.sqlite3_column_count;
661             auto rowData = new DBValue[](cntColumns);
662 
663             foreach (n; 0 .. cntColumns)
664             {
665                 int colType = _stmtHandle.sqlite3_column_type(n);
666                 final switch (colType)
667                 {
668                 case SQLITE_INTEGER:
669                     rowData[n] = DBValue(_stmtHandle.sqlite3_column_int64(n));
670                     break;
671 
672                 case SQLITE_FLOAT:
673                     rowData[n] = DBValue(_stmtHandle.sqlite3_column_double(n));
674                     break;
675 
676                 case SQLITE3_TEXT:
677                     const sqliteText = _stmtHandle.sqlite3_column_text(n);
678                     auto textSlice = sqliteText.fromStringz;
679                     string textString = textSlice.idup;
680                     rowData[n] = DBValue(textString);
681                     break;
682 
683                 case SQLITE_BLOB:
684                     immutable cntBytes = _stmtHandle.sqlite3_column_bytes(n);
685                     const blob = _stmtHandle.sqlite3_column_blob(n);
686                     const blobBytes = cast(ubyte[]) blob[0 .. cntBytes];
687                     const(ubyte)[] blobArray = blobBytes.idup;
688                     rowData[n] = DBValue(blobArray);
689                     break;
690 
691                 case SQLITE_NULL:
692                     rowData[n] = DBValue(null);
693                     break;
694                 }
695             }
696 
697             _front = Row(rowData);
698         }
699     }
700 }
701 
702 static assert(isQueryCompiler!SQLite3);
703 
704 private
705 {
706 pure:
707 
708     void joinToSQL(const Join[] joinClause, ref Appender!string sql)
709     {
710         foreach (join; joinClause)
711         {
712             final switch (join.type) with (Join)
713             {
714             case Type.invalid:
715                 assert(0, "Join.Type.invalid");
716 
717             case Type.inner:
718                 sql ~= ` JOIN "`;
719                 break;
720 
721             case Type.leftOuter:
722                 sql ~= ` LEFT OUTER JOIN "`;
723                 break;
724 
725             case Type.rightOuter:
726                 sql ~= ` RIGHT OUTER JOIN "`;
727                 break;
728 
729             case Type.fullOuter:
730                 sql ~= ` FULL OUTER JOIN "`;
731                 break;
732 
733             case Type.cross:
734                 sql ~= ` CROSS JOIN "`;
735                 break;
736             }
737 
738             sql ~= escapeIdentifier(join.target.table.name);
739             sql ~= `"`;
740 
741             if (join.target.name is null)
742                 return;
743 
744             sql ~= ` ON "`;
745             sql ~= escapeIdentifier(join.target.table.name);
746             sql ~= `"."`;
747             sql ~= escapeIdentifier(join.target.name);
748             sql ~= `" = "`;
749 
750             if (join.source.table.name !is null)
751             {
752                 sql ~= escapeIdentifier(join.source.table.name);
753                 sql ~= `"."`;
754             }
755 
756             sql ~= escapeIdentifier(join.source.name);
757             sql ~= '"';
758         }
759     }
760 
761     void whereToSQL(const Where where, ref Appender!string sql)
762     {
763         if (where.tokens.length == 0)
764             return;
765 
766         sql ~= " WHERE";
767 
768         Token.Type prev;
769 
770         foreach (Token t; where.tokens)
771         {
772             final switch (t.type) with (Token)
773             {
774             case Type.columnTable:
775                 sql ~= ` "`;
776                 (delegate() @trusted { sql ~= t.data.str.escapeIdentifier(); })();
777                 sql ~= `".`;
778                 break;
779             case Type.column:
780                 if (prev != Type.columnTable)
781                     sql ~= ' ';
782                 sql ~= '"';
783                 (delegate() @trusted { sql ~= t.data.str.escapeIdentifier(); })();
784                 sql ~= '"';
785                 break;
786             case Type.placeholder:
787                 sql ~= " ?";
788                 break;
789             case Type.comparisonOperator:
790                 sql ~= t.data.op.toSQL;
791                 break;
792 
793             case Type.and:
794                 sql ~= " AND";
795                 break;
796             case Type.or:
797                 sql ~= " OR";
798                 break;
799 
800             case Type.not:
801                 sql ~= " NOT";
802                 break;
803 
804             case Type.leftParenthesis:
805                 sql ~= " (";
806                 break;
807             case Type.rightParenthesis:
808                 sql ~= " )";
809                 break;
810 
811             case Type.invalid:
812                 assert(0, "Invalid SQL token in where clause");
813             }
814 
815             prev = t.type;
816         }
817     }
818 
819     void limitToSQL(CompilerQuery q, ref Appender!string sql)
820     {
821         if (!q.limit.enabled)
822             return;
823 
824         sql ~= " LIMIT ?";
825 
826         if (!q.limit.offsetEnabled)
827             return;
828 
829         sql ~= " OFFSET ?";
830     }
831 
832     void orderByToSQL(CompilerQuery q, ref Appender!string sql)
833     {
834         if (q.orderBy.length == 0)
835             return;
836 
837         sql ~= " ORDER BY ";
838 
839         foreach (idx, OrderingTerm term; q.orderBy)
840         {
841             if (idx > 0)
842                 sql ~= ", ";
843 
844             if (term.column.table.name !is null)
845             {
846                 sql ~= '"';
847                 sql ~= escapeIdentifier(term.column.table.name);
848                 sql ~= `".`;
849             }
850             sql ~= '"';
851             sql ~= escapeIdentifier(term.column.name);
852             sql ~= '"';
853 
854             if (term.orderingSequence == OrderingSequence.desc)
855                 sql ~= " DESC";
856         }
857     }
858 
859     void toSQL(SelectExpression se, ref Appender!string sql)
860     {
861         sql ~= ' ';
862 
863         enum switchCase(string aggr) = `case ` ~ aggr ~ `: sql ~= "` ~ aggr ~ `("; break;`;
864 
865         final switch (se.aggregateFunction) with (AggregateFunction)
866         {
867             mixin(switchCase!"avg");
868             mixin(switchCase!"count");
869             mixin(switchCase!"max");
870             mixin(switchCase!"min");
871             mixin(switchCase!"sum");
872             mixin(switchCase!"group_concat");
873         case none:
874             break;
875         }
876 
877         if (se.distinct)
878             sql ~= "DISTINCT ";
879 
880         if (se.column.table.name !is null)
881         {
882             sql ~= '"';
883             sql ~= se.column.table.name;
884             sql ~= `".`;
885         }
886 
887         if (se.column.name == "*")
888         {
889             sql ~= '*';
890         }
891         else
892         {
893             sql ~= '"';
894             sql ~= se.column.name.escapeIdentifier;
895             sql ~= '"';
896         }
897 
898         if (se.aggregateFunction != AggregateFunction.none)
899             sql ~= ')';
900     }
901 
902     string toSQL(ComparisonOperator op)
903     {
904         final switch (op) with (ComparisonOperator)
905         {
906         case invalid:
907             assert(0, "Invalid comparison operator");
908 
909         case equals:
910             return " =";
911         case notEquals:
912             return " <>";
913         case lessThan:
914             return " <";
915         case greaterThan:
916             return " >";
917         case lessThanOrEquals:
918             return " <=";
919         case greaterThanOrEquals:
920             return " >=";
921         case in_:
922             return " IN";
923         case notIn:
924             return " NOT IN";
925         case like:
926             return " LIKE";
927         case notLike:
928             return " NOT LIKE";
929         case isNull:
930             return " IS NULL";
931         case isNotNull:
932             return " IS NOT NULL";
933         }
934     }
935 
936     string escapeIdentifier(string tableOrColumn) pure
937     {
938         import std.string : replace;
939 
940         return tableOrColumn.replace('"', `""`);
941     }
942 }