codlet_sqlx/migration.rs
1//! Migration runner for codlet SQLite tables (RFC-011 §10.4).
2//!
3//! The SQL is embedded at compile time via `include_str!`. Host applications
4//! own the migration *application order* — this function is idempotent and
5//! safe to call on startup, but the host decides when and how to run it
6//! relative to its own migrations (RFC-011 §10.4).
7
8use sqlx::SqlitePool;
9
10/// Run codlet's embedded SQLite migrations against `pool`.
11///
12/// Uses `IF NOT EXISTS` semantics; safe to call on every startup.
13///
14/// # Errors
15/// Returns a [`sqlx::Error`] if the SQL execution fails.
16pub async fn run_migrations(pool: &SqlitePool) -> Result<(), sqlx::Error> {
17 // WAL mode gives better concurrent read/write performance and is
18 // recommended for codlet's workload.
19 sqlx::query("PRAGMA journal_mode = WAL")
20 .execute(pool)
21 .await?;
22 // Enforce foreign key constraints if the host schema uses them.
23 sqlx::query("PRAGMA foreign_keys = ON")
24 .execute(pool)
25 .await?;
26
27 let migration_sql = include_str!("../migrations/0001_initial.sql");
28
29 // Split on statement boundaries and execute each statement separately,
30 // since SQLx's `execute` does not support multiple statements in one call.
31 for stmt in migration_sql.split(';') {
32 // Strip leading comment lines and whitespace from each segment, then
33 // execute only non-empty segments. A segment that is entirely comments
34 // (e.g. the preamble before the first real statement) is silently
35 // skipped; a segment that starts with comments but contains SQL is
36 // executed with the comments stripped.
37 let trimmed: String = stmt
38 .lines()
39 .filter(|l| !l.trim_start().starts_with("--"))
40 .collect::<Vec<_>>()
41 .join("\n");
42 let trimmed = trimmed.trim().to_owned();
43 if trimmed.is_empty() {
44 continue;
45 }
46 // Safety: SQL comes from our own static migration files, not user input.
47 sqlx::query(sqlx::AssertSqlSafe(trimmed.as_str()))
48 .execute(pool)
49 .await?;
50 }
51
52 Ok(())
53}