1 module miniorm.queries;
2 
3 import std.algorithm : joiner, map;
4 import std.conv : to;
5 import std.exception : enforce;
6 import std.format : format;
7 import std..string : join;
8 
9 import d2sqlite3;
10 import sumtype;
11 
12 import miniorm.api : Miniorm;
13 import miniorm.exception;
14 import miniorm.schema : tableName, fieldToCol, fieldToCol, ColumnName;
15 
16 public import miniorm.query_ast : OrderingTermSort, InsertOpt;
17 
18 debug (miniorm) import std.stdio : stderr;
19 
20 version (unittest) {
21     import unit_threaded.assertions;
22 }
23 
24 auto select(T)() {
25     return Select!T(tableName!T);
26 }
27 
28 struct Select(T) {
29     import miniorm.query_ast;
30 
31     miniorm.query_ast.Select query;
32 
33     this(miniorm.query_ast.Select q) {
34         this.query = q;
35     }
36 
37     this(string from) {
38         this.query.from = Blob(from).From;
39     }
40 
41     /// Convert to a SQL statement that can e.g. be pretty printed.
42     Sql toSql() {
43         return query.Query.Sql;
44     }
45 
46     /// Count the number of matching rows.
47     auto count() @safe pure {
48         miniorm.query_ast.Select rval = query;
49         rval.columns.required = ResultColumn(ResultColumnExpr(Blob("count(*)")));
50         return Select!T(rval);
51     }
52 
53     /// Order the result by `s` in the order the fields are defined in `T`.
54     auto orderBy(OrderingTermSort s, string[] fields = null) @trusted pure {
55         OrderingTerm required;
56         OrderingTerm[] optional;
57 
58         if (fields) {
59             required = OrderingTerm(Blob("'" ~ fields[0] ~ "'"), s);
60             foreach (f; fields[1 .. $])
61                 optional ~= OrderingTerm(Blob("'" ~ f ~ "'"), s);
62         } else {
63             enum fields_ = fieldToCol!("", T);
64             static foreach (i, field; fields_) {
65                 static if (i == 0)
66                     required = OrderingTerm(Blob(field.quoteColumnName), s);
67                 else
68                     optional ~= OrderingTerm(Blob(field.quoteColumnName), s);
69             }
70         }
71 
72         miniorm.query_ast.Select rval = query;
73         rval.orderBy = OrderBy(required, optional);
74         return Select!T(rval);
75     }
76 
77     /// Limit the query to this number of answers
78     auto limit(long value) @trusted pure {
79         import std.conv : to;
80 
81         miniorm.query_ast.Select rval = query;
82         rval.limit = Limit(Blob(value.to!string));
83         return Select!T(rval);
84     }
85 
86     mixin WhereMixin!(T, typeof(this), miniorm.query_ast.Select);
87 }
88 
89 unittest {
90     static struct Foo {
91         ulong id;
92         string text;
93         ulong ts;
94     }
95 
96     select!Foo.where("foo = :bar", Bind("bar")).or("batman IS NULL")
97         .and("batman = :hero", Bind("hero")).toSql.toString.shouldEqual(
98                 "SELECT * FROM Foo WHERE foo = :bar OR batman IS NULL AND batman = :hero;");
99 }
100 
101 @("shall be possible to have a member of enum type")
102 unittest {
103     static struct Foo {
104         enum MyEnum : string {
105             foo = "batman",
106             bar = "robin",
107         }
108 
109         ulong id;
110         MyEnum enum_;
111     }
112 
113     select!Foo.where("enum_ = 'robin'")
114         .toSql.toString.shouldEqual("SELECT * FROM Foo WHERE enum_ = 'robin';");
115 }
116 
117 auto insert(T)() {
118     return Insert!T(tableName!T).insert;
119 }
120 
121 auto insertOrReplace(T)() {
122     return Insert!T(tableName!T).insertOrReplace;
123 }
124 
125 auto insertOrIgnore(T)() {
126     return Insert!T(tableName!T).insertOrIgnore;
127 }
128 
129 struct Insert(T) {
130     import miniorm.query_ast;
131 
132     miniorm.query_ast.Insert query;
133 
134     this(miniorm.query_ast.Insert q) {
135         this.query = q;
136     }
137 
138     this(string tableName) {
139         this.query.table = TableRef(tableName);
140     }
141 
142     /// Convert to a SQL statement that can e.g. be pretty printed.
143     Sql toSql() {
144         return query.Query.Sql;
145     }
146 
147     void run(ref Miniorm db) {
148         db.run(toSql.toString);
149     }
150 
151     auto op(InsertOpt o) @safe pure nothrow const @nogc {
152         miniorm.query_ast.Insert rval = query;
153         rval.opt = o;
154         return Insert!T(rval);
155     }
156 
157     /// Returns: number of values that the query is sized for.
158     size_t getValues() {
159         return query.values.value.match!((Values v) => 1 + v.optional.length, _ => 0);
160     }
161 
162     /// Returns: number of columns to insert per value.
163     size_t getColumns() {
164         return query.columns.value.match!((ColumnNames v) => 1 + v.optional.length, (None v) => 0);
165     }
166 
167     /// Number of values the user wants to insert.
168     auto values(size_t cnt)
169     in(cnt >= 1, "values must be >=1") {
170         import std.array : array;
171         import std.range : repeat;
172 
173         Value val;
174         val.required = Expr("?");
175         val.optional = query.columns.value.match!((ColumnNames v) => Expr("?")
176                 .repeat(v.optional.length).array, (None v) => null);
177 
178         Values values;
179         foreach (i; 0 .. cnt) {
180             if (i == 0)
181                 values.required = val;
182             else
183                 values.optional ~= val;
184         }
185 
186         miniorm.query_ast.Insert rval = query;
187         rval.values = InsertValues(values);
188         return Insert!T(rval);
189     }
190 
191     /// Insert a new row.
192     auto insert() @safe pure nothrow const {
193         return op(InsertOpt.Insert).setColumns(true);
194     }
195 
196     /// Insert or replace an existing row.
197     auto insertOrReplace() @safe pure nothrow const {
198         return op(InsertOpt.InsertOrReplace).setColumns(false);
199     }
200 
201     auto insertOrIgnore() @safe pure nothrow const {
202         return op(InsertOpt.InsertOrIgnore).setColumns(false);
203     }
204 
205     // TODO the name is bad.
206     /// Specify columns to insert/replace values in.
207     private auto setColumns(bool insert_) @safe pure const {
208         enum fields = fieldToCol!("", T);
209 
210         ColumnNames columns;
211         bool addRequired = true;
212         foreach (field; fields) {
213             if (field.isPrimaryKey && insert_)
214                 continue;
215 
216             if (addRequired) {
217                 columns.required = miniorm.query_ast.ColumnName(field.columnName);
218                 addRequired = false;
219             } else
220                 columns.optional ~= miniorm.query_ast.ColumnName(field.columnName);
221         }
222 
223         miniorm.query_ast.Insert rval = query;
224         rval.columns = InsertColumns(columns);
225         return Insert!T(rval);
226     }
227 }
228 
229 unittest {
230     static struct Foo {
231         ulong id;
232         string text;
233         float val;
234         ulong ts;
235 
236         @ColumnName("version")
237         string version_;
238     }
239 
240     insertOrReplace!Foo.values(1).toSql.toString.shouldEqual(
241             "INSERT OR REPLACE INTO Foo ('id','text','val','ts','version') VALUES (?,?,?,?,?);");
242     insert!Foo.values(1).toSql.toString.shouldEqual(
243             "INSERT INTO Foo ('text','val','ts','version') VALUES (?,?,?,?);");
244 
245     insertOrReplace!Foo.values(2).toSql.toString.shouldEqual(
246             "INSERT OR REPLACE INTO Foo ('id','text','val','ts','version') VALUES (?,?,?,?,?),(?,?,?,?,?);");
247 
248     insertOrIgnore!Foo.values(2).toSql.toString.shouldEqual(
249             "INSERT OR IGNORE INTO Foo ('id','text','val','ts','version') VALUES (?,?,?,?,?),(?,?,?,?,?);");
250 
251     insert!Foo.values(2).toSql.toString.shouldEqual(
252             "INSERT INTO Foo ('text','val','ts','version') VALUES (?,?,?,?),(?,?,?,?);");
253 }
254 
255 unittest {
256     static struct Foo {
257         ulong id;
258         string text;
259         float val;
260         ulong ts;
261     }
262 
263     static struct Bar {
264         ulong id;
265         float value;
266         Foo foo;
267     }
268 
269     insertOrReplace!Bar.values(1).toSql.toString.shouldEqual(
270             "INSERT OR REPLACE INTO Bar ('id','value','foo.id','foo.text','foo.val','foo.ts') VALUES (?,?,?,?,?,?);");
271     insertOrIgnore!Bar.values(1).toSql.toString.shouldEqual(
272             "INSERT OR IGNORE INTO Bar ('id','value','foo.id','foo.text','foo.val','foo.ts') VALUES (?,?,?,?,?,?);");
273     insert!Bar.values(1).toSql.toString.shouldEqual(
274             "INSERT INTO Bar ('value','foo.id','foo.text','foo.val','foo.ts') VALUES (?,?,?,?,?);");
275     insert!Bar.values(3).toSql.toString.shouldEqual(
276             "INSERT INTO Bar ('value','foo.id','foo.text','foo.val','foo.ts') VALUES (?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?);");
277 }
278 
279 unittest {
280     struct Foo {
281         string text;
282         float val;
283         ulong ts;
284     }
285 
286     struct Bar {
287         float v;
288         Foo foo;
289     }
290 
291     struct Baz {
292         ulong id;
293         float v;
294         Bar xyz;
295         float w;
296     }
297 
298     insertOrReplace!Baz.values(1).toSql.toString.shouldEqual("INSERT OR REPLACE INTO Baz ('id','v','xyz.v','xyz.foo.text','xyz.foo.val','xyz.foo.ts','w') VALUES (?,?,?,?,?,?,?);");
299 }
300 
301 auto delete_(T)() {
302     return Delete!T(tableName!T);
303 }
304 
305 struct Delete(T) {
306     import miniorm.query_ast;
307 
308     miniorm.query_ast.Delete query;
309 
310     this(miniorm.query_ast.Delete q) {
311         this.query = q;
312     }
313 
314     this(string tableName) {
315         this.query.table = TableRef(tableName);
316     }
317 
318     /// Convert to a SQL statement that can e.g. be pretty printed.
319     Sql toSql() {
320         return query.Query.Sql;
321     }
322 
323     void run(ref Miniorm db) {
324         db.run(toSql.toString);
325     }
326 
327     mixin WhereMixin!(T, typeof(this), miniorm.query_ast.Delete);
328 }
329 
330 mixin template WhereMixin(T, QueryT, AstT) {
331     import std.datetime : SysTime;
332     import std.traits : isNumeric, isSomeString;
333 
334     Bind[] binds;
335 
336     /// Add a WHERE clause with a bound value.
337     auto where(string lhs, Bind[] b = null) {
338         binds ~= b;
339         return this.where(lhs);
340     }
341 
342     auto where(string lhs, Bind b) {
343         return this.where(lhs, [b]);
344     }
345 
346     /// Add a WHERE condition.
347     private auto where(string condition) @trusted pure {
348         import miniorm.query_ast;
349 
350         static struct WhereOptional {
351             QueryT value;
352             alias value this;
353 
354             private auto where(string condition, WhereOp op, Bind[] b) @trusted pure {
355                 QueryT rval = value;
356                 rval.binds ~= b;
357 
358                 Where w = value.query.where.tryMatch!((Where v) => v);
359                 WhereExpr we = w.tryMatch!((WhereExpr v) => v);
360                 we.optional ~= WhereExpr.Opt(op, Expr(condition));
361                 rval.query.where = Where(we);
362                 return WhereOptional(rval);
363             }
364 
365             WhereOptional and(string lhs, Bind[] b = null) @safe pure {
366                 return where(lhs, WhereOp.AND, b);
367             }
368 
369             WhereOptional and(string lhs, Bind b) @safe pure {
370                 return where(lhs, WhereOp.AND, [b]);
371             }
372 
373             WhereOptional or(string lhs, Bind[] b = null) @safe pure {
374                 return where(lhs, WhereOp.OR, b);
375             }
376 
377             WhereOptional or(string lhs, Bind b) @safe pure {
378                 return where(lhs, WhereOp.OR, [b]);
379             }
380         }
381 
382         AstT ast = query;
383         ast.where = WhereExpr(Expr(condition)).Where;
384 
385         auto rval = QueryT(ast);
386         rval.binds = binds;
387 
388         return WhereOptional(rval);
389     }
390 }
391 
392 struct Bind {
393     import std.range : isOutputRange, put;
394 
395     alias Key = SumType!(string, int);
396 
397     Key key;
398 
399     this(string k) {
400         key = Key(k);
401     }
402 
403     this(int k) {
404         key = Key(k);
405     }
406 
407     string toString() @safe pure const {
408         import std.array : appender;
409 
410         auto buf = appender!string;
411         toString(buf);
412         return buf.data;
413     }
414 
415     void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) {
416         key.match!((string k) { put(w, ":"); put(w, k); }, (int k) {
417             put(w, k.to!string);
418         });
419     }
420 }
421 
422 unittest {
423     static struct Foo {
424         ulong id;
425         string text;
426         ulong ts;
427     }
428 
429     delete_!Foo.where("text = privet").and("ts > 123")
430         .toSql.toString.shouldEqual("DELETE FROM Foo WHERE text = privet AND ts > 123;");
431 }
432 
433 auto count(T)() {
434     return Count!T(tableName!T);
435 }
436 
437 struct Count(T) {
438     import miniorm.query_ast : Sql;
439 
440     Select!T query_;
441 
442     this(miniorm.query_ast.Select q) {
443         this.query_ = Select!T(q);
444     }
445 
446     this(string from) {
447         this.query_ = Select!T(from).count;
448     }
449 
450     /// Convert to a SQL statement that can e.g. be pretty printed.
451     Sql toSql() {
452         return query_.toSql;
453     }
454 
455     private ref miniorm.query_ast.Select query() @safe pure nothrow @nogc {
456         return query_.query;
457     }
458 
459     mixin WhereMixin!(T, typeof(this), miniorm.query_ast.Select);
460 }
461 
462 unittest {
463     static struct Foo {
464         ulong id;
465         string text;
466         ulong ts;
467     }
468 
469     count!Foo.where("text = privet").and("ts > 123").toSql.toString.shouldEqual(
470             "SELECT count(*) FROM Foo WHERE text = privet AND ts > 123;");
471 }