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\Generation\Plan\ProductionPattern;
14use SqlFaker\MySql\MySqlProvider;
15use SqlFixture\Provider\FixtureGenerator;
16use SqlFixture\Provider\PlatformFactory;
17
18/**
19 * Mutates SQL structure and lexical choices, then checks schema acceptance and the generated 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::constrained('create_table_stmt', [
41 'create_table_stmt' => [ProductionPattern::containing('table_element_list')],
42 ])->requiringNonEmpty()->withExpansionBudget($maxExpansions);
43 }
44
45 /**
46 * Accepted schemas must generate exactly their writable columns, including explicit overrides.
47 *
48 * @throws Error When generated fixture columns differ from the parsed schema
49 */
50 public function __invoke(string $input): void
51 {
52 $plan = (new BytePlanCompiler())->compile($input, $this->planner, $this->constraints);
53 $sql = $this->sqlProvider->generate($plan);
54 $schema = PlatformFactory::createSchemaParser(PlatformFactory::DRIVER_MYSQL, $this->grammarVersion)->parse($sql);
55 $this->faker->seed(crc32(str_pad($input, 4, "\0")));
56 $generator = new FixtureGenerator($this->faker);
57 $row = $generator->generate($schema);
58 $writable = array_filter($schema->columns, static fn ($column): bool => !$column->autoIncrement && !$column->generated);
59 if (array_keys($row) !== array_keys($writable)) {
60 throw new Error("Writable column mismatch; grammar={$this->grammarVersion}; input=" . bin2hex($input) . "\nSQL: " . $sql);
61 }
62 $overridden = $generator->generate($schema, $row);
63 if ($overridden !== $row) {
64 throw new Error('Override preservation mismatch; input=' . bin2hex($input) . "\nSQL: " . $sql);
65 }
66 }
67}
68