packages/sql-faker/tests/Unit/Generation/Exception/LexicalExceptionTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Generation\Exception;
6
7use Faker\Factory;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use SqlFaker\Generation\Exception\LexicalException;
12use SqlFaker\Generation\Value\RandomCharacters;
13use SqlFaker\Grammar\Resource\SqlVersion;
14use SqlFaker\Grammar\Resource\SqlVersionRegistry;
15use SqlFaker\MySql\Generation\LexicalGrammar;
16use SqlFaker\MySql\Generation\Tokenization\KeywordIndex;
17use SqlFaker\MySql\Generation\Tokenization\MySqlTokenizer;
18use SqlFaker\MySql\Generation\Value\LiteralGenerator;
19
20#[CoversClass(LexicalException::class)]
21#[UsesClass(LexicalGrammar::class)]
22#[UsesClass(LiteralGenerator::class)]
23#[UsesClass(\SqlFaker\PostgreSql\Generation\Value\LiteralGenerator::class)]
24#[UsesClass(\SqlFaker\Sqlite\Generation\Value\LiteralGenerator::class)]
25#[UsesClass(SqlVersion::class)]
26#[UsesClass(KeywordIndex::class)]
27#[UsesClass(\SqlFaker\PostgreSql\Generation\Tokenization\KeywordIndex::class)]
28#[UsesClass(\SqlFaker\Sqlite\Generation\Tokenization\KeywordIndex::class)]
29#[UsesClass(RandomCharacters::class)]
30#[UsesClass(SqlVersionRegistry::class)]
31#[UsesClass(MySqlTokenizer::class)]
32#[UsesClass(\SqlFaker\MySql\Generation\Tokenization\MySqlQuoting::class)]
33#[UsesClass(\SqlFaker\Generation\Candidate\ChoiceLexemeGenerator::class)]
34#[UsesClass(\SqlFaker\Generation\Candidate\FixedLexemeGenerator::class)]
35#[UsesClass(\SqlFaker\Generation\Candidate\IntegerLexemeGenerator::class)]
36#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeInput::class)]
37#[UsesClass(\SqlFaker\Generation\Candidate\MatchingLexemeGenerator::class)]
38#[UsesClass(\SqlFaker\Generation\Candidate\ValueLexemeGenerator::class)]
39#[UsesClass(\SqlFaker\Generation\Candidate\SequenceLexemeGenerator::class)]
40#[UsesClass(\SqlFaker\Generation\Output\CandidateResolver::class)]
41#[UsesClass(\SqlFaker\Generation\Lexeme\ResolvedOutput::class)]
42#[UsesClass(\SqlFaker\Generation\Output\ReverseLexemeGenerator::class)]
43#[UsesClass(\SqlFaker\Generation\Output\CombinedSpacingRule::class)]
44#[UsesClass(\SqlFaker\Generation\Token\TerminalOccurrence::class)]
45#[UsesClass(\SqlFaker\Generation\Token\TerminalSequence::class)]
46#[UsesClass(\SqlFaker\Generation\Candidate\VersionCase::class)]
47#[UsesClass(\SqlFaker\Generation\Candidate\VersionedLexemeGenerator::class)]
48#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\DefinitionFactory::class)]
49#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\KeywordLexemeGenerator::class)]
50#[UsesClass(\SqlFaker\Generation\Plan\GenerationPlan::class)]
51#[UsesClass(\SqlFaker\Generation\Plan\ProductionPattern::class)]
52#[UsesClass(\SqlFaker\Generation\Lexeme\Lexeme::class)]
53#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeCandidates::class)]
54#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeSequence::class)]
55#[UsesClass(\SqlFaker\Generation\Lexeme\OutputPart::class)]
56#[UsesClass(\SqlFaker\Generation\Output\SqlSerializer::class)]
57#[UsesClass(\SqlFaker\MySql\Generation\Spacing\KeywordPhraseSpacingRule::class)]
58#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeBoundary::class)]
59#[UsesClass(\SqlFaker\Generation\Lexeme\SpacingConstraint::class)]
60#[UsesClass(\SqlFaker\Generation\Token\ProductionOccurrence::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\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\PostgreSql\Generation\Value\DollarQuotedDomain::class)]
76#[UsesClass(\SqlFaker\Generation\Output\BoundaryCompletion::class)]
77#[UsesClass(\SqlFaker\Generation\Value\ValueChoices::class)]
78#[UsesClass(\SqlFaker\Generation\Choice\BytePlanCompiler::class)]
79#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\CharsetLexemeGenerator::class)]
80#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\CharsetValueLexemeGenerator::class)]
81#[UsesClass(\SqlFaker\MySql\Generation\Value\IdentifierDomain::class)]
82#[UsesClass(\SqlFaker\PostgreSql\Generation\Value\IdentifierDomain::class)]
83#[UsesClass(\SqlFaker\Sqlite\Generation\Value\IdentifierDomain::class)]
84#[UsesClass(\SqlFaker\MySql\Generation\Value\QuotedDomain::class)]
85#[UsesClass(\SqlFaker\PostgreSql\Generation\Value\QuotedDomain::class)]
86#[UsesClass(\SqlFaker\Sqlite\Generation\Value\QuotedDomain::class)]
87#[UsesClass(\SqlFaker\MySql\Generation\Value\RadixDomain::class)]
88#[UsesClass(\SqlFaker\Generation\Value\WordDomain::class)]
89#[UsesClass(\SqlFaker\Generation\Value\Utf8::class)]
90#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\LexicalDefinition::class)]
91#[UsesClass(\SqlFaker\PostgreSql\Generation\Lexeme\LexicalDefinition::class)]
92#[UsesClass(\SqlFaker\Sqlite\Generation\Lexeme\LexicalDefinition::class)]
93final class LexicalExceptionTest extends TestCase
94{
95 public function testTokenizingUnsupportedInputReportsTheOffsetAndTheInput(): void
96 {
97 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
98
99 $this->expectException(LexicalException::class);
100 $this->expectExceptionMessage('Unsupported MySQL lexical input at offset 0:');
101
102 $lexical->tokenize("\x00");
103 }
104
105 public function testRealizingAnUnknownTerminalReportsTheTerminal(): void
106 {
107 $lexical = new LexicalGrammar(Factory::create(), 'mysql-8.4.7');
108
109 $this->expectException(LexicalException::class);
110 $this->expectExceptionMessage('Unsupported MySQL terminal for mysql-8.4.7: NOT_A_TERMINAL');
111
112 $lexical->realize(['NOT_A_TERMINAL']);
113 }
114
115 public function testUnsupportedTerminalNamesTheTerminalAndTheProfile(): void
116 {
117 self::assertSame(
118 'Unsupported MySQL terminal for mysql-8.4.7: NOT_A_TERMINAL',
119 LexicalException::unsupportedTerminal('MySQL', 'mysql-8.4.7', 'NOT_A_TERMINAL')->getMessage(),
120 );
121 }
122
123 public function testUnsupportedInputNamesTheOffsetAndTheText(): void
124 {
125 self::assertSame(
126 'Unsupported SQLite lexical input at offset 3: abc',
127 LexicalException::unsupportedInput('SQLite', 3, 'abc')->getMessage(),
128 );
129 }
130
131 public function testNoProgressNamesTheOffsetTheReadGotStuckAt(): void
132 {
133 self::assertSame(
134 'SQLite lexer made no progress at offset 3: abc',
135 LexicalException::noProgress('SQLite', 3, 'abc')->getMessage(),
136 );
137 }
138
139 public function testUnterminatedQuotedTokenNamesTheText(): void
140 {
141 self::assertSame(
142 "Unterminated MySQL quoted token: 'abc",
143 LexicalException::unterminatedQuotedToken('MySQL', "'abc")->getMessage(),
144 );
145 }
146
147 public function testUnterminatedBracketIdentifierNamesTheDialect(): void
148 {
149 self::assertSame(
150 'Unterminated SQLite bracket identifier.',
151 LexicalException::unterminatedBracketIdentifier('SQLite')->getMessage(),
152 );
153 }
154
155 public function testUnterminatedBlockCommentNamesTheDialect(): void
156 {
157 self::assertSame(
158 'Unterminated PostgreSQL block comment.',
159 LexicalException::unterminatedBlockComment('PostgreSQL')->getMessage(),
160 );
161 }
162
163 public function testUnterminatedDollarQuotedStringNamesTheDialect(): void
164 {
165 self::assertSame(
166 'Unterminated MySQL dollar-quoted string.',
167 LexicalException::unterminatedDollarQuotedString('MySQL')->getMessage(),
168 );
169 }
170
171 public function testLexemeDoesNotRealizeTerminalNamesBoth(): void
172 {
173 self::assertSame(
174 'Requested MySQL lexeme does not realize IDENT: 42',
175 LexicalException::lexemeDoesNotRealizeTerminal('MySQL', 'IDENT', '42')->getMessage(),
176 );
177 }
178
179 public function testNoWitnessForLexemeNamesBoth(): void
180 {
181 self::assertSame(
182 'MySQL lexical catalog has no IDENT witness for: 42',
183 LexicalException::noWitnessForLexeme('MySQL', 'IDENT', '42')->getMessage(),
184 );
185 }
186
187 public function testRenderedSubstitutesBytesJsonHasNoEncodingFor(): void
188 {
189 self::assertSame('["\\ufffd"]', LexicalException::rendered(["\xB1"]));
190 }
191
192 public function testRenderedWritesAnOrdinarySequenceAsJson(): void
193 {
194 self::assertSame('["IDENT"]', LexicalException::rendered(['IDENT']));
195 }
196
197 public function testRoundTripMismatchCarriesBothSequencesAndTheText(): void
198 {
199 $message = LexicalException::roundTripMismatch(
200 'MySQL',
201 'mysql-8.4.7',
202 ['SELECT_SYM'],
203 ['IDENT'],
204 'select',
205 )->getMessage();
206
207 self::assertSame(
208 "MySQL lexical round-trip failed for mysql-8.4.7.\n"
209 . "Expected: [\"SELECT_SYM\"]\n"
210 . "Actual: [\"IDENT\"]\n"
211 . 'SQL: select',
212 $message,
213 );
214 }
215}
216