git » sdk » main » tree

[main] / borogove / persistence / SqliteDriver.hx

package borogove.persistence;

import haxe.io.Bytes;
import thenshim.Promise;
import sys.db.Connection;
using Lambda;

class SqliteDriver {
	final dbs: sys.thread.Deque<Connection> = new sys.thread.Deque();
	private final writePool: sys.thread.IThreadPool = new sys.thread.FixedThreadPool(1);
	private final readPool: sys.thread.IThreadPool;
	private final dbfile: String;
	private final ready: Promise<Bool>;
	private var setReady: (Bool)->Void;
	private var mainLoop: sys.thread.EventLoop;

	public function new(dbfile: String, migrate: (Array<String>->Promise<haxe.iterators.ArrayIterator<Dynamic>>)->Promise<Any>) {
		this.dbfile = dbfile;
		readPool = Config.constrainedMemoryMode ? writePool : new sys.thread.ElasticThreadPool(10);
		ready = new Promise((resolve, reject) -> setReady = resolve);
		mainLoop = sys.thread.Thread.current().events;

		writePool.run(() -> {
			final db = sys.db.Sqlite.open(dbfile);
			db.request("PRAGMA journal_mode=WAL");
			dbs.push(db);
			migrate((sql) -> this.execute(writePool, sql, [])).then(_ -> {
				setReady(true);
			});
		});
	}

	private function execute(pool: sys.thread.IThreadPool, qs: Array<String>, params: Array<Dynamic>) {
		return new Promise((resolve, reject) -> {
			pool.run(() -> {
				var db = dbs.pop(false);
				try {
					if (db == null) {
						db = sys.db.Sqlite.open(dbfile);
					}
					var result = null;
					for (q in qs) {
						if (result == null) {
							final prepared = prepare(db, q, params);
							result = db.request(prepared);
						} else {
							db.request(q);
						}
					}
					// In testing, not copying to an array here caused BAD ACCESS sometimes
					// Though from sqlite docs it seems like it should be safe?
					final arr = { iterator: () -> result }.array();
					dbs.push(db);
					mainLoop.run(() -> { resolve(arr.iterator()); });
				} catch (e) {
					dbs.push(db);
					mainLoop.run(() -> reject(e));
				}
			});
		});
	}

	public function exec(sql: haxe.extern.EitherType<String, Array<String>>, ?params: Array<Dynamic>) {
		return ready.then(_ -> {
			final qs = Std.isOfType(sql, String) ? [sql] : sql;
			final pool = StringTools.startsWith(qs[0], "SELECT") ? readPool : writePool;
			return execute(pool, qs, params ?? []);
		});
	}

	private function prepare(db: Connection, sql:String, params: Array<Dynamic>): String {
		return ~/\?/gm.map(sql, f -> {
			var p = params.shift();
			return switch (Type.typeof(p)) {
				case TClass(String):
					db.quote(p);
				case TBool:
					p == true ? "1" : "0";
				case TFloat:
					Std.string(p);
				case TInt:
					Std.string(p);
				case TNull:
					"NULL";
				case TClass(Array):
					var bytes:Bytes = Bytes.ofData(p);
					"X'" + bytes.toHex() + "'";
				case TClass(haxe.io.Bytes):
					var bytes:Bytes = cast p;
					"X'" + bytes.toHex() + "'";
				case _:
					throw("UKNONWN: " + Type.typeof(p));
			}
		});
	}
}