Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 91 additions & 3 deletions test-parity/node-suite/sqlite/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,93 @@
# node:sqlite granular parity suite

Focused deterministic cases for Perry's `node:sqlite` compatibility layer. These
cover `DatabaseSync` behavior that can be compared without a real SQLite
extension or external database service.
Focused deterministic cases for Perry's `node:sqlite` compatibility layer. The
suite intentionally starts with isolated in-memory databases and compares
observable behavior rather than SQLite-version-specific error text.

## Coverage

- `database/`: construction and option validation, deferred/open/close/dispose
state, batch execution, SQL errors, defensive/DQS configuration, in-memory
location, runtime limits, and serialization/deserialization.
- `statements/`: prepare validation; `run()`, `get()`, `all()`, and `iterate()`;
null-prototype rows; change metadata; and iterator invalidation.
- `binding/`: anonymous and numbered positional parameters, prefixed and bare
named parameters, unknown-name policy, null/text/number/BigInt/blob binding,
every standard typed-array view, `DataView`, and invalid values.
- `metadata/`: `sourceSQL`, `expandedSQL`, `columns()`, `readBigInts`, and
`returnArrays`, including per-statement overrides.
- `transactions/`, `functions/`, and `authorizer/`: commit/rollback/savepoints,
stable constraint diagnostics, scalar functions, aggregate/window functions,
callback/options validation, and authorizer allow/deny/ignore/clear behavior.
- `tag-store/`: caching, capacity/eviction, `size`, `clear`, `db`, all four query
methods, closed-database invalidation, and call-shape validation.
- `sessions/`: deterministic in-memory changeset/patchset creation, table
filtering, and changeset application.
- `extension-loading/`: policy controls and missing-extension diagnostics only;
it never loads a native binary.

Each fixture owns and explicitly closes every database it opens. No fixture
creates a persistent database or depends on execution order.

## Upstream selection

The correctness oracle is the pinned Node.js 26.5.0 tree at
[`bebd1b8`](https://github.com/nodejs/node/tree/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/test/parallel).
The selection was made from all 18 `test-sqlite*.js`/`.mjs` files there, with
the core behavior primarily drawn from
[`test-sqlite-database-sync.js`](https://github.com/nodejs/node/blob/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/test/parallel/test-sqlite-database-sync.js),
[`test-sqlite-statement-sync.js`](https://github.com/nodejs/node/blob/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/test/parallel/test-sqlite-statement-sync.js),
[`test-sqlite-named-parameters.js`](https://github.com/nodejs/node/blob/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/test/parallel/test-sqlite-named-parameters.js),
[`test-sqlite-data-types.js`](https://github.com/nodejs/node/blob/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/test/parallel/test-sqlite-data-types.js),
and
[`test-sqlite-template-tag.js`](https://github.com/nodejs/node/blob/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/test/parallel/test-sqlite-template-tag.js).

Deno's corresponding selection was reviewed at
[`tests/unit_node/sqlite_test.ts` (`f8a17c8`)](https://github.com/denoland/deno/blob/f8a17c8171569fa2870d740030aaa59c91fdf9ee/tests/unit_node/sqlite_test.ts).
It independently emphasizes batch execution, mixed/numbered binding, blobs,
iterator reuse, serialization, sessions, aggregates, and `SQLTagStore`; those
deterministic overlaps are represented here. Bun's tree at
[`c4fad46`](https://github.com/oven-sh/bun/tree/c4fad462e7dc20e5e9780f848db42e1e2f52186d/test)
contains `bun:sqlite` tests but no `node:sqlite` compatibility selection, so its
SQLite-specific cases were not treated as Node API coverage.

## Stopping judgment and exclusions

The current boundary is the deterministic in-memory API. Remaining Node cases
were not copied when they were redundant with a smaller fixture or belonged to
one of these separate-risk categories:

- filesystem paths (`string`, `Buffer`, and `URL`), read-only databases,
file-backed `location()`, backup, permissions, and timeout or concurrent-lock
behavior;
- real extension loading and native extension binaries;
- optional SQLite build features and compile options (math/percentile, dbstat,
FTS3/4/5, RTree, RBU, and Geopoly);
- large strings/databases, garbage-collection retention, leak/stress checks,
worker/process concurrency, callback re-entrancy crash guards, corruption,
and recovery;
- the full changeset conflict matrix and platform-sensitive authorizer or
extension edge cases.

Those exclusions require dedicated runtime/native or filesystem batches rather
than weakening this suite's deterministic signal. Error fixtures assert stable
`name`/`code` and behavior; they deliberately avoid bundled-SQLite message text.

## Measured Perry diagnostics

Repeated clean focused runs against Node 26.5.0 measure **24/40**. All 40
fixtures exit cleanly under the Node oracle, and the 16 mismatches reproduce in
isolation; none are harness, temporary-path, cleanup, ordering, or host-Node
failures. They identify these stable compatibility gaps:

- numbered positional parameters; non-`Uint8Array` typed-array views; empty
blob and symbol binding; and the `Symbol.for("sqlite-type")` property;
- timeout and limit validation/enumerability, default DQS error classification,
and the currently missing serialize/deserialize surface;
- function-valued aggregate starts, scalar varargs with SQL `NULL`, aggregate
window result callback count, and numeric `expandedSQL` formatting;
- iterator post-completion/reset invalidation, NUL-containing prepare error
classification, and absent SQLite `errcode` metadata on constraint errors.

The passing cases independently confirm the remaining covered behavior rather
than masking those gaps behind feature detection or conditional assertions.
45 changes: 45 additions & 0 deletions test-parity/node-suite/sqlite/authorizer/basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// parity-node-argv: --experimental-sqlite
import { constants, DatabaseSync } from "node:sqlite";

function probe(label: string, fn: () => unknown) {
try {
const value: any = fn();
console.log(label, "OK", value?.value ?? value);
} catch (error: any) {
console.log(label, "THROW", error.name, error.code || "no-code");
}
}

const db = new DatabaseSync(":memory:");
db.exec(
"CREATE TABLE data(id INTEGER, value TEXT); INSERT INTO data VALUES (1, 'one')",
);
const calls: string[] = [];
db.setAuthorizer(
(action: number, first: string | null, second: string | null) => {
calls.push(`${action}:${String(first)}:${String(second)}`);
return constants.SQLITE_OK;
},
);
console.log(
"allowed:",
db.prepare("SELECT value FROM data WHERE id = 1").get().value,
calls.length > 0,
);
console.log(
"read observed:",
calls.some((call) => call.startsWith(`${constants.SQLITE_READ}:data:`)),
);

db.setAuthorizer(() => constants.SQLITE_DENY);
probe("denied", () => db.exec("SELECT 1"));
db.setAuthorizer(null);
probe("cleared", () => db.prepare("SELECT 2 AS value").get());

for (const value of [undefined, 1, "callback", {}, []]) {
probe(
`invalid ${value === undefined ? "undefined" : Array.isArray(value) ? "array" : typeof value}`,
() => db.setAuthorizer(value as any),
);
}
db.close();
60 changes: 60 additions & 0 deletions test-parity/node-suite/sqlite/authorizer/ignore-and-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// parity-node-argv: --experimental-sqlite
import { constants, DatabaseSync } from "node:sqlite";

function probe(label: string, fn: () => unknown) {
try {
const value: any = fn();
console.log(label, "OK", value?.value ?? value);
} catch (error: any) {
console.log(label, "THROW", error.name, error.code || "no-code");
}
}

const db = new DatabaseSync(":memory:");
db.exec(
"CREATE TABLE data(id INTEGER, value TEXT); INSERT INTO data VALUES (1, 'one')",
);

db.setAuthorizer(
(action: number, table: string | null, column: string | null) => {
if (
action === constants.SQLITE_READ &&
table === "data" &&
column === "value"
) {
return constants.SQLITE_IGNORE;
}
return constants.SQLITE_OK;
},
);
const ignored: any = db.prepare("SELECT id, value FROM data").get();
console.log("ignored read:", ignored.id, String(ignored.value));

db.setAuthorizer((action: number) =>
action === constants.SQLITE_INSERT
? constants.SQLITE_IGNORE
: constants.SQLITE_OK,
);
db.prepare("INSERT INTO data VALUES (2, 'two')").run();
db.setAuthorizer(null);
console.log(
"ignored insert:",
(db.prepare("SELECT count(*) AS n FROM data").get() as any).n,
);

for (const [label, callback] of [
["undefined return", () => undefined],
["string return", () => "0"],
["invalid code", () => 3],
[
"throw",
() => {
throw new RangeError("authorizer marker");
},
],
] as const) {
db.setAuthorizer(callback as any);
probe(label, () => db.prepare("SELECT 1").get());
}
db.setAuthorizer(null);
db.close();
41 changes: 41 additions & 0 deletions test-parity/node-suite/sqlite/binding/duplicate-and-ambiguous.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// parity-node-argv: --experimental-sqlite
import { DatabaseSync } from "node:sqlite";

function probe(label: string, fn: () => unknown) {
try {
const value: any = fn();
console.log(label, "OK", value?.changes ?? value);
} catch (error: any) {
console.log(label, "THROW", error.name, error.code || "no-code");
}
}

const db = new DatabaseSync(":memory:");
db.exec("CREATE TABLE data(a, b, c)");
probe("duplicate bare", () =>
db
.prepare("INSERT INTO data VALUES ($value, $value, $value)")
.run({ value: 1 }),
);
probe("duplicate prefixed", () =>
db
.prepare("INSERT INTO data VALUES ($value, $value, $value)")
.run({ $value: 2 }),
);
probe("ambiguous bare", () =>
db
.prepare("INSERT INTO data VALUES ($value, @value, :value)")
.run({ value: 3 }),
);
probe("ambiguous prefixed", () =>
db
.prepare("INSERT INTO data VALUES ($value, @value, :value)")
.run({ $value: 3, "@value": 4, ":value": 5 }),
);
console.log(
"rows:",
(db.prepare("SELECT a, b, c FROM data ORDER BY rowid").all() as any[])
.map((row) => `${row.a},${row.b},${row.c}`)
.join("|"),
);
db.close();
35 changes: 35 additions & 0 deletions test-parity/node-suite/sqlite/binding/named-and-bare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// parity-node-argv: --experimental-sqlite
import { DatabaseSync } from "node:sqlite";

function probe(label: string, fn: () => unknown) {
try {
const value: any = fn();
console.log(label, "OK", value?.changes ?? value);
} catch (error: any) {
console.log(label, "THROW", error.name, error.code || "no-code");
}
}

const db = new DatabaseSync(":memory:");
db.exec("CREATE TABLE data(a, b, c)");
const named = db.prepare("INSERT INTO data VALUES ($a, :b, @c)");
probe("prefixed", () => named.run({ $a: 1, ":b": 2, "@c": 3 }));
probe("bare", () => named.run({ a: 4, b: 5, c: 6 }));
probe("mixed", () =>
db.prepare("INSERT INTO data VALUES ($a, ?, ?)").run({ a: 7 }, 8, 9),
);
probe("missing", () => named.run({ a: 10, b: 11 }));
probe("primitive map", () => named.run(1 as any));

const strict = db.prepare("SELECT $value AS value", {
allowBareNamedParameters: false,
});
probe("strict prefixed", () => (strict.get({ $value: "yes" }) as any).value);
probe("strict bare", () => strict.get({ value: "no" }));

for (const row of db
.prepare("SELECT a, b, c FROM data ORDER BY rowid")
.all() as any[]) {
console.log("row:", row.a, row.b, row.c);
}
db.close();
34 changes: 34 additions & 0 deletions test-parity/node-suite/sqlite/binding/positional-and-numbered.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// parity-node-argv: --experimental-sqlite
import { DatabaseSync } from "node:sqlite";

function probe(label: string, fn: () => unknown) {
try {
const value: any = fn();
console.log(label, "OK", value?.changes ?? value);
} catch (error: any) {
console.log(label, "THROW", error.name, error.code || "no-code");
}
}

const db = new DatabaseSync(":memory:");
db.exec("CREATE TABLE data(a, b, c)");
probe("anonymous", () =>
db.prepare("INSERT INTO data VALUES (?, ?, ?)").run(1, "two", null),
);
probe("numbered", () =>
db.prepare("INSERT INTO data VALUES (?2, ?1, ?2)").run("one", "two"),
);
probe("numbered gap", () =>
db.prepare("INSERT INTO data VALUES (?1, ?3, ?1)").run("a", "unused", "c"),
);
probe("too many", () =>
db.prepare("INSERT INTO data VALUES (?, ?, ?)").run(1, 2, 3, 4),
);
probe("unbound", () => db.prepare("INSERT INTO data VALUES (?, ?, ?)").run(9));

for (const row of db
.prepare("SELECT a, b, c, typeof(c) AS tc FROM data ORDER BY rowid")
.all() as any[]) {
console.log("row:", String(row.a), String(row.b), String(row.c), row.tc);
}
db.close();
43 changes: 43 additions & 0 deletions test-parity/node-suite/sqlite/binding/typed-array-views.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// parity-node-argv: --experimental-sqlite
import { DatabaseSync } from "node:sqlite";

const bytes = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
const constructors: [string, new (buffer: ArrayBuffer) => ArrayBufferView][] = [
["Int8Array", Int8Array],
["Uint8Array", Uint8Array],
["Uint8ClampedArray", Uint8ClampedArray],
["Int16Array", Int16Array],
["Uint16Array", Uint16Array],
["Int32Array", Int32Array],
["Uint32Array", Uint32Array],
["Float32Array", Float32Array],
["Float64Array", Float64Array],
["BigInt64Array", BigInt64Array],
["BigUint64Array", BigUint64Array],
["DataView", DataView],
];

const db = new DatabaseSync(":memory:");
db.exec("CREATE TABLE views(name TEXT PRIMARY KEY, value BLOB)");
const insert = db.prepare("INSERT INTO views VALUES (?, ?)");
const lookup = db.prepare(
"SELECT value FROM views WHERE name = ? AND value = ?",
);

for (const [name, Constructor] of constructors) {
try {
const input = new Constructor(bytes.buffer.slice(0));
insert.run(name, input);
const row: any = lookup.get(name, input);
console.log(
name,
"OK",
row.value instanceof Uint8Array,
row.value.length,
Array.from(row.value).join(","),
);
} catch (error: any) {
console.log(name, "THROW", error.name, error.code || "no-code");
}
}
db.close();
34 changes: 34 additions & 0 deletions test-parity/node-suite/sqlite/binding/unknown-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// parity-node-argv: --experimental-sqlite
import { DatabaseSync } from "node:sqlite";

function probe(label: string, fn: () => unknown) {
try {
const value: any = fn();
console.log(label, "OK", value?.value ?? value);
} catch (error: any) {
console.log(label, "THROW", error.name, error.code || "no-code");
}
}

const db = new DatabaseSync(":memory:");
const defaultStatement = db.prepare("SELECT $value AS value");
probe("default exact", () => defaultStatement.get({ value: 1 }));
probe("default unknown", () => defaultStatement.get({ value: 2, extra: 3 }));

const permissive = db.prepare("SELECT $value AS value", {
allowUnknownNamedParameters: true,
});
probe("prepare allow", () => permissive.get({ value: 4, extra: 5 }));
permissive.setAllowUnknownNamedParameters(false);
probe("toggle deny", () => permissive.get({ value: 6, extra: 7 }));
permissive.setAllowUnknownNamedParameters(true);
probe("toggle allow", () => permissive.get({ value: 8, extra: 9 }));

const permissiveDb = new DatabaseSync(":memory:", {
allowUnknownNamedParameters: true,
});
probe("database allow", () =>
permissiveDb.prepare("SELECT $value AS value").get({ value: 10, extra: 11 }),
);
db.close();
permissiveDb.close();
Loading
Loading