#![cfg(test)]
use std::time::Duration;
use prax_sqlite::{SqliteConfig, SqlitePool};
use tempfile::TempDir;
fn skip_unless_e2e() -> bool {
std::env::var("PRAX_E2E").ok().as_deref() == Some("1")
}
struct TestDb {
pool: SqlitePool,
_tempdir: TempDir,
}
async fn test_db() -> TestDb {
let tempdir = tempfile::tempdir().expect("tempdir");
let path = tempdir.path().join("e2e.sqlite");
let config = SqliteConfig::file(&path);
let pool = SqlitePool::builder()
.config(config)
.max_connections(2)
.connection_timeout(Duration::from_secs(5))
.build()
.await
.expect("build sqlite pool");
TestDb {
pool,
_tempdir: tempdir,
}
}
#[tokio::test]
#[ignore = "SQLite E2E — run via `docker compose run --rm test-sqlite`"]
async fn e2e_crud_roundtrip() {
if !skip_unless_e2e() {
return;
}
let db = test_db().await;
let pool = &db.pool;
let conn = pool.get().await.expect("acquire");
conn.execute_batch(
"CREATE TABLE e2e_crud (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
score INTEGER NOT NULL
)",
)
.await
.expect("create table");
let rowid = conn
.execute_insert_params(
"INSERT INTO e2e_crud (name, score) VALUES (?, ?)",
vec![
rusqlite::types::Value::Text("alice".into()),
rusqlite::types::Value::Integer(42),
],
)
.await
.expect("insert");
assert!(rowid > 0);
let rows = conn
.query("SELECT name, score FROM e2e_crud")
.await
.expect("select");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["name"], serde_json::json!("alice"));
assert_eq!(rows[0]["score"], serde_json::json!(42));
let n = conn
.execute_params(
"UPDATE e2e_crud SET score = ? WHERE name = ?",
vec![
rusqlite::types::Value::Integer(100),
rusqlite::types::Value::Text("alice".into()),
],
)
.await
.expect("update");
assert_eq!(n, 1);
let row = conn
.query_one("SELECT score FROM e2e_crud WHERE name = 'alice'")
.await
.expect("query_one");
assert_eq!(row["score"], serde_json::json!(100));
let n = conn.execute("DELETE FROM e2e_crud").await.expect("delete");
assert_eq!(n, 1);
}
#[tokio::test]
#[ignore = "SQLite E2E — run via `docker compose run --rm test-sqlite`"]
async fn e2e_null_and_default_roundtrip() {
if !skip_unless_e2e() {
return;
}
let db = test_db().await;
let pool = &db.pool;
let conn = pool.get().await.expect("conn");
conn.execute_batch(
"CREATE TABLE e2e_nulls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
opt TEXT,
blob_col BLOB,
real_col REAL NOT NULL DEFAULT 0.0
)",
)
.await
.expect("create");
conn.execute_params(
"INSERT INTO e2e_nulls (opt, blob_col) VALUES (?, ?)",
vec![
rusqlite::types::Value::Null,
rusqlite::types::Value::Blob(vec![0xDE, 0xAD, 0xBE, 0xEF]),
],
)
.await
.expect("insert");
let row = conn
.query_one("SELECT opt, real_col FROM e2e_nulls")
.await
.expect("query");
assert_eq!(row["opt"], serde_json::Value::Null);
assert_eq!(row["real_col"], serde_json::json!(0.0));
}
#[tokio::test]
#[ignore = "SQLite E2E — run via `docker compose run --rm test-sqlite`"]
async fn e2e_missing_row_returns_none() {
if !skip_unless_e2e() {
return;
}
let db = test_db().await;
let pool = &db.pool;
let conn = pool.get().await.expect("conn");
conn.execute_batch("CREATE TABLE e2e_empty (id INTEGER PRIMARY KEY)")
.await
.expect("create");
let row = conn
.query_optional("SELECT id FROM e2e_empty WHERE id = 999")
.await
.expect("query_optional");
assert!(row.is_none());
}
#[tokio::test]
#[ignore = "SQLite E2E — run via `docker compose run --rm test-sqlite`"]
async fn e2e_batch_of_ddl_then_dml() {
if !skip_unless_e2e() {
return;
}
let db = test_db().await;
let pool = &db.pool;
let conn = pool.get().await.expect("conn");
conn.execute_batch(
"CREATE TABLE e2e_batch_a (id INTEGER PRIMARY KEY, v INT);
CREATE TABLE e2e_batch_b (id INTEGER PRIMARY KEY, v INT);
INSERT INTO e2e_batch_a (v) VALUES (1), (2), (3);
INSERT INTO e2e_batch_b (v) VALUES (10);",
)
.await
.expect("batch");
let row = conn
.query_one("SELECT COUNT(*) AS n FROM e2e_batch_a")
.await
.expect("count a");
assert_eq!(row["n"], serde_json::json!(3));
let row = conn
.query_one("SELECT COUNT(*) AS n FROM e2e_batch_b")
.await
.expect("count b");
assert_eq!(row["n"], serde_json::json!(1));
}
#[tokio::test]
#[ignore = "SQLite E2E — run via `docker compose run --rm test-sqlite`"]
async fn e2e_pool_reuses_connections() {
if !skip_unless_e2e() {
return;
}
let db = test_db().await;
let pool = &db.pool;
for i in 0..32_i32 {
let conn = pool.get().await.expect("acquire");
let row = conn
.query_one(&format!("SELECT {i} AS v"))
.await
.expect("query");
assert_eq!(row["v"], serde_json::json!(i));
}
}
#[tokio::test]
#[ignore = "SQLite E2E — run via `docker compose run --rm test-sqlite`"]
async fn e2e_query_many_typed_decodes_rows() {
use prax_query::row::{FromRow, RowError, RowRef};
use prax_query::traits::{Model, QueryEngine};
use prax_sqlite::SqliteEngine;
#[derive(Debug)]
struct Item {
id: i32,
name: String,
}
impl Model for Item {
const MODEL_NAME: &'static str = "Item";
const TABLE_NAME: &'static str = "items";
const PRIMARY_KEY: &'static [&'static str] = &["id"];
const COLUMNS: &'static [&'static str] = &["id", "name"];
}
impl FromRow for Item {
fn from_row(r: &impl RowRef) -> Result<Self, RowError> {
Ok(Item {
id: r.get_i32("id")?,
name: r.get_string("name")?,
})
}
}
if !skip_unless_e2e() {
return;
}
let db = test_db().await;
let pool = &db.pool;
let engine = SqliteEngine::new(pool.clone());
let conn = pool.get().await.expect("conn");
conn.execute_batch("CREATE TABLE e2e_query_many (id INTEGER PRIMARY KEY, name TEXT NOT NULL)")
.await
.expect("create table");
conn.execute_batch("INSERT INTO e2e_query_many (id, name) VALUES (1, 'a'), (2, 'b')")
.await
.expect("insert");
let rows = engine
.query_many::<Item>("SELECT id, name FROM e2e_query_many ORDER BY id", vec![])
.await
.expect("query_many");
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].id, 1);
assert_eq!(rows[0].name, "a");
assert_eq!(rows[1].id, 2);
assert_eq!(rows[1].name, "b");
}
#[tokio::test]
#[ignore = "SQLite E2E — run via `docker compose run --rm test-sqlite`"]
async fn e2e_query_one_with_multiple_rows_behavior() {
use prax_query::filter::FilterValue;
use prax_query::row::{FromRow, RowError, RowRef};
use prax_query::traits::{Model, QueryEngine};
use prax_sqlite::SqliteEngine;
#[derive(Debug)]
struct Person {
id: i32,
email: String,
}
impl Model for Person {
const MODEL_NAME: &'static str = "Person";
const TABLE_NAME: &'static str = "e2e_sqlite_one_multi";
const PRIMARY_KEY: &'static [&'static str] = &["id"];
const COLUMNS: &'static [&'static str] = &["id", "email"];
}
impl FromRow for Person {
fn from_row(r: &impl RowRef) -> Result<Self, RowError> {
Ok(Person {
id: r.get_i32("id")?,
email: r.get_string("email")?,
})
}
}
if !skip_unless_e2e() {
return;
}
let db = test_db().await;
let pool = &db.pool;
let engine = SqliteEngine::new(pool.clone());
engine
.execute_raw("DROP TABLE IF EXISTS e2e_sqlite_one_multi", vec![])
.await
.unwrap();
engine
.execute_raw(
"CREATE TABLE e2e_sqlite_one_multi (id INTEGER PRIMARY KEY AUTOINCREMENT, \
email TEXT NOT NULL)",
vec![],
)
.await
.unwrap();
engine
.execute_raw(
"INSERT INTO e2e_sqlite_one_multi (email) VALUES ('a@x.com'), ('b@x.com')",
vec![],
)
.await
.unwrap();
let result = engine
.query_one::<Person>(
"SELECT id, email FROM e2e_sqlite_one_multi ORDER BY id",
Vec::<FilterValue>::new(),
)
.await;
match result {
Ok(p) => {
assert_eq!(
p.email, "a@x.com",
"SQLite query_one should return the first row (by ORDER BY) \
when 2+ rows match; this documents the rusqlite 'take first' \
semantics and must not regress silently."
);
}
Err(e) => {
panic!(
"SQLite engine is documented to return the first row on a \
multi-row query_one (rusqlite::Rows::next semantics, via \
query_first_row). If the engine was changed to error, \
update this test, the Unreleased CHANGELOG migration \
guide, and any callers that relied on the implicit \
'take first' behavior. Got: {e:?}"
);
}
}
engine
.execute_raw("DROP TABLE e2e_sqlite_one_multi", vec![])
.await
.unwrap();
}