use super::*;
use crate::parse_query_file;
use camino::Utf8Path;
use dibs_db_schema::{Column, ForeignKey, PgType, Schema, SourceLocation, Table};
use facet_testhelpers::test;
fn parse_test(source: &str) -> (crate::QueryFile, Arc<crate::QSource>) {
parse_query_file(Utf8Path::new("<test>"), source).unwrap()
}
type ColDef = (&'static str, PgType, bool);
fn make_test_table(name: &str, columns: &[ColDef], fks: Vec<ForeignKey>) -> Table {
Table {
name: name.to_string(),
columns: columns
.iter()
.map(|(col_name, pg_type, nullable)| Column {
name: col_name.to_string(),
pg_type: *pg_type,
rust_type: Some(pg_type.to_rust_type().to_string()),
nullable: *nullable,
default: None,
primary_key: *col_name == "id",
unique: false,
auto_generated: false,
long: false,
label: false,
enum_variants: vec![],
doc: None,
icon: None,
lang: None,
subtype: None,
})
.collect(),
check_constraints: vec![],
trigger_checks: vec![],
foreign_keys: fks,
indices: vec![],
source: SourceLocation::default(),
doc: None,
icon: None,
}
}
fn make_test_schema(tables: Vec<Table>) -> Schema {
Schema {
tables: tables.into_iter().map(|t| (t.name.clone(), t)).collect(),
}
}
fn empty_schema() -> Schema {
Schema::default()
}
#[test]
fn test_generate_simple_query() {
let source = r#"
AllProducts @select{
from product
fields { id, handle, status }
}
"#;
let (file, qsource) = parse_test(source);
let schema = make_test_schema(vec![make_test_table(
"product",
&[
("id", PgType::BigInt, false),
("handle", PgType::Text, false),
("status", PgType::Text, false),
],
vec![],
)]);
let code = generate_rust_code(&file, &schema, qsource).unwrap();
assert!(code.code.contains("pub struct AllProductsResult"));
assert!(code.code.contains("pub async fn all_products"));
assert!(code.code.contains("pub id: i64"));
assert!(code.code.contains("pub handle: String"));
assert!(code.code.contains("#[derive(Debug, Clone, Facet)]"));
}
#[test]
fn test_generate_query_with_params() {
let source = r#"
ProductByHandle @select{
params { handle @string }
from product
where { handle $handle }
first true
fields { id, handle }
}
"#;
let (file, qsource) = parse_test(source);
let schema = make_test_schema(vec![make_test_table(
"product",
&[
("id", PgType::BigInt, false),
("handle", PgType::Text, false),
],
vec![],
)]);
let code = generate_rust_code(&file, &schema, qsource).unwrap();
assert!(code.code.contains("handle: &String"));
assert!(code.code.contains("Ok(None)"));
assert!(code.code.contains("ProductByHandleResult {"));
}
#[test]
fn test_generate_query_with_float_param() {
let source = r#"
ClipsLongerThan @select{
params { min_seconds @float }
from clip
where { duration_s @gte($min_seconds) }
fields { id, duration_s }
}
"#;
let (file, qsource) = parse_test(source);
let schema = make_test_schema(vec![make_test_table(
"clip",
&[
("id", PgType::BigInt, false),
("duration_s", PgType::DoublePrecision, false),
],
vec![],
)]);
let code = generate_rust_code(&file, &schema, qsource).unwrap();
assert!(
code.code.contains("min_seconds: &f64"),
"float param should generate an &f64 argument. Got:\n{}",
code.code
);
}
#[test]
fn test_generate_query_with_relation() {
let source = r#"
ProductListing @select{
from product
fields {
id
translation @rel{
from product_translation
first true
fields { title, description }
}
}
}
"#;
let (file, qsource) = parse_test(source);
let schema = make_test_schema(vec![
make_test_table("product", &[("id", PgType::BigInt, false)], vec![]),
make_test_table(
"product_translation",
&[
("id", PgType::BigInt, false),
("product_id", PgType::BigInt, false),
("title", PgType::Text, false),
("description", PgType::Text, true),
],
vec![ForeignKey {
columns: vec!["product_id".to_string()],
references_table: "product".to_string(),
references_columns: vec!["id".to_string()],
}],
),
]);
let code = generate_rust_code(&file, &schema, qsource).unwrap();
assert!(
code.code
.contains("pub translation: Option<ProductListingTranslation>")
);
assert!(code.code.contains("pub struct ProductListingTranslation"));
assert!(code.code.contains("pub title: String"));
}
#[test]
fn test_generate_raw_sql_query() {
let source = r#"
TrendingProducts @select{
params { locale @string, days @int }
sql <<SQL
SELECT id, title FROM products WHERE locale = $1
SQL
returns { id @int, title @string }
}
"#;
let (file, qsource) = parse_test(source);
let code = generate_rust_code(&file, &empty_schema(), qsource).unwrap();
assert!(code.code.contains("locale: &String"));
assert!(code.code.contains("days: &i64"));
assert!(code.code.contains("pub id: i64"));
assert!(code.code.contains("pub title: String"));
assert!(code.code.contains("SELECT id, title FROM products"));
}
#[test]
fn test_snake_case() {
assert_eq!(to_snake_case("ProductListing"), "product_listing");
assert_eq!(to_snake_case("AllProducts"), "all_products");
assert_eq!(to_snake_case("ID"), "i_d");
}
#[test]
fn test_pascal_case() {
assert_eq!(to_pascal_case("translation"), "Translation");
assert_eq!(to_pascal_case("product_variant"), "ProductVariant");
}
#[test]
fn test_generate_join_query() {
let source = r#"
ProductWithTranslation @select{
params { handle @string }
from product
where { handle $handle }
first true
fields {
id, handle, translation @rel{
from product_translation
first true
fields { title, description }
}
}
}
"#;
let (file, qsource) = parse_test(source);
let schema = make_test_schema(vec![
make_test_table(
"product",
&[
("id", PgType::BigInt, false),
("handle", PgType::Text, false),
],
vec![],
),
make_test_table(
"product_translation",
&[
("id", PgType::BigInt, false),
("product_id", PgType::BigInt, false),
("title", PgType::Text, false),
("description", PgType::Text, true),
],
vec![ForeignKey {
columns: vec!["product_id".to_string()],
references_table: "product".to_string(),
references_columns: vec!["id".to_string()],
}],
),
]);
let code = generate_rust_code(&file, &schema, qsource).unwrap();
tracing::info!("Generated code:\n{}", code.code);
assert!(
code.code
.contains("pub struct ProductWithTranslationResult")
);
assert!(code.code.contains("pub id: i64"));
assert!(code.code.contains("pub handle: String"));
assert!(
code.code
.contains("pub translation: Option<ProductWithTranslationTranslation>")
);
assert!(
code.code
.contains("pub struct ProductWithTranslationTranslation")
);
assert!(code.code.contains("LEFT JOIN"));
assert!(code.code.contains("product_translation"));
assert!(code.code.contains("translation_title"));
assert!(code.code.contains("translation_description"));
assert!(
code.code
.contains(".map(|_| ProductWithTranslationTranslation"),
"Expected relation construction inside .map() call"
);
assert!(
code.code.contains("title: flat_row.translation_title"),
"Expected title field assignment from flat row"
);
}
#[test]
fn test_generate_vec_relation_query() {
let source = r#"
ProductWithVariants @select{
from product
fields {
id, handle, variants @rel{
from product_variant
fields { id, sku }
}
}
}
"#;
let (file, qsource) = parse_test(source);
let schema = make_test_schema(vec![
make_test_table(
"product",
&[
("id", PgType::BigInt, false),
("handle", PgType::Text, false),
],
vec![],
),
make_test_table(
"product_variant",
&[
("id", PgType::BigInt, false),
("product_id", PgType::BigInt, false),
("sku", PgType::Text, false),
],
vec![ForeignKey {
columns: vec!["product_id".to_string()],
references_table: "product".to_string(),
references_columns: vec!["id".to_string()],
}],
),
]);
let code = generate_rust_code(&file, &schema, qsource).unwrap();
tracing::info!("Generated code:\n{}", code.code);
assert!(
code.code.contains("pub struct ProductWithVariantsResult"),
"Should generate result struct"
);
assert!(code.code.contains("pub id: i64"), "Should have id field");
assert!(
code.code.contains("pub handle: String"),
"Should have handle field"
);
assert!(
code.code
.contains("pub variants: Vec<ProductWithVariantsVariants>"),
"Should have Vec variants field"
);
assert!(
code.code.contains("pub struct ProductWithVariantsVariants"),
"Should generate nested Variants struct"
);
assert!(code.code.contains("LEFT JOIN"), "Should use LEFT JOIN");
assert!(
code.code.contains("product_variant"),
"Should join product_variant"
);
assert!(
code.code.contains("HashMap"),
"Should use HashMap for grouping"
);
assert!(
code.code.contains("grouped.entry"),
"Should use entry API for grouping"
);
assert!(code.code.contains(".push("), "Should push to Vec relation");
assert!(
code.code.contains("variants: Vec::new()"),
"Should initialize Vec as empty"
);
assert!(
code.code.contains("entry.variants.push"),
"Should append to variants"
);
}
#[test]
fn test_generate_count_query() {
let source = r#"
ProductWithVariantCount @select{
from product
fields { id, handle, variant_count @count(product_variant) }
}
"#;
let (file, qsource) = parse_test(source);
let schema = make_test_schema(vec![
make_test_table(
"product",
&[
("id", PgType::BigInt, false),
("handle", PgType::Text, false),
],
vec![],
),
make_test_table(
"product_variant",
&[
("id", PgType::BigInt, false),
("product_id", PgType::BigInt, false),
("sku", PgType::Text, false),
],
vec![ForeignKey {
columns: vec!["product_id".to_string()],
references_table: "product".to_string(),
references_columns: vec!["id".to_string()],
}],
),
]);
let code = generate_rust_code(&file, &schema, qsource).unwrap();
tracing::info!("Generated code:\n{}", code.code);
assert!(
code.code
.contains("pub struct ProductWithVariantCountResult"),
"Should generate result struct"
);
assert!(
code.code.contains("pub variant_count: i64"),
"Should have variant_count field as i64"
);
assert!(
code.code.contains("SELECT COUNT(*)"),
"Should generate COUNT subquery in SQL"
);
assert!(
code.code.contains("product_variant"),
"Should reference product_variant table in COUNT"
);
assert!(
code.code.contains("variant_count"),
"Should alias the COUNT result"
);
}
#[test]
fn test_generate_nested_vec_relation_query() {
let source = r#"
ProductWithVariantsAndPrices @select{
from product
fields {
id, handle, variants @rel{
from product_variant
fields { id, sku, prices @rel{
from variant_price
fields { id, currency_code, amount }
}}
}
}
}
"#;
let (file, qsource) = parse_test(source);
let schema = make_test_schema(vec![
make_test_table(
"product",
&[
("id", PgType::BigInt, false),
("handle", PgType::Text, false),
],
vec![],
),
make_test_table(
"product_variant",
&[
("id", PgType::BigInt, false),
("product_id", PgType::BigInt, false),
("sku", PgType::Text, false),
],
vec![ForeignKey {
columns: vec!["product_id".to_string()],
references_table: "product".to_string(),
references_columns: vec!["id".to_string()],
}],
),
make_test_table(
"variant_price",
&[
("id", PgType::BigInt, false),
("variant_id", PgType::BigInt, false),
("currency_code", PgType::Text, false),
("amount", PgType::BigInt, false),
],
vec![ForeignKey {
columns: vec!["variant_id".to_string()],
references_table: "product_variant".to_string(),
references_columns: vec!["id".to_string()],
}],
),
]);
let code = generate_rust_code(&file, &schema, qsource).unwrap();
tracing::info!("Generated code:\n{}", code.code);
assert!(
code.code
.contains("pub struct ProductWithVariantsAndPricesResult"),
"Should generate top-level result struct"
);
assert!(
code.code
.contains("pub struct ProductWithVariantsAndPricesVariants"),
"Should generate nested Variants struct"
);
assert!(
code.code
.contains("pub struct ProductWithVariantsAndPricesVariantsPrices"),
"Should generate nested Prices struct"
);
assert!(
code.code
.contains("pub variants: Vec<ProductWithVariantsAndPricesVariants>"),
"Should have Vec variants field"
);
assert!(
code.code
.contains("pub prices: Vec<ProductWithVariantsAndPricesVariantsPrices>"),
"Should have Vec prices field in Variants"
);
assert!(code.code.contains("LEFT JOIN"), "Should use LEFT JOIN");
assert!(
code.code.contains("product_variant"),
"Should join product_variant"
);
assert!(
code.code.contains("variant_price"),
"Should join variant_price"
);
assert!(
code.code.contains("variants_prices_currency_code"),
"Should have nested column alias"
);
assert!(
code.code.contains("HashSet"),
"Should use HashSet for deduplication"
);
assert!(
code.code.contains("seen_variants"),
"Should track seen variants"
);
assert!(
code.code.contains("seen_variants_prices"),
"Should track seen nested prices"
);
}
#[test]
fn test_generate_insert_code() {
let source = r#"
CreateUser @insert{
params { name @string, email @string }
into users
values { name $name, email $email, created_at @now }
returning { id, name, email, created_at }
}
"#;
let (file, qsource) = parse_test(source);
let schema = make_test_schema(vec![make_test_table(
"users",
&[
("id", PgType::BigInt, false),
("name", PgType::Text, false),
("email", PgType::Text, false),
("created_at", PgType::Timestamptz, false),
],
vec![],
)]);
let code = generate_rust_code(&file, &schema, qsource).unwrap();
assert!(code.code.contains("pub struct CreateUserResult"));
assert!(code.code.contains("pub async fn create_user"));
assert!(code.code.contains("name: &String"));
assert!(code.code.contains("email: &String"));
assert!(code.code.contains("INSERT INTO"));
assert!(code.code.contains("RETURNING"));
assert!(
code.code
.contains("Result<Option<CreateUserResult>, QueryError>")
);
}
#[test]
fn test_generate_upsert_code() {
let source = r#"
UpsertProduct @upsert{
params { id @uuid, name @string, price @decimal }
into products
on-conflict {
target { id }
update { name, price, updated_at @now }
}
values { id $id, name $name, price $price }
returning { id, name, price, updated_at }
}
"#;
let (file, qsource) = parse_test(source);
let schema = make_test_schema(vec![make_test_table(
"products",
&[
("id", PgType::Uuid, false),
("name", PgType::Text, false),
("price", PgType::Numeric, false),
("updated_at", PgType::Timestamptz, false),
],
vec![],
)]);
let code = generate_rust_code(&file, &schema, qsource).unwrap();
assert!(code.code.contains("pub struct UpsertProductResult"));
assert!(code.code.contains("pub async fn upsert_product"));
assert!(code.code.contains("id: &Uuid"));
assert!(code.code.contains("ON CONFLICT"));
assert!(code.code.contains("DO UPDATE SET"));
}
#[test]
fn test_generate_update_code() {
let source = r#"
UpdateUserEmail @update{
params { id @uuid, email @string }
table users
set { email $email, updated_at @now }
where { id $id }
returning { id, email, updated_at }
}
"#;
let (file, qsource) = parse_test(source);
let schema = make_test_schema(vec![make_test_table(
"users",
&[
("id", PgType::Uuid, false),
("email", PgType::Text, false),
("updated_at", PgType::Timestamptz, false),
],
vec![],
)]);
let code = generate_rust_code(&file, &schema, qsource).unwrap();
assert!(code.code.contains("pub struct UpdateUserEmailResult"));
assert!(code.code.contains("pub async fn update_user_email"));
assert!(code.code.contains("UPDATE"));
assert!(code.code.contains("SET"));
assert!(code.code.contains("WHERE"));
}
#[test]
fn test_generate_delete_code() {
let source = r#"
DeleteUser @delete{
params { id @uuid }
from users
where { id $id }
returning { id }
}
"#;
let (file, qsource) = parse_test(source);
let schema = make_test_schema(vec![make_test_table(
"users",
&[("id", PgType::Uuid, false)],
vec![],
)]);
let code = generate_rust_code(&file, &schema, qsource).unwrap();
assert!(code.code.contains("pub struct DeleteUserResult"));
assert!(code.code.contains("pub async fn delete_user"));
assert!(code.code.contains("DELETE FROM"));
assert!(code.code.contains("WHERE"));
}
#[test]
fn test_generate_insert_without_returning() {
let source = r#"
InsertLog @insert{
params { message @string }
into logs
values { message $message, created_at @now }
}
"#;
let (file, qsource) = parse_test(source);
let code = generate_rust_code(&file, &empty_schema(), qsource).unwrap();
assert!(!code.code.contains("pub struct InsertLogResult"));
assert!(code.code.contains("pub async fn insert_log"));
assert!(code.code.contains("client.execute"));
assert!(code.code.contains("Result<u64, QueryError>"));
}
#[test]
fn test_generate_insert_many_code() {
let source = r#"
BulkCreateProducts @insert-many{
params { handle @string, status @string }
into products
values { handle $handle, status $status, created_at @now }
returning { id, handle, status }
}
"#;
let (file, qsource) = parse_test(source);
let schema = make_test_schema(vec![make_test_table(
"products",
&[
("id", PgType::BigInt, false),
("handle", PgType::Text, false),
("status", PgType::Text, false),
],
vec![],
)]);
let code = generate_rust_code(&file, &schema, qsource).unwrap();
assert!(
code.code.contains("pub struct BulkCreateProductsParams"),
"Should generate params struct"
);
assert!(
code.code.contains("pub handle: String"),
"Params struct should have handle field"
);
assert!(
code.code.contains("pub status: String"),
"Params struct should have status field"
);
assert!(
code.code.contains("pub struct BulkCreateProductsResult"),
"Should generate result struct"
);
assert!(
code.code.contains("pub async fn bulk_create_products"),
"Should generate bulk_create_products function"
);
assert!(
code.code.contains("items: &[BulkCreateProductsParams]"),
"Function should take slice of params"
);
assert!(
code.code
.contains("Result<Vec<BulkCreateProductsResult>, QueryError>"),
"Should return Vec of results"
);
assert!(
code.code.contains("handle_arr"),
"Should create handle array"
);
assert!(
code.code.contains("status_arr"),
"Should create status array"
);
assert!(code.code.contains("UNNEST"), "SQL should use UNNEST");
}
#[test]
fn test_generate_upsert_many_code() {
let source = r#"
BulkUpsertProducts @upsert-many{
params { handle @string, status @string }
into products
on-conflict {
target { handle }
update { status, updated_at @now }
}
values { handle $handle, status $status, created_at @now }
returning { id, handle, status }
}
"#;
let (file, qsource) = parse_test(source);
let schema = make_test_schema(vec![make_test_table(
"products",
&[
("id", PgType::BigInt, false),
("handle", PgType::Text, false),
("status", PgType::Text, false),
],
vec![],
)]);
let code = generate_rust_code(&file, &schema, qsource).unwrap();
assert!(
code.code.contains("pub struct BulkUpsertProductsParams"),
"Should generate params struct"
);
assert!(
code.code.contains("pub struct BulkUpsertProductsResult"),
"Should generate result struct"
);
assert!(
code.code.contains("pub async fn bulk_upsert_products"),
"Should generate bulk_upsert_products function"
);
assert!(
code.code.contains("items: &[BulkUpsertProductsParams]"),
"Function should take slice of params"
);
assert!(
code.code.contains("ON CONFLICT"),
"SQL should use ON CONFLICT"
);
assert!(
code.code.contains("DO UPDATE SET"),
"SQL should have DO UPDATE SET"
);
}
#[test]
fn test_generate_insert_many_without_returning() {
let source = r#"
BulkInsertLogs @insert-many{
params { message @string }
into logs
values { message $message, created_at @now }
}
"#;
let (file, qsource) = parse_test(source);
let code = generate_rust_code(&file, &empty_schema(), qsource).unwrap();
assert!(
!code.code.contains("pub struct BulkInsertLogsResult"),
"Should NOT generate result struct"
);
assert!(
code.code.contains("pub struct BulkInsertLogsParams"),
"Should generate params struct"
);
assert!(code.code.contains("client.execute"), "Should use execute()");
assert!(
code.code.contains("Result<u64, QueryError>"),
"Should return Result<u64>"
);
}