1#![warn(missing_docs)]
2
3mod value;
21pub(crate) use value::Value;
22
23use async_trait::async_trait;
24use rusqlite::Connection as RusqliteConnection;
25use std::{
26 borrow::Cow,
27 path::{Path, PathBuf},
28 sync::Arc,
29};
30use toasty_core::{
31 Result, Schema,
32 driver::{
33 Capability, ConnectContext, Driver, ExecResponse, QueryLogConfig,
34 log::QueryLog,
35 operation::{IsolationLevel, Operation, RawSqlRet, Transaction, TypedValue},
36 },
37 schema::{
38 db::{self, Migration, Table},
39 diff,
40 },
41 stmt,
42};
43use toasty_sql::{self as sql};
44use url::Url;
45
46enum SqlReturn {
47 Count,
48 Infer,
49 Types(Vec<stmt::Type>),
50}
51
52#[derive(Debug)]
62pub enum Sqlite {
63 File(PathBuf),
65 InMemory,
67}
68
69impl Sqlite {
70 pub fn new(url: impl Into<String>) -> Result<Self> {
72 let url_str = url.into();
73 let url = Url::parse(&url_str).map_err(toasty_core::Error::driver_operation_failed)?;
74
75 if url.scheme() != "sqlite" {
76 return Err(toasty_core::Error::invalid_connection_url(format!(
77 "connection URL does not have a `sqlite` scheme; url={}",
78 url_str
79 )));
80 }
81
82 if url.path() == ":memory:" {
83 Ok(Self::InMemory)
84 } else {
85 Ok(Self::File(PathBuf::from(
86 percent_encoding::percent_decode(url.path().as_bytes())
87 .decode_utf8_lossy()
88 .to_string()
89 .as_str(),
90 )))
91 }
92 }
93
94 pub fn in_memory() -> Self {
96 Self::InMemory
97 }
98
99 pub fn open<P: AsRef<Path>>(path: P) -> Self {
101 Self::File(path.as_ref().to_path_buf())
102 }
103}
104
105#[async_trait]
106impl Driver for Sqlite {
107 fn url(&self) -> Cow<'_, str> {
108 match self {
109 Sqlite::InMemory => Cow::Borrowed("sqlite::memory:"),
110 Sqlite::File(path) => Cow::Owned(format!("sqlite:{}", path.display())),
111 }
112 }
113
114 fn capability(&self) -> &'static Capability {
115 &Capability::SQLITE
116 }
117
118 async fn connect(
119 &self,
120 cx: &ConnectContext,
121 ) -> toasty_core::Result<Box<dyn toasty_core::Connection>> {
122 let mut connection = match self {
123 Sqlite::File(path) => Connection::open(path)?,
124 Sqlite::InMemory => Connection::in_memory(),
125 };
126 connection.query_log = cx.query_log;
127 Ok(Box::new(connection))
128 }
129
130 fn max_connections(&self) -> Option<usize> {
131 matches!(self, Self::InMemory).then_some(1)
132 }
133
134 fn generate_migration(&self, schema_diff: &diff::Schema<'_>) -> Migration {
135 let statements = sql::MigrationStatement::from_diff(schema_diff, &Capability::SQLITE);
136
137 let sql_strings: Vec<String> = statements
138 .iter()
139 .map(|stmt| sql::Serializer::sqlite(stmt.schema()).serialize(stmt.statement()))
140 .collect();
141
142 Migration::new_sql_with_breakpoints(&sql_strings)
143 }
144
145 async fn reset_db(&self) -> toasty_core::Result<()> {
146 match self {
147 Sqlite::File(path) => {
148 if path.exists() {
150 std::fs::remove_file(path)
151 .map_err(toasty_core::Error::driver_operation_failed)?;
152 }
153 }
154 Sqlite::InMemory => {
155 }
157 }
158
159 Ok(())
160 }
161}
162
163#[derive(Debug)]
165pub struct Connection {
166 connection: RusqliteConnection,
167 query_log: QueryLogConfig,
168}
169
170impl Connection {
171 pub fn in_memory() -> Self {
173 let connection = RusqliteConnection::open_in_memory().unwrap();
174
175 Self {
176 connection,
177 query_log: QueryLogConfig::default(),
178 }
179 }
180
181 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
183 let connection =
184 RusqliteConnection::open(path).map_err(toasty_core::Error::driver_operation_failed)?;
185 let sqlite = Self {
186 connection,
187 query_log: QueryLogConfig::default(),
188 };
189 Ok(sqlite)
190 }
191
192 fn exec_sql(
193 &mut self,
194 sql_str: &str,
195 typed_params: Vec<TypedValue>,
196 ret: SqlReturn,
197 ) -> Result<ExecResponse> {
198 let mut log = QueryLog::sql(
199 &self.query_log,
200 "sqlite",
201 sql_str,
202 typed_params.iter().map(|tv| &tv.value),
203 );
204 let result = self.exec_sql_inner(sql_str, typed_params, ret, &mut log);
205 log.finish(&result);
206 result
207 }
208
209 fn exec_sql_inner(
210 &mut self,
211 sql_str: &str,
212 typed_params: Vec<TypedValue>,
213 ret: SqlReturn,
214 log: &mut QueryLog<'_>,
215 ) -> Result<ExecResponse> {
216 let mut stmt = self
217 .connection
218 .prepare_cached(sql_str)
219 .map_err(toasty_core::Error::driver_operation_failed)?;
220
221 let params = typed_params
222 .into_iter()
223 .map(|tv| Value::from(tv.value))
224 .collect::<Vec<_>>();
225
226 if matches!(ret, SqlReturn::Count) {
227 let count = stmt
228 .execute(rusqlite::params_from_iter(params.iter()))
229 .map_err(toasty_core::Error::driver_operation_failed)?;
230
231 return Ok(ExecResponse::count(count as _));
232 }
233
234 let mut rows = stmt
235 .query(rusqlite::params_from_iter(params.iter()))
236 .map_err(toasty_core::Error::driver_operation_failed)?;
237
238 let mut values = vec![];
239 let column_count = rows.as_ref().map(|stmt| stmt.column_count()).unwrap_or(0);
240
241 loop {
242 match rows.next() {
243 Ok(Some(row)) => {
244 let items = match &ret {
245 SqlReturn::Count => unreachable!(),
246 SqlReturn::Infer => (0..column_count)
247 .map(|index| Value::from_sql_infer(row, index).into_inner())
248 .collect(),
249 SqlReturn::Types(ret_tys) => ret_tys
250 .iter()
251 .enumerate()
252 .map(|(index, ret_ty)| Value::from_sql(row, index, ret_ty).into_inner())
253 .collect(),
254 };
255
256 values.push(stmt::ValueRecord::from_vec(items).into());
257 }
258 Ok(None) => break,
259 Err(err) => {
260 return Err(toasty_core::Error::driver_operation_failed(err));
261 }
262 }
263 }
264
265 log.rows(values.len() as u64);
266 Ok(ExecResponse::value_stream(stmt::ValueStream::from_vec(
267 values,
268 )))
269 }
270}
271
272#[async_trait]
273impl toasty_core::driver::Connection for Connection {
274 async fn exec(&mut self, schema: &Arc<Schema>, op: Operation) -> Result<ExecResponse> {
275 tracing::trace!(driver = "sqlite", op = %op.name(), "driver exec");
276
277 let (sql, typed_params, ret_tys) = match op {
278 Operation::QuerySql(op) => {
279 assert!(
280 op.last_insert_id_hack.is_none(),
281 "last_insert_id_hack is MySQL-specific and should not be set for SQLite"
282 );
283 (sql::Statement::from(op.stmt), op.params, op.ret)
284 }
285 Operation::RawSql(op) => {
286 let ret = match op.ret {
287 RawSqlRet::None => SqlReturn::Count,
288 RawSqlRet::Infer => SqlReturn::Infer,
289 RawSqlRet::Types(types) => SqlReturn::Types(types),
290 };
291 return self.exec_sql(&op.sql, op.params, ret);
292 }
293 Operation::Transaction(mut op) => {
295 if let Transaction::Start { isolation, .. } = &mut op {
296 if !matches!(isolation, Some(IsolationLevel::Serializable) | None) {
297 return Err(toasty_core::Error::unsupported_feature(
298 "SQLite only supports Serializable isolation",
299 ));
300 }
301 *isolation = None;
302 }
303 let sql = sql::Serializer::sqlite(&schema.db).serialize_transaction(&op);
304 self.connection
305 .execute(&sql, [])
306 .map_err(toasty_core::Error::driver_operation_failed)?;
307 return Ok(ExecResponse::count(0));
308 }
309 _ => todo!("op={:#?}", op),
310 };
311
312 let ret = match &sql {
313 sql::Statement::Query(stmt) => match &stmt.body {
314 stmt::ExprSet::Select(_) => SqlReturn::Types(ret_tys.unwrap()),
315 _ => todo!(),
316 },
317 sql::Statement::Insert(stmt) => stmt
318 .returning
319 .as_ref()
320 .map(|_| SqlReturn::Types(ret_tys.unwrap()))
321 .unwrap_or(SqlReturn::Count),
322 sql::Statement::Delete(stmt) => stmt
323 .returning
324 .as_ref()
325 .map(|_| SqlReturn::Types(ret_tys.unwrap()))
326 .unwrap_or(SqlReturn::Count),
327 sql::Statement::Update(stmt) => {
328 assert!(stmt.condition.is_none(), "stmt={stmt:#?}");
329 stmt.returning
330 .as_ref()
331 .map(|_| SqlReturn::Types(ret_tys.unwrap()))
332 .unwrap_or(SqlReturn::Count)
333 }
334 _ => SqlReturn::Count,
335 };
336
337 let sql_str = sql::Serializer::sqlite(&schema.db).serialize(&sql);
338 self.exec_sql(&sql_str, typed_params, ret)
339 }
340
341 async fn push_schema(&mut self, schema: &Schema) -> Result<()> {
342 for table in &schema.db.tables {
343 tracing::debug!(table = %table.name, "creating table");
344 self.create_table(&schema.db, table)?;
345 }
346
347 Ok(())
348 }
349
350 async fn applied_migrations(
351 &mut self,
352 ) -> Result<Vec<toasty_core::schema::db::AppliedMigration>> {
353 self.connection
355 .execute(
356 "CREATE TABLE IF NOT EXISTS __toasty_migrations (
357 id INTEGER PRIMARY KEY,
358 name TEXT NOT NULL,
359 applied_at TEXT NOT NULL
360 )",
361 [],
362 )
363 .map_err(toasty_core::Error::driver_operation_failed)?;
364
365 let mut stmt = self
367 .connection
368 .prepare("SELECT id FROM __toasty_migrations ORDER BY applied_at")
369 .map_err(toasty_core::Error::driver_operation_failed)?;
370
371 let rows = stmt
372 .query_map([], |row| {
373 let id: i64 = row.get(0)?;
374 Ok(toasty_core::schema::db::AppliedMigration::new(id as u64))
375 })
376 .map_err(toasty_core::Error::driver_operation_failed)?;
377
378 rows.collect::<rusqlite::Result<Vec<_>>>()
379 .map_err(toasty_core::Error::driver_operation_failed)
380 }
381
382 async fn apply_migration(
383 &mut self,
384 id: u64,
385 name: &str,
386 migration: &toasty_core::schema::db::Migration,
387 ) -> Result<()> {
388 tracing::info!(id = id, name = %name, "applying migration");
389 self.connection
391 .execute(
392 "CREATE TABLE IF NOT EXISTS __toasty_migrations (
393 id INTEGER PRIMARY KEY,
394 name TEXT NOT NULL,
395 applied_at TEXT NOT NULL
396 )",
397 [],
398 )
399 .map_err(toasty_core::Error::driver_operation_failed)?;
400
401 self.connection
403 .execute("BEGIN", [])
404 .map_err(toasty_core::Error::driver_operation_failed)?;
405
406 for statement in migration.statements() {
408 if let Err(e) = self
409 .connection
410 .execute(statement, [])
411 .map_err(toasty_core::Error::driver_operation_failed)
412 {
413 self.connection
414 .execute("ROLLBACK", [])
415 .map_err(toasty_core::Error::driver_operation_failed)?;
416 return Err(e);
417 }
418 }
419
420 if let Err(e) = self.connection.execute(
422 "INSERT INTO __toasty_migrations (id, name, applied_at) VALUES (?1, ?2, datetime('now'))",
423 rusqlite::params![id as i64, name],
424 ).map_err(toasty_core::Error::driver_operation_failed) {
425 self.connection.execute("ROLLBACK", []).map_err(toasty_core::Error::driver_operation_failed)?;
426 return Err(e);
427 }
428
429 self.connection
431 .execute("COMMIT", [])
432 .map_err(toasty_core::Error::driver_operation_failed)?;
433 Ok(())
434 }
435}
436
437impl Connection {
438 fn create_table(&mut self, schema: &db::Schema, table: &Table) -> Result<()> {
439 let serializer = sql::Serializer::sqlite(schema);
440
441 let stmt = serializer.serialize(&sql::Statement::create_table(table, &Capability::SQLITE));
442
443 self.connection
444 .execute(&stmt, [])
445 .map_err(toasty_core::Error::driver_operation_failed)?;
446
447 for index in &table.indices {
449 if index.primary_key {
451 continue;
452 }
453
454 let stmt = serializer.serialize(&sql::Statement::create_index(index));
455
456 self.connection
457 .execute(&stmt, [])
458 .map_err(toasty_core::Error::driver_operation_failed)?;
459 }
460 Ok(())
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::Sqlite;
467 use std::path::PathBuf;
468
469 fn file_path(url: &str) -> PathBuf {
471 match Sqlite::new(url).unwrap() {
472 Sqlite::File(path) => path,
473 Sqlite::InMemory => panic!("expected a file-backed database for {url}"),
474 }
475 }
476
477 #[test]
478 fn new_decodes_percent_encoded_path() {
479 assert_eq!(
484 file_path("sqlite:/tmp/my db.sqlite"),
485 PathBuf::from("/tmp/my db.sqlite")
486 );
487 assert_eq!(
488 file_path("sqlite:///tmp/my%20db.sqlite"),
489 PathBuf::from("/tmp/my db.sqlite")
490 );
491 assert_eq!(
492 file_path("sqlite:/tmp/d%C3%A9j%C3%A0.db"),
493 PathBuf::from("/tmp/déjà.db")
494 );
495 assert_eq!(
497 file_path("sqlite:/tmp/a+b.db"),
498 PathBuf::from("/tmp/a+b.db")
499 );
500 }
501
502 #[test]
503 fn new_memory_url_stays_in_memory() {
504 assert!(matches!(
505 Sqlite::new("sqlite::memory:").unwrap(),
506 Sqlite::InMemory
507 ));
508 }
509}