uqa-engine 0.3.5

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

use crate::Engine;
use std::sync::Arc;
use uqa_execution::schema::events::persistence::{RULES_METADATA_KEY, TRIGGERS_METADATA_KEY};

#[test]
fn load_only_rule_restore_rejects_legacy_metadata_without_publishing_or_writing() {
    let directory = tempfile::tempdir().unwrap();
    let engine = Engine::open(&directory.path().join("rules.db")).unwrap();
    engine
        .sql(
            "CREATE TABLE items(id integer); CREATE RULE saved AS ON DELETE TO items DO NOTHING",
            &[],
        )
        .unwrap();
    let before = engine.durable.rules.snapshot();
    let catalog = engine.storage.catalog.as_ref().unwrap();
    let legacy = r#"{"format_version":2,"rules":[]}"#;
    catalog.set_metadata(RULES_METADATA_KEY, legacy).unwrap();
    let error = engine
        .event_restore_context()
        .restore_rules_from_metadata(catalog.as_ref(), false)
        .unwrap_err();
    assert!(error
        .to_string()
        .contains("requires an initial-open format migration"));
    assert!(Arc::ptr_eq(&before, &engine.durable.rules.snapshot()));
    assert_eq!(
        catalog.get_metadata(RULES_METADATA_KEY).unwrap().as_deref(),
        Some(legacy)
    );
}

#[test]
fn trigger_identity_migration_is_initial_only_and_survives_reopen() {
    let directory = tempfile::tempdir().unwrap();
    let path = directory.path().join("triggers.db");
    let engine = Engine::open(&path).unwrap();
    engine.sql("CREATE TABLE items(id integer); CREATE FUNCTION handler() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END $$; CREATE TRIGGER saved BEFORE INSERT ON items FOR EACH ROW EXECUTE FUNCTION handler()",&[]).unwrap();
    let catalog = engine.storage.catalog.as_ref().unwrap();
    let mut stored: serde_json::Value = serde_json::from_str(
        &catalog
            .get_metadata(TRIGGERS_METADATA_KEY)
            .unwrap()
            .unwrap(),
    )
    .unwrap();
    let trigger = stored["triggers"][0].as_object_mut().unwrap();
    trigger.remove("object_id");
    trigger.remove("function_object_id");
    let legacy = serde_json::to_string(&stored).unwrap();
    catalog
        .set_metadata(TRIGGERS_METADATA_KEY, &legacy)
        .unwrap();
    let before = engine.durable.triggers.snapshot();
    let Err(error) = engine.new_session() else {
        panic!("secondary session must not migrate trigger identities");
    };
    assert!(error
        .to_string()
        .contains("requires an initial-open function-identity migration"));
    assert!(Arc::ptr_eq(&before, &engine.durable.triggers.snapshot()));
    assert_eq!(
        catalog
            .get_metadata(TRIGGERS_METADATA_KEY)
            .unwrap()
            .unwrap(),
        legacy
    );
    drop(engine);
    let engine = Engine::open(&path).unwrap();
    let catalog = engine.storage.catalog.as_ref().unwrap();
    let migrated = engine.event_lookup_context().list_triggers();
    assert_eq!(migrated.len(), 1);
    assert_eq!(
        migrated[0].object_id,
        Some([198, 118, 61, 239, 113, 219, 106, 187, 17, 102, 87, 113, 78, 105, 185, 52])
    );
    assert!(migrated[0].function_object_id.is_some());
    let encoded = catalog
        .get_metadata(TRIGGERS_METADATA_KEY)
        .unwrap()
        .unwrap();
    drop(engine);
    let reopened = Engine::open(&path).unwrap();
    assert_eq!(
        reopened.event_lookup_context().list_triggers()[0].object_id,
        migrated[0].object_id
    );
    assert_eq!(
        reopened
            .storage
            .catalog
            .as_ref()
            .unwrap()
            .get_metadata(TRIGGERS_METADATA_KEY)
            .unwrap()
            .unwrap(),
        encoded
    );
}

#[test]
fn load_only_event_restore_preserves_session_temporary_rule_and_trigger_definitions() {
    let directory = tempfile::tempdir().unwrap();
    let engine = Engine::open(&directory.path().join("temporary.db")).unwrap();
    engine.sql("CREATE TABLE items(id integer); CREATE TEMP TABLE temporary_items(id integer); CREATE FUNCTION handler() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END $$; CREATE TRIGGER persistent_trigger BEFORE INSERT ON items FOR EACH ROW EXECUTE FUNCTION handler(); CREATE TRIGGER temporary_trigger BEFORE INSERT ON temporary_items FOR EACH ROW EXECUTE FUNCTION handler(); CREATE RULE persistent_rule AS ON DELETE TO items DO NOTHING; CREATE RULE temporary_rule AS ON DELETE TO temporary_items DO NOTHING",&[]).unwrap();
    let temporary = engine
        .durable
        .triggers
        .read()
        .keys()
        .find(|relation| relation.name == "temporary_items")
        .cloned()
        .unwrap();
    let rules_before = serde_json::to_string(&engine.durable.rules.read()[&temporary]).unwrap();
    let triggers_before =
        serde_json::to_string(&engine.durable.triggers.read()[&temporary]).unwrap();
    let catalog = engine.storage.catalog.as_ref().unwrap();
    let durable_rules = catalog.get_metadata(RULES_METADATA_KEY).unwrap().unwrap();
    let durable_triggers = catalog
        .get_metadata(TRIGGERS_METADATA_KEY)
        .unwrap()
        .unwrap();
    assert!(!durable_rules.contains("temporary_items"));
    assert!(!durable_triggers.contains("temporary_items"));
    let context = engine.event_restore_context();
    context
        .restore_triggers_from_metadata(catalog.as_ref(), false)
        .unwrap();
    context
        .restore_rules_from_metadata(catalog.as_ref(), false)
        .unwrap();
    assert_eq!(
        serde_json::to_string(&engine.durable.rules.read()[&temporary]).unwrap(),
        rules_before
    );
    assert_eq!(
        serde_json::to_string(&engine.durable.triggers.read()[&temporary]).unwrap(),
        triggers_before
    );
    assert_eq!(
        catalog.get_metadata(RULES_METADATA_KEY).unwrap().unwrap(),
        durable_rules
    );
    assert_eq!(
        catalog
            .get_metadata(TRIGGERS_METADATA_KEY)
            .unwrap()
            .unwrap(),
        durable_triggers
    );
}