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