Files
Clipboard/lib/migrate.js
2026-09-12 13:59:12 +08:00

99 lines
3.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 };