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