Merge 'bindings/javascript: Switch to napi v3' from Diego Reis

Switch to napi [v3](https://napi.rs/blog/announce-v3).
With the exception of `Statement.iterate()`,  the behavior is preserved.
I had to temporarily remove it because the trait `Generator` doesn't
supports the new lifetime scoped values, I already brought this issue in
napi's discord server and it should be fixed soon.

Closes #2262
This commit is contained in:
Pekka Enberg
2025-07-25 20:19:37 +03:00
15 changed files with 3821 additions and 2166 deletions

View File

@@ -13,7 +13,7 @@ on:
env:
DEBUG: napi:*
APP_NAME: turso
MACOSX_DEPLOYMENT_TARGET: '10.13'
MACOSX_DEPLOYMENT_TARGET: "10.13"
defaults:
run:
@@ -87,7 +87,7 @@ jobs:
if: ${{ matrix.settings.docker }}
with:
image: ${{ matrix.settings.docker }}
options: '--user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build/bindings/javascript'
options: "--user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build/bindings/javascript"
run: ${{ matrix.settings.build }}
- name: Build
run: ${{ matrix.settings.build }}
@@ -110,7 +110,7 @@ jobs:
- host: macos-13
target: x86_64-apple-darwin
node:
- '20'
- "20"
runs-on: ${{ matrix.settings.host }}
steps:
- uses: actions/checkout@v4
@@ -139,7 +139,7 @@ jobs:
fail-fast: false
matrix:
node:
- '20'
- "20"
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
@@ -176,12 +176,15 @@ jobs:
uses: actions/download-artifact@v4
with:
name: bindings-x86_64-apple-darwin
path: bindings/javascript/artifacts
path: bindings/javascript
- name: Download macOS arm64 artifact
uses: actions/download-artifact@v4
with:
name: bindings-aarch64-apple-darwin
path: bindings/javascript/artifacts
path: bindings/javascript
- name: List packages
run: ls -R .
shell: bash
- name: Combine binaries
run: yarn universal
- name: Upload artifact

74
Cargo.lock generated
View File

@@ -556,9 +556,9 @@ checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "convert_case"
version = "0.6.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
dependencies = [
"unicode-segmentation",
]
@@ -749,14 +749,20 @@ dependencies = [
[[package]]
name = "ctor"
version = "0.2.9"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
checksum = "a4735f265ba6a1188052ca32d461028a7d1125868be18e287e756019da7607b5"
dependencies = [
"quote",
"syn 2.0.100",
"ctor-proc-macro",
"dtor",
]
[[package]]
name = "ctor-proc-macro"
version = "0.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d"
[[package]]
name = "ctrlc"
version = "3.4.5"
@@ -931,6 +937,21 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "dtor"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97cbdf2ad6846025e8e25df05171abfb30e3ababa12ee0a0e44b9bbe570633a8"
dependencies = [
"dtor-proc-macro",
]
[[package]]
name = "dtor-proc-macro"
version = "0.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055"
[[package]]
name = "dyn-clone"
version = "1.0.19"
@@ -2192,31 +2213,32 @@ dependencies = [
[[package]]
name = "napi"
version = "2.16.17"
version = "3.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3"
checksum = "afaf586c21f260e9dc327ae3585fc6efcbb24a416d5151da38bbd35a1f2663c8"
dependencies = [
"bitflags 2.9.0",
"ctor",
"napi-derive",
"napi-build",
"napi-sys",
"once_cell",
"nohash-hasher",
"rustc-hash",
]
[[package]]
name = "napi-build"
version = "2.2.0"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03acbfa4f156a32188bfa09b86dc11a431b5725253fc1fc6f6df5bed273382c4"
checksum = "dcae8ad5609d14afb3a3b91dee88c757016261b151e9dcecabf1b2a31a6cab14"
[[package]]
name = "napi-derive"
version = "2.16.13"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
checksum = "43e61844e0c0bb81e711f2084abe7cff187b03ca21ff8b000cb59bbda61e15a9"
dependencies = [
"cfg-if",
"convert_case",
"ctor",
"napi-derive-backend",
"proc-macro2",
"quote",
@@ -2225,24 +2247,22 @@ dependencies = [
[[package]]
name = "napi-derive-backend"
version = "1.0.75"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
checksum = "b7ab19e9b98efb13895f492a2e367ca50c955ac3c4723613af73fdda4011afcc"
dependencies = [
"convert_case",
"once_cell",
"proc-macro2",
"quote",
"regex",
"semver",
"syn 2.0.100",
]
[[package]]
name = "napi-sys"
version = "2.4.0"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
checksum = "3e4e7135a8f97aa0f1509cce21a8a1f9dcec1b50d8dee006b48a5adb69a9d64d"
dependencies = [
"libloading",
]
@@ -2290,6 +2310,12 @@ dependencies = [
"libc",
]
[[package]]
name = "nohash-hasher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
name = "notify"
version = "8.0.0"
@@ -3046,6 +3072,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc_version"
version = "0.4.1"

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