1 module tests.dbal_sqlite3.tests;
2 
3 import oceandrift.db.dbal;
4 import oceandrift.db.sqlite3;
5 import oceandrift.db.orm;
6 
7 @safe:
8 
9 void print(SQLiteX ex) @trusted
10 {
11     import std.stdio;
12 
13     stderr.writeln(
14         "SQLite3 Exception; status code: ", cast(int) ex.code, " = ", ex.code,
15         "; msg: ", ex.msg,
16         "\n\tEX Trace:", ex.info, "\n\t--- End of Trace"
17     );
18 }
19 
20 struct Person
21 {
22     ulong id = 0;
23     string name;
24     ubyte age;
25 }
26 
27 unittest
28 {
29     auto driver = new SQLite3(":memory:", OpenMode.create);
30     driver.connect();
31     scope (exit)
32         driver.close();
33 
34     driver.execute(
35         `CREATE TABLE "person" (id INTEGER PRIMARY KEY, name TEXT, age INTEGER UNSIGNED)`
36     );
37 
38     auto em = EntityManager!SQLite3(driver);
39 
40     {
41         Person p;
42         immutable bool success = driver.get!(Person)(1, p);
43         assert(!success);
44     }
45 
46     {
47         Person p;
48         immutable bool success = em.get!(Person)(1, p);
49         assert(!success);
50     }
51 
52     driver.execute(
53         `INSERT INTO "person" (name, age) VALUES ("Walter", 65), ("Tom", 30), ("Carl", 60)`
54     );
55 
56     {
57         Person p;
58         immutable bool success = em.get!Person(1, p);
59         assert(success);
60         assert(p.name == "Walter");
61         assert(p.age == 65);
62     }
63 
64     {
65         EntityCollection!Person ec =
66             em.find!Person()
67             .where("age", ComparisonOperator.greaterThanOrEquals, 60)
68             .select()
69             .via(driver);
70         assert(!ec.empty);
71 
72         Person p1 = ec.front;
73         assert(p1.name == "Walter");
74         assert(p1.age >= 60);
75 
76         ec.popFront();
77         assert(!ec.empty);
78 
79         Person p2 = ec.front;
80         assert(p2.name == "Carl", p2.name);
81         assert(p2.age >= 60);
82 
83         ec.popFront();
84         assert(ec.empty);
85     }
86 
87     {
88         EntityCollection!Person ec = em
89             .find!Person()
90             .where("age", ComparisonOperator.greaterThanOrEquals, 60)
91             .select()
92             .via(driver);
93         assert(!ec.empty);
94 
95         Person p1 = ec.front;
96         assert(p1.name == "Walter");
97         assert(p1.age >= 60);
98 
99         ec.popFront();
100         assert(!ec.empty);
101 
102         Person p2 = ec.front;
103         assert(p2.name == "Carl");
104         assert(p2.age >= 60);
105 
106         ec.popFront();
107         assert(ec.empty);
108     }
109 
110     {
111         EntityCollection!Person ec = em
112             .find!Person()
113             .whereParentheses(q => q
114                     .where("age", ComparisonOperator.greaterThanOrEquals, 60)
115             )
116             .select()
117             .via(driver);
118         assert(!ec.empty);
119 
120         Person p1 = ec.front;
121         assert(p1.name == "Walter");
122         assert(p1.age >= 60);
123 
124         ec.popFront();
125         assert(!ec.empty);
126 
127         Person p2 = ec.front;
128         assert(p2.name == "Carl");
129         assert(p2.age >= 60);
130 
131         ec.popFront();
132         assert(ec.empty);
133     }
134 
135     {
136         EntityCollection!Person ec = em
137             .find!Person()
138             .whereParentheses(q => q
139                     .where("age", ComparisonOperator.greaterThanOrEquals, 60)
140                     .limit(1)
141             )
142             .select()
143             .via(driver);
144         assert(!ec.empty);
145 
146         Person p1 = ec.front;
147         assert(p1.name == "Walter");
148         assert(p1.age >= 60);
149 
150         ec.popFront();
151         assert(ec.empty);
152     }
153 
154     {
155         EntityCollection!Person ec = em
156             .find!Person()
157             .whereParentheses(
158                 (Query q) => q
159                     .where("age", ComparisonOperator.greaterThanOrEquals, 60)
160                     .limit(1, 1)
161             )
162             .select()
163             .via(driver);
164         assert(!ec.empty);
165 
166         Person p1 = ec.front;
167         assert(p1.name == "Carl");
168         assert(p1.age >= 60);
169 
170         ec.popFront();
171         assert(ec.empty);
172     }
173 
174     {
175         Person p;
176         immutable bool success = em.get!Person(2, p);
177         assert(success);
178         assert(p.name == "Tom");
179         assert(p.age == 30);
180 
181         em.remove(p);
182         Person p2;
183         immutable bool notRemoved = em.get!Person(2, p2);
184         assert(!notRemoved);
185 
186         Person p3;
187         immutable bool p3stillThere = em.get!Person(3, p3);
188         assert(p3stillThere);
189         assert(p3.name == "Carl");
190     }
191 
192     {
193         auto p = Person(0, "Peter", 45);
194 
195         em.save(p);
196         assert(p.id == 4); // 1-4 is used by previous rows
197 
198         Person tmp;
199         immutable personFound = em.get!Person(4, tmp);
200         assert(personFound);
201 
202         p.age += 1;
203         em.save(p);
204         assert(p.id == 4); // ID remains unchanged
205 
206         Person tmp2;
207         immutable personFoundAgain = em.get!Person(p.id, tmp2);
208         assert(personFoundAgain);
209         assert(tmp2.age == 46); // update propagated
210 
211         immutable idxCopy = em.store(p); // store a copy
212         assert(idxCopy == 5);
213 
214         em.update(p);
215     }
216 }
217 
218 class Mountain
219 {
220     ulong id = 0;
221     string name;
222     string location;
223     uint height;
224 
225     public this() // ORM
226     {
227     }
228 
229     public this(string name, string location, uint height)
230     {
231         this.name = name;
232         this.location = location;
233         this.height = height;
234     }
235 }
236 
237 static assert(isEntityType!Mountain);
238 
239 unittest
240 {
241     auto db = new SQLite3(":memory:", OpenMode.create);
242     db.connect();
243     scope (exit)
244         db.close();
245 
246     db.execute(
247         `CREATE TABLE "mountain" (id INTEGER PRIMARY KEY, name TEXT, location TEXT, height INTEGER UNSIGNED)`
248     );
249 
250     auto em = EntityManager!SQLite3(db);
251 
252     {
253         auto mt1 = new Mountain("Hill 1", "Nowhere", 3000);
254         em.save(mt1);
255         auto mt2 = new Mountain("Snowmountain", "Elsewhere", 4111);
256         em.save(mt2);
257         auto mt3 = new Mountain("Mt. Skyscrape", "Somewhere", 4987);
258         em.save(mt3);
259         auto mt4 = new Mountain("Little Hill", "Nowhere", 1200);
260         em.save(mt4);
261         auto mt5 = new Mountain("K2010", "Nowhere", 2010);
262         em.save(mt5);
263         auto mt6 = new Mountain("Icy Heights", "Elsewhere", 3201);
264         em.save(mt6);
265         auto mt7 = new Mountain("Mt. Nowhere", "Nowhere", 6408);
266         em.save(mt7);
267 
268         Statement stmt = db.prepare("SELECT COUNT(*) FROM mountain");
269         stmt.execute();
270         assert(stmt.front[0].getAs!int == 7);
271     }
272 
273     {
274         enum PreCollection!(Mountain, SQLite3) pcMt4000 =
275             em.find!Mountain()
276                 .where("height", ComparisonOperator.greaterThanOrEquals, 4000);
277 
278         enum BuiltPreCollection!Mountain bpcMt4000 = pcMt4000.select();
279 
280         EntityCollection!Mountain mt4000 = db.map(bpcMt4000);
281 
282         assert(!mt4000.empty);
283 
284         int n = 0;
285         foreach (Mountain mt; mt4000)
286         {
287             assert(mt.height >= 4000);
288             ++n;
289         }
290         assert(n == 3);
291     }
292 
293     {
294         static immutable int cap = 3000;
295         static immutable string loc = "Nowhere";
296         enum PreCollection!(Mountain, SQLite3) pc = em.find!Mountain()
297                 .where("height", '<', cap)
298                 .where("location", '=', loc);
299 
300         immutable ulong cnt = pc.countVia(db);
301         assert(cnt == 2);
302 
303         enum BuiltQuery qCntCT = pc.count();
304         Statement stmt = db.prepareBuiltQuery(qCntCT);
305         stmt.execute();
306         immutable cnt2 = stmt.front[0].getAs!ulong;
307         assert(cnt2 == 2);
308     }
309 
310     {
311         static immutable string loc = "Elsewhere";
312         enum PreCollection!(Mountain, SQLite3) pc = em.find!Mountain().where("location", '=', loc);
313 
314         immutable DBValue maxV = pc.aggregateVia(AggregateFunction.max, "height", db);
315 
316         immutable int max = maxV.getAs!int;
317         assert(max == 4111);
318     }
319 
320     {
321         enum BuiltQuery bq = em
322             .find!Mountain()
323             .aggregate(AggregateFunction.min, "height");
324 
325         Statement stmt = db.prepareBuiltQuery(bq);
326         stmt.execute();
327         assert(!stmt.empty);
328 
329         immutable int min = stmt.front[0].getAs!int;
330         assert(min == 1200);
331     }
332 
333     {
334         enum BuiltPreCollection!Mountain bpc = em
335                 .find!Mountain()
336                 .limit(1)
337                 .select();
338 
339         EntityCollection!Mountain ec = bpc.via(db);
340         assert(!ec.empty);
341 
342         ec.popFront();
343         assert(ec.empty);
344     }
345 
346     {
347         enum BuiltPreCollection!Mountain bpc = em
348                 .find!Mountain()
349                 .limit(3)
350                 .select();
351 
352         EntityCollection!Mountain ec = bpc.via(db);
353         assert(!ec.empty);
354 
355         ec.popFront();
356         assert(!ec.empty);
357 
358         ec.popFront();
359         assert(!ec.empty);
360 
361         ec.popFront();
362         assert(ec.empty);
363     }
364 
365     {
366         enum BuiltPreCollection!Mountain bpc = em
367                 .find!Mountain()
368                 .orderBy("height")
369                 .limit(2, 3)
370                 .select();
371 
372         EntityCollection!Mountain ec = bpc.via(db);
373         assert(!ec.empty);
374 
375         const Mountain m1 = ec.front;
376         assert(m1.height == 3201);
377 
378         ec.popFront();
379         assert(!ec.empty);
380         const Mountain m2 = ec.front;
381         assert(m2.name == "Snowmountain");
382 
383         ec.popFront();
384         assert(ec.empty);
385     }
386 
387     {
388         enum BuiltPreCollection!Mountain bpc = em
389             .find!Mountain()
390             .where("height", '<')
391             .orderBy("height")
392             .select();
393 
394         PreparedCollection!Mountain pc = bpc.prepareCollection(db);
395         pc.bind(0, 3000);
396         EntityCollection!Mountain ec = pc.execute();
397         assert(!ec.empty);
398 
399         const Mountain m1 = ec.front;
400         assert(m1.height == 1200);
401 
402         ec.popFront();
403         assert(!ec.empty);
404         const Mountain m2 = ec.front;
405         assert(m2.name == "K2010");
406 
407         ec.popFront();
408         assert(ec.empty);
409     }
410 
411     {
412         EntityCollection!Mountain ec = em
413             .find!Mountain()
414             .orderBy("height", desc)
415             .limit(1)
416             .selectVia(db);
417 
418         assert(!ec.empty);
419 
420         const Mountain highest = ec.front;
421         assert(highest.name == "Mt. Nowhere");
422 
423         ec.popFront();
424         assert(ec.empty);
425     }
426 
427     {
428         static immutable string loc = "Elsewhere";
429         enum PreCollection!(Mountain, SQLite3) pc = em.find!Mountain().where("location", '=', loc);
430         enum BuiltQuery bq = pc.delete_();
431 
432         Statement stmt = db.prepareBuiltQuery(bq);
433         stmt.execute();
434         assert(stmt.empty);
435     }
436 
437     {
438         em.find!Mountain().where("height", '<', 3000).deleteVia(db);
439 
440         ulong left = em.find!Mountain().countVia(db);
441         assert(left == 3);
442 
443         EntityCollection!Mountain mtsLeft = em.find!Mountain().orderBy("height").selectVia(db);
444 
445         assert(!mtsLeft.empty);
446         assert(mtsLeft.front.name == "Hill 1");
447 
448         mtsLeft.popFront();
449         assert(!mtsLeft.empty);
450         assert(mtsLeft.front.name == "Mt. Skyscrape");
451 
452         mtsLeft.popFront();
453         assert(!mtsLeft.empty);
454         assert(mtsLeft.front.name == "Mt. Nowhere");
455 
456         mtsLeft.popFront();
457         assert(mtsLeft.empty);
458     }
459 }
460 
461 unittest  // many2one + one2many
462 {
463     auto db = new SQLite3(":memory:", OpenMode.create);
464     db.connect();
465     scope (exit)
466         db.close();
467 
468     db.execute(
469         `CREATE TABLE "author" (
470             "id" INTEGER PRIMARY KEY,
471             "name" TEXT,
472             "birthday" DATETIME
473         );`
474     );
475 
476     db.execute(
477         `CREATE TABLE "book" (
478             "id" INTEGER PRIMARY KEY,
479             "name" TEXT,
480             "published" DATE,
481             "author_id" INTEGER UNSIGNED REFERENCES author(id)
482         );`
483     );
484 
485     auto em = EntityManager!SQLite3(db);
486 
487     struct Author
488     {
489         string name;
490         mixin EntityID;
491     }
492 
493     struct Book
494     {
495         string name;
496         Date published;
497         ulong authorID;
498         mixin EntityID;
499     }
500 
501     {
502         auto adam = Author("Adam D. Ruppe");
503         em.save(adam);
504         auto someone = Author("Some One");
505         em.save(someone);
506         auto nobody = Author("No Body");
507         em.save(nobody);
508 
509         em.store(Book("D Cookbook", Date(2014, 5, 26), adam.id));
510         em.store(Book("Funny Things", Date(2016, 2, 4), someone.id));
511         em.store(Book("Very funny Things", Date(2017, 3, 3), someone.id));
512         em.store(Book("Fancy Things", Date(2017, 9, 9), someone.id));
513         em.store(Book("Other Things", Date(2018, 12, 20), someone.id));
514         em.store(Book("Stories", Date(2012, 7, 1), nobody.id));
515         em.store(Book("Stories, vol.2", Date(2018, 4, 8), nobody.id));
516         em.store(Book("Stories, vol.3", Date(2019, 8, 10), nobody.id));
517         em.store(Book("Much more Stories", Date(2020, 11, 11), nobody.id));
518     }
519 
520     {
521         Book b;
522         immutable bookFound = em.get(4, b);
523         assert(bookFound);
524 
525         Author a;
526         immutable authorFound = em.manyToOne!Author(b, a);
527         assert(authorFound);
528         assert(a.name == "Some One");
529 
530         assert(a.id == b.authorID);
531         assert(a.name == "Some One");
532     }
533 
534     {
535         auto authorsFound = em.find!Author.where("name", '=', "No Body").selectVia(db);
536         assert(!authorsFound.empty);
537 
538         Author a = authorsFound.front;
539         assert(a.name == "No Body");
540 
541         authorsFound.popFront();
542         assert(authorsFound.empty);
543 
544         auto pcBooks = em.oneToMany!Book(a);
545 
546         immutable ulong cntBooks = pcBooks.countVia(db);
547         assert(cntBooks == 4);
548 
549         EntityCollection!Book booksSince2019 = pcBooks
550             .where("published", '>', Date(2019, 1, 1))
551             .orderBy("published")
552             .selectVia(db);
553         assert(!booksSince2019.empty);
554 
555         assert(booksSince2019.front.authorID == a.id);
556         assert(booksSince2019.front.published.year >= 2019);
557         assert(booksSince2019.front.name == "Stories, vol.3");
558 
559         booksSince2019.popFront();
560         assert(!booksSince2019.empty);
561 
562         assert(booksSince2019.front.authorID == a.id);
563         assert(booksSince2019.front.published.year >= 2019);
564         assert(booksSince2019.front.name == "Much more Stories");
565 
566         booksSince2019.popFront();
567         assert(booksSince2019.empty);
568     }
569 }
570 
571 unittest  // many2many
572 {
573     auto db = new SQLite3(":memory:", OpenMode.create);
574     db.connect();
575     scope (exit)
576         db.close();
577 
578     struct Thing
579     {
580         string name;
581         mixin EntityID;
582     }
583 
584     struct Tag
585     {
586         string name;
587         mixin EntityID;
588     }
589 
590     assert(joinTableName!(Thing, Tag) == "tag_thing");
591     assert(joinTableName!(Tag, Thing) == "tag_thing");
592 
593     db.execute(
594         `CREATE TABLE "thing" (
595             "id" INTEGER PRIMARY KEY,
596             "name" TEXT
597         );`
598     );
599 
600     db.execute(
601         `CREATE TABLE "tag" (
602             "id" INTEGER PRIMARY KEY,
603             "name" TEXT
604         );`
605     );
606 
607     db.execute(
608         `CREATE TABLE "tag_thing" (
609             "tag_id" INTEGER REFERENCES tag(id),
610             "thing_id" INTEGER REFERENCES thing(id),
611             PRIMARY KEY ("tag_id", "thing_id")
612         );`
613     );
614 
615     auto em = EntityManager!SQLite3(db);
616 
617     auto fruit = Tag("Fruit");
618     em.save(fruit);
619     auto berry = Tag("Berry");
620     em.save(berry);
621     auto red = Tag("red");
622     em.save(red);
623 
624     auto apple = Thing("Apple");
625     em.save(apple);
626     em.manyToManyAssign(fruit, apple);
627     em.manyToManyAssign(red, apple);
628     auto pear = Thing("Pear");
629     em.save(pear);
630     em.manyToManyAssign(fruit, pear);
631     auto plum = Thing("Plum");
632     em.save(plum);
633     em.manyToManyAssign(fruit, plum);
634     auto banana = Thing("Banana");
635     em.save(banana);
636     em.manyToManyAssign(fruit, banana);
637     auto raspberry = Thing("Raspberry");
638     em.save(raspberry);
639     em.manyToManyAssign(raspberry, fruit);
640     em.manyToManyAssign(raspberry, berry);
641     auto blueberry = Thing("Blueberry");
642     em.save(blueberry);
643     em.manyToManyAssign(blueberry, fruit);
644     em.manyToManyAssign(blueberry, berry);
645     auto strawberry = Thing("Strawberry");
646     em.save(strawberry);
647     em.manyToManyAssign(strawberry, fruit);
648     em.manyToManyAssign(strawberry, berry);
649     em.manyToManyAssign(strawberry, red);
650     auto book = Thing("Book");
651     em.save(book);
652 
653     assert(em.manyToMany!Tag(apple).countVia(db) == 2);
654     assert(em.manyToMany!Tag(pear).countVia(db) == 1);
655     assert(em.manyToMany!Tag(strawberry).countVia(db) == 3);
656     assert(em.manyToMany!Tag(book).countVia(db) == 0);
657 
658     assert(em.manyToMany!Thing(fruit).countVia(db) == 7);
659     assert(em.manyToMany!Thing(berry).countVia(db) == 3);
660     assert(em.manyToMany!Thing(red).countVia(db) == 2);
661 
662     {
663         em.manyToManyAssign(book, fruit); // actually wrong…
664         assert(em.manyToMany!Tag(book).countVia(db) == 1);
665 
666         em.manyToManyUnassign(book, fruit); // …so unassign
667         assert(em.manyToMany!Tag(book).countVia(db) == 0);
668     }
669 }