packages/ztd-query-postgres/fuzz/SchemaParserFuzzTest.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\Schema\PgSqlSchemaParser;
14use ZtdQuery\Schema\TableDefinition;
15
16/**
17 * Fuzz tests for PgSqlSchemaParser::parse().
18 *
19 * Guards the following properties:
20 * - P-SP-0: parse() never crashes on any generated CREATE TABLE
21 * - P-SP-1: primaryKeys is a subset of columns
22 * - P-SP-2: columnTypes keys are a subset of columns
23 * - P-SP-3: notNullColumns is a subset of columns
24 * - P-SP-4: uniqueConstraints columns are subsets of columns
25 * - P-SP-5: typedColumns keys are a subset of columns
26 * - P-SP-6: Non-null result always has non-empty columns
27 * - P-SP-7: parse() returns null for non-CREATE TABLE statements
28 */
29#[CoversNothing]
30#[Large]
31final class SchemaParserFuzzTest extends TestCase
32{
33    private const ITERATIONS = 100;
34    private PgSqlSchemaParser $parser;
35    private PostgreSqlProvider $provider;
36    #[Override]
37    protected function setUp(): void
38    {
39        $this->parser = new PgSqlSchemaParser();
40        $faker = Factory::create();
41        $this->provider = new PostgreSqlProvider($faker, 'pg-17.2');
42        $faker->seed(20260815);
43    }
44    /**
45     * Test parse does not crash on random create table.
46     */
47    public function testParseDoesNotCrashOnRandomCreateTable(): void
48    {
49        for ($i = 0; $i < self::ITERATIONS; $i++) {
50            $sql = $this->provider->createTableStatement(50);
51            $result = $this->parser->parse($sql);
52            if ($result !== null) {
53                self::assertInstanceOf(TableDefinition::class, $result);
54            }
55        }
56        self::addToAssertionCount(self::ITERATIONS);
57    }
58    /**
59     * Check that accepted definitions retain consistent column references.
60     *
61     * SQLFaker can generate constraints naming undeclared columns. The parser
62     * may reject these definitions; accepted metadata must satisfy its contract.
63     */
64    public function testParseStructuralInvariantsOnRandomCreateTable(): void
65    {
66        for ($i = 0; $i < self::ITERATIONS; $i++) {
67            $sql = $this->provider->createTableStatement(50);
68            $result = $this->parser->parse($sql);
69            if ($result === null) {
70                continue;
71            }
72            foreach ($result->primaryKeys as $pk) {
73                self::assertContains($pk, $result->columns, "Primary key '{$pk}' is not in columns on iteration {$i} with SQL: {$sql}");
74            }
75            foreach (array_keys($result->columnTypes) as $colName) {
76                self::assertContains($colName, $result->columns, "Column type key '{$colName}' is not in columns on iteration {$i} with SQL: {$sql}");
77            }
78            foreach ($result->notNullColumns as $notNull) {
79                self::assertContains($notNull, $result->columns, "Not-null column '{$notNull}' is not in columns on iteration {$i} with SQL: {$sql}");
80            }
81            foreach ($result->uniqueConstraints as $constraintName => $constraintCols) {
82                foreach ($constraintCols as $col) {
83                    self::assertContains($col, $result->columns, "Unique constraint '{$constraintName}' column '{$col}' is not in columns on iteration {$i} with SQL: {$sql}");
84                }
85            }
86            self::assertNotEmpty($result->columns, "columns is empty for non-null result on iteration {$i} with SQL: {$sql}");
87            foreach (array_keys($result->typedColumns) as $typedCol) {
88                self::assertContains($typedCol, $result->columns, "typedColumns key '{$typedCol}' is not in columns on iteration {$i} with SQL: {$sql}");
89            }
90        }
91        self::addToAssertionCount(self::ITERATIONS);
92    }
93    /**
94     * Test parse returns null on non create table sql.
95     */
96    public function testParseReturnsNullOnNonCreateTableSql(): void
97    {
98        for ($i = 0; $i < self::ITERATIONS; $i++) {
99            $sql = $this->provider->selectStatement(50);
100            $result = $this->parser->parse($sql);
101            self::assertNull($result, "parse() should return null for SELECT on iteration {$i} with SQL: {$sql}");
102        }
103        self::addToAssertionCount(self::ITERATIONS);
104    }
105}
106