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    /// The constraints derived on the partitions of a partitioned referenced table, as [`ForeignKey::referenced_partitions`] holds them.
139    #[serde(default, skip_serializing_if = "Vec::is_empty")]
140    pub referenced_partitions: Vec<super::ReferencedPartitionConstraint>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct CreateTable {
145    pub name: String,
146    /// Local SQL relation identifier used while binding expressions declared inside the table definition.
147    pub qualifier: String,
148    pub columns: Vec<ColumnDef>,
149    /// `CREATE TABLE IF NOT EXISTS` - silently ignore the statement
150    /// when a table with this name already exists.
151    pub if_not_exists: bool,
152    /// Table-level `CHECK (...)` constraints. Each entry is an
153    /// expression that must evaluate truthy against every row.
154    #[allow(dead_code)]
155    pub checks: Vec<TableCheck>,
156    /// Table-level `FOREIGN KEY (col, ...) REFERENCES parent(col, ...)`.
157    pub foreign_keys: Vec<ForeignKey>,
158    /// Every declared `PRIMARY KEY` / `UNIQUE` constraint, including
159    /// column-level declarations. Keeping the typed key (rather than only
160    /// setting per-column flags) preserves composite-key and `NULLS NOT
161    /// DISTINCT` semantics through planning and catalog persistence.
162    #[serde(default)]
163    pub key_constraints: Vec<TableKeyConstraint>,
164    /// `PostgreSQL` relation persistence selected by `TEMPORARY` or `UNLOGGED`.
165    #[serde(default)]
166    pub persistence: RelationPersistence,
167    /// Transaction-end behavior for temporary tables.
168    #[serde(default)]
169    pub on_commit: OnCommitAction,
170    /// Direct inheritance and declarative-partitioning metadata. The engine
171    /// resolves parent names and merges their row types atomically at create
172    /// time, then persists the canonical hierarchy with the table schema.
173    #[serde(default)]
174    pub hierarchy: TableHierarchy,
175}
176
177/// 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.
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179pub struct DeferredCreateTable {
180    pub name: String,
181    pub persistence: RelationPersistence,
182    pub definition_sql: String,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
186pub enum TableKeyConstraintKind {
187    PrimaryKey,
188    Unique,
189}
190
191/// A table key whose columns are compared as one tuple.
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193pub struct TableKeyConstraint {
194    /// Independent catalog row lifetime, retained while the owning index changes its name.
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    pub catalog_identity: Option<ConstraintCatalogIdentity>,
197    pub name: Option<String>,
198    pub kind: TableKeyConstraintKind,
199    pub columns: Vec<String>,
200    /// 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.
201    #[serde(default, skip_serializing_if = "Vec::is_empty")]
202    pub included_columns: Vec<String>,
203    /// `PostgreSQL` UNIQUE keys normally treat every NULL-containing tuple as
204    /// distinct. `UNIQUE NULLS NOT DISTINCT` opts into NULL equality.
205    #[serde(default)]
206    pub nulls_not_distinct: bool,
207    /// The final key column is a range or multirange compared by overlap.
208    #[serde(default)]
209    pub without_overlaps: bool,
210}
211
212/// Durable table-level constraints that do not fit in `ColumnDef`.
213///
214/// `serde(default)` on the catalog field containing this structure keeps
215/// databases written before constraint persistence backward compatible.
216#[derive(Debug, Clone, Default, Serialize, Deserialize)]
217pub struct TableConstraintSet {
218    /// Distinguish a declared zero-column SQL relation from a schema-free document table. Missing legacy metadata retains inference from existing columns.
219    #[serde(default, skip_serializing_if = "Option::is_none")]
220    pub columns_declared: Option<bool>,
221    #[serde(default)]
222    pub checks: Vec<TableCheck>,
223    #[serde(default)]
224    pub foreign_keys: Vec<ForeignKey>,
225    #[serde(default)]
226    pub key_constraints: Vec<TableKeyConstraint>,
227    /// Stored alongside the table definition so reopen preserves `pg_class.relpersistence` for unlogged tables.
228    #[serde(default)]
229    pub persistence: RelationPersistence,
230    /// Permanent and unlogged tables always use the default. Temporary tables are session-local and therefore never write this field to disk.
231    #[serde(default)]
232    pub on_commit: OnCommitAction,
233    /// Durable relation hierarchy and partition-bound metadata.
234    #[serde(default)]
235    pub hierarchy: TableHierarchy,
236}
237
238/// `CHECK (expr)` constraint with an optional name (`CONSTRAINT <name>
239/// CHECK (...)`).
240#[derive(Debug, Clone, Serialize, Deserialize)]
241#[expect(
242    clippy::struct_excessive_bools,
243    reason = "CHECK catalog flags are independent PostgreSQL properties"
244)]
245pub struct TableCheck {
246    pub name: Option<String>,
247    /// Durable identity of this CHECK, assigned when its definition is published.
248    #[serde(default, skip_serializing_if = "Option::is_none")]
249    pub object_id: Option<[u8; 16]>,
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub catalog_oid: Option<i64>,
252    /// Whether this relation declares this CHECK locally, independently from inherited copies. Missing legacy origin retains the historical local projection.
253    #[serde(default = "default_true", skip_serializing_if = "is_true")]
254    pub is_local: bool,
255    pub expr: Expr,
256    #[serde(default = "default_true")]
257    pub enforced: bool,
258    #[serde(default = "default_true")]
259    pub validated: bool,
260    #[serde(default)]
261    pub no_inherit: bool,
262    /// Runtime form of the bound CHECK retained after `DETACH PARTITION ... CONCURRENTLY`.
263    #[serde(default, skip_serializing_if = "Option::is_none")]
264    pub partition_constraint: Option<DetachedPartitionConstraint>,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct DetachedPartitionConstraint {
269    pub spec: PartitionSpec,
270    pub bound: PartitionBound,
271}
272
273/// Table-level foreign key. Compilation preserves an omitted referenced column list as empty; validation fills it from the primary key before publication.
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
275#[allow(clippy::struct_excessive_bools)]
276pub struct ForeignKey {
277    /// Incarnation of the selected unique index, retained across index and constraint renames.
278    #[serde(default, skip_serializing_if = "Option::is_none")]
279    pub referenced_index: Option<[u8; 16]>,
280    /// Name of the selected unique index in the referenced relation namespace.
281    #[serde(default, skip_serializing_if = "Option::is_none")]
282    pub referenced_key: Option<String>,
283    pub name: Option<String>,
284    /// Logical foreign-key identity shared by a partition family for enforcement and deferred events.
285    #[serde(default, skip_serializing_if = "Option::is_none")]
286    pub object_id: Option<[u8; 16]>,
287    /// Independent catalog row lifetime and OID, preserved through renames and distinct in each partition.
288    #[serde(default, skip_serializing_if = "Option::is_none")]
289    pub catalog_identity: Option<ConstraintCatalogIdentity>,
290    pub local_columns: Vec<String>,
291    pub ref_table: String,
292    pub ref_columns: Vec<String>,
293    #[serde(default)]
294    pub on_update: ForeignKeyAction,
295    #[serde(default)]
296    pub on_delete: ForeignKeyAction,
297    /// Optional column subset for `ON DELETE SET NULL (...)` and
298    /// `ON DELETE SET DEFAULT (...)`. Empty means every local FK
299    /// column participates.
300    #[serde(default)]
301    pub on_delete_set_columns: Vec<String>,
302    #[serde(default)]
303    pub match_type: ForeignKeyMatch,
304    #[serde(default = "default_true")]
305    pub enforced: bool,
306    #[serde(default = "default_true")]
307    pub validated: bool,
308    #[serde(default)]
309    pub deferrable: bool,
310    #[serde(default)]
311    pub initially_deferred: bool,
312    /// The final local and referenced columns use `PostgreSQL` PERIOD coverage.
313    #[serde(default)]
314    pub period: bool,
315    /// The constraints derived on the partitions of a partitioned referenced table, parents before their partitions, in the order `PostgreSQL` creates them. Only the foreign key a referencing relation declares holds them; the copies on its partitions hold none.
316    #[serde(default, skip_serializing_if = "Vec::is_empty")]
317    pub referenced_partitions: Vec<super::ReferencedPartitionConstraint>,
318}
319
320const fn default_true() -> bool {
321    true
322}
323
324#[expect(
325    clippy::trivially_copy_pass_by_ref,
326    reason = "serde skip_serializing_if requires a borrowed field"
327)]
328const fn is_true(value: &bool) -> bool {
329    *value
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
333pub enum ForeignKeyAction {
334    #[default]
335    NoAction,
336    Restrict,
337    Cascade,
338    SetNull,
339    SetDefault,
340}
341
342#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
343pub enum ForeignKeyMatch {
344    #[default]
345    Simple,
346    Full,
347}