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 let tx = conn
162 .unchecked_transaction()
163 .map_err(|err| DbError::Batch(err.to_string()))?;
164 tx.execute_batch(migration.sql)
165 .map_err(|err| DbError::Batch(err.to_string()))?;
166 tx.execute(
167 "INSERT INTO harness_migrations (id, applied_at, checksum) VALUES (?1, ?2, ?3)",
168 rusqlite::params![key, iso_now(), checksum],
169 )
170 .map_err(|err| DbError::Batch(err.to_string()))?;
171 tx.commit().map_err(|err| DbError::Batch(err.to_string()))?;
172 }
173 Ok(())
174 }
175}
176
177fn iso_now() -> String {
178 time::OffsetDateTime::now_utc()
179 .format(&time::format_description::well_known::Rfc3339)
180 .unwrap_or_default()
181}
182
183#[async_trait]
184impl Database for SqliteDatabase {
185 async fn execute(&self, stmt: &Statement) -> Result<u64, DbError> {
186 let conn = self.conn.lock().expect("sqlite lock uncontended");
187 let params: Vec<SqliteValue> = stmt.values.0.iter().map(sea_to_sqlite).collect();
188 let changed = conn
189 .execute(&stmt.sql, rusqlite::params_from_iter(params))
190 .map_err(|err| DbError::Execute(err.to_string()))?;
191 Ok(u64::try_from(changed).unwrap_or(u64::MAX))
192 }
193
194 async fn query(&self, stmt: &Statement) -> Result<Rows, DbError> {
195 let conn = self.conn.lock().expect("sqlite lock uncontended");
196 let params: Vec<SqliteValue> = stmt.values.0.iter().map(sea_to_sqlite).collect();
197 let mut prepared = conn
198 .prepare(&stmt.sql)
199 .map_err(|err| DbError::Query(err.to_string()))?;
200 let column_names: Vec<String> = prepared
201 .column_names()
202 .into_iter()
203 .map(str::to_owned)
204 .collect();
205 let mut rows = prepared
206 .query(rusqlite::params_from_iter(params))
207 .map_err(|err| DbError::Query(err.to_string()))?;
208 let mut out = Vec::new();
209 while let Some(row) = rows.next().map_err(|err| db_err(&err))? {
210 let mut columns = Vec::with_capacity(column_names.len());
211 for (index, name) in column_names.iter().enumerate() {
212 let value: SqliteValue = row.get(index).map_err(|err| db_err(&err))?;
213 columns.push((name.clone(), sqlite_to_sea(value)));
214 }
215 out.push(Row::new(columns));
216 }
217 Ok(Rows::new(out))
218 }
219
220 async fn batch(&self, stmts: &[Statement]) -> Result<(), DbError> {
222 let conn = self.conn.lock().expect("sqlite lock uncontended");
223 let tx = conn
224 .unchecked_transaction()
225 .map_err(|err| DbError::Batch(err.to_string()))?;
226 for stmt in stmts {
227 let params: Vec<SqliteValue> = stmt.values.0.iter().map(sea_to_sqlite).collect();
228 tx.execute(&stmt.sql, rusqlite::params_from_iter(params))
229 .map_err(|err| DbError::Batch(err.to_string()))?;
230 }
231 tx.commit().map_err(|err| DbError::Batch(err.to_string()))?;
232 Ok(())
233 }
234}