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 SqliteValue::Integer(i64::try_from(v).unwrap_or(i64::MAX))
50 }),
51 SeaValue::Float(v) => v.map_or(null, |v| SqliteValue::Real(f64::from(v))),
52 SeaValue::Double(v) => v.map_or(null, SqliteValue::Real),
53 SeaValue::String(v) => v
54 .as_ref()
55 .map_or(null, |v| SqliteValue::Text(v.to_string())),
56 SeaValue::Char(v) => v.map_or(null, |v| SqliteValue::Text(v.to_string())),
57 SeaValue::Bytes(v) => v
58 .as_ref()
59 .map_or(null, |v| SqliteValue::Blob((**v).clone())),
60 }
61}
62
63fn sqlite_to_sea(value: SqliteValue) -> SeaValue {
64 match value {
65 SqliteValue::Null => SeaValue::String(None),
66 SqliteValue::Integer(v) => SeaValue::BigInt(Some(v)),
67 SqliteValue::Real(v) => SeaValue::Double(Some(v)),
68 SqliteValue::Text(v) => SeaValue::String(Some(Box::new(v))),
69 SqliteValue::Blob(v) => SeaValue::Bytes(Some(Box::new(v))),
70 }
71}
72
73impl SqliteDatabase {
74 pub fn open(path: &str) -> rusqlite::Result<Self> {
80 Ok(Self {
81 conn: Guarded::new(Connection::open(path)?),
82 })
83 }
84
85 pub fn in_memory() -> rusqlite::Result<Self> {
91 Ok(Self {
92 conn: Guarded::new(Connection::open_in_memory()?),
93 })
94 }
95}
96
97impl SqliteDatabase {
98 pub fn apply_migrations(
111 &self,
112 module: &str,
113 migrations: &[SqlMigration],
114 ) -> Result<(), DbError> {
115 let conn = self.conn.lock().expect("sqlite lock uncontended");
116 conn.execute_batch(
117 "CREATE TABLE IF NOT EXISTS harness_migrations (
118 id TEXT PRIMARY KEY,
119 applied_at TEXT NOT NULL,
120 checksum TEXT
121 );",
122 )
123 .map_err(|err| DbError::Batch(err.to_string()))?;
124 let has_checksum = conn
129 .query_row(
130 "SELECT COUNT(*) FROM pragma_table_info('harness_migrations') \
131 WHERE name = 'checksum'",
132 [],
133 |row| row.get::<_, i64>(0),
134 )
135 .map(|count| count > 0)
136 .map_err(|err| db_err(&err))?;
137 if !has_checksum {
138 conn.execute_batch("ALTER TABLE harness_migrations ADD COLUMN checksum TEXT;")
139 .map_err(|err| DbError::Batch(err.to_string()))?;
140 }
141
142 for migration in migrations {
143 let key = format!("{module}/{}", migration.id);
144 let checksum = cratefield_core::migration_checksum(migration.sql);
145 let recorded: Option<Option<String>> = conn
146 .query_row(
147 "SELECT checksum FROM harness_migrations WHERE id = ?1",
148 [&key],
149 |row| row.get::<_, Option<String>>(0),
150 )
151 .optional()
152 .map_err(|err| db_err(&err))?;
153 if let Some(recorded) = recorded {
154 if let Some(recorded) = recorded.filter(|hash| hash != &checksum) {
155 return Err(DbError::Batch(cratefield_core::migration_edited(
156 &key, &recorded, &checksum,
157 )));
158 }
159 continue;
160 }
161 if !migration.transactional {
162 if !cratefield_core::is_idempotent_sql(migration.sql) {
166 return Err(DbError::Batch(cratefield_core::migration_missing_guard(
167 &key,
168 )));
169 }
170 conn.execute_batch(migration.sql)
171 .map_err(|err| DbError::Batch(err.to_string()))?;
172 conn.execute(
173 "INSERT INTO harness_migrations (id, applied_at, checksum) VALUES (?1, ?2, ?3)",
174 rusqlite::params![key, iso_now(), checksum],
175 )
176 .map_err(|err| DbError::Batch(err.to_string()))?;
177 continue;
178 }
179 let tx = conn
180 .unchecked_transaction()
181 .map_err(|err| DbError::Batch(err.to_string()))?;
182 tx.execute_batch(migration.sql)
183 .map_err(|err| DbError::Batch(err.to_string()))?;
184 tx.execute(
185 "INSERT INTO harness_migrations (id, applied_at, checksum) VALUES (?1, ?2, ?3)",
186 rusqlite::params![key, iso_now(), checksum],
187 )
188 .map_err(|err| DbError::Batch(err.to_string()))?;
189 tx.commit().map_err(|err| DbError::Batch(err.to_string()))?;
190 }
191 Ok(())
192 }
193}
194
195fn iso_now() -> String {
196 time::OffsetDateTime::now_utc()
197 .format(&time::format_description::well_known::Rfc3339)
198 .unwrap_or_default()
199}
200
201#[async_trait]
202impl Database for SqliteDatabase {
203 async fn execute(&self, stmt: &Statement) -> Result<u64, DbError> {
204 let conn = self.conn.lock().expect("sqlite lock uncontended");
205 let params: Vec<SqliteValue> = stmt.values.0.iter().map(sea_to_sqlite).collect();
206 let changed = conn
207 .execute(&stmt.sql, rusqlite::params_from_iter(params))
208 .map_err(|err| DbError::Execute(err.to_string()))?;
209 Ok(u64::try_from(changed).unwrap_or(u64::MAX))
210 }
211
212 async fn query(&self, stmt: &Statement) -> Result<Rows, DbError> {
213 let conn = self.conn.lock().expect("sqlite lock uncontended");
214 let params: Vec<SqliteValue> = stmt.values.0.iter().map(sea_to_sqlite).collect();
215 let mut prepared = conn
216 .prepare(&stmt.sql)
217 .map_err(|err| DbError::Query(err.to_string()))?;
218 let column_names: Vec<String> = prepared
219 .column_names()
220 .into_iter()
221 .map(str::to_owned)
222 .collect();
223 let mut rows = prepared
224 .query(rusqlite::params_from_iter(params))
225 .map_err(|err| DbError::Query(err.to_string()))?;
226 let mut out = Vec::new();
227 while let Some(row) = rows.next().map_err(|err| db_err(&err))? {
228 let mut columns = Vec::with_capacity(column_names.len());
229 for (index, name) in column_names.iter().enumerate() {
230 let value: SqliteValue = row.get(index).map_err(|err| db_err(&err))?;
231 columns.push((name.clone(), sqlite_to_sea(value)));
232 }
233 out.push(Row::new(columns));
234 }
235 Ok(Rows::new(out))
236 }
237
238 async fn batch_atomic(&self, stmts: &[Statement]) -> Result<(), DbError> {
240 let conn = self.conn.lock().expect("sqlite lock uncontended");
241 let tx = conn
242 .unchecked_transaction()
243 .map_err(|err| DbError::Batch(err.to_string()))?;
244 for stmt in stmts {
245 let params: Vec<SqliteValue> = stmt.values.0.iter().map(sea_to_sqlite).collect();
246 tx.execute(&stmt.sql, rusqlite::params_from_iter(params))
247 .map_err(|err| DbError::Batch(err.to_string()))?;
248 }
249 tx.commit().map_err(|err| DbError::Batch(err.to_string()))?;
250 Ok(())
251 }
252}