'use strict'; /** * 回归测试:v1 -> v2 迁移 * * 曾经的 bug:migrate() 用 STEPS[v - 1] 取步骤,但 STEPS[0] 就是 v1->v2 的步骤, * 所以 v=2 时取到的是 STEPS[1] === undefined,整个 FTS5 迁移从来没跑过。 * 表现:clips_fts 永远不存在,搜索一直静默退回 LIKE 全表扫描。 */ const assert = require('node:assert'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const Database = require('better-sqlite3'); const { migrate, STEPS } = require('../lib/migrate.js'); let passed = 0; function check(name, fn) { try { fn(); console.log(' ok -', name); passed++; } catch (e) { console.error(' FAIL -', name, e.message); process.exitCode = 1; } } function freshDb() { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'clip-mig2-')); const db = new Database(path.join(tmp, 'h.db')); db.exec(`CREATE TABLE clips ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT, text TEXT, top_at REAL, created_at REAL NOT NULL DEFAULT 0)`); return db; } check('STEPS[0] is the v1->v2 step (index maps version-2, not version-1)', () => { assert.strictEqual(typeof STEPS[0], 'function'); assert.match(STEPS[0].name, /v1_to_v2/); }); // 探测这个 SQLite 构建是否带 FTS5;没有的话下面的断言换成"优雅降级" let fts5 = true; { const db = freshDb(); try { db.exec(`CREATE VIRTUAL TABLE t USING fts5(x)`); } catch (_) { fts5 = false; } db.close(); } check('migrate actually reaches v2 and reports it', () => { const db = freshDb(); const reached = migrate(db, 1, 2); if (fts5) { assert.strictEqual(reached, 2, '迁移应到达 v2'); assert.strictEqual(db.pragma('user_version', { simple: true }), 2); } else { assert.strictEqual(reached, 1, '没有 FTS5 时应停在 v1,而不是谎报成功'); } db.close(); }); if (fts5) { check('clips_fts exists and is searchable after migrate', () => { const db = freshDb(); db.prepare('INSERT INTO clips(type,text) VALUES(?,?)').run('text', 'hello world'); migrate(db, 1, 2); const rows = db.prepare(`SELECT rowid FROM clips_fts WHERE clips_fts MATCH 'hello'`).all(); assert.strictEqual(rows.length, 1, '已有数据应被回填进 FTS 索引'); db.close(); }); check('migrate is idempotent across restarts (no duplicate index entries)', () => { const db = freshDb(); db.prepare('INSERT INTO clips(type,text) VALUES(?,?)').run('text', 'hello world'); for (let launch = 0; launch < 3; launch++) migrate(db, 1, 2); const n = db.prepare(`SELECT count(*) c FROM clips_fts WHERE clips_fts MATCH 'hello'`).get().c; assert.strictEqual(n, 1, '重复启动不应重复回填'); db.exec(`INSERT INTO clips_fts(clips_fts) VALUES('integrity-check')`); db.close(); }); check('user_version short-circuits the backfill on subsequent runs', () => { const db = freshDb(); migrate(db, 1, 2); // 直接往 clips 插一行但绕开触发器是做不到的,所以改为验证: // 第二次 migrate 不再执行回填(否则这行会被索引两次) db.prepare('INSERT INTO clips(type,text) VALUES(?,?)').run('text', 'unique token zzz'); migrate(db, 1, 2); const n = db.prepare(`SELECT count(*) c FROM clips_fts WHERE clips_fts MATCH 'zzz'`).get().c; assert.strictEqual(n, 1); db.close(); }); check('AFTER UPDATE trigger skips FTS churn when only top_at changes', () => { const db = freshDb(); db.prepare('INSERT INTO clips(type,text) VALUES(?,?)').run('text', 'hello world'); migrate(db, 1, 2); db.prepare('UPDATE clips SET top_at = ? WHERE id = 1').run(123); assert.strictEqual( db.prepare(`SELECT count(*) c FROM clips_fts WHERE clips_fts MATCH 'hello'`).get().c, 1, '置顶不应影响可搜索性'); db.exec(`INSERT INTO clips_fts(clips_fts) VALUES('integrity-check')`); db.close(); }); check('AFTER UPDATE trigger still re-indexes when text changes', () => { const db = freshDb(); db.prepare('INSERT INTO clips(type,text) VALUES(?,?)').run('text', 'hello world'); migrate(db, 1, 2); db.prepare('UPDATE clips SET text = ? WHERE id = 1').run('goodbye moon'); assert.strictEqual(db.prepare(`SELECT count(*) c FROM clips_fts WHERE clips_fts MATCH 'hello'`).get().c, 0); assert.strictEqual(db.prepare(`SELECT count(*) c FROM clips_fts WHERE clips_fts MATCH 'goodbye'`).get().c, 1); db.close(); }); check('DELETE keeps the FTS index consistent', () => { const db = freshDb(); db.prepare('INSERT INTO clips(type,text) VALUES(?,?)').run('text', 'hello world'); migrate(db, 1, 2); db.prepare('DELETE FROM clips WHERE id = 1').run(); assert.strictEqual(db.prepare(`SELECT count(*) c FROM clips_fts WHERE clips_fts MATCH 'hello'`).get().c, 0); db.exec(`INSERT INTO clips_fts(clips_fts) VALUES('integrity-check')`); db.close(); }); } else { console.log(' SKIP - FTS5 unavailable in this SQLite build'); } check('migrate on a broken db degrades instead of throwing', () => { const bad = { pragma() { throw new Error('nope'); }, exec() { throw new Error('nope'); } }; assert.strictEqual(migrate(bad, 1, 2), 1); }); console.log(`\n${passed} checks passed`);