packages/ztd-query-postgres/fuzz/RewriteFuzzTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Fuzz;
6
7use Faker\Factory;
8use Override;
9use PHPUnit\Framework\Attributes\CoversNothing;
10use PHPUnit\Framework\Attributes\Large;
11use PHPUnit\Framework\TestCase;
12use SqlFaker\PostgreSqlProvider;
13use ZtdQuery\Exception\UnknownSchemaException;
14use ZtdQuery\Exception\UnsupportedSqlException;
15use ZtdQuery\Platform\Postgres\Rewrite\PgSqlQueryGuard;
16use ZtdQuery\Platform\Postgres\Rewrite\PgSqlRewriter;
17use ZtdQuery\Platform\Postgres\Rewrite\Transformer\DeleteTransformer;
18use ZtdQuery\Platform\Postgres\Rewrite\Transformer\InsertTransformer;
19use ZtdQuery\Platform\Postgres\Rewrite\Transformer\PgSqlTransformer;
20use ZtdQuery\Platform\Postgres\Rewrite\Transformer\SelectTransformer;
21use ZtdQuery\Platform\Postgres\Rewrite\Transformer\UpdateTransformer;
22use ZtdQuery\Platform\Postgres\Schema\PgSqlSchemaParser;
23use ZtdQuery\Platform\Postgres\Shadow\PgSqlMutationResolver;
24use ZtdQuery\Platform\Postgres\Sql\PgSqlIdentifierQuoter;
25use ZtdQuery\Platform\Postgres\Sql\PgSqlParser;
26use ZtdQuery\Platform\Postgres\Sql\Value\PgSqlCastRenderer;
27use ZtdQuery\Rewrite\QueryKind;
28use ZtdQuery\Schema\TableDefinition;
29use ZtdQuery\Schema\TableDefinitionRegistry;
30use ZtdQuery\Shadow\ShadowStore;
31
32/**
33 * Fuzz tests for PgSqlRewriter::rewrite().
34 *
35 * Guards the following properties:
36 * - INV-L2-01: rewrite() only throws UnsupportedSqlException or UnknownSchemaException (not arbitrary exceptions)
37 * - INV-L2-02: WRITE_SIMULATED/DDL_SIMULATED plans must have non-null mutation
38 * - INV-L2-03: READ plans must have null mutation
39 * - INV-L2-04: Rewritten SQL must not be empty
40 * - INV-L2-05: classify() and rewrite() must agree on QueryKind
41 * - Kind correctness per SQL type: SELECT->READ, INSERT/UPDATE/DELETE->WRITE_SIMULATED, DDL->DDL_SIMULATED
42 */
43#[CoversNothing]
44#[Large]
45final class RewriteFuzzTest extends TestCase
46{
47    private const ITERATIONS = 100;
48    private PgSqlRewriter $rewriter;
49    private PgSqlQueryGuard $guard;
50    private PostgreSqlProvider $provider;
51    #[Override]
52    protected function setUp(): void
53    {
54        $parser = new PgSqlParser();
55        $this->guard = new PgSqlQueryGuard($parser);
56        $shadowStore = new ShadowStore();
57        $registry = new TableDefinitionRegistry();
58        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id'], []));
59        $castRenderer = new PgSqlCastRenderer();
60        $quoter = new PgSqlIdentifierQuoter();
61        $selectTransformer = new SelectTransformer($castRenderer, $quoter);
62        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
63        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
64        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
65        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
66        $schemaParser = new PgSqlSchemaParser();
67        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
68        $this->rewriter = new PgSqlRewriter($this->guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
69        $faker = Factory::create();
70        $this->provider = new PostgreSqlProvider($faker, 'pg-17.2');
71        $faker->seed(20260815);
72    }
73    /**
74     * Test rewrite select returns read kind.
75     */
76    public function testRewriteSelectReturnsReadKind(): void
77    {
78        for ($i = 0; $i < self::ITERATIONS; $i++) {
79            $sql = $this->provider->selectStatement(50);
80            try {
81                $plan = $this->rewriter->rewrite($sql);
82                self::assertNotEmpty($plan->sql());
83                self::assertSame(QueryKind::READ, $plan->kind(), "SELECT rewrite should produce READ kind on iteration {$i}");
84                self::assertNull($plan->mutation(), "READ plan must have no mutation on iteration {$i}");
85            } catch (UnsupportedSqlException|UnknownSchemaException) {
86                continue;
87            }
88        }
89        self::addToAssertionCount(self::ITERATIONS);
90    }
91    /**
92     * Test rewrite insert returns write simulated kind.
93     */
94    public function testRewriteInsertReturnsWriteSimulatedKind(): void
95    {
96        for ($i = 0; $i < self::ITERATIONS; $i++) {
97            $sql = $this->provider->insertStatement(50);
98            try {
99                $plan = $this->rewriter->rewrite($sql);
100                self::assertNotEmpty($plan->sql());
101                self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind(), "INSERT rewrite should produce WRITE_SIMULATED kind on iteration {$i}");
102                self::assertNotNull($plan->mutation(), "WRITE_SIMULATED plan must have a mutation on iteration {$i}");
103            } catch (UnsupportedSqlException|UnknownSchemaException) {
104                continue;
105            }
106        }
107        self::addToAssertionCount(self::ITERATIONS);
108    }
109    /**
110     * Test rewrite update returns write simulated kind.
111     */
112    public function testRewriteUpdateReturnsWriteSimulatedKind(): void
113    {
114        for ($i = 0; $i < self::ITERATIONS; $i++) {
115            $sql = $this->provider->updateStatement(50);
116            try {
117                $plan = $this->rewriter->rewrite($sql);
118                self::assertNotEmpty($plan->sql());
119                self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind(), "UPDATE rewrite should produce WRITE_SIMULATED kind on iteration {$i}");
120                self::assertNotNull($plan->mutation(), "WRITE_SIMULATED plan must have a mutation on iteration {$i}");
121            } catch (UnsupportedSqlException|UnknownSchemaException) {
122                continue;
123            }
124        }
125        self::addToAssertionCount(self::ITERATIONS);
126    }
127    /**
128     * Test rewrite delete returns write simulated kind.
129     */
130    public function testRewriteDeleteReturnsWriteSimulatedKind(): void
131    {
132        for ($i = 0; $i < self::ITERATIONS; $i++) {
133            $sql = $this->provider->deleteStatement(50);
134            try {
135                $plan = $this->rewriter->rewrite($sql);
136                self::assertNotEmpty($plan->sql());
137                self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind(), "DELETE rewrite should produce WRITE_SIMULATED kind on iteration {$i}");
138                self::assertNotNull($plan->mutation(), "WRITE_SIMULATED plan must have a mutation on iteration {$i}");
139            } catch (UnsupportedSqlException|UnknownSchemaException) {
140                continue;
141            }
142        }
143        self::addToAssertionCount(self::ITERATIONS);
144    }
145    /**
146     * Test rewrite create table returns ddl simulated kind.
147     */
148    public function testRewriteCreateTableReturnsDdlSimulatedKind(): void
149    {
150        for ($i = 0; $i < self::ITERATIONS; $i++) {
151            $sql = $this->provider->createTableStatement(50);
152            try {
153                $plan = $this->rewriter->rewrite($sql);
154                self::assertNotEmpty($plan->sql());
155                self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind(), "CREATE TABLE rewrite should produce DDL_SIMULATED kind on iteration {$i}");
156            } catch (UnsupportedSqlException|UnknownSchemaException) {
157                continue;
158            }
159        }
160        self::addToAssertionCount(self::ITERATIONS);
161    }
162    /**
163     * Test rewrite drop table returns ddl simulated kind.
164     */
165    public function testRewriteDropTableReturnsDdlSimulatedKind(): void
166    {
167        for ($i = 0; $i < self::ITERATIONS; $i++) {
168            $sql = $this->provider->dropTableStatement(50);
169            try {
170                $plan = $this->rewriter->rewrite($sql);
171                self::assertNotEmpty($plan->sql());
172                self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind(), "DROP TABLE rewrite should produce DDL_SIMULATED kind on iteration {$i}");
173            } catch (UnsupportedSqlException|UnknownSchemaException) {
174                continue;
175            }
176        }
177        self::addToAssertionCount(self::ITERATIONS);
178    }
179    /**
180     * INV-L2-01: rewrite() must only throw UnsupportedSqlException or UnknownSchemaException.
181     * INV-L2-02/03/04: Plan consistency (mutation presence, non-empty SQL).
182     */
183    public function testRewriteExceptionTypesAndPlanConsistency(): void
184    {
185        for ($i = 0; $i < self::ITERATIONS; $i++) {
186            $sql = $this->provider->sql(maxDepth: 50);
187            try {
188                $plan = $this->rewriter->rewrite($sql);
189                self::assertNotEmpty($plan->sql(), "Rewritten SQL is empty on iteration {$i}");
190                if ($plan->kind() === QueryKind::WRITE_SIMULATED || $plan->kind() === QueryKind::DDL_SIMULATED) {
191                    self::assertNotNull($plan->mutation(), "{$plan->kind()->value} plan must have mutation on iteration {$i}");
192                }
193                if ($plan->kind() === QueryKind::READ) {
194                    self::assertNull($plan->mutation(), "READ plan must have no mutation on iteration {$i}");
195                }
196            } catch (UnsupportedSqlException|UnknownSchemaException) {
197                continue;
198            }
199        }
200        self::addToAssertionCount(self::ITERATIONS);
201    }
202    /**
203     * INV-L2-05: classify() and rewrite() must agree on QueryKind.
204     */
205    public function testClassifyRewriteAgreement(): void
206    {
207        for ($i = 0; $i < self::ITERATIONS; $i++) {
208            $sql = $this->provider->sql(maxDepth: 50);
209            $classifyResult = $this->guard->classify($sql);
210            if ($classifyResult === null) {
211                continue;
212            }
213            try {
214                $plan = $this->rewriter->rewrite($sql);
215                self::assertSame($classifyResult, $plan->kind(), "classify() returned {$classifyResult->value} but rewrite() returned {$plan->kind()->value} on iteration {$i} with SQL: {$sql}");
216            } catch (UnsupportedSqlException|UnknownSchemaException) {
217                continue;
218            }
219        }
220        self::addToAssertionCount(self::ITERATIONS);
221    }
222}
223