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