lix 0.18.0

Embeddable version control for apps and AI agents.
Documentation
use lix::Value;
use serde_json::json;

use super::assert_rows_eq;

simulation_test!(
    jsonb_projection_predicates_resolve_input_column_types,
    |sim| async move {
        let engine = sim.boot_engine().await;
        let session = sim.wrap_session(engine.open_session().await.unwrap(), &engine);
        let schema = json!({
            "$schema": "https://lix.dev/schema-v1.json",
            "key": "jsonb_projection_typecheck_contract",
            "columns": [
                {"name": "id", "type": "text", "nullable": false},
                {"name": "payload", "type": "jsonb", "nullable": false}
            ],
            "primary_key": ["id"]
        });
        session
            .execute(
                "INSERT INTO lix_registered_schema (value) VALUES ($1)",
                &[Value::Jsonb(schema.into())],
            )
            .await
            .expect("schema insert should succeed");

        for (id, payload) in [
            ("json-null", Value::Jsonb(json!(null).into())),
            ("object", Value::Jsonb(json!({}).into())),
            ("array", Value::Jsonb(json!([]).into())),
        ] {
            session
                .execute(
                    "INSERT INTO jsonb_projection_typecheck_contract (id, payload) \
                     VALUES ($1, $2)",
                    &[Value::Text(id.into()), payload],
                )
                .await
                .expect("projection fixture insert should succeed");
        }

        assert_rows_eq(
            session
                .execute(
                    "SELECT id, \
                            payload = CAST('null' AS JSONB), \
                            payload IN (CAST('null' AS JSONB), CAST('{}' AS JSONB)), \
                            (payload = CAST('null' AS JSONB)) OR \
                                payload IN (CAST('{}' AS JSONB)), \
                            CASE WHEN payload = CAST('null' AS JSONB) \
                                 THEN 'match' ELSE 'other' END \
                     FROM jsonb_projection_typecheck_contract ORDER BY id",
                    &[],
                )
                .await
                .expect("JSONB predicates in projections should resolve input types"),
            vec![
                vec![
                    Value::Text("array".into()),
                    Value::Boolean(false),
                    Value::Boolean(false),
                    Value::Boolean(false),
                    Value::Text("other".into()),
                ],
                vec![
                    Value::Text("json-null".into()),
                    Value::Boolean(true),
                    Value::Boolean(true),
                    Value::Boolean(true),
                    Value::Text("match".into()),
                ],
                vec![
                    Value::Text("object".into()),
                    Value::Boolean(false),
                    Value::Boolean(true),
                    Value::Boolean(true),
                    Value::Text("other".into()),
                ],
            ],
        );

        assert_rows_eq(
            session
                .execute(
                    "SELECT id \
                     FROM jsonb_projection_typecheck_contract \
                     WHERE payload = CAST('null' AS JSONB)",
                    &[],
                )
                .await
                .expect("the same JSONB predicate in WHERE should succeed"),
            vec![vec![Value::Text("json-null".into())]],
        );

        let error = session
            .execute(
                "SELECT payload = $1 \
                 FROM jsonb_projection_typecheck_contract",
                &[Value::Text("null".into())],
            )
            .await
            .expect_err("bare text parameters must remain invalid in projections");
        assert_eq!(error.code, lix::LixError::CODE_TYPE_MISMATCH);

        let nested_error = session
            .execute(
                "SELECT payload IN ( \
                     SELECT payload FROM jsonb_projection_typecheck_contract \
                     WHERE payload = $1 \
                 ) FROM jsonb_projection_typecheck_contract",
                &[Value::Text("null".into())],
            )
            .await
            .expect_err("bare text parameters must remain invalid in nested predicates");
        assert_eq!(nested_error.code, lix::LixError::CODE_TYPE_MISMATCH);

        let nested_type_error = session
            .execute(
                "SELECT payload IN ( \
                     SELECT id FROM jsonb_projection_typecheck_contract \
                 ) FROM jsonb_projection_typecheck_contract",
                &[],
            )
            .await
            .expect_err("JSONB must not compare with a text subquery result");
        assert_eq!(nested_type_error.code, lix::LixError::CODE_TYPE_MISMATCH);

        assert_rows_eq(
            session
                .execute(
                    "SELECT id FROM jsonb_projection_typecheck_contract WHERE $1 IN ( \
                         SELECT payload FROM jsonb_projection_typecheck_contract \
                     ) ORDER BY id",
                    &[Value::Jsonb(json!(null).into())],
                )
                .await
                .expect("JSONB parameters should type-check against subquery results"),
            vec![
                vec![Value::Text("array".into())],
                vec![Value::Text("json-null".into())],
                vec![Value::Text("object".into())],
            ],
        );

        let parameter_subquery_error = session
            .execute(
                "SELECT id FROM jsonb_projection_typecheck_contract WHERE $1 IN ( \
                     SELECT payload FROM jsonb_projection_typecheck_contract \
                 )",
                &[Value::Text("null".into())],
            )
            .await
            .expect_err("text parameters must remain invalid against JSONB subqueries");
        assert_eq!(
            parameter_subquery_error.code,
            lix::LixError::CODE_TYPE_MISMATCH
        );
    }
);

