#![allow(clippy::unwrap_used)]
use fraiseql_error::FraiseQLError;
use serde_json::json;
use sqlx::Executor as _;
use super::*;
use crate::{
traits::{DatabaseAdapter, DirectMutationContext, DirectMutationOp, MutationStrategy},
types::DatabaseType,
where_clause::WhereClause,
};
async fn setup_user_table(n: usize) -> SqliteAdapter {
let adapter = SqliteAdapter::in_memory().await.expect("Failed to create SQLite adapter");
adapter
.pool
.execute("CREATE TABLE \"v_user\" (id INTEGER PRIMARY KEY, data TEXT)")
.await
.expect("Failed to create v_user");
for i in 1..=n {
let row = format!(
r#"INSERT INTO "v_user" (data) VALUES ('{{"id":{i},"name":"user{i}","age":{age},"active":{active},"score":{score},"deleted_at":null}}')"#,
age = 20 + i,
active = if i % 2 == 0 { "true" } else { "false" },
score = i * 10,
);
adapter.pool.execute(row.as_str()).await.expect("Failed to insert row");
}
adapter
}
#[tokio::test]
async fn test_in_memory_adapter_creation() {
let adapter = SqliteAdapter::in_memory().await.expect("Failed to create SQLite adapter");
let metrics = adapter.pool_metrics();
assert!(metrics.total_connections > 0);
assert_eq!(adapter.database_type(), DatabaseType::SQLite);
}
#[tokio::test]
async fn test_health_check() {
let adapter = SqliteAdapter::in_memory().await.expect("Failed to create SQLite adapter");
adapter.health_check().await.expect("Health check failed");
}
#[tokio::test]
async fn test_raw_query() {
let adapter = SqliteAdapter::in_memory().await.expect("Failed to create SQLite adapter");
sqlx::query("CREATE TABLE test_table (id INTEGER PRIMARY KEY, data TEXT)")
.execute(&adapter.pool)
.await
.expect("Failed to create table");
sqlx::query("INSERT INTO test_table (data) VALUES ('{\"name\": \"test\"}')")
.execute(&adapter.pool)
.await
.expect("Failed to insert data");
let results = adapter
.execute_raw_query("SELECT * FROM test_table")
.await
.expect("Failed to execute query");
assert_eq!(results.len(), 1);
assert!(results[0].contains_key("id"));
assert!(results[0].contains_key("data"));
}
#[tokio::test]
async fn test_parameterized_limit_only() {
let adapter = SqliteAdapter::in_memory().await.expect("Failed to create SQLite adapter");
sqlx::query("CREATE TABLE \"v_user\" (id INTEGER PRIMARY KEY, data TEXT)")
.execute(&adapter.pool)
.await
.expect("Failed to create table");
for i in 1..=5 {
sqlx::query(&format!(
"INSERT INTO \"v_user\" (data) VALUES ('{{\"id\": {}, \"name\": \"user{}\"}}') ",
i, i
))
.execute(&adapter.pool)
.await
.expect("Failed to insert data");
}
let results = adapter
.execute_where_query("v_user", None, Some(2), None, None)
.await
.expect("Failed to execute query");
assert_eq!(results.len(), 2);
}
#[tokio::test]
async fn test_parameterized_offset_only() {
let adapter = SqliteAdapter::in_memory().await.expect("Failed to create SQLite adapter");
sqlx::query("CREATE TABLE \"v_user\" (id INTEGER PRIMARY KEY, data TEXT)")
.execute(&adapter.pool)
.await
.expect("Failed to create table");
for i in 1..=5 {
sqlx::query(&format!(
"INSERT INTO \"v_user\" (data) VALUES ('{{\"id\": {}, \"name\": \"user{}\"}}') ",
i, i
))
.execute(&adapter.pool)
.await
.expect("Failed to insert data");
}
let results = adapter
.execute_where_query("v_user", None, None, Some(2), None)
.await
.expect("Failed to execute query");
assert_eq!(results.len(), 3);
}
#[tokio::test]
async fn test_parameterized_limit_and_offset() {
let adapter = SqliteAdapter::in_memory().await.expect("Failed to create SQLite adapter");
sqlx::query("CREATE TABLE \"v_user\" (id INTEGER PRIMARY KEY, data TEXT)")
.execute(&adapter.pool)
.await
.expect("Failed to create table");
for i in 1..=5 {
sqlx::query(&format!(
"INSERT INTO \"v_user\" (data) VALUES ('{{\"id\": {}, \"name\": \"user{}\"}}') ",
i, i
))
.execute(&adapter.pool)
.await
.expect("Failed to insert data");
}
let results = adapter
.execute_where_query("v_user", None, Some(2), Some(1), None)
.await
.expect("Failed to execute query");
assert_eq!(results.len(), 2);
}
#[tokio::test]
async fn test_function_call_returns_unsupported_error() {
let adapter = SqliteAdapter::in_memory().await.expect("Failed to create SQLite adapter");
let err = adapter
.execute_function_call("fn_create_user", &[json!("alice")])
.await
.expect_err("Expected Unsupported error");
assert!(
matches!(err, FraiseQLError::Unsupported { .. }),
"Expected Unsupported error, got: {err:?}"
);
assert!(
err.to_string().contains("fn_create_user"),
"Error message should name the function"
);
}
#[tokio::test]
async fn test_supports_mutations() {
let adapter = SqliteAdapter::in_memory().await.unwrap();
assert!(adapter.supports_mutations());
assert_eq!(adapter.mutation_strategy(), MutationStrategy::DirectSql);
}
async fn setup_mutation_table() -> SqliteAdapter {
let adapter = SqliteAdapter::in_memory().await.expect("Failed to create SQLite adapter");
adapter
.pool
.execute(
"CREATE TABLE \"users\" (\
pk_user INTEGER PRIMARY KEY AUTOINCREMENT, \
name TEXT NOT NULL, \
email TEXT NOT NULL UNIQUE, \
tenant_id TEXT\
)",
)
.await
.expect("Failed to create users table");
adapter
}
#[tokio::test]
async fn test_direct_mutation_insert() {
let adapter = setup_mutation_table().await;
let columns = vec!["name".to_string(), "email".to_string()];
let values = vec![json!("Alice"), json!("alice@example.com")];
let ctx = DirectMutationContext {
operation: DirectMutationOp::Insert,
table: "users",
columns: &columns,
values: &values,
inject_columns: &[],
return_type: "User",
};
let rows = adapter.execute_direct_mutation(&ctx).await.unwrap();
assert_eq!(rows.len(), 1);
let row = &rows[0];
assert_eq!(row["status"], json!("new"));
assert_eq!(row["entity_type"], json!("User"));
assert!(row["entity_id"].is_null());
let entity = &row["entity"];
assert_eq!(entity["name"], "Alice");
assert_eq!(entity["email"], "alice@example.com");
assert_eq!(entity["pk_user"], 1);
}
#[tokio::test]
async fn test_direct_mutation_insert_with_inject_params() {
let adapter = setup_mutation_table().await;
let columns = vec!["name".to_string(), "email".to_string()];
let inject_columns = vec!["tenant_id".to_string()];
let values = vec![json!("Bob"), json!("bob@example.com"), json!("tenant-42")];
let ctx = DirectMutationContext {
operation: DirectMutationOp::Insert,
table: "users",
columns: &columns,
values: &values,
inject_columns: &inject_columns,
return_type: "User",
};
let rows = adapter.execute_direct_mutation(&ctx).await.unwrap();
let entity = &rows[0]["entity"];
assert_eq!(entity["tenant_id"], "tenant-42");
}
#[tokio::test]
async fn test_direct_mutation_update() {
let adapter = setup_mutation_table().await;
adapter
.pool
.execute("INSERT INTO \"users\" (name, email) VALUES ('Alice', 'alice@example.com')")
.await
.unwrap();
let columns = vec!["pk_user".to_string(), "name".to_string()];
let values = vec![json!(1), json!("Alice Updated")];
let ctx = DirectMutationContext {
operation: DirectMutationOp::Update,
table: "users",
columns: &columns,
values: &values,
inject_columns: &[],
return_type: "User",
};
let rows = adapter.execute_direct_mutation(&ctx).await.unwrap();
let row = &rows[0];
assert_eq!(row["status"], json!("updated"));
assert_eq!(row["entity_id"], json!("1"));
assert_eq!(row["entity"]["name"], "Alice Updated");
assert_eq!(row["entity"]["email"], "alice@example.com");
}
#[tokio::test]
async fn test_direct_mutation_delete() {
let adapter = setup_mutation_table().await;
adapter
.pool
.execute("INSERT INTO \"users\" (name, email) VALUES ('Alice', 'alice@example.com')")
.await
.unwrap();
let columns = vec!["pk_user".to_string()];
let values = vec![json!(1)];
let ctx = DirectMutationContext {
operation: DirectMutationOp::Delete,
table: "users",
columns: &columns,
values: &values,
inject_columns: &[],
return_type: "User",
};
let rows = adapter.execute_direct_mutation(&ctx).await.unwrap();
let row = &rows[0];
assert_eq!(row["status"], json!("deleted"));
assert_eq!(row["entity_id"], json!("1"));
assert_eq!(row["entity"]["name"], "Alice");
let remaining = adapter.execute_raw_query("SELECT * FROM \"users\"").await.unwrap();
assert!(remaining.is_empty());
}
#[tokio::test]
async fn test_direct_mutation_delete_nonexistent_row() {
let adapter = setup_mutation_table().await;
let columns = vec!["pk_user".to_string()];
let values = vec![json!(999)];
let ctx = DirectMutationContext {
operation: DirectMutationOp::Delete,
table: "users",
columns: &columns,
values: &values,
inject_columns: &[],
return_type: "User",
};
let err = adapter
.execute_direct_mutation(&ctx)
.await
.expect_err("Expected error for nonexistent row");
assert!(matches!(err, FraiseQLError::Validation { .. }));
assert!(err.to_string().contains("no rows"));
}
#[tokio::test]
async fn test_direct_mutation_constraint_violation() {
let adapter = setup_mutation_table().await;
adapter
.pool
.execute("INSERT INTO \"users\" (name, email) VALUES ('Alice', 'alice@example.com')")
.await
.unwrap();
let columns = vec!["name".to_string(), "email".to_string()];
let values = vec![json!("Bob"), json!("alice@example.com")];
let ctx = DirectMutationContext {
operation: DirectMutationOp::Insert,
table: "users",
columns: &columns,
values: &values,
inject_columns: &[],
return_type: "User",
};
let err = adapter
.execute_direct_mutation(&ctx)
.await
.expect_err("Expected constraint violation");
assert!(matches!(err, FraiseQLError::Database { .. }));
}
#[tokio::test]
async fn test_direct_mutation_null_handling() {
let adapter = setup_mutation_table().await;
adapter
.pool
.execute("INSERT INTO \"users\" (name, email) VALUES ('Alice', 'alice@example.com')")
.await
.unwrap();
let columns = vec!["pk_user".to_string(), "tenant_id".to_string()];
let values = vec![json!(1), serde_json::Value::Null];
let ctx = DirectMutationContext {
operation: DirectMutationOp::Update,
table: "users",
columns: &columns,
values: &values,
inject_columns: &[],
return_type: "User",
};
let rows = adapter.execute_direct_mutation(&ctx).await.unwrap();
assert!(rows[0]["entity"]["tenant_id"].is_null());
}
#[tokio::test]
async fn test_where_eq_operator() {
let adapter = setup_user_table(5).await;
let clause = WhereClause::Field {
path: vec!["name".to_string()],
operator: crate::where_clause::WhereOperator::Eq,
value: json!("user3"),
};
let results = adapter
.execute_where_query("v_user", Some(&clause), None, None, None)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].as_value()["name"], "user3");
}
#[tokio::test]
async fn test_where_neq_operator() {
let adapter = setup_user_table(3).await;
let clause = WhereClause::Field {
path: vec!["name".to_string()],
operator: crate::where_clause::WhereOperator::Neq,
value: json!("user1"),
};
let results = adapter
.execute_where_query("v_user", Some(&clause), None, None, None)
.await
.unwrap();
assert_eq!(results.len(), 2);
}
#[tokio::test]
async fn test_where_gt_operator() {
let adapter = setup_user_table(5).await;
let clause = WhereClause::Field {
path: vec!["age".to_string()],
operator: crate::where_clause::WhereOperator::Gt,
value: json!(23),
};
let results = adapter
.execute_where_query("v_user", Some(&clause), None, None, None)
.await
.unwrap();
assert_eq!(results.len(), 2);
}
#[tokio::test]
async fn test_where_gte_operator() {
let adapter = setup_user_table(5).await;
let clause = WhereClause::Field {
path: vec!["age".to_string()],
operator: crate::where_clause::WhereOperator::Gte,
value: json!(23),
};
let results = adapter
.execute_where_query("v_user", Some(&clause), None, None, None)
.await
.unwrap();
assert_eq!(results.len(), 3);
}
#[tokio::test]
async fn test_where_lt_operator() {
let adapter = setup_user_table(5).await;
let clause = WhereClause::Field {
path: vec!["age".to_string()],
operator: crate::where_clause::WhereOperator::Lt,
value: json!(23),
};
let results = adapter
.execute_where_query("v_user", Some(&clause), None, None, None)
.await
.unwrap();
assert_eq!(results.len(), 2);
}
#[tokio::test]
async fn test_where_lte_operator() {
let adapter = setup_user_table(5).await;
let clause = WhereClause::Field {
path: vec!["age".to_string()],
operator: crate::where_clause::WhereOperator::Lte,
value: json!(23),
};
let results = adapter
.execute_where_query("v_user", Some(&clause), None, None, None)
.await
.unwrap();
assert_eq!(results.len(), 3);
}
#[tokio::test]
async fn test_where_in_operator() {
let adapter = setup_user_table(5).await;
let clause = WhereClause::Field {
path: vec!["name".to_string()],
operator: crate::where_clause::WhereOperator::In,
value: json!(["user1", "user3", "user5"]),
};
let results = adapter
.execute_where_query("v_user", Some(&clause), None, None, None)
.await
.unwrap();
assert_eq!(results.len(), 3);
}
#[tokio::test]
async fn test_where_not_in_operator() {
let adapter = setup_user_table(5).await;
let clause = WhereClause::Field {
path: vec!["name".to_string()],
operator: crate::where_clause::WhereOperator::Nin,
value: json!(["user1", "user2"]),
};
let results = adapter
.execute_where_query("v_user", Some(&clause), None, None, None)
.await
.unwrap();
assert_eq!(results.len(), 3);
}
#[tokio::test]
async fn test_where_like_operator() {
let adapter = setup_user_table(5).await;
let clause = WhereClause::Field {
path: vec!["name".to_string()],
operator: crate::where_clause::WhereOperator::Like,
value: json!("user%"),
};
let results = adapter
.execute_where_query("v_user", Some(&clause), None, None, None)
.await
.unwrap();
assert_eq!(results.len(), 5);
}
#[tokio::test]
async fn test_where_is_null_operator() {
let adapter = setup_user_table(3).await;
let clause = WhereClause::Field {
path: vec!["deleted_at".to_string()],
operator: crate::where_clause::WhereOperator::IsNull,
value: json!(true),
};
let results = adapter
.execute_where_query("v_user", Some(&clause), None, None, None)
.await
.unwrap();
assert_eq!(results.len(), 3);
}
#[tokio::test]
async fn test_where_is_not_null_operator() {
let adapter = setup_user_table(3).await;
let clause = WhereClause::Field {
path: vec!["deleted_at".to_string()],
operator: crate::where_clause::WhereOperator::IsNull,
value: json!(false),
};
let results = adapter
.execute_where_query("v_user", Some(&clause), None, None, None)
.await
.unwrap();
assert_eq!(results.len(), 0);
}
#[tokio::test]
async fn test_where_multiple_conditions_and() {
let adapter = setup_user_table(5).await;
let clause = WhereClause::And(vec![
WhereClause::Field {
path: vec!["name".to_string()],
operator: crate::where_clause::WhereOperator::Eq,
value: json!("user2"),
},
WhereClause::Field {
path: vec!["age".to_string()],
operator: crate::where_clause::WhereOperator::Eq,
value: json!(22),
},
]);
let results = adapter
.execute_where_query("v_user", Some(&clause), None, None, None)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].as_value()["name"], "user2");
}
#[tokio::test]
async fn test_where_multiple_conditions_or() {
let adapter = setup_user_table(5).await;
let clause = WhereClause::Or(vec![
WhereClause::Field {
path: vec!["name".to_string()],
operator: crate::where_clause::WhereOperator::Eq,
value: json!("user1"),
},
WhereClause::Field {
path: vec!["name".to_string()],
operator: crate::where_clause::WhereOperator::Eq,
value: json!("user5"),
},
]);
let results = adapter
.execute_where_query("v_user", Some(&clause), None, None, None)
.await
.unwrap();
assert_eq!(results.len(), 2);
}
#[tokio::test]
async fn test_empty_result_set() {
let adapter = setup_user_table(3).await;
let clause = WhereClause::Field {
path: vec!["name".to_string()],
operator: crate::where_clause::WhereOperator::Eq,
value: json!("nonexistent"),
};
let results = adapter
.execute_where_query("v_user", Some(&clause), None, None, None)
.await
.unwrap();
assert!(results.is_empty());
}
#[tokio::test]
async fn test_invalid_raw_query_returns_error() {
let adapter = SqliteAdapter::in_memory().await.unwrap();
let err = adapter
.execute_raw_query("SELECT * FROM nonexistent_table_xyz")
.await
.expect_err("Expected database error");
assert!(matches!(err, FraiseQLError::Database { .. }));
}
#[tokio::test]
async fn test_pool_metrics_when_idle() {
let adapter = SqliteAdapter::in_memory().await.unwrap();
let metrics = adapter.pool_metrics();
assert!(metrics.idle_connections <= metrics.total_connections);
assert_eq!(metrics.waiting_requests, 0);
}
#[tokio::test]
async fn test_explain_query_returns_plan() {
let adapter = setup_user_table(3).await;
let result = adapter
.explain_query("SELECT data FROM \"v_user\"", &[])
.await
.expect("explain_query should succeed");
assert!(result.as_array().is_some_and(|a| !a.is_empty()));
}
#[tokio::test]
async fn test_projection_filters_fields() {
use crate::types::SqlProjectionHint;
let adapter = setup_user_table(3).await;
let projection = SqlProjectionHint {
database: crate::DatabaseType::SQLite,
projection_template: "json_object('name', json_extract(data, '$.name')) AS data"
.to_string(),
estimated_reduction_percent: 50,
};
let results = adapter
.execute_with_projection("v_user", Some(&projection), None, None, None, None)
.await
.expect("execute_with_projection should succeed");
assert_eq!(results.len(), 3);
for row in &results {
assert!(row.as_value().get("name").is_some());
assert!(row.as_value().get("age").is_none());
}
}