uqa-engine 0.2.3

Engine: schema-aware table store, catalog restore, transactions
//
// Unified Query Algebra
//
// Copyright (c) 2023-2026 Cognica, Inc.
//

use super::*;
use crate::StoredViewKind;

#[test]
fn catalog_views_share_schema_and_graph_allocations_until_mutation() {
    let engine = Engine::new();
    engine
        .sql(
            "CREATE TABLE items (id INTEGER PRIMARY KEY, label TEXT)",
            &[],
        )
        .unwrap();
    engine.create_graph("shared_graph").unwrap();
    let first = engine.catalog_read_view();
    let second = engine.catalog_read_view();
    let relation = crate::RelationIdentity::new("public", "items");
    assert!(Arc::ptr_eq(
        &first.snapshot.tables[&relation].columns,
        &second.snapshot.tables[&relation].columns
    ));
    assert!(Arc::ptr_eq(
        &first.snapshot.durable.graphs,
        &second.snapshot.durable.graphs
    ));
    assert!(Arc::ptr_eq(
        &first.snapshot.durable.schemas,
        &second.snapshot.durable.schemas
    ));
    engine
        .sql("ALTER TABLE items ADD COLUMN extra INTEGER", &[])
        .unwrap();
    engine.create_graph("later_graph").unwrap();
    let current = engine.catalog_read_view();
    assert_eq!(first.snapshot.tables[&relation].columns.len(), 2);
    assert_eq!(current.snapshot.tables[&relation].columns.len(), 3);
    assert_eq!(first.graph_names(), vec!["shared_graph"]);
    assert_eq!(current.graph_names(), vec!["later_graph", "shared_graph"]);
}

#[test]
fn capability_views_expose_only_their_owned_state() {
    let engine = Engine::new();
    let catalog = engine.catalog_read_view();
    let session = engine.session_execution_view();
    let runtime = engine.query_runtime_view();
    assert!(catalog.has_schema("public"));
    assert_eq!(session.search_path(), vec!["public"]);
    assert_eq!(session.current_user(), "uqa");
    assert_eq!(session.session_user(), "uqa");
    assert_eq!(session.transaction_depth(), 0);
    assert_eq!(session.transaction_snapshot_identity(), None);
    assert_eq!(runtime.work_mem_bytes().unwrap(), 64 * 1024 * 1024);
    runtime.check_cancelled().unwrap();
}

#[test]
fn mutation_coordinator_publishes_schema_changes_without_engine_recovery() {
    let engine = Engine::new();
    let snapshot = engine.catalog_read_view();
    let before = engine.catalog_epochs();
    assert!(engine
        .mutation_coordinator()
        .register_schema("capability_test", false, "uqa")
        .unwrap());
    assert!(!snapshot.has_schema("capability_test"));
    assert!(engine.catalog_read_view().has_schema("capability_test"));
    let after = engine.catalog_epochs();
    assert_eq!(after.catalog_registry, before.catalog_registry);
    assert!(
        engine
            .epochs
            .catalog_registry
            .published
            .load(Ordering::Acquire)
            > before.catalog_registry
    );
}

#[test]
fn catalog_read_view_keeps_statement_relation_snapshot() {
    let engine = Engine::new();
    let snapshot = engine.catalog_read_view();
    engine
        .create_table(
            "snapshot_table",
            uqa_analysis::standard_analyzer("english"),
            Vec::new(),
        )
        .unwrap();
    let resolution = engine.session_execution_view().relation_name_resolution();
    assert_eq!(
        snapshot.table_name(&resolution, "snapshot_table").unwrap(),
        None
    );
    assert_eq!(
        engine
            .catalog_read_view()
            .table_name(&resolution, "snapshot_table")
            .unwrap()
            .as_deref(),
        Some("public.snapshot_table")
    );
}

