update
This commit is contained in:
98
lib/migrate.js
Normal file
98
lib/migrate.js
Normal file
@@ -0,0 +1,98 @@
|
||||
'use strict';
|
||||
|
||||
const { devWarn } = require('./log');
|
||||
|
||||
// clips -> clips_fts 的同步触发器。v2 / v3 共用。
|
||||
const FTS_TRIGGERS = `
|
||||
CREATE TRIGGER IF NOT EXISTS clips_ai AFTER INSERT ON clips BEGIN
|
||||
INSERT INTO clips_fts(rowid, text) VALUES (new.id, new.text);
|
||||
END;
|
||||
CREATE TRIGGER IF NOT EXISTS clips_ad AFTER DELETE ON clips BEGIN
|
||||
INSERT INTO clips_fts(clips_fts, rowid, text) VALUES('delete', old.id, old.text);
|
||||
END;
|
||||
-- 只在 text 真的变了才重建索引。没有这个 WHEN 的话,每次
|
||||
-- "UPDATE clips SET top_at = ?"(也就是每次复制/置顶)都会白白
|
||||
-- 把该行从 FTS 删掉再插回去。
|
||||
CREATE TRIGGER IF NOT EXISTS clips_au AFTER UPDATE ON clips
|
||||
WHEN old.text IS NOT new.text BEGIN
|
||||
INSERT INTO clips_fts(clips_fts, rowid, text) VALUES('delete', old.id, old.text);
|
||||
INSERT INTO clips_fts(rowid, text) VALUES (new.id, new.text);
|
||||
END;
|
||||
`;
|
||||
|
||||
// STEPS[i] upgrades the schema from version (i + 1) to (i + 2).
|
||||
// STEPS[0] therefore is the v1 -> v2 step.
|
||||
const STEPS = [
|
||||
// v1 -> v2: add FTS5 table + triggers (idempotent).
|
||||
function v1_to_v2(db) {
|
||||
db.exec(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS clips_fts USING fts5(text, content='clips', content_rowid='id');
|
||||
${FTS_TRIGGERS}
|
||||
INSERT INTO clips_fts(rowid, text) SELECT id, text FROM clips WHERE text IS NOT NULL;
|
||||
`);
|
||||
},
|
||||
|
||||
// v2 -> v3: 换成 trigram 分词器。
|
||||
//
|
||||
// v2 用的是默认的 unicode61 分词器,它把一整串连续的中文当成**一个** token,
|
||||
// 而 FTS5 的 MATCH 是整 token 匹配、不做子串匹配。结果:搜"公园"在
|
||||
// "今天去公园散步"里一条都搜不到 —— 对一个中文剪贴板工具来说等于搜索全废。
|
||||
// (v2 这一步之前因为索引下标算错从没真正执行过,所以线上一直悄悄退回 LIKE,
|
||||
// 反而"正好"是对的;把 v2 修好之后这个坑才会暴露出来。)
|
||||
//
|
||||
// trigram 分词器按 3 字符滑窗切分,支持子串匹配,中文可用。
|
||||
// 代价:≤2 字符的查询它匹配不到,这部分由调用方退回 LIKE 处理。
|
||||
function v2_to_v3(db) {
|
||||
db.exec(`
|
||||
DROP TRIGGER IF EXISTS clips_ai;
|
||||
DROP TRIGGER IF EXISTS clips_ad;
|
||||
DROP TRIGGER IF EXISTS clips_au;
|
||||
DROP TABLE IF EXISTS clips_fts;
|
||||
CREATE VIRTUAL TABLE clips_fts USING fts5(
|
||||
text, content='clips', content_rowid='id', tokenize='trigram'
|
||||
);
|
||||
${FTS_TRIGGERS}
|
||||
INSERT INTO clips_fts(rowid, text) SELECT id, text FROM clips WHERE text IS NOT NULL;
|
||||
`);
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Applies each pending schema step in order.
|
||||
*
|
||||
* The DB's own `PRAGMA user_version` is the source of truth for what has already
|
||||
* been applied — not the config file, which only records the version the app
|
||||
* *wants*. Passing a config-derived `fromVersion` would re-run every step on
|
||||
* every launch (re-indexing the whole FTS table each time the app starts).
|
||||
*
|
||||
* Returns the version actually reached, so callers can tell whether a step was
|
||||
* skipped because of an error.
|
||||
*/
|
||||
function migrate(db, fromVersion, toVersion) {
|
||||
let current = fromVersion;
|
||||
try {
|
||||
const row = db.pragma('user_version', { simple: true });
|
||||
if (typeof row === 'number' && row > 0) current = row;
|
||||
} catch (e) {
|
||||
// Non-sqlite stub (tests) — fall back to the caller-supplied version.
|
||||
}
|
||||
|
||||
for (let v = current + 1; v <= toVersion; v++) {
|
||||
const step = STEPS[v - 2];
|
||||
if (!step) continue;
|
||||
try {
|
||||
step(db);
|
||||
current = v;
|
||||
try { db.pragma(`user_version = ${v}`); } catch (e) {}
|
||||
} catch (e) {
|
||||
// Most likely FTS5 is not compiled into this SQLite build. Leave the
|
||||
// version marker where it is so a later launch can retry, and let the
|
||||
// caller fall back to LIKE-based search.
|
||||
devWarn('[migrate] step', v, 'failed, leaving schema at v' + current + ':', e.message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
module.exports = { migrate, STEPS };
|
||||
Reference in New Issue
Block a user