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