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(crate) 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
155/// One row from the secondary-index registry.
156#[derive(Debug, Clone)]
157pub struct CatalogIndexRow {
158    pub relation: RelationIdentity,
159    pub index_type: String,
160    pub table_name: String,
161    pub columns_json: String,
162    pub parameters_json: String,
163    /// Durable semantic definition; absent on legacy ordinary secondary indexes.
164    pub definition_json: Option<String>,
165}
166
167/// Values persisted into one column-statistics row.
168#[derive(Debug, Clone, Copy)]
169pub struct ColumnStatsInput<'a> {
170    pub table_name: &'a str,
171    pub column_name: &'a str,
172    pub distinct_count: i64,
173    pub null_count: i64,
174    pub min_value: Option<&'a str>,
175    pub max_value: Option<&'a str>,
176    pub row_count: i64,
177    pub histogram_json: &'a str,
178    pub mcv_values_json: &'a str,
179    pub mcv_frequencies_json: &'a str,
180}
181
182impl<'a> ColumnStatsInput<'a> {
183    pub fn basic(
184        table_name: &'a str,
185        column_name: &'a str,
186        distinct_count: i64,
187        null_count: i64,
188        min_value: Option<&'a str>,
189        max_value: Option<&'a str>,
190        row_count: i64,
191    ) -> Self {
192        Self {
193            table_name,
194            column_name,
195            distinct_count,
196            null_count,
197            min_value,
198            max_value,
199            row_count,
200            histogram_json: "[]",
201            mcv_values_json: "[]",
202            mcv_frequencies_json: "[]",
203        }
204    }
205}
206
207/// One row from persisted column statistics.
208#[derive(Debug, Clone, PartialEq)]
209pub struct ColumnStatsRow {
210    pub column_name: String,
211    pub distinct_count: i64,
212    pub null_count: i64,
213    pub min_value: Option<String>,
214    pub max_value: Option<String>,
215    pub row_count: i64,
216    pub histogram_json: String,
217    pub mcv_values_json: String,
218    pub mcv_frequencies_json: String,
219}
220
221/// Durable SQL sequence state. Sequence allocation is implemented by the
222/// catalog backend as one atomic mutation so independent engine sessions (and
223/// independently opened engines) cannot return the same value.
224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
225pub struct SequenceOptions {
226    pub data_type: String,
227    /// `None` is accepted only while decoding a legacy row and is resolved from its increment direction by the engine.
228    pub min_value: Option<i64>,
229    /// `None` is accepted only while decoding a legacy row and is resolved from its increment direction by the engine.
230    pub max_value: Option<i64>,
231    pub cycle: bool,
232    #[serde(default = "default_sequence_cache_size")]
233    pub cache_size: i64,
234}
235
236const fn default_sequence_cache_size() -> i64 {
237    1
238}
239
240impl Default for SequenceOptions {
241    fn default() -> Self {
242        Self {
243            data_type: "bigint".into(),
244            min_value: None,
245            max_value: None,
246            cycle: false,
247            cache_size: default_sequence_cache_size(),
248        }
249    }
250}
251
252/// Dependency strength of a sequence owner. Ordinary `OWNED BY` and `SERIAL` use an automatic dependency, while an identity column owns its sequence through an internal dependency that cannot be reassigned or dropped directly.
253#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
254#[serde(rename_all = "snake_case")]
255pub enum SequenceOwnerDependency {
256    #[default]
257    Automatic,
258    Internal,
259}
260
261impl SequenceOwnerDependency {
262    #[must_use]
263    pub const fn catalog_code(self) -> &'static str {
264        match self {
265            Self::Automatic => "a",
266            Self::Internal => "i",
267        }
268    }
269}
270
271/// Stable owner identity for a sequence dependency. Names are deliberately excluded so table and column renames do not require dependency rewrites.
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
273pub struct SequenceOwner {
274    pub table_object_id: [u8; 16],
275    pub column_object_id: [u8; 16],
276    #[serde(default)]
277    pub dependency: SequenceOwnerDependency,
278}
279
280/// Grantable privileges carried by one sequence ACL path.
281#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
282pub struct SequencePrivileges {
283    #[serde(default)]
284    pub select: bool,
285    #[serde(default)]
286    pub update: bool,
287    #[serde(default)]
288    pub usage: bool,
289}
290
291impl SequencePrivileges {
292    pub const ALL: Self = Self {
293        select: true,
294        update: true,
295        usage: true,
296    };
297
298    #[must_use]
299    pub const fn is_empty(self) -> bool {
300        !self.select && !self.update && !self.usage
301    }
302
303    #[must_use]
304    pub const fn intersects(self, other: Self) -> bool {
305        self.select && other.select || self.update && other.update || self.usage && other.usage
306    }
307
308    pub fn insert(&mut self, other: Self) {
309        self.select |= other.select;
310        self.update |= other.update;
311        self.usage |= other.usage;
312    }
313
314    pub fn remove(&mut self, other: Self) {
315        self.select &= !other.select;
316        self.update &= !other.update;
317        self.usage &= !other.usage;
318    }
319}
320
321/// One explicit sequence ACL path. `None` on `SequenceRow::acl` retains `PostgreSQL`'s default owner-only privileges.
322#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
323pub struct SequenceAclEntry {
324    pub role: String,
325    /// Legacy persisted entries without an explicit grantor originate from the sequence owner.
326    #[serde(default, skip_serializing_if = "Option::is_none")]
327    pub grantor: Option<String>,
328    #[serde(default)]
329    pub privileges: SequencePrivileges,
330    #[serde(default)]
331    pub grant_options: SequencePrivileges,
332}
333
334#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
335pub struct SequenceRow {
336    pub relation: RelationIdentity,
337    /// SQL role that owns the sequence. Legacy catalogs predate roles and therefore belong to the bootstrap role.
338    #[serde(default = "default_sequence_role_owner")]
339    pub role_owner: String,
340    /// Explicit ACL entries. `None` represents `PostgreSQL`'s null default ACL, in which the owner has all ordinary privileges.
341    #[serde(default, skip_serializing_if = "Option::is_none")]
342    pub acl: Option<Vec<SequenceAclEntry>>,
343    /// Stable identity of this sequence incarnation. Dropping and recreating the same qualified name must allocate a different value.
344    #[serde(default)]
345    pub object_id: [u8; 16],
346    /// Changes for every successful definition-changing `ALTER SEQUENCE` while remaining stable across name lifecycle operations, value reservations, and `setval`.
347    #[serde(default)]
348    pub definition_generation: [u8; 16],
349    pub start: i64,
350    pub increment: i64,
351    pub current: i64,
352    /// False until the first allocation returns `current` verbatim.
353    pub called: bool,
354    /// Number of values whose advancement is already durable in the sequence log.
355    #[serde(default)]
356    pub log_count: i64,
357    /// `PostgreSQL` `pg_class.relpersistence` code. Durable sequence rows accept only permanent (`p`) and unlogged (`u`) values.
358    pub persistence: String,
359    #[serde(default)]
360    pub owner: Option<SequenceOwner>,
361    #[serde(default)]
362    pub options: SequenceOptions,
363}
364
365fn default_sequence_role_owner() -> String {
366    "uqa".into()
367}
368
369/// Physical sequence position consumed by one atomic reservation.
370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
371pub struct SequenceValuePosition {
372    pub current: i64,
373    pub called: bool,
374    pub log_count: i64,
375}
376
377/// One atomic sequence reservation. `first_value` is returned immediately, while the remaining values through `last_value` belong to the allocating session.
378#[derive(Debug, Clone, Copy, PartialEq, Eq)]
379pub struct SequenceValueReservation {
380    pub first_value: i64,
381    pub last_value: i64,
382    pub count: i64,
383    pub log_count: i64,
384}
385
386#[derive(Debug, Clone, Copy, PartialEq, Eq)]
387pub enum SequenceReservationResult {
388    Missing,
389    DefinitionChanged,
390    Exhausted,
391    Reserved(SequenceValueReservation),
392}
393
394/// 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.
395#[must_use]
396pub fn sequence_value_reservation(
397    position: SequenceValuePosition,
398    increment: i64,
399    min_value: i64,
400    max_value: i64,
401    cycle: bool,
402    cache_size: i64,
403) -> Option<SequenceValueReservation> {
404    let SequenceValuePosition {
405        current,
406        called,
407        log_count,
408    } = position;
409    debug_assert_ne!(increment, 0);
410    debug_assert!(cache_size > 0);
411    let first_value = if called {
412        match current
413            .checked_add(increment)
414            .filter(|value| (min_value..=max_value).contains(value))
415        {
416            Some(value) => value,
417            None if cycle && increment > 0 => min_value,
418            None if cycle => max_value,
419            None => return None,
420        }
421    } else {
422        current
423    };
424    let distance = if increment > 0 {
425        i128::from(max_value) - i128::from(first_value)
426    } else {
427        i128::from(first_value) - i128::from(min_value)
428    };
429    let step = i128::from(increment).abs();
430    let available = distance / step + 1;
431    let count = available.min(i128::from(cache_size));
432    let last_value = i128::from(first_value) + i128::from(increment) * (count - 1);
433    let initial_count = i128::from(!called);
434    let cache_fetch = i128::from(cache_size) - initial_count;
435    let mut fetch = cache_fetch;
436    let mut next_log_count = i128::from(log_count);
437    if i128::from(log_count) < cache_fetch || !called {
438        fetch += 32;
439        next_log_count = fetch;
440    }
441    let fetched = fetch.min(available - initial_count);
442    next_log_count -= fetched.min(cache_fetch);
443    next_log_count -= fetch - fetched;
444    Some(SequenceValueReservation {
445        first_value,
446        last_value: i64::try_from(last_value).expect("reserved sequence value stays in bounds"),
447        count: i64::try_from(count).expect("reservation count cannot exceed cache size"),
448        log_count: i64::try_from(next_log_count)
449            .expect("persisted sequence log count cannot exceed the cache request"),
450    })
451}
452
453/// Engine-facing catalog facade for persistent metadata.
454pub trait CatalogFacade: Send + Sync {
455    /// Read transactional cache generations without loading schemas or column
456    /// statistics. Providers without change tracking use conservative reloads.
457    fn cache_revisions(&self) -> StorageBackendResult<Option<CatalogCacheRevisions>> {
458        Ok(None)
459    }
460
461    fn set_metadata(&self, key: &str, value: &str) -> StorageBackendResult<()>;
462    fn get_metadata(&self, key: &str) -> StorageBackendResult<Option<String>>;
463    fn fts_storage_was_reset(&self) -> bool {
464        false
465    }
466
467    /// Atomically migrate the former flat relation namespace into typed,
468    /// schema-owned catalog objects. Implementations must reject normalized
469    /// or cross-kind collisions instead of merging either object.
470    fn migrate_relation_namespace(&self) -> StorageBackendResult<()>;
471
472    fn save_schema_row(&self, schema: &SchemaRow) -> StorageBackendResult<()>;
473    fn drop_schema(&self, name: &str) -> StorageBackendResult<()>;
474    fn load_schema_rows(&self) -> StorageBackendResult<Vec<SchemaRow>>;
475
476    fn save_schema(&self, name: &str) -> StorageBackendResult<()> {
477        self.save_schema_row(&SchemaRow::legacy(name))
478    }
479
480    fn load_schemas(&self) -> StorageBackendResult<Vec<String>> {
481        Ok(self
482            .load_schema_rows()?
483            .into_iter()
484            .map(|schema| schema.name)
485            .collect())
486    }
487
488    fn save_table(&self, schema: &TableSchema) -> StorageBackendResult<()>;
489    fn load_tables(&self) -> StorageBackendResult<Vec<TableSchema>>;
490    fn drop_table(&self, name: &str) -> StorageBackendResult<()>;
491    /// Remove a table definition and every catalog/data row owned by it as
492    /// one atomic catalog operation.
493    fn drop_table_and_data(&self, name: &str) -> StorageBackendResult<()>;
494    fn purge_table_data(&self, name: &str) -> StorageBackendResult<()>;
495    fn rename_table_data(&self, from: &str, to: &str) -> StorageBackendResult<()>;
496    fn drop_column_data(&self, table_name: &str, column_name: &str) -> StorageBackendResult<()>;
497    fn rename_column_data(
498        &self,
499        table_name: &str,
500        from: &str,
501        to: &str,
502    ) -> StorageBackendResult<()>;
503
504    fn save_model(&self, name: &str, json: &str) -> StorageBackendResult<()>;
505    fn load_models(&self) -> StorageBackendResult<Vec<(String, String)>>;
506    fn load_model(&self, name: &str) -> StorageBackendResult<Option<String>>;
507    fn drop_model(&self, name: &str) -> StorageBackendResult<()>;
508
509    fn save_scoring_params(&self, name: &str, params_json: &str) -> StorageBackendResult<()>;
510    fn load_scoring_params(&self, name: &str) -> StorageBackendResult<Option<String>>;
511    fn load_all_scoring_params(&self) -> StorageBackendResult<Vec<(String, String)>>;
512    fn drop_scoring_params(&self, name: &str) -> StorageBackendResult<()>;
513
514    fn create_sequence_row(&self, sequence: &SequenceRow) -> StorageBackendResult<bool>;
515    fn replace_sequence_row(&self, sequence: &SequenceRow) -> StorageBackendResult<bool>;
516    /// Atomically move one sequence catalog row and its shared relation claim while preserving object identity and physical value state.
517    fn rename_sequence_row(&self, from: &str, to: &str) -> StorageBackendResult<bool>;
518    fn drop_sequence_row(&self, name: &str) -> StorageBackendResult<bool>;
519    fn load_sequence_rows(&self) -> StorageBackendResult<Vec<SequenceRow>>;
520    fn reserve_sequence_values(
521        &self,
522        name: &str,
523        object_id: [u8; 16],
524        definition_generation: [u8; 16],
525    ) -> StorageBackendResult<SequenceReservationResult>;
526    /// 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.
527    fn next_sequence_value(
528        &self,
529        name: &str,
530        object_id: [u8; 16],
531    ) -> StorageBackendResult<Option<i64>> {
532        loop {
533            let relation =
534                RelationIdentity::from_legacy_name(name).map_err(StorageBackendError::Other)?;
535            let Some(row) = self
536                .load_sequence_rows()?
537                .into_iter()
538                .find(|row| row.relation == relation && row.object_id == object_id)
539            else {
540                return Ok(None);
541            };
542            match self.reserve_sequence_values(name, object_id, row.definition_generation)? {
543                SequenceReservationResult::Reserved(reservation) => {
544                    return Ok(Some(reservation.first_value));
545                }
546                SequenceReservationResult::DefinitionChanged => {}
547                SequenceReservationResult::Missing => return Ok(None),
548                SequenceReservationResult::Exhausted => {
549                    return Err(StorageBackendError::Other(format!(
550                        "sequence `{name}` exhausted"
551                    )));
552                }
553            }
554        }
555    }
556    fn set_sequence_value(
557        &self,
558        name: &str,
559        object_id: [u8; 16],
560        value: i64,
561        called: bool,
562        log_count: i64,
563    ) -> StorageBackendResult<Option<i64>>;
564
565    fn save_view(&self, view: &ViewRow) -> StorageBackendResult<()>;
566    /// Atomically move one view catalog row and its shared relation claim.
567    fn rename_view(
568        &self,
569        from: &RelationIdentity,
570        to: &RelationIdentity,
571    ) -> StorageBackendResult<bool>;
572    fn drop_view(&self, relation: &RelationIdentity) -> StorageBackendResult<bool>;
573    fn load_views(&self) -> StorageBackendResult<Vec<ViewRow>>;
574
575    fn save_named_graph(&self, name: &str) -> StorageBackendResult<()>;
576    fn drop_named_graph(&self, name: &str) -> StorageBackendResult<()>;
577    fn load_named_graphs(&self) -> StorageBackendResult<Vec<String>>;
578    fn named_graph_exists(&self, name: &str) -> StorageBackendResult<bool>;
579
580    /// Read one entity without loading any other graph payload.
581    fn graph_vertex(&self, id: u64) -> StorageBackendResult<Option<GraphVertexRow>>;
582    fn graph_edge(&self, id: u64) -> StorageBackendResult<Option<EdgeRow>>;
583
584    /// Read a bounded, strictly ascending identity page from the pinned
585    /// storage snapshot. Filters must be applied before the page limit.
586    fn graph_entity_ids(
587        &self,
588        filter: GraphEntityFilter<'_>,
589        after: Option<u64>,
590        limit: usize,
591    ) -> StorageBackendResult<Vec<u64>>;
592
593    fn graph_entity_count(&self, filter: GraphEntityFilter<'_>) -> StorageBackendResult<u64>;
594    fn graph_entity_max_id(&self, kind: GraphEntityKind) -> StorageBackendResult<Option<u64>>;
595    fn graph_entity_memberships(
596        &self,
597        kind: GraphEntityKind,
598        id: u64,
599    ) -> StorageBackendResult<Vec<String>>;
600    fn graph_has_membership(
601        &self,
602        kind: GraphEntityKind,
603        id: u64,
604        graph: &str,
605    ) -> StorageBackendResult<bool>;
606    /// Explicit bulk snapshot/export of one named graph. The caller pins
607    /// the storage transaction. Engine startup, sessions, and query handles
608    /// use direct access methods instead of retaining this owned copy.
609    fn load_named_graph_snapshot(&self, name: &str) -> StorageBackendResult<Option<GraphSnapshot>> {
610        graph_snapshot::load(self, name)
611    }
612    fn save_vertex(
613        &self,
614        vertex_id: u64,
615        label: &str,
616        properties_json: &str,
617    ) -> StorageBackendResult<()>;
618    fn delete_vertex(&self, vertex_id: u64) -> StorageBackendResult<()>;
619    fn load_vertices(&self) -> StorageBackendResult<Vec<(u64, String, String)>>;
620    fn save_edge(
621        &self,
622        edge_id: u64,
623        source_id: u64,
624        target_id: u64,
625        label: &str,
626        properties_json: &str,
627    ) -> StorageBackendResult<()>;
628    fn delete_edge(&self, edge_id: u64) -> StorageBackendResult<()>;
629    fn load_edges(&self) -> StorageBackendResult<Vec<EdgeRow>>;
630    fn save_graph_membership(
631        &self,
632        entity_type: &str,
633        entity_id: u64,
634        graph_name: &str,
635    ) -> StorageBackendResult<()>;
636    fn delete_graph_membership(
637        &self,
638        entity_type: &str,
639        entity_id: u64,
640        graph_name: &str,
641    ) -> StorageBackendResult<()>;
642    fn delete_graph_membership_for_graph(&self, graph_name: &str) -> StorageBackendResult<()>;
643    fn load_graph_memberships(&self) -> StorageBackendResult<Vec<(String, u64, String)>>;
644    fn purge_orphan_graph_entities(&self) -> StorageBackendResult<()>;
645    fn replace_named_graph(
646        &self,
647        graph_name: &str,
648        snapshot: &GraphSnapshot,
649    ) -> StorageBackendResult<()>;
650    fn drop_named_graph_data(&self, graph_name: &str) -> StorageBackendResult<()>;
651
652    fn save_analyzer(&self, name: &str, config_json: &str) -> StorageBackendResult<()>;
653    fn drop_analyzer(&self, name: &str) -> StorageBackendResult<()>;
654    fn load_analyzers(&self) -> StorageBackendResult<Vec<(String, String)>>;
655
656    fn save_table_field_analyzer(
657        &self,
658        table_name: &str,
659        field: &str,
660        phase: &str,
661        analyzer_name: &str,
662    ) -> StorageBackendResult<()>;
663    fn replace_table_field_analyzer(
664        &self,
665        table_name: &str,
666        field: &str,
667        phase: &str,
668        analyzer_name: &str,
669    ) -> StorageBackendResult<()>;
670    fn drop_table_field_analyzer_field(
671        &self,
672        table_name: &str,
673        field: &str,
674    ) -> StorageBackendResult<()>;
675    fn drop_table_field_analyzers(&self, table_name: &str) -> StorageBackendResult<()>;
676    fn load_table_field_analyzers(
677        &self,
678    ) -> StorageBackendResult<Vec<(String, String, String, String)>>;
679
680    fn save_foreign_server(
681        &self,
682        name: &str,
683        fdw_type: &str,
684        options_json: &str,
685    ) -> StorageBackendResult<()>;
686    fn drop_foreign_server(&self, name: &str) -> StorageBackendResult<()>;
687    fn load_foreign_servers(&self) -> StorageBackendResult<Vec<(String, String, String)>>;
688
689    fn save_foreign_table(&self, row: &ForeignTableRow) -> StorageBackendResult<()>;
690    /// Atomically move one foreign-table catalog row and its shared relation claim.
691    fn rename_foreign_table(
692        &self,
693        from: &RelationIdentity,
694        to: &RelationIdentity,
695    ) -> StorageBackendResult<bool>;
696    fn update_foreign_table_security(
697        &self,
698        relation: &RelationIdentity,
699        role_owner: &str,
700        acl: Option<&[TableAclEntry]>,
701        column_acls: &std::collections::BTreeMap<String, Vec<TableAclEntry>>,
702    ) -> StorageBackendResult<bool>;
703    fn drop_foreign_table(&self, relation: &RelationIdentity) -> StorageBackendResult<()>;
704    fn load_foreign_tables(&self) -> StorageBackendResult<Vec<ForeignTableRow>>;
705
706    fn save_catalog_index(
707        &self,
708        relation: &RelationIdentity,
709        index_type: &str,
710        table_name: &str,
711        columns_json: &str,
712        parameters_json: &str,
713    ) -> StorageBackendResult<()> {
714        self.save_catalog_index_row(&CatalogIndexRow {
715            relation: relation.clone(),
716            index_type: index_type.to_string(),
717            table_name: table_name.to_string(),
718            columns_json: columns_json.to_string(),
719            parameters_json: parameters_json.to_string(),
720            definition_json: None,
721        })
722    }
723    fn save_catalog_index_row(&self, index: &CatalogIndexRow) -> StorageBackendResult<()>;
724    fn drop_catalog_index(&self, relation: &RelationIdentity) -> StorageBackendResult<()>;
725    fn drop_catalog_indexes_for_table(&self, table_name: &str) -> StorageBackendResult<()>;
726    fn load_catalog_indexes(&self) -> StorageBackendResult<Vec<CatalogIndexRow>>;
727
728    fn save_path_index(
729        &self,
730        graph_name: &str,
731        label_sequences_json: &str,
732    ) -> StorageBackendResult<()>;
733    fn drop_path_index(&self, graph_name: &str) -> StorageBackendResult<()>;
734    fn load_path_indexes(&self) -> StorageBackendResult<Vec<(String, String)>>;
735
736    /// Durable reachability records, paged lexicographically by (source, target).
737    /// Building calls clear, bounded saves, then finish inside one transaction.
738    /// Graph mutations invalidate the materialization atomically; definitions remain.
739    fn clear_path_index_data(&self, index: &str) -> StorageBackendResult<()>;
740    fn save_path_index_pairs(
741        &self,
742        index: &str,
743        sequence: &str,
744        pairs: &[(u64, u64)],
745    ) -> StorageBackendResult<()>;
746    fn finish_path_index_data(
747        &self,
748        index: &str,
749        graph: &str,
750        definition: &str,
751    ) -> StorageBackendResult<()>;
752    fn path_index_data_is_current(
753        &self,
754        index: &str,
755        definition: &str,
756    ) -> StorageBackendResult<bool>;
757    fn path_index_pairs(
758        &self,
759        index: &str,
760        sequence: &str,
761        after: Option<(u64, u64)>,
762        limit: usize,
763    ) -> StorageBackendResult<Vec<(u64, u64)>>;
764
765    fn save_column_stats(&self, stats: ColumnStatsInput<'_>) -> StorageBackendResult<()>;
766    /// Atomically replace the complete statistics snapshot for one table.
767    /// Implementations must leave the prior snapshot intact if any row fails.
768    fn replace_column_stats(
769        &self,
770        table_name: &str,
771        stats: &[ColumnStatsInput<'_>],
772    ) -> StorageBackendResult<()>;
773    fn load_column_stats(&self, table_name: &str) -> StorageBackendResult<Vec<ColumnStatsRow>>;
774    fn delete_column_stats(&self, table_name: &str) -> StorageBackendResult<()>;
775}
776
777#[cfg(test)]
778mod tests {
779    use super::{
780        sequence_value_reservation, RelationIdentity, SequenceValuePosition,
781        SequenceValueReservation,
782    };
783
784    const fn sequence_position(
785        current: i64,
786        called: bool,
787        log_count: i64,
788    ) -> SequenceValuePosition {
789        SequenceValuePosition {
790            current,
791            called,
792            log_count,
793        }
794    }
795
796    #[test]
797    fn relation_identity_rendering_is_reversible_and_collision_free() {
798        let left = RelationIdentity::new("a.b", "c");
799        let right = RelationIdentity::new("a", "b.c");
800        assert_eq!(left.qualified_name(), "\"a.b\".c");
801        assert_eq!(right.qualified_name(), "a.\"b.c\"");
802        assert_ne!(left.qualified_name(), right.qualified_name());
803        assert_eq!(
804            RelationIdentity::from_legacy_name(&left.qualified_name()).unwrap(),
805            left
806        );
807        assert_eq!(
808            RelationIdentity::from_legacy_name(&right.qualified_name()).unwrap(),
809            right
810        );
811    }
812
813    #[test]
814    fn relation_identity_preserves_quotes_and_unqualified_public_alias() {
815        let quoted = RelationIdentity::new("public", "a\"b.c");
816        assert_eq!(quoted.qualified_name(), "public.\"a\"\"b.c\"");
817        assert_eq!(
818            quoted.canonical_and_legacy_public_names(),
819            vec![
820                "public.\"a\"\"b.c\"".to_string(),
821                "\"a\"\"b.c\"".to_string()
822            ]
823        );
824        assert_eq!(
825            RelationIdentity::from_legacy_name(&quoted.qualified_name()).unwrap(),
826            quoted
827        );
828        assert_eq!(
829            RelationIdentity::from_legacy_name("plain").unwrap(),
830            RelationIdentity::new("public", "plain")
831        );
832        assert_eq!(
833            RelationIdentity::new("app", "plain").canonical_and_legacy_public_names(),
834            vec!["app.plain".to_string()]
835        );
836        assert_eq!(
837            RelationIdentity::new("public", "Upper").canonical_and_legacy_public_names(),
838            vec![
839                "public.\"Upper\"".to_string(),
840                "\"Upper\"".to_string(),
841                "Upper".to_string()
842            ]
843        );
844    }
845
846    #[test]
847    fn sequence_reservations_track_postgresql_log_counts() {
848        assert_eq!(
849            sequence_value_reservation(sequence_position(1, false, 0), 1, 1, i64::MAX, false, 1),
850            Some(SequenceValueReservation {
851                first_value: 1,
852                last_value: 1,
853                count: 1,
854                log_count: 32,
855            })
856        );
857        assert_eq!(
858            sequence_value_reservation(sequence_position(1, true, 32), 1, 1, i64::MAX, false, 1),
859            Some(SequenceValueReservation {
860                first_value: 2,
861                last_value: 2,
862                count: 1,
863                log_count: 31,
864            })
865        );
866        assert_eq!(
867            sequence_value_reservation(sequence_position(1, false, 0), 1, 1, i64::MAX, false, 10),
868            Some(SequenceValueReservation {
869                first_value: 1,
870                last_value: 10,
871                count: 10,
872                log_count: 32,
873            })
874        );
875        assert_eq!(
876            sequence_value_reservation(sequence_position(5, false, 0), 2, 3, 9, true, 3),
877            Some(SequenceValueReservation {
878                first_value: 5,
879                last_value: 9,
880                count: 3,
881                log_count: 0,
882            })
883        );
884        assert_eq!(
885            sequence_value_reservation(
886                sequence_position(1, false, 0),
887                1,
888                1,
889                i64::MAX,
890                false,
891                i64::MAX,
892            ),
893            Some(SequenceValueReservation {
894                first_value: 1,
895                last_value: i64::MAX,
896                count: i64::MAX,
897                log_count: 0,
898            })
899        );
900    }
901}