architect_sdk/db/
sqlite.rs1use super::dialect::Dialect;
7use super::types::{CanonicalType, TypeCategory, TypeSupport};
8
9pub struct SqliteDialect;
10
11fn sqlite_cast(t: &CanonicalType) -> Option<&'static str> {
15 match t {
16 CanonicalType::SmallInt
17 | CanonicalType::Int
18 | CanonicalType::BigInt
19 | CanonicalType::Serial
20 | CanonicalType::BigSerial
21 | CanonicalType::Boolean => Some("INTEGER"),
22 CanonicalType::Real | CanonicalType::Double | CanonicalType::Decimal(_) => Some("REAL"),
23 _ => None,
24 }
25}
26
27impl Dialect for SqliteDialect {
28 fn name(&self) -> &'static str {
29 "sqlite"
30 }
31
32 fn ddl_type(&self, t: &CanonicalType) -> String {
33 match t {
34 CanonicalType::Text
35 | CanonicalType::Varchar(_)
36 | CanonicalType::Char(_)
37 | CanonicalType::Uuid | CanonicalType::Asset => "TEXT".to_string(),
39
40 CanonicalType::SmallInt | CanonicalType::Int | CanonicalType::BigInt => {
41 "INTEGER".to_string()
42 }
43 CanonicalType::Serial | CanonicalType::BigSerial => "INTEGER".to_string(),
45
46 CanonicalType::Real | CanonicalType::Double => "REAL".to_string(),
47 CanonicalType::Decimal(_) => "NUMERIC".to_string(),
48 CanonicalType::Boolean => "INTEGER".to_string(), CanonicalType::Json | CanonicalType::Jsonb => "TEXT".to_string(),
50 CanonicalType::Timestamp | CanonicalType::TimestampNtz => "TEXT".to_string(),
51 CanonicalType::Date => "TEXT".to_string(),
52 CanonicalType::Time | CanonicalType::Timetz => "TEXT".to_string(),
53 CanonicalType::Bytes => "BLOB".to_string(),
54 CanonicalType::AssetArray | CanonicalType::Array(_) => "TEXT".to_string(),
55 CanonicalType::Custom(s) => s.clone(),
56 }
57 }
58
59 fn cast_name(&self, _t: &CanonicalType) -> Option<String> {
60 None
61 }
62
63 fn type_category(&self, t: &CanonicalType) -> TypeCategory {
64 super::types::type_category(t)
65 }
66
67 fn type_support(&self, t: &CanonicalType) -> TypeSupport {
68 match t {
69 CanonicalType::Jsonb => {
70 TypeSupport::Degraded("TEXT", "JSONB not available on SQLite; using TEXT")
71 }
72 CanonicalType::Timetz => TypeSupport::Degraded(
73 "TEXT",
74 "SQLite has no TIME WITH TIME ZONE; storing as ISO-8601 TEXT",
75 ),
76 CanonicalType::Array(_) => TypeSupport::Degraded(
77 "TEXT",
78 "SQLite has no native array type; stored as JSON TEXT",
79 ),
80 CanonicalType::Asset => TypeSupport::Emulated("TEXT"),
81 CanonicalType::AssetArray => TypeSupport::Emulated("TEXT"),
82 _ => TypeSupport::Native(self.ddl_type(t).leak()),
83 }
84 }
85
86 fn quote_ident(&self, s: &str) -> String {
87 format!("\"{}\"", s.replace('"', "\"\""))
88 }
89
90 fn placeholder(&self, _n: usize) -> String {
91 "?".to_string()
92 }
93
94 fn cast_expr(&self, placeholder: &str, _cast: &str) -> String {
95 placeholder.to_string()
96 }
97
98 fn now_fn(&self) -> &'static str {
99 "CURRENT_TIMESTAMP"
102 }
103
104 fn uuid_default_expr(&self) -> &'static str {
105 "lower(hex(randomblob(4)))||'-'||lower(hex(randomblob(2)))||'-4'||\
107 substr(lower(hex(randomblob(2))),2)||'-'||\
108 substr('89ab',abs(random())%4+1,1)||\
109 substr(lower(hex(randomblob(2))),2)||'-'||lower(hex(randomblob(6)))"
110 }
111
112 fn returning_clause(&self, cols: &str) -> String {
113 format!("RETURNING {}", cols)
114 }
115
116 fn upsert_conflict(&self, conflict_cols: &[&str], set_pairs: &str) -> String {
117 let cols = conflict_cols
118 .iter()
119 .map(|c| self.quote_ident(c))
120 .collect::<Vec<_>>()
121 .join(", ");
122 format!("ON CONFLICT ({}) DO UPDATE SET {}", cols, set_pairs)
123 }
124
125 fn to_one_subquery(&self, col_exprs: &[String], from_clause: &str) -> String {
126 let pairs = col_exprs
127 .iter()
128 .map(|c| format!("'{}', {}", c.trim_matches('"'), c))
129 .collect::<Vec<_>>()
130 .join(", ");
131 format!(
132 "(SELECT json_object({}) FROM {} LIMIT 1)",
133 pairs, from_clause
134 )
135 }
136
137 fn to_many_subquery(&self, col_exprs: &[String], from_clause: &str) -> String {
138 let pairs = col_exprs
139 .iter()
140 .map(|c| format!("'{}', {}", c.trim_matches('"'), c))
141 .collect::<Vec<_>>()
142 .join(", ");
143 format!(
144 "(SELECT COALESCE(json_group_array(json_object({})), '[]') FROM {})",
145 pairs, from_clause
146 )
147 }
148
149 fn json_extract_text(&self, col: &str, key: &str) -> String {
150 format!("{}->>'$.{}'", col, key.replace('\'', "''"))
151 }
152
153 fn json_extract_typed(&self, col: &str, key: &str, t: &CanonicalType) -> String {
154 let base = self.json_extract_text(col, key);
155 match sqlite_cast(t) {
156 Some(cast) => format!("CAST({} AS {})", base, cast),
157 None => base,
158 }
159 }
160
161 fn case_insensitive_like(&self, col: &str, placeholder: &str) -> String {
162 format!("{} LIKE {}", col, placeholder)
164 }
165
166 fn sys_json_type(&self) -> &'static str {
167 "TEXT"
168 }
169
170 fn sys_timestamp_type(&self) -> &'static str {
171 "TEXT"
172 }
173
174 fn sys_bigserial_type(&self) -> &'static str {
175 "INTEGER"
176 }
177
178 fn sys_bytes_type(&self) -> &'static str {
179 "BLOB"
180 }
181
182 fn audit_timestamp_type(&self) -> &'static str {
183 "TEXT"
184 }
185
186 fn supports_schemas(&self) -> bool {
187 false
188 }
189
190 fn default_now_plus_hours(&self, _hours: u32) -> Option<String> {
191 None
193 }
194
195 fn supports_rls(&self) -> bool {
196 false
197 }
198
199 fn supports_named_enum_types(&self) -> bool {
200 false
201 }
202
203 fn supports_index_include(&self) -> bool {
204 false
205 }
206
207 fn set_tenant_session_sql(&self, _tenant_id: &str) -> Option<String> {
208 None
209 }
210}