Files

315 lines
10 KiB
Rust

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::<Vec<_>>();
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();
}