1 /++ 2 Generic interface for RDBMS access. Use with one of the implementations in [arsd.mysql], [arsd.sqlite], [arsd.postgres], or [arsd.mssql]. I'm sorry the docs are not good, but a little bit goes a long way: 3 4 --- 5 auto db = new Sqlite("file.db"); // see the implementations for constructors 6 // then the interface, for any impl can be as simple as: 7 8 foreach(row; db.query("SELECT id, name FROM people")) { 9 string id = row[0]; 10 string name = row[1]; 11 } 12 13 db.query("INSERT INTO people (id, name) VALUES (?, ?)", 5, "Adam"); 14 --- 15 16 To convert to other types, just use [std.conv.to] since everything comes out of this as simple strings with the exception of binary data, 17 which you'll want to cast to const(ubyte)[]. 18 19 History: 20 Originally written prior to 2011. 21 22 On August 2, 2022, the behavior of BLOB (or BYTEA in postgres) changed significantly. 23 Before, it would convert to strings with `to!string(bytes)` on insert and platform specific 24 on query. It didn't really work at all. 25 26 It now actually stores ubyte[] as a blob and retrieves it without modification. Note you need to 27 cast it. 28 29 This is potentially breaking, but since it didn't work much before I doubt anyone was using it successfully 30 but this might be a problem. I advise you to retest. 31 32 Be aware I don't like this string interface much anymore and want to change it significantly but idk 33 how to work it in without breaking a decade of code. 34 35 On June 7, 2023 (dub 11.0), I started the process of moving away from strings as the inner storage unit. This is a potentially breaking change, but you can use `.toString` to convert as needed and `alias this` will try to do this automatically in many situations. See [DatabaseDatum] for details. This transition is not yet complete. 36 +/ 37 module arsd.database; 38 39 // FIXME: add some kind of connection pool thing we can easily use 40 41 // I should do a prepared statement as a template string arg 42 43 public import std.variant; 44 import std.string; 45 public import std.datetime; 46 47 static import arsd.core; 48 private import arsd.core : LimitedVariant; 49 50 /* 51 Database 2.0 plan, WIP: 52 53 // Do I want to do some kind of RAII? 54 auto database = Database(new MySql("connection info")); 55 56 * Prepared statement support 57 * Queries with separate args whenever we can with consistent interface 58 * Query returns some typed info when we can. 59 * ....? 60 61 62 PreparedStatement prepareStatement(string sql); 63 64 Might be worth looking at doing the preparations in static ctors 65 so they are always done once per program... 66 */ 67 68 /// 69 interface Database { 70 /// Actually implements the query for the database. The query() method 71 /// below might be easier to use. 72 ResultSet queryImpl(string sql, Variant[] args...); 73 74 /// Escapes data for inclusion into an sql string literal 75 string escape(string sqlData); 76 /// Escapes binary data for inclusion into a sql string. Note that unlike `escape`, the returned string here SHOULD include the quotes. 77 string escapeBinaryString(const(ubyte)[] sqlData); 78 79 /// query to start a transaction, only here because sqlite is apparently different in syntax... 80 void startTransaction(); 81 82 /// Just executes a query. It supports placeholders for parameters 83 final ResultSet query(T...)(string sql, T t) { 84 Variant[] args; 85 foreach(arg; t) { 86 Variant a; 87 static if(__traits(compiles, a = arg)) 88 a = arg; 89 else 90 a = to!string(t); 91 args ~= a; 92 } 93 return queryImpl(sql, args); 94 } 95 96 /// turns a systime into a value understandable by the target database as a timestamp to be concated into a query. so it should be quoted and escaped etc as necessary 97 string sysTimeToValue(SysTime); 98 99 /// Prepared statement api 100 /* 101 PreparedStatement prepareStatement(string sql, int numberOfArguments); 102 103 */ 104 } 105 import std.stdio; 106 107 // Added Oct 26, 2021 108 Row queryOneRow(string file = __FILE__, size_t line = __LINE__, T...)(Database db, string sql, T t) { 109 auto res = db.query(sql, t); 110 if(res.empty) 111 throw new Exception("no row in result", file, line); 112 auto row = res.front; 113 return row; 114 } 115 116 Ret queryOneColumn(Ret, string file = __FILE__, size_t line = __LINE__, T...)(Database db, string sql, T t) { 117 auto row = queryOneRow(db, sql, t); 118 return to!Ret(row[0]); 119 } 120 121 struct Query { 122 ResultSet result; 123 this(T...)(Database db, string sql, T t) if(T.length!=1 || !is(T[0]==Variant[])) { 124 result = db.query(sql, t); 125 } 126 // Version for dynamic generation of args: (Needs to be a template for coexistence with other constructor. 127 this(T...)(Database db, string sql, T args) if (T.length==1 && is(T[0] == Variant[])) { 128 result = db.queryImpl(sql, args); 129 } 130 131 int opApply(T)(T dg) if(is(T == delegate)) { 132 import std.traits; 133 foreach(row; result) { 134 ParameterTypeTuple!dg tuple; 135 136 foreach(i, item; tuple) { 137 tuple[i] = to!(typeof(item))(row[i]); 138 } 139 140 if(auto result = dg(tuple)) 141 return result; 142 } 143 144 return 0; 145 } 146 } 147 148 /++ 149 Represents a single item in a result. A row is a set of these `DatabaseDatum`s. 150 151 History: 152 Added June 2, 2023 (dub v11.0). Prior to this, it would always use `string`. This has `alias toString this` to try to maintain compatibility. 153 +/ 154 struct DatabaseDatum { 155 int platformSpecificTag; 156 LimitedVariant storage; 157 158 /++ 159 These are normally constructed by the library, so you shouldn't need these constructors. If you're writing a new database implementation though, here it is. 160 +/ 161 package this(string s) { 162 storage = s; 163 } 164 165 /++ 166 Returns `true` if the item was `NULL` in the database. 167 +/ 168 bool isNull() { 169 return storage.contains == LimitedVariant.Contains.null_; 170 } 171 172 /++ 173 Converts the datum to a string in a format specified by the database. 174 +/ 175 string toString() { 176 return storage.toString(); 177 } 178 /++ 179 For compatibility with earlier versions of the api, all data can easily convert to string implicitly and opCast keeps to!x(this) working. 180 +/ 181 alias toString this; 182 183 /// ditto 184 T opCast(T)() { 185 import std.conv; 186 return to!T(this.toString); 187 } 188 } 189 190 /++ 191 A row in a result set from a query. 192 193 You can access this as either an array or associative array: 194 195 --- 196 foreach(Row row; db.query("SELECT id, name FROM mytable")) { 197 // can access by index or by name 198 row[0] == row["id"]; 199 row[1] == row["name"]; 200 201 // can also iterate over the results 202 foreach(name, data; row) { 203 // will send name = "id", data = the thing 204 // and then next loop will be name = "name", data = the thing 205 } 206 } 207 --- 208 +/ 209 struct Row { 210 package DatabaseDatum[] row; 211 package ResultSet resultSet; 212 213 /++ 214 Allows for access by index or column name. 215 +/ 216 DatabaseDatum opIndex(size_t idx, string file = __FILE__, int line = __LINE__) { 217 if(idx >= row.length) 218 throw new Exception(text("index ", idx, " is out of bounds on result"), file, line); 219 return row[idx]; 220 } 221 222 /// ditto 223 DatabaseDatum opIndex(string name, string file = __FILE__, int line = __LINE__) { 224 auto idx = resultSet.getFieldIndex(name); 225 if(idx >= row.length) 226 throw new Exception(text("no field ", name, " in result"), file, line); 227 return row[idx]; 228 } 229 230 /++ 231 Provides a string representation of the row, for quick eyeball debugging. You probably won't want the format this prints in (and don't rely upon it, as it is subject to change at any time without notice!), but it might be useful for use with `writeln`. 232 +/ 233 string toString() { 234 return to!string(row); 235 } 236 237 /++ 238 Allows iteration over the columns with the `foreach` statement. 239 240 History: 241 Prior to June 11, 2023 (dub v11.0), the order of iteration was undefined. It is now guaranteed to be in the same order as it was returned by the database (which is determined by your original query). Additionally, prior to this date, the datum was typed `string`. `DatabaseDatum` should implicitly convert to string, so your code is unlikely to break, but if you did specify the type explicitly you may need to update your code. 242 243 The overload with one argument, having just the datum without the name, was also added on June 11, 2023 (dub v11.0). 244 +/ 245 int opApply(int delegate(string, DatabaseDatum) dg) { 246 string[] fn = resultSet.fieldNames(); 247 foreach(idx, item; row) 248 mixin(yield("fn[idx], item")); 249 250 return 0; 251 } 252 253 /// ditto 254 int opApply(int delegate(DatabaseDatum) dg) { 255 foreach(item; row) 256 mixin(yield("item")); 257 return 0; 258 } 259 260 /++ 261 Hacky conversion to simpler types. 262 263 I'd recommend against using these in new code. I wrote them back around 2011 as a hack for something I was doing back then. Among the downsides of these is type information loss in both functions (since strings discard the information tag) and column order loss in `toAA` (since D associative arrays do not maintain any defined order). Additionally, they to make an additional copy of the result row, which you may be able to avoid by looping over it directly. 264 265 I may formally deprecate them in a future release. 266 +/ 267 string[] toStringArray() { 268 string[] row; 269 foreach(item; this.row) 270 row ~= item; 271 return row; 272 } 273 274 /// ditto 275 string[string] toAA() { 276 string[string] a; 277 278 string[] fn = resultSet.fieldNames(); 279 280 foreach(i, r; row) 281 a[fn[i]] = r; 282 283 return a; 284 } 285 286 } 287 import std.conv; 288 289 interface ResultSet { 290 // name for associative array to result index 291 int getFieldIndex(string field); 292 string[] fieldNames(); 293 294 // this is a range that can offer other ranges to access it 295 bool empty() @property; 296 Row front() @property; 297 void popFront() ; 298 size_t length() @property; 299 300 /* deprecated */ final ResultSet byAssoc() { return this; } 301 } 302 303 class DatabaseException : Exception { 304 this(string msg, string file = __FILE__, size_t line = __LINE__) { 305 super(msg, file, line); 306 } 307 } 308 309 310 311 abstract class SqlBuilder { } 312 313 class InsertBuilder : SqlBuilder { 314 private string table; 315 private string[] fields; 316 private string[] fieldsSetSql; 317 private Variant[] values; 318 319 /// 320 void setTable(string table) { 321 this.table = table; 322 } 323 324 /// same as adding the arr as values one by one. assumes DB column name matches AA key. 325 void addVariablesFromAssociativeArray(in string[string] arr, string[] names...) { 326 foreach(name; names) { 327 fields ~= name; 328 if(name in arr) { 329 fieldsSetSql ~= "?"; 330 values ~= Variant(arr[name]); 331 } else { 332 fieldsSetSql ~= "null"; 333 } 334 } 335 } 336 337 /// 338 void addVariable(T)(string name, T value) { 339 fields ~= name; 340 fieldsSetSql ~= "?"; 341 values ~= Variant(value); 342 } 343 344 /// if you use a placeholder, be sure to [addValueForHandWrittenPlaceholder] immediately 345 void addFieldWithSql(string name, string sql) { 346 fields ~= name; 347 fieldsSetSql ~= sql; 348 } 349 350 /// for addFieldWithSql that includes a placeholder 351 void addValueForHandWrittenPlaceholder(T)(T value) { 352 values ~= Variant(value); 353 } 354 355 /// executes the query 356 auto execute(Database db, string supplementalSql = null) { 357 return db.queryImpl(this.toSql() ~ supplementalSql, values); 358 } 359 360 string toSql() { 361 string sql = "INSERT INTO\n"; 362 sql ~= "\t" ~ table ~ " (\n"; 363 foreach(idx, field; fields) { 364 sql ~= "\t\t" ~ field ~ ((idx != fields.length - 1) ? ",\n" : "\n"); 365 } 366 sql ~= "\t) VALUES (\n"; 367 foreach(idx, field; fieldsSetSql) { 368 sql ~= "\t\t" ~ field ~ ((idx != fieldsSetSql.length - 1) ? ",\n" : "\n"); 369 } 370 sql ~= "\t)\n"; 371 return sql; 372 } 373 } 374 375 /// WARNING: this is as susceptible to SQL injections as you would be writing it out by hand 376 class SelectBuilder : SqlBuilder { 377 string[] fields; 378 string table; 379 string[] joins; 380 string[] wheres; 381 string[] orderBys; 382 string[] groupBys; 383 384 int limit; 385 int limitStart; 386 387 Variant[string] vars; 388 void setVariable(T)(string name, T value) { 389 assert(name.length); 390 if(name[0] == '?') 391 name = name[1 .. $]; 392 vars[name] = Variant(value); 393 } 394 395 Database db; 396 this(Database db = null) { 397 this.db = db; 398 } 399 400 /* 401 It would be nice to put variables right here in the builder 402 403 ?name 404 405 will prolly be the syntax, and we'll do a Variant[string] of them. 406 407 Anything not translated here will of course be in the ending string too 408 */ 409 410 SelectBuilder cloned() { 411 auto s = new SelectBuilder(this.db); 412 s.fields = this.fields.dup; 413 s.table = this.table; 414 s.joins = this.joins.dup; 415 s.wheres = this.wheres.dup; 416 s.orderBys = this.orderBys.dup; 417 s.groupBys = this.groupBys.dup; 418 s.limit = this.limit; 419 s.limitStart = this.limitStart; 420 421 foreach(k, v; this.vars) 422 s.vars[k] = v; 423 424 return s; 425 } 426 427 override string toString() { 428 string sql = "SELECT "; 429 430 // the fields first 431 { 432 bool outputted = false; 433 foreach(field; fields) { 434 if(outputted) 435 sql ~= ", "; 436 else 437 outputted = true; 438 439 sql ~= field; // "`" ~ field ~ "`"; 440 } 441 } 442 443 sql ~= " FROM " ~ table; 444 445 if(joins.length) { 446 foreach(join; joins) 447 sql ~= " " ~ join; 448 } 449 450 if(wheres.length) { 451 bool outputted = false; 452 sql ~= " WHERE "; 453 foreach(w; wheres) { 454 if(outputted) 455 sql ~= " AND "; 456 else 457 outputted = true; 458 sql ~= "(" ~ w ~ ")"; 459 } 460 } 461 462 if(groupBys.length) { 463 bool outputted = false; 464 sql ~= " GROUP BY "; 465 foreach(o; groupBys) { 466 if(outputted) 467 sql ~= ", "; 468 else 469 outputted = true; 470 sql ~= o; 471 } 472 } 473 474 if(orderBys.length) { 475 bool outputted = false; 476 sql ~= " ORDER BY "; 477 foreach(o; orderBys) { 478 if(outputted) 479 sql ~= ", "; 480 else 481 outputted = true; 482 sql ~= o; 483 } 484 } 485 486 if(limit) { 487 sql ~= " LIMIT "; 488 if(limitStart) 489 sql ~= to!string(limitStart) ~ ", "; 490 sql ~= to!string(limit); 491 } 492 493 if(db is null) 494 return sql; 495 496 return escapedVariants(db, sql, vars); 497 } 498 } 499 500 501 // /////////////////////sql////////////////////////////////// 502 503 package string tohexsql(const(ubyte)[] b) { 504 char[] x; 505 x.length = b.length * 2 + 3; 506 int pos = 0; 507 x[pos++] = 'x'; 508 x[pos++] = '\''; 509 510 char tohex(ubyte a) { 511 if(a < 10) 512 return cast(char)(a + '0'); 513 else 514 return cast(char)(a - 10 + 'A'); 515 } 516 517 foreach(item; b) { 518 x[pos++] = tohex(item >> 4); 519 x[pos++] = tohex(item & 0x0f); 520 } 521 522 x[pos++] = '\''; 523 524 return cast(string) x; 525 } 526 527 // used in the internal placeholder thing 528 string toSql(Database db, Variant a) { 529 530 string binary(const(ubyte)[] b) { 531 if(b is null) 532 return "NULL"; 533 else 534 return db.escapeBinaryString(b); 535 } 536 537 auto v = a.peek!(void*); 538 if(v && (*v is null)) { 539 return "NULL"; 540 } else if(auto t = a.peek!(SysTime)) { 541 return db.sysTimeToValue(*t); 542 } else if(auto t = a.peek!(DateTime)) { 543 // FIXME: this might be broken cuz of timezones! 544 return db.sysTimeToValue(cast(SysTime) *t); 545 } else if(auto t = a.peek!(ubyte[])) { 546 return binary(*t); 547 } else if(auto t = a.peek!(immutable(ubyte)[])) { 548 return binary(*t); 549 } else if(auto t = a.peek!string) { 550 auto str = *t; 551 if(str is null) 552 return "NULL"; 553 else 554 return '\'' ~ db.escape(str) ~ '\''; 555 } else { 556 string str = to!string(a); 557 return '\'' ~ db.escape(str) ~ '\''; 558 } 559 560 assert(0); 561 } 562 563 // just for convenience; "str".toSql(db); 564 string toSql(string s, Database db) { 565 //if(s is null) 566 //return "NULL"; 567 return '\'' ~ db.escape(s) ~ '\''; 568 } 569 570 string toSql(long s, Database db) { 571 return to!string(s); 572 } 573 574 string escapedVariants(Database db, in string sql, Variant[string] t) { 575 if(t.keys.length <= 0 || sql.indexOf("?") == -1) { 576 return sql; 577 } 578 579 string fixedup; 580 int currentStart = 0; 581 // FIXME: let's make ?? render as ? so we have some escaping capability 582 foreach(i, dchar c; sql) { 583 if(c == '?') { 584 fixedup ~= sql[currentStart .. i]; 585 586 int idxStart = cast(int) i + 1; 587 int idxLength; 588 589 bool isFirst = true; 590 591 while(idxStart + idxLength < sql.length) { 592 char C = sql[idxStart + idxLength]; 593 594 if((C >= 'a' && C <= 'z') || (C >= 'A' && C <= 'Z') || C == '_' || (!isFirst && C >= '0' && C <= '9')) 595 idxLength++; 596 else 597 break; 598 599 isFirst = false; 600 } 601 602 auto idx = sql[idxStart .. idxStart + idxLength]; 603 604 if(idx in t) { 605 fixedup ~= toSql(db, t[idx]); 606 currentStart = idxStart + idxLength; 607 } else { 608 // just leave it there, it might be done on another layer 609 currentStart = cast(int) i; 610 } 611 } 612 } 613 614 fixedup ~= sql[currentStart .. $]; 615 616 return fixedup; 617 } 618 619 /// Note: ?n params are zero based! 620 string escapedVariants(Database db, in string sql, Variant[] t) { 621 // FIXME: let's make ?? render as ? so we have some escaping capability 622 // if nothing to escape or nothing to escape with, don't bother 623 if(t.length > 0 && sql.indexOf("?") != -1) { 624 string fixedup; 625 int currentIndex; 626 int currentStart = 0; 627 foreach(i, dchar c; sql) { 628 if(c == '?') { 629 fixedup ~= sql[currentStart .. i]; 630 631 int idx = -1; 632 currentStart = cast(int) i + 1; 633 if((i + 1) < sql.length) { 634 auto n = sql[i + 1]; 635 if(n >= '0' && n <= '9') { 636 currentStart = cast(int) i + 2; 637 idx = n - '0'; 638 } 639 } 640 if(idx == -1) { 641 idx = currentIndex; 642 currentIndex++; 643 } 644 645 if(idx < 0 || idx >= t.length) 646 throw new Exception("SQL Parameter index is out of bounds: " ~ to!string(idx) ~ " at `"~sql[0 .. i]~"`"); 647 648 fixedup ~= toSql(db, t[idx]); 649 } 650 } 651 652 fixedup ~= sql[currentStart .. $]; 653 654 return fixedup; 655 /* 656 string fixedup; 657 int pos = 0; 658 659 660 void escAndAdd(string str, int q) { 661 fixedup ~= sql[pos..q] ~ '\'' ~ db.escape(str) ~ '\''; 662 663 } 664 665 foreach(a; t) { 666 int q = sql[pos..$].indexOf("?"); 667 if(q == -1) 668 break; 669 q += pos; 670 671 auto v = a.peek!(void*); 672 if(v && (*v is null)) 673 fixedup ~= sql[pos..q] ~ "NULL"; 674 else { 675 string str = to!string(a); 676 escAndAdd(str, q); 677 } 678 679 pos = q+1; 680 } 681 682 fixedup ~= sql[pos..$]; 683 684 sql = fixedup; 685 */ 686 } 687 688 return sql; 689 } 690 691 692 693 694 695 696 enum UpdateOrInsertMode { 697 CheckForMe, 698 AlwaysUpdate, 699 AlwaysInsert 700 } 701 702 703 // BIG FIXME: this should really use prepared statements 704 int updateOrInsert(Database db, string table, string[string] values, string where, UpdateOrInsertMode mode = UpdateOrInsertMode.CheckForMe, string key = "id") { 705 706 string identifierQuote = ""; 707 708 bool insert = false; 709 710 final switch(mode) { 711 case UpdateOrInsertMode.CheckForMe: 712 auto res = db.query("SELECT "~key~" FROM "~identifierQuote~db.escape(table)~identifierQuote~" WHERE " ~ where); 713 insert = res.empty; 714 715 break; 716 case UpdateOrInsertMode.AlwaysInsert: 717 insert = true; 718 break; 719 case UpdateOrInsertMode.AlwaysUpdate: 720 insert = false; 721 break; 722 } 723 724 725 if(insert) { 726 string insertSql = "INSERT INTO " ~identifierQuote ~ db.escape(table) ~ identifierQuote ~ " "; 727 728 bool outputted = false; 729 string vs, cs; 730 foreach(column, value; values) { 731 if(column is null) 732 continue; 733 if(outputted) { 734 vs ~= ", "; 735 cs ~= ", "; 736 } else 737 outputted = true; 738 739 //cs ~= "`" ~ db.escape(column) ~ "`"; 740 cs ~= identifierQuote ~ column ~ identifierQuote; // FIXME: possible insecure 741 if(value is null) 742 vs ~= "NULL"; 743 else 744 vs ~= "'" ~ db.escape(value) ~ "'"; 745 } 746 747 if(!outputted) 748 return 0; 749 750 751 insertSql ~= "(" ~ cs ~ ")"; 752 insertSql ~= " VALUES "; 753 insertSql ~= "(" ~ vs ~ ")"; 754 755 db.query(insertSql); 756 757 return 0; // db.lastInsertId; 758 } else { 759 string updateSql = "UPDATE "~identifierQuote~db.escape(table)~identifierQuote~" SET "; 760 761 bool outputted = false; 762 foreach(column, value; values) { 763 if(column is null) 764 continue; 765 if(outputted) 766 updateSql ~= ", "; 767 else 768 outputted = true; 769 770 if(value is null) 771 updateSql ~= identifierQuote ~ db.escape(column) ~ identifierQuote ~ " = NULL"; 772 else 773 updateSql ~= identifierQuote ~ db.escape(column) ~ identifierQuote ~ " = '" ~ db.escape(value) ~ "'"; 774 } 775 776 if(!outputted) 777 return 0; 778 779 updateSql ~= " WHERE " ~ where; 780 781 db.query(updateSql); 782 return 0; 783 } 784 } 785 786 787 788 789 790 string fixupSqlForDataObjectUse(string sql, string[string] keyMapping = null) { 791 792 string[] tableNames; 793 794 string piece = sql; 795 sizediff_t idx; 796 while((idx = piece.indexOf("JOIN")) != -1) { 797 auto start = idx + 5; 798 auto i = start; 799 while(piece[i] != ' ' && piece[i] != '\n' && piece[i] != '\t' && piece[i] != ',') 800 i++; 801 auto end = i; 802 803 tableNames ~= strip(piece[start..end]); 804 805 piece = piece[end..$]; 806 } 807 808 idx = sql.indexOf("FROM"); 809 if(idx != -1) { 810 auto start = idx + 5; 811 auto i = start; 812 start = i; 813 while(i < sql.length && !(sql[i] > 'A' && sql[i] <= 'Z')) // if not uppercase, except for A (for AS) to avoid SQL keywords (hack) 814 i++; 815 816 auto from = sql[start..i]; 817 auto pieces = from.split(","); 818 foreach(p; pieces) { 819 p = p.strip(); 820 start = 0; 821 i = 0; 822 while(i < p.length && p[i] != ' ' && p[i] != '\n' && p[i] != '\t' && p[i] != ',') 823 i++; 824 825 tableNames ~= strip(p[start..i]); 826 } 827 828 string sqlToAdd; 829 foreach(tbl; tableNames) { 830 if(tbl.length) { 831 string keyName = "id"; 832 if(tbl in keyMapping) 833 keyName = keyMapping[tbl]; 834 sqlToAdd ~= ", " ~ tbl ~ "." ~ keyName ~ " AS " ~ "id_from_" ~ tbl; 835 } 836 } 837 838 sqlToAdd ~= " "; 839 840 sql = sql[0..idx] ~ sqlToAdd ~ sql[idx..$]; 841 } 842 843 return sql; 844 } 845 846 847 848 849 850 /* 851 This is like a result set 852 853 854 DataObject res = [...]; 855 856 res.name = "Something"; 857 858 res.commit; // runs the actual update or insert 859 860 861 res = new DataObject(fields, tables 862 863 864 865 866 867 868 869 when doing a select, we need to figure out all the tables and modify the query to include the ids we need 870 871 872 search for FROM and JOIN 873 the next token is the table name 874 875 right before the FROM, add the ids of each table 876 877 878 given: 879 SELECT name, phone FROM customers LEFT JOIN phones ON customer.id = phones.cust_id 880 881 we want: 882 SELECT name, phone, customers.id AS id_from_customers, phones.id AS id_from_phones FROM customers LEFT JOIN phones ON customer.id[...]; 883 884 */ 885 886 mixin template DataObjectConstructors() { 887 this(Database db, string[string] res, Tuple!(string, string)[string] mappings) { 888 super(db, res, mappings); 889 } 890 } 891 892 private string yield(string what) { return `if(auto result = dg(`~what~`)) return result;`; } 893 894 import std.typecons; 895 import std.json; // for json value making 896 class DataObject { 897 // lets you just free-form set fields, assuming they all come from the given table 898 // note it doesn't try to handle joins for new rows. you've gotta do that yourself 899 this(Database db, string table, UpdateOrInsertMode mode = UpdateOrInsertMode.CheckForMe) { 900 assert(db !is null); 901 this.db = db; 902 this.table = table; 903 904 this.mode = mode; 905 } 906 907 JSONValue makeJsonValue() { 908 JSONValue val; 909 JSONValue[string] valo; 910 //val.type = JSON_TYPE.OBJECT; 911 foreach(k, v; fields) { 912 JSONValue s; 913 //s.type = JSON_TYPE.STRING; 914 s.str = v; 915 valo[k] = s; 916 } 917 val = valo; 918 return val; 919 } 920 921 this(Database db, string[string] res, Tuple!(string, string)[string] mappings) { 922 this.db = db; 923 this.mappings = mappings; 924 this.fields = res; 925 926 mode = UpdateOrInsertMode.AlwaysUpdate; 927 } 928 929 string table; 930 // table, column [alias] 931 Tuple!(string, string)[string] mappings; 932 933 // value [field] [table] 934 string[string][string] multiTableKeys; // note this is not set internally tight now 935 // but it can be set manually to do multi table mappings for automatic update 936 937 938 string opDispatch(string field, string file = __FILE__, size_t line = __LINE__)() 939 if((field.length < 8 || field[0..8] != "id_from_") && field != "popFront") 940 { 941 if(field !in fields) 942 throw new Exception("no such field " ~ field, file, line); 943 944 return fields[field]; 945 } 946 947 string opDispatch(string field, T)(T t) 948 if((field.length < 8 || field[0..8] != "id_from_") && field != "popFront") 949 { 950 static if(__traits(compiles, t is null)) { 951 if(t is null) 952 setImpl(field, null); 953 else 954 setImpl(field, to!string(t)); 955 } else 956 setImpl(field, to!string(t)); 957 958 return fields[field]; 959 } 960 961 962 private void setImpl(string field, string value) { 963 if(field in fields) { 964 if(fields[field] != value) 965 changed[field] = true; 966 } else { 967 changed[field] = true; 968 } 969 970 fields[field] = value; 971 } 972 973 public void setWithoutChange(string field, string value) { 974 fields[field] = value; 975 } 976 977 int opApply(int delegate(ref string) dg) { 978 foreach(a; fields) 979 mixin(yield("a")); 980 981 return 0; 982 } 983 984 int opApply(int delegate(ref string, ref string) dg) { 985 foreach(a, b; fields) 986 mixin(yield("a, b")); 987 988 return 0; 989 } 990 991 992 string opIndex(string field, string file = __FILE__, size_t line = __LINE__) { 993 if(field !in fields) 994 throw new DatabaseException("No such field in data object: " ~ field, file, line); 995 return fields[field]; 996 } 997 998 string opIndexAssign(string value, string field) { 999 setImpl(field, value); 1000 return value; 1001 } 1002 1003 string* opBinary(string op)(string key) if(op == "in") { 1004 return key in fields; 1005 } 1006 1007 string[string] fields; 1008 bool[string] changed; 1009 1010 void commitChanges() { 1011 commitChanges(cast(string) null, null); 1012 } 1013 1014 void commitChanges(string key, string keyField) { 1015 commitChanges(key is null ? null : [key], keyField is null ? null : [keyField]); 1016 } 1017 1018 void commitChanges(string[] keys, string[] keyFields = null) { 1019 string[string][string] toUpdate; 1020 int updateCount = 0; 1021 foreach(field, c; changed) { 1022 if(c) { 1023 string tbl, col; 1024 if(mappings is null) { 1025 tbl = this.table; 1026 col = field; 1027 } else { 1028 if(field !in mappings) 1029 assert(0, "no such mapping for " ~ field); 1030 auto m = mappings[field]; 1031 tbl = m[0]; 1032 col = m[1]; 1033 } 1034 1035 toUpdate[tbl][col] = fields[field]; 1036 updateCount++; 1037 } 1038 } 1039 1040 if(updateCount) { 1041 db.startTransaction(); 1042 scope(success) db.query("COMMIT"); 1043 scope(failure) db.query("ROLLBACK"); 1044 1045 foreach(tbl, values; toUpdate) { 1046 string where, keyFieldToPass; 1047 1048 if(keys is null) { 1049 keys = [null]; 1050 } 1051 1052 if(multiTableKeys is null || tbl !in multiTableKeys) 1053 foreach(i, key; keys) { 1054 string keyField; 1055 1056 if(key is null) { 1057 key = "id_from_" ~ tbl; 1058 if(key !in fields) 1059 key = "id"; 1060 } 1061 1062 if(i >= keyFields.length || keyFields[i] is null) { 1063 if(key == "id_from_" ~ tbl) 1064 keyField = "id"; 1065 else 1066 keyField = key; 1067 } else { 1068 keyField = keyFields[i]; 1069 } 1070 1071 1072 if(where.length) 1073 where ~= " AND "; 1074 1075 auto f = key in fields ? fields[key] : null; 1076 if(f is null) 1077 where ~= keyField ~ " = NULL"; 1078 else 1079 where ~= keyField ~ " = '"~db.escape(f)~"'" ; 1080 if(keyFieldToPass.length) 1081 keyFieldToPass ~= ", "; 1082 1083 keyFieldToPass ~= keyField; 1084 } 1085 else { 1086 foreach(keyField, v; multiTableKeys[tbl]) { 1087 if(where.length) 1088 where ~= " AND "; 1089 1090 where ~= keyField ~ " = '"~db.escape(v)~"'" ; 1091 if(keyFieldToPass.length) 1092 keyFieldToPass ~= ", "; 1093 1094 keyFieldToPass ~= keyField; 1095 } 1096 } 1097 1098 1099 1100 updateOrInsert(db, tbl, values, where, mode, keyFieldToPass); 1101 } 1102 1103 changed = null; 1104 } 1105 } 1106 1107 void commitDelete() { 1108 if(mode == UpdateOrInsertMode.AlwaysInsert) 1109 throw new Exception("Cannot delete an item not in the database"); 1110 1111 assert(table.length); // FIXME, should work with fancy items too 1112 1113 // FIXME: escaping and primary key questions 1114 db.query("DELETE FROM " ~ table ~ " WHERE id = '" ~ db.escape(fields["id"]) ~ "'"); 1115 } 1116 1117 string getAlias(string table, string column) { 1118 string ali; 1119 if(mappings is null) { 1120 if(this.table is null) { 1121 mappings[column] = tuple(table, column); 1122 return column; 1123 } else { 1124 assert(table == this.table); 1125 ali = column; 1126 } 1127 } else { 1128 foreach(a, what; mappings) 1129 if(what[0] == table && what[1] == column 1130 && a.indexOf("id_from_") == -1) { 1131 ali = a; 1132 break; 1133 } 1134 } 1135 1136 return ali; 1137 } 1138 1139 void set(string table, string column, string value) { 1140 string ali = getAlias(table, column); 1141 //assert(ali in fields); 1142 setImpl(ali, value); 1143 } 1144 1145 string select(string table, string column) { 1146 string ali = getAlias(table, column); 1147 //assert(ali in fields); 1148 if(ali in fields) 1149 return fields[ali]; 1150 return null; 1151 } 1152 1153 DataObject addNew() { 1154 auto n = new DataObject(db, null); 1155 1156 n.db = this.db; 1157 n.table = this.table; 1158 n.mappings = this.mappings; 1159 1160 foreach(k, v; this.fields) 1161 if(k.indexOf("id_from_") == -1) 1162 n.fields[k] = v; 1163 else 1164 n.fields[k] = null; // don't copy ids 1165 1166 n.mode = UpdateOrInsertMode.AlwaysInsert; 1167 1168 return n; 1169 } 1170 1171 Database db; 1172 UpdateOrInsertMode mode; 1173 } 1174 1175 /** 1176 You can subclass DataObject if you want to 1177 get some compile time checks or better types. 1178 1179 You'll want to disable opDispatch, then forward your 1180 properties to the super opDispatch. 1181 */ 1182 1183 /*mixin*/ string DataObjectField(T, string table, string column, string aliasAs = null)() { 1184 string aliasAs_; 1185 if(aliasAs is null) 1186 aliasAs_ = column; 1187 else 1188 aliasAs_ = aliasAs; 1189 return ` 1190 @property void `~aliasAs_~`(`~T.stringof~` setTo) { 1191 super.set("`~table~`", "`~column~`", to!string(setTo)); 1192 } 1193 1194 @property `~T.stringof~` `~aliasAs_~` () { 1195 return to!(`~T.stringof~`)(super.select("`~table~`", "`~column~`")); 1196 } 1197 `; 1198 } 1199 1200 mixin template StrictDataObject() { 1201 // disable opdispatch 1202 string opDispatch(string name)(...) if (0) {} 1203 } 1204 1205 1206 string createDataObjectFieldsFromAlias(string table, fieldsToUse)() { 1207 string ret; 1208 1209 fieldsToUse f; 1210 foreach(member; __traits(allMembers, fieldsToUse)) { 1211 ret ~= DataObjectField!(typeof(__traits(getMember, f, member)), table, member); 1212 } 1213 1214 return ret; 1215 } 1216 1217 1218 /** 1219 This creates an editable data object out of a simple struct. 1220 1221 struct MyFields { 1222 int id; 1223 string name; 1224 } 1225 1226 alias SimpleDataObject!("my_table", MyFields) User; 1227 1228 1229 User a = new User(db); 1230 1231 a.id = 30; 1232 a.name = "hello"; 1233 a.commitChanges(); // tries an update or insert on the my_table table 1234 1235 1236 Unlike the base DataObject class, this template provides compile time 1237 checking for types and names, based on the struct you pass in: 1238 1239 a.id = "aa"; // compile error 1240 1241 a.notAField; // compile error 1242 */ 1243 class SimpleDataObject(string tableToUse, fieldsToUse) : DataObject { 1244 mixin StrictDataObject!(); 1245 1246 mixin(createDataObjectFieldsFromAlias!(tableToUse, fieldsToUse)()); 1247 1248 this(Database db) { 1249 super(db, tableToUse); 1250 } 1251 } 1252 1253 /** 1254 Given some SQL, it finds the CREATE TABLE 1255 instruction for the given tableName. 1256 (this is so it can find one entry from 1257 a file with several SQL commands. But it 1258 may break on a complex file, so try to only 1259 feed it simple sql files.) 1260 1261 From that, it pulls out the members to create a 1262 simple struct based on it. 1263 1264 It's not terribly smart, so it will probably 1265 break on complex tables. 1266 1267 Data types handled: 1268 1269 ``` 1270 INTEGER, SMALLINT, MEDIUMINT -> D's int 1271 TINYINT -> D's bool 1272 BIGINT -> D's long 1273 TEXT, VARCHAR -> D's string 1274 FLOAT, DOUBLE -> D's double 1275 ``` 1276 1277 It also reads DEFAULT values to pass to D, except for NULL. 1278 It ignores any length restrictions. 1279 1280 Bugs: 1281 $(LIST 1282 * Skips all constraints 1283 * Doesn't handle nullable fields, except with strings 1284 * It only handles SQL keywords if they are all caps 1285 ) 1286 1287 This, when combined with SimpleDataObject!(), 1288 can automatically create usable D classes from 1289 SQL input. 1290 */ 1291 struct StructFromCreateTable(string sql, string tableName) { 1292 mixin(getCreateTable(sql, tableName)); 1293 } 1294 1295 string getCreateTable(string sql, string tableName) { 1296 skip: 1297 while(readWord(sql) != "CREATE") {} 1298 1299 assert(readWord(sql) == "TABLE"); 1300 1301 if(readWord(sql) != tableName) 1302 goto skip; 1303 1304 assert(readWord(sql) == "("); 1305 1306 int state; 1307 int parens; 1308 1309 struct Field { 1310 string name; 1311 string type; 1312 string defaultValue; 1313 } 1314 Field*[] fields; 1315 1316 string word = readWord(sql); 1317 Field* current = new Field(); // well, this is interesting... under new DMD, not using new breaks it in CTFE because it overwrites the one entry! 1318 while(word != ")" || parens) { 1319 if(word == ")") { 1320 parens --; 1321 word = readWord(sql); 1322 continue; 1323 } 1324 if(word == "(") { 1325 parens ++; 1326 word = readWord(sql); 1327 continue; 1328 } 1329 switch(state) { 1330 default: assert(0); 1331 case 0: 1332 if(word[0] >= 'A' && word[0] <= 'Z') { 1333 state = 4; 1334 break; // we want to skip this since it starts with a keyword (we hope) 1335 } 1336 current.name = word; 1337 state = 1; 1338 break; 1339 case 1: 1340 current.type ~= word; 1341 state = 2; 1342 break; 1343 case 2: 1344 if(word == "DEFAULT") 1345 state = 3; 1346 else if (word == ",") { 1347 fields ~= current; 1348 current = new Field(); 1349 state = 0; // next 1350 } 1351 break; 1352 case 3: 1353 current.defaultValue = word; 1354 state = 2; // back to skipping 1355 break; 1356 case 4: 1357 if(word == ",") 1358 state = 0; 1359 } 1360 1361 word = readWord(sql); 1362 } 1363 1364 if(current.name !is null) 1365 fields ~= current; 1366 1367 1368 string structCode; 1369 foreach(field; fields) { 1370 structCode ~= "\t"; 1371 1372 switch(field.type) { 1373 case "INTEGER": 1374 case "SMALLINT": 1375 case "MEDIUMINT": 1376 case "SERIAL": // added Oct 23, 2021 1377 structCode ~= "int"; 1378 break; 1379 case "BOOLEAN": 1380 case "TINYINT": 1381 structCode ~= "bool"; 1382 break; 1383 case "BIGINT": 1384 structCode ~= "long"; 1385 break; 1386 case "CHAR": 1387 case "char": 1388 case "VARCHAR": 1389 case "varchar": 1390 case "TEXT": 1391 case "text": 1392 case "TIMESTAMPTZ": // added Oct 23, 2021 1393 structCode ~= "string"; 1394 break; 1395 case "FLOAT": 1396 case "DOUBLE": 1397 structCode ~= "double"; 1398 break; 1399 default: 1400 assert(0, "unknown type " ~ field.type ~ " for " ~ field.name); 1401 } 1402 1403 structCode ~= " "; 1404 structCode ~= field.name; 1405 1406 if(field.defaultValue !is null) { 1407 structCode ~= " = " ~ field.defaultValue; 1408 } 1409 1410 structCode ~= ";\n"; 1411 } 1412 1413 return structCode; 1414 } 1415 1416 string readWord(ref string src) { 1417 reset: 1418 while(src[0] == ' ' || src[0] == '\t' || src[0] == '\n') 1419 src = src[1..$]; 1420 if(src.length >= 2 && src[0] == '-' && src[1] == '-') { // a comment, skip it 1421 while(src[0] != '\n') 1422 src = src[1..$]; 1423 goto reset; 1424 } 1425 1426 int start, pos; 1427 if(src[0] == '`') { 1428 src = src[1..$]; 1429 while(src[pos] != '`') 1430 pos++; 1431 goto gotit; 1432 } 1433 1434 1435 while( 1436 (src[pos] >= 'A' && src[pos] <= 'Z') 1437 || 1438 (src[pos] >= 'a' && src[pos] <= 'z') 1439 || 1440 (src[pos] >= '0' && src[pos] <= '9') 1441 || 1442 src[pos] == '_' 1443 ) 1444 pos++; 1445 gotit: 1446 if(pos == 0) 1447 pos = 1; 1448 1449 string tmp = src[0..pos]; 1450 1451 if(src[pos] == '`') 1452 pos++; // skip the ending quote; 1453 1454 src = src[pos..$]; 1455 1456 return tmp; 1457 } 1458 1459 /// Combines StructFromCreateTable and SimpleDataObject into a one-stop template. 1460 /// alias DataObjectFromSqlCreateTable(import("file.sql"), "my_table") MyTable; 1461 template DataObjectFromSqlCreateTable(string sql, string tableName) { 1462 alias SimpleDataObject!(tableName, StructFromCreateTable!(sql, tableName)) DataObjectFromSqlCreateTable; 1463 } 1464 1465 /+ 1466 class MyDataObject : DataObject { 1467 this() { 1468 super(new Database("localhost", "root", "pass", "social"), null); 1469 } 1470 1471 mixin StrictDataObject!(); 1472 1473 mixin(DataObjectField!(int, "users", "id")); 1474 } 1475 1476 void main() { 1477 auto a = new MyDataObject; 1478 1479 a.fields["id"] = "10"; 1480 1481 a.id = 34; 1482 1483 a.commitChanges; 1484 } 1485 +/ 1486 1487 /* 1488 alias DataObjectFromSqlCreateTable!(import("db.sql"), "users") Test; 1489 1490 void main() { 1491 auto a = new Test(null); 1492 1493 a.cool = "way"; 1494 a.value = 100; 1495 } 1496 */ 1497 1498 void typeinfoBugWorkaround() { 1499 assert(0, to!string(typeid(immutable(char[])[immutable(char)[]]))); 1500 } 1501 1502 mixin template DatabaseOperations(string table) { 1503 DataObject getAsDb(Database db) { 1504 return objectToDataObject!(typeof(this))(this, db, table); 1505 } 1506 1507 static typeof(this) fromRow(Row row) { 1508 return rowToObject!(typeof(this))(row); 1509 } 1510 1511 static typeof(this) fromId(Database db, long id) { 1512 auto query = new SelectBuilder(db); 1513 query.table = table; 1514 query.fields ~= "*"; 1515 query.wheres ~= "id = ?0"; 1516 auto res = db.query(query.toString(), id); 1517 if(res.empty) 1518 throw new Exception("no such row"); 1519 return fromRow(res.front); 1520 } 1521 1522 } 1523 1524 string toDbName(string s) { 1525 import std.string; 1526 return s.toLower ~ "s"; 1527 } 1528 1529 /++ 1530 Easy interop with [arsd.cgi] serveRestObject classes. 1531 1532 History: 1533 Added October 31, 2021. 1534 1535 Warning: not stable/supported at this time. 1536 +/ 1537 mixin template DatabaseRestObject(alias getDb) { 1538 override void save() { 1539 this.id = this.saveToDatabase(getDb()); 1540 } 1541 1542 override void load(string urlId) { 1543 import std.conv; 1544 this.id = to!int(urlId); 1545 this.loadFromDatabase(getDb()); 1546 } 1547 } 1548 1549 void loadFromDatabase(T)(T t, Database database, string tableName = toDbName(__traits(identifier, T))) { 1550 static assert(is(T == class), "structs wont work for this function, try rowToObject instead for now and complain to me adding struct support is easy enough"); 1551 auto query = new SelectBuilder(database); 1552 query.table = tableName; 1553 query.fields ~= "*"; 1554 query.wheres ~= "id = ?0"; 1555 auto res = database.query(query.toString(), t.id); 1556 if(res.empty) 1557 throw new Exception("no such row"); 1558 1559 rowToObject(res.front, t); 1560 } 1561 1562 auto saveToDatabase(T)(T t, Database database, string tableName = toDbName(__traits(identifier, T))) { 1563 DataObject obj = objectToDataObject(t, database, tableName, t.id ? UpdateOrInsertMode.AlwaysUpdate : UpdateOrInsertMode.AlwaysInsert); 1564 if(!t.id) { 1565 import std.random; // omg i hate htis 1566 obj.id = uniform(2, int.max); 1567 } 1568 obj.commitChanges; 1569 return t.id; 1570 } 1571 1572 /+ + 1573 auto builder = UpdateBuilder("rooms"); 1574 builder.player_one_selection = challenge; 1575 builder.execute(db, id); 1576 +/ 1577 private struct UpdateBuilder { 1578 this(T)(string table, T id) { 1579 this.table = table; 1580 import std.conv; 1581 this.id = to!string(id); 1582 } 1583 1584 } 1585 1586 import std.traits, std.datetime; 1587 enum DbSave; 1588 enum DbNullable; 1589 alias AliasHelper(alias T) = T; 1590 1591 T rowToObject(T)(Row row) { 1592 T t; 1593 static if(is(T == class)) 1594 t = new T(); 1595 rowToObject(row, t); 1596 return t; 1597 } 1598 1599 void rowToObject(T)(Row row, ref T t) { 1600 import arsd.dom, arsd.cgi; 1601 1602 foreach(memberName; __traits(allMembers, T)) { 1603 alias member = AliasHelper!(__traits(getMember, t, memberName)); 1604 foreach(attr; __traits(getAttributes, member)) { 1605 static if(is(attr == DbSave)) { 1606 static if(is(typeof(member) == enum)) 1607 __traits(getMember, t, memberName) = cast(typeof(member)) to!int(row[memberName]); 1608 else static if(is(typeof(member) == bool)) { 1609 __traits(getMember, t, memberName) = row[memberName][0] == 't'; 1610 } else static if(is(typeof(member) == Html)) { 1611 __traits(getMember, t, memberName).source = row[memberName]; 1612 } else static if(is(typeof(member) == DateTime)) 1613 __traits(getMember, t, memberName) = cast(DateTime) dTimeToSysTime(to!long(row[memberName])); 1614 else { 1615 if(row[memberName].length) 1616 __traits(getMember, t, memberName) = to!(typeof(member))(row[memberName]); 1617 // otherwise, we'll leave it as .init - most likely null 1618 } 1619 } 1620 } 1621 } 1622 } 1623 1624 DataObject objectToDataObject(T)(T t, Database db, string table, UpdateOrInsertMode mode = UpdateOrInsertMode.CheckForMe) { 1625 import arsd.dom, arsd.cgi; 1626 1627 DataObject obj = new DataObject(db, table, mode); 1628 foreach(memberName; __traits(allMembers, T)) { 1629 alias member = AliasHelper!(__traits(getMember, t, memberName)); 1630 foreach(attr; __traits(getAttributes, member)) { 1631 static if(is(attr == DbSave)) { 1632 static if(is(typeof(member) == enum)) 1633 obj.opDispatch!memberName(cast(int) __traits(getMember, t, memberName)); 1634 else static if(is(typeof(member) == Html)) { 1635 obj.opDispatch!memberName(__traits(getMember, t, memberName).source); 1636 } else static if(is(typeof(member) == DateTime)) 1637 obj.opDispatch!memberName(dateTimeToDTime(__traits(getMember, t, memberName))); 1638 else { 1639 bool done; 1640 foreach(attr2; __traits(getAttributes, member)) { 1641 static if(is(attr2 == DbNullable)) { 1642 if(__traits(getMember, t, memberName) == 0) 1643 done = true; 1644 } 1645 } 1646 1647 if(!done) { 1648 static if(memberName == "id") { 1649 if(__traits(getMember, t, memberName)) { 1650 // maybe i shouldn't actually set the id but idk 1651 obj.opDispatch!memberName(__traits(getMember, t, memberName)); 1652 } else { 1653 // it is null, let the system do something about it like auto increment 1654 1655 } 1656 } else 1657 obj.opDispatch!memberName(__traits(getMember, t, memberName)); 1658 } 1659 } 1660 } 1661 } 1662 } 1663 return obj; 1664 } 1665 1666 1667 1668 void fillData(T)(string delegate(string, string) setter, T obj, string name) { 1669 fillData( (k, v) { setter(k, v); }, obj, name); 1670 } 1671 1672 void fillData(T)(void delegate(string, string) setter, T obj, string name) { 1673 import arsd.dom, arsd.cgi; 1674 1675 import std.traits; 1676 static if(!isSomeString!T && isArray!T) { 1677 // FIXME: indexing 1678 foreach(o; obj) 1679 fillData(setter, o, name); 1680 } else static if(is(T == DateTime)) { 1681 fillData(setter, obj.toISOExtString(), name); 1682 } else static if(is(T == Html)) { 1683 fillData(setter, obj.source, name); 1684 } else static if(is(T == struct)) { 1685 foreach(idx, memberName; __traits(allMembers, T)) { 1686 alias member = AliasHelper!(__traits(getMember, obj, memberName)); 1687 static if(!is(typeof(member) == function)) 1688 fillData(setter, __traits(getMember, obj, memberName), name ~ "." ~ memberName); 1689 else static if(is(typeof(member) == function)) { 1690 static if(functionAttributes!member & FunctionAttribute.property) { 1691 fillData(setter, __traits(getMember, obj, memberName)(), name ~ "." ~ memberName); 1692 } 1693 } 1694 } 1695 } else { 1696 auto value = to!string(obj); 1697 setter(name, value); 1698 } 1699 } 1700 1701 struct varchar(size_t max) { 1702 private string payload; 1703 1704 this(string s, string file = __FILE__, size_t line = __LINE__) { 1705 opAssign(s, file, line); 1706 } 1707 1708 typeof(this) opAssign(string s, string file = __FILE__, size_t line = __LINE__) { 1709 if(s.length > max) 1710 throw new Exception(s ~ " :: too long", file, line); 1711 payload = s; 1712 1713 return this; 1714 } 1715 1716 string asString() { 1717 return payload; 1718 1719 } 1720 alias asString this; 1721 } 1722 1723 version (unittest) 1724 { 1725 /// Unittest utility that returns a predefined set of values 1726 package (arsd) final class PredefinedResultSet : ResultSet 1727 { 1728 string[] fields; 1729 Row[] rows; 1730 size_t current; 1731 1732 this(string[] fields, Row[] rows) 1733 { 1734 this.fields = fields; 1735 this.rows = rows; 1736 foreach (ref row; rows) 1737 row.resultSet = this; 1738 } 1739 1740 int getFieldIndex(const string field) const 1741 { 1742 foreach (const idx, const val; fields) 1743 if (val == field) 1744 return cast(int) idx; 1745 1746 assert(false, "No field with name: " ~ field); 1747 } 1748 1749 string[] fieldNames() 1750 { 1751 return fields; 1752 } 1753 1754 @property bool empty() const 1755 { 1756 return current == rows.length; 1757 } 1758 1759 Row front() @property 1760 { 1761 assert(!empty); 1762 return rows[current]; 1763 } 1764 1765 void popFront() 1766 { 1767 assert(!empty); 1768 current++; 1769 } 1770 1771 size_t length() @property 1772 { 1773 return rows.length - current; 1774 } 1775 } 1776 }