fraiseql-core 2.19.0

Core execution engine for FraiseQL v2 - Compiled GraphQL over SQL
Documentation
#![allow(clippy::unwrap_used)] // Reason: test code, panics are acceptable

use serde_json::{Value, json};

use super::DecimalIndex;
use crate::{
    db::traits::{DecimalShape, quote_decimals},
    schema::{
        CompiledSchema, FieldDefinition, FieldType, InterfaceDefinition, QueryDefinition,
        TypeDefinition, UnionDefinition,
    },
};

fn field(name: &str, field_type: FieldType) -> FieldDefinition {
    FieldDefinition::new(name, field_type)
}

fn list(item: FieldType) -> FieldType {
    FieldType::List {
        item:           Box::new(item),
        nullable_items: false,
    }
}

fn object(name: &str) -> FieldType {
    FieldType::Object(name.to_string())
}

fn schema(types: Vec<TypeDefinition>) -> CompiledSchema {
    CompiledSchema {
        types,
        ..CompiledSchema::default()
    }
}

fn type_def(name: &str, fields: Vec<FieldDefinition>) -> TypeDefinition {
    fields
        .into_iter()
        .fold(TypeDefinition::new(name, format!("v_{name}")), TypeDefinition::with_field)
}

fn implementing(mut t: TypeDefinition, interface: &str) -> TypeDefinition {
    t.implements.push(interface.to_string());
    t
}

fn read(shape: &DecimalShape, src: &str) -> Value {
    let mut out = Vec::new();
    quote_decimals(src.as_bytes(), shape, &mut out);
    serde_json::from_slice(&out).unwrap()
}

fn shape_of(schema: &CompiledSchema, type_name: &str) -> Option<DecimalShape> {
    DecimalIndex::new(schema)
        .shape(type_name)
        .map(|shape| DecimalShape::clone(shape))
}

#[test]
fn a_decimal_field_is_found_under_its_name_its_stored_key_and_its_alias() {
    let mut aliased = field("listPrice", FieldType::Decimal);
    aliased.alias = Some("price".to_string());
    let schema = schema(vec![type_def(
        "Product",
        vec![
            field("unitPrice", FieldType::Decimal),
            aliased,
            field("rating", FieldType::Float),
        ],
    )]);
    let shape = shape_of(&schema, "Product").unwrap();
    assert_eq!(
        read(
            &shape,
            r#"{"unitPrice": 1.50, "unit_price": 2.50, "price": 3.50, "list_price": 4.50, "rating": 4.50}"#
        ),
        json!({"unitPrice": "1.50", "unit_price": "2.50", "price": "3.50", "list_price": "4.50", "rating": 4.5})
    );
}

#[test]
fn a_type_that_reaches_no_decimal_has_no_shape() {
    let schema = schema(vec![
        type_def(
            "User",
            vec![
                field("score", FieldType::Float),
                field("friends", list(object("User"))),
            ],
        ),
        type_def("Product", vec![field("price", FieldType::Decimal)]),
    ]);
    assert!(shape_of(&schema, "User").is_none(), "a recursive type with no Decimal");
    assert!(shape_of(&schema, "Missing").is_none());
}

#[test]
fn a_decimal_below_nested_objects_and_lists_is_reached() {
    let schema = schema(vec![
        type_def(
            "Customer",
            vec![
                field("name", FieldType::String),
                field("lastOrder", object("Order")),
            ],
        ),
        type_def("Order", vec![field("lines", list(object("Line")))]),
        type_def(
            "Line",
            vec![
                field("amount", FieldType::Decimal),
                field("rates", list(FieldType::Decimal)),
            ],
        ),
    ]);
    let shape = shape_of(&schema, "Customer").unwrap();
    assert_eq!(
        read(
            &shape,
            r#"{"last_order": {"lines": [{"amount": 1.50, "rates": [0.10, 0.20]}]}}"#
        ),
        json!({"last_order": {"lines": [{"amount": "1.50", "rates": ["0.10", "0.20"]}]}})
    );
}

#[test]
fn a_recursive_type_holding_a_decimal_is_read_at_every_depth() {
    let schema = schema(vec![type_def(
        "User",
        vec![
            field("balance", FieldType::Decimal),
            field("friends", list(object("User"))),
        ],
    )]);
    let shape = shape_of(&schema, "User").unwrap();
    assert_eq!(
        read(
            &shape,
            r#"{"balance": 1.10, "friends": [{"balance": 2.20, "friends": [{"balance": 3.30}]}]}"#
        ),
        json!({"balance": "1.10", "friends": [{"balance": "2.20", "friends": [{"balance": "3.30"}]}]})
    );
}

/// `Invoice.amount` is a `Decimal`; `Rating.amount` is a `Float`.
fn union_schema() -> CompiledSchema {
    let mut schema = schema(vec![
        implementing(type_def("Invoice", vec![field("amount", FieldType::Decimal)]), "Entry"),
        implementing(type_def("Rating", vec![field("amount", FieldType::Float)]), "Entry"),
        type_def(
            "Feed",
            vec![
                field("items", list(FieldType::Union("Item".to_string()))),
                field("entries", list(FieldType::Interface("Entry".to_string()))),
            ],
        ),
    ]);
    schema.unions.push(
        UnionDefinition::new("Item")
            .with_members(vec!["Invoice".to_string(), "Rating".to_string()]),
    );
    schema.interfaces.push(InterfaceDefinition::new("Entry"));
    schema
}

#[test]
fn a_union_or_interface_member_is_read_as_the_type_it_names() {
    let schema = union_schema();
    let doc =
        r#"[{"__typename": "Invoice", "amount": 1.50}, {"__typename": "Rating", "amount": 1.50}]"#;
    let quoted = json!([{"__typename": "Invoice", "amount": "1.50"}, {"__typename": "Rating", "amount": 1.5}]);
    assert_eq!(read(&shape_of(&schema, "Item").unwrap(), doc), quoted, "a union");
    assert_eq!(read(&shape_of(&schema, "Entry").unwrap(), doc), quoted, "an interface");
    let feed = shape_of(&schema, "Feed").unwrap();
    assert_eq!(
        read(&feed, &format!(r#"{{"items": {doc}, "entries": {doc}}}"#)),
        json!({"items": quoted, "entries": quoted}),
        "a union and an interface below an object"
    );
}

#[test]
fn a_query_of_an_abstract_type_reads_a_document_naming_no_type_as_the_one_stored_in_its_view() {
    let mut schema = union_schema();
    let mut stored = QueryDefinition::new("invoices", "Item");
    stored.sql_source = Some("v_Invoice".to_string());
    let mut shared = QueryDefinition::new("feed", "Item");
    shared.sql_source = Some("v_items".to_string());
    schema.queries.extend([stored, shared]);
    let index = DecimalIndex::new(&schema);
    let doc = r#"{"amount": 1.50}"#;
    assert_eq!(read(index.read_by("invoices").unwrap(), doc), json!({"amount": "1.50"}));
    assert_eq!(
        read(index.read_by("feed").unwrap(), doc),
        json!({"amount": 1.5}),
        "a view no single member is stored in names no fallback"
    );
}

#[test]
fn a_union_of_types_that_reach_no_decimal_has_no_shape() {
    let mut schema = schema(vec![type_def("Rating", vec![field("amount", FieldType::Float)])]);
    schema
        .unions
        .push(UnionDefinition::new("Only").with_members(vec!["Rating".to_string()]));
    assert!(shape_of(&schema, "Only").is_none());
}