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            // SQLite has one binary type and no length enforcement — affinity
49            // again, so 32 bytes is a Rust-side rule here (`M3.41`).
50            ColTy::Digest => "BLOB",
51        }
52        .to_owned()
53    }
54
55    fn quote(&self, identifier: &str) -> String {
56        format!("\"{}\"", identifier.replace('"', "\"\""))
57    }
58
59    fn placeholder(&self) -> Placeholder {
60        Placeholder::Question
61    }
62
63    fn append_only_sql(&self, table: &Table) -> Vec<String> {
64        // SQLite has triggers, so the guarantee lives in the database rather
65        // than in application code — where it would end the first time somebody
66        // opened the file with the `sqlite3` CLI.
67        let name = self.quote(table.name);
68        ["UPDATE", "DELETE"]
69            .into_iter()
70            .map(|op| {
71                format!(
72                    "CREATE TRIGGER IF NOT EXISTS {} BEFORE {op} ON {name} BEGIN \
73                     SELECT RAISE(ABORT, '{} is append-only (openEHR V8.10)'); END",
74                    self.quote(&format!("trg_{}_no_{}", table.name, op.to_lowercase())),
75                    table.name
76                )
77            })
78            .collect()
79    }
80}