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 }