cratefield_adapter_sqlite/
lib.rs1#![doc = include_str!("../README.md")]
13#![forbid(unsafe_code)]
14
15use async_trait::async_trait;
16use cratefield_core::{Database, DbError, Row, Rows, SqlMigration, Statement};
17use rusqlite::types::Value as SqliteValue;
18use rusqlite::{Connection, OptionalExtension};
19use sea_query::Value as SeaValue;
20#[allow(clippy::disallowed_types)]
22type Guarded<T> = std::sync::Mutex<T>;
23
24pub struct SqliteDatabase {
26 #[allow(clippy::disallowed_types)]
28 conn: Guarded<Connection>,
29}
30
31fn db_err(err: &rusqlite::Error) -> DbError {
32 DbError::Query(err.to_string())
33}
34
35fn sea_to_sqlite(value: &SeaValue) -> SqliteValue {
36 let null = SqliteValue::Null;
37 match value {
38 SeaValue::Bool(v) => v.map_or(null, |v| SqliteValue::Integer(i64::from(v))),
39 SeaValue::TinyInt(v) => v.map_or(null, |v| SqliteValue::Integer(i64::from(v))),
40 SeaValue::SmallInt(v) => v.map_or(null, |v| SqliteValue::Integer(i64::from(v))),
41 SeaValue::Int(v) => v.map_or(null, |v| SqliteValue::Integer(i64::from(v))),
42 SeaValue::BigInt(v) => v.map_or(null, SqliteValue::Integer),
43 SeaValue::TinyUnsigned(v) => v.map_or(null, |v| SqliteValue::Integer(i64::from(v))),
46 SeaValue::SmallUnsigned(v) => v.map_or(null, |v| SqliteValue::Integer(i64::from(v))),
47 SeaValue::Unsigned(v) => v.map_or(null, |v| SqliteValue::Integer(i64::from(v))),
48 SeaValue::BigUnsigned(v) => v.map_or(null, |v| {
49 if let Ok(i) = i64::try_from(v) {
50 SqliteValue::Integer(i)
51 } else {
52 tracing::error!("BigUnsigned overflow: {} exceeds i64::MAX", v);
53 SqliteValue::Integer(i64::MAX)
54 }
55 }),
56 SeaValue::Float(v) => v.map_or(null, |v| SqliteValue::Real(f64::from(v))),
57 SeaValue::Double(v) => v.map_or(null, SqliteValue::Real),
58 SeaValue::String(v) => v
59 .as_ref()
60 .map_or(null, |v| SqliteValue::Text(v.to_string())),
61 SeaValue::Char(v) => v.map_or(null, |v| SqliteValue::Text(v.to_string())),
62 SeaValue::Bytes(v) => v
63 .as_ref()
64 .map_or(null, |v| SqliteValue::Blob((**v).clone())),
65 }
66}
67
68fn sqlite_to_sea(value: SqliteValue) -> SeaValue {
69 match value {
70 SqliteValue::Null => SeaValue::String(None),
71 SqliteValue::Integer(v) => SeaValue::BigInt(Some(v)),
72 SqliteValue::Real(v) => SeaValue::Double(Some(v)),
73 SqliteValue::Text(v) => SeaValue::String(Some(Box::new(v))),
74 SqliteValue::Blob(v) => SeaValue::Bytes(Some(Box::new(v))),
75 }
76}
77
78impl SqliteDatabase {
79 pub fn open(path: &str) -> rusqlite::Result<Self> {
85 Ok(Self {
86 conn: Guarded::new(Connection::open(path)?),
87 })
88 }
89
90 pub fn in_memory() -> rusqlite::Result<Self> {
96 Ok(Self {
97 conn: Guarded::new(Connection::open_in_memory()?),
98 })
99 }
100}
101
102impl SqliteDatabase {
103 pub fn apply_migrations(
116 &self,
117 module: &str,
118 migrations: &[SqlMigration],
119 ) -> Result<(), DbError> {
120 let conn = self.conn.lock().expect("sqlite lock uncontended");
121 conn.execute_batch(
122 "CREATE TABLE IF NOT EXISTS harness_migrations (
123 id TEXT PRIMARY KEY,
124 applied_at TEXT NOT NULL,
125 checksum TEXT
126 );",
127 )
128 .map_err(|err| DbError::Batch(err.to_string()))?;
129 let has_checksum = conn
134 .query_row(
135 "SELECT COUNT(*) FROM pragma_table_info('harness_migrations') \
136 WHERE name = 'checksum'",
137 [],
138 |row| row.get::<_, i64>(0),
139 )
140 .map(|count| count > 0)
141 .map_err(|err| db_err(&err))?;
142 if !has_checksum {
143 conn.execute_batch("ALTER TABLE harness_migrations ADD COLUMN checksum TEXT;")
144 .map_err(|err| DbError::Batch(err.to_string()))?;
145 }
146
147 for migration in migrations {
148 let key = format!("{module}/{}", migration.id);
149 let checksum = cratefield_core::migration_checksum(migration.sql);
150 let recorded: Option<Option<String>> = conn
151 .query_row(
152 "SELECT checksum FROM harness_migrations WHERE id = ?1",
153 [&key],
154 |row| row.get::<_, Option<String>>(0),
155 )
156 .optional()
157 .map_err(|err| db_err(&err))?;
158 if let Some(recorded) = recorded {
159 if let Some(recorded) = recorded.filter(|hash| hash != &checksum) {
160 return Err(DbError::Batch(cratefield_core::migration_edited(
161 &key, &recorded, &checksum,
162 )));
163 }
164 continue;
165 }
166 if !migration.transactional {
167 if !cratefield_core::is_idempotent_sql(migration.sql) {
171 return Err(DbError::Batch(cratefield_core::migration_missing_guard(
172 &key,
173 )));
174 }
175 conn.execute_batch(migration.sql)
176 .map_err(|err| DbError::Batch(err.to_string()))?;
177 conn.execute(
178 "INSERT INTO harness_migrations (id, applied_at, checksum) VALUES (?1, ?2, ?3)",
179 rusqlite::params![key, iso_now(), checksum],
180 )
181 .map_err(|err| DbError::Batch(err.to_string()))?;
182 continue;
183 }
184 let tx = conn
185 .unchecked_transaction()
186 .map_err(|err| DbError::Batch(err.to_string()))?;
187 tx.execute_batch(migration.sql)
188 .map_err(|err| DbError::Batch(err.to_string()))?;
189 tx.execute(
190 "INSERT INTO harness_migrations (id, applied_at, checksum) VALUES (?1, ?2, ?3)",
191 rusqlite::params![key, iso_now(), checksum],
192 )
193 .map_err(|err| DbError::Batch(err.to_string()))?;
194 tx.commit().map_err(|err| DbError::Batch(err.to_string()))?;
195 }
196 Ok(())
197 }
198}
199
200fn iso_now() -> String {
201 time::OffsetDateTime::now_utc()
202 .format(&time::format_description::well_known::Rfc3339)
203 .unwrap_or_default()
204}
205
206#[async_trait]
207impl Database for SqliteDatabase {
208 async fn execute(&self, stmt: &Statement) -> Result<u64, DbError> {
209 let conn = self.conn.lock().expect("sqlite lock uncontended");
210 let params: Vec<SqliteValue> = stmt.values.0.iter().map(sea_to_sqlite).collect();
211 let changed = conn
212 .execute(&stmt.sql, rusqlite::params_from_iter(params))
213 .map_err(|err| DbError::Execute(err.to_string()))?;
214 Ok(u64::try_from(changed).unwrap_or(u64::MAX))
215 }
216
217 async fn query(&self, stmt: &Statement) -> Result<Rows, DbError> {
218 let conn = self.conn.lock().expect("sqlite lock uncontended");
219 let params: Vec<SqliteValue> = stmt.values.0.iter().map(sea_to_sqlite).collect();
220 let mut prepared = conn
221 .prepare(&stmt.sql)
222 .map_err(|err| DbError::Query(err.to_string()))?;
223 let column_names: Vec<String> = prepared
224 .column_names()
225 .into_iter()
226 .map(str::to_owned)
227 .collect();
228 let mut rows = prepared
229 .query(rusqlite::params_from_iter(params))
230 .map_err(|err| DbError::Query(err.to_string()))?;
231 let mut out = Vec::new();
232 while let Some(row) = rows.next().map_err(|err| db_err(&err))? {
233 let mut columns = Vec::with_capacity(column_names.len());
234 for (index, name) in column_names.iter().enumerate() {
235 let value: SqliteValue = row.get(index).map_err(|err| db_err(&err))?;
236 columns.push((name.clone(), sqlite_to_sea(value)));
237 }
238 out.push(Row::new(columns));
239 }
240 Ok(Rows::new(out))
241 }
242
243 async fn batch_atomic(&self, stmts: &[Statement]) -> Result<(), DbError> {
245 let conn = self.conn.lock().expect("sqlite lock uncontended");
246 let tx = conn
247 .unchecked_transaction()
248 .map_err(|err| DbError::Batch(err.to_string()))?;
249 for stmt in stmts {
250 let params: Vec<SqliteValue> = stmt.values.0.iter().map(sea_to_sqlite).collect();
251 tx.execute(&stmt.sql, rusqlite::params_from_iter(params))
252 .map_err(|err| DbError::Batch(err.to_string()))?;
253 }
254 tx.commit().map_err(|err| DbError::Batch(err.to_string()))?;
255 Ok(())
256 }
257}