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 included_columns: Vec::new(),
324 nulls_not_distinct: false,
325 without_overlaps: false,
326 });
327 changed = true;
328 }
329 }
330 changed
331}
332
333pub fn foreign_keys_match_without_object_id(
334 left: &crate::ast::ForeignKey,
335 right: &crate::ast::ForeignKey,
336) -> bool {
337 let mut left = left.clone();
338 let mut right = right.clone();
339 left.object_id = None;
340 right.object_id = None;
341 left.catalog_identity = None;
342 right.catalog_identity = None;
343 left == right
344}
345
346pub fn foreign_key_provenance_matches(
348 left: &crate::ast::ForeignKey,
349 right: &crate::ast::ForeignKey,
350) -> bool {
351 match (left.catalog_identity, right.catalog_identity) {
352 (Some(left), Some(right)) => left == right,
353 _ => {
354 (left.object_id.is_some() && left.object_id == right.object_id)
355 || foreign_keys_match_without_object_id(left, right)
356 }
357 }
358}
359
360pub fn synchronize_partition_inherited_foreign_key_ids(
361 constraints: &mut crate::ast::TableConstraintSet,
362) -> bool {
363 let mut changed = false;
364 for inherited_index in 0..constraints.hierarchy.partition_inherited_foreign_keys.len() {
365 let inherited = &constraints.hierarchy.partition_inherited_foreign_keys[inherited_index];
366 let Some(foreign_key_index) = constraints
367 .foreign_keys
368 .iter()
369 .position(|foreign_key| foreign_key_provenance_matches(foreign_key, inherited))
370 else {
371 continue;
372 };
373 let object_id = constraints.foreign_keys[foreign_key_index]
374 .object_id
375 .or(inherited.object_id);
376 if constraints.foreign_keys[foreign_key_index].object_id != object_id {
377 constraints.foreign_keys[foreign_key_index].object_id = object_id;
378 changed = true;
379 }
380 if constraints.hierarchy.partition_inherited_foreign_keys[inherited_index].object_id
381 != object_id
382 {
383 constraints.hierarchy.partition_inherited_foreign_keys[inherited_index].object_id =
384 object_id;
385 changed = true;
386 }
387 let catalog_identity = constraints.foreign_keys[foreign_key_index].catalog_identity;
388 if constraints.hierarchy.partition_inherited_foreign_keys[inherited_index].catalog_identity
389 != catalog_identity
390 {
391 constraints.hierarchy.partition_inherited_foreign_keys[inherited_index]
392 .catalog_identity = catalog_identity;
393 changed = true;
394 }
395 }
396 changed
397}
398
399fn assign_constraint_object_id(
400 target: &mut Option<[u8; 16]>,
401 allocate: &mut CatalogIdentityAllocator<'_>,
402) -> ConstraintMetadataResult<bool> {
403 assign_catalog_object_id(target, "foreign-key constraint", allocate)
404}
405
406fn assign_catalog_object_id(
407 target: &mut Option<[u8; 16]>,
408 object_kind: &str,
409 allocate: &mut CatalogIdentityAllocator<'_>,
410) -> ConstraintMetadataResult<bool> {
411 if target.is_some() {
412 return Ok(false);
413 }
414 *target = Some(allocate.allocate_object_id(object_kind)?);
415 Ok(true)
416}
417
418fn record_constraint_name(
419 used: &mut BTreeSet<String>,
420 name: Option<&str>,
421) -> ConstraintMetadataResult<()> {
422 let Some(name) = name else {
423 return Ok(());
424 };
425 if name.is_empty() {
426 return Err(ConstraintMetadataError::Invalid(
427 "constraint name must not be empty".into(),
428 ));
429 }
430 if !used.insert(name.to_string()) {
431 return Err(ConstraintMetadataError::Invalid(format!(
432 "constraint `{name}` is declared more than once"
433 )));
434 }
435 Ok(())
436}
437
438pub(super) fn assign_constraint_name(
439 target: &mut Option<String>,
440 parts: (&str, &str, &str),
441 used: &mut BTreeSet<String>,
442) -> ConstraintMetadataResult<bool> {
443 if target.is_some() {
444 return Ok(false);
445 }
446 let base = super::indexes::names::object_name(parts.0, parts.1, parts.2);
447 if used.insert(base.clone()) {
448 *target = Some(base);
449 return Ok(true);
450 }
451 for suffix in 1_u64.. {
452 let label = format!("{}{suffix}", parts.2);
453 let candidate = super::indexes::names::object_name(parts.0, parts.1, &label);
454 if used.insert(candidate.clone()) {
455 *target = Some(candidate);
456 return Ok(true);
457 }
458 }
459 Err(ConstraintMetadataError::Invalid(format!(
460 "constraint name suffix space exhausted for `{base}`"
461 )))
462}
463
464fn constraint_column_component(
465 columns: &[String],
466 relation: &RelationIdentity,
467) -> ConstraintMetadataResult<String> {
468 if columns.is_empty() {
469 return Err(ConstraintMetadataError::Invalid(format!(
470 "constraint on table `{}` has no columns",
471 relation.qualified_name()
472 )));
473 }
474 Ok(columns.join("_"))
475}
476
477fn collect_constraint_columns(expression: &crate::ast::Expr, output: &mut Vec<String>) {
478 use crate::ast::{Expr, FrameBound};
479 match expression {
480 Expr::Column(name) | Expr::QualifiedColumn { column: name, .. } => {
481 if !output.contains(name) {
482 output.push(name.clone());
483 }
484 }
485 Expr::Func {
486 args,
487 order_by,
488 filter,
489 ..
490 } => {
491 for argument in args {
492 collect_constraint_columns(argument, output);
493 }
494 for order in order_by {
495 collect_constraint_columns(&order.expr, output);
496 }
497 if let Some(filter) = filter {
498 collect_constraint_columns(filter, output);
499 }
500 }
501 Expr::Array(items) | Expr::Row(items) | Expr::And(items) | Expr::Or(items) => {
502 for item in items {
503 collect_constraint_columns(item, output);
504 }
505 }
506 Expr::Binary { lhs, rhs, .. } => {
507 collect_constraint_columns(lhs, output);
508 collect_constraint_columns(rhs, output);
509 }
510 Expr::Not(inner)
511 | Expr::UnaryMinus(inner)
512 | Expr::IsNull { expr: inner, .. }
513 | Expr::Cast { expr: inner, .. } => {
514 collect_constraint_columns(inner, output);
515 }
516 Expr::Between { expr, low, high } => {
517 collect_constraint_columns(expr, output);
518 collect_constraint_columns(low, output);
519 collect_constraint_columns(high, output);
520 }
521 Expr::InList { expr, list, .. } => {
522 collect_constraint_columns(expr, output);
523 for item in list {
524 collect_constraint_columns(item, output);
525 }
526 }
527 Expr::WindowCall { args, spec, .. } => {
528 for argument in args {
529 collect_constraint_columns(argument, output);
530 }
531 for expression in &spec.partition_by {
532 collect_constraint_columns(expression, output);
533 }
534 for order in &spec.order_by {
535 collect_constraint_columns(&order.expr, output);
536 }
537 if let Some(frame) = &spec.frame {
538 for bound in [&frame.start, &frame.end] {
539 if let FrameBound::Preceding(expression) | FrameBound::Following(expression) =
540 bound
541 {
542 collect_constraint_columns(expression, output);
543 }
544 }
545 }
546 }
547 Expr::Case {
548 base,
549 when,
550 else_branch,
551 } => {
552 if let Some(base) = base {
553 collect_constraint_columns(base, output);
554 }
555 for (condition, result) in when {
556 collect_constraint_columns(condition, output);
557 collect_constraint_columns(result, output);
558 }
559 if let Some(else_branch) = else_branch {
560 collect_constraint_columns(else_branch, output);
561 }
562 }
563 Expr::InSubquery { expr, .. } => collect_constraint_columns(expr, output),
564 Expr::Default
565 | Expr::Star
566 | Expr::QualifiedStar(_)
567 | Expr::InternalColumn(_)
568 | Expr::Literal(_)
569 | Expr::TypedLiteral { .. }
570 | Expr::Param(_)
571 | Expr::ScalarSubquery(_)
572 | Expr::Exists { .. } => {}
573 }
574}