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