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 schema;
19mod table;
20
21pub use schema::{SchemaAclEntry, SchemaPrivileges, SchemaRow};
22pub use table::{TableAclEntry, TablePrivileges};
23
24/// Durable identity of a SQL relation.
25///
26/// The schema and local name are stored separately so `foo` and
27/// `public.foo` can never become two physical catalog identities for the
28/// same SQL object.
29#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
30pub struct RelationIdentity {
31    pub schema: String,
32    pub name: String,
33}
34
35impl RelationIdentity {
36    pub fn new(schema: impl Into<String>, name: impl Into<String>) -> Self {
37        Self {
38            schema: schema.into(),
39            name: name.into(),
40        }
41    }
42
43    pub fn qualified_name(&self) -> String {
44        format!(
45            "{}.{}",
46            render_relation_component(&self.schema),
47            render_relation_component(&self.name)
48        )
49    }
50
51    /// Physical owner keys that can refer to this relation. New writes use
52    /// only the canonical qualified name. Catalog cleanup also accepts the
53    /// former unqualified key for `public` relations so data written before
54    /// relation identities became schema-aware cannot survive its owner.
55    pub(crate) fn canonical_and_legacy_public_names(&self) -> Vec<String> {
56        let canonical = self.qualified_name();
57        if self.schema != "public" {
58            return vec![canonical];
59        }
60        let mut names = vec![canonical];
61        let rendered_alias = render_relation_component(&self.name);
62        if !names.contains(&rendered_alias) {
63            names.push(rendered_alias);
64        }
65        // The direct Rust API historically accepted a decoded local name as
66        // well as SQL-rendered text. Include that spelling only when parsing
67        // it maps back to this exact relation; for example, raw `a.b` must not
68        // be removed while dropping the distinct public relation `"a.b"`.
69        if RelationIdentity::from_legacy_name(&self.name).is_ok_and(|raw| raw == *self)
70            && !names.contains(&self.name)
71        {
72            names.push(self.name.clone());
73        }
74        names
75    }
76
77    /// Decode a SQL relation reference or a former flat catalog key.
78    /// Unqualified objects belong to `public`. Quoted components preserve
79    /// embedded dots and escaped quotes, so `public.\"a.b\"` is distinct from
80    /// `\"public.a\".b` all the way down to physical storage keys.
81    pub fn from_legacy_name(value: &str) -> Result<Self, String> {
82        let (schema, name) = Self::parse_reference(value)?;
83        Ok(Self::new(
84            schema.unwrap_or_else(|| "public".to_string()),
85            name,
86        ))
87    }
88
89    /// Recover an index identity from the former flat index catalog. The stored value is a decoded local identifier rather than a relation reference, so dots and quotes remain part of the local name and the owning table supplies the schema.
90    pub(crate) fn from_legacy_index_name(value: &str, table: &Self) -> Self {
91        Self::new(&table.schema, value)
92    }
93
94    /// Parse a possibly-unqualified SQL relation reference without choosing a
95    /// search-path schema. Components use `PostgreSQL` double-quote escaping.
96    pub fn parse_reference(value: &str) -> Result<(Option<String>, String), String> {
97        let components = parse_relation_components(value)?;
98        match components.as_slice() {
99            [name] => Ok((None, name.clone())),
100            [schema, name] => Ok((Some(schema.clone()), name.clone())),
101            _ => Err(format!("invalid persisted relation name `{value}`")),
102        }
103    }
104}
105
106fn render_relation_component(component: &str) -> String {
107    let can_render_bare = component
108        .bytes()
109        .enumerate()
110        .all(|(index, byte)| match byte {
111            b'a'..=b'z' | b'_' => true,
112            b'0'..=b'9' | b'$' => index != 0,
113            _ => false,
114        });
115    if can_render_bare && !component.is_empty() {
116        component.to_string()
117    } else {
118        format!("\"{}\"", component.replace('"', "\"\""))
119    }
120}
121
122fn parse_relation_components(value: &str) -> Result<Vec<String>, String> {
123    if value.is_empty() {
124        return Err("persisted relation name is empty".to_string());
125    }
126    let mut components = Vec::with_capacity(2);
127    let mut chars = value.char_indices().peekable();
128    while chars.peek().is_some() {
129        let mut component = String::new();
130        if chars.peek().is_some_and(|(_, ch)| *ch == '"') {
131            chars.next();
132            let mut terminated = false;
133            while let Some((_, ch)) = chars.next() {
134                if ch != '"' {
135                    component.push(ch);
136                    continue;
137                }
138                if chars.peek().is_some_and(|(_, next)| *next == '"') {
139                    chars.next();
140                    component.push('"');
141                } else {
142                    terminated = true;
143                    break;
144                }
145            }
146            if !terminated {
147                return Err(format!("unterminated quoted relation name `{value}`"));
148            }
149            if chars.peek().is_some_and(|(_, ch)| *ch != '.') {
150                return Err(format!("invalid persisted relation name `{value}`"));
151            }
152        } else {
153            while let Some((_, ch)) = chars.peek() {
154                if *ch == '.' {
155                    break;
156                }
157                if *ch == '"' {
158                    return Err(format!("invalid persisted relation name `{value}`"));
159                }
160                component.push(*ch);
161                chars.next();
162            }
163        }
164        if component.is_empty() {
165            return Err(format!("invalid persisted relation name `{value}`"));
166        }
167        components.push(component);
168        if components.len() > 2 {
169            return Err(format!("invalid persisted relation name `{value}`"));
170        }
171        match chars.next() {
172            Some((_, '.')) if chars.peek().is_some() => {}
173            Some(_) => return Err(format!("invalid persisted relation name `{value}`")),
174            None => break,
175        }
176    }
177    Ok(components)
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
181#[serde(rename_all = "snake_case")]
182pub enum RelationKind {
183    Table,
184    View,
185    Sequence,
186    ForeignTable,
187    Index,
188}
189
190impl RelationKind {
191    pub fn as_str(self) -> &'static str {
192        match self {
193            Self::Table => "table",
194            Self::View => "view",
195            Self::Sequence => "sequence",
196            Self::ForeignTable => "foreign_table",
197            Self::Index => "index",
198        }
199    }
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct TableSchema {
204    pub relation: RelationIdentity,
205    /// SQL role that owns the relation. Catalogs created before table role ownership was persisted belong to the bootstrap role.
206    #[serde(default = "legacy_table_role_owner")]
207    pub role_owner: String,
208    /// Explicit table ACL paths. `None` represents `PostgreSQL`'s null default ACL, in which the owner has every ordinary table privilege.
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub acl: Option<Vec<TableAclEntry>>,
211    /// 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.
212    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
213    pub column_acls: std::collections::BTreeMap<String, Vec<TableAclEntry>>,
214    /// 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.
215    #[serde(default)]
216    pub object_id: [u8; 16],
217    /// 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.
218    #[serde(default)]
219    pub storage_generation: [u8; 16],
220    pub analyzer_json: String,
221    pub fts_fields: Vec<String>,
222    pub vector_fields: Vec<VectorFieldSchema>,
223    /// Serialized `Vec<uqa_sql::ast::ColumnDef>` capturing the schema
224    /// columns (name, type, `auto_increment`, flags). Empty for
225    /// tables created by the legacy code path before column tracking
226    /// existed.
227    #[serde(default)]
228    pub columns_json: String,
229    /// Serialized `uqa_sql::ast::TableConstraintSet`. Empty for catalogs
230    /// created before durable table constraints were introduced.
231    #[serde(default)]
232    pub constraints_json: String,
233}
234
235fn legacy_table_role_owner() -> String {
236    "uqa".into()
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct VectorFieldSchema {
241    pub field: String,
242    pub dimensions: u32,
243}
244
245/// One row from graph-edge persistence, represented as a typed struct so the
246/// catalog API stays explicit and clippy-clean.
247#[derive(Debug, Clone)]
248pub struct EdgeRow {
249    pub edge_id: u64,
250    pub source_id: u64,
251    pub target_id: u64,
252    pub label: String,
253    pub properties_json: String,
254}
255
256/// One vertex in an atomic named-graph snapshot replacement.
257#[derive(Debug, Clone)]
258pub struct GraphVertexRow {
259    pub vertex_id: u64,
260    pub label: String,
261    pub properties_json: String,
262}
263
264/// Complete persisted shape of one named graph. Catalog implementations
265/// replace the graph membership and entity rows as one atomic operation.
266#[derive(Debug, Clone)]
267pub struct GraphSnapshot {
268    pub vertices: Vec<GraphVertexRow>,
269    pub edges: Vec<EdgeRow>,
270    pub label_registry_json: String,
271}
272
273/// One row from the foreign-table registry.
274#[derive(Debug, Clone)]
275pub struct ForeignTableRow {
276    pub relation: RelationIdentity,
277    /// SQL role that owns the foreign table. Catalogs created before foreign-table role ownership was persisted belong to the bootstrap role.
278    pub role_owner: String,
279    /// Explicit relation-wide ACL. `None` preserves `PostgreSQL`'s implicit owner-only default ACL.
280    pub acl: Option<Vec<TableAclEntry>>,
281    /// Explicit per-column ACL paths keyed by the foreign table's public column name.
282    pub column_acls: std::collections::BTreeMap<String, Vec<TableAclEntry>>,
283    pub server_name: String,
284    pub columns_json: String,
285    pub options_json: String,
286}
287
288/// One durable view definition. `definition_json` contains a serialized
289/// planner query plan, while ownership remains a typed catalog relation.
290#[derive(Debug, Clone)]
291pub struct ViewRow {
292    pub relation: RelationIdentity,
293    /// SQL role that owns the view. Catalogs created before view role ownership was persisted belong to the bootstrap role.
294    pub role_owner: String,
295    /// Explicit relation-wide ACL. `None` preserves `PostgreSQL`'s implicit owner-only default ACL.
296    pub acl: Option<Vec<TableAclEntry>>,
297    /// Explicit per-column ACL paths keyed by the view's public column name.
298    pub column_acls: std::collections::BTreeMap<String, Vec<TableAclEntry>>,
299    pub definition_json: String,
300}
301
302/// One row from the secondary-index registry.
303#[derive(Debug, Clone)]
304pub struct CatalogIndexRow {
305    pub relation: RelationIdentity,
306    pub index_type: String,
307    pub table_name: String,
308    pub columns_json: String,
309    pub parameters_json: String,
310    /// Durable semantic definition; absent on legacy ordinary secondary indexes.
311    pub definition_json: Option<String>,
312}
313
314/// Values persisted into one column-statistics row.
315#[derive(Debug, Clone, Copy)]
316pub struct ColumnStatsInput<'a> {
317    pub table_name: &'a str,
318    pub column_name: &'a str,
319    pub distinct_count: i64,
320    pub null_count: i64,
321    pub min_value: Option<&'a str>,
322    pub max_value: Option<&'a str>,
323    pub row_count: i64,
324    pub histogram_json: &'a str,
325    pub mcv_values_json: &'a str,
326    pub mcv_frequencies_json: &'a str,
327}
328
329impl<'a> ColumnStatsInput<'a> {
330    pub fn basic(
331        table_name: &'a str,
332        column_name: &'a str,
333        distinct_count: i64,
334        null_count: i64,
335        min_value: Option<&'a str>,
336        max_value: Option<&'a str>,
337        row_count: i64,
338    ) -> Self {
339        Self {
340            table_name,
341            column_name,
342            distinct_count,
343            null_count,
344            min_value,
345            max_value,
346            row_count,
347            histogram_json: "[]",
348            mcv_values_json: "[]",
349            mcv_frequencies_json: "[]",
350        }
351    }
352}
353
354/// One row from persisted column statistics.
355#[derive(Debug, Clone, PartialEq)]
356pub struct ColumnStatsRow {
357    pub column_name: String,
358    pub distinct_count: i64,
359    pub null_count: i64,
360    pub min_value: Option<String>,
361    pub max_value: Option<String>,
362    pub row_count: i64,
363    pub histogram_json: String,
364    pub mcv_values_json: String,
365    pub mcv_frequencies_json: String,
366}
367
368/// Durable SQL sequence state. Sequence allocation is implemented by the
369/// catalog backend as one atomic mutation so independent engine sessions (and
370/// independently opened engines) cannot return the same value.
371#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
372pub struct SequenceOptions {
373    pub data_type: String,
374    /// `None` is accepted only while decoding a legacy row and is resolved from its increment direction by the engine.
375    pub min_value: Option<i64>,
376    /// `None` is accepted only while decoding a legacy row and is resolved from its increment direction by the engine.
377    pub max_value: Option<i64>,
378    pub cycle: bool,
379    #[serde(default = "default_sequence_cache_size")]
380    pub cache_size: i64,
381}
382
383const fn default_sequence_cache_size() -> i64 {
384    1
385}
386
387impl Default for SequenceOptions {
388    fn default() -> Self {
389        Self {
390            data_type: "bigint".into(),
391            min_value: None,
392            max_value: None,
393            cycle: false,
394            cache_size: default_sequence_cache_size(),
395        }
396    }
397}
398
399/// 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.
400#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
401#[serde(rename_all = "snake_case")]
402pub enum SequenceOwnerDependency {
403    #[default]
404    Automatic,
405    Internal,
406}
407
408impl SequenceOwnerDependency {
409    #[must_use]
410    pub const fn catalog_code(self) -> &'static str {
411        match self {
412            Self::Automatic => "a",
413            Self::Internal => "i",
414        }
415    }
416}
417
418/// Stable owner identity for a sequence dependency. Names are deliberately excluded so table and column renames do not require dependency rewrites.
419#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
420pub struct SequenceOwner {
421    pub table_object_id: [u8; 16],
422    pub column_object_id: [u8; 16],
423    #[serde(default)]
424    pub dependency: SequenceOwnerDependency,
425}
426
427/// Grantable privileges carried by one sequence ACL path.
428#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
429pub struct SequencePrivileges {
430    #[serde(default)]
431    pub select: bool,
432    #[serde(default)]
433    pub update: bool,
434    #[serde(default)]
435    pub usage: bool,
436}
437
438impl SequencePrivileges {
439    pub const ALL: Self = Self {
440        select: true,
441        update: true,
442        usage: true,
443    };
444
445    #[must_use]
446    pub const fn is_empty(self) -> bool {
447        !self.select && !self.update && !self.usage
448    }
449
450    #[must_use]
451    pub const fn intersects(self, other: Self) -> bool {
452        self.select && other.select || self.update && other.update || self.usage && other.usage
453    }
454
455    pub fn insert(&mut self, other: Self) {
456        self.select |= other.select;
457        self.update |= other.update;
458        self.usage |= other.usage;
459    }
460
461    pub fn remove(&mut self, other: Self) {
462        self.select &= !other.select;
463        self.update &= !other.update;
464        self.usage &= !other.usage;
465    }
466}
467
468/// One explicit sequence ACL path. `None` on `SequenceRow::acl` retains `PostgreSQL`'s default owner-only privileges.
469#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
470pub struct SequenceAclEntry {
471    pub role: String,
472    /// Legacy persisted entries without an explicit grantor originate from the sequence owner.
473    #[serde(default, skip_serializing_if = "Option::is_none")]
474    pub grantor: Option<String>,
475    #[serde(default)]
476    pub privileges: SequencePrivileges,
477    #[serde(default)]
478    pub grant_options: SequencePrivileges,
479}
480
481#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
482pub struct SequenceRow {
483    pub relation: RelationIdentity,
484    /// SQL role that owns the sequence. Legacy catalogs predate roles and therefore belong to the bootstrap role.
485    #[serde(default = "default_sequence_role_owner")]
486    pub role_owner: String,
487    /// Explicit ACL entries. `None` represents `PostgreSQL`'s null default ACL, in which the owner has all ordinary privileges.
488    #[serde(default, skip_serializing_if = "Option::is_none")]
489    pub acl: Option<Vec<SequenceAclEntry>>,
490    /// Stable identity of this sequence incarnation. Dropping and recreating the same qualified name must allocate a different value.
491    #[serde(default)]
492    pub object_id: [u8; 16],
493    /// Changes for every successful definition-changing `ALTER SEQUENCE` while remaining stable across name lifecycle operations, value reservations, and `setval`.
494    #[serde(default)]
495    pub definition_generation: [u8; 16],
496    pub start: i64,
497    pub increment: i64,
498    pub current: i64,
499    /// False until the first allocation returns `current` verbatim.
500    pub called: bool,
501    /// Number of values whose advancement is already durable in the sequence log.
502    #[serde(default)]
503    pub log_count: i64,
504    /// `PostgreSQL` `pg_class.relpersistence` code. Durable sequence rows accept only permanent (`p`) and unlogged (`u`) values.
505    pub persistence: String,
506    #[serde(default)]
507    pub owner: Option<SequenceOwner>,
508    #[serde(default)]
509    pub options: SequenceOptions,
510}
511
512fn default_sequence_role_owner() -> String {
513    "uqa".into()
514}
515
516/// Physical sequence position consumed by one atomic reservation.
517#[derive(Debug, Clone, Copy, PartialEq, Eq)]
518pub struct SequenceValuePosition {
519    pub current: i64,
520    pub called: bool,
521    pub log_count: i64,
522}
523
524/// One atomic sequence reservation. `first_value` is returned immediately, while the remaining values through `last_value` belong to the allocating session.
525#[derive(Debug, Clone, Copy, PartialEq, Eq)]
526pub struct SequenceValueReservation {
527    pub first_value: i64,
528    pub last_value: i64,
529    pub count: i64,
530    pub log_count: i64,
531}
532
533#[derive(Debug, Clone, Copy, PartialEq, Eq)]
534pub enum SequenceReservationResult {
535    Missing,
536    DefinitionChanged,
537    Exhausted,
538    Reserved(SequenceValueReservation),
539}
540
541/// 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.
542#[must_use]
543pub fn sequence_value_reservation(
544    position: SequenceValuePosition,
545    increment: i64,
546    min_value: i64,
547    max_value: i64,
548    cycle: bool,
549    cache_size: i64,
550) -> Option<SequenceValueReservation> {
551    let SequenceValuePosition {
552        current,
553        called,
554        log_count,
555    } = position;
556    debug_assert_ne!(increment, 0);
557    debug_assert!(cache_size > 0);
558    let first_value = if called {
559        match current
560            .checked_add(increment)
561            .filter(|value| (min_value..=max_value).contains(value))
562        {
563            Some(value) => value,
564            None if cycle && increment > 0 => min_value,
565            None if cycle => max_value,
566            None => return None,
567        }
568    } else {
569        current
570    };
571    let distance = if increment > 0 {
572        i128::from(max_value) - i128::from(first_value)
573    } else {
574        i128::from(first_value) - i128::from(min_value)
575    };
576    let step = i128::from(increment).abs();
577    let available = distance / step + 1;
578    let count = available.min(i128::from(cache_size));
579    let last_value = i128::from(first_value) + i128::from(increment) * (count - 1);
580    let initial_count = i128::from(!called);
581    let cache_fetch = i128::from(cache_size) - initial_count;
582    let mut fetch = cache_fetch;
583    let mut next_log_count = i128::from(log_count);
584    if i128::from(log_count) < cache_fetch || !called {
585        fetch += 32;
586        next_log_count = fetch;
587    }
588    let fetched = fetch.min(available - initial_count);
589    next_log_count -= fetched.min(cache_fetch);
590    next_log_count -= fetch - fetched;
591    Some(SequenceValueReservation {
592        first_value,
593        last_value: i64::try_from(last_value).expect("reserved sequence value stays in bounds"),
594        count: i64::try_from(count).expect("reservation count cannot exceed cache size"),
595        log_count: i64::try_from(next_log_count)
596            .expect("persisted sequence log count cannot exceed the cache request"),
597    })
598}
599
600/// Engine-facing catalog facade for persistent metadata.
601pub trait CatalogFacade: Send + Sync {
602    fn set_metadata(&self, key: &str, value: &str) -> StorageBackendResult<()>;
603    fn get_metadata(&self, key: &str) -> StorageBackendResult<Option<String>>;
604    fn fts_storage_was_reset(&self) -> bool {
605        false
606    }
607
608    /// Atomically migrate the former flat relation namespace into typed,
609    /// schema-owned catalog objects. Implementations must reject normalized
610    /// or cross-kind collisions instead of merging either object.
611    fn migrate_relation_namespace(&self) -> StorageBackendResult<()>;
612
613    fn save_schema_row(&self, schema: &SchemaRow) -> StorageBackendResult<()>;
614    fn drop_schema(&self, name: &str) -> StorageBackendResult<()>;
615    fn load_schema_rows(&self) -> StorageBackendResult<Vec<SchemaRow>>;
616
617    fn save_schema(&self, name: &str) -> StorageBackendResult<()> {
618        self.save_schema_row(&SchemaRow::legacy(name))
619    }
620
621    fn load_schemas(&self) -> StorageBackendResult<Vec<String>> {
622        Ok(self
623            .load_schema_rows()?
624            .into_iter()
625            .map(|schema| schema.name)
626            .collect())
627    }
628
629    fn save_table(&self, schema: &TableSchema) -> StorageBackendResult<()>;
630    fn load_tables(&self) -> StorageBackendResult<Vec<TableSchema>>;
631    fn drop_table(&self, name: &str) -> StorageBackendResult<()>;
632    /// Remove a table definition and every catalog/data row owned by it as
633    /// one atomic catalog operation.
634    fn drop_table_and_data(&self, name: &str) -> StorageBackendResult<()>;
635    fn purge_table_data(&self, name: &str) -> StorageBackendResult<()>;
636    fn rename_table_data(&self, from: &str, to: &str) -> StorageBackendResult<()>;
637    fn drop_column_data(&self, table_name: &str, column_name: &str) -> StorageBackendResult<()>;
638    fn rename_column_data(
639        &self,
640        table_name: &str,
641        from: &str,
642        to: &str,
643    ) -> StorageBackendResult<()>;
644
645    fn save_model(&self, name: &str, json: &str) -> StorageBackendResult<()>;
646    fn load_models(&self) -> StorageBackendResult<Vec<(String, String)>>;
647    fn load_model(&self, name: &str) -> StorageBackendResult<Option<String>>;
648    fn drop_model(&self, name: &str) -> StorageBackendResult<()>;
649
650    fn save_scoring_params(&self, name: &str, params_json: &str) -> StorageBackendResult<()>;
651    fn load_scoring_params(&self, name: &str) -> StorageBackendResult<Option<String>>;
652    fn load_all_scoring_params(&self) -> StorageBackendResult<Vec<(String, String)>>;
653    fn drop_scoring_params(&self, name: &str) -> StorageBackendResult<()>;
654
655    fn create_sequence_row(&self, sequence: &SequenceRow) -> StorageBackendResult<bool>;
656    fn replace_sequence_row(&self, sequence: &SequenceRow) -> StorageBackendResult<bool>;
657    /// Atomically move one sequence catalog row and its shared relation claim while preserving object identity and physical value state.
658    fn rename_sequence_row(&self, from: &str, to: &str) -> StorageBackendResult<bool>;
659    fn drop_sequence_row(&self, name: &str) -> StorageBackendResult<bool>;
660    fn load_sequence_rows(&self) -> StorageBackendResult<Vec<SequenceRow>>;
661    fn reserve_sequence_values(
662        &self,
663        name: &str,
664        object_id: [u8; 16],
665        definition_generation: [u8; 16],
666    ) -> StorageBackendResult<SequenceReservationResult>;
667    /// 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.
668    fn next_sequence_value(
669        &self,
670        name: &str,
671        object_id: [u8; 16],
672    ) -> StorageBackendResult<Option<i64>> {
673        loop {
674            let relation =
675                RelationIdentity::from_legacy_name(name).map_err(StorageBackendError::Other)?;
676            let Some(row) = self
677                .load_sequence_rows()?
678                .into_iter()
679                .find(|row| row.relation == relation && row.object_id == object_id)
680            else {
681                return Ok(None);
682            };
683            match self.reserve_sequence_values(name, object_id, row.definition_generation)? {
684                SequenceReservationResult::Reserved(reservation) => {
685                    return Ok(Some(reservation.first_value));
686                }
687                SequenceReservationResult::DefinitionChanged => {}
688                SequenceReservationResult::Missing => return Ok(None),
689                SequenceReservationResult::Exhausted => {
690                    return Err(StorageBackendError::Other(format!(
691                        "sequence `{name}` exhausted"
692                    )));
693                }
694            }
695        }
696    }
697    fn set_sequence_value(
698        &self,
699        name: &str,
700        object_id: [u8; 16],
701        value: i64,
702        called: bool,
703        log_count: i64,
704    ) -> StorageBackendResult<Option<i64>>;
705
706    fn save_view(&self, view: &ViewRow) -> StorageBackendResult<()>;
707    /// Atomically move one view catalog row and its shared relation claim.
708    fn rename_view(
709        &self,
710        from: &RelationIdentity,
711        to: &RelationIdentity,
712    ) -> StorageBackendResult<bool>;
713    fn drop_view(&self, relation: &RelationIdentity) -> StorageBackendResult<bool>;
714    fn load_views(&self) -> StorageBackendResult<Vec<ViewRow>>;
715
716    fn save_named_graph(&self, name: &str) -> StorageBackendResult<()>;
717    fn drop_named_graph(&self, name: &str) -> StorageBackendResult<()>;
718    fn load_named_graphs(&self) -> StorageBackendResult<Vec<String>>;
719    fn save_vertex(
720        &self,
721        vertex_id: u64,
722        label: &str,
723        properties_json: &str,
724    ) -> StorageBackendResult<()>;
725    fn delete_vertex(&self, vertex_id: u64) -> StorageBackendResult<()>;
726    fn load_vertices(&self) -> StorageBackendResult<Vec<(u64, String, String)>>;
727    fn save_edge(
728        &self,
729        edge_id: u64,
730        source_id: u64,
731        target_id: u64,
732        label: &str,
733        properties_json: &str,
734    ) -> StorageBackendResult<()>;
735    fn delete_edge(&self, edge_id: u64) -> StorageBackendResult<()>;
736    fn load_edges(&self) -> StorageBackendResult<Vec<EdgeRow>>;
737    fn save_graph_membership(
738        &self,
739        entity_type: &str,
740        entity_id: u64,
741        graph_name: &str,
742    ) -> StorageBackendResult<()>;
743    fn delete_graph_membership(
744        &self,
745        entity_type: &str,
746        entity_id: u64,
747        graph_name: &str,
748    ) -> StorageBackendResult<()>;
749    fn delete_graph_membership_for_graph(&self, graph_name: &str) -> StorageBackendResult<()>;
750    fn load_graph_memberships(&self) -> StorageBackendResult<Vec<(String, u64, String)>>;
751    fn purge_orphan_graph_entities(&self) -> StorageBackendResult<()>;
752    fn replace_named_graph(
753        &self,
754        graph_name: &str,
755        snapshot: &GraphSnapshot,
756    ) -> StorageBackendResult<()>;
757    fn drop_named_graph_data(&self, graph_name: &str) -> StorageBackendResult<()>;
758
759    fn save_analyzer(&self, name: &str, config_json: &str) -> StorageBackendResult<()>;
760    fn drop_analyzer(&self, name: &str) -> StorageBackendResult<()>;
761    fn load_analyzers(&self) -> StorageBackendResult<Vec<(String, String)>>;
762
763    fn save_table_field_analyzer(
764        &self,
765        table_name: &str,
766        field: &str,
767        phase: &str,
768        analyzer_name: &str,
769    ) -> StorageBackendResult<()>;
770    fn replace_table_field_analyzer(
771        &self,
772        table_name: &str,
773        field: &str,
774        phase: &str,
775        analyzer_name: &str,
776    ) -> StorageBackendResult<()>;
777    fn drop_table_field_analyzer_field(
778        &self,
779        table_name: &str,
780        field: &str,
781    ) -> StorageBackendResult<()>;
782    fn drop_table_field_analyzers(&self, table_name: &str) -> StorageBackendResult<()>;
783    fn load_table_field_analyzers(
784        &self,
785    ) -> StorageBackendResult<Vec<(String, String, String, String)>>;
786
787    fn save_foreign_server(
788        &self,
789        name: &str,
790        fdw_type: &str,
791        options_json: &str,
792    ) -> StorageBackendResult<()>;
793    fn drop_foreign_server(&self, name: &str) -> StorageBackendResult<()>;
794    fn load_foreign_servers(&self) -> StorageBackendResult<Vec<(String, String, String)>>;
795
796    fn save_foreign_table(&self, row: &ForeignTableRow) -> StorageBackendResult<()>;
797    /// Atomically move one foreign-table catalog row and its shared relation claim.
798    fn rename_foreign_table(
799        &self,
800        from: &RelationIdentity,
801        to: &RelationIdentity,
802    ) -> StorageBackendResult<bool>;
803    fn update_foreign_table_security(
804        &self,
805        relation: &RelationIdentity,
806        role_owner: &str,
807        acl: Option<&[TableAclEntry]>,
808        column_acls: &std::collections::BTreeMap<String, Vec<TableAclEntry>>,
809    ) -> StorageBackendResult<bool>;
810    fn drop_foreign_table(&self, relation: &RelationIdentity) -> StorageBackendResult<()>;
811    fn load_foreign_tables(&self) -> StorageBackendResult<Vec<ForeignTableRow>>;
812
813    fn save_catalog_index(
814        &self,
815        relation: &RelationIdentity,
816        index_type: &str,
817        table_name: &str,
818        columns_json: &str,
819        parameters_json: &str,
820    ) -> StorageBackendResult<()> {
821        self.save_catalog_index_row(&CatalogIndexRow {
822            relation: relation.clone(),
823            index_type: index_type.to_string(),
824            table_name: table_name.to_string(),
825            columns_json: columns_json.to_string(),
826            parameters_json: parameters_json.to_string(),
827            definition_json: None,
828        })
829    }
830    fn save_catalog_index_row(&self, index: &CatalogIndexRow) -> StorageBackendResult<()>;
831    fn drop_catalog_index(&self, relation: &RelationIdentity) -> StorageBackendResult<()>;
832    fn drop_catalog_indexes_for_table(&self, table_name: &str) -> StorageBackendResult<()>;
833    fn load_catalog_indexes(&self) -> StorageBackendResult<Vec<CatalogIndexRow>>;
834
835    fn save_path_index(
836        &self,
837        graph_name: &str,
838        label_sequences_json: &str,
839    ) -> StorageBackendResult<()>;
840    fn drop_path_index(&self, graph_name: &str) -> StorageBackendResult<()>;
841    fn load_path_indexes(&self) -> StorageBackendResult<Vec<(String, String)>>;
842
843    fn save_column_stats(&self, stats: ColumnStatsInput<'_>) -> StorageBackendResult<()>;
844    /// Atomically replace the complete statistics snapshot for one table.
845    /// Implementations must leave the prior snapshot intact if any row fails.
846    fn replace_column_stats(
847        &self,
848        table_name: &str,
849        stats: &[ColumnStatsInput<'_>],
850    ) -> StorageBackendResult<()>;
851    fn load_column_stats(&self, table_name: &str) -> StorageBackendResult<Vec<ColumnStatsRow>>;
852    fn delete_column_stats(&self, table_name: &str) -> StorageBackendResult<()>;
853}
854
855#[cfg(test)]
856mod tests {
857    use super::{
858        sequence_value_reservation, RelationIdentity, SequenceValuePosition,
859        SequenceValueReservation,
860    };
861
862    const fn sequence_position(
863        current: i64,
864        called: bool,
865        log_count: i64,
866    ) -> SequenceValuePosition {
867        SequenceValuePosition {
868            current,
869            called,
870            log_count,
871        }
872    }
873
874    #[test]
875    fn relation_identity_rendering_is_reversible_and_collision_free() {
876        let left = RelationIdentity::new("a.b", "c");
877        let right = RelationIdentity::new("a", "b.c");
878        assert_eq!(left.qualified_name(), "\"a.b\".c");
879        assert_eq!(right.qualified_name(), "a.\"b.c\"");
880        assert_ne!(left.qualified_name(), right.qualified_name());
881        assert_eq!(
882            RelationIdentity::from_legacy_name(&left.qualified_name()).unwrap(),
883            left
884        );
885        assert_eq!(
886            RelationIdentity::from_legacy_name(&right.qualified_name()).unwrap(),
887            right
888        );
889    }
890
891    #[test]
892    fn relation_identity_preserves_quotes_and_unqualified_public_alias() {
893        let quoted = RelationIdentity::new("public", "a\"b.c");
894        assert_eq!(quoted.qualified_name(), "public.\"a\"\"b.c\"");
895        assert_eq!(
896            quoted.canonical_and_legacy_public_names(),
897            vec![
898                "public.\"a\"\"b.c\"".to_string(),
899                "\"a\"\"b.c\"".to_string()
900            ]
901        );
902        assert_eq!(
903            RelationIdentity::from_legacy_name(&quoted.qualified_name()).unwrap(),
904            quoted
905        );
906        assert_eq!(
907            RelationIdentity::from_legacy_name("plain").unwrap(),
908            RelationIdentity::new("public", "plain")
909        );
910        assert_eq!(
911            RelationIdentity::new("app", "plain").canonical_and_legacy_public_names(),
912            vec!["app.plain".to_string()]
913        );
914        assert_eq!(
915            RelationIdentity::new("public", "Upper").canonical_and_legacy_public_names(),
916            vec![
917                "public.\"Upper\"".to_string(),
918                "\"Upper\"".to_string(),
919                "Upper".to_string()
920            ]
921        );
922    }
923
924    #[test]
925    fn sequence_reservations_track_postgresql_log_counts() {
926        assert_eq!(
927            sequence_value_reservation(sequence_position(1, false, 0), 1, 1, i64::MAX, false, 1),
928            Some(SequenceValueReservation {
929                first_value: 1,
930                last_value: 1,
931                count: 1,
932                log_count: 32,
933            })
934        );
935        assert_eq!(
936            sequence_value_reservation(sequence_position(1, true, 32), 1, 1, i64::MAX, false, 1),
937            Some(SequenceValueReservation {
938                first_value: 2,
939                last_value: 2,
940                count: 1,
941                log_count: 31,
942            })
943        );
944        assert_eq!(
945            sequence_value_reservation(sequence_position(1, false, 0), 1, 1, i64::MAX, false, 10),
946            Some(SequenceValueReservation {
947                first_value: 1,
948                last_value: 10,
949                count: 10,
950                log_count: 32,
951            })
952        );
953        assert_eq!(
954            sequence_value_reservation(sequence_position(5, false, 0), 2, 3, 9, true, 3),
955            Some(SequenceValueReservation {
956                first_value: 5,
957                last_value: 9,
958                count: 3,
959                log_count: 0,
960            })
961        );
962        assert_eq!(
963            sequence_value_reservation(
964                sequence_position(1, false, 0),
965                1,
966                1,
967                i64::MAX,
968                false,
969                i64::MAX,
970            ),
971            Some(SequenceValueReservation {
972                first_value: 1,
973                last_value: i64::MAX,
974                count: i64::MAX,
975                log_count: 0,
976            })
977        );
978    }
979}