1use std::collections::BTreeSet;
9use uqa_core::RelationIdentity;
10pub mod identity;
11
12#[derive(Default)]
14pub struct ConstraintNameScope {
15 pub events: BTreeSet<String>,
16 pub schema: BTreeSet<String>,
17}
18
19#[derive(Debug)]
20pub enum ConstraintMetadataError {
21 Invalid(String),
22 Execution(Box<crate::SQLError>),
23}
24
25impl std::fmt::Display for ConstraintMetadataError {
26 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 match self {
28 Self::Invalid(message) => formatter.write_str(message),
29 Self::Execution(error) => std::fmt::Display::fmt(error, formatter),
30 }
31 }
32}
33
34impl std::error::Error for ConstraintMetadataError {
35 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
36 match self {
37 Self::Invalid(_) => None,
38 Self::Execution(error) => Some(error.as_ref()),
39 }
40 }
41}
42pub type ConstraintMetadataResult<T> = Result<T, ConstraintMetadataError>;
43pub type CatalogIdentityAllocator<'a> = dyn CatalogObjectAllocator + 'a;
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
46pub enum CatalogOidClass {
47 Constraint,
48 Relation,
49}
50
51impl CatalogOidClass {
52 pub const fn class_id(self) -> u32 {
53 match self {
54 Self::Constraint => 2606,
55 Self::Relation => 1259,
56 }
57 }
58
59 pub const fn label(self) -> &'static str {
60 match self {
61 Self::Constraint => "constraint",
62 Self::Relation => "relation",
63 }
64 }
65}
66
67pub trait CatalogObjectAllocator {
69 fn include_catalog_identity(
71 &mut self,
72 _relation: &RelationIdentity,
73 _class: CatalogOidClass,
74 _identity: crate::ast::ConstraintCatalogIdentity,
75 ) -> ConstraintMetadataResult<()> {
76 Ok(())
77 }
78
79 fn allocate_object_id(&mut self, kind: &str) -> ConstraintMetadataResult<[u8; 16]>;
80
81 fn allocate_catalog_oid(
82 &mut self,
83 class: CatalogOidClass,
84 object_id: &[u8; 16],
85 ) -> ConstraintMetadataResult<i64>;
86}
87
88impl<F> CatalogObjectAllocator for F
89where
90 F: FnMut(&str) -> ConstraintMetadataResult<[u8; 16]>,
91{
92 fn allocate_object_id(&mut self, kind: &str) -> ConstraintMetadataResult<[u8; 16]> {
93 self(kind)
94 }
95
96 fn allocate_catalog_oid(
97 &mut self,
98 class: CatalogOidClass,
99 object_id: &[u8; 16],
100 ) -> ConstraintMetadataResult<i64> {
101 Ok(crate::catalog::oids::stable_object_oid(
102 class.label(),
103 object_id,
104 ))
105 }
106}
107
108pub fn materialize_constraint_metadata(
109 relation: &RelationIdentity,
110 columns: &mut [crate::ast::ColumnDef],
111 constraints: &mut crate::ast::TableConstraintSet,
112 allocate: &mut CatalogIdentityAllocator<'_>,
113) -> ConstraintMetadataResult<bool> {
114 materialize_constraint_metadata_with_names(
115 relation,
116 columns,
117 constraints,
118 allocate,
119 &ConstraintNameScope::default(),
120 )
121}
122
123pub fn materialize_constraint_metadata_with_names(
125 relation: &RelationIdentity,
126 columns: &mut [crate::ast::ColumnDef],
127 constraints: &mut crate::ast::TableConstraintSet,
128 allocate: &mut CatalogIdentityAllocator<'_>,
129 names: &ConstraintNameScope,
130) -> ConstraintMetadataResult<bool> {
131 identity::claims::validate_present_identities(columns, constraints)?;
132 for identity in identity::claims::identities(columns, constraints) {
133 allocate.include_catalog_identity(relation, CatalogOidClass::Constraint, identity)?;
134 }
135 let mut changed = materialize_column_key_constraints(columns, constraints);
137 let mut used = constraint_names_for_assignment(relation, columns, constraints, names)?;
138
139 let mut column_object_ids = BTreeSet::new();
140 for column in columns.iter_mut() {
141 if column
142 .object_id
143 .is_some_and(|object_id| !column_object_ids.insert(object_id))
144 {
145 column.object_id = None;
146 }
147 changed |= assign_catalog_object_id(&mut column.object_id, "column", allocate)?;
148 if let Some(object_id) = column.object_id {
149 column_object_ids.insert(object_id);
150 }
151 if column.not_null {
152 changed |= assign_constraint_name(
153 &mut column.not_null_name,
154 (&relation.name, &column.name, "not_null"),
155 &mut used,
156 )?;
157 changed |= identity::materialize_not_null_identity(column, allocate)?;
158 }
159 if column.check.is_some() {
160 changed |= assign_constraint_name(
161 &mut column.check_name,
162 (&relation.name, &column.name, "check"),
163 &mut used,
164 )?;
165 changed |= assign_catalog_object_id(
166 &mut column.check_object_id,
167 "CHECK constraint",
168 allocate,
169 )?;
170 changed |= identity::materialize_check_oid(
171 column.check_object_id,
172 &mut column.check_catalog_oid,
173 allocate,
174 )?;
175 }
176 if let Some(reference) = &mut column.references {
177 changed |= assign_constraint_name(
178 &mut reference.name,
179 (&relation.name, &column.name, "fkey"),
180 &mut used,
181 )?;
182 changed |= assign_constraint_object_id(&mut reference.object_id, allocate)?;
183 changed |=
184 identity::foreign_keys::materialize(&mut reference.catalog_identity, allocate)?;
185 }
186 }
187 for constraint in &mut constraints.key_constraints {
188 let (component, label) = match constraint.kind {
189 crate::ast::TableKeyConstraintKind::PrimaryKey => (String::new(), "pkey"),
190 crate::ast::TableKeyConstraintKind::Unique => (
191 constraint_column_component(&constraint.columns, relation)?,
192 "key",
193 ),
194 };
195 changed |= assign_constraint_name(
196 &mut constraint.name,
197 (&relation.name, &component, label),
198 &mut used,
199 )?;
200 changed |= identity::materialize_key_identity(constraint, allocate)?;
201 }
202 changed |= materialize_checks(relation, &mut constraints.checks, &mut used, allocate)?;
203 changed |= synchronize_partition_inherited_foreign_key_ids(constraints);
204 for constraint in &mut constraints.foreign_keys {
205 let component = constraint_column_component(&constraint.local_columns, relation)?;
206 changed |= assign_constraint_name(
207 &mut constraint.name,
208 (&relation.name, &component, "fkey"),
209 &mut used,
210 )?;
211 changed |= assign_constraint_object_id(&mut constraint.object_id, allocate)?;
212 changed |= identity::foreign_keys::materialize(&mut constraint.catalog_identity, allocate)?;
213 }
214 changed |= synchronize_partition_inherited_foreign_key_ids(constraints);
215 changed |= identity::keys::synchronize_provenance(constraints);
216 identity::claims::validate_constraint_identities(columns, constraints)?;
217 Ok(changed)
218}
219
220fn materialize_checks(
221 relation: &RelationIdentity,
222 checks: &mut [crate::ast::TableCheck],
223 used: &mut BTreeSet<String>,
224 allocate: &mut CatalogIdentityAllocator<'_>,
225) -> ConstraintMetadataResult<bool> {
226 let mut changed = false;
227 for constraint in checks {
228 let mut referenced_columns = Vec::new();
229 collect_constraint_columns(&constraint.expr, &mut referenced_columns);
230 let component = if referenced_columns.len() == 1 {
231 referenced_columns[0].as_str()
232 } else {
233 ""
234 };
235 changed |= assign_constraint_name(
236 &mut constraint.name,
237 (&relation.name, component, "check"),
238 used,
239 )?;
240 changed |=
241 assign_catalog_object_id(&mut constraint.object_id, "CHECK constraint", allocate)?;
242 changed |= identity::materialize_check_oid(
243 constraint.object_id,
244 &mut constraint.catalog_oid,
245 allocate,
246 )?;
247 }
248 Ok(changed)
249}
250
251fn constraint_names_for_assignment(
252 relation: &RelationIdentity,
253 columns: &[crate::ast::ColumnDef],
254 constraints: &crate::ast::TableConstraintSet,
255 names: &ConstraintNameScope,
256) -> ConstraintMetadataResult<BTreeSet<String>> {
257 let mut used = BTreeSet::new();
258 for column in columns {
259 record_constraint_name(&mut used, column.not_null_name.as_deref())?;
260 record_constraint_name(&mut used, column.check_name.as_deref())?;
261 record_constraint_name(
262 &mut used,
263 column
264 .references
265 .as_ref()
266 .and_then(|reference| reference.name.as_deref()),
267 )?;
268 }
269 for constraint in &constraints.key_constraints {
270 record_constraint_name(&mut used, constraint.name.as_deref())?;
271 }
272 for constraint in &constraints.checks {
273 record_constraint_name(&mut used, constraint.name.as_deref())?;
274 }
275 for constraint in &constraints.foreign_keys {
276 record_constraint_name(&mut used, constraint.name.as_deref())?;
277 }
278 for name in &names.events {
279 if !used.insert(name.clone()) {
280 return Err(ConstraintMetadataError::Execution(Box::new(
281 crate::schema::constraint_changes::constraint_error(
282 "42710",
283 format!(
284 "constraint \"{name}\" for relation \"{}\" already exists",
285 relation.name
286 ),
287 ),
288 )));
289 }
290 }
291 used.extend(names.schema.iter().cloned());
292 Ok(used)
293}
294
295pub fn materialize_column_key_constraints(
296 columns: &[crate::ast::ColumnDef],
297 constraints: &mut crate::ast::TableConstraintSet,
298) -> bool {
299 let mut changed = false;
300 for column in columns {
301 for (present, kind) in [
302 (
303 column.primary_key,
304 crate::ast::TableKeyConstraintKind::PrimaryKey,
305 ),
306 (column.unique, crate::ast::TableKeyConstraintKind::Unique),
307 ] {
308 if !present
309 || constraints.key_constraints.iter().any(|constraint| {
310 constraint.kind == kind
311 && constraint.columns.as_slice() == [column.name.as_str()]
312 })
313 {
314 continue;
315 }
316 constraints
317 .key_constraints
318 .push(crate::ast::TableKeyConstraint {
319 catalog_identity: None,
320 name: None,
321 kind,
322 columns: vec![column.name.clone()],
323 nulls_not_distinct: false,
324 without_overlaps: false,
325 });
326 changed = true;
327 }
328 }
329 changed
330}
331
332pub fn foreign_keys_match_without_object_id(
333 left: &crate::ast::ForeignKey,
334 right: &crate::ast::ForeignKey,
335) -> bool {
336 let mut left = left.clone();
337 let mut right = right.clone();
338 left.object_id = None;
339 right.object_id = None;
340 left.catalog_identity = None;
341 right.catalog_identity = None;
342 left == right
343}
344
345pub fn foreign_key_provenance_matches(
347 left: &crate::ast::ForeignKey,
348 right: &crate::ast::ForeignKey,
349) -> bool {
350 match (left.catalog_identity, right.catalog_identity) {
351 (Some(left), Some(right)) => left == right,
352 _ => {
353 (left.object_id.is_some() && left.object_id == right.object_id)
354 || foreign_keys_match_without_object_id(left, right)
355 }
356 }
357}
358
359pub fn synchronize_partition_inherited_foreign_key_ids(
360 constraints: &mut crate::ast::TableConstraintSet,
361) -> bool {
362 let mut changed = false;
363 for inherited_index in 0..constraints.hierarchy.partition_inherited_foreign_keys.len() {
364 let inherited = &constraints.hierarchy.partition_inherited_foreign_keys[inherited_index];
365 let Some(foreign_key_index) = constraints
366 .foreign_keys
367 .iter()
368 .position(|foreign_key| foreign_key_provenance_matches(foreign_key, inherited))
369 else {
370 continue;
371 };
372 let object_id = constraints.foreign_keys[foreign_key_index]
373 .object_id
374 .or(inherited.object_id);
375 if constraints.foreign_keys[foreign_key_index].object_id != object_id {
376 constraints.foreign_keys[foreign_key_index].object_id = object_id;
377 changed = true;
378 }
379 if constraints.hierarchy.partition_inherited_foreign_keys[inherited_index].object_id
380 != object_id
381 {
382 constraints.hierarchy.partition_inherited_foreign_keys[inherited_index].object_id =
383 object_id;
384 changed = true;
385 }
386 let catalog_identity = constraints.foreign_keys[foreign_key_index].catalog_identity;
387 if constraints.hierarchy.partition_inherited_foreign_keys[inherited_index].catalog_identity
388 != catalog_identity
389 {
390 constraints.hierarchy.partition_inherited_foreign_keys[inherited_index]
391 .catalog_identity = catalog_identity;
392 changed = true;
393 }
394 }
395 changed
396}
397
398fn assign_constraint_object_id(
399 target: &mut Option<[u8; 16]>,
400 allocate: &mut CatalogIdentityAllocator<'_>,
401) -> ConstraintMetadataResult<bool> {
402 assign_catalog_object_id(target, "foreign-key constraint", allocate)
403}
404
405fn assign_catalog_object_id(
406 target: &mut Option<[u8; 16]>,
407 object_kind: &str,
408 allocate: &mut CatalogIdentityAllocator<'_>,
409) -> ConstraintMetadataResult<bool> {
410 if target.is_some() {
411 return Ok(false);
412 }
413 *target = Some(allocate.allocate_object_id(object_kind)?);
414 Ok(true)
415}
416
417fn record_constraint_name(
418 used: &mut BTreeSet<String>,
419 name: Option<&str>,
420) -> ConstraintMetadataResult<()> {
421 let Some(name) = name else {
422 return Ok(());
423 };
424 if name.is_empty() {
425 return Err(ConstraintMetadataError::Invalid(
426 "constraint name must not be empty".into(),
427 ));
428 }
429 if !used.insert(name.to_string()) {
430 return Err(ConstraintMetadataError::Invalid(format!(
431 "constraint `{name}` is declared more than once"
432 )));
433 }
434 Ok(())
435}
436
437pub(super) fn assign_constraint_name(
438 target: &mut Option<String>,
439 parts: (&str, &str, &str),
440 used: &mut BTreeSet<String>,
441) -> ConstraintMetadataResult<bool> {
442 if target.is_some() {
443 return Ok(false);
444 }
445 let base = super::indexes::names::object_name(parts.0, parts.1, parts.2);
446 if used.insert(base.clone()) {
447 *target = Some(base);
448 return Ok(true);
449 }
450 for suffix in 1_u64.. {
451 let label = format!("{}{suffix}", parts.2);
452 let candidate = super::indexes::names::object_name(parts.0, parts.1, &label);
453 if used.insert(candidate.clone()) {
454 *target = Some(candidate);
455 return Ok(true);
456 }
457 }
458 Err(ConstraintMetadataError::Invalid(format!(
459 "constraint name suffix space exhausted for `{base}`"
460 )))
461}
462
463fn constraint_column_component(
464 columns: &[String],
465 relation: &RelationIdentity,
466) -> ConstraintMetadataResult<String> {
467 if columns.is_empty() {
468 return Err(ConstraintMetadataError::Invalid(format!(
469 "constraint on table `{}` has no columns",
470 relation.qualified_name()
471 )));
472 }
473 Ok(columns.join("_"))
474}
475
476fn collect_constraint_columns(expression: &crate::ast::Expr, output: &mut Vec<String>) {
477 use crate::ast::{Expr, FrameBound};
478 match expression {
479 Expr::Column(name) | Expr::QualifiedColumn { column: name, .. } => {
480 if !output.contains(name) {
481 output.push(name.clone());
482 }
483 }
484 Expr::Func {
485 args,
486 order_by,
487 filter,
488 ..
489 } => {
490 for argument in args {
491 collect_constraint_columns(argument, output);
492 }
493 for order in order_by {
494 collect_constraint_columns(&order.expr, output);
495 }
496 if let Some(filter) = filter {
497 collect_constraint_columns(filter, output);
498 }
499 }
500 Expr::Array(items) | Expr::Row(items) | Expr::And(items) | Expr::Or(items) => {
501 for item in items {
502 collect_constraint_columns(item, output);
503 }
504 }
505 Expr::Binary { lhs, rhs, .. } => {
506 collect_constraint_columns(lhs, output);
507 collect_constraint_columns(rhs, output);
508 }
509 Expr::Not(inner)
510 | Expr::UnaryMinus(inner)
511 | Expr::IsNull { expr: inner, .. }
512 | Expr::Cast { expr: inner, .. } => {
513 collect_constraint_columns(inner, output);
514 }
515 Expr::Between { expr, low, high } => {
516 collect_constraint_columns(expr, output);
517 collect_constraint_columns(low, output);
518 collect_constraint_columns(high, output);
519 }
520 Expr::InList { expr, list, .. } => {
521 collect_constraint_columns(expr, output);
522 for item in list {
523 collect_constraint_columns(item, output);
524 }
525 }
526 Expr::WindowCall { args, spec, .. } => {
527 for argument in args {
528 collect_constraint_columns(argument, output);
529 }
530 for expression in &spec.partition_by {
531 collect_constraint_columns(expression, output);
532 }
533 for order in &spec.order_by {
534 collect_constraint_columns(&order.expr, output);
535 }
536 if let Some(frame) = &spec.frame {
537 for bound in [&frame.start, &frame.end] {
538 if let FrameBound::Preceding(expression) | FrameBound::Following(expression) =
539 bound
540 {
541 collect_constraint_columns(expression, output);
542 }
543 }
544 }
545 }
546 Expr::Case {
547 base,
548 when,
549 else_branch,
550 } => {
551 if let Some(base) = base {
552 collect_constraint_columns(base, output);
553 }
554 for (condition, result) in when {
555 collect_constraint_columns(condition, output);
556 collect_constraint_columns(result, output);
557 }
558 if let Some(else_branch) = else_branch {
559 collect_constraint_columns(else_branch, output);
560 }
561 }
562 Expr::InSubquery { expr, .. } => collect_constraint_columns(expr, output),
563 Expr::Default
564 | Expr::Star
565 | Expr::QualifiedStar(_)
566 | Expr::InternalColumn(_)
567 | Expr::Literal(_)
568 | Expr::TypedLiteral { .. }
569 | Expr::Param(_)
570 | Expr::ScalarSubquery(_)
571 | Expr::Exists { .. } => {}
572 }
573}