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