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