1 module sqlbuilder.dialect.sqlite; 2 3 public import sqlbuilder.dialect.common : where, changed, limit, orderBy, 4 groupBy, exprCol, as, withoutAs, concat, count, ascend, descend, 5 Parameter, simplifyConditions; 6 7 private import sqlbuilder.dialect.common : SQLImpl, append; 8 9 import sqlbuilder.types; 10 import sqlbuilder.traits; 11 import sqlbuilder.util; 12 13 import std.typecons : Nullable, nullable; 14 import std.datetime : DateTime, Date, SysTime; 15 import std.traits; 16 import std.conv; 17 18 alias Blob = const(ubyte)[]; 19 20 // define a simple tagged union for the parameters. We don't need anything 21 // complex, because none of the types are complex. 22 struct PType 23 { 24 private import std.range; 25 enum Tag 26 { 27 Integer, 28 Float, 29 Text, 30 Blob, 31 Null 32 } 33 34 private 35 { 36 Tag _tag; 37 union _Value 38 { 39 long longData; // tag 0 40 double doubleData; // tag 1 41 string stringData; // tag 2 42 Blob blobData; // tag 3 43 } 44 _Value _value; 45 } 46 47 this(T)(T val) 48 { 49 opAssign(val); 50 } 51 52 Tag tag() 53 { 54 return _tag; 55 } 56 57 void opAssign(PType val) 58 { 59 this._tag = val._tag; 60 this._value = val._value; 61 } 62 63 void opAssign(T)(T val) 64 { 65 static if(is(T == Nullable!U, U)) 66 { 67 if(val.isNull) 68 { 69 _value.blobData = null; 70 _tag = Tag.Null; 71 } 72 else 73 opAssign(val.get); 74 } 75 else static if(is(T == typeof(null))) 76 { 77 // zero out the union 78 _value.blobData = null; 79 _tag = Tag.Null; 80 } 81 else static if(is(T == DateTime) || is(T == SysTime) || is(T == Date)) 82 { 83 _value.stringData = val.toISOExtString; 84 _tag = Tag.Text; 85 } 86 else static if(canStringMarshal!T || isSomeString!T) 87 { 88 _value.stringData = val.to!string; 89 _tag = Tag.Text; 90 } 91 else static if (isIntegral!T || isSomeChar!T || isBoolean!T) 92 { 93 _value.longData = val.to!long; 94 _tag = Tag.Integer; 95 } 96 else static if (isFloatingPoint!T) 97 { 98 _value.doubleData = val; 99 _tag = Tag.Float; 100 } 101 else static if(is(immutable(T) == immutable(Blob))) 102 { 103 _value.blobData = val; 104 _tag = Tag.Blob; 105 } 106 else 107 { 108 static assert(false, "Cannot assign type " ~ T.stringof ~ " to PType"); 109 } 110 } 111 112 bool isNull() 113 { 114 return _tag == Tag.Null; 115 } 116 117 T get(T)() 118 { 119 static if(is(T == Nullable!U, U)) 120 { 121 if(isNull) 122 return T.init; 123 else 124 return T(get!U()); 125 } 126 else 127 { 128 void enforceTag(Tag required) 129 { 130 if(_tag != required) 131 throw new Exception("Inappropriate tag type for " ~ T.stringof ~ ": " ~ _tag.to!string); 132 } 133 static if(canStringMarshal!T || isSomeString!T) 134 { 135 enforceTag(Tag.Text); 136 return parseFromString!T(_value.stringData); 137 } 138 else static if (isIntegral!T || isSomeChar!T || isBoolean!T) 139 { 140 enforceTag(Tag.Integer); 141 return _value.longData.to!T; 142 } 143 else static if (isFloatingPoint!T) 144 { 145 // TODO: should we support integer values also? 146 enforceTag(Tag.Float); 147 return _value.doubleData; 148 } 149 else static if(is(T == Blob)) 150 { 151 enforceTag(Tag.Blob); 152 return _value.blobData; 153 } 154 else 155 static assert(false, "Cannot get type " ~ T.stringof ~ " from PType"); 156 } 157 } 158 159 void toString(Out)(ref Out output) if (isOutputRange!(Out, dchar)) 160 { 161 import std.format; 162 with(Tag) final switch(_tag) 163 { 164 case Integer: 165 formattedWrite(output, "%s", _value.longData); 166 break; 167 case Float: 168 formattedWrite(output, "%s", _value.doubleData); 169 break; 170 case Text: 171 formattedWrite(output, "%s", _value.stringData); 172 break; 173 case Blob: 174 formattedWrite(output, "[%(0x%02x, %)]", _value.blobData); 175 break; 176 case Null: 177 put(output, "null"); 178 break; 179 } 180 } 181 182 string toString() 183 { 184 import std.array; 185 Appender!string app; 186 toString(app); 187 return app.data; 188 } 189 } 190 191 private PType toPType(T)(T val) 192 { 193 static if(is(T : Nullable!U, U)) 194 { 195 if(val.isNull) 196 return PType(null); 197 return toPType(val.get); 198 } 199 else static if(is(typeof(val.dbValue))) 200 { 201 return toPType(val.dbValue); 202 } 203 else static if(is(T == enum)) 204 { 205 import std.traits : OriginalType; 206 return toPType(cast(OriginalType!T)val); 207 } 208 else 209 { 210 return PType(val); 211 } 212 } 213 214 auto param(T)(T val) 215 { 216 import std.range : only; 217 return Parameter!PType(only(val.toPType)); 218 } 219 220 auto optional(T)(T val, bool isValid) 221 { 222 import std.range : only; 223 return Parameter!(PType, true)(only(val.toPType), isValid); 224 } 225 226 alias _impl = SQLImpl!(PType, param, true); 227 228 static foreach(f; __traits(allMembers, _impl)) 229 mixin("alias " ~ f ~ " = _impl." ~ f ~ ";"); 230 231 alias UntypedQuery = typeof(select()); 232 233 private void sqlPut(bool includeObjectSeparators, bool includeTableQualifiers, App)(ref App app, ExprString expr) 234 { 235 BitStack andor; 236 andor.push(true); // default to and 237 foreach(x; expr.data) 238 sqlPut!(includeObjectSeparators, includeTableQualifiers)(app, x, andor); 239 } 240 241 private void sqlPut(bool includeObjectSeparators, bool includeTableQualifiers, App)(ref App app, ExprString expr, ref BitStack andor) 242 { 243 foreach(x; expr.data) 244 sqlPut!(includeObjectSeparators, includeTableQualifiers)(app, x, andor); 245 } 246 247 private void sqlPut(bool includeObjectSeparators, bool includeTableQualifiers, App)(ref App app, string x) 248 { 249 BitStack andor; 250 andor.push(true); // default to and 251 sqlPut!(includeObjectSeparators, includeTableQualifiers)(app, x, andor); 252 } 253 254 private void sqlPut(bool includeObjectSeparators, bool includeTableQualifiers, App)(ref App app, string x, ref BitStack andor) 255 { 256 import std.range : put; 257 with(Spec) switch(getSpec(x)) 258 { 259 case id: 260 put(app, '"'); 261 put(app, x[2 .. $]); 262 put(app, '"'); 263 break; 264 case tableid: 265 static if(includeTableQualifiers) 266 { 267 put(app, '"'); 268 put(app, x[2 .. $]); 269 put(app, `".`); 270 } 271 break; 272 case leftJoin: 273 put(app, " LEFT JOIN "); 274 break; 275 case rightJoin: 276 put(app, " RIGHT JOIN "); 277 break; 278 case innerJoin: 279 put(app, " INNER JOIN "); 280 break; 281 case outerJoin: 282 put(app, " OUTER JOIN "); 283 break; 284 case param: 285 put(app, '?'); 286 break; 287 case none: 288 put(app, x); 289 break; 290 case objend: 291 static if(includeObjectSeparators) 292 put(app, `, 1 AS "_objend"`); 293 break; 294 case separator: 295 if(andor.peek) 296 put(app, ") AND ("); 297 else 298 put(app, ") OR ("); 299 break; 300 case beginAnd: 301 andor.push(true); 302 // 2 parentheses, one for the group, and one for the first term 303 put(app, "(("); 304 break; 305 case beginOr: 306 andor.push(false); 307 put(app, "(("); 308 break; 309 case endGroup: 310 if(!andor.length) 311 throw new Exception("Error, inconsistent groupings"); 312 andor.pop; 313 put(app, "))"); 314 break; 315 default: 316 throw new Exception("Unknown spec in: " ~ x); 317 } 318 } 319 320 auto params(T)(T t) 321 { 322 static if(isQuery!T) 323 { 324 return paramsImpl!("fields", "joins", "conditions", "groups", "orders")(t); 325 } 326 else static if(is(T : Insert!P, P)) 327 { 328 return paramsImpl!("colNames", "colValues")(t); 329 } 330 else static if(is(T : Update!P, P)) 331 { 332 return paramsImpl!("joins", "settings", "conditions")(t); 333 } 334 else static if(is(T : Delete!P, P)) 335 { 336 return paramsImpl!("joins", "conditions")(t); 337 } 338 else static assert("Unsupported type for params property: " ~ T.stringof); 339 } 340 341 string sql(bool includeObjectSeparators = false, QP...)(Query!(QP) q) 342 { 343 import std.array : Appender; 344 import std.range : put; 345 import std.format : formattedWrite; 346 Appender!string app; 347 assert(q.fields.expr); 348 assert(q.joins.expr); 349 350 // add a set of fragments given the prefix and the separator 351 void addFragment(SQLFragment!(q.ItemType) item, string prefix, string postfix = null) 352 { 353 BitStack bits; 354 bits.push(true); 355 if(item.expr) 356 { 357 put(app, prefix); 358 sqlPut!(includeObjectSeparators, true)(app, item.expr, bits); 359 if(postfix.length) 360 put(app, postfix); 361 } 362 } 363 364 // fields 365 addFragment(q.fields, "SELECT "); 366 // joins 367 addFragment(q.joins, " FROM "); 368 // CONDITIONS 369 addFragment(q.conditions, " WHERE (", ")"); 370 // GROUP BY 371 addFragment(q.groups, " GROUP BY "); 372 // ORDER BY 373 addFragment(q.orders, " ORDER BY "); 374 375 if(q.limitQty) 376 { 377 if(q.limitOffset) 378 formattedWrite(app, " LIMIT %s OFFSET %s", q.limitQty, q.limitOffset); 379 else 380 formattedWrite(app, " LIMIT %s", q.limitQty); 381 } 382 383 return app.data; 384 } 385 386 // return an sql statement that queries the total items from the given query 387 // (no limits, no offsets) 388 // 389 // The resulting query can be sent to fetch or fetchOne, and this will get a 390 // single ulong that counts the total rows. 391 392 string sql(Item)(Insert!Item ins) 393 { 394 import std.array : Appender; 395 import std.range : put; 396 Appender!string app; 397 assert(ins.tableid.length); 398 assert(ins.colNames.expr); 399 assert(ins.colValues.expr); 400 401 put(app, `INSERT INTO "`); 402 put(app, ins.tableid); 403 put(app, `" (`); 404 sqlPut!(false, false)(app, ins.colNames.expr); 405 put(app, ") VALUES ("); 406 sqlPut!(false, true)(app, ins.colValues.expr); 407 put(app, ")"); 408 409 return app.data; 410 } 411 412 string sql(Item)(Update!Item upd) 413 { 414 // MySQL is quite forgiving, so just include everything as it is written. 415 import std.array : Appender; 416 import std.range : put; 417 Appender!string app; 418 assert(upd.settings.expr); 419 assert(upd.joins.expr); 420 421 // add a set of fragments given the prefix and the separator 422 void addFragment(SQLFragment!(upd.ItemType) item, string prefix, string postfix = null) 423 { 424 if(item.expr) 425 { 426 put(app, prefix); 427 sqlPut!(false, true)(app, item.expr); 428 if(postfix.length) 429 put(app, postfix); 430 } 431 } 432 433 // first table 434 put(app, "UPDATE "); 435 app.sqlPut!(false, true)(upd.joins.expr.data[0]); // add the existing table 436 // fields 437 addFragment(upd.settings, " SET "); 438 439 // extra joins 440 if(upd.joins.expr.data.length > 1) 441 { 442 import std.algorithm : map; 443 // need to build an alternate expression string with "_" as the main table name. 444 static immutable string altTableName = "_".makeSpec(Spec.tableid); 445 auto altFrag = upd.joins; 446 auto origTable = upd.joins.expr.data[0]; 447 altFrag.expr.data = [origTable, ` AS "_"`]; 448 altFrag.expr.data.append(upd.joins.expr.data[1 .. $] 449 .map!(x => getSpec(x) == Spec.tableid && x[2 .. $] == origTable[2 .. $] ? altTableName : x)); 450 addFragment(altFrag, " FROM "); 451 put(app, ` WHERE "_"."id"=`); 452 app.sqlPut!(false, true)(origTable); 453 put(app, `."id"`); 454 455 // CONDITIONS 456 addFragment(upd.conditions, " AND (", ")"); 457 } 458 else 459 { 460 // CONDITIONS 461 addFragment(upd.conditions, " WHERE (", ")"); 462 } 463 464 return app.data; 465 } 466 467 string sql(Item)(Delete!Item del) 468 { 469 // DELETE FROM table LEFT JOIN other tables. 470 import std.array : Appender; 471 import std.range : put; 472 Appender!string app; 473 assert(del.joins.expr); 474 475 // add a set of fragments given the prefix and the separator 476 void addFragment(SQLFragment!(del.ItemType) item, string prefix, string postfix = null) 477 { 478 if(item.expr) 479 { 480 put(app, prefix); 481 sqlPut!(false, true)(app, item.expr); 482 if(postfix.length) 483 put(app, postfix); 484 } 485 } 486 487 put(app, "DELETE FROM "); 488 sqlPut!(false, true)(app, ExprString(del.joins.expr.data[0 .. 1])); 489 // if there is at least one join, we have to change the syntax 490 import std.algorithm : canFind; 491 if(del.joins.expr.data.canFind!(s => s.getSpec.isJoin)) 492 { 493 // there are joins. Use a where select statement to find the rows to 494 // delete. Map based on rowids. 495 put(app, " WHERE rowid IN (SELECT "); 496 sqlPut!(false, true)(app, ExprString(del.joins.expr.data[0 .. 1])); 497 put(app, ".rowid "); 498 addFragment(del.joins, " FROM "); 499 addFragment(del.conditions, " WHERE (", ")"); 500 put(app, ")"); 501 } 502 else 503 { 504 // simple delete, no joins. 505 addFragment(del.conditions, " WHERE (", ")"); 506 } 507 508 return app.data; 509 } 510 511 private template dbValueType(T) 512 { 513 static if(is(T : Nullable!U, U)) 514 alias dbValueType = .dbValueType!U; 515 else static if(is(T : AllowNullType!Args, Args...)) 516 alias dbValueType = .dbValueType!(T.type); 517 else static if(is(typeof(T.init.dbValue))) 518 { 519 alias dbValueType = typeof((){T val = T.init; return val.dbValue;}()); 520 } 521 else static if(is(T == enum)) 522 { 523 alias dbValueType = OriginalType!T; 524 } 525 else 526 alias dbValueType = T; 527 } 528 529 private enum canStringMarshal(T) = 530 (is(typeof(T.init.toString((in char[]) {}))) || is(typeof(T.init.toString()))) 531 && is(typeof(parseFromString!T)); 532 533 private T parseFromString(T)(string textData) 534 { 535 static if(isSomeString!T) 536 return textData.to!T; 537 // handle the timestamp types specially, these are very common DB types. 538 // TODO: handle all the time types 539 else static if(is(T == DateTime) || is(T == SysTime) || is(T == Date)) 540 return T.fromISOExtString(textData); 541 else static if(is(typeof(T.fromString(textData)))) 542 return T.fromString(textData); 543 else static if(is(typeof(to!T(textData)))) 544 return textData.to!T; 545 else 546 static assert(false, "Cannot parse ", T, " from a string"); 547 } 548 549 // match the D type to a specific MySQL type 550 private template getFieldType(T) 551 { 552 alias RT = dbValueType!T; 553 static if(is(RT == T)) 554 { 555 static if (isIntegral!T || isSomeChar!T || isBoolean!T) 556 enum getFieldType = "INTEGER"; 557 else static if(canStringMarshal!T || isSomeString!T) 558 enum getFieldType = "TEXT"; 559 else static if(isFloatingPoint!T) 560 enum getFieldType = "REAL"; 561 else static if(is(immutable(T) == immutable(Blob))) 562 enum getFieldType = "BLOB"; 563 else 564 static assert(0, "Unknown mapping for type " ~ T.stringof); 565 } 566 else 567 alias getFieldType = .getFieldType!RT; 568 } 569 570 571 // generate an SQL statement to insert a table definition. 572 template createTableSql(T, bool doForeignKeys = false, bool ifNotExists = false) 573 { 574 string generate() 575 { 576 import sqlbuilder.uda; 577 import sqlbuilder.traits; 578 auto result = `CREATE TABLE ` ~ (ifNotExists ? `IF NOT EXISTS ` : ``) ~ 579 `"` ~ getTableName!T ~ `" (`; 580 int autoIncFields = 0; 581 foreach(field; FieldNameTuple!T) 582 { 583 alias fieldType = typeof(__traits(getMember, T, field)); 584 // Relations are not columns to store in the DB. 585 static if(!hasUDA!(__traits(getMember, T, field), ignore) && 586 !is(fieldType == Relation)) 587 { 588 string name = result ~= `"` 589 ~ getColumnName!(__traits(getMember, T, field)) ~ `" `; 590 alias colTypeUDAs = getUDAs!(__traits(getMember, T, field), colType); 591 static if(colTypeUDAs.length > 0) 592 result ~= colTypeUDAs[0].type; 593 else 594 result ~= getFieldType!fieldType; 595 static if(!possibleNullColumn!(__traits(getMember, T, field))) 596 result ~= " NOT NULL"; 597 static if(hasUDA!(__traits(getMember, T, field), unique)) 598 result ~= " UNIQUE"; 599 static if(hasUDA!(__traits(getMember, T, field), autoIncrement)) 600 { 601 static assert(hasUDA!(__traits(getMember, T, field), primaryKey)); 602 result ~= " PRIMARY KEY AUTOINCREMENT"; 603 if(++autoIncFields > 1) 604 { 605 assert(0, "Auto increment only allowed on one field!"); 606 } 607 } 608 609 result ~= ","; 610 } 611 } 612 // add primary key 613 alias keyFields = getSymbolsByUDA!(T, primaryKey); 614 static if(keyFields.length > 0) 615 { 616 if(autoIncFields > 0) 617 { 618 assert(keyFields.length == 1, "Only one primary key allowed when autoIncrement is used"); 619 } 620 else 621 { 622 result ~= " PRIMARY KEY ("; 623 static foreach(i, alias kf; keyFields) 624 { 625 static if(i != 0) 626 result ~= ","; 627 result ~= `"` ~ getColumnName!(kf) ~ `"`; 628 } 629 result ~= "),"; 630 } 631 } 632 633 static if(doForeignKeys) 634 { 635 import sqlbuilder.uda; 636 import sqlbuilder.traits; 637 foreach(field; FieldNameTuple!T) 638 { 639 // only look for field relations, not Relation items which have no 640 // local field. 641 static if(isField!(T, field) && isRelationField!(__traits(getMember, T, field))) 642 { 643 // get the relation name 644 alias mappings = getMappingsFor!(__traits(getMember, T, field)); 645 enum relation = getRelationFor!(__traits(getMember, T, field)); 646 result ~= `FOREIGN KEY ("`; 647 foreach(i, m; mappings) 648 { 649 static if(i != 0) 650 result ~= `", "`; 651 result ~= getColumnName!(__traits(getMember, T, m.key)); 652 } 653 result ~= `") REFERENCES "` ~ getTableName!(relation.foreign_table) ~ `" ("`; 654 foreach(i, m; mappings) 655 { 656 static if(i != 0) 657 result ~= `", "`; 658 result ~= getColumnName!(__traits(getMember, relation.foreign_table, m.foreign_key)); 659 } 660 result ~= `"),`; 661 } 662 } 663 } 664 665 return result[0 .. $-1] ~ ")"; 666 } 667 enum createTableSql = generate(); 668 } 669 670 enum dropTableSql(T) = `DROP TABLE IF EXISTS "` ~ getTableName!T ~ `"`; 671 672 // if we have sqlite as a dependency, provide direct serialization from a ResultRange 673 version(Have_d2sqlite3) 674 { 675 private import sqlbuilder.dialect.impl : objFieldNames; 676 private import d2sqlite3: Row, SqliteType, ResultRange, Database, Statement; 677 678 PType getPType(Row r, size_t idx) 679 { 680 PType result; 681 final switch(r.columnType(idx)) 682 { 683 case SqliteType.INTEGER: 684 result = r.peek!long(idx); 685 break; 686 case SqliteType.FLOAT: 687 result = r.peek!double(idx); 688 break; 689 case SqliteType.TEXT: 690 result = r.peek!string(idx); 691 break; 692 case SqliteType.BLOB: 693 result = r.peek!(immutable(ubyte)[])(idx); 694 break; 695 case SqliteType.NULL: 696 result = null; 697 break; 698 } 699 return result; 700 } 701 702 private auto getLeaf(T)(PType v) 703 { 704 static if(is(T : Nullable!U, U)) 705 { 706 if(v.isNull) 707 { 708 return T.init; 709 } 710 return T(getLeaf!U(v)); 711 } 712 else static if(is(T == AllowNullType!Args, Args...)) 713 { 714 if(v.isNull) 715 return T.nullVal; 716 return getLeaf!(T.type)(v); 717 } 718 else static if(is(T == PType)) 719 { 720 // just return as-is 721 return v; 722 } 723 else 724 { 725 alias RT = dbValueType!T; 726 // null not tolerated 727 static if(is(RT == T)) 728 return v.get!T; 729 else static if(is(typeof(T.fromDbValue(RT.init)))) 730 return T.fromDbValue(v.get!RT); 731 else static if(is(typeof(T(RT.init)))) 732 return T(v.get!RT); 733 else static if(is(typeof(RT.init.to!T))) 734 return v.get!RT.to!T; 735 else 736 static assert(0, "Cannot figure out how to convert database value of type " ~ RT.stringof ~ " to D type " ~ T.stringof); 737 } 738 } 739 740 struct DefaultObjectDeserializer(T) 741 { 742 static if(is(T == Nullable!U, U)) 743 { 744 private alias RT = U; 745 private enum isNullType = true; 746 } 747 else 748 { 749 private alias RT = T; 750 private enum isNullType = false; 751 } 752 size_t[objFieldNames!RT.length] colIds = size_t.max; 753 // returns true if we know about this columm otherwise false. 754 bool mapColumnId(string colname, size_t idx) 755 { 756 objSwitch: 757 switch(colname) 758 { 759 static foreach(fnum, fname; objFieldNames!RT) 760 { 761 case getColumnName!(__traits(getMember, RT, fname)): 762 colIds[fnum] = idx; 763 return true; 764 } 765 default: 766 // unhandled. 767 return false; 768 } 769 } 770 string[] unmappedColumns() 771 { 772 string[] result; 773 static foreach(fnum, fname; objFieldNames!RT) 774 { 775 if(colIds[fnum] == size_t.max) 776 result ~= fname; 777 } 778 return result; 779 } 780 781 auto deserializeRow(Row r) 782 { 783 // deserialize all the data, but if any are null that can't be, 784 // make the whole thing null. 785 RT result; 786 static if(isNullType) 787 bool wholeObjNull = false; 788 foreach(idx, n; objFieldNames!RT) 789 { 790 if(colIds[idx] != size_t.max) 791 { 792 import sqlbuilder.uda; 793 alias mem = __traits(getMember, RT, n); 794 795 // TODO: figure out to merge this code with the specific 796 // AllowNullType code. 797 static foreach(alias att; __traits(getAttributes, mem)) 798 { 799 static if(__traits(isSame, att, allowNull)) 800 enum nullValue = typeof(mem).init; 801 else static if(is(typeof(att) : AllowNull!U, U)) 802 enum nullValue = att.nullValue; 803 804 } 805 806 static if(is(typeof(nullValue))) 807 { 808 // use Nullable!X to get the data 809 auto v = getLeaf!(Nullable!(typeof(mem)))(r.getPType(colIds[idx])); 810 __traits(getMember, result, n) = v.get(nullValue); 811 } 812 else static if(isNullType && !(is(typeof(mem) == Nullable!N, N))) 813 { 814 // get as nullable, then set the whole object 815 // to null if it is null. 816 static if(is(typeof(mem) == void)) 817 pragma(msg, n); 818 auto v = getLeaf!(Nullable!(typeof(mem)))(r.getPType(colIds[idx])); 819 if(v.isNull) 820 { 821 wholeObjNull = true; 822 break; 823 } 824 else 825 __traits(getMember, result, n) = v.get; 826 } 827 else 828 { 829 __traits(getMember, result, n) = getLeaf!(typeof(mem))(r.getPType(colIds[idx])); 830 } 831 } 832 } 833 834 static if(isNullType) 835 { 836 if(wholeObjNull) 837 return Nullable!RT.init; // return a null value 838 return result.nullable; 839 } 840 else 841 return result; 842 } 843 } 844 845 struct NonObjectDeserializer(T) 846 { 847 size_t colId = size_t.max; 848 bool mapColumnId(string colname, size_t idx) 849 { 850 if(colId == size_t.max) 851 { 852 // ignore the name. 853 colId = idx; 854 return true; 855 } 856 // only have one column 857 return false; 858 } 859 860 auto deserializeRow(Row r) 861 { 862 return getLeaf!T(r.getPType(colId)); 863 } 864 } 865 866 struct ChangeDeserializer(T) 867 { 868 static if(is(T == Changed!(ColTypes), ColTypes...)) 869 alias CT = ColTypes; 870 else 871 static assert(0, "ChangeDeserializer must only be used with a Changed type, not ", T); 872 size_t[CT.length] colIds = 0; 873 size_t filled = 0; 874 bool mapColumnId(string colname, size_t idx) 875 { 876 if(filled == colIds.length) 877 return false; 878 colIds[filled++] = idx; 879 return true; 880 } 881 882 auto deserializeRow(Row r) 883 { 884 T result; 885 static foreach(idx, U; CT) 886 result.val[idx] = getLeaf!U(r.getPType(colIds[idx])); 887 return result; 888 } 889 } 890 891 private void setArg(Statement p, size_t idx, PType arg) 892 { 893 // note sqlite parameter indexes are 1-based 894 int idxi = cast(int)idx + 1; 895 with(PType.Tag) final switch(arg.tag) 896 { 897 case Integer: 898 p.bind(idxi, arg.get!long); 899 break; 900 case Float: 901 p.bind(idxi, arg.get!double); 902 break; 903 case Text: 904 // work around null pointer bug. 905 // See https://github.com/dlang-community/d2sqlite3/issues/77 906 auto str = arg.get!string; 907 if(str.ptr is null) 908 str = ""; 909 p.bind(idxi, str); 910 break; 911 case Blob: 912 // work around null pointer bug. 913 // See https://github.com/dlang-community/d2sqlite3/issues/77 914 auto data = arg.get!(.Blob); 915 if(data.ptr is null) 916 data = (data.ptr + 1)[0 .. 0]; 917 p.bind(idxi, data); 918 break; 919 case Null: 920 p.bind(idxi, null); 921 break; 922 } 923 } 924 925 // fetch a range of serialized items 926 auto fetch(bool throwOnExtraColumns = false, QD...)(Database conn, Query!(QD) q) 927 { 928 import std.range; 929 scope(failure) 930 { 931 import std.stdio; 932 writeln("Failed SQL is: ", q.sql!true); 933 } 934 auto p = conn.prepare(q.sql!true); 935 import std.range : enumerate; 936 foreach(idx, arg; q.params.enumerate) 937 p.setArg(idx, arg); 938 auto seq = p.execute(); 939 940 static if(q.QueryTypes.length == 0 || is(q.QueryTypes[0] == void)) 941 { 942 // just return the range generated from the query, types weren't provided. 943 return seq; 944 } 945 else 946 { 947 // do deserialization. 948 // custom types can define a sqlbuilderDeserializer static member 949 // to initialize a custom deserializer. 950 template getDeserializer(T) { 951 static if(is(T == RowObj!U, U)) 952 { 953 static if(__traits(hasMember, U, "initialColumnIds")) 954 static assert(0, "initialColumnIds is no longer the correct way to hook custom deserialization. please use sqlbuilderDeserializer."); 955 static if(__traits(hasMember, U, "sqlbuilderDeserializer")) 956 alias getDeserializer = typeof(() {return U.sqlbuilderDeserializer;}()); 957 else 958 alias getDeserializer = DefaultObjectDeserializer!U; 959 } 960 else static if(is(T == Changed!(Args), Args...)) 961 alias getDeserializer = ChangeDeserializer!T; 962 else 963 alias getDeserializer = NonObjectDeserializer!T; 964 } 965 966 import std.meta : staticMap; 967 alias colT = staticMap!(getDeserializer, q.QueryTypes); 968 static struct SerializedRange 969 { 970 private ResultRange seq; 971 private q.RowTypes row; // here is where we store the front element 972 private colT deserializers; 973 974 auto front() 975 { 976 static if(row.length == 1) 977 return row[0]; 978 else 979 { 980 import std.typecons : tuple; 981 return tuple(row); 982 } 983 } 984 985 bool empty() { return seq.empty; } 986 987 private void loadItem(bool isfirst) 988 { 989 if(!seq.empty) 990 { 991 auto oldRow = row; 992 auto sqlRow = seq.front; 993 static foreach(i; 0 .. row.length) 994 { 995 row[i] = deserializers[i].deserializeRow(sqlRow); 996 static if(isInstanceOf!(Changed, typeof(row[i]))) 997 // the changed flag needs to be set based on if 998 // the previous row is equal to this row 999 1000 row[i]._changed = isfirst || oldRow[i].val != row[i].val; 1001 } 1002 } 1003 } 1004 1005 void popFront() 1006 { 1007 seq.popFront; 1008 loadItem(false); 1009 } 1010 } 1011 1012 SerializedRange result; 1013 result.seq = seq; 1014 if(seq.empty) 1015 return result; 1016 1017 string[] colNames = new string[seq.front.length]; 1018 foreach(i; 0 .. seq.front.length) 1019 colNames[i] = seq.front.columnName(i); 1020 size_t colIdx; 1021 1022 foreach(idx, T; q.QueryTypes) 1023 { 1024 static if(is(T == RowObj!U, U)) 1025 { 1026 // handle this differently, because this could have a 1027 // custom deserializer, and we want to avoid passing in the 1028 // object end marker. 1029 static if(__traits(hasMember, U, "sqlbuilderDeserializer")) 1030 result.deserializers[idx] = U.sqlbuilderDeserializer; 1031 // serialize columns until we hit _objEnd 1032 while(colNames[colIdx] != "_objend") 1033 { 1034 if(!result.deserializers[idx].mapColumnId(colNames[colIdx], colIdx)) 1035 { 1036 static if(throwOnExtraColumns) 1037 throw new Exception("Unknown column name found: " ~ colNames[colIdx]); 1038 } 1039 ++colIdx; 1040 } 1041 ++colIdx; // skip the object end. 1042 } 1043 else 1044 { 1045 // Let the deserializer decide how many columns to accept 1046 while(colIdx < colNames.length && 1047 result.deserializers[idx].mapColumnId(colNames[colIdx], colIdx)) 1048 ++colIdx; 1049 } 1050 } 1051 1052 result.loadItem(true); 1053 return result; 1054 } 1055 } 1056 1057 long fetchTotal(QP...)(Database conn, Query!(QP) q) 1058 { 1059 q.limitQty = 0; 1060 q.limitOffset = 0; 1061 q.orders = q.orders.init; 1062 q.fields = SQLFragment!(q.ItemType)(ExprString("1")); 1063 auto sql = "SELECT COUNT(*) FROM(" ~ q.sql ~ ") `counter`"; 1064 ResultRange seq; 1065 auto p = conn.prepare(sql); 1066 import std.range : enumerate; 1067 foreach(idx, arg; q.params.enumerate) 1068 p.setArg(idx, arg); 1069 seq = p.execute(); 1070 return seq.empty ? 0 : seq.front.peek!long(0); 1071 } 1072 1073 auto fetchOne(bool throwOnExtraColumns = false, QD...)(Database conn, Query!(QD) q) 1074 { 1075 auto r = fetch!throwOnExtraColumns(conn, q.limit(1)); 1076 import std.range : ElementType; 1077 if(r.empty) 1078 throw new Exception("No items of type " ~ ElementType!(typeof(r)).stringof ~ " retreived from query"); 1079 return r.front; 1080 } 1081 1082 auto fetchOne(bool throwOnExtraColumns = false, T, QD...)(Database conn, Query!(QD) q, T defaultValue) 1083 { 1084 auto r = fetch!throwOnExtraColumns(conn, q.limit(1)); 1085 return r.empty ? defaultValue : r.front; 1086 } 1087 1088 template fetchUsingKey(T, bool throwOnExtraColumns = false) if (hasPrimaryKey!T) 1089 { 1090 auto fetchUsingKey(Args...)(Database conn, Args args) if (Args.length == primaryKeyFields!T.length) 1091 { 1092 import sqlbuilder.dataset; 1093 DataSet!T ds; 1094 return conn.fetchOne!throwOnExtraColumns(select(ds).havingKey(ds, args)); 1095 } 1096 } 1097 1098 auto fetchUsingKey(T, bool throwOnExtraColumns = false, Args...)(Database conn, T defaultValue, Args args) if (hasPrimaryKey!T && Args.length == primaryKeyFields!T.length) 1099 { 1100 import sqlbuilder.dataset; 1101 DataSet!T ds; 1102 return conn.fetchOne!throwOnExtraColumns(select(ds).havingKey(ds, args), defaultValue); 1103 } 1104 1105 // returns the rows affected 1106 long perform(Q)(Database conn, Q stmt) if(is(Q : Insert!P, P) || 1107 is(Q : Update!P, P) || 1108 is(Q : Delete!P, P)) 1109 { 1110 import std.range : enumerate; 1111 import std.stdio; 1112 scope(failure) writeln("failed statement is ", stmt.sql, " fields are ", stmt.params); 1113 auto p = conn.prepare(stmt.sql); 1114 foreach(idx, arg; stmt.params.enumerate) 1115 p.setArg(idx, arg); 1116 auto result = p.execute; 1117 return conn.changes(); 1118 } 1119 1120 auto ref T create(T)(Database conn, auto ref T blueprint) 1121 { 1122 import sqlbuilder.uda; 1123 1124 // use insert to create it 1125 auto ins = insert(blueprint); 1126 auto rowsAffected = conn.perform(ins); 1127 // check for an autoInc field 1128 foreach(fname; __traits(allMembers, T)) 1129 static if(isField!(T, fname) && 1130 hasUDA!(__traits(getMember, T, fname), autoIncrement)) 1131 { 1132 __traits(getMember, blueprint, fname) = 1133 cast(typeof(__traits(getMember, blueprint, fname)))conn.lastInsertRowid; 1134 } 1135 return blueprint; 1136 } 1137 1138 // returns true if the item was present. Only valid for records that have a 1139 // primary key. 1140 bool save(T)(Database conn, T item) if (hasPrimaryKey!T) 1141 { 1142 return conn.perform(update(item)) == 1; 1143 } 1144 1145 // returns true if the item was erased from the db. 1146 bool erase(T)(Database conn, T item) if (hasPrimaryKey!T) 1147 { 1148 return conn.perform(remove(item)) == 1; 1149 } 1150 1151 unittest 1152 { 1153 import sqlbuilder.dataset; 1154 import std.stdio; 1155 import std.range; 1156 import std.algorithm; 1157 static import std.file; 1158 auto tmpfilename = "./testDB.sqlite"; 1159 if(std.file.exists(tmpfilename)) 1160 std.file.remove(tmpfilename); 1161 auto conn = Database(tmpfilename); 1162 1163 conn.execute(createTableSql!(Author, true)); 1164 conn.execute(createTableSql!(book, true)); 1165 1166 conn.execute(createTableSql!(review, true)); 1167 auto steve = conn.create(Author("Steven", "Schveighoffer")); 1168 auto ds = DataSet!Author(); 1169 conn.perform(insert(ds.tableDef).set(ds.firstName, "Andrei".param).set(ds.lastName, Expr(`"Alexandrescu"`)).set(ds.ynAnswer, MyBool(true).param)); 1170 auto andreiId = cast(int)conn.lastInsertRowid(); 1171 conn.create(book("This Module", steve.id)); 1172 conn.create(book("The D Programming Language", andreiId)); 1173 auto book3 = conn.create(book("Modern C++ design", andreiId)); 1174 // always a change if we update, as long as the book is found. 1175 assert(conn.perform(update(book3)) == 1); 1176 book3.title = "Not so Modern C++ Design"; 1177 book3.book_type = BookType.Fiction; 1178 assert(conn.save(book3)); 1179 foreach(auth, book; conn.fetch(select(ds, ds.books).where(ds.lastName, " = ", "Alexandrescu".param))) 1180 { 1181 writeln("author: ", auth, ", book: ", book); 1182 } 1183 1184 foreach(newauth, auth, book; conn.fetch(select(ds.changed, ds, ds.books).orderBy(ds.id))) 1185 { 1186 if(newauth) 1187 writeln("Author: ", auth); 1188 writeln(" book: ", book); 1189 } 1190 1191 foreach(newauth, auth, book; conn.fetch(select(ds.id.changed, ds, ds.books).orderBy(ds.id))) 1192 { 1193 if(newauth) 1194 writeln("Author: ", auth); 1195 writeln(" book: ", book); 1196 } 1197 auto book4 = conn.create(book("Remove Me", steve.id)); 1198 writeln(book4); 1199 assert(conn.erase(book4) == 1); 1200 DataSet!book ds2; 1201 //assert(conn.perform(removeFrom(ds2.tableDef).where(ds2.author.lastName, " = ", "Alexandrescu".param)) == 2); 1202 assert(conn.perform(removeFrom(ds2.tableDef).havingKey(ds2.author, andreiId)) == 2); 1203 assert(conn.perform(removeFrom(ds2.tableDef).havingKey(ds2.author, steve)) == 1); 1204 1205 // generate a table with a null value 1206 import sqlbuilder.uda; 1207 static struct foo 1208 { 1209 @primaryKey int id; 1210 Nullable!int col1; 1211 } 1212 1213 conn.execute(createTableSql!foo); 1214 auto rds = DataSet!review.init; 1215 conn.perform(insert(rds.tableDef).set(rds.book_id, 1.param)); 1216 writeln(conn.fetch(select(rds))); 1217 1218 // test allowNull columns 1219 auto nullrating = conn.fetchOne(select(rds.rating).where(rds.book_id, " = ", 1.param)); 1220 assert(nullrating == -1); 1221 1222 // try a custom object 1223 static struct CustomObj 1224 { 1225 PType[] things; 1226 string[] colnames; 1227 1228 // custom serialization 1229 struct CustomObjSerializer { 1230 size_t startColId = -1; 1231 size_t endColId = -1; 1232 string[] names; 1233 bool mapColumnId(string colname, size_t idx) 1234 { 1235 if(startColId == -1) 1236 startColId = idx; 1237 endColId = idx + 1; 1238 names ~= colname; 1239 return true; 1240 } 1241 1242 CustomObj deserializeRow(Row r) 1243 { 1244 import std.array; 1245 CustomObj result; 1246 result.colnames = names; 1247 result.things = zip(cycle(only(r)), iota(startColId, endColId)).map!(t => t[0].getPType(t[1])).array; 1248 return result; 1249 } 1250 } 1251 enum sqlbuilderDeserializer = CustomObjSerializer.init; 1252 } 1253 1254 // fetch the custom row from the table 1255 auto customrow = ColumnDef!(RowObj!CustomObj)(TableDef("author", ExprString("author".makeSpec(Spec.id))), ExprString("*", objEndSpec)); 1256 auto query3 = select(customrow); 1257 writeln("query3 is ", query3.sql); 1258 foreach(co; conn.fetch(select(customrow))) 1259 { 1260 static assert(is(typeof(co) == CustomObj)); 1261 writefln("Got author row (column names = %-(%s, %)): %s", co.colnames, co.things); 1262 } 1263 } 1264 }