Skip to main content

eventuary_sqlite/
schema.rs

1use rusqlite::Connection;
2
3use eventuary_core::{Error, Result};
4
5use crate::relation::SqliteRelationName;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub struct Migration {
9    pub name: &'static str,
10    pub sql: &'static str,
11}
12
13#[derive(Clone, Copy, Debug)]
14pub struct RelationReplacement<'a> {
15    pub token: &'static str,
16    pub relation: &'a SqliteRelationName,
17}
18
19pub fn render_migration_sql(
20    migration: &Migration,
21    replacements: &[RelationReplacement<'_>],
22) -> String {
23    let mut sql = migration.sql.to_owned();
24    for replacement in replacements {
25        sql = sql.replace(replacement.token, &replacement.relation.render());
26    }
27    sql
28}
29
30pub fn render_schema_sql(
31    migrations: &[Migration],
32    replacements: &[RelationReplacement<'_>],
33) -> String {
34    let mut sql = String::new();
35    for migration in migrations {
36        sql.push_str(&render_migration_sql(migration, replacements));
37        if !sql.ends_with('\n') {
38            sql.push('\n');
39        }
40    }
41    sql
42}
43
44pub fn apply_schema(
45    conn: &Connection,
46    migrations: &[Migration],
47    replacements: &[RelationReplacement<'_>],
48) -> Result<()> {
49    for migration in migrations {
50        let sql = render_migration_sql(migration, replacements);
51        for statement in sql.split(';').map(str::trim).filter(|s| !s.is_empty()) {
52            let result = conn.execute(statement, []);
53            match result {
54                Ok(_) => {}
55                Err(ref e) if is_duplicate_add_column_error(statement, e) => {}
56                Err(e) => return Err(Error::Store(format!("apply {}: {e}", migration.name))),
57            }
58        }
59    }
60    Ok(())
61}
62
63fn is_duplicate_add_column_error(statement: &str, e: &rusqlite::Error) -> bool {
64    is_add_column_statement(statement) && e.to_string().contains("duplicate column name")
65}
66
67fn is_add_column_statement(statement: &str) -> bool {
68    let normalized = statement
69        .split_whitespace()
70        .collect::<Vec<_>>()
71        .join(" ")
72        .to_ascii_uppercase();
73    normalized.starts_with("ALTER TABLE ") && normalized.contains(" ADD COLUMN ")
74}