feat(schema): v19 卡片/分组组合表与迁移回填

- 新增 card / card_group:卡片只存标题与顺序,内容由勾选分组组成,支持混合格式
- v18→v19 迁移:按现有 section_block 回填卡片与分组勾选(card-<section>)
- 新库 seed 完成后同样回填,旧界面暂不受影响
- 测试 59 passed(含 15→19 迁移)
This commit is contained in:
2026-10-30 20:32:58 +08:00
parent e52db54bdc
commit eb4fded8db
2 changed files with 68 additions and 2 deletions
+2 -1
View File
@@ -600,8 +600,9 @@ mod tests {
let fresh = Connection::open_in_memory().expect("内存库应能打开");
fresh
.execute_batch(
"CREATE TABLE node (key TEXT PRIMARY KEY, parent_key TEXT, kind TEXT, view_mode TEXT);
"CREATE TABLE node (key TEXT PRIMARY KEY, parent_key TEXT, title TEXT, kind TEXT, view_mode TEXT);
CREATE TABLE entry (key TEXT PRIMARY KEY, node_key TEXT NOT NULL, name TEXT NOT NULL);
CREATE TABLE section_block (id INTEGER PRIMARY KEY, node_key TEXT NOT NULL, title TEXT, preview_limit INTEGER, sort INTEGER NOT NULL DEFAULT 0);
PRAGMA user_version = 15;",
)
.expect("应能造出 v15 老库");
+66 -1
View File
@@ -141,6 +141,44 @@ CREATE TABLE IF NOT EXISTS audit (
target TEXT,
detail TEXT
);
-- ========== v19:首页卡片(展示层组合) ==========
-- 一张卡片 = 标题 + 顺序;内容由 card_group 勾选分组组成,卡片不拥有数据
CREATE TABLE IF NOT EXISTS card (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
sort INTEGER NOT NULL DEFAULT 0
);
-- 卡片包含哪些分组(可混合格式),每个分组可单独设预览条数
CREATE TABLE IF NOT EXISTS card_group (
card_id INTEGER NOT NULL REFERENCES card(id) ON DELETE CASCADE,
group_key TEXT NOT NULL REFERENCES node(key) ON DELETE CASCADE ON UPDATE CASCADE,
sort INTEGER NOT NULL DEFAULT 0,
preview_limit INTEGER,
PRIMARY KEY (card_id, group_key)
);
"#;
/// 把老的「分区 → 一个展示块」迁移成「卡片 → 多个分组」。
const BACKFILL_CARDS_SQL: &str = r#"
INSERT OR IGNORE INTO card (key, title, sort)
SELECT 'card-' || COALESCE(p.key, n.key),
COALESCE(b.title, COALESCE(p.title, n.title)),
MIN(b.sort)
FROM section_block b
JOIN node n ON n.key = b.node_key
LEFT JOIN node p ON p.key = n.parent_key
GROUP BY COALESCE(p.key, n.key);
INSERT OR IGNORE INTO card_group (card_id, group_key, sort, preview_limit)
SELECT c.id, b.node_key, b.sort, b.preview_limit
FROM section_block b
JOIN node n ON n.key = b.node_key
LEFT JOIN node p ON p.key = n.parent_key
JOIN card c ON c.key = 'card-' || COALESCE(p.key, n.key);
"#;
/// v16 → v17:条目加 `updated_at`(乐观锁),新增审计表。
@@ -172,6 +210,24 @@ WHERE kind = 'section' AND key IN (
);
"#;
/// v18 → v19:首页卡片改为「卡片 + 勾选分组」。
const V19_SQL: &str = r#"
CREATE TABLE IF NOT EXISTS card (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
sort INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS card_group (
card_id INTEGER NOT NULL REFERENCES card(id) ON DELETE CASCADE,
group_key TEXT NOT NULL REFERENCES node(key) ON DELETE CASCADE ON UPDATE CASCADE,
sort INTEGER NOT NULL DEFAULT 0,
preview_limit INTEGER,
PRIMARY KEY (card_id, group_key)
);
"#;
/// v15 → v16 的增量(老库升级用;新库直接走 SCHEMA_SQL)。
const V16_SQL: &str = r#"
CREATE TABLE IF NOT EXISTS admin (
@@ -213,7 +269,7 @@ pub struct SeedReport {
///
/// 从 v16 起有了第一套迁移:`migrate` 会把老库逐级升上来,
/// 只有「比程序更新」或「没有迁移路径」的版本才拒绝启动。
pub const SCHEMA_VERSION: i64 = 18;
pub const SCHEMA_VERSION: i64 = 19;
fn current_version(conn: &Connection) -> Result<i64, StashError> {
conn.pragma_query_value(None, "user_version", |row| row.get(0))
@@ -262,6 +318,12 @@ pub fn migrate(conn: &Connection) -> Result<i64, StashError> {
version = 18;
}
if version == 18 {
conn.execute_batch(V19_SQL).map_err(map_error)?;
conn.execute_batch(BACKFILL_CARDS_SQL).map_err(map_error)?;
version = 19;
}
conn.pragma_update(None, "user_version", version)
.map_err(map_error)?;
@@ -371,6 +433,9 @@ pub fn seed(conn: &mut Connection) -> Result<SeedReport, StashError> {
report.blocks += 1;
}
// 老结构(分区 → 展示块)同步生成卡片,保证新首页立刻有内容
tx.execute_batch(BACKFILL_CARDS_SQL).map_err(map_error)?;
tx.commit().map_err(map_error)?;
Ok(report)