packages/sql-faker/tests/Unit/MySql/Generation/LexicalGrammarTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\MySql\Generation;
6
7use Closure;
8use Faker\Factory;
9use PHPUnit\Framework\Attributes\CoversClass;
10use PHPUnit\Framework\Attributes\DataProvider;
11use PHPUnit\Framework\Attributes\UsesClass;
12use PHPUnit\Framework\TestCase;
13use SqlFaker\Generation\Exception\LexicalException;
14use SqlFaker\Generation\Plan\GenerationPlan;
15use SqlFaker\Generation\Token\TerminalSequence;
16use SqlFaker\Generation\Value\RandomCharacters;
17use SqlFaker\Grammar\Resource\SqlVersion;
18use SqlFaker\Grammar\Resource\SqlVersionRegistry;
19use SqlFaker\MySql\Generation\LexicalGrammar;
20use SqlFaker\MySql\Generation\Tokenization\KeywordIndex;
21use SqlFaker\MySql\Generation\Tokenization\MySqlTokenizer;
22use SqlFaker\MySql\Generation\Value\LiteralGenerator;
23
24#[CoversClass(LexicalGrammar::class)]
25#[CoversClass(LiteralGenerator::class)]
26#[UsesClass(SqlVersion::class)]
27#[UsesClass(LexicalException::class)]
28#[UsesClass(KeywordIndex::class)]
29#[UsesClass(RandomCharacters::class)]
30#[UsesClass(SqlVersionRegistry::class)]
31#[UsesClass(MySqlTokenizer::class)]
32#[UsesClass(\SqlFaker\MySql\Generation\Tokenization\MySqlQuoting::class)]
33#[UsesClass(GenerationPlan::class)]
34#[UsesClass(\SqlFaker\Generation\Candidate\ChoiceLexemeGenerator::class)]
35#[UsesClass(\SqlFaker\Generation\Candidate\FixedLexemeGenerator::class)]
36#[UsesClass(\SqlFaker\Generation\Candidate\IntegerLexemeGenerator::class)]
37#[UsesClass(\SqlFaker\Generation\Lexeme\Lexeme::class)]
38#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeCandidates::class)]
39#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeInput::class)]
40#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeSequence::class)]
41#[UsesClass(\SqlFaker\Generation\Candidate\MatchingLexemeGenerator::class)]
42#[UsesClass(\SqlFaker\Generation\Candidate\ValueLexemeGenerator::class)]
43#[UsesClass(\SqlFaker\Generation\Candidate\SequenceLexemeGenerator::class)]
44#[UsesClass(\SqlFaker\Generation\Output\CandidateResolver::class)]
45#[UsesClass(\SqlFaker\Generation\Lexeme\OutputPart::class)]
46#[UsesClass(\SqlFaker\Generation\Lexeme\ResolvedOutput::class)]
47#[UsesClass(\SqlFaker\Generation\Output\ReverseLexemeGenerator::class)]
48#[UsesClass(\SqlFaker\Generation\Output\SqlSerializer::class)]
49#[UsesClass(\SqlFaker\Generation\Output\CombinedSpacingRule::class)]
50#[UsesClass(\SqlFaker\MySql\Generation\Spacing\KeywordPhraseSpacingRule::class)]
51#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeBoundary::class)]
52#[UsesClass(\SqlFaker\Generation\Lexeme\SpacingConstraint::class)]
53#[UsesClass(\SqlFaker\Generation\Token\TerminalOccurrence::class)]
54#[UsesClass(TerminalSequence::class)]
55#[UsesClass(\SqlFaker\Generation\Candidate\VersionCase::class)]
56#[UsesClass(\SqlFaker\Generation\Candidate\VersionedLexemeGenerator::class)]
57#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\DefinitionFactory::class)]
58#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\KeywordLexemeGenerator::class)]
59#[UsesClass(\SqlFaker\MySql\Generation\Spacing\CloneAddressSpacingRule::class)]
60#[UsesClass(\SqlFaker\MySql\Generation\Spacing\FunctionSpacingRule::class)]
61#[UsesClass(\SqlFaker\MySql\Generation\Spacing\QualifiedNameSpacingRule::class)]
62#[UsesClass(\SqlFaker\MySql\Generation\Spacing\VariableSpacingRule::class)]
63#[UsesClass(\SqlFaker\Generation\Plan\ProductionPattern::class)]
64#[UsesClass(\SqlFaker\Generation\Token\ProductionOccurrence::class)]
65#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\BoundedIntegerLexemeGenerator::class)]
66#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\ReplicationTablePatternLexemeGenerator::class)]
67#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\SizeNumberLexemeGenerator::class)]
68#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\FactorLexemeGenerator::class)]
69#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\PrecisionLexemeGenerator::class)]
70#[UsesClass(\SqlFaker\Generation\Value\CharacterDomain::class)]
71#[UsesClass(\SqlFaker\Generation\Value\ChoiceDomain::class)]
72#[UsesClass(\SqlFaker\Generation\Value\IntegerDomain::class)]
73#[UsesClass(\SqlFaker\Generation\Value\SequenceDomain::class)]
74#[UsesClass(\SqlFaker\MySql\Generation\Value\DollarQuotedDomain::class)]
75#[UsesClass(\SqlFaker\Generation\Output\BoundaryCompletion::class)]
76#[UsesClass(\SqlFaker\Generation\Value\ValueChoices::class)]
77#[UsesClass(\SqlFaker\Generation\Choice\BytePlanCompiler::class)]
78#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\CharsetLexemeGenerator::class)]
79#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\CharsetValueLexemeGenerator::class)]
80#[UsesClass(\SqlFaker\MySql\Generation\Value\IdentifierDomain::class)]
81#[UsesClass(\SqlFaker\MySql\Generation\Value\QuotedDomain::class)]
82#[UsesClass(\SqlFaker\MySql\Generation\Value\RadixDomain::class)]
83#[UsesClass(\SqlFaker\Generation\Value\Utf8::class)]
84#[UsesClass(\SqlFaker\Generation\Value\WordDomain::class)]
85#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\LexicalDefinition::class)]
86final class LexicalGrammarTest extends TestCase
87{
88 public function testGeneratesPublicProviderLexemesThroughDialectGrammar(): void
89 {
90 $faker = Factory::create();
91 $faker->seed(12345);
92 $lexical = new LexicalGrammar($faker, 'mysql-8.4.7');
93 $sql = implode(' ', [
94 $lexical->generateQuotedIdentifier(3, 3),
95 $lexical->generateStringLiteral(3, 3),
96 $lexical->generateNationalStringLiteral(3, 3),
97 $lexical->generateDollarQuotedString(3, 3),
98 $lexical->generateIntegerLiteral(10, 10),
99 $lexical->generateDecimalLiteral(4, 2),
100 $lexical->generateFloatLiteral(4, 2, 2, 2),
101 $lexical->generateHexLiteral(4, 4),
102 $lexical->generateQuotedHexLiteral(2, 2),
103 $lexical->generateBinaryLiteral(4, 4),
104 ]);
105
106 self::assertSame([
107 'IDENT_QUOTED', 'TEXT_STRING', 'NCHAR_STRING', 'DOLLAR_QUOTED_STRING_SYM', 'NUM',
108 'DECIMAL_NUM', 'FLOAT_NUM', 'HEX_NUM', 'HEX_NUM', 'BIN_NUM',
109 ], $lexical->tokenize($sql));
110 self::assertSame('10', $lexical->generateLongIntegerLiteral(10, 10));
111 self::assertMatchesRegularExpression('/^[0-9]{1,20}$/', $lexical->generateUnsignedBigIntLiteral());
112 self::assertSame(
113 ['@', 'LEX_HOSTNAME'],
114 $lexical->tokenize('@' . $lexical->generateHostname(2, 2, 3)),
115 );
116 }
117
118 /**
119 * @param Closure(LexicalGrammar): string $withDefaults
120 * @param Closure(LexicalGrammar): string $withExplicitBounds
121 */
122 #[DataProvider('providerPublicLexemeDefaults')]
123 public function testPublicLexemeDefaultBounds(Closure $withDefaults, Closure $withExplicitBounds): void
124 {
125 $faker = Factory::create();
126 $grammar = new LexicalGrammar($faker, 'mysql-8.4.7');
127
128 $faker->seed(20_260_824);
129 $generated = $withDefaults($grammar);
130
131 $faker->seed(20_260_824);
132 $explicit = $withExplicitBounds($grammar);
133
134 self::assertNotSame('', $generated);
135 self::assertSame($generated, $explicit);
136 }
137
138 /**
139 * @return iterable<string, array{Closure(LexicalGrammar): string, Closure(LexicalGrammar): string}>
140 */
141 public static function providerPublicLexemeDefaults(): iterable
142 {
143 yield 'quoted identifier' => [
144 static fn (LexicalGrammar $grammar): string => $grammar->generateQuotedIdentifier(),
145 static fn (LexicalGrammar $grammar): string => $grammar->generateQuotedIdentifier(1, 64),
146 ];
147 yield 'string' => [
148 static fn (LexicalGrammar $grammar): string => $grammar->generateStringLiteral(),
149 static fn (LexicalGrammar $grammar): string => $grammar->generateStringLiteral(1, 255),
150 ];
151 yield 'national string' => [
152 static fn (LexicalGrammar $grammar): string => $grammar->generateNationalStringLiteral(),
153 static fn (LexicalGrammar $grammar): string => $grammar->generateNationalStringLiteral(1, 255),
154 ];
155 yield 'dollar quoted string' => [
156 static fn (LexicalGrammar $grammar): string => $grammar->generateDollarQuotedString(),
157 static fn (LexicalGrammar $grammar): string => $grammar->generateDollarQuotedString(1, 255),
158 ];
159 yield 'integer' => [
160 static fn (LexicalGrammar $grammar): string => $grammar->generateIntegerLiteral(),
161 static fn (LexicalGrammar $grammar): string => $grammar->generateIntegerLiteral(1, 2147483647),
162 ];
163 yield 'long integer' => [
164 static fn (LexicalGrammar $grammar): string => $grammar->generateLongIntegerLiteral(),
165 static fn (LexicalGrammar $grammar): string => $grammar->generateLongIntegerLiteral(0, 2147483647),
166 ];
167 yield 'unsigned big integer' => [
168 static fn (LexicalGrammar $grammar): string => $grammar->generateUnsignedBigIntLiteral(),
169 static fn (LexicalGrammar $grammar): string => $grammar->generateUnsignedBigIntLiteral(1, 20),
170 ];
171 yield 'decimal' => [
172 static fn (LexicalGrammar $grammar): string => $grammar->generateDecimalLiteral(),
173 static fn (LexicalGrammar $grammar): string => $grammar->generateDecimalLiteral(10, 2),
174 ];
175 yield 'float' => [
176 static fn (LexicalGrammar $grammar): string => $grammar->generateFloatLiteral(),
177 static fn (LexicalGrammar $grammar): string => $grammar->generateFloatLiteral(10, 2, -38, 38),
178 ];
179 yield 'hex' => [
180 static fn (LexicalGrammar $grammar): string => $grammar->generateHexLiteral(),
181 static fn (LexicalGrammar $grammar): string => $grammar->generateHexLiteral(1, 16),
182 ];
183 yield 'quoted hex' => [
184 static fn (LexicalGrammar $grammar): string => $grammar->generateQuotedHexLiteral(),
185 static fn (LexicalGrammar $grammar): string => $grammar->generateQuotedHexLiteral(1, 8),
186 ];
187 yield 'binary' => [
188 static fn (LexicalGrammar $grammar): string => $grammar->generateBinaryLiteral(),
189 static fn (LexicalGrammar $grammar): string => $grammar->generateBinaryLiteral(1, 64),
190 ];
191 yield 'hostname' => [
192 static fn (LexicalGrammar $grammar): string => $grammar->generateHostname(),
193 static fn (LexicalGrammar $grammar): string => $grammar->generateHostname(1, 4, 63),
194 ];
195 }
196
197 public function testTokenizesQuotedValuesHexValuesAndCommentsAsSingleTokens(): void
198 {
199 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
200 $sql = <<<'SQL'
201SELECT `values`, 'FROM ''items''', X'af', B'101' /* UPDATE */ # DELETE
202FROM items
203SQL;
204
205 self::assertSame([
206 'SELECT_SYM', 'IDENT_QUOTED', ',', 'TEXT_STRING', ',', 'HEX_NUM', ',', 'BIN_NUM', 'FROM', 'IDENT',
207 ], $lexical->tokenize($sql));
208 }
209
210 public function testTokenizesEveryLiteralNumberAndOperatorClass(): void
211 {
212 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
213 $sql = <<<'SQL'
2140 2147483647 2147483648 9223372036854775807 9223372036854775808 000 000000000002147483648
2150XAF 0b10 x'af' b'01' n'x' 1.2 .5 1E2 _UTF8MB4 _name ?
216WITH ROLLUP || <=> ->> && <= <> != >= << >> := ->
217! % & ( ) * + , - . / : ; @ ^ { } | ~ = < >
218SQL;
219
220 self::assertSame([
221 'NUM', 'NUM', 'LONG_NUM', 'LONG_NUM', 'ULONGLONG_NUM', 'NUM', 'LONG_NUM',
222 'HEX_NUM', 'BIN_NUM', 'HEX_NUM', 'BIN_NUM', 'NCHAR_STRING', 'DECIMAL_NUM', 'DECIMAL_NUM',
223 'FLOAT_NUM', 'UNDERSCORE_CHARSET', 'IDENT', 'PARAM_MARKER',
224 'WITH_ROLLUP_SYM', 'OR2_SYM', 'EQUAL_SYM', '->>', 'AND_AND_SYM', 'LE', 'NE', 'NE', 'GE',
225 'SHIFT_LEFT', 'SHIFT_RIGHT', ':=', '->',
226 '!', '%', '&', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '@', '^', '{', '}', '|', '~',
227 'EQ', 'LT', 'GT_SYM',
228 ], $lexical->tokenize($sql));
229 }
230
231 public function testTokenizesDollarQuotedStringAndContinuesWithTheRemainingInput(): void
232 {
233 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
234
235 self::assertSame(
236 ['DOLLAR_QUOTED_STRING_SYM', 'PARAM_MARKER'],
237 $lexical->tokenize('$$value$$ ?'),
238 );
239 }
240
241 public function testCombinesWithRollupAtTheEndOfInput(): void
242 {
243 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
244
245 self::assertSame(['WITH_ROLLUP_SYM'], $lexical->tokenize('WITH ROLLUP'));
246 }
247
248 public function testUsesFunctionTokensOnlyWhenFollowedByAnOpeningParenthesis(): void
249 {
250 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
251
252 self::assertSame(
253 ['SELECT_SYM', 'IDENT', 'COUNT_SYM', '(', '*', ')'],
254 $lexical->tokenize('select count COUNT(*)'),
255 );
256 }
257
258 public function testVersionProfileControlsDollarQuotedStringSupport(): void
259 {
260 $faker = Factory::create();
261
262 $beforeSupport = new LexicalGrammar($faker, 'mysql-8.0.44');
263 $afterSupport = new LexicalGrammar($faker, 'mysql-8.1.0');
264
265 self::assertSame('mysql-8.0.44', $beforeSupport->version());
266 self::assertSame('$tag$text$tag$', $afterSupport->realize(['DOLLAR_QUOTED_STRING_SYM'], GenerationPlan::all()->withLexemes([
267 'DOLLAR_QUOTED_STRING_SYM' => ['$tag$text$tag$'],
268 ])));
269 $this->expectException(LexicalException::class);
270 $this->expectExceptionMessage('DOLLAR_QUOTED_STRING_SYM');
271 $beforeSupport->realize(['DOLLAR_QUOTED_STRING_SYM']);
272 }
273
274 #[DataProvider('providerInvalidSql')]
275 public function testRejectsInvalidSql(string $version, string $sql, string $message): void
276 {
277 $lexical = new LexicalGrammar(Factory::create(), $version);
278
279 $this->expectException(LexicalException::class);
280 $this->expectExceptionMessage($message);
281
282 $lexical->tokenize($sql);
283 }
284
285 public function testTokenizesHostnameWithoutStoppingTheRemainingInput(): void
286 {
287 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
288
289 self::assertSame(
290 ['SELECT_SYM', 'IDENT', '@', 'LEX_HOSTNAME', 'FROM', 'IDENT'],
291 $lexical->tokenize('SELECT sqlfakeruser@host.example FROM items'),
292 );
293 }
294
295 public function testRealizesAndChecksAParserTokenSequence(): void
296 {
297 $faker = Factory::create();
298 $faker->seed(12);
299 $lexical = new LexicalGrammar($faker, 'mysql-8.4.7');
300
301 $sql = $lexical->realize(['SELECT_SYM', 'IDENT_QUOTED', 'FROM', 'IDENT', 'WHERE', 'IDENT', 'EQ', 'HEX_NUM']);
302
303 self::assertSame([
304 'SELECT_SYM', 'IDENT_QUOTED', 'FROM', 'IDENT', 'WHERE', 'IDENT', 'EQ', 'HEX_NUM',
305 ], $lexical->tokenize($sql));
306 }
307
308 #[DataProvider('providerFixedTerminal')]
309 public function testRealizeOfFixedTerminal(string $terminal, string $expected): void
310 {
311 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
312
313 self::assertSame($expected, $lexical->realize([$terminal]));
314 }
315
316 public function testRealizeOfGeneratedTerminals(): void
317 {
318 $faker = Factory::create();
319 $faker->seed(17);
320 $lexical = new LexicalGrammar($faker, 'mysql-8.4.7');
321
322 self::assertMatchesRegularExpression('/^_[A-Za-z0-9_]+$/', $lexical->realize(['IDENT']));
323 self::assertStringStartsWith('`', $lexical->realize(['IDENT_QUOTED']));
324 self::assertStringStartsWith("'", $lexical->realize(['TEXT_STRING']));
325 self::assertStringStartsWith("N'", $lexical->realize(['NCHAR_STRING']));
326 self::assertContains($lexical->realize(['DOLLAR_QUOTED_STRING_SYM']), ['$$text$$', '$tag$text$tag$']);
327 self::assertMatchesRegularExpression('/^\d+$/', $lexical->realize(['NUM']));
328 self::assertMatchesRegularExpression('/^\d+$/', $lexical->realize(['LONG_NUM']));
329 self::assertSame('18446744073709551615', $lexical->realize(['ULONGLONG_NUM']));
330 self::assertSame(['DECIMAL_NUM'], $lexical->tokenize($lexical->realize(['DECIMAL_NUM'])));
331 self::assertSame(['UNDERSCORE_CHARSET'], $lexical->tokenize($lexical->realize(['UNDERSCORE_CHARSET'])));
332 self::assertMatchesRegularExpression('/^[+-]?\d+(?:\.\d+)?[eE][+-]?\d+$/', $lexical->realize(['FLOAT_NUM']));
333 self::assertSame(['HEX_NUM'], $lexical->tokenize($lexical->realize(['HEX_NUM'])));
334 self::assertSame(['BIN_NUM'], $lexical->tokenize($lexical->realize(['BIN_NUM'])));
335 }
336
337 /**
338 * @return iterable<string, array{string, string, string}>
339 */
340 public static function providerInvalidSql(): iterable
341 {
342 yield 'backtick quoted identifier' => ['mysql-8.4.7', '`name', 'Unterminated MySQL quoted token'];
343 yield 'single quoted string' => ['mysql-8.4.7', "'value", 'Unterminated MySQL quoted token'];
344 yield 'dollar quoted string' => ['mysql-8.4.7', '$$value', 'Unterminated MySQL dollar-quoted string.'];
345 yield 'block comment' => ['mysql-8.4.7', '/* comment', 'Unterminated MySQL block comment.'];
346 yield 'unsupported character' => ['mysql-8.4.7', 'SELECT \\', 'offset 7: SELECT \\'];
347 yield 'dollar quote before support' => ['mysql-8.0.44', '$$value$$', 'offset 0: $$value$$'];
348 }
349
350 /**
351 * @return iterable<string, array{string, string}>
352 */
353 public static function providerFixedTerminal(): iterable
354 {
355 yield 'EQ' => ['EQ', '='];
356 yield 'EQUAL_SYM' => ['EQUAL_SYM', '<=>'];
357 yield 'LT' => ['LT', '<'];
358 yield 'GT_SYM' => ['GT_SYM', '>'];
359 yield 'LE' => ['LE', '<='];
360 yield 'GE' => ['GE', '>='];
361 yield 'SHIFT_LEFT' => ['SHIFT_LEFT', '<<'];
362 yield 'SHIFT_RIGHT' => ['SHIFT_RIGHT', '>>'];
363 yield 'AND_AND_SYM' => ['AND_AND_SYM', '&&'];
364 yield 'OR_OR_SYM' => ['OR_OR_SYM', '||'];
365 yield 'NOT2_SYM' => ['NOT2_SYM', 'NOT'];
366 yield 'SET_VAR' => ['SET_VAR', ':='];
367 yield 'JSON_SEPARATOR_SYM' => ['JSON_SEPARATOR_SYM', '->'];
368 yield 'JSON_UNQUOTED_SEPARATOR_SYM' => ['JSON_UNQUOTED_SEPARATOR_SYM', '->>'];
369 yield 'WITH_ROLLUP_SYM' => ['WITH_ROLLUP_SYM', 'WITH ROLLUP'];
370 yield 'PARAM_MARKER' => ['PARAM_MARKER', '?'];
371 }
372
373 public function testGenerateQuotedIdentifierWrapsTheNameInBackticks(): void
374 {
375 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
376
377 self::assertMatchesRegularExpression(
378 '/^`.+`$/',
379 $lexical->generateQuotedIdentifier(),
380 );
381 }
382
383 public function testGenerateStringLiteralWrapsTheBodyInSingleQuotes(): void
384 {
385 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
386
387 self::assertMatchesRegularExpression(
388 '/^\'.*\'$/s',
389 $lexical->generateStringLiteral(),
390 );
391 }
392
393 public function testGenerateNationalStringLiteralPrefixesTheStringWithN(): void
394 {
395 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
396
397 self::assertMatchesRegularExpression(
398 '/^N\'.*\'$/s',
399 $lexical->generateNationalStringLiteral(),
400 );
401 }
402
403 public function testGenerateDollarQuotedStringWrapsTheBodyInDoubleDollars(): void
404 {
405 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
406
407 self::assertMatchesRegularExpression(
408 '/^\$\$.*\$\$$/s',
409 $lexical->generateDollarQuotedString(),
410 );
411 }
412
413 public function testGenerateIntegerLiteralWritesOnlyDigits(): void
414 {
415 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
416
417 self::assertMatchesRegularExpression(
418 '/^\d+$/',
419 $lexical->generateIntegerLiteral(),
420 );
421 }
422
423 public function testGenerateLongIntegerLiteralWritesOnlyDigits(): void
424 {
425 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
426
427 self::assertMatchesRegularExpression(
428 '/^\d+$/',
429 $lexical->generateLongIntegerLiteral(),
430 );
431 }
432
433 public function testGenerateUnsignedBigIntLiteralWritesOnlyDigits(): void
434 {
435 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
436
437 self::assertMatchesRegularExpression(
438 '/^\d+$/',
439 $lexical->generateUnsignedBigIntLiteral(),
440 );
441 }
442
443 public function testGenerateDecimalLiteralWritesDigitsAroundAPoint(): void
444 {
445 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
446
447 self::assertMatchesRegularExpression(
448 '/^-?\d+\.\d+$/',
449 $lexical->generateDecimalLiteral(),
450 );
451 }
452
453 public function testGenerateFloatLiteralWritesAnExponent(): void
454 {
455 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
456
457 self::assertMatchesRegularExpression(
458 '/[eE][+-]?\d+$/',
459 $lexical->generateFloatLiteral(),
460 );
461 }
462
463 public function testGenerateHexLiteralWritesHexDigitsAfterAPrefix(): void
464 {
465 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
466
467 self::assertMatchesRegularExpression(
468 '/^0x[0-9a-fA-F]+$/',
469 $lexical->generateHexLiteral(),
470 );
471 }
472
473 public function testGenerateQuotedHexLiteralWritesWholeBytesInQuotes(): void
474 {
475 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
476
477 self::assertMatchesRegularExpression(
478 '/^X\'(?:[0-9a-fA-F]{2})+\'$/',
479 $lexical->generateQuotedHexLiteral(),
480 );
481 }
482
483 public function testGenerateBinaryLiteralWritesBitsAfterAPrefix(): void
484 {
485 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
486
487 self::assertMatchesRegularExpression(
488 '/^0b[01]+$/',
489 $lexical->generateBinaryLiteral(),
490 );
491 }
492
493 public function testGenerateHostnameWritesDotSeparatedParts(): void
494 {
495 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
496
497 self::assertMatchesRegularExpression(
498 '/^[A-Za-z0-9.-]+$/',
499 $lexical->generateHostname(),
500 );
501 }
502 public function testRealizeSequenceHonorsThePlannedStringWithoutRequiringTrivia(): void
503 {
504 $grammar = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
505 $plan = GenerationPlan::all()->withLexemes(['TEXT_STRING' => ["'a''b'"]]);
506 self::assertSame("'a''b'", $grammar->realizeSequence(TerminalSequence::fromNames(['TEXT_STRING']), $plan));
507 }
508
509 public function testIsNonOutputDoesNotConfuseOrdinaryValuesWithParserMarkers(): void
510 {
511 $grammar = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
512 self::assertFalse($grammar->isNonOutput('TEXT_STRING'));
513 self::assertFalse($grammar->isNonOutput('UNIMPLEMENTED'));
514 }
515
516 public function testRealizeReportsAnUnimplementedTerminalAtItsActualUse(): void
517 {
518 $grammar = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
519 $this->expectException(LexicalException::class);
520 $this->expectExceptionMessage('UNIMPLEMENTED');
521 $grammar->realize(['UNIMPLEMENTED']);
522 }
523
524 public function testRealizeKeepsContextualUserVariableSpellingWithoutTokenizerEqualityAsAGate(): void
525 {
526 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
527 self::assertSame('@_sqlfaker_identifier', $lexical->realize(['@', 'IDENT']));
528 }
529
530 public function testRealizeRetainsBothRegisteredInequalitySpellings(): void
531 {
532 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
533 self::assertSame('!=', $lexical->realizeSequence(TerminalSequence::fromNames(['NE']), GenerationPlan::all()->withLexemes(['NE' => ['!=']])));
534 self::assertSame('<>', $lexical->realizeSequence(TerminalSequence::fromNames(['NE']), GenerationPlan::all()->withLexemes(['NE' => ['<>']])));
535 }
536 public function testResolveSequenceExposesTheChosenCandidateAndItsOutput(): void
537 {
538 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
539 $output = $lexical->resolveSequence(TerminalSequence::fromNames(['NUM']), null, static fn (int $count): int => $count - 1);
540 self::assertSame('2', $output->parts[0]->lexeme->text);
541 self::assertCount(1, $output->candidates);
542 self::assertSame('', $output->parts[0]->separator);
543 }
544}
545