Skip to main content

uqa_sql/catalog/events/
persistence.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Durable event-catalog envelopes and persistence selection over live relation metadata.
8use super::{RuleCatalog, StoredRule, StoredTrigger, TriggerCatalog};
9use crate::ast::RelationPersistence;
10use serde::{Deserialize, Serialize};
11use uqa_core::RelationIdentity;
12pub const RULE_CATALOG_FORMAT_VERSION: u32 = 3;
13pub trait EventRelationPersistence {
14    fn rule_relation_is_temporary(&self, relation: &RelationIdentity) -> bool;
15    fn trigger_relation_persistence(
16        &self,
17        relation: &RelationIdentity,
18    ) -> Option<RelationPersistence>;
19}
20#[derive(Default, Serialize, Deserialize)]
21pub struct StoredTriggerCatalog {
22    #[serde(default, skip_serializing_if = "Vec::is_empty")]
23    pub triggers: Vec<StoredTrigger>,
24}
25
26#[derive(Default, Serialize, Deserialize)]
27pub struct StoredRuleCatalog {
28    #[serde(default)]
29    pub format_version: u32,
30    #[serde(default, skip_serializing_if = "Vec::is_empty")]
31    pub rules: Vec<StoredRule>,
32}
33
34pub fn stored_rules_snapshot(
35    rules: &RuleCatalog,
36    relations: &dyn EventRelationPersistence,
37) -> StoredRuleCatalog {
38    StoredRuleCatalog {
39        format_version: RULE_CATALOG_FORMAT_VERSION,
40        rules: rules
41            .iter()
42            .filter(|(relation, _)| !relations.rule_relation_is_temporary(relation))
43            .flat_map(|(_, entries)| entries.values().cloned())
44            .collect(),
45    }
46}
47
48pub fn stored_triggers_snapshot(
49    triggers: &TriggerCatalog,
50    relations: &dyn EventRelationPersistence,
51) -> StoredTriggerCatalog {
52    StoredTriggerCatalog {
53        triggers: triggers
54            .iter()
55            .filter(|(relation, _)| {
56                relations
57                    .trigger_relation_persistence(relation)
58                    .is_some_and(|persistence| persistence != RelationPersistence::Temporary)
59            })
60            .flat_map(|(_, entries)| entries.values().cloned())
61            .collect(),
62    }
63}
64
65pub fn rule_catalog_requires_migration(
66    format_version: u32,
67    allows_migration: bool,
68) -> Result<bool, String> {
69    if format_version > RULE_CATALOG_FORMAT_VERSION {
70        return Err(format!(
71            "rule catalog format {format_version} is newer than supported format {RULE_CATALOG_FORMAT_VERSION}"
72        ));
73    }
74    let migrating_catalog = format_version < RULE_CATALOG_FORMAT_VERSION;
75    if migrating_catalog && !allows_migration {
76        return Err("rule catalog requires an initial-open format migration".into());
77    }
78
79    Ok(migrating_catalog)
80}
81
82#[cfg(test)]
83mod tests;