#[test]
fn catalog_read_view_keeps_all_live_projection_families_on_one_snapshot() {
    let engine = Engine::new();
    let snapshot = engine.catalog_read_view();

    engine
        .sql("CREATE SEQUENCE snapshot_sequence", &[])
        .unwrap();
    engine
        .sql("CREATE VIEW snapshot_view AS SELECT 1 AS id", &[])
        .unwrap();
    engine.sql("CREATE ROLE snapshot_role", &[]).unwrap();
    engine.create_graph("snapshot_graph").unwrap();

    assert!(snapshot.sequences().is_empty());
    assert!(snapshot.views_of_kind(StoredViewKind::View).is_empty());
    assert!(!snapshot.roles().any(|role| role.name == "snapshot_role"));
    assert!(snapshot.graph_names().is_empty());

    let current = engine.catalog_read_view();
    assert!(current
        .sequences()
        .iter()
        .any(|(name, _, _, _)| name == "public.snapshot_sequence"));
    assert!(current
        .views_of_kind(StoredViewKind::View)
        .iter()
        .any(|(name, _)| name == "public.snapshot_view"));
    assert!(current.roles().any(|role| role.name == "snapshot_role"));
    assert_eq!(current.graph_names(), vec!["snapshot_graph"]);
}

#[test]
fn catalog_read_view_keeps_routines_triggers_and_rules_on_one_snapshot() {
    let engine = Engine::new();
    let snapshot = engine.catalog_read_view();

    engine
        .sql(
            "CREATE TABLE snapshot_events (id INTEGER); CREATE FUNCTION snapshot_trigger_function() RETURNS trigger LANGUAGE plpgsql AS 'BEGIN RETURN NEW; END'; CREATE TRIGGER snapshot_trigger BEFORE INSERT ON snapshot_events FOR EACH ROW EXECUTE FUNCTION snapshot_trigger_function(); CREATE RULE snapshot_rule AS ON UPDATE TO snapshot_events DO ALSO NOTHING",
            &[],
        )
        .unwrap();

    assert!(snapshot.all_sql_functions().is_empty());
    assert!(snapshot.triggers().is_empty());
    assert!(snapshot.rules().is_empty());

    let current = engine.catalog_read_view();
    assert_eq!(current.all_sql_functions().len(), 1);
    assert!(current
        .triggers()
        .iter()
        .any(|trigger| trigger.definition.name == "snapshot_trigger"));
    assert!(current
        .rules()
        .iter()
        .any(|rule| rule.definition.name == "snapshot_rule"));
}

#[test]
fn relation_name_resolution_keeps_the_statement_search_path() {
    let engine = Engine::new();
    engine
        .sql(
            "CREATE SCHEMA first_path; CREATE SCHEMA second_path; CREATE TABLE first_path.items (id integer); CREATE TABLE second_path.items (id integer)",
            &[],
        )
        .unwrap();
    engine.set_variable("search_path", "first_path").unwrap();
    let catalog = engine.catalog_read_view();
    let resolution = engine.session_execution_view().relation_name_resolution();

    engine.set_variable("search_path", "second_path").unwrap();
    let current_resolution = engine.session_execution_view().relation_name_resolution();

    assert_eq!(
        catalog.table_name(&resolution, "items").unwrap().as_deref(),
        Some("first_path.items")
    );
    assert_eq!(
        catalog
            .table_name(&current_resolution, "items")
            .unwrap()
            .as_deref(),
        Some("second_path.items")
    );
}

#[test]
fn relation_resolution_preserves_the_missing_namespace_outcome() {
    let engine = Engine::new();
    let catalog = engine.catalog_read_view();
    let resolution = engine.session_execution_view().relation_name_resolution();

    assert_eq!(
        catalog
            .relation_kind_resolution(&resolution, "missing_schema.items")
            .unwrap(),
        RelationResolution::MissingSchema("missing_schema".into())
    );
    assert_eq!(
        catalog
            .relation_kind_resolution(&resolution, "missing_items")
            .unwrap(),
        RelationResolution::MissingRelation
    );
    assert_eq!(
        catalog
            .relation_kind_resolution(&resolution, "pg_temp.missing_items")
            .unwrap(),
        RelationResolution::MissingSchema("pg_temp".into())
    );

    engine
        .sql(
            "CREATE TEMP TABLE allocate_temp_namespace (id integer)",
            &[],
        )
        .unwrap();
    let allocated = engine.session_execution_view().relation_name_resolution();
    assert_eq!(
        engine
            .catalog_read_view()
            .relation_kind_resolution(&allocated, "pg_temp.missing_items")
            .unwrap(),
        RelationResolution::MissingRelation
    );

    let mut bound = resolution;
    bound.set_lookup_mode(RelationLookupMode::Bound);
    assert_eq!(
        catalog
            .relation_kind_resolution(&bound, "missing_schema.items")
            .unwrap(),
        RelationResolution::MissingRelation
    );
}