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