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