mod support; use dever_runtime::orm::{ErrorKind, Uuid, Value, decimal, decimal_storage, uuid}; use dever_runtime::sqlite::Database; #[test] fn sqlite_invalid_text_preserves_the_column_and_error_kind_in_all_decoders() { dever_runtime::task::run_entry(async { let root = support::temp::TemporaryDirectory::new(); let database = Database::open(root.path().join("invalid-text.db"), 1, 4).unwrap(); let check = |error: dever_runtime::orm::Error| { assert_eq!(error.kind(), ErrorKind::Database); assert!(error.to_string().contains("index 1")); }; const INVALID_TEXT: &str = "SELECT 7, CAST(x'ff' AS TEXT)"; check(database.query(INVALID_TEXT, vec![]).await.unwrap_err()); check( database .query_bounded(INVALID_TEXT, vec![], 1) .await .unwrap_err(), ); check( database .query_owned(INVALID_TEXT.into(), vec![]) .await .unwrap_err(), ); let stream = database.stream(INVALID_TEXT, vec![], 1).await.unwrap(); check(stream.pull().await.unwrap_err()); stream.close(); let rows = database.query("SELECT 1", vec![]).await.unwrap(); assert_eq!(rows[0].get(0).unwrap(), &Value::Int(1)); Ok(()) }) .unwrap(); } #[tokio::test] async fn sqlite_pool_executes_parameterized_queries_and_classifies_constraints() { let root = support::temp::TemporaryDirectory::new(); let database = Database::open(root.path().join("app.db"), 1, 50).unwrap(); assert_eq!(database.max_page_size(), 50); database .execute_batch( "CREATE TABLE user (\ id INTEGER PRIMARY KEY AUTOINCREMENT, \ email TEXT NOT NULL UNIQUE, \ enabled INTEGER NOT NULL\ )" .to_owned(), ) .await .unwrap(); database .execute( "INSERT INTO user (email, enabled) VALUES (?1, ?2)", vec![Value::Text("admin@example.com".into()), Value::Bool(true)], ) .await .unwrap(); let rows = database .query( "SELECT id, email, enabled FROM user WHERE email = ?1", vec![Value::Text("admin@example.com".into())], ) .await .unwrap(); assert_eq!(rows.len(), 1); assert_eq!(rows[0].get(0).unwrap(), &Value::Int(1)); assert_eq!( rows[0].get(1).unwrap(), &Value::Text("admin@example.com".into()) ); assert_eq!(rows[0].get(2).unwrap(), &Value::Int(1)); let duplicate = database .execute( "INSERT INTO user (email, enabled) VALUES (?1, ?2)", vec![Value::Text("admin@example.com".into()), Value::Bool(false)], ) .await .unwrap_err(); assert_eq!(duplicate.kind(), ErrorKind::Constraint); let expected_uuid = Uuid::parse("018f08d8-59b0-7cc2-98c4-dc0c0c07398f").unwrap(); database .execute_batch( "CREATE TABLE uuid_value (value BLOB NOT NULL CHECK (length(value) = 16))".to_owned(), ) .await .unwrap(); database .execute( "INSERT INTO uuid_value (value) VALUES (?1)", vec![Value::Uuid(expected_uuid)], ) .await .unwrap(); let mut uuid_rows = database .query("SELECT value FROM uuid_value", vec![]) .await .unwrap(); let stored_uuid = uuid(uuid_rows.remove(0).into_values().next().unwrap()).unwrap(); assert_eq!(stored_uuid, expected_uuid); } #[tokio::test] async fn sqlite_transactions_commit_or_rollback_on_the_owned_connection() { let root = support::temp::TemporaryDirectory::new(); let database = Database::open(root.path().join("transaction.db"), 1, 50).unwrap(); database .execute_batch("CREATE TABLE event (id INTEGER PRIMARY KEY, name TEXT NOT NULL)".to_owned()) .await .unwrap(); let transaction = database.begin().await.unwrap(); transaction .execute( "INSERT INTO event (id, name) VALUES (?1, ?2)", vec![Value::Int(1), Value::Text("rolled back".into())], ) .await .unwrap(); transaction.rollback().await.unwrap(); assert!( database .query("SELECT id FROM event", vec![]) .await .unwrap() .is_empty() ); let transaction = database.begin().await.unwrap(); transaction .execute( "INSERT INTO event (id, name) VALUES (?1, ?2)", vec![Value::Int(2), Value::Text("committed".into())], ) .await .unwrap(); transaction.commit().await.unwrap(); let rows = database .query("SELECT id, name FROM event", vec![]) .await .unwrap(); assert_eq!(rows.len(), 1); assert_eq!(rows[0].get(0).unwrap(), &Value::Int(2)); assert_eq!(rows[0].get(1).unwrap(), &Value::Text("committed".into())); let abandoned = database.begin().await.unwrap(); abandoned .execute( "INSERT INTO event (id, name) VALUES (?1, ?2)", vec![Value::Int(3), Value::Text("abandoned".into())], ) .await .unwrap(); drop(abandoned); let rows = database .query("SELECT id FROM event WHERE id = ?1", vec![Value::Int(3)]) .await .unwrap(); assert!(rows.is_empty()); } #[test] fn decimal_and_uuid_values_have_stable_storage_round_trips() { let decimal_values = ["-10", "-2", "-0.01", "0", "0.01", "2", "10"]; let encoded = decimal_values .iter() .map(|value| decimal_storage(value, 8, 2).unwrap()) .collect::>(); let mut sorted = encoded.clone(); sorted.sort(); assert_eq!(sorted, encoded); for (expected, encoded) in decimal_values.into_iter().zip(encoded) { assert_eq!( decimal(Value::Decimal(encoded)).unwrap(), decimal(Value::Text(expected.to_owned())).unwrap() ); } assert!(decimal_storage("1.234", 8, 2).is_err()); assert!(decimal_storage("1000", 3, 0).is_err()); assert!(decimal_storage("1e2147483647", 8, 2).is_err()); assert_eq!( decimal_storage("0e2147483647", 8, 2).unwrap(), "D100000000:02" ); let text = "018f08d8-59b0-7cc2-98c4-dc0c0c07398f"; let value = Uuid::parse(text).unwrap(); assert_eq!(value.to_string(), text); assert_eq!(uuid(Value::Uuid(value)).unwrap(), value); assert!(Uuid::parse("not-a-uuid").is_err()); let generated = Uuid::new_v7().unwrap(); let generated_bytes = generated.into_bytes(); assert_eq!(generated_bytes[6] >> 4, 7); assert_eq!(generated_bytes[8] & 0xc0, 0x80); assert_eq!(Uuid::parse(&generated.to_string()).unwrap(), generated); } #[tokio::test] async fn cancelled_sqlite_work_does_not_return_an_unknown_connection_to_the_pool() { let root = support::temp::TemporaryDirectory::new(); let database = std::sync::Arc::new(Database::open(root.path().join("cancel.db"), 1, 50).unwrap()); let query_database = database.clone(); let query = tokio::spawn(async move { query_database .query( "WITH RECURSIVE values_(value) AS (\ VALUES(0) UNION ALL SELECT value + 1 FROM values_ WHERE value < 100000000\ ) SELECT sum(value) FROM values_", vec![], ) .await }); tokio::task::yield_now().await; query.abort(); let _ = query.await; let rows = tokio::time::timeout( std::time::Duration::from_secs(2), database.query("SELECT 1", vec![]), ) .await .expect("the pool must replace the cancelled connection") .unwrap(); assert_eq!(rows[0].get(0).unwrap(), &Value::Int(1)); } #[test] fn sqlite_row_stream_is_bounded_and_releases_its_connection_when_closed_early_or_dropped() { dever_runtime::task::run_entry(async { let root = support::temp::TemporaryDirectory::new(); let database = Database::open(root.path().join("stream.db"), 1, 4).unwrap(); database .execute_batch( "CREATE TABLE event (id INTEGER PRIMARY KEY, name TEXT NOT NULL);\ INSERT INTO event (id, name) VALUES \ (1, 'one'), (2, 'two'), (3, 'three'), (4, 'four'), (5, 'five')" .to_owned(), ) .await .unwrap(); let stream = database .stream("SELECT id, name FROM event ORDER BY id", vec![], 2) .await .unwrap(); let first = stream.pull().await.unwrap().unwrap(); assert_eq!(first.get(0).unwrap(), &Value::Int(1)); let waiting_for_connection = tokio::time::timeout( std::time::Duration::from_millis(50), database.query("SELECT count(*) FROM event", vec![]), ) .await; assert!( waiting_for_connection.is_err(), "the stream must retain the only connection until it is closed" ); stream.close(); let rows = tokio::time::timeout( std::time::Duration::from_secs(2), database.query("SELECT count(*) FROM event", vec![]), ) .await .expect("closing the row stream must return its only connection") .unwrap(); assert_eq!(rows[0].get(0).unwrap(), &Value::Int(5)); let stream = database .stream("SELECT id FROM event ORDER BY id", vec![], 1) .await .unwrap(); let mut ids = Vec::new(); while let Some(row) = stream.pull().await.unwrap() { ids.push(row.get(0).unwrap().clone()); } assert_eq!( ids, vec![ Value::Int(1), Value::Int(2), Value::Int(3), Value::Int(4), Value::Int(5), ] ); let stream = database .stream("SELECT id FROM event ORDER BY id", vec![], 1) .await .unwrap(); assert!(stream.pull().await.unwrap().is_some()); drop(stream); let rows = tokio::time::timeout( std::time::Duration::from_secs(2), database.query("SELECT count(*) FROM event", vec![]), ) .await .expect("dropping the row stream must return its only connection") .unwrap(); assert_eq!(rows[0].get(0).unwrap(), &Value::Int(5)); Ok(()) }) .unwrap(); }