Skip to main content

uqa_storage/
catalog.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Backend-neutral persistent catalog facade.
8//!
9//! The engine depends on this trait for table metadata, analyzers, models,
10//! graph registries, and planner statistics. Concrete storage layers such as
11//! `SQLite` or a future RocksDB-backed catalog implement it behind the same
12//! object-safe boundary.
13
14use serde::{Deserialize, Serialize};
15
16use crate::backend::{StorageBackendError, StorageBackendResult};
17
18mod cache_revisions;
19mod graph_access;
20pub mod graph_guards;
21pub mod graph_identifiers;
22pub mod graph_observations;
23pub(crate) mod graph_snapshot;
24mod identity;
25mod relation;
26pub mod relation_acl;
27mod relation_security;
28mod schema;
29mod sequence_security;
30
31pub use cache_revisions::CatalogCacheRevisions;
32pub use graph_access::validate_graph_page;
33pub use graph_access::{
34    GraphEntityFilter, GraphEntityKind, GraphEntitySelector, MAX_GRAPH_ID_PAGE,
35};
36pub use identity::new_nonzero_catalog_identity;
37pub use relation::RelationIdentity;
38pub use relation_security::{BoundRelationSecurity, LegacyRelationSecurity, RelationSecurityRow};
39pub use sequence_security::{BoundSequenceSecurity, LegacySequenceSecurity, SequenceSecurityRow};
40mod table;
41
42pub use schema::{BoundSchemaRow, SchemaAclEntry, SchemaPrivileges, SchemaRow};
43pub use table::{TableAclEntry, TablePrivileges};
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub enum RelationKind {
48    Table,
49    View,
50    Sequence,
51    ForeignTable,
52    Index,
53}
54
55impl RelationKind {
56    pub fn as_str(self) -> &'static str {
57        match self {
58            Self::Table => "table",
59            Self::View => "view",
60            Self::Sequence => "sequence",
61            Self::ForeignTable => "foreign_table",
62            Self::Index => "index",
63        }
64    }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct TableSchema {
69    pub relation: RelationIdentity,
70    #[serde(flatten)]
71    pub security: RelationSecurityRow,
72    /// Stable logical relation identity. `CREATE TABLE` allocates a new value, while renames, schema changes, `TRUNCATE`, and reopen preserve it. A zero value marks a legacy catalog row that the engine upgrades during open.
73    #[serde(default)]
74    pub object_id: [u8; 16],
75    /// Stable physical-storage identity. `CREATE TABLE` and `TRUNCATE` allocate a new value, while schema-only alterations and renames preserve it. A zero value marks a legacy catalog row that the engine upgrades during open.
76    #[serde(default)]
77    pub storage_generation: [u8; 16],
78    pub analyzer_json: String,
79    pub fts_fields: Vec<String>,
80    pub vector_fields: Vec<VectorFieldSchema>,
81    /// Serialized `Vec<uqa_sql::ast::ColumnDef>` capturing the schema
82    /// columns (name, type, `auto_increment`, flags). Empty for
83    /// tables created by the legacy code path before column tracking
84    /// existed.
85    #[serde(default)]
86    pub columns_json: String,
87    /// Serialized `uqa_sql::ast::TableConstraintSet`. Empty for catalogs
88    /// created before durable table constraints were introduced.
89    #[serde(default)]
90    pub constraints_json: String,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct VectorFieldSchema {
95    pub field: String,
96    pub dimensions: u32,
97}
98
99/// One row from graph-edge persistence, represented as a typed struct so the
100/// catalog API stays explicit and clippy-clean.
101#[derive(Debug, Clone)]
102pub struct EdgeRow {
103    pub edge_id: u64,
104    pub source_id: u64,
105    pub target_id: u64,
106    pub label: String,
107    pub properties_json: String,
108}
109
110/// One vertex in an atomic named-graph snapshot replacement.
111#[derive(Debug, Clone)]
112pub struct GraphVertexRow {
113    pub vertex_id: u64,
114    pub label: String,
115    pub properties_json: String,
116}
117
118/// Complete persisted shape of one named graph. Catalog implementations
119/// replace the graph membership and entity rows as one atomic operation.
120#[derive(Debug, Clone)]
121pub struct GraphSnapshot {
122    pub vertices: Vec<GraphVertexRow>,
123    pub edges: Vec<EdgeRow>,
124    pub label_registry_json: String,
125}
126
127/// One row from the foreign-table registry.
128#[derive(Debug, Clone)]
129pub struct ForeignTableRow {
130    pub relation: RelationIdentity,
131    pub security: RelationSecurityRow,
132    pub server_name: String,
133    pub columns_json: String,
134    pub options_json: String,
135}
136
137/// One durable view definition. `definition_json` contains a serialized
138/// planner query plan, while ownership remains a typed catalog relation.
139#[derive(Debug, Clone)]
140pub struct ViewRow {
141    pub relation: RelationIdentity,
142    pub security: RelationSecurityRow,
143    pub definition_json: String,
144}
145
146pub use uqa_core::catalog_index::CatalogIndexRow;
147
148/// Values persisted into one column-statistics row.
149#[derive(Debug, Clone, Copy)]
150pub struct ColumnStatsInput<'a> {
151    pub table_name: &'a str,
152    pub column_name: &'a str,
153    pub distinct_count: i64,
154    pub null_count: i64,
155    pub min_value: Option<&'a str>,
156    pub max_value: Option<&'a str>,
157    pub row_count: i64,
158    pub histogram_json: &'a str,
159    pub mcv_values_json: &'a str,
160    pub mcv_frequencies_json: &'a str,
161}
162
163impl<'a> ColumnStatsInput<'a> {
164    pub fn basic(
165        table_name: &'a str,
166        column_name: &'a str,
167        distinct_count: i64,
168        null_count: i64,
169        min_value: Option<&'a str>,
170        max_value: Option<&'a str>,
171        row_count: i64,
172    ) -> Self {
173        Self {
174            table_name,
175            column_name,
176            distinct_count,
177            null_count,
178            min_value,
179            max_value,
180            row_count,
181            histogram_json: "[]",
182            mcv_values_json: "[]",
183            mcv_frequencies_json: "[]",
184        }
185    }
186}
187
188/// One row from persisted column statistics.
189#[derive(Debug, Clone, PartialEq)]
190pub struct ColumnStatsRow {
191    pub column_name: String,
192    pub distinct_count: i64,
193    pub null_count: i64,
194    pub min_value: Option<String>,
195    pub max_value: Option<String>,
196    pub row_count: i64,
197    pub histogram_json: String,
198    pub mcv_values_json: String,
199    pub mcv_frequencies_json: String,
200}
201
202/// Durable SQL sequence state. Sequence allocation is implemented by the
203/// catalog backend as one atomic mutation so independent engine sessions (and
204/// independently opened engines) cannot return the same value.
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
206pub struct SequenceOptions {
207    pub data_type: String,
208    /// `None` is accepted only while decoding a legacy row and is resolved from its increment direction by native sequence restoration.
209    pub min_value: Option<i64>,
210    /// `None` is accepted only while decoding a legacy row and is resolved from its increment direction by native sequence restoration.
211    pub max_value: Option<i64>,
212    pub cycle: bool,
213    #[serde(default = "default_sequence_cache_size")]
214    pub cache_size: i64,
215}
216
217const fn default_sequence_cache_size() -> i64 {
218    1
219}
220
221impl Default for SequenceOptions {
222    fn default() -> Self {
223        Self {
224            data_type: "bigint".into(),
225            min_value: None,
226            max_value: None,
227            cycle: false,
228            cache_size: default_sequence_cache_size(),
229        }
230    }
231}
232
233pub use uqa_core::catalog_sequence::{
234    SequenceAclEntry, SequenceOwner, SequenceOwnerDependency, SequencePrivileges,
235};
236
237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
238pub struct SequenceRow {
239    pub relation: RelationIdentity,
240    /// Captured owner and ACL role identities, or a legacy row awaiting initial-open migration.
241    #[serde(flatten)]
242    pub security: SequenceSecurityRow,
243    /// Stable identity of this sequence incarnation. Dropping and recreating the same qualified name must allocate a different value.
244    #[serde(default)]
245    pub object_id: [u8; 16],
246    /// Changes when value-generation options or persistence replace the sequence's allocation state. Stable across ownership and name changes, value reservations, and `setval`.
247    #[serde(default)]
248    pub definition_generation: [u8; 16],
249    pub start: i64,
250    pub increment: i64,
251    pub current: i64,
252    /// False until the first allocation returns `current` verbatim.
253    pub called: bool,
254    /// Number of values whose advancement is already durable in the sequence log.
255    #[serde(default)]
256    pub log_count: i64,
257    /// `PostgreSQL` `pg_class.relpersistence` code. Durable sequence rows accept only permanent (`p`) and unlogged (`u`) values.
258    pub persistence: String,
259    #[serde(default)]
260    pub owner: Option<SequenceOwner>,
261    #[serde(default)]
262    pub options: SequenceOptions,
263}
264
265/// Physical sequence position consumed by one atomic reservation.
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub struct SequenceValuePosition {
268    pub current: i64,
269    pub called: bool,
270    pub log_count: i64,
271}
272
273/// One atomic sequence reservation. `first_value` is returned immediately, while the remaining values through `last_value` belong to the allocating session.
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub struct SequenceValueReservation {
276    pub first_value: i64,
277    pub last_value: i64,
278    pub count: i64,
279    pub log_count: i64,
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283pub enum SequenceReservationResult {
284    Missing,
285    DefinitionChanged,
286    Exhausted,
287    Reserved(SequenceValueReservation),
288}
289
290/// A value update applies only to the definition whose bounds the caller validated.
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292pub enum SequenceSetValueResult {
293    Missing,
294    DefinitionChanged,
295    Set(i64),
296}
297
298/// Reserve up to `cache_size` values without crossing a sequence bound. Cycling is applied when selecting the first value of a new reservation, matching `PostgreSQL`'s boundary-truncated cache blocks.
299#[must_use]
300pub fn sequence_value_reservation(
301    position: SequenceValuePosition,
302    increment: i64,
303    min_value: i64,
304    max_value: i64,
305    cycle: bool,
306    cache_size: i64,
307) -> Option<SequenceValueReservation> {
308    let SequenceValuePosition {
309        current,
310        called,
311        log_count,
312    } = position;
313    debug_assert_ne!(increment, 0);
314    debug_assert!(cache_size > 0);
315    let first_value = if called {
316        match current
317            .checked_add(increment)
318            .filter(|value| (min_value..=max_value).contains(value))
319        {
320            Some(value) => value,
321            None if cycle && increment > 0 => min_value,
322            None if cycle => max_value,
323            None => return None,
324        }
325    } else {
326        current
327    };
328    let distance = if increment > 0 {
329        i128::from(max_value) - i128::from(first_value)
330    } else {
331        i128::from(first_value) - i128::from(min_value)
332    };
333    let step = i128::from(increment).abs();
334    let available = distance / step + 1;
335    let count = available.min(i128::from(cache_size));
336    let last_value = i128::from(first_value) + i128::from(increment) * (count - 1);
337    let initial_count = i128::from(!called);
338    let cache_fetch = i128::from(cache_size) - initial_count;
339    let mut fetch = cache_fetch;
340    let mut next_log_count = i128::from(log_count);
341    if i128::from(log_count) < cache_fetch || !called {
342        fetch += 32;
343        next_log_count = fetch;
344    }
345    let fetched = fetch.min(available - initial_count);
346    next_log_count -= fetched.min(cache_fetch);
347    next_log_count -= fetch - fetched;
348    Some(SequenceValueReservation {
349        first_value,
350        last_value: i64::try_from(last_value).expect("reserved sequence value stays in bounds"),
351        count: i64::try_from(count).expect("reservation count cannot exceed cache size"),
352        log_count: i64::try_from(next_log_count)
353            .expect("persisted sequence log count cannot exceed the cache request"),
354    })
355}
356
357/// Engine-facing catalog facade for persistent metadata.
358pub trait CatalogFacade: Send + Sync {
359    /// Transaction model and database incarnation shared with the paired backend. Wrappers must forward this with the session affinity.
360    fn transaction_model(&self) -> crate::StorageTransactionModel {
361        crate::StorageTransactionModel::ProviderSerialized
362    }
363
364    /// Identity shared with the paired data backend's transaction context. Wrappers must delegate this when their underlying catalog reports an identity.
365    fn transaction_affinity(&self) -> Option<crate::StorageSessionAffinity> {
366        None
367    }
368
369    /// Prepare durable catalog storage inside the backend's owning initial-restore transaction. Already initialized catalogs may keep the default.
370    fn initialize_storage(&self) -> StorageBackendResult<()> {
371        Ok(())
372    }
373
374    /// Read transactional cache generations without loading schemas or column
375    /// statistics. Providers without change tracking use conservative reloads.
376    fn cache_revisions(&self) -> StorageBackendResult<Option<CatalogCacheRevisions>> {
377        Ok(None)
378    }
379
380    fn set_metadata(&self, key: &str, value: &str) -> StorageBackendResult<()>;
381    /// Remove one metadata record, retaining its private deletion and write precondition until transaction end.
382    fn delete_metadata(&self, key: &str) -> StorageBackendResult<()>;
383    fn get_metadata(&self, key: &str) -> StorageBackendResult<Option<String>>;
384    /// Read matching metadata keys from one catalog snapshot; the prefix is literal, including NUL and wildcard characters.
385    fn metadata_with_prefix(&self, prefix: &str) -> StorageBackendResult<Vec<(String, String)>>;
386    /// Replace one already bound relation or attribute ACL without rewriting its definition. The caller holds the relation lifetime and catalog tuple locks until transaction end.
387    fn save_relation_acl(
388        &self,
389        relation: &RelationIdentity,
390        column: Option<&str>,
391        entry: &relation_acl::RelationAclTuple,
392    ) -> StorageBackendResult<()> {
393        if !self.transaction_model().is_versioned() {
394            return relation_acl::save_serialized(self, relation, column, entry);
395        }
396        self.set_metadata(&relation_acl::key(relation, column), &entry.encode(column)?)
397    }
398    /// Whether this metadata key has a transaction-private replacement in the current session.
399    fn metadata_has_private_changes(&self, _key: &str) -> StorageBackendResult<bool> {
400        if self.transaction_model().is_versioned() {
401            return Err(StorageBackendError::Other(
402                "private metadata provenance is not supported by this catalog".into(),
403            ));
404        }
405        Ok(false)
406    }
407    /// Persist evaluated maintenance state. Concurrent providers merge its counters under their existing record-publication boundary; serialized providers use an ordinary metadata replacement.
408    fn save_statistics_maintenance(
409        &self,
410        table: &str,
411        state: &crate::statistics_maintenance::StatisticsMaintenance,
412    ) -> StorageBackendResult<()> {
413        self.set_metadata(
414            &crate::statistics_maintenance::StatisticsMaintenance::key(table),
415            &serde_json::to_string(state)?,
416        )
417    }
418    fn fts_storage_was_reset(&self) -> bool {
419        false
420    }
421
422    /// Atomically migrate the former flat relation namespace into typed,
423    /// schema-owned catalog objects. Implementations must reject normalized
424    /// or cross-kind collisions instead of merging either object.
425    fn migrate_relation_namespace(&self) -> StorageBackendResult<()>;
426
427    fn save_schema_row(&self, schema: &SchemaRow) -> StorageBackendResult<()>;
428    fn drop_schema(&self, name: &str) -> StorageBackendResult<()>;
429    fn load_schema_rows(&self) -> StorageBackendResult<Vec<SchemaRow>>;
430
431    /// Whether this exact schema has a private creation, replacement or deletion in the current session.
432    fn schema_has_private_changes(&self, _name: &str) -> StorageBackendResult<bool> {
433        if self.transaction_model().is_versioned() {
434            return Err(StorageBackendError::Other(
435                "private schema provenance is not supported by this catalog".into(),
436            ));
437        }
438        Ok(false)
439    }
440
441    fn save_schema(&self, name: &str) -> StorageBackendResult<()> {
442        self.save_schema_row(&SchemaRow::bootstrap(name))
443    }
444
445    fn load_schemas(&self) -> StorageBackendResult<Vec<String>> {
446        Ok(self
447            .load_schema_rows()?
448            .into_iter()
449            .map(|schema| schema.name().to_owned())
450            .collect())
451    }
452
453    fn save_table(&self, schema: &TableSchema) -> StorageBackendResult<()>;
454    fn load_tables(&self) -> StorageBackendResult<Vec<TableSchema>>;
455    fn drop_table(&self, name: &str) -> StorageBackendResult<()>;
456    /// Remove a table definition and every catalog/data row owned by it as
457    /// one atomic catalog operation.
458    fn drop_table_and_data(&self, name: &str) -> StorageBackendResult<()>;
459    fn purge_table_data(&self, name: &str) -> StorageBackendResult<()>;
460    fn rename_table_data(&self, from: &str, to: &str) -> StorageBackendResult<()>;
461    fn drop_column_data(&self, table_name: &str, column_name: &str) -> StorageBackendResult<()>;
462    fn rename_column_data(
463        &self,
464        table_name: &str,
465        from: &str,
466        to: &str,
467    ) -> StorageBackendResult<()>;
468
469    fn save_model(&self, name: &str, json: &str) -> StorageBackendResult<()>;
470    fn load_models(&self) -> StorageBackendResult<Vec<(String, String)>>;
471    fn load_model(&self, name: &str) -> StorageBackendResult<Option<String>>;
472    fn drop_model(&self, name: &str) -> StorageBackendResult<()>;
473
474    fn save_scoring_params(&self, name: &str, params_json: &str) -> StorageBackendResult<()>;
475    fn load_scoring_params(&self, name: &str) -> StorageBackendResult<Option<String>>;
476    fn load_all_scoring_params(&self) -> StorageBackendResult<Vec<(String, String)>>;
477    fn drop_scoring_params(&self, name: &str) -> StorageBackendResult<()>;
478
479    fn create_sequence_row(&self, sequence: &SequenceRow) -> StorageBackendResult<bool>;
480    fn replace_sequence_row(&self, sequence: &SequenceRow) -> StorageBackendResult<bool>;
481    /// Atomically move one sequence catalog row and its shared relation claim while preserving object identity and physical value state.
482    fn rename_sequence_row(&self, from: &str, to: &str) -> StorageBackendResult<bool>;
483    fn drop_sequence_row(&self, name: &str) -> StorageBackendResult<bool>;
484    fn load_sequence_rows(&self) -> StorageBackendResult<Vec<SequenceRow>>;
485
486    /// Whether this sequence has transaction-private state that an autonomous value allocation must not bypass. Versioned catalogs must inspect their retained private records, including creation, rename and definition replacement.
487    fn sequence_has_private_changes(
488        &self,
489        _relation: &RelationIdentity,
490        _object_id: [u8; 16],
491    ) -> StorageBackendResult<bool> {
492        if self.transaction_model().is_versioned() {
493            return Err(StorageBackendError::Other(
494                "private sequence provenance is not supported by this catalog".into(),
495            ));
496        }
497        Ok(false)
498    }
499    fn reserve_sequence_values(
500        &self,
501        name: &str,
502        object_id: [u8; 16],
503        definition_generation: [u8; 16],
504    ) -> StorageBackendResult<SequenceReservationResult>;
505    /// Compatibility allocation API for callers that do not retain a session cache. A cached reservation's unused values are intentionally abandoned, just as when a `PostgreSQL` session disconnects.
506    fn next_sequence_value(
507        &self,
508        name: &str,
509        object_id: [u8; 16],
510    ) -> StorageBackendResult<Option<i64>> {
511        loop {
512            let relation =
513                RelationIdentity::from_legacy_name(name).map_err(StorageBackendError::Other)?;
514            let Some(row) = self
515                .load_sequence_rows()?
516                .into_iter()
517                .find(|row| row.relation == relation && row.object_id == object_id)
518            else {
519                return Ok(None);
520            };
521            match self.reserve_sequence_values(name, object_id, row.definition_generation)? {
522                SequenceReservationResult::Reserved(reservation) => {
523                    return Ok(Some(reservation.first_value));
524                }
525                SequenceReservationResult::DefinitionChanged => {}
526                SequenceReservationResult::Missing => return Ok(None),
527                SequenceReservationResult::Exhausted => {
528                    return Err(StorageBackendError::Other(format!(
529                        "sequence `{name}` exhausted"
530                    )));
531                }
532            }
533        }
534    }
535    fn set_sequence_value(
536        &self,
537        name: &str,
538        object_id: [u8; 16],
539        definition_generation: [u8; 16],
540        value: i64,
541        called: bool,
542        log_count: i64,
543    ) -> StorageBackendResult<SequenceSetValueResult>;
544
545    fn save_view(&self, view: &ViewRow) -> StorageBackendResult<()>;
546    /// Atomically move one view catalog row and its shared relation claim.
547    fn rename_view(
548        &self,
549        from: &RelationIdentity,
550        to: &RelationIdentity,
551    ) -> StorageBackendResult<bool>;
552    fn drop_view(&self, relation: &RelationIdentity) -> StorageBackendResult<bool>;
553    fn load_views(&self) -> StorageBackendResult<Vec<ViewRow>>;
554
555    fn save_named_graph(&self, name: &str) -> StorageBackendResult<()>;
556    /// Bind graph mutations to their definition and allocation generation while allowing independent data writers to share the definition. Concurrent backends and catalog wrappers must implement this capability.
557    fn guard_graph_definition(&self, _graph: Option<&str>) -> StorageBackendResult<()> {
558        Err(crate::StorageBackendError::Other(
559            "graph definition guards are not supported".into(),
560        ))
561    }
562    fn drop_named_graph(&self, name: &str) -> StorageBackendResult<()>;
563    fn load_named_graphs(&self) -> StorageBackendResult<Vec<String>>;
564    fn named_graph_exists(&self, name: &str) -> StorageBackendResult<bool>;
565
566    /// Read one entity without loading any other graph payload.
567    fn graph_vertex(&self, id: u64) -> StorageBackendResult<Option<GraphVertexRow>>;
568    fn graph_edge(&self, id: u64) -> StorageBackendResult<Option<EdgeRow>>;
569
570    /// Read a bounded, strictly ascending identity page from the pinned
571    /// storage snapshot. Filters must be applied before the page limit.
572    fn graph_entity_ids(
573        &self,
574        filter: GraphEntityFilter<'_>,
575        after: Option<u64>,
576        limit: usize,
577    ) -> StorageBackendResult<Vec<u64>>;
578
579    fn graph_entity_count(&self, filter: GraphEntityFilter<'_>) -> StorageBackendResult<u64>;
580    fn graph_entity_max_id(&self, kind: GraphEntityKind) -> StorageBackendResult<Option<u64>>;
581    fn graph_entity_memberships(
582        &self,
583        kind: GraphEntityKind,
584        id: u64,
585    ) -> StorageBackendResult<Vec<String>>;
586    fn graph_has_membership(
587        &self,
588        kind: GraphEntityKind,
589        id: u64,
590        graph: &str,
591    ) -> StorageBackendResult<bool>;
592    /// Explicit bulk snapshot/export of one named graph. The caller pins
593    /// the storage transaction. Engine startup, sessions, and query handles
594    /// use direct access methods instead of retaining this owned copy.
595    fn load_named_graph_snapshot(&self, name: &str) -> StorageBackendResult<Option<GraphSnapshot>> {
596        graph_snapshot::load(self, name)
597    }
598    fn save_vertex(
599        &self,
600        vertex_id: u64,
601        label: &str,
602        properties_json: &str,
603    ) -> StorageBackendResult<()>;
604    fn delete_vertex(&self, vertex_id: u64) -> StorageBackendResult<()>;
605    fn load_vertices(&self) -> StorageBackendResult<Vec<(u64, String, String)>>;
606    fn save_edge(
607        &self,
608        edge_id: u64,
609        source_id: u64,
610        target_id: u64,
611        label: &str,
612        properties_json: &str,
613    ) -> StorageBackendResult<()>;
614    fn delete_edge(&self, edge_id: u64) -> StorageBackendResult<()>;
615    fn load_edges(&self) -> StorageBackendResult<Vec<EdgeRow>>;
616    fn save_graph_membership(
617        &self,
618        entity_type: &str,
619        entity_id: u64,
620        graph_name: &str,
621    ) -> StorageBackendResult<()>;
622    fn delete_graph_membership(
623        &self,
624        entity_type: &str,
625        entity_id: u64,
626        graph_name: &str,
627    ) -> StorageBackendResult<()>;
628    fn delete_graph_membership_for_graph(&self, graph_name: &str) -> StorageBackendResult<()>;
629    fn load_graph_memberships(&self) -> StorageBackendResult<Vec<(String, u64, String)>>;
630    fn purge_orphan_graph_entities(&self) -> StorageBackendResult<()>;
631    fn replace_named_graph(
632        &self,
633        graph_name: &str,
634        snapshot: &GraphSnapshot,
635    ) -> StorageBackendResult<()>;
636    fn drop_named_graph_data(&self, graph_name: &str) -> StorageBackendResult<()>;
637
638    fn save_analyzer(&self, name: &str, config_json: &str) -> StorageBackendResult<()>;
639    fn drop_analyzer(&self, name: &str) -> StorageBackendResult<()>;
640    fn load_analyzers(&self) -> StorageBackendResult<Vec<(String, String)>>;
641
642    /// Save a named definition's resolved descriptor with its diagnostic configuration in the same transaction.
643    fn save_analyzer_revision(
644        &self,
645        _name: &str,
646        _config_json: &str,
647        _descriptor_json: &str,
648    ) -> StorageBackendResult<()> {
649        Err(StorageBackendError::Other(
650            "durable analyzer descriptors are not supported by this catalog".into(),
651        ))
652    }
653
654    /// Resolved named descriptors; legacy definitions appear only in `load_analyzers` until migrated.
655    fn load_analyzer_descriptors(&self) -> StorageBackendResult<Vec<(String, String)>> {
656        Ok(Vec::new())
657    }
658
659    /// Atomically replace a field's complete independent binding and its compatibility label. An empty label denotes the physical field's unnamed default.
660    fn replace_table_field_analyzer_binding(
661        &self,
662        _table: &str,
663        _field: &str,
664        _phase: &str,
665        _name: &str,
666        _binding_json: &str,
667    ) -> StorageBackendResult<()> {
668        Err(StorageBackendError::Other(
669            "durable analyzer bindings are not supported by this catalog".into(),
670        ))
671    }
672
673    /// `(table, field, binding JSON)` rows. Each field has one complete binding envelope.
674    fn load_table_field_analyzer_bindings(
675        &self,
676    ) -> StorageBackendResult<Vec<(String, String, String)>> {
677        Ok(Vec::new())
678    }
679
680    fn save_table_field_analyzer(
681        &self,
682        table_name: &str,
683        field: &str,
684        phase: &str,
685        analyzer_name: &str,
686    ) -> StorageBackendResult<()>;
687    fn replace_table_field_analyzer(
688        &self,
689        table_name: &str,
690        field: &str,
691        phase: &str,
692        analyzer_name: &str,
693    ) -> StorageBackendResult<()>;
694    fn drop_table_field_analyzer_field(
695        &self,
696        table_name: &str,
697        field: &str,
698    ) -> StorageBackendResult<()>;
699    fn drop_table_field_analyzers(&self, table_name: &str) -> StorageBackendResult<()>;
700    fn load_table_field_analyzers(
701        &self,
702    ) -> StorageBackendResult<Vec<(String, String, String, String)>>;
703
704    fn save_foreign_server(
705        &self,
706        name: &str,
707        fdw_type: &str,
708        options_json: &str,
709    ) -> StorageBackendResult<()>;
710    fn drop_foreign_server(&self, name: &str) -> StorageBackendResult<()>;
711    fn load_foreign_servers(&self) -> StorageBackendResult<Vec<(String, String, String)>>;
712
713    fn save_foreign_table(&self, row: &ForeignTableRow) -> StorageBackendResult<()>;
714    /// Atomically move one foreign-table catalog row and its shared relation claim.
715    fn rename_foreign_table(
716        &self,
717        from: &RelationIdentity,
718        to: &RelationIdentity,
719    ) -> StorageBackendResult<bool>;
720    fn update_foreign_table_security(
721        &self,
722        relation: &RelationIdentity,
723        security: &RelationSecurityRow,
724    ) -> StorageBackendResult<bool>;
725    fn drop_foreign_table(&self, relation: &RelationIdentity) -> StorageBackendResult<()>;
726    fn load_foreign_tables(&self) -> StorageBackendResult<Vec<ForeignTableRow>>;
727
728    fn save_catalog_index(
729        &self,
730        relation: &RelationIdentity,
731        index_type: &str,
732        table_name: &str,
733        columns_json: &str,
734        parameters_json: &str,
735    ) -> StorageBackendResult<()> {
736        self.save_catalog_index_row(&CatalogIndexRow {
737            relation: relation.clone(),
738            index_type: index_type.to_string(),
739            table_name: table_name.to_string(),
740            columns_json: columns_json.to_string(),
741            parameters_json: parameters_json.to_string(),
742            definition_json: None,
743        })
744    }
745    fn save_catalog_index_row(&self, index: &CatalogIndexRow) -> StorageBackendResult<()>;
746    fn drop_catalog_index(&self, relation: &RelationIdentity) -> StorageBackendResult<()>;
747    fn drop_catalog_indexes_for_table(&self, table_name: &str) -> StorageBackendResult<()>;
748    fn load_catalog_indexes(&self) -> StorageBackendResult<Vec<CatalogIndexRow>>;
749
750    fn save_path_index(
751        &self,
752        graph_name: &str,
753        label_sequences_json: &str,
754    ) -> StorageBackendResult<()>;
755    fn drop_path_index(&self, graph_name: &str) -> StorageBackendResult<()>;
756    fn load_path_indexes(&self) -> StorageBackendResult<Vec<(String, String)>>;
757
758    /// Durable reachability records, paged lexicographically by (source, target).
759    /// Building calls clear, bounded saves, then finish inside one transaction.
760    /// Graph mutations invalidate the materialization atomically; definitions remain.
761    fn clear_path_index_data(&self, index: &str) -> StorageBackendResult<()>;
762    fn save_path_index_pairs(
763        &self,
764        index: &str,
765        sequence: &str,
766        pairs: &[(u64, u64)],
767    ) -> StorageBackendResult<()>;
768    fn finish_path_index_data(
769        &self,
770        index: &str,
771        graph: &str,
772        definition: &str,
773    ) -> StorageBackendResult<()>;
774    fn path_index_data_is_current(
775        &self,
776        index: &str,
777        definition: &str,
778    ) -> StorageBackendResult<bool>;
779    fn path_index_pairs(
780        &self,
781        index: &str,
782        sequence: &str,
783        after: Option<(u64, u64)>,
784        limit: usize,
785    ) -> StorageBackendResult<Vec<(u64, u64)>>;
786
787    fn save_column_stats(&self, stats: ColumnStatsInput<'_>) -> StorageBackendResult<()>;
788    /// Atomically replace the complete statistics snapshot for one table.
789    /// Implementations must leave the prior snapshot intact if any row fails.
790    fn replace_column_stats(
791        &self,
792        table_name: &str,
793        stats: &[ColumnStatsInput<'_>],
794    ) -> StorageBackendResult<()>;
795    fn load_column_stats(&self, table_name: &str) -> StorageBackendResult<Vec<ColumnStatsRow>>;
796    fn delete_column_stats(&self, table_name: &str) -> StorageBackendResult<()>;
797}
798
799#[cfg(test)]
800mod tests {
801    use super::{
802        sequence_value_reservation, RelationIdentity, SequenceValuePosition,
803        SequenceValueReservation,
804    };
805
806    const fn sequence_position(
807        current: i64,
808        called: bool,
809        log_count: i64,
810    ) -> SequenceValuePosition {
811        SequenceValuePosition {
812            current,
813            called,
814            log_count,
815        }
816    }
817
818    #[test]
819    fn relation_identity_rendering_is_reversible_and_collision_free() {
820        let left = RelationIdentity::new("a.b", "c");
821        let right = RelationIdentity::new("a", "b.c");
822        assert_eq!(left.qualified_name(), "\"a.b\".c");
823        assert_eq!(right.qualified_name(), "a.\"b.c\"");
824        assert_ne!(left.qualified_name(), right.qualified_name());
825        assert_eq!(
826            RelationIdentity::from_legacy_name(&left.qualified_name()).unwrap(),
827            left
828        );
829        assert_eq!(
830            RelationIdentity::from_legacy_name(&right.qualified_name()).unwrap(),
831            right
832        );
833    }
834
835    #[test]
836    fn relation_identity_preserves_quotes_and_unqualified_public_alias() {
837        let quoted = RelationIdentity::new("public", "a\"b.c");
838        assert_eq!(quoted.qualified_name(), "public.\"a\"\"b.c\"");
839        assert_eq!(
840            quoted.canonical_and_legacy_public_names(),
841            vec![
842                "public.\"a\"\"b.c\"".to_string(),
843                "\"a\"\"b.c\"".to_string()
844            ]
845        );
846        assert_eq!(
847            RelationIdentity::from_legacy_name(&quoted.qualified_name()).unwrap(),
848            quoted
849        );
850        assert_eq!(
851            RelationIdentity::from_legacy_name("plain").unwrap(),
852            RelationIdentity::new("public", "plain")
853        );
854        assert_eq!(
855            RelationIdentity::new("app", "plain").canonical_and_legacy_public_names(),
856            vec!["app.plain".to_string()]
857        );
858        assert_eq!(
859            RelationIdentity::new("public", "Upper").canonical_and_legacy_public_names(),
860            vec![
861                "public.\"Upper\"".to_string(),
862                "\"Upper\"".to_string(),
863                "Upper".to_string()
864            ]
865        );
866    }
867
868    #[test]
869    fn sequence_reservations_track_postgresql_log_counts() {
870        assert_eq!(
871            sequence_value_reservation(sequence_position(1, false, 0), 1, 1, i64::MAX, false, 1),
872            Some(SequenceValueReservation {
873                first_value: 1,
874                last_value: 1,
875                count: 1,
876                log_count: 32,
877            })
878        );
879        assert_eq!(
880            sequence_value_reservation(sequence_position(1, true, 32), 1, 1, i64::MAX, false, 1),
881            Some(SequenceValueReservation {
882                first_value: 2,
883                last_value: 2,
884                count: 1,
885                log_count: 31,
886            })
887        );
888        assert_eq!(
889            sequence_value_reservation(sequence_position(1, false, 0), 1, 1, i64::MAX, false, 10),
890            Some(SequenceValueReservation {
891                first_value: 1,
892                last_value: 10,
893                count: 10,
894                log_count: 32,
895            })
896        );
897        assert_eq!(
898            sequence_value_reservation(sequence_position(5, false, 0), 2, 3, 9, true, 3),
899            Some(SequenceValueReservation {
900                first_value: 5,
901                last_value: 9,
902                count: 3,
903                log_count: 0,
904            })
905        );
906        assert_eq!(
907            sequence_value_reservation(
908                sequence_position(1, false, 0),
909                1,
910                1,
911                i64::MAX,
912                false,
913                i64::MAX,
914            ),
915            Some(SequenceValueReservation {
916                first_value: 1,
917                last_value: i64::MAX,
918                count: i64::MAX,
919                log_count: 0,
920            })
921        );
922    }
923}