ax_rd
7 hours ago a5c01a3f115399b55f43d639c8278b8d68c6211c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package index
 
import (
    "fmt"
    "os"
)
 
const currentSchemaVersion = 2
 
// migrate 幂等的 schema 升级:v1 → v2。
// v2 变更:nodes 加 aliases/status/size/mtime/content_hash 列;
// edges 加 provenance 列;新建 unresolved_links、schema_versions 表;
// nodes_fts 改 external-content + 触发器。迁移前自动备份 .db 文件。
func (s *Store) migrate() error {
    // 读当前版本
    var version int
    err := s.db.QueryRow(`SELECT value FROM schema_versions WHERE key='version'`).Scan(&version)
    if err != nil && version == 0 {
        // 首次:schema_versions 表可能不存在
        version = 1
    }
    if version >= currentSchemaVersion {
        return nil
    }
 
    // 备份
    if err := s.backupDB(); err != nil {
        return fmt.Errorf("备份失败: %w", err)
    }
 
    // v1 → v2
    if err := s.migrateV1toV2(); err != nil {
        return fmt.Errorf("v1→v2 迁移失败: %w", err)
    }
    return nil
}
 
// backupDB 迁移前备份 .db 文件(已有备份则跳过)。
// 用 VACUUM INTO 而非 os.ReadFile:WAL 模式下未 checkpoint 的数据在
// -wal 文件里,直读主文件会漏掉这部分,导致备份不一致;VACUUM INTO
// 会合并 WAL 生成一份完整的独立库文件(SQLite 3.27+)。
func (s *Store) backupDB() error {
    if s.dbPath == "" {
        return nil // 无路径信息时跳过备份(测试场景)
    }
    dst := s.dbPath + ".bak"
    if _, err := os.Stat(dst); err == nil {
        return nil // 已有备份
    }
    _, err := s.db.Exec(`VACUUM INTO ?`, dst)
    return err
}
 
// migrateV1toV2 执行 v1 → v2 的全部变更。
// 注意:版本号(schema_versions)必须在最后一步(FTS 重建)成功后才写入,
// 否则中途失败时后续 Open 会读到 version=2 直接早退,半迁移状态无法重试。
func (s *Store) migrateV1toV2() error {
    // 1. schema_versions 表(只建表,版本号推迟到最后写入)
    if _, err := s.db.Exec(`CREATE TABLE IF NOT EXISTS schema_versions (
        key TEXT PRIMARY KEY, value TEXT NOT NULL)`); err != nil {
        return err
    }
 
    // 2. nodes 加列(逐列检查,幂等)
    for _, col := range []struct{ name, def string }{
        {"aliases", "ALTER TABLE nodes ADD COLUMN aliases TEXT"},
        {"status", "ALTER TABLE nodes ADD COLUMN status TEXT"},
        {"size", "ALTER TABLE nodes ADD COLUMN size INTEGER"},
        {"mtime", "ALTER TABLE nodes ADD COLUMN mtime INTEGER"},
        {"content_hash", "ALTER TABLE nodes ADD COLUMN content_hash TEXT"},
    } {
        if !s.nodeColumnExists(col.name) {
            if _, err := s.db.Exec(col.def); err != nil {
                return fmt.Errorf("加列 %s 失败: %w", col.name, err)
            }
        }
    }
 
    // 3. edges 加 provenance 列
    if !s.edgeColumnExists("provenance") {
        if _, err := s.db.Exec(`ALTER TABLE edges ADD COLUMN provenance TEXT`); err != nil {
            return fmt.Errorf("edges 加 provenance 失败: %w", err)
        }
    }
 
    // 4. unresolved_links 表
    if _, err := s.db.Exec(`CREATE TABLE IF NOT EXISTS unresolved_links (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        from_node INTEGER NOT NULL,
        link_text TEXT NOT NULL,
        name_tail TEXT NOT NULL,
        status TEXT NOT NULL DEFAULT 'pending',
        created_at TEXT DEFAULT (datetime('now')),
        last_attempt TEXT)`); err != nil {
        return err
    }
    if _, err := s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_unresolved_tail
        ON unresolved_links(name_tail) WHERE status='failed'`); err != nil {
        return err
    }
 
    // 5. FTS 改 external-content:删旧表重建 + 触发器 + 重灌
    if _, err := s.db.Exec(`DROP TABLE IF EXISTS nodes_fts`); err != nil {
        return err
    }
    if err := s.CreateFTS(); err != nil {
        return err
    }
    if err := s.PopulateFTS(); err != nil {
        return err
    }
 
    // 6. 最后一步:全部成功后才写入版本号(参数化,引用常量 currentSchemaVersion)。
    // 提前写入会导致中途失败时后续 Open 读到 version=2 直接早退,半迁移状态无法重试。
    if _, err := s.db.Exec(`INSERT OR REPLACE INTO schema_versions VALUES ('version', ?)`,
        currentSchemaVersion); err != nil {
        return err
    }
    return nil
}
 
// nodeColumnExists 检查 nodes 表是否已有指定列
func (s *Store) nodeColumnExists(name string) bool {
    var n int
    s.db.QueryRow(`SELECT COUNT(*) FROM pragma_table_info('nodes') WHERE name=?`, name).Scan(&n)
    return n > 0
}
 
// edgeColumnExists 检查 edges 表是否已有指定列
func (s *Store) edgeColumnExists(name string) bool {
    var n int
    s.db.QueryRow(`SELECT COUNT(*) FROM pragma_table_info('edges') WHERE name=?`, name).Scan(&n)
    return n > 0
}