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