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