This commit is contained in:
2026-09-12 13:59:12 +08:00
commit 941ce4baab
71 changed files with 13037 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
'use strict';
/**
* 回归测试v1 -> v2 迁移
*
* 曾经的 bugmigrate() 用 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`);