From c87abed9ff68bfff0f1ce83235b2210fdd5655e0 Mon Sep 17 00:00:00 2001 From: Oscar Franco Date: Sun, 14 Jun 2026 09:29:10 -0400 Subject: [PATCH 1/3] Revert manual utf-8 parsing --- example/src/tests/queries.ts | 1760 +++++++++++++++++----------------- 1 file changed, 895 insertions(+), 865 deletions(-) diff --git a/example/src/tests/queries.ts b/example/src/tests/queries.ts index 1b3db324..9c3f92de 100644 --- a/example/src/tests/queries.ts +++ b/example/src/tests/queries.ts @@ -1,473 +1,490 @@ import { - // openRemote, - // openSync, - type DB, - isLibsql, - isTurso, - open, - type SQLBatchTuple, + // openRemote, + // openSync, + type DB, + isLibsql, + isTurso, + open, + type SQLBatchTuple, } from "@op-engineering/op-sqlite"; -import { - afterEach, - beforeEach, - describe, - expect, - it, -} from "@op-engineering/op-test"; +import { afterEach, beforeEach, describe, expect, it } from "@op-engineering/op-test"; import { chance, sleep } from "./utils"; // import pkg from '../../package.json' describe("Queries tests", () => { - let db: DB; - - beforeEach(async () => { - db = open({ - name: "queries.sqlite", - encryptionKey: "test", - }); - - await db.execute("DROP TABLE IF EXISTS User;"); - await db.execute("DROP TABLE IF EXISTS T1;"); - await db.execute("DROP TABLE IF EXISTS T2;"); - await db.execute( - "CREATE TABLE User (id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL, nickname TEXT) STRICT;", - ); - }); - - afterEach(() => { - if (db) { - db.delete(); - // @ts-expect-error - db = null; - } - }); - - if (isLibsql()) { - // itOnly('Remote open a turso database', async () => { - // const remoteDb = openRemote({ - // url: 'libsql://foo-ospfranco.turso.io', - // authToken: - // 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3MTY5NTc5OTUsImlkIjoiZmJkNzZmMjYtZTliYy00MGJiLTlmYmYtMDczZjFmMjdjOGY4In0.U3cAWBOvcdiqoPN3MB81sco7x8CGOjjtZ1ZEf30uo2iPcAmOuJzcnAznmDlZ6SpQd4qzuJxE4mAIoRlOkpzgBQ', - // }); - // console.log('Running select 1'); - // const res = await remoteDb.execute('SELECT 1'); - // console.log('after select 1;'); - // expect(res.rowsAffected).toEqual(0); - // }); - // it('Open a libsql database replicated to turso', async () => { - // const remoteDb = openSync({ - // url: 'libsql://foo-ospfranco.turso.io', - // authToken: - // 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3MTY5NTc5OTUsImlkIjoiZmJkNzZmMjYtZTliYy00MGJiLTlmYmYtMDczZjFmMjdjOGY4In0.U3cAWBOvcdiqoPN3MB81sco7x8CGOjjtZ1ZEf30uo2iPcAmOuJzcnAznmDlZ6SpQd4qzuJxE4mAIoRlOkpzgBQ', - // name: 'my replica', - // libsqlSyncInterval: 1000, - // encryptionKey: 'blah', - // }); - // const res = await remoteDb.execute('SELECT 1'); - // remoteDb.sync(); - // expect(res.rowsAffected).toEqual(0); - // }); - } - - it("Can create multiple connections to same db", async () => { - const db2 = open({ - name: "queries.sqlite", - encryptionKey: "test", - }); - - const db3 = open({ - name: "queries.sqlite", - encryptionKey: "test", - }); - - const promises = [ - db.execute("SELECT 1"), - db2.execute("SELECT 1"), - db3.execute("SELECT 1"), - ]; - - const res = await Promise.all(promises); - res.forEach((r) => { - expect(r.rowsAffected).toEqual(0); - expect(r.rows[0]?.["1"]).toEqual(1); - }); - }); - - it("Trying to pass object as param should throw", async () => { - try { - // @ts-expect-error - await db.execute("SELECT ?", [{ foo: "bar" }]); - } catch (e: any) { - expect( - e.message.includes( - "Object is not an ArrayBuffer or ArrayBuffer view", - ), - ).toEqual(true); - } - }); - - it("interrupt is safe to call with no in-flight query", () => { - if (isLibsql() || isTurso()) { - return; - } - - let threw = false; - try { - db.interrupt(); - } catch (_e) { - threw = true; - } - - expect(threw).toEqual(false); - }); - - it("interrupt aborts an in-flight query and rolls back the transaction", async () => { - if (isLibsql() || isTurso()) { - return; - } - - await db.execute("DROP TABLE IF EXISTS InterruptTest;"); - await db.execute("CREATE TABLE InterruptTest (n INTEGER);"); - - const longQuery = ` + let db: DB; + + beforeEach(async () => { + db = open({ + name: "queries.sqlite", + encryptionKey: "test", + }); + + await db.execute("DROP TABLE IF EXISTS User;"); + await db.execute("DROP TABLE IF EXISTS T1;"); + await db.execute("DROP TABLE IF EXISTS T2;"); + await db.execute( + "CREATE TABLE User (id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL, nickname TEXT) STRICT;", + ); + }); + + afterEach(() => { + if (db) { + db.delete(); + // @ts-expect-error + db = null; + } + }); + + if (isLibsql()) { + // itOnly('Remote open a turso database', async () => { + // const remoteDb = openRemote({ + // url: 'libsql://foo-ospfranco.turso.io', + // authToken: + // 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3MTY5NTc5OTUsImlkIjoiZmJkNzZmMjYtZTliYy00MGJiLTlmYmYtMDczZjFmMjdjOGY4In0.U3cAWBOvcdiqoPN3MB81sco7x8CGOjjtZ1ZEf30uo2iPcAmOuJzcnAznmDlZ6SpQd4qzuJxE4mAIoRlOkpzgBQ', + // }); + // console.log('Running select 1'); + // const res = await remoteDb.execute('SELECT 1'); + // console.log('after select 1;'); + // expect(res.rowsAffected).toEqual(0); + // }); + // it('Open a libsql database replicated to turso', async () => { + // const remoteDb = openSync({ + // url: 'libsql://foo-ospfranco.turso.io', + // authToken: + // 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3MTY5NTc5OTUsImlkIjoiZmJkNzZmMjYtZTliYy00MGJiLTlmYmYtMDczZjFmMjdjOGY4In0.U3cAWBOvcdiqoPN3MB81sco7x8CGOjjtZ1ZEf30uo2iPcAmOuJzcnAznmDlZ6SpQd4qzuJxE4mAIoRlOkpzgBQ', + // name: 'my replica', + // libsqlSyncInterval: 1000, + // encryptionKey: 'blah', + // }); + // const res = await remoteDb.execute('SELECT 1'); + // remoteDb.sync(); + // expect(res.rowsAffected).toEqual(0); + // }); + } + + it("Can create multiple connections to same db", async () => { + const db2 = open({ + name: "queries.sqlite", + encryptionKey: "test", + }); + + const db3 = open({ + name: "queries.sqlite", + encryptionKey: "test", + }); + + const promises = [db.execute("SELECT 1"), db2.execute("SELECT 1"), db3.execute("SELECT 1")]; + + const res = await Promise.all(promises); + res.forEach((r) => { + expect(r.rowsAffected).toEqual(0); + expect(r.rows[0]?.["1"]).toEqual(1); + }); + }); + + it("Trying to pass object as param should throw", async () => { + try { + // @ts-expect-error + await db.execute("SELECT ?", [{ foo: "bar" }]); + } catch (e: any) { + expect(e.message.includes("Object is not an ArrayBuffer or ArrayBuffer view")).toEqual(true); + } + }); + + it("interrupt is safe to call with no in-flight query", () => { + if (isLibsql() || isTurso()) { + return; + } + + let threw = false; + try { + db.interrupt(); + } catch (_e) { + threw = true; + } + + expect(threw).toEqual(false); + }); + + it("interrupt aborts an in-flight query and rolls back the transaction", async () => { + if (isLibsql() || isTurso()) { + return; + } + + await db.execute("DROP TABLE IF EXISTS InterruptTest;"); + await db.execute("CREATE TABLE InterruptTest (n INTEGER);"); + + const longQuery = ` WITH RECURSIVE seq(n) AS ( SELECT 1 UNION ALL SELECT n + 1 FROM seq WHERE n < 100000000 ) INSERT INTO InterruptTest SELECT n FROM seq; `; - const queryPromise = db.execute(longQuery); + const queryPromise = db.execute(longQuery); - await sleep(50); - db.interrupt(); + await sleep(50); + db.interrupt(); - let interrupted = false; - try { - await queryPromise; - } catch (e: any) { - interrupted = /interrupt|interrupted|abort|code 9|SQLITE_INTERRUPT/i.test( - String(e?.message ?? e), - ); - } + let interrupted = false; + try { + await queryPromise; + } catch (e: any) { + interrupted = /interrupt|interrupted|abort|code 9|SQLITE_INTERRUPT/i.test( + String(e?.message ?? e), + ); + } - expect(interrupted).toEqual(true); + expect(interrupted).toEqual(true); - const count = await db.execute("SELECT COUNT(*) AS n FROM InterruptTest;"); - expect(count.rows[0]!.n).toEqual(0); - }); + const count = await db.execute("SELECT COUNT(*) AS n FROM InterruptTest;"); + expect(count.rows[0]!.n).toEqual(0); + }); - it("close interrupts an in-flight query before teardown", async () => { - if (isLibsql() || isTurso()) { - return; - } + it("close interrupts an in-flight query before teardown", async () => { + if (isLibsql() || isTurso()) { + return; + } - await db.execute("DROP TABLE IF EXISTS CloseInterruptTest;"); - await db.execute("CREATE TABLE CloseInterruptTest (n INTEGER);"); + await db.execute("DROP TABLE IF EXISTS CloseInterruptTest;"); + await db.execute("CREATE TABLE CloseInterruptTest (n INTEGER);"); - const longQuery = ` + const longQuery = ` WITH RECURSIVE seq(n) AS ( SELECT 1 UNION ALL SELECT n + 1 FROM seq WHERE n < 100000000 ) INSERT INTO CloseInterruptTest SELECT n FROM seq; `; - const queryPromise = db.execute(longQuery); - - await sleep(50); - const startedAt = Date.now(); - db.close(); - const elapsedMs = Date.now() - startedAt; - - await queryPromise.catch(() => undefined); - expect(elapsedMs < 2000).toEqual(true); - - const cleanupDb = open({ - name: "queries.sqlite", - encryptionKey: "test", - }); - cleanupDb.delete(); - - // @ts-expect-error Prevent afterEach from deleting a closed handle. - db = null; - }); - - it("executeSync", () => { - const res = db.executeSync("SELECT 1"); - expect(res.rowsAffected).toEqual(0); - - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - const res2 = db.executeSync( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res2.rowsAffected).toEqual(1); - expect(res2.insertId).toEqual(1); - // expect(res2.rows).toBe([]); - expect(res2.rows?.length).toEqual(0); - }); - - it("Insert", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - const res = await db.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - // expect(res.metadata).toEqual([]); - expect(res.rows).toDeepEqual([]); - expect(res.rows?.length).toEqual(0); - }); - - it("Casts booleans to ints correctly", async () => { - await db.execute(`SELECT ?`, [1]); - await db.execute(`SELECT ?`, [true]); - }); - - it("Insert and query with host objects", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - const res = await db.executeWithHostObjects( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - // expect(res.metadata).toEqual([]); - expect(res.rows).toDeepEqual([]); - expect(res.rows?.length).toEqual(0); - - const queryRes = await db.executeWithHostObjects("SELECT * FROM User"); - - expect(queryRes.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it("Query without params", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id, name, age, networth], - ); - - const res = await db.execute("SELECT * FROM User"); - - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it("Query with params", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id, name, age, networth], - ); - - const res = await db.execute("SELECT * FROM User WHERE id = ?", [id]); - - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it("Query with sqlite functions", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - // COUNT(*) - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id, name, age, networth], - ); - - const countRes = await db.execute("SELECT COUNT(*) as count FROM User"); - - expect(countRes.rows?.length).toEqual(1); - expect(countRes.rows?.[0]?.count).toEqual(1); - - // SUM(age) - const id2 = chance.integer(); - const name2 = chance.name(); - const age2 = chance.integer(); - const networth2 = chance.floating(); - - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id2, name2, age2, networth2], - ); - - const sumRes = await db.execute("SELECT SUM(age) as sum FROM User;"); - - expect(sumRes.rows[0]!.sum).toEqual(age + age2); - - const maxRes = await db.execute("SELECT MAX(networth) as `max` FROM User;"); - const minRes = await db.execute("SELECT MIN(networth) as `min` FROM User;"); - const maxNetworth = Math.max(networth, networth2); - const minNetworth = Math.min(networth, networth2); - - expect(maxRes.rows[0]!.max).toEqual(maxNetworth); - expect(minRes.rows[0]!.min).toEqual(minNetworth); - }); - - it("Executes all the statements in a single string", async () => { - if (isLibsql() || isTurso()) { - return; - } - await db.execute( - `CREATE TABLE T1 ( id INT PRIMARY KEY) STRICT; + const queryPromise = db.execute(longQuery); + + await sleep(50); + const startedAt = Date.now(); + db.close(); + const elapsedMs = Date.now() - startedAt; + + await queryPromise.catch(() => undefined); + expect(elapsedMs < 2000).toEqual(true); + + const cleanupDb = open({ + name: "queries.sqlite", + encryptionKey: "test", + }); + cleanupDb.delete(); + + // @ts-expect-error Prevent afterEach from deleting a closed handle. + db = null; + }); + + it("executeSync", () => { + const res = db.executeSync("SELECT 1"); + expect(res.rowsAffected).toEqual(0); + + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + const res2 = db.executeSync('INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [ + id, + name, + age, + networth, + ]); + + expect(res2.rowsAffected).toEqual(1); + expect(res2.insertId).toEqual(1); + // expect(res2.rows).toBe([]); + expect(res2.rows?.length).toEqual(0); + }); + + it("Insert", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + const res = await db.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + // expect(res.metadata).toEqual([]); + expect(res.rows).toDeepEqual([]); + expect(res.rows?.length).toEqual(0); + }); + + it("Casts booleans to ints correctly", async () => { + await db.execute(`SELECT ?`, [1]); + await db.execute(`SELECT ?`, [true]); + }); + + it("Insert and query with host objects", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + const res = await db.executeWithHostObjects( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + // expect(res.metadata).toEqual([]); + expect(res.rows).toDeepEqual([]); + expect(res.rows?.length).toEqual(0); + + const queryRes = await db.executeWithHostObjects("SELECT * FROM User"); + + expect(queryRes.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Query without params", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id, + name, + age, + networth, + ]); + + const res = await db.execute("SELECT * FROM User"); + + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Query with params", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id, + name, + age, + networth, + ]); + + const res = await db.execute("SELECT * FROM User WHERE id = ?", [id]); + + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Preserves non-ASCII strings when binding params (issue #417)", async () => { + const value = JSON.stringify({ + bullet: "Kimball Wildlife Refuge • Burlingame State Park", + smartQuotes: "John “Jack” Doe", + }); + + await db.execute("DROP TABLE IF EXISTS UnicodeRepro;"); + await db.execute("CREATE TABLE UnicodeRepro (value TEXT) STRICT;"); + await db.execute("INSERT INTO UnicodeRepro (value) VALUES (?)", [value]); + + const result = await db.execute("SELECT value FROM UnicodeRepro;"); + expect(result.rows[0]!.value).toEqual(value); + + const hexResult = await db.execute("SELECT hex(value) AS valueHex FROM UnicodeRepro;"); + expect(hexResult.rows[0]!.valueHex.includes("E280A2")).toEqual(true); + expect(hexResult.rows[0]!.valueHex.includes("E2809C")).toEqual(true); + expect(hexResult.rows[0]!.valueHex.includes("E2809D")).toEqual(true); + }); + + it("Query with sqlite functions", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + // COUNT(*) + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id, + name, + age, + networth, + ]); + + const countRes = await db.execute("SELECT COUNT(*) as count FROM User"); + + expect(countRes.rows?.length).toEqual(1); + expect(countRes.rows?.[0]?.count).toEqual(1); + + // SUM(age) + const id2 = chance.integer(); + const name2 = chance.name(); + const age2 = chance.integer(); + const networth2 = chance.floating(); + + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id2, + name2, + age2, + networth2, + ]); + + const sumRes = await db.execute("SELECT SUM(age) as sum FROM User;"); + + expect(sumRes.rows[0]!.sum).toEqual(age + age2); + + const maxRes = await db.execute("SELECT MAX(networth) as `max` FROM User;"); + const minRes = await db.execute("SELECT MIN(networth) as `min` FROM User;"); + const maxNetworth = Math.max(networth, networth2); + const minNetworth = Math.min(networth, networth2); + + expect(maxRes.rows[0]!.max).toEqual(maxNetworth); + expect(minRes.rows[0]!.min).toEqual(minNetworth); + }); + + it("Executes all the statements in a single string", async () => { + if (isLibsql() || isTurso()) { + return; + } + await db.execute( + `CREATE TABLE T1 ( id INT PRIMARY KEY) STRICT; CREATE TABLE T2 ( id INT PRIMARY KEY) STRICT;`, - ); - - const t1name = await db.execute( - "SELECT name FROM sqlite_master WHERE type='table' AND name='T1';", - ); - - expect(t1name.rows[0]!.name).toEqual("T1"); - - const t2name = await db.execute( - "SELECT name FROM sqlite_master WHERE type='table' AND name='T2';", - ); - - expect(t2name.rows[0]!.name).toEqual("T2"); - }); - - it("Failed insert", async () => { - const id = chance.string(); - const name = chance.name(); - const age = chance.string(); - const networth = chance.string(); - try { - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id, name, age, networth], - ); - } catch (e: any) { - expect(typeof e).toEqual("object"); - - expect(!!e.message).toEqual(true); - } - }); - - it("Transaction, auto commit", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async (tx) => { - const res = await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - // expect(res.metadata).toEqual([]); - expect(res.rows).toDeepEqual([]); - expect(res.rows?.length).toEqual(0); - }); - - const res = await db.execute("SELECT * FROM User"); - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it("Transaction, manual commit", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async (tx) => { - const res = await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - expect(res.rows).toDeepEqual([]); - expect(res.rows?.length).toEqual(0); - - await tx.commit(); - }); - - const res = await db.execute("SELECT * FROM User"); - // console.log(res); - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it("Transaction, executed in order", async () => { - const xs = 10; - const actual: unknown[] = []; - - // ARRANGE: Generate expected data - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - - // ACT: Start multiple transactions to upsert and select the same record - const promises = []; - for (let i = 1; i <= xs; i++) { - const promised = db.transaction(async (tx) => { - // ACT: Upsert statement to create record / increment the value - await tx.execute( - ` + ); + + const t1name = await db.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='T1';", + ); + + expect(t1name.rows[0]!.name).toEqual("T1"); + + const t2name = await db.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='T2';", + ); + + expect(t2name.rows[0]!.name).toEqual("T2"); + }); + + it("Failed insert", async () => { + const id = chance.string(); + const name = chance.name(); + const age = chance.string(); + const networth = chance.string(); + try { + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id, + name, + age, + networth, + ]); + } catch (e: any) { + expect(typeof e).toEqual("object"); + + expect(!!e.message).toEqual(true); + } + }); + + it("Transaction, auto commit", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.transaction(async (tx) => { + const res = await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + // expect(res.metadata).toEqual([]); + expect(res.rows).toDeepEqual([]); + expect(res.rows?.length).toEqual(0); + }); + + const res = await db.execute("SELECT * FROM User"); + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Transaction, manual commit", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.transaction(async (tx) => { + const res = await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + expect(res.rows).toDeepEqual([]); + expect(res.rows?.length).toEqual(0); + + await tx.commit(); + }); + + const res = await db.execute("SELECT * FROM User"); + // console.log(res); + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Transaction, executed in order", async () => { + const xs = 10; + const actual: unknown[] = []; + + // ARRANGE: Generate expected data + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + + // ACT: Start multiple transactions to upsert and select the same record + const promises = []; + for (let i = 1; i <= xs; i++) { + const promised = db.transaction(async (tx) => { + // ACT: Upsert statement to create record / increment the value + await tx.execute( + ` INSERT OR REPLACE INTO [User] ([id], [name], [age], [networth]) SELECT ?, ?, ?, IFNULL(( @@ -476,426 +493,439 @@ describe("Queries tests", () => { WHERE [id] = ? ), 0) `, - [id, name, age, id], - ); - - // ACT: Select statement to get incremented value and store it for checking later - const results = await tx.execute( - "SELECT [networth] FROM [User] WHERE [id] = ?", - [id], - ); - - actual.push(results.rows[0]!.networth); - }); - - promises.push(promised); - } - - // ACT: Wait for all transactions to complete - await Promise.all(promises); - - // ASSERT: That the expected values where returned - const expected = Array(xs) - .fill(0) - .map((_, index) => index * 1000); - - expect(actual).toDeepEqual(expected); - }); - - it("Transaction, cannot execute after commit", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async (tx) => { - const res = await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - // expect(res.metadata).toEqual([]); - expect(res.rows).toDeepEqual([]); - expect(res.rows.length).toEqual(0); - - await tx.commit(); - - try { - await tx.execute('SELECT * FROM "User"'); - } catch (e) { - expect(!!e).toEqual(true); - } - }); - - const res = await db.execute("SELECT * FROM User"); - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it("Incorrect transaction, manual rollback", async () => { - const id = chance.string(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async (tx) => { - try { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - } catch (_e) { - await tx.rollback(); - } - }); - - const res = await db.execute("SELECT * FROM User"); - expect(res.rows).toDeepEqual([]); - }); - - it("Correctly throws", async () => { - const id = chance.string(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - try { - await db.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - } catch (e: any) { - expect(!!e).toEqual(true); - } - }); - - it("Rollback", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async (tx) => { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - await tx.rollback(); - const res = await db.execute("SELECT * FROM User"); - expect(res.rows).toDeepEqual([]); - }); - }); - - it("Execute raw sync should return just an array of objects", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id, name, age, networth], - ); - - const res = db.executeRawSync("SELECT id, name, age, networth FROM User"); - expect(res).toDeepEqual([[id, name, age, networth]]); - }); - - it("Transaction, rejects on callback error", async () => { - const promised = db.transaction(() => { - throw new Error("Error from callback"); - }); - - // ASSERT: should return a promise that eventually rejects - expect(typeof promised === "object"); - try { - await promised; - // expect.fail('Should not resolve'); - } catch (e) { - // expect(e).to.be.a.instanceof(Error); - expect((e as Error)?.message).toEqual("Error from callback"); - } - }); - - it("Transaction, rejects on invalid query", async () => { - const promised = db.transaction(async (tx) => { - await tx.execute("SELECT * FROM [tableThatDoesNotExist];"); - }); - - // ASSERT: should return a promise that eventually rejects - // expect(promised).to.have.property('then').that.is.a('function'); - try { - await promised; - // expect.fail('Should not resolve'); - } catch (e) { - // expect(e).to.be.a.instanceof(Error); - expect(((e as Error)?.message?.length ?? 0) > 0).toBe(true); - } - }); - - it("Transaction, handle async callback", async () => { - let ranCallback = false; - const promised = db.transaction(async (tx) => { - await new Promise((done) => { - setTimeout(() => done(), 50); - }); - tx.execute("SELECT * FROM User;"); - ranCallback = true; - }); - - // ASSERT: should return a promise that eventually rejects - // expect(promised).to.have.property('then').that.is.a('function'); - await promised; - expect(ranCallback).toEqual(true); - }); - - it("executeBatch", async () => { - const id1 = chance.integer(); - const name1 = chance.name(); - const age1 = chance.integer(); - const networth1 = chance.floating(); - - const id2 = chance.integer(); - const name2 = chance.name(); - const age2 = chance.integer(); - const networth2 = chance.floating(); - - const commands: SQLBatchTuple[] = [ - ['SELECT * FROM "User"', []], - ['SELECT * FROM "User"'], - [ - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id1, name1, age1, networth1], - ], - [ - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [[id2, name2, age2, networth2]], - ], - ]; - - await db.executeBatch(commands); - - const res = await db.execute("SELECT * FROM User"); - - expect(res.rows).toDeepEqual([ - { id: id1, name: name1, age: age1, networth: networth1, nickname: null }, - { - id: id2, - name: name2, - age: age2, - networth: networth2, - nickname: null, - }, - ]); - }); - - it("Batch execute with BLOB", async () => { - const db = open({ - name: "queries.sqlite", - encryptionKey: "test", - }); - - await db.execute("DROP TABLE IF EXISTS User;"); - await db.execute( - "CREATE TABLE IF NOT EXISTS User (id TEXT PRIMARY KEY NOT NULL, name TEXT NOT NULL, age INT, networth BLOB, nickname TEXT) STRICT;", - ); - const id1 = "1"; - const name1 = "name1"; - const age1 = 12; - const networth1 = new Uint8Array([1, 2, 3]); - - const id2 = "2"; - const name2 = "name2"; - const age2 = 17; - const networth2 = new Uint8Array([3, 2, 1]); - - const commands: SQLBatchTuple[] = [ - [ - 'INSERT OR REPLACE INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id1, name1, age1, networth1], - ], - [ - 'INSERT OR REPLACE INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [[id2, name2, age2, networth2]], - ], - ]; - - // bomb~ (NOBRIDGE) ERROR Error: Exception in HostFunction: - await db.executeBatch(commands); - }); - - it("DumbHostObject allows to write known props", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id, name, age, networth], - ); - - const res = await db.executeWithHostObjects("SELECT * FROM User"); - - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - - res.rows[0]!.name = "quack_changed"; - - expect(res.rows[0]!.name).toEqual("quack_changed"); - }); - - it("DumbHostObject allows to write new props", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id, name, age, networth], - ); - - const res = await db.executeWithHostObjects("SELECT * FROM User"); - - expect(res.rows!).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - - res.rows[0]!.myWeirdProp = "quack_changed"; - - expect(res.rows[0]!.myWeirdProp).toEqual("quack_changed"); - }); - - it("Execute raw should return just an array of objects", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.execute( - "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", - [id, name, age, networth], - ); - - const res = await db.executeRaw("SELECT id, name, age, networth FROM User"); - expect(res).toDeepEqual([[id, name, age, networth]]); - }); - - it("Create fts5 virtual table", async () => { - if (isTurso()) { - return; - } - - await db.execute( - "CREATE VIRTUAL TABLE fts5_table USING fts5(name, content);", - ); - await db.execute("INSERT INTO fts5_table (name, content) VALUES(?, ?)", [ - "test", - "test content", - ]); - - const res = await db.execute("SELECT * FROM fts5_table"); - expect(res.rows).toDeepEqual([{ name: "test", content: "test content" }]); - }); - - it("Various queries", async () => { - await db.execute("SELECT 1 "); - await db.execute("SELECT 1 "); - await db.execute("SELECT 1; ", []); - await db.execute("SELECT ?; ", [1]); - }); - - it("Handles concurrent transactions correctly", async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - const transaction1 = db.transaction(async (tx) => { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - }); - - const transaction2 = db.transaction(async (tx) => { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id + 1, name, age, networth], - ); - }); - - await Promise.all([transaction1, transaction2]); - - const res = await db.execute("SELECT * FROM User"); - expect(res.rows.length).toEqual(2); - }); - - it("Pragma user_version", () => { - const res = db.executeSync("PRAGMA user_version"); - expect(res.rows).toDeepEqual([{ user_version: 0 }]); - }); - - // const sqliteVecEnabled = pkg?.['op-sqlite']?.sqliteVec === true; - // if (sqliteVecEnabled) { - // it('sqlite-vec extension: vector similarity search', async () => { - // // Create a virtual table for storing vectors - // await db.execute(` - // CREATE VIRTUAL TABLE vec_items USING vec0( - // embedding FLOAT[8] - // ) - // `); - - // // Insert some sample vectors - // await db.execute(` - // INSERT INTO vec_items(rowid, embedding) - // VALUES - // (1, '[-0.200, 0.250, 0.341, -0.211, 0.645, 0.935, -0.316, -0.924]'), - // (2, '[0.443, -0.501, 0.355, -0.771, 0.707, -0.708, -0.185, 0.362]'), - // (3, '[0.716, -0.927, 0.134, 0.052, -0.669, 0.793, -0.634, -0.162]'), - // (4, '[-0.710, 0.330, 0.656, 0.041, -0.990, 0.726, 0.385, -0.958]') - // `); - - // // Perform KNN query to find the 2 nearest neighbors - // const queryVector = '[0.890, 0.544, 0.825, 0.961, 0.358, 0.0196, 0.521, 0.175]'; - // const result = await db.execute(` - // SELECT rowid, distance - // FROM vec_items - // WHERE embedding MATCH ? - // ORDER BY distance - // LIMIT 2 - // `, [queryVector]); - - // // Verify results - // expect(result.rows.length).toEqual(2); - // expect(result.rows[0]!.rowid).toEqual(2); - // expect(result.rows[1]!.rowid).toEqual(1); - - // // Verify distances are positive numbers - // const distance0 = result.rows[0]!.distance as number; - // const distance1 = result.rows[1]!.distance as number; - // expect(typeof distance0).toEqual('number'); - // expect(distance0 > 0).toBeTruthy(); - // expect(distance1 > 0).toBeTruthy(); - // }); - // } + [id, name, age, id], + ); + + // ACT: Select statement to get incremented value and store it for checking later + const results = await tx.execute("SELECT [networth] FROM [User] WHERE [id] = ?", [id]); + + actual.push(results.rows[0]!.networth); + }); + + promises.push(promised); + } + + // ACT: Wait for all transactions to complete + await Promise.all(promises); + + // ASSERT: That the expected values where returned + const expected = Array(xs) + .fill(0) + .map((_, index) => index * 1000); + + expect(actual).toDeepEqual(expected); + }); + + it("Transaction, cannot execute after commit", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.transaction(async (tx) => { + const res = await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + // expect(res.metadata).toEqual([]); + expect(res.rows).toDeepEqual([]); + expect(res.rows.length).toEqual(0); + + await tx.commit(); + + try { + await tx.execute('SELECT * FROM "User"'); + } catch (e) { + expect(!!e).toEqual(true); + } + }); + + const res = await db.execute("SELECT * FROM User"); + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Incorrect transaction, manual rollback", async () => { + const id = chance.string(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.transaction(async (tx) => { + try { + await tx.execute('INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [ + id, + name, + age, + networth, + ]); + } catch (_e) { + await tx.rollback(); + } + }); + + const res = await db.execute("SELECT * FROM User"); + expect(res.rows).toDeepEqual([]); + }); + + it("Correctly throws", async () => { + const id = chance.string(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + try { + await db.execute('INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [ + id, + name, + age, + networth, + ]); + } catch (e: any) { + expect(!!e).toEqual(true); + } + }); + + it("Rollback", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.transaction(async (tx) => { + await tx.execute('INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [ + id, + name, + age, + networth, + ]); + await tx.rollback(); + const res = await db.execute("SELECT * FROM User"); + expect(res.rows).toDeepEqual([]); + }); + }); + + it("Execute raw sync should return just an array of objects", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id, + name, + age, + networth, + ]); + + const res = db.executeRawSync("SELECT id, name, age, networth FROM User"); + expect(res).toDeepEqual([[id, name, age, networth]]); + }); + + it("Transaction, rejects on callback error", async () => { + const promised = db.transaction(() => { + throw new Error("Error from callback"); + }); + + // ASSERT: should return a promise that eventually rejects + expect(typeof promised === "object"); + try { + await promised; + // expect.fail('Should not resolve'); + } catch (e) { + // expect(e).to.be.a.instanceof(Error); + expect((e as Error)?.message).toEqual("Error from callback"); + } + }); + + it("Transaction, rejects on invalid query", async () => { + const promised = db.transaction(async (tx) => { + await tx.execute("SELECT * FROM [tableThatDoesNotExist];"); + }); + + // ASSERT: should return a promise that eventually rejects + // expect(promised).to.have.property('then').that.is.a('function'); + try { + await promised; + // expect.fail('Should not resolve'); + } catch (e) { + // expect(e).to.be.a.instanceof(Error); + expect(((e as Error)?.message?.length ?? 0) > 0).toBe(true); + } + }); + + it("Transaction, handle async callback", async () => { + let ranCallback = false; + const promised = db.transaction(async (tx) => { + await new Promise((done) => { + setTimeout(() => done(), 50); + }); + tx.execute("SELECT * FROM User;"); + ranCallback = true; + }); + + // ASSERT: should return a promise that eventually rejects + // expect(promised).to.have.property('then').that.is.a('function'); + await promised; + expect(ranCallback).toEqual(true); + }); + + it("executeBatch", async () => { + const id1 = chance.integer(); + const name1 = chance.name(); + const age1 = chance.integer(); + const networth1 = chance.floating(); + + const id2 = chance.integer(); + const name2 = chance.name(); + const age2 = chance.integer(); + const networth2 = chance.floating(); + + const commands: SQLBatchTuple[] = [ + ['SELECT * FROM "User"', []], + ['SELECT * FROM "User"'], + [ + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id1, name1, age1, networth1], + ], + [ + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [[id2, name2, age2, networth2]], + ], + ]; + + await db.executeBatch(commands); + + const res = await db.execute("SELECT * FROM User"); + + expect(res.rows).toDeepEqual([ + { id: id1, name: name1, age: age1, networth: networth1, nickname: null }, + { + id: id2, + name: name2, + age: age2, + networth: networth2, + nickname: null, + }, + ]); + }); + + it("Batch execute with BLOB", async () => { + const db = open({ + name: "queries.sqlite", + encryptionKey: "test", + }); + + await db.execute("DROP TABLE IF EXISTS User;"); + await db.execute( + "CREATE TABLE IF NOT EXISTS User (id TEXT PRIMARY KEY NOT NULL, name TEXT NOT NULL, age INT, networth BLOB, nickname TEXT) STRICT;", + ); + const id1 = "1"; + const name1 = "name1"; + const age1 = 12; + const networth1 = new Uint8Array([1, 2, 3]); + + const id2 = "2"; + const name2 = "name2"; + const age2 = 17; + const networth2 = new Uint8Array([3, 2, 1]); + + const commands: SQLBatchTuple[] = [ + [ + 'INSERT OR REPLACE INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id1, name1, age1, networth1], + ], + [ + 'INSERT OR REPLACE INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [[id2, name2, age2, networth2]], + ], + ]; + + // bomb~ (NOBRIDGE) ERROR Error: Exception in HostFunction: + await db.executeBatch(commands); + }); + + it("DumbHostObject allows to write known props", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id, + name, + age, + networth, + ]); + + const res = await db.executeWithHostObjects("SELECT * FROM User"); + + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + + res.rows[0]!.name = "quack_changed"; + + expect(res.rows[0]!.name).toEqual("quack_changed"); + }); + + it("DumbHostObject allows to write new props", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id, + name, + age, + networth, + ]); + + const res = await db.executeWithHostObjects("SELECT * FROM User"); + + expect(res.rows!).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + + res.rows[0]!.myWeirdProp = "quack_changed"; + + expect(res.rows[0]!.myWeirdProp).toEqual("quack_changed"); + }); + + it("Execute raw should return just an array of objects", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.execute("INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", [ + id, + name, + age, + networth, + ]); + + const res = await db.executeRaw("SELECT id, name, age, networth FROM User"); + expect(res).toDeepEqual([[id, name, age, networth]]); + }); + + it("Create fts5 virtual table", async () => { + if (isTurso()) { + return; + } + + await db.execute("CREATE VIRTUAL TABLE fts5_table USING fts5(name, content);"); + await db.execute("INSERT INTO fts5_table (name, content) VALUES(?, ?)", [ + "test", + "test content", + ]); + + const res = await db.execute("SELECT * FROM fts5_table"); + expect(res.rows).toDeepEqual([{ name: "test", content: "test content" }]); + }); + + it("Various queries", async () => { + await db.execute("SELECT 1 "); + await db.execute("SELECT 1 "); + await db.execute("SELECT 1; ", []); + await db.execute("SELECT ?; ", [1]); + }); + + it("Handles concurrent transactions correctly", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + const transaction1 = db.transaction(async (tx) => { + await tx.execute('INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [ + id, + name, + age, + networth, + ]); + }); + + const transaction2 = db.transaction(async (tx) => { + await tx.execute('INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [ + id + 1, + name, + age, + networth, + ]); + }); + + await Promise.all([transaction1, transaction2]); + + const res = await db.execute("SELECT * FROM User"); + expect(res.rows.length).toEqual(2); + }); + + it("Pragma user_version", () => { + const res = db.executeSync("PRAGMA user_version"); + expect(res.rows).toDeepEqual([{ user_version: 0 }]); + }); + + // const sqliteVecEnabled = pkg?.['op-sqlite']?.sqliteVec === true; + // if (sqliteVecEnabled) { + // it('sqlite-vec extension: vector similarity search', async () => { + // // Create a virtual table for storing vectors + // await db.execute(` + // CREATE VIRTUAL TABLE vec_items USING vec0( + // embedding FLOAT[8] + // ) + // `); + + // // Insert some sample vectors + // await db.execute(` + // INSERT INTO vec_items(rowid, embedding) + // VALUES + // (1, '[-0.200, 0.250, 0.341, -0.211, 0.645, 0.935, -0.316, -0.924]'), + // (2, '[0.443, -0.501, 0.355, -0.771, 0.707, -0.708, -0.185, 0.362]'), + // (3, '[0.716, -0.927, 0.134, 0.052, -0.669, 0.793, -0.634, -0.162]'), + // (4, '[-0.710, 0.330, 0.656, 0.041, -0.990, 0.726, 0.385, -0.958]') + // `); + + // // Perform KNN query to find the 2 nearest neighbors + // const queryVector = '[0.890, 0.544, 0.825, 0.961, 0.358, 0.0196, 0.521, 0.175]'; + // const result = await db.execute(` + // SELECT rowid, distance + // FROM vec_items + // WHERE embedding MATCH ? + // ORDER BY distance + // LIMIT 2 + // `, [queryVector]); + + // // Verify results + // expect(result.rows.length).toEqual(2); + // expect(result.rows[0]!.rowid).toEqual(2); + // expect(result.rows[1]!.rowid).toEqual(1); + + // // Verify distances are positive numbers + // const distance0 = result.rows[0]!.distance as number; + // const distance1 = result.rows[1]!.distance as number; + // expect(typeof distance0).toEqual('number'); + // expect(distance0 > 0).toBeTruthy(); + // expect(distance1 > 0).toBeTruthy(); + // }); + // } }); From f8efc0c4a0b2c9ab9b118437101d74fb003758de Mon Sep 17 00:00:00 2001 From: Oscar Franco Date: Sun, 14 Jun 2026 09:29:44 -0400 Subject: [PATCH 2/3] Revert manual utf-8 parsing --- cpp/DBHostObject.cpp | 43 +++++++++++++++++-------------------------- cpp/OPSqlite.cpp | 36 +++++++++++------------------------- cpp/utils.cpp | 8 +++----- cpp/utils.hpp | 25 ------------------------- 4 files changed, 31 insertions(+), 81 deletions(-) diff --git a/cpp/DBHostObject.cpp b/cpp/DBHostObject.cpp index 7e11123e..cec53059 100644 --- a/cpp/DBHostObject.cpp +++ b/cpp/DBHostObject.cpp @@ -241,18 +241,12 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { auto obj_params = args[0].asObject(rt); std::string secondary_db_name = - jsi_string_to_utf8(rt, - obj_params.getProperty(rt, "secondaryDbFileName") - .asString(rt)); - std::string alias = - jsi_string_to_utf8(rt, - obj_params.getProperty(rt, "alias").asString(rt)); + obj_params.getProperty(rt, "secondaryDbFileName").asString(rt).utf8(rt); + std::string alias = obj_params.getProperty(rt, "alias").asString(rt).utf8(rt); if (obj_params.hasProperty(rt, "location")) { std::string location = - jsi_string_to_utf8(rt, - obj_params.getProperty(rt, "location") - .asString(rt)); + obj_params.getProperty(rt, "location").asString(rt).utf8(rt); secondary_db_path = secondary_db_path + location; } @@ -283,7 +277,7 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { throw std::runtime_error("[op-sqlite] alias must be a strings"); } - std::string alias = jsi_string_to_utf8(rt, args[0].asString(rt)); + std::string alias = args[0].asString(rt).utf8(rt); if (alias.find('\0') != std::string::npos) { throw std::runtime_error( "[op-sqlite] detach alias must not contain a zero byte"); @@ -372,7 +366,7 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { }); function_map["executeRaw"] = HFN(this) { - const std::string query = jsi_string_to_utf8(rt, args[0].asString(rt)); + const std::string query = args[0].asString(rt).utf8(rt); const std::vector params = count == 2 && args[1].isObject() ? to_variant_vec(rt, args[1]) : std::vector(); @@ -399,7 +393,7 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { }); function_map["executeSync"] = HFN(this) { - std::string query = jsi_string_to_utf8(rt, args[0].asString(rt)); + std::string query = args[0].asString(rt).utf8(rt); std::vector params; if (count == 2) { @@ -415,7 +409,7 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { }); function_map["executeRawSync"] = HFN(this) { - const std::string query = jsi_string_to_utf8(rt, args[0].asString(rt)); + const std::string query = args[0].asString(rt).utf8(rt); std::vector params = count == 2 && args[1].isObject() ? to_variant_vec(rt, args[1]) : std::vector(); @@ -432,7 +426,7 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { }); function_map["execute"] = HFN(this) { - const std::string query = jsi_string_to_utf8(rt, args[0].asString(rt)); + const std::string query = args[0].asString(rt).utf8(rt); std::vector params = count == 2 && args[1].isObject() ? to_variant_vec(rt, args[1]) : std::vector(); @@ -454,7 +448,7 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { }); function_map["executeWithHostObjects"] = HFN(this) { - const std::string query = jsi_string_to_utf8(rt, args[0].asString(rt)); + const std::string query = args[0].asString(rt).utf8(rt); std::vector params = count == 2 && args[1].isObject() ? to_variant_vec(rt, args[1]) : std::vector(); @@ -555,8 +549,7 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { "[op-sqlite][loadFile] Incorrect parameter count"); } - const std::string sqlFileName = - jsi_string_to_utf8(rt, args[0].asString(rt)); + const std::string sqlFileName = args[0].asString(rt).utf8(rt); return promisify( rt, thread_pool, @@ -617,10 +610,10 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { }); function_map["loadExtension"] = HFN(this) { - auto path = jsi_string_to_utf8(rt, args[0].asString(rt)); + auto path = args[0].asString(rt).utf8(rt); std::string entry_point; if (count > 1 && args[1].isString()) { - entry_point = jsi_string_to_utf8(rt, args[1].asString(rt)); + entry_point = args[1].asString(rt).utf8(rt); } opsqlite_load_extension(db, path, entry_point); @@ -631,7 +624,7 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { auto query = args[0].asObject(rt); const std::string query_str = - jsi_string_to_utf8(rt, query.getProperty(rt, "query").asString(rt)); + query.getProperty(rt, "query").asString(rt).utf8(rt); auto js_args = query.getProperty(rt, "arguments"); auto js_discriminators = query.getProperty(rt, "fireOn").asObject(rt).asArray(rt); @@ -648,10 +641,8 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { for (size_t i = 0; i < js_discriminators.length(rt); i++) { auto js_discriminator = js_discriminators.getValueAtIndex(rt, i).asObject(rt); - std::string table = - jsi_string_to_utf8(rt, - js_discriminator.getProperty(rt, "table") - .asString(rt)); + std::string table = + js_discriminator.getProperty(rt, "table").asString(rt).utf8(rt); std::vector ids; if (js_discriminator.hasProperty(rt, "ids")) { auto js_ids = @@ -687,7 +678,7 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { #endif function_map["prepareStatement"] = HFN(this) { - auto query = jsi_string_to_utf8(rt, args[0].asString(rt)); + auto query = args[0].asString(rt).utf8(rt); #ifdef OP_SQLITE_USE_LIBSQL libsql_stmt_t statement = opsqlite_libsql_prepare_statement(db, query); #else @@ -709,7 +700,7 @@ void DBHostObject::create_jsi_functions(jsi::Runtime &rt) { "[op-sqlite][open] database location must be a string"); } - std::string last_path = jsi_string_to_utf8(rt, args[0].asString(rt)); + std::string last_path = args[0].asString(rt).utf8(rt); if (last_path == ":memory:") { path = ":memory:"; diff --git a/cpp/OPSqlite.cpp b/cpp/OPSqlite.cpp index 23780590..9264bb51 100644 --- a/cpp/OPSqlite.cpp +++ b/cpp/OPSqlite.cpp @@ -56,22 +56,18 @@ void install(jsi::Runtime &rt, auto open = HFN0 { jsi::Object options = args[0].asObject(rt); - std::string name = - jsi_string_to_utf8(rt, options.getProperty(rt, "name").asString(rt)); + std::string name = options.getProperty(rt, "name").asString(rt).utf8(rt); std::string path = std::string(_base_path); std::string location; std::string encryption_key; if (options.hasProperty(rt, "location")) { - location = jsi_string_to_utf8( - rt, options.getProperty(rt, "location").asString(rt)); + location = options.getProperty(rt, "location").asString(rt).utf8(rt); } if (options.hasProperty(rt, "encryptionKey")) { encryption_key = - jsi_string_to_utf8(rt, - options.getProperty(rt, "encryptionKey") - .asString(rt)); + options.getProperty(rt, "encryptionKey").asString(rt).utf8(rt); } if (!location.empty()) { @@ -126,12 +122,10 @@ void install(jsi::Runtime &rt, auto open_remote = HFN(=) { jsi::Object options = args[0].asObject(rt); - std::string url = - jsi_string_to_utf8(rt, options.getProperty(rt, "url").asString(rt)); + std::string url = options.getProperty(rt, "url").asString(rt).utf8(rt); std::string auth_token = - jsi_string_to_utf8(rt, - options.getProperty(rt, "authToken").asString(rt)); + options.getProperty(rt, "authToken").asString(rt).utf8(rt); #ifdef OP_SQLITE_USE_LIBSQL std::shared_ptr db = @@ -149,14 +143,11 @@ void install(jsi::Runtime &rt, auto open_sync = HFN(=) { jsi::Object options = args[0].asObject(rt); - std::string name = - jsi_string_to_utf8(rt, options.getProperty(rt, "name").asString(rt)); + std::string name = options.getProperty(rt, "name").asString(rt).utf8(rt); std::string path = std::string(_base_path); - std::string url = - jsi_string_to_utf8(rt, options.getProperty(rt, "url").asString(rt)); + std::string url = options.getProperty(rt, "url").asString(rt).utf8(rt); std::string auth_token = - jsi_string_to_utf8(rt, - options.getProperty(rt, "authToken").asString(rt)); + options.getProperty(rt, "authToken").asString(rt).utf8(rt); int sync_interval = 0; if (options.hasProperty(rt, "libsqlSyncInterval")) { @@ -172,23 +163,18 @@ void install(jsi::Runtime &rt, std::string encryption_key; if (options.hasProperty(rt, "encryptionKey")) { encryption_key = - jsi_string_to_utf8(rt, - options.getProperty(rt, "encryptionKey") - .asString(rt)); + options.getProperty(rt, "encryptionKey").asString(rt).utf8(rt); } std::string remote_encryption_key; if (options.hasProperty(rt, "remoteEncryptionKey")) { remote_encryption_key = - jsi_string_to_utf8(rt, - options.getProperty(rt, "remoteEncryptionKey") - .asString(rt)); + options.getProperty(rt, "remoteEncryptionKey").asString(rt).utf8(rt); } std::string location; if (options.hasProperty(rt, "location")) { - location = jsi_string_to_utf8( - rt, options.getProperty(rt, "location").asString(rt)); + location = options.getProperty(rt, "location").asString(rt).utf8(rt); } if (!location.empty()) { if (location == ":memory:") { diff --git a/cpp/utils.cpp b/cpp/utils.cpp index eac75aa3..086bd3e0 100644 --- a/cpp/utils.cpp +++ b/cpp/utils.cpp @@ -93,7 +93,7 @@ inline JSVariant to_variant(jsi::Runtime &rt, const jsi::Value &value) { return JSVariant(doubleVal); } } else if (value.isString()) { - std::string strVal = jsi_string_to_utf8(rt, value.asString(rt)); + std::string strVal = value.asString(rt).utf8(rt); return JSVariant(strVal); } else if (value.isObject()) { auto obj = value.asObject(rt); @@ -145,8 +145,7 @@ std::vector to_string_vec(jsi::Runtime &rt, jsi::Value const &xs) { jsi::Array values = xs.asObject(rt).asArray(rt); std::vector res; for (int ii = 0; ii < values.length(rt); ii++) { - std::string value = - jsi_string_to_utf8(rt, values.getValueAtIndex(rt, ii).asString(rt)); + std::string value = values.getValueAtIndex(rt, ii).asString(rt).utf8(rt); res.emplace_back(value); } return res; @@ -267,8 +266,7 @@ void to_batch_arguments(jsi::Runtime &rt, jsi::Array const &tuples, continue; } - const std::string query = - jsi_string_to_utf8(rt, tuple.getValueAtIndex(rt, 0).asString(rt)); + const std::string query = tuple.getValueAtIndex(rt, 0).asString(rt).utf8(rt); if (length == 1) { commands->push_back({query}); continue; diff --git a/cpp/utils.hpp b/cpp/utils.hpp index f7d40ed5..12627aa3 100644 --- a/cpp/utils.hpp +++ b/cpp/utils.hpp @@ -19,31 +19,6 @@ namespace opsqlite { namespace jsi = facebook::jsi; namespace react = facebook::react; -struct JSIStringDataAppender { - std::string *out; - - void operator()(bool ascii, const void *data, size_t num) const { - if (ascii) { - out->append(static_cast(data), num); - return; - } - - const auto *u16 = static_cast(data); - out->reserve(out->size() + num); - for (size_t i = 0; i < num; i++) { - out->push_back(static_cast(u16[i])); - } - } -}; - -inline std::string jsi_string_to_utf8(jsi::Runtime &rt, - const jsi::String &value) { - std::string result; - JSIStringDataAppender cb{&result}; - value.getStringData(rt, cb); - return result; -} - jsi::Value to_jsi(jsi::Runtime &rt, const JSVariant &value); JSVariant to_variant(jsi::Runtime &rt, jsi::Value const &value); From 9a71866c959db86d968cc7ffcb0ab3e5c92fec51 Mon Sep 17 00:00:00 2001 From: Oscar Franco Date: Sun, 14 Jun 2026 09:33:36 -0400 Subject: [PATCH 3/3] TS fix --- example/src/tests/queries.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/example/src/tests/queries.ts b/example/src/tests/queries.ts index 9c3f92de..62332b87 100644 --- a/example/src/tests/queries.ts +++ b/example/src/tests/queries.ts @@ -300,7 +300,7 @@ describe("Queries tests", () => { ]); }); - it("Preserves non-ASCII strings when binding params (issue #417)", async () => { + it("Preserves non-ASCII strings when binding params", async () => { const value = JSON.stringify({ bullet: "Kimball Wildlife Refuge • Burlingame State Park", smartQuotes: "John “Jack” Doe", @@ -314,9 +314,13 @@ describe("Queries tests", () => { expect(result.rows[0]!.value).toEqual(value); const hexResult = await db.execute("SELECT hex(value) AS valueHex FROM UnicodeRepro;"); - expect(hexResult.rows[0]!.valueHex.includes("E280A2")).toEqual(true); - expect(hexResult.rows[0]!.valueHex.includes("E2809C")).toEqual(true); - expect(hexResult.rows[0]!.valueHex.includes("E2809D")).toEqual(true); + const valueHex = hexResult.rows[0]?.valueHex; + if (typeof valueHex !== "string") { + throw new Error("Expected hex(value) to return a string"); + } + expect(valueHex.includes("E280A2")).toEqual(true); + expect(valueHex.includes("E2809C")).toEqual(true); + expect(valueHex.includes("E2809D")).toEqual(true); }); it("Query with sqlite functions", async () => {