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