1 /**
2 A SQLite driver for EzDb.
3 */
4 module ezdb.driver.sqlite;
5 
6 import ezdb.repository;
7 import ezdb.entity;
8 import ezdb.foreign;
9 
10 import d2sqlite3;
11 import optional;
12 
13 import std.conv;
14 import std.stdio;
15 import std.range;
16 import std.algorithm;
17 import std.traits;
18 import std.exception;
19 
20 /**
21 The strategy used to create tables.
22 */
23 enum DDLStrategy
24 {
25     /// Creates the table if one doesn't exist.
26     /// Doesn't do anything else.
27     create,
28 
29     /// Drops a table if one exists, and then recreates it.
30     drop_create,
31 }
32 
33 private template GetIdColumn(Entity)
34 {
35     enum GetIdColumn = getSymbolsByUDA!(Entity, primaryKey)[0].stringof;
36 }
37 
38 /**
39 A factory that can create SQLite databases.
40 */
41 final class SqliteFactory
42 {
43     private Database _db;
44     private int _openConnections = 0;
45     private bool _open = true;
46 
47     /**
48     Creates a new SQLite factory.
49     */
50     this(string filename = "sqlite.db")
51     {
52         _db = Database(filename);
53         _db.execute("PRAGMA foreign_keys = ON;");
54     }
55 
56     /**
57     Returns `true` if the factory has been fully closed, `false` if it is still
58     possible to open new repositories.
59     */
60     bool isClosed()
61     {
62         return !_open;
63     }
64 
65     /**
66     Opens a connection to a SQLite database.
67     */
68     auto open(Repository)()
69     in (isClosed == false)
70     {
71         _openConnections++;
72         return new SqliteDriver!Repository(this);
73     }
74 
75     /**
76     Attempts to close the database, if it is no longer being used.
77     */
78     private void close()
79     {
80         _openConnections--;
81         if (_openConnections <= 0)
82         {
83             _open = false;
84             _db.close();
85         }
86     }
87 
88     /**
89     Gets a reference to the Sqlite database.
90     */
91     private ref Database db()
92     {
93         return _db;
94     }
95 }
96 
97 /**
98 Implements a repository using a Sqlite database.
99 */
100 final class SqliteDriver(Db : Repository!Entity, Entity) : Db
101 {
102     private enum Table = Entity.stringof;
103     private enum IdColumn = GetIdColumn!Entity;
104     private SqliteFactory _factory;
105     private immutable DDLStrategy _strategy;
106 
107     /**
108     Creates a SQLite database.
109     Params:
110       filename = The name of the file used to store the database.
111     */
112     this(SqliteFactory factory, DDLStrategy strategy = DDLStrategy.create)
113     {
114         _strategy = strategy;
115         _factory = factory;
116 
117         final switch (strategy)
118         {
119             case DDLStrategy.drop_create:
120                 dropTable();
121                 createTable();
122                 break;
123             case DDLStrategy.create:
124                 createTable();
125         }
126     }
127 
128     private void dropTable()
129     {
130         _factory.db.run(text("DROP TABLE IF EXISTS ", Table));
131     }
132 
133     private void createTable()
134     {
135         string statement = CreationStatement!Entity;
136         _factory.db.run(statement);
137     }
138 
139     private PrimaryKeyType!Entity lastRowId()
140     {
141         return _factory.db
142             .execute("SELECT last_insert_rowid()")
143             .oneValue!(PrimaryKeyType!Entity);
144     }
145 
146     override void close()
147     {
148         _factory.close();
149     }
150 
151     override void remove(PrimaryKeyType!Entity id)
152     {
153         auto statement = _factory.db.prepare(text("DELETE FROM ", Table, " WHERE ", IdColumn, " = :id"));
154         statement.bind(":id", id);
155         statement.execute();
156         statement.reset();
157     }
158 
159     override Optional!Entity find(PrimaryKeyType!Entity id)
160     {
161         auto statement = _factory.db.prepare(text("SELECT * FROM ", Table, " WHERE ",
162             IdColumn, " = :id"));
163         statement.bind(":id", id);
164         auto results = statement.execute();
165         if (results.empty)
166             return no!Entity;
167         auto result = results.front().as!Entity;
168         statement.reset();
169         return some(result);
170     }
171 
172     override Entity[] findAll()
173     {
174         auto statement = _factory.db.prepare(text("SELECT * FROM ", Table));
175         auto results = statement.execute();
176         Entity[] entities;
177         foreach (result; results)
178         {
179             entities ~= result.as!Entity;
180         }
181         statement.reset();
182         return entities;
183     }
184 
185     override Entity save(Entity entity)
186     {
187         string statementString = InsertStatement!Entity;
188         auto statement = _factory.db.prepare(statementString);
189         static foreach (name; FieldNameTuple!Entity)
190         {
191             static if (!hasUDA!(__traits(getMember, Entity, name), primaryKey))
192             {
193                 statement.bind(":" ~ name, __traits(getMember, entity, name));
194             }
195         }
196         statement.execute();
197         statement.reset();
198         return find(lastRowId()).front;
199     }
200 }
201 
202 private template CreationStatement(Entity)
203 {
204     static CreationStatement = text("CREATE TABLE IF NOT EXISTS ", Entity.stringof,
205         " (", parseCreationMembers!Entity, ")");
206 }
207 
208 private string parseCreationMembers(Entity)()
209 {
210     string[] lines;
211     string[] foreignKeys;
212     static foreach (memberName; FieldNameTuple!Entity)
213     {{
214         string[] attributes = [memberName];
215         alias member = __traits(getMember, Entity, memberName);
216 
217         // Add the SQL type identifier
218         static if (is(typeof(member) == int))
219             attributes ~= "INTEGER";
220         else static if (is(typeof(member) == string))
221             attributes ~= "TEXT";
222         else
223             assert(0, "Cannot convert field of type " ~ typeof(member).stringof ~ " to a SQL type");
224 
225         // Add the primary key attribute if necessary.
226         static if (hasUDA!(member, primaryKey))
227             attributes ~= ["PRIMARY KEY", "AUTOINCREMENT"];
228         else static if (IsForeign!(member))
229         {
230             alias foreign = GetForeignEntity!member;
231             foreignKeys ~= text("FOREIGN KEY (", memberName, ") REFERENCES ",
232                 foreign.stringof, "(", GetIdColumn!foreign, ")");
233         }
234 
235         // Add the Not Null attribute.
236         attributes ~= "NOT NULL";
237         lines ~= attributes.join(' ');
238     }}
239     return (lines ~ foreignKeys).join(", ");
240 }
241 
242 private template InsertStatement(Entity)
243 {
244     static InsertStatement = text("INSERT INTO ", Entity.stringof,
245         "(", [FieldNameTuple!Entity].join(", "), ") VALUES ",
246         "(", [FieldNameTuple!Entity].map!(member => ":" ~ member).join(", "), ")");
247 }
248 
249 @("Can create a SQLite database")
250 unittest
251 {
252     static struct Entity
253     {
254         @primaryKey int id;
255     }
256     static interface Repo : Repository!Entity {}
257     auto db = new SqliteFactory(":memory:").open!Repo;
258     scope(exit) db.close();
259     assert(db !is null);
260 }
261 
262 @("Empty database should return no results")
263 unittest
264 {
265     static struct Entity
266     {
267         @primaryKey int id;
268     }
269     static interface Repo : Repository!Entity {}
270     auto db = new SqliteFactory(":memory:").open!Repo;
271     scope(exit) db.close();
272     assert(db.findAll() == [], "findAll() should return an empty list of the database is empty");
273 }
274 
275 @("Save() should return a saved instance")
276 unittest
277 {
278     static struct Entity
279     {
280         @primaryKey int id;
281         int value;
282     }
283     static interface Repo : Repository!Entity {}
284     auto db = new SqliteFactory(":memory:").open!Repo;
285     scope(exit) db.close();
286 
287     Entity toSave;
288     toSave.value = 5;
289 
290     const saved1 = db.save(toSave);
291     assert(saved1.value == 5, "Entity.value was not correctly saved");
292     assert(saved1.id == 1, "Entity.id was not generated");
293 
294     const saved2 = db.save(toSave);
295     assert(saved2.value == 5, "Entity.value was not correctly saved");
296     assert(saved2.id == 2, "Entity.id was not generated");
297 }
298 
299 @("findAll() should return all instances when saved")
300 unittest
301 {
302     static struct Entity
303     {
304         @primaryKey int id;
305         int value;
306     }
307     static interface Repo : Repository!Entity {}
308     auto db = new SqliteFactory(":memory:").open!Repo;
309     scope(exit) db.close();
310 
311     Entity toSave;
312     toSave.value = 5;
313 
314     const saved = db.save(toSave);
315 
316     assert(db.findAll() == [saved], "Did not correctly retrieve all results");
317 }
318 
319 @("remove() should remove an instance")
320 unittest
321 {
322     static struct Entity
323     {
324         @primaryKey int id;
325         int value;
326     }
327     static interface Repo : Repository!Entity {}
328     auto db = new SqliteFactory(":memory:").open!Repo;
329     scope(exit) db.close();
330 
331     Entity toSave;
332     const saved = db.save(toSave);
333     db.remove(saved.id);
334 }
335 
336 @("find() should return an empty optional if no row can be found")
337 unittest
338 {
339     static struct Entity
340     {
341         @primaryKey int id;
342         int value;
343     }
344     static interface Repo : Repository!Entity {}
345     auto db = new SqliteFactory(":memory:").open!Repo;
346     scope(exit) db.close();
347 
348     assert(db.find(0).empty, "Result was not empty");
349 }
350 
351 @("An invalid foreign key will cause an error")
352 unittest
353 {
354     static struct Parent
355     {
356         @primaryKey int id;
357     }
358 
359     static struct Child
360     {
361         @primaryKey
362         int id;
363 
364         @foreign!Parent
365         int child;
366     }
367 
368     static interface ParentRepo : Repository!Parent {}
369     static interface ChildRepo : Repository!Child {}
370     auto factory = new SqliteFactory(":memory:");
371     auto parentDb = factory.open!ParentRepo;
372     scope(exit) parentDb.close();
373     auto db = factory.open!ChildRepo;
374     scope(exit) db.close();
375 
376     Child child;
377     child.child = 5;
378     assertThrown(db.save(child));
379 }
380 
381 @("A valid foreign key will be accepted")
382 unittest
383 {
384     static struct Parent
385     {
386         @primaryKey int id;
387     }
388 
389     static struct Child
390     {
391         @primaryKey
392         int id;
393 
394         @foreign!Parent
395         int child;
396     }
397 
398     static interface ParentRepo : Repository!Parent {}
399     static interface ChildRepo : Repository!Child {}
400     auto factory = new SqliteFactory(":memory:");
401     auto parentDb = factory.open!ParentRepo;
402     scope(exit) parentDb.close();
403     auto db = factory.open!ChildRepo;
404     scope(exit) db.close();
405 
406     Parent parent;
407     parent = parentDb.save(parent);
408 
409     Child child;
410     child.child = parent.id;
411     db.save(child);
412 }