simulation_test!(
    jsonb_numeric_parameter_matches_integral_decimal_across_paths,
    |sim| async move {
        let engine = sim.boot_engine().await;
        let session = sim.wrap_session(engine.open_session().await.unwrap(), &engine);
        let schema = json!({
            "$schema": "https://lix.dev/schema-v1.json",
            "key": "jsonb_numeric_comparison_contract",
            "columns": [
                {"name": "id", "type": "text", "nullable": false},
                {"name": "payload", "type": "jsonb", "nullable": false}
            ],
            "primary_key": ["id"]
        });
        session
            .execute(
                "INSERT INTO lix_registered_schema (value) VALUES ($1)",
                &[Value::Jsonb(schema.into())],
            )
            .await
            .expect("numeric comparison schema should register");
        session
            .execute(
                "INSERT INTO jsonb_numeric_comparison_contract (id, payload) \
                 VALUES ('one', $1)",
                &[Value::Jsonb(json!(1).into())],
            )
            .await
            .expect("numeric JSONB fixture should insert");

        let decimal = Value::Jsonb(json!(1.0).into());
        assert_rows_eq(
            session
                .execute(
                    "SELECT id FROM jsonb_numeric_comparison_contract \
                     WHERE payload = $1",
                    std::slice::from_ref(&decimal),
                )
                .await
                .expect("bare JSONB parameters should use canonical numeric equality"),
            vec![vec![Value::Text("one".into())]],
        );

        assert_rows_eq(
            session
                .execute(
                    "SELECT id FROM jsonb_numeric_comparison_contract \
                     WHERE payload = CAST($1 AS JSONB)",
                    std::slice::from_ref(&decimal),
                )
                .await
                .expect("JSONB cast should normalize integral decimal parameters"),
            vec![vec![Value::Text("one".into())]],
        );

        assert_rows_eq(
            session
                .execute(
                    "UPDATE jsonb_numeric_comparison_contract SET payload = payload \
                     WHERE payload = $1 AND id LIKE '%' RETURNING id",
                    std::slice::from_ref(&decimal),
                )
                .await
                .expect("generic JSONB equality should use canonical numeric parameters"),
            vec![vec![Value::Text("one".into())]],
        );

        assert_rows_eq(
            session
                .execute(
                    "UPDATE jsonb_numeric_comparison_contract SET payload = payload \
                     WHERE payload = $1 RETURNING id",
                    std::slice::from_ref(&decimal),
                )
                .await
                .expect("direct JSONB equality should use JSONB numeric semantics"),
            vec![vec![Value::Text("one".into())]],
        );

        let generic_cast_update = session
            .execute(
                "UPDATE jsonb_numeric_comparison_contract \
                 SET payload = CAST($1 AS JSONB) \
                 WHERE id LIKE 'one' RETURNING payload",
                std::slice::from_ref(&decimal),
            )
            .await
            .expect("generic JSONB UPDATE should normalize integral decimals");
        assert_eq!(
            generic_cast_update.column_types(),
            [lix::ResultColumnType::Jsonb]
        );
        assert_rows_eq(
            generic_cast_update,
            vec![vec![Value::Jsonb(json!(1).into())]],
        );
    }
);