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