Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/skade

grpc/tests/grpc_roundtrip.rs

//! In-process gRPC round-trip against the native `skade.katalog.v1.Catalog`
//! service — the RED-when-broken conformance test for skade-katalog-grpc.
//!
//! Spins the tonic service over an embedded `RedbCatalog` on an ephemeral port,
//! connects a generated client, and drives create-namespace → create-table →
//! load-table → commit-table (a property update). Asserts the commit is
//! persisted (a fresh load sees the new property). A second test commits with a
//! stale `assert-table-uuid` requirement and asserts the server returns gRPC
//! **ABORTED** — the optimistic-concurrency guard mapped from
//! `CatalogCommitConflicts`.

use std::collections::HashMap;
use std::sync::Arc;

use iceberg::io::LocalFsStorageFactory;
use iceberg::spec::{NestedField, PrimitiveType, Schema, Type};
use iceberg::{CatalogBuilder, TableRequirement, TableUpdate};
use skade_katalog::{RedbCatalog, RedbCatalogBuilder};
use skade_katalog_grpc::pb;
use tempfile::TempDir;

#[cfg(feature = "testmatrix")]
macro_rules! matrix {
    ($check:expr, $ok:expr, $detail:expr) => {
        nornir_testmatrix::functional_status("skade-katalog-grpc", $check, $ok, $detail);
    };
}
#[cfg(not(feature = "testmatrix"))]
macro_rules! matrix {
    ($check:expr, $ok:expr, $detail:expr) => {{
        let _ = ($check, $ok, $detail);
    }};
}

fn schema() -> Schema {
    Schema::builder()
        .with_schema_id(0)
        .with_fields(vec![
            NestedField::required(1, "id", Type::Primitive(PrimitiveType::Long)).into(),
        ])
        .build()
        .expect("schema")
}

async fn embedded(tmp: &TempDir) -> RedbCatalog {
    let warehouse = tmp.path().join("warehouse");
    std::fs::create_dir_all(&warehouse).unwrap();
    RedbCatalogBuilder::default()
        .db_path(tmp.path().join("catalog.redb"))
        .warehouse_location(format!("file://{}", warehouse.display()))
        .with_storage_factory(Arc::new(LocalFsStorageFactory))
        .load("skade", HashMap::new())
        .await
        .expect("embedded catalog")
}

type Client = pb::catalog_client::CatalogClient<tonic::transport::Channel>;

async fn connect(addr: std::net::SocketAddr) -> Client {
    // Tiny retry: the spawned server may not have accepted yet.
    for _ in 0..50 {
        if let Ok(c) = Client::connect(format!("http://{addr}")).await {
            return c;
        }
        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
    }
    panic!("could not connect to gRPC server at {addr}");
}

#[tokio::test]
async fn grpc_create_load_commit_round_trips() {
    let tmp = TempDir::new().unwrap();
    let cat = embedded(&tmp).await;
    let bound = skade_katalog_grpc::spawn(cat, "127.0.0.1:0".parse().unwrap())
        .await
        .expect("grpc bound");
    let mut client = connect(bound).await;

    // create namespace
    client
        .create_namespace(pb::CreateNamespaceReq {
            levels: vec!["db".into()],
            properties: HashMap::new(),
        })
        .await
        .expect("create_namespace");

    // create table
    let created = client
        .create_table(pb::CreateTableReq {
            namespace: vec!["db".into()],
            name: "t".into(),
            schema_json: serde_json::to_string(&schema()).unwrap(),
            location: String::new(),
        })
        .await
        .expect("create_table")
        .into_inner();
    let loc_before = created.metadata_location.clone();
    assert!(!loc_before.is_empty(), "create returns a metadata location");

    // load table
    let loaded = client
        .load_table(pb::TableIdentMsg { namespace: vec!["db".into()], name: "t".into() })
        .await
        .expect("load_table")
        .into_inner();
    assert_eq!(loaded.metadata_location, loc_before);

    // commit a property update through the gRPC commit_table path
    let update = TableUpdate::SetProperties {
        updates: HashMap::from([("answer".to_string(), "42".to_string())]),
    };
    let committed = client
        .commit_table(pb::CommitTableReq {
            namespace: vec!["db".into()],
            name: "t".into(),
            requirements_json: vec![],
            updates_json: vec![serde_json::to_string(&update).unwrap()],
        })
        .await
        .expect("commit_table")
        .into_inner();
    assert_ne!(committed.metadata_location, loc_before, "commit advances the pointer");

    // a fresh load sees the persisted property
    let reloaded = client
        .load_table(pb::TableIdentMsg { namespace: vec!["db".into()], name: "t".into() })
        .await
        .expect("reload")
        .into_inner();
    assert_eq!(reloaded.metadata_location, committed.metadata_location);
    let meta: serde_json::Value = serde_json::from_slice(&reloaded.metadata_json).unwrap();
    assert_eq!(
        meta["properties"]["answer"].as_str(),
        Some("42"),
        "gRPC load sees the committed property"
    );
    matrix!("commit_table.round_trip", true, "gRPC create+load+commit persisted");
}

#[tokio::test]
async fn grpc_commit_conflicts_on_stale_requirement() {
    let tmp = TempDir::new().unwrap();
    let cat = embedded(&tmp).await;
    let bound = skade_katalog_grpc::spawn(cat, "127.0.0.1:0".parse().unwrap())
        .await
        .expect("grpc bound");
    let mut client = connect(bound).await;

    client
        .create_namespace(pb::CreateNamespaceReq { levels: vec!["db".into()], properties: HashMap::new() })
        .await
        .expect("create_namespace");
    client
        .create_table(pb::CreateTableReq {
            namespace: vec!["db".into()],
            name: "t".into(),
            schema_json: serde_json::to_string(&schema()).unwrap(),
            location: String::new(),
        })
        .await
        .expect("create_table");

    // A requirement asserting a UUID the table does not have → CatalogCommitConflicts.
    let bad = TableRequirement::UuidMatch { uuid: uuid::Uuid::nil() };
    let status = client
        .commit_table(pb::CommitTableReq {
            namespace: vec!["db".into()],
            name: "t".into(),
            requirements_json: vec![serde_json::to_string(&bad).unwrap()],
            updates_json: vec![],
        })
        .await
        .expect_err("stale requirement must be rejected");
    assert_eq!(
        status.code(),
        tonic::Code::Aborted,
        "stale assert-table-uuid must map to gRPC ABORTED, got {status:?}"
    );
    matrix!("commit_table.conflict", status.code() == tonic::Code::Aborted, "stale uuid → ABORTED");
}