diff --git a/packages/cli/package.json b/packages/cli/package.json index b7fd4e0..4b0c67e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -20,8 +20,7 @@ }, "dependencies": { "@uncaged/nerve-core": "workspace:*", - "citty": "^0.1.6", - "sql.js": "^1.14.1" + "citty": "^0.1.6" }, "devDependencies": { "@types/better-sqlite3": "^7.6.13", diff --git a/packages/cli/src/__tests__/sense-sqlite.test.ts b/packages/cli/src/__tests__/sense-sqlite.test.ts index bcf1ead..d195852 100644 --- a/packages/cli/src/__tests__/sense-sqlite.test.ts +++ b/packages/cli/src/__tests__/sense-sqlite.test.ts @@ -2,12 +2,12 @@ * Tests for sense SQLite helpers used by `nerve sense schema` / `nerve sense query`. */ -import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { DatabaseSync } from "node:sqlite"; -import initSqlJs, { type Database } from "sql.js"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { assertSenseDbExists, @@ -17,17 +17,11 @@ import { listTableSqlStatements, parseSenseQueryArgs, pickDefaultPreviewTable, - queryAsObjects, senseDbPath, } from "../sense-sqlite.js"; -let SQL: Awaited>; let tmpDir: string; -beforeAll(async () => { - SQL = await initSqlJs(); -}); - beforeEach(() => { tmpDir = join( tmpdir(), @@ -40,22 +34,6 @@ afterEach(() => { rmSync(tmpDir, { recursive: true, force: true }); }); -/** Helper: create a SQLite db file with the given setup SQL. */ -function createDb(name: string, setupSql: string): void { - const db = new SQL.Database(); - db.run(setupSql); - const data = db.export(); - db.close(); - writeFileSync(join(tmpDir, "data", "senses", `${name}.db`), Buffer.from(data)); -} - -/** Helper: open an in-memory db with setup SQL for unit tests. */ -function memDb(setupSql?: string): Database { - const db = new SQL.Database(); - if (setupSql) db.run(setupSql); - return db; -} - describe("senseDbPath", () => { it("points at data/senses/.db under the given root", () => { expect(senseDbPath("/root", "cpu-usage")).toBe(join("/root", "data", "senses", "cpu-usage.db")); @@ -68,14 +46,18 @@ describe("assertSenseDbExists", () => { }); it("returns the path when the file exists", () => { - createDb("x", "SELECT 1"); - expect(assertSenseDbExists(tmpDir, "x")).toBe(join(tmpDir, "data", "senses", "x.db")); + const p = join(tmpDir, "data", "senses", "x.db"); + new DatabaseSync(p).close(); + expect(assertSenseDbExists(tmpDir, "x")).toBe(p); }); }); describe("listTableSqlStatements", () => { it("returns CREATE statements ordered by tbl_name", () => { - const db = memDb("CREATE TABLE zebra (id INTEGER); CREATE TABLE alpha (id INTEGER);"); + const p = join(tmpDir, "data", "senses", "t.db"); + const db = new DatabaseSync(p); + db.exec("CREATE TABLE zebra (id INTEGER)"); + db.exec("CREATE TABLE alpha (id INTEGER)"); const stmts = listTableSqlStatements(db); db.close(); expect(stmts).toHaveLength(2); @@ -86,16 +68,18 @@ describe("listTableSqlStatements", () => { describe("pickDefaultPreviewTable", () => { it("prefers non-_migrations tables when both exist", () => { - const db = memDb( - `CREATE TABLE _migrations (name TEXT PRIMARY KEY); - CREATE TABLE readings (id INTEGER);`, - ); + const p = join(tmpDir, "data", "senses", "t.db"); + const db = new DatabaseSync(p); + db.exec("CREATE TABLE _migrations (name TEXT PRIMARY KEY)"); + db.exec("CREATE TABLE readings (id INTEGER)"); expect(pickDefaultPreviewTable(db)).toBe("readings"); db.close(); }); it("uses _migrations when it is the only table", () => { - const db = memDb("CREATE TABLE _migrations (name TEXT PRIMARY KEY);"); + const p = join(tmpDir, "data", "senses", "t.db"); + const db = new DatabaseSync(p); + db.exec("CREATE TABLE _migrations (name TEXT PRIMARY KEY)"); expect(pickDefaultPreviewTable(db)).toBe("_migrations"); db.close(); }); @@ -153,29 +137,22 @@ describe("collectColumnKeys", () => { }); }); -describe("queryAsObjects", () => { - it("converts columnar sql.js results to row objects", () => { - const db = memDb("CREATE TABLE t (x INTEGER, y TEXT); INSERT INTO t VALUES (1, 'a'), (2, 'b');"); - const rows = queryAsObjects(db, "SELECT * FROM t ORDER BY x"); - db.close(); - expect(rows).toEqual([ - { x: 1, y: "a" }, - { x: 2, y: "b" }, - ]); - }); -}); - describe("readonly query integration", () => { - it("runs default preview SQL on a real db file", () => { - createDb("demo", "CREATE TABLE items (id INTEGER PRIMARY KEY, v TEXT); INSERT INTO items (v) VALUES ('a'), ('b');"); + it("runs default preview SQL on a real db", () => { + const p = join(tmpDir, "data", "senses", "demo.db"); + const rw = new DatabaseSync(p); + rw.exec("CREATE TABLE items (id INTEGER PRIMARY KEY, v TEXT)"); + rw.exec("INSERT INTO items (v) VALUES ('a'), ('b')"); + rw.close(); - const buffer = require("node:fs").readFileSync(join(tmpDir, "data", "senses", "demo.db")); - const db = new SQL.Database(buffer); + const db = new DatabaseSync(p, { readOnly: true }); const table = pickDefaultPreviewTable(db); expect(table).toBe("items"); - if (table === null) throw new Error("expected items table"); + if (table === null) { + throw new Error("expected items table"); + } const sql = defaultPreviewSql(table); - const rows = queryAsObjects(db, sql); + const rows = db.prepare(sql).all() as Record[]; db.close(); expect(rows.length).toBeGreaterThanOrEqual(1); }); diff --git a/packages/cli/src/commands/sense.ts b/packages/cli/src/commands/sense.ts index 19c22a6..96b7877 100644 --- a/packages/cli/src/commands/sense.ts +++ b/packages/cli/src/commands/sense.ts @@ -1,5 +1,6 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; +import { DatabaseSync } from "node:sqlite"; import { type SenseInfo, parseNerveConfig } from "@uncaged/nerve-core"; import { defineCommand } from "citty"; @@ -13,7 +14,6 @@ import { openSenseDb, parseSenseQueryArgs, pickDefaultPreviewTable, - queryAsObjects, } from "../sense-sqlite.js"; import { getNerveRoot, getSocketPath, isRunning } from "../workspace.js"; @@ -80,7 +80,6 @@ const senseListCommand = defineCommand({ }, async run() { if (!isRunning()) { - // Daemon not running — show static info from nerve.yaml process.stderr.write( "⚠️ Daemon is not running — showing static config only (no last signal time).\n\n", ); @@ -171,9 +170,9 @@ const senseSchemaCommand = defineCommand({ }, async run({ args }) { const nerveRoot = getNerveRoot(); - let db: ReturnType>["Database"]> | undefined; + let db: DatabaseSync | undefined; try { - db = await openSenseDb(nerveRoot, args.name); + db = openSenseDb(nerveRoot, args.name); const statements = listTableSqlStatements(db); if (args.json) { process.stdout.write(`${JSON.stringify(statements, null, 2)}\n`); @@ -217,7 +216,7 @@ const senseQueryCommand = defineCommand({ }, async run({ args, rawArgs }) { const nerveRoot = getNerveRoot(); - let db: ReturnType>["Database"]> | undefined; + let db: DatabaseSync | undefined; try { let parsed: { name: string; sql: string | undefined }; try { @@ -228,7 +227,7 @@ const senseQueryCommand = defineCommand({ process.exit(1); } - db = await openSenseDb(nerveRoot, args.name); + db = openSenseDb(nerveRoot, args.name); let sql = parsed.sql?.trim(); if (!sql) { @@ -241,7 +240,7 @@ const senseQueryCommand = defineCommand({ } } - const rows = queryAsObjects(db, sql); + const rows = db.prepare(sql).all() as Record[]; if (args.json) { process.stdout.write(`${JSON.stringify(rows, null, 2)}\n`); diff --git a/packages/cli/src/sense-sqlite.ts b/packages/cli/src/sense-sqlite.ts index f79ed34..af82885 100644 --- a/packages/cli/src/sense-sqlite.ts +++ b/packages/cli/src/sense-sqlite.ts @@ -1,25 +1,6 @@ -import { existsSync, readFileSync } from "node:fs"; +import { existsSync } from "node:fs"; import { join } from "node:path"; - -import initSqlJs, { type Database } from "sql.js"; - -// ── WASM singleton ────────────────────────────────────────────────────────── -let _SQL: Awaited> | null = null; - -async function getSQL() { - if (!_SQL) { - _SQL = await initSqlJs(); - } - return _SQL; -} - -/** Open a sense SQLite database (readonly, loaded into memory via sql.js). */ -export async function openSenseDb(nerveRoot: string, senseName: string): Promise { - const path = assertSenseDbExists(nerveRoot, senseName); - const SQL = await getSQL(); - const buffer = readFileSync(path); - return new SQL.Database(buffer); -} +import { DatabaseSync } from "node:sqlite"; /** SQLite path for a sense under the nerve workspace root. */ export function senseDbPath(nerveRoot: string, senseName: string): string { @@ -34,31 +15,39 @@ export function assertSenseDbExists(nerveRoot: string, senseName: string): strin return path; } +/** Open a sense SQLite database in readonly mode using node:sqlite. */ +export function openSenseDb(nerveRoot: string, senseName: string): DatabaseSync { + const path = assertSenseDbExists(nerveRoot, senseName); + return new DatabaseSync(path, { readOnly: true }); +} + /** `SELECT sql FROM sqlite_master WHERE type='table'` (non-null sql only). */ -export function listTableSqlStatements(db: Database): string[] { - const results = db.exec( - `SELECT sql FROM sqlite_master WHERE type = 'table' AND sql IS NOT NULL ORDER BY tbl_name`, - ); - if (results.length === 0) return []; - return results[0].values.map((row) => row[0] as string); +export function listTableSqlStatements(db: DatabaseSync): string[] { + const rows = db + .prepare( + `SELECT sql FROM sqlite_master WHERE type = 'table' AND sql IS NOT NULL ORDER BY tbl_name`, + ) + .all() as { sql: string }[]; + return rows.map((r) => r.sql); } /** * Table used for `nerve sense query ` with no SQL. * Prefers real data tables over `_migrations`, then lexicographic by name. */ -export function pickDefaultPreviewTable(db: Database): string | null { - const results = db.exec( - `SELECT name FROM sqlite_master - WHERE type = 'table' AND sql IS NOT NULL - AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' - ORDER BY - CASE WHEN name = '_migrations' THEN 1 ELSE 0 END, - name - LIMIT 1`, - ); - if (results.length === 0 || results[0].values.length === 0) return null; - return results[0].values[0][0] as string; +export function pickDefaultPreviewTable(db: DatabaseSync): string | null { + const row = db + .prepare( + `SELECT name FROM sqlite_master + WHERE type = 'table' AND sql IS NOT NULL + AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' + ORDER BY + CASE WHEN name = '_migrations' THEN 1 ELSE 0 END, + name + LIMIT 1`, + ) + .get() as { name: string } | undefined; + return row?.name ?? null; } export function defaultPreviewSql(table: string): string { @@ -93,7 +82,7 @@ function stringifyCell(value: unknown): string { if (typeof value === "bigint") return value.toString(); if (typeof value === "number" || typeof value === "boolean") return String(value); if (typeof value === "string") return value; - if (value instanceof Uint8Array) return Buffer.from(value).toString("hex"); + if (Buffer.isBuffer(value)) return value.toString("hex"); try { return JSON.stringify(value); } catch { @@ -136,20 +125,3 @@ export function formatRowsAsAlignedTable(rows: Record[]): strin const body = cells.map((r) => r.map((cell, j) => cell.padEnd(widths[j])).join(" | ")).join("\n"); return `${header}\n${sep}\n${body}\n`; } - -/** - * Run a SQL query via sql.js and return rows as key-value objects. - * sql.js returns columnar data; this converts to the familiar row format. - */ -export function queryAsObjects(db: Database, sql: string): Record[] { - const results = db.exec(sql); - if (results.length === 0) return []; - const { columns, values } = results[0]; - return values.map((row) => { - const obj: Record = {}; - for (let i = 0; i < columns.length; i++) { - obj[columns[i]] = row[i]; - } - return obj; - }); -} diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts index 464898f..402a489 100644 --- a/packages/cli/tsup.config.ts +++ b/packages/cli/tsup.config.ts @@ -9,5 +9,5 @@ export default defineConfig({ js: "#!/usr/bin/env node", }, /** Daemon is loaded from workspace node_modules at runtime — never bundle it. */ - external: ["@uncaged/nerve-daemon", "sql.js"], + external: ["@uncaged/nerve-daemon"], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f81efd..eb9c56c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,9 +26,6 @@ importers: citty: specifier: ^0.1.6 version: 0.1.6 - sql.js: - specifier: ^1.14.1 - version: 1.14.1 devDependencies: '@types/better-sqlite3': specifier: ^7.6.13 @@ -2004,7 +2001,8 @@ snapshots: source-map@0.7.6: {} - sql.js@1.14.1: {} + sql.js@1.14.1: + optional: true stackback@0.0.2: {}