1use alloc::boxed::Box;
14use alloc::string::{String, ToString};
15use alloc::vec::Vec;
16
17use spg_sql::ast::Expr;
18use spg_storage::{Catalog, ColumnSchema, Row, StorageError, Value};
19
20use crate::aggregate;
21use crate::eval::{self, EvalError};
22use crate::{Engine, EngineError, check_unsigned_range, coerce_value, value_to_literal_expr};
23
24type KeyStrFn<'a> = dyn Fn(&[Value<'static>]) -> Result<Option<String>, EngineError> + 'a;
27
28pub(crate) fn resolve_foreign_key(
42 local_table_name: &str,
43 local_cols: &[ColumnSchema],
44 fk: spg_sql::ast::ForeignKeyConstraint,
45 catalog: &Catalog,
46) -> Result<spg_storage::ForeignKeyConstraint, EngineError> {
47 let mut local_columns = Vec::with_capacity(fk.columns.len());
49 for name in &fk.columns {
50 let pos = local_cols
51 .iter()
52 .position(|c| c.name == *name)
53 .ok_or_else(|| {
54 EngineError::Unsupported(alloc::format!(
55 "FOREIGN KEY references unknown local column {name:?}"
56 ))
57 })?;
58 local_columns.push(pos);
59 }
60 let is_self_ref = fk.parent_table == local_table_name;
64 let (parent_cols_for_lookup, parent_table_str): (&[ColumnSchema], &str) = if is_self_ref {
65 (local_cols, local_table_name)
66 } else {
67 let parent_table = catalog.get(&fk.parent_table).ok_or_else(|| {
68 EngineError::Storage(StorageError::TableNotFound {
69 name: fk.parent_table.clone(),
70 })
71 })?;
72 (
73 parent_table.schema().columns.as_slice(),
74 fk.parent_table.as_str(),
75 )
76 };
77 let parent_columns: Vec<usize> = if fk.parent_columns.is_empty() {
82 if fk.columns.len() != 1 {
83 return Err(EngineError::Unsupported(
84 "composite FOREIGN KEY without explicit parent column list is not supported \
85 — list the parent columns explicitly"
86 .into(),
87 ));
88 }
89 let pos = pick_pk_index_column(catalog, parent_table_str, is_self_ref, local_cols)
91 .ok_or_else(|| {
92 EngineError::Unsupported(alloc::format!(
93 "parent table {parent_table_str:?} has no PRIMARY-key / UNIQUE BTree index \
94 to default the FOREIGN KEY against"
95 ))
96 })?;
97 alloc::vec![pos]
98 } else {
99 let mut out = Vec::with_capacity(fk.parent_columns.len());
100 for name in &fk.parent_columns {
101 let pos = parent_cols_for_lookup
102 .iter()
103 .position(|c| c.name == *name)
104 .ok_or_else(|| {
105 EngineError::Unsupported(alloc::format!(
106 "FOREIGN KEY references unknown parent column \
107 {name:?} on table {parent_table_str:?}"
108 ))
109 })?;
110 out.push(pos);
111 }
112 out
113 };
114 if parent_columns.len() != local_columns.len() {
115 return Err(EngineError::Unsupported(alloc::format!(
116 "FOREIGN KEY arity mismatch: {} local columns vs {} parent columns",
117 local_columns.len(),
118 parent_columns.len()
119 )));
120 }
121 if !is_self_ref {
131 let parent_table = catalog.get(&fk.parent_table).expect("checked above");
132 let primary_parent_col = parent_columns[0];
133 let has_btree = parent_table
134 .schema()
135 .columns
136 .get(primary_parent_col)
137 .is_some()
138 && parent_table.indices().iter().any(|idx| {
139 matches!(idx.kind, spg_storage::IndexKind::BTree(_))
140 && idx.column_position == primary_parent_col
141 && idx.partial_predicate.is_none()
142 });
143 if !has_btree {
144 return Err(EngineError::Unsupported(alloc::format!(
145 "FOREIGN KEY parent column on {:?} is not covered by an unconditional BTree \
146 index — create one with `CREATE INDEX ... ON {} ({})` first",
147 parent_table_str,
148 parent_table_str,
149 parent_table.schema().columns[primary_parent_col].name,
150 )));
151 }
152 }
153 let on_delete = fk_action_sql_to_storage(fk.on_delete);
154 let on_update = fk_action_sql_to_storage(fk.on_update);
155 let match_type = match fk.match_type {
156 spg_sql::ast::MatchType::Simple => spg_storage::MatchType::Simple,
157 spg_sql::ast::MatchType::Full => spg_storage::MatchType::Full,
158 };
159 Ok(spg_storage::ForeignKeyConstraint {
160 name: fk.name,
161 local_columns,
162 parent_table: fk.parent_table,
163 parent_columns,
164 on_delete,
165 on_update,
166 deferrable: fk.deferrable,
167 initially_deferred: fk.initially_deferred,
168 match_type,
169 })
170}
171
172fn pick_pk_index_column(
178 catalog: &Catalog,
179 parent_name: &str,
180 is_self_ref: bool,
181 local_cols: &[ColumnSchema],
182) -> Option<usize> {
183 if is_self_ref {
184 let _ = local_cols;
188 return Some(0);
189 }
190 let parent = catalog.get(parent_name)?;
191 parent.indices().iter().find_map(|idx| {
192 if matches!(idx.kind, spg_storage::IndexKind::BTree(_))
193 && idx.partial_predicate.is_none()
194 && idx.included_columns.is_empty()
195 && idx.expression.is_none()
196 {
197 Some(idx.column_position)
198 } else {
199 None
200 }
201 })
202}
203
204pub(crate) fn on_conflict_arbiters(
231 catalog: &Catalog,
232 table_name: &str,
233 target: &[String],
234 from_constraint_name: bool,
235) -> Result<Vec<(Vec<usize>, bool)>, EngineError> {
236 let table = catalog.get(table_name).ok_or_else(|| {
237 EngineError::Storage(StorageError::TableNotFound {
238 name: table_name.into(),
239 })
240 })?;
241 let schema = table.schema();
242 let unique_btree_cols: Vec<usize> = table
243 .indices()
244 .iter()
245 .filter(|idx| {
246 idx.is_unique
247 && matches!(idx.kind, spg_storage::IndexKind::BTree(_))
248 && idx.partial_predicate.is_none()
249 && idx.expression.is_none()
250 })
251 .map(|idx| idx.column_position)
252 .collect();
253 if target.is_empty() {
254 let mut out: Vec<(Vec<usize>, bool)> = schema
255 .uniqueness_constraints
256 .iter()
257 .map(|uc| (uc.columns.clone(), uc.nulls_not_distinct))
258 .collect();
259 for &pos in &unique_btree_cols {
260 if !out.iter().any(|(cols, _)| cols == &alloc::vec![pos]) {
261 out.push((alloc::vec![pos], false));
262 }
263 }
264 if out.is_empty() {
270 for idx in table.indices() {
271 if matches!(idx.kind, spg_storage::IndexKind::BTree(_))
272 && idx.partial_predicate.is_none()
273 && idx.expression.is_none()
274 && idx.included_columns.is_empty()
275 {
276 out.push((alloc::vec![idx.column_position], false));
277 }
278 }
279 }
280 return Ok(out);
281 }
282 let mut positions = Vec::with_capacity(target.len());
283 for name in target {
284 let pos = schema
285 .columns
286 .iter()
287 .position(|c| c.name == *name)
288 .ok_or_else(|| {
289 EngineError::Unsupported(alloc::format!(
290 "ON CONFLICT target column {name:?} not found on {table_name:?}"
291 ))
292 })?;
293 positions.push(pos);
294 }
295 let mut sorted = positions.clone();
296 sorted.sort_unstable();
297 let matched_uc = schema.uniqueness_constraints.iter().find(|uc| {
298 let mut u = uc.columns.clone();
299 u.sort_unstable();
300 u == sorted
301 });
302 let _ = from_constraint_name;
311 let nnd = matched_uc.is_some_and(|uc| uc.nulls_not_distinct);
312 Ok(alloc::vec![(positions, nnd)])
313}
314
315fn locator_is_tombstoned(table: &spg_storage::Table, loc: &spg_storage::RowLocator) -> bool {
327 loc.as_hot()
328 .is_some_and(|i| table.headers().get(i).is_some_and(|h| h.is_deleted()))
329}
330
331fn on_conflict_key_exists(
334 catalog: &Catalog,
335 table_name: &str,
336 column_pos: usize,
337 key: &Value,
338) -> bool {
339 let Some(table) = catalog.get(table_name) else {
340 return false;
341 };
342 let Some(idx_key) = spg_storage::IndexKey::from_value(key) else {
343 return false;
344 };
345 table.indices().iter().any(|idx| {
346 matches!(idx.kind, spg_storage::IndexKind::BTree(_))
347 && idx.column_position == column_pos
348 && idx.partial_predicate.is_none()
349 && idx
354 .lookup_eq(&idx_key)
355 .iter()
356 .any(|loc| !locator_is_tombstoned(table, loc))
357 })
358}
359
360pub(crate) fn lookup_row_position_by_keys(
366 catalog: &Catalog,
367 table_name: &str,
368 column_positions: &[usize],
369 key: &[&Value],
370) -> Option<usize> {
371 let table = catalog.get(table_name)?;
372 table.rows().iter().enumerate().position(|(row_idx, r)| {
379 !table.headers().get(row_idx).is_some_and(|h| h.is_deleted())
380 && column_positions
381 .iter()
382 .enumerate()
383 .all(|(i, &pos)| r.values.get(pos) == Some(key[i]))
384 })
385}
386
387pub(crate) fn on_conflict_keys_exist(
392 catalog: &Catalog,
393 table_name: &str,
394 column_positions: &[usize],
395 key: &[&Value],
396) -> bool {
397 if column_positions.len() == 1 {
398 return on_conflict_key_exists(catalog, table_name, column_positions[0], key[0]);
399 }
400 let Some(table) = catalog.get(table_name) else {
401 return false;
402 };
403 let matches = |r: &Row<'static>| {
404 column_positions
405 .iter()
406 .enumerate()
407 .all(|(i, &pos)| r.values.get(pos) == Some(key[i]))
408 };
409 let hot_hit = table.rows().iter().enumerate().any(|(row_idx, r)| {
415 !table.headers().get(row_idx).is_some_and(|h| h.is_deleted()) && matches(r)
416 });
417 if hot_hit {
418 return true;
419 }
420 iter_cold_rows_of_parent(catalog, table)
425 .iter()
426 .any(&matches)
427}
428
429pub(crate) fn apply_on_conflict_assignments(
442 catalog: &Catalog,
443 table_name: &str,
444 alias: Option<&str>,
445 target_pos: usize,
446 incoming: &[Value<'static>],
447 assignments: &[(String, Expr)],
448 where_: Option<&Expr>,
449 sess: Option<&crate::eval::DmlSession>,
452) -> Result<Option<Vec<Value<'static>>>, EngineError> {
453 let table = catalog.get(table_name).ok_or_else(|| {
454 EngineError::Storage(StorageError::TableNotFound {
455 name: table_name.into(),
456 })
457 })?;
458 let schema_cols = table.schema().columns.clone();
459 let existing = table
460 .rows()
461 .get(target_pos)
462 .ok_or_else(|| {
463 EngineError::Unsupported(alloc::format!(
464 "ON CONFLICT DO UPDATE: row position {target_pos} out of bounds on {table_name:?}"
465 ))
466 })?
467 .clone();
468 let mut ctx = eval::EvalContext::new(&schema_cols, Some(alias.unwrap_or(table_name)));
473 if let Some(sv) = sess {
474 ctx = ctx.with_session(sv);
475 }
476 if let Some(w) = where_ {
478 let pred = w.clone();
479 let pred = substitute_excluded_refs(pred, &schema_cols, incoming);
480 let v = eval::eval_expr(&pred, &existing, &ctx)?;
481 if !matches!(v, Value::Bool(true)) {
482 return Ok(None);
483 }
484 }
485 if assignments.is_empty() {
490 return Ok(Some(incoming.to_vec()));
491 }
492 let mut new_values = existing.values.clone();
493 for (col_name, expr) in assignments {
494 let target_idx = schema_cols
495 .iter()
496 .position(|c| c.name == *col_name)
497 .ok_or_else(|| {
498 EngineError::Eval(EvalError::ColumnNotFound {
499 name: col_name.clone(),
500 })
501 })?;
502 let sub = substitute_excluded_refs(expr.clone(), &schema_cols, incoming);
503 let v = eval::eval_expr(&sub, &existing, &ctx)?;
504 let coerced = coerce_value(v, schema_cols[target_idx].ty, col_name, target_idx)?;
505 let coerced = crate::conversions::truncate_to_column_fsp(coerced, &schema_cols[target_idx]);
506 check_unsigned_range(&coerced, &schema_cols[target_idx], target_idx)?;
507 new_values[target_idx] = coerced;
508 }
509 Ok(Some(new_values))
510}
511
512fn substitute_excluded_refs(
517 expr: Expr,
518 schema_cols: &[ColumnSchema],
519 incoming: &[Value<'static>],
520) -> Expr {
521 use spg_sql::ast::ColumnName;
522 match expr {
523 Expr::Column(ColumnName { qualifier, name })
524 if qualifier
525 .as_deref()
526 .is_some_and(|q| q.eq_ignore_ascii_case("excluded")) =>
527 {
528 let pos = schema_cols.iter().position(|c| c.name == name);
529 match pos {
530 Some(p) => {
531 let v = incoming.get(p).cloned().unwrap_or(Value::Null);
532 value_to_literal_expr(v)
533 .unwrap_or_else(|_| Expr::Literal(spg_sql::ast::Literal::Null))
534 }
535 None => Expr::Column(ColumnName { qualifier, name }),
536 }
537 }
538 Expr::Binary { op, lhs, rhs } => Expr::Binary {
539 op,
540 lhs: Box::new(substitute_excluded_refs(*lhs, schema_cols, incoming)),
541 rhs: Box::new(substitute_excluded_refs(*rhs, schema_cols, incoming)),
542 },
543 Expr::Unary { op, expr } => Expr::Unary {
544 op,
545 expr: Box::new(substitute_excluded_refs(*expr, schema_cols, incoming)),
546 },
547 Expr::FunctionCall { name, args } => Expr::FunctionCall {
548 name,
549 args: args
550 .into_iter()
551 .map(|a| substitute_excluded_refs(a, schema_cols, incoming))
552 .collect(),
553 },
554 Expr::Cast { expr, target } => Expr::Cast {
561 expr: Box::new(substitute_excluded_refs(*expr, schema_cols, incoming)),
562 target,
563 },
564 Expr::IsNull { expr, negated } => Expr::IsNull {
565 expr: Box::new(substitute_excluded_refs(*expr, schema_cols, incoming)),
566 negated,
567 },
568 Expr::Like {
569 expr,
570 pattern,
571 negated,
572 case_insensitive,
573 } => Expr::Like {
574 expr: Box::new(substitute_excluded_refs(*expr, schema_cols, incoming)),
575 pattern: Box::new(substitute_excluded_refs(*pattern, schema_cols, incoming)),
576 negated,
577 case_insensitive,
578 },
579 Expr::InList {
580 expr,
581 list,
582 negated,
583 } => Expr::InList {
584 expr: Box::new(substitute_excluded_refs(*expr, schema_cols, incoming)),
585 list: list
586 .into_iter()
587 .map(|e| substitute_excluded_refs(e, schema_cols, incoming))
588 .collect(),
589 negated,
590 },
591 Expr::Case {
592 operand,
593 branches,
594 else_branch,
595 } => Expr::Case {
596 operand: operand.map(|o| Box::new(substitute_excluded_refs(*o, schema_cols, incoming))),
597 branches: branches
598 .into_iter()
599 .map(|(w, t)| {
600 (
601 substitute_excluded_refs(w, schema_cols, incoming),
602 substitute_excluded_refs(t, schema_cols, incoming),
603 )
604 })
605 .collect(),
606 else_branch: else_branch
607 .map(|e| Box::new(substitute_excluded_refs(*e, schema_cols, incoming))),
608 },
609 other => other,
613 }
614}
615
616fn indexkeyable_type(ty: &spg_storage::DataType) -> bool {
622 use spg_storage::DataType as D;
623 matches!(
624 ty,
625 D::SmallInt
626 | D::Int
627 | D::BigInt
628 | D::Text
629 | D::Varchar(_)
630 | D::Char(_)
631 | D::Bool
632 | D::Uuid
633 | D::Date
634 | D::Timestamp
635 )
636}
637
638fn probe_btree(table: &spg_storage::Table, leading_pos: usize) -> Option<&spg_storage::Index> {
644 table.indices().iter().find(|i| {
645 matches!(i.kind, spg_storage::IndexKind::BTree(_))
646 && i.column_position == leading_pos
647 && i.expression.is_none()
648 && i.partial_predicate.is_none()
649 })
650}
651
652fn uc_probe_index<'t>(
664 table: &'t spg_storage::Table,
665 columns: &[usize],
666 nulls_not_distinct: bool,
667 mysql: bool,
668) -> Option<&'t spg_storage::Index> {
669 if nulls_not_distinct || columns.is_empty() {
670 return None;
671 }
672 if mysql {
678 return None;
679 }
680 let schema = table.schema();
681 let collation_ok = columns.iter().all(|&i| {
682 schema
683 .columns
684 .get(i)
685 .is_some_and(|c| !matches!(c.collation, spg_storage::Collation::CaseInsensitive))
686 });
687 if !collation_ok {
688 return None;
689 }
690 if !schema
691 .columns
692 .get(columns[0])
693 .is_some_and(|c| indexkeyable_type(&c.ty))
694 {
695 return None;
696 }
697 probe_btree(table, columns[0])
698}
699
700pub static UNIQ_PROBE_CALLS: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
717pub static UNIQ_PROBE_LOCATORS: core::sync::atomic::AtomicU64 =
718 core::sync::atomic::AtomicU64::new(0);
719
720fn probe_key_conflict(
721 table: &spg_storage::Table,
722 idx: &spg_storage::Index,
723 leading_val: &Value<'static>,
724 key: &[Value<'static>],
725 fold: &dyn Fn(&[Value<'static>]) -> Vec<Value<'static>>,
726) -> Option<usize> {
727 let ik = spg_storage::IndexKey::from_value(leading_val)?;
728 crate::bump_counter!(crate::constraints::UNIQ_PROBE_CALLS);
729 crate::bump_counter!(
730 crate::constraints::UNIQ_PROBE_LOCATORS,
731 idx.lookup_eq(&ik).len() as u64
732 );
733 for loc in idx.lookup_eq(&ik) {
734 let spg_storage::RowLocator::Hot(ri) = loc else {
735 continue;
736 };
737 if table.headers().get(*ri).is_some_and(|h| h.is_deleted()) {
738 continue;
739 }
740 let Some(prow) = table.rows().get(*ri) else {
741 continue;
742 };
743 if fold(&prow.values) == key {
744 return Some(*ri);
745 }
746 }
747 None
748}
749
750pub(crate) fn enforce_uniqueness_inserts(
751 catalog: &Catalog,
752 child_table: &str,
753 constraints: &[spg_storage::UniquenessConstraint],
754 rows: &[Vec<Value<'static>>],
755 mysql: bool,
756) -> Result<(), EngineError> {
757 if constraints.is_empty() {
758 return Ok(());
759 }
760 let table = catalog.get(child_table).ok_or_else(|| {
761 EngineError::Storage(StorageError::TableNotFound {
762 name: child_table.into(),
763 })
764 })?;
765 let schema = table.schema();
766 for uc in constraints {
776 let fold_key = |values: &[Value<'static>]| -> Vec<Value<'static>> {
777 uc.columns
778 .iter()
779 .map(|&i| {
780 let v = values.get(i).cloned().unwrap_or(Value::Null);
781 collated_key_cell(&v, i, schema, mysql)
782 })
783 .collect()
784 };
785 if let Some(idx) = uc_probe_index(table, &uc.columns, uc.nulls_not_distinct, mysql) {
792 let mut batch_seen: hashbrown::HashSet<String> =
793 hashbrown::HashSet::with_capacity(rows.len());
794 let mut probe_ok = true;
795 for row_values in rows.iter() {
796 let key = fold_key(row_values);
797 if key.iter().any(|v| matches!(v, Value::Null)) && !uc.nulls_not_distinct {
798 continue;
799 }
800 let leading = row_values
801 .get(uc.columns[0])
802 .cloned()
803 .unwrap_or(Value::Null);
804 if spg_storage::IndexKey::from_value(&leading).is_none() {
805 probe_ok = false;
808 break;
809 }
810 let dup_in_batch = !batch_seen.insert(aggregate::encode_key(&key));
811 if dup_in_batch
812 || probe_key_conflict(table, idx, &leading, &key, &fold_key).is_some()
813 {
814 let conname = crate::system_catalog::pg_unique_conname(table, uc, child_table);
815 let detail = unique_key_detail(
816 &uc.columns
817 .iter()
818 .map(|&i| table.schema().columns[i].name.clone())
819 .collect::<Vec<_>>(),
820 &key,
821 );
822 return Err(EngineError::Unsupported(alloc::format!(
823 "duplicate key value violates unique constraint \"{conname}\" \
824 on table \"{child_table}\"{detail}"
825 )));
826 }
827 }
828 if probe_ok {
829 continue;
830 }
831 }
832 let mut seen: hashbrown::HashSet<String> =
833 hashbrown::HashSet::with_capacity(table.rows().len() + rows.len());
834 for (row_idx, prow) in table.rows().iter().enumerate() {
835 if table.headers().get(row_idx).is_some_and(|h| h.is_deleted()) {
845 continue;
846 }
847 let key = fold_key(&prow.values);
848 if key.iter().any(|v| matches!(v, Value::Null)) && !uc.nulls_not_distinct {
849 continue;
850 }
851 seen.insert(aggregate::encode_key(&key));
852 }
853 for (batch_idx, row_values) in rows.iter().enumerate() {
854 let key = fold_key(row_values);
855 if key.iter().any(|v| matches!(v, Value::Null)) && !uc.nulls_not_distinct {
856 continue;
857 }
858 if !seen.insert(aggregate::encode_key(&key)) {
859 let conname = crate::system_catalog::pg_unique_conname(table, uc, child_table);
863 let detail = unique_key_detail(
864 &uc.columns
865 .iter()
866 .map(|&i| table.schema().columns[i].name.clone())
867 .collect::<Vec<_>>(),
868 &key,
869 );
870 return Err(EngineError::Unsupported(alloc::format!(
871 "duplicate key value violates unique constraint \"{conname}\" \
872 on table \"{child_table}\"{detail}"
873 )));
874 }
875 }
876 }
877 Ok(())
878}
879
880fn exclude_op_binop(op: &str) -> Option<spg_sql::ast::BinOp> {
883 use spg_sql::ast::BinOp;
884 Some(match op {
885 "&&" => BinOp::InetOverlap,
886 "=" => BinOp::Eq,
887 "@>" => BinOp::JsonContains,
888 "<@" => BinOp::JsonContainedBy,
889 "&<" => BinOp::OverLeft,
890 "&>" => BinOp::OverRight,
891 _ => return None,
892 })
893}
894
895fn excl_rows_conflict(
900 ex: &spg_storage::ExclusionConstraint,
901 newr: &[Value<'static>],
902 oldr: &[Value<'static>],
903) -> Result<bool, EngineError> {
904 for (pos, op) in &ex.elements {
905 let a = newr.get(*pos).cloned().unwrap_or(Value::Null);
906 let b = oldr.get(*pos).cloned().unwrap_or(Value::Null);
907 if matches!(a, Value::Null) || matches!(b, Value::Null) {
908 return Ok(false);
909 }
910 let binop = exclude_op_binop(op).ok_or_else(|| {
911 EngineError::Unsupported(alloc::format!("unsupported EXCLUDE operator {op:?}"))
912 })?;
913 match eval::apply_binary(binop, a, b)? {
916 Value::Bool(true) => {}
917 _ => return Ok(false),
918 }
919 }
920 Ok(true)
921}
922
923enum ExclProbe {
926 Conflict(Vec<Value<'static>>),
928 NoOverlap,
931 Inconclusive,
935}
936
937enum KeyProbe {
939 Conflict(Vec<Value<'static>>),
940 LiveClear,
942 AllDead,
944 Absent,
946}
947
948fn excl_probe_existing(
958 table: &spg_storage::Table,
959 ex: &spg_storage::ExclusionConstraint,
960 index_col: usize,
961 newr: &[Value<'static>],
962 exclude: Option<&hashbrown::HashSet<usize>>,
963) -> Result<ExclProbe, EngineError> {
964 let Some(map) = table.excl_range_index(index_col) else {
965 return Ok(ExclProbe::Inconclusive);
966 };
967 let cand = newr.get(index_col).cloned().unwrap_or(Value::Null);
968 if matches!(cand, Value::Null) {
969 return Ok(ExclProbe::NoOverlap); }
971 let Some(cand_key) = spg_storage::range_excl_index_key(&cand) else {
972 return Ok(ExclProbe::Inconclusive); };
974 let probe_entry = |entry: Option<(&(i128, u8), &Vec<spg_storage::RowLocator>)>|
975 -> Result<KeyProbe, EngineError> {
976 let Some((_, locs)) = entry else {
977 return Ok(KeyProbe::Absent);
978 };
979 let mut saw_live = false;
980 for loc in locs {
981 if locator_is_tombstoned(table, loc) {
982 continue;
983 }
984 let spg_storage::RowLocator::Hot(ri) = loc else {
985 continue; };
987 if exclude.is_some_and(|s| s.contains(ri)) {
991 continue;
992 }
993 let Some(prow) = table.rows().get(*ri) else {
994 continue;
995 };
996 saw_live = true;
997 if excl_rows_conflict(ex, newr, &prow.values)? {
998 return Ok(KeyProbe::Conflict(prow.values.clone()));
999 }
1000 }
1001 Ok(if saw_live {
1002 KeyProbe::LiveClear
1003 } else {
1004 KeyProbe::AllDead
1005 })
1006 };
1007 let pred = probe_entry(map.predecessor(&cand_key))?;
1008 if let KeyProbe::Conflict(old) = pred {
1009 return Ok(ExclProbe::Conflict(old));
1010 }
1011 let succ = probe_entry(
1012 map.range(
1013 core::ops::Bound::Included(&cand_key),
1014 core::ops::Bound::Unbounded,
1015 )
1016 .next(),
1017 )?;
1018 if let KeyProbe::Conflict(old) = succ {
1019 return Ok(ExclProbe::Conflict(old));
1020 }
1021 if matches!(pred, KeyProbe::AllDead) || matches!(succ, KeyProbe::AllDead) {
1022 Ok(ExclProbe::Inconclusive)
1023 } else {
1024 Ok(ExclProbe::NoOverlap)
1025 }
1026}
1027
1028pub(crate) fn enforce_exclusion_inserts(
1039 catalog: &Catalog,
1040 child_table: &str,
1041 constraints: &[spg_storage::ExclusionConstraint],
1042 rows: &[Vec<Value<'static>>],
1043) -> Result<(), EngineError> {
1044 if constraints.is_empty() {
1045 return Ok(());
1046 }
1047 let table = catalog.get(child_table).ok_or_else(|| {
1048 EngineError::Storage(StorageError::TableNotFound {
1049 name: child_table.into(),
1050 })
1051 })?;
1052 let conflicts = excl_rows_conflict;
1053 for ex in constraints {
1054 let idx_col = ex
1059 .elements
1060 .iter()
1061 .find(|(pos, op)| op == "&&" && table.excl_range_index(*pos).is_some())
1062 .map(|(pos, _)| *pos);
1063 for newr in rows.iter() {
1066 let mut proved_clear = false;
1067 if let Some(col) = idx_col {
1068 match excl_probe_existing(table, ex, col, newr, None)? {
1069 ExclProbe::Conflict(old) => {
1070 return Err(exclusion_violation(table, ex, child_table, newr, &old));
1071 }
1072 ExclProbe::NoOverlap => proved_clear = true,
1073 ExclProbe::Inconclusive => {} }
1075 }
1076 if proved_clear {
1077 continue;
1078 }
1079 for (row_idx, prow) in table.rows().iter().enumerate() {
1082 if table.headers().get(row_idx).is_some_and(|h| h.is_deleted()) {
1083 continue;
1084 }
1085 if conflicts(ex, newr, &prow.values)? {
1086 return Err(exclusion_violation(
1087 table,
1088 ex,
1089 child_table,
1090 newr,
1091 &prow.values,
1092 ));
1093 }
1094 }
1095 }
1096 if !(ex.elements.len() == 1
1110 && ex.elements[0].1 == "&&"
1111 && intra_batch_proven_disjoint(ex.elements[0].0, rows)?)
1112 {
1113 for i in 0..rows.len() {
1114 for j in (i + 1)..rows.len() {
1115 if conflicts(ex, &rows[j], &rows[i])? {
1116 return Err(exclusion_violation(
1117 table,
1118 ex,
1119 child_table,
1120 &rows[j],
1121 &rows[i],
1122 ));
1123 }
1124 }
1125 }
1126 }
1127 }
1128 Ok(())
1129}
1130
1131fn range_lower_sort_key(v: &Value<'_>) -> Option<(i128, u8)> {
1138 let Value::Range {
1139 lower,
1140 lower_inc,
1141 empty,
1142 ..
1143 } = v
1144 else {
1145 return None;
1146 };
1147 if *empty {
1148 return None;
1149 }
1150 let key = match lower {
1151 None => i128::MIN,
1152 Some(b) => match b.as_ref() {
1153 Value::SmallInt(n) => i128::from(*n),
1154 Value::Int(n) => i128::from(*n),
1155 Value::BigInt(n) => i128::from(*n),
1156 Value::Date(n) => i128::from(*n),
1159 Value::Timestamp(n) => i128::from(*n),
1160 _ => return None,
1161 },
1162 };
1163 Some((key, u8::from(!*lower_inc)))
1164}
1165
1166fn intra_batch_proven_disjoint(
1176 pos: usize,
1177 rows: &[Vec<Value<'static>>],
1178) -> Result<bool, EngineError> {
1179 let mut keyed: Vec<((i128, u8), usize)> = Vec::with_capacity(rows.len());
1180 for (i, r) in rows.iter().enumerate() {
1181 match r.get(pos) {
1182 None => return Ok(false), Some(Value::Null) => continue, Some(v @ Value::Range { empty, .. }) => {
1185 if *empty {
1186 continue; }
1188 match range_lower_sort_key(v) {
1189 Some(k) => keyed.push((k, i)),
1190 None => return Ok(false), }
1192 }
1193 Some(_) => return Ok(false), }
1195 }
1196 if keyed.len() < 2 {
1197 return Ok(true); }
1199 keyed.sort_by_key(|k| k.0);
1200 for w in keyed.windows(2) {
1201 let a = rows[w[0].1][pos].clone();
1202 let b = rows[w[1].1][pos].clone();
1203 if let Value::Bool(true) = eval::apply_binary(spg_sql::ast::BinOp::InetOverlap, a, b)? {
1205 return Ok(false);
1206 }
1207 }
1208 Ok(true) }
1210
1211pub(crate) fn enforce_exclusion_updates(
1217 catalog: &Catalog,
1218 table_name: &str,
1219 constraints: &[spg_storage::ExclusionConstraint],
1220 planned: &[(usize, Vec<Value<'static>>)],
1221) -> Result<(), EngineError> {
1222 if constraints.is_empty() || planned.is_empty() {
1223 return Ok(());
1224 }
1225 let table = catalog.get(table_name).ok_or_else(|| {
1226 EngineError::Storage(StorageError::TableNotFound {
1227 name: table_name.into(),
1228 })
1229 })?;
1230 let updated: hashbrown::HashSet<usize> = planned.iter().map(|(p, _)| *p).collect();
1231 let conflicts = excl_rows_conflict;
1232 for ex in constraints {
1233 let idx_col = ex
1237 .elements
1238 .iter()
1239 .find(|(pos, op)| op == "&&" && table.excl_range_index(*pos).is_some())
1240 .map(|(pos, _)| *pos);
1241 for (_pos, newr) in planned {
1242 let mut proved_clear = false;
1243 if let Some(col) = idx_col {
1244 match excl_probe_existing(table, ex, col, newr, Some(&updated))? {
1245 ExclProbe::Conflict(old) => {
1246 return Err(exclusion_violation(table, ex, table_name, newr, &old));
1247 }
1248 ExclProbe::NoOverlap => proved_clear = true,
1249 ExclProbe::Inconclusive => {}
1250 }
1251 }
1252 if proved_clear {
1253 continue;
1254 }
1255 for (row_idx, prow) in table.rows().iter().enumerate() {
1256 if table.headers().get(row_idx).is_some_and(|h| h.is_deleted()) {
1257 continue;
1258 }
1259 if updated.contains(&row_idx) {
1260 continue;
1261 }
1262 if conflicts(ex, newr, &prow.values)? {
1263 return Err(exclusion_violation(
1264 table,
1265 ex,
1266 table_name,
1267 newr,
1268 &prow.values,
1269 ));
1270 }
1271 }
1272 }
1273 for i in 0..planned.len() {
1274 for j in (i + 1)..planned.len() {
1275 if conflicts(ex, &planned[j].1, &planned[i].1)? {
1276 return Err(exclusion_violation(
1277 table,
1278 ex,
1279 table_name,
1280 &planned[j].1,
1281 &planned[i].1,
1282 ));
1283 }
1284 }
1285 }
1286 }
1287 Ok(())
1288}
1289
1290fn exclusion_violation(
1296 table: &spg_storage::Table,
1297 ex: &spg_storage::ExclusionConstraint,
1298 child_table: &str,
1299 newr: &[Value<'static>],
1300 oldr: &[Value<'static>],
1301) -> EngineError {
1302 let render = |vals: &[Value<'static>]| -> (String, String) {
1303 let cols = ex
1304 .elements
1305 .iter()
1306 .map(|(p, _)| table.schema().columns[*p].name.clone())
1307 .collect::<Vec<_>>()
1308 .join(", ");
1309 let rendered = ex
1310 .elements
1311 .iter()
1312 .map(|(p, _)| {
1313 let v = vals.get(*p).cloned().unwrap_or(Value::Null);
1314 match v {
1315 Value::Text(s) => s.to_string(),
1316 other => crate::eval::value_to_text(&other),
1317 }
1318 })
1319 .collect::<Vec<_>>()
1320 .join(", ");
1321 (cols, rendered)
1322 };
1323 let (cols, new_vals) = render(newr);
1324 let (_, old_vals) = render(oldr);
1325 EngineError::Unsupported(alloc::format!(
1326 "conflicting key value violates exclusion constraint \"{}\" \
1327 on table \"{child_table}\" DETAIL: Key ({cols})=({new_vals}) \
1328 conflicts with existing key ({cols})=({old_vals}).",
1329 ex.name
1330 ))
1331}
1332
1333fn unique_key_detail(cols: &[String], key: &[Value<'_>]) -> String {
1338 let vals = key
1339 .iter()
1340 .map(|v| match v {
1341 Value::Text(s) => s.to_string(),
1342 Value::Null => alloc::string::String::from("null"),
1345 other => crate::eval::value_to_text(other),
1346 })
1347 .collect::<Vec<_>>()
1348 .join(", ");
1349 alloc::format!(
1350 " DETAIL: Key ({})=({vals}) already exists.",
1351 cols.join(", ")
1352 )
1353}
1354
1355fn fk_violation_message(
1358 child: &spg_storage::Table,
1359 child_table: &str,
1360 fk: &spg_storage::ForeignKeyConstraint,
1361 key_vals: &[&Value<'_>],
1362) -> String {
1363 let conname = crate::system_catalog::pg_fk_conname(child, fk, child_table);
1364 let cols = fk
1365 .local_columns
1366 .iter()
1367 .map(|&p| {
1368 child
1369 .schema()
1370 .columns
1371 .get(p)
1372 .map_or_else(|| alloc::format!("col{p}"), |c| c.name.clone())
1373 })
1374 .collect::<Vec<_>>()
1375 .join(", ");
1376 let vals = key_vals
1377 .iter()
1378 .map(|v| match v {
1379 Value::Text(s) => s.to_string(),
1380 other => crate::eval::value_to_text(other),
1381 })
1382 .collect::<Vec<_>>()
1383 .join(", ");
1384 alloc::format!(
1385 "insert or update on table \"{child_table}\" violates foreign key \
1386 constraint \"{conname}\" DETAIL: Key ({cols})=({vals}) is not present \
1387 in table \"{}\".",
1388 fk.parent_table
1389 )
1390}
1391
1392fn fk_restrict_message(
1396 catalog: &Catalog,
1397 parent_name: &str,
1398 child: &spg_storage::Table,
1399 child_name: &str,
1400 fk: &spg_storage::ForeignKeyConstraint,
1401 parent_key: &[&Value<'_>],
1402 action: spg_storage::FkAction,
1403) -> String {
1404 let conname = crate::system_catalog::pg_fk_conname(child, fk, child_name);
1405 let pcols = match catalog.get(parent_name) {
1406 Some(parent) => fk
1407 .parent_columns
1408 .iter()
1409 .map(|&p| {
1410 parent
1411 .schema()
1412 .columns
1413 .get(p)
1414 .map_or_else(|| alloc::format!("col{p}"), |c| c.name.clone())
1415 })
1416 .collect::<Vec<_>>()
1417 .join(", "),
1418 None => "?".into(),
1419 };
1420 let vals = parent_key
1421 .iter()
1422 .map(|v| match v {
1423 Value::Text(s) => s.to_string(),
1424 other => crate::eval::value_to_text(other),
1425 })
1426 .collect::<Vec<_>>()
1427 .join(", ");
1428 if matches!(action, spg_storage::FkAction::Restrict) {
1439 return alloc::format!(
1440 "update or delete on table \"{parent_name}\" violates RESTRICT \
1441 setting of foreign key constraint \"{conname}\" on table \"{child_name}\" \
1442 DETAIL: Key ({pcols})=({vals}) is referenced from table \"{child_name}\"."
1443 );
1444 }
1445 alloc::format!(
1446 "update or delete on table \"{parent_name}\" violates foreign key \
1447 constraint \"{conname}\" on table \"{child_name}\" \
1448 DETAIL: Key ({pcols})=({vals}) is still referenced from table \"{child_name}\"."
1449 )
1450}
1451
1452fn collated_key_cell(
1459 v: &spg_storage::Value,
1460 column_position: usize,
1461 schema: &spg_storage::TableSchema,
1462 mysql: bool,
1463) -> spg_storage::Value<'static> {
1464 let explicit_binary = schema
1475 .columns
1476 .get(column_position)
1477 .is_some_and(|c| matches!(c.collation, spg_storage::Collation::Binary));
1478 if mysql && !explicit_binary {
1479 match v {
1480 spg_storage::Value::Text(s) => {
1481 return spg_storage::Value::text(spg_storage::mysql_compare_fold(s));
1482 }
1483 spg_storage::Value::BpChar(s) => {
1484 return spg_storage::Value::text(spg_storage::mysql_ci_fold(
1485 s.trim_end_matches(' '),
1486 ));
1487 }
1488 _ => return v.clone().into_owned(),
1489 }
1490 }
1491 match (v, schema.columns.get(column_position).map(|c| c.collation)) {
1492 (spg_storage::Value::Text(s), Some(spg_storage::Collation::CaseInsensitive)) => {
1493 spg_storage::Value::text(s.to_ascii_lowercase())
1494 }
1495 _ => v.clone().into_owned(),
1496 }
1497}
1498
1499fn predicate_truthy(v: &spg_storage::Value) -> bool {
1507 use spg_storage::Value as V;
1508 match v {
1509 V::Bool(b) => *b,
1510 V::Int(n) => *n != 0,
1511 V::BigInt(n) => *n != 0,
1512 V::SmallInt(n) => *n != 0,
1513 _ => false,
1514 }
1515}
1516
1517pub(crate) fn check_existing_unique_violation(
1522 idx: &spg_storage::Index,
1523 schema: &spg_storage::TableSchema,
1524 rows: &[spg_storage::Row<'static>],
1525 mysql: bool,
1526) -> Result<(), EngineError> {
1527 let predicate_expr = match idx.partial_predicate.as_deref() {
1528 Some(s) => Some(spg_sql::parser::parse_expression(s).map_err(|e| {
1529 EngineError::Unsupported(alloc::format!(
1530 "stored partial predicate {s:?} failed to re-parse: {e:?}"
1531 ))
1532 })?),
1533 None => None,
1534 };
1535 let ctx = eval::EvalContext::new(&schema.columns, None);
1536 let key_positions = unique_key_positions(idx);
1537 let mut seen: alloc::vec::Vec<alloc::vec::Vec<spg_storage::Value<'static>>> =
1538 alloc::vec::Vec::new();
1539 for row in rows {
1540 if let Some(expr) = &predicate_expr {
1541 let v = eval::eval_expr(expr, row, &ctx).map_err(|e| {
1542 EngineError::Unsupported(alloc::format!(
1543 "evaluating UNIQUE INDEX predicate against existing row: {e:?}"
1544 ))
1545 })?;
1546 if !predicate_truthy(&v) {
1547 continue;
1548 }
1549 }
1550 let key: alloc::vec::Vec<spg_storage::Value<'static>> = key_positions
1551 .iter()
1552 .map(|&p| {
1553 let v = row
1554 .values
1555 .get(p)
1556 .cloned()
1557 .unwrap_or(spg_storage::Value::Null);
1558 collated_key_cell(&v, p, schema, mysql)
1559 })
1560 .collect();
1561 if !idx.nulls_not_distinct && key.iter().any(|v| matches!(v, spg_storage::Value::Null)) {
1565 continue;
1566 }
1567 if seen.iter().any(|other| *other == key) {
1568 return Err(EngineError::Unsupported(alloc::format!(
1570 "could not create unique index {:?}",
1571 idx.name
1572 )));
1573 }
1574 seen.push(key);
1575 }
1576 Ok(())
1577}
1578
1579fn unique_key_positions(idx: &spg_storage::Index) -> alloc::vec::Vec<usize> {
1583 let mut out = alloc::vec::Vec::with_capacity(1 + idx.extra_column_positions.len());
1584 out.push(idx.column_position);
1585 out.extend_from_slice(&idx.extra_column_positions);
1586 out
1587}
1588
1589pub(crate) fn enforce_unique_index_inserts(
1597 catalog: &Catalog,
1598 table_name: &str,
1599 rows: &[alloc::vec::Vec<spg_storage::Value<'static>>],
1600 mysql: bool,
1601) -> Result<(), EngineError> {
1602 let table = catalog.get(table_name).ok_or_else(|| {
1603 EngineError::Storage(StorageError::TableNotFound {
1604 name: table_name.into(),
1605 })
1606 })?;
1607 let schema = table.schema();
1608 let ctx = eval::EvalContext::new(&schema.columns, None);
1609 for idx in table.indices() {
1610 if !idx.is_unique {
1611 continue;
1612 }
1613 let predicate_expr = match idx.partial_predicate.as_deref() {
1615 Some(s) => Some(spg_sql::parser::parse_expression(s).map_err(|e| {
1616 EngineError::Unsupported(alloc::format!(
1617 "UNIQUE INDEX {:?} predicate {s:?} failed to re-parse: {e:?}",
1618 idx.name
1619 ))
1620 })?),
1621 None => None,
1622 };
1623 let expr_key = match idx.expression.as_deref() {
1629 Some(s) => Some(spg_sql::parser::parse_expression(s).map_err(|e| {
1630 EngineError::Unsupported(alloc::format!(
1631 "UNIQUE INDEX {:?} expression {s:?} failed to re-parse: {e:?}",
1632 idx.name
1633 ))
1634 })?),
1635 None => None,
1636 };
1637 let key_positions = unique_key_positions(idx);
1638 let key_col_names: alloc::vec::Vec<alloc::string::String> = match &expr_key {
1641 Some(_) => alloc::vec![idx.expression.clone().unwrap_or_else(|| idx.name.clone())],
1642 None => key_positions
1643 .iter()
1644 .map(|&p| {
1645 schema
1646 .columns
1647 .get(p)
1648 .map_or_else(|| alloc::format!("col{p}"), |c| c.name.clone())
1649 })
1650 .collect(),
1651 };
1652 let key_of = |values: &[spg_storage::Value<'static>]| -> Result<alloc::vec::Vec<spg_storage::Value<'static>>, EngineError> {
1653 if let Some(expr) = &expr_key {
1654 let tmp_row = spg_storage::Row {
1655 values: values.to_vec(),
1656 };
1657 let v = eval::eval_expr(expr, &tmp_row, &ctx).map_err(|e| {
1658 EngineError::Unsupported(alloc::format!(
1659 "UNIQUE INDEX {:?} expression eval: {e:?}",
1660 idx.name
1661 ))
1662 })?;
1663 return Ok(alloc::vec![v]);
1664 }
1665 Ok(key_positions
1666 .iter()
1667 .map(|&p| {
1668 let v = values.get(p).cloned().unwrap_or(spg_storage::Value::Null);
1669 collated_key_cell(&v, p, schema, mysql)
1670 })
1671 .collect())
1672 };
1673 let participates = |values: &[spg_storage::Value<'static>]| -> Result<bool, EngineError> {
1674 let Some(expr) = &predicate_expr else {
1675 return Ok(true);
1676 };
1677 let tmp_row = spg_storage::Row {
1678 values: values.to_vec(),
1679 };
1680 let v = eval::eval_expr(expr, &tmp_row, &ctx).map_err(|e| {
1681 EngineError::Unsupported(alloc::format!(
1682 "UNIQUE INDEX {:?} predicate eval: {e:?}",
1683 idx.name
1684 ))
1685 })?;
1686 Ok(predicate_truthy(&v))
1687 };
1688 if idx.expression.is_none()
1693 && idx.partial_predicate.is_none()
1694 && !idx.nulls_not_distinct
1695 && matches!(idx.kind, spg_storage::IndexKind::BTree(_))
1696 {
1697 let positions = unique_key_positions(idx);
1698 let schema_ok = !mysql
1699 && positions.iter().all(|&i| {
1700 schema.columns.get(i).is_some_and(|c| {
1701 !matches!(c.collation, spg_storage::Collation::CaseInsensitive)
1702 })
1703 })
1704 && schema
1705 .columns
1706 .get(idx.column_position)
1707 .is_some_and(|c| indexkeyable_type(&c.ty));
1708 if schema_ok {
1709 let fold =
1710 |values: &[spg_storage::Value<'static>]| -> Vec<spg_storage::Value<'static>> {
1711 positions
1712 .iter()
1713 .map(|&p| {
1714 let v = values.get(p).cloned().unwrap_or(spg_storage::Value::Null);
1715 collated_key_cell(&v, p, schema, mysql)
1716 })
1717 .collect()
1718 };
1719 let mut batch_seen: hashbrown::HashSet<String> =
1720 hashbrown::HashSet::with_capacity(rows.len());
1721 let mut probe_ok = true;
1722 for row_values in rows.iter() {
1723 let key = fold(row_values);
1724 if key.iter().any(|v| matches!(v, spg_storage::Value::Null)) {
1725 continue;
1726 }
1727 let leading = row_values
1728 .get(idx.column_position)
1729 .cloned()
1730 .unwrap_or(spg_storage::Value::Null);
1731 if spg_storage::IndexKey::from_value(&leading).is_none() {
1732 probe_ok = false;
1733 break;
1734 }
1735 if !batch_seen.insert(aggregate::encode_key(&key))
1736 || probe_key_conflict(table, idx, &leading, &key, &fold).is_some()
1737 {
1738 let detail = unique_key_detail(&key_col_names, &key);
1742 return Err(EngineError::Unsupported(alloc::format!(
1743 "duplicate key value violates unique constraint \"{}\" \
1744 on table \"{table_name}\"{detail}",
1745 idx.name
1746 )));
1747 }
1748 }
1749 if probe_ok {
1750 continue;
1751 }
1752 }
1753 }
1754 let mut seen: hashbrown::HashSet<String> =
1759 hashbrown::HashSet::with_capacity(table.rows().len() + rows.len());
1760 for (row_idx, prow) in table.rows().iter().enumerate() {
1761 if table.headers().get(row_idx).is_some_and(|h| h.is_deleted()) {
1767 continue;
1768 }
1769 if !participates(&prow.values)? {
1770 continue;
1771 }
1772 let key = key_of(&prow.values)?;
1773 if !idx.nulls_not_distinct && key.iter().any(|v| matches!(v, spg_storage::Value::Null))
1776 {
1777 continue;
1778 }
1779 seen.insert(aggregate::encode_key(&key));
1780 }
1781 for (batch_idx, row_values) in rows.iter().enumerate() {
1782 if !participates(row_values)? {
1783 continue;
1784 }
1785 let key = key_of(row_values)?;
1786 if !idx.nulls_not_distinct && key.iter().any(|v| matches!(v, spg_storage::Value::Null))
1787 {
1788 continue;
1789 }
1790 if !seen.insert(aggregate::encode_key(&key)) {
1791 let detail = unique_key_detail(&key_col_names, &key);
1794 return Err(EngineError::Unsupported(alloc::format!(
1795 "duplicate key value violates unique constraint \"{}\" \
1796 on table \"{table_name}\"{detail}",
1797 idx.name
1798 )));
1799 }
1800 }
1801 }
1802 Ok(())
1803}
1804
1805#[allow(clippy::too_many_lines)]
1844fn probe_replay(
1845 table: &spg_storage::Table,
1846 idx: &spg_storage::Index,
1847 columns: &[usize],
1848 planned: &[(usize, Vec<Value<'static>>)],
1849 schema: &spg_storage::TableSchema,
1850 key_str: &KeyStrFn<'_>,
1851 on_conflict: &dyn Fn(usize) -> EngineError,
1852 mysql: bool,
1853) -> Result<bool, EngineError> {
1854 let fold = |values: &[Value<'static>]| -> Vec<Value<'static>> {
1855 columns
1856 .iter()
1857 .map(|&i| {
1858 let v = values.get(i).cloned().unwrap_or(Value::Null);
1859 collated_key_cell(&v, i, schema, mysql)
1860 })
1861 .collect()
1862 };
1863 let mut added: hashbrown::HashSet<String> = hashbrown::HashSet::new();
1864 let mut removed: hashbrown::HashSet<String> = hashbrown::HashSet::new();
1865 for (pos, new_vals) in planned {
1866 let old_key = match table.rows().get(*pos) {
1867 Some(r) => key_str(&r.values)?,
1868 None => None,
1869 };
1870 let new_key = key_str(new_vals)?;
1871 if old_key == new_key {
1872 continue;
1873 }
1874 if let Some(ok) = old_key {
1875 if !added.remove(&ok) {
1876 removed.insert(ok);
1877 }
1878 }
1879 if let Some(nk) = new_key {
1880 if added.contains(&nk) {
1881 return Err(on_conflict(*pos));
1882 }
1883 if !removed.contains(&nk) {
1884 let key_vec = fold(new_vals);
1885 let leading = new_vals.get(columns[0]).cloned().unwrap_or(Value::Null);
1886 if spg_storage::IndexKey::from_value(&leading).is_none() {
1887 return Ok(false);
1888 }
1889 if let Some(ri) = probe_key_conflict(table, idx, &leading, &key_vec, &fold)
1890 && ri != *pos
1891 {
1892 return Err(on_conflict(*pos));
1893 }
1894 }
1895 added.insert(nk);
1896 }
1897 }
1898 Ok(true)
1899}
1900
1901pub(crate) fn enforce_unique_updates(
1902 catalog: &Catalog,
1903 table_name: &str,
1904 planned: &[(usize, Vec<Value<'static>>)],
1905 changed_cols: &hashbrown::HashSet<usize>,
1906 mysql: bool,
1907) -> Result<(), EngineError> {
1908 if planned.is_empty() {
1909 return Ok(());
1910 }
1911 let table = catalog.get(table_name).ok_or_else(|| {
1912 EngineError::Storage(StorageError::TableNotFound {
1913 name: table_name.into(),
1914 })
1915 })?;
1916 let schema = table.schema();
1917
1918 let replay = |key_str: &KeyStrFn<'_>,
1923 on_conflict: &dyn Fn(usize) -> EngineError|
1924 -> Result<(), EngineError> {
1925 let mut index: hashbrown::HashSet<String> =
1926 hashbrown::HashSet::with_capacity(table.rows().len());
1927 for (row_idx, prow) in table.rows().iter().enumerate() {
1928 if table.headers().get(row_idx).is_some_and(|h| h.is_deleted()) {
1929 continue;
1930 }
1931 if let Some(k) = key_str(&prow.values)? {
1932 index.insert(k);
1933 }
1934 }
1935 for (pos, new_vals) in planned {
1936 let old_key = match table.rows().get(*pos) {
1937 Some(r) => key_str(&r.values)?,
1938 None => None,
1939 };
1940 let new_key = key_str(new_vals)?;
1941 if old_key == new_key {
1942 continue; }
1944 if let Some(ok) = &old_key {
1945 index.remove(ok);
1946 }
1947 if let Some(nk) = new_key
1948 && !index.insert(nk)
1949 {
1950 return Err(on_conflict(*pos));
1951 }
1952 }
1953 Ok(())
1954 };
1955
1956 for uc in &schema.uniqueness_constraints {
1958 if !uc.columns.iter().any(|c| changed_cols.contains(c)) {
1959 continue;
1960 }
1961 let key_str = |values: &[Value<'static>]| -> Result<Option<String>, EngineError> {
1962 let key: Vec<Value<'static>> = uc
1963 .columns
1964 .iter()
1965 .map(|&i| {
1966 let v = values.get(i).cloned().unwrap_or(Value::Null);
1967 collated_key_cell(&v, i, schema, mysql)
1968 })
1969 .collect();
1970 if key.iter().any(|v| matches!(v, Value::Null)) && !uc.nulls_not_distinct {
1971 return Ok(None);
1972 }
1973 Ok(Some(aggregate::encode_key(&key)))
1974 };
1975 let on_conflict = |_pos: usize| -> EngineError {
1976 let conname = if uc.is_primary_key {
1979 alloc::format!("{table_name}_pkey")
1980 } else {
1981 let cols = uc
1982 .columns
1983 .iter()
1984 .map(|&i| schema.columns[i].name.clone())
1985 .collect::<Vec<_>>()
1986 .join("_");
1987 alloc::format!("{table_name}_{cols}_key")
1988 };
1989 EngineError::Unsupported(alloc::format!(
1990 "duplicate key value violates unique constraint \"{conname}\" \
1991 on table \"{table_name}\""
1992 ))
1993 };
1994 if let Some(pidx) = uc_probe_index(table, &uc.columns, uc.nulls_not_distinct, mysql)
1996 && probe_replay(
1997 table,
1998 pidx,
1999 &uc.columns,
2000 planned,
2001 schema,
2002 &key_str,
2003 &on_conflict,
2004 mysql,
2005 )?
2006 {
2007 continue;
2008 }
2009 replay(&key_str, &on_conflict)?;
2010 }
2011
2012 let ctx = eval::EvalContext::new(&schema.columns, None);
2014 for idx in table.indices() {
2015 if !idx.is_unique {
2016 continue;
2017 }
2018 let is_expr_or_partial = idx.expression.is_some() || idx.partial_predicate.is_some();
2019 let key_positions = unique_key_positions(idx);
2020 if !is_expr_or_partial && !key_positions.iter().any(|c| changed_cols.contains(c)) {
2023 continue;
2024 }
2025 let predicate_expr = match idx.partial_predicate.as_deref() {
2026 Some(s) => Some(spg_sql::parser::parse_expression(s).map_err(|e| {
2027 EngineError::Unsupported(alloc::format!(
2028 "UNIQUE INDEX {:?} predicate {s:?} failed to re-parse: {e:?}",
2029 idx.name
2030 ))
2031 })?),
2032 None => None,
2033 };
2034 let expr_key = match idx.expression.as_deref() {
2035 Some(s) => Some(spg_sql::parser::parse_expression(s).map_err(|e| {
2036 EngineError::Unsupported(alloc::format!(
2037 "UNIQUE INDEX {:?} expression {s:?} failed to re-parse: {e:?}",
2038 idx.name
2039 ))
2040 })?),
2041 None => None,
2042 };
2043 let key_str = |values: &[Value<'static>]| -> Result<Option<String>, EngineError> {
2044 if let Some(pred) = &predicate_expr {
2046 let tmp_row = spg_storage::Row {
2047 values: values.to_vec(),
2048 };
2049 let v = eval::eval_expr(pred, &tmp_row, &ctx).map_err(|e| {
2050 EngineError::Unsupported(alloc::format!(
2051 "UNIQUE INDEX {:?} predicate eval: {e:?}",
2052 idx.name
2053 ))
2054 })?;
2055 if !predicate_truthy(&v) {
2056 return Ok(None);
2057 }
2058 }
2059 let key: Vec<Value<'static>> = if let Some(expr) = &expr_key {
2060 let tmp_row = spg_storage::Row {
2061 values: values.to_vec(),
2062 };
2063 let v = eval::eval_expr(expr, &tmp_row, &ctx).map_err(|e| {
2064 EngineError::Unsupported(alloc::format!(
2065 "UNIQUE INDEX {:?} expression eval: {e:?}",
2066 idx.name
2067 ))
2068 })?;
2069 alloc::vec![v]
2070 } else {
2071 key_positions
2072 .iter()
2073 .map(|&p| {
2074 let v = values.get(p).cloned().unwrap_or(Value::Null);
2075 collated_key_cell(&v, p, schema, mysql)
2076 })
2077 .collect()
2078 };
2079 if key.iter().any(|v| matches!(v, Value::Null)) {
2080 return Ok(None);
2081 }
2082 Ok(Some(aggregate::encode_key(&key)))
2083 };
2084 let on_conflict = |pos: usize| -> EngineError {
2085 EngineError::Unsupported(alloc::format!(
2086 "UNIQUE INDEX {:?} violation on {table_name:?}: \
2087 UPDATE of row #{pos} duplicates an existing key",
2088 idx.name
2089 ))
2090 };
2091 if !mysql
2095 && !is_expr_or_partial
2096 && !idx.nulls_not_distinct
2097 && matches!(idx.kind, spg_storage::IndexKind::BTree(_))
2098 && key_positions.iter().all(|&i| {
2099 schema.columns.get(i).is_some_and(|c| {
2100 !matches!(c.collation, spg_storage::Collation::CaseInsensitive)
2101 })
2102 })
2103 && schema
2104 .columns
2105 .get(idx.column_position)
2106 .is_some_and(|c| indexkeyable_type(&c.ty))
2107 && probe_replay(
2108 table,
2109 idx,
2110 &key_positions,
2111 planned,
2112 schema,
2113 &key_str,
2114 &on_conflict,
2115 mysql,
2116 )?
2117 {
2118 continue;
2119 }
2120 replay(&key_str, &on_conflict)?;
2121 }
2122 Ok(())
2123}
2124
2125pub(crate) fn any_column_changed(
2133 filter_cols: &[String],
2134 schema_cols: &[ColumnSchema],
2135 old_row: &Row<'static>,
2136 new_row: &Row<'static>,
2137) -> bool {
2138 for col_name in filter_cols {
2139 let Some(pos) = schema_cols
2140 .iter()
2141 .position(|c| c.name.eq_ignore_ascii_case(col_name))
2142 else {
2143 continue;
2144 };
2145 let old_v = old_row.values.get(pos);
2146 let new_v = new_row.values.get(pos);
2147 if old_v != new_v {
2148 return true;
2149 }
2150 }
2151 false
2152}
2153
2154pub(crate) fn format_failing_row(row_values: &[Value<'static>]) -> String {
2159 row_values
2160 .iter()
2161 .map(|v| match v {
2162 Value::Null => "null".to_string(),
2163 Value::Text(s) => s.to_string(),
2164 other => crate::eval::value_to_text(other),
2165 })
2166 .collect::<Vec<_>>()
2167 .join(", ")
2168}
2169
2170pub(crate) fn enforce_not_null(
2178 catalog: &Catalog,
2179 table_name: &str,
2180 rows: &[alloc::vec::Vec<Value<'static>>],
2181) -> Result<(), EngineError> {
2182 let table = catalog.get(table_name).ok_or_else(|| {
2183 EngineError::Storage(StorageError::TableNotFound {
2184 name: table_name.into(),
2185 })
2186 })?;
2187 let cols = &table.schema().columns;
2188 for row in rows {
2189 for (val, col) in row.iter().zip(cols) {
2190 if val.is_null() && !col.nullable {
2191 if let Some(dname) = &col.user_domain_type
2195 && catalog
2196 .domain_types()
2197 .get(dname)
2198 .is_some_and(|d| !d.nullable)
2199 {
2200 return Err(EngineError::Unsupported(alloc::format!(
2201 "domain {dname} does not allow null values"
2202 )));
2203 }
2204 return Err(EngineError::Unsupported(alloc::format!(
2205 "null value in column \"{}\" of relation \"{table_name}\" \
2206 violates not-null constraint DETAIL: Failing row contains ({}).",
2207 col.name,
2208 format_failing_row(row)
2209 )));
2210 }
2211 }
2212 }
2213 Ok(())
2214}
2215
2216pub(crate) fn enforce_check_constraints(
2221 catalog: &Catalog,
2222 table_name: &str,
2223 rows: &[alloc::vec::Vec<spg_storage::Value<'static>>],
2224 sess: Option<&crate::eval::DmlSession>,
2229) -> Result<(), EngineError> {
2230 let table = catalog.get(table_name).ok_or_else(|| {
2231 EngineError::Storage(StorageError::TableNotFound {
2232 name: table_name.into(),
2233 })
2234 })?;
2235 let schema = table.schema();
2236 let mut domain_checks_per_col: alloc::vec::Vec<(
2241 usize,
2242 String,
2243 alloc::vec::Vec<(String, Expr)>,
2244 )> = alloc::vec::Vec::new();
2245 for (idx, col) in schema.columns.iter().enumerate() {
2246 let Some(dname) = &col.user_domain_type else {
2247 continue;
2248 };
2249 let Some(dom) = catalog.domain_types().get(dname) else {
2250 continue;
2251 };
2252 let mut parsed_for_col: alloc::vec::Vec<(alloc::string::String, Expr)> =
2257 alloc::vec::Vec::with_capacity(dom.checks.len());
2258 for chk in &dom.checks {
2259 let src = &chk.expr;
2260 let expr = spg_sql::parser::parse_expression(src).map_err(|e| {
2261 EngineError::Unsupported(alloc::format!(
2262 "DOMAIN {dname:?} CHECK ({src:?}) on column {:?}: re-parse failed: {e:?}",
2263 col.name
2264 ))
2265 })?;
2266 parsed_for_col.push((chk.name.clone(), expr));
2267 }
2268 if !parsed_for_col.is_empty() {
2269 domain_checks_per_col.push((idx, dname.clone(), parsed_for_col));
2270 }
2271 }
2272 if schema.checks.is_empty() && domain_checks_per_col.is_empty() {
2273 return Ok(());
2274 }
2275 let mut ctx = eval::EvalContext::new(&schema.columns, None);
2276 if let Some(s) = sess {
2277 ctx = ctx.with_session(s);
2278 }
2279 let mut parsed: alloc::vec::Vec<(usize, Expr)> = alloc::vec::Vec::new();
2280 for (i, src) in schema.checks.iter().enumerate() {
2281 let expr = spg_sql::parser::parse_expression(&src.expr).map_err(|e| {
2282 let pred = &src.expr;
2283 EngineError::Unsupported(alloc::format!(
2284 "CHECK constraint #{i} on {table_name:?} ({pred:?}) failed to re-parse: {e:?}"
2285 ))
2286 })?;
2287 parsed.push((i, expr));
2288 }
2289 for (batch_idx, row_values) in rows.iter().enumerate() {
2290 let tmp_row = spg_storage::Row {
2291 values: row_values.clone(),
2292 };
2293 for (i, expr) in &parsed {
2294 let v = eval::eval_expr(expr, &tmp_row, &ctx).map_err(|e| {
2295 EngineError::Unsupported(alloc::format!(
2296 "CHECK constraint #{i} on {table_name:?} eval at row #{batch_idx}: {e:?}"
2297 ))
2298 })?;
2299 if matches!(v, spg_storage::Value::Bool(false)) {
2301 let names =
2303 crate::system_catalog::pg_check_connames(table, table_name, &schema.checks);
2304 let conname = names
2305 .get(*i)
2306 .cloned()
2307 .unwrap_or_else(|| alloc::format!("{table_name}_check"));
2308 let failing = format_failing_row(row_values);
2309 return Err(EngineError::Unsupported(alloc::format!(
2310 "new row for relation \"{table_name}\" violates check constraint \
2311 \"{conname}\" DETAIL: Failing row contains ({failing})."
2312 )));
2313 }
2314 }
2315 for (col_idx, dname, checks) in &domain_checks_per_col {
2321 let cell = row_values
2322 .get(*col_idx)
2323 .cloned()
2324 .unwrap_or(spg_storage::Value::Null);
2325 let synth_cols = alloc::vec![spg_storage::ColumnSchema::new(
2326 "value",
2327 schema.columns[*col_idx].ty,
2328 schema.columns[*col_idx].nullable,
2329 )];
2330 let mut synth_ctx = eval::EvalContext::new(&synth_cols, None);
2331 if let Some(s) = sess {
2332 synth_ctx = synth_ctx.with_session(s);
2333 }
2334 let synth_row = spg_storage::Row {
2335 values: alloc::vec![cell],
2336 };
2337 for (ci, (cname, expr)) in checks.iter().enumerate() {
2338 let v = eval::eval_expr(expr, &synth_row, &synth_ctx).map_err(|e| {
2339 EngineError::Unsupported(alloc::format!(
2340 "DOMAIN CHECK #{ci} on column {:?} eval at row #{batch_idx}: {e:?}",
2341 schema.columns[*col_idx].name
2342 ))
2343 })?;
2344 if matches!(v, spg_storage::Value::Bool(false)) {
2345 return Err(EngineError::Unsupported(alloc::format!(
2349 "value for domain {dname} violates check constraint \"{cname}\""
2350 )));
2351 }
2352 }
2353 }
2354 }
2355 Ok(())
2356}
2357
2358pub(crate) fn iter_cold_rows_of_parent(
2365 catalog: &Catalog,
2366 parent: &spg_storage::Table,
2367) -> Vec<Row<'static>> {
2368 let schema = parent.schema();
2369 let Some(pk_col_pos) = schema
2370 .uniqueness_constraints
2371 .iter()
2372 .find(|u| u.is_primary_key && u.columns.len() == 1)
2373 .map(|u| u.columns[0])
2374 else {
2375 return Vec::new();
2376 };
2377 let Some(idx) = parent.indices().iter().find(|i| {
2378 i.column_position == pk_col_pos && matches!(i.kind, spg_storage::IndexKind::BTree(_))
2379 }) else {
2380 return Vec::new();
2381 };
2382 let table_name = schema.name.as_str();
2383 let mut out = Vec::new();
2384 for (key, locators) in idx.iter_asc() {
2385 for loc in locators {
2386 if let spg_storage::RowLocator::Cold { segment_id, .. } = loc
2387 && let Some(row) = catalog.resolve_cold_locator(table_name, *segment_id, key)
2388 {
2389 out.push(row);
2390 }
2391 }
2392 }
2393 out
2394}
2395
2396pub(crate) fn iter_cold_rows_with_locator_map(
2412 catalog: &Catalog,
2413 table: &spg_storage::Table,
2414) -> (Vec<Row<'static>>, hashbrown::HashMap<i64, usize>) {
2415 let schema = table.schema();
2416 let Some(pk_col_pos) = schema
2417 .uniqueness_constraints
2418 .iter()
2419 .find(|u| u.is_primary_key && u.columns.len() == 1)
2420 .map(|u| u.columns[0])
2421 else {
2422 return (Vec::new(), hashbrown::HashMap::new());
2423 };
2424 let Some(idx) = table.indices().iter().find(|i| {
2425 i.column_position == pk_col_pos && matches!(i.kind, spg_storage::IndexKind::BTree(_))
2426 }) else {
2427 return (Vec::new(), hashbrown::HashMap::new());
2428 };
2429 let table_name = schema.name.as_str();
2430 let mut rows = Vec::new();
2431 let mut map: hashbrown::HashMap<i64, usize> = hashbrown::HashMap::new();
2432 for (key, locators) in idx.iter_asc() {
2433 let spg_storage::IndexKey::Int(pk_value) = key else {
2438 continue;
2439 };
2440 for loc in locators {
2441 if let spg_storage::RowLocator::Cold { segment_id, .. } = loc
2442 && let Some(row) = catalog.resolve_cold_locator(table_name, *segment_id, key)
2443 {
2444 let offset = rows.len();
2445 rows.push(row);
2446 map.insert(*pk_value, offset);
2447 }
2448 }
2449 }
2450 (rows, map)
2451}
2452
2453pub(crate) fn iter_cold_rows_with_pk_key(
2454 catalog: &Catalog,
2455 table: &spg_storage::Table,
2456) -> Vec<(spg_storage::IndexKey, Row<'static>)> {
2457 let schema = table.schema();
2458 let Some(pk_col_pos) = schema
2459 .uniqueness_constraints
2460 .iter()
2461 .find(|u| u.is_primary_key && u.columns.len() == 1)
2462 .map(|u| u.columns[0])
2463 else {
2464 return Vec::new();
2465 };
2466 let Some(idx) = table.indices().iter().find(|i| {
2467 i.column_position == pk_col_pos && matches!(i.kind, spg_storage::IndexKind::BTree(_))
2468 }) else {
2469 return Vec::new();
2470 };
2471 let table_name = schema.name.as_str();
2472 let mut out = Vec::new();
2473 for (key, locators) in idx.iter_asc() {
2474 for loc in locators {
2475 if let spg_storage::RowLocator::Cold { segment_id, .. } = loc
2476 && let Some(row) = catalog.resolve_cold_locator(table_name, *segment_id, key)
2477 {
2478 out.push((key.clone(), row));
2479 }
2480 }
2481 }
2482 out
2483}
2484
2485pub(crate) fn pk_btree_index_name(table: &spg_storage::Table) -> Option<String> {
2490 let schema = table.schema();
2491 let pk_col_pos = schema
2492 .uniqueness_constraints
2493 .iter()
2494 .find(|u| u.is_primary_key && u.columns.len() == 1)
2495 .map(|u| u.columns[0])?;
2496 table.indices().iter().find_map(|i| {
2497 if i.column_position == pk_col_pos && matches!(i.kind, spg_storage::IndexKind::BTree(_)) {
2498 Some(i.name.clone())
2499 } else {
2500 None
2501 }
2502 })
2503}
2504
2505pub(crate) fn enforce_fk_inserts(
2506 catalog: &Catalog,
2507 child_table: &str,
2508 fks: &[spg_storage::ForeignKeyConstraint],
2509 rows: &[Vec<Value<'static>>],
2510) -> Result<(), EngineError> {
2511 for fk in fks {
2512 let parent_is_self = fk.parent_table == child_table;
2513 let parent = if parent_is_self {
2514 catalog.get(child_table).ok_or_else(|| {
2517 EngineError::Storage(StorageError::TableNotFound {
2518 name: child_table.into(),
2519 })
2520 })?
2521 } else {
2522 catalog.get(&fk.parent_table).ok_or_else(|| {
2523 EngineError::Storage(StorageError::TableNotFound {
2524 name: fk.parent_table.clone(),
2525 })
2526 })?
2527 };
2528 let cold_parent_rows: alloc::vec::Vec<Row<'static>> = if fk.local_columns.len() == 1 {
2537 Vec::new()
2538 } else {
2539 iter_cold_rows_of_parent(catalog, parent)
2540 };
2541 for (batch_idx, row_values) in rows.iter().enumerate() {
2542 if fk.local_columns.len() == 1 {
2546 let v = &row_values[fk.local_columns[0]];
2547 if matches!(v, Value::Null) {
2548 continue;
2549 }
2550 let parent_col = fk.parent_columns[0];
2551 let key = spg_storage::IndexKey::from_value(v).ok_or_else(|| {
2552 EngineError::Unsupported(alloc::format!(
2553 "FOREIGN KEY column value of type {} is not index-eligible",
2554 crate::conversions::pg_type_name_for_error_opt(v.data_type())
2555 ))
2556 })?;
2557 let present_committed = parent.indices().iter().any(|idx| {
2558 matches!(idx.kind, spg_storage::IndexKind::BTree(_))
2559 && idx.column_position == parent_col
2560 && idx.partial_predicate.is_none()
2561 && idx
2568 .lookup_eq(&key)
2569 .iter()
2570 .any(|loc| !locator_is_tombstoned(parent, loc))
2571 });
2572 let present_in_batch = parent_is_self
2576 && rows[..batch_idx]
2577 .iter()
2578 .any(|earlier| earlier.get(parent_col) == Some(v));
2579 if !(present_committed || present_in_batch) {
2580 let child = catalog.get(child_table).ok_or_else(|| {
2582 EngineError::Storage(StorageError::TableNotFound {
2583 name: child_table.into(),
2584 })
2585 })?;
2586 return Err(EngineError::Unsupported(fk_violation_message(
2587 child,
2588 child_table,
2589 fk,
2590 &[v],
2591 )));
2592 }
2593 } else {
2594 let null_cnt = fk
2601 .local_columns
2602 .iter()
2603 .filter(|&&i| matches!(row_values.get(i), Some(Value::Null)))
2604 .count();
2605 match fk.match_type {
2606 spg_storage::MatchType::Simple => {
2607 if null_cnt > 0 {
2608 continue;
2609 }
2610 }
2611 spg_storage::MatchType::Full => {
2612 if null_cnt == fk.local_columns.len() {
2613 continue;
2614 }
2615 if null_cnt > 0 {
2616 return Err(EngineError::Unsupported(
2617 "insert or update violates foreign key constraint: MATCH FULL \
2618 does not allow mixing of null and nonnull key values"
2619 .into(),
2620 ));
2621 }
2622 }
2623 }
2624 let local: Vec<&Value> = fk.local_columns.iter().map(|&i| &row_values[i]).collect();
2625 let matches_parent_row = |prow: &Row<'static>| {
2626 fk.parent_columns
2627 .iter()
2628 .enumerate()
2629 .all(|(i, &pi)| prow.values.get(pi) == Some(local[i]))
2630 };
2631 let hot_parent_match = parent.rows().iter().enumerate().any(|(row_idx, prow)| {
2637 !parent
2638 .headers()
2639 .get(row_idx)
2640 .is_some_and(|h| h.is_deleted())
2641 && matches_parent_row(prow)
2642 });
2643 let parent_match_committed =
2644 hot_parent_match || cold_parent_rows.iter().any(&matches_parent_row);
2645 let parent_match_in_batch = parent_is_self
2646 && rows[..batch_idx].iter().any(|earlier| {
2647 fk.parent_columns
2648 .iter()
2649 .enumerate()
2650 .all(|(i, &pi)| earlier.get(pi) == Some(local[i]))
2651 });
2652 if !(parent_match_committed || parent_match_in_batch) {
2653 let child = catalog.get(child_table).ok_or_else(|| {
2654 EngineError::Storage(StorageError::TableNotFound {
2655 name: child_table.into(),
2656 })
2657 })?;
2658 return Err(EngineError::Unsupported(fk_violation_message(
2659 child,
2660 child_table,
2661 fk,
2662 &local,
2663 )));
2664 }
2665 }
2666 }
2667 }
2668 Ok(())
2669}
2670
2671#[derive(Debug, Clone)]
2675pub(crate) struct FkChildStep {
2676 child_table: String,
2677 action: FkChildAction,
2678}
2679
2680#[derive(Debug, Clone)]
2681pub(crate) enum FkChildAction {
2682 Delete { positions: Vec<usize> },
2684 SetNull {
2688 positions: Vec<usize>,
2689 columns: Vec<usize>,
2690 },
2691 SetDefault {
2695 positions: Vec<usize>,
2696 columns: Vec<usize>,
2697 defaults: Vec<Value<'static>>,
2698 },
2699}
2700
2701pub(crate) fn any_fk_child_references(catalog: &Catalog, table_name: &str) -> bool {
2721 catalog.table_names().into_iter().any(|child_name| {
2722 catalog.get(&child_name).is_some_and(|c| {
2723 c.schema()
2724 .foreign_keys
2725 .iter()
2726 .any(|fk| fk.parent_table == table_name)
2727 })
2728 })
2729}
2730
2731pub(crate) fn plan_fk_parent_deletions(
2732 catalog: &Catalog,
2733 parent_table_name: &str,
2734 to_delete_positions: &[usize],
2735 to_delete_rows: &[Vec<Value<'static>>],
2736) -> Result<Vec<FkChildStep>, EngineError> {
2737 use alloc::collections::{BTreeMap, BTreeSet};
2738 if to_delete_rows.is_empty() {
2739 return Ok(Vec::new());
2740 }
2741 let mut delete_plan: BTreeMap<String, BTreeSet<usize>> = BTreeMap::new();
2742 let mut setnull_plan: BTreeMap<String, BTreeSet<(usize, usize)>> = BTreeMap::new();
2744 let mut setdefault_plan: BTreeMap<String, BTreeMap<(usize, usize), Value>> = BTreeMap::new();
2745 let mut visited: BTreeSet<(String, usize)> = BTreeSet::new();
2746 for &p in to_delete_positions {
2747 visited.insert((parent_table_name.to_string(), p));
2748 }
2749 let mut work: Vec<(String, Vec<Value<'static>>)> = to_delete_rows
2750 .iter()
2751 .map(|r| (parent_table_name.to_string(), r.clone()))
2752 .collect();
2753 while let Some((cur_parent, parent_row)) = work.pop() {
2754 for child_name in catalog.table_names() {
2755 let child = catalog
2756 .get(&child_name)
2757 .expect("table_names → catalog.get round-trip is total");
2758 for fk in &child.schema().foreign_keys {
2759 if fk.parent_table != cur_parent {
2760 continue;
2761 }
2762 let parent_key: Vec<&Value> = fk
2763 .parent_columns
2764 .iter()
2765 .map(|&pi| &parent_row[pi])
2766 .collect();
2767 if parent_key.iter().any(|v| matches!(v, Value::Null)) {
2768 continue;
2769 }
2770 if iter_cold_rows_of_parent(catalog, child).iter().any(|crow| {
2781 fk.local_columns
2782 .iter()
2783 .enumerate()
2784 .all(|(i, &li)| crow.values.get(li) == Some(parent_key[i]))
2785 }) {
2786 return Err(EngineError::Unsupported(alloc::format!(
2787 "DELETE on {cur_parent:?}: cold-tier child row in {child_name:?} \
2788 references the doomed parent key; cold-tier mutation by this \
2789 FK action is a v7.37 candidate. Run COMPACT or move the cold \
2790 rows back to the hot tier and retry."
2791 )));
2792 }
2793 for (child_row_idx, child_row) in child.rows().iter().enumerate() {
2794 if child_name == cur_parent
2795 && visited.contains(&(child_name.clone(), child_row_idx))
2796 {
2797 continue;
2798 }
2799 let matches_key = fk
2800 .local_columns
2801 .iter()
2802 .enumerate()
2803 .all(|(i, &li)| child_row.values.get(li) == Some(parent_key[i]));
2804 if !matches_key {
2805 continue;
2806 }
2807 match fk.on_delete {
2808 spg_storage::FkAction::Restrict | spg_storage::FkAction::NoAction => {
2809 return Err(EngineError::Unsupported(fk_restrict_message(
2811 catalog,
2812 &cur_parent,
2813 child,
2814 &child_name,
2815 fk,
2816 &parent_key,
2817 fk.on_delete,
2818 )));
2819 }
2820 spg_storage::FkAction::Cascade => {
2821 if visited.insert((child_name.clone(), child_row_idx)) {
2822 delete_plan
2823 .entry(child_name.clone())
2824 .or_default()
2825 .insert(child_row_idx);
2826 work.push((child_name.clone(), child_row.values.clone()));
2827 }
2828 }
2829 spg_storage::FkAction::SetNull => {
2830 for &li in &fk.local_columns {
2832 let col = child.schema().columns.get(li).ok_or_else(|| {
2833 EngineError::Unsupported(alloc::format!(
2834 "FK local column {li} missing in {child_name:?}"
2835 ))
2836 })?;
2837 if !col.nullable {
2838 return Err(EngineError::Unsupported(alloc::format!(
2839 "FOREIGN KEY ON DELETE SET NULL: column \
2840 {child_name:?}.{:?} is NOT NULL — cannot SET NULL",
2841 col.name,
2842 )));
2843 }
2844 }
2845 let entry = setnull_plan.entry(child_name.clone()).or_default();
2846 for &li in &fk.local_columns {
2847 entry.insert((child_row_idx, li));
2848 }
2849 }
2850 spg_storage::FkAction::SetDefault => {
2851 let entry = setdefault_plan.entry(child_name.clone()).or_default();
2853 for &li in &fk.local_columns {
2854 let col = child.schema().columns.get(li).ok_or_else(|| {
2855 EngineError::Unsupported(alloc::format!(
2856 "FK local column {li} missing in {child_name:?}"
2857 ))
2858 })?;
2859 let default = col.default.clone().ok_or_else(|| {
2860 EngineError::Unsupported(alloc::format!(
2861 "FOREIGN KEY ON DELETE SET DEFAULT: column \
2862 {child_name:?}.{:?} has no DEFAULT declared",
2863 col.name,
2864 ))
2865 })?;
2866 entry.insert((child_row_idx, li), default);
2867 }
2868 }
2869 }
2870 }
2871 }
2872 }
2873 }
2874 let mut steps: Vec<FkChildStep> = Vec::new();
2882 for (child_table, entries) in setnull_plan {
2883 let (positions, columns): (Vec<usize>, Vec<usize>) = entries.into_iter().unzip();
2884 steps.push(FkChildStep {
2885 child_table,
2886 action: FkChildAction::SetNull { positions, columns },
2887 });
2888 }
2889 for (child_table, entries) in setdefault_plan {
2890 let mut positions = Vec::with_capacity(entries.len());
2891 let mut columns = Vec::with_capacity(entries.len());
2892 let mut defaults = Vec::with_capacity(entries.len());
2893 for ((p, c), v) in entries {
2894 positions.push(p);
2895 columns.push(c);
2896 defaults.push(v);
2897 }
2898 steps.push(FkChildStep {
2899 child_table,
2900 action: FkChildAction::SetDefault {
2901 positions,
2902 columns,
2903 defaults,
2904 },
2905 });
2906 }
2907 for (child_table, positions) in delete_plan {
2908 steps.push(FkChildStep {
2909 child_table,
2910 action: FkChildAction::Delete {
2911 positions: positions.into_iter().collect(),
2912 },
2913 });
2914 }
2915 Ok(steps)
2916}
2917
2918pub(crate) fn plan_fk_parent_updates(
2935 catalog: &Catalog,
2936 parent_table_name: &str,
2937 plan_with_old: &[(usize, Vec<Value<'static>>, Vec<Value<'static>>)],
2938) -> Result<Vec<FkChildStep>, EngineError> {
2939 use alloc::collections::BTreeMap;
2940 if plan_with_old.is_empty() {
2941 return Ok(Vec::new());
2942 }
2943 let delete_plan: BTreeMap<String, alloc::collections::BTreeSet<usize>> = BTreeMap::new();
2948 let mut setnull_plan: BTreeMap<String, alloc::collections::BTreeSet<(usize, usize)>> =
2949 BTreeMap::new();
2950 let mut setdefault_plan: BTreeMap<String, BTreeMap<(usize, usize), Value>> = BTreeMap::new();
2951 let mut cascade_plan: BTreeMap<String, BTreeMap<(usize, usize), Value>> = BTreeMap::new();
2953
2954 for child_name in catalog.table_names() {
2955 let child = catalog
2956 .get(&child_name)
2957 .expect("table_names → catalog.get total");
2958 for fk in &child.schema().foreign_keys {
2959 if fk.parent_table != parent_table_name {
2960 continue;
2961 }
2962 for (_pos, old_row, new_row) in plan_with_old {
2963 let key_changed = fk
2965 .parent_columns
2966 .iter()
2967 .any(|&pi| old_row.get(pi) != new_row.get(pi));
2968 if !key_changed {
2969 continue;
2970 }
2971 let old_key: Vec<&Value> =
2973 fk.parent_columns.iter().map(|&pi| &old_row[pi]).collect();
2974 if old_key.iter().any(|v| matches!(v, Value::Null)) {
2975 continue;
2977 }
2978 let new_key: Vec<&Value> =
2979 fk.parent_columns.iter().map(|&pi| &new_row[pi]).collect();
2980 if iter_cold_rows_of_parent(catalog, child).iter().any(|crow| {
2986 fk.local_columns
2987 .iter()
2988 .enumerate()
2989 .all(|(i, &li)| crow.values.get(li) == Some(old_key[i]))
2990 }) {
2991 return Err(EngineError::Unsupported(alloc::format!(
2992 "UPDATE on {parent_table_name:?}: cold-tier child row in \
2993 {child_name:?} references the changing parent key; cold-tier \
2994 mutation by this FK action is a v7.37 candidate. Run COMPACT \
2995 or move the cold rows back to the hot tier and retry."
2996 )));
2997 }
2998 for (child_row_idx, child_row) in child.rows().iter().enumerate() {
2999 if child_name == parent_table_name
3002 && plan_with_old.iter().any(|(p, _, _)| *p == child_row_idx)
3003 {
3004 continue;
3005 }
3006 let matches_key = fk
3007 .local_columns
3008 .iter()
3009 .enumerate()
3010 .all(|(i, &li)| child_row.values.get(li) == Some(old_key[i]));
3011 if !matches_key {
3012 continue;
3013 }
3014 match fk.on_update {
3015 spg_storage::FkAction::Restrict | spg_storage::FkAction::NoAction => {
3016 return Err(EngineError::Unsupported(fk_restrict_message(
3017 catalog,
3018 parent_table_name,
3019 child,
3020 &child_name,
3021 fk,
3022 &old_key,
3023 fk.on_update,
3024 )));
3025 }
3026 spg_storage::FkAction::Cascade => {
3027 let entry = cascade_plan.entry(child_name.clone()).or_default();
3029 for (i, &li) in fk.local_columns.iter().enumerate() {
3030 entry.insert((child_row_idx, li), new_key[i].clone());
3031 }
3032 }
3033 spg_storage::FkAction::SetNull => {
3034 for &li in &fk.local_columns {
3035 let col = child.schema().columns.get(li).ok_or_else(|| {
3036 EngineError::Unsupported(alloc::format!(
3037 "FK local column {li} missing in {child_name:?}"
3038 ))
3039 })?;
3040 if !col.nullable {
3041 return Err(EngineError::Unsupported(alloc::format!(
3042 "FOREIGN KEY ON UPDATE SET NULL: column \
3043 {child_name:?}.{:?} is NOT NULL",
3044 col.name,
3045 )));
3046 }
3047 }
3048 let entry = setnull_plan.entry(child_name.clone()).or_default();
3049 for &li in &fk.local_columns {
3050 entry.insert((child_row_idx, li));
3051 }
3052 }
3053 spg_storage::FkAction::SetDefault => {
3054 let entry = setdefault_plan.entry(child_name.clone()).or_default();
3055 for &li in &fk.local_columns {
3056 let col = child.schema().columns.get(li).ok_or_else(|| {
3057 EngineError::Unsupported(alloc::format!(
3058 "FK local column {li} missing in {child_name:?}"
3059 ))
3060 })?;
3061 let default = col.default.clone().ok_or_else(|| {
3062 EngineError::Unsupported(alloc::format!(
3063 "FOREIGN KEY ON UPDATE SET DEFAULT: column \
3064 {child_name:?}.{:?} has no DEFAULT",
3065 col.name,
3066 ))
3067 })?;
3068 entry.insert((child_row_idx, li), default);
3069 }
3070 }
3071 }
3072 }
3073 }
3074 }
3075 }
3076 let mut steps: Vec<FkChildStep> = Vec::new();
3079 for (child_table, entries) in cascade_plan {
3080 let mut positions = Vec::with_capacity(entries.len());
3081 let mut columns = Vec::with_capacity(entries.len());
3082 let mut defaults = Vec::with_capacity(entries.len());
3083 for ((p, c), v) in entries {
3084 positions.push(p);
3085 columns.push(c);
3086 defaults.push(v);
3087 }
3088 steps.push(FkChildStep {
3093 child_table,
3094 action: FkChildAction::SetDefault {
3095 positions,
3096 columns,
3097 defaults,
3098 },
3099 });
3100 }
3101 for (child_table, entries) in setnull_plan {
3102 let (positions, columns): (Vec<usize>, Vec<usize>) = entries.into_iter().unzip();
3103 steps.push(FkChildStep {
3104 child_table,
3105 action: FkChildAction::SetNull { positions, columns },
3106 });
3107 }
3108 for (child_table, entries) in setdefault_plan {
3109 let mut positions = Vec::with_capacity(entries.len());
3110 let mut columns = Vec::with_capacity(entries.len());
3111 let mut defaults = Vec::with_capacity(entries.len());
3112 for ((p, c), v) in entries {
3113 positions.push(p);
3114 columns.push(c);
3115 defaults.push(v);
3116 }
3117 steps.push(FkChildStep {
3118 child_table,
3119 action: FkChildAction::SetDefault {
3120 positions,
3121 columns,
3122 defaults,
3123 },
3124 });
3125 }
3126 let _ = delete_plan; Ok(steps)
3128}
3129
3130pub(crate) fn apply_fk_child_step(
3134 catalog: &mut Catalog,
3135 step: &FkChildStep,
3136) -> Result<(), EngineError> {
3137 let child = catalog.get_mut(&step.child_table).ok_or_else(|| {
3138 EngineError::Storage(StorageError::TableNotFound {
3139 name: step.child_table.clone(),
3140 })
3141 })?;
3142 match &step.action {
3143 FkChildAction::Delete { positions } => {
3144 let _ = child.delete_rows(positions);
3145 }
3146 FkChildAction::SetNull { positions, columns } => {
3147 apply_per_cell_writes(child, positions, columns, |_| Value::Null)?;
3148 }
3149 FkChildAction::SetDefault {
3150 positions,
3151 columns,
3152 defaults,
3153 } => {
3154 apply_per_cell_writes(child, positions, columns, |i| defaults[i].clone())?;
3155 }
3156 }
3157 Ok(())
3158}
3159
3160fn apply_per_cell_writes(
3166 child: &mut spg_storage::Table,
3167 positions: &[usize],
3168 columns: &[usize],
3169 mut value_for: impl FnMut(usize) -> Value<'static>,
3170) -> Result<(), EngineError> {
3171 use alloc::collections::BTreeMap;
3172 let mut by_row: BTreeMap<usize, Vec<(usize, Value<'static>)>> = BTreeMap::new();
3173 for i in 0..positions.len() {
3174 by_row
3175 .entry(positions[i])
3176 .or_default()
3177 .push((columns[i], value_for(i)));
3178 }
3179 for (pos, mutations) in by_row {
3180 let mut new_values = child.rows()[pos].values.clone();
3181 for (col, v) in mutations {
3182 if let Some(slot) = new_values.get_mut(col) {
3183 *slot = v;
3184 }
3185 }
3186 child
3187 .update_row(pos, new_values)
3188 .map_err(EngineError::Storage)?;
3189 }
3190 Ok(())
3191}
3192
3193fn fk_action_sql_to_storage(a: spg_sql::ast::FkAction) -> spg_storage::FkAction {
3194 match a {
3195 spg_sql::ast::FkAction::Restrict => spg_storage::FkAction::Restrict,
3196 spg_sql::ast::FkAction::Cascade => spg_storage::FkAction::Cascade,
3197 spg_sql::ast::FkAction::SetNull => spg_storage::FkAction::SetNull,
3198 spg_sql::ast::FkAction::SetDefault => spg_storage::FkAction::SetDefault,
3199 spg_sql::ast::FkAction::NoAction => spg_storage::FkAction::NoAction,
3200 }
3201}
3202
3203impl Engine {
3204 pub(crate) fn drain_pending_foreign_keys(&mut self) -> Result<(), EngineError> {
3211 let pending = core::mem::take(&mut self.pending_foreign_keys);
3212 for (child, fk) in pending {
3213 let cols_snapshot = match self.active_catalog().get(&child) {
3217 Some(t) => t.schema().columns.clone(),
3218 None => continue,
3219 };
3220 let storage_fk =
3221 resolve_foreign_key(&child, &cols_snapshot, fk, self.active_catalog())?;
3222 let table = self
3223 .active_catalog_mut()
3224 .get_mut(&child)
3225 .expect("checked above");
3226 table.schema_mut().foreign_keys.push(storage_fk);
3227 }
3228 Ok(())
3229 }
3230}
3231
3232impl Engine {
3233 pub(crate) fn fk_is_deferred_now(&self, fk: &spg_storage::ForeignKeyConstraint) -> bool {
3241 if !fk.deferrable {
3242 return false;
3243 }
3244 let Some(tx_id) = self.current_tx else {
3245 return false;
3246 };
3247 let Some(st) = self.tx_catalogs.get(&tx_id) else {
3248 return false;
3249 };
3250 fk_deferred_in(st, fk)
3251 }
3252
3253 pub(crate) fn immediate_fks(
3255 &self,
3256 fks: &[spg_storage::ForeignKeyConstraint],
3257 ) -> alloc::vec::Vec<spg_storage::ForeignKeyConstraint> {
3258 fks.iter()
3259 .filter(|fk| !self.fk_is_deferred_now(fk))
3260 .cloned()
3261 .collect()
3262 }
3263
3264 pub(crate) fn run_deferred_fk_checks(&mut self) -> Result<(), EngineError> {
3273 self.run_deferred_fk_checks_inner(None)
3274 }
3275
3276 pub(crate) fn run_deferred_fk_checks_for(
3282 &mut self,
3283 names: &[String],
3284 ) -> Result<(), EngineError> {
3285 self.run_deferred_fk_checks_inner(Some(names))
3286 }
3287
3288 fn run_deferred_fk_checks_inner(&mut self, only: Option<&[String]>) -> Result<(), EngineError> {
3289 let Some(tx_id) = self.current_tx else {
3290 return Ok(());
3291 };
3292 let Some(st) = self.tx_catalogs.get(&tx_id) else {
3293 return Ok(());
3294 };
3295 let tables: alloc::vec::Vec<String> = st.touched_tables.iter().cloned().collect();
3296 let deferred_now = |fk: &spg_storage::ForeignKeyConstraint| {
3297 if let Some(names) = only
3298 && !fk
3299 .name
3300 .as_deref()
3301 .is_some_and(|n| names.iter().any(|w| w == n))
3302 {
3303 return false;
3304 }
3305 fk.deferrable && fk_deferred_in(st, fk)
3306 };
3307 for tname in &tables {
3308 let Some(t) = st.catalog.get(tname) else {
3309 continue;
3310 };
3311 let fks: alloc::vec::Vec<_> = t
3312 .schema()
3313 .foreign_keys
3314 .iter()
3315 .filter(|f| deferred_now(f))
3316 .cloned()
3317 .collect();
3318 if fks.is_empty() {
3319 continue;
3320 }
3321 let rows: alloc::vec::Vec<alloc::vec::Vec<Value<'static>>> = t
3326 .rows()
3327 .iter()
3328 .enumerate()
3329 .filter(|(i, _)| !t.headers().get(*i).is_some_and(|h| h.is_deleted()))
3330 .map(|(_, r)| r.values.clone())
3331 .collect();
3332 enforce_fk_inserts(&st.catalog, tname, &fks, &rows)?;
3333 }
3334 for tname in &tables {
3339 let Some(t) = st.catalog.get(tname) else {
3340 continue;
3341 };
3342 let deferred_ucs: alloc::vec::Vec<(
3343 spg_storage::UniquenessConstraint,
3344 alloc::string::String,
3345 )> = t
3346 .schema()
3347 .uniqueness_constraints
3348 .iter()
3349 .filter(|uc| uc.deferrable)
3350 .map(|uc| {
3351 let conname = crate::system_catalog::pg_unique_conname(t, uc, tname);
3352 (uc.clone(), conname)
3353 })
3354 .filter(|(uc, conname)| {
3355 if let Some(names) = only
3356 && !names.iter().any(|w| w == conname)
3357 {
3358 return false;
3359 }
3360 uc_deferred_in(st, uc, conname)
3361 })
3362 .collect();
3363 for (uc, _) in &deferred_ucs {
3364 validate_uniqueness_whole_table(&st.catalog, tname, uc, self.backslash_escapes)?;
3365 }
3366 }
3367 Ok(())
3368 }
3369}
3370
3371pub(crate) fn uc_deferred_in(
3390 st: &crate::TxState,
3391 uc: &spg_storage::UniquenessConstraint,
3392 conname: &str,
3393) -> bool {
3394 if let Some(explicit) = st.constraints_deferred_by_name.get(conname) {
3395 return *explicit;
3396 }
3397 st.constraints_deferred.unwrap_or(uc.initially_deferred)
3398}
3399
3400pub(crate) fn validate_uniqueness_whole_table(
3406 catalog: &Catalog,
3407 tname: &str,
3408 uc: &spg_storage::UniquenessConstraint,
3409 mysql: bool,
3410) -> Result<(), EngineError> {
3411 let Some(table) = catalog.get(tname) else {
3412 return Ok(());
3413 };
3414 let schema = table.schema();
3415 let mut seen: hashbrown::HashSet<alloc::string::String> = hashbrown::HashSet::new();
3416 for (i, row) in table.rows().iter().enumerate() {
3417 if table.headers().get(i).is_some_and(|h| h.is_deleted()) {
3418 continue;
3419 }
3420 let key: Vec<Value<'static>> = uc
3421 .columns
3422 .iter()
3423 .map(|&ci| {
3424 let v = row.values.get(ci).cloned().unwrap_or(Value::Null);
3425 collated_key_cell(&v, ci, schema, mysql)
3426 })
3427 .collect();
3428 if !uc.nulls_not_distinct && key.iter().any(Value::is_null) {
3431 continue;
3432 }
3433 let encoded = alloc::format!("{key:?}");
3434 if !seen.insert(encoded) {
3435 let conname = crate::system_catalog::pg_unique_conname(table, uc, tname);
3436 let detail = unique_key_detail(
3437 &uc.columns
3438 .iter()
3439 .map(|&ci| schema.columns[ci].name.clone())
3440 .collect::<Vec<_>>(),
3441 &key,
3442 );
3443 return Err(EngineError::Unsupported(alloc::format!(
3444 "duplicate key value violates unique constraint \"{conname}\" \
3445 on table \"{tname}\"{detail}"
3446 )));
3447 }
3448 }
3449 Ok(())
3450}
3451
3452pub(crate) fn fk_deferred_in(st: &crate::TxState, fk: &spg_storage::ForeignKeyConstraint) -> bool {
3453 if let Some(name) = fk.name.as_deref()
3454 && let Some(explicit) = st.constraints_deferred_by_name.get(name)
3455 {
3456 return *explicit;
3457 }
3458 st.constraints_deferred.unwrap_or(fk.initially_deferred)
3459}
3460
3461impl crate::Engine {
3462 pub(crate) fn exec_set_constraints(
3473 &mut self,
3474 names: &[alloc::string::String],
3475 deferred: bool,
3476 ) -> Result<crate::QueryResult, EngineError> {
3477 if !self.current_tx.is_some_and(|tx| self.is_tx_open(tx)) {
3484 self.warning(alloc::string::String::from(
3485 "SET CONSTRAINTS can only be used in transaction blocks",
3486 ));
3487 }
3488 for n in names {
3491 match self.find_fk_by_name(n) {
3492 Some(fk) if fk.deferrable => {}
3493 Some(_) => {
3494 return Err(EngineError::Unsupported(alloc::format!(
3495 "constraint \"{n}\" is not deferrable"
3496 )));
3497 }
3498 None => match self.find_uc_by_name(n) {
3501 Some(uc) if uc.deferrable => {}
3502 Some(_) => {
3503 return Err(EngineError::Unsupported(alloc::format!(
3504 "constraint \"{n}\" is not deferrable"
3505 )));
3506 }
3507 None => {
3508 return Err(EngineError::Unsupported(alloc::format!(
3509 "constraint \"{n}\" does not exist"
3510 )));
3511 }
3512 },
3513 }
3514 }
3515 if !deferred {
3521 if names.is_empty() {
3522 self.run_deferred_fk_checks()?;
3523 } else {
3524 self.run_deferred_fk_checks_for(names)?;
3525 }
3526 }
3527 if let Some(tx_id) = self.current_tx
3528 && let Some(st) = self.tx_catalogs.get_mut(&tx_id)
3529 {
3530 if names.is_empty() {
3531 st.constraints_deferred = Some(deferred);
3535 st.constraints_deferred_by_name.clear();
3536 } else {
3537 for n in names {
3538 st.constraints_deferred_by_name.insert(n.clone(), deferred);
3539 }
3540 }
3541 }
3542 Ok(crate::QueryResult::CommandOk {
3543 affected: 0,
3544 modified_catalog: false,
3545 })
3546 }
3547
3548 fn find_uc_by_name(&self, name: &str) -> Option<spg_storage::UniquenessConstraint> {
3556 let cat = self.active_catalog();
3557 cat.table_names().into_iter().find_map(|tname| {
3558 let t = cat.get(&tname)?;
3559 t.schema()
3560 .uniqueness_constraints
3561 .iter()
3562 .find(|uc| crate::system_catalog::pg_unique_conname(t, uc, &tname) == name)
3563 .cloned()
3564 })
3565 }
3566
3567 fn find_fk_by_name(&self, name: &str) -> Option<spg_storage::ForeignKeyConstraint> {
3568 let cat = self.active_catalog();
3569 cat.table_names().into_iter().find_map(|t| {
3570 cat.get(&t).and_then(|tbl| {
3571 tbl.schema()
3572 .foreign_keys
3573 .iter()
3574 .find(|fk| fk.name.as_deref() == Some(name))
3575 .cloned()
3576 })
3577 })
3578 }
3579}
3580
3581pub fn validate_check_against_existing_rows(
3594 table: &spg_storage::Table,
3595 table_name: &str,
3596 conname: &str,
3597 expr_src: &str,
3598) -> Result<(), EngineError> {
3599 let expr = spg_sql::parser::parse_expression(expr_src).map_err(|e| {
3600 EngineError::Unsupported(alloc::format!(
3601 "CHECK constraint {conname:?} on {table_name:?} ({expr_src:?}) failed to parse: {e:?}"
3602 ))
3603 })?;
3604 let schema = table.schema();
3605 let ctx = eval::EvalContext::new(&schema.columns, None);
3606 let headers = table.headers();
3607 for (i, row) in table.rows().iter().enumerate() {
3608 if headers
3609 .get(i)
3610 .is_some_and(|h| h.xmax != spg_storage::row_header::XMAX_ALIVE)
3611 {
3612 continue;
3613 }
3614 let v = eval::eval_expr(&expr, row, &ctx).map_err(|e| {
3615 EngineError::Unsupported(alloc::format!(
3616 "CHECK constraint {conname:?} on {table_name:?} eval at row #{i}: {e:?}"
3617 ))
3618 })?;
3619 if matches!(v, spg_storage::Value::Bool(false)) {
3621 return Err(EngineError::Unsupported(alloc::format!(
3622 "check constraint \"{conname}\" of relation \"{table_name}\" \
3623 is violated by some row"
3624 )));
3625 }
3626 }
3627 Ok(())
3628}