Skip to main content

atomr_persistence_sql/
dialect.rs

1//! URL scheme detection for the supported SQL dialects.
2
3use crate::config::SqlDialect;
4
5pub fn detect_dialect(url: &str) -> Option<SqlDialect> {
6    let lower = url.to_ascii_lowercase();
7    if lower.starts_with("sqlite:") {
8        Some(SqlDialect::Sqlite)
9    } else if lower.starts_with("postgres:") || lower.starts_with("postgresql:") {
10        Some(SqlDialect::Postgres)
11    } else if lower.starts_with("mysql:") || lower.starts_with("mariadb:") {
12        Some(SqlDialect::MySql)
13    } else if lower.starts_with("mssql:") || lower.starts_with("sqlserver:") {
14        Some(SqlDialect::MsSql)
15    } else {
16        None
17    }
18}
19
20pub(crate) fn sqlite_migration() -> &'static str {
21    include_str!("../migrations/sqlite/001_init.sql")
22}
23
24pub(crate) fn postgres_migration() -> &'static str {
25    include_str!("../migrations/postgres/001_init.sql")
26}
27
28pub(crate) fn mysql_migration() -> &'static str {
29    include_str!("../migrations/mysql/001_init.sql")
30}
31
32pub(crate) fn mssql_migration() -> &'static str {
33    include_str!("../migrations/mssql/001_init.sql")
34}
35
36pub(crate) fn migration_for(dialect: SqlDialect) -> &'static str {
37    match dialect {
38        SqlDialect::Sqlite => sqlite_migration(),
39        SqlDialect::Postgres => postgres_migration(),
40        SqlDialect::MySql => mysql_migration(),
41        SqlDialect::MsSql => mssql_migration(),
42    }
43}
44
45/// Step `002_worm.sql` — additive WORM hash-chain + bitemporal columns
46/// (FR-9 / FR-8). Applied after [`migration_for`] as a second statement
47/// batch by [`crate::schema::ensure_schema`].
48pub(crate) fn worm_migration_for(dialect: SqlDialect) -> &'static str {
49    match dialect {
50        SqlDialect::Sqlite => include_str!("../migrations/sqlite/002_worm.sql"),
51        SqlDialect::Postgres => include_str!("../migrations/postgres/002_worm.sql"),
52        SqlDialect::MySql => include_str!("../migrations/mysql/002_worm.sql"),
53        SqlDialect::MsSql => include_str!("../migrations/mssql/002_worm.sql"),
54    }
55}
56
57/// DDL that enforces WORM `deny_update_delete` for the given dialect.
58///
59/// Only SQLite is enforced (and tested) in-process via a `RAISE(ABORT)`
60/// trigger. Other dialects return an empty string here because their
61/// enforcement requires elevated privileges (REVOKE / DENY) or DDL that the
62/// embedded migration runner cannot portably execute; those are documented
63/// inline in each `002_worm.sql` for operators to apply out-of-band.
64pub(crate) fn worm_deny_trigger_for(dialect: SqlDialect) -> &'static str {
65    match dialect {
66        // Trigger bodies contain inner `;` (after SELECT), so they are
67        // returned as a single string split on the `@@` sentinel by the
68        // installer rather than by the naive `;` splitter.
69        SqlDialect::Sqlite => concat!(
70            "CREATE TRIGGER IF NOT EXISTS event_journal_worm_no_update ",
71            "BEFORE UPDATE ON event_journal BEGIN SELECT RAISE(ABORT, 'WORM: event_journal is append-only'); END",
72            "@@",
73            "CREATE TRIGGER IF NOT EXISTS event_journal_worm_no_delete ",
74            "BEFORE DELETE ON event_journal BEGIN SELECT RAISE(ABORT, 'WORM: event_journal is append-only'); END",
75        ),
76        // Documented in each dialect's 002_worm.sql; applied out-of-band.
77        SqlDialect::Postgres | SqlDialect::MySql | SqlDialect::MsSql => "",
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn detects_all_schemes() {
87        assert_eq!(detect_dialect("sqlite::memory:"), Some(SqlDialect::Sqlite));
88        assert_eq!(detect_dialect("postgres://a"), Some(SqlDialect::Postgres));
89        assert_eq!(detect_dialect("postgresql://a"), Some(SqlDialect::Postgres));
90        assert_eq!(detect_dialect("mysql://a"), Some(SqlDialect::MySql));
91        assert_eq!(detect_dialect("mssql://a"), Some(SqlDialect::MsSql));
92        assert_eq!(detect_dialect("https://x"), None);
93    }
94
95    #[test]
96    fn migrations_embedded() {
97        assert!(migration_for(SqlDialect::Sqlite).contains("event_journal"));
98        assert!(migration_for(SqlDialect::Postgres).contains("event_journal"));
99        assert!(migration_for(SqlDialect::MySql).contains("event_journal"));
100    }
101}