packages/sql-fixture/fuzz/Target/CreateTableTarget.php
1<?php
2
3declare(strict_types=1);
4
5namespace Fuzz\Target;
6
7use Error;
8use Faker\Factory;
9use Faker\Generator;
10use SqlFaker\Generation\Choice\BytePlanCompiler;
11use SqlFaker\Generation\Choice\PlanBuilder;
12use SqlFaker\Generation\Plan\GenerationPlan;
13use SqlFaker\MySqlProvider;
14use SqlFixture\FixtureGenerator;
15use SqlFixture\Platform\MySql\MySqlSchemaParser;
16use SqlFixture\Schema\SchemaParseException;
17
18/**
19 * Mutates SQL structure and lexical choices, then checks the accepted-schema row contract.
20 */
21final class CreateTableTarget
22{
23 private readonly Generator $faker;
24 private readonly MySqlProvider $sqlProvider;
25 private readonly PlanBuilder $planner;
26
27 /**
28 * @var GenerationPlan<bool>
29 */
30 private readonly GenerationPlan $constraints;
31
32 /**
33 * Bounds grammar expansion while keeping raw input choices available to the planner.
34 */
35 public function __construct(private readonly string $grammarVersion, int $maxExpansions = 128)
36 {
37 $this->faker = Factory::create();
38 $this->sqlProvider = new MySqlProvider($this->faker, $grammarVersion);
39 $this->planner = $this->sqlProvider->planner();
40 $this->constraints = GenerationPlan::fromRule('create_table_stmt')->requiringNonEmpty()->withExpansionBudget($maxExpansions);
41 }
42
43 /**
44 * Accepted schemas must generate exactly their writable columns, including explicit overrides.
45 *
46 * @throws Error When generated fixture columns differ from the parsed schema
47 */
48 public function __invoke(string $input): void
49 {
50 $plan = (new BytePlanCompiler())->compile($input, $this->planner, $this->constraints);
51 $sql = $this->sqlProvider->generate($plan);
52 try {
53 $schema = (new MySqlSchemaParser())->parse($sql);
54 } catch (SchemaParseException) {
55 return;
56 }
57 $this->faker->seed(crc32(str_pad($input, 4, "\0")));
58 $generator = new FixtureGenerator($this->faker);
59 $row = $generator->generate($schema);
60 $writable = array_filter($schema->columns, static fn ($column): bool => !$column->autoIncrement && !$column->generated);
61 if (array_keys($row) !== array_keys($writable)) {
62 throw new Error("Writable column mismatch; grammar={$this->grammarVersion}; input=" . bin2hex($input) . "\nSQL: " . $sql);
63 }
64 $overridden = $generator->generate($schema, $row);
65 if ($overridden !== $row) {
66 throw new Error('Override preservation mismatch; input=' . bin2hex($input) . "\nSQL: " . $sql);
67 }
68 }
69}
70