lix 0.15.1

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

use crate::{validate_lix_schema, validate_lix_schema_definition};

fn schema() -> serde_json::Value {
    json!({
        "$schema": "https://lix.dev/schema-v1.json",
        "key": "acme_note",
        "columns": [
            { "name": "id", "type": "uuid", "nullable": false, "default_expression": "uuidv7()" },
            { "name": "title", "type": "text", "nullable": false },
            { "name": "payload", "type": "jsonb", "nullable": false },
            { "name": "rank", "type": "int8", "nullable": false }
        ],
        "primary_key": ["id"],
        "unique": [["title"]]
    })
}

#[test]
fn accepts_schema_v1_and_rows() {
    let schema = schema();
    validate_lix_schema_definition(&schema).expect("valid Schema v1");
    validate_lix_schema(
        &schema,
        &json!({
            "id": "01920000-0000-7000-8000-000000000001",
            "title": "hello",
            "payload": { "nested": [1, null, true] },
            "rank": 1
        }),
    )
    .expect("valid row");
}

#[test]
fn rejects_json_schema_and_unknown_fields() {
    let legacy = json!({
        "x-lix-key": "acme_note",
        "x-lix-primary-key": ["/id"],
        "type": "object",
        "properties": { "id": { "type": "string" } }
    });
    assert!(validate_lix_schema_definition(&legacy).is_err());

    let mut unknown = schema();
    unknown["columns"][0]["format"] = json!("uuid");
    assert!(validate_lix_schema_definition(&unknown).is_err());
}

#[test]
fn distinguishes_sql_null_from_jsonb_null() {
    let schema = schema();
    validate_lix_schema(
        &schema,
        &json!({
            "id": "01920000-0000-7000-8000-000000000001",
            "title": "hello",
            "payload": null,
            "rank": 1
        }),
    )
    .expect("JSONB null is a non-NULL JSONB value");

    let error = validate_lix_schema(
        &schema,
        &json!({
            "id": "01920000-0000-7000-8000-000000000001",
            "title": null,
            "payload": {},
            "rank": 1
        }),
    )
    .expect_err("NOT NULL text rejects SQL NULL");
    assert!(error.message.contains("/title"));
}

#[test]
fn restricts_defaults_to_supported_postgresql_expression() {
    let mut invalid = schema();
    invalid["columns"][0]["default_expression"] = json!("agent_context() || random()");
    assert!(validate_lix_schema_definition(&invalid).is_err());
}

#[tokio::test]
async fn postgresql_jsonb_syntax_registers_queries_and_updates_rows() {
    use crate::Value;
    use crate::engine::Engine;
    use crate::storage::Memory;

    let storage = Memory::new();
    Engine::initialize(storage.clone()).await.unwrap();
    let engine = Engine::new(storage).await.unwrap();
    let session = engine.open_session().await.unwrap();
    let definition = json!({
        "$schema": "https://lix.dev/schema-v1.json",
        "key": "acme_jsonb_probe",
        "columns": [
            { "name": "id", "type": "text", "nullable": false },
            { "name": "payload", "type": "jsonb", "nullable": false }
        ],
        "primary_key": ["id"]
    });
    session
        .execute(
            "INSERT INTO lix_registered_schema (schema_key, value) VALUES ($1, $2)",
            &[
                Value::Text("acme_jsonb_probe".into()),
                Value::Jsonb(definition.into()),
            ],
        )
        .await
        .unwrap();
    session
        .execute(
            "INSERT INTO acme_jsonb_probe (id, payload) VALUES ('a', '{\"name\":\"Ada\",\"user\":{\"names\":[\"Ada\",\"Lin\"]}}'::jsonb)",
            &[],
        )
        .await
        .unwrap();
    let selected = session
        .execute(
            "SELECT payload ->> 'name' FROM acme_jsonb_probe WHERE id = 'a'",
            &[],
        )
        .await
        .unwrap();
    assert_eq!(selected.rows()[0].values(), &[Value::Text("Ada".into())]);

    let operators = session
        .execute(
            "SELECT payload #>> '{user,names,-1}', \
             payload @> '{\"name\":\"Ada\"}', \
             payload ? 'name' FROM acme_jsonb_probe WHERE id = 'a'",
            &[],
        )
        .await
        .unwrap();
    assert_eq!(
        operators.rows()[0].values(),
        &[
            Value::Text("Lin".into()),
            Value::Boolean(true),
            Value::Boolean(true),
        ]
    );

    let parameterized_exists = session
        .execute(
            "SELECT payload ? $1 FROM acme_jsonb_probe WHERE id = 'a'",
            &[Value::Text("name".into())],
        )
        .await
        .unwrap();
    assert_eq!(
        parameterized_exists.rows()[0].values(),
        &[Value::Boolean(true)]
    );

    let semantics = session
        .execute(
            "SELECT \
                '{\"b\":2,\"a\":1}'::jsonb = '{\"a\":1,\"b\":2}'::jsonb, \
                '{\"a\":1,\"a\":2}'::jsonb = '{\"a\":2}'::jsonb, \
                '1.0'::jsonb = '1'::jsonb, \
                '{\"a\":[1,2,3]}'::jsonb @> '{\"a\":[2]}'::jsonb, \
                '{\"a\":null}'::jsonb -> 'a', \
                '{\"a\":null}'::jsonb -> 'missing'",
            &[],
        )
        .await
        .unwrap();
    assert_eq!(
        semantics.rows()[0].values(),
        &[
            Value::Boolean(true),
            Value::Boolean(true),
            Value::Boolean(true),
            Value::Boolean(true),
            Value::Jsonb(json!(null).into()),
            Value::Null,
        ]
    );

    let nul_error = session
        .execute("SELECT '\"\\u0000\"'::jsonb", &[])
        .await
        .expect_err("PostgreSQL JSONB rejects Unicode NUL");
    assert!(nul_error.message.contains("NUL"), "{nul_error:?}");
}