packages/sql-faker/tests/Unit/MySql/Generation/Lexeme/DefinitionFactoryTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\MySql\Generation\Lexeme;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use RuntimeException;
12use SqlFaker\Generation\Lexeme\LexemeInput;
13use SqlFaker\Generation\Lexeme\ResolvedOutput;
14use SqlFaker\Generation\Output\SqlSerializer;
15use SqlFaker\Generation\Token\TerminalSequence;
16use SqlFaker\MySql\Generation\Lexeme\DefinitionFactory;
17
18#[CoversClass(DefinitionFactory::class)]
19#[UsesClass(LexemeInput::class)]
20#[UsesClass(ResolvedOutput::class)]
21#[UsesClass(SqlSerializer::class)]
22#[UsesClass(TerminalSequence::class)]
23#[UsesClass(\SqlFaker\Generation\Plan\GenerationPlan::class)]
24#[UsesClass(\SqlFaker\Generation\Plan\ProductionPattern::class)]
25#[UsesClass(\SqlFaker\Generation\Candidate\ChoiceLexemeGenerator::class)]
26#[UsesClass(\SqlFaker\Generation\Candidate\FixedLexemeGenerator::class)]
27#[UsesClass(\SqlFaker\Generation\Candidate\IntegerLexemeGenerator::class)]
28#[UsesClass(\SqlFaker\Generation\Lexeme\Lexeme::class)]
29#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeCandidates::class)]
30#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeSequence::class)]
31#[UsesClass(\SqlFaker\Generation\Candidate\MatchingLexemeGenerator::class)]
32#[UsesClass(\SqlFaker\Generation\Candidate\ValueLexemeGenerator::class)]
33#[UsesClass(\SqlFaker\Generation\Candidate\SequenceLexemeGenerator::class)]
34#[UsesClass(\SqlFaker\Generation\Output\BoundaryCompletion::class)]
35#[UsesClass(\SqlFaker\Generation\Output\CandidateResolver::class)]
36#[UsesClass(\SqlFaker\Generation\Lexeme\OutputPart::class)]
37#[UsesClass(\SqlFaker\Generation\Output\ReverseLexemeGenerator::class)]
38#[UsesClass(\SqlFaker\Generation\Output\CombinedSpacingRule::class)]
39#[UsesClass(\SqlFaker\MySql\Generation\Spacing\KeywordPhraseSpacingRule::class)]
40#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeBoundary::class)]
41#[UsesClass(\SqlFaker\Generation\Lexeme\SpacingConstraint::class)]
42#[UsesClass(\SqlFaker\Generation\Token\ProductionOccurrence::class)]
43#[UsesClass(\SqlFaker\Generation\Token\TerminalOccurrence::class)]
44#[UsesClass(\SqlFaker\Generation\Value\CharacterDomain::class)]
45#[UsesClass(\SqlFaker\Generation\Value\ChoiceDomain::class)]
46#[UsesClass(\SqlFaker\MySql\Generation\Value\DollarQuotedDomain::class)]
47#[UsesClass(\SqlFaker\Generation\Value\IntegerDomain::class)]
48#[UsesClass(\SqlFaker\Generation\Value\SequenceDomain::class)]
49#[UsesClass(\SqlFaker\Generation\Value\ValueChoices::class)]
50#[UsesClass(\SqlFaker\Generation\Candidate\VersionCase::class)]
51#[UsesClass(\SqlFaker\Generation\Candidate\VersionedLexemeGenerator::class)]
52#[UsesClass(\SqlFaker\Generation\Exception\LexicalException::class)]
53#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\BoundedIntegerLexemeGenerator::class)]
54#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\CharsetLexemeGenerator::class)]
55#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\CharsetValueLexemeGenerator::class)]
56#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\FactorLexemeGenerator::class)]
57#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\KeywordLexemeGenerator::class)]
58#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\PrecisionLexemeGenerator::class)]
59#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\ReplicationTablePatternLexemeGenerator::class)]
60#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\SizeNumberLexemeGenerator::class)]
61#[UsesClass(\SqlFaker\MySql\Generation\Spacing\CloneAddressSpacingRule::class)]
62#[UsesClass(\SqlFaker\MySql\Generation\Spacing\FunctionSpacingRule::class)]
63#[UsesClass(\SqlFaker\MySql\Generation\Spacing\QualifiedNameSpacingRule::class)]
64#[UsesClass(\SqlFaker\MySql\Generation\Spacing\VariableSpacingRule::class)]
65#[UsesClass(\SqlFaker\MySql\Generation\Value\IdentifierDomain::class)]
66#[UsesClass(\SqlFaker\MySql\Generation\Value\QuotedDomain::class)]
67#[UsesClass(\SqlFaker\MySql\Generation\Value\RadixDomain::class)]
68#[UsesClass(\SqlFaker\Generation\Value\Utf8::class)]
69#[UsesClass(\SqlFaker\Generation\Value\WordDomain::class)]
70#[UsesClass(\SqlFaker\Grammar\Resource\SqlVersionRegistry::class)]
71#[UsesClass(\SqlFaker\Grammar\Resource\SqlVersion::class)]
72#[UsesClass(\SqlFaker\Generation\Choice\BytePlanCompiler::class)]
73#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\LexicalDefinition::class)]
74final class DefinitionFactoryTest extends TestCase
75{
76    public function testCreateCombinesLexicalOutputAndBoundaryDecisions(): void
77    {
78        $pipeline = (new DefinitionFactory())->create('mysql-8.4.7')->pipeline;
79        $result = $pipeline->generate(TerminalSequence::fromNames(['NUM', 'END_OF_INPUT']), null, static fn (int $count): int => 0);
80        self::assertSame('1', (new SqlSerializer())->serialize($result->pieces()));
81    }
82
83    public function testLexemesRejectsAnUnreviewedVersion(): void
84    {
85        $this->expectException(RuntimeException::class);
86        (new DefinitionFactory())->create('future-version')->lexemes;
87    }
88
89    #[DataProvider('providerKeyBlockSizes')]
90    public function testLexemesUsesTheTwoByteKeyBlockSizeDomain(string $version, string $value, bool $valid): void
91    {
92        $generator = (new DefinitionFactory())->create($version)->lexemes;
93        $candidates = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['KEY_BLOCK_SIZE_NUMBER']), 0, new ResolvedOutput(), $value));
94        self::assertNotNull($candidates);
95        self::assertSame($valid ? [$value] : [], array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$candidates->sequences()]));
96    }
97
98    /**
99     * @return iterable<array{string, string, bool}>
100     */
101    public static function providerKeyBlockSizes(): iterable
102    {
103        foreach (['mysql-5.6.51', 'mysql-5.7.44', 'mysql-8.0.44', 'mysql-8.1.0', 'mysql-8.2.0', 'mysql-8.3.0', 'mysql-8.4.7', 'mysql-9.0.1', 'mysql-9.1.0'] as $version) {
104            foreach ([['0', true], ['1', true], ['65535', true], ['0xffff', true], ["X'ffff'", true], ['65536', false], ['0x10000', false], ['2147483648', false]] as [$value, $valid]) {
105                yield [$version, $value, $valid];
106            }
107        }
108    }
109    #[DataProvider('providerNumericLimits')]
110    public function testLexemesKeepsParserNumericBoundaries(string $terminal, string $value, bool $valid): void
111    {
112        $generator = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes;
113        $candidates = $generator->generate(new LexemeInput(TerminalSequence::fromNames([$terminal]), 0, new ResolvedOutput(), $value));
114        self::assertNotNull($candidates);
115        self::assertSame($valid ? [$value] : [], array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$candidates->sequences()]));
116    }
117
118    /**
119     * @return list<array{string, string, bool}>
120     */
121    public static function providerNumericLimits(): array
122    {
123        return [
124
125            ['WEIGHT_STRING_LENGTH', '0', false], ['WEIGHT_STRING_LENGTH', '1', true],
126            ['WEIGHT_STRING_LENGTH', '2147483647', true], ['WEIGHT_STRING_LENGTH', '2147483648', false],
127            ['STATS_SAMPLE_PAGES_NUMBER', '0', false], ['STATS_SAMPLE_PAGES_NUMBER', '1', true],
128            ['STATS_SAMPLE_PAGES_NUMBER', '65535', true], ['STATS_SAMPLE_PAGES_NUMBER', '65536', false],
129            ['SOURCE_DELAY_NUMBER', '0', true], ['SOURCE_DELAY_NUMBER', '2147483647', true],
130            ['SOURCE_DELAY_NUMBER', '2147483648', false],
131            ['DISPLAY_WIDTH_NUMBER', '0', true],
132            ['DISPLAY_WIDTH_NUMBER', '255', true],
133            ['DISPLAY_WIDTH_NUMBER', '256', false],
134            ['DISPLAY_WIDTH_NUMBER', '-1', false],
135            ['BIT_WIDTH_NUMBER', '0', false],
136            ['BIT_WIDTH_NUMBER', '1', true],
137            ['BIT_WIDTH_NUMBER', '64', true],
138            ['BIT_WIDTH_NUMBER', '65', false],
139            ['BIT_WIDTH_NUMBER', '-1', false],
140            ['PARTITION_COUNT_NUMBER', '0', false],
141            ['PARTITION_COUNT_NUMBER', '1', true],
142            ['PARTITION_COUNT_NUMBER', '4294967295', true],
143            ['PARTITION_COUNT_NUMBER', '4294967296', false],
144            ['DECIMAL_PRECISION_NUMBER', '0', true],
145            ['DECIMAL_PRECISION_NUMBER', '65', true],
146            ['DECIMAL_PRECISION_NUMBER', '66', false],
147            ['DECIMAL_PRECISION_NUMBER', '-1', false],
148            ['FLOAT_PRECISION_NUMBER', '0', true],
149            ['FLOAT_PRECISION_NUMBER', '53', true],
150            ['FLOAT_PRECISION_NUMBER', '54', false],
151            ['FLOAT_PRECISION_NUMBER', '-1', false],
152            ['VARCHAR_LENGTH_NUMBER', '0', true],
153            ['VARCHAR_LENGTH_NUMBER', '65535', true],
154            ['VARCHAR_LENGTH_NUMBER', '65536', false],
155            ['VARCHAR_LENGTH_NUMBER', '-1', false],
156            ['FIELD_LENGTH_NUMBER', '0', true],
157            ['FIELD_LENGTH_NUMBER', '4294967295', true],
158            ['FIELD_LENGTH_NUMBER', '4294967296', false],
159            ['FIELD_LENGTH_NUMBER', '-1', false],
160            ['AVG_ROW_LENGTH_NUMBER', '0', true], ['AVG_ROW_LENGTH_NUMBER', '4294967295', true],
161            ['AVG_ROW_LENGTH_NUMBER', '4294967296', false], ['AVG_ROW_LENGTH_NUMBER', '-1', false],
162            ['KEY_ALGORITHM_NUMBER', '1', true], ['KEY_ALGORITHM_NUMBER', '2', true],
163            ['KEY_ALGORITHM_NUMBER', '0x02', true], ['KEY_ALGORITHM_NUMBER', '0', false], ['KEY_ALGORITHM_NUMBER', '3', false],
164            ['YEAR_WIDTH_NUMBER', '4', true], ['YEAR_WIDTH_NUMBER', '04', true],
165            ['YEAR_WIDTH_NUMBER', '2', false], ['YEAR_WIDTH_NUMBER', '5', false],
166        ];
167    }
168
169    public function testValuesLeavesUnknownTerminalsUnclaimed(): void
170    {
171        self::assertNull((new DefinitionFactory())->create('mysql-8.4.7')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames(['UNKNOWN']), 0, new ResolvedOutput())));
172    }
173
174    public function testNamesChecksSourceDelimiterOrValueBoundariesValue(): void
175    {
176        $generator = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes;
177        $valid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['IDENT_QUOTED']), 0, new ResolvedOutput(), '`a``b`'));
178        $invalid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['IDENT_QUOTED']), 0, new ResolvedOutput(), '`unclosed'));
179        self::assertNotNull($valid);
180        self::assertNotNull($invalid);
181        self::assertSame('`a``b`', [...$valid->sequences()][0]->lexemes[0]->text);
182        self::assertSame([], [...$invalid->sequences()]);
183    }
184
185    public function testStringsChecksSourceDelimiterOrValueBoundariesValue(): void
186    {
187        $generator = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes;
188        $valid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['TEXT_STRING']), 0, new ResolvedOutput(), "'a''b'"));
189        $invalid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['TEXT_STRING']), 0, new ResolvedOutput(), "'unclosed"));
190        self::assertNotNull($valid);
191        self::assertNotNull($invalid);
192        self::assertSame("'a''b'", [...$valid->sequences()][0]->lexemes[0]->text);
193        self::assertSame([], [...$invalid->sequences()]);
194    }
195
196    public function testNumbersChecksSourceDelimiterOrValueBoundariesValue(): void
197    {
198        $generator = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes;
199        $valid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['NUM']), 0, new ResolvedOutput(), '2147483647'));
200        $invalid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['NUM']), 0, new ResolvedOutput(), '2147483648'));
201        self::assertNotNull($valid);
202        self::assertNotNull($invalid);
203        self::assertSame('2147483647', [...$valid->sequences()][0]->lexemes[0]->text);
204        self::assertSame([], [...$invalid->sequences()]);
205    }
206
207    public function testBinaryChecksSourceDelimiterOrValueBoundariesValue(): void
208    {
209        $generator = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes;
210        $valid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['HEX_NUM']), 0, new ResolvedOutput(), "X'0f'"));
211        $invalid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['HEX_NUM']), 0, new ResolvedOutput(), "X'f'"));
212        self::assertNotNull($valid);
213        self::assertNotNull($invalid);
214        self::assertSame("X'0f'", [...$valid->sequences()][0]->lexemes[0]->text);
215        self::assertSame([], [...$invalid->sequences()]);
216    }
217
218    #[DataProvider('providerValueTokensValue')]
219    public function testCreateOffersACompleteCandidateForEverySourceValueTokenValue(string $terminal): void
220    {
221        $result = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames([$terminal]), 0, new ResolvedOutput()));
222        self::assertNotNull($result);
223        self::assertNotEmpty([...$result->sequences()]);
224    }
225
226    /**
227     * @return list<array{string}>
228     */
229    public static function providerValueTokensValue(): array
230    {
231        return [['IDENT'], ['IDENT_QUOTED'], ['LEX_HOSTNAME'], ['UNDERSCORE_CHARSET'], ['TEXT_STRING'], ['NCHAR_STRING'], ['NUM'], ['LONG_NUM'], ['ULONGLONG_NUM'], ['DECIMAL_NUM'], ['FLOAT_NUM'], ['HEX_NUM'], ['BIN_NUM']];
232    }
233
234    /**
235     * @param list<string> $expected
236     */
237    #[DataProvider('providerLexicalFormsValue')]
238    public function testCreateRetainsValidSourceSpellingsAndRejectsMalformedOnesValue(string $terminal, string $spelling, array $expected): void
239    {
240        $result = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames([$terminal]), 0, new ResolvedOutput(), $spelling));
241        self::assertNotNull($result);
242        self::assertSame($expected, array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$result->sequences()]));
243    }
244
245    /**
246     * @return list<array{string, string, list<string>}>
247     */
248    public static function providerLexicalFormsValue(): array
249    {
250        return [
251            ['IDENT', 'alpha_2', ['alpha_2']],
252            ['IDENT', '$bad name', []],
253            ['LEX_HOSTNAME', 'host.example', ['host.example']],
254            ['LEX_HOSTNAME', 'host/name', []],
255            ['UNDERSCORE_CHARSET', '_UTF8MB4', ['_UTF8MB4']],
256            ['UNDERSCORE_CHARSET', 'utf8mb4', []],
257            ['NCHAR_STRING', 'n\'a\\\'b\'', ['n\'a\\\'b\'']],
258            ['NCHAR_STRING', 'N\'unclosed', []],
259            ['LONG_NUM', '9223372036854775807', ['9223372036854775807']],
260            ['LONG_NUM', '2147483647', []],
261            ['ULONGLONG_NUM', '9223372036854775808', ['9223372036854775808']],
262            ['ULONGLONG_NUM', '18446744073709551616', []],
263            ['DECIMAL_NUM', '1', []],
264            ['DECIMAL_NUM', '18446744073709551615', []],
265            ['DECIMAL_NUM', '18446744073709551616', ['18446744073709551616']],
266            ['DECIMAL_NUM', '.75', ['.75']],
267            ['DECIMAL_NUM', '1.5e2', []],
268            ['FLOAT_NUM', '12.5E-4', ['12.5E-4']],
269            ['FLOAT_NUM', '12.5', []],
270            ['BIN_NUM', 'b\'001\'', ['b\'001\'']],
271            ['BIN_NUM', 'B\'02\'', []],
272        ];
273    }
274
275    public function testBinaryDomainKeepsExplicitByteSpellingsAvailableForCompatibleCharsetsValue(): void
276    {
277        $input = new LexemeInput(TerminalSequence::fromNames(['HEX_NUM']), 0, new ResolvedOutput(), "X'ff'");
278        $result = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes->generate($input);
279        self::assertNotNull($result);
280        self::assertSame("X'ff'", [...$result->sequences()][0]->lexemes[0]->text);
281    }
282
283    public function testStringsKeepsConstructedIntroducedValuesValidUnderAnExplicitAsciiCharsetValue(): void
284    {
285        $tokens = TerminalSequence::fromNames(['UNDERSCORE_CHARSET', 'TEXT_STRING']);
286        $result = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes->generate(new LexemeInput($tokens, 1, new ResolvedOutput(), values: new \SqlFaker\Generation\Value\ValueChoices(static fn (int $count): int => $count - 1)));
287        self::assertNotNull($result);
288        self::assertSame(1, preg_match('/\A[\x00-\x7f]*\z/D', [...$result->sequences()][0]->lexemes[0]->text));
289    }
290
291    /**
292     * @param list<int> $decisions
293     * @param list<string> $expected
294     */
295    #[DataProvider('providerConstructedBoundariesValue')]
296    public function testCreateConstructsScannerBoundaryValuesValue(string $terminal, array $decisions, array $expected): void
297    {
298        $values = new \SqlFaker\Generation\Value\ValueChoices(static function (int $count) use (&$decisions): int {
299            return array_shift($decisions) ?? $count - 1;
300        });
301        $result = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames([$terminal]), 0, new ResolvedOutput(), values: $values));
302        self::assertNotNull($result);
303        self::assertSame($expected, array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$result->sequences()]));
304    }
305
306    /**
307     * @return iterable<array{string, list<int>, list<string>}>
308     */
309    public static function providerConstructedBoundariesValue(): iterable
310    {
311        $minimum = [1, ...array_fill(0, 512, 0)];
312        $padding = str_repeat('0', 16);
313
314        yield ['IDENT', $minimum, ['_sf']];
315        yield ['IDENT', [], ['_sf' . str_repeat('$', 60)]];
316        yield ['IDENT_QUOTED', $minimum, ['`a`']];
317        yield ['IDENT_QUOTED', [], ['`' . str_repeat('猫', 64) . '`']];
318        yield ['LEX_HOSTNAME', $minimum, ['a']];
319        yield ['LEX_HOSTNAME', [], [str_repeat('$', 64)]];
320        yield ['TEXT_STRING', $minimum, ["''"]];
321        yield ['TEXT_STRING', [], ["'" . str_repeat('猫', 255) . "'"]];
322        yield ['NCHAR_STRING', $minimum, ["N''"]];
323        yield ['NCHAR_STRING', [], ["n'" . str_repeat('猫', 255) . "'"]];
324        yield ['NUM', $minimum, ['0']];
325        yield ['NUM', [], [$padding . '2147483647']];
326        yield ['LONG_NUM', $minimum, ['2147483648']];
327        yield ['LONG_NUM', [], [$padding . '9223372036854775807']];
328        yield ['ULONGLONG_NUM', $minimum, ['9223372036854775808']];
329        yield ['ULONGLONG_NUM', [], [$padding . '18446744073709551615']];
330        yield ['DECIMAL_NUM', $minimum, ['18446744073709551616', '1.5']];
331        yield ['DECIMAL_NUM', [], [$padding . str_repeat('9', 65), '.' . str_repeat('9', 20)]];
332        yield ['FLOAT_NUM', $minimum, ['0e0']];
333        yield ['FLOAT_NUM', [], ['.' . str_repeat('9', 20) . 'E-999']];
334        yield ['HEX_NUM', $minimum, ['0x0']];
335        yield ['HEX_NUM', [], ["X'" . str_repeat('F', 32) . "'"]];
336        yield ['BIN_NUM', $minimum, ['0b0']];
337        yield ['BIN_NUM', [], ["B'" . str_repeat('1', 64) . "'"]];
338
339        foreach (range(1, 127) as $byte) {
340            $encoded = str_replace(['\\', "'"], ['\\\\', "''"], chr($byte));
341            yield ['TEXT_STRING', [1, 0, 1, $byte - 1], ["'" . $encoded . "'"]];
342            yield ['NCHAR_STRING', [1, 0, 1, $byte - 1], ["N'" . $encoded . "'"]];
343        }
344    }
345
346    /**
347     * @param list<int> $decisions
348     */
349    #[DataProvider('providerIntroducedBoundariesValue')]
350    public function testBinaryAndStringsConstructCompleteAsciiValuesAfterIntroducersValue(string $terminal, array $decisions, string $expected): void
351    {
352        $values = new \SqlFaker\Generation\Value\ValueChoices(static function (int $count) use (&$decisions): int {
353            return array_shift($decisions) ?? $count - 1;
354        });
355        $result = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames(['UNDERSCORE_CHARSET', $terminal]), 1, new ResolvedOutput(), values: $values));
356        self::assertNotNull($result);
357        self::assertSame([$expected], array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$result->sequences()]));
358    }
359
360    /**
361     * @return iterable<array{string, list<int>, string}>
362     */
363    public static function providerIntroducedBoundariesValue(): iterable
364    {
365        yield ['TEXT_STRING', [], "'" . str_repeat("\x7f", 255) . "'"];
366        yield ['TEXT_STRING', [1, 0, 0], "''"];
367        yield ['HEX_NUM', [], "X'" . str_repeat('7f', 16) . "'"];
368        yield ['HEX_NUM', [1, 0], '0x' . str_repeat('7f', 16)];
369        yield ['HEX_NUM', [1, 0, 0, 0], '0x00'];
370        yield ['HEX_NUM', [1, 1, 0], "X''"];
371        yield ['BIN_NUM', [], "B'" . str_repeat('01111111', 8) . "'"];
372        yield ['BIN_NUM', [1, 0], '0b' . str_repeat('01111111', 8)];
373        yield ['BIN_NUM', [1, 0, 0, 0], '0b00000000'];
374        yield ['BIN_NUM', [1, 1, 0], "B''"];
375        foreach (range(1, 127) as $byte) {
376            $encoded = str_replace(['\\', "'"], ['\\\\', "''"], chr($byte));
377            yield ['TEXT_STRING', [1, 0, 1, $byte - 1], "'" . $encoded . "'"];
378        }
379    }
380
381    public function testSymbolsKeepsTheCompleteOutput(): void
382    {
383        $result = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames(['SET_VAR']), 0, new ResolvedOutput()));
384        self::assertNotNull($result);
385        self::assertSame(':=', implode(' ', array_map(static fn ($lexeme): string => $lexeme->text, [...$result->sequences()][0]->lexemes)));
386    }
387
388    public function testJsonKeepsTheCompleteDeclaredOutputSymbol(): void
389    {
390        $result = (new DefinitionFactory())->create('mysql-5.7.44')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames(['JSON_SEPARATOR_SYM']), 0, new ResolvedOutput()));
391        self::assertNotNull($result);
392        self::assertSame('->', implode(' ', array_map(static fn ($lexeme): string => $lexeme->text, [...$result->sequences()][0]->lexemes)));
393    }
394
395    public function testPhrasesKeepsTheCompleteDeclaredOutputSymbol(): void
396    {
397        $result = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames(['WITH_ROLLUP_SYM']), 0, new ResolvedOutput()));
398        self::assertNotNull($result);
399        self::assertSame('WITH ROLLUP', implode(' ', array_map(static fn ($lexeme): string => $lexeme->text, [...$result->sequences()][0]->lexemes)));
400    }
401
402    public function testSelectorsKeepsTheCompleteDeclaredOutputSymbol(): void
403    {
404        $result = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames(['GRAMMAR_SELECTOR_EXPR']), 0, new ResolvedOutput()));
405        self::assertNotNull($result);
406        self::assertSame('', implode(' ', array_map(static fn ($lexeme): string => $lexeme->text, [...$result->sequences()][0]->lexemes)));
407    }
408
409    public function testPhraseKeepsBothWordsInTheSameOriginalOccurrenceSymbol(): void
410    {
411        $result = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames(['WITH_ROLLUP_SYM']), 0, new ResolvedOutput()));
412        self::assertNotNull($result);
413        $lexemes = [...$result->sequences()][0]->lexemes;
414        self::assertSame($lexemes[0]->origin, $lexemes[1]->origin);
415        self::assertSame($lexemes[0]->phrase, $lexemes[1]->phrase);
416    }
417
418    public function testNonOutputDoesNotMisclassifyAStatementKeywordAsAMarkerSymbol(): void
419    {
420        $markers = (new DefinitionFactory())->create('mysql-8.4.7')->nonOutput;
421        self::assertContains('END_OF_INPUT', $markers);
422        self::assertContains('GRAMMAR_SELECTOR_EXPR', $markers);
423        self::assertNotContains('SELECT_SYM', $markers);
424    }
425
426    public function testDollarStringsUsesTheFirstMatchingDelimiter(): void
427    {
428        $generator = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes;
429        $valid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['DOLLAR_QUOTED_STRING_SYM']), 0, new ResolvedOutput(), '$tag$a$other$b$tag$'));
430        $invalid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['DOLLAR_QUOTED_STRING_SYM']), 0, new ResolvedOutput(), '$tag$a$tag$b$tag$'));
431        self::assertNotNull($valid);
432        self::assertNotNull($invalid);
433        self::assertCount(1, [...$valid->sequences()]);
434        self::assertSame([], [...$invalid->sequences()]);
435    }
436
437    public function testDollarVersionsDoesNotAssumeFutureCompatibility(): void
438    {
439        $definitions = new DefinitionFactory();
440        self::assertNotContains('mysql-8.0.44', $definitions->create('mysql-8.4.7')->dollarVersions);
441        self::assertContains('mysql-8.1.0', $definitions->create('mysql-8.4.7')->dollarVersions);
442        $this->expectException(RuntimeException::class);
443        $definitions->create('mysql-9.2.0');
444    }
445
446    /**
447     * @param list<int> $decisions
448     */
449    #[DataProvider('providerConstructedStringsDollarString')]
450    public function testCreateConstructsCompleteDollarDelimitersAndUnescapedBodiesDollarString(array $decisions, string $expected): void
451    {
452        $values = new \SqlFaker\Generation\Value\ValueChoices(static function (int $count) use (&$decisions): int {
453            return array_shift($decisions) ?? $count - 1;
454        });
455        $result = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames(['DOLLAR_QUOTED_STRING_SYM']), 0, new ResolvedOutput(), values: $values));
456        self::assertNotNull($result);
457        self::assertSame([$expected], array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$result->sequences()]));
458    }
459
460    /**
461     * @return iterable<array{list<int>, string}>
462     */
463    public static function providerConstructedStringsDollarString(): iterable
464    {
465        yield [[1, 0, 0], '$$$$'];
466        yield [[], '$' . str_repeat('猫', 16) . '$' . str_repeat('猫', 255) . '$' . str_repeat('猫', 16) . '$'];
467        foreach ([...range(1, 35), ...range(37, 127)] as $index => $byte) {
468            yield [[1, 0, 1, $index], '$$' . chr($byte) . '$$'];
469        }
470    }
471
472    public function testContextualValuesRestrictsTheDomain(): void
473    {
474        $generator = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes;
475        $result = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['ROTATE_KEY_ENGINE']), 0, new ResolvedOutput(), null));
476        self::assertNotNull($result);
477        self::assertSame(['INNODB', 'BINLOG'], array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$result->sequences()]));
478        $invalid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['ROTATE_KEY_ENGINE']), 0, new ResolvedOutput(), 'unknown'));
479        self::assertNotNull($invalid);
480        self::assertSame([], [...$invalid->sequences()]);
481    }
482
483    public function testContextualWordPreservesExplicitCase(): void
484    {
485        $generator = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes;
486        $result = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['ROTATE_KEY_ENGINE']), 0, new ResolvedOutput(), 'innodb'));
487        self::assertNotNull($result);
488        self::assertSame('innodb', [...$result->sequences()][0]->lexemes[0]->text);
489    }
490    public function testCreateEnumeratesOnlySourceAcceptedTernaryValuesContextualValue(): void
491    {
492        $generator = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes;
493        $input = new LexemeInput(TerminalSequence::fromNames(['TERNARY_OPTION_NUMBER']), 0, new ResolvedOutput());
494        $result = $generator->generate($input);
495        self::assertNotNull($result);
496        self::assertSame(['0', '1'], array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$result->sequences()]));
497        $invalid = $generator->generate(new LexemeInput($input->terminals, 0, new ResolvedOutput(), '2'));
498        self::assertNotNull($invalid);
499        self::assertSame([], [...$invalid->sequences()]);
500    }
501
502    /**
503     * @param list<string> $spellings
504     */
505    #[DataProvider('providerContextualDomainsContextualValue')]
506    public function testCreatePreservesTheVersionedParserValueDomainsContextualValue(string $version, string $terminal, array $spellings): void
507    {
508        $result = (new DefinitionFactory())->create($version)->lexemes->generate(new LexemeInput(TerminalSequence::fromNames([$terminal]), 0, new ResolvedOutput()));
509        self::assertNotNull($result);
510        self::assertSame($spellings, array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$result->sequences()]));
511    }
512
513    /**
514     * @return iterable<array{string, string, list<string>}>
515     */
516    public static function providerContextualDomainsContextualValue(): iterable
517    {
518        yield ['mysql-5.7.44', 'ROTATE_KEY_ENGINE', ['INNODB']];
519        yield ['mysql-5.7.44', 'REPLICATION_TABLE_PATTERN', ["'db.table'", "'db.%'", "'%.table'"]];
520        foreach (['mysql-8.0.44', 'mysql-8.1.0', 'mysql-8.2.0', 'mysql-8.3.0', 'mysql-8.4.7', 'mysql-9.0.1', 'mysql-9.1.0'] as $version) {
521            yield [$version, 'REPLICATION_TABLE_PATTERN', ["'db.table'", "'db.%'", "'%.table'"]];
522            yield [$version, 'REDO_ENGINE', ['INNODB']];
523            yield [$version, 'REDO_LOG_NAME', ['REDO_LOG']];
524            yield [$version, 'LOAD_COUNT_NAME', ['COUNT']];
525            yield [$version, 'LOAD_SOURCE_COUNT', ['1']];
526            yield [$version, 'REPLICATION_FLAG_NUMBER', ['0', '1']];
527            yield [$version, 'BINLOG_RESET_INDEX', ['1', '2000000000', "X'01'"]];
528        }
529    }
530
531    #[DataProvider('providerLiteralNamesContextualValue')]
532    public function testCreateRetainsContextualSpellingsAndTheirSourceDefinition(string $spelling): void
533    {
534        $generator = (new DefinitionFactory())->create('mysql-8.4.7')->lexemes;
535        $input = TerminalSequence::fromNames(['REDO_LOG_NAME']);
536        $result = $generator->generate(new LexemeInput($input, 0, new ResolvedOutput(), $spelling));
537        self::assertNotNull($result);
538        $candidates = [...$result->sequences()];
539        self::assertCount(1, $candidates);
540        self::assertSame($spelling, $candidates[0]->lexemes[0]->text);
541        self::assertSame('sql/sql_yacc.yy:alter_instance_action', $candidates[0]->lexemes[0]->definition);
542        $invalid = $generator->generate(new LexemeInput($input, 0, new ResolvedOutput(), 'axb'));
543        self::assertNotNull($invalid);
544        self::assertSame([], [...$invalid->sequences()]);
545    }
546    /**
547     * @return iterable<array{string}>
548     */
549    public static function providerLiteralNamesContextualValue(): iterable
550    {
551        yield ['redo_log'];
552        yield ['REDO_LOG'];
553    }
554
555    public function testKeywordsKeepsAliasesFunctionCategoriesAndReleaseChangesExplicit(): void
556    {
557        $factory = new DefinitionFactory();
558        $current = $factory->create('mysql-8.4.7');
559        self::assertSame(['CURRENT_TIMESTAMP', 'LOCALTIME', 'LOCALTIMESTAMP'], $current->keywords['NOW_SYM']);
560        self::assertSame(['NOW'], $current->functions['NOW_SYM']);
561        self::assertSame(['BIGINT', 'INT8'], $current->keywords['BIGINT_SYM']);
562        self::assertArrayNotHasKey('MASTER_HOST_SYM', $current->keywords);
563        self::assertSame(['MASTER_HOST'], $factory->create('mysql-8.3.0')->keywords['MASTER_HOST_SYM']);
564        self::assertSame(['BIGINT', 'INT8'], $factory->create('mysql-5.6.51')->keywords['BIGINT']);
565        self::assertArrayNotHasKey('VECTOR_SYM', $current->keywords);
566        self::assertSame(['VECTOR'], $factory->create('mysql-9.0.1')->keywords['VECTOR_SYM']);
567    }
568}
569