packages/ztd-query-postgres/fuzz/ClassifyFuzzTest.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\Platform\Postgres\Rewrite\PgSqlQueryGuard;
14use ZtdQuery\Platform\Postgres\Sql\PgSqlParser;
15use ZtdQuery\Rewrite\QueryKind;
16
17/**
18 * Fuzz tests for PgSqlQueryGuard::classify().
19 *
20 * Guards the following properties:
21 * - INV-L1-01: classify() never throws on any input
22 * - INV-L1-02: classify() is deterministic (same input -> same output)
23 * - Kind correctness: SELECT->READ, INSERT/UPDATE/DELETE->WRITE_SIMULATED, DDL->DDL_SIMULATED
24 */
25#[CoversNothing]
26#[Large]
27final class ClassifyFuzzTest extends TestCase
28{
29 private const ITERATIONS = 100;
30 private PgSqlQueryGuard $guard;
31 private PostgreSqlProvider $provider;
32 #[Override]
33 protected function setUp(): void
34 {
35 $this->guard = new PgSqlQueryGuard(new PgSqlParser());
36 $faker = Factory::create();
37 $this->provider = new PostgreSqlProvider($faker, 'pg-17.2');
38 $faker->seed(20260815);
39 }
40 /**
41 * INV-L1-01: classify() must never throw on any generated SQL.
42 * INV-L1-02: classify() must be deterministic (same SQL -> same result).
43 */
44 public function testClassifyNeverThrowsAndIsDeterministic(): void
45 {
46 for ($i = 0; $i < self::ITERATIONS; $i++) {
47 $sql = $this->provider->sql(maxDepth: 50);
48 $result1 = $this->guard->classify($sql);
49 $result2 = $this->guard->classify($sql);
50 self::assertSame($result1, $result2, "classify() returned different results for the same SQL on iteration {$i}: {$sql}");
51 }
52 self::addToAssertionCount(self::ITERATIONS);
53 }
54 /**
55 * Test classify select returns read or null.
56 */
57 public function testClassifySelectReturnsReadOrNull(): void
58 {
59 for ($i = 0; $i < self::ITERATIONS; $i++) {
60 $sql = $this->provider->selectStatement(50);
61 $result = $this->guard->classify($sql);
62 if ($result !== null) {
63 self::assertSame(QueryKind::READ, $result, "SELECT should classify as READ on iteration {$i} with SQL: {$sql}");
64 }
65 }
66 self::addToAssertionCount(self::ITERATIONS);
67 }
68 /**
69 * Test classify insert returns write simulated or null.
70 */
71 public function testClassifyInsertReturnsWriteSimulatedOrNull(): void
72 {
73 for ($i = 0; $i < self::ITERATIONS; $i++) {
74 $sql = $this->provider->insertStatement(50);
75 $result = $this->guard->classify($sql);
76 if ($result !== null) {
77 self::assertSame(QueryKind::WRITE_SIMULATED, $result, "INSERT should classify as WRITE_SIMULATED on iteration {$i} with SQL: {$sql}");
78 }
79 }
80 self::addToAssertionCount(self::ITERATIONS);
81 }
82 /**
83 * Test classify update returns write simulated or null.
84 */
85 public function testClassifyUpdateReturnsWriteSimulatedOrNull(): void
86 {
87 for ($i = 0; $i < self::ITERATIONS; $i++) {
88 $sql = $this->provider->updateStatement(50);
89 $result = $this->guard->classify($sql);
90 if ($result !== null) {
91 self::assertSame(QueryKind::WRITE_SIMULATED, $result, "UPDATE should classify as WRITE_SIMULATED on iteration {$i} with SQL: {$sql}");
92 }
93 }
94 self::addToAssertionCount(self::ITERATIONS);
95 }
96 /**
97 * Test classify delete returns write simulated or null.
98 */
99 public function testClassifyDeleteReturnsWriteSimulatedOrNull(): void
100 {
101 for ($i = 0; $i < self::ITERATIONS; $i++) {
102 $sql = $this->provider->deleteStatement(50);
103 $result = $this->guard->classify($sql);
104 if ($result !== null) {
105 self::assertSame(QueryKind::WRITE_SIMULATED, $result, "DELETE should classify as WRITE_SIMULATED on iteration {$i} with SQL: {$sql}");
106 }
107 }
108 self::addToAssertionCount(self::ITERATIONS);
109 }
110 /**
111 * Test classify create table returns ddl simulated or null.
112 */
113 public function testClassifyCreateTableReturnsDdlSimulatedOrNull(): void
114 {
115 for ($i = 0; $i < self::ITERATIONS; $i++) {
116 $sql = $this->provider->createTableStatement(50);
117 $result = $this->guard->classify($sql);
118 if ($result !== null) {
119 self::assertSame(QueryKind::DDL_SIMULATED, $result, "CREATE TABLE should classify as DDL_SIMULATED on iteration {$i} with SQL: {$sql}");
120 }
121 }
122 self::addToAssertionCount(self::ITERATIONS);
123 }
124 /**
125 * Test classify drop table returns ddl simulated or null.
126 */
127 public function testClassifyDropTableReturnsDdlSimulatedOrNull(): void
128 {
129 for ($i = 0; $i < self::ITERATIONS; $i++) {
130 $sql = $this->provider->dropTableStatement(50);
131 $result = $this->guard->classify($sql);
132 if ($result !== null) {
133 self::assertSame(QueryKind::DDL_SIMULATED, $result, "DROP TABLE should classify as DDL_SIMULATED on iteration {$i} with SQL: {$sql}");
134 }
135 }
136 self::addToAssertionCount(self::ITERATIONS);
137 }
138 /**
139 * Test classify alter table returns ddl simulated or null.
140 */
141 public function testClassifyAlterTableReturnsDdlSimulatedOrNull(): void
142 {
143 for ($i = 0; $i < self::ITERATIONS; $i++) {
144 $sql = $this->provider->alterTableStatement(50);
145 $result = $this->guard->classify($sql);
146 if ($result !== null) {
147 self::assertSame(QueryKind::DDL_SIMULATED, $result, "ALTER TABLE should classify as DDL_SIMULATED on iteration {$i} with SQL: {$sql}");
148 }
149 }
150 self::addToAssertionCount(self::ITERATIONS);
151 }
152}
153