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

nordisk/skade

skade-flight/tests/nested_namespaces.rs

//! Nested namespaces through the Flight SQL door: a one-, two- and three-level
//! namespace each served as ONE schema named `a.b`-style, listed by
//! `GetDbSchemas` / `GetTables` (Power BI's navigator) and SELECTed by quoted
//! schema name — over real TCP with the stock `FlightSqlServiceClient`.

use std::sync::Arc;
use std::time::Duration;

use arrow_array::{Array, Int64Array, RecordBatch, StringArray};
use arrow_schema::{DataType, Field, Schema};
use futures::TryStreamExt;
use skade_flight::arrow_flight::sql::client::FlightSqlServiceClient;
use skade_flight::arrow_flight::sql::{CommandGetDbSchemas, CommandGetTables};
use skade_flight::engine::EngineOptions;
use skade_flight::{Auth, Engine};
use tonic::transport::Channel;

fn ids(n: i64) -> (Schema, RecordBatch) {
    let schema = Schema::new(vec![Field::new("id", DataType::Int64, false)]);
    let batch = RecordBatch::try_new(Arc::new(schema.clone()), vec![Arc::new(Int64Array::from((1..=n).collect::<Vec<_>>()))]).unwrap();
    (schema, batch)
}

async fn collect(client: &mut FlightSqlServiceClient<Channel>, info: arrow_flight::FlightInfo) -> Vec<RecordBatch> {
    let ticket = info.endpoint[0].ticket.clone().unwrap();
    client.do_get(ticket).await.unwrap().try_collect().await.unwrap()
}

fn strings(batches: &[RecordBatch], column: &str) -> Vec<String> {
    batches
        .iter()
        .flat_map(|b| {
            let col = b.column_by_name(column).unwrap_or_else(|| panic!("no column {column}"));
            let col = col.as_any().downcast_ref::<StringArray>().unwrap();
            (0..col.len()).map(|i| col.value(i).to_string()).collect::<Vec<_>>()
        })
        .collect()
}

async fn count(client: &mut FlightSqlServiceClient<Channel>, sql: &str) -> i64 {
    let info = client.execute(sql.to_string(), None).await.unwrap_or_else(|e| panic!("{sql}: {e}"));
    let batches = collect(client, info).await;
    batches[0].column(0).as_any().downcast_ref::<Int64Array>().unwrap().value(0)
}

#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn one_two_and_three_level_namespaces_list_and_select_over_flight_sql() {
    let dir = tempfile::tempdir().unwrap();
    let wh = skade::Warehouse::open(dir.path().join("wh")).await.unwrap();
    for (name, n) in [("one.t1", 1), ("a.b.t2", 2), ("x.y.z.t3", 3)] {
        let (s, b) = ids(n);
        wh.create_table(name, &s).await.unwrap().append(&[b]).await.unwrap();
    }
    let opts = EngineOptions {
        default_namespace: "one".into(),
        target_partitions: 2,
        refresh_after: Duration::from_millis(50),
        ..Default::default()
    };
    let engine = Arc::new(Engine::with_engine_options(wh, opts).await.unwrap());

    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
    let addr = listener.local_addr().unwrap();
    let _server = {
        let engine = engine.clone();
        tokio::spawn(async move {
            skade_flight::serve_on(engine, Arc::new(Auth::open()), listener, Default::default(), futures::future::pending())
                .await
                .unwrap();
        })
    };
    let channel = Channel::from_shared(format!("http://{addr}")).unwrap().connect().await.unwrap();
    let mut c = FlightSqlServiceClient::new(channel);

    // GetDbSchemas: every depth is one schema, parents included.
    let info = c.get_db_schemas(CommandGetDbSchemas::default()).await.unwrap();
    let schemas = strings(&collect(&mut c, info).await, "db_schema_name");
    for expected in ["main", "one", "a", "a.b", "x", "x.y", "x.y.z"] {
        assert!(schemas.iter().any(|s| s == expected), "{expected} missing from {schemas:?}");
    }

    // GetTables: each table under its flattened schema.
    let info = c.get_tables(CommandGetTables::default()).await.unwrap();
    let batches = collect(&mut c, info).await;
    let pairs: Vec<(String, String)> =
        strings(&batches, "db_schema_name").into_iter().zip(strings(&batches, "table_name")).collect();
    for (schema, table) in [("one", "t1"), ("a.b", "t2"), ("x.y.z", "t3")] {
        assert!(pairs.iter().any(|(s, t)| s == schema && t == table), "{schema}.{table} missing from {pairs:?}");
    }

    // SELECT by quoted flattened schema name, at each depth.
    assert_eq!(count(&mut c, "SELECT count(*) FROM skade.one.t1").await, 1);
    assert_eq!(count(&mut c, r#"SELECT count(*) FROM skade."a.b".t2"#).await, 2);
    assert_eq!(count(&mut c, r#"SELECT count(*) FROM skade."x.y.z".t3"#).await, 3);
    assert_eq!(count(&mut c, r#"SELECT count(*) FROM "a.b".t2"#).await, 2);
    // The default schema is the one-level namespace.
    assert_eq!(count(&mut c, "SELECT count(*) FROM t1").await, 1);

    // GetPrimaryKeys maps the flattened name back to the elements (no identity → empty, not an error).
    assert!(engine.primary_keys("x.y.z", "t3").await.unwrap().is_empty());
}