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    /// `PostgreSQL` UNIQUE keys normally treat every NULL-containing tuple as
198    /// distinct. `UNIQUE NULLS NOT DISTINCT` opts into NULL equality.
199    #[serde(default)]
200    pub nulls_not_distinct: bool,
201    /// The final key column is a range or multirange compared by overlap.
202    #[serde(default)]
203    pub without_overlaps: bool,
204}
205
206/// Durable table-level constraints that do not fit in `ColumnDef`.
207///
208/// `serde(default)` on the catalog field containing this structure keeps
209/// databases written before constraint persistence backward compatible.
210#[derive(Debug, Clone, Default, Serialize, Deserialize)]
211pub struct TableConstraintSet {
212    /// Distinguish a declared zero-column SQL relation from a schema-free document table. Missing legacy metadata retains inference from existing columns.
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub columns_declared: Option<bool>,
215    #[serde(default)]
216    pub checks: Vec<TableCheck>,
217    #[serde(default)]
218    pub foreign_keys: Vec<ForeignKey>,
219    #[serde(default)]
220    pub key_constraints: Vec<TableKeyConstraint>,
221    /// Stored alongside the table definition so reopen preserves `pg_class.relpersistence` for unlogged tables.
222    #[serde(default)]
223    pub persistence: RelationPersistence,
224    /// Permanent and unlogged tables always use the default. Temporary tables are session-local and therefore never write this field to disk.
225    #[serde(default)]
226    pub on_commit: OnCommitAction,
227    /// Durable relation hierarchy and partition-bound metadata.
228    #[serde(default)]
229    pub hierarchy: TableHierarchy,
230}
231
232/// `CHECK (expr)` constraint with an optional name (`CONSTRAINT <name>
233/// CHECK (...)`).
234#[derive(Debug, Clone, Serialize, Deserialize)]
235#[expect(
236    clippy::struct_excessive_bools,
237    reason = "CHECK catalog flags are independent PostgreSQL properties"
238)]
239pub struct TableCheck {
240    pub name: Option<String>,
241    /// Durable identity of this CHECK, assigned when its definition is published.
242    #[serde(default, skip_serializing_if = "Option::is_none")]
243    pub object_id: Option<[u8; 16]>,
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub catalog_oid: Option<i64>,
246    /// Whether this relation declares this CHECK locally, independently from inherited copies. Missing legacy origin retains the historical local projection.
247    #[serde(default = "default_true", skip_serializing_if = "is_true")]
248    pub is_local: bool,
249    pub expr: Expr,
250    #[serde(default = "default_true")]
251    pub enforced: bool,
252    #[serde(default = "default_true")]
253    pub validated: bool,
254    #[serde(default)]
255    pub no_inherit: bool,
256    /// Runtime form of the bound CHECK retained after `DETACH PARTITION ... CONCURRENTLY`.
257    #[serde(default, skip_serializing_if = "Option::is_none")]
258    pub partition_constraint: Option<DetachedPartitionConstraint>,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct DetachedPartitionConstraint {
263    pub spec: PartitionSpec,
264    pub bound: PartitionBound,
265}
266
267/// Table-level foreign key. Compilation preserves an omitted referenced column list as empty; validation fills it from the primary key before publication.
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
269#[allow(clippy::struct_excessive_bools)]
270pub struct ForeignKey {
271    /// Incarnation of the selected unique index, retained across index and constraint renames.
272    #[serde(default, skip_serializing_if = "Option::is_none")]
273    pub referenced_index: Option<[u8; 16]>,
274    /// Name of the selected unique index in the referenced relation namespace.
275    #[serde(default, skip_serializing_if = "Option::is_none")]
276    pub referenced_key: Option<String>,
277    pub name: Option<String>,
278    /// Logical foreign-key identity shared by a partition family for enforcement and deferred events.
279    #[serde(default, skip_serializing_if = "Option::is_none")]
280    pub object_id: Option<[u8; 16]>,
281    /// Independent catalog row lifetime and OID, preserved through renames and distinct in each partition.
282    #[serde(default, skip_serializing_if = "Option::is_none")]
283    pub catalog_identity: Option<ConstraintCatalogIdentity>,
284    pub local_columns: Vec<String>,
285    pub ref_table: String,
286    pub ref_columns: Vec<String>,
287    #[serde(default)]
288    pub on_update: ForeignKeyAction,
289    #[serde(default)]
290    pub on_delete: ForeignKeyAction,
291    /// Optional column subset for `ON DELETE SET NULL (...)` and
292    /// `ON DELETE SET DEFAULT (...)`. Empty means every local FK
293    /// column participates.
294    #[serde(default)]
295    pub on_delete_set_columns: Vec<String>,
296    #[serde(default)]
297    pub match_type: ForeignKeyMatch,
298    #[serde(default = "default_true")]
299    pub enforced: bool,
300    #[serde(default = "default_true")]
301    pub validated: bool,
302    #[serde(default)]
303    pub deferrable: bool,
304    #[serde(default)]
305    pub initially_deferred: bool,
306    /// The final local and referenced columns use `PostgreSQL` PERIOD coverage.
307    #[serde(default)]
308    pub period: bool,
309}
310
311const fn default_true() -> bool {
312    true
313}
314
315#[expect(
316    clippy::trivially_copy_pass_by_ref,
317    reason = "serde skip_serializing_if requires a borrowed field"
318)]
319const fn is_true(value: &bool) -> bool {
320    *value
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
324pub enum ForeignKeyAction {
325    #[default]
326    NoAction,
327    Restrict,
328    Cascade,
329    SetNull,
330    SetDefault,
331}
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
334pub enum ForeignKeyMatch {
335    #[default]
336    Simple,
337    Full,
338}