#![cfg(feature = "postgres")]
use postgrest_parser::{parse_query_string, QueryBuilder, SchemaCache};
use sqlx::postgres::PgPoolOptions;
use std::sync::Arc;
async fn get_pool() -> sqlx::PgPool {
PgPoolOptions::new()
.max_connections(5)
.connect("postgres://postgres:postgres@localhost:5433/postgrest_parser_test")
.await
.expect("Failed to connect to test database. Is docker-compose up?")
}
#[tokio::test]
async fn test_customers_with_filters_and_ordering() {
let pool = get_pool().await;
let cache = Arc::new(SchemaCache::load_from_database(&pool).await.unwrap());
let params =
parse_query_string("select=id,name,email&metadata->>tier=eq.gold&order=name.asc").unwrap();
let mut builder = QueryBuilder::new()
.with_schema_cache(cache)
.with_schema("public");
let result = builder.build_select("customers", ¶ms).unwrap();
let rows = sqlx::query(&result.query)
.bind(result.params[0].as_str().unwrap()) .fetch_all(&pool)
.await
.unwrap();
assert!(!rows.is_empty(), "Should find gold tier customers");
println!("Found {} gold tier customers", rows.len());
}
#[tokio::test]
async fn test_orders_with_customer_details() {
let pool = get_pool().await;
let cache = Arc::new(SchemaCache::load_from_database(&pool).await.unwrap());
let params = parse_query_string(
"select=id,total_amount,status,customers(name,email)&status=eq.completed&order=total_amount.desc"
).unwrap();
let mut builder = QueryBuilder::new()
.with_schema_cache(cache)
.with_schema("public");
let result = builder.build_select("orders", ¶ms).unwrap();
println!("Generated SQL:\n{}\n", result.query);
assert!(result.query.contains("WHERE"));
assert!(result.query.contains("ORDER BY"));
assert!(result
.query
.contains(r#""orders"."customer_id" = "customers"."id""#));
let status_value = result.params[0].as_str().unwrap();
let rows = sqlx::query(&result.query)
.bind(status_value)
.fetch_all(&pool)
.await
.unwrap();
assert!(!rows.is_empty(), "Should find completed orders");
}
#[tokio::test]
async fn test_customers_with_all_orders() {
let pool = get_pool().await;
let cache = Arc::new(SchemaCache::load_from_database(&pool).await.unwrap());
let params =
parse_query_string("select=id,name,email,orders(id,status,total_amount)&limit=10").unwrap();
let mut builder = QueryBuilder::new()
.with_schema_cache(cache)
.with_schema("public");
let result = builder.build_select("customers", ¶ms).unwrap();
println!("SQL:\n{}\n", result.query);
let limit_value = result.params[0].as_i64().unwrap();
let rows = sqlx::query(&result.query)
.bind(limit_value)
.fetch_all(&pool)
.await
.unwrap();
assert!(!rows.is_empty(), "Should return customers");
assert!(rows.len() <= 10, "Should respect limit");
}
#[tokio::test]
async fn test_pagination_with_offset() {
let pool = get_pool().await;
let cache = Arc::new(SchemaCache::load_from_database(&pool).await.unwrap());
let params =
parse_query_string("select=id,name,price&order=price.desc&limit=3&offset=2").unwrap();
let mut builder = QueryBuilder::new()
.with_schema_cache(cache)
.with_schema("public");
let result = builder.build_select("products", ¶ms).unwrap();
let limit_value = result.params[0].as_i64().unwrap();
let offset_value = result.params[1].as_i64().unwrap();
let rows = sqlx::query(&result.query)
.bind(limit_value)
.bind(offset_value)
.fetch_all(&pool)
.await
.unwrap();
assert!(rows.len() <= 3, "Should respect limit");
}
#[tokio::test]
async fn test_range_filters() {
let pool = get_pool().await;
let cache = Arc::new(SchemaCache::load_from_database(&pool).await.unwrap());
let params = parse_query_string("select=id,name,price&price=gte.50&price=lte.150").unwrap();
let mut builder = QueryBuilder::new()
.with_schema_cache(cache)
.with_schema("public");
let result = builder.build_select("products", ¶ms).unwrap();
println!("Range filter SQL:\n{}\n", result.query);
println!("Params: {:?}", result.params);
assert_eq!(result.params.len(), 2, "Should have two filter parameters");
assert!(result.query.contains(">="), "Should have >= operator");
assert!(result.query.contains("<="), "Should have <= operator");
let min_price = if result.params[0].is_string() {
result.params[0].as_str().unwrap().parse::<f64>().unwrap()
} else {
result.params[0].as_f64().unwrap()
};
let max_price = if result.params[1].is_string() {
result.params[1].as_str().unwrap().parse::<f64>().unwrap()
} else {
result.params[1].as_f64().unwrap()
};
let rows = sqlx::query(&result.query)
.bind(min_price)
.bind(max_price)
.fetch_all(&pool)
.await
.unwrap();
println!("Found {} products in price range 50-150", rows.len());
assert!(!rows.is_empty(), "Should find products in price range");
}
#[tokio::test]
async fn test_in_operator_with_list() {
let pool = get_pool().await;
let cache = Arc::new(SchemaCache::load_from_database(&pool).await.unwrap());
let params =
parse_query_string("select=id,status,total_amount&status=in.(pending,completed)").unwrap();
let mut builder = QueryBuilder::new()
.with_schema_cache(cache)
.with_schema("public");
let result = builder.build_select("orders", ¶ms).unwrap();
println!("IN operator SQL:\n{}\n", result.query);
assert!(result.query.contains("ANY"));
let statuses = result.params[0].as_array().unwrap();
let status_strs: Vec<&str> = statuses.iter().map(|v| v.as_str().unwrap()).collect();
let rows = sqlx::query(&result.query)
.bind(&status_strs[..]) .fetch_all(&pool)
.await
.unwrap();
assert!(
!rows.is_empty(),
"Should find orders with specified statuses"
);
}
#[tokio::test]
async fn test_or_logic_filter() {
let pool = get_pool().await;
let cache = Arc::new(SchemaCache::load_from_database(&pool).await.unwrap());
let params = parse_query_string(
"select=id,name,category&or=(category.eq.Electronics,category.eq.Office)",
)
.unwrap();
let mut builder = QueryBuilder::new()
.with_schema_cache(cache)
.with_schema("public");
let result = builder.build_select("products", ¶ms).unwrap();
println!("OR logic SQL:\n{}\n", result.query);
let cat1 = result.params[0].as_str().unwrap();
let cat2 = result.params[1].as_str().unwrap();
let rows = sqlx::query(&result.query)
.bind(cat1)
.bind(cat2)
.fetch_all(&pool)
.await
.unwrap();
assert!(!rows.is_empty(), "Should find products in either category");
}
#[tokio::test]
async fn test_pattern_matching() {
let pool = get_pool().await;
let cache = Arc::new(SchemaCache::load_from_database(&pool).await.unwrap());
let params = parse_query_string("select=id,name,price&name=like.*Laptop*").unwrap();
let mut builder = QueryBuilder::new()
.with_schema_cache(cache)
.with_schema("public");
let result = builder.build_select("products", ¶ms).unwrap();
println!("Pattern matching SQL:\n{}\n", result.query);
println!("Params: {:?}", result.params);
let pattern = result.params[0].as_str().unwrap();
let rows = sqlx::query(&result.query)
.bind(pattern)
.fetch_all(&pool)
.await
.unwrap();
println!(
"Found {} products matching pattern '{}'",
rows.len(),
pattern
);
if rows.is_empty() {
println!("Warning: No matches found. Check pattern conversion.");
}
}
#[tokio::test]
async fn test_null_handling() {
let pool = get_pool().await;
let cache = Arc::new(SchemaCache::load_from_database(&pool).await.unwrap());
let params = parse_query_string("select=id,status,notes¬es=is.null").unwrap();
let mut builder = QueryBuilder::new()
.with_schema_cache(cache)
.with_schema("public");
let result = builder.build_select("orders", ¶ms).unwrap();
println!("NULL check SQL:\n{}\n", result.query);
assert!(result.query.contains("IS NULL"));
let rows = sqlx::query(&result.query).fetch_all(&pool).await.unwrap();
println!("Found {} orders with null notes", rows.len());
}
#[tokio::test]
async fn test_junction_table_many_to_many() {
let pool = get_pool().await;
let cache = Arc::new(SchemaCache::load_from_database(&pool).await.unwrap());
let params = parse_query_string("select=id,title,published&published=eq.true").unwrap();
let mut builder = QueryBuilder::new()
.with_schema_cache(cache)
.with_schema("public");
let result = builder.build_select("posts", ¶ms).unwrap();
println!("Posts query SQL:\n{}\n", result.query);
println!("Params: {:?}", result.params);
let published_value = if result.params[0].is_boolean() {
result.params[0].as_bool().unwrap()
} else if result.params[0].is_string() {
result.params[0].as_str().unwrap() == "true"
} else {
true
};
let rows = sqlx::query(&result.query)
.bind(published_value)
.fetch_all(&pool)
.await
.unwrap();
assert!(!rows.is_empty(), "Should find published posts");
}
#[tokio::test]
async fn test_one_to_one_relationship() {
let pool = get_pool().await;
let cache = Arc::new(SchemaCache::load_from_database(&pool).await.unwrap());
let params = parse_query_string("select=id,name,customer_profiles(bio,avatar_url)").unwrap();
let mut builder = QueryBuilder::new()
.with_schema_cache(cache)
.with_schema("public");
let result = builder.build_select("customers", ¶ms).unwrap();
println!("One-to-one SQL:\n{}\n", result.query);
assert!(result
.query
.contains(r#""customer_profiles"."customer_id" = "customers"."id""#));
let rows = sqlx::query(&result.query).fetch_all(&pool).await.unwrap();
assert!(!rows.is_empty(), "Should return customers");
}
#[tokio::test]
async fn test_complex_combined_query() {
let pool = get_pool().await;
let cache = Arc::new(SchemaCache::load_from_database(&pool).await.unwrap());
let params = parse_query_string(
"select=name,email,orders(id,status,total_amount)&\
metadata->>tier=in.(gold,silver)&\
order=name.asc&\
limit=5",
)
.unwrap();
let mut builder = QueryBuilder::new()
.with_schema_cache(cache)
.with_schema("public");
let result = builder.build_select("customers", ¶ms).unwrap();
println!("Complex query SQL:\n{}\n", result.query);
assert!(result.query.contains("WHERE"));
assert!(result.query.contains("ORDER BY"));
assert!(result.query.contains("LIMIT"));
assert!(result
.query
.contains(r#""orders"."customer_id" = "customers"."id""#));
let tier_array = result.params[0].as_array().unwrap();
let tier_strs: Vec<&str> = tier_array.iter().map(|v| v.as_str().unwrap()).collect();
let limit_value = result.params[1].as_i64().unwrap();
let rows = sqlx::query(&result.query)
.bind(&tier_strs[..]) .bind(limit_value)
.fetch_all(&pool)
.await
.unwrap();
println!("Found {} matching customers", rows.len());
assert!(rows.len() <= 5, "Should respect limit");
}