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