packages/sql-faker/src/Generation/SqlGenerator.php
1<?php
2
3declare(strict_types=1);
4
5namespace SqlFaker\Generation;
6
7use Closure;
8use Faker\Generator;
9use SqlFaker\Generation\Choice\PlanBuilder;
10use SqlFaker\Generation\Coverage\GeneratorRevision;
11use SqlFaker\Generation\Coverage\GrammarCoverage;
12use SqlFaker\Generation\Coverage\GrammarCoverageInventory;
13use SqlFaker\Generation\Coverage\SequenceObservation;
14use SqlFaker\Generation\Derivation\TokenGenerator;
15use SqlFaker\Generation\Exception\GenerationException;
16use SqlFaker\Generation\Exception\LexicalException;
17use SqlFaker\Generation\Lexeme\LexicalGrammar;
18use SqlFaker\Generation\Lexeme\ResolvedOutput;
19use SqlFaker\Generation\Output\SqlSerializer;
20use SqlFaker\Generation\Plan\GenerationPlan;
21use SqlFaker\Generation\Token\TerminalSequence;
22use SqlFaker\Generation\Token\TokenRewriter;
23use SqlFaker\Grammar\Model\Grammar;
24
25/**
26 * Derives terminals, rewrites structural constraints, and realizes lexemes once.
27 * Dialect definitions supply the syntax and boundary rules; no completed SQL is retried or repaired.
28 *
29 * @visibility root
30 */
31final class SqlGenerator
32{
33 private ?TokenGenerator $tokens = null;
34
35 /**
36 * The latest terminal sequence retains both the original grammar choices and their rewrites.
37 */
38 public ?TerminalSequence $lastSequence = null;
39
40 /**
41 * Selected lexical candidates and resolved boundary rules from the latest generation.
42 */
43 public ?ResolvedOutput $lastOutput = null;
44
45 /**
46 * Binds grammar, lexical definitions and structural rules before generation starts.
47 * @param (Closure(string|null): string)|null $startSymbol Resolves explicitly requested release aliases
48 */
49 public function __construct(
50 private readonly Grammar $grammar,
51 private readonly Generator $faker,
52 private readonly LexicalGrammar $lexicalGrammar,
53 private readonly ?TokenRewriter $rewriter = null,
54 private readonly ?Closure $startSymbol = null,
55 private readonly ?GrammarCoverage $coverage = null,
56 ?Grammar $original = null,
57 ) {
58 $coverage?->register(new GrammarCoverageInventory($grammar, $grammar->startSymbol, $lexicalGrammar->version(), $original), GeneratorRevision::current());
59 }
60
61 /**
62 * Reuses the same grammar, rewrite rules and lexical definitions to compile plans.
63 */
64 public function planner(): PlanBuilder
65 {
66 return new PlanBuilder($this->grammar, $this->lexicalGrammar, $this->rewriter, $this->startSymbol);
67 }
68
69 /**
70 * Generates once through the declared stages; candidate absence remains an error.
71 * @template TRequiresNonEmpty of bool
72 * @param GenerationPlan<TRequiresNonEmpty> $plan
73 * @return (TRequiresNonEmpty is true ? non-empty-string : string)
74 * @throws GenerationException When the grammar or plan cannot produce the requested output
75 * @throws LexicalException When no applicable lexical realization exists
76 */
77 public function generate(GenerationPlan $plan): string
78 {
79 $this->lastSequence = null;
80 $this->lastOutput = null;
81 $requested = $plan->startRule();
82 $root = $requested ?? $this->grammar->startSymbol;
83 $this->coverage?->beginGeneration($root, ['budget' => $plan->expansionBudget(), 'lexicalTarget' => $plan->lexicalTarget()]);
84 $this->coverage?->beginAttempt(0);
85 try {
86 $root = $requested !== null && $this->startSymbol !== null ? ($this->startSymbol)($requested) : $root;
87 $sql = $plan->lexicalTarget() !== null ? $this->lexicalGrammar->generate($plan) : $this->realize($root, $plan);
88 if ($sql === '' && $plan->requiresNonEmpty()) {
89 throw GenerationException::planRequiresNonEmptyOutput($this->lexicalGrammar->version());
90 }
91 $this->coverage?->commitAttempt(
92 hash('sha256', $sql),
93 $this->lastSequence === null ? [] : (new SequenceObservation())->preserved($this->lastSequence)
94 );
95 return $sql;
96 } finally {
97 $this->coverage?->endGeneration();
98 }
99 }
100
101 /**
102 * Derives and realizes once, retaining source selection independently of transformed output.
103 * @param GenerationPlan<bool> $plan
104 * @throws GenerationException When derivation cannot complete
105 * @throws LexicalException When a terminal has no compatible realization
106 */
107 public function realize(string $root, GenerationPlan $plan): string
108 {
109 $tokens = ($this->tokens ??= new TokenGenerator($this->grammar, $this->faker, $this->lexicalGrammar->isNonOutput(...)))->generate($root, $plan);
110 $this->lastSequence = $this->rewriter?->rewrite($tokens) ?? $tokens;
111 $this->coverage?->recordSequence($this->lastSequence);
112 $this->lastOutput = $this->lexicalGrammar->resolveSequence($this->lastSequence, $plan, fn (int $count): int => $this->faker->numberBetween(0, $count - 1));
113 $this->coverage?->recordOutput($this->lastOutput);
114 return (new SqlSerializer())->serialize($this->lastOutput->pieces());
115 }
116}
117