From 5d24c242da3aba00a6156ef123a9d445ad13ec48 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Thu, 16 Jul 2026 10:40:27 +0200 Subject: [PATCH] test: expand node sqlite parity coverage --- test-parity/node-suite/sqlite/README.md | 94 ++++++++++++++++++- .../node-suite/sqlite/authorizer/basic.ts | 45 +++++++++ .../authorizer/ignore-and-validation.ts | 60 ++++++++++++ .../sqlite/binding/duplicate-and-ambiguous.ts | 41 ++++++++ .../sqlite/binding/named-and-bare.ts | 35 +++++++ .../sqlite/binding/positional-and-numbered.ts | 34 +++++++ .../sqlite/binding/typed-array-views.ts | 43 +++++++++ .../sqlite/binding/unknown-names.ts | 34 +++++++ .../node-suite/sqlite/binding/value-types.ts | 56 +++++++++++ .../sqlite/database/config-and-location.ts | 61 ++++++++++++ .../node-suite/sqlite/database/disposal.ts | 31 ++++++ .../sqlite/database/exec-and-constraints.ts | 44 +++++++++ .../node-suite/sqlite/database/lifecycle.ts | 31 ++++++ .../node-suite/sqlite/database/limits.ts | 68 ++++++++++++++ .../sqlite/database/options-validation.ts | 45 +++++++++ .../sqlite/database/serialize-roundtrip.ts | 63 +++++++++++++ .../node-suite/sqlite/database/type-symbol.ts | 12 +++ .../sqlite/extension-loading/controls.ts | 30 +++++- .../sqlite/functions/aggregate-direct-only.ts | 51 ++++++++++ .../sqlite/functions/aggregate-options.ts | 63 +++++++++++++ .../sqlite/functions/aggregate-window.ts | 62 ++++++++++++ .../node-suite/sqlite/functions/aggregate.ts | 59 ++++++++++++ .../node-suite/sqlite/functions/scalar.ts | 51 ++++++++++ .../sqlite/functions/value-roundtrip.ts | 76 +++++++++++++++ .../node-suite/sqlite/metadata/columns.ts | 41 ++++++++ .../sqlite/metadata/result-toggles.ts | 60 ++++++++++++ .../node-suite/sqlite/metadata/sql-text.ts | 14 +++ .../sqlite/sessions/changeset-roundtrip.ts | 55 +++++++++++ .../sqlite/sessions/table-filter.ts | 32 +++++++ .../sqlite/statements/change-metadata.ts | 30 ++++++ .../node-suite/sqlite/statements/iterate.ts | 27 ++++++ .../statements/iterator-invalidation.ts | 45 +++++++++ .../sqlite/statements/prepare-validation.ts | 30 ++++++ .../sqlite/statements/run-get-all.ts | 51 ++++++++++ .../sqlite/statements/special-column-names.ts | 31 ++++++ .../sqlite/tag-store/basic-operations.ts | 45 +++++++++ .../sqlite/tag-store/capacity-and-eviction.ts | 39 ++++++++ .../sqlite/tag-store/interpolation-safety.ts | 24 +++++ .../sqlite/tag-store/validation-and-close.ts | 38 ++++++++ .../sqlite/transactions/commit-rollback.ts | 36 +++++++ .../sqlite/transactions/constraint-errors.ts | 46 +++++++++ test-parity/node_suite_baseline.json | 4 +- 42 files changed, 1827 insertions(+), 10 deletions(-) create mode 100644 test-parity/node-suite/sqlite/authorizer/basic.ts create mode 100644 test-parity/node-suite/sqlite/authorizer/ignore-and-validation.ts create mode 100644 test-parity/node-suite/sqlite/binding/duplicate-and-ambiguous.ts create mode 100644 test-parity/node-suite/sqlite/binding/named-and-bare.ts create mode 100644 test-parity/node-suite/sqlite/binding/positional-and-numbered.ts create mode 100644 test-parity/node-suite/sqlite/binding/typed-array-views.ts create mode 100644 test-parity/node-suite/sqlite/binding/unknown-names.ts create mode 100644 test-parity/node-suite/sqlite/binding/value-types.ts create mode 100644 test-parity/node-suite/sqlite/database/config-and-location.ts create mode 100644 test-parity/node-suite/sqlite/database/disposal.ts create mode 100644 test-parity/node-suite/sqlite/database/exec-and-constraints.ts create mode 100644 test-parity/node-suite/sqlite/database/lifecycle.ts create mode 100644 test-parity/node-suite/sqlite/database/limits.ts create mode 100644 test-parity/node-suite/sqlite/database/options-validation.ts create mode 100644 test-parity/node-suite/sqlite/database/serialize-roundtrip.ts create mode 100644 test-parity/node-suite/sqlite/database/type-symbol.ts create mode 100644 test-parity/node-suite/sqlite/functions/aggregate-direct-only.ts create mode 100644 test-parity/node-suite/sqlite/functions/aggregate-options.ts create mode 100644 test-parity/node-suite/sqlite/functions/aggregate-window.ts create mode 100644 test-parity/node-suite/sqlite/functions/aggregate.ts create mode 100644 test-parity/node-suite/sqlite/functions/scalar.ts create mode 100644 test-parity/node-suite/sqlite/functions/value-roundtrip.ts create mode 100644 test-parity/node-suite/sqlite/metadata/columns.ts create mode 100644 test-parity/node-suite/sqlite/metadata/result-toggles.ts create mode 100644 test-parity/node-suite/sqlite/metadata/sql-text.ts create mode 100644 test-parity/node-suite/sqlite/sessions/changeset-roundtrip.ts create mode 100644 test-parity/node-suite/sqlite/sessions/table-filter.ts create mode 100644 test-parity/node-suite/sqlite/statements/change-metadata.ts create mode 100644 test-parity/node-suite/sqlite/statements/iterate.ts create mode 100644 test-parity/node-suite/sqlite/statements/iterator-invalidation.ts create mode 100644 test-parity/node-suite/sqlite/statements/prepare-validation.ts create mode 100644 test-parity/node-suite/sqlite/statements/run-get-all.ts create mode 100644 test-parity/node-suite/sqlite/statements/special-column-names.ts create mode 100644 test-parity/node-suite/sqlite/tag-store/basic-operations.ts create mode 100644 test-parity/node-suite/sqlite/tag-store/capacity-and-eviction.ts create mode 100644 test-parity/node-suite/sqlite/tag-store/interpolation-safety.ts create mode 100644 test-parity/node-suite/sqlite/tag-store/validation-and-close.ts create mode 100644 test-parity/node-suite/sqlite/transactions/commit-rollback.ts create mode 100644 test-parity/node-suite/sqlite/transactions/constraint-errors.ts diff --git a/test-parity/node-suite/sqlite/README.md b/test-parity/node-suite/sqlite/README.md index 5021263f6d..fc977ff308 100644 --- a/test-parity/node-suite/sqlite/README.md +++ b/test-parity/node-suite/sqlite/README.md @@ -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. diff --git a/test-parity/node-suite/sqlite/authorizer/basic.ts b/test-parity/node-suite/sqlite/authorizer/basic.ts new file mode 100644 index 0000000000..add2a5c7f7 --- /dev/null +++ b/test-parity/node-suite/sqlite/authorizer/basic.ts @@ -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(); diff --git a/test-parity/node-suite/sqlite/authorizer/ignore-and-validation.ts b/test-parity/node-suite/sqlite/authorizer/ignore-and-validation.ts new file mode 100644 index 0000000000..98723bfe8c --- /dev/null +++ b/test-parity/node-suite/sqlite/authorizer/ignore-and-validation.ts @@ -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(); diff --git a/test-parity/node-suite/sqlite/binding/duplicate-and-ambiguous.ts b/test-parity/node-suite/sqlite/binding/duplicate-and-ambiguous.ts new file mode 100644 index 0000000000..a347e33840 --- /dev/null +++ b/test-parity/node-suite/sqlite/binding/duplicate-and-ambiguous.ts @@ -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(); diff --git a/test-parity/node-suite/sqlite/binding/named-and-bare.ts b/test-parity/node-suite/sqlite/binding/named-and-bare.ts new file mode 100644 index 0000000000..26d170d339 --- /dev/null +++ b/test-parity/node-suite/sqlite/binding/named-and-bare.ts @@ -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(); diff --git a/test-parity/node-suite/sqlite/binding/positional-and-numbered.ts b/test-parity/node-suite/sqlite/binding/positional-and-numbered.ts new file mode 100644 index 0000000000..34bfc84151 --- /dev/null +++ b/test-parity/node-suite/sqlite/binding/positional-and-numbered.ts @@ -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(); diff --git a/test-parity/node-suite/sqlite/binding/typed-array-views.ts b/test-parity/node-suite/sqlite/binding/typed-array-views.ts new file mode 100644 index 0000000000..50367f8194 --- /dev/null +++ b/test-parity/node-suite/sqlite/binding/typed-array-views.ts @@ -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(); diff --git a/test-parity/node-suite/sqlite/binding/unknown-names.ts b/test-parity/node-suite/sqlite/binding/unknown-names.ts new file mode 100644 index 0000000000..357669c78f --- /dev/null +++ b/test-parity/node-suite/sqlite/binding/unknown-names.ts @@ -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(); diff --git a/test-parity/node-suite/sqlite/binding/value-types.ts b/test-parity/node-suite/sqlite/binding/value-types.ts new file mode 100644 index 0000000000..c76125daff --- /dev/null +++ b/test-parity/node-suite/sqlite/binding/value-types.ts @@ -0,0 +1,56 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +function bytes(value: any) { + return Array.from(value as Uint8Array).join(","); +} + +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:", { readBigInts: true }); +db.exec("CREATE TABLE values_table(id INTEGER PRIMARY KEY, value)"); +const insert = db.prepare("INSERT INTO values_table(value) VALUES (?)"); +probe("null", () => insert.run(null)); +probe("text", () => insert.run("hello")); +probe("number", () => insert.run(1.25)); +probe("bigint", () => insert.run(9007199254740993n)); +probe("buffer", () => insert.run(Buffer.from([1, 2, 255]))); +probe("uint8", () => insert.run(new Uint8Array([3, 4]))); +probe("dataview", () => + insert.run(new DataView(Uint8Array.from([5, 6]).buffer)), +); +probe("empty blob", () => insert.run(new Uint8Array())); + +const rows: any[] = db + .prepare("SELECT value, typeof(value) AS type FROM values_table ORDER BY id") + .all(); +for (const [index, row] of rows.entries()) { + const value = row.value; + console.log( + "row:", + index + 1, + row.type, + typeof value, + row.type === "blob" ? bytes(value) : String(value), + ); +} + +for (const value of [ + undefined, + true, + {}, + [], + Symbol("x"), + 2n ** 63n, + -(2n ** 63n) - 1n, +]) { + probe(`invalid ${typeof value}`, () => insert.run(value as any)); +} +db.close(); diff --git a/test-parity/node-suite/sqlite/database/config-and-location.ts b/test-parity/node-suite/sqlite/database/config-and-location.ts new file mode 100644 index 0000000000..00dccdd032 --- /dev/null +++ b/test-parity/node-suite/sqlite/database/config-and-location.ts @@ -0,0 +1,61 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +function probe(label: string, fn: () => unknown) { + try { + console.log(label, "OK", String(fn())); + } catch (error: any) { + console.log(label, "THROW", error.name, error.code || "no-code"); + } +} + +function defensiveMode(db: DatabaseSync) { + db.exec("PRAGMA journal_mode=OFF"); + return (db.prepare("PRAGMA journal_mode").get() as any).journal_mode; +} + +const defaults = new DatabaseSync(":memory:"); +console.log("default defensive:", defensiveMode(defaults)); +console.log( + "memory location:", + String(defaults.location()), + String(defaults.location("main")), +); +probe("location type", () => defaults.location(1 as any)); +probe("location unknown", () => defaults.location("missing")); +defaults.close(); + +const disabled = new DatabaseSync(":memory:", { defensive: false }); +console.log("disabled defensive:", defensiveMode(disabled)); +disabled.close(); + +const enabled = new DatabaseSync(":memory:", { defensive: false }); +enabled.enableDefensive(true); +console.log("enabled defensive:", defensiveMode(enabled)); +probe("defensive value", () => enabled.enableDefensive(1 as any)); +enabled.close(); + +const toggledOff = new DatabaseSync(":memory:"); +toggledOff.enableDefensive(false); +console.log("toggled off defensive:", defensiveMode(toggledOff)); +toggledOff.close(); + +for (const value of [null, 0, "false", {}]) { + probe(`defensive option ${value === null ? "null" : typeof value}`, () => { + const db = new DatabaseSync(":memory:", { defensive: value as any }); + db.close(); + }); +} + +const dqs = new DatabaseSync(":memory:", { + enableDoubleQuotedStringLiterals: true, +}); +console.log( + "dqs enabled:", + (dqs.prepare('SELECT "literal" AS value').get() as any).value, +); +dqs.close(); + +const noDqs = new DatabaseSync(":memory:"); +probe("dqs default", () => noDqs.prepare('SELECT "literal" AS value').get()); +noDqs.close(); diff --git a/test-parity/node-suite/sqlite/database/disposal.ts b/test-parity/node-suite/sqlite/database/disposal.ts new file mode 100644 index 0000000000..315385df1c --- /dev/null +++ b/test-parity/node-suite/sqlite/database/disposal.ts @@ -0,0 +1,31 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +function probe(label: string, fn: () => unknown) { + try { + console.log(label, "OK", String(fn())); + } catch (error: any) { + console.log(label, "THROW", error.name, error.code || "no-code"); + } +} + +const db = new DatabaseSync(":memory:"); +const statement = db.prepare("SELECT 1 AS value"); +const session = db.createSession(); +console.log( + "dispose shapes:", + typeof (db as any)[Symbol.dispose], + typeof (session as any)[Symbol.dispose], +); +probe("session dispose", () => (session as any)[Symbol.dispose]()); +probe("session dispose twice", () => (session as any)[Symbol.dispose]()); +probe("session changeset", () => session.changeset()); + +probe("database dispose", () => (db as any)[Symbol.dispose]()); +console.log("database closed:", db.isOpen); +probe("database dispose twice", () => (db as any)[Symbol.dispose]()); +probe("statement finalized", () => statement.get()); + +const alreadyClosed = new DatabaseSync(":memory:"); +alreadyClosed.close(); +probe("dispose closed", () => (alreadyClosed as any)[Symbol.dispose]()); diff --git a/test-parity/node-suite/sqlite/database/exec-and-constraints.ts b/test-parity/node-suite/sqlite/database/exec-and-constraints.ts new file mode 100644 index 0000000000..ba6dbca5d9 --- /dev/null +++ b/test-parity/node-suite/sqlite/database/exec-and-constraints.ts @@ -0,0 +1,44 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +function probe(label: string, fn: () => unknown) { + try { + const value = fn(); + console.log(label, "OK", value === undefined ? "undefined" : String(value)); + } catch (error: any) { + console.log(label, "THROW", error?.name, error?.code || "no-code"); + } +} + +const db = new DatabaseSync(":memory:"); +probe("exec batch", () => + db.exec(` + CREATE TABLE parent(id INTEGER PRIMARY KEY); + CREATE TABLE child(parent_id INTEGER REFERENCES parent(id)); + INSERT INTO parent VALUES (1); + INSERT INTO child VALUES (1); + `), +); +console.log("counts:", db.prepare("SELECT count(*) AS n FROM parent").get().n); +probe("foreign key", () => db.exec("INSERT INTO child VALUES (99)")); +probe("syntax", () => db.exec("SELEC 1")); +for (const value of [undefined, null, 1, true, {}, []]) { + probe(`exec ${value === null ? "null" : typeof value}`, () => + db.exec(value as any), + ); +} +db.close(); + +const noForeignKeys = new DatabaseSync(":memory:", { + enableForeignKeyConstraints: false, +}); +noForeignKeys.exec(` + CREATE TABLE parent(id INTEGER PRIMARY KEY); + CREATE TABLE child(parent_id INTEGER REFERENCES parent(id)); + INSERT INTO child VALUES (99); +`); +console.log( + "foreign keys disabled:", + noForeignKeys.prepare("SELECT count(*) AS n FROM child").get().n, +); +noForeignKeys.close(); diff --git a/test-parity/node-suite/sqlite/database/lifecycle.ts b/test-parity/node-suite/sqlite/database/lifecycle.ts new file mode 100644 index 0000000000..4f656924eb --- /dev/null +++ b/test-parity/node-suite/sqlite/database/lifecycle.ts @@ -0,0 +1,31 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +function probe(label: string, fn: () => unknown) { + try { + const value = fn(); + console.log(label, "OK", value === undefined ? "undefined" : String(value)); + } catch (error: any) { + console.log(label, "THROW", error?.name, error?.code || "no-code"); + } +} + +console.log("constructor:", typeof DatabaseSync, DatabaseSync.length); +probe("without new", () => (DatabaseSync as any)(":memory:")); + +const deferred = new DatabaseSync(":memory:", { open: false }); +console.log("deferred initial:", deferred.isOpen); +probe("transaction closed", () => deferred.isTransaction); +probe("exec closed", () => deferred.exec("SELECT 1")); +probe("prepare closed", () => deferred.prepare("SELECT 1")); +probe("close closed", () => deferred.close()); +probe("open", () => deferred.open()); +console.log("opened:", deferred.isOpen, deferred.isTransaction); +probe("open twice", () => deferred.open()); +probe("close", () => deferred.close()); +console.log("closed:", deferred.isOpen); +probe("close twice", () => deferred.close()); + +const immediate = new DatabaseSync(":memory:"); +console.log("immediate:", immediate.isOpen, immediate.isTransaction); +immediate.close(); diff --git a/test-parity/node-suite/sqlite/database/limits.ts b/test-parity/node-suite/sqlite/database/limits.ts new file mode 100644 index 0000000000..0ee3ed2c4f --- /dev/null +++ b/test-parity/node-suite/sqlite/database/limits.ts @@ -0,0 +1,68 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +function probe(label: string, fn: () => unknown) { + try { + console.log(label, "OK", String(fn())); + } catch (error: any) { + console.log(label, "THROW", error.name, error.code || "no-code"); + } +} + +const names = [ + "length", + "sqlLength", + "column", + "exprDepth", + "compoundSelect", + "vdbeOp", + "functionArg", + "attach", + "likePatternLength", + "variableNumber", + "triggerDepth", +] as const; + +const db = new DatabaseSync(":memory:"); +console.log("keys:", Object.keys(db.limits).join(",")); +console.log( + "types:", + names.every((name) => typeof db.limits[name] === "number"), +); +const originalLength = db.limits.length; +db.limits.length = 100000; +console.log("set length:", db.limits.length); +db.limits.length = Infinity; +console.log("reset length:", db.limits.length === originalLength); +for (const value of [-1, 1.5, NaN, -Infinity, "1", null]) { + probe( + `set ${String(value)}`, + () => ((db.limits.length = value as any), db.limits.length), + ); +} +db.close(); +probe("get closed", () => db.limits.length); +probe("set closed", () => ((db.limits.length = 10), db.limits.length)); + +const limited = new DatabaseSync(":memory:", { + limits: { column: 3, variableNumber: 2, compoundSelect: 1 }, +}); +console.log( + "constructor limits:", + limited.limits.column, + limited.limits.variableNumber, + limited.limits.compoundSelect, +); +probe("column enforced", () => + limited.exec("CREATE TABLE too_wide(a, b, c, d)"), +); +probe("variable enforced", () => limited.prepare("SELECT ?, ?, ?")); +probe("compound enforced", () => limited.exec("SELECT 1 UNION SELECT 2")); +limited.close(); + +for (const limits of [null, 1, "limits", { length: -1 }, { length: 1.5 }]) { + probe(`constructor ${limits === null ? "null" : typeof limits}`, () => { + const invalid = new DatabaseSync(":memory:", { limits: limits as any }); + invalid.close(); + }); +} diff --git a/test-parity/node-suite/sqlite/database/options-validation.ts b/test-parity/node-suite/sqlite/database/options-validation.ts new file mode 100644 index 0000000000..79600517fd --- /dev/null +++ b/test-parity/node-suite/sqlite/database/options-validation.ts @@ -0,0 +1,45 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +function describe(value: any) { + if (value === undefined) return "undefined"; + if (value === null) return "null"; + if (Array.isArray(value)) return "array"; + return typeof value; +} + +function construct(label: string, path: any, options?: any) { + try { + const db = new DatabaseSync(path, options); + console.log(label, "OK", db.isOpen); + if (db.isOpen) db.close(); + } catch (error: any) { + console.log(label, "THROW", error?.name, error?.code || "no-code"); + } +} + +for (const path of [undefined, null, 0, true, {}, [], "bad\0path"]) { + construct(`path ${describe(path)}`, path); +} +construct("buffer memory", Buffer.from(":memory:")); + +for (const options of [null, 0, true, "x"]) { + construct(`options ${describe(options)}`, ":memory:", options); +} + +for (const name of [ + "open", + "readOnly", + "enableForeignKeyConstraints", + "enableDoubleQuotedStringLiterals", + "readBigInts", + "returnArrays", + "allowBareNamedParameters", + "allowUnknownNamedParameters", +] as const) { + construct(`option ${name}`, ":memory:", { [name]: 1 }); +} + +for (const timeout of [-1, 0.5, NaN, Infinity, "1"]) { + construct(`timeout ${String(timeout)}`, ":memory:", { timeout }); +} diff --git a/test-parity/node-suite/sqlite/database/serialize-roundtrip.ts b/test-parity/node-suite/sqlite/database/serialize-roundtrip.ts new file mode 100644 index 0000000000..744767a4e8 --- /dev/null +++ b/test-parity/node-suite/sqlite/database/serialize-roundtrip.ts @@ -0,0 +1,63 @@ +// 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 instanceof Uint8Array ? `bytes:${value.length > 0}` : String(value), + ); + } catch (error: any) { + console.log(label, "THROW", error.name, error.code || "no-code"); + } +} + +const source = new DatabaseSync(":memory:"); +console.log( + "surface:", + typeof (source as any).serialize, + typeof (source as any).deserialize, +); +source.exec("CREATE TABLE data(id INTEGER PRIMARY KEY, text TEXT, blob BLOB)"); +source + .prepare("INSERT INTO data VALUES (?, ?, ?)") + .run(1, "one", Uint8Array.from([1, 2, 3])); + +let image: Uint8Array | undefined; +try { + image = (source as any).serialize(); + const header = new TextDecoder().decode(image.slice(0, 15)); + console.log( + "serialized:", + image instanceof Uint8Array, + image.length > 0, + header, + ); +} catch (error: any) { + console.log("serialized: THROW", error.name, error.code || "no-code"); +} + +if (image) { + const target = new DatabaseSync(":memory:"); + target.exec("CREATE TABLE replaced(value TEXT)"); + probe("deserialize", () => (target as any).deserialize(image)); + const row: any = target.prepare("SELECT id, text, blob FROM data").get(); + console.log("roundtrip:", row.id, row.text, Array.from(row.blob).join(",")); + target.close(); +} + +probe("serialize dbName", () => (source as any).serialize("main")); +probe("serialize bad dbName", () => (source as any).serialize(1)); +source.close(); +probe("serialize closed", () => (source as any).serialize()); + +const validation = new DatabaseSync(":memory:"); +for (const value of [undefined, null, "image", {}, [], new Uint8Array()]) { + probe( + `deserialize ${value === null ? "null" : Array.isArray(value) ? "array" : typeof value}`, + () => (validation as any).deserialize(value), + ); +} +validation.close(); diff --git a/test-parity/node-suite/sqlite/database/type-symbol.ts b/test-parity/node-suite/sqlite/database/type-symbol.ts new file mode 100644 index 0000000000..174dddf83e --- /dev/null +++ b/test-parity/node-suite/sqlite/database/type-symbol.ts @@ -0,0 +1,12 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +const db = new DatabaseSync(":memory:"); +const sqliteType = Symbol.for("sqlite-type"); +console.log("value:", (db as any)[sqliteType]); +console.log("own:", Object.prototype.hasOwnProperty.call(db, sqliteType)); +console.log( + "symbol key:", + Object.getOwnPropertySymbols(db).includes(sqliteType), +); +db.close(); diff --git a/test-parity/node-suite/sqlite/extension-loading/controls.ts b/test-parity/node-suite/sqlite/extension-loading/controls.ts index 719a817ca7..75cdf7065f 100644 --- a/test-parity/node-suite/sqlite/extension-loading/controls.ts +++ b/test-parity/node-suite/sqlite/extension-loading/controls.ts @@ -1,7 +1,13 @@ // parity-node-argv: --experimental-sqlite import { DatabaseSync } from "node:sqlite"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; -const missingExtensionPath = "/tmp/perry-node-sqlite-missing-extension"; +const extensionDir = fs.mkdtempSync( + path.join(os.tmpdir(), "perry-node-sqlite-extension-"), +); +const missingExtensionPath = path.join(extensionDir, "missing-extension"); function summarize(value) { return value === undefined ? "undefined" : String(value); @@ -22,14 +28,20 @@ function report(label, fn) { console.log("constructor:", typeof DatabaseSync); const defaultDb = new DatabaseSync(":memory:"); -console.log("method shapes:", typeof defaultDb.enableLoadExtension, typeof defaultDb.loadExtension); +console.log( + "method shapes:", + typeof defaultDb.enableLoadExtension, + typeof defaultDb.loadExtension, +); report("default enable false", () => defaultDb.enableLoadExtension(false)); report("default enable true", () => defaultDb.enableLoadExtension(true)); report("default load", () => defaultDb.loadExtension(missingExtensionPath)); for (const value of [undefined, null, 0, "true"]) { - report(`enable arg ${String(value)}`, () => defaultDb.enableLoadExtension(value)); + report(`enable arg ${String(value)}`, () => + defaultDb.enableLoadExtension(value), + ); } for (const value of [null, 1, "true"]) { @@ -42,6 +54,14 @@ for (const value of [null, 1, "true"]) { const enabledDb = new DatabaseSync(":memory:", { allowExtension: true }); report("enabled enable true", () => enabledDb.enableLoadExtension(true)); -report("enabled load missing", () => enabledDb.loadExtension(missingExtensionPath)); +report("enabled load missing", () => + enabledDb.loadExtension(missingExtensionPath), +); report("enabled disable", () => enabledDb.enableLoadExtension(false)); -report("enabled load disabled", () => enabledDb.loadExtension(missingExtensionPath)); +report("enabled load disabled", () => + enabledDb.loadExtension(missingExtensionPath), +); + +defaultDb.close(); +enabledDb.close(); +fs.rmSync(extensionDir, { recursive: true, force: true }); diff --git a/test-parity/node-suite/sqlite/functions/aggregate-direct-only.ts b/test-parity/node-suite/sqlite/functions/aggregate-direct-only.ts new file mode 100644 index 0000000000..bf7e59574d --- /dev/null +++ b/test-parity/node-suite/sqlite/functions/aggregate-direct-only.ts @@ -0,0 +1,51 @@ +// 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:"); + +function register(name: string, directOnly?: boolean) { + const options: any = { + start: 0, + step: (total: number, value: number) => total + value, + inverse: (total: number, value: number) => total - value, + }; + if (directOnly !== undefined) options.directOnly = directOnly; + db.aggregate(name, options); + db.exec(` + CREATE TABLE ${name}_data(value INTEGER); + CREATE TRIGGER ${name}_trigger AFTER INSERT ON ${name}_data BEGIN + SELECT ${name}(value) OVER () FROM ${name}_data; + END; + `); +} + +register("default_aggregate"); +register("false_aggregate", false); +register("true_aggregate", true); + +probe("default trigger", () => + db.prepare("INSERT INTO default_aggregate_data VALUES (?)").run(1), +); +probe("false trigger", () => + db.prepare("INSERT INTO false_aggregate_data VALUES (?)").run(2), +); +probe("true trigger", () => + db.prepare("INSERT INTO true_aggregate_data VALUES (?)").run(3), +); +console.log( + "counts:", + (db.prepare("SELECT count(*) AS n FROM default_aggregate_data").get() as any) + .n, + (db.prepare("SELECT count(*) AS n FROM false_aggregate_data").get() as any).n, + (db.prepare("SELECT count(*) AS n FROM true_aggregate_data").get() as any).n, +); +db.close(); diff --git a/test-parity/node-suite/sqlite/functions/aggregate-options.ts b/test-parity/node-suite/sqlite/functions/aggregate-options.ts new file mode 100644 index 0000000000..6d5e0566a2 --- /dev/null +++ b/test-parity/node-suite/sqlite/functions/aggregate-options.ts @@ -0,0 +1,63 @@ +// 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:", { readBigInts: true }); +db.exec( + "CREATE TABLE data(value INTEGER); INSERT INTO data VALUES (1), (2), (3)", +); +db.aggregate("big_sum", { + start: 0n, + useBigIntArguments: true, + step: (total: bigint, value: bigint) => total + value, +}); +const big: any = db.prepare("SELECT big_sum(value) AS value FROM data").get(); +console.log("bigint:", typeof big.value, String(big.value)); + +for (const [label, options] of [ + ["missing start", { step: () => 0 }], + ["missing step", { start: 0 }], + ["bigint option", { start: 0, step: () => 0, useBigIntArguments: "true" }], + ["varargs option", { start: 0, step: () => 0, varargs: 1 }], + ["direct option", { start: 0, step: () => 0, directOnly: null }], + ["inverse option", { start: 0, step: () => 0, inverse: 1 }], + ["result option", { start: 0, step: () => 0, result: 1 }], +] as const) { + probe(label, () => + db.aggregate(`invalid_${label.replace(" ", "_")}`, options as any), + ); +} + +db.aggregate("start_error", { + start: () => { + throw new RangeError("start marker"); + }, + step: () => 0, +}); +probe("start error", () => db.prepare("SELECT start_error() AS value").get()); + +db.aggregate("step_error", { + start: 0, + step: () => { + throw new RangeError("step marker"); + }, +}); +probe("step error", () => db.prepare("SELECT step_error() AS value").get()); + +db.aggregate("result_error", { + start: 0, + step: (total: number) => total, + result: () => { + throw new RangeError("result marker"); + }, +}); +probe("result error", () => db.prepare("SELECT result_error() AS value").get()); +db.close(); diff --git a/test-parity/node-suite/sqlite/functions/aggregate-window.ts b/test-parity/node-suite/sqlite/functions/aggregate-window.ts new file mode 100644 index 0000000000..1844fa2937 --- /dev/null +++ b/test-parity/node-suite/sqlite/functions/aggregate-window.ts @@ -0,0 +1,62 @@ +// 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:"); +db.exec( + "CREATE TABLE data(id INTEGER, value INTEGER); INSERT INTO data VALUES (1, 4), (2, 5), (3, 3)", +); +let results = 0; +db.aggregate("moving_sum", { + start: 0, + step: (total: number, value: number) => total + value, + inverse: (total: number, value: number) => total - value, + result: (total: number) => { + results++; + return total; + }, +}); +const rows: any[] = db + .prepare( + ` + SELECT id, moving_sum(value) OVER ( + ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING + ) AS value + FROM data ORDER BY id + `, + ) + .all(); +console.log( + "window:", + rows.map((row) => `${row.id}:${row.value}`).join(","), + results, +); + +db.aggregate("aggregate_only", { + start: 0, + step: (total: number, value: number) => total + value, +}); +probe("missing inverse", () => + db.prepare("SELECT aggregate_only(value) OVER () AS value FROM data").get(), +); + +db.aggregate("fixed_arity", { + start: 0, + step: (total: number, first: number, second: number) => + total + first + second, +}); +probe("fixed valid", () => + db.prepare("SELECT fixed_arity(1, 2) AS value").get(), +); +probe("fixed invalid", () => + db.prepare("SELECT fixed_arity(1) AS value").get(), +); +db.close(); diff --git a/test-parity/node-suite/sqlite/functions/aggregate.ts b/test-parity/node-suite/sqlite/functions/aggregate.ts new file mode 100644 index 0000000000..10036961b0 --- /dev/null +++ b/test-parity/node-suite/sqlite/functions/aggregate.ts @@ -0,0 +1,59 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +function probe(label: string, fn: () => unknown) { + try { + console.log(label, "OK", String(fn())); + } catch (error: any) { + console.log(label, "THROW", error.name, error.code || "no-code"); + } +} + +const db = new DatabaseSync(":memory:"); +db.exec( + "CREATE TABLE data(value INTEGER); INSERT INTO data VALUES (1), (2), (3)", +); + +db.aggregate("sum_plus", { + start: 10, + step: (total: number, value: number) => total + value, + result: (total: number) => total * 2, +}); +console.log( + "value start:", + db.prepare("SELECT sum_plus(value) AS value FROM data").get().value, +); + +let starts = 0; +db.aggregate("joined", { + start: () => { + starts++; + return ""; + }, + step: (text: string, value: number) => text + value, +}); +console.log( + "function start:", + db.prepare("SELECT joined(value) AS value FROM data").get().value, + starts, +); + +db.aggregate("var_count", { + start: 0, + varargs: true, + step: (count: number, ...values: any[]) => count + values.length, +}); +console.log( + "varargs:", + db.prepare("SELECT var_count(value, value + 1) AS value FROM data").get() + .value, +); + +for (const [label, call] of [ + ["name", () => db.aggregate(1 as any, { step: () => 0 })], + ["options", () => db.aggregate("bad_options", null as any)], + ["step", () => db.aggregate("bad_step", { step: 1 as any })], +] as const) { + probe(`invalid ${label}`, call); +} +db.close(); diff --git a/test-parity/node-suite/sqlite/functions/scalar.ts b/test-parity/node-suite/sqlite/functions/scalar.ts new file mode 100644 index 0000000000..17f0ba4c51 --- /dev/null +++ b/test-parity/node-suite/sqlite/functions/scalar.ts @@ -0,0 +1,51 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +function probe(label: string, fn: () => unknown) { + try { + console.log(label, "OK", String(fn())); + } catch (error: any) { + console.log(label, "THROW", error.name, error.code || "no-code"); + } +} + +const db = new DatabaseSync(":memory:"); +let calls = 0; +db.function("double_value", { deterministic: true }, (value: number) => { + calls++; + return value * 2; +}); +db.function("join_values", { varargs: true }, (...values: any[]) => + values.join("|"), +); +db.function("blob_tail", (value: Uint8Array) => value[value.length - 1]); + +console.log( + "double:", + db.prepare("SELECT double_value(21) AS value").get().value, + calls, +); +console.log( + "varargs:", + db.prepare("SELECT join_values('a', 2, NULL) AS value").get().value, +); +console.log( + "blob:", + db.prepare("SELECT blob_tail(?) AS value").get(Uint8Array.from([3, 7, 9])) + .value, +); +probe("wrong arity", () => db.prepare("SELECT double_value(1, 2)").get()); + +db.function("throws_value", () => { + throw new RangeError("callback marker"); +}); +probe("callback throw", () => db.prepare("SELECT throws_value()").get()); + +for (const [label, call] of [ + ["name", () => db.function(1 as any, () => 1)], + ["callback", () => db.function("bad_callback", 1 as any)], + ["options", () => db.function("bad_options", null as any, () => 1)], +] as const) { + probe(`invalid ${label}`, call); +} +db.close(); diff --git a/test-parity/node-suite/sqlite/functions/value-roundtrip.ts b/test-parity/node-suite/sqlite/functions/value-roundtrip.ts new file mode 100644 index 0000000000..73434d61db --- /dev/null +++ b/test-parity/node-suite/sqlite/functions/value-roundtrip.ts @@ -0,0 +1,76 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +function probe(label: string, fn: () => unknown) { + try { + const value: any = fn(); + if ( + value && + typeof value === "object" && + Object.prototype.hasOwnProperty.call(value, "value") + ) { + console.log(label, "OK", `${typeof value.value}:${String(value.value)}`); + } else { + console.log(label, "OK", String(value)); + } + } catch (error: any) { + console.log(label, "THROW", error.name, error.code || "no-code"); + } +} + +const db = new DatabaseSync(":memory:", { readBigInts: true }); +const seen: string[] = []; +db.function("describe_value", (value: any) => { + seen.push( + value instanceof Uint8Array + ? `blob:${Array.from(value).join(",")}` + : `${typeof value}:${String(value)}`, + ); + return value; +}); + +const statement = db.prepare("SELECT describe_value(?) AS value"); +for (const [label, value] of [ + ["null", null], + ["integer", 42], + ["real", 1.25], + ["text", "hello"], + ["blob", Uint8Array.from([7, 8])], +] as const) { + const row: any = statement.get(value); + console.log( + label, + typeof row.value, + row.value instanceof Uint8Array + ? Array.from(row.value).join(",") + : String(row.value), + ); +} + +db.function( + "describe_bigint", + { useBigIntArguments: true }, + (value: bigint) => { + seen.push(`bigint:${String(value)}`); + return value; + }, +); +const big: any = db + .prepare("SELECT describe_bigint(?) AS value") + .get(9007199254740993n); +console.log("bigint", typeof big.value, String(big.value)); +console.log("seen:", seen.join("|")); + +for (const [label, value] of [ + ["undefined", undefined], + ["boolean", true], + ["object", {}], + ["array", []], + ["symbol", Symbol("value")], +] as const) { + db.function(`return_${label}`, () => value as any); + probe(`return ${label}`, () => + db.prepare(`SELECT return_${label}() AS value`).get(), + ); +} +db.close(); diff --git a/test-parity/node-suite/sqlite/metadata/columns.ts b/test-parity/node-suite/sqlite/metadata/columns.ts new file mode 100644 index 0000000000..2a275cafde --- /dev/null +++ b/test-parity/node-suite/sqlite/metadata/columns.ts @@ -0,0 +1,41 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +function describe(label: string, statement: any) { + const columns = statement.columns(); + console.log(label, "count", columns.length); + for (const column of columns) { + console.log( + label, + Object.getPrototypeOf(column) === null ? "null-proto" : "other-proto", + String(column.name), + String(column.column), + String(column.table), + String(column.database), + String(column.type), + ); + } +} + +const db = new DatabaseSync(":memory:"); +db.exec(` + CREATE TABLE left_table(id INTEGER PRIMARY KEY, name TEXT, payload BLOB); + CREATE TABLE right_table(owner INTEGER, score REAL); +`); +describe( + "join", + db.prepare(` + SELECT l.id AS item_id, l.name, r.score, l.id + 1 AS next_id + FROM left_table AS l JOIN right_table AS r ON r.owner = l.id + `), +); +describe("write", db.prepare("INSERT INTO left_table(name) VALUES (?)")); + +const closedStatement = db.prepare("SELECT name FROM left_table"); +db.close(); +try { + closedStatement.columns(); + console.log("closed columns: OK"); +} catch (error: any) { + console.log("closed columns: THROW", error.name, error.code || "no-code"); +} diff --git a/test-parity/node-suite/sqlite/metadata/result-toggles.ts b/test-parity/node-suite/sqlite/metadata/result-toggles.ts new file mode 100644 index 0000000000..cd95bddd6c --- /dev/null +++ b/test-parity/node-suite/sqlite/metadata/result-toggles.ts @@ -0,0 +1,60 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +function show(label: string, row: any) { + if (Array.isArray(row)) { + console.log( + label, + "array", + row.map((value) => `${typeof value}:${String(value)}`).join(","), + ); + } else { + console.log( + label, + "object", + Object.keys(row) + .map((key) => `${key}=${typeof row[key]}:${String(row[key])}`) + .join(","), + ); + } +} + +function probe(label: string, fn: () => unknown) { + try { + console.log(label, "OK", String(fn())); + } 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, big INTEGER); INSERT INTO data VALUES (1, 9007199254740992)", +); +const statement = db.prepare("SELECT id, big FROM data"); +probe("number overflow", () => statement.get()); +statement.setReadBigInts(true); +show("bigints", statement.get()); +statement.setReturnArrays(true); +show("arrays and bigints", statement.get()); +statement.setReadBigInts(false); +probe("array overflow", () => statement.get()); + +const configured = db.prepare("SELECT 2 AS id, 42 AS value", { + readBigInts: true, + returnArrays: true, +}); +show("prepare options", configured.get()); +configured.setReturnArrays(false); +configured.setReadBigInts(false); +show("overridden", configured.get()); + +for (const [label, call] of [ + ["read", () => statement.setReadBigInts(undefined as any)], + ["arrays", () => statement.setReturnArrays(1 as any)], + ["bare", () => statement.setAllowBareNamedParameters("true" as any)], + ["unknown", () => statement.setAllowUnknownNamedParameters(null as any)], +] as const) { + probe(`invalid ${label}`, call); +} +db.close(); diff --git a/test-parity/node-suite/sqlite/metadata/sql-text.ts b/test-parity/node-suite/sqlite/metadata/sql-text.ts new file mode 100644 index 0000000000..05ee9b939a --- /dev/null +++ b/test-parity/node-suite/sqlite/metadata/sql-text.ts @@ -0,0 +1,14 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +const db = new DatabaseSync(":memory:"); +db.exec("CREATE TABLE data(id INTEGER PRIMARY KEY, name TEXT, payload BLOB)"); +const sql = "INSERT INTO data(id, name, payload) VALUES ($id, ?, ?)"; +const statement = db.prepare(sql); +console.log("source before:", statement.sourceSQL); +console.log("expanded before:", statement.expandedSQL); +statement.run({ id: 7 }, "O'Reilly", Uint8Array.from([0, 1, 255])); +console.log("source after:", statement.sourceSQL); +console.log("expanded after:", statement.expandedSQL); +console.log("source stable:", statement.sourceSQL === sql); +db.close(); diff --git a/test-parity/node-suite/sqlite/sessions/changeset-roundtrip.ts b/test-parity/node-suite/sqlite/sessions/changeset-roundtrip.ts new file mode 100644 index 0000000000..59e74b0135 --- /dev/null +++ b/test-parity/node-suite/sqlite/sessions/changeset-roundtrip.ts @@ -0,0 +1,55 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +function createDatabase() { + const db = new DatabaseSync(":memory:"); + db.exec("CREATE TABLE data(id INTEGER PRIMARY KEY, value TEXT) STRICT"); + return db; +} + +function probe(label: string, fn: () => unknown) { + try { + const value: any = fn(); + console.log( + label, + "OK", + value instanceof Uint8Array ? `bytes:${value.length}` : String(value), + ); + } catch (error: any) { + console.log(label, "THROW", error.name, error.code || "no-code"); + } +} + +const source = createDatabase(); +const session = source.createSession(); +source.prepare("INSERT INTO data VALUES (?, ?)").run(1, "one"); +source.prepare("INSERT INTO data VALUES (?, ?)").run(2, "two"); +const changeset = session.changeset(); +const patchset = session.patchset(); +console.log( + "sets:", + changeset instanceof Uint8Array, + changeset.length > 0, + patchset instanceof Uint8Array, + patchset.length > 0, +); + +const target = createDatabase(); +console.log("apply:", target.applyChangeset(changeset)); +console.log( + "rows:", + ( + target + .prepare("SELECT id || ':' || value AS row FROM data ORDER BY id") + .all() as any[] + ) + .map((row) => row.row) + .join(","), +); + +session.close(); +probe("changeset closed session", () => session.changeset()); +source.close(); +target.close(); +probe("create closed db", () => source.createSession()); +probe("apply closed db", () => target.applyChangeset(changeset)); diff --git a/test-parity/node-suite/sqlite/sessions/table-filter.ts b/test-parity/node-suite/sqlite/sessions/table-filter.ts new file mode 100644 index 0000000000..499b273b6c --- /dev/null +++ b/test-parity/node-suite/sqlite/sessions/table-filter.ts @@ -0,0 +1,32 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +function createDatabase() { + const db = new DatabaseSync(":memory:"); + db.exec(` + CREATE TABLE tracked(id INTEGER PRIMARY KEY, value TEXT) STRICT; + CREATE TABLE ignored(id INTEGER PRIMARY KEY, value TEXT) STRICT; + `); + return db; +} + +const source = createDatabase(); +const session = source.createSession({ table: "tracked" }); +source.prepare("INSERT INTO tracked VALUES (?, ?)").run(1, "tracked"); +source.prepare("INSERT INTO ignored VALUES (?, ?)").run(1, "ignored"); +const changeset = session.changeset(); + +const target = createDatabase(); +console.log("apply:", target.applyChangeset(changeset)); +console.log( + "tracked:", + (target.prepare("SELECT value FROM tracked").get() as any).value, +); +console.log( + "ignored:", + target.prepare("SELECT value FROM ignored").all().length, +); + +session.close(); +source.close(); +target.close(); diff --git a/test-parity/node-suite/sqlite/statements/change-metadata.ts b/test-parity/node-suite/sqlite/statements/change-metadata.ts new file mode 100644 index 0000000000..6695619fbf --- /dev/null +++ b/test-parity/node-suite/sqlite/statements/change-metadata.ts @@ -0,0 +1,30 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +function result(label: string, value: any) { + console.log( + label, + typeof value.changes, + String(value.changes), + typeof value.lastInsertRowid, + String(value.lastInsertRowid), + ); +} + +const db = new DatabaseSync(":memory:"); +result( + "create:", + db.prepare("CREATE TABLE data(id INTEGER PRIMARY KEY, value TEXT)").run(), +); +const insert = db.prepare("INSERT INTO data(value) VALUES (?)"); +result("insert one:", insert.run("a")); +result("insert two:", insert.run("b")); +result("update:", db.prepare("UPDATE data SET value = upper(value)").run()); +result("delete:", db.prepare("DELETE FROM data WHERE id = ?").run(1)); +result("select:", db.prepare("SELECT * FROM data").run()); +console.log( + "remaining:", + db.prepare("SELECT id, value FROM data").get().id, + db.prepare("SELECT value FROM data").get().value, +); +db.close(); diff --git a/test-parity/node-suite/sqlite/statements/iterate.ts b/test-parity/node-suite/sqlite/statements/iterate.ts new file mode 100644 index 0000000000..3cd0573420 --- /dev/null +++ b/test-parity/node-suite/sqlite/statements/iterate.ts @@ -0,0 +1,27 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +const db = new DatabaseSync(":memory:"); +db.exec(` + CREATE TABLE items(id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO items(name) VALUES ('a'), ('b'), ('c'); +`); + +const statement = db.prepare("SELECT id, name FROM items ORDER BY id"); +const iterator: any = statement.iterate(); +console.log("shape:", typeof iterator.next, typeof iterator[Symbol.iterator]); +console.log("self iterable:", iterator[Symbol.iterator]() === iterator); +const first = iterator.next(); +console.log("first:", first.done, first.value.id, first.value.name); +const rest: string[] = []; +for (const row of iterator) rest.push(`${row.id}:${row.name}`); +console.log("rest:", rest.join(",")); +console.log("done:", JSON.stringify(iterator.next())); + +const secondPass = [...statement.iterate()].map((row: any) => row.name); +console.log("second pass:", secondPass.join(",")); +console.log( + "empty:", + [...db.prepare("SELECT * FROM items WHERE id = -1").iterate()].length, +); +db.close(); diff --git a/test-parity/node-suite/sqlite/statements/iterator-invalidation.ts b/test-parity/node-suite/sqlite/statements/iterator-invalidation.ts new file mode 100644 index 0000000000..c52f63b4fc --- /dev/null +++ b/test-parity/node-suite/sqlite/statements/iterator-invalidation.ts @@ -0,0 +1,45 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +function next(label: string, iterator: Iterator) { + try { + const value = iterator.next(); + console.log(label, "OK", value.done, value.value?.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(value INTEGER); INSERT INTO data VALUES (1), (2), (3)", +); +const statement = db.prepare("SELECT value FROM data ORDER BY value"); + +let iterator = statement.iterate(); +next("get before", iterator); +statement.get(); +next("get invalidates", iterator); + +iterator = statement.iterate(); +next("all before", iterator); +statement.all(); +next("all invalidates", iterator); + +iterator = statement.iterate(); +next("run before", iterator); +statement.run(); +next("run invalidates", iterator); + +iterator = statement.iterate(); +next("iterate before", iterator); +const replacement = statement.iterate(); +next("iterate invalidates", iterator); +next("replacement works", replacement); + +const other = db.prepare("SELECT value FROM data ORDER BY value DESC"); +iterator = statement.iterate(); +next("other before", iterator); +other.get(); +next("other preserves", iterator); +db.close(); diff --git a/test-parity/node-suite/sqlite/statements/prepare-validation.ts b/test-parity/node-suite/sqlite/statements/prepare-validation.ts new file mode 100644 index 0000000000..ff10f08328 --- /dev/null +++ b/test-parity/node-suite/sqlite/statements/prepare-validation.ts @@ -0,0 +1,30 @@ +// 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", typeof value); + } catch (error: any) { + console.log(label, "THROW", error.name, error.code || "no-code"); + } +} + +const db = new DatabaseSync(":memory:"); +probe("valid", () => db.prepare("SELECT 1")); +probe("syntax", () => db.prepare("SELEC 1")); +probe("multiple", () => db.prepare("SELECT 1; SELECT 2")); +probe("empty", () => db.prepare("")); +probe("nul", () => db.prepare("SELECT\0 1")); +for (const value of [undefined, null, 1, true, {}, []]) { + probe( + `value ${value === null ? "null" : Array.isArray(value) ? "array" : typeof value}`, + () => db.prepare(value as any), + ); +} +for (const options of [null, 1, true, "options"]) { + probe(`options ${options === null ? "null" : typeof options}`, () => + db.prepare("SELECT 1", options as any), + ); +} +db.close(); diff --git a/test-parity/node-suite/sqlite/statements/run-get-all.ts b/test-parity/node-suite/sqlite/statements/run-get-all.ts new file mode 100644 index 0000000000..b2e95029a2 --- /dev/null +++ b/test-parity/node-suite/sqlite/statements/run-get-all.ts @@ -0,0 +1,51 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync, StatementSync } from "node:sqlite"; + +function proto(row: any) { + return row === undefined + ? "undefined" + : Object.getPrototypeOf(row) === null + ? "null" + : "other"; +} + +const db = new DatabaseSync(":memory:"); +console.log("StatementSync:", typeof StatementSync, StatementSync.length); +try { + new (StatementSync as any)(); + console.log("direct construct: OK"); +} catch (error: any) { + console.log("direct construct: THROW", error.name, error.code || "no-code"); +} + +console.log( + "exec return:", + String( + db.exec( + "CREATE TABLE items(id INTEGER PRIMARY KEY, name TEXT, score REAL)", + ), + ), +); +const insert = db.prepare("INSERT INTO items(name, score) VALUES (?, ?)"); +console.log("insert 1:", JSON.stringify(insert.run("alpha", 1.5))); +console.log("insert 2:", JSON.stringify(insert.run("beta", 2.5))); + +const select = db.prepare("SELECT id, name, score FROM items ORDER BY id"); +const first: any = select.get(); +console.log("get:", proto(first), first.id, first.name, first.score); +const all: any[] = select.all(); +console.log( + "all:", + all.length, + proto(all[0]), + all.map((row) => row.name).join(","), +); +console.log( + "empty get:", + String(db.prepare("SELECT * FROM items WHERE id = -1").get()), +); +console.log( + "empty all:", + db.prepare("SELECT * FROM items WHERE id = -1").all().length, +); +db.close(); diff --git a/test-parity/node-suite/sqlite/statements/special-column-names.ts b/test-parity/node-suite/sqlite/statements/special-column-names.ts new file mode 100644 index 0000000000..0af04e97e2 --- /dev/null +++ b/test-parity/node-suite/sqlite/statements/special-column-names.ts @@ -0,0 +1,31 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +const db = new DatabaseSync(":memory:"); +const row: any = db + .prepare( + "SELECT 1 AS __proto__, 2 AS constructor, 3 AS toString, 4 AS hasOwnProperty", + ) + .get(); +console.log("prototype:", Object.getPrototypeOf(row) === null); +console.log("keys:", Object.keys(row).join(",")); +console.log( + "values:", + row.__proto__, + row.constructor, + row.toString, + row.hasOwnProperty, +); +console.log( + "own:", + Object.prototype.hasOwnProperty.call(row, "__proto__"), + Object.prototype.hasOwnProperty.call(row, "constructor"), + Object.prototype.hasOwnProperty.call(row, "toString"), + Object.prototype.hasOwnProperty.call(row, "hasOwnProperty"), +); + +const arrays = db + .prepare("SELECT 5 AS __proto__, 6 AS constructor", { returnArrays: true }) + .get() as any[]; +console.log("array values:", arrays.join(",")); +db.close(); diff --git a/test-parity/node-suite/sqlite/tag-store/basic-operations.ts b/test-parity/node-suite/sqlite/tag-store/basic-operations.ts new file mode 100644 index 0000000000..6b2975ca08 --- /dev/null +++ b/test-parity/node-suite/sqlite/tag-store/basic-operations.ts @@ -0,0 +1,45 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +const db = new DatabaseSync(":memory:"); +db.exec("CREATE TABLE items(id INTEGER PRIMARY KEY, name TEXT)"); +const sql = db.createTagStore(10); +console.log( + "shape:", + typeof sql.run, + typeof sql.get, + typeof sql.all, + typeof sql.iterate, + typeof sql.clear, +); +console.log("identity:", sql.db === db, sql.capacity, sql.size); + +console.log( + "run 1:", + String(sql.run`INSERT INTO items(name) VALUES (${"alpha"})`.changes), + sql.size, +); +console.log( + "run 2:", + String(sql.run`INSERT INTO items(name) VALUES (${"beta"})`.changes), + sql.size, +); +const first: any = sql.get`SELECT id, name FROM items WHERE name = ${"alpha"}`; +console.log( + "get:", + first.id, + first.name, + Object.getPrototypeOf(first) === null, + sql.size, +); +const all: any[] = sql.all`SELECT id, name FROM items ORDER BY id`; +console.log("all:", all.length, all.map((row) => row.name).join(","), sql.size); +const iterated = [...sql.iterate`SELECT name FROM items ORDER BY id`].map( + (row: any) => row.name, +); +console.log("iterate:", iterated.join(","), sql.size); +console.log("missing:", String(sql.get`SELECT * FROM items WHERE id = ${99}`)); + +sql.clear(); +console.log("cleared:", sql.size, sql.capacity); +db.close(); diff --git a/test-parity/node-suite/sqlite/tag-store/capacity-and-eviction.ts b/test-parity/node-suite/sqlite/tag-store/capacity-and-eviction.ts new file mode 100644 index 0000000000..683a79d68f --- /dev/null +++ b/test-parity/node-suite/sqlite/tag-store/capacity-and-eviction.ts @@ -0,0 +1,39 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +const db = new DatabaseSync(":memory:"); +const small = db.createTagStore(2); +console.log("small initial:", small.capacity, small.size); +console.log("one:", (small.get`SELECT ${1} AS value` as any).value, small.size); +console.log( + "two:", + (small.get`SELECT ${2} AS value, 2 AS marker` as any).value, + small.size, +); +console.log( + "one cached:", + (small.get`SELECT ${3} AS value` as any).value, + small.size, +); +console.log( + "three evicts:", + (small.get`SELECT ${4} AS value, 3 AS marker` as any).value, + small.size, +); +console.log( + "two again:", + (small.get`SELECT ${5} AS value, 2 AS marker` as any).value, + small.size, +); + +const zero = db.createTagStore(0); +console.log( + "zero:", + zero.capacity, + zero.size, + (zero.get`SELECT ${6} AS value` as any).value, + zero.size, +); +const defaults = db.createTagStore(); +console.log("default:", defaults.capacity, defaults.size); +db.close(); diff --git a/test-parity/node-suite/sqlite/tag-store/interpolation-safety.ts b/test-parity/node-suite/sqlite/tag-store/interpolation-safety.ts new file mode 100644 index 0000000000..700e5a629f --- /dev/null +++ b/test-parity/node-suite/sqlite/tag-store/interpolation-safety.ts @@ -0,0 +1,24 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +const db = new DatabaseSync(":memory:"); +db.exec("CREATE TABLE data(value TEXT)"); +const sql = db.createTagStore(5); +const payload = "x'); DROP TABLE data; --"; +console.log( + "insert:", + String(sql.run`INSERT INTO data(value) VALUES (${payload})`.changes), +); +console.log("stored:", (sql.get`SELECT value FROM data` as any).value); +console.log( + "table intact:", + (sql.get`SELECT count(*) AS n FROM data` as any).n, +); + +function lookup(value: string) { + return sql.get`SELECT value FROM data WHERE value = ${value}` as any; +} +console.log("cached first:", lookup(payload).value, sql.size); +console.log("cached miss:", String(lookup("missing")), sql.size); +console.log("cached again:", lookup(payload).value, sql.size); +db.close(); diff --git a/test-parity/node-suite/sqlite/tag-store/validation-and-close.ts b/test-parity/node-suite/sqlite/tag-store/validation-and-close.ts new file mode 100644 index 0000000000..39973532be --- /dev/null +++ b/test-parity/node-suite/sqlite/tag-store/validation-and-close.ts @@ -0,0 +1,38 @@ +// 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 closed = new DatabaseSync(":memory:", { open: false }); +probe("create closed", () => closed.createTagStore(1)); + +const db = new DatabaseSync(":memory:"); +const sql: any = db.createTagStore(2); +for (const [label, call] of [ + ["get string", () => sql.get("SELECT 1")], + ["all object", () => sql.all({})], + ["run number", () => sql.run(1)], + ["iterate null", () => sql.iterate(null)], +] as const) { + probe(label, call); +} + +probe("sql error", () => sql.get`SELECT * FROM missing_table`); +console.log( + "before close:", + (sql.get`SELECT ${1} AS value` as any).value, + sql.size, +); +db.close(); +probe("get after close", () => sql.get`SELECT 1 AS value`); +probe("all after close", () => sql.all`SELECT 1 AS value`); +probe("run after close", () => sql.run`SELECT 1`); +probe("iterate after close", () => sql.iterate`SELECT 1 AS value`); +console.log("closed db identity:", sql.db === db, db.isOpen); diff --git a/test-parity/node-suite/sqlite/transactions/commit-rollback.ts b/test-parity/node-suite/sqlite/transactions/commit-rollback.ts new file mode 100644 index 0000000000..8a9cccdaf0 --- /dev/null +++ b/test-parity/node-suite/sqlite/transactions/commit-rollback.ts @@ -0,0 +1,36 @@ +// parity-node-argv: --experimental-sqlite +import { DatabaseSync } from "node:sqlite"; + +const db = new DatabaseSync(":memory:"); +db.exec("CREATE TABLE events(value TEXT)"); +console.log("initial:", db.isTransaction); + +db.exec("BEGIN"); +console.log("after begin:", db.isTransaction); +db.prepare("INSERT INTO events VALUES (?)").run("committed"); +db.exec("COMMIT"); +console.log( + "after commit:", + db.isTransaction, + db.prepare("SELECT count(*) AS n FROM events").get().n, +); + +db.exec("BEGIN IMMEDIATE"); +db.prepare("INSERT INTO events VALUES (?)").run("rolled back"); +console.log("during rollback:", db.isTransaction); +db.exec("ROLLBACK"); +console.log( + "after rollback:", + db.isTransaction, + db.prepare("SELECT count(*) AS n FROM events").get().n, +); + +db.exec("BEGIN; SAVEPOINT nested"); +db.prepare("INSERT INTO events VALUES (?)").run("savepoint"); +db.exec("ROLLBACK TO nested; RELEASE nested; COMMIT"); +console.log( + "after savepoint:", + db.isTransaction, + db.prepare("SELECT count(*) AS n FROM events").get().n, +); +db.close(); diff --git a/test-parity/node-suite/sqlite/transactions/constraint-errors.ts b/test-parity/node-suite/sqlite/transactions/constraint-errors.ts new file mode 100644 index 0000000000..ff7d55b1b8 --- /dev/null +++ b/test-parity/node-suite/sqlite/transactions/constraint-errors.ts @@ -0,0 +1,46 @@ +// 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", + typeof error.errcode === "number" ? error.errcode : "no-errcode", + ); + } +} + +const db = new DatabaseSync(":memory:"); +db.exec(` + CREATE TABLE data( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + score INTEGER CHECK(score >= 0) + ) STRICT; +`); +probe("valid", () => + db.prepare("INSERT INTO data VALUES (?, ?, ?)").run(1, "one", 1), +); +probe("primary", () => + db.prepare("INSERT INTO data VALUES (?, ?, ?)").run(1, "two", 2), +); +probe("unique", () => + db.prepare("INSERT INTO data VALUES (?, ?, ?)").run(2, "one", 2), +); +probe("not null", () => + db.prepare("INSERT INTO data VALUES (?, ?, ?)").run(3, null, 3), +); +probe("check", () => + db.prepare("INSERT INTO data VALUES (?, ?, ?)").run(4, "four", -1), +); +probe("strict type", () => + db.prepare("INSERT INTO data VALUES (?, ?, ?)").run(5, "five", "bad"), +); +console.log("survivors:", db.prepare("SELECT count(*) AS n FROM data").get().n); +db.close(); diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index 15e974f4c1..34a27deefd 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -155,8 +155,8 @@ "total": 1 }, "sqlite": { - "pass": 1, - "total": 1 + "pass": 24, + "total": 40 }, "stream": { "pass": 770,