packages/sql-semantics-mysql/fuzz/Target/SchemaTarget.php

1<?php
2
3declare(strict_types=1);
4
5namespace Fuzz\Target;
6
7use Error;
8use SqlFormatter\Facade\Formatter;
9use SqlSemantics\Facade\Schema;
10use Throwable;
11
12/**
13 * Every planned definition must survive analysis, writing and immutable composition.
14 */
15final class SchemaTarget
16{
17    public function __construct(private readonly Schema $semantics, private readonly Formatter $compact, private readonly string $grammarVersion)
18    {
19    }
20
21    /**
22     * No generated input or semantic rejection is filtered out.
23     */
24    public function verify(string $sql, string $input): void
25    {
26        try {
27            $state = $this->semantics->analyze($sql);
28            if (count($state->tables) !== 1) {
29                throw new Error('A planned declaration must define one table.');
30            }
31            $before = serialize($state);
32            $printed = \SqlSemantics\Statement\Writer::render($state->tables[0]->source);
33            if ($this->compact->format($sql) !== $this->compact->format($printed)) {
34                throw new Error('Schema state lost declaration structure.');
35            }
36            $again = $this->semantics->analyze($printed);
37            if (serialize($again) !== $before || serialize($state) !== $before) {
38                throw new Error('Schema state is not stable across reconstruction.');
39            }
40            if (str_contains($before, 'SqlParser\\')) {
41                throw new Error('Schema state retained a parser object.');
42            }
43            $reset = $this->semantics->analyze('DROP TABLE IF EXISTS schema_fuzz_previous; ' . $printed . ';');
44            if (serialize($reset) !== $before) {
45                throw new Error('An unrelated conditional reset changed declared state.');
46            }
47        } catch (Throwable $error) {
48            throw new Error("Schema property failed\nGrammar: {$this->grammarVersion}\nInput (hex): " . bin2hex($input) . "\nSQL: {$sql}\n{$error->getMessage()}", 0, $error);
49        }
50    }
51}
52