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}