rustbrain_core/storage/
migrations.rs1use crate::error::{BrainError, Result};
8use rusqlite::Connection;
9
10pub const SCHEMA_VERSION: u32 = 1;
14
15pub fn migrate(conn: &Connection) -> Result<()> {
17 conn.execute_batch(
18 "
19 CREATE TABLE IF NOT EXISTS schema_meta (
20 key TEXT PRIMARY KEY NOT NULL,
21 value TEXT NOT NULL
22 );
23 ",
24 )?;
25
26 let current = read_version(conn)?;
27 if current > SCHEMA_VERSION {
28 return Err(BrainError::SchemaVersion {
29 found: current,
30 supported: SCHEMA_VERSION,
31 });
32 }
33
34 if current < 1 {
35 migrate_v1(conn)?;
36 write_version(conn, 1)?;
37 }
38
39 Ok(())
40}
41
42fn read_version(conn: &Connection) -> Result<u32> {
43 let mut stmt = conn.prepare("SELECT value FROM schema_meta WHERE key = 'schema_version'")?;
44 let mut rows = stmt.query([])?;
45 if let Some(row) = rows.next()? {
46 let v: String = row.get(0)?;
47 Ok(v.parse::<u32>().unwrap_or(0))
48 } else {
49 Ok(0)
50 }
51}
52
53fn write_version(conn: &Connection, version: u32) -> Result<()> {
54 conn.execute(
55 "INSERT INTO schema_meta (key, value) VALUES ('schema_version', ?1)
56 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
57 [version.to_string()],
58 )?;
59 Ok(())
60}
61
62fn migrate_v1(conn: &Connection) -> Result<()> {
63 conn.execute_batch(
64 "
65 PRAGMA foreign_keys = ON;
66
67 CREATE TABLE IF NOT EXISTS nodes (
68 id TEXT PRIMARY KEY,
69 node_type TEXT NOT NULL,
70 title TEXT NOT NULL,
71 file_path TEXT,
72 symbol_hash INTEGER,
73 summary TEXT,
74 content_hash TEXT,
75 created_at INTEGER NOT NULL,
76 updated_at INTEGER NOT NULL
77 );
78
79 CREATE TABLE IF NOT EXISTS edges (
80 source_id TEXT NOT NULL,
81 target_id TEXT NOT NULL,
82 relation_type TEXT NOT NULL,
83 weight REAL NOT NULL DEFAULT 1.0,
84 decay_rate REAL DEFAULT 0.0,
85 created_at INTEGER NOT NULL,
86 PRIMARY KEY (source_id, target_id, relation_type),
87 FOREIGN KEY (source_id) REFERENCES nodes(id) ON DELETE CASCADE,
88 FOREIGN KEY (target_id) REFERENCES nodes(id) ON DELETE CASCADE
89 );
90
91 CREATE TABLE IF NOT EXISTS symbol_anchors (
92 symbol_hash INTEGER PRIMARY KEY,
93 crate_name TEXT NOT NULL,
94 module_path TEXT NOT NULL,
95 symbol_name TEXT NOT NULL,
96 file_path TEXT NOT NULL,
97 start_line INTEGER NOT NULL,
98 end_line INTEGER NOT NULL,
99 doc_comment TEXT
100 );
101
102 CREATE TABLE IF NOT EXISTS node_tags (
103 node_id TEXT NOT NULL,
104 tag TEXT NOT NULL,
105 PRIMARY KEY (node_id, tag),
106 FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE
107 );
108
109 CREATE TABLE IF NOT EXISTS node_aliases (
110 alias TEXT PRIMARY KEY NOT NULL,
111 node_id TEXT NOT NULL,
112 FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE
113 );
114
115 CREATE TABLE IF NOT EXISTS pending_links (
116 source_id TEXT NOT NULL,
117 raw_target TEXT NOT NULL,
118 relation_type TEXT NOT NULL,
119 created_at INTEGER NOT NULL,
120 PRIMARY KEY (source_id, raw_target, relation_type),
121 FOREIGN KEY (source_id) REFERENCES nodes(id) ON DELETE CASCADE
122 );
123
124 CREATE VIRTUAL TABLE IF NOT EXISTS node_fts USING fts5(
125 node_id UNINDEXED,
126 title,
127 content,
128 tags
129 );
130
131 CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);
132 CREATE INDEX IF NOT EXISTS idx_node_aliases_node ON node_aliases(node_id);
133 CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(node_type);
134 ",
135 )?;
136 Ok(())
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use rusqlite::Connection;
143
144 #[test]
145 fn migrate_fresh_db_to_v1() {
146 let conn = Connection::open_in_memory().unwrap();
147 migrate(&conn).unwrap();
148 assert_eq!(read_version(&conn).unwrap(), SCHEMA_VERSION);
149 migrate(&conn).unwrap();
151 assert_eq!(read_version(&conn).unwrap(), SCHEMA_VERSION);
152 }
153}