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) @safe 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) @safe 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 struct Insert(T) {
124     import miniorm.query_ast;
125 
126     miniorm.query_ast.Insert query;
127 
128     this(miniorm.query_ast.Insert q) {
129         this.query = q;
130     }
131 
132     this(string tableName) {
133         this.query.table = TableRef(tableName);
134     }
135 
136     /// Convert to a SQL statement that can e.g. be pretty printed.
137     Sql toSql() {
138         return query.Query.Sql;
139     }
140 
141     void run(ref Miniorm db) {
142         db.run(toSql.toString);
143     }
144 
145     auto op(InsertOpt o) @safe pure nothrow const @nogc {
146         miniorm.query_ast.Insert rval = query;
147         rval.opt = o;
148         return Insert!T(rval);
149     }
150 
151     /// Returns: number of values that the query is sized for.
152     size_t getValues() {
153         return query.values.value.match!((Values v) => 1 + v.optional.length, _ => 0);
154     }
155 
156     /// Returns: number of columns to insert per value.
157     size_t getColumns() {
158         return query.columns.value.match!((ColumnNames v) => 1 + v.optional.length, (None v) => 0);
159     }
160 
161     /// Number of values the user wants to insert.
162     auto values(size_t cnt)
163     in(cnt >= 1, "values must be >=1") {
164         import std.array : array;
165         import std.range : repeat;
166 
167         Value val;
168         val.required = Expr("?");
169         val.optional = query.columns.value.match!((ColumnNames v) => Expr("?")
170                 .repeat(v.optional.length).array, (None v) => null);
171 
172         Values values;
173         foreach (i; 0 .. cnt) {
174             if (i == 0)
175                 values.required = val;
176             else
177                 values.optional ~= val;
178         }
179 
180         miniorm.query_ast.Insert rval = query;
181         rval.values = InsertValues(values);
182         return Insert!T(rval);
183     }
184 
185     /// Insert a new row.
186     auto insert() @safe pure nothrow const {
187         return op(InsertOpt.Insert).setColumns(true);
188     }
189 
190     /// Insert or replace an existing row.
191     auto insertOrReplace() @safe pure nothrow const {
192         return op(InsertOpt.InsertOrReplace).setColumns(false);
193     }
194 
195     // TODO the name is bad.
196     /// Specify columns to insert/replace values in.
197     private auto setColumns(bool insert_) @safe pure const {
198         enum fields = fieldToCol!("", T);
199 
200         ColumnNames columns;
201         bool addRequired = true;
202         foreach (field; fields) {
203             if (field.isPrimaryKey && insert_)
204                 continue;
205 
206             if (addRequired) {
207                 columns.required = miniorm.query_ast.ColumnName(field.columnName);
208                 addRequired = false;
209             } else
210                 columns.optional ~= miniorm.query_ast.ColumnName(field.columnName);
211         }
212 
213         miniorm.query_ast.Insert rval = query;
214         rval.columns = InsertColumns(columns);
215         return Insert!T(rval);
216     }
217 }
218 
219 unittest {
220     static struct Foo {
221         ulong id;
222         string text;
223         float val;
224         ulong ts;
225 
226         @ColumnName("version")
227         string version_;
228     }
229 
230     insertOrReplace!Foo.values(1).toSql.toString.shouldEqual(
231             "INSERT OR REPLACE INTO Foo ('id','text','val','ts','version') VALUES (?,?,?,?,?);");
232     insert!Foo.values(1).toSql.toString.shouldEqual(
233             "INSERT INTO Foo ('text','val','ts','version') VALUES (?,?,?,?);");
234 
235     insertOrReplace!Foo.values(2).toSql.toString.shouldEqual(
236             "INSERT OR REPLACE INTO Foo ('id','text','val','ts','version') VALUES (?,?,?,?,?),(?,?,?,?,?);");
237 
238     insert!Foo.values(2).toSql.toString.shouldEqual(
239             "INSERT INTO Foo ('text','val','ts','version') VALUES (?,?,?,?),(?,?,?,?);");
240 }
241 
242 unittest {
243     static struct Foo {
244         ulong id;
245         string text;
246         float val;
247         ulong ts;
248     }
249 
250     static struct Bar {
251         ulong id;
252         float value;
253         Foo foo;
254     }
255 
256     insertOrReplace!Bar.values(1).toSql.toString.shouldEqual(
257             "INSERT OR REPLACE INTO Bar ('id','value','foo.id','foo.text','foo.val','foo.ts') VALUES (?,?,?,?,?,?);");
258     insert!Bar.values(1).toSql.toString.shouldEqual(
259             "INSERT INTO Bar ('value','foo.id','foo.text','foo.val','foo.ts') VALUES (?,?,?,?,?);");
260     insert!Bar.values(3).toSql.toString.shouldEqual(
261             "INSERT INTO Bar ('value','foo.id','foo.text','foo.val','foo.ts') VALUES (?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?);");
262 }
263 
264 unittest {
265     struct Foo {
266         string text;
267         float val;
268         ulong ts;
269     }
270 
271     struct Bar {
272         float v;
273         Foo foo;
274     }
275 
276     struct Baz {
277         ulong id;
278         float v;
279         Bar xyz;
280         float w;
281     }
282 
283     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 (?,?,?,?,?,?,?);");
284 }
285 
286 auto delete_(T)() {
287     return Delete!T(tableName!T);
288 }
289 
290 struct Delete(T) {
291     import miniorm.query_ast;
292 
293     miniorm.query_ast.Delete query;
294 
295     this(miniorm.query_ast.Delete q) {
296         this.query = q;
297     }
298 
299     this(string tableName) {
300         this.query.table = TableRef(tableName);
301     }
302 
303     /// Convert to a SQL statement that can e.g. be pretty printed.
304     Sql toSql() {
305         return query.Query.Sql;
306     }
307 
308     void run(ref Miniorm db) {
309         db.run(toSql.toString);
310     }
311 
312     mixin WhereMixin!(T, typeof(this), miniorm.query_ast.Delete);
313 }
314 
315 mixin template WhereMixin(T, QueryT, AstT) {
316     import std.datetime : SysTime;
317     import std.traits : isNumeric, isSomeString;
318 
319     /// Automatically quotes `rhs`.
320     auto where(string lhs, string rhs) {
321         import std.format : format;
322 
323         return this.where(format("%s '%s'", lhs, rhs));
324     }
325 
326     /// Converts `rhs` to a datetime that sqlite understand.
327     auto where(string lhs, SysTime rhs) {
328         import std.format : format;
329         import miniorm.api : toSqliteDateTime;
330 
331         return this.where(format("%s datetime('%s')", lhs, rhs.toUTC.toSqliteDateTime));
332     }
333 
334     auto where(T)(string lhs, T rhs) if (isNumeric!T || isSomeString!T) {
335         import std.format : format;
336 
337         return this.where(format("%s %s", lhs, rhs));
338     }
339 
340     /// Add a WHERE condition.
341     auto where(string condition) @safe pure {
342         import miniorm.query_ast;
343 
344         static struct WhereOptional {
345             QueryT!T value;
346             alias value this;
347 
348             private auto where(string condition, WhereOp op) @safe pure {
349                 import sumtype;
350 
351                 QueryT!T rval = value;
352 
353                 Where w = value.query.where.tryMatch!((Where v) => v);
354                 WhereExpr we = w.tryMatch!((WhereExpr v) => v);
355                 we.optional ~= WhereExpr.Opt(op, Expr(condition));
356                 rval.query.where = Where(we);
357                 return WhereOptional(rval);
358             }
359 
360             WhereOptional and(string condition) @safe pure {
361                 return where(condition, WhereOp.AND);
362             }
363 
364             WhereOptional or(string condition) @safe pure {
365                 return where(condition, WhereOp.OR);
366             }
367         }
368 
369         AstT rval = query;
370         rval.where = WhereExpr(Expr(condition)).Where;
371 
372         return WhereOptional(typeof(this)(rval));
373     }
374 }
375 
376 unittest {
377     static struct Foo {
378         ulong id;
379         string text;
380         ulong ts;
381     }
382 
383     delete_!Foo.where("text = privet").and("ts > 123")
384         .toSql.toString.shouldEqual("DELETE FROM Foo WHERE text = privet AND ts > 123;");
385 }
386 
387 auto count(T)() {
388     //return Count!T(Select!T(tableName!T).count);
389     //import miniorm.query_ast;
390     //
391     //miniorm.query_ast.Select query;
392     return Count!T(tableName!T);
393 }
394 
395 struct Count(T) {
396     import miniorm.query_ast : Sql;
397 
398     Select!T query_;
399 
400     this(miniorm.query_ast.Select q) {
401         this.query_ = Select!T(q);
402     }
403 
404     this(string from) {
405         this.query_ = Select!T(from).count;
406     }
407 
408     /// Convert to a SQL statement that can e.g. be pretty printed.
409     Sql toSql() {
410         return query_.toSql;
411     }
412 
413     private ref miniorm.query_ast.Select query() @safe pure nothrow @nogc {
414         return query_.query;
415     }
416 
417     mixin WhereMixin!(T, typeof(this), miniorm.query_ast.Select);
418 }
419 
420 unittest {
421     static struct Foo {
422         ulong id;
423         string text;
424         ulong ts;
425     }
426 
427     count!Foo.where("text = privet").and("ts > 123").toSql.toString.shouldEqual(
428             "SELECT count(*) FROM Foo WHERE text = privet AND ts > 123;");
429 }