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