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    #[serde(default)]
197    pub checks: Vec<TableCheck>,
198    #[serde(default)]
199    pub foreign_keys: Vec<ForeignKey>,
200    #[serde(default)]
201    pub key_constraints: Vec<TableKeyConstraint>,
202    /// Stored alongside the table definition so reopen preserves `pg_class.relpersistence` for unlogged tables.
203    #[serde(default)]
204    pub persistence: RelationPersistence,
205    /// Permanent and unlogged tables always use the default. Temporary tables are session-local and therefore never write this field to disk.
206    #[serde(default)]
207    pub on_commit: OnCommitAction,
208    /// Durable relation hierarchy and partition-bound metadata.
209    #[serde(default)]
210    pub hierarchy: TableHierarchy,
211}
212
213/// `CHECK (expr)` constraint with an optional name (`CONSTRAINT <name>
214/// CHECK (...)`).
215#[derive(Debug, Clone, Serialize, Deserialize)]
216#[expect(
217    clippy::struct_excessive_bools,
218    reason = "CHECK catalog flags are independent PostgreSQL properties"
219)]
220pub struct TableCheck {
221    pub name: Option<String>,
222    /// Durable identity of this CHECK, assigned when its definition is published.
223    #[serde(default, skip_serializing_if = "Option::is_none")]
224    pub object_id: Option<[u8; 16]>,
225    /// Whether this relation declares this CHECK locally, independently from inherited copies. Missing legacy origin retains the historical local projection.
226    #[serde(default = "default_true", skip_serializing_if = "is_true")]
227    pub is_local: bool,
228    pub expr: Expr,
229    #[serde(default = "default_true")]
230    pub enforced: bool,
231    #[serde(default = "default_true")]
232    pub validated: bool,
233    #[serde(default)]
234    pub no_inherit: bool,
235    /// Runtime form of the bound CHECK retained after `DETACH PARTITION ... CONCURRENTLY`.
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    pub partition_constraint: Option<DetachedPartitionConstraint>,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct DetachedPartitionConstraint {
242    pub spec: PartitionSpec,
243    pub bound: PartitionBound,
244}
245
246/// Table-level foreign key. Compilation preserves an omitted referenced column list as empty; validation fills it from the primary key before publication.
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
248#[allow(clippy::struct_excessive_bools)]
249pub struct ForeignKey {
250    /// Name of the selected unique index in the referenced relation namespace.
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub referenced_key: Option<String>,
253    pub name: Option<String>,
254    /// Durable identity of the catalog constraint object. The engine assigns
255    /// this when the constraint is published; parsed SQL leaves it unset.
256    #[serde(default, skip_serializing_if = "Option::is_none")]
257    pub object_id: Option<[u8; 16]>,
258    pub local_columns: Vec<String>,
259    pub ref_table: String,
260    pub ref_columns: Vec<String>,
261    #[serde(default)]
262    pub on_update: ForeignKeyAction,
263    #[serde(default)]
264    pub on_delete: ForeignKeyAction,
265    /// Optional column subset for `ON DELETE SET NULL (...)` and
266    /// `ON DELETE SET DEFAULT (...)`. Empty means every local FK
267    /// column participates.
268    #[serde(default)]
269    pub on_delete_set_columns: Vec<String>,
270    #[serde(default)]
271    pub match_type: ForeignKeyMatch,
272    #[serde(default = "default_true")]
273    pub enforced: bool,
274    #[serde(default = "default_true")]
275    pub validated: bool,
276    #[serde(default)]
277    pub deferrable: bool,
278    #[serde(default)]
279    pub initially_deferred: bool,
280    /// The final local and referenced columns use `PostgreSQL` PERIOD coverage.
281    #[serde(default)]
282    pub period: bool,
283}
284
285const fn default_true() -> bool {
286    true
287}
288
289#[expect(
290    clippy::trivially_copy_pass_by_ref,
291    reason = "serde skip_serializing_if requires a borrowed field"
292)]
293const fn is_true(value: &bool) -> bool {
294    *value
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
298pub enum ForeignKeyAction {
299    #[default]
300    NoAction,
301    Restrict,
302    Cascade,
303    SetNull,
304    SetDefault,
305}
306
307#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
308pub enum ForeignKeyMatch {
309    #[default]
310    Simple,
311    Full,
312}