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 }