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
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 文件(已有备份则跳过)
func (s *Store) backupDB() error {
    if s.dbPath == "" {
        return nil // 无路径信息时跳过备份(测试场景)
    }
    dst := s.dbPath + ".bak"
    if _, err := os.Stat(dst); err == nil {
        return nil // 已有备份
    }
    data, err := os.ReadFile(s.dbPath)
    if err != nil {
        return err
    }
    return os.WriteFile(dst, data, 0644)
}
 
// migrateV1toV2 执行 v1 → v2 的全部变更
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
    }
    if _, err := s.db.Exec(`INSERT OR REPLACE INTO schema_versions VALUES ('version', '2')`); 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
    }
    return s.PopulateFTS()
}
 
// 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
}