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