packages/ztd-query-mysql/fuzz/Robustness/Invariant/RewritePlanConsistencyChecker.php
1<?php
2
3declare(strict_types=1);
4
5namespace Fuzz\Robustness\Invariant;
6
7use ZtdQuery\Exception\UnknownSchemaException;
8use ZtdQuery\Exception\UnsupportedSqlException;
9use ZtdQuery\Platform\MySql\Rewrite\MySqlRewriter;
10use ZtdQuery\Rewrite\QueryKind;
11use ZtdQuery\Rewrite\RewritePlan;
12use ZtdQuery\Shadow\Mutation\Row\MultiDeleteMutation;
13use ZtdQuery\Shadow\Mutation\Row\MultiUpdateMutation;
14
15/**
16 * Implements the Rewrite Plan Consistency Checker contract for MySQL.
17 */
18final class RewritePlanConsistencyChecker implements InvariantChecker
19{
20 private MySqlRewriter $rewriter;
21
22 /**
23 * Configure the dependencies used by this operation.
24 */
25 public function __construct(MySqlRewriter $rewriter)
26 {
27 $this->rewriter = $rewriter;
28 }
29
30 /**
31 * Check for the supplied MySQL input.
32 */
33 public function check(string $sql): ?InvariantViolation
34 {
35 try {
36 $plan = $this->rewriter->rewrite($sql);
37 } catch (UnsupportedSqlException | UnknownSchemaException) {
38 return null;
39 }
40
41 return $this->checkPlan($plan, $sql);
42 }
43
44 /**
45 * Check Plan for the supplied MySQL input.
46 */
47 public function checkPlan(RewritePlan $plan, string $sql): ?InvariantViolation
48 {
49 $kind = $plan->kind();
50 $mutation = $plan->mutation();
51
52 if (($kind === QueryKind::WRITE_SIMULATED || $kind === QueryKind::DDL_SIMULATED) && $mutation === null) {
53 return new InvariantViolation(
54 'INV-L2-02',
55 sprintf('%s plan has null mutation', $kind->value),
56 $sql,
57 ['kind' => $kind->value]
58 );
59 }
60
61 if (($kind === QueryKind::READ || $kind === QueryKind::SKIPPED) && $mutation !== null) {
62 return new InvariantViolation(
63 'INV-L2-03',
64 sprintf('%s plan has non-null mutation', $kind->value),
65 $sql,
66 ['kind' => $kind->value, 'mutation_class' => get_class($mutation)]
67 );
68 }
69
70 if ($plan->sql() === '') {
71 return new InvariantViolation(
72 'INV-L2-04',
73 'Rewritten SQL is empty',
74 $sql,
75 ['kind' => $kind->value]
76 );
77 }
78
79 if ($mutation instanceof MultiDeleteMutation || $mutation instanceof MultiUpdateMutation) {
80 foreach (array_keys($mutation->tableNames()) as $targetIndex) {
81 $metadataColumn = '__ztd_multi_' . $targetIndex . '_value_0';
82 if (!str_contains($plan->sql(), $metadataColumn)) {
83 return new InvariantViolation(
84 'INV-L2-08',
85 'multi-table mutation target is missing from the result projection',
86 $sql,
87 [
88 'target_index' => $targetIndex,
89 'rewrite_sql' => $plan->sql(),
90 ],
91 );
92 }
93 }
94 }
95
96 $shadowTables = ['users', 'orders', 'order_items', 'products'];
97 $relationParser = new \ZtdQuery\Platform\MySql\Sql\Relation\MySqlSelectRelationParser();
98 $normalizedInput = $relationParser->unqualify($sql, $shadowTables);
99 $normalizedPlan = $relationParser->unqualify($plan->sql(), $shadowTables);
100 if ($normalizedInput !== $sql && $normalizedPlan !== $plan->sql()) {
101 return new InvariantViolation(
102 'INV-L2-07',
103 'schema-qualified shadow source survived rewrite',
104 $sql,
105 ['rewrite_sql' => $plan->sql()]
106 );
107 }
108
109 return null;
110 }
111}
112