eventuary_sqlite/
schema.rs1use 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}