mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-18 17:14:20 +01:00
This PR fixes 3 tests that check param binding and iteration. ----- as part of https://github.com/tursodatabase/turso/issues/1900 Closes #2097
491 lines
13 KiB
JavaScript
491 lines
13 KiB
JavaScript
import crypto from 'crypto';
|
|
import fs from 'fs';
|
|
import DualTest from "./dual-test.mjs";
|
|
|
|
const dualTest = new DualTest();
|
|
|
|
new DualTest(":memory:").both("Open in-memory database", async (t) => {
|
|
const db = t.context.db;
|
|
t.is(db.memory, true);
|
|
});
|
|
|
|
dualTest.beforeEach(async (t) => {
|
|
const db = t.context.db;
|
|
|
|
db.exec(`
|
|
DROP TABLE IF EXISTS users;
|
|
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)
|
|
`);
|
|
db.exec(
|
|
"INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"
|
|
);
|
|
db.exec(
|
|
"INSERT INTO users (id, name, email) VALUES (2, 'Bob', 'bob@example.com')"
|
|
);
|
|
});
|
|
|
|
dualTest.onlySqlitePasses("Statement.prepare() error", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
t.throws(() => {
|
|
return db.prepare("SYNTAX ERROR");
|
|
}, {
|
|
any: true,
|
|
instanceOf: t.context.errorType,
|
|
message: 'near "SYNTAX": syntax error'
|
|
});
|
|
});
|
|
|
|
dualTest.both("Statement.run() returning rows", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
const stmt = db.prepare("SELECT 1");
|
|
const info = stmt.run();
|
|
t.is(info.changes, 0);
|
|
});
|
|
|
|
dualTest.both("Statement.run() [positional]", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
const stmt = db.prepare("INSERT INTO users(name, email) VALUES (?, ?)");
|
|
const info = stmt.run(["Carol", "carol@example.net"]);
|
|
t.is(info.changes, 1);
|
|
t.is(info.lastInsertRowid, 3);
|
|
|
|
// Verify that the data is inserted
|
|
const stmt2 = db.prepare("SELECT * FROM users WHERE id = 3");
|
|
t.is(stmt2.get().name, "Carol");
|
|
t.is(stmt2.get().email, "carol@example.net");
|
|
});
|
|
|
|
dualTest.both("Statement.run() [named]", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
const stmt = db.prepare("INSERT INTO users(name, email) VALUES (@name, @email);");
|
|
const info = stmt.run({ "name": "Carol", "email": "carol@example.net" });
|
|
t.is(info.changes, 1);
|
|
t.is(info.lastInsertRowid, 3);
|
|
});
|
|
|
|
dualTest.both("Statement.get() returns no rows", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
const stmt = db.prepare("SELECT * FROM users WHERE id = 0");
|
|
t.is(stmt.get(), undefined);
|
|
});
|
|
|
|
dualTest.both("Statement.get() [no parameters]", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
var stmt = 0;
|
|
|
|
stmt = db.prepare("SELECT * FROM users");
|
|
t.is(stmt.get().name, "Alice");
|
|
t.deepEqual(stmt.raw().get(), [1, 'Alice', 'alice@example.org']);
|
|
});
|
|
|
|
dualTest.both("Statement.get() [positional]", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
var stmt = 0;
|
|
|
|
stmt = db.prepare("SELECT * FROM users WHERE id = ?");
|
|
t.is(stmt.get(0), undefined);
|
|
t.is(stmt.get([0]), undefined);
|
|
t.is(stmt.get(1).name, "Alice");
|
|
t.is(stmt.get(2).name, "Bob");
|
|
|
|
stmt = db.prepare("SELECT * FROM users WHERE id = ?1");
|
|
t.is(stmt.get({ 1: 0 }), undefined);
|
|
t.is(stmt.get({ 1: 1 }).name, "Alice");
|
|
t.is(stmt.get({ 1: 2 }).name, "Bob");
|
|
});
|
|
|
|
dualTest.both("Statement.get() [named]", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
var stmt = undefined;
|
|
|
|
stmt = db.prepare("SELECT :b, :a");
|
|
t.deepEqual(stmt.raw().get({ a: 'a', b: 'b' }), ['b', 'a']);
|
|
|
|
stmt = db.prepare("SELECT * FROM users WHERE id = :id");
|
|
t.is(stmt.get({ id: 0 }), undefined);
|
|
t.is(stmt.get({ id: 1 }).name, "Alice");
|
|
t.is(stmt.get({ id: 2 }).name, "Bob");
|
|
|
|
stmt = db.prepare("SELECT * FROM users WHERE id = @id");
|
|
t.is(stmt.get({ id: 0 }), undefined);
|
|
t.is(stmt.get({ id: 1 }).name, "Alice");
|
|
t.is(stmt.get({ id: 2 }).name, "Bob");
|
|
|
|
stmt = db.prepare("SELECT * FROM users WHERE id = $id");
|
|
t.is(stmt.get({ id: 0 }), undefined);
|
|
t.is(stmt.get({ id: 1 }).name, "Alice");
|
|
t.is(stmt.get({ id: 2 }).name, "Bob");
|
|
});
|
|
|
|
dualTest.both("Statement.get() [raw]", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
const stmt = db.prepare("SELECT * FROM users WHERE id = ?");
|
|
t.deepEqual(stmt.raw().get(1), [1, "Alice", "alice@example.org"]);
|
|
});
|
|
|
|
dualTest.both("Statement.iterate() [empty]", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
const stmt = db.prepare("SELECT * FROM users WHERE id = 0");
|
|
t.is(stmt.iterate().next().done, true);
|
|
t.is(stmt.iterate([]).next().done, true);
|
|
t.is(stmt.iterate({}).next().done, true);
|
|
});
|
|
|
|
dualTest.both("Statement.iterate()", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
const stmt = db.prepare("SELECT * FROM users");
|
|
const expected = [1, 2];
|
|
var idx = 0;
|
|
for (const row of stmt.iterate()) {
|
|
t.is(row.id, expected[idx++]);
|
|
}
|
|
});
|
|
|
|
dualTest.both("Statement.all()", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
const stmt = db.prepare("SELECT * FROM users");
|
|
const expected = [
|
|
{ id: 1, name: "Alice", email: "alice@example.org" },
|
|
{ id: 2, name: "Bob", email: "bob@example.com" },
|
|
];
|
|
t.deepEqual(stmt.all(), expected);
|
|
});
|
|
|
|
dualTest.both("Statement.all() [raw]", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
const stmt = db.prepare("SELECT * FROM users");
|
|
const expected = [
|
|
[1, "Alice", "alice@example.org"],
|
|
[2, "Bob", "bob@example.com"],
|
|
];
|
|
t.deepEqual(stmt.raw().all(), expected);
|
|
});
|
|
|
|
dualTest.both("Statement.all() [pluck]", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
const stmt = db.prepare("SELECT * FROM users");
|
|
const expected = [
|
|
1,
|
|
2,
|
|
];
|
|
t.deepEqual(stmt.pluck().all(), expected);
|
|
});
|
|
|
|
dualTest.both(
|
|
"Statement.raw() [passing false should disable raw mode]",
|
|
async (t) => {
|
|
const db = t.context.db;
|
|
|
|
const stmt = db.prepare("SELECT * FROM users");
|
|
const expected = [
|
|
{ id: 1, name: "Alice", email: "alice@example.org" },
|
|
{ id: 2, name: "Bob", email: "bob@example.com" },
|
|
];
|
|
t.deepEqual(stmt.raw(false).all(), expected);
|
|
},
|
|
);
|
|
|
|
dualTest.both(
|
|
"Statement.pluck() [passing false should disable pluck mode]",
|
|
async (t) => {
|
|
const db = t.context.db;
|
|
|
|
const stmt = db.prepare("SELECT * FROM users");
|
|
const expected = [
|
|
{ id: 1, name: "Alice", email: "alice@example.org" },
|
|
{ id: 2, name: "Bob", email: "bob@example.com" },
|
|
];
|
|
t.deepEqual(stmt.pluck(false).all(), expected);
|
|
},
|
|
);
|
|
|
|
dualTest.onlySqlitePasses(
|
|
"Statement.all() [default safe integers]",
|
|
async (t) => {
|
|
const db = t.context.db;
|
|
db.defaultSafeIntegers();
|
|
const stmt = db.prepare("SELECT * FROM users");
|
|
const expected = [
|
|
[1n, "Alice", "alice@example.org"],
|
|
[2n, "Bob", "bob@example.com"],
|
|
];
|
|
t.deepEqual(stmt.raw().all(), expected);
|
|
},
|
|
);
|
|
|
|
dualTest.onlySqlitePasses(
|
|
"Statement.all() [statement safe integers]",
|
|
async (t) => {
|
|
const db = t.context.db;
|
|
const stmt = db.prepare("SELECT * FROM users");
|
|
stmt.safeIntegers();
|
|
const expected = [
|
|
[1n, "Alice", "alice@example.org"],
|
|
[2n, "Bob", "bob@example.com"],
|
|
];
|
|
t.deepEqual(stmt.raw().all(), expected);
|
|
},
|
|
);
|
|
|
|
dualTest.onlySqlitePasses("Statement.raw() [failure]", async (t) => {
|
|
const db = t.context.db;
|
|
const stmt = db.prepare("INSERT INTO users (id, name, email) VALUES (?, ?, ?)");
|
|
await t.throws(() => {
|
|
stmt.raw()
|
|
}, {
|
|
message: 'The raw() method is only for statements that return data'
|
|
});
|
|
});
|
|
|
|
dualTest.onlySqlitePasses("Statement.run() with array bind parameter", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
db.exec(`
|
|
DROP TABLE IF EXISTS t;
|
|
CREATE TABLE t (value BLOB);
|
|
`);
|
|
|
|
const array = [1, 2, 3];
|
|
|
|
const insertStmt = db.prepare("INSERT INTO t (value) VALUES (?)");
|
|
await t.throws(() => {
|
|
insertStmt.run([array]);
|
|
}, {
|
|
message: 'SQLite3 can only bind numbers, strings, bigints, buffers, and null'
|
|
});
|
|
});
|
|
|
|
dualTest.onlySqlitePasses("Statement.run() with Float32Array bind parameter", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
db.exec(`
|
|
DROP TABLE IF EXISTS t;
|
|
CREATE TABLE t (value BLOB);
|
|
`);
|
|
|
|
const array = new Float32Array([1, 2, 3]);
|
|
|
|
const insertStmt = db.prepare("INSERT INTO t (value) VALUES (?)");
|
|
insertStmt.run([array]);
|
|
|
|
const selectStmt = db.prepare("SELECT value FROM t");
|
|
t.deepEqual(selectStmt.raw().get()[0], Buffer.from(array.buffer));
|
|
});
|
|
|
|
/// This test is not supported by better-sqlite3, but is supported by libsql.
|
|
/// Therefore, when implementing it in Turso, only enable the test for Turso.
|
|
dualTest.skip("Statement.run() for vector feature with Float32Array bind parameter", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
db.exec(`
|
|
DROP TABLE IF EXISTS t;
|
|
CREATE TABLE t (embedding FLOAT32(8));
|
|
CREATE INDEX t_idx ON t ( libsql_vector_idx(embedding) );
|
|
`);
|
|
|
|
const insertStmt = db.prepare("INSERT INTO t VALUES (?)");
|
|
insertStmt.run([new Float32Array([1, 1, 1, 1, 1, 1, 1, 1])]);
|
|
insertStmt.run([new Float32Array([-1, -1, -1, -1, -1, -1, -1, -1])]);
|
|
|
|
const selectStmt = db.prepare("SELECT embedding FROM vector_top_k('t_idx', vector('[2,2,2,2,2,2,2,2]'), 1) n JOIN t ON n.rowid = t.rowid");
|
|
t.deepEqual(selectStmt.raw().get()[0], Buffer.from(new Float32Array([1, 1, 1, 1, 1, 1, 1, 1]).buffer));
|
|
|
|
// we need to explicitly delete this table because later when sqlite-based (not LibSQL) tests will delete table 't' they will leave 't_idx_shadow' table untouched
|
|
db.exec(`DROP TABLE t`);
|
|
});
|
|
|
|
dualTest.onlySqlitePasses("Statement.columns()", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
var stmt = undefined;
|
|
|
|
stmt = db.prepare("SELECT 1");
|
|
t.deepEqual(stmt.columns(), [
|
|
{
|
|
column: null,
|
|
database: null,
|
|
name: '1',
|
|
table: null,
|
|
type: null,
|
|
},
|
|
]);
|
|
|
|
stmt = db.prepare("SELECT * FROM users WHERE id = ?");
|
|
t.deepEqual(stmt.columns(), [
|
|
{
|
|
column: "id",
|
|
database: "main",
|
|
name: "id",
|
|
table: "users",
|
|
type: "INTEGER",
|
|
},
|
|
{
|
|
column: "name",
|
|
database: "main",
|
|
name: "name",
|
|
table: "users",
|
|
type: "TEXT",
|
|
},
|
|
{
|
|
column: "email",
|
|
database: "main",
|
|
name: "email",
|
|
table: "users",
|
|
type: "TEXT",
|
|
},
|
|
]);
|
|
});
|
|
|
|
dualTest.onlySqlitePasses("Database.transaction()", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
const insert = db.prepare(
|
|
"INSERT INTO users(name, email) VALUES (:name, :email)"
|
|
);
|
|
|
|
const insertMany = db.transaction((users) => {
|
|
t.is(db.inTransaction, true);
|
|
for (const user of users) insert.run(user);
|
|
});
|
|
|
|
t.is(db.inTransaction, false);
|
|
insertMany([
|
|
{ name: "Joey", email: "joey@example.org" },
|
|
{ name: "Sally", email: "sally@example.org" },
|
|
{ name: "Junior", email: "junior@example.org" },
|
|
]);
|
|
t.is(db.inTransaction, false);
|
|
|
|
const stmt = db.prepare("SELECT * FROM users WHERE id = ?");
|
|
t.is(stmt.get(3).name, "Joey");
|
|
t.is(stmt.get(4).name, "Sally");
|
|
t.is(stmt.get(5).name, "Junior");
|
|
});
|
|
|
|
dualTest.onlySqlitePasses("Database.transaction().immediate()", async (t) => {
|
|
const db = t.context.db;
|
|
const insert = db.prepare(
|
|
"INSERT INTO users(name, email) VALUES (:name, :email)"
|
|
);
|
|
const insertMany = db.transaction((users) => {
|
|
t.is(db.inTransaction, true);
|
|
for (const user of users) insert.run(user);
|
|
});
|
|
t.is(db.inTransaction, false);
|
|
insertMany.immediate([
|
|
{ name: "Joey", email: "joey@example.org" },
|
|
{ name: "Sally", email: "sally@example.org" },
|
|
{ name: "Junior", email: "junior@example.org" },
|
|
]);
|
|
t.is(db.inTransaction, false);
|
|
});
|
|
|
|
dualTest.onlySqlitePasses("values", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
const stmt = db.prepare("SELECT ?").raw();
|
|
t.deepEqual(stmt.get(1), [1]);
|
|
t.deepEqual(stmt.get(Number.MIN_VALUE), [Number.MIN_VALUE]);
|
|
t.deepEqual(stmt.get(Number.MAX_VALUE), [Number.MAX_VALUE]);
|
|
t.deepEqual(stmt.get(Number.MAX_SAFE_INTEGER), [Number.MAX_SAFE_INTEGER]);
|
|
t.deepEqual(stmt.get(9007199254740991n), [9007199254740991]);
|
|
});
|
|
|
|
dualTest.both("Database.pragma()", async (t) => {
|
|
const db = t.context.db;
|
|
db.pragma("cache_size = 2000");
|
|
t.deepEqual(db.pragma("cache_size"), [{ "cache_size": 2000 }]);
|
|
});
|
|
|
|
dualTest.both("errors", async (t) => {
|
|
const db = t.context.db;
|
|
|
|
const syntaxError = await t.throws(() => {
|
|
db.exec("SYNTAX ERROR");
|
|
}, {
|
|
any: true,
|
|
instanceOf: t.context.errorType,
|
|
message: /near "SYNTAX": syntax error/,
|
|
code: 'SQLITE_ERROR'
|
|
});
|
|
const noTableError = await t.throws(() => {
|
|
db.exec("SELECT * FROM missing_table");
|
|
}, {
|
|
any: true,
|
|
instanceOf: t.context.errorType,
|
|
message: /(Parse error: Table missing_table not found|no such table: missing_table)/,
|
|
code: 'SQLITE_ERROR'
|
|
});
|
|
|
|
if (t.context.provider === 'libsql') {
|
|
t.is(noTableError.rawCode, 1)
|
|
t.is(syntaxError.rawCode, 1)
|
|
}
|
|
});
|
|
|
|
dualTest.onlySqlitePasses("Database.prepare() after close()", async (t) => {
|
|
const db = t.context.db;
|
|
db.close();
|
|
t.throws(() => {
|
|
db.prepare("SELECT 1");
|
|
}, {
|
|
instanceOf: TypeError,
|
|
message: "The database connection is not open"
|
|
});
|
|
});
|
|
|
|
dualTest.onlySqlitePasses("Database.exec() after close()", async (t) => {
|
|
const db = t.context.db;
|
|
db.close();
|
|
t.throws(() => {
|
|
db.exec("SELECT 1");
|
|
}, {
|
|
instanceOf: TypeError,
|
|
message: "The database connection is not open"
|
|
});
|
|
});
|
|
|
|
/// Generate a unique database filename
|
|
const genDatabaseFilename = () => {
|
|
return `test-${crypto.randomBytes(8).toString('hex')}.db`;
|
|
};
|
|
|
|
new DualTest(genDatabaseFilename).onlySqlitePasses("Timeout option", async (t) => {
|
|
t.teardown(() => fs.unlinkSync(t.context.path));
|
|
|
|
const timeout = 1000;
|
|
const { db: conn1 } = t.context;
|
|
conn1.exec("CREATE TABLE t(x)");
|
|
conn1.exec("BEGIN IMMEDIATE");
|
|
conn1.exec("INSERT INTO t VALUES (1)")
|
|
const options = { timeout };
|
|
const conn2 = t.context.connect(t.context.path, options);
|
|
const start = Date.now();
|
|
try {
|
|
conn2.exec("INSERT INTO t VALUES (1)")
|
|
} catch (e) {
|
|
t.is(e.code, "SQLITE_BUSY");
|
|
const end = Date.now();
|
|
const elapsed = end - start;
|
|
// Allow some tolerance for the timeout.
|
|
t.is(elapsed > timeout / 2, true);
|
|
}
|
|
conn1.close();
|
|
conn2.close();
|
|
});
|
|
|