1use rusqlite::{Connection, Result};
10use tracing::{info, warn};
11
12struct Migration {
14 version: &'static str,
15 sql: &'static str,
16}
17
18const MIGRATIONS: &[Migration] = &[
23 Migration {
24 version: "001_add_session_lifecycle",
25 sql: include_str!("../../migrations/001_add_session_lifecycle.sql"),
26 },
27 Migration {
28 version: "002_add_multi_path_sessions",
29 sql: include_str!("../../migrations/002_add_multi_path_sessions.sql"),
30 },
31 Migration {
32 version: "003_add_agent_sessions",
33 sql: include_str!("../../migrations/003_add_agent_sessions.sql"),
34 },
35 Migration {
36 version: "004_add_memory_and_tasks",
37 sql: include_str!("../../migrations/004_add_memory_and_tasks.sql"),
38 },
39 Migration {
40 version: "005_add_checkpoint_grouping",
41 sql: include_str!("../../migrations/005_add_checkpoint_grouping.sql"),
42 },
43 Migration {
44 version: "006_rename_tasks_to_issues",
45 sql: include_str!("../../migrations/006_rename_tasks_to_issues.sql"),
46 },
47 Migration {
48 version: "007_embeddings_support",
49 sql: include_str!("../../migrations/007_embeddings_support.sql"),
50 },
51 Migration {
52 version: "008_dynamic_vec_dimensions",
53 sql: include_str!("../../migrations/008_dynamic_vec_dimensions.sql"),
54 },
55 Migration {
56 version: "009_rename_task_to_reminder",
57 sql: include_str!("../../migrations/009_rename_task_to_reminder.sql"),
58 },
59 Migration {
60 version: "010_issue_projects",
61 sql: include_str!("../../migrations/010_issue_projects.sql"),
62 },
63 Migration {
64 version: "011_blob_embeddings",
65 sql: include_str!("../../migrations/011_blob_embeddings.sql"),
66 },
67 Migration {
68 version: "012_tiered_embeddings",
69 sql: include_str!("../../migrations/012_tiered_embeddings.sql"),
70 },
71];
72
73pub fn run_migrations(conn: &Connection) -> Result<()> {
85 conn.execute(
87 "CREATE TABLE IF NOT EXISTS schema_migrations (
88 version TEXT PRIMARY KEY,
89 applied_at INTEGER NOT NULL
90 )",
91 [],
92 )?;
93
94 let applied: std::collections::HashSet<String> = conn
96 .prepare("SELECT version FROM schema_migrations")?
97 .query_map([], |row| row.get(0))?
98 .collect::<Result<_, _>>()?;
99
100 for migration in MIGRATIONS {
102 if applied.contains(migration.version) {
103 continue;
104 }
105
106 info!(version = migration.version, "Applying migration");
107
108 if let Err(e) = conn.execute_batch(migration.sql) {
110 let err_str = e.to_string();
111 if err_str.contains("duplicate column name") {
115 warn!(
116 version = migration.version,
117 "Migration partially applied (columns exist), marking complete"
118 );
119 } else if err_str.contains("no such module: vec0") {
120 warn!(
121 version = migration.version,
122 "Skipping sqlite-vec virtual table (not available in Rust CLI)"
123 );
124 } else {
125 return Err(e);
126 }
127 }
128
129 conn.execute(
131 "INSERT INTO schema_migrations (version, applied_at) VALUES (?1, ?2)",
132 rusqlite::params![migration.version, chrono::Utc::now().timestamp_millis()],
133 )?;
134
135 info!(version = migration.version, "Migration complete");
136 }
137
138 Ok(())
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::storage::schema::SCHEMA_SQL;
145
146 fn setup_db(conn: &Connection) {
148 conn.execute_batch(SCHEMA_SQL).expect("Base schema should apply");
149 }
150
151 #[test]
152 fn test_migrations_compile() {
153 assert!(!MIGRATIONS.is_empty());
156 assert_eq!(MIGRATIONS.len(), 12);
157 }
158
159 #[test]
160 fn test_run_migrations_fresh_db() {
161 let conn = Connection::open_in_memory().unwrap();
162 setup_db(&conn);
163 run_migrations(&conn).expect("Migrations should apply to fresh database");
164
165 let count: i32 = conn
167 .query_row("SELECT COUNT(*) FROM schema_migrations", [], |row| {
168 row.get(0)
169 })
170 .unwrap();
171 assert_eq!(count, 12);
172 }
173
174 #[test]
175 fn test_run_migrations_idempotent() {
176 let conn = Connection::open_in_memory().unwrap();
177 setup_db(&conn);
178
179 run_migrations(&conn).expect("First run should succeed");
181 run_migrations(&conn).expect("Second run should succeed (idempotent)");
182
183 let count: i32 = conn
185 .query_row("SELECT COUNT(*) FROM schema_migrations", [], |row| {
186 row.get(0)
187 })
188 .unwrap();
189 assert_eq!(count, 12);
190 }
191}