packages/sql-faker/tests/Unit/PostgreSql/Generation/Lexeme/DefinitionFactoryTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\PostgreSql\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\PostgreSql\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\RegisteredLexemeGenerator::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\Generation\Lexeme\LexemeBoundary::class)]
40#[UsesClass(\SqlFaker\Generation\Lexeme\SpacingConstraint::class)]
41#[UsesClass(\SqlFaker\Generation\Token\ProductionOccurrence::class)]
42#[UsesClass(\SqlFaker\Generation\Token\TerminalOccurrence::class)]
43#[UsesClass(\SqlFaker\Generation\Value\CharacterDomain::class)]
44#[UsesClass(\SqlFaker\Generation\Value\ChoiceDomain::class)]
45#[UsesClass(\SqlFaker\PostgreSql\Generation\Value\DollarQuotedDomain::class)]
46#[UsesClass(\SqlFaker\Generation\Value\IntegerDomain::class)]
47#[UsesClass(\SqlFaker\Generation\Value\SequenceDomain::class)]
48#[UsesClass(\SqlFaker\Generation\Value\ValueChoices::class)]
49#[UsesClass(\SqlFaker\Generation\Candidate\VersionCase::class)]
50#[UsesClass(\SqlFaker\Generation\Candidate\VersionedLexemeGenerator::class)]
51#[UsesClass(\SqlFaker\Generation\Exception\LexicalException::class)]
52#[UsesClass(\SqlFaker\PostgreSql\Generation\Lexeme\HashBoundLexemeGenerator::class)]
53#[UsesClass(\SqlFaker\PostgreSql\Generation\Lexeme\KeywordLexemeGenerator::class)]
54#[UsesClass(\SqlFaker\PostgreSql\Generation\Lookahead\PgLookahead::class)]
55#[UsesClass(\SqlFaker\PostgreSql\Generation\Value\IdentifierDomain::class)]
56#[UsesClass(\SqlFaker\PostgreSql\Generation\Value\QuotedDomain::class)]
57#[UsesClass(\SqlFaker\PostgreSql\Generation\Value\OperatorDomain::class)]
58#[UsesClass(\SqlFaker\Generation\Value\WordDomain::class)]
59#[UsesClass(\SqlFaker\Grammar\Resource\SqlVersionRegistry::class)]
60#[UsesClass(\SqlFaker\Grammar\Resource\SqlVersion::class)]
61#[UsesClass(\SqlFaker\Generation\Choice\BytePlanCompiler::class)]
62#[UsesClass(\SqlFaker\PostgreSql\Generation\Lexeme\LexicalDefinition::class)]
63final class DefinitionFactoryTest extends TestCase
64{
65 public function testCreateCombinesLexicalOutputAndBoundaryDecisions(): void
66 {
67 $pipeline = (new DefinitionFactory())->create('pg-17.2')->pipeline;
68 $result = $pipeline->generate(TerminalSequence::fromNames(['ICONST', 'MODE_TYPE_NAME']), null, static fn (int $count): int => 0);
69 self::assertSame('1', (new SqlSerializer())->serialize($result->pieces()));
70 }
71
72 public function testLexemesRejectsAnUnreviewedVersion(): void
73 {
74 $this->expectException(RuntimeException::class);
75 (new DefinitionFactory())->create('future-version')->lexemes;
76 }
77
78 public function testSymbolsKeepMultiCharacterOperatorsIndivisible(): void
79 {
80 $result = (new DefinitionFactory())->create('pg-17.2')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames(['TYPECAST']), 0, new ResolvedOutput()));
81 self::assertNotNull($result);
82 self::assertSame('::', [...$result->sequences()][0]->lexemes[0]->text);
83 }
84
85 public function testNonOutputContainsParserSelectorsOnly(): void
86 {
87 $markers = (new DefinitionFactory())->create('pg-17.2')->nonOutput;
88 self::assertContains('MODE_TYPE_NAME', $markers);
89 self::assertNotContains('SELECT', $markers);
90 }
91
92 public function testValuesLeavesUnknownTerminalsUnclaimed(): void
93 {
94 self::assertNull((new DefinitionFactory())->create('pg-17.2')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames(['UNDECLARED_TEST_TOKEN']), 0, new ResolvedOutput())));
95 }
96
97 public function testNamesChecksSourceDelimiterOrValueBoundariesValue(): void
98 {
99 $generator = (new DefinitionFactory())->create('pg-17.2')->lexemes;
100 $valid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['IDENT']), 0, new ResolvedOutput(), '"a""b"'));
101 $invalid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['IDENT']), 0, new ResolvedOutput(), '"unclosed'));
102 self::assertNotNull($valid);
103 self::assertNotNull($invalid);
104 self::assertSame('"a""b"', [...$valid->sequences()][0]->lexemes[0]->text);
105 self::assertSame([], [...$invalid->sequences()]);
106 }
107
108 public function testStringsChecksSourceDelimiterOrValueBoundariesValue(): void
109 {
110 $generator = (new DefinitionFactory())->create('pg-17.2')->lexemes;
111 $valid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['SCONST']), 0, new ResolvedOutput(), "'a''b'"));
112 $invalid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['SCONST']), 0, new ResolvedOutput(), "'unclosed"));
113 self::assertNotNull($valid);
114 self::assertNotNull($invalid);
115 self::assertSame("'a''b'", [...$valid->sequences()][0]->lexemes[0]->text);
116 self::assertSame([], [...$invalid->sequences()]);
117 }
118
119 public function testNumbersChecksSourceDelimiterOrValueBoundariesValue(): void
120 {
121 $generator = (new DefinitionFactory())->create('pg-17.2')->lexemes;
122 $valid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['Op']), 0, new ResolvedOutput(), '?&'));
123 $invalid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['Op']), 0, new ResolvedOutput(), '/*'));
124 self::assertNotNull($valid);
125 self::assertNotNull($invalid);
126 self::assertSame('?&', [...$valid->sequences()][0]->lexemes[0]->text);
127 self::assertSame([], [...$invalid->sequences()]);
128 }
129
130 #[DataProvider('providerValueTokensValue')]
131 public function testCreateOffersACompleteCandidateForEverySourceValueTokenValue(string $terminal): void
132 {
133 $result = (new DefinitionFactory())->create('pg-17.2')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames([$terminal]), 0, new ResolvedOutput()));
134 self::assertNotNull($result);
135 self::assertNotEmpty([...$result->sequences()]);
136 }
137
138 /**
139 * @return list<array{string}>
140 */
141 public static function providerValueTokensValue(): array
142 {
143 return [['IDENT'], ['UIDENT'], ['PARAM'], ['SCONST'], ['USCONST'], ['BCONST'], ['XCONST'], ['ICONST'], ['FCONST'], ['Op']];
144 }
145
146 /**
147 * @param list<string> $expected
148 */
149 #[DataProvider('providerLexicalFormsValue')]
150 public function testCreateRetainsValidSourceSpellingsAndRejectsMalformedOnesValue(string $terminal, string $spelling, array $expected): void
151 {
152 $result = (new DefinitionFactory())->create('pg-17.2')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames([$terminal]), 0, new ResolvedOutput(), $spelling));
153 self::assertNotNull($result);
154 self::assertSame($expected, array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$result->sequences()]));
155 }
156
157 /**
158 * @return list<array{string, string, list<string>}>
159 */
160 public static function providerLexicalFormsValue(): array
161 {
162 return [
163 ['FLOAT_PRECISION_NUMBER', '53', ['53']],
164 ['FLOAT_PRECISION_NUMBER', '54', []],
165 ['FLOAT_PRECISION_NUMBER', '0', []],
166 ['COLUMN_POSITION_NUMBER', '32767', ['32767']],
167 ['COLUMN_POSITION_NUMBER', '32768', []],
168 ['COLUMN_POSITION_NUMBER', '0', []],
169 ['SCONST', "'ordinary'", ["'ordinary'"]],
170 ['SCONST', "'with\0nul'", []],
171 ['USCONST', "U&'ordinary'", ["U&'ordinary'"]],
172 ['USCONST', "U&'with\0nul'", []],
173 ['UIDENT', 'u&"a""b"', ['u&"a""b"']],
174 ['UIDENT', 'u&"unterminated', []],
175 ['PARAM', '$17', ['$17']],
176 ['PARAM', '$', []],
177 ['SCONST', 'E\'a\\\'b\'', ['E\'a\\\'b\'']],
178 ['SCONST', '\'unclosed', []],
179 ['USCONST', 'u&\'a\'\'b\'', ['u&\'a\'\'b\'']],
180 ['USCONST', 'u\'wrong prefix\'', []],
181 ['BCONST', 'b\'001\'', ['b\'001\'']],
182 ['BCONST', 'b\'02\'', []],
183 ['XCONST', 'x\'f\'', ['x\'f\'']],
184 ['XCONST', 'x\'g\'', []],
185 ['ICONST', '1_000', ['1_000']],
186 ['ICONST', '1__000', []],
187 ['FCONST', '1', []],
188 ['FCONST', '2147483647', []],
189 ['FCONST', '2147483648', ['2147483648']],
190 ['FCONST', '.5e+7', ['.5e+7']],
191 ['FCONST', '.5e', []],
192 ['Op', '?@', ['?@']],
193 ['Op', '/*', []],
194 ['Op', '|-', ['|-']],
195 ['Op', '--', []],
196 ];
197 }
198 #[DataProvider('providerDollarStringsValue')]
199 public function testStringsRecognizesDollarDelimitersLiterallyValue(string $value, bool $valid): void
200 {
201 $result = (new DefinitionFactory())->create('pg-17.2')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames(['SCONST']), 0, new ResolvedOutput(), $value));
202 self::assertNotNull($result);
203 self::assertSame($valid ? [$value] : [], array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$result->sequences()]));
204 }
205
206 /**
207 * @return iterable<array{string, bool}>
208 */
209 public static function providerDollarStringsValue(): iterable
210 {
211 yield ['$$text$$', true];
212 yield ['$tag$text$tag$', true];
213 yield ['$_0$a\'b$_0$', true];
214 yield ['$tag$text$other$', false];
215 yield ['$0$text$0$', false];
216 yield ['$$a$$b$$', false];
217 }
218
219 /**
220 * @param list<int> $decisions
221 * @param list<string> $expected
222 */
223 #[DataProvider('providerConstructedBoundariesValue')]
224 public function testCreateConstructsScannerBoundaryValuesValue(string $terminal, array $decisions, array $expected): void
225 {
226 $values = new \SqlFaker\Generation\Value\ValueChoices(static function (int $count) use (&$decisions): int {
227 return array_shift($decisions) ?? $count - 1;
228 });
229 $result = (new DefinitionFactory())->create('pg-17.2')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames([$terminal]), 0, new ResolvedOutput(), values: $values));
230 self::assertNotNull($result);
231 self::assertSame($expected, array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$result->sequences()]));
232 }
233
234 /**
235 * @return iterable<array{string, list<int>, list<string>}>
236 */
237 public static function providerConstructedBoundariesValue(): iterable
238 {
239 $minimum = [1, ...array_fill(0, 512, 0)];
240 $padding = str_repeat('0', 16);
241
242 yield ['IDENT', $minimum, ['_sf']];
243 yield ['IDENT', [], ['"' . str_repeat('猫', 63) . '"']];
244 yield ['IDENT', [1, 0], ['_sf' . str_repeat('$', 59)]];
245 yield ['UIDENT', $minimum, ['U&"a"']];
246 yield ['UIDENT', [], ['u&"' . str_repeat('猫', 63) . '"']];
247 yield ['PARAM', $minimum, ['$1']];
248 yield ['PARAM', [], ['$99999']];
249 yield ['SCONST', $minimum, ["''"]];
250 yield ['SCONST', [], ['$' . str_repeat('_', 17) . '$' . str_repeat('😀', 255) . '$' . str_repeat('_', 17) . '$']];
251 yield ['SCONST', [1, 0], ["'" . str_repeat('😀', 255) . "'"]];
252 yield ['SCONST', [1, 1], ["e'" . str_repeat('😀', 255) . "'"]];
253 yield ['USCONST', $minimum, ["U&''"]];
254 yield ['USCONST', [], ["u&'" . str_repeat('😀', 255) . "'"]];
255 yield ['BCONST', $minimum, ["B''"]];
256 yield ['BCONST', [], ["b'" . str_repeat('1', 255) . "'"]];
257 yield ['XCONST', $minimum, ["X''"]];
258 yield ['XCONST', [], ["x'" . str_repeat('F', 255) . "'"]];
259 yield ['FLOAT_PRECISION_NUMBER', $minimum, ['1']];
260 yield ['FLOAT_PRECISION_NUMBER', [], [$padding . '53']];
261 yield ['COLUMN_POSITION_NUMBER', $minimum, ['1']];
262 yield ['COLUMN_POSITION_NUMBER', [], [$padding . '32767']];
263 yield ['ICONST', $minimum, ['0']];
264 yield ['ICONST', [], [$padding . '2147483647']];
265 yield ['FCONST', $minimum, ['2147483648', '1.5', '.5', '1e2']];
266 yield ['FCONST', [], [$padding . str_repeat('9', 65), '.' . str_repeat('9', 20) . 'E-999']];
267 yield ['FCONST', [0, 1, 0], ['2147483648', '.' . str_repeat('9', 20)]];
268 yield ['Op', $minimum, ['?']];
269 yield ['Op', [], ['?' . str_repeat('~', 31)]];
270
271 foreach (range(1, 127) as $byte) {
272 $plain = str_replace("'", "''", chr($byte));
273 $escaped = str_replace('\\', '\\\\', $plain);
274 yield ['SCONST', [1, 0, 0, 1, $byte - 1], ["'" . $plain . "'"]];
275 yield ['SCONST', [1, 1, 0, 1, $byte - 1], ["E'" . $escaped . "'"]];
276 yield ['USCONST', [1, 0, 1, $byte - 1], ["U&'" . $escaped . "'"]];
277 }
278 }
279
280 public function testContextualNamesRestrictsTheDomain(): void
281 {
282 $generator = (new DefinitionFactory())->create('pg-17.2')->lexemes;
283 $result = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['POLICY_MODE']), 0, new ResolvedOutput(), null));
284 self::assertNotNull($result);
285 self::assertSame(['PERMISSIVE', 'RESTRICTIVE'], array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$result->sequences()]));
286 $invalid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['POLICY_MODE']), 0, new ResolvedOutput(), 'unknown'));
287 self::assertNotNull($invalid);
288 self::assertSame([], [...$invalid->sequences()]);
289 }
290
291 public function testContextualWordPreservesExplicitCase(): void
292 {
293 $generator = (new DefinitionFactory())->create('pg-17.2')->lexemes;
294 $result = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['POLICY_MODE']), 0, new ResolvedOutput(), 'permissive'));
295 self::assertNotNull($result);
296 self::assertSame('permissive', [...$result->sequences()][0]->lexemes[0]->text);
297 }
298
299 public function testCreateIncludesEveryGrammarEncodingAndPartitionStrategyContextualName(): void
300 {
301 $generator = (new DefinitionFactory())->create('pg-17.2')->lexemes;
302 $encoding = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['JSON_ENCODING']), 0, new ResolvedOutput()));
303 $strategy = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['PARTITION_STRATEGY']), 0, new ResolvedOutput()));
304 self::assertNotNull($encoding);
305 self::assertNotNull($strategy);
306 self::assertSame(['UTF8', 'UTF16', 'UTF32'], array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$encoding->sequences()]));
307 self::assertSame(['LIST', 'RANGE', 'HASH'], array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$strategy->sequences()]));
308 }
309
310 public function testKeywordsKeepsSourceSpellingsBesideTheirTerminals(): void
311 {
312 $keywords = (new DefinitionFactory())->create('pg-17.2')->keywords;
313 self::assertSame(['SELECT'], $keywords['SELECT']);
314 self::assertSame(['INTEGER'], $keywords['INTEGER']);
315 self::assertSame(['INT'], $keywords['INT_P']);
316 }
317
318 public function testKeywordLexemesLeavesUnclaimedNamesAvailableToOtherDomains(): void
319 {
320 $factory = new DefinitionFactory();
321 $generator = $factory->create('pg-17.2')->lexemes;
322 self::assertNull($generator->generate(new LexemeInput(TerminalSequence::fromNames(['UNDECLARED_TEST_TOKEN']), 0, new ResolvedOutput())));
323 $result = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['SELECT']), 0, new ResolvedOutput()));
324 self::assertNotNull($result);
325 self::assertSame('SELECT', [...$result->sequences()][0]->lexemes[0]->text);
326 }
327
328 public function testKeywordsRejectsUnsupportedReleases(): void
329 {
330 $this->expectException(RuntimeException::class);
331 (new DefinitionFactory())->create('unknown')->keywords;
332 }
333}
334