mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
fix(scripts/q): use stmt.reader instead of keyword sniffing for SELECT detection
The first-keyword check (`WITH` → SELECT path) was wrong for CTEs that precede mutations (e.g. `WITH stale AS (...) DELETE FROM t WHERE ...`). These would be routed through `db.prepare().all()` instead of executing the mutation. Use better-sqlite3's `stmt.reader` property, which asks SQLite's own parser whether the statement returns data. Single mutations go through `stmt.run()`; compound statements (which `prepare()` rejects) fall back to `db.exec()`. Add a regression test for WITH...DELETE. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -84,6 +84,17 @@ describe('scripts/q.ts', () => {
|
||||
expect(ids).toEqual([2, 9]);
|
||||
});
|
||||
|
||||
it('WITH...DELETE is treated as a mutation, not a query', () => {
|
||||
const r = run("WITH stale AS (SELECT id FROM t WHERE name = 'alice') DELETE FROM t WHERE id IN (SELECT id FROM stale)");
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toBe('');
|
||||
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
const rows = db.prepare('SELECT name FROM t').all() as { name: string }[];
|
||||
db.close();
|
||||
expect(rows).toEqual([{ name: 'bob' }]);
|
||||
});
|
||||
|
||||
it('exits 2 with usage when args are missing', () => {
|
||||
const r = spawnSync('pnpm', ['exec', 'tsx', Q], {
|
||||
encoding: 'utf-8',
|
||||
|
||||
+27
-15
@@ -4,10 +4,11 @@
|
||||
* Usage:
|
||||
* pnpm exec tsx scripts/q.ts <db-path> "<sql>"
|
||||
*
|
||||
* Detects SELECT vs mutation on the first keyword. SELECT/WITH queries
|
||||
* print rows in sqlite3 CLI default ("list") format — pipe-separated,
|
||||
* no header — so existing skill text reads identically. Anything else
|
||||
* runs through db.exec() and prints nothing on success.
|
||||
* Uses better-sqlite3's stmt.reader property to distinguish queries
|
||||
* (SELECT / WITH...SELECT) from mutations. Queries print rows in
|
||||
* sqlite3 CLI default ("list") format — pipe-separated, no header —
|
||||
* so existing skill text reads identically. Mutations run via
|
||||
* stmt.run() (single statement) or db.exec() (compound).
|
||||
*
|
||||
* Why this exists: setup/verify.ts:5 codifies that NanoClaw avoids
|
||||
* depending on the sqlite3 CLI binary; setup never installs or probes
|
||||
@@ -28,18 +29,29 @@ if (!dbPath || sql === undefined) {
|
||||
|
||||
const db = new Database(dbPath);
|
||||
try {
|
||||
const firstKeyword = sql.trim().split(/\s+/)[0]?.toUpperCase() ?? '';
|
||||
if (firstKeyword === 'SELECT' || firstKeyword === 'WITH') {
|
||||
const rows = db.prepare(sql).all() as Record<string, unknown>[];
|
||||
for (const row of rows) {
|
||||
console.log(
|
||||
Object.values(row)
|
||||
.map((v) => (v === null ? '' : String(v)))
|
||||
.join('|'),
|
||||
);
|
||||
try {
|
||||
const stmt = db.prepare(sql);
|
||||
if (stmt.reader) {
|
||||
const rows = stmt.all() as Record<string, unknown>[];
|
||||
for (const row of rows) {
|
||||
console.log(
|
||||
Object.values(row)
|
||||
.map((v) => (v === null ? '' : String(v)))
|
||||
.join('|'),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
stmt.run();
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
// better-sqlite3 throws on compound statements ("contains more than
|
||||
// one statement"). Compound SQL in skills is always mutations
|
||||
// (e.g. "DELETE ...; INSERT ...;"), so fall back to db.exec().
|
||||
if (e instanceof Error && /more than one statement/i.test(e.message)) {
|
||||
db.exec(sql);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
db.exec(sql);
|
||||
}
|
||||
} finally {
|
||||
db.close();
|
||||
|
||||
Reference in New Issue
Block a user