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