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}