Skip to main content

openehr_sqlite/
dialect.rs

1//! The `SQLite` dialect.
2
3use openehr_store::schema::Table;
4use openehr_store::{ColTy, Dialect, Placeholder};
5
6/// The `SQLite` dialect.
7///
8/// ```
9/// use openehr_sqlite::SqliteDialect;
10/// use openehr_store::{ColTy, Dialect};
11///
12/// // `SQLite` has one string type and one integer type, so the *distinction*
13/// // between an authoritative instant and its derived partner has to be
14/// // carried by the choice of storage class rather than by two date types.
15/// assert_eq!(SqliteDialect.col_sql(ColTy::Instant), "TEXT");
16/// assert_eq!(SqliteDialect.col_sql(ColTy::InstantUtc), "INTEGER");
17/// ```
18#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
19pub struct SqliteDialect;
20
21impl Dialect for SqliteDialect {
22    fn name(&self) -> &'static str {
23        "SQLite"
24    }
25
26    // The arms genuinely coincide: `SQLite` has one string storage class and one
27    // integer one, so several logical types must map to the same SQL. Merging
28    // them into one arm would lose which logical types this dialect was asked
29    // about, which is what the next reader needs.
30    #[allow(clippy::match_same_arms)]
31    fn col_sql(&self, ty: ColTy) -> String {
32        match ty {
33            // SQLite's declared types are advisory — it applies type affinity
34            // rather than enforcement — so lengths would be documentation that
35            // nothing checks. Better to say TEXT and mean it.
36            ColTy::Id(_) | ColTy::Text(_) | ColTy::LongText | ColTy::Instant => "TEXT",
37            // No JSON type. The JSON1 extension operates on TEXT, so TEXT is
38            // both the honest declaration and the working one.
39            ColTy::Json => "TEXT",
40            // Seconds from the Unix epoch, not a string. SQLite has no date
41            // type at all, and storing the derived instant as text would make
42            // it sort identically to the authoritative column — collapsing the
43            // very distinction the two columns exist to keep (`D3.10`).
44            ColTy::InstantUtc | ColTy::Int => "INTEGER",
45            // SQLite has no boolean; 0 and 1 in an INTEGER column is the
46            // documented convention.
47            ColTy::Bool => "INTEGER",
48        }
49        .to_owned()
50    }
51
52    fn quote(&self, identifier: &str) -> String {
53        format!("\"{}\"", identifier.replace('"', "\"\""))
54    }
55
56    fn placeholder(&self) -> Placeholder {
57        Placeholder::Question
58    }
59
60    fn append_only_sql(&self, table: &Table) -> Vec<String> {
61        // SQLite has triggers, so the guarantee lives in the database rather
62        // than in application code — where it would end the first time somebody
63        // opened the file with the `sqlite3` CLI.
64        let name = self.quote(table.name);
65        ["UPDATE", "DELETE"]
66            .into_iter()
67            .map(|op| {
68                format!(
69                    "CREATE TRIGGER IF NOT EXISTS {} BEFORE {op} ON {name} BEGIN \
70                     SELECT RAISE(ABORT, '{} is append-only (openEHR V8.10)'); END",
71                    self.quote(&format!("trg_{}_no_{}", table.name, op.to_lowercase())),
72                    table.name
73                )
74            })
75            .collect()
76    }
77}