fraiseql-core 2.19.0

Core execution engine for FraiseQL v2 - Compiled GraphQL over SQL
Documentation
//! Unit pins for the response-bytes ceiling: the byte estimator the transport
//! tests depend on, and the budget that accumulates across a streamed read.

#![allow(clippy::unwrap_used, clippy::panic)] // Reason: test code, panics acceptable

use super::*;

/// The arithmetic the executor tests depend on, stated once here so a change to
/// the estimator shows up as a failure in this file rather than as a puzzling
/// off-by-N in a transport test.
#[test]
fn a_flat_document_is_charged_its_keys_values_and_punctuation() {
    let v = serde_json::json!({"id": "1", "name": "Alice"});
    // {} = 2; "id" member = 4 + 2 + (2 + 1) = 9; "name" member = 4 + 4 + (2 + 5) = 15.
    assert_eq!(json_bytes(&v), 26);
}

#[test]
fn an_empty_container_is_charged_only_its_brackets() {
    assert_eq!(json_bytes(&serde_json::json!({})), 2);
    assert_eq!(json_bytes(&serde_json::json!([])), 2);
}

/// Nesting is charged, which is the property that makes this a size ceiling
/// rather than a row count: one row of a deeply nested document is not one unit
/// of work.
#[test]
fn nesting_is_charged_all_the_way_down() {
    let flat = serde_json::json!({"a": "x"});
    let nested = serde_json::json!({"a": {"b": {"c": "x"}}});
    assert!(
        json_bytes(&nested) > json_bytes(&flat),
        "a nested document must cost more than a flat one"
    );
}

#[test]
fn a_column_row_is_charged_its_payload() {
    assert_eq!(column_bytes(&ColumnValue::Text("Alice".into())), 5);
    assert_eq!(column_bytes(&ColumnValue::Null), 0);
    assert_eq!(column_bytes(&ColumnValue::Boolean(true)), 1);
}

/// No ceiling declared means no budget at all — not a budget of `u64::MAX`,
/// which would behave like one right up until it overflowed.
#[test]
fn no_declared_ceiling_yields_no_budget() {
    assert!(ResponseBudget::new(None).is_none());
}

/// The total accumulates, and the refusal reports the running total rather than
/// the charge that crossed the line.
#[test]
fn charges_accumulate_and_the_error_carries_the_running_total() {
    let budget = ResponseBudget::new(Some(10)).expect("a ceiling was declared");
    budget.charge(4).expect("under");
    budget.charge(4).expect("still under");
    assert_eq!(budget.used(), 8);

    let err = budget.charge(4).unwrap_err();
    match err {
        FraiseQLError::ResponseTooLarge { bytes, limit } => {
            assert_eq!(bytes, 12, "4 + 4 + 4, not the 4 that crossed it");
            assert_eq!(limit, 10);
        },
        other => panic!("expected ResponseTooLarge, got {other:?}"),
    }
}

/// **Two holders of one budget charge one total.**
///
/// The contract the whole aggregate bound rests on: a transport that answers one request
/// with several reads hands each of them a `&ResponseBudget`, and the ceiling then bounds
/// the response rather than each read. Charging through `&self` is what makes that
/// possible, so it is asserted here rather than only through a served request — a change
/// back to `&mut self` over a plain `u64` would give every holder its own copy, and the
/// only other test that could notice needs a database.
#[test]
fn two_shared_handles_charge_one_running_total() {
    let budget = ResponseBudget::new(Some(10)).expect("a ceiling was declared");
    let first: &ResponseBudget = &budget;
    let second: &ResponseBudget = &budget;

    first.charge(6).expect("under on its own");
    // Six again would fit a budget of ten if this handle had one of its own.
    let err = second.charge(6).unwrap_err();
    match err {
        FraiseQLError::ResponseTooLarge { bytes, limit } => {
            assert_eq!(bytes, 12, "both handles charged the same total");
            assert_eq!(limit, 10);
        },
        other => panic!("expected ResponseTooLarge, got {other:?}"),
    }
    assert_eq!(budget.used(), 12, "and the total is visible through the owner");
}

/// Exactly at the ceiling is under it — the comparison is `>`, not `>=`, so a
/// deployment that sizes the ceiling to its largest legitimate response still
/// serves that response.
#[test]
fn exactly_at_the_ceiling_is_allowed() {
    let budget = ResponseBudget::new(Some(10)).expect("a ceiling was declared");
    budget.charge(10).expect("10 is not over 10");
}

/// A pathological document cannot wrap the counter into a pass.
#[test]
fn an_overflowing_charge_saturates_rather_than_wrapping() {
    let budget = ResponseBudget::new(Some(u64::MAX - 1)).expect("declared");
    budget.charge(u64::MAX).expect_err("saturates to MAX, which is over MAX - 1");
}

/// A row read as text is charged what the same row parsed is charged, whatever it holds.
#[test]
fn a_row_read_as_text_is_charged_as_the_value_it_parses_to() {
    for text in [
        r#"{"id" : "aaaa", "name" : "Alice", "n" : 12, "ok" : true, "gone" : null}"#,
        r#"{"profile" : {"tier" : "gold", "address" : {"city" : "Paris"}}}"#,
        r#"{"tags": ["a", "b", ""], "nested": [[1, 2.5], [], [{"k": "naïve 漢字"}]]}"#,
        r#"{"quote" : "a \"b\" \\ c\n", "empty" : {}}"#,
        "[]",
        "7",
    ] {
        let mut rows = fraiseql_db::RawRows::new();
        rows.push(text.as_bytes());
        let value: serde_json::Value = serde_json::from_str(text).unwrap();
        assert_eq!(tally_bytes(&rows.rows()[0].tally), json_bytes(&value), "{text}");
    }
}

#[test]
fn rows_read_as_text_are_refused_where_the_parsed_rows_are() {
    let texts = [
        r#"{"a" : "xxxxxxxxxx"}"#,
        r#"{"b" : "yyyyyyyyyy"}"#,
        r#"{"c" : 1}"#,
    ];
    let mut raw = fraiseql_db::RawRows::new();
    let mut parsed = Vec::new();
    for text in texts {
        raw.push(text.as_bytes());
        parsed.push(JsonbValue::new(serde_json::from_str(text).unwrap()));
    }
    let limit = json_bytes(parsed[0].as_value()) + json_bytes(parsed[1].as_value());
    let refused = |result: Result<()>| match result {
        Err(FraiseQLError::ResponseTooLarge { bytes, limit }) => Some((bytes, limit)),
        _ => None,
    };
    let by_text = refused(ResponseBudget::new(Some(limit)).unwrap().charge_raw_rows(&raw));
    let by_value = refused(ResponseBudget::new(Some(limit)).unwrap().charge_jsonb_rows(&parsed));
    assert!(by_value.is_some());
    assert_eq!(by_text, by_value);
}