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    /// Value exposed for physical rows captured before this column was added. This is the catalog equivalent of `PostgreSQL`'s `attmissingval`.
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub missing_value: Option<uqa_core::Value>,
27    pub primary_key: bool,
28    pub not_null: bool,
29    /// Whether `NOT NULL` was declared as its own constraint instead of being
30    /// implied by `PRIMARY KEY` or an auto-incrementing identity.
31    #[serde(default)]
32    pub not_null_explicit: bool,
33    /// Durable `PostgreSQL` 18 `NOT NULL` constraint name. Parsing leaves an
34    /// unnamed declaration as `None`; table registration assigns and persists
35    /// `PostgreSQL`'s generated name before the constraint becomes visible.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub not_null_name: Option<String>,
38    /// Independent NOT NULL lifetime and public catalog OID, retained through column, relation, and constraint renames.
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub not_null_identity: Option<ConstraintCatalogIdentity>,
41    /// Whether the named `NOT NULL` constraint has been validated against
42    /// every pre-existing row. `NOT VALID` still enforces future writes.
43    #[serde(default = "default_true")]
44    pub not_null_validated: bool,
45    /// Durable `NO INHERIT` state for `PostgreSQL` 18 named `NOT NULL`
46    /// constraints.
47    #[serde(default)]
48    pub not_null_no_inherit: bool,
49    /// Whether this relation declares its NOT NULL constraint locally, independently from inherited parent constraints. Older serialized definitions retain their original local catalog projection.
50    #[serde(default = "default_true", skip_serializing_if = "is_true")]
51    pub not_null_is_local: bool,
52    /// 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.
53    #[serde(
54        default,
55        deserialize_with = "deserialize_auto_increment",
56        skip_serializing_if = "Option::is_none"
57    )]
58    pub auto_increment: Option<AutoIncrement>,
59    /// `UNIQUE` column constraint -- the engine rejects an INSERT
60    /// whose value for this column already exists in another row.
61    #[serde(default)]
62    pub unique: bool,
63    /// `DEFAULT <expr>`. Evaluated at INSERT time when the column is
64    /// not present in the row tuple. Persisted in catalog metadata so
65    /// reopened engines keep the same INSERT semantics.
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub default: Option<Expr>,
68    /// `PostgreSQL` 18 generated-column definition. Stored values are refreshed
69    /// on every row write; virtual values are evaluated from the physical row
70    /// only when a logical row is read.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub generated: Option<GeneratedColumn>,
73    /// `CHECK (<expr>)` column-level constraint. Evaluated at INSERT
74    /// (and UPDATE-replace) time against the row being written.
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub check: Option<Expr>,
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub check_name: Option<String>,
79    #[serde(default = "default_true")]
80    pub check_enforced: bool,
81    #[serde(default = "default_true")]
82    pub check_validated: bool,
83    #[serde(default)]
84    pub check_no_inherit: bool,
85    /// Whether this relation declares its column CHECK locally, independently from inherited copies. Missing legacy origin retains the historical local projection.
86    #[serde(default = "default_true", skip_serializing_if = "is_true")]
87    pub check_is_local: bool,
88    /// Durable identity of the column CHECK, preserved across constraint and relation renames.
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub check_object_id: Option<[u8; 16]>,
91    /// Public CHECK address allocated separately from its durable incarnation.
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub check_catalog_oid: Option<i64>,
94    /// Column-level `REFERENCES parent[(col)]` foreign key. An omitted column is resolved to the referenced primary key before publication.
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub references: Option<ForeignKeyRef>,
97}
98
99pub use uqa_core::catalog_identity::CatalogObjectIdentity as ConstraintCatalogIdentity;
100
101/// `REFERENCES table[(column)]` reference target.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[allow(clippy::struct_excessive_bools)]
104pub struct ForeignKeyRef {
105    /// Incarnation of the selected unique index; names are retained only for diagnostics and legacy conversion.
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub referenced_index: Option<[u8; 16]>,
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub referenced_key: Option<String>,
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub name: Option<String>,
112    /// Logical foreign-key identity shared by a partition family for enforcement and deferred events.
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub object_id: Option<[u8; 16]>,
115    /// Independent catalog row lifetime and OID, preserved through renames and distinct in each partition.
116    #[serde(default, skip_serializing_if = "Option::is_none")]
117    pub catalog_identity: Option<ConstraintCatalogIdentity>,
118    pub table: String,
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub column: Option<String>,
121    #[serde(default)]
122    pub on_update: ForeignKeyAction,
123    #[serde(default)]
124    pub on_delete: ForeignKeyAction,
125    #[serde(default)]
126    pub match_type: ForeignKeyMatch,
127    #[serde(default = "default_true")]
128    pub enforced: bool,
129    #[serde(default = "default_true")]
130    pub validated: bool,
131    #[serde(default)]
132    pub deferrable: bool,
133    #[serde(default)]
134    pub initially_deferred: bool,
135    /// `REFERENCES table (..., PERIOD column)` temporal coverage semantics.
136    #[serde(default)]
137    pub period: bool,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct CreateTable {
142    pub name: String,
143    /// Local SQL relation identifier used while binding expressions declared inside the table definition.
144    pub qualifier: String,
145    pub columns: Vec<ColumnDef>,
146    /// `CREATE TABLE IF NOT EXISTS` - silently ignore the statement
147    /// when a table with this name already exists.
148    pub if_not_exists: bool,
149    /// Table-level `CHECK (...)` constraints. Each entry is an
150    /// expression that must evaluate truthy against every row.
151    #[allow(dead_code)]
152    pub checks: Vec<TableCheck>,
153    /// Table-level `FOREIGN KEY (col, ...) REFERENCES parent(col, ...)`.
154    pub foreign_keys: Vec<ForeignKey>,
155    /// Every declared `PRIMARY KEY` / `UNIQUE` constraint, including
156    /// column-level declarations. Keeping the typed key (rather than only
157    /// setting per-column flags) preserves composite-key and `NULLS NOT
158    /// DISTINCT` semantics through planning and catalog persistence.
159    #[serde(default)]
160    pub key_constraints: Vec<TableKeyConstraint>,
161    /// `PostgreSQL` relation persistence selected by `TEMPORARY` or `UNLOGGED`.
162    #[serde(default)]
163    pub persistence: RelationPersistence,
164    /// Transaction-end behavior for temporary tables.
165    #[serde(default)]
166    pub on_commit: OnCommitAction,
167    /// Direct inheritance and declarative-partitioning metadata. The engine
168    /// resolves parent names and merges their row types atomically at create
169    /// time, then persists the canonical hierarchy with the table schema.
170    #[serde(default)]
171    pub hierarchy: TableHierarchy,
172}
173
174/// 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.
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176pub struct DeferredCreateTable {
177    pub name: String,
178    pub persistence: RelationPersistence,
179    pub definition_sql: String,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
183pub enum TableKeyConstraintKind {
184    PrimaryKey,
185    Unique,
186}
187
188/// A table key whose columns are compared as one tuple.
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190pub struct TableKeyConstraint {
191    /// Independent catalog row lifetime, retained while the owning index changes its name.
192    #[serde(default, skip_serializing_if = "Option::is_none")]
193    pub catalog_identity: Option<ConstraintCatalogIdentity>,
194    pub name: Option<String>,
195    pub kind: TableKeyConstraintKind,
196    pub columns: Vec<String>,
197    /// 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.
198    #[serde(default, skip_serializing_if = "Vec::is_empty")]
199    pub included_columns: Vec<String>,
200    /// `PostgreSQL` UNIQUE keys normally treat every NULL-containing tuple as
201    /// distinct. `UNIQUE NULLS NOT DISTINCT` opts into NULL equality.
202    #[serde(default)]
203    pub nulls_not_distinct: bool,
204    /// The final key column is a range or multirange compared by overlap.
205    #[serde(default)]
206    pub without_overlaps: bool,
207}
208
209/// Durable table-level constraints that do not fit in `ColumnDef`.
210///
211/// `serde(default)` on the catalog field containing this structure keeps
212/// databases written before constraint persistence backward compatible.
213#[derive(Debug, Clone, Default, Serialize, Deserialize)]
214pub struct TableConstraintSet {
215    /// Distinguish a declared zero-column SQL relation from a schema-free document table. Missing legacy metadata retains inference from existing columns.
216    #[serde(default, skip_serializing_if = "Option::is_none")]
217    pub columns_declared: Option<bool>,
218    #[serde(default)]
219    pub checks: Vec<TableCheck>,
220    #[serde(default)]
221    pub foreign_keys: Vec<ForeignKey>,
222    #[serde(default)]
223    pub key_constraints: Vec<TableKeyConstraint>,
224    /// Stored alongside the table definition so reopen preserves `pg_class.relpersistence` for unlogged tables.
225    #[serde(default)]
226    pub persistence: RelationPersistence,
227    /// Permanent and unlogged tables always use the default. Temporary tables are session-local and therefore never write this field to disk.
228    #[serde(default)]
229    pub on_commit: OnCommitAction,
230    /// Durable relation hierarchy and partition-bound metadata.
231    #[serde(default)]
232    pub hierarchy: TableHierarchy,
233}
234
235/// `CHECK (expr)` constraint with an optional name (`CONSTRAINT <name>
236/// CHECK (...)`).
237#[derive(Debug, Clone, Serialize, Deserialize)]
238#[expect(
239    clippy::struct_excessive_bools,
240    reason = "CHECK catalog flags are independent PostgreSQL properties"
241)]
242pub struct TableCheck {
243    pub name: Option<String>,
244    /// Durable identity of this CHECK, assigned when its definition is published.
245    #[serde(default, skip_serializing_if = "Option::is_none")]
246    pub object_id: Option<[u8; 16]>,
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub catalog_oid: Option<i64>,
249    /// Whether this relation declares this CHECK locally, independently from inherited copies. Missing legacy origin retains the historical local projection.
250    #[serde(default = "default_true", skip_serializing_if = "is_true")]
251    pub is_local: bool,
252    pub expr: Expr,
253    #[serde(default = "default_true")]
254    pub enforced: bool,
255    #[serde(default = "default_true")]
256    pub validated: bool,
257    #[serde(default)]
258    pub no_inherit: bool,
259    /// Runtime form of the bound CHECK retained after `DETACH PARTITION ... CONCURRENTLY`.
260    #[serde(default, skip_serializing_if = "Option::is_none")]
261    pub partition_constraint: Option<DetachedPartitionConstraint>,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct DetachedPartitionConstraint {
266    pub spec: PartitionSpec,
267    pub bound: PartitionBound,
268}
269
270/// Table-level foreign key. Compilation preserves an omitted referenced column list as empty; validation fills it from the primary key before publication.
271#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
272#[allow(clippy::struct_excessive_bools)]
273pub struct ForeignKey {
274    /// Incarnation of the selected unique index, retained across index and constraint renames.
275    #[serde(default, skip_serializing_if = "Option::is_none")]
276    pub referenced_index: Option<[u8; 16]>,
277    /// Name of the selected unique index in the referenced relation namespace.
278    #[serde(default, skip_serializing_if = "Option::is_none")]
279    pub referenced_key: Option<String>,
280    pub name: Option<String>,
281    /// Logical foreign-key identity shared by a partition family for enforcement and deferred events.
282    #[serde(default, skip_serializing_if = "Option::is_none")]
283    pub object_id: Option<[u8; 16]>,
284    /// Independent catalog row lifetime and OID, preserved through renames and distinct in each partition.
285    #[serde(default, skip_serializing_if = "Option::is_none")]
286    pub catalog_identity: Option<ConstraintCatalogIdentity>,
287    pub local_columns: Vec<String>,
288    pub ref_table: String,
289    pub ref_columns: Vec<String>,
290    #[serde(default)]
291    pub on_update: ForeignKeyAction,
292    #[serde(default)]
293    pub on_delete: ForeignKeyAction,
294    /// Optional column subset for `ON DELETE SET NULL (...)` and
295    /// `ON DELETE SET DEFAULT (...)`. Empty means every local FK
296    /// column participates.
297    #[serde(default)]
298    pub on_delete_set_columns: Vec<String>,
299    #[serde(default)]
300    pub match_type: ForeignKeyMatch,
301    #[serde(default = "default_true")]
302    pub enforced: bool,
303    #[serde(default = "default_true")]
304    pub validated: bool,
305    #[serde(default)]
306    pub deferrable: bool,
307    #[serde(default)]
308    pub initially_deferred: bool,
309    /// The final local and referenced columns use `PostgreSQL` PERIOD coverage.
310    #[serde(default)]
311    pub period: bool,
312}
313
314const fn default_true() -> bool {
315    true
316}
317
318#[expect(
319    clippy::trivially_copy_pass_by_ref,
320    reason = "serde skip_serializing_if requires a borrowed field"
321)]
322const fn is_true(value: &bool) -> bool {
323    *value
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
327pub enum ForeignKeyAction {
328    #[default]
329    NoAction,
330    Restrict,
331    Cascade,
332    SetNull,
333    SetDefault,
334}
335
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
337pub enum ForeignKeyMatch {
338    #[default]
339    Simple,
340    Full,
341}