packages/ztd-query-sqlite/fuzz/Robustness/Target/RewriteCheck.php

1<?php
2
3declare(strict_types=1);
4
5namespace Fuzz\Robustness\Target;
6
7use Error;
8use ZtdQuery\Exception\UnknownSchemaException;
9use ZtdQuery\Exception\UnsupportedSqlException;
10use ZtdQuery\Platform\Sqlite\Rewrite\Index\SqliteIndexHintStripper;
11use ZtdQuery\Platform\Sqlite\Rewrite\SqliteQueryGuard;
12use ZtdQuery\Platform\Sqlite\Rewrite\SqliteRewriter;
13use ZtdQuery\Platform\Sqlite\Sql\Attach\SqliteInMemoryAttachStatement;
14use ZtdQuery\Platform\Sqlite\Sql\Diagnostic\SqliteReadOnlyDiagnosticStatement;
15use ZtdQuery\Platform\Sqlite\Sql\Relation\SqliteSelectRelationParser;
16use ZtdQuery\Rewrite\QueryKind;
17
18/**
19 * Checks the public rewrite-plan contract and the removal of physical-table references.
20 */
21final class RewriteCheck
22{
23    /**
24     * Allows only absent fixture schemas and unsupported SQL; all other failures are findings.
25     *
26     * @throws Error When a rewrite violates its classification or shadowing contract.
27     */
28    public static function verify(SqliteQueryGuard $guard, SqliteRewriter $rewriter, string $sql): void
29    {
30        $kind = $guard->classify($sql);
31        if ($kind !== $guard->classify($sql)) {
32            throw new Error('Classification changed for identical SQL');
33        }
34        $passthrough = SqliteReadOnlyDiagnosticStatement::isSafe($sql) || SqliteInMemoryAttachStatement::isSafe($sql);
35        if ($passthrough && $kind !== QueryKind::READ) {
36            throw new Error('Safe passthrough was not classified as READ');
37        }
38        try {
39            $plan = $rewriter->rewrite($sql);
40        } catch (UnsupportedSqlException | UnknownSchemaException $rejection) {
41            if ($passthrough) {
42                throw new Error('Safe passthrough was rejected', 0, $rejection);
43            }
44            return;
45        }
46        if ($plan->kind() !== $kind) {
47            throw new Error('Classification and rewrite disagree on the query kind');
48        }
49        if ($passthrough && $plan->sql() !== $sql) {
50            throw new Error('Safe passthrough SQL was changed');
51        }
52        $writes = $kind === QueryKind::WRITE_SIMULATED || $kind === QueryKind::DDL_SIMULATED;
53        if ($writes !== ($plan->mutation() !== null)) {
54            throw new Error('Mutation presence does not match the query kind');
55        }
56        if ($plan->sql() === '') {
57            throw new Error('Rewritten SQL is empty');
58        }
59        $tables = ['users', 'orders', 'order_items', 'products'];
60        $relations = new SqliteSelectRelationParser();
61        if ($relations->unqualify($sql, $tables) !== $sql && $relations->unqualify($plan->sql(), $tables) !== $plan->sql()) {
62            throw new Error('Schema-qualified shadow source survived rewrite');
63        }
64        $hints = new SqliteIndexHintStripper();
65        if ($hints->strip($sql, $tables) !== $sql && $hints->strip($plan->sql(), $tables) !== $plan->sql()) {
66            throw new Error('Physical index hint survived shadow-source rewrite');
67        }
68    }
69}
70