| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193 |
- /**
- * SQLite 引擎能力探针
- *
- * 工作台库用 node:sqlite 的 DatabaseSync,而 bun 编译产物里没有这个内置模块,只有 bun:sqlite。
- * 本脚本用同一组断言分别在两个引擎上跑,输出能力矩阵,用来判断适配层是否可行、差异在哪。
- *
- * 用法:
- * node scripts/sqlite-engine-probe.mjs
- * bun scripts/sqlite-engine-probe.mjs
- */
- import fs from 'node:fs';
- import os from 'node:os';
- import path from 'node:path';
- const isBun = typeof globalThis.Bun !== 'undefined';
- const engine = isBun ? 'bun:sqlite' : 'node:sqlite';
- async function openDatabase(file, options = {}) {
- if (isBun) {
- const { Database } = await import('bun:sqlite');
- // bun 用全小写 readonly,node 用驼峰 readOnly;且传空对象会因为没有任何 open flag 报 SQLITE_MISUSE
- return new Database(file, options.readOnly ? { readonly: true } : { create: true, readwrite: true });
- }
- const { DatabaseSync } = await import('node:sqlite');
- return new DatabaseSync(file, options.readOnly ? { readOnly: true } : {});
- }
- const results = [];
- function check(name, fn) {
- try {
- const detail = fn();
- results.push({ name, ok: true, detail: detail === undefined ? '' : String(detail) });
- } catch (error) {
- results.push({ name, ok: false, detail: error.message.slice(0, 120) });
- }
- }
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sqlite-probe-'));
- const file = path.join(dir, 'probe.db');
- const db = await openDatabase(file);
- check('exec 多语句 PRAGMA', () => {
- db.exec('PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000;');
- });
- check('WAL 已生效', () => {
- const row = db.prepare('PRAGMA journal_mode').get();
- const mode = String(row.journal_mode || row['journal_mode'] || '').toLowerCase();
- if (mode !== 'wal') throw new Error(`journal_mode=${mode}`);
- return mode;
- });
- check('busy_timeout 已设置', () => {
- const row = db.prepare('PRAGMA busy_timeout').get();
- const value = Number(row.timeout ?? row.busy_timeout ?? 0);
- if (value !== 5000) throw new Error(`busy_timeout=${value}`);
- return value;
- });
- check('建表含 CHECK 与外键', () => {
- db.exec(`
- CREATE TABLE parent (
- id TEXT PRIMARY KEY,
- mode TEXT NOT NULL DEFAULT 'review' CHECK(mode IN ('review','auto','human')),
- created_at TEXT NOT NULL
- );
- CREATE TABLE child (
- id TEXT PRIMARY KEY,
- parent_id TEXT NOT NULL REFERENCES parent(id) ON DELETE CASCADE,
- body TEXT NOT NULL
- );
- `);
- });
- check('run 位置参数并返回 changes', () => {
- const result = db.prepare('INSERT INTO parent(id,mode,created_at) VALUES(?,?,?)')
- .run('p1', 'review', new Date().toISOString());
- if (Number(result.changes) !== 1) throw new Error(`changes=${result.changes}`);
- return `changes=${result.changes} lastInsertRowid=${result.lastInsertRowid}`;
- });
- check('get 命中返回行对象', () => {
- const row = db.prepare('SELECT * FROM parent WHERE id=?').get('p1');
- if (!row || row.mode !== 'review') throw new Error(JSON.stringify(row));
- return `mode=${row.mode}`;
- });
- check('get 未命中的返回值', () => {
- const row = db.prepare('SELECT * FROM parent WHERE id=?').get('missing');
- // 生产代码写法是 .get(...)?.value ?? fallback,null 与 undefined 都能走通
- if (row !== undefined && row !== null) throw new Error(`返回了 ${JSON.stringify(row)}`);
- return row === null ? 'null' : 'undefined';
- });
- check('all 返回数组', () => {
- const rows = db.prepare('SELECT * FROM parent').all();
- if (!Array.isArray(rows) || rows.length !== 1) throw new Error(`len=${rows?.length}`);
- return `len=${rows.length}`;
- });
- check('PRAGMA table_info 走 prepare.all', () => {
- const rows = db.prepare('PRAGMA table_info(parent)').all();
- const names = rows.map(item => item.name);
- if (!names.includes('mode')) throw new Error(names.join(','));
- return names.join(',');
- });
- check('CHECK 约束生效', () => {
- try {
- db.prepare('INSERT INTO parent(id,mode,created_at) VALUES(?,?,?)').run('bad', 'illegal', 'now');
- } catch {
- return '违规插入已被拒绝';
- }
- throw new Error('CHECK 未拦截非法值');
- });
- check('外键约束生效', () => {
- try {
- db.prepare('INSERT INTO child(id,parent_id,body) VALUES(?,?,?)').run('c1', 'nonexistent', 'x');
- } catch {
- return '孤儿行已被拒绝';
- }
- throw new Error('外键未拦截孤儿行');
- });
- check('显式事务 COMMIT', () => {
- db.exec('BEGIN');
- db.prepare('INSERT INTO parent(id,mode,created_at) VALUES(?,?,?)').run('p2', 'auto', 'now');
- db.exec('COMMIT');
- const row = db.prepare('SELECT * FROM parent WHERE id=?').get('p2');
- if (!row) throw new Error('提交后查不到');
- });
- check('显式事务 ROLLBACK', () => {
- db.exec('BEGIN');
- db.prepare('INSERT INTO parent(id,mode,created_at) VALUES(?,?,?)').run('p3', 'auto', 'now');
- db.exec('ROLLBACK');
- const row = db.prepare('SELECT * FROM parent WHERE id=?').get('p3');
- if (row) throw new Error('回滚后仍查得到');
- });
- check('ON CONFLICT DO UPDATE', () => {
- db.exec('CREATE TABLE settings(key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL)');
- const sql = `INSERT INTO settings(key,value,updated_at) VALUES(?,?,?)
- ON CONFLICT(key) DO UPDATE SET value=excluded.value,updated_at=excluded.updated_at`;
- db.prepare(sql).run('k', 'v1', 'now');
- db.prepare(sql).run('k', 'v2', 'later');
- const row = db.prepare('SELECT value FROM settings WHERE key=?').get('k');
- if (row.value !== 'v2') throw new Error(`value=${row.value}`);
- return 'upsert 生效';
- });
- check('WAL 附属文件已落盘', () => {
- const walFile = `${file}-wal`;
- if (!fs.existsSync(walFile)) throw new Error('未生成 -wal 文件');
- return path.basename(walFile);
- });
- let readOnlyDetail = '';
- try {
- const ro = await openDatabase(file, { readOnly: true });
- const row = ro.prepare('SELECT COUNT(*) AS n FROM parent').get();
- readOnlyDetail = `可读,count=${row.n}`;
- try {
- ro.prepare('INSERT INTO parent(id,mode,created_at) VALUES(?,?,?)').run('ro', 'auto', 'now');
- readOnlyDetail += ';但写入未被拒绝';
- results.push({ name: '只读模式', ok: false, detail: readOnlyDetail });
- } catch {
- results.push({ name: '只读模式', ok: true, detail: `${readOnlyDetail};写入已拒绝` });
- }
- ro.close();
- } catch (error) {
- results.push({ name: '只读模式', ok: false, detail: error.message.slice(0, 120) });
- }
- db.close();
- const failed = results.filter(item => !item.ok);
- process.stdout.write(`\n引擎:${engine}(${isBun ? `bun ${Bun.version}` : `node ${process.version}`})\n\n`);
- for (const item of results) {
- process.stdout.write(` ${item.ok ? '[ok] ' : '[FAIL]'} ${item.name}${item.detail ? ` — ${item.detail}` : ''}\n`);
- }
- process.stdout.write(`\n${results.length - failed.length}/${results.length} 通过\n`);
- // Windows 下句柄可能尚未完全释放,清理失败不影响探测结论
- try {
- fs.rmSync(dir, { recursive: true, force: true });
- } catch {
- process.stdout.write(`(临时目录未能清理,可手动删除:${dir})\n`);
- }
- process.exit(failed.length ? 1 : 0);
|