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