eidetica/backend/database/sql/
schema.rs1use crate::Result;
20use crate::backend::errors::BackendError;
21
22use super::{SqlxBackend, SqlxResultExt};
23
24pub const SCHEMA_VERSION: i64 = 1;
28
29pub const CREATE_TABLES: &[&str] = &[
33 "CREATE TABLE IF NOT EXISTS schema_version (
36 version BIGINT PRIMARY KEY
37 )",
38 "CREATE TABLE IF NOT EXISTS entries (
41 id TEXT PRIMARY KEY NOT NULL,
42 tree_id TEXT NOT NULL,
43 is_root BIGINT NOT NULL DEFAULT 0,
44 verification_status BIGINT NOT NULL DEFAULT 0,
45 height BIGINT NOT NULL DEFAULT 0,
46 entry_json TEXT NOT NULL
47 )",
48 "CREATE TABLE IF NOT EXISTS tree_parents (
51 child_id TEXT NOT NULL,
52 parent_id TEXT NOT NULL,
53 PRIMARY KEY (child_id, parent_id)
54 )",
55 "CREATE TABLE IF NOT EXISTS subtrees (
58 tree_id TEXT NOT NULL,
59 entry_id TEXT NOT NULL,
60 store_name TEXT NOT NULL,
61 height BIGINT NOT NULL,
62 data TEXT,
63 PRIMARY KEY (entry_id, store_name)
64 )",
65 "CREATE TABLE IF NOT EXISTS store_parents (
68 child_id TEXT NOT NULL,
69 parent_id TEXT NOT NULL,
70 store_name TEXT NOT NULL,
71 PRIMARY KEY (child_id, parent_id, store_name)
72 )",
73 "CREATE TABLE IF NOT EXISTS tips (
77 entry_id TEXT NOT NULL,
78 tree_id TEXT NOT NULL,
79 store_name TEXT NOT NULL DEFAULT '',
80 PRIMARY KEY (entry_id, tree_id, store_name)
81 )",
82 "CREATE TABLE IF NOT EXISTS instance_metadata (
86 singleton BIGINT PRIMARY KEY DEFAULT 1 CHECK (singleton = 1),
87 data TEXT NOT NULL
88 )",
89 "CREATE TABLE IF NOT EXISTS crdt_cache (
91 entry_id TEXT NOT NULL,
92 store_name TEXT NOT NULL,
93 state TEXT NOT NULL,
94 PRIMARY KEY (entry_id, store_name)
95 )",
96];
97
98pub const CREATE_INDEXES: &[&str] = &[
100 "CREATE INDEX IF NOT EXISTS idx_entries_tree_id ON entries(tree_id)",
102 "CREATE INDEX IF NOT EXISTS idx_entries_tree_height ON entries(tree_id, height DESC, id)",
103 "CREATE INDEX IF NOT EXISTS idx_entries_verification ON entries(verification_status)",
104 "CREATE INDEX IF NOT EXISTS idx_entries_is_root ON entries(is_root)",
105 "CREATE INDEX IF NOT EXISTS idx_tree_parents_parent ON tree_parents(parent_id)",
107 "CREATE INDEX IF NOT EXISTS idx_tree_parents_child ON tree_parents(child_id)",
108 "CREATE INDEX IF NOT EXISTS idx_subtrees_tree_store_height ON subtrees(tree_id, store_name, height DESC, entry_id)",
110 "CREATE INDEX IF NOT EXISTS idx_subtrees_store_height ON subtrees(store_name, height DESC, entry_id)",
111 "CREATE INDEX IF NOT EXISTS idx_store_parents_parent ON store_parents(store_name, parent_id)",
112 "CREATE INDEX IF NOT EXISTS idx_store_parents_child ON store_parents(store_name, child_id)",
113 "CREATE INDEX IF NOT EXISTS idx_tips_tree_store ON tips(tree_id, store_name)",
115];
116
117pub async fn initialize(backend: &SqlxBackend) -> Result<()> {
122 let pool = backend.pool();
123
124 for statement in CREATE_TABLES {
126 sqlx::query(statement)
127 .execute(pool)
128 .await
129 .sql_context("Schema creation failed")?;
130 }
131
132 let row: Option<(i64,)> = sqlx::query_as("SELECT version FROM schema_version")
134 .fetch_optional(pool)
135 .await
136 .sql_context("Failed to check schema version")?;
137
138 if row.is_none() {
139 sqlx::query("INSERT INTO schema_version (version) VALUES ($1)")
141 .bind(SCHEMA_VERSION)
142 .execute(pool)
143 .await
144 .sql_context("Failed to initialize schema version")?;
145 } else if let Some((current_version,)) = row
146 && current_version < SCHEMA_VERSION
147 {
148 migrate(backend, current_version, SCHEMA_VERSION).await?;
150 }
151
152 for statement in CREATE_INDEXES {
154 sqlx::query(statement)
155 .execute(pool)
156 .await
157 .sql_context("Index creation failed")?;
158 }
159
160 Ok(())
161}
162
163async fn migrate(backend: &SqlxBackend, from: i64, to: i64) -> Result<()> {
168 tracing::info!(from, to, "Starting SQL schema migration");
169
170 let mut current = from;
171 while current < to {
172 let next = current + 1;
173 tracing::info!(from = current, to = next, "Running migration");
174
175 run_migration(backend, current, next).await?;
176
177 sqlx::query("UPDATE schema_version SET version = $1")
179 .bind(next)
180 .execute(backend.pool())
181 .await
182 .sql_context("Failed to update schema version")?;
183
184 tracing::info!(version = next, "Migration completed");
185 current = next;
186 }
187
188 tracing::info!(from, to, "All migrations completed successfully");
189 Ok(())
190}
191
192async fn run_migration(backend: &SqlxBackend, from: i64, to: i64) -> Result<()> {
209 let _ = backend;
220
221 Err(BackendError::SqlxError {
222 reason: format!(
223 "Unknown migration path: v{from} to v{to}. \
224 This likely means SCHEMA_VERSION was incremented without adding a migration."
225 ),
226 source: None,
227 }
228 .into())
229}