alopex-sql 0.6.0

SQL parser components for the Alopex DB dialect
#![cfg(target_os = "linux")]

use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct PocSpan {
    start: i64,
    finish: i64,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct PocExpr {
    kind: String,
    value: String,
    table: String,
    column: String,
    op: String,
    args: Vec<PocExpr>,
    span: PocSpan,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PocFrom {
    kind: String,
    left_table: String,
    right_table: String,
    join_type: String,
    condition: PocExpr,
    span: PocSpan,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PocAst {
    kind: String,
    distinct: bool,
    select_list: Vec<PocExpr>,
    from_clause: PocFrom,
    where_expr: PocExpr,
    span: PocSpan,
}

fn span(start: i64, finish: i64) -> PocSpan {
    PocSpan { start, finish }
}

fn column(table: &str, name: &str, span: PocSpan) -> PocExpr {
    PocExpr {
        kind: "ColumnRef".to_owned(),
        value: String::new(),
        table: table.to_owned(),
        column: name.to_owned(),
        op: String::new(),
        args: Vec::new(),
        span,
    }
}

fn literal(value: &str, span: PocSpan) -> PocExpr {
    PocExpr {
        kind: "StringLiteral".to_owned(),
        value: value.to_owned(),
        table: String::new(),
        column: String::new(),
        op: String::new(),
        args: Vec::new(),
        span,
    }
}

fn binary(op: &str, left: PocExpr, right: PocExpr, span: PocSpan) -> PocExpr {
    PocExpr {
        kind: "BinaryOp".to_owned(),
        value: String::new(),
        table: String::new(),
        column: String::new(),
        op: op.to_owned(),
        args: vec![left, right],
        span,
    }
}

fn expected_ast() -> PocAst {
    let user_id = column("users", "id", span(7, 15));
    let order_user_id = column("orders", "user_id", span(40, 54));
    let status = column("orders", "status", span(61, 74));

    PocAst {
        kind: "Select".to_owned(),
        distinct: true,
        select_list: vec![user_id.clone(), column("orders", "total", span(17, 29))],
        from_clause: PocFrom {
            kind: "Join".to_owned(),
            left_table: "users".to_owned(),
            right_table: "orders".to_owned(),
            join_type: "Inner".to_owned(),
            condition: binary("Eq", user_id, order_user_id, span(40, 54)),
            span: span(31, 54),
        },
        where_expr: binary("Eq", status, literal("paid", span(77, 83)), span(61, 83)),
        span: span(0, 83),
    }
}

fn run_nim_poc(out_dir: &Path) {
    let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let repo_root = manifest_dir
        .parent()
        .and_then(Path::parent)
        .expect("alopex-sql crate is under crates/");
    let container_out_dir = "/workspace/target/nim-sql-parser-poc";
    fs::create_dir_all(out_dir).expect("create PoC artifact directory");

    let output = Command::new("docker")
        .args([
            "run",
            "--rm",
            "-v",
            &format!("{}:/workspace", repo_root.display()),
            "-w",
            "/workspace/crates/alopex-sql/nim-sql-parser",
            "-e",
            &format!("ALOPEX_POC_OUT_DIR={container_out_dir}"),
            "-e",
            "PATH=/opt/nim/bin:/usr/local/bin:/usr/bin:/bin",
            "nimlang/nim:2.2",
            "sh",
            "-c",
            &format!(
                "nimble install -y msgpack4nim && nim c -r --hints:off --verbosity:0 -o:{container_out_dir}/poc_msgpack_bin tests/poc_msgpack.nim"
            ),
        ])
        .current_dir(repo_root)
        .output()
        .expect("run nimlang/nim:2.2 Docker container for MessagePack PoC");

    assert!(
        output.status.success(),
        "Nim MessagePack PoC failed in nimlang/nim:2.2\nstatus: {}\nstdout:\n{}\nstderr:\n{}",
        output.status,
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );

    eprintln!("{}", String::from_utf8_lossy(&output.stdout));
}

#[test]
#[ignore = "requires Docker access to run the Nim MessagePack PoC container"]
fn msgpack_from_nim_roundtrips_into_rust_struct() {
    let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let repo_root = manifest_dir
        .parent()
        .and_then(Path::parent)
        .expect("alopex-sql crate is under crates/");
    let out_dir = repo_root.join("target/nim-sql-parser-poc");
    run_nim_poc(&out_dir);

    let msgpack_bytes =
        fs::read(out_dir.join("poc_ast.msgpack")).expect("read Nim MessagePack fixture");
    let ast: PocAst =
        rmp_serde::from_slice(&msgpack_bytes).expect("decode Nim MessagePack with rmp-serde");
    assert_eq!(ast, expected_ast());

    let rust_msgpack = rmp_serde::to_vec_named(&ast).expect("encode Rust AST with rmp-serde");
    let roundtripped: PocAst =
        rmp_serde::from_slice(&rust_msgpack).expect("decode Rust MessagePack with rmp-serde");
    assert_eq!(roundtripped, ast);

    let json_bytes = fs::read(out_dir.join("poc_ast.json")).expect("read Nim JSON control fixture");
    let json_ast: PocAst =
        serde_json::from_slice(&json_bytes).expect("decode Nim JSON control fixture");
    assert_eq!(json_ast, ast);

    let bench =
        fs::read_to_string(out_dir.join("poc_bench.csv")).expect("read PoC benchmark report");
    eprintln!("{bench}");
}