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