packages/ztd-query-core/src/Shadow/ReferentialIntegrityEnforcer.php
1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Shadow;
6
7use ZtdQuery\Exception\ForeignKeyViolationException;
8use ZtdQuery\Schema\TableDefinition;
9use ZtdQuery\Schema\TableDefinitionRegistry;
10use ZtdQuery\Shadow\Mutation\DataMutation;
11use ZtdQuery\Shadow\Mutation\ShadowMutation;
12
13/**
14 * Carries what a statement did outward through the constraints that declare it.
15 *
16 * A write to a parent table does not stop at that table: a key declared
17 * ON DELETE CASCADE takes the children with it, and those children may
18 * themselves be parents. What happened is therefore followed outward until
19 * nothing more follows, and only then is the result checked for the references
20 * it has left dangling.
21 *
22 * @phpstan-import-type Row from TableDefinition
23 */
24final class ReferentialIntegrityEnforcer
25{
26 /**
27 * @param TableDefinitionRegistry $registry Answers what a table declares
28 */
29 public function __construct(private readonly TableDefinitionRegistry $registry)
30 {
31 }
32
33 /**
34 * Applies every consequence of a statement, and refuses one it cannot.
35 *
36 * @param ShadowStore $before Shadow as it was
37 * @param ShadowStore $after Shadow as it became, written back in place
38 * @param ShadowMutation $mutation Statement that was simulated
39 * @param array<int, Row> $resultRows Rows the rewritten statement read back
40 * @param string $sql Statement being simulated, for the refusal
41 *
42 * @throws ForeignKeyViolationException When a constraint forbids the statement or is left broken
43 */
44 public function synchronize(
45 ShadowStore $before,
46 ShadowStore $after,
47 ShadowMutation $mutation,
48 array $resultRows,
49 string $sql,
50 ): void {
51 if (!$mutation instanceof DataMutation) {
52 return;
53 }
54
55 $ends = new ForeignKeyEnds($this->registry);
56 $cascade = new ForeignKeyCascade($ends);
57
58 $pending = (new TableTransitions($this->registry))->of($before, $after, $mutation, $resultRows);
59 while ($pending !== []) {
60 $parent = array_shift($pending);
61 foreach ($this->registry->getAll() as $childTable => $childDefinition) {
62 foreach ($childDefinition->foreignKeys as $constraintName => $foreignKey) {
63 if (strcasecmp($foreignKey->referencedTable, $parent->table) !== 0) {
64 continue;
65 }
66 $child = $cascade->of($after, $childTable, $constraintName, $foreignKey, $parent, $sql);
67 if ($child !== null) {
68 $pending[] = $child;
69 }
70 }
71 }
72 }
73
74 (new ForeignKeyIntegrity($this->registry, $ends))->assertHolds($after, $sql);
75 }
76}
77