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    fn create_sequence_row(&self, sequence: &SequenceRow) -> StorageBackendResult<bool>;
394    /// Replace a sequence's definition, ownership and security. A row that keeps its object identity and definition generation keeps its stored value state, which only value operations move; the value state of `sequence` is stored with a new generation.
395    fn replace_sequence_row(&self, sequence: &SequenceRow) -> StorageBackendResult<bool>;
396    /// Atomically move one sequence catalog row and its shared relation claim while preserving object identity and physical value state.
397    fn rename_sequence_row(&self, from: &str, to: &str) -> StorageBackendResult<bool>;
398    fn drop_sequence_row(&self, name: &str) -> StorageBackendResult<bool>;
399    fn load_sequence_rows(&self) -> StorageBackendResult<Vec<SequenceRow>>;
400
401    /// 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.
402    fn sequence_has_private_changes(
403        &self,
404        _relation: &RelationIdentity,
405        _object_id: [u8; 16],
406    ) -> StorageBackendResult<bool> {
407        if self.transaction_model().is_versioned() {
408            return Err(StorageBackendError::Other(
409                "private sequence provenance is not supported by this catalog".into(),
410            ));
411        }
412        Ok(false)
413    }
414    fn reserve_sequence_values(
415        &self,
416        name: &str,
417        object_id: [u8; 16],
418        definition_generation: [u8; 16],
419    ) -> StorageBackendResult<SequenceReservationResult>;
420    /// 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.
421    fn next_sequence_value(
422        &self,
423        name: &str,
424        object_id: [u8; 16],
425    ) -> StorageBackendResult<Option<i64>> {
426        loop {
427            let relation =
428                RelationIdentity::from_legacy_name(name).map_err(StorageBackendError::Other)?;
429            let Some(row) = self
430                .load_sequence_rows()?
431                .into_iter()
432                .find(|row| row.relation == relation && row.object_id == object_id)
433            else {
434                return Ok(None);
435            };
436            match self.reserve_sequence_values(name, object_id, row.definition_generation)? {
437                SequenceReservationResult::Reserved(reservation) => {
438                    return Ok(Some(reservation.first_value));
439                }
440                SequenceReservationResult::DefinitionChanged => {}
441                SequenceReservationResult::Missing => return Ok(None),
442                SequenceReservationResult::Exhausted => {
443                    return Err(StorageBackendError::Other(format!(
444                        "sequence `{name}` exhausted"
445                    )));
446                }
447            }
448        }
449    }
450    fn set_sequence_value(
451        &self,
452        name: &str,
453        object_id: [u8; 16],
454        definition_generation: [u8; 16],
455        value: i64,
456        called: bool,
457        log_count: i64,
458    ) -> StorageBackendResult<SequenceSetValueResult>;
459    /// Move the durable position of a sequence 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.
460    fn log_sequence_values(
461        &self,
462        name: &str,
463        object_id: [u8; 16],
464        definition_generation: [u8; 16],
465        expected: (i64, bool),
466        logged: SequenceValuePosition,
467    ) -> StorageBackendResult<SequenceLogResult>;
468
469    fn save_view(&self, view: &ViewRow) -> StorageBackendResult<()>;
470    /// Atomically move one view catalog row and its shared relation claim.
471    fn rename_view(
472        &self,
473        from: &RelationIdentity,
474        to: &RelationIdentity,
475    ) -> StorageBackendResult<bool>;
476    fn drop_view(&self, relation: &RelationIdentity) -> StorageBackendResult<bool>;
477    fn load_views(&self) -> StorageBackendResult<Vec<ViewRow>>;
478
479    fn save_named_graph(&self, name: &str) -> StorageBackendResult<()>;
480    /// 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.
481    fn guard_graph_definition(&self, _graph: Option<&str>) -> StorageBackendResult<()> {
482        Err(crate::StorageBackendError::Other(
483            "graph definition guards are not supported".into(),
484        ))
485    }
486    fn drop_named_graph(&self, name: &str) -> StorageBackendResult<()>;
487    fn load_named_graphs(&self) -> StorageBackendResult<Vec<String>>;
488    fn named_graph_exists(&self, name: &str) -> StorageBackendResult<bool>;
489
490    /// Read one entity without loading any other graph payload.
491    fn graph_vertex(&self, id: u64) -> StorageBackendResult<Option<GraphVertexRow>>;
492
493    /// Visit requested vertices on the pinned view, including missing rows and
494    /// duplicates in caller order. The internal callback must not reenter
495    /// storage. A false result stops before the next identity is admitted.
496    /// `None` declines the capability without invoking the callback; `Some`
497    /// counts all invocations, including the one that stopped the read.
498    fn for_each_graph_vertex_borrowed(
499        &self,
500        _ids: &[u64],
501        _visit: &mut dyn FnMut(u64, Option<&GraphVertexRow>) -> bool,
502    ) -> StorageBackendResult<Option<usize>> {
503        Ok(None)
504    }
505
506    fn graph_edge(&self, id: u64) -> StorageBackendResult<Option<EdgeRow>>;
507
508    /// Read a bounded, strictly ascending identity page from the pinned
509    /// storage snapshot. Filters must be applied before the page limit.
510    fn graph_entity_ids(
511        &self,
512        filter: GraphEntityFilter<'_>,
513        after: Option<u64>,
514        limit: usize,
515    ) -> StorageBackendResult<Vec<u64>>;
516
517    fn graph_entity_count(&self, filter: GraphEntityFilter<'_>) -> StorageBackendResult<u64>;
518    fn graph_entity_max_id(&self, kind: GraphEntityKind) -> StorageBackendResult<Option<u64>>;
519    fn graph_entity_memberships(
520        &self,
521        kind: GraphEntityKind,
522        id: u64,
523    ) -> StorageBackendResult<Vec<String>>;
524    fn graph_has_membership(
525        &self,
526        kind: GraphEntityKind,
527        id: u64,
528        graph: &str,
529    ) -> StorageBackendResult<bool>;
530    /// Explicit bulk snapshot/export of one named graph. The caller pins
531    /// the storage transaction. Engine startup, sessions, and query handles
532    /// use direct access methods instead of retaining this owned copy.
533    fn load_named_graph_snapshot(&self, name: &str) -> StorageBackendResult<Option<GraphSnapshot>> {
534        graph_snapshot::load(self, name)
535    }
536    fn save_vertex(
537        &self,
538        vertex_id: u64,
539        label: &str,
540        properties_json: &str,
541    ) -> StorageBackendResult<()>;
542    fn delete_vertex(&self, vertex_id: u64) -> StorageBackendResult<()>;
543    fn load_vertices(&self) -> StorageBackendResult<Vec<(u64, String, String)>>;
544    fn save_edge(
545        &self,
546        edge_id: u64,
547        source_id: u64,
548        target_id: u64,
549        label: &str,
550        properties_json: &str,
551    ) -> StorageBackendResult<()>;
552    fn delete_edge(&self, edge_id: u64) -> StorageBackendResult<()>;
553    fn load_edges(&self) -> StorageBackendResult<Vec<EdgeRow>>;
554    fn save_graph_membership(
555        &self,
556        entity_type: &str,
557        entity_id: u64,
558        graph_name: &str,
559    ) -> StorageBackendResult<()>;
560    fn delete_graph_membership(
561        &self,
562        entity_type: &str,
563        entity_id: u64,
564        graph_name: &str,
565    ) -> StorageBackendResult<()>;
566    fn delete_graph_membership_for_graph(&self, graph_name: &str) -> StorageBackendResult<()>;
567    fn load_graph_memberships(&self) -> StorageBackendResult<Vec<(String, u64, String)>>;
568    fn purge_orphan_graph_entities(&self) -> StorageBackendResult<()>;
569    fn replace_named_graph(
570        &self,
571        graph_name: &str,
572        snapshot: &GraphSnapshot,
573    ) -> StorageBackendResult<()>;
574    fn drop_named_graph_data(&self, graph_name: &str) -> StorageBackendResult<()>;
575
576    fn save_analyzer(&self, name: &str, config_json: &str) -> StorageBackendResult<()>;
577    fn drop_analyzer(&self, name: &str) -> StorageBackendResult<()>;
578    fn load_analyzers(&self) -> StorageBackendResult<Vec<(String, String)>>;
579
580    /// Save a named definition's resolved descriptor with its diagnostic configuration in the same transaction.
581    fn save_analyzer_revision(
582        &self,
583        _name: &str,
584        _config_json: &str,
585        _descriptor_json: &str,
586    ) -> StorageBackendResult<()> {
587        Err(StorageBackendError::Other(
588            "durable analyzer descriptors are not supported by this catalog".into(),
589        ))
590    }
591
592    /// Resolved named descriptors; legacy definitions appear only in `load_analyzers` until migrated.
593    fn load_analyzer_descriptors(&self) -> StorageBackendResult<Vec<(String, String)>> {
594        Ok(Vec::new())
595    }
596
597    /// Atomically replace a field's complete independent binding and its compatibility label. An empty label denotes the physical field's unnamed default.
598    fn replace_table_field_analyzer_binding(
599        &self,
600        _table: &str,
601        _field: &str,
602        _phase: &str,
603        _name: &str,
604        _binding_json: &str,
605    ) -> StorageBackendResult<()> {
606        Err(StorageBackendError::Other(
607            "durable analyzer bindings are not supported by this catalog".into(),
608        ))
609    }
610
611    /// `(table, field, binding JSON)` rows. Each field has one complete binding envelope.
612    fn load_table_field_analyzer_bindings(
613        &self,
614    ) -> StorageBackendResult<Vec<(String, String, String)>> {
615        Ok(Vec::new())
616    }
617
618    fn save_table_field_analyzer(
619        &self,
620        table_name: &str,
621        field: &str,
622        phase: &str,
623        analyzer_name: &str,
624    ) -> StorageBackendResult<()>;
625    fn replace_table_field_analyzer(
626        &self,
627        table_name: &str,
628        field: &str,
629        phase: &str,
630        analyzer_name: &str,
631    ) -> StorageBackendResult<()>;
632    fn drop_table_field_analyzer_field(
633        &self,
634        table_name: &str,
635        field: &str,
636    ) -> StorageBackendResult<()>;
637    fn drop_table_field_analyzers(&self, table_name: &str) -> StorageBackendResult<()>;
638    fn load_table_field_analyzers(
639        &self,
640    ) -> StorageBackendResult<Vec<(String, String, String, String)>>;
641
642    fn save_foreign_server(
643        &self,
644        name: &str,
645        fdw_type: &str,
646        options_json: &str,
647    ) -> StorageBackendResult<()>;
648    fn drop_foreign_server(&self, name: &str) -> StorageBackendResult<()>;
649    fn load_foreign_servers(&self) -> StorageBackendResult<Vec<(String, String, String)>>;
650
651    fn save_foreign_table(&self, row: &ForeignTableRow) -> StorageBackendResult<()>;
652    /// Atomically move one foreign-table catalog row and its shared relation claim.
653    fn rename_foreign_table(
654        &self,
655        from: &RelationIdentity,
656        to: &RelationIdentity,
657    ) -> StorageBackendResult<bool>;
658    fn update_foreign_table_security(
659        &self,
660        relation: &RelationIdentity,
661        security: &RelationSecurityRow,
662    ) -> StorageBackendResult<bool>;
663    fn drop_foreign_table(&self, relation: &RelationIdentity) -> StorageBackendResult<()>;
664    fn load_foreign_tables(&self) -> StorageBackendResult<Vec<ForeignTableRow>>;
665
666    fn save_catalog_index(
667        &self,
668        relation: &RelationIdentity,
669        index_type: &str,
670        table_name: &str,
671        columns_json: &str,
672        parameters_json: &str,
673    ) -> StorageBackendResult<()> {
674        self.save_catalog_index_row(&CatalogIndexRow {
675            relation: relation.clone(),
676            index_type: index_type.to_string(),
677            table_name: table_name.to_string(),
678            columns_json: columns_json.to_string(),
679            parameters_json: parameters_json.to_string(),
680            definition_json: None,
681        })
682    }
683    fn save_catalog_index_row(&self, index: &CatalogIndexRow) -> StorageBackendResult<()>;
684    fn drop_catalog_index(&self, relation: &RelationIdentity) -> StorageBackendResult<()>;
685    fn drop_catalog_indexes_for_table(&self, table_name: &str) -> StorageBackendResult<()>;
686    fn load_catalog_indexes(&self) -> StorageBackendResult<Vec<CatalogIndexRow>>;
687
688    fn save_path_index(
689        &self,
690        graph_name: &str,
691        label_sequences_json: &str,
692    ) -> StorageBackendResult<()>;
693    fn drop_path_index(&self, graph_name: &str) -> StorageBackendResult<()>;
694    fn load_path_indexes(&self) -> StorageBackendResult<Vec<(String, String)>>;
695
696    /// Durable reachability records, paged lexicographically by (source, target).
697    /// Building calls clear, bounded saves, then finish inside one transaction.
698    /// Graph mutations invalidate the materialization atomically; definitions remain.
699    fn clear_path_index_data(&self, index: &str) -> StorageBackendResult<()>;
700    fn save_path_index_pairs(
701        &self,
702        index: &str,
703        sequence: &str,
704        pairs: &[(u64, u64)],
705    ) -> StorageBackendResult<()>;
706    fn finish_path_index_data(
707        &self,
708        index: &str,
709        graph: &str,
710        definition: &str,
711    ) -> StorageBackendResult<()>;
712    fn path_index_data_is_current(
713        &self,
714        index: &str,
715        definition: &str,
716    ) -> StorageBackendResult<bool>;
717    fn path_index_pairs(
718        &self,
719        index: &str,
720        sequence: &str,
721        after: Option<(u64, u64)>,
722        limit: usize,
723    ) -> StorageBackendResult<Vec<(u64, u64)>>;
724
725    fn save_column_stats(&self, stats: ColumnStatsInput<'_>) -> StorageBackendResult<()>;
726    /// Atomically replace the complete statistics snapshot for one table.
727    /// Implementations must leave the prior snapshot intact if any row fails.
728    fn replace_column_stats(
729        &self,
730        table_name: &str,
731        stats: &[ColumnStatsInput<'_>],
732    ) -> StorageBackendResult<()>;
733    fn load_column_stats(&self, table_name: &str) -> StorageBackendResult<Vec<ColumnStatsRow>>;
734    fn delete_column_stats(&self, table_name: &str) -> StorageBackendResult<()>;
735}
736
737#[cfg(test)]
738mod tests {
739    use super::RelationIdentity;
740
741    #[test]
742    fn relation_identity_rendering_is_reversible_and_collision_free() {
743        let left = RelationIdentity::new("a.b", "c");
744        let right = RelationIdentity::new("a", "b.c");
745        assert_eq!(left.qualified_name(), "\"a.b\".c");
746        assert_eq!(right.qualified_name(), "a.\"b.c\"");
747        assert_ne!(left.qualified_name(), right.qualified_name());
748        assert_eq!(
749            RelationIdentity::from_legacy_name(&left.qualified_name()).unwrap(),
750            left
751        );
752        assert_eq!(
753            RelationIdentity::from_legacy_name(&right.qualified_name()).unwrap(),
754            right
755        );
756    }
757
758    #[test]
759    fn relation_identity_preserves_quotes_and_unqualified_public_alias() {
760        let quoted = RelationIdentity::new("public", "a\"b.c");
761        assert_eq!(quoted.qualified_name(), "public.\"a\"\"b.c\"");
762        assert_eq!(
763            quoted.canonical_and_legacy_public_names(),
764            vec![
765                "public.\"a\"\"b.c\"".to_string(),
766                "\"a\"\"b.c\"".to_string()
767            ]
768        );
769        assert_eq!(
770            RelationIdentity::from_legacy_name(&quoted.qualified_name()).unwrap(),
771            quoted
772        );
773        assert_eq!(
774            RelationIdentity::from_legacy_name("plain").unwrap(),
775            RelationIdentity::new("public", "plain")
776        );
777        assert_eq!(
778            RelationIdentity::new("app", "plain").canonical_and_legacy_public_names(),
779            vec!["app.plain".to_string()]
780        );
781        assert_eq!(
782            RelationIdentity::new("public", "Upper").canonical_and_legacy_public_names(),
783            vec![
784                "public.\"Upper\"".to_string(),
785                "\"Upper\"".to_string(),
786                "Upper".to_string()
787            ]
788        );
789    }
790}