#![allow(clippy::unwrap_used)]
#![allow(clippy::items_after_statements)]
#![allow(clippy::panic)]
use fraiseql_error::FraiseQLError;
use serde_json::json;
use super::*;
use crate::{WhereClause, WhereOperator, traits::DatabaseAdapter, types::DatabaseType};
const TEST_DB_URL: &str =
"postgresql://fraiseql_test:fraiseql_test_password@localhost:5433/test_fraiseql";
async fn create_test_adapter() -> PostgresAdapter {
PostgresAdapter::new(TEST_DB_URL)
.await
.expect("Failed to create test adapter - is PostgreSQL running? Use: docker compose -f docker-compose.test.yml up -d postgres-test")
}
#[tokio::test]
async fn test_adapter_creation() {
let adapter = create_test_adapter().await;
let metrics = adapter.pool_metrics();
assert!(metrics.total_connections > 0);
assert_eq!(adapter.database_type(), DatabaseType::PostgreSQL);
}
#[tokio::test]
async fn test_adapter_with_custom_pool_size() {
let adapter = PostgresAdapter::with_pool_size(TEST_DB_URL, 5)
.await
.expect("Failed to create adapter");
let metrics = adapter.pool_metrics();
assert!(metrics.total_connections >= 1, "Pool should have at least 1 connection");
assert!(metrics.total_connections <= 5, "Pool should not exceed max_size of 5");
}
#[tokio::test]
async fn test_health_check() {
let adapter = create_test_adapter().await;
adapter.health_check().await.expect("Health check failed");
}
#[tokio::test]
async fn test_pool_metrics() {
let adapter = create_test_adapter().await;
let metrics = adapter.pool_metrics();
assert!(metrics.total_connections > 0);
assert!(metrics.idle_connections <= metrics.total_connections);
assert_eq!(metrics.active_connections, metrics.total_connections - metrics.idle_connections);
}
#[tokio::test]
async fn test_query_all_users() {
let adapter = create_test_adapter().await;
let results = adapter
.execute_where_query("v_user", None, None, None, None)
.await
.expect("Failed to query users");
assert_eq!(results.len(), 5, "Should have 5 test users");
let first_user = results[0].as_value();
assert!(first_user.get("id").is_some());
assert!(first_user.get("email").is_some());
assert!(first_user.get("name").is_some());
}
#[tokio::test]
async fn test_query_all_posts() {
let adapter = create_test_adapter().await;
let results = adapter
.execute_where_query("v_post", None, None, None, None)
.await
.expect("Failed to query posts");
assert_eq!(results.len(), 4, "Should have 4 test posts");
let first_post = results[0].as_value();
assert!(first_post.get("author").is_some());
assert!(first_post["author"].get("name").is_some());
}
#[tokio::test]
async fn test_where_eq() {
let adapter = create_test_adapter().await;
let where_clause = WhereClause::Field {
path: vec!["email".to_string()],
operator: WhereOperator::Eq,
value: json!("alice@example.com"),
};
let results = adapter
.execute_where_query("v_user", Some(&where_clause), None, None, None)
.await
.expect("Failed to execute query");
assert_eq!(results.len(), 1);
assert_eq!(results[0].as_value()["email"], "alice@example.com");
}
#[tokio::test]
async fn test_where_neq() {
let adapter = create_test_adapter().await;
let where_clause = WhereClause::Field {
path: vec!["role".to_string()],
operator: WhereOperator::Neq,
value: json!("user"),
};
let results = adapter
.execute_where_query("v_user", Some(&where_clause), None, None, None)
.await
.expect("Failed to execute query");
assert!(results.len() >= 2);
for result in &results {
assert_ne!(result.as_value()["role"], "user");
}
}
#[tokio::test]
async fn test_where_gt() {
let adapter = create_test_adapter().await;
let where_clause = WhereClause::Field {
path: vec!["age".to_string()],
operator: WhereOperator::Gt,
value: json!(30),
};
let results = adapter
.execute_where_query("v_user", Some(&where_clause), None, None, None)
.await
.expect("Failed to execute query");
assert!(!results.is_empty(), "Should return at least one result");
assert_eq!(results.len(), 1, "Should return exactly 1 user (Charlie with age 35)");
for result in &results {
let age = result.as_value()["age"].as_i64().unwrap();
assert!(age > 30, "Age should be > 30, but got {}", age);
}
}
#[tokio::test]
async fn test_where_gte() {
let adapter = create_test_adapter().await;
let where_clause = WhereClause::Field {
path: vec!["age".to_string()],
operator: WhereOperator::Gte,
value: json!(30),
};
let results = adapter
.execute_where_query("v_user", Some(&where_clause), None, None, None)
.await
.expect("Failed to execute query");
for result in &results {
let age = result.as_value()["age"].as_i64().unwrap();
assert!(age >= 30);
}
}
#[tokio::test]
async fn test_where_icontains() {
let adapter = create_test_adapter().await;
let where_clause = WhereClause::Field {
path: vec!["email".to_string()],
operator: WhereOperator::Icontains,
value: json!("example.com"),
};
let results = adapter
.execute_where_query("v_user", Some(&where_clause), None, None, None)
.await
.expect("Failed to execute query");
assert!(results.len() >= 3);
for result in &results {
let email = result.as_value()["email"].as_str().unwrap();
assert!(email.to_lowercase().contains("example.com"));
}
}
#[tokio::test]
async fn test_where_startswith() {
let adapter = create_test_adapter().await;
let where_clause = WhereClause::Field {
path: vec!["name".to_string()],
operator: WhereOperator::Startswith,
value: json!("Alice"),
};
let results = adapter
.execute_where_query("v_user", Some(&where_clause), None, None, None)
.await
.expect("Failed to execute query");
assert_eq!(results.len(), 1);
assert!(results[0].as_value()["name"].as_str().unwrap().starts_with("Alice"));
}
#[tokio::test]
async fn test_where_and() {
let adapter = create_test_adapter().await;
let where_clause = WhereClause::And(vec![
WhereClause::Field {
path: vec!["active".to_string()],
operator: WhereOperator::Eq,
value: json!(true),
},
WhereClause::Field {
path: vec!["age".to_string()],
operator: WhereOperator::Gte,
value: json!(25),
},
]);
let results = adapter
.execute_where_query("v_user", Some(&where_clause), None, None, None)
.await
.expect("Failed to execute query");
for result in &results {
assert_eq!(result.as_value()["active"], true);
let age = result.as_value()["age"].as_i64().unwrap();
assert!(age >= 25);
}
}
#[tokio::test]
async fn test_where_or() {
let adapter = create_test_adapter().await;
let where_clause = WhereClause::Or(vec![
WhereClause::Field {
path: vec!["role".to_string()],
operator: WhereOperator::Eq,
value: json!("admin"),
},
WhereClause::Field {
path: vec!["role".to_string()],
operator: WhereOperator::Eq,
value: json!("moderator"),
},
]);
let results = adapter
.execute_where_query("v_user", Some(&where_clause), None, None, None)
.await
.expect("Failed to execute query");
assert!(results.len() >= 2);
for result in &results {
let role = result.as_value()["role"].as_str().unwrap();
assert!(role == "admin" || role == "moderator");
}
}
#[tokio::test]
async fn test_where_not() {
let adapter = create_test_adapter().await;
let where_clause = WhereClause::Not(Box::new(WhereClause::Field {
path: vec!["active".to_string()],
operator: WhereOperator::Eq,
value: json!(true),
}));
let results = adapter
.execute_where_query("v_user", Some(&where_clause), None, None, None)
.await
.expect("Failed to execute query");
for result in &results {
assert_ne!(result.as_value()["active"], json!(true));
}
}
#[tokio::test]
async fn test_where_in() {
let adapter = create_test_adapter().await;
let where_clause = WhereClause::Field {
path: vec!["role".to_string()],
operator: WhereOperator::In,
value: json!(["admin", "moderator"]),
};
let results = adapter
.execute_where_query("v_user", Some(&where_clause), None, None, None)
.await
.expect("Failed to execute query");
assert!(results.len() >= 2);
for result in &results {
let role = result.as_value()["role"].as_str().unwrap();
assert!(role == "admin" || role == "moderator");
}
}
#[tokio::test]
async fn test_limit() {
let adapter = create_test_adapter().await;
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_offset() {
let adapter = create_test_adapter().await;
let results_all = adapter
.execute_where_query("v_user", None, None, None, None)
.await
.expect("Failed to execute query");
let results_offset = adapter
.execute_where_query("v_user", None, None, Some(2), None)
.await
.expect("Failed to execute query");
assert_eq!(results_offset.len(), results_all.len() - 2);
}
#[tokio::test]
async fn test_limit_and_offset() {
let adapter = create_test_adapter().await;
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_nested_object_query() {
let adapter = create_test_adapter().await;
let where_clause = WhereClause::Field {
path: vec!["metadata".to_string(), "city".to_string()],
operator: WhereOperator::Eq,
value: json!("Paris"),
};
let results = adapter
.execute_where_query("v_user", Some(&where_clause), None, None, None)
.await
.expect("Failed to execute query");
assert!(!results.is_empty());
for result in &results {
assert_eq!(result.as_value()["metadata"]["city"], "Paris");
}
}
#[tokio::test]
async fn test_complex_nested_where() {
let adapter = create_test_adapter().await;
let where_clause = WhereClause::And(vec![
WhereClause::Field {
path: vec!["active".to_string()],
operator: WhereOperator::Eq,
value: json!(true),
},
WhereClause::Or(vec![
WhereClause::Field {
path: vec!["role".to_string()],
operator: WhereOperator::Eq,
value: json!("admin"),
},
WhereClause::Field {
path: vec!["age".to_string()],
operator: WhereOperator::Gte,
value: json!(30),
},
]),
]);
let results = adapter
.execute_where_query("v_user", Some(&where_clause), None, None, None)
.await
.expect("Failed to execute query");
for result in &results {
assert_eq!(result.as_value()["active"], true);
let role = result.as_value()["role"].as_str().unwrap();
let age = result.as_value()["age"].as_i64().unwrap();
assert!(role == "admin" || age >= 30);
}
}
#[tokio::test]
async fn test_invalid_view_name() {
let adapter = create_test_adapter().await;
let result = adapter.execute_where_query("v_nonexistent", None, None, None, None).await;
assert!(
matches!(result, Err(FraiseQLError::Database { .. })),
"expected Database error for nonexistent view, got: {result:?}"
);
}
#[tokio::test]
async fn test_invalid_connection_string() {
let result =
PostgresAdapter::new("postgresql://invalid:invalid@localhost:9999/nonexistent").await;
assert!(
matches!(result, Err(FraiseQLError::ConnectionPool { .. })),
"expected ConnectionPool error for invalid connection string, got: {result:?}"
);
}
#[tokio::test]
async fn test_parameterized_limit_only() {
let adapter = create_test_adapter().await;
let results = adapter
.execute_where_query("v_user", None, Some(3), None, None)
.await
.expect("Failed to execute query");
assert_eq!(results.len(), 3, "Should return exactly 3 results with parameterized LIMIT");
}
#[tokio::test]
async fn test_parameterized_offset_only() {
let adapter = create_test_adapter().await;
let results_all = adapter
.execute_where_query("v_user", None, None, None, None)
.await
.expect("Failed to execute query");
let offset_val = 1;
let results_offset = adapter
.execute_where_query("v_user", None, None, Some(offset_val), None)
.await
.expect("Failed to execute query");
assert_eq!(results_offset.len(), results_all.len() - offset_val as usize);
}
#[tokio::test]
async fn test_parameterized_limit_and_offset() {
let adapter = create_test_adapter().await;
let limit_val = 2;
let offset_val = 1;
let results = adapter
.execute_where_query("v_user", None, Some(limit_val), Some(offset_val), None)
.await
.expect("Failed to execute query");
assert_eq!(results.len(), limit_val as usize, "Should return exactly {} results", limit_val);
}
#[tokio::test]
async fn test_parameterized_limit_with_where_clause() {
let adapter = create_test_adapter().await;
let where_clause = WhereClause::Field {
path: vec!["active".to_string()],
operator: WhereOperator::Eq,
value: json!(true),
};
let results = adapter
.execute_where_query("v_user", Some(&where_clause), Some(2), None, None)
.await
.expect("Failed to execute query");
assert!(results.len() <= 2);
for result in &results {
assert_eq!(result.as_value()["active"], true);
}
}
#[tokio::test]
async fn test_parameterized_limit_and_offset_with_where_clause() {
let adapter = create_test_adapter().await;
let where_clause = WhereClause::Field {
path: vec!["active".to_string()],
operator: WhereOperator::Eq,
value: json!(true),
};
let results = adapter
.execute_where_query("v_user", Some(&where_clause), Some(2), Some(1), None)
.await
.expect("Failed to execute query");
assert!(results.len() <= 2);
for result in &results {
assert_eq!(result.as_value()["active"], true);
}
}
#[tokio::test]
async fn test_mutation_timing_disabled_by_default() {
let adapter = create_test_adapter().await;
assert!(!adapter.mutation_timing_enabled());
}
#[tokio::test]
async fn test_mutation_timing_with_builder() {
let adapter = create_test_adapter().await.with_mutation_timing("fraiseql.started_at");
assert!(adapter.mutation_timing_enabled());
}
#[tokio::test]
async fn test_mutation_timing_custom_variable() {
let adapter = create_test_adapter().await.with_mutation_timing("app.custom_timer");
assert!(adapter.mutation_timing_enabled());
}
#[tokio::test]
async fn test_execute_function_call_with_timing_disabled() {
let adapter = create_test_adapter().await;
let result = adapter.execute_function_call("fn_nonexistent", &[]).await;
assert!(
matches!(result, Err(FraiseQLError::Database { .. })),
"expected Database error for nonexistent function (timing disabled), got: {result:?}"
);
}
#[tokio::test]
async fn test_execute_function_call_with_timing_enabled() {
let adapter = create_test_adapter().await.with_mutation_timing("fraiseql.started_at");
let result = adapter.execute_function_call("fn_nonexistent", &[]).await;
assert!(
matches!(result, Err(FraiseQLError::Database { .. })),
"expected Database error for nonexistent function (timing enabled), got: {result:?}"
);
}
#[tokio::test]
async fn execute_raw_query_surfaces_sqlstate_22p02_for_malformed_cast() {
let adapter = create_test_adapter().await;
let result = adapter.execute_raw_query("SELECT 'not-a-uuid'::uuid").await;
match result {
Err(FraiseQLError::Database { sql_state, .. }) => {
assert_eq!(
sql_state.as_deref(),
Some("22P02"),
"a malformed uuid cast must surface SQLSTATE 22P02 (class 22 → HTTP 400)"
);
},
other => panic!("expected Database error carrying sql_state, got: {other:?}"),
}
}
#[tokio::test]
async fn pool_prewarms_to_min_size() {
let adapter = PostgresAdapter::with_pool_config(
TEST_DB_URL,
PoolPrewarmConfig {
min_size: 5,
max_size: 20,
timeout_secs: None,
},
)
.await
.expect("adapter should be created");
let metrics = adapter.pool_metrics();
assert!(
metrics.idle_connections >= 5,
"expected >=5 idle connections after pre-warm, got {}",
metrics.idle_connections
);
}
#[tokio::test]
async fn pool_prewarm_zero_min_size_creates_one_connection() {
let adapter = PostgresAdapter::with_pool_config(
TEST_DB_URL,
PoolPrewarmConfig {
min_size: 0,
max_size: 10,
timeout_secs: None,
},
)
.await
.expect("adapter should be created");
let metrics = adapter.pool_metrics();
assert_eq!(
metrics.idle_connections, 1,
"expected exactly 1 idle connection with min_size=0"
);
}
#[tokio::test]
async fn pool_prewarm_min_capped_at_max() {
let adapter = PostgresAdapter::with_pool_config(
TEST_DB_URL,
PoolPrewarmConfig {
min_size: 100,
max_size: 3,
timeout_secs: None,
},
)
.await
.expect("adapter should not panic when min_size > max_size");
let metrics = adapter.pool_metrics();
assert!(
metrics.idle_connections <= 3,
"idle connections ({}) should not exceed max_size (3)",
metrics.idle_connections
);
}
#[tokio::test]
async fn pool_timeout_causes_fast_failure_when_exhausted() {
let adapter = PostgresAdapter::with_pool_config(
TEST_DB_URL,
PoolPrewarmConfig {
min_size: 1,
max_size: 1,
timeout_secs: Some(1),
},
)
.await
.expect("adapter created");
let _held = adapter.pool().get().await.expect("first get ok");
let start = std::time::Instant::now();
let result = adapter.acquire_connection_with_retry().await;
let elapsed = start.elapsed();
assert!(result.is_err(), "should fail when pool exhausted");
assert!(elapsed.as_secs() < 3, "timeout should fail fast, took {}s", elapsed.as_secs());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("timeout") || err_msg.contains("busy"),
"error should mention exhaustion, got: {err_msg}"
);
}
#[tokio::test]
async fn acquire_does_not_retry_on_timeout_error() {
let adapter = PostgresAdapter::with_pool_config(
TEST_DB_URL,
PoolPrewarmConfig {
min_size: 1,
max_size: 1,
timeout_secs: Some(1),
},
)
.await
.expect("adapter created");
let _hold = adapter.pool().get().await.unwrap();
let err = adapter.acquire_connection_with_retry().await.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("timeout") || msg.contains("busy"),
"error should mention exhaustion, got: {msg}"
);
}
#[tokio::test]
async fn execute_raw_null_data_column_errors_instead_of_panicking() {
let adapter = create_test_adapter().await;
let result = adapter.execute_raw("SELECT NULL::jsonb AS data", &[]).await;
match result {
Err(FraiseQLError::Database { message, .. }) => {
assert!(
message.contains("NULL `data`"),
"expected a NULL-data Database error naming the column, got: {message}"
);
},
other => panic!("expected FraiseQLError::Database for NULL data, got: {other:?}"),
}
}
#[tokio::test]
async fn execute_raw_non_jsonb_data_column_errors_instead_of_panicking() {
let adapter = create_test_adapter().await;
let result = adapter.execute_raw("SELECT 42 AS data", &[]).await;
assert!(
matches!(result, Err(FraiseQLError::Database { .. })),
"expected FraiseQLError::Database for a non-JSONB data column, got: {result:?}"
);
}
#[tokio::test]
async fn row_to_map_renders_numeric_uuid_and_timestamp() {
let adapter = create_test_adapter().await;
let rows = adapter
.execute_raw_query(
"SELECT \
SUM(v)::numeric AS revenue, \
'550e8400-e29b-41d4-a716-446655440000'::uuid AS uid, \
'2024-01-15T10:30:00Z'::timestamptz AS ts \
FROM (VALUES (100.50::numeric), (200.25::numeric)) t(v)",
)
.await
.expect("query failed");
let row = &rows[0];
assert_eq!(row["revenue"], json!(300.75), "NUMERIC SUM must not be null");
assert_eq!(
row["uid"],
json!("550e8400-e29b-41d4-a716-446655440000"),
"UUID must not be null"
);
assert!(
row["ts"].as_str().is_some_and(|s| s.starts_with("2024-01-15")),
"timestamptz must render as ISO text, got: {:?}",
row["ts"]
);
}
#[tokio::test]
async fn row_to_map_renders_smallint() {
let adapter = create_test_adapter().await;
let rows = adapter
.execute_raw_query("SELECT 404::int2 AS http_status")
.await
.expect("query failed");
let row = &rows[0];
assert_eq!(row["http_status"], json!(404), "SMALLINT/int2 must not be null");
}
#[tokio::test]
async fn row_to_map_type_conformance() {
let adapter = create_test_adapter().await;
let rows = adapter
.execute_raw_query(
"SELECT \
7::int2 AS c_smallint, \
1::int4 AS c_int, \
9000000000::int8 AS c_bigint, \
1.5::float8 AS c_float, \
true AS c_bool, \
'hi'::text AS c_text, \
42.42::numeric AS c_numeric, \
'11111111-2222-3333-4444-555555555555'::uuid AS c_uuid, \
'2030-06-13T08:00:00Z'::timestamptz AS c_tstz, \
'2030-06-13 08:00:00'::timestamp AS c_ts, \
'2030-06-13'::date AS c_date, \
ARRAY['a','b']::text[] AS c_textarr, \
'{\"k\":1}'::jsonb AS c_jsonb, \
NULL::text AS c_null",
)
.await
.expect("query failed");
let r = &rows[0];
assert_eq!(r["c_smallint"], json!(7));
assert_eq!(r["c_int"], json!(1));
assert_eq!(r["c_bigint"], json!(9_000_000_000i64));
assert_eq!(r["c_float"], json!(1.5));
assert_eq!(r["c_bool"], json!(true));
assert_eq!(r["c_text"], json!("hi"));
assert_eq!(r["c_numeric"], json!(42.42));
assert_eq!(r["c_uuid"], json!("11111111-2222-3333-4444-555555555555"));
assert!(r["c_tstz"].as_str().is_some_and(|s| s.starts_with("2030-06-13")));
assert!(r["c_ts"].as_str().is_some_and(|s| s.starts_with("2030-06-13")));
assert_eq!(r["c_date"], json!("2030-06-13"));
assert_eq!(r["c_textarr"], json!(["a", "b"]));
assert_eq!(r["c_jsonb"], json!({"k": 1}));
assert_eq!(r["c_null"], serde_json::Value::Null);
}