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