hawdb-sql 0.11.0

An embedded Rust graph database for local data-plane workloads
Documentation
// Copyright 2026 Nowledge
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use super::*;
use serde_json::{json, Value};

const FRONTEND: &str = include_str!("../../../../fixtures/frontend_corpus_v1.jsonl");
const BINDINGS: &str = include_str!("../../../../fixtures/frontend_create_bindings_v1.json");

fn catalog() -> Snapshot {
    use SqlDataType::*;
    let mut tables = vec![
        table("memories", &[("id", BigInt)]),
        table("documents", &[("id", BigInt)]),
        table("entities", &[("id", BigInt), ("name", Text)]),
        table(
            "chunks",
            &[("id", BigInt), ("content", Text), ("rank", BigInt)],
        ),
    ];
    let mut mentions = table(
        "mentions",
        &[
            ("document_id", BigInt),
            ("entity_id", BigInt),
            ("confidence", DoublePrecision),
        ],
    );
    mentions.primary_key = vec!["document_id".into(), "entity_id".into()];
    mentions.columns[0].nullable = false;
    mentions.columns[1].nullable = false;
    mentions.foreign_keys = [("document_id", "documents"), ("entity_id", "entities")]
        .into_iter()
        .map(|(column, target)| PgqSourceForeignKeySchema {
            columns: vec![column.into()],
            referenced_table: vec!["public".into(), target.into()],
            referenced_columns: vec!["id".into()],
        })
        .collect();
    tables.push(mentions);
    tables.extend(snapshot().0.into_values());
    let mut no_key = table("no_key", &[("id", BigInt)]);
    no_key.primary_key.clear();
    tables.push(no_key);
    Snapshot(
        tables
            .into_iter()
            .map(|table| (table.name.clone(), table))
            .collect(),
    )
}

fn graph_json(graph: &PropertyGraphSchema) -> Value {
    let labels = |labels: &BTreeMap<String, PropertyGraphElementSchema>| {
        labels
            .iter()
            .map(|(name, label)| {
                (
                    name.clone(),
                    label
                        .properties
                        .iter()
                        .map(|(name, data_type)| (name.clone(), format!("{data_type:?}")))
                        .collect::<BTreeMap<_, _>>(),
                )
            })
            .collect::<BTreeMap<_, _>>()
    };
    json!({"name":graph.name,"vertex_labels":labels(&graph.vertex_labels),"edge_labels":labels(&graph.edge_labels)})
}

fn check_fixture(fixture: &Value) -> std::result::Result<(), String> {
    if fixture["protocol"] != "hawdb-pgq-create-corpus-v1"
        || fixture["postgres_revision"] != "3d00537feb565c410baf41bb301eee338e4b2317"
        || fixture["source_corpus_sha256"] != crate::test_support::sha256_hex(FRONTEND.as_bytes())
    {
        return Err("creation corpus provenance drift".into());
    }
    let exclusions = fixture["profile_exclusions"]
        .as_array()
        .ok_or("missing profile exclusions")?;
    let expected = BTreeSet::from([
        "six-scalars",
        "creation-functions",
        "literal-syntax",
        "expression-overloads",
        "read-only-descriptor",
    ]);
    let actual = exclusions
        .iter()
        .map(|entry| entry["id"].as_str().ok_or("missing exclusion id"))
        .collect::<std::result::Result<BTreeSet<_>, _>>()?;
    if actual != expected
        || exclusions.len() != expected.len()
        || exclusions
            .iter()
            .any(|entry| entry["reason"].as_str().is_none_or(str::is_empty))
    {
        return Err("missing, duplicate or stale profile exclusion".into());
    }
    let cases: Vec<Value> = FRONTEND
        .lines()
        .map(|line| serde_json::from_str(line).unwrap())
        .filter(|case: &Value| case["family"] == "create_property_graph")
        .collect();
    let bindings = fixture["bindings"]
        .as_array()
        .ok_or("missing creation bindings")?;
    if bindings.len() != cases.len() {
        return Err("missing creation binding outcome".into());
    }
    let mut seen = BTreeSet::new();
    for binding in bindings {
        let id = binding["id"].as_str().ok_or("missing case id")?;
        if !seen.insert(id) {
            return Err("duplicate creation binding".into());
        }
        let case = cases
            .iter()
            .find(|case| case["id"] == id)
            .ok_or("unreferenced creation binding")?;
        let sql = case["sql"].as_str().ok_or("missing SQL")?;
        if binding["sql_sha256"] != crate::test_support::sha256_hex(sql.as_bytes()) {
            return Err("creation SQL drift".into());
        }
        let actual = match parse_postgres_statement(sql) {
            Err(error) => json!({"status":"parse_reject", "code":format!("{:?}",error.code)}),
            Ok(PostgresStatementSyntax::CreatePropertyGraph(create)) => {
                match bind_postgres_create_property_graph(sql, &create, &catalog()) {
                    Ok(graph) => json!({"status":"accept","schema":graph_json(&graph)}),
                    Err(error) => json!({"status":"bind_reject","code":format!("{:?}",error.code)}),
                }
            }
            Ok(_) => return Err("creation case changed statement family".into()),
        };
        if actual != binding["outcome"] {
            return Err(format!(
                "{id}: expected {}, got {actual}",
                binding["outcome"]
            ));
        }
    }
    let extra = fixture["additional_cases"]
        .as_array()
        .ok_or("missing additional binding cases")?;
    if extra.len() != 20 {
        return Err("missing or unreviewed additional binding case".into());
    }
    let mut families = BTreeSet::new();
    for case in extra {
        let id = case["id"].as_str().ok_or("missing additional id")?;
        if !seen.insert(id) {
            return Err("duplicate binding case".into());
        }
        let family = case["family"].as_str().ok_or("missing binding family")?;
        families.insert(family);
        if case["reference"].as_str().is_none_or(str::is_empty) {
            return Err("missing binding provenance".into());
        }
        let sql = case["sql"].as_str().ok_or("missing additional SQL")?;
        let actual = match parse_postgres_statement(sql) {
            Err(error) => json!({"status":"parse_reject", "code":format!("{:?}",error.code)}),
            Ok(PostgresStatementSyntax::CreatePropertyGraph(create)) => {
                match bind_postgres_create_property_graph(sql, &create, &catalog()) {
                    Ok(graph) => json!({"status":"accept","schema":graph_json(&graph)}),
                    Err(error) => json!({"status":"bind_reject","code":format!("{:?}",error.code)}),
                }
            }
            _ => return Err("binding case changed family".into()),
        };
        if actual != case["outcome"] {
            return Err(format!("{id}: expected {}, got {actual}", case["outcome"]));
        }
    }
    if families
        != BTreeSet::from([
            "elements",
            "keys",
            "endpoints",
            "labels",
            "properties",
            "profile",
        ])
    {
        return Err("missing or stale binding family".into());
    }
    Ok(())
}

#[test]
fn all_existing_creation_corpus_cases_have_bound_outcomes() {
    check_fixture(&serde_json::from_str(BINDINGS).unwrap()).unwrap();
}

#[test]
fn creation_corpus_rejects_coverage_provenance_profile_and_schema_drift() {
    let original: Value = serde_json::from_str(BINDINGS).unwrap();
    for mutation in 0..8 {
        let mut fixture = original.clone();
        match mutation {
            0 => {
                fixture["bindings"].as_array_mut().unwrap().pop();
            }
            1 => {
                fixture["bindings"][0]["sql_sha256"] = json!("wrong");
            }
            2 => {
                fixture["profile_exclusions"].as_array_mut().unwrap().pop();
            }
            3 => {
                fixture["bindings"][0]["outcome"]["schema"]["vertex_labels"] = json!({});
            }
            4 => {
                fixture["source_corpus_sha256"] = json!("wrong");
            }
            5 => {
                fixture["bindings"][1] = fixture["bindings"][0].clone();
            }
            6 => {
                fixture["additional_cases"] = json!([]);
            }
            _ => {
                fixture["additional_cases"].as_array_mut().unwrap().pop();
            }
        }
        assert!(check_fixture(&fixture).is_err(), "mutation {mutation}");
    }
}