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