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

nordisk/knut

crates/knut-bifrost/tests/common/mod.rs

//! Shared by the live Iceberg → Doris tests (`live_iceberg_doris.rs`, and the
//! container harness): Doris SQL over the FE's HTTP API, and the people rows.
//!
//! The FE runs a statement posted to `/api/query/default_cluster/<db>` with
//! HTTP Basic, so DDL and checks need no MySQL client and no shell.

#![allow(dead_code)]

pub mod rig;

use std::sync::Arc;

use arrow_array::{Int64Array, RecordBatch, StringArray};
use arrow_schema::{DataType, Field, Schema};

/// Doris SQL over the FE's HTTP API.
pub struct DorisSql {
    pub fe: String,
    pub user: String,
    pub password: String,
    client: reqwest::Client,
}

impl DorisSql {
    pub fn new(fe: &str, user: &str, password: &str) -> Self {
        Self {
            fe: fe.trim_end_matches('/').to_string(),
            user: user.into(),
            password: password.into(),
            client: tls_roots(reqwest::Client::builder())
                .timeout(std::time::Duration::from_secs(600))
                .build()
                .expect("http client"),
        }
    }

    /// From `DORIS_USER` / `DORIS_PASSWORD`.
    pub fn from_env(fe: &str) -> Self {
        Self::new(
            fe,
            &std::env::var("DORIS_USER").expect("DORIS_USER"),
            &std::env::var("DORIS_PASSWORD").unwrap_or_default(),
        )
    }

    /// Run one statement; the result rows (empty for DDL). Panics with the
    /// FE's own message on failure — a test wants the reason, not a code.
    pub async fn exec(&self, stmt: &str) -> Vec<Vec<serde_json::Value>> {
        let url = format!("{}/api/query/default_cluster/information_schema", self.fe);
        let resp = self
            .client
            .post(&url)
            .basic_auth(&self.user, Some(&self.password))
            .json(&serde_json::json!({ "stmt": stmt }))
            .send()
            .await
            .unwrap_or_else(|e| panic!("doris sql {stmt}: {e}"));
        let v: serde_json::Value = resp.json().await.expect("doris sql answers JSON");
        if v.get("code").and_then(serde_json::Value::as_i64) != Some(0) {
            panic!("doris sql `{stmt}` failed: {v}");
        }
        v.pointer("/data/data")
            .and_then(serde_json::Value::as_array)
            .map(|rows| rows.iter().filter_map(|r| r.as_array().cloned()).collect())
            .unwrap_or_default()
    }

    /// `select <expr> from <table> [where …]` → one integer.
    pub async fn scalar(&self, stmt: &str) -> i64 {
        let rows = self.exec(stmt).await;
        let cell = rows.first().and_then(|r| r.first()).cloned().unwrap_or_default();
        cell.as_i64()
            .or_else(|| cell.as_str().and_then(|s| s.parse().ok()))
            .unwrap_or_else(|| panic!("`{stmt}` gave no integer: {rows:?}"))
    }

    /// `count(*)` that has to read the rows (a bare count may come from metadata).
    pub async fn count(&self, table: &str) -> i64 {
        self.scalar(&format!("select count(*) from {table} where id >= 0")).await
    }

    /// Wait until the FE answers SQL (a fresh container needs a while).
    pub async fn wait_ready(&self, timeout: std::time::Duration) {
        let t0 = std::time::Instant::now();
        loop {
            let url = format!("{}/api/query/default_cluster/information_schema", self.fe);
            let ok = self
                .client
                .post(&url)
                .basic_auth(&self.user, Some(&self.password))
                .json(&serde_json::json!({ "stmt": "select 1" }))
                .send()
                .await
                .ok()
                .map(|r| r.status().is_success())
                .unwrap_or(false);
            if ok {
                return;
            }
            assert!(t0.elapsed() < timeout, "doris FE {} never answered SQL", self.fe);
            tokio::time::sleep(std::time::Duration::from_secs(2)).await;
        }
    }
}

/// The destination DDL: a unique-key merge-on-write table (so deletes apply)
/// with an inverted index on `name` — the native storage the lake cannot give.
pub fn people_ddl(db: &str, table: &str, props: &str) -> String {
    format!(
        "CREATE TABLE IF NOT EXISTS {db}.{table} (\
           id BIGINT NOT NULL, \
           name VARCHAR(64), \
           pnr BIGINT, \
           INDEX idx_name (name) USING INVERTED\
         ) UNIQUE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 4 \
         PROPERTIES (\"enable_unique_key_merge_on_write\" = \"true\"{props})"
    )
}

pub fn people_schema() -> Arc<Schema> {
    Arc::new(Schema::new(vec![
        Field::new("id", DataType::Int64, false),
        Field::new("name", DataType::Utf8, true),
        Field::new("pnr", DataType::Int64, true),
    ]))
}

/// Rows `ids`, named `p<id>` (or `<tag><id>`, to tell an update from the original).
pub fn people(ids: std::ops::Range<i64>, tag: &str) -> RecordBatch {
    let n: Vec<i64> = ids.collect();
    let names: Vec<String> = n.iter().map(|i| format!("{tag}{i}")).collect();
    let pnr: Vec<i64> = n.iter().map(|i| 19_000_000_0000 + i).collect();
    RecordBatch::try_new(
        people_schema(),
        vec![
            Arc::new(Int64Array::from(n)),
            Arc::new(StringArray::from(names)),
            Arc::new(Int64Array::from(pnr)),
        ],
    )
    .unwrap()
}

/// A name no two runs share (tables are never dropped — MinIO data is never deleted).
pub fn run_tag() -> String {
    let ns = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs();
    format!("r{ns}")
}

/// reqwest 0.13 with reqwest 0.12 `rustls-tls` behaviour (2026-10-02): the ring
/// provider and the bundled Mozilla roots (webpki), not 0.13's aws-lc-rs + OS
/// trust store. Roots added later on the builder are merged in, as in 0.12.
#[allow(dead_code)]
fn tls_roots(b: reqwest::ClientBuilder) -> reqwest::ClientBuilder {
    let _ = rustls::crypto::ring::default_provider().install_default();
    b.tls_certs_only(
        webpki_root_certs::TLS_SERVER_ROOT_CERTS
            .iter()
            .filter_map(|c| reqwest::Certificate::from_der(c).ok()),
    )
}