From 74beeb2a04426ec067bdb08c1d12159d0267b257 Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Thu, 9 Jul 2026 09:21:33 -0400 Subject: [PATCH 1/7] feat: make the SQL engine pluggable and reconstruct types from schema Decouples SqlDatabase/SqlEngine from sql.js so any compatible backend can be plugged in (e.g. node:sqlite, better-sqlite3), with samples/ demonstrating both. Also improves column type reconstruction from the normalized schema. Co-Authored-By: Claude Sonnet 5 --- README.md | 32 ++++--- samples/node-better-sqlite3-engine/README.md | 35 +++++++ samples/node-better-sqlite3-engine/engine.js | 30 ++++++ samples/node-better-sqlite3-engine/index.js | 47 ++++++++++ .../node-better-sqlite3-engine/package.json | 9 ++ samples/node-sqlite-engine/README.md | 33 +++++++ samples/node-sqlite-engine/engine.js | 33 +++++++ samples/node-sqlite-engine/index.js | 45 +++++++++ src/cdbToSql.ts | 10 +- src/index.ts | 4 +- src/sqlToCdb.ts | 4 +- src/types.ts | 22 ++++- test/cdbToSql.test.ts | 16 ++-- test/mocks/mockSqlDatabase.ts | 94 +++---------------- test/roundtrip.test.ts | 6 +- 15 files changed, 301 insertions(+), 119 deletions(-) create mode 100644 samples/node-better-sqlite3-engine/README.md create mode 100644 samples/node-better-sqlite3-engine/engine.js create mode 100644 samples/node-better-sqlite3-engine/index.js create mode 100644 samples/node-better-sqlite3-engine/package.json create mode 100644 samples/node-sqlite-engine/README.md create mode 100644 samples/node-sqlite-engine/engine.js create mode 100644 samples/node-sqlite-engine/index.js diff --git a/README.md b/README.md index 94fb2ca..c5bbbc4 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,10 @@ npx cdb-converter --version | `.cdb` | CDB → SQLite | `.sqlite` | | `.sqlite` / `.db` | SQLite → CDB | `.cdb` | -| Option | Effect | -| ------------------- | -------------------------------------------------------------------------------------------------- | +| Option | Effect | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `-n`, `--normalize` | (CDB → SQLite only) reconstruct PK/FK constraints from PCM naming conventions. See [Normalized schema](#normalized-schema). | -| `--index-fk` | Implies `--normalize`; also indexes every FK column for faster JOINs (roughly doubles output size). | +| `--index-fk` | Implies `--normalize`; also indexes every FK column for faster JOINs (roughly doubles output size). | ## Library usage @@ -195,12 +195,13 @@ const decompressed = decompressCdb(compressed); // accepts compressed or raw inp ## API reference -### `cdbToSql(cdbBuffer, SQL, options?): Database` +### `cdbToSql(cdbBuffer, SQL, options?): SqlDatabase` Convert CDB binary data into a SQLite database instance. - **`cdbBuffer`** — `ArrayBuffer | Uint8Array`, raw CDB data (compressed or uncompressed). -- **`SQL`** — `SqlJsStatic`, the module returned by `initSqlJs()`. +- **`SQL`** — a `SqlEngine` (the module returned by `initSqlJs()` satisfies this out of the box). +- **returns** — a `SqlDatabase` with the CDB tables loaded. - **`options.normalize`** — `boolean` (default `false`). Reconstruct PK/FK constraints from PCM naming conventions. See [Normalized schema](#normalized-schema). - **`options.indexForeignKeys`** — `boolean` (default `false`). When normalizing, also index every FK column for faster JOINs (roughly doubles the output size). - **returns** — a `sql.js` `Database` with the CDB tables loaded. @@ -209,9 +210,16 @@ Convert CDB binary data into a SQLite database instance. Convert a SQLite database back to CDB binary format (automatically compressed). -- **`db`** — a `sql.js` `Database` instance. +- **`db`** — a `SqlDatabase` instance. - **returns** — compressed CDB binary data as an `ArrayBuffer`. +### Using a different SQLite engine + +The library's public API is not tied to `sql.js` — it is typed against the minimal, self-contained `SqlEngine`/`SqlDatabase` interfaces exported from the package root, so any object matching that shape works, with no change required to the library: + +- [Node.js — swapping the SQLite engine](./samples/node-sqlite-engine/) — uses the bundled, tested [`node:sqlite`](https://nodejs.org/api/sqlite.html) adapter (`cdb-converter/adapters/node-sqlite`). +- [Node.js — using better-sqlite3 as the SQLite engine](./samples/node-better-sqlite3-engine/) — a hand-written adapter for [`better-sqlite3`](https://github.com/WiseLibs/better-sqlite3), not shipped by this package since it would add a native dependency most consumers don't need. + ### `compressCdb(data): ArrayBuffer` Compress CDB data using zlib deflate. Accepts `ArrayBuffer | Uint8Array`. @@ -269,11 +277,11 @@ A full `cdb → sqlite → cdb` round-trip on a real ~60k-row database stays wel Normalization is opt-in and costs only what you ask for (measured against the default conversion, ~60k rows): -| Mode | Conversion time | Output size | -| --------------------------------------------- | --------------- | ----------- | -| Default (flat) | baseline | baseline | -| `normalize` | +~10% | +~40% | -| `normalize` + `indexForeignKeys` | +~40% | +~130% | +| Mode | Conversion time | Output size | +| -------------------------------- | --------------- | ----------- | +| Default (flat) | baseline | baseline | +| `normalize` | +~10% | +~40% | +| `normalize` + `indexForeignKeys` | +~40% | +~130% | See **[bench/README.md](bench/README.md)** for the full per-fixture numbers, the bundle breakdown, and how to reproduce them (`npm run bench`). @@ -284,6 +292,8 @@ Runnable examples live in the [samples](./samples/) folder: - [Browser](./samples/browser/) — convert a `.cdb` file to SQLite directly in the browser. - [Node.js — CDB to SQLite](./samples/node-cdb-to-sql/) — convert a `.cdb` file into a `.sqlite` file. - [Node.js — SQLite to CDB](./samples/node-sql-to-cdb/) — convert a `.sqlite` or `.db` file back into a `.cdb` file. +- [Node.js — swapping the SQLite engine](./samples/node-sqlite-engine/) — use the bundled `node:sqlite` adapter instead of `sql.js`. +- [Node.js — using better-sqlite3 as the SQLite engine](./samples/node-better-sqlite3-engine/) — wire up a hand-written `better-sqlite3` adapter. ## License diff --git a/samples/node-better-sqlite3-engine/README.md b/samples/node-better-sqlite3-engine/README.md new file mode 100644 index 0000000..232a13f --- /dev/null +++ b/samples/node-better-sqlite3-engine/README.md @@ -0,0 +1,35 @@ +# Node.js — using better-sqlite3 as the SQLite engine + +This sample shows that `cdb-converter` isn't tied to `sql.js` or the bundled `node:sqlite` adapter: [`engine.js`](./engine.js) is a small hand-written `SqlEngine`/`SqlDatabase` adapter wrapping [`better-sqlite3`](https://github.com/WiseLibs/better-sqlite3), used as a drop-in replacement for `initSqlJs()`. + +Unlike the `node:sqlite` adapter, this one is **not shipped by `cdb-converter`** — `better-sqlite3` is a native dependency (requires `node-gyp`/prebuilt bindings) that most consumers don't need, so it's kept out of the main package and lives only in this sample. + +## What it does + +- Read a `.cdb` file from disk +- Convert it with `cdbToSql`, passing `betterSqlite3Engine` instead of a `sql.js` module +- Print detected table names from `DB_STRUCTURE` + +## Prerequisites + +- Install dependencies from the repository root: `npm install` +- Build the library from the repository root: `npm run build` +- Install this sample's own dependency: `npm install` from within `samples/node-better-sqlite3-engine/` + +## Usage + +From the repository root: + +```bash +node samples/node-better-sqlite3-engine/index.js +``` + +## Example + +```bash +node samples/node-better-sqlite3-engine/index.js ./Career_1.cdb +``` + +## Adapting other engines + +See [`engine.js`](./engine.js) for the adapter shape. Any object matching `SqlEngine`/`SqlDatabase` (exported from `cdb-converter`) works the same way — `sqlite3`, `wa-sqlite`, `absurd-sql`, etc. can be wired up on the same principle. diff --git a/samples/node-better-sqlite3-engine/engine.js b/samples/node-better-sqlite3-engine/engine.js new file mode 100644 index 0000000..c911bee --- /dev/null +++ b/samples/node-better-sqlite3-engine/engine.js @@ -0,0 +1,30 @@ +import Database from "better-sqlite3"; + +function wrap(raw) { + return { + run(sql, params = []) { + raw.prepare(sql).run(...params); + }, + exec(sql) { + const stmt = raw.prepare(sql).raw(); + const rows = stmt.all(); + if (rows.length === 0) return []; + const columns = stmt.columns().map((c) => c.name); + return [{ columns, values: rows }]; + }, + export() { + return raw.serialize(); + }, + close() { + raw.close(); + }, + }; +} + +function DatabaseEngine(data) { + return wrap(new Database(data ?? ":memory:")); +} + +export const betterSqlite3Engine = { + Database: DatabaseEngine, +}; diff --git a/samples/node-better-sqlite3-engine/index.js b/samples/node-better-sqlite3-engine/index.js new file mode 100644 index 0000000..4c71cff --- /dev/null +++ b/samples/node-better-sqlite3-engine/index.js @@ -0,0 +1,47 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import process from "node:process"; +import { cdbToSql } from "../../dist/index.mjs"; +import { betterSqlite3Engine } from "./engine.js"; + +function usage() { + console.error( + "Usage: node samples/node-better-sqlite3-engine/index.js ", + ); +} + +async function main() { + const [inputArg] = process.argv.slice(2); + + if (!inputArg) { + usage(); + process.exitCode = 1; + return; + } + + const inputPath = resolve(process.cwd(), inputArg); + const cdbBytes = await readFile(inputPath); + + // Same cdbToSql call as with sql.js, but backed by better-sqlite3 instead. + const db = cdbToSql(cdbBytes, betterSqlite3Engine); + try { + const tables = db.exec( + "SELECT TableName, ID FROM DB_STRUCTURE ORDER BY ID", + ); + const rows = tables[0]?.values || []; + + console.log(`Input : ${inputPath}`); + console.log(`Tables : ${rows.length}`); + + for (const [tableName, tableId] of rows) { + console.log(`- ${tableName} (#${tableId})`); + } + } finally { + db.close(); + } +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; +}); diff --git a/samples/node-better-sqlite3-engine/package.json b/samples/node-better-sqlite3-engine/package.json new file mode 100644 index 0000000..4f59434 --- /dev/null +++ b/samples/node-better-sqlite3-engine/package.json @@ -0,0 +1,9 @@ +{ + "name": "node-better-sqlite3-engine-sample", + "private": true, + "type": "module", + "description": "Sample showing cdb-converter used with a hand-written better-sqlite3 SqlEngine adapter.", + "dependencies": { + "better-sqlite3": "^11.3.0" + } +} diff --git a/samples/node-sqlite-engine/README.md b/samples/node-sqlite-engine/README.md new file mode 100644 index 0000000..d4b8c7e --- /dev/null +++ b/samples/node-sqlite-engine/README.md @@ -0,0 +1,33 @@ +# Node.js — swapping the SQLite engine + +This sample shows that `cdb-converter` isn't tied to `sql.js`: it uses the bundled `node:sqlite` adapter (`cdb-converter/adapters/node-sqlite`) as a drop-in replacement for `initSqlJs()`. + +## What it does + +- Read a `.cdb` file from disk +- Convert it with `cdbToSql`, passing `nodeSqliteEngine` instead of a `sql.js` module +- Print detected table names from `DB_STRUCTURE` + +## Prerequisites + +- Node.js 26.1.0+ (required by `node:sqlite`'s `serialize()`/`deserialize()`) +- Install dependencies from the repository root: `npm install` +- Build the library from the repository root: `npm run build` + +## Usage + +From the repository root: + +```bash +node samples/node-sqlite-engine/index.js +``` + +## Example + +```bash +node samples/node-sqlite-engine/index.js ./Career_1.cdb +``` + +## Using your own engine + +Any object matching the `SqlEngine`/`SqlDatabase` interfaces exported from `cdb-converter` works in place of `sql.js`. See [`src/adapters/node-sqlite.ts`](../../src/adapters/node-sqlite.ts) for a reference implementation to adapt to other engines (e.g. `better-sqlite3`, `wa-sqlite`). diff --git a/samples/node-sqlite-engine/engine.js b/samples/node-sqlite-engine/engine.js new file mode 100644 index 0000000..0ce1fa0 --- /dev/null +++ b/samples/node-sqlite-engine/engine.js @@ -0,0 +1,33 @@ +import { DatabaseSync } from "node:sqlite"; + +function wrap(raw) { + return { + run(sql, params = []) { + raw.prepare(sql).run(...params); + }, + exec(sql) { + const stmt = raw.prepare(sql); + stmt.setReturnArrays(true); + const rows = stmt.all(); + if (rows.length === 0) return []; + const columns = stmt.columns().map((c) => c.name); + return [{ columns, values: rows }]; + }, + export() { + return raw.serialize(); + }, + close() { + raw.close(); + }, + }; +} + +function DatabaseEngine(data) { + const raw = new DatabaseSync(":memory:"); + if (data) raw.deserialize(Uint8Array.from(data)); + return wrap(raw); +} + +export const nodeSqliteEngine = { + Database: DatabaseEngine, +}; diff --git a/samples/node-sqlite-engine/index.js b/samples/node-sqlite-engine/index.js new file mode 100644 index 0000000..aab54f5 --- /dev/null +++ b/samples/node-sqlite-engine/index.js @@ -0,0 +1,45 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import process from "node:process"; +import { cdbToSql } from "../../dist/index.mjs"; +import { nodeSqliteEngine } from "./engine.js"; + +function usage() { + console.error("Usage: node samples/node-sqlite-engine/index.js "); +} + +async function main() { + const [inputArg] = process.argv.slice(2); + + if (!inputArg) { + usage(); + process.exitCode = 1; + return; + } + + const inputPath = resolve(process.cwd(), inputArg); + const cdbBytes = await readFile(inputPath); + + // Same cdbToSql call as with sql.js, but backed by node:sqlite instead. + const db = cdbToSql(cdbBytes, nodeSqliteEngine); + try { + const tables = db.exec( + "SELECT TableName, ID FROM DB_STRUCTURE ORDER BY ID", + ); + const rows = tables[0]?.values || []; + + console.log(`Input : ${inputPath}`); + console.log(`Tables : ${rows.length}`); + + for (const [tableName, tableId] of rows) { + console.log(`- ${tableName} (#${tableId})`); + } + } finally { + db.close(); + } +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; +}); diff --git a/src/cdbToSql.ts b/src/cdbToSql.ts index c95e180..b6a0d21 100644 --- a/src/cdbToSql.ts +++ b/src/cdbToSql.ts @@ -2,16 +2,16 @@ * Convert CDB binary format to SQLite database */ -import type { SqlValue } from "sql.js"; import { decompressCdb } from "./compression"; -import { inferKeys } from "./keyInference"; import type { TableKeys } from "./keyInference"; +import { inferKeys } from "./keyInference"; import { CDBReader } from "./reader"; import { CHUNK_TYPE, DATA_TYPE } from "./tableMetadata"; import type { CdbToSqlOptions, SqlDatabase, - SqlJsStatic, + SqlEngine, + SqlValue, TableInfo, } from "./types"; @@ -114,12 +114,12 @@ function buildTableConstraints( */ export function cdbToSql( cdbData: ArrayBuffer | Uint8Array, - SQL: SqlJsStatic, + SQL: SqlEngine, options?: CdbToSqlOptions, ): SqlDatabase { const decompressedData = decompressCdb(cdbData); const reader = new CDBReader(decompressedData); - const db = new SQL.Database() as SqlDatabase; + const db = new SQL.Database(); db.run("PRAGMA foreign_keys = OFF"); const wrapperChunk = reader.readChunk(); diff --git a/src/index.ts b/src/index.ts index a49fe70..8011ac9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,9 @@ export type { ColumnInfo, ColumnMetadata, SqlDatabase, - SqlJsStatic, + SqlEngine, + SqlExecResult, + SqlValue, TableInfo, CDBChunk, } from "./types"; diff --git a/src/sqlToCdb.ts b/src/sqlToCdb.ts index a286e41..2714df2 100644 --- a/src/sqlToCdb.ts +++ b/src/sqlToCdb.ts @@ -57,7 +57,7 @@ function parseColumnMetadata(colName: string, colType: string): ColumnMetadata { export function sqlToCdb(db: SqlDatabase): ArrayBuffer { const structureInfo = db.exec(`PRAGMA table_info("DB_STRUCTURE")`); const hasFlagsColumn = - structureInfo.length > 0 && + (structureInfo[0]?.values.length ?? 0) > 0 && structureInfo[0].values.some((row) => row[1] === "Flags"); const tablesResult = db.exec( @@ -65,7 +65,7 @@ export function sqlToCdb(db: SqlDatabase): ArrayBuffer { ? `SELECT TableName, ID, Flags FROM DB_STRUCTURE ORDER BY ID` : `SELECT TableName, ID FROM DB_STRUCTURE ORDER BY ID`, ); - if (tablesResult.length === 0) { + if ((tablesResult[0]?.values.length ?? 0) === 0) { throw new Error("No DB_STRUCTURE table found"); } diff --git a/src/types.ts b/src/types.ts index 280518b..b1c2358 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,3 @@ -import type { Database } from "sql.js"; - -export type { SqlJsStatic } from "sql.js"; - export enum DataType { INTEGER = 0, FLOAT = 1, @@ -69,7 +65,23 @@ export interface CDBChunk { export type ColumnData = Array; -export interface SqlDatabase extends Database {} +export type SqlValue = string | number | Uint8Array | null; + +export interface SqlExecResult { + columns: string[]; + values: SqlValue[][]; +} + +export interface SqlDatabase { + run(sql: string, params?: SqlValue[]): void; + exec(sql: string): SqlExecResult[]; + export(): Uint8Array; + close(): void; +} + +export interface SqlEngine { + Database: new (data?: Uint8Array | number[]) => SqlDatabase; +} export interface ColumnMetadata { sqliteType: string; diff --git a/test/cdbToSql.test.ts b/test/cdbToSql.test.ts index 3e2f794..af9c6cb 100644 --- a/test/cdbToSql.test.ts +++ b/test/cdbToSql.test.ts @@ -1,8 +1,7 @@ -import type { BindParams, QueryExecResult } from "sql.js"; import { describe, expect, it, vi } from "vitest"; import { cdbToSql } from "../src/index"; -import type { SqlJsStatic } from "../src/types"; -import { MockSqlDatabaseBase, MockStatement } from "./mocks/mockSqlDatabase"; +import type { SqlEngine, SqlExecResult, SqlValue } from "../src/types"; +import { MockSqlDatabaseBase } from "./mocks/mockSqlDatabase"; vi.mock("../src/compression", () => ({ decompressCdb: vi.fn((data: ArrayBuffer | Uint8Array) => data), @@ -20,7 +19,7 @@ vi.mock("../src/reader", () => ({ type MockSqlDatabase = MockDatabase; -function createMockSqlJs(): SqlJsStatic & { +function createMockSqlJs(): SqlEngine & { createdDatabases: MockSqlDatabase[]; } { const createdDatabases: MockSqlDatabase[] = []; @@ -34,16 +33,15 @@ function createMockSqlJs(): SqlJsStatic & { return { Database: MockDatabaseImpl, - Statement: MockStatement, createdDatabases, }; } class MockDatabase extends MockSqlDatabaseBase { tables: Map = new Map(); - sqlOperations: Array<{ sql: string; params?: BindParams }> = []; + sqlOperations: Array<{ sql: string; params?: SqlValue[] }> = []; - override run(sql: string, params?: BindParams): this { + override run(sql: string, params?: SqlValue[]): void { this.sqlOperations.push({ sql, params }); if (sql.includes("CREATE TABLE")) { @@ -60,11 +58,9 @@ class MockDatabase extends MockSqlDatabaseBase { } } } - - return this; } - override exec(sql: string): QueryExecResult[] { + override exec(sql: string): SqlExecResult[] { if (sql.includes("SELECT TableName, ID FROM DB_STRUCTURE")) { return [ { diff --git a/test/mocks/mockSqlDatabase.ts b/test/mocks/mockSqlDatabase.ts index 1f576ee..2241eef 100644 --- a/test/mocks/mockSqlDatabase.ts +++ b/test/mocks/mockSqlDatabase.ts @@ -1,34 +1,13 @@ /** - * Fully-typed mocks for sql.js' `Database` / `Statement` / `SqlJsStatic`. - * - * `SqlDatabase` extends sql.js' `Database`, which exposes ~11 methods. Tests only - * ever drive a handful of them (`exec`, `run`, `export`), so hand-written mocks used - * to reach for `as unknown as SqlDatabase` to silence the missing members. - * - * The building blocks here implement the *whole* surface instead — the methods a test - * cares about are provided, every other method is a stub that throws if it is ever - * called. The results are real `Database` / `Statement` / `SqlJsStatic` values, so no - * `unknown` cast is needed anywhere. + * Mocks for the library's own `SqlDatabase` interface (4 methods: `run`, `exec`, + * `export`, `close`), independent of any concrete SQLite engine. */ -import type { - BindParams, - Database, - ParamsObject, - QueryExecResult, - SqlValue, - Statement, - StatementIterator, -} from "sql.js"; -import type { SqlDatabase } from "../../src/types"; - -const notImplemented = (method: string) => (): never => { - throw new Error(`Mock sql.js: "${method}" was called but not stubbed`); -}; +import type { SqlDatabase, SqlExecResult, SqlValue } from "../../src/types"; export interface MockSqlDatabaseOptions { /** Handles `db.exec(sql)`. Return the rows the code under test should observe. */ - exec: (sql: string, params?: BindParams) => QueryExecResult[]; + exec: (sql: string) => SqlExecResult[]; /** Handles `db.export()`. Defaults to a tiny non-empty buffer. */ export?: () => Uint8Array; } @@ -40,53 +19,23 @@ export interface MockSqlDatabaseOptions { export function createMockSqlDatabase( options: MockSqlDatabaseOptions, ): SqlDatabase { - const db: SqlDatabase = { + return { exec: options.exec, export: options.export ?? (() => new Uint8Array([0, 1, 2])), - run: () => db, + run: () => {}, close: () => {}, - create_function: () => db, - updateHook: () => db, - each: notImplemented("each"), - getRowsModified: notImplemented("getRowsModified"), - handleError: notImplemented("handleError"), - iterateStatements: notImplemented("iterateStatements"), - prepare: notImplemented("prepare"), }; - - return db; -} - -/** Stub `Statement`. Every method throws — provided only to satisfy `SqlJsStatic`. */ -export class MockStatement implements Statement { - bind = notImplemented("Statement.bind") as (values?: BindParams) => boolean; - free = notImplemented("Statement.free") as () => boolean; - freemem = notImplemented("Statement.freemem"); - get = notImplemented("Statement.get") as (params?: BindParams) => SqlValue[]; - getAsObject = notImplemented("Statement.getAsObject") as ( - params?: BindParams, - ) => ParamsObject; - getColumnNames = notImplemented("Statement.getColumnNames") as () => string[]; - getNormalizedSQL = notImplemented( - "Statement.getNormalizedSQL", - ) as () => string; - getSQL = notImplemented("Statement.getSQL") as () => string; - reset = notImplemented("Statement.reset") as () => void; - run = notImplemented("Statement.run") as (values?: BindParams) => void; - step = notImplemented("Statement.step") as () => boolean; } /** - * Base class implementing the full {@link Database} surface with throwing stubs. - * Extend it and override only the methods your test drives (typically `run` / `exec` - * / `export`); instances are genuine `Database` values, so no cast is required. + * Base class implementing the {@link SqlDatabase} surface with default no-op/empty + * behaviour. Extend it and override only the methods your test drives (typically + * `run` / `exec` / `export`). */ -export class MockSqlDatabaseBase implements Database { - run(_sql: string, _params?: BindParams): Database { - return this; - } +export class MockSqlDatabaseBase implements SqlDatabase { + run(_sql: string, _params?: SqlValue[]): void {} - exec(_sql: string, _params?: BindParams): QueryExecResult[] { + exec(_sql: string): SqlExecResult[] { return []; } @@ -95,23 +44,4 @@ export class MockSqlDatabaseBase implements Database { } close(): void {} - - create_function( - _name: string, - _func: (...args: unknown[]) => unknown, - ): Database { - return this; - } - - updateHook(): Database { - return this; - } - - each = notImplemented("each") as Database["each"]; - getRowsModified = notImplemented("getRowsModified"); - handleError = notImplemented("handleError"); - iterateStatements = notImplemented( - "iterateStatements", - ) as () => StatementIterator; - prepare = notImplemented("prepare") as () => Statement; } diff --git a/test/roundtrip.test.ts b/test/roundtrip.test.ts index a405023..a0fcfb2 100644 --- a/test/roundtrip.test.ts +++ b/test/roundtrip.test.ts @@ -17,10 +17,10 @@ import { readFileSync } from "node:fs"; import initSqlJs from "sql.js"; import { beforeAll, describe, expect, it } from "vitest"; import { cdbToSql, sqlToCdb } from "../src/index"; -import type { SqlDatabase, SqlJsStatic } from "../src/types"; +import type { SqlDatabase, SqlEngine } from "../src/types"; import { saveFixtures } from "./fixtures/save.fixture"; -let SQL: SqlJsStatic; +let SQL: SqlEngine; beforeAll(async () => { SQL = await initSqlJs(); @@ -74,7 +74,7 @@ describe("cdb <-> sql round-trip (no data loss)", () => { // 2. Serialize to SQLite bytes and reopen — mirrors the CLI writing a .sqlite // file and reading it back, dropping any in-memory-only state. - const db2 = new SQL.Database(db1.export()) as SqlDatabase; + const db2 = new SQL.Database(db1.export()); // 3. sql -> cdb -> sql const db3 = cdbToSql(sqlToCdb(db2), SQL); From 85f9709d61894d7701e3e7fe8ee61faa863f32e0 Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Mon, 13 Jul 2026 08:14:57 -0400 Subject: [PATCH 2/7] feat: add better-sqlite3 and node-sqlite engines with usage examples --- samples/node-better-sqlite3-engine/engine.ts | 32 ++++++++++++++ samples/node-better-sqlite3-engine/index.ts | 46 ++++++++++++++++++++ samples/node-sqlite-engine/engine.ts | 34 +++++++++++++++ samples/node-sqlite-engine/index.ts | 45 +++++++++++++++++++ src/types.ts | 2 +- 5 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 samples/node-better-sqlite3-engine/engine.ts create mode 100644 samples/node-better-sqlite3-engine/index.ts create mode 100644 samples/node-sqlite-engine/engine.ts create mode 100644 samples/node-sqlite-engine/index.ts diff --git a/samples/node-better-sqlite3-engine/engine.ts b/samples/node-better-sqlite3-engine/engine.ts new file mode 100644 index 0000000..e2374a1 --- /dev/null +++ b/samples/node-better-sqlite3-engine/engine.ts @@ -0,0 +1,32 @@ +import Database from "better-sqlite3"; +import type { SqlDatabase, SqlEngine } from "../../src/types"; + +function wrap(raw: Database) { + return { + run(sql, params = []) { + raw.prepare(sql).run(...params); + }, + exec(sql) { + const stmt = raw.prepare(sql).raw(); + const rows = stmt.all(); + if (rows.length === 0) return []; + const columns = stmt.columns().map((c: any) => c.name); + return [{ columns, values: rows }]; + }, + export() { + return raw.serialize(); + }, + close() { + raw.close(); + }, + } as SqlDatabase; +} + +function DatabaseEngine(data: Uint8Array | number[] | undefined): SqlDatabase { + const raw = new Database(data ?? ":memory:"); + return wrap(raw); +} + +export const betterSqlite3Engine: SqlEngine = { + Database: DatabaseEngine, +}; diff --git a/samples/node-better-sqlite3-engine/index.ts b/samples/node-better-sqlite3-engine/index.ts new file mode 100644 index 0000000..dcc65c9 --- /dev/null +++ b/samples/node-better-sqlite3-engine/index.ts @@ -0,0 +1,46 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import process from "node:process"; +import { cdbToSql } from "../../dist/index"; +import { betterSqlite3Engine } from "./engine"; + +function usage() { + console.error( + "Usage: node samples/node-better-sqlite3-engine/index.js ", + ); +} + +async function main() { + const [inputArg] = process.argv.slice(2); + + if (!inputArg) { + usage(); + process.exitCode = 1; + return; + } + + const inputPath = resolve(process.cwd(), inputArg); + const cdbBytes = await readFile(inputPath); + + const db = cdbToSql(cdbBytes, betterSqlite3Engine); + try { + const tables = db.exec( + "SELECT TableName, ID FROM DB_STRUCTURE ORDER BY ID", + ); + const rows = tables[0]?.values || []; + + console.log(`Input : ${inputPath}`); + console.log(`Tables : ${rows.length}`); + + for (const [tableName, tableId] of rows) { + console.log(`- ${tableName} (#${tableId})`); + } + } finally { + db.close(); + } +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; +}); diff --git a/samples/node-sqlite-engine/engine.ts b/samples/node-sqlite-engine/engine.ts new file mode 100644 index 0000000..36dc4dc --- /dev/null +++ b/samples/node-sqlite-engine/engine.ts @@ -0,0 +1,34 @@ +import { DatabaseSync } from "node:sqlite"; +import type { SqlDatabase } from "../../src/types"; + +function wrap(raw: DatabaseSync) { + return { + run(sql, params = []) { + raw.prepare(sql).run(...params); + }, + exec(sql) { + const stmt = raw.prepare(sql); + stmt.setReturnArrays(true); + const rows = stmt.all(); + if (rows.length === 0) return []; + const columns = stmt.columns().map((c) => c.name); + return [{ columns, values: rows }]; + }, + export() { + return raw.serialize(); + }, + close() { + raw.close(); + }, + } as SqlDatabase; +} + +function DatabaseEngine(data: Uint8Array | number[]) { + const raw = new DatabaseSync(":memory:"); + if (data) raw.deserialize(Uint8Array.from(data)); + return wrap(raw); +} + +export const nodeSqliteEngine = { + Database: DatabaseEngine, +}; diff --git a/samples/node-sqlite-engine/index.ts b/samples/node-sqlite-engine/index.ts new file mode 100644 index 0000000..c2603ff --- /dev/null +++ b/samples/node-sqlite-engine/index.ts @@ -0,0 +1,45 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import process from "node:process"; +import { cdbToSql } from "../../dist/index"; +import { nodeSqliteEngine } from "./engine"; + +function usage() { + console.error("Usage: node samples/node-sqlite-engine/index.js "); +} + +async function main() { + const [inputArg] = process.argv.slice(2); + + if (!inputArg) { + usage(); + process.exitCode = 1; + return; + } + + const inputPath = resolve(process.cwd(), inputArg); + const cdbBytes = await readFile(inputPath); + + // Same cdbToSql call as with sql.js, but backed by node:sqlite instead. + const db = cdbToSql(cdbBytes, nodeSqliteEngine); + try { + const tables = db.exec( + "SELECT TableName, ID FROM DB_STRUCTURE ORDER BY ID", + ); + const rows = tables[0]?.values || []; + + console.log(`Input : ${inputPath}`); + console.log(`Tables : ${rows.length}`); + + for (const [tableName, tableId] of rows) { + console.log(`- ${tableName} (#${tableId})`); + } + } finally { + db.close(); + } +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; +}); diff --git a/src/types.ts b/src/types.ts index b1c2358..e330c38 100644 --- a/src/types.ts +++ b/src/types.ts @@ -80,7 +80,7 @@ export interface SqlDatabase { } export interface SqlEngine { - Database: new (data?: Uint8Array | number[]) => SqlDatabase; + Database: new (data?: Uint8Array | number[] | undefined) => SqlDatabase; } export interface ColumnMetadata { From 2994c780e1b187ee4cd7c79e88d1430d1d646d15 Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Mon, 13 Jul 2026 08:15:14 -0400 Subject: [PATCH 3/7] Refactor SQLite engine samples to use better-sqlite3 and node:sqlite --- samples/node-better-sqlite3-engine/README.md | 4 +- samples/node-better-sqlite3-engine/engine.js | 67 ++- samples/node-better-sqlite3-engine/engine.ts | 32 -- samples/node-better-sqlite3-engine/index.js | 1 - samples/node-better-sqlite3-engine/index.ts | 46 -- .../package-lock.json | 492 ++++++++++++++++++ .../node-better-sqlite3-engine/package.json | 3 + samples/node-sqlite-engine/README.md | 9 +- samples/node-sqlite-engine/engine.js | 74 ++- samples/node-sqlite-engine/engine.ts | 34 -- samples/node-sqlite-engine/index.js | 1 - samples/node-sqlite-engine/index.ts | 45 -- samples/node-sqlite-engine/package-lock.json | 30 ++ samples/node-sqlite-engine/package.json | 9 + 14 files changed, 633 insertions(+), 214 deletions(-) delete mode 100644 samples/node-better-sqlite3-engine/engine.ts delete mode 100644 samples/node-better-sqlite3-engine/index.ts create mode 100644 samples/node-better-sqlite3-engine/package-lock.json delete mode 100644 samples/node-sqlite-engine/engine.ts delete mode 100644 samples/node-sqlite-engine/index.ts create mode 100644 samples/node-sqlite-engine/package-lock.json create mode 100644 samples/node-sqlite-engine/package.json diff --git a/samples/node-better-sqlite3-engine/README.md b/samples/node-better-sqlite3-engine/README.md index 232a13f..bb12eea 100644 --- a/samples/node-better-sqlite3-engine/README.md +++ b/samples/node-better-sqlite3-engine/README.md @@ -1,8 +1,8 @@ # Node.js — using better-sqlite3 as the SQLite engine -This sample shows that `cdb-converter` isn't tied to `sql.js` or the bundled `node:sqlite` adapter: [`engine.js`](./engine.js) is a small hand-written `SqlEngine`/`SqlDatabase` adapter wrapping [`better-sqlite3`](https://github.com/WiseLibs/better-sqlite3), used as a drop-in replacement for `initSqlJs()`. +This sample shows that `cdb-converter` isn't tied to `sql.js`: [`engine.js`](./engine.js) is a small hand-written `SqlEngine`/`SqlDatabase` adapter wrapping [`better-sqlite3`](https://github.com/WiseLibs/better-sqlite3), used as a drop-in replacement for `initSqlJs()`. See the [`node-sqlite-engine`](../node-sqlite-engine) sample for the equivalent adapter built on Node's built-in `node:sqlite` instead. -Unlike the `node:sqlite` adapter, this one is **not shipped by `cdb-converter`** — `better-sqlite3` is a native dependency (requires `node-gyp`/prebuilt bindings) that most consumers don't need, so it's kept out of the main package and lives only in this sample. +Neither adapter is shipped by `cdb-converter` itself — both are hand-written, sample-local `SqlEngine`/`SqlDatabase` implementations. `better-sqlite3` in particular is a native dependency (requires `node-gyp`/prebuilt bindings) that most consumers don't need, so it's kept out of the main package and lives only in this sample. ## What it does diff --git a/samples/node-better-sqlite3-engine/engine.js b/samples/node-better-sqlite3-engine/engine.js index c911bee..3104740 100644 --- a/samples/node-better-sqlite3-engine/engine.js +++ b/samples/node-better-sqlite3-engine/engine.js @@ -1,30 +1,51 @@ +// @ts-check import Database from "better-sqlite3"; -function wrap(raw) { - return { - run(sql, params = []) { - raw.prepare(sql).run(...params); - }, - exec(sql) { - const stmt = raw.prepare(sql).raw(); - const rows = stmt.all(); - if (rows.length === 0) return []; - const columns = stmt.columns().map((c) => c.name); - return [{ columns, values: rows }]; - }, - export() { - return raw.serialize(); - }, - close() { - raw.close(); - }, - }; -} +/** @typedef {import("../../src/types").SqlDatabase} SqlDatabase */ +/** @typedef {import("../../src/types").SqlEngine} SqlEngine */ +/** @typedef {import("../../src/types").SqlValue} SqlValue */ +/** @typedef {import("../../src/types").SqlExecResult} SqlExecResult */ + +/** @implements {SqlDatabase} */ +class BetterSqlite3Database { + /** @type {import("better-sqlite3").Database} */ + db; + + /** @param {Uint8Array | number[] | undefined} data */ + constructor(data) { + this.db = data ? new Database(Buffer.from(data)) : new Database(":memory:"); + } + + /** + * @param {string} sql + * @param {SqlValue[]} [params] + */ + run(sql, params = []) { + this.db.prepare(sql).run(...params); + } + + /** + * @param {string} sql + * @returns {SqlExecResult[]} + */ + exec(sql) { + const stmt = this.db.prepare(sql).raw(); + const rows = /** @type {SqlValue[][]} */ (stmt.all()); + if (rows.length === 0) return []; + const columns = stmt.columns().map((c) => c.name); + return [{ columns, values: rows }]; + } + + export() { + return this.db.serialize(); + } -function DatabaseEngine(data) { - return wrap(new Database(data ?? ":memory:")); + close() { + this.db.close(); + } } +/** @type {SqlEngine} */ export const betterSqlite3Engine = { - Database: DatabaseEngine, + Database: BetterSqlite3Database, }; diff --git a/samples/node-better-sqlite3-engine/engine.ts b/samples/node-better-sqlite3-engine/engine.ts deleted file mode 100644 index e2374a1..0000000 --- a/samples/node-better-sqlite3-engine/engine.ts +++ /dev/null @@ -1,32 +0,0 @@ -import Database from "better-sqlite3"; -import type { SqlDatabase, SqlEngine } from "../../src/types"; - -function wrap(raw: Database) { - return { - run(sql, params = []) { - raw.prepare(sql).run(...params); - }, - exec(sql) { - const stmt = raw.prepare(sql).raw(); - const rows = stmt.all(); - if (rows.length === 0) return []; - const columns = stmt.columns().map((c: any) => c.name); - return [{ columns, values: rows }]; - }, - export() { - return raw.serialize(); - }, - close() { - raw.close(); - }, - } as SqlDatabase; -} - -function DatabaseEngine(data: Uint8Array | number[] | undefined): SqlDatabase { - const raw = new Database(data ?? ":memory:"); - return wrap(raw); -} - -export const betterSqlite3Engine: SqlEngine = { - Database: DatabaseEngine, -}; diff --git a/samples/node-better-sqlite3-engine/index.js b/samples/node-better-sqlite3-engine/index.js index 4c71cff..ca6e345 100644 --- a/samples/node-better-sqlite3-engine/index.js +++ b/samples/node-better-sqlite3-engine/index.js @@ -22,7 +22,6 @@ async function main() { const inputPath = resolve(process.cwd(), inputArg); const cdbBytes = await readFile(inputPath); - // Same cdbToSql call as with sql.js, but backed by better-sqlite3 instead. const db = cdbToSql(cdbBytes, betterSqlite3Engine); try { const tables = db.exec( diff --git a/samples/node-better-sqlite3-engine/index.ts b/samples/node-better-sqlite3-engine/index.ts deleted file mode 100644 index dcc65c9..0000000 --- a/samples/node-better-sqlite3-engine/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { readFile } from "node:fs/promises"; -import { resolve } from "node:path"; -import process from "node:process"; -import { cdbToSql } from "../../dist/index"; -import { betterSqlite3Engine } from "./engine"; - -function usage() { - console.error( - "Usage: node samples/node-better-sqlite3-engine/index.js ", - ); -} - -async function main() { - const [inputArg] = process.argv.slice(2); - - if (!inputArg) { - usage(); - process.exitCode = 1; - return; - } - - const inputPath = resolve(process.cwd(), inputArg); - const cdbBytes = await readFile(inputPath); - - const db = cdbToSql(cdbBytes, betterSqlite3Engine); - try { - const tables = db.exec( - "SELECT TableName, ID FROM DB_STRUCTURE ORDER BY ID", - ); - const rows = tables[0]?.values || []; - - console.log(`Input : ${inputPath}`); - console.log(`Tables : ${rows.length}`); - - for (const [tableName, tableId] of rows) { - console.log(`- ${tableName} (#${tableId})`); - } - } finally { - db.close(); - } -} - -main().catch((error) => { - console.error(error instanceof Error ? error.message : error); - process.exitCode = 1; -}); diff --git a/samples/node-better-sqlite3-engine/package-lock.json b/samples/node-better-sqlite3-engine/package-lock.json new file mode 100644 index 0000000..d88b824 --- /dev/null +++ b/samples/node-better-sqlite3-engine/package-lock.json @@ -0,0 +1,492 @@ +{ + "name": "node-better-sqlite3-engine-sample", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node-better-sqlite3-engine-sample", + "dependencies": { + "better-sqlite3": "^11.3.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/samples/node-better-sqlite3-engine/package.json b/samples/node-better-sqlite3-engine/package.json index 4f59434..e6ac2ed 100644 --- a/samples/node-better-sqlite3-engine/package.json +++ b/samples/node-better-sqlite3-engine/package.json @@ -5,5 +5,8 @@ "description": "Sample showing cdb-converter used with a hand-written better-sqlite3 SqlEngine adapter.", "dependencies": { "better-sqlite3": "^11.3.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13" } } diff --git a/samples/node-sqlite-engine/README.md b/samples/node-sqlite-engine/README.md index d4b8c7e..79de7c7 100644 --- a/samples/node-sqlite-engine/README.md +++ b/samples/node-sqlite-engine/README.md @@ -1,6 +1,6 @@ # Node.js — swapping the SQLite engine -This sample shows that `cdb-converter` isn't tied to `sql.js`: it uses the bundled `node:sqlite` adapter (`cdb-converter/adapters/node-sqlite`) as a drop-in replacement for `initSqlJs()`. +This sample shows that `cdb-converter` isn't tied to `sql.js`: [`engine.js`](./engine.js) is a small hand-written `SqlEngine`/`SqlDatabase` adapter wrapping Node's built-in `node:sqlite`, used as a drop-in replacement for `initSqlJs()`. ## What it does @@ -10,9 +10,10 @@ This sample shows that `cdb-converter` isn't tied to `sql.js`: it uses the bundl ## Prerequisites -- Node.js 26.1.0+ (required by `node:sqlite`'s `serialize()`/`deserialize()`) +- Node.js 24.16.0+ or 26.1.0+ (required by `node:sqlite`'s `serialize()`/`deserialize()`) - Install dependencies from the repository root: `npm install` - Build the library from the repository root: `npm run build` +- Optional, for editor type-checking of `engine.js` (`@ts-check`): install this sample's own dev dependency with `npm install` from within `samples/node-sqlite-engine/` ## Usage @@ -28,6 +29,6 @@ node samples/node-sqlite-engine/index.js node samples/node-sqlite-engine/index.js ./Career_1.cdb ``` -## Using your own engine +## Adapting other engines -Any object matching the `SqlEngine`/`SqlDatabase` interfaces exported from `cdb-converter` works in place of `sql.js`. See [`src/adapters/node-sqlite.ts`](../../src/adapters/node-sqlite.ts) for a reference implementation to adapt to other engines (e.g. `better-sqlite3`, `wa-sqlite`). +See [`engine.js`](./engine.js) for the adapter shape. Any object matching `SqlEngine`/`SqlDatabase` (exported from `cdb-converter`) works the same way — `better-sqlite3` (see the [`node-better-sqlite3-engine`](../node-better-sqlite3-engine) sample), `wa-sqlite`, `absurd-sql`, etc. can be wired up on the same principle. diff --git a/samples/node-sqlite-engine/engine.js b/samples/node-sqlite-engine/engine.js index 0ce1fa0..ae3971a 100644 --- a/samples/node-sqlite-engine/engine.js +++ b/samples/node-sqlite-engine/engine.js @@ -1,33 +1,55 @@ +// @ts-check import { DatabaseSync } from "node:sqlite"; -function wrap(raw) { - return { - run(sql, params = []) { - raw.prepare(sql).run(...params); - }, - exec(sql) { - const stmt = raw.prepare(sql); - stmt.setReturnArrays(true); - const rows = stmt.all(); - if (rows.length === 0) return []; - const columns = stmt.columns().map((c) => c.name); - return [{ columns, values: rows }]; - }, - export() { - return raw.serialize(); - }, - close() { - raw.close(); - }, - }; -} +/** @typedef {import("../../src/types").SqlDatabase} SqlDatabase */ +/** @typedef {import("../../src/types").SqlEngine} SqlEngine */ +/** @typedef {import("../../src/types").SqlValue} SqlValue */ +/** @typedef {import("../../src/types").SqlExecResult} SqlExecResult */ + +/** @implements {SqlDatabase} */ +class NodeSqliteDatabase { + /** @type {DatabaseSync} */ + db; + + /** @param {Uint8Array | number[] | undefined} data */ + constructor(data) { + this.db = new DatabaseSync(":memory:"); + if (data) this.db.deserialize(Uint8Array.from(data)); + } + + /** + * @param {string} sql + * @param {SqlValue[]} [params] + */ + run(sql, params = []) { + this.db.prepare(sql).run(...params); + } + + /** + * @param {string} sql + * @returns {SqlExecResult[]} + */ + exec(sql) { + const stmt = this.db.prepare(sql); + stmt.setReturnArrays(true); + const rows = /** @type {SqlValue[][]} */ ( + /** @type {unknown} */ (stmt.all()) + ); + if (rows.length === 0) return []; + const columns = stmt.columns().map((c) => c.name); + return [{ columns, values: rows }]; + } + + export() { + return this.db.serialize(); + } -function DatabaseEngine(data) { - const raw = new DatabaseSync(":memory:"); - if (data) raw.deserialize(Uint8Array.from(data)); - return wrap(raw); + close() { + this.db.close(); + } } +/** @type {SqlEngine} */ export const nodeSqliteEngine = { - Database: DatabaseEngine, + Database: NodeSqliteDatabase, }; diff --git a/samples/node-sqlite-engine/engine.ts b/samples/node-sqlite-engine/engine.ts deleted file mode 100644 index 36dc4dc..0000000 --- a/samples/node-sqlite-engine/engine.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { DatabaseSync } from "node:sqlite"; -import type { SqlDatabase } from "../../src/types"; - -function wrap(raw: DatabaseSync) { - return { - run(sql, params = []) { - raw.prepare(sql).run(...params); - }, - exec(sql) { - const stmt = raw.prepare(sql); - stmt.setReturnArrays(true); - const rows = stmt.all(); - if (rows.length === 0) return []; - const columns = stmt.columns().map((c) => c.name); - return [{ columns, values: rows }]; - }, - export() { - return raw.serialize(); - }, - close() { - raw.close(); - }, - } as SqlDatabase; -} - -function DatabaseEngine(data: Uint8Array | number[]) { - const raw = new DatabaseSync(":memory:"); - if (data) raw.deserialize(Uint8Array.from(data)); - return wrap(raw); -} - -export const nodeSqliteEngine = { - Database: DatabaseEngine, -}; diff --git a/samples/node-sqlite-engine/index.js b/samples/node-sqlite-engine/index.js index aab54f5..7588112 100644 --- a/samples/node-sqlite-engine/index.js +++ b/samples/node-sqlite-engine/index.js @@ -20,7 +20,6 @@ async function main() { const inputPath = resolve(process.cwd(), inputArg); const cdbBytes = await readFile(inputPath); - // Same cdbToSql call as with sql.js, but backed by node:sqlite instead. const db = cdbToSql(cdbBytes, nodeSqliteEngine); try { const tables = db.exec( diff --git a/samples/node-sqlite-engine/index.ts b/samples/node-sqlite-engine/index.ts deleted file mode 100644 index c2603ff..0000000 --- a/samples/node-sqlite-engine/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { readFile } from "node:fs/promises"; -import { resolve } from "node:path"; -import process from "node:process"; -import { cdbToSql } from "../../dist/index"; -import { nodeSqliteEngine } from "./engine"; - -function usage() { - console.error("Usage: node samples/node-sqlite-engine/index.js "); -} - -async function main() { - const [inputArg] = process.argv.slice(2); - - if (!inputArg) { - usage(); - process.exitCode = 1; - return; - } - - const inputPath = resolve(process.cwd(), inputArg); - const cdbBytes = await readFile(inputPath); - - // Same cdbToSql call as with sql.js, but backed by node:sqlite instead. - const db = cdbToSql(cdbBytes, nodeSqliteEngine); - try { - const tables = db.exec( - "SELECT TableName, ID FROM DB_STRUCTURE ORDER BY ID", - ); - const rows = tables[0]?.values || []; - - console.log(`Input : ${inputPath}`); - console.log(`Tables : ${rows.length}`); - - for (const [tableName, tableId] of rows) { - console.log(`- ${tableName} (#${tableId})`); - } - } finally { - db.close(); - } -} - -main().catch((error) => { - console.error(error instanceof Error ? error.message : error); - process.exitCode = 1; -}); diff --git a/samples/node-sqlite-engine/package-lock.json b/samples/node-sqlite-engine/package-lock.json new file mode 100644 index 0000000..b8a59d4 --- /dev/null +++ b/samples/node-sqlite-engine/package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "node-sqlite-engine-sample", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node-sqlite-engine-sample", + "devDependencies": { + "@types/node": "^26.1.1" + } + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/samples/node-sqlite-engine/package.json b/samples/node-sqlite-engine/package.json new file mode 100644 index 0000000..5493879 --- /dev/null +++ b/samples/node-sqlite-engine/package.json @@ -0,0 +1,9 @@ +{ + "name": "node-sqlite-engine-sample", + "private": true, + "type": "module", + "description": "Sample showing cdb-converter used with a hand-written node:sqlite SqlEngine adapter.", + "devDependencies": { + "@types/node": "^26.1.1" + } +} From 3561252651b1d851948e0ed371102f07579add65 Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Mon, 13 Jul 2026 08:18:07 -0400 Subject: [PATCH 4/7] chore: remove node-sqlite-engine sample package files --- samples/node-sqlite-engine/package-lock.json | 30 -------------------- samples/node-sqlite-engine/package.json | 9 ------ 2 files changed, 39 deletions(-) delete mode 100644 samples/node-sqlite-engine/package-lock.json delete mode 100644 samples/node-sqlite-engine/package.json diff --git a/samples/node-sqlite-engine/package-lock.json b/samples/node-sqlite-engine/package-lock.json deleted file mode 100644 index b8a59d4..0000000 --- a/samples/node-sqlite-engine/package-lock.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "node-sqlite-engine-sample", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "node-sqlite-engine-sample", - "devDependencies": { - "@types/node": "^26.1.1" - } - }, - "node_modules/@types/node": { - "version": "26.1.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", - "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~8.3.0" - } - }, - "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", - "dev": true, - "license": "MIT" - } - } -} diff --git a/samples/node-sqlite-engine/package.json b/samples/node-sqlite-engine/package.json deleted file mode 100644 index 5493879..0000000 --- a/samples/node-sqlite-engine/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "node-sqlite-engine-sample", - "private": true, - "type": "module", - "description": "Sample showing cdb-converter used with a hand-written node:sqlite SqlEngine adapter.", - "devDependencies": { - "@types/node": "^26.1.1" - } -} From 5756bfa27e4f3a728dd541feb62e81db3eef530c Mon Sep 17 00:00:00 2001 From: Mathieu Picciolli Date: Wed, 15 Jul 2026 08:25:04 -0400 Subject: [PATCH 5/7] refactor: replace sql.js with better-sqlite3 engine in samples and core logic - Removed the node-better-sqlite3-engine sample and its package.json. - Updated node-cdb-to-sql and node-sql-to-cdb samples to use better-sqlite3 engine instead of sql.js. - Deleted the node-sqlite-engine sample and its associated files. - Introduced better-sqlite3 engine implementation in src/engines/better-sqlite3.ts. - Added sql-js engine creation function in src/engines/sql-js.ts for potential future use. - Updated CLI to utilize better-sqlite3 engine for database operations. - Adjusted tests to validate functionality with better-sqlite3 engine. --- README.md | 51 +- bench/README.md | 63 ++- bench/roundtrip.bench.ts | 11 +- package-lock.json | 467 ++++++++++++++++- package.json | 33 +- samples/node-better-sqlite3-engine/README.md | 35 -- samples/node-better-sqlite3-engine/engine.js | 51 -- samples/node-better-sqlite3-engine/index.js | 46 -- .../package-lock.json | 492 ------------------ .../node-better-sqlite3-engine/package.json | 12 - samples/node-cdb-to-sql/README.md | 2 +- samples/node-cdb-to-sql/index.js | 5 +- samples/node-sql-to-cdb/README.md | 2 +- samples/node-sql-to-cdb/index.js | 5 +- samples/node-sqlite-engine/README.md | 34 -- samples/node-sqlite-engine/engine.js | 55 -- samples/node-sqlite-engine/index.js | 44 -- src/cli.ts | 10 +- src/engines/better-sqlite3.ts | 34 ++ src/engines/sql-js.ts | 9 + test/engines/sqlJs.test.ts | 20 + test/normalize.test.ts | 9 +- test/roundtrip.test.ts | 15 +- tsup.config.ts | 19 + 24 files changed, 659 insertions(+), 865 deletions(-) delete mode 100644 samples/node-better-sqlite3-engine/README.md delete mode 100644 samples/node-better-sqlite3-engine/engine.js delete mode 100644 samples/node-better-sqlite3-engine/index.js delete mode 100644 samples/node-better-sqlite3-engine/package-lock.json delete mode 100644 samples/node-better-sqlite3-engine/package.json delete mode 100644 samples/node-sqlite-engine/README.md delete mode 100644 samples/node-sqlite-engine/engine.js delete mode 100644 samples/node-sqlite-engine/index.js create mode 100644 src/engines/better-sqlite3.ts create mode 100644 src/engines/sql-js.ts create mode 100644 test/engines/sqlJs.test.ts diff --git a/README.md b/README.md index c5bbbc4..b955d6b 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ The conversion is **lossless**: a full `cdb → sqlite → cdb` round-trip prese - **CLI included** — convert files without writing any code; direction is auto-detected. - **Lossless round-trip** — table flags, column order, and data types survive an export/reopen cycle. - **Optional relational schema** — reconstruct `PRIMARY KEY` / `FOREIGN KEY` constraints for JOINs and ER diagrams, without breaking the round-trip. -- **Isomorphic** — runs in Node.js and in the browser via [sql.js](https://github.com/sql-js/sql.js). -- **Lightweight** — the library's own code is ~28 kB, with only `pako` and `sql.js` as dependencies. +- **Node-first, browser-capable** — the CLI and default `better-sqlite3` engine target Node.js; an optional `sql.js` (WASM) engine covers the browser. +- **Lightweight** — the library's own code is ~28 kB, with only `pako` as a hard dependency. SQLite engines (`better-sqlite3`, `sql.js`) are optional and installed separately. - **TypeScript-first** — native type definitions and full IDE support. - **Tree-shakeable** — pure functions, no side effects, ESM + CommonJS builds. @@ -48,7 +48,7 @@ npm install cdb-converter ``` > [!NOTE] -> Requires **Node.js 22 or newer**. In the browser, `sql.js` loads its WebAssembly runtime on demand. +> Requires **Node.js 22 or newer**. The CLI and the default `better-sqlite3` engine need `better-sqlite3` (a native dependency); install it alongside `cdb-converter`. For the browser, install `sql.js` instead — it loads its WebAssembly runtime on demand. The fastest way to try it is the CLI: @@ -94,14 +94,12 @@ npx cdb-converter --version ```typescript import fs from "node:fs"; -import initSqlJs from "sql.js"; +import { betterSqlite3Engine } from "cdb-converter/engines/better-sqlite3"; import { cdbToSql } from "cdb-converter"; -const SQL = await initSqlJs(); - // Read and convert a CDB file const cdbBuffer = fs.readFileSync("save.cdb"); -const db = cdbToSql(cdbBuffer, SQL); +const db = cdbToSql(cdbBuffer, betterSqlite3Engine); // Query it like any SQLite database const result = db.exec("SELECT * FROM Teams LIMIT 5"); @@ -112,14 +110,14 @@ fs.writeFileSync("save.sqlite", db.export()); ``` > [!IMPORTANT] -> You must pass the initialized `sql.js` module returned by `initSqlJs()`. This library does not initialize `sql.js` for you: that setup is asynchronous and environment-specific (the caller decides how the wasm file is loaded in Node.js or the browser). +> You must pass a `SqlEngine`. `cdb-converter/engines/better-sqlite3` (requires the `better-sqlite3` package) is the default for Node.js; `cdb-converter/engines/sql-js` (requires the `sql.js` package) works in the browser but needs an `await` to initialize its WASM runtime. See [Using a different SQLite engine](#using-a-different-sqlite-engine). ### Normalized schema By default the SQLite output is a flat mirror of the CDB tables, with no relational constraints. Pass `{ normalize: true }` to reconstruct `PRIMARY KEY` and `FOREIGN KEY` constraints from the PCM naming conventions (`ID{table}` identity columns and `fkID{target}` references), turning the export into a proper relational database — ready for JOINs, entity-relationship diagrams, and schema introspection tools. ```typescript -const db = cdbToSql(cdbBuffer, SQL, { normalize: true }); +const db = cdbToSql(cdbBuffer, betterSqlite3Engine, { normalize: true }); // Relationships are now navigable: db.exec(` @@ -138,24 +136,22 @@ Notes: ```typescript // Lean: constraints only (~+40% size) -cdbToSql(cdbBuffer, SQL, { normalize: true }); +cdbToSql(cdbBuffer, betterSqlite3Engine, { normalize: true }); // Heavier, faster JOINs: also index FK columns (~2x size) -cdbToSql(cdbBuffer, SQL, { normalize: true, indexForeignKeys: true }); +cdbToSql(cdbBuffer, betterSqlite3Engine, { normalize: true, indexForeignKeys: true }); ``` ### SQLite to CDB ```typescript import fs from "node:fs"; -import initSqlJs from "sql.js"; +import { betterSqlite3Engine } from "cdb-converter/engines/better-sqlite3"; import { sqlToCdb } from "cdb-converter"; -const SQL = await initSqlJs(); - // Load a SQLite database and convert back to CDB const sqliteBuffer = fs.readFileSync("save.sqlite"); -const db = new SQL.Database(sqliteBuffer); +const db = new betterSqlite3Engine.Database(sqliteBuffer); const cdbBuffer = sqlToCdb(db); // automatically compressed fs.writeFileSync("save.cdb", Buffer.from(cdbBuffer)); @@ -178,8 +174,9 @@ const decompressed = decompressCdb(compressed); // accepts compressed or raw inp