packages/ztd-query-postgres/fuzz/FullPipelineFuzzTest.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\TableDefinitionRegistry;
29use ZtdQuery\Shadow\ShadowStore;
30
31/**
32 * Full pipeline fuzz test for PostgreSQL.
33 *
34 * Tests the complete pipeline: schema parse -> register -> populate ShadowStore
35 * -> rewrite DML -> apply mutation -> verify integrity.
36 *
37 * Guards the following properties:
38 * - P-SM-1: INSERT mutation increases row count
39 * - P-SM-3: UPDATE mutation preserves row count
40 * - P-SM-5: Mutations only affect the target table (table isolation)
41 * - INV-L4-01: ShadowStore maintains array-of-arrays structure after mutations
42 * - INV-L2-02/03: Plan consistency (mutation presence matches kind)
43 * - DDL -> DML pipeline continuity (CREATE TABLE enables subsequent DML)
44 */
45#[CoversNothing]
46#[Large]
47final class FullPipelineFuzzTest extends TestCase
48{
49 private const ITERATIONS = 50;
50 private PgSqlSchemaParser $schemaParser;
51 private PostgreSqlProvider $provider;
52 private \Faker\Generator $faker;
53 #[Override]
54 protected function setUp(): void
55 {
56 $this->schemaParser = new PgSqlSchemaParser();
57 $this->faker = Factory::create();
58 $this->faker->seed(20260815);
59 $this->provider = new PostgreSqlProvider($this->faker, 'pg-17.2');
60 }
61
62
63
64 /**
65 * Test create table then select does not crash.
66 */
67 public function testCreateTableThenSelectDoesNotCrash(): void
68 {
69 $this->faker->seed(20260815);
70 for ($i = 0; $i < self::ITERATIONS; $i++) {
71 $createSql = $this->provider->createTableStatement(50);
72 $definition = $this->schemaParser->parse($createSql);
73 if ($definition === null || $definition->columns === []) {
74 continue;
75 }
76 $shadowStore = new ShadowStore();
77 $registry = new TableDefinitionRegistry();
78 $tableName = (new Input\SchemaRows())->extractTableName($createSql);
79 if ($tableName === null) {
80 continue;
81 }
82 $registry->register($tableName, $definition);
83 $fixtureRows = (new Input\SchemaRows())->generateFixtureRows($definition, $this->faker->numberBetween(0, 5));
84 $shadowStore->set($tableName, $fixtureRows);
85 $parser = new PgSqlParser();
86 $guard = new PgSqlQueryGuard($parser);
87 $castRenderer = new PgSqlCastRenderer();
88 $quoter = new PgSqlIdentifierQuoter();
89 $selectTransformer = new SelectTransformer($castRenderer, $quoter);
90 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
91 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
92 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
93 $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
94 $schemaParser = new PgSqlSchemaParser();
95 $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
96 $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
97 $selectSql = 'SELECT * FROM ' . (new PgSqlIdentifierQuoter())->quote($tableName);
98 try {
99 $plan = $rewriter->rewrite($selectSql);
100 self::assertNotEmpty($plan->sql());
101 self::assertSame(QueryKind::READ, $plan->kind());
102 self::assertNull($plan->mutation());
103 } catch (UnsupportedSqlException|UnknownSchemaException) {
104 continue;
105 }
106 }
107 self::addToAssertionCount(self::ITERATIONS);
108 }
109 /**
110 * Test create table then insert does not crash.
111 */
112 public function testCreateTableThenInsertDoesNotCrash(): void
113 {
114 $this->faker->seed(20260815);
115 for ($i = 0; $i < self::ITERATIONS; $i++) {
116 $createSql = $this->provider->createTableStatement(50);
117 $definition = $this->schemaParser->parse($createSql);
118 if ($definition === null || $definition->columns === []) {
119 continue;
120 }
121 $shadowStore = new ShadowStore();
122 $registry = new TableDefinitionRegistry();
123 $tableName = (new Input\SchemaRows())->extractTableName($createSql);
124 if ($tableName === null) {
125 continue;
126 }
127 $registry->register($tableName, $definition);
128 $shadowStore->set($tableName, []);
129 $parser = new PgSqlParser();
130 $guard = new PgSqlQueryGuard($parser);
131 $castRenderer = new PgSqlCastRenderer();
132 $quoter = new PgSqlIdentifierQuoter();
133 $selectTransformer = new SelectTransformer($castRenderer, $quoter);
134 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
135 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
136 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
137 $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
138 $schemaParser = new PgSqlSchemaParser();
139 $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
140 $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
141 $values = (new Input\InsertLiterals($this->faker))->buildInsertValues($definition);
142 $insertSql = 'INSERT INTO ' . (new PgSqlIdentifierQuoter())->quote($tableName) . ' (' . implode(', ', array_map(fn (string $c) => (new PgSqlIdentifierQuoter())->quote($c), $definition->columns)) . ') VALUES (' . $values . ')';
143 try {
144 $plan = $rewriter->rewrite($insertSql);
145 self::assertNotEmpty($plan->sql());
146 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
147 if ($plan->mutation() !== null) {
148 $countBefore = count($shadowStore->get($tableName));
149 $fakeResultRows = [(new Input\SchemaRows())->generateFixtureRows($definition, 1)[0]];
150 $plan->mutation()->apply($shadowStore, $fakeResultRows);
151 $storedRows = $shadowStore->get($tableName);
152 self::assertNotEmpty($storedRows);
153 self::assertGreaterThanOrEqual($countBefore, count($storedRows), "INSERT mutation should not decrease row count on iteration {$i}");
154 }
155 } catch (UnsupportedSqlException|UnknownSchemaException) {
156 continue;
157 }
158 }
159 self::addToAssertionCount(self::ITERATIONS);
160 }
161 /**
162 * Test create table then update does not crash.
163 */
164 public function testCreateTableThenUpdateDoesNotCrash(): void
165 {
166 $this->faker->seed(20260815);
167 for ($i = 0; $i < self::ITERATIONS; $i++) {
168 $createSql = $this->provider->createTableStatement(50);
169 $definition = $this->schemaParser->parse($createSql);
170 if ($definition === null || $definition->columns === [] || $definition->primaryKeys === []) {
171 continue;
172 }
173 $shadowStore = new ShadowStore();
174 $registry = new TableDefinitionRegistry();
175 $tableName = (new Input\SchemaRows())->extractTableName($createSql);
176 if ($tableName === null) {
177 continue;
178 }
179 $registry->register($tableName, $definition);
180 $fixtureRows = (new Input\SchemaRows())->generateFixtureRows($definition, 3);
181 $shadowStore->set($tableName, $fixtureRows);
182 $parser = new PgSqlParser();
183 $guard = new PgSqlQueryGuard($parser);
184 $castRenderer = new PgSqlCastRenderer();
185 $quoter = new PgSqlIdentifierQuoter();
186 $selectTransformer = new SelectTransformer($castRenderer, $quoter);
187 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
188 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
189 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
190 $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
191 $schemaParser = new PgSqlSchemaParser();
192 $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
193 $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
194 $firstCol = $definition->columns[0];
195 $updateSql = 'UPDATE ' . (new PgSqlIdentifierQuoter())->quote($tableName) . ' SET ' . (new PgSqlIdentifierQuoter())->quote($firstCol) . ' = ' . (new PgSqlIdentifierQuoter())->quote($firstCol);
196 try {
197 $plan = $rewriter->rewrite($updateSql);
198 self::assertNotEmpty($plan->sql());
199 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
200 if ($plan->mutation() !== null) {
201 $countBefore = count($shadowStore->get($tableName));
202 $plan->mutation()->apply($shadowStore, $fixtureRows);
203 $storedRows = $shadowStore->get($tableName);
204 self::assertCount($countBefore, $storedRows, "UPDATE mutation should preserve row count on iteration {$i}");
205 }
206 } catch (UnsupportedSqlException|UnknownSchemaException) {
207 continue;
208 }
209 }
210 self::addToAssertionCount(self::ITERATIONS);
211 }
212 /**
213 * Test create table then delete does not crash.
214 */
215 public function testCreateTableThenDeleteDoesNotCrash(): void
216 {
217 $this->faker->seed(20260815);
218 for ($i = 0; $i < self::ITERATIONS; $i++) {
219 $createSql = $this->provider->createTableStatement(50);
220 $definition = $this->schemaParser->parse($createSql);
221 if ($definition === null || $definition->columns === []) {
222 continue;
223 }
224 $shadowStore = new ShadowStore();
225 $registry = new TableDefinitionRegistry();
226 $tableName = (new Input\SchemaRows())->extractTableName($createSql);
227 if ($tableName === null) {
228 continue;
229 }
230 $registry->register($tableName, $definition);
231 $fixtureRows = (new Input\SchemaRows())->generateFixtureRows($definition, 3);
232 $shadowStore->set($tableName, $fixtureRows);
233 $parser = new PgSqlParser();
234 $guard = new PgSqlQueryGuard($parser);
235 $castRenderer = new PgSqlCastRenderer();
236 $quoter = new PgSqlIdentifierQuoter();
237 $selectTransformer = new SelectTransformer($castRenderer, $quoter);
238 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
239 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
240 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
241 $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
242 $schemaParser = new PgSqlSchemaParser();
243 $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
244 $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
245 $deleteSql = 'DELETE FROM ' . (new PgSqlIdentifierQuoter())->quote($tableName);
246 try {
247 $plan = $rewriter->rewrite($deleteSql);
248 self::assertNotEmpty($plan->sql());
249 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
250 if ($plan->mutation() !== null) {
251 $plan->mutation()->apply($shadowStore, $fixtureRows);
252 $storedRows = $shadowStore->get($tableName);
253 self::assertSame([], $storedRows, "Deleting every result row should empty the shadow table on iteration {$i}");
254 }
255 } catch (UnsupportedSqlException|UnknownSchemaException) {
256 continue;
257 }
258 }
259 self::addToAssertionCount(self::ITERATIONS);
260 }
261 /**
262 * Test create table rewrite registers then dml succeeds.
263 */
264 public function testCreateTableRewriteRegistersThenDmlSucceeds(): void
265 {
266 $this->faker->seed(20260815);
267 for ($i = 0; $i < self::ITERATIONS; $i++) {
268 $createSql = $this->provider->createTableStatement(50);
269 $definition = $this->schemaParser->parse($createSql);
270 if ($definition === null || $definition->columns === []) {
271 continue;
272 }
273 $shadowStore = new ShadowStore();
274 $registry = new TableDefinitionRegistry();
275 $tableName = (new Input\SchemaRows())->extractTableName($createSql);
276 if ($tableName === null) {
277 continue;
278 }
279 $parser = new PgSqlParser();
280 $guard = new PgSqlQueryGuard($parser);
281 $castRenderer = new PgSqlCastRenderer();
282 $quoter = new PgSqlIdentifierQuoter();
283 $selectTransformer = new SelectTransformer($castRenderer, $quoter);
284 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
285 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
286 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
287 $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
288 $schemaParser = new PgSqlSchemaParser();
289 $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
290 $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
291 try {
292 $createPlan = $rewriter->rewrite($createSql);
293 self::assertSame(QueryKind::DDL_SIMULATED, $createPlan->kind());
294 if ($createPlan->mutation() !== null) {
295 $createPlan->mutation()->apply($shadowStore, []);
296 }
297 $selectSql = 'SELECT * FROM ' . (new PgSqlIdentifierQuoter())->quote($tableName);
298 $selectPlan = $rewriter->rewrite($selectSql);
299 self::assertNotEmpty($selectPlan->sql());
300 self::assertSame(QueryKind::READ, $selectPlan->kind());
301 } catch (UnsupportedSqlException|UnknownSchemaException) {
302 continue;
303 }
304 }
305 self::addToAssertionCount(self::ITERATIONS);
306 }
307 /**
308 * Test shadow store integrity after multiple operations.
309 */
310 public function testShadowStoreIntegrityAfterMultipleOperations(): void
311 {
312 $this->faker->seed(20260815);
313 for ($i = 0; $i < self::ITERATIONS; $i++) {
314 $createSql = $this->provider->createTableStatement(50);
315 $definition = $this->schemaParser->parse($createSql);
316 if ($definition === null || $definition->columns === []) {
317 continue;
318 }
319 $shadowStore = new ShadowStore();
320 $registry = new TableDefinitionRegistry();
321 $tableName = (new Input\SchemaRows())->extractTableName($createSql);
322 if ($tableName === null) {
323 continue;
324 }
325 $registry->register($tableName, $definition);
326 $fixtureRows = (new Input\SchemaRows())->generateFixtureRows($definition, 3);
327 $shadowStore->set($tableName, $fixtureRows);
328 $parser = new PgSqlParser();
329 $guard = new PgSqlQueryGuard($parser);
330 $castRenderer = new PgSqlCastRenderer();
331 $quoter = new PgSqlIdentifierQuoter();
332 $selectTransformer = new SelectTransformer($castRenderer, $quoter);
333 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
334 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
335 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
336 $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
337 $schemaParser = new PgSqlSchemaParser();
338 $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
339 $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
340 $operations = ['SELECT * FROM ' . (new PgSqlIdentifierQuoter())->quote($tableName), 'INSERT INTO ' . (new PgSqlIdentifierQuoter())->quote($tableName) . ' (' . implode(', ', array_map(fn (string $c) => (new PgSqlIdentifierQuoter())->quote($c), $definition->columns)) . ') VALUES (' . (new Input\InsertLiterals($this->faker))->buildInsertValues($definition) . ')'];
341 if ($definition->primaryKeys !== []) {
342 $operations[] = 'UPDATE ' . (new PgSqlIdentifierQuoter())->quote($tableName) . ' SET ' . (new PgSqlIdentifierQuoter())->quote($definition->columns[0]) . ' = ' . (new PgSqlIdentifierQuoter())->quote($definition->columns[0]);
343 }
344 $operations[] = 'DELETE FROM ' . (new PgSqlIdentifierQuoter())->quote($tableName);
345 foreach ($operations as $sql) {
346 try {
347 $plan = $rewriter->rewrite($sql);
348 self::assertNotEmpty($plan->sql());
349 self::assertInstanceOf(QueryKind::class, $plan->kind());
350 if ($plan->mutation() !== null) {
351 $fakeRows = (new Input\SchemaRows())->generateFixtureRows($definition, 1);
352 $plan->mutation()->apply($shadowStore, $fakeRows);
353 }
354 $allData = $shadowStore->getAll();
355 foreach ($allData as $tblName => $tblRows) {
356 self::assertNotEmpty($tblName, 'ShadowStore table name must not be empty');
357 foreach ($tblRows as $rowIdx => $row) {
358 self::assertNotEmpty($row, "ShadowStore table '{$tblName}' row {$rowIdx} must not be empty");
359 }
360 }
361 self::assertArrayHasKey($tableName, $allData);
362 if ($plan->kind() === QueryKind::READ) {
363 self::assertNull($plan->mutation(), 'READ plan must have no mutation');
364 } elseif ($plan->kind() === QueryKind::WRITE_SIMULATED) {
365 self::assertNotNull($plan->mutation(), 'WRITE_SIMULATED plan must have a mutation');
366 } elseif ($plan->kind() === QueryKind::DDL_SIMULATED) {
367 self::assertNotNull($plan->mutation(), 'DDL_SIMULATED plan must have a mutation');
368 }
369 } catch (UnsupportedSqlException|UnknownSchemaException) {
370 continue;
371 }
372 }
373 }
374 self::addToAssertionCount(self::ITERATIONS);
375 }
376
377
378
379}
380