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