bind/js: Switch to napi v3

This commit is contained in:
Diego Reis
2025-07-24 18:18:54 -03:00
parent cb42102a6e
commit 0f95cf7751
14 changed files with 3812 additions and 2160 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,4 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.6.0.cjs
yarnPath: .yarn/releases/yarn-4.9.2.cjs
enableHardenedMode: false

View File

@@ -12,8 +12,8 @@ crate-type = ["cdylib"]
[dependencies]
turso_core = { workspace = true }
napi = { version = "2.16.17", default-features = false, features = ["napi4"] }
napi-derive = { version = "2.16.13", default-features = true }
napi = { version = "3.1.3", default-features = false }
napi-derive = { version = "3.1.1", default-features = true }
[build-dependencies]
napi-build = "2.2.0"
napi-build = "2.2.3"

View File

@@ -1,7 +1,7 @@
import crypto from 'crypto';
import crypto from "crypto";
import fs from "node:fs";
import { fileURLToPath } from "url";
import path from "node:path"
import path from "node:path";
import DualTest from "./dual-test.mjs";
const inMemoryTest = new DualTest(":memory:");
@@ -22,23 +22,30 @@ foobarTest.both("Property .name of database", async (t) => {
t.is(db.name, t.context.path);
});
new DualTest("foobar.db", { readonly: true })
.both("Property .readonly of database if set", async (t) => {
new DualTest("foobar.db", { readonly: true }).both(
"Property .readonly of database if set",
async (t) => {
const db = t.context.db;
t.is(db.readonly, true);
});
},
);
const genDatabaseFilename = () => {
return `test-${crypto.randomBytes(8).toString('hex')}.db`;
return `test-${crypto.randomBytes(8).toString("hex")}.db`;
};
new DualTest().both("opening a read-only database fails if the file doesn't exist", async (t) => {
t.throws(() => t.context.connect(genDatabaseFilename(), { readonly: true }),
{
any: true,
code: 'SQLITE_CANTOPEN',
});
})
new DualTest().both(
"opening a read-only database fails if the file doesn't exist",
async (t) => {
t.throws(
() => t.context.connect(genDatabaseFilename(), { readonly: true }),
{
any: true,
code: "SQLITE_CANTOPEN",
},
);
},
);
foobarTest.both("Property .readonly of database if not set", async (t) => {
const db = t.context.db;
@@ -59,55 +66,64 @@ inMemoryTest.both("Statement.get() returns data", async (t) => {
t.is(result2["1"], 1);
});
inMemoryTest.both("Statement.get() returns undefined when no data", async (t) => {
const db = t.context.db;
const stmt = db.prepare("SELECT 1 WHERE 1 = 2");
const result = stmt.get();
t.is(result, undefined);
});
inMemoryTest.both(
"Statement.get() returns undefined when no data",
async (t) => {
const db = t.context.db;
const stmt = db.prepare("SELECT 1 WHERE 1 = 2");
const result = stmt.get();
t.is(result, undefined);
},
);
inMemoryTest.both("Statement.run() returns correct result object", async (t) => {
const db = t.context.db;
db.prepare("CREATE TABLE users (name TEXT)").run();
const rows = db.prepare("INSERT INTO users (name) VALUES (?)").run("Alice");
t.deepEqual(rows, { changes: 1, lastInsertRowid: 1 });
});
inMemoryTest.both(
"Statement.run() returns correct result object",
async (t) => {
const db = t.context.db;
db.prepare("CREATE TABLE users (name TEXT)").run();
const rows = db.prepare("INSERT INTO users (name) VALUES (?)").run("Alice");
t.deepEqual(rows, { changes: 1, lastInsertRowid: 1 });
},
);
inMemoryTest.both("Statment.iterate() should correctly return an iterable object", async (t) => {
const db = t.context.db;
db.prepare(
"CREATE TABLE users (name TEXT, age INTEGER, nationality TEXT)",
).run();
db.prepare("INSERT INTO users (name, age, nationality) VALUES (?, ?, ?)").run(
["Alice", 42],
"UK",
);
db.prepare("INSERT INTO users (name, age, nationality) VALUES (?, ?, ?)").run(
"Bob",
24,
"USA",
);
inMemoryTest.onlySqlitePasses(
"Statment.iterate() should correctly return an iterable object",
async (t) => {
const db = t.context.db;
db.prepare(
"CREATE TABLE users (name TEXT, age INTEGER, nationality TEXT)",
).run();
db.prepare(
"INSERT INTO users (name, age, nationality) VALUES (?, ?, ?)",
).run(["Alice", 42], "UK");
db.prepare(
"INSERT INTO users (name, age, nationality) VALUES (?, ?, ?)",
).run("Bob", 24, "USA");
let rows = db.prepare("SELECT * FROM users").iterate();
for (const row of rows) {
t.truthy(row.name);
t.truthy(row.nationality);
t.true(typeof row.age === "number");
}
});
let rows = db.prepare("SELECT * FROM users").iterate();
for (const row of rows) {
t.truthy(row.name);
t.truthy(row.nationality);
t.true(typeof row.age === "number");
}
},
);
inMemoryTest.both("Empty prepared statement should throw the correct error", async (t) => {
const db = t.context.db;
t.throws(
() => {
db.prepare("");
},
{
instanceOf: RangeError,
message: "The supplied SQL string contains no statements",
},
);
});
inMemoryTest.both(
"Empty prepared statement should throw the correct error",
async (t) => {
const db = t.context.db;
t.throws(
() => {
db.prepare("");
},
{
instanceOf: RangeError,
message: "The supplied SQL string contains no statements",
},
);
},
);
inMemoryTest.both("Test pragma()", async (t) => {
const db = t.context.db;
@@ -129,8 +145,22 @@ inMemoryTest.both("pragma table_list", async (t) => {
let expectedValue = [
{ cid: 0, name: "type", type: "TEXT", notnull: 0, dflt_value: null, pk: 0 },
{ cid: 1, name: "name", type: "TEXT", notnull: 0, dflt_value: null, pk: 0 },
{ cid: 2, name: "tbl_name", type: "TEXT", notnull: 0, dflt_value: null, pk: 0 },
{ cid: 3, name: "rootpage", type: "INT", notnull: 0, dflt_value: null, pk: 0 },
{
cid: 2,
name: "tbl_name",
type: "TEXT",
notnull: 0,
dflt_value: null,
pk: 0,
},
{
cid: 3,
name: "rootpage",
type: "INT",
notnull: 0,
dflt_value: null,
pk: 0,
},
{ cid: 4, name: "sql", type: "TEXT", notnull: 0, dflt_value: null, pk: 0 },
];
t.deepEqual(actual, expectedValue);
@@ -144,236 +174,272 @@ inMemoryTest.both("simple pragma table_list", async (t) => {
t.deepEqual(actual, expectedValue);
});
inMemoryTest.both("Statement shouldn't bind twice with bind()", async (t) => {
const db = t.context.db;
db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run();
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 42);
let stmt = db.prepare("SELECT * FROM users WHERE name = ?").bind("Alice");
inMemoryTest.onlySqlitePasses(
"Statement shouldn't bind twice with bind()",
async (t) => {
const db = t.context.db;
db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run();
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 42);
let stmt = db.prepare("SELECT * FROM users WHERE name = ?").bind("Alice");
for (const row of stmt.iterate()) {
let row = stmt.get();
t.truthy(row.name);
t.true(typeof row.age === "number");
}
t.throws(
() => {
stmt.bind("Bob");
},
{
instanceOf: TypeError,
message: 'The bind() method can only be invoked once per statement object',
},
);
});
t.throws(
() => {
stmt.bind("Bob");
},
{
instanceOf: TypeError,
message:
"The bind() method can only be invoked once per statement object",
},
);
},
);
inMemoryTest.both("Test pluck(): Rows should only have the values of the first column", async (t) => {
const db = t.context.db;
db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run();
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 42);
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Bob", 24);
inMemoryTest.both(
"Test pluck(): Rows should only have the values of the first column",
async (t) => {
const db = t.context.db;
db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run();
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 42);
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Bob", 24);
let stmt = db.prepare("SELECT * FROM users").pluck();
let stmt = db.prepare("SELECT * FROM users").pluck();
for (const row of stmt.iterate()) {
t.truthy(row);
t.assert(typeof row === "string");
}
});
for (const row of stmt.all()) {
t.truthy(row);
t.assert(typeof row === "string");
}
},
);
inMemoryTest.both("Test raw(): Rows should be returned as arrays", async (t) => {
const db = t.context.db;
db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run();
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 42);
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Bob", 24);
inMemoryTest.both(
"Test raw(): Rows should be returned as arrays",
async (t) => {
const db = t.context.db;
db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run();
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 42);
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Bob", 24);
let stmt = db.prepare("SELECT * FROM users").raw();
let stmt = db.prepare("SELECT * FROM users").raw();
for (const row of stmt.all()) {
t.true(Array.isArray(row));
t.true(typeof row[0] === "string");
t.true(typeof row[1] === "number");
}
for (const row of stmt.iterate()) {
stmt = db.prepare("SELECT * FROM users WHERE name = ?").raw();
const row = stmt.get("Alice");
t.true(Array.isArray(row));
t.true(typeof row[0] === "string");
t.true(typeof row[1] === "number");
}
t.is(row.length, 2);
t.is(row[0], "Alice");
t.is(row[1], 42);
stmt = db.prepare("SELECT * FROM users WHERE name = ?").raw();
const row = stmt.get("Alice");
t.true(Array.isArray(row));
t.is(row.length, 2);
t.is(row[0], "Alice");
t.is(row[1], 42);
const noRow = stmt.get("Charlie");
t.is(noRow, undefined);
const noRow = stmt.get("Charlie");
t.is(noRow, undefined);
stmt = db.prepare("SELECT * FROM users").raw();
const rows = stmt.all();
t.true(Array.isArray(rows));
t.is(rows.length, 2);
t.deepEqual(rows[0], ["Alice", 42]);
t.deepEqual(rows[1], ["Bob", 24]);
},
);
stmt = db.prepare("SELECT * FROM users").raw();
const rows = stmt.all();
t.true(Array.isArray(rows));
t.is(rows.length, 2);
t.deepEqual(rows[0], ["Alice", 42]);
t.deepEqual(rows[1], ["Bob", 24]);
});
inMemoryTest.onlySqlitePasses(
"Test expand(): Columns should be namespaced",
async (t) => {
const expandedResults = [
{
users: {
name: "Alice",
type: "premium",
},
addresses: {
userName: "Alice",
type: "home",
street: "Alice's street",
},
},
{
users: {
name: "Bob",
type: "basic",
},
addresses: {
userName: "Bob",
type: "work",
street: "Bob's street",
},
},
];
inMemoryTest.onlySqlitePasses("Test expand(): Columns should be namespaced", async (t) => {
const expandedResults = [
{
users: {
let regularResults = [
{
name: "Alice",
type: "premium",
},
addresses: {
userName: "Alice",
type: "home",
street: "Alice's street",
type: "home",
userName: "Alice",
},
},
{
users: {
{
name: "Bob",
type: "basic",
},
addresses: {
userName: "Bob",
type: "work",
street: "Bob's street",
type: "work",
userName: "Bob",
},
},
];
];
let regularResults = [
{
name: "Alice",
street: "Alice's street",
type: "home",
userName: "Alice",
},
{
name: "Bob",
street: "Bob's street",
type: "work",
userName: "Bob",
},
];
const db = t.context.db;
db.prepare("CREATE TABLE users (name TEXT, type TEXT)").run();
db.prepare(
"CREATE TABLE addresses (userName TEXT, street TEXT, type TEXT)",
).run();
db.prepare("INSERT INTO users (name, type) VALUES (?, ?)").run(
"Alice",
"premium",
);
db.prepare("INSERT INTO users (name, type) VALUES (?, ?)").run(
"Bob",
"basic",
);
db.prepare(
"INSERT INTO addresses (userName, street, type) VALUES (?, ?, ?)",
).run("Alice", "Alice's street", "home");
db.prepare(
"INSERT INTO addresses (userName, street, type) VALUES (?, ?, ?)",
).run("Bob", "Bob's street", "work");
const db = t.context.db;
db.prepare("CREATE TABLE users (name TEXT, type TEXT)").run();
db.prepare("CREATE TABLE addresses (userName TEXT, street TEXT, type TEXT)")
.run();
db.prepare("INSERT INTO users (name, type) VALUES (?, ?)")
.run("Alice", "premium");
db.prepare("INSERT INTO users (name, type) VALUES (?, ?)")
.run("Bob", "basic");
db.prepare("INSERT INTO addresses (userName, street, type) VALUES (?, ?, ?)")
.run("Alice", "Alice's street", "home");
db.prepare("INSERT INTO addresses (userName, street, type) VALUES (?, ?, ?)")
.run("Bob", "Bob's street", "work");
let allRows = db
.prepare(
"SELECT * FROM users u JOIN addresses a ON (u.name = a.userName)",
)
.expand(true)
.all();
let allRows = db
.prepare("SELECT * FROM users u JOIN addresses a ON (u.name = a.userName)")
.expand(true)
.all();
t.deepEqual(allRows, expandedResults);
t.deepEqual(allRows, expandedResults);
allRows = db
.prepare(
"SELECT * FROM users u JOIN addresses a ON (u.name = a.userName)",
)
.expand()
.all();
allRows = db
.prepare("SELECT * FROM users u JOIN addresses a ON (u.name = a.userName)")
.expand()
.all();
t.deepEqual(allRows, expandedResults);
t.deepEqual(allRows, expandedResults);
allRows = db
.prepare(
"SELECT * FROM users u JOIN addresses a ON (u.name = a.userName)",
)
.expand(false)
.all();
allRows = db
.prepare("SELECT * FROM users u JOIN addresses a ON (u.name = a.userName)")
.expand(false)
.all();
t.deepEqual(allRows, regularResults);
},
);
t.deepEqual(allRows, regularResults);
});
inMemoryTest.both(
"Presentation modes should be mutually exclusive",
async (t) => {
const db = t.context.db;
db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run();
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 42);
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Bob", 24);
inMemoryTest.both("Presentation modes should be mutually exclusive", async (t) => {
const db = t.context.db;
db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run();
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 42);
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Bob", 24);
// test raw()
let stmt = db.prepare("SELECT * FROM users").pluck().raw();
for (const row of stmt.all()) {
t.true(Array.isArray(row));
t.true(typeof row[0] === "string");
t.true(typeof row[1] === "number");
}
// test raw()
let stmt = db.prepare("SELECT * FROM users").pluck().raw();
for (const row of stmt.iterate()) {
stmt = db.prepare("SELECT * FROM users WHERE name = ?").raw();
const row = stmt.get("Alice");
t.true(Array.isArray(row));
t.true(typeof row[0] === "string");
t.true(typeof row[1] === "number");
}
t.is(row.length, 2);
t.is(row[0], "Alice");
t.is(row[1], 42);
stmt = db.prepare("SELECT * FROM users WHERE name = ?").raw();
const row = stmt.get("Alice");
t.true(Array.isArray(row));
t.is(row.length, 2);
t.is(row[0], "Alice");
t.is(row[1], 42);
const noRow = stmt.get("Charlie");
t.is(noRow, undefined);
const noRow = stmt.get("Charlie");
t.is(noRow, undefined);
stmt = db.prepare("SELECT * FROM users").raw();
let rows = stmt.all();
t.true(Array.isArray(rows));
t.is(rows.length, 2);
t.deepEqual(rows[0], ["Alice", 42]);
t.deepEqual(rows[1], ["Bob", 24]);
stmt = db.prepare("SELECT * FROM users").raw();
let rows = stmt.all();
t.true(Array.isArray(rows));
t.is(rows.length, 2);
t.deepEqual(rows[0], ["Alice", 42]);
t.deepEqual(rows[1], ["Bob", 24]);
// test pluck()
stmt = db.prepare("SELECT * FROM users").raw().pluck();
// test pluck()
stmt = db.prepare("SELECT * FROM users").raw().pluck();
for (const name of stmt.all()) {
t.truthy(name);
t.assert(typeof name === "string");
}
},
);
for (const name of stmt.iterate()) {
t.truthy(name);
t.assert(typeof name === "string");
}
});
inMemoryTest.onlySqlitePasses(
"Presentation mode 'expand' should be mutually exclusive",
async (t) => {
// this test can be appended to the previous one when 'expand' is implemented in Turso
const db = t.context.db;
db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run();
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 42);
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Bob", 24);
inMemoryTest.onlySqlitePasses("Presentation mode 'expand' should be mutually exclusive", async (t) => {
// this test can be appended to the previous one when 'expand' is implemented in Turso
const db = t.context.db;
db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run();
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 42);
db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Bob", 24);
let stmt = db.prepare("SELECT * FROM users").pluck().raw();
let stmt = db.prepare("SELECT * FROM users").pluck().raw();
// test expand()
stmt = db.prepare("SELECT * FROM users").raw().pluck().expand();
const rows = stmt.all();
t.true(Array.isArray(rows));
t.is(rows.length, 2);
t.deepEqual(rows[0], { users: { name: "Alice", age: 42 } });
t.deepEqual(rows[1], { users: { name: "Bob", age: 24 } });
},
);
// test expand()
stmt = db.prepare("SELECT * FROM users").raw().pluck().expand();
const rows = stmt.all();
t.true(Array.isArray(rows));
t.is(rows.length, 2);
t.deepEqual(rows[0], { users: { name: "Alice", age: 42 } });
t.deepEqual(rows[1], { users: { name: "Bob", age: 24 } });
})
inMemoryTest.both(
"Test exec(): Should correctly load multiple statements from file",
async (t) => {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
inMemoryTest.both("Test exec(): Should correctly load multiple statements from file", async (t) => {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const db = t.context.db;
const file = fs.readFileSync(
path.resolve(__dirname, "./artifacts/basic-test.sql"),
"utf8",
);
db.exec(file);
let rows = db.prepare("SELECT * FROM users").all();
for (const row of rows) {
t.truthy(row.name);
t.true(typeof row.age === "number");
}
},
);
const db = t.context.db;
const file = fs.readFileSync(path.resolve(__dirname, "./artifacts/basic-test.sql"), "utf8");
db.exec(file);
let rows = db.prepare("SELECT * FROM users").iterate();
for (const row of rows) {
t.truthy(row.name);
t.true(typeof row.age === "number");
}
});
inMemoryTest.both(
"Test Statement.database gets the database object",
async (t) => {
const db = t.context.db;
let stmt = db.prepare("SELECT 1");
t.is(stmt.database, db);
},
);
inMemoryTest.both("Test Statement.database gets the database object", async t => {
const db = t.context.db;
let stmt = db.prepare("SELECT 1");
t.is(stmt.database, db);
});
inMemoryTest.both("Test Statement.source", async t => {
inMemoryTest.both("Test Statement.source", async (t) => {
const db = t.context.db;
let sql = "CREATE TABLE t (id int)";
let stmt = db.prepare(sql);
t.is(stmt.source, sql);
});

View File

@@ -1,5 +1,5 @@
import crypto from 'crypto';
import fs from 'fs';
import crypto from "crypto";
import fs from "fs";
import DualTest from "./dual-test.mjs";
const dualTest = new DualTest();
@@ -17,23 +17,26 @@ dualTest.beforeEach(async (t) => {
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')"
"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')"
"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'
});
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) => {
@@ -61,8 +64,10 @@ dualTest.both("Statement.run() [positional]", async (t) => {
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" });
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);
});
@@ -81,7 +86,7 @@ dualTest.both("Statement.get() [no parameters]", async (t) => {
stmt = db.prepare("SELECT * FROM users");
t.is(stmt.get().name, "Alice");
t.deepEqual(stmt.raw().get(), [1, 'Alice', 'alice@example.org']);
t.deepEqual(stmt.raw().get(), [1, "Alice", "alice@example.org"]);
});
dualTest.both("Statement.get() [positional]", async (t) => {
@@ -107,7 +112,7 @@ dualTest.both("Statement.get() [named]", async (t) => {
var stmt = undefined;
stmt = db.prepare("SELECT :b, :a");
t.deepEqual(stmt.raw().get({ a: 'a', b: 'b' }), ['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);
@@ -132,7 +137,7 @@ dualTest.both("Statement.get() [raw]", async (t) => {
t.deepEqual(stmt.raw().get(1), [1, "Alice", "alice@example.org"]);
});
dualTest.both("Statement.iterate() [empty]", async (t) => {
dualTest.onlySqlitePasses("Statement.iterate() [empty]", async (t) => {
const db = t.context.db;
const stmt = db.prepare("SELECT * FROM users WHERE id = 0");
@@ -141,7 +146,7 @@ dualTest.both("Statement.iterate() [empty]", async (t) => {
t.is(stmt.iterate({}).next().done, true);
});
dualTest.both("Statement.iterate()", async (t) => {
dualTest.onlySqlitePasses("Statement.iterate()", async (t) => {
const db = t.context.db;
const stmt = db.prepare("SELECT * FROM users");
@@ -178,10 +183,7 @@ dualTest.both("Statement.all() [pluck]", async (t) => {
const db = t.context.db;
const stmt = db.prepare("SELECT * FROM users");
const expected = [
1,
2,
];
const expected = [1, 2];
t.deepEqual(stmt.pluck().all(), expected);
});
@@ -243,70 +245,93 @@ dualTest.onlySqlitePasses(
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'
});
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;
dualTest.onlySqlitePasses(
"Statement.run() with array bind parameter",
async (t) => {
const db = t.context.db;
db.exec(`
db.exec(`
DROP TABLE IF EXISTS t;
CREATE TABLE t (value BLOB);
`);
const array = [1, 2, 3];
const array = [1, 2, 3];
const insertStmt = db.prepare("INSERT INTO t (value) VALUES (?)");
await t.throws(() => {
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]);
}, {
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));
});
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;
dualTest.skip(
"Statement.run() for vector feature with Float32Array bind parameter",
async (t) => {
const db = t.context.db;
db.exec(`
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 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));
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`);
});
// 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;
@@ -318,7 +343,7 @@ dualTest.onlySqlitePasses("Statement.columns()", async (t) => {
{
column: null,
database: null,
name: '1',
name: "1",
table: null,
type: null,
},
@@ -354,7 +379,7 @@ dualTest.onlySqlitePasses("Database.transaction()", async (t) => {
const db = t.context.db;
const insert = db.prepare(
"INSERT INTO users(name, email) VALUES (:name, :email)"
"INSERT INTO users(name, email) VALUES (:name, :email)",
);
const insertMany = db.transaction((users) => {
@@ -379,7 +404,7 @@ dualTest.onlySqlitePasses("Database.transaction()", async (t) => {
dualTest.onlySqlitePasses("Database.transaction().immediate()", async (t) => {
const db = t.context.db;
const insert = db.prepare(
"INSERT INTO users(name, email) VALUES (:name, :email)"
"INSERT INTO users(name, email) VALUES (:name, :email)",
);
const insertMany = db.transaction((users) => {
t.is(db.inTransaction, true);
@@ -408,83 +433,98 @@ dualTest.onlySqlitePasses("values", async (t) => {
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 }]);
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'
});
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)
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"
});
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"
});
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`;
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();
});
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();
},
);

View File

@@ -1,20 +1,5 @@
/* tslint:disable */
/* eslint-disable */
/* auto-generated by NAPI-RS */
export interface OpenDatabaseOptions {
readonly?: boolean
fileMustExist?: boolean
timeout?: number
}
export interface PragmaOptions {
simple: boolean
}
export interface RunResult {
changes: number
lastInsertRowid: number
}
/* eslint-disable */
export declare class Database {
memory: boolean
readonly: boolean
@@ -32,11 +17,11 @@ export declare class Database {
exec(sql: string): void
close(): void
}
export declare class Statement {
source: string
get(args?: Array<unknown> | undefined | null): unknown
run(args?: Array<unknown> | undefined | null): RunResult
iterate(args?: Array<unknown> | undefined | null): IteratorStatement
all(args?: Array<unknown> | undefined | null): unknown
pluck(pluck?: boolean | undefined | null): void
static expand(): void
@@ -44,6 +29,18 @@ export declare class Statement {
static columns(): void
bind(args?: Array<unknown> | undefined | null): Statement
}
export declare class IteratorStatement {
[Symbol.iterator](): Iterator<unknown, void, void>
export interface OpenDatabaseOptions {
readonly?: boolean
fileMustExist?: boolean
timeout?: number
}
export interface PragmaOptions {
simple: boolean
}
export interface RunResult {
changes: number
lastInsertRowid: number
}

View File

@@ -1,317 +1,397 @@
/* tslint:disable */
// prettier-ignore
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
/* auto-generated by NAPI-RS */
const { existsSync, readFileSync } = require('fs')
const { join } = require('path')
const { platform, arch } = process
const { createRequire } = require('node:module')
require = createRequire(__filename)
const { readFileSync } = require('node:fs')
let nativeBinding = null
let localFileExisted = false
let loadError = null
const loadErrors = []
function isMusl() {
// For Node 10
if (!process.report || typeof process.report.getReport !== 'function') {
try {
const lddPath = require('child_process').execSync('which ldd').toString().trim()
return readFileSync(lddPath, 'utf8').includes('musl')
} catch (e) {
return true
const isMusl = () => {
let musl = false
if (process.platform === 'linux') {
musl = isMuslFromFilesystem()
if (musl === null) {
musl = isMuslFromReport()
}
} else {
const { glibcVersionRuntime } = process.report.getReport().header
return !glibcVersionRuntime
if (musl === null) {
musl = isMuslFromChildProcess()
}
}
return musl
}
const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-')
const isMuslFromFilesystem = () => {
try {
return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl')
} catch {
return null
}
}
switch (platform) {
case 'android':
switch (arch) {
case 'arm64':
localFileExisted = existsSync(join(__dirname, 'turso.android-arm64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./turso.android-arm64.node')
} else {
nativeBinding = require('@tursodatabase/turso-android-arm64')
}
} catch (e) {
loadError = e
}
break
case 'arm':
localFileExisted = existsSync(join(__dirname, 'turso.android-arm-eabi.node'))
try {
if (localFileExisted) {
nativeBinding = require('./turso.android-arm-eabi.node')
} else {
nativeBinding = require('@tursodatabase/turso-android-arm-eabi')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Android ${arch}`)
const isMuslFromReport = () => {
let report = null
if (typeof process.report?.getReport === 'function') {
process.report.excludeNetwork = true
report = process.report.getReport()
}
if (!report) {
return null
}
if (report.header && report.header.glibcVersionRuntime) {
return false
}
if (Array.isArray(report.sharedObjects)) {
if (report.sharedObjects.some(isFileMusl)) {
return true
}
break
case 'win32':
switch (arch) {
case 'x64':
localFileExisted = existsSync(
join(__dirname, 'turso.win32-x64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./turso.win32-x64-msvc.node')
} else {
nativeBinding = require('@tursodatabase/turso-win32-x64-msvc')
}
} catch (e) {
loadError = e
}
break
case 'ia32':
localFileExisted = existsSync(
join(__dirname, 'turso.win32-ia32-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./turso.win32-ia32-msvc.node')
} else {
nativeBinding = require('@tursodatabase/turso-win32-ia32-msvc')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'turso.win32-arm64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./turso.win32-arm64-msvc.node')
} else {
nativeBinding = require('@tursodatabase/turso-win32-arm64-msvc')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Windows: ${arch}`)
}
break
case 'darwin':
localFileExisted = existsSync(join(__dirname, 'turso.darwin-universal.node'))
}
return false
}
const isMuslFromChildProcess = () => {
try {
return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl')
} catch (e) {
// If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false
return false
}
}
function requireNative() {
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
try {
if (localFileExisted) {
nativeBinding = require('./turso.darwin-universal.node')
} else {
nativeBinding = require('@tursodatabase/turso-darwin-universal')
nativeBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH);
} catch (err) {
loadErrors.push(err)
}
} else if (process.platform === 'android') {
if (process.arch === 'arm64') {
try {
return require('./turso.android-arm64.node')
} catch (e) {
loadErrors.push(e)
}
break
} catch {}
switch (arch) {
case 'x64':
localFileExisted = existsSync(join(__dirname, 'turso.darwin-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./turso.darwin-x64.node')
} else {
nativeBinding = require('@tursodatabase/turso-darwin-x64')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'turso.darwin-arm64.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./turso.darwin-arm64.node')
} else {
nativeBinding = require('@tursodatabase/turso-darwin-arm64')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on macOS: ${arch}`)
try {
return require('@tursodatabase/turso-android-arm64')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm') {
try {
return require('./turso.android-arm-eabi.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-android-arm-eabi')
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`))
}
break
case 'freebsd':
if (arch !== 'x64') {
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
} else if (process.platform === 'win32') {
if (process.arch === 'x64') {
try {
return require('./turso.win32-x64-msvc.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-win32-x64-msvc')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'ia32') {
try {
return require('./turso.win32-ia32-msvc.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-win32-ia32-msvc')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm64') {
try {
return require('./turso.win32-arm64-msvc.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-win32-arm64-msvc')
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`))
}
localFileExisted = existsSync(join(__dirname, 'turso.freebsd-x64.node'))
} else if (process.platform === 'darwin') {
try {
if (localFileExisted) {
nativeBinding = require('./turso.freebsd-x64.node')
} else {
nativeBinding = require('@tursodatabase/turso-freebsd-x64')
}
return require('./turso.darwin-universal.node')
} catch (e) {
loadError = e
loadErrors.push(e)
}
break
case 'linux':
switch (arch) {
case 'x64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'turso.linux-x64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./turso.linux-x64-musl.node')
} else {
nativeBinding = require('@tursodatabase/turso-linux-x64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'turso.linux-x64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./turso.linux-x64-gnu.node')
} else {
nativeBinding = require('@tursodatabase/turso-linux-x64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'turso.linux-arm64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./turso.linux-arm64-musl.node')
} else {
nativeBinding = require('@tursodatabase/turso-linux-arm64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'turso.linux-arm64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./turso.linux-arm64-gnu.node')
} else {
nativeBinding = require('@tursodatabase/turso-linux-arm64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'turso.linux-arm-musleabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./turso.linux-arm-musleabihf.node')
} else {
nativeBinding = require('@tursodatabase/turso-linux-arm-musleabihf')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'turso.linux-arm-gnueabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./turso.linux-arm-gnueabihf.node')
} else {
nativeBinding = require('@tursodatabase/turso-linux-arm-gnueabihf')
}
} catch (e) {
loadError = e
}
}
break
case 'riscv64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'turso.linux-riscv64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./turso.linux-riscv64-musl.node')
} else {
nativeBinding = require('@tursodatabase/turso-linux-riscv64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'turso.linux-riscv64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./turso.linux-riscv64-gnu.node')
} else {
nativeBinding = require('@tursodatabase/turso-linux-riscv64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 's390x':
localFileExisted = existsSync(
join(__dirname, 'turso.linux-s390x-gnu.node')
)
try {
return require('@tursodatabase/turso-darwin-universal')
} catch (e) {
loadErrors.push(e)
}
if (process.arch === 'x64') {
try {
return require('./turso.darwin-x64.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-darwin-x64')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm64') {
try {
return require('./turso.darwin-arm64.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-darwin-arm64')
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`))
}
} else if (process.platform === 'freebsd') {
if (process.arch === 'x64') {
try {
return require('./turso.freebsd-x64.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-freebsd-x64')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm64') {
try {
return require('./turso.freebsd-arm64.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-freebsd-arm64')
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`))
}
} else if (process.platform === 'linux') {
if (process.arch === 'x64') {
if (isMusl()) {
try {
if (localFileExisted) {
nativeBinding = require('./turso.linux-s390x-gnu.node')
} else {
nativeBinding = require('@tursodatabase/turso-linux-s390x-gnu')
}
return require('./turso.linux-x64-musl.node')
} catch (e) {
loadError = e
loadErrors.push(e)
}
break
default:
throw new Error(`Unsupported architecture on Linux: ${arch}`)
try {
return require('@tursodatabase/turso-linux-x64-musl')
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./turso.linux-x64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-linux-x64-gnu')
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'arm64') {
if (isMusl()) {
try {
return require('./turso.linux-arm64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-linux-arm64-musl')
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./turso.linux-arm64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-linux-arm64-gnu')
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'arm') {
if (isMusl()) {
try {
return require('./turso.linux-arm-musleabihf.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-linux-arm-musleabihf')
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./turso.linux-arm-gnueabihf.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-linux-arm-gnueabihf')
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'riscv64') {
if (isMusl()) {
try {
return require('./turso.linux-riscv64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-linux-riscv64-musl')
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./turso.linux-riscv64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-linux-riscv64-gnu')
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'ppc64') {
try {
return require('./turso.linux-ppc64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-linux-ppc64-gnu')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 's390x') {
try {
return require('./turso.linux-s390x-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-linux-s390x-gnu')
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`))
}
break
default:
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
} else if (process.platform === 'openharmony') {
if (process.arch === 'arm64') {
try {
return require('./turso.linux-arm64-ohos.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-linux-arm64-ohos')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'x64') {
try {
return require('./turso.linux-x64-ohos.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-linux-x64-ohos')
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm') {
try {
return require('./turso.linux-arm-ohos.node')
} catch (e) {
loadErrors.push(e)
}
try {
return require('@tursodatabase/turso-linux-arm-ohos')
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`))
}
} else {
loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`))
}
}
nativeBinding = requireNative()
if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) {
try {
nativeBinding = require('./turso.wasi.cjs')
} catch (err) {
if (process.env.NAPI_RS_FORCE_WASI) {
loadErrors.push(err)
}
}
if (!nativeBinding) {
try {
nativeBinding = require('@tursodatabase/turso-wasm32-wasi')
} catch (err) {
if (process.env.NAPI_RS_FORCE_WASI) {
loadErrors.push(err)
}
}
}
}
if (!nativeBinding) {
if (loadError) {
throw loadError
if (loadErrors.length > 0) {
throw new Error(
`Cannot find native binding. ` +
`npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` +
'Please try `npm i` again after removing both package-lock.json and node_modules directory.',
{ cause: loadErrors }
)
}
throw new Error(`Failed to load native binding`)
}
const { Database, Statement, IteratorStatement } = nativeBinding
module.exports.Database = Database
module.exports.Statement = Statement
module.exports.IteratorStatement = IteratorStatement
module.exports = nativeBinding
module.exports.Database = nativeBinding.Database
module.exports.Statement = nativeBinding.Statement

File diff suppressed because it is too large Load Diff

View File

@@ -9,20 +9,16 @@
"main": "wrapper.js",
"types": "index.d.ts",
"napi": {
"name": "turso",
"release": false,
"triples": {
"defaults": false,
"additional": [
"x86_64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
"universal-apple-darwin"
]
}
"binaryName": "turso",
"targets": [
"x86_64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
"universal-apple-darwin"
]
},
"license": "MIT",
"devDependencies": {
"@napi-rs/cli": "^2.18.4",
"@napi-rs/cli": "^3.0.4",
"ava": "^6.0.1",
"better-sqlite3": "^11.9.1"
},
@@ -38,8 +34,8 @@
"build:debug": "napi build --platform",
"prepublishOnly": "napi prepublish -t npm",
"test": "ava",
"universal": "napi universal",
"universal": "napi universalize",
"version": "napi version"
},
"packageManager": "yarn@4.6.0"
}
"packageManager": "yarn@4.9.2"
}

View File

@@ -6,8 +6,8 @@ use std::num::{NonZero, NonZeroUsize};
use std::rc::Rc;
use std::sync::Arc;
use napi::iterator::Generator;
use napi::{bindgen_prelude::ObjectFinalize, Env, JsUnknown};
use napi::bindgen_prelude::{JsObjectValue, Null, Object, ToNapiValue};
use napi::{bindgen_prelude::ObjectFinalize, Env, JsValue, Unknown};
use napi_derive::napi;
use turso_core::{LimboError, StepResult};
@@ -107,12 +107,12 @@ impl Database {
}
#[napi]
pub fn pragma(
pub fn pragma<'env>(
&self,
env: Env,
env: &'env Env,
pragma_name: String,
options: Option<PragmaOptions>,
) -> napi::Result<JsUnknown> {
) -> napi::Result<Unknown<'env>> {
let sql = format!("PRAGMA {pragma_name}");
let stmt = self.prepare(sql)?;
match options {
@@ -122,10 +122,10 @@ impl Database {
match stmt.step().map_err(into_napi_error)? {
turso_core::StepResult::Row => {
let row: Vec<_> = stmt.row().unwrap().get_values().cloned().collect();
return to_js_value(&env, &row[0]);
return to_js_value(env, row[0].clone());
}
turso_core::StepResult::Done => {
return Ok(env.get_undefined()?.into_unknown())
return ToNapiValue::into_unknown((), env);
}
turso_core::StepResult::IO => {
stmt.run_once().map_err(into_napi_error)?;
@@ -141,7 +141,7 @@ impl Database {
}
}
}
_ => stmt.run_internal(env, None),
_ => Ok(stmt.run_internal(env, None)?),
}
}
@@ -266,7 +266,11 @@ impl Statement {
}
#[napi]
pub fn get(&self, env: Env, args: Option<Vec<JsUnknown>>) -> napi::Result<JsUnknown> {
pub fn get<'env>(
&self,
env: &'env Env,
args: Option<Vec<Unknown>>,
) -> napi::Result<Unknown<'env>> {
let mut stmt = self.check_and_bind(env, args)?;
loop {
@@ -279,12 +283,11 @@ impl Statement {
PresentationMode::Raw => {
let mut raw_obj = env.create_array(row.len() as u32)?;
for (idx, value) in row.get_values().enumerate() {
let js_value = to_js_value(&env, value);
let js_value = to_js_value(env, value.clone());
raw_obj.set(idx as u32, js_value)?;
}
return Ok(raw_obj.coerce_to_object()?.into_unknown());
return Ok(raw_obj.coerce_to_object()?.to_unknown());
}
PresentationMode::Pluck => {
let (_, value) =
@@ -292,25 +295,25 @@ impl Statement {
napi::Status::GenericFailure,
"Pluck mode requires at least one column in the result",
))?;
let js_value = to_js_value(&env, value)?;
return Ok(js_value);
let result = to_js_value(env, value.clone())?;
return ToNapiValue::into_unknown(result, env);
}
PresentationMode::None => {
let mut obj = env.create_object()?;
let mut obj = Object::new(env)?;
for (idx, value) in row.get_values().enumerate() {
let key = stmt.get_column_name(idx);
let js_value = to_js_value(&env, value);
let js_value = to_js_value(env, value.clone());
obj.set_named_property(&key, js_value)?;
}
return Ok(obj.into_unknown());
return Ok(obj.to_unknown());
}
}
}
turso_core::StepResult::Done => return Ok(env.get_undefined()?.into_unknown()),
turso_core::StepResult::Done => return ToNapiValue::into_unknown((), env),
turso_core::StepResult::IO => {
stmt.run_once().map_err(into_napi_error)?;
continue;
@@ -326,11 +329,15 @@ impl Statement {
}
#[napi]
pub fn run(&self, env: Env, args: Option<Vec<JsUnknown>>) -> napi::Result<RunResult> {
self.run_and_build_info_object(|| self.run_internal(env, args))
pub fn run(&self, env: Env, args: Option<Vec<Unknown>>) -> napi::Result<RunResult> {
self.run_and_build_info_object(|| self.run_internal(&env, args))
}
fn run_internal(&self, env: Env, args: Option<Vec<JsUnknown>>) -> napi::Result<JsUnknown> {
fn run_internal<'env>(
&self,
env: &'env Env,
args: Option<Vec<Unknown>>,
) -> napi::Result<Unknown<'env>> {
let stmt = self.check_and_bind(env, args)?;
self.internal_all(env, stmt)
@@ -358,38 +365,22 @@ impl Statement {
}
#[napi]
pub fn iterate(
pub fn all<'env>(
&self,
env: Env,
args: Option<Vec<JsUnknown>>,
) -> napi::Result<IteratorStatement> {
if let Some(some_args) = args.as_ref() {
if some_args.iter().len() != 0 {
self.check_and_bind(env, args)?;
}
}
Ok(IteratorStatement {
stmt: Rc::clone(&self.inner),
_database: self.database.clone(),
env,
presentation_mode: self.presentation_mode.clone(),
})
}
#[napi]
pub fn all(&self, env: Env, args: Option<Vec<JsUnknown>>) -> napi::Result<JsUnknown> {
env: &'env Env,
args: Option<Vec<Unknown>>,
) -> napi::Result<Unknown<'env>> {
let stmt = self.check_and_bind(env, args)?;
self.internal_all(env, stmt)
}
fn internal_all(
fn internal_all<'env>(
&self,
env: Env,
env: &'env Env,
mut stmt: RefMut<'_, turso_core::Statement>,
) -> napi::Result<JsUnknown> {
let mut results = env.create_empty_array()?;
) -> napi::Result<Unknown<'env>> {
let mut results = env.create_array(1)?;
let mut index = 0;
loop {
match stmt.step().map_err(into_napi_error)? {
@@ -400,7 +391,7 @@ impl Statement {
PresentationMode::Raw => {
let mut raw_array = env.create_array(row.len() as u32)?;
for (idx, value) in row.get_values().enumerate() {
let js_value = to_js_value(&env, value)?;
let js_value = to_js_value(env, value.clone())?;
raw_array.set(idx as u32, js_value)?;
}
results.set_element(index, raw_array.coerce_to_object()?)?;
@@ -413,16 +404,16 @@ impl Statement {
napi::Status::GenericFailure,
"Pluck mode requires at least one column in the result",
))?;
let js_value = to_js_value(&env, value)?;
let js_value = to_js_value(env, value.clone())?;
results.set_element(index, js_value)?;
index += 1;
continue;
}
PresentationMode::None => {
let mut obj = env.create_object()?;
let mut obj = Object::new(env)?;
for (idx, value) in row.get_values().enumerate() {
let key = stmt.get_column_name(idx);
let js_value = to_js_value(&env, value);
let js_value = to_js_value(env, value.clone());
obj.set_named_property(&key, js_value)?;
}
results.set_element(index, obj)?;
@@ -445,7 +436,7 @@ impl Statement {
}
}
Ok(results.into_unknown())
Ok(results.to_unknown())
}
#[napi]
@@ -475,8 +466,8 @@ impl Statement {
}
#[napi]
pub fn bind(&mut self, env: Env, args: Option<Vec<JsUnknown>>) -> napi::Result<Self, String> {
self.check_and_bind(env, args)
pub fn bind(&mut self, env: Env, args: Option<Vec<Unknown>>) -> napi::Result<Self, String> {
self.check_and_bind(&env, args)
.map_err(with_sqlite_error_message)?;
self.binded = true;
@@ -487,8 +478,8 @@ impl Statement {
/// and bind values to variables.
fn check_and_bind(
&self,
env: Env,
args: Option<Vec<JsUnknown>>,
env: &Env,
args: Option<Vec<Unknown>>,
) -> napi::Result<RefMut<'_, turso_core::Statement>> {
let mut stmt = self.inner.borrow_mut();
stmt.reset();
@@ -507,8 +498,7 @@ impl Statement {
if args.len() == 1 {
if matches!(args[0].get_type()?, napi::ValueType::Object) {
let obj: napi::JsObject =
args.into_iter().next().unwrap().coerce_to_object()?;
let obj: Object = args.into_iter().next().unwrap().coerce_to_object()?;
if obj.is_array()? {
bind_positional_param_array(&mut stmt, &obj)?;
@@ -529,7 +519,7 @@ impl Statement {
fn bind_positional_params(
stmt: &mut RefMut<'_, turso_core::Statement>,
args: Vec<JsUnknown>,
args: Vec<Unknown>,
) -> Result<(), napi::Error> {
for (i, elem) in args.into_iter().enumerate() {
let value = from_js_value(elem)?;
@@ -540,7 +530,7 @@ fn bind_positional_params(
fn bind_host_params(
stmt: &mut RefMut<'_, turso_core::Statement>,
obj: &napi::JsObject,
obj: &Object,
) -> Result<(), napi::Error> {
if first_key_is_number(obj) {
bind_numbered_params(stmt, obj)?;
@@ -551,8 +541,8 @@ fn bind_host_params(
Ok(())
}
fn first_key_is_number(obj: &napi::JsObject) -> bool {
napi::JsObject::keys(obj)
fn first_key_is_number(obj: &Object) -> bool {
Object::keys(obj)
.iter()
.flatten()
.filter(|key| matches!(obj.has_own_property(key), Ok(result) if result))
@@ -562,9 +552,9 @@ fn first_key_is_number(obj: &napi::JsObject) -> bool {
fn bind_numbered_params(
stmt: &mut RefMut<'_, turso_core::Statement>,
obj: &napi::JsObject,
obj: &Object,
) -> Result<(), napi::Error> {
for key in napi::JsObject::keys(obj)?.iter() {
for key in Object::keys(obj)?.iter() {
let Ok(param_idx) = str::parse::<u32>(key) else {
return Err(napi::Error::new(
napi::Status::GenericFailure,
@@ -585,7 +575,7 @@ fn bind_numbered_params(
fn bind_named_params(
stmt: &mut RefMut<'_, turso_core::Statement>,
obj: &napi::JsObject,
obj: &Object,
) -> Result<(), napi::Error> {
for idx in 1..stmt.parameters_count() + 1 {
let non_zero_idx = NonZero::new(idx).unwrap();
@@ -597,7 +587,7 @@ fn bind_named_params(
)));
};
let value = obj.get_named_property::<napi::JsUnknown>(&name[1..])?;
let value = obj.get_named_property::<napi::Unknown>(&name[1..])?;
stmt.bind_at(non_zero_idx, from_js_value(value)?);
}
@@ -606,7 +596,7 @@ fn bind_named_params(
fn bind_positional_param_array(
stmt: &mut RefMut<'_, turso_core::Statement>,
obj: &napi::JsObject,
obj: &Object,
) -> Result<(), napi::Error> {
assert!(obj.is_array()?, "bind_array can only be called with arrays");
@@ -622,90 +612,29 @@ fn bind_positional_param_array(
fn bind_single_param(
stmt: &mut RefMut<'_, turso_core::Statement>,
obj: napi::JsUnknown,
obj: napi::Unknown,
) -> Result<(), napi::Error> {
stmt.bind_at(NonZero::new(1).unwrap(), from_js_value(obj)?);
Ok(())
}
#[napi(iterator)]
pub struct IteratorStatement {
stmt: Rc<RefCell<turso_core::Statement>>,
_database: Database,
env: Env,
presentation_mode: PresentationMode,
}
#[napi]
impl Generator for IteratorStatement {
type Yield = JsUnknown;
type Next = ();
type Return = ();
fn next(&mut self, _: Option<Self::Next>) -> Option<Self::Yield> {
let mut stmt = self.stmt.borrow_mut();
loop {
match stmt.step().ok()? {
turso_core::StepResult::Row => {
let row = stmt.row().unwrap();
match self.presentation_mode {
PresentationMode::Raw => {
let mut raw_array = self.env.create_array(row.len() as u32).ok()?;
for (idx, value) in row.get_values().enumerate() {
let js_value = to_js_value(&self.env, value);
raw_array.set(idx as u32, js_value).ok()?;
}
return Some(raw_array.coerce_to_object().ok()?.into_unknown());
}
PresentationMode::Pluck => {
let (_, value) = row.get_values().enumerate().next()?;
return to_js_value(&self.env, value).ok();
}
PresentationMode::None => {
let mut js_row = self.env.create_object().ok()?;
for (idx, value) in row.get_values().enumerate() {
let key = stmt.get_column_name(idx);
let js_value = to_js_value(&self.env, value);
js_row.set_named_property(&key, js_value).ok()?;
}
return Some(js_row.into_unknown());
}
}
}
turso_core::StepResult::Done => return None,
turso_core::StepResult::IO => {
stmt.run_once().ok()?;
continue;
}
turso_core::StepResult::Interrupt | turso_core::StepResult::Busy => return None,
}
}
}
}
fn to_js_value(env: &napi::Env, value: &turso_core::Value) -> napi::Result<JsUnknown> {
fn to_js_value<'a>(env: &'a napi::Env, value: turso_core::Value) -> napi::Result<Unknown<'a>> {
match value {
turso_core::Value::Null => Ok(env.get_null()?.into_unknown()),
turso_core::Value::Integer(i) => Ok(env.create_int64(*i)?.into_unknown()),
turso_core::Value::Float(f) => Ok(env.create_double(*f)?.into_unknown()),
turso_core::Value::Text(s) => Ok(env.create_string(s.as_str())?.into_unknown()),
turso_core::Value::Blob(b) => Ok(env.create_buffer_copy(b.as_slice())?.into_unknown()),
turso_core::Value::Null => ToNapiValue::into_unknown(Null, env),
turso_core::Value::Integer(i) => ToNapiValue::into_unknown(i, env),
turso_core::Value::Float(f) => ToNapiValue::into_unknown(f, env),
turso_core::Value::Text(s) => ToNapiValue::into_unknown(s.as_str(), env),
turso_core::Value::Blob(b) => ToNapiValue::into_unknown(b, env),
}
}
fn from_js_value(value: JsUnknown) -> napi::Result<turso_core::Value> {
fn from_js_value(value: Unknown<'_>) -> napi::Result<turso_core::Value> {
match value.get_type()? {
napi::ValueType::Undefined | napi::ValueType::Null | napi::ValueType::Unknown => {
Ok(turso_core::Value::Null)
}
napi::ValueType::Boolean => {
let b = value.coerce_to_bool()?.get_value()?;
let b = value.coerce_to_bool()?;
Ok(turso_core::Value::Integer(b as i64))
}
napi::ValueType::Number => {
@@ -801,10 +730,10 @@ fn into_napi_error_with_message(
#[inline]
fn with_sqlite_error_message(err: napi::Error) -> napi::Error<String> {
napi::Error::new("SQLITE_ERROR".to_owned(), err.reason)
napi::Error::new("SQLITE_ERROR".to_owned(), err.reason.clone())
}
#[inline]
fn into_convertible_type_error_message(error_type: &str) -> String {
"[TURSO_CONVERT_TYPE]".to_owned() + error_type
"[TURSO_CONVERT_TYPE] ".to_owned() + error_type
}

View File

@@ -5,11 +5,14 @@ const { Database: NativeDB } = require("./index.js");
const SqliteError = require("./sqlite-error.js");
const convertibleErrorTypes = { TypeError };
const CONVERTIBLE_ERROR_PREFIX = '[TURSO_CONVERT_TYPE]';
const CONVERTIBLE_ERROR_PREFIX = "[TURSO_CONVERT_TYPE]";
function convertError(err) {
if ((err.code ?? '').startsWith(CONVERTIBLE_ERROR_PREFIX)) {
return createErrorByName(err.code.substring(CONVERTIBLE_ERROR_PREFIX.length), err.message);
if ((err.code ?? "").startsWith(CONVERTIBLE_ERROR_PREFIX)) {
return createErrorByName(
err.code.substring(CONVERTIBLE_ERROR_PREFIX.length),
err.message,
);
}
return new SqliteError(err.message, err.code, err.rawCode);
@@ -40,7 +43,8 @@ class Database {
*/
constructor(path, opts = {}) {
opts.readonly = opts.readonly === undefined ? false : opts.readonly;
opts.fileMustExist = opts.fileMustExist === undefined ? false : opts.fileMustExist;
opts.fileMustExist =
opts.fileMustExist === undefined ? false : opts.fileMustExist;
opts.timeout = opts.timeout === undefined ? 0 : opts.timeout;
this.db = new NativeDB(path, opts);
@@ -66,8 +70,8 @@ class Database {
open: {
get() {
return this.db.open;
}
}
},
},
});
}
@@ -78,7 +82,7 @@ class Database {
*/
prepare(sql) {
if (!sql) {
throw new RangeError('The supplied SQL string contains no statements');
throw new RangeError("The supplied SQL string contains no statements");
}
try {
@@ -265,10 +269,7 @@ class Statement {
* @param bindParameters - The bind parameters for executing the statement.
*/
*iterate(...bindParameters) {
// revisit this solution when https://github.com/napi-rs/napi-rs/issues/2574 is fixed
for (const row of this.stmt.iterate(bindParameters.flat())) {
yield row;
}
throw new Error("not implemented");
}
/**

File diff suppressed because it is too large Load Diff