packages/ztd-query-mysql/fuzz/Robustness/Invariant/ClassifyRewriteAgreementChecker.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\MySqlQueryGuard;
10use ZtdQuery\Platform\MySql\Rewrite\MySqlRewriter;
11use ZtdQuery\Platform\MySql\Sql\Diagnostic\MySqlReadOnlyDiagnosticStatement;
12use ZtdQuery\Rewrite\QueryKind;
13
14/**
15 * Implements the Classify Rewrite Agreement Checker contract for MySQL.
16 */
17final class ClassifyRewriteAgreementChecker implements InvariantChecker
18{
19    private MySqlQueryGuard $guard;
20    private MySqlRewriter $rewriter;
21
22    /**
23     * Configure the dependencies used by this operation.
24     */
25    public function __construct(MySqlQueryGuard $guard, MySqlRewriter $rewriter)
26    {
27        $this->guard = $guard;
28        $this->rewriter = $rewriter;
29    }
30
31    /**
32     * Check for the supplied MySQL input.
33     */
34    public function check(string $sql): ?InvariantViolation
35    {
36        $diagnostic = MySqlReadOnlyDiagnosticStatement::isSafe($sql);
37
38        $classifyResult = $this->guard->classify($sql);
39
40        if ($diagnostic && $classifyResult !== QueryKind::READ) {
41            return new InvariantViolation('INV-L2-06', 'read-only diagnostic was not classified as READ', $sql);
42        }
43
44        try {
45            $plan = $this->rewriter->rewrite($sql);
46        } catch (UnknownSchemaException $exception) {
47            if ($diagnostic) {
48                return new InvariantViolation('INV-L2-06', 'read-only diagnostic required schema metadata', $sql, ['exception' => $exception::class]);
49            }
50
51            return null;
52        } catch (UnsupportedSqlException $exception) {
53            if ($diagnostic) {
54                return new InvariantViolation('INV-L2-06', 'read-only diagnostic was rejected', $sql, ['exception' => $exception::class]);
55            }
56
57            return null;
58        }
59
60        if ($diagnostic && ($plan->kind() !== QueryKind::READ || $plan->sql() !== $sql)) {
61            return new InvariantViolation('INV-L2-06', 'read-only diagnostic was not preserved as an unchanged READ plan', $sql);
62        }
63
64        if ($plan->kind() !== $classifyResult) {
65            return new InvariantViolation(
66                'INV-L2-05',
67                'classify() and rewrite() disagree on QueryKind',
68                $sql,
69                [
70                    'classify_result' => $classifyResult?->value,
71                    'rewrite_kind' => $plan->kind()->value,
72                ]
73            );
74        }
75
76        return null;
77    }
78}
79