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 83 /// Just executes a query. It supports placeholders for parameters 84 final ResultSet query(T...)(string sql, T t) { 85 Variant[] args; 86 foreach(arg; t) { 87 Variant a; 88 static if(__traits(compiles, a = arg)) 89 a = arg; 90 else 91 a = to!string(t); 92 args ~= a; 93 } 94 return queryImpl(sql, args); 95 } 96 97 final ResultSet query(Args...)(arsd.core.InterpolationHeader header, Args args, arsd.core.InterpolationFooter footer) { 98 return queryImpl(sqlFromInterpolatedArgs!Args, variantsFromInterpolatedArgs(args)); 99 } 100 101 final void withTransaction(scope void delegate() dg) { 102 this.startTransaction(); 103 scope(success) 104 this.query("COMMIT"); 105 scope(failure) 106 this.query("ROLLBACK"); 107 dg(); 108 } 109 110 /// query to start a transaction, only here because sqlite is apparently different in syntax... 111 void startTransaction(); 112 113 /// Actually implements the query for the database. The query() method 114 /// below might be easier to use. 115 ResultSet queryImpl(string sql, Variant[] args...); 116 117 /// Escapes data for inclusion into an sql string literal 118 string escape(string sqlData); 119 /// Escapes binary data for inclusion into a sql string. Note that unlike `escape`, the returned string here SHOULD include the quotes. 120 string escapeBinaryString(const(ubyte)[] sqlData); 121 /// 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 122 string sysTimeToValue(SysTime); 123 // see test/dbis.d 124 125 /++ 126 Return true if the connection appears to be alive 127 128 History: 129 Added October 30, 2025 130 +/ 131 bool isAlive(); 132 133 /// Prepared statement api 134 /* 135 PreparedStatement prepareStatement(string sql, int numberOfArguments); 136 137 */ 138 } 139 140 // Added Oct 26, 2021 141 Row queryOneRow(string file = __FILE__, size_t line = __LINE__, T...)(Database db, string sql, T t) { 142 auto res = db.query(sql, t); 143 import arsd.core; 144 if(res.empty) 145 throw ArsdException!("no row in result")(sql, t, file, line); 146 auto row = res.front; 147 return row; 148 } 149 150 Ret queryOneColumn(Ret, string file = __FILE__, size_t line = __LINE__, T...)(Database db, string sql, T t) { 151 auto row = queryOneRow(db, sql, t); 152 return to!Ret(row[0]); 153 } 154 155 struct Query { 156 ResultSet result; 157 this(T...)(Database db, string sql, T t) if(T.length!=1 || !is(T[0]==Variant[])) { 158 result = db.query(sql, t); 159 } 160 // Version for dynamic generation of args: (Needs to be a template for coexistence with other constructor. 161 this(T...)(Database db, string sql, T args) if (T.length==1 && is(T[0] == Variant[])) { 162 result = db.queryImpl(sql, args); 163 } 164 165 int opApply(T)(T dg) if(is(T == delegate)) { 166 import std.traits; 167 foreach(row; result) { 168 ParameterTypeTuple!dg tuple; 169 170 foreach(i, item; tuple) { 171 tuple[i] = to!(typeof(item))(row[i]); 172 } 173 174 if(auto result = dg(tuple)) 175 return result; 176 } 177 178 return 0; 179 } 180 } 181 182 /++ 183 Represents a single item in a result. A row is a set of these `DatabaseDatum`s. 184 185 History: 186 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. 187 +/ 188 struct DatabaseDatum { 189 int platformSpecificTag; 190 LimitedVariant storage; 191 192 /++ 193 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. 194 +/ 195 package this(string s) { 196 storage = s; 197 } 198 199 /++ 200 Returns `true` if the item was `NULL` in the database. 201 +/ 202 bool isNull() { 203 return storage.contains == LimitedVariant.Contains.null_; 204 } 205 206 /++ 207 Converts the datum to a string in a format specified by the database. 208 +/ 209 string toString() { 210 if(isNull()) 211 return null; 212 213 return storage.toString(); 214 } 215 /++ 216 For compatibility with earlier versions of the api, all data can easily convert to string implicitly and opCast keeps to!x(this) working. 217 218 The toArsdJsVar one is in particular subject to change. 219 +/ 220 alias toString this; 221 222 /// ditto 223 T opCast(T)() { 224 import std.conv; 225 return to!T(this.toString); 226 } 227 228 /// ditto 229 string toArsdJsVar() { return this.toString; } 230 231 /++ 232 Explicit indicator that you want a NULL value for the database. 233 234 History: 235 Added December 8, 2025 236 +/ 237 static DatabaseDatum NULL() { 238 return DatabaseDatum(); 239 } 240 } 241 242 unittest { 243 // tbh this is more of a phobos test but rvaluerefparam has messed it up before 244 auto db = DatabaseDatum("1234567"); 245 assert(to!int(db) == 1234567); 246 assert(to!long(db) == 1234567); 247 assert(to!int(DatabaseDatum("1234567")) == 1234567); 248 assert(to!long(DatabaseDatum("1234567")) == 1234567); 249 250 assert(DatabaseDatum.NULL.isNull()); 251 } 252 253 /++ 254 A row in a result set from a query. 255 256 You can access this as either an array or associative array: 257 258 --- 259 foreach(Row row; db.query("SELECT id, name FROM mytable")) { 260 // can access by index or by name 261 row[0] == row["id"]; 262 row[1] == row["name"]; 263 264 // can also iterate over the results 265 foreach(name, data; row) { 266 // will send name = "id", data = the thing 267 // and then next loop will be name = "name", data = the thing 268 } 269 } 270 --- 271 +/ 272 struct Row { 273 package DatabaseDatum[] row; 274 package ResultSet resultSet; 275 276 /++ 277 Allows for access by index or column name. 278 +/ 279 DatabaseDatum opIndex(size_t idx, string file = __FILE__, int line = __LINE__) { 280 if(idx >= row.length) 281 throw new Exception(text("index ", idx, " is out of bounds on result"), file, line); 282 return row[idx]; 283 } 284 285 /// ditto 286 DatabaseDatum opIndex(string name, string file = __FILE__, int line = __LINE__) { 287 auto idx = resultSet.getFieldIndex(name); 288 if(idx >= row.length) 289 throw new Exception(text("no field ", name, " in result"), file, line); 290 return row[idx]; 291 } 292 293 /++ 294 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`. 295 +/ 296 string toString() { 297 return to!string(row); 298 } 299 300 /++ 301 Allows iteration over the columns with the `foreach` statement. 302 303 History: 304 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. 305 306 The overload with one argument, having just the datum without the name, was also added on June 11, 2023 (dub v11.0). 307 +/ 308 int opApply(int delegate(string, DatabaseDatum) dg) { 309 string[] fn = resultSet.fieldNames(); 310 foreach(idx, item; row) 311 mixin(yield("fn[idx], item")); 312 313 return 0; 314 } 315 316 /// ditto 317 int opApply(int delegate(DatabaseDatum) dg) { 318 foreach(item; row) 319 mixin(yield("item")); 320 return 0; 321 } 322 323 /++ 324 Hacky conversion to simpler types. 325 326 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. 327 328 I may formally deprecate them in a future release. 329 +/ 330 string[] toStringArray() { 331 string[] row; 332 foreach(item; this.row) 333 row ~= item; 334 return row; 335 } 336 337 /// ditto 338 string[string] toAA() { 339 string[string] a; 340 341 string[] fn = resultSet.fieldNames(); 342 343 foreach(i, r; row) 344 a[fn[i]] = r; 345 346 return a; 347 } 348 349 } 350 import std.conv; 351 352 interface ResultSet { 353 // name for associative array to result index 354 int getFieldIndex(string field); 355 string[] fieldNames(); 356 357 // this is a range that can offer other ranges to access it 358 bool empty() @property; 359 Row front() @property; 360 void popFront() ; 361 size_t length() @property; 362 363 /* deprecated */ final ResultSet byAssoc() { return this; } 364 } 365 366 /++ 367 Converts a database result set to a html table, using [arsd.dom]. 368 369 History: 370 Added October 29, 2025 371 +/ 372 auto resultSetToHtmlTable()(ResultSet resultSet) { 373 import arsd.dom; 374 375 Table table = cast(Table) Element.make("table"); 376 table.appendHeaderRow(resultSet.fieldNames); 377 foreach(row; resultSet) { 378 table.appendRow(row.toStringArray()); 379 } 380 381 return table; 382 } 383 384 abstract class ConnectionPoolBase : arsd.core.SynchronizableObject { 385 protected struct DatabaseListItem { 386 Database db; 387 DatabaseListItem* nextAvailable; 388 } 389 390 private DatabaseListItem* firstAvailable; 391 392 // FIXME: add a connection count limit and some kind of wait mechanism for one to become available 393 394 final protected void makeAvailable(DatabaseListItem* what) { 395 synchronized(this) { 396 auto keep = this.firstAvailable; 397 what.nextAvailable = keep; 398 this.firstAvailable = what; 399 } 400 } 401 402 final protected DatabaseListItem* getNext() { 403 DatabaseListItem* toUse; 404 synchronized(this) { 405 if(this.firstAvailable !is null) { 406 toUse = this.firstAvailable; 407 this.firstAvailable = this.firstAvailable.nextAvailable; 408 } 409 } 410 411 return toUse; 412 } 413 } 414 415 /++ 416 PooledConnection is an RAII holder for a database connection that is automatically recycled to the pool it came from (unless you [discard] it). 417 418 History: 419 Added October 29, 2025 420 +/ 421 struct PooledConnection(ConnectionPoolType) { 422 private ConnectionPoolType.DatabaseListItem* dli; 423 private ConnectionPoolType pool; 424 private bool discarded; 425 private this(ConnectionPoolType.DatabaseListItem* dli, ConnectionPoolType pool) { 426 this.dli = dli; 427 this.pool = pool; 428 } 429 430 @disable this(this); 431 432 /++ 433 Indicates you want the connection discarded instead of returned to the pool when you're finished with it. 434 435 You should call this if you know the connection is dead. 436 +/ 437 void discard() { 438 this.discarded = true; 439 } 440 441 ~this() { 442 import core.memory; 443 if(GC.inFinalizer) 444 return; 445 if(!discarded && dli.db.isAlive) { 446 // FIXME: a connection must not be returned to the pool unless it is both alive and idle; any pending query work would screw up the next user 447 // it is the user's responsibility to live with other state though like prepared statements or whatever saved per-connection. 448 pool.makeAvailable(dli); 449 } 450 } 451 452 /++ 453 454 +/ 455 ConnectionPoolType.DriverType borrow() @system return { 456 return cast(ConnectionPoolType.DriverType) dli.db; // static_cast 457 } 458 459 /++ 460 +/ 461 ResultSet rtQuery(T...)(T t) { 462 return dli.db.query(t); 463 } 464 465 /++ 466 467 +/ 468 template query(string file = __FILE__, size_t line = __LINE__, Args...) { 469 enum asSql = sqlFromInterpolatedArgs!(Args); 470 __gshared queryMetadata = new QueryMetadata!(asSql, file, line); 471 @(arsd.core.standalone) @system shared static this() { 472 ConnectionPoolType.registeredQueries_ ~= queryMetadata; 473 } 474 475 auto query(arsd.core.InterpolationHeader ihead, Args args, arsd.core.InterpolationFooter ifoot) { 476 return new QueryResult!queryMetadata(dli.db.queryImpl(asSql, variantsFromInterpolatedArgs(args))); 477 } 478 } 479 } 480 481 /++ 482 483 +/ 484 unittest { 485 import arsd.database; 486 487 shared dbPool = new shared ConnectionPool!(() => new MockDatabase())(); 488 489 void main() { 490 auto db = dbPool.get(); 491 foreach(row; db.query(i"SELECT * FROM test")) { 492 if(row.id.isNull) 493 continue; 494 auto id = row.id.get!int; 495 496 } 497 498 } 499 500 main(); // remove from docs 501 } 502 503 private Variant[] variantsFromInterpolatedArgs(Args...)(Args args) { 504 Variant[] ret; 505 506 import arsd.core; 507 508 foreach(arg; args) { 509 static if(is(typeof(arg) == InterpolationHeader)) 510 {} 511 else 512 static if(is(typeof(arg) == InterpolationFooter)) 513 {} 514 else 515 static if(is(typeof(arg) == InterpolatedLiteral!sql, string sql)) 516 {} 517 else 518 static if(is(typeof(arg) == InterpolatedExpression!code, string code)) 519 {} 520 else 521 static if(is(typeof(arg) == AdHocBuiltStruct!(tag, names, Values), string tag, string[] names, Values...)) { 522 static if(tag == "VALUES") { 523 foreach(value; arg.values) { 524 static if(is(value == sql_!code, string code)) { 525 // intentionally blank 526 } else { 527 ret ~= Variant(value); 528 } 529 } 530 531 } else static assert(0); 532 } 533 // FIXME: iraw and sql!"" too and VALUES 534 else 535 ret ~= Variant(arg); 536 } 537 538 return ret; 539 } 540 541 private string sqlFromInterpolatedArgs(Args...)() { 542 string ret; 543 544 import arsd.core; 545 546 foreach(arg; Args) { 547 static if(is(arg == InterpolationHeader)) 548 {} 549 else 550 static if(is(arg == InterpolationFooter)) 551 {} 552 else 553 static if(is(arg == InterpolatedLiteral!sql, string sql)) 554 ret ~= sql; 555 else 556 static if(is(arg == InterpolatedExpression!code, string code)) 557 {} 558 else 559 static if(is(arg == AdHocBuiltStruct!(tag, names, values), string tag, string[] names, values...)) { 560 static if(tag == "VALUES") { 561 ret ~= "("; 562 foreach(idx, name; names) { 563 if(idx) ret ~= ", "; 564 ret ~= name; 565 } 566 ret ~= ") VALUES ("; 567 foreach(idx, value; values) { 568 if(idx) ret ~= ", "; 569 570 static if(is(value == sql_!code, string code)) { 571 ret ~= code; 572 } else { 573 ret ~= "?"; 574 } 575 } 576 ret ~= ")"; 577 } 578 else static assert(0); 579 } 580 // FIXME: iraw and sql_!"" too 581 else 582 ret ~= "?"; 583 } 584 585 return ret; 586 587 } 588 589 /+ 590 +/ 591 592 struct AssociatedDatabaseDatum(alias queryMetadata, string name, string file, size_t line) { 593 @(arsd.core.standalone) @system shared static this() { 594 queryMetadata.registerName(name, file, line); 595 } 596 597 template get(T, string file = __FILE__, size_t line = __LINE__) { 598 shared static this() { 599 // FIXME: empty string and null must be distinguishable in arsd.core 600 static if(is(T == string)) 601 T t = "sample"; 602 else 603 T t = T.init; 604 queryMetadata.registerType(name, LimitedVariant(t), T.stringof, file, line); 605 } 606 607 T get() { 608 import std.conv; 609 return datum.toString().to!T; 610 } 611 } 612 613 DatabaseDatum datum; 614 615 bool isNull() { 616 return datum.isNull(); 617 } 618 619 string toString() { 620 if(isNull) 621 return null; 622 else 623 return datum.toString(); 624 } 625 626 alias toString this; 627 } 628 629 private abstract class QueryResultBase { 630 631 } 632 633 class QueryResult(alias queryMetadata) : QueryResultBase { 634 private ResultSet resultSet; 635 636 this(ResultSet resultSet) { 637 this.resultSet = resultSet; 638 } 639 640 QueryResultRow!queryMetadata front() { 641 return new QueryResultRow!queryMetadata(resultSet.front); 642 } 643 644 bool empty() { 645 return resultSet.empty; 646 } 647 648 void popFront() { 649 resultSet.popFront(); 650 } 651 } 652 653 class QueryResultRow(alias queryMetadata) { 654 Row row; 655 this(Row row) { 656 this.row = row; 657 } 658 659 AssociatedDatabaseDatum!(queryMetadata, name, file, line) opDispatch(string name, string file = __FILE__, size_t line = __LINE__)() if(name != "__dtor") { 660 return typeof(return)(row[name]); 661 } 662 663 // i could support an opSlice. maybe opIndex tho it won't be CT bound checked w/o a ct!0 thing 664 // also want opApply which discards type check prolly and just gives you the datum. 665 666 int opApply(int delegate(string, DatabaseDatum) dg) { 667 string[] fn = row.resultSet.fieldNames(); 668 foreach(idx, item; row.row) 669 mixin(yield("fn[idx], item")); 670 671 return 0; 672 } 673 674 /// ditto 675 int opApply(int delegate(DatabaseDatum) dg) { 676 foreach(item; row.row) 677 mixin(yield("item")); 678 return 0; 679 } 680 } 681 682 struct ReferencedColumn { 683 string name; 684 LimitedVariant sampleData; 685 string assumedType; 686 string actualType; 687 string file; 688 size_t line; 689 } 690 691 class QueryMetadataBase { 692 ReferencedColumn[] names; 693 void registerName(string name, string file, size_t line) { 694 names ~= ReferencedColumn(name, LimitedVariant.init, null, null, file, line); 695 } 696 void registerType(string name, LimitedVariant sample, string type, string file, size_t line) { 697 foreach(ref n; names) 698 if(n.name == name) { 699 if(n.assumedType.length && type.length) { 700 n.actualType = type; 701 } 702 n.assumedType = type; 703 n.sampleData = sample; 704 n.file = file; 705 n.line = line; 706 return; 707 } 708 names ~= ReferencedColumn(name, sample, type, type, file, line); 709 } 710 711 abstract string sql() const; 712 abstract string file() const; 713 abstract size_t line() const; 714 } 715 716 class QueryMetadata(string q, string file_, size_t line_) : QueryMetadataBase { 717 override string sql() const { return q; } 718 override string file() const { return file_; } 719 override size_t line() const { return line_; } 720 } 721 722 version(unittest) 723 class MockDatabase : Database { 724 void startTransaction() {} 725 string sysTimeToValue(SysTime s) { return null; } 726 bool isAlive() { return true; } 727 728 ResultSet queryImpl(string sql, Variant[] args...) { 729 return new PredefinedResultSet(null, null); 730 } 731 string escape(string sqlData) { 732 return null; 733 } 734 string escapeBinaryString(const(ubyte)[] sqlData) { 735 return null; 736 } 737 } 738 739 /++ 740 Helpers for interpolated queries. 741 742 History: 743 Added October 31, 2025 744 745 See_Also: 746 [arsd.core.iraw] 747 +/ 748 auto VALUES() { 749 import arsd.core; 750 return AdHocBuiltStruct!"VALUES"(); 751 } 752 753 /// ditto 754 auto sql(string s)() { 755 return sql_!s(); 756 } 757 758 private struct sql_(string s) { } 759 760 /++ 761 A ConnectionPool manages a set of shared connections to a database. 762 763 764 Create one like this: 765 766 --- 767 // at top level 768 shared dbPool = new shared ConnectionPool!(() => new PostgreSql("dbname=me"))(); 769 770 void main() { 771 auto db = dbPool.get(); // in the function, get it and use it temporarily 772 } 773 --- 774 775 History: 776 Added October 29, 2025 777 +/ 778 class ConnectionPool(alias connectionFactory) : ConnectionPoolBase { 779 private alias unsharedThis = ConnectionPool!connectionFactory; 780 781 static if(is(typeof(connectionFactory) DriverType == return)) { 782 static if(!is(DriverType : Database)) 783 static assert(0, "unusable connectionFactory - it needs to return an instance of Database"); 784 } else { 785 static assert(0, "unusable connectionFactory - it needs to be a function"); 786 } 787 788 private __gshared QueryMetadataBase[] registeredQueries_; 789 790 immutable(QueryMetadataBase[]) registeredQueries() shared { 791 return cast(immutable(QueryMetadataBase[])) registeredQueries_; 792 } 793 794 bool checkQueries()(DriverType db) shared { 795 bool succeeded = true; 796 797 import arsd.postgres; // FIXME is this really postgres only? looks like sqlite has no similar function... maybe make a view then sqlite3_table_column_metadata ? 798 static assert(is(DriverType == PostgreSql), "Only implemented for postgres right now"); 799 800 int count; 801 import arsd.core; 802 import arsd.conv; 803 foreach(q; registeredQueries) { 804 //writeln(q.file, ":", q.line, " ", q.sql); 805 try { 806 try { 807 string dbSpecificSql; 808 int placeholderNumber = 1; 809 size_t lastCopied = 0; 810 foreach(idx, ch; q.sql) { 811 if(ch == '?') { 812 dbSpecificSql ~= q.sql[lastCopied .. idx]; 813 lastCopied = idx + 1; 814 dbSpecificSql ~= "$" ~ to!string(placeholderNumber); 815 placeholderNumber++; 816 } 817 } 818 dbSpecificSql ~= q.sql[lastCopied .. $]; 819 // FIXME: pipeline this 820 db.query("PREPARE thing_"~to!string(++count)~" AS " ~ dbSpecificSql); 821 } catch(Exception e) { 822 e.file = q.file; 823 e.line = q.line; 824 throw e; 825 // continue; 826 } 827 // this mysql function looks about right: https://dev.mysql.com/doc/c-api/8.0/en/mysql-stmt-result-metadata.html 828 // could maybe emulate by trying it in a rolled back transaction though. 829 auto desca = describePrepared(db,"thing_"~arsd.conv.to!string(count)); 830 LimitedVariant[string] byName; 831 foreach(col; desca.result) { 832 byName[col.fieldName] = col.type.storage; 833 } 834 835 foreach(name; q.names) { 836 if(name.name !in byName) 837 throw ArsdException!"you reference unknown field"(name.name, name.file, name.line); 838 if(name.assumedType.length == 0) 839 continue; 840 if(byName[name.name].contains != name.sampleData.contains) 841 throw ArsdException!"type mismatch"( 842 name.name, 843 arsd.conv.to!string(byName[name.name].contains), 844 arsd.conv.to!string(name.sampleData.contains), 845 name.file, 846 name.line, 847 ); 848 849 // i think this is redundant 850 if(name.assumedType.length && name.actualType.length && name.actualType != name.assumedType) { 851 throw ArsdException!"usage mismatch"(name.assumedType, name.actualType, name.file, name.line); 852 } 853 } 854 } catch(Exception e) { 855 writeln(e.toString()); 856 succeeded = false; 857 } 858 } 859 860 if(!succeeded) 861 writeln("db check failed."); 862 863 return succeeded; 864 } 865 866 /++ 867 868 +/ 869 public PooledConnection!(unsharedThis) get() shared { 870 auto toUse = (cast(unsharedThis) this).getNext(); 871 872 if(toUse is null) 873 toUse = new DatabaseListItem(connectionFactory()); 874 875 return PooledConnection!(unsharedThis)(toUse, cast(unsharedThis) this); 876 } 877 } 878 879 /++ 880 Parent class of various forms of errors you can get when using the database.d library. It may be thrown generically when other details are not provided by a driver. 881 882 See_Also: 883 [DatabaseConnectionException], [SqlException], [DataUsageException] 884 885 History: 886 Added prior to July 2011. 887 +/ 888 class DatabaseException : Exception { 889 this(string msg, string file = __FILE__, size_t line = __LINE__) { 890 super(msg, file, line); 891 } 892 } 893 894 /++ 895 Thrown when something is wrong with your connection to the database server. 896 897 History: 898 Added December 11, 2025 899 +/ 900 class DatabaseConnectionException : DatabaseException { 901 this(string msg, string file = __FILE__, size_t line = __LINE__) { 902 super(msg, file, line); 903 } 904 } 905 906 /++ 907 Thrown when your sql query has reached the database server, but failed to run there for some reason. 908 909 It is possible for this to be thrown on a connection problem in a query too, if the driver didn't differentiate the cause. 910 911 History: 912 Added December 11, 2025 913 +/ 914 class SqlException : DatabaseException { 915 this(string msg, string file = __FILE__, size_t line = __LINE__) { 916 super(msg, file, line); 917 } 918 } 919 920 /++ 921 Thrown when you use result data incorrectly. These almost always are preventable, but may be the result of a schema change and a `select *` query too. 922 923 History: 924 Added December 11, 2025 925 +/ 926 class DataUsageException : DatabaseException { 927 this(string msg, string file = __FILE__, size_t line = __LINE__) { 928 super(msg, file, line); 929 } 930 } 931 932 933 abstract class SqlBuilder { } 934 935 class InsertBuilder : SqlBuilder { 936 private string table; 937 private string[] fields; 938 private string[] fieldsSetSql; 939 private Variant[] values; 940 941 /// 942 void setTable(string table) { 943 this.table = table; 944 } 945 946 /// same as adding the arr as values one by one. assumes DB column name matches AA key. 947 void addVariablesFromAssociativeArray(in string[string] arr, string[] names...) { 948 foreach(name; names) { 949 fields ~= name; 950 if(name in arr) { 951 fieldsSetSql ~= "?"; 952 values ~= Variant(arr[name]); 953 } else { 954 fieldsSetSql ~= "null"; 955 } 956 } 957 } 958 959 /// 960 void addVariable(T)(string name, T value) { 961 fields ~= name; 962 fieldsSetSql ~= "?"; 963 values ~= Variant(value); 964 } 965 966 /// if you use a placeholder, be sure to [addValueForHandWrittenPlaceholder] immediately 967 void addFieldWithSql(string name, string sql) { 968 fields ~= name; 969 fieldsSetSql ~= sql; 970 } 971 972 /// for addFieldWithSql that includes a placeholder 973 void addValueForHandWrittenPlaceholder(T)(T value) { 974 values ~= Variant(value); 975 } 976 977 /// executes the query 978 auto execute(Database db, string supplementalSql = null) { 979 return db.queryImpl(this.toSql() ~ supplementalSql, values); 980 } 981 982 string toSql() { 983 string sql = "INSERT INTO\n"; 984 sql ~= "\t" ~ table ~ " (\n"; 985 foreach(idx, field; fields) { 986 sql ~= "\t\t" ~ field ~ ((idx != fields.length - 1) ? ",\n" : "\n"); 987 } 988 sql ~= "\t) VALUES (\n"; 989 foreach(idx, field; fieldsSetSql) { 990 sql ~= "\t\t" ~ field ~ ((idx != fieldsSetSql.length - 1) ? ",\n" : "\n"); 991 } 992 sql ~= "\t)\n"; 993 return sql; 994 } 995 } 996 997 /// WARNING: this is as susceptible to SQL injections as you would be writing it out by hand 998 class SelectBuilder : SqlBuilder { 999 string[] fields; 1000 string table; 1001 string[] joins; 1002 string[] wheres; 1003 string[] orderBys; 1004 string[] groupBys; 1005 1006 int limit; 1007 int limitStart; 1008 1009 Variant[string] vars; 1010 void setVariable(T)(string name, T value) { 1011 assert(name.length); 1012 if(name[0] == '?') 1013 name = name[1 .. $]; 1014 vars[name] = Variant(value); 1015 } 1016 1017 Database db; 1018 this(Database db = null) { 1019 this.db = db; 1020 } 1021 1022 /* 1023 It would be nice to put variables right here in the builder 1024 1025 ?name 1026 1027 will prolly be the syntax, and we'll do a Variant[string] of them. 1028 1029 Anything not translated here will of course be in the ending string too 1030 */ 1031 1032 SelectBuilder cloned() { 1033 auto s = new SelectBuilder(this.db); 1034 s.fields = this.fields.dup; 1035 s.table = this.table; 1036 s.joins = this.joins.dup; 1037 s.wheres = this.wheres.dup; 1038 s.orderBys = this.orderBys.dup; 1039 s.groupBys = this.groupBys.dup; 1040 s.limit = this.limit; 1041 s.limitStart = this.limitStart; 1042 1043 foreach(k, v; this.vars) 1044 s.vars[k] = v; 1045 1046 return s; 1047 } 1048 1049 override string toString() { 1050 string sql = "SELECT "; 1051 1052 // the fields first 1053 { 1054 bool outputted = false; 1055 foreach(field; fields) { 1056 if(outputted) 1057 sql ~= ", "; 1058 else 1059 outputted = true; 1060 1061 sql ~= field; // "`" ~ field ~ "`"; 1062 } 1063 } 1064 1065 sql ~= " FROM " ~ table; 1066 1067 if(joins.length) { 1068 foreach(join; joins) 1069 sql ~= " " ~ join; 1070 } 1071 1072 if(wheres.length) { 1073 bool outputted = false; 1074 sql ~= " WHERE "; 1075 foreach(w; wheres) { 1076 if(outputted) 1077 sql ~= " AND "; 1078 else 1079 outputted = true; 1080 sql ~= "(" ~ w ~ ")"; 1081 } 1082 } 1083 1084 if(groupBys.length) { 1085 bool outputted = false; 1086 sql ~= " GROUP BY "; 1087 foreach(o; groupBys) { 1088 if(outputted) 1089 sql ~= ", "; 1090 else 1091 outputted = true; 1092 sql ~= o; 1093 } 1094 } 1095 1096 if(orderBys.length) { 1097 bool outputted = false; 1098 sql ~= " ORDER BY "; 1099 foreach(o; orderBys) { 1100 if(outputted) 1101 sql ~= ", "; 1102 else 1103 outputted = true; 1104 sql ~= o; 1105 } 1106 } 1107 1108 if(limit) { 1109 sql ~= " LIMIT "; 1110 if(limitStart) 1111 sql ~= to!string(limitStart) ~ ", "; 1112 sql ~= to!string(limit); 1113 } 1114 1115 if(db is null) 1116 return sql; 1117 1118 return escapedVariants(db, sql, vars); 1119 } 1120 } 1121 1122 1123 // /////////////////////sql////////////////////////////////// 1124 1125 package string tohexsql(const(ubyte)[] b) { 1126 char[] x; 1127 x.length = b.length * 2 + 3; 1128 int pos = 0; 1129 x[pos++] = 'x'; 1130 x[pos++] = '\''; 1131 1132 char tohex(ubyte a) { 1133 if(a < 10) 1134 return cast(char)(a + '0'); 1135 else 1136 return cast(char)(a - 10 + 'A'); 1137 } 1138 1139 foreach(item; b) { 1140 x[pos++] = tohex(item >> 4); 1141 x[pos++] = tohex(item & 0x0f); 1142 } 1143 1144 x[pos++] = '\''; 1145 1146 return cast(string) x; 1147 } 1148 1149 // used in the internal placeholder thing 1150 string toSql(Database db, Variant a) { 1151 1152 string binary(const(ubyte)[] b) { 1153 if(b is null) 1154 return "NULL"; 1155 else 1156 return db.escapeBinaryString(b); 1157 } 1158 1159 auto v = a.peek!(void*); 1160 if(v && (*v is null)) { 1161 return "NULL"; 1162 } else if(auto t = a.peek!(SysTime)) { 1163 return db.sysTimeToValue(*t); 1164 } else if(auto t = a.peek!(DateTime)) { 1165 // FIXME: this might be broken cuz of timezones! 1166 return db.sysTimeToValue(cast(SysTime) *t); 1167 } else if(auto t = a.peek!(ubyte[])) { 1168 return binary(*t); 1169 } else if(auto t = a.peek!(immutable(ubyte)[])) { 1170 return binary(*t); 1171 } else if(auto t = a.peek!string) { 1172 auto str = *t; 1173 if(str is null) 1174 return "NULL"; 1175 else 1176 return '\'' ~ db.escape(str) ~ '\''; 1177 } else { 1178 string str = to!string(a); 1179 return '\'' ~ db.escape(str) ~ '\''; 1180 } 1181 1182 assert(0); 1183 } 1184 1185 // just for convenience; "str".toSql(db); 1186 string toSql(string s, Database db) { 1187 //if(s is null) 1188 //return "NULL"; 1189 return '\'' ~ db.escape(s) ~ '\''; 1190 } 1191 1192 string toSql(long s, Database db) { 1193 return to!string(s); 1194 } 1195 1196 string escapedVariants(Database db, in string sql, Variant[string] t) { 1197 if(t.keys.length <= 0 || sql.indexOf("?") == -1) { 1198 return sql; 1199 } 1200 1201 string fixedup; 1202 int currentStart = 0; 1203 // FIXME: let's make ?? render as ? so we have some escaping capability 1204 foreach(i, dchar c; sql) { 1205 if(c == '?') { 1206 fixedup ~= sql[currentStart .. i]; 1207 1208 int idxStart = cast(int) i + 1; 1209 int idxLength; 1210 1211 bool isFirst = true; 1212 1213 while(idxStart + idxLength < sql.length) { 1214 char C = sql[idxStart + idxLength]; 1215 1216 if((C >= 'a' && C <= 'z') || (C >= 'A' && C <= 'Z') || C == '_' || (!isFirst && C >= '0' && C <= '9')) 1217 idxLength++; 1218 else 1219 break; 1220 1221 isFirst = false; 1222 } 1223 1224 auto idx = sql[idxStart .. idxStart + idxLength]; 1225 1226 if(idx in t) { 1227 fixedup ~= toSql(db, t[idx]); 1228 currentStart = idxStart + idxLength; 1229 } else { 1230 // just leave it there, it might be done on another layer 1231 currentStart = cast(int) i; 1232 } 1233 } 1234 } 1235 1236 fixedup ~= sql[currentStart .. $]; 1237 1238 return fixedup; 1239 } 1240 1241 /// Note: ?n params are zero based! 1242 string escapedVariants(Database db, in string sql, Variant[] t) { 1243 // FIXME: let's make ?? render as ? so we have some escaping capability 1244 // if nothing to escape or nothing to escape with, don't bother 1245 if(t.length > 0 && sql.indexOf("?") != -1) { 1246 string fixedup; 1247 int currentIndex; 1248 int currentStart = 0; 1249 foreach(i, dchar c; sql) { 1250 if(c == '?') { 1251 fixedup ~= sql[currentStart .. i]; 1252 1253 int idx = -1; 1254 currentStart = cast(int) i + 1; 1255 if((i + 1) < sql.length) { 1256 auto n = sql[i + 1]; 1257 if(n >= '0' && n <= '9') { 1258 currentStart = cast(int) i + 2; 1259 idx = n - '0'; 1260 } 1261 } 1262 if(idx == -1) { 1263 idx = currentIndex; 1264 currentIndex++; 1265 } 1266 1267 if(idx < 0 || idx >= t.length) 1268 throw new Exception("SQL Parameter index is out of bounds: " ~ to!string(idx) ~ " at `"~sql[0 .. i]~"`"); 1269 1270 fixedup ~= toSql(db, t[idx]); 1271 } 1272 } 1273 1274 fixedup ~= sql[currentStart .. $]; 1275 1276 return fixedup; 1277 /* 1278 string fixedup; 1279 int pos = 0; 1280 1281 1282 void escAndAdd(string str, int q) { 1283 fixedup ~= sql[pos..q] ~ '\'' ~ db.escape(str) ~ '\''; 1284 1285 } 1286 1287 foreach(a; t) { 1288 int q = sql[pos..$].indexOf("?"); 1289 if(q == -1) 1290 break; 1291 q += pos; 1292 1293 auto v = a.peek!(void*); 1294 if(v && (*v is null)) 1295 fixedup ~= sql[pos..q] ~ "NULL"; 1296 else { 1297 string str = to!string(a); 1298 escAndAdd(str, q); 1299 } 1300 1301 pos = q+1; 1302 } 1303 1304 fixedup ~= sql[pos..$]; 1305 1306 sql = fixedup; 1307 */ 1308 } 1309 1310 return sql; 1311 } 1312 1313 1314 1315 1316 1317 1318 enum UpdateOrInsertMode { 1319 CheckForMe, 1320 AlwaysUpdate, 1321 AlwaysInsert 1322 } 1323 1324 1325 // BIG FIXME: this should really use prepared statements 1326 int updateOrInsert(Database db, string table, string[string] values, string where, UpdateOrInsertMode mode = UpdateOrInsertMode.CheckForMe, string key = "id") { 1327 1328 string identifierQuote = ""; 1329 1330 bool insert = false; 1331 1332 final switch(mode) { 1333 case UpdateOrInsertMode.CheckForMe: 1334 auto res = db.query("SELECT "~key~" FROM "~identifierQuote~db.escape(table)~identifierQuote~" WHERE " ~ where); 1335 insert = res.empty; 1336 1337 break; 1338 case UpdateOrInsertMode.AlwaysInsert: 1339 insert = true; 1340 break; 1341 case UpdateOrInsertMode.AlwaysUpdate: 1342 insert = false; 1343 break; 1344 } 1345 1346 1347 if(insert) { 1348 string insertSql = "INSERT INTO " ~identifierQuote ~ db.escape(table) ~ identifierQuote ~ " "; 1349 1350 bool outputted = false; 1351 string vs, cs; 1352 foreach(column, value; values) { 1353 if(column is null) 1354 continue; 1355 if(outputted) { 1356 vs ~= ", "; 1357 cs ~= ", "; 1358 } else 1359 outputted = true; 1360 1361 //cs ~= "`" ~ db.escape(column) ~ "`"; 1362 cs ~= identifierQuote ~ column ~ identifierQuote; // FIXME: possible insecure 1363 if(value is null) 1364 vs ~= "NULL"; 1365 else 1366 vs ~= "'" ~ db.escape(value) ~ "'"; 1367 } 1368 1369 if(!outputted) 1370 return 0; 1371 1372 1373 insertSql ~= "(" ~ cs ~ ")"; 1374 insertSql ~= " VALUES "; 1375 insertSql ~= "(" ~ vs ~ ")"; 1376 1377 db.query(insertSql); 1378 1379 return 0; // db.lastInsertId; 1380 } else { 1381 string updateSql = "UPDATE "~identifierQuote~db.escape(table)~identifierQuote~" SET "; 1382 1383 bool outputted = false; 1384 foreach(column, value; values) { 1385 if(column is null) 1386 continue; 1387 if(outputted) 1388 updateSql ~= ", "; 1389 else 1390 outputted = true; 1391 1392 if(value is null) 1393 updateSql ~= identifierQuote ~ db.escape(column) ~ identifierQuote ~ " = NULL"; 1394 else 1395 updateSql ~= identifierQuote ~ db.escape(column) ~ identifierQuote ~ " = '" ~ db.escape(value) ~ "'"; 1396 } 1397 1398 if(!outputted) 1399 return 0; 1400 1401 updateSql ~= " WHERE " ~ where; 1402 1403 db.query(updateSql); 1404 return 0; 1405 } 1406 } 1407 1408 1409 1410 1411 1412 string fixupSqlForDataObjectUse(string sql, string[string] keyMapping = null) { 1413 1414 string[] tableNames; 1415 1416 string piece = sql; 1417 sizediff_t idx; 1418 while((idx = piece.indexOf("JOIN")) != -1) { 1419 auto start = idx + 5; 1420 auto i = start; 1421 while(piece[i] != ' ' && piece[i] != '\n' && piece[i] != '\t' && piece[i] != ',') 1422 i++; 1423 auto end = i; 1424 1425 tableNames ~= strip(piece[start..end]); 1426 1427 piece = piece[end..$]; 1428 } 1429 1430 idx = sql.indexOf("FROM"); 1431 if(idx != -1) { 1432 auto start = idx + 5; 1433 auto i = start; 1434 start = i; 1435 while(i < sql.length && !(sql[i] > 'A' && sql[i] <= 'Z')) // if not uppercase, except for A (for AS) to avoid SQL keywords (hack) 1436 i++; 1437 1438 auto from = sql[start..i]; 1439 auto pieces = from.split(","); 1440 foreach(p; pieces) { 1441 p = p.strip(); 1442 start = 0; 1443 i = 0; 1444 while(i < p.length && p[i] != ' ' && p[i] != '\n' && p[i] != '\t' && p[i] != ',') 1445 i++; 1446 1447 tableNames ~= strip(p[start..i]); 1448 } 1449 1450 string sqlToAdd; 1451 foreach(tbl; tableNames) { 1452 if(tbl.length) { 1453 string keyName = "id"; 1454 if(tbl in keyMapping) 1455 keyName = keyMapping[tbl]; 1456 sqlToAdd ~= ", " ~ tbl ~ "." ~ keyName ~ " AS " ~ "id_from_" ~ tbl; 1457 } 1458 } 1459 1460 sqlToAdd ~= " "; 1461 1462 sql = sql[0..idx] ~ sqlToAdd ~ sql[idx..$]; 1463 } 1464 1465 return sql; 1466 } 1467 1468 1469 1470 1471 1472 /* 1473 This is like a result set 1474 1475 1476 DataObject res = [...]; 1477 1478 res.name = "Something"; 1479 1480 res.commit; // runs the actual update or insert 1481 1482 1483 res = new DataObject(fields, tables 1484 1485 1486 1487 1488 1489 1490 1491 when doing a select, we need to figure out all the tables and modify the query to include the ids we need 1492 1493 1494 search for FROM and JOIN 1495 the next token is the table name 1496 1497 right before the FROM, add the ids of each table 1498 1499 1500 given: 1501 SELECT name, phone FROM customers LEFT JOIN phones ON customer.id = phones.cust_id 1502 1503 we want: 1504 SELECT name, phone, customers.id AS id_from_customers, phones.id AS id_from_phones FROM customers LEFT JOIN phones ON customer.id[...]; 1505 1506 */ 1507 1508 mixin template DataObjectConstructors() { 1509 this(Database db, string[string] res, Tuple!(string, string)[string] mappings) { 1510 super(db, res, mappings); 1511 } 1512 } 1513 1514 private string yield(string what) { return `if(auto result = dg(`~what~`)) return result;`; } 1515 1516 import std.typecons; 1517 import std.json; // for json value making 1518 class DataObject { 1519 // lets you just free-form set fields, assuming they all come from the given table 1520 // note it doesn't try to handle joins for new rows. you've gotta do that yourself 1521 this(Database db, string table, UpdateOrInsertMode mode = UpdateOrInsertMode.CheckForMe) { 1522 assert(db !is null); 1523 this.db = db; 1524 this.table = table; 1525 1526 this.mode = mode; 1527 } 1528 1529 JSONValue makeJsonValue() { 1530 JSONValue val; 1531 JSONValue[string] valo; 1532 //val.type = JSON_TYPE.OBJECT; 1533 foreach(k, v; fields) { 1534 JSONValue s; 1535 //s.type = JSON_TYPE.STRING; 1536 s.str = v; 1537 valo[k] = s; 1538 } 1539 val = valo; 1540 return val; 1541 } 1542 1543 this(Database db, string[string] res, Tuple!(string, string)[string] mappings) { 1544 this.db = db; 1545 this.mappings = mappings; 1546 this.fields = res; 1547 1548 mode = UpdateOrInsertMode.AlwaysUpdate; 1549 } 1550 1551 string table; 1552 // table, column [alias] 1553 Tuple!(string, string)[string] mappings; 1554 1555 // value [field] [table] 1556 string[string][string] multiTableKeys; // note this is not set internally tight now 1557 // but it can be set manually to do multi table mappings for automatic update 1558 1559 1560 string opDispatch(string field, string file = __FILE__, size_t line = __LINE__)() 1561 if((field.length < 8 || field[0..8] != "id_from_") && field != "popFront") 1562 { 1563 if(field !in fields) 1564 throw new Exception("no such field " ~ field, file, line); 1565 1566 return fields[field]; 1567 } 1568 1569 string opDispatch(string field, T)(T t) 1570 if((field.length < 8 || field[0..8] != "id_from_") && field != "popFront") 1571 { 1572 static if(__traits(compiles, t is null)) { 1573 if(t is null) 1574 setImpl(field, null); 1575 else 1576 setImpl(field, to!string(t)); 1577 } else 1578 setImpl(field, to!string(t)); 1579 1580 return fields[field]; 1581 } 1582 1583 1584 private void setImpl(string field, string value) { 1585 if(field in fields) { 1586 if(fields[field] != value) 1587 changed[field] = true; 1588 } else { 1589 changed[field] = true; 1590 } 1591 1592 fields[field] = value; 1593 } 1594 1595 public void setWithoutChange(string field, string value) { 1596 fields[field] = value; 1597 } 1598 1599 int opApply(int delegate(ref string) dg) { 1600 foreach(a; fields) 1601 mixin(yield("a")); 1602 1603 return 0; 1604 } 1605 1606 int opApply(int delegate(ref string, ref string) dg) { 1607 foreach(a, b; fields) 1608 mixin(yield("a, b")); 1609 1610 return 0; 1611 } 1612 1613 1614 string opIndex(string field, string file = __FILE__, size_t line = __LINE__) { 1615 if(field !in fields) 1616 throw new DataUsageException("No such field in data object: " ~ field, file, line); 1617 return fields[field]; 1618 } 1619 1620 string opIndexAssign(string value, string field) { 1621 setImpl(field, value); 1622 return value; 1623 } 1624 1625 string* opBinary(string op)(string key) if(op == "in") { 1626 return key in fields; 1627 } 1628 1629 string[string] fields; 1630 bool[string] changed; 1631 1632 void commitChanges() { 1633 commitChanges(cast(string) null, null); 1634 } 1635 1636 void commitChanges(string key, string keyField) { 1637 commitChanges(key is null ? null : [key], keyField is null ? null : [keyField]); 1638 } 1639 1640 void commitChanges(string[] keys, string[] keyFields = null) { 1641 string[string][string] toUpdate; 1642 int updateCount = 0; 1643 foreach(field, c; changed) { 1644 if(c) { 1645 string tbl, col; 1646 if(mappings is null) { 1647 tbl = this.table; 1648 col = field; 1649 } else { 1650 if(field !in mappings) 1651 assert(0, "no such mapping for " ~ field); 1652 auto m = mappings[field]; 1653 tbl = m[0]; 1654 col = m[1]; 1655 } 1656 1657 toUpdate[tbl][col] = fields[field]; 1658 updateCount++; 1659 } 1660 } 1661 1662 if(updateCount) { 1663 db.startTransaction(); 1664 scope(success) db.query("COMMIT"); 1665 scope(failure) db.query("ROLLBACK"); 1666 1667 foreach(tbl, values; toUpdate) { 1668 string where, keyFieldToPass; 1669 1670 if(keys is null) { 1671 keys = [null]; 1672 } 1673 1674 if(multiTableKeys is null || tbl !in multiTableKeys) 1675 foreach(i, key; keys) { 1676 string keyField; 1677 1678 if(key is null) { 1679 key = "id_from_" ~ tbl; 1680 if(key !in fields) 1681 key = "id"; 1682 } 1683 1684 if(i >= keyFields.length || keyFields[i] is null) { 1685 if(key == "id_from_" ~ tbl) 1686 keyField = "id"; 1687 else 1688 keyField = key; 1689 } else { 1690 keyField = keyFields[i]; 1691 } 1692 1693 1694 if(where.length) 1695 where ~= " AND "; 1696 1697 auto f = key in fields ? fields[key] : null; 1698 if(f is null) 1699 where ~= keyField ~ " = NULL"; 1700 else 1701 where ~= keyField ~ " = '"~db.escape(f)~"'" ; 1702 if(keyFieldToPass.length) 1703 keyFieldToPass ~= ", "; 1704 1705 keyFieldToPass ~= keyField; 1706 } 1707 else { 1708 foreach(keyField, v; multiTableKeys[tbl]) { 1709 if(where.length) 1710 where ~= " AND "; 1711 1712 where ~= keyField ~ " = '"~db.escape(v)~"'" ; 1713 if(keyFieldToPass.length) 1714 keyFieldToPass ~= ", "; 1715 1716 keyFieldToPass ~= keyField; 1717 } 1718 } 1719 1720 1721 1722 updateOrInsert(db, tbl, values, where, mode, keyFieldToPass); 1723 } 1724 1725 changed = null; 1726 } 1727 } 1728 1729 void commitDelete() { 1730 if(mode == UpdateOrInsertMode.AlwaysInsert) 1731 throw new Exception("Cannot delete an item not in the database"); 1732 1733 assert(table.length); // FIXME, should work with fancy items too 1734 1735 // FIXME: escaping and primary key questions 1736 db.query("DELETE FROM " ~ table ~ " WHERE id = '" ~ db.escape(fields["id"]) ~ "'"); 1737 } 1738 1739 string getAlias(string table, string column) { 1740 string ali; 1741 if(mappings is null) { 1742 if(this.table is null) { 1743 mappings[column] = tuple(table, column); 1744 return column; 1745 } else { 1746 assert(table == this.table); 1747 ali = column; 1748 } 1749 } else { 1750 foreach(a, what; mappings) 1751 if(what[0] == table && what[1] == column 1752 && a.indexOf("id_from_") == -1) { 1753 ali = a; 1754 break; 1755 } 1756 } 1757 1758 return ali; 1759 } 1760 1761 void set(string table, string column, string value) { 1762 string ali = getAlias(table, column); 1763 //assert(ali in fields); 1764 setImpl(ali, value); 1765 } 1766 1767 string select(string table, string column) { 1768 string ali = getAlias(table, column); 1769 //assert(ali in fields); 1770 if(ali in fields) 1771 return fields[ali]; 1772 return null; 1773 } 1774 1775 DataObject addNew() { 1776 auto n = new DataObject(db, null); 1777 1778 n.db = this.db; 1779 n.table = this.table; 1780 n.mappings = this.mappings; 1781 1782 foreach(k, v; this.fields) 1783 if(k.indexOf("id_from_") == -1) 1784 n.fields[k] = v; 1785 else 1786 n.fields[k] = null; // don't copy ids 1787 1788 n.mode = UpdateOrInsertMode.AlwaysInsert; 1789 1790 return n; 1791 } 1792 1793 Database db; 1794 UpdateOrInsertMode mode; 1795 } 1796 1797 /** 1798 You can subclass DataObject if you want to 1799 get some compile time checks or better types. 1800 1801 You'll want to disable opDispatch, then forward your 1802 properties to the super opDispatch. 1803 */ 1804 1805 /*mixin*/ string DataObjectField(T, string table, string column, string aliasAs = null)() { 1806 string aliasAs_; 1807 if(aliasAs is null) 1808 aliasAs_ = column; 1809 else 1810 aliasAs_ = aliasAs; 1811 return ` 1812 @property void `~aliasAs_~`(`~T.stringof~` setTo) { 1813 super.set("`~table~`", "`~column~`", to!string(setTo)); 1814 } 1815 1816 @property `~T.stringof~` `~aliasAs_~` () { 1817 return to!(`~T.stringof~`)(super.select("`~table~`", "`~column~`")); 1818 } 1819 `; 1820 } 1821 1822 mixin template StrictDataObject() { 1823 // disable opdispatch 1824 string opDispatch(string name)(...) if (0) {} 1825 } 1826 1827 1828 string createDataObjectFieldsFromAlias(string table, fieldsToUse)() { 1829 string ret; 1830 1831 fieldsToUse f; 1832 foreach(member; __traits(allMembers, fieldsToUse)) { 1833 ret ~= DataObjectField!(typeof(__traits(getMember, f, member)), table, member); 1834 } 1835 1836 return ret; 1837 } 1838 1839 1840 /** 1841 This creates an editable data object out of a simple struct. 1842 1843 struct MyFields { 1844 int id; 1845 string name; 1846 } 1847 1848 alias SimpleDataObject!("my_table", MyFields) User; 1849 1850 1851 User a = new User(db); 1852 1853 a.id = 30; 1854 a.name = "hello"; 1855 a.commitChanges(); // tries an update or insert on the my_table table 1856 1857 1858 Unlike the base DataObject class, this template provides compile time 1859 checking for types and names, based on the struct you pass in: 1860 1861 a.id = "aa"; // compile error 1862 1863 a.notAField; // compile error 1864 */ 1865 class SimpleDataObject(string tableToUse, fieldsToUse) : DataObject { 1866 mixin StrictDataObject!(); 1867 1868 mixin(createDataObjectFieldsFromAlias!(tableToUse, fieldsToUse)()); 1869 1870 this(Database db) { 1871 super(db, tableToUse); 1872 } 1873 } 1874 1875 /** 1876 Given some SQL, it finds the CREATE TABLE 1877 instruction for the given tableName. 1878 (this is so it can find one entry from 1879 a file with several SQL commands. But it 1880 may break on a complex file, so try to only 1881 feed it simple sql files.) 1882 1883 From that, it pulls out the members to create a 1884 simple struct based on it. 1885 1886 It's not terribly smart, so it will probably 1887 break on complex tables. 1888 1889 Data types handled: 1890 1891 ``` 1892 INTEGER, SMALLINT, MEDIUMINT -> D's int 1893 TINYINT -> D's bool 1894 BIGINT -> D's long 1895 TEXT, VARCHAR -> D's string 1896 FLOAT, DOUBLE -> D's double 1897 ``` 1898 1899 It also reads DEFAULT values to pass to D, except for NULL. 1900 It ignores any length restrictions. 1901 1902 Bugs: 1903 $(LIST 1904 * Skips all constraints 1905 * Doesn't handle nullable fields, except with strings 1906 * It only handles SQL keywords if they are all caps 1907 ) 1908 1909 This, when combined with SimpleDataObject!(), 1910 can automatically create usable D classes from 1911 SQL input. 1912 */ 1913 struct StructFromCreateTable(string sql, string tableName) { 1914 mixin(getCreateTable(sql, tableName)); 1915 } 1916 1917 string getCreateTable(string sql, string tableName) { 1918 skip: 1919 while(readWord(sql) != "CREATE") {} 1920 1921 assert(readWord(sql) == "TABLE"); 1922 1923 if(readWord(sql) != tableName) 1924 goto skip; 1925 1926 assert(readWord(sql) == "("); 1927 1928 int state; 1929 int parens; 1930 1931 struct Field { 1932 string name; 1933 string type; 1934 string defaultValue; 1935 } 1936 Field*[] fields; 1937 1938 string word = readWord(sql); 1939 Field* current = new Field(); // well, this is interesting... under new DMD, not using new breaks it in CTFE because it overwrites the one entry! 1940 while(word != ")" || parens) { 1941 if(word == ")") { 1942 parens --; 1943 word = readWord(sql); 1944 continue; 1945 } 1946 if(word == "(") { 1947 parens ++; 1948 word = readWord(sql); 1949 continue; 1950 } 1951 switch(state) { 1952 default: assert(0); 1953 case 0: 1954 if(word[0] >= 'A' && word[0] <= 'Z') { 1955 state = 4; 1956 break; // we want to skip this since it starts with a keyword (we hope) 1957 } 1958 current.name = word; 1959 state = 1; 1960 break; 1961 case 1: 1962 current.type ~= word; 1963 state = 2; 1964 break; 1965 case 2: 1966 if(word == "DEFAULT") 1967 state = 3; 1968 else if (word == ",") { 1969 fields ~= current; 1970 current = new Field(); 1971 state = 0; // next 1972 } 1973 break; 1974 case 3: 1975 current.defaultValue = word; 1976 state = 2; // back to skipping 1977 break; 1978 case 4: 1979 if(word == ",") 1980 state = 0; 1981 } 1982 1983 word = readWord(sql); 1984 } 1985 1986 if(current.name !is null) 1987 fields ~= current; 1988 1989 1990 string structCode; 1991 foreach(field; fields) { 1992 structCode ~= "\t"; 1993 1994 switch(field.type) { 1995 case "INTEGER": 1996 case "SMALLINT": 1997 case "MEDIUMINT": 1998 case "SERIAL": // added Oct 23, 2021 1999 structCode ~= "int"; 2000 break; 2001 case "BOOLEAN": 2002 case "TINYINT": 2003 structCode ~= "bool"; 2004 break; 2005 case "BIGINT": 2006 structCode ~= "long"; 2007 break; 2008 case "CHAR": 2009 case "char": 2010 case "VARCHAR": 2011 case "varchar": 2012 case "TEXT": 2013 case "text": 2014 case "TIMESTAMPTZ": // added Oct 23, 2021 2015 structCode ~= "string"; 2016 break; 2017 case "FLOAT": 2018 case "DOUBLE": 2019 structCode ~= "double"; 2020 break; 2021 default: 2022 assert(0, "unknown type " ~ field.type ~ " for " ~ field.name); 2023 } 2024 2025 structCode ~= " "; 2026 structCode ~= field.name; 2027 2028 if(field.defaultValue !is null) { 2029 structCode ~= " = " ~ field.defaultValue; 2030 } 2031 2032 structCode ~= ";\n"; 2033 } 2034 2035 return structCode; 2036 } 2037 2038 string readWord(ref string src) { 2039 reset: 2040 while(src[0] == ' ' || src[0] == '\t' || src[0] == '\n') 2041 src = src[1..$]; 2042 if(src.length >= 2 && src[0] == '-' && src[1] == '-') { // a comment, skip it 2043 while(src[0] != '\n') 2044 src = src[1..$]; 2045 goto reset; 2046 } 2047 2048 int start, pos; 2049 if(src[0] == '`') { 2050 src = src[1..$]; 2051 while(src[pos] != '`') 2052 pos++; 2053 goto gotit; 2054 } 2055 2056 2057 while( 2058 (src[pos] >= 'A' && src[pos] <= 'Z') 2059 || 2060 (src[pos] >= 'a' && src[pos] <= 'z') 2061 || 2062 (src[pos] >= '0' && src[pos] <= '9') 2063 || 2064 src[pos] == '_' 2065 ) 2066 pos++; 2067 gotit: 2068 if(pos == 0) 2069 pos = 1; 2070 2071 string tmp = src[0..pos]; 2072 2073 if(src[pos] == '`') 2074 pos++; // skip the ending quote; 2075 2076 src = src[pos..$]; 2077 2078 return tmp; 2079 } 2080 2081 /// Combines StructFromCreateTable and SimpleDataObject into a one-stop template. 2082 /// alias DataObjectFromSqlCreateTable(import("file.sql"), "my_table") MyTable; 2083 template DataObjectFromSqlCreateTable(string sql, string tableName) { 2084 alias SimpleDataObject!(tableName, StructFromCreateTable!(sql, tableName)) DataObjectFromSqlCreateTable; 2085 } 2086 2087 /+ 2088 class MyDataObject : DataObject { 2089 this() { 2090 super(new Database("localhost", "root", "pass", "social"), null); 2091 } 2092 2093 mixin StrictDataObject!(); 2094 2095 mixin(DataObjectField!(int, "users", "id")); 2096 } 2097 2098 void main() { 2099 auto a = new MyDataObject; 2100 2101 a.fields["id"] = "10"; 2102 2103 a.id = 34; 2104 2105 a.commitChanges; 2106 } 2107 +/ 2108 2109 /* 2110 alias DataObjectFromSqlCreateTable!(import("db.sql"), "users") Test; 2111 2112 void main() { 2113 auto a = new Test(null); 2114 2115 a.cool = "way"; 2116 a.value = 100; 2117 } 2118 */ 2119 2120 void typeinfoBugWorkaround() { 2121 assert(0, to!string(typeid(immutable(char[])[immutable(char)[]]))); 2122 } 2123 2124 mixin template DatabaseOperations(string table) { 2125 DataObject getAsDb(Database db) { 2126 return objectToDataObject!(typeof(this))(this, db, table); 2127 } 2128 2129 static typeof(this) fromRow(Row row) { 2130 return rowToObject!(typeof(this))(row); 2131 } 2132 2133 static typeof(this) fromId(Database db, long id) { 2134 auto query = new SelectBuilder(db); 2135 query.table = table; 2136 query.fields ~= "*"; 2137 query.wheres ~= "id = ?0"; 2138 auto res = db.query(query.toString(), id); 2139 if(res.empty) 2140 throw new Exception("no such row"); 2141 return fromRow(res.front); 2142 } 2143 2144 } 2145 2146 string toDbName(string s) { 2147 import std.string; 2148 return s.toLower ~ "s"; 2149 } 2150 2151 /++ 2152 Easy interop with [arsd.cgi] serveRestObject classes. 2153 2154 History: 2155 Added October 31, 2021. 2156 2157 Warning: not stable/supported at this time. 2158 +/ 2159 mixin template DatabaseRestObject(alias getDb) { 2160 override void save() { 2161 this.id = this.saveToDatabase(getDb()); 2162 } 2163 2164 override void load(string urlId) { 2165 import std.conv; 2166 this.id = to!int(urlId); 2167 this.loadFromDatabase(getDb()); 2168 } 2169 } 2170 2171 void loadFromDatabase(T)(T t, Database database, string tableName = toDbName(__traits(identifier, T))) { 2172 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"); 2173 auto query = new SelectBuilder(database); 2174 query.table = tableName; 2175 query.fields ~= "*"; 2176 query.wheres ~= "id = ?0"; 2177 auto res = database.query(query.toString(), t.id); 2178 if(res.empty) 2179 throw new Exception("no such row"); 2180 2181 rowToObject(res.front, t); 2182 } 2183 2184 auto saveToDatabase(T)(T t, Database database, string tableName = toDbName(__traits(identifier, T))) { 2185 DataObject obj = objectToDataObject(t, database, tableName, t.id ? UpdateOrInsertMode.AlwaysUpdate : UpdateOrInsertMode.AlwaysInsert); 2186 if(!t.id) { 2187 import std.random; // omg i hate htis 2188 obj.id = uniform(2, int.max); 2189 } 2190 obj.commitChanges; 2191 return t.id; 2192 } 2193 2194 /+ + 2195 auto builder = UpdateBuilder("rooms"); 2196 builder.player_one_selection = challenge; 2197 builder.execute(db, id); 2198 +/ 2199 private struct UpdateBuilder { 2200 this(T)(string table, T id) { 2201 this.table = table; 2202 import std.conv; 2203 this.id = to!string(id); 2204 } 2205 2206 } 2207 2208 import std.traits, std.datetime; 2209 enum DbSave; 2210 enum DbNullable; 2211 alias AliasHelper(alias T) = T; 2212 2213 T rowToObject(T)(Row row) { 2214 T t; 2215 static if(is(T == class)) 2216 t = new T(); 2217 rowToObject(row, t); 2218 return t; 2219 } 2220 2221 void rowToObject(T)(Row row, ref T t) { 2222 import arsd.dom, arsd.cgi; 2223 2224 foreach(memberName; __traits(allMembers, T)) { 2225 alias member = AliasHelper!(__traits(getMember, t, memberName)); 2226 foreach(attr; __traits(getAttributes, member)) { 2227 static if(is(attr == DbSave)) { 2228 static if(is(typeof(member) == enum)) 2229 __traits(getMember, t, memberName) = cast(typeof(member)) to!int(row[memberName]); 2230 else static if(is(typeof(member) == bool)) { 2231 __traits(getMember, t, memberName) = row[memberName][0] == 't'; 2232 } else static if(is(typeof(member) == Html)) { 2233 __traits(getMember, t, memberName).source = row[memberName]; 2234 } else static if(is(typeof(member) == DateTime)) 2235 __traits(getMember, t, memberName) = cast(DateTime) dTimeToSysTime(to!long(row[memberName])); 2236 else { 2237 if(row[memberName].length) 2238 __traits(getMember, t, memberName) = to!(typeof(member))(row[memberName]); 2239 // otherwise, we'll leave it as .init - most likely null 2240 } 2241 } 2242 } 2243 } 2244 } 2245 2246 DataObject objectToDataObject(T)(T t, Database db, string table, UpdateOrInsertMode mode = UpdateOrInsertMode.CheckForMe) { 2247 import arsd.dom, arsd.cgi; 2248 2249 DataObject obj = new DataObject(db, table, mode); 2250 foreach(memberName; __traits(allMembers, T)) { 2251 alias member = AliasHelper!(__traits(getMember, t, memberName)); 2252 foreach(attr; __traits(getAttributes, member)) { 2253 static if(is(attr == DbSave)) { 2254 static if(is(typeof(member) == enum)) 2255 obj.opDispatch!memberName(cast(int) __traits(getMember, t, memberName)); 2256 else static if(is(typeof(member) == Html)) { 2257 obj.opDispatch!memberName(__traits(getMember, t, memberName).source); 2258 } else static if(is(typeof(member) == DateTime)) 2259 obj.opDispatch!memberName(dateTimeToDTime(__traits(getMember, t, memberName))); 2260 else { 2261 bool done; 2262 foreach(attr2; __traits(getAttributes, member)) { 2263 static if(is(attr2 == DbNullable)) { 2264 if(__traits(getMember, t, memberName) == 0) 2265 done = true; 2266 } 2267 } 2268 2269 if(!done) { 2270 static if(memberName == "id") { 2271 if(__traits(getMember, t, memberName)) { 2272 // maybe i shouldn't actually set the id but idk 2273 obj.opDispatch!memberName(__traits(getMember, t, memberName)); 2274 } else { 2275 // it is null, let the system do something about it like auto increment 2276 2277 } 2278 } else 2279 obj.opDispatch!memberName(__traits(getMember, t, memberName)); 2280 } 2281 } 2282 } 2283 } 2284 } 2285 return obj; 2286 } 2287 2288 2289 2290 void fillData(T)(string delegate(string, string) setter, T obj, string name) { 2291 fillData( (k, v) { setter(k, v); }, obj, name); 2292 } 2293 2294 void fillData(T)(void delegate(string, string) setter, T obj, string name) { 2295 import arsd.dom, arsd.cgi; 2296 2297 import std.traits; 2298 static if(!isSomeString!T && isArray!T) { 2299 // FIXME: indexing 2300 foreach(o; obj) 2301 fillData(setter, o, name); 2302 } else static if(is(T == DateTime)) { 2303 fillData(setter, obj.toISOExtString(), name); 2304 } else static if(is(T == Html)) { 2305 fillData(setter, obj.source, name); 2306 } else static if(is(T == struct)) { 2307 foreach(idx, memberName; __traits(allMembers, T)) { 2308 alias member = AliasHelper!(__traits(getMember, obj, memberName)); 2309 static if(!is(typeof(member) == function)) 2310 fillData(setter, __traits(getMember, obj, memberName), name ~ "." ~ memberName); 2311 else static if(is(typeof(member) == function)) { 2312 static if(functionAttributes!member & FunctionAttribute.property) { 2313 fillData(setter, __traits(getMember, obj, memberName)(), name ~ "." ~ memberName); 2314 } 2315 } 2316 } 2317 } else { 2318 auto value = to!string(obj); 2319 setter(name, value); 2320 } 2321 } 2322 2323 struct varchar(size_t max) { 2324 private string payload; 2325 2326 this(string s, string file = __FILE__, size_t line = __LINE__) { 2327 opAssign(s, file, line); 2328 } 2329 2330 typeof(this) opAssign(string s, string file = __FILE__, size_t line = __LINE__) { 2331 if(s.length > max) 2332 throw new Exception(s ~ " :: too long", file, line); 2333 payload = s; 2334 2335 return this; 2336 } 2337 2338 string asString() { 2339 return payload; 2340 2341 } 2342 alias asString this; 2343 } 2344 2345 version (unittest) 2346 { 2347 /// Unittest utility that returns a predefined set of values 2348 package (arsd) final class PredefinedResultSet : ResultSet 2349 { 2350 string[] fields; 2351 Row[] rows; 2352 size_t current; 2353 2354 this(string[] fields, Row[] rows) 2355 { 2356 this.fields = fields; 2357 this.rows = rows; 2358 foreach (ref row; rows) 2359 row.resultSet = this; 2360 } 2361 2362 int getFieldIndex(const string field) const 2363 { 2364 foreach (const idx, const val; fields) 2365 if (val == field) 2366 return cast(int) idx; 2367 2368 assert(false, "No field with name: " ~ field); 2369 } 2370 2371 string[] fieldNames() 2372 { 2373 return fields; 2374 } 2375 2376 @property bool empty() const 2377 { 2378 return current == rows.length; 2379 } 2380 2381 Row front() @property 2382 { 2383 assert(!empty); 2384 return rows[current]; 2385 } 2386 2387 void popFront() 2388 { 2389 assert(!empty); 2390 current++; 2391 } 2392 2393 size_t length() @property 2394 { 2395 return rows.length - current; 2396 } 2397 } 2398 }