distributed 4.0.0

CQRS/ES framework for Rust using Plain Old Rust Structs — append-only events, replay, snapshots, outbox, service bus, and pluggable infrastructure
Documentation
//! Dialect-honest comparison surface (ship-dialect-1).
//!
//! SQLite engines must not expose Postgres jsonb operators on
//! `JSON_comparison_exp`. Failure is GraphQL unknown-field (type honesty),
//! with compile-time reject remaining as defense-in-depth.

use async_graphql::Request;
use distributed::graphql::{
    comparison_op_fields, include_postgres_json_comparison_ops, read, GraphqlEngine,
    ModelPermissions, POSTGRES_JSON_COMPARISON_OPS,
};
use distributed::ReadModel;
use serde::{Deserialize, Serialize};
use sqlx::sqlite::SqlitePoolOptions;

use super::common::{assert_no_sql_leak, error_messages, session};

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ReadModel)]
#[table("docs")]
struct DocView {
    #[id("doc_id")]
    doc_id: String,
    /// JSON column → GraphQL JSON scalar → JSON_comparison_exp.
    payload: serde_json::Value,
}

async fn seed_docs() -> sqlx::SqlitePool {
    let pool = SqlitePoolOptions::new()
        .connect("sqlite::memory:")
        .await
        .unwrap();
    sqlx::query(
        "CREATE TABLE docs (doc_id TEXT PRIMARY KEY, payload TEXT NOT NULL);
         INSERT INTO docs VALUES ('d1', '{\"a\":1}');",
    )
    .execute(&pool)
    .await
    .unwrap();
    pool
}

fn engine(pool: sqlx::SqlitePool) -> GraphqlEngine {
    GraphqlEngine::builder(pool)
        .roles(&["user"])
        .model::<DocView>(ModelPermissions::new().grant("user", read().all_columns()))
        .build()
        .unwrap()
}

/// Matrix helper: shared naming policy says SQLite omits PG JSON ops.
#[test]
fn naming_matrix_sqlite_omits_pg_json_ops() {
    assert!(!include_postgres_json_comparison_ops(false));
    let fields = comparison_op_fields("JSON", false);
    for op in POSTGRES_JSON_COMPARISON_OPS {
        assert!(!fields.contains(op), "unexpected {op} in SQLite JSON ops");
    }
}

/// SQLite runtime: `_contains` is not a field on JSON_comparison_exp.
#[tokio::test]
async fn sqlite_json_contains_is_unknown_field() {
    let engine = engine(seed_docs().await);
    let s = session("user", "u");
    let resp = engine
        .execute(
            &s,
            Request::new(r#"{ docs(where: { payload: { _contains: { a: 1 } } }) { doc_id } }"#),
        )
        .await;
    assert!(
        !resp.errors.is_empty(),
        "SQLite must not accept _contains on JSON: {:?}",
        resp.data
    );
    assert_no_sql_leak(&resp);
    let msgs = error_messages(&resp);
    assert!(
        msgs.contains("_contains") || msgs.contains("unknown field") || msgs.contains("invalid"),
        "expected unknown/invalid field for _contains, got {msgs}"
    );
}

#[tokio::test]
async fn sqlite_json_has_key_is_unknown_field() {
    let engine = engine(seed_docs().await);
    let s = session("user", "u");
    let resp = engine
        .execute(
            &s,
            Request::new(r#"{ docs(where: { payload: { _has_key: "a" } }) { doc_id } }"#),
        )
        .await;
    assert!(!resp.errors.is_empty());
    assert_no_sql_leak(&resp);
    let msgs = error_messages(&resp);
    assert!(
        msgs.contains("_has_key") || msgs.contains("unknown field") || msgs.contains("invalid"),
        "{msgs}"
    );
}

#[tokio::test]
async fn sqlite_json_contained_in_is_unknown_field() {
    let engine = engine(seed_docs().await);
    let s = session("user", "u");
    let resp = engine
        .execute(
            &s,
            Request::new(
                r#"{ docs(where: { payload: { _contained_in: { a: 1, b: 2 } } }) { doc_id } }"#,
            ),
        )
        .await;
    assert!(!resp.errors.is_empty());
    assert_no_sql_leak(&resp);
}

/// Portable `_eq` is a known field on JSON_comparison_exp (SQLite).
#[tokio::test]
async fn sqlite_json_eq_is_known_field() {
    let engine = engine(seed_docs().await);
    let s = session("user", "u");
    let resp = engine
        .execute(
            &s,
            Request::new(r#"{ docs(where: { payload: { _eq: { a: 1 } } }) { doc_id } }"#),
        )
        .await;
    if !resp.errors.is_empty() {
        let msgs = error_messages(&resp);
        assert!(
            !msgs.contains("unknown field"),
            "portable _eq must be a known field on JSON_comparison_exp: {msgs}"
        );
        assert_no_sql_leak(&resp);
    } else {
        let data = serde_json::to_value(&resp.data).unwrap();
        assert!(data["docs"].as_array().is_some());
    }
}