1use std::sync::Arc;
32
33use radixdb_core::{
34 DataType, Error, ForeignKeyAction, ForeignKeyConstraint, Result, Row, Schema, Value,
35};
36use radixdb_storage::expression::Expression as StorageExpression;
37use radixdb_storage::mvcc::engine::MVCCEngine;
38use radixdb_storage::traits::Engine;
39
40fn validate_cascade_row(
41 schema: &Schema,
42 row: &Row,
43 compiled_checks: &[(String, crate::expression::SharedProgram)],
44 check_vm: &mut crate::expression::ExprVM,
45) -> Result<()> {
46 crate::mutation::validation::validate_resulting_row_constraints(
47 schema,
48 compiled_checks,
49 row,
50 check_vm,
51 )
52}
53
54pub fn check_parent_exists(
62 engine: &MVCCEngine,
63 txn_id: i64,
64 schema: &Schema,
65 row: &radixdb_core::Row,
66) -> Result<()> {
67 for fk in &schema.foreign_keys {
68 let fk_value = match row.get(fk.column_index) {
69 Some(v) if !v.is_null() => v,
70 _ => continue, };
72
73 if !parent_row_exists(
74 engine,
75 txn_id,
76 &fk.referenced_table,
77 &fk.referenced_column,
78 fk_value,
79 )? {
80 return Err(Error::foreign_key_violation(
81 &schema.table_name,
82 &fk.column_name,
83 &fk.referenced_table,
84 &fk.referenced_column,
85 format!(
86 "referenced row with {} = {} does not exist",
87 fk.referenced_column, fk_value
88 ),
89 ));
90 }
91 }
92 Ok(())
93}
94
95pub fn validate_fk_value(
101 engine: &MVCCEngine,
102 txn_id: i64,
103 fk: &ForeignKeyConstraint,
104 value: &Value,
105 child_table: &str,
106) -> Result<()> {
107 if !parent_row_exists(
108 engine,
109 txn_id,
110 &fk.referenced_table,
111 &fk.referenced_column,
112 value,
113 )? {
114 return Err(Error::foreign_key_violation(
115 child_table,
116 &fk.column_name,
117 &fk.referenced_table,
118 &fk.referenced_column,
119 format!(
120 "referenced row with {} = {} does not exist",
121 fk.referenced_column, value
122 ),
123 ));
124 }
125 Ok(())
126}
127
128fn parent_row_exists(
134 engine: &MVCCEngine,
135 txn_id: i64,
136 parent_table: &str,
137 parent_column: &str,
138 value: &Value,
139) -> Result<bool> {
140 let parent_schema = engine
141 .get_table_schema_for_txn(txn_id, parent_table)
142 .map_err(|_| {
143 Error::internal(format!(
144 "foreign key references non-existent table '{}'",
145 parent_table
146 ))
147 })?;
148
149 let (_, ref_col) = parent_schema.find_column(parent_column).ok_or_else(|| {
150 Error::internal(format!(
151 "foreign key references non-existent column '{}' in table '{}'",
152 parent_column, parent_table
153 ))
154 })?;
155
156 let parent = engine.get_table_for_txn(txn_id, parent_table)?;
157 if ref_col.primary_key && ref_col.data_type == DataType::Integer {
158 if let Value::Integer(row_id) = value {
159 let mut matches = [false];
160 let hits = parent.probe_visible_row_ids(&[*row_id], &mut matches)?;
161 return Ok(hits == 1 && matches[0]);
162 }
163 }
164
165 let mut expr = radixdb_storage::expression::ComparisonExpr::new(
167 ref_col.name.as_str(),
168 radixdb_core::Operator::Eq,
169 value.clone(),
170 );
171 expr.prepare_for_schema(&parent_schema);
172
173 let rows = parent.collect_rows_with_limit_unordered(Some(&expr), 1, 0)?;
174 Ok(!rows.is_empty())
175}
176
177pub fn find_referencing_fks(
181 engine: &MVCCEngine,
182 parent_table: &str,
183) -> Arc<Vec<(String, ForeignKeyConstraint)>> {
184 engine.find_referencing_fks(parent_table)
185}
186
187pub fn find_referencing_fks_for_txn(
188 engine: &MVCCEngine,
189 txn_id: i64,
190 parent_table: &str,
191) -> Arc<Vec<(String, ForeignKeyConstraint)>> {
192 engine.find_referencing_fks_for_txn(txn_id, parent_table)
193}
194
195pub fn enforce_delete_actions_iter<'a>(
208 engine: &MVCCEngine,
209 txn_id: i64,
210 parent_table: &str,
211 parent_schema: &radixdb_core::Schema,
212 deleted_rows: impl Iterator<Item = &'a Row>,
213 referencing_fks: &[(String, ForeignKeyConstraint)],
214) -> Result<i32> {
215 if referencing_fks.is_empty() {
216 return Ok(0);
217 }
218
219 let mut total_affected = 0i32;
220
221 for row in deleted_rows {
222 for (child_table_name, fk) in referencing_fks {
223 let (referenced_index, _) = parent_schema
224 .find_column(&fk.referenced_column)
225 .ok_or_else(|| {
226 Error::internal(format!(
227 "foreign key references missing column '{}.{}'",
228 parent_table, fk.referenced_column
229 ))
230 })?;
231 let referenced_value = row.get(referenced_index).ok_or_else(|| {
232 Error::internal(format!(
233 "deleted row is missing referenced column '{}.{}'",
234 parent_table, fk.referenced_column
235 ))
236 })?;
237 let action = fk.on_delete;
238
239 match action {
240 ForeignKeyAction::Restrict | ForeignKeyAction::NoAction => {
241 if child_rows_exist(engine, txn_id, child_table_name, fk, referenced_value)? {
243 return Err(Error::foreign_key_violation(
244 child_table_name,
245 &fk.column_name,
246 parent_table,
247 &fk.referenced_column,
248 format!(
249 "cannot delete row with {} = {} — still referenced by table '{}'",
250 fk.referenced_column, referenced_value, child_table_name
251 ),
252 ));
253 }
254 }
255 ForeignKeyAction::Cascade => {
256 let affected =
258 cascade_delete(engine, txn_id, child_table_name, fk, referenced_value)?;
259 total_affected = total_affected.saturating_add(affected);
260 }
261 ForeignKeyAction::SetNull => {
262 let affected =
264 set_null_on_delete(engine, txn_id, child_table_name, fk, referenced_value)?;
265 total_affected = total_affected.saturating_add(affected);
266 }
267 }
268 }
269 }
270
271 Ok(total_affected)
272}
273
274pub fn pre_check_restrict_for_update(
279 engine: &MVCCEngine,
280 txn_id: i64,
281 parent_table: &str,
282 old_value: &Value,
283 referencing_fks: &[(String, ForeignKeyConstraint)],
284) -> Result<()> {
285 pre_check_restrict_recursive(engine, txn_id, parent_table, old_value, referencing_fks, 0)
286}
287
288pub fn fk_tree_needs_precheck(
293 engine: &MVCCEngine,
294 txn_id: i64,
295 referencing_fks: &[(String, ForeignKeyConstraint)],
296) -> bool {
297 fk_tree_needs_precheck_recursive(engine, txn_id, referencing_fks, 0)
298}
299
300fn fk_tree_needs_precheck_recursive(
301 engine: &MVCCEngine,
302 txn_id: i64,
303 referencing_fks: &[(String, ForeignKeyConstraint)],
304 depth: usize,
305) -> bool {
306 if depth >= MAX_CASCADE_DEPTH {
307 return true; }
309 for (child_table_name, fk) in referencing_fks {
310 match fk.on_update {
311 ForeignKeyAction::Restrict | ForeignKeyAction::NoAction => {
312 return true;
313 }
314 ForeignKeyAction::Cascade | ForeignKeyAction::SetNull => {
315 let grandchild_fks = find_referencing_fks_for_txn(engine, txn_id, child_table_name);
316 let child_fk_col = &fk.column_name;
317 let relevant: Vec<_> = grandchild_fks
318 .iter()
319 .filter(|(_, gfk)| gfk.referenced_column == *child_fk_col)
320 .cloned()
321 .collect();
322 if !relevant.is_empty()
323 && fk_tree_needs_precheck_recursive(engine, txn_id, &relevant, depth + 1)
324 {
325 return true;
326 }
327 }
328 }
329 }
330 false
331}
332
333fn pre_check_restrict_recursive(
334 engine: &MVCCEngine,
335 txn_id: i64,
336 parent_table: &str,
337 old_value: &Value,
338 referencing_fks: &[(String, ForeignKeyConstraint)],
339 depth: usize,
340) -> Result<()> {
341 for (child_table_name, fk) in referencing_fks {
342 if !child_rows_exist(engine, txn_id, child_table_name, fk, old_value)? {
345 continue;
346 }
347 match fk.on_update {
348 ForeignKeyAction::Restrict | ForeignKeyAction::NoAction => {
349 return Err(Error::foreign_key_violation(
350 child_table_name,
351 &fk.column_name,
352 parent_table,
353 &fk.referenced_column,
354 format!(
355 "cannot update row with {} = {} — still referenced by table '{}'",
356 fk.referenced_column, old_value, child_table_name
357 ),
358 ));
359 }
360 ForeignKeyAction::Cascade | ForeignKeyAction::SetNull => {
361 if depth >= MAX_CASCADE_DEPTH {
364 return Err(Error::internal(format!(
365 "foreign key CASCADE depth limit ({}) exceeded — possible circular reference",
366 MAX_CASCADE_DEPTH
367 )));
368 }
369 let grandchild_fks = find_referencing_fks_for_txn(engine, txn_id, child_table_name);
370 let child_fk_col = &fk.column_name;
371 let relevant: Vec<_> = grandchild_fks
372 .iter()
373 .filter(|(_, gfk)| gfk.referenced_column == *child_fk_col)
374 .cloned()
375 .collect();
376 if !relevant.is_empty() {
377 pre_check_restrict_recursive(
378 engine,
379 txn_id,
380 child_table_name,
381 old_value,
382 &relevant,
383 depth + 1,
384 )?;
385 }
386 }
387 }
388 }
389 Ok(())
390}
391
392pub fn enforce_update_actions(
396 engine: &MVCCEngine,
397 txn_id: i64,
398 old_pk_value: &Value,
399 new_pk_value: &Value,
400 referencing_fks: &[(String, ForeignKeyConstraint)],
401) -> Result<i32> {
402 if referencing_fks.is_empty() {
403 return Ok(0);
404 }
405
406 let mut total_affected = 0i32;
407
408 for (child_table_name, fk) in referencing_fks {
409 let action = fk.on_update;
410
411 match action {
412 ForeignKeyAction::Restrict | ForeignKeyAction::NoAction => {
413 }
416 ForeignKeyAction::Cascade => {
417 let affected = cascade_update(
418 engine,
419 txn_id,
420 child_table_name,
421 fk,
422 old_pk_value,
423 new_pk_value,
424 )?;
425 total_affected = total_affected.saturating_add(affected);
426 }
427 ForeignKeyAction::SetNull => {
428 let affected =
429 set_null_on_delete(engine, txn_id, child_table_name, fk, old_pk_value)?;
430 total_affected = total_affected.saturating_add(affected);
431 }
432 }
433 }
434
435 Ok(total_affected)
436}
437
438fn child_rows_exist(
446 engine: &MVCCEngine,
447 txn_id: i64,
448 child_table: &str,
449 fk: &ForeignKeyConstraint,
450 parent_pk_value: &Value,
451) -> Result<bool> {
452 let child = engine.get_table_for_txn(txn_id, child_table)?;
453 let child_schema = child.schema();
454
455 let col_name = &child_schema.columns[fk.column_index].name;
457 let mut expr = radixdb_storage::expression::ComparisonExpr::new(
458 col_name.as_str(),
459 radixdb_core::Operator::Eq,
460 parent_pk_value.clone(),
461 );
462 expr.prepare_for_schema(child_schema);
463
464 let rows = child.collect_rows_with_limit_unordered(Some(&expr), 1, 0)?;
465 Ok(!rows.is_empty())
466}
467
468const MAX_CASCADE_DEPTH: usize = 16;
470
471fn cascade_delete(
475 engine: &MVCCEngine,
476 txn_id: i64,
477 child_table: &str,
478 fk: &ForeignKeyConstraint,
479 parent_pk_value: &Value,
480) -> Result<i32> {
481 cascade_delete_recursive(engine, txn_id, child_table, fk, parent_pk_value, 0)
482}
483
484fn cascade_delete_recursive(
485 engine: &MVCCEngine,
486 txn_id: i64,
487 child_table: &str,
488 fk: &ForeignKeyConstraint,
489 parent_pk_value: &Value,
490 depth: usize,
491) -> Result<i32> {
492 if depth >= MAX_CASCADE_DEPTH {
493 return Err(Error::internal(format!(
494 "foreign key CASCADE depth limit ({}) exceeded — possible circular reference",
495 MAX_CASCADE_DEPTH
496 )));
497 }
498
499 let grandchild_fks = find_referencing_fks_for_txn(engine, txn_id, child_table);
502 let mut deleted_child_rows: Vec<Row> = Vec::new();
503 let child_schema = engine.get_table_schema(child_table)?;
504
505 if !grandchild_fks.is_empty() {
506 let child_handle = engine.get_table_for_txn(txn_id, child_table)?;
510 let col_name = &child_schema.columns[fk.column_index].name;
511 let mut filter = radixdb_storage::expression::ComparisonExpr::new(
512 col_name.as_str(),
513 radixdb_core::Operator::Eq,
514 parent_pk_value.clone(),
515 );
516 filter.prepare_for_schema(&child_schema);
517 deleted_child_rows = child_handle
518 .collect_all_rows(Some(&filter))?
519 .into_iter()
520 .map(|(_, row)| row)
521 .collect();
522 }
523
524 if !grandchild_fks.is_empty() && !deleted_child_rows.is_empty() {
528 for child_row in &deleted_child_rows {
529 for (grandchild_table, grandchild_fk) in grandchild_fks.iter() {
530 let (referenced_index, _) = child_schema
531 .find_column(&grandchild_fk.referenced_column)
532 .ok_or_else(|| {
533 Error::internal(format!(
534 "foreign key references missing column '{}.{}'",
535 child_table, grandchild_fk.referenced_column
536 ))
537 })?;
538 let child_key = child_row.get(referenced_index).ok_or_else(|| {
539 Error::internal(format!(
540 "cascaded row is missing referenced column '{}.{}'",
541 child_table, grandchild_fk.referenced_column
542 ))
543 })?;
544 if matches!(
545 grandchild_fk.on_delete,
546 ForeignKeyAction::Restrict | ForeignKeyAction::NoAction
547 ) && child_rows_exist(
548 engine,
549 txn_id,
550 grandchild_table,
551 grandchild_fk,
552 child_key,
553 )? {
554 return Err(Error::foreign_key_violation(
555 grandchild_table,
556 &grandchild_fk.column_name,
557 child_table,
558 &grandchild_fk.referenced_column,
559 format!(
560 "cannot cascade-delete row with {} = {} — still referenced by table '{}'",
561 grandchild_fk.referenced_column, child_key, grandchild_table
562 ),
563 ));
564 }
565 }
566 }
567 }
568
569 let mut child = engine.get_table_for_txn(txn_id, child_table)?;
571 let col_name = &child_schema.columns[fk.column_index].name;
572 let mut expr = radixdb_storage::expression::ComparisonExpr::new(
573 col_name.as_str(),
574 radixdb_core::Operator::Eq,
575 parent_pk_value.clone(),
576 );
577 expr.prepare_for_schema(&child_schema);
578
579 let count = child.delete(Some(&expr))?;
580 let mut total = count;
584
585 if !grandchild_fks.is_empty() && !deleted_child_rows.is_empty() {
587 for child_row in &deleted_child_rows {
588 for (grandchild_table, grandchild_fk) in grandchild_fks.iter() {
589 let (referenced_index, _) = child_schema
590 .find_column(&grandchild_fk.referenced_column)
591 .ok_or_else(|| {
592 Error::internal(format!(
593 "foreign key references missing column '{}.{}'",
594 child_table, grandchild_fk.referenced_column
595 ))
596 })?;
597 let child_key = child_row.get(referenced_index).ok_or_else(|| {
598 Error::internal(format!(
599 "cascaded row is missing referenced column '{}.{}'",
600 child_table, grandchild_fk.referenced_column
601 ))
602 })?;
603 match grandchild_fk.on_delete {
604 ForeignKeyAction::Restrict | ForeignKeyAction::NoAction => {
605 }
607 ForeignKeyAction::Cascade => {
608 let affected = cascade_delete_recursive(
609 engine,
610 txn_id,
611 grandchild_table,
612 grandchild_fk,
613 child_key,
614 depth + 1,
615 )?;
616 total = total.saturating_add(affected);
617 }
618 ForeignKeyAction::SetNull => {
619 let affected = set_null_on_delete(
620 engine,
621 txn_id,
622 grandchild_table,
623 grandchild_fk,
624 child_key,
625 )?;
626 total = total.saturating_add(affected);
627 }
628 }
629 }
630 }
631 }
632
633 Ok(total)
634}
635
636fn cascade_update(
640 engine: &MVCCEngine,
641 txn_id: i64,
642 child_table: &str,
643 fk: &ForeignKeyConstraint,
644 old_value: &Value,
645 new_value: &Value,
646) -> Result<i32> {
647 cascade_update_recursive(engine, txn_id, child_table, fk, old_value, new_value, 0)
648}
649
650fn cascade_update_recursive(
651 engine: &MVCCEngine,
652 txn_id: i64,
653 child_table: &str,
654 fk: &ForeignKeyConstraint,
655 old_value: &Value,
656 new_value: &Value,
657 depth: usize,
658) -> Result<i32> {
659 if !child_rows_exist(engine, txn_id, child_table, fk, old_value)? {
662 return Ok(0);
663 }
664
665 if depth >= MAX_CASCADE_DEPTH {
667 return Err(Error::internal(format!(
668 "foreign key CASCADE depth limit ({}) exceeded — possible circular reference",
669 MAX_CASCADE_DEPTH
670 )));
671 }
672
673 let grandchild_fks = find_referencing_fks_for_txn(engine, txn_id, child_table);
675 let child_fk_col = &fk.column_name;
676
677 let relevant_grandchild_fks: Vec<_> = grandchild_fks
678 .iter()
679 .filter(|(_, gfk)| gfk.referenced_column == *child_fk_col)
680 .collect();
681
682 if !relevant_grandchild_fks.is_empty() {
684 for (grandchild_table, grandchild_fk) in &relevant_grandchild_fks {
685 if matches!(
686 grandchild_fk.on_update,
687 ForeignKeyAction::Restrict | ForeignKeyAction::NoAction
688 ) && child_rows_exist(engine, txn_id, grandchild_table, grandchild_fk, old_value)?
689 {
690 return Err(Error::foreign_key_violation(
691 grandchild_table,
692 &grandchild_fk.column_name,
693 child_table,
694 &grandchild_fk.referenced_column,
695 format!(
696 "cannot cascade-update row with {} = {} — still referenced by table '{}'",
697 grandchild_fk.referenced_column, old_value, grandchild_table
698 ),
699 ));
700 }
701 }
702 }
703
704 let mut child = engine.get_table_for_txn(txn_id, child_table)?;
706 let col_idx = fk.column_index;
707 let new_val = new_value.clone();
708
709 let child_schema = child.schema().clone();
710 let compiled_checks =
711 crate::mutation::validation::compile_table_check_constraints(&child_schema)?;
712 let mut check_vm = crate::expression::ExprVM::new();
713 let col_name = &child_schema.columns[col_idx].name;
714 let mut expr = radixdb_storage::expression::ComparisonExpr::new(
715 col_name.as_str(),
716 radixdb_core::Operator::Eq,
717 old_value.clone(),
718 );
719 expr.prepare_for_schema(&child_schema);
720
721 let count = child.update(Some(&expr), &mut |mut row| {
722 let _ = row.set(col_idx, new_val.clone());
723 validate_cascade_row(&child_schema, &row, &compiled_checks, &mut check_vm)?;
724 Ok((row, true))
725 })?;
726
727 let mut total = count;
728
729 if !relevant_grandchild_fks.is_empty() && count > 0 {
730 for (grandchild_table, grandchild_fk) in &relevant_grandchild_fks {
731 match grandchild_fk.on_update {
732 ForeignKeyAction::Restrict | ForeignKeyAction::NoAction => {
733 }
735 ForeignKeyAction::Cascade => {
736 let affected = cascade_update_recursive(
737 engine,
738 txn_id,
739 grandchild_table,
740 grandchild_fk,
741 old_value,
742 new_value,
743 depth + 1,
744 )?;
745 total = total.saturating_add(affected);
746 }
747 ForeignKeyAction::SetNull => {
748 let affected = set_null_recursive(
749 engine,
750 txn_id,
751 grandchild_table,
752 grandchild_fk,
753 old_value,
754 depth + 1,
755 )?;
756 total = total.saturating_add(affected);
757 }
758 }
759 }
760 }
761
762 Ok(total)
763}
764
765fn set_null_on_delete(
768 engine: &MVCCEngine,
769 txn_id: i64,
770 child_table: &str,
771 fk: &ForeignKeyConstraint,
772 parent_pk_value: &Value,
773) -> Result<i32> {
774 set_null_recursive(engine, txn_id, child_table, fk, parent_pk_value, 0)
775}
776
777fn set_null_recursive(
778 engine: &MVCCEngine,
779 txn_id: i64,
780 child_table: &str,
781 fk: &ForeignKeyConstraint,
782 parent_pk_value: &Value,
783 depth: usize,
784) -> Result<i32> {
785 if !child_rows_exist(engine, txn_id, child_table, fk, parent_pk_value)? {
786 return Ok(0);
787 }
788 if depth >= MAX_CASCADE_DEPTH {
789 return Err(Error::internal(format!(
790 "foreign key CASCADE depth limit ({}) exceeded — possible circular reference",
791 MAX_CASCADE_DEPTH
792 )));
793 }
794
795 let mut child = engine.get_table_for_txn(txn_id, child_table)?;
796
797 let col_idx = fk.column_index;
798
799 let child_schema = child.schema().clone();
801 if !child_schema.columns[col_idx].nullable {
802 return Err(Error::foreign_key_violation(
803 child_table,
804 &fk.column_name,
805 &fk.referenced_table,
806 &fk.referenced_column,
807 format!(
808 "cannot SET NULL on non-nullable column '{}'",
809 fk.column_name
810 ),
811 ));
812 }
813
814 let null_val = Value::null(child_schema.columns[col_idx].data_type);
815 let grandchild_fks = find_referencing_fks_for_txn(engine, txn_id, child_table);
816 let relevant_grandchild_fks: Vec<_> = grandchild_fks
817 .iter()
818 .filter(|(_, grandchild_fk)| grandchild_fk.referenced_column == fk.column_name)
819 .cloned()
820 .collect();
821 if !relevant_grandchild_fks.is_empty() {
822 pre_check_restrict_recursive(
823 engine,
824 txn_id,
825 child_table,
826 parent_pk_value,
827 &relevant_grandchild_fks,
828 depth + 1,
829 )?;
830 }
831
832 let compiled_checks =
833 crate::mutation::validation::compile_table_check_constraints(&child_schema)?;
834 let mut check_vm = crate::expression::ExprVM::new();
835 let col_name = &child_schema.columns[col_idx].name;
836 let mut expr = radixdb_storage::expression::ComparisonExpr::new(
837 col_name.as_str(),
838 radixdb_core::Operator::Eq,
839 parent_pk_value.clone(),
840 );
841 expr.prepare_for_schema(&child_schema);
842
843 let count = child.update(Some(&expr), &mut |mut row| {
844 let _ = row.set(col_idx, null_val.clone());
845 validate_cascade_row(&child_schema, &row, &compiled_checks, &mut check_vm)?;
846 Ok((row, true))
847 })?;
848 let mut total = count;
851 if count > 0 {
852 for (grandchild_table, grandchild_fk) in &relevant_grandchild_fks {
853 let affected = match grandchild_fk.on_update {
854 ForeignKeyAction::Restrict | ForeignKeyAction::NoAction => 0,
855 ForeignKeyAction::Cascade => cascade_update_recursive(
856 engine,
857 txn_id,
858 grandchild_table,
859 grandchild_fk,
860 parent_pk_value,
861 &null_val,
862 depth + 1,
863 )?,
864 ForeignKeyAction::SetNull => set_null_recursive(
865 engine,
866 txn_id,
867 grandchild_table,
868 grandchild_fk,
869 parent_pk_value,
870 depth + 1,
871 )?,
872 };
873 total = total.saturating_add(affected);
874 }
875 }
876
877 Ok(total)
878}
879
880pub fn check_no_referencing_rows(
892 engine: &MVCCEngine,
893 parent_table: &str,
894 txn_id: Option<i64>,
895) -> Result<()> {
896 let referencing = if let Some(txn_id) = txn_id {
897 find_referencing_fks_for_txn(engine, txn_id, parent_table)
898 } else {
899 find_referencing_fks(engine, parent_table)
900 };
901 if referencing.is_empty() {
902 return Ok(());
903 }
904
905 for (child_table, fk) in referencing.iter() {
906 let child_schema = engine.get_table_schema(child_table)?;
909 let col_name = &child_schema.columns[fk.column_index].name;
910 let mut not_null_expr =
911 radixdb_storage::expression::NullCheckExpr::is_not_null(col_name.as_str());
912 not_null_expr.prepare_for_schema(&child_schema);
913
914 let has_ref = if let Some(tid) = txn_id {
915 let child = engine.get_table_for_txn(tid, child_table)?;
916 !child
917 .collect_rows_with_limit_unordered(Some(¬_null_expr), 1, 0)?
918 .is_empty()
919 } else {
920 let tx = engine.begin_transaction()?;
921 let child = tx.get_table(child_table)?;
922 !child
923 .collect_rows_with_limit_unordered(Some(¬_null_expr), 1, 0)?
924 .is_empty()
925 };
926
927 if has_ref {
928 return Err(Error::foreign_key_violation(
929 child_table,
930 &fk.column_name,
931 parent_table,
932 &fk.referenced_column,
933 format!(
934 "cannot drop/truncate table '{}' — rows in '{}' still reference it",
935 parent_table, child_table
936 ),
937 ));
938 }
939 }
940
941 Ok(())
942}