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