Skip to main content

uqa_sql/ast/
constraints.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Column and table constraint nodes shared by CREATE and ALTER TABLE.
8
9use serde::{Deserialize, Serialize};
10
11use super::{
12    deserialize_auto_increment, AutoIncrement, ColumnType, Expr, GeneratedColumn, OnCommitAction,
13    PartitionBound, PartitionSpec, RelationPersistence, TableHierarchy,
14};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[allow(clippy::struct_excessive_bools)]
18pub struct ColumnDef {
19    pub name: String,
20    pub ty: ColumnType,
21    /// The selected collation of the physical attribute. Legacy definitions use their type's default.
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub collation_oid: Option<u32>,
24    /// Durable identity of this catalog column. Logical names can change while a fixed transaction snapshot continues to address the same column.
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub object_id: Option<[u8; 16]>,
27    /// Stable relation attribute number. Parsed declarations have no number until publication; renaming and dropping other columns never change it.
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub attribute_number: Option<i16>,
30    /// Value exposed for physical rows captured before this column was added. This is the catalog equivalent of `PostgreSQL`'s `attmissingval`.
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub missing_value: Option<uqa_core::Value>,
33    pub primary_key: bool,
34    pub not_null: bool,
35    /// Whether `NOT NULL` was declared as its own constraint instead of being
36    /// implied by `PRIMARY KEY` or an auto-incrementing identity.
37    #[serde(default)]
38    pub not_null_explicit: bool,
39    /// Durable `PostgreSQL` 18 `NOT NULL` constraint name. Parsing leaves an
40    /// unnamed declaration as `None`; table registration assigns and persists
41    /// `PostgreSQL`'s generated name before the constraint becomes visible.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub not_null_name: Option<String>,
44    /// Independent NOT NULL lifetime and public catalog OID, retained through column, relation, and constraint renames.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub not_null_identity: Option<ConstraintCatalogIdentity>,
47    /// Whether the named `NOT NULL` constraint has been validated against
48    /// every pre-existing row. `NOT VALID` still enforces future writes.
49    #[serde(default = "default_true")]
50    pub not_null_validated: bool,
51    /// Durable `NO INHERIT` state for `PostgreSQL` 18 named `NOT NULL`
52    /// constraints.
53    #[serde(default)]
54    pub not_null_no_inherit: bool,
55    /// Whether this relation declares its NOT NULL constraint locally, independently from inherited parent constraints. Older serialized definitions retain their original local catalog projection.
56    #[serde(default = "default_true", skip_serializing_if = "is_true")]
57    pub not_null_is_local: bool,
58    /// Sequence provenance for `SERIAL` / `BIGSERIAL` and identity columns. The custom decoder accepts the legacy boolean representation written by releases that merged both SQL features into one table counter.
59    #[serde(
60        default,
61        deserialize_with = "deserialize_auto_increment",
62        skip_serializing_if = "Option::is_none"
63    )]
64    pub auto_increment: Option<AutoIncrement>,
65    /// `UNIQUE` column constraint -- the engine rejects an INSERT
66    /// whose value for this column already exists in another row.
67    #[serde(default)]
68    pub unique: bool,
69    /// `DEFAULT <expr>`. Evaluated at INSERT time when the column is
70    /// not present in the row tuple. Persisted in catalog metadata so
71    /// reopened engines keep the same INSERT semantics.
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub default: Option<Expr>,
74    /// `PostgreSQL` 18 generated-column definition. Stored values are refreshed
75    /// on every row write; virtual values are evaluated from the physical row
76    /// only when a logical row is read.
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub generated: Option<GeneratedColumn>,
79    /// The `pg_attrdef` OID of the default or generation expression, allocated when the expression was set; an expression set before OIDs were recorded derives it from the table and column names.
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub default_catalog_oid: Option<i64>,
82    /// `CHECK (<expr>)` column-level constraint. Evaluated at INSERT
83    /// (and UPDATE-replace) time against the row being written.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub check: Option<Expr>,
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub check_name: Option<String>,
88    #[serde(default = "default_true")]
89    pub check_enforced: bool,
90    #[serde(default = "default_true")]
91    pub check_validated: bool,
92    #[serde(default)]
93    pub check_no_inherit: bool,
94    /// Whether this relation declares its column CHECK locally, independently from inherited copies. Missing legacy origin retains the historical local projection.
95    #[serde(default = "default_true", skip_serializing_if = "is_true")]
96    pub check_is_local: bool,
97    /// Durable identity of the column CHECK, preserved across constraint and relation renames.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub check_object_id: Option<[u8; 16]>,
100    /// Public CHECK address allocated separately from its durable incarnation.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub check_catalog_oid: Option<i64>,
103    /// Column-level `REFERENCES parent[(col)]` foreign key. An omitted column is resolved to the referenced primary key before publication.
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub references: Option<ForeignKeyRef>,
106}
107
108impl ColumnDef {
109    /// A nullable column without a default, generation expression, identity or constraint.
110    #[must_use]
111    pub fn nullable(name: impl Into<String>, ty: ColumnType) -> Self {
112        Self {
113            name: name.into(),
114            ty,
115            collation_oid: None,
116            object_id: None,
117            attribute_number: None,
118            missing_value: None,
119            primary_key: false,
120            not_null: false,
121            not_null_explicit: false,
122            not_null_name: None,
123            not_null_identity: None,
124            not_null_validated: true,
125            not_null_no_inherit: false,
126            not_null_is_local: true,
127            auto_increment: None,
128            unique: false,
129            default: None,
130            generated: None,
131            check: None,
132            check_name: None,
133            check_enforced: true,
134            check_validated: true,
135            check_no_inherit: false,
136            check_is_local: true,
137            check_object_id: None,
138            check_catalog_oid: None,
139            default_catalog_oid: None,
140            references: None,
141        }
142    }
143}
144
145pub use uqa_core::catalog_identity::CatalogObjectIdentity as ConstraintCatalogIdentity;
146
147/// `REFERENCES table[(column)]` reference target.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149#[allow(clippy::struct_excessive_bools)]
150pub struct ForeignKeyRef {
151    /// Incarnation of the selected unique index; names are retained only for diagnostics and legacy conversion.
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    pub referenced_index: Option<[u8; 16]>,
154    #[serde(default, skip_serializing_if = "Option::is_none")]
155    pub referenced_key: Option<String>,
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub name: Option<String>,
158    /// Logical foreign-key identity shared by a partition family for enforcement and deferred events.
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub object_id: Option<[u8; 16]>,
161    /// Independent catalog row lifetime and OID, preserved through renames and distinct in each partition.
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub catalog_identity: Option<ConstraintCatalogIdentity>,
164    pub table: String,
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub column: Option<String>,
167    #[serde(default)]
168    pub on_update: ForeignKeyAction,
169    #[serde(default)]
170    pub on_delete: ForeignKeyAction,
171    #[serde(default)]
172    pub match_type: ForeignKeyMatch,
173    #[serde(default = "default_true")]
174    pub enforced: bool,
175    #[serde(default = "default_true")]
176    pub validated: bool,
177    #[serde(default)]
178    pub deferrable: bool,
179    #[serde(default)]
180    pub initially_deferred: bool,
181    /// `REFERENCES table (..., PERIOD column)` temporal coverage semantics.
182    #[serde(default)]
183    pub period: bool,
184    /// The constraints derived on the partitions of a partitioned referenced table, as [`ForeignKey::referenced_partitions`] holds them.
185    #[serde(default, skip_serializing_if = "Vec::is_empty")]
186    pub referenced_partitions: Vec<super::ReferencedPartitionConstraint>,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct CreateTable {
191    pub name: String,
192    /// Local SQL relation identifier used while binding expressions declared inside the table definition.
193    pub qualifier: String,
194    pub columns: Vec<ColumnDef>,
195    /// `CREATE TABLE IF NOT EXISTS` - silently ignore the statement
196    /// when a table with this name already exists.
197    pub if_not_exists: bool,
198    /// Table-level `CHECK (...)` constraints. Each entry is an
199    /// expression that must evaluate truthy against every row.
200    #[allow(dead_code)]
201    pub checks: Vec<TableCheck>,
202    /// Table-level `FOREIGN KEY (col, ...) REFERENCES parent(col, ...)`.
203    pub foreign_keys: Vec<ForeignKey>,
204    /// Foreign keys in the statement's written order, before inheritance adds any constraints. Only the statement carries this order.
205    #[serde(default, skip_serializing_if = "Vec::is_empty")]
206    pub foreign_key_order: Vec<DeclaredForeignKey>,
207    /// Every declared `PRIMARY KEY` / `UNIQUE` constraint, including
208    /// column-level declarations. Keeping the typed key (rather than only
209    /// setting per-column flags) preserves composite-key and `NULLS NOT
210    /// DISTINCT` semantics through planning and catalog persistence.
211    #[serde(default)]
212    pub key_constraints: Vec<TableKeyConstraint>,
213    /// `PostgreSQL` relation persistence selected by `TEMPORARY` or `UNLOGGED`.
214    #[serde(default)]
215    pub persistence: RelationPersistence,
216    /// Transaction-end behavior for temporary tables.
217    #[serde(default)]
218    pub on_commit: OnCommitAction,
219    /// Direct inheritance and declarative-partitioning metadata. The engine
220    /// resolves parent names and merges their row types atomically at create
221    /// time, then persists the canonical hierarchy with the table schema.
222    #[serde(default)]
223    pub hierarchy: TableHierarchy,
224    /// Each CHECK the statement declares, in written order, which `DefineRelation` adds in that order. Only the statement carries it.
225    #[serde(default, skip_serializing_if = "Vec::is_empty")]
226    pub check_order: Vec<DeclaredCheck>,
227    /// The columns, NOT NULL table constraints and table PRIMARY KEY constraints the statement declares, in written order, as `transformCreateStmt` examines them. Only the statement carries it.
228    #[serde(default, skip_serializing_if = "Vec::is_empty")]
229    pub element_order: Vec<DeclaredElement>,
230    /// The NOT NULL constraints the statement declares, in the order `AddRelationNotNullConstraints` takes them once the relation's columns exist: each column's clause or implied constraint and each table constraint in written order, then the ones a table PRIMARY KEY adds. Only the statement carries it.
231    #[serde(default, skip_serializing_if = "Vec::is_empty")]
232    pub not_null_declarations: Vec<NotNullDeclaration>,
233    /// The columns a `PARTITION OF` statement declares without a type, which are options on the parent's columns and take their types when the columns merge. Only the statement carries it.
234    #[serde(default, skip_serializing_if = "Vec::is_empty")]
235    pub untyped_columns: Vec<String>,
236}
237
238/// An element of a CREATE TABLE statement that `transformCreateStmt` examines in written order.
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240pub enum DeclaredElement {
241    /// The statement's next column, with the clauses it writes.
242    Column(ColumnDeclaration),
243    /// A table constraint `NOT NULL column`.
244    NotNull(NotNullDeclaration),
245    /// A table PRIMARY KEY constraint, whose columns `transformIndexConstraints` makes NOT NULL after every element is examined.
246    PrimaryKey { columns: Vec<String> },
247    /// A table PRIMARY KEY or UNIQUE constraint that is DEFERRABLE or INITIALLY DEFERRED.
248    DeferrableKey,
249}
250
251/// A foreign key written as a column clause or a table constraint.
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
253pub enum DeclaredForeignKey {
254    Column(String),
255    Table(usize),
256}
257
258/// A NOT NULL constraint a CREATE TABLE statement declares or implies, which `AddRelationNotNullConstraints` resolves against the relation's columns once they exist.
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
260pub struct NotNullDeclaration {
261    pub column: String,
262    pub name: Option<String>,
263    pub no_inherit: bool,
264    /// Whether the statement wrote the constraint, as a column clause or a table constraint, rather than implying it with a PRIMARY KEY, SERIAL or identity.
265    pub explicit: bool,
266}
267
268/// The clauses a column definition writes, in written order, which `transformColumnDefinition` checks against each other.
269#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
270pub struct ColumnDeclaration {
271    /// The written name, resolved before column constraints are examined.
272    #[serde(default, skip_serializing_if = "Option::is_none")]
273    pub collation: Option<String>,
274    /// The column's type is SERIAL, SMALLSERIAL or BIGSERIAL, which adds a default after the written clauses.
275    pub serial: bool,
276    /// The column's type is an array of a SERIAL type.
277    pub serial_array: bool,
278    pub clauses: Vec<ColumnClause>,
279}
280
281/// One clause of a column definition.
282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
283pub struct ColumnClause {
284    pub kind: ColumnClauseKind,
285    /// The name a `CONSTRAINT` clause gives it.
286    pub name: Option<String>,
287    pub no_inherit: bool,
288}
289
290/// The kind of a column definition's clause.
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
292pub enum ColumnClauseKind {
293    Null,
294    NotNull,
295    Default,
296    Identity,
297    Generated,
298    Check,
299    PrimaryKey,
300    Unique,
301    ForeignKey,
302    Deferrable,
303    NotDeferrable,
304    InitiallyDeferred,
305    InitiallyImmediate,
306    Enforced,
307    NotEnforced,
308}
309
310/// Where a CHECK that a CREATE TABLE statement declares is held.
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
312pub enum DeclaredCheck {
313    /// The CHECK the named column holds as its own.
314    Column(String),
315    /// The entry at this position among the statement's own entries of `CreateTable::checks`.
316    Table(usize),
317}
318
319/// A syntactically valid `CREATE TABLE IF NOT EXISTS` whose definition must be analyzed only after execution has established that the target relation does not already exist.
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
321pub struct DeferredCreateTable {
322    pub name: String,
323    pub persistence: RelationPersistence,
324    pub definition_sql: String,
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
328pub enum TableKeyConstraintKind {
329    PrimaryKey,
330    Unique,
331}
332
333impl TableKeyConstraintKind {
334    /// The constraint type as SQL spells it, which `PostgreSQL` diagnostics name.
335    #[must_use]
336    pub const fn sql_label(self) -> &'static str {
337        match self {
338            Self::PrimaryKey => "PRIMARY KEY",
339            Self::Unique => "UNIQUE",
340        }
341    }
342}
343
344/// A table key whose columns are compared as one tuple.
345#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
346pub struct TableKeyConstraint {
347    /// Independent catalog row lifetime, retained while the owning index changes its name.
348    #[serde(default, skip_serializing_if = "Option::is_none")]
349    pub catalog_identity: Option<ConstraintCatalogIdentity>,
350    /// The identity of the index that enforces the key (`conindid`), reserved before the constraint's own as `index_create` creates the index before `index_constraint_create`. Keys created before it was recorded bind their index when it is registered.
351    #[serde(default, skip_serializing_if = "Option::is_none")]
352    pub index_identity: Option<ConstraintCatalogIdentity>,
353    pub name: Option<String>,
354    pub kind: TableKeyConstraintKind,
355    pub columns: Vec<String>,
356    /// Columns that the supporting index carries beside the key, in declaration order. They take no part in uniqueness, and `PostgreSQL` lets them repeat and name key columns.
357    #[serde(default, skip_serializing_if = "Vec::is_empty")]
358    pub included_columns: Vec<String>,
359    /// `PostgreSQL` UNIQUE keys normally treat every NULL-containing tuple as
360    /// distinct. `UNIQUE NULLS NOT DISTINCT` opts into NULL equality.
361    #[serde(default)]
362    pub nulls_not_distinct: bool,
363    /// The final key column is a range or multirange compared by overlap.
364    #[serde(default)]
365    pub without_overlaps: bool,
366}
367
368/// Durable table-level constraints that do not fit in `ColumnDef`.
369///
370/// `serde(default)` on the catalog field containing this structure keeps
371/// databases written before constraint persistence backward compatible.
372#[derive(Debug, Clone, Default, Serialize, Deserialize)]
373pub struct TableConstraintSet {
374    /// Physical attribute metadata retained after a column is removed from the live schema.
375    #[serde(default, skip_serializing_if = "Vec::is_empty")]
376    pub dropped_attributes: Vec<crate::catalog::relation_attributes::DroppedAttribute>,
377    /// Distinguish a declared zero-column SQL relation from a schema-free document table. Missing legacy metadata retains inference from existing columns.
378    #[serde(default, skip_serializing_if = "Option::is_none")]
379    pub columns_declared: Option<bool>,
380    #[serde(default)]
381    pub checks: Vec<TableCheck>,
382    #[serde(default)]
383    pub foreign_keys: Vec<ForeignKey>,
384    #[serde(default)]
385    pub key_constraints: Vec<TableKeyConstraint>,
386    /// Stored alongside the table definition so reopen preserves `pg_class.relpersistence` for unlogged tables.
387    #[serde(default)]
388    pub persistence: RelationPersistence,
389    /// Permanent and unlogged tables always use the default. Temporary tables are session-local and therefore never write this field to disk.
390    #[serde(default)]
391    pub on_commit: OnCommitAction,
392    /// Durable relation hierarchy and partition-bound metadata.
393    #[serde(default)]
394    pub hierarchy: TableHierarchy,
395    /// The relation's public OIDs, allocated when it was created. Tables created before OIDs were recorded derive them from their identity.
396    #[serde(default, skip_serializing_if = "Option::is_none")]
397    pub catalog_oids: Option<crate::catalog::relation_oids::RelationCatalogOids>,
398    /// The generated row-array type name, which can move independently when an explicit type takes its name.
399    #[serde(default, skip_serializing_if = "Option::is_none")]
400    pub row_type_array_name: Option<String>,
401}
402
403/// `CHECK (expr)` constraint with an optional name (`CONSTRAINT <name>
404/// CHECK (...)`).
405#[derive(Debug, Clone, Serialize, Deserialize)]
406#[expect(
407    clippy::struct_excessive_bools,
408    reason = "CHECK catalog flags are independent PostgreSQL properties"
409)]
410pub struct TableCheck {
411    pub name: Option<String>,
412    /// Durable identity of this CHECK, assigned when its definition is published.
413    #[serde(default, skip_serializing_if = "Option::is_none")]
414    pub object_id: Option<[u8; 16]>,
415    #[serde(default, skip_serializing_if = "Option::is_none")]
416    pub catalog_oid: Option<i64>,
417    /// Whether this relation declares this CHECK locally, independently from inherited copies. Missing legacy origin retains the historical local projection.
418    #[serde(default = "default_true", skip_serializing_if = "is_true")]
419    pub is_local: bool,
420    pub expr: Expr,
421    #[serde(default = "default_true")]
422    pub enforced: bool,
423    #[serde(default = "default_true")]
424    pub validated: bool,
425    #[serde(default)]
426    pub no_inherit: bool,
427    /// Runtime form of the bound CHECK retained after `DETACH PARTITION ... CONCURRENTLY`.
428    #[serde(default, skip_serializing_if = "Option::is_none")]
429    pub partition_constraint: Option<DetachedPartitionConstraint>,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct DetachedPartitionConstraint {
434    pub spec: PartitionSpec,
435    pub bound: PartitionBound,
436}
437
438/// Table-level foreign key. Compilation preserves an omitted referenced column list as empty; validation fills it from the primary key before publication.
439#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
440#[allow(clippy::struct_excessive_bools)]
441pub struct ForeignKey {
442    /// Incarnation of the selected unique index, retained across index and constraint renames.
443    #[serde(default, skip_serializing_if = "Option::is_none")]
444    pub referenced_index: Option<[u8; 16]>,
445    /// Name of the selected unique index in the referenced relation namespace.
446    #[serde(default, skip_serializing_if = "Option::is_none")]
447    pub referenced_key: Option<String>,
448    pub name: Option<String>,
449    /// Logical foreign-key identity shared by a partition family for enforcement and deferred events.
450    #[serde(default, skip_serializing_if = "Option::is_none")]
451    pub object_id: Option<[u8; 16]>,
452    /// Independent catalog row lifetime and OID, preserved through renames and distinct in each partition.
453    #[serde(default, skip_serializing_if = "Option::is_none")]
454    pub catalog_identity: Option<ConstraintCatalogIdentity>,
455    pub local_columns: Vec<String>,
456    pub ref_table: String,
457    pub ref_columns: Vec<String>,
458    #[serde(default)]
459    pub on_update: ForeignKeyAction,
460    #[serde(default)]
461    pub on_delete: ForeignKeyAction,
462    /// Optional column subset for `ON DELETE SET NULL (...)` and
463    /// `ON DELETE SET DEFAULT (...)`. Empty means every local FK
464    /// column participates.
465    #[serde(default)]
466    pub on_delete_set_columns: Vec<String>,
467    #[serde(default)]
468    pub match_type: ForeignKeyMatch,
469    #[serde(default = "default_true")]
470    pub enforced: bool,
471    #[serde(default = "default_true")]
472    pub validated: bool,
473    #[serde(default)]
474    pub deferrable: bool,
475    #[serde(default)]
476    pub initially_deferred: bool,
477    /// The final local and referenced columns use `PostgreSQL` PERIOD coverage.
478    #[serde(default)]
479    pub period: bool,
480    /// The constraints derived on the partitions of a partitioned referenced table, parents before their partitions, in the order `PostgreSQL` creates them. Only the foreign key a referencing relation declares holds them; the copies on its partitions hold none.
481    #[serde(default, skip_serializing_if = "Vec::is_empty")]
482    pub referenced_partitions: Vec<super::ReferencedPartitionConstraint>,
483}
484
485const fn default_true() -> bool {
486    true
487}
488
489#[expect(
490    clippy::trivially_copy_pass_by_ref,
491    reason = "serde skip_serializing_if requires a borrowed field"
492)]
493const fn is_true(value: &bool) -> bool {
494    *value
495}
496
497#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
498pub enum ForeignKeyAction {
499    #[default]
500    NoAction,
501    Restrict,
502    Cascade,
503    SetNull,
504    SetDefault,
505}
506
507#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
508pub enum ForeignKeyMatch {
509    #[default]
510    Simple,
511    Full,
512}