#![cfg(all(
feature = "sqlite",
feature = "uuid",
feature = "chrono",
feature = "json"
))]
#![expect(
clippy::unwrap_used,
reason = "test fixture — setup failure should panic"
)]
use chrono::{DateTime, Utc};
use fletch_orm::built_query::BuiltQuery;
use fletch_orm::{Column, ColumnType, Entity, FromRow, Pool, Value};
use serde_json::Value as JsonValue;
use uuid::Uuid;
const TEST_UUID: Uuid = uuid::uuid!("550e8400-e29b-41d4-a716-446655440000");
const TEST_TS_STR: &str = "2024-06-15T12:30:45Z";
const TEST_JSON: &str = r#"{"key":"value","nested":{"arr":[1,2,null]}}"#;
fn test_ts() -> DateTime<Utc> {
DateTime::parse_from_rfc3339(TEST_TS_STR)
.unwrap()
.with_timezone(&Utc)
}
fn test_json() -> JsonValue {
serde_json::from_str(TEST_JSON).unwrap()
}
#[derive(Debug, Clone, PartialEq, FromRow)]
struct TypedRecord {
id: i64,
record_uuid: Uuid,
metadata: JsonValue,
created_at: DateTime<Utc>,
}
impl Entity for TypedRecord {
type Id = i64;
fn table_name() -> &'static str {
"typed_records"
}
fn id_column() -> &'static str {
"id"
}
fn columns() -> &'static [Column] {
static COLS: [Column; 4] = [
Column::new("id", ColumnType::Integer),
Column::new("record_uuid", ColumnType::Uuid),
Column::new("metadata", ColumnType::Json),
Column::new("created_at", ColumnType::Timestamp),
];
&COLS
}
fn id(&self) -> Self::Id {
self.id
}
fn values(&self) -> Vec<Value> {
vec![
Value::I64(self.id),
Value::Uuid(self.record_uuid),
Value::Json(self.metadata.clone()),
Value::Timestamp(self.created_at),
]
}
}
#[derive(Debug, PartialEq, FromRow)]
struct NullableRow {
id: i64,
maybe_json: Option<JsonValue>,
}
async fn setup_pool() -> Pool<sqlx::Sqlite> {
let pool = Pool::<sqlx::Sqlite>::connect("sqlite::memory:")
.await
.unwrap();
sqlx::query(
"CREATE TABLE typed_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
record_uuid TEXT NOT NULL,
metadata TEXT NOT NULL,
created_at TEXT NOT NULL
)",
)
.execute(pool.inner())
.await
.unwrap();
pool
}
async fn seed_record(pool: &Pool<sqlx::Sqlite>) {
sqlx::query(
"INSERT INTO typed_records (id, record_uuid, metadata, created_at) VALUES (1, ?, ?, ?)",
)
.bind(TEST_UUID)
.bind(test_json())
.bind(test_ts())
.execute(pool.inner())
.await
.unwrap();
}
async fn setup_nullable_pool() -> Pool<sqlx::Sqlite> {
let pool = Pool::<sqlx::Sqlite>::connect("sqlite::memory:")
.await
.unwrap();
sqlx::query(
"CREATE TABLE nullable_typed (
id INTEGER PRIMARY KEY,
maybe_json TEXT
)",
)
.execute(pool.inner())
.await
.unwrap();
pool
}
#[tokio::test]
async fn fetch_all_binds_uuid() {
let pool = setup_pool().await;
seed_record(&pool).await;
let rows: Vec<TypedRecord> = pool
.fetch_all(
"SELECT id, record_uuid, metadata, created_at FROM typed_records WHERE record_uuid = ?",
&[Value::Uuid(TEST_UUID)],
)
.await
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].record_uuid, TEST_UUID);
}
#[tokio::test]
async fn fetch_all_binds_json() {
let pool = setup_pool().await;
seed_record(&pool).await;
let rows: Vec<TypedRecord> = pool
.fetch_all(
"SELECT id, record_uuid, metadata, created_at FROM typed_records WHERE metadata = ?",
&[Value::Json(test_json())],
)
.await
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].metadata, test_json());
}
#[tokio::test]
async fn fetch_all_binds_timestamp() {
let pool = setup_pool().await;
seed_record(&pool).await;
let rows: Vec<TypedRecord> = pool
.fetch_all(
"SELECT id, record_uuid, metadata, created_at FROM typed_records WHERE created_at = ?",
&[Value::Timestamp(test_ts())],
)
.await
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].created_at, test_ts());
}
#[tokio::test]
async fn fetch_all_binds_all_three_in_where() {
let pool = setup_pool().await;
seed_record(&pool).await;
let rows: Vec<TypedRecord> = pool
.fetch_all(
"SELECT id, record_uuid, metadata, created_at FROM typed_records \
WHERE record_uuid = ? AND metadata = ? AND created_at = ?",
&[
Value::Uuid(TEST_UUID),
Value::Json(test_json()),
Value::Timestamp(test_ts()),
],
)
.await
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].record_uuid, TEST_UUID);
assert_eq!(rows[0].metadata, test_json());
assert_eq!(rows[0].created_at, test_ts());
}
#[tokio::test]
async fn insert_and_find_by_id_typed_columns() {
let pool = setup_pool().await;
let entity = TypedRecord {
id: 0,
record_uuid: TEST_UUID,
metadata: test_json(),
created_at: test_ts(),
};
let inserted = pool.insert(&entity).await.unwrap();
assert_ne!(inserted.id, 0);
assert_eq!(inserted.record_uuid, TEST_UUID);
assert_eq!(inserted.metadata, test_json());
assert_eq!(inserted.created_at, test_ts());
let found = pool.find_by_id::<TypedRecord>(inserted.id).await.unwrap();
assert_eq!(found, inserted);
}
#[tokio::test]
async fn fetch_all_binds_json_null() {
let pool = setup_nullable_pool().await;
BuiltQuery {
sql: "INSERT INTO nullable_typed (id, maybe_json) VALUES (1, ?)".into(),
values: vec![Value::Null],
}
.execute::<sqlx::Sqlite, _>(pool.inner())
.await
.unwrap();
let rows: Vec<NullableRow> = pool
.fetch_all(
"SELECT id, maybe_json FROM nullable_typed WHERE id = ?",
&[Value::I64(1)],
)
.await
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].maybe_json, None);
}