git » sdk » commit 741eab1

Allow prepared statements for all queries in a many

author Stephen Paul Weber
2026-02-04 19:04:49 UTC
committer Stephen Paul Weber
2026-02-04 19:04:49 UTC
parent 3624a86e6dc1e56696e17fdcfe49f334f23d60e4

Allow prepared statements for all queries in a many

borogove/persistence/Sqlite.hx +53 -17
borogove/persistence/SqliteDriver.hx +10 -36
borogove/persistence/SqliteDriver.js.hx +11 -6

diff --git a/borogove/persistence/Sqlite.hx b/borogove/persistence/Sqlite.hx
index 000e1cd..b0cff33 100644
--- a/borogove/persistence/Sqlite.hx
+++ b/borogove/persistence/Sqlite.hx
@@ -32,6 +32,42 @@ class Sqlite implements Persistence implements KeyValueStore {
 	final db: SqliteDriver;
 	final media: MediaStore;
 
+	@:allow(borogove)
+	private static function prepare(q: { sql: String, ?params: Array<Dynamic> }): String {
+		return ~/\?/gm.map(q.sql, f -> {
+			var p = (q.params ?? []).shift();
+			return switch (Type.typeof(p)) {
+				case TClass(String):
+					if (p.indexOf("\000") >= 0) {
+						var hexChars = new Array<String>();
+						for (i in 0...p.length) {
+							hexChars.push(StringTools.hex(StringTools.fastCodeAt(p, i), 2));
+						}
+						"x'" + hexChars.join("") + "'";
+					} else {
+						"'" + p.split("'").join("''") + "'";
+					}
+				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));
+			}
+		});
+
+	}
+
 	/**
 		Create a basic persistence layer based on sqlite
 
@@ -43,7 +79,7 @@ class Sqlite implements Persistence implements KeyValueStore {
 		this.media = media;
 		media.setKV(this);
 		db = new SqliteDriver(dbfile, (exec) -> {
-			exec(["PRAGMA user_version;"]).then(iter -> {
+			exec(["PRAGMA user_version"]).then(iter -> {
 				final version = Std.parseInt(iter.next()?.user_version) ?? 0;
 				return Promise.resolve(null).then(_ -> {
 					if (version < 1) {
@@ -62,9 +98,9 @@ class Sqlite implements Persistence implements KeyValueStore {
 							type INTEGER NOT NULL,
 							stanza TEXT NOT NULL,
 							PRIMARY KEY (account_id, mam_id, mam_by, stanza_id)
-						) STRICT;",
-						"CREATE INDEX messages_created_at ON messages (account_id, chat_id, created_at);",
-						"CREATE INDEX messages_correction_id ON messages (correction_id);",
+						) STRICT",
+						"CREATE INDEX messages_created_at ON messages (account_id, chat_id, created_at)",
+						"CREATE INDEX messages_correction_id ON messages (correction_id)",
 						"CREATE TABLE chats (
 							account_id TEXT NOT NULL,
 							chat_id TEXT NOT NULL,
@@ -80,15 +116,15 @@ class Sqlite implements Persistence implements KeyValueStore {
 							presence BLOB NOT NULL,
 							class TEXT NOT NULL,
 							PRIMARY KEY (account_id, chat_id)
-						) STRICT;",
+						) STRICT",
 						"CREATE TABLE keyvaluepairs (
 							k TEXT NOT NULL PRIMARY KEY,
 							v TEXT NOT NULL
-						) STRICT;",
+						) STRICT",
 						"CREATE TABLE caps (
 							sha1 BLOB NOT NULL PRIMARY KEY,
 							caps BLOB NOT NULL
-						) STRICT;",
+						) STRICT",
 						"CREATE TABLE services (
 							account_id TEXT NOT NULL,
 							service_id TEXT NOT NULL,
@@ -96,7 +132,7 @@ class Sqlite implements Persistence implements KeyValueStore {
 							node TEXT,
 							caps BLOB NOT NULL,
 							PRIMARY KEY (account_id, service_id)
-						) STRICT;",
+						) STRICT",
 						"CREATE TABLE accounts (
 							account_id TEXT NOT NULL,
 							client_id TEXT NOT NULL,
@@ -105,7 +141,7 @@ class Sqlite implements Persistence implements KeyValueStore {
 							fast_count INTEGER NOT NULL DEFAULT 0,
 							sm_state BLOB,
 							PRIMARY KEY (account_id)
-						) STRICT;",
+						) STRICT",
 						"CREATE TABLE reactions (
 							account_id TEXT NOT NULL,
 							update_id TEXT NOT NULL,
@@ -118,22 +154,22 @@ class Sqlite implements Persistence implements KeyValueStore {
 							reactions BLOB NOT NULL,
 							kind INTEGER NOT NULL,
 							PRIMARY KEY (account_id, chat_id, sender_id, update_id)
-						) STRICT;",
-						"PRAGMA user_version = 1;"]);
+						) STRICT",
+						"PRAGMA user_version = 1"]);
 					}
 					return Promise.resolve(null);
 				}).then(_ -> {
 					if (version < 2) {
-						return exec(["ALTER TABLE chats ADD COLUMN notifications_filtered INTEGER;",
-						"ALTER TABLE chats ADD COLUMN notify_mention INTEGER NOT NULL DEFAULT 0;",
-						"ALTER TABLE chats ADD COLUMN notify_reply INTEGER NOT NULL DEFAULT 0;",
-						"PRAGMA user_version = 2;"]);
+						return exec(["ALTER TABLE chats ADD COLUMN notifications_filtered INTEGER",
+						"ALTER TABLE chats ADD COLUMN notify_mention INTEGER NOT NULL DEFAULT 0",
+						"ALTER TABLE chats ADD COLUMN notify_reply INTEGER NOT NULL DEFAULT 0",
+						"PRAGMA user_version = 2"]);
 					}
 					return Promise.resolve(null);
 				}).then(_ -> {
 					if (version < 3) {
-						return exec(["ALTER TABLE messages ADD COLUMN status_text TEXT;",
-						"PRAGMA user_version = 3;"]);
+						return exec(["ALTER TABLE messages ADD COLUMN status_text TEXT",
+						"PRAGMA user_version = 3"]);
 					}
 					return Promise.resolve(null);
 				});
diff --git a/borogove/persistence/SqliteDriver.hx b/borogove/persistence/SqliteDriver.hx
index d9af267..296440e 100644
--- a/borogove/persistence/SqliteDriver.hx
+++ b/borogove/persistence/SqliteDriver.hx
@@ -24,13 +24,13 @@ class SqliteDriver {
 			db.request("PRAGMA temp_store=2");
 			if (Config.constrainedMemoryMode) db.request("PRAGMA cache_size=0");
 			dbs.push(db);
-			migrate((sql) -> this.execute(writePool, sql, [])).then(_ -> {
+			migrate((sql) -> this.execute(writePool, sql.map(q -> { sql: q, params: [] }))).then(_ -> {
 				setReady(true);
 			});
 		});
 	}
 
-	private function execute(pool: sys.thread.IThreadPool, qs: Array<String>, params: Array<Dynamic>) {
+	private function execute(pool: sys.thread.IThreadPool, qs: Array<{ sql: String, ?params: Array<Dynamic> }>) {
 		return new Promise((resolve, reject) -> {
 			pool.run(() -> {
 				var db = dbs.pop(false);
@@ -40,12 +40,8 @@ class SqliteDriver {
 					}
 					var result = null;
 					for (q in qs) {
-						if (result == null) {
-							final prepared = prepare(db, q, params);
-							result = db.request(prepared);
-						} else {
-							db.request(q);
-						}
+						final prepared = Sqlite.prepare(q);
+						result = db.request(prepared);
 					}
 					// In testing, not copying to an array here caused BAD ACCESS sometimes
 					// Though from sqlite docs it seems like it should be safe?
@@ -60,37 +56,15 @@ class SqliteDriver {
 		});
 	}
 
-	public function exec(sql: haxe.extern.EitherType<String, Array<String>>, ?params: Array<Dynamic>) {
+
+	public function execMany(qs: Array<{ sql: 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 ?? []);
+			final pool = StringTools.startsWith(qs[0].sql, "SELECT") ? readPool : writePool;
+			return execute(pool, qs);
 		});
 	}
 
-	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));
-			}
-		});
+	public function exec(sql: String, ?params: Array<Dynamic>) {
+		return execMany([{ sql: sql, params: params }]);
 	}
 }
diff --git a/borogove/persistence/SqliteDriver.js.hx b/borogove/persistence/SqliteDriver.js.hx
index 9b65d8f..0c760f9 100644
--- a/borogove/persistence/SqliteDriver.js.hx
+++ b/borogove/persistence/SqliteDriver.js.hx
@@ -36,25 +36,26 @@ class SqliteDriver {
 			return sqlite("open", { filename: dbfile, vfs: "opfs-sahpool" });
 		}).then(openResult -> {
 			dbId = openResult.dbId;
-			return migrate((sql) -> this.exec(sql));
+			return migrate((sql) -> this.execMany(sql.map(q -> { sql: q, params: [] })));
 		});
 	}
 
-	public function exec(sql: haxe.extern.EitherType<String, Array<String>>, ?params: Array<Dynamic>): Promise<haxe.iterators.ArrayIterator<Dynamic>> {
+	public function execMany(qs: Array<{ sql: String, ?params: Array<Dynamic> }>): Promise<haxe.iterators.ArrayIterator<Dynamic>> {
 		if (sqlite == null || dbId == null) {
 			// Not ready yet
 			return new Promise((resolve, reject) -> haxe.Timer.delay(() -> resolve(null), 100))
-				.then(_ -> exec(sql, params));
+				.then(_ -> execMany(qs));
 		}
 
-		final qs = Std.isOfType(sql, String) ? sql : (cast sql).join("");
+		final first = qs.shift();
+		final sql = qs.map(q -> Sqlite.prepare(q) + ";");
 		final items: Array<Dynamic> = [];
 		var signalAllDone;
 		final allDone = new Promise((resolve, reject) -> signalAllDone = resolve);
 		return sqlite('exec', {
 			dbId: dbId,
-			sql: qs,
-			bind: (params ?? []).map(formatParam),
+			sql: [first.sql + ";"].concat(sql),
+			bind: (first.params ?? []).map(formatParam),
 			rowMode: "object",
 			callback: (r) -> {
 				if (r.rowNumber == null) {
@@ -67,6 +68,10 @@ class SqliteDriver {
 		}).then(_ -> allDone).then(_ -> items.iterator());
 	}
 
+	public function exec(sql: String, ?params: Array<Dynamic>) {
+		return execMany([{ sql: sql, params: params }]);
+	}
+
 	private function formatParam(p: Dynamic): Dynamic {
 		return switch (Type.typeof(p)) {
 			case TClass(haxe.io.Bytes):