packages/ztd-query-postgres/fuzz/TransformerFuzzTest.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\UnsupportedSqlException;
14use ZtdQuery\Platform\Postgres\Rewrite\Transformer\SelectTransformer;
15use ZtdQuery\Platform\Postgres\Sql\PgSqlIdentifierQuoter;
16use ZtdQuery\Platform\Postgres\Sql\Value\PgSqlCastRenderer;
17use ZtdQuery\Schema\ColumnDeclaration;
18use ZtdQuery\Schema\ColumnTypeFamily;
19
20/**
21 * Fuzz tests for SelectTransformer::transform().
22 *
23 * Guards the following properties:
24 * - P-TF-1: transform() accepts generated SELECTs or reports UnsupportedSqlException
25 * - P-TF-2: When SQL references a shadowed table, the output contains a WITH clause (CTE injection)
26 * - P-TF-3: Accepted SELECTs with empty table context retain their SQL
27 * - P-TF-4: Empty-row shadow tables still inject CTE with WHERE FALSE
28 */
29#[CoversNothing]
30#[Large]
31final class TransformerFuzzTest extends TestCase
32{
33 private const ITERATIONS = 100;
34 private SelectTransformer $transformer;
35 private PostgreSqlProvider $provider;
36 #[Override]
37 protected function setUp(): void
38 {
39 $this->transformer = new SelectTransformer(new PgSqlCastRenderer(), new PgSqlIdentifierQuoter());
40 $faker = Factory::create();
41 $this->provider = new PostgreSqlProvider($faker, 'pg-17.2');
42 $faker->seed(20260815);
43 }
44 /**
45 * Test transform does not crash on random select with empty tables.
46 */
47 public function testTransformDoesNotCrashOnRandomSelectWithEmptyTables(): void
48 {
49 for ($i = 0; $i < self::ITERATIONS; $i++) {
50 $sql = $this->provider->selectStatement(50);
51 try {
52 $result = $this->transformer->transform($sql, []);
53 } catch (UnsupportedSqlException $exception) {
54 self::assertStringContainsString('ZTD Write Protection:', $exception->getMessage());
55 continue;
56 }
57 self::assertNotEmpty($result, "transform() returned empty string on iteration {$i}");
58 self::assertSame($sql, $result);
59 }
60 self::addToAssertionCount(self::ITERATIONS);
61 }
62 /**
63 * Test transform with shadow data contains with clause.
64 */
65 public function testTransformWithShadowDataContainsWithClause(): void
66 {
67 $tables = ['users' => ['rows' => [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com'], ['id' => 2, 'name' => 'Bob', 'email' => 'bob@example.com']], 'columns' => ['id', 'name', 'email'], 'columnTypes' => ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]]];
68 $withCount = 0;
69 for ($i = 0; $i < self::ITERATIONS; $i++) {
70 $sql = $this->provider->selectStatement(50);
71 try {
72 $result = $this->transformer->transform($sql, $tables);
73 } catch (UnsupportedSqlException $exception) {
74 self::assertStringContainsString('ZTD Write Protection:', $exception->getMessage());
75 continue;
76 }
77 self::assertNotEmpty($result, "transform() returned empty string on iteration {$i}");
78 if (stripos($sql, 'users') !== false) {
79 self::assertStringContainsString('WITH', $result, "transform() should inject CTE when SQL references shadowed table on iteration {$i}");
80 $withCount++;
81 }
82 }
83 self::addToAssertionCount(self::ITERATIONS);
84 }
85 /**
86 * Test transform with empty rows contains with clause.
87 */
88 public function testTransformWithEmptyRowsContainsWithClause(): void
89 {
90 $tables = ['users' => ['rows' => [], 'columns' => ['id', 'name', 'email'], 'columnTypes' => ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]]];
91 for ($i = 0; $i < self::ITERATIONS; $i++) {
92 $sql = $this->provider->selectStatement(50);
93 try {
94 $result = $this->transformer->transform($sql, $tables);
95 } catch (UnsupportedSqlException $exception) {
96 self::assertStringContainsString('ZTD Write Protection:', $exception->getMessage());
97 continue;
98 }
99 self::assertNotEmpty($result, "transform() returned empty string on iteration {$i}");
100 if (stripos($sql, 'users') !== false) {
101 self::assertStringContainsString('WITH', $result, "transform() should inject CTE when SQL references shadowed table on iteration {$i}");
102 }
103 }
104 self::addToAssertionCount(self::ITERATIONS);
105 }
106}
107