packages/sql-faker/tests/Unit/Sqlite/Generation/Lexeme/DefinitionFactoryTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Sqlite\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\Sqlite\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\Lexeme\Lexeme::class)]
28#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeCandidates::class)]
29#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeSequence::class)]
30#[UsesClass(\SqlFaker\Generation\Candidate\MatchingLexemeGenerator::class)]
31#[UsesClass(\SqlFaker\Generation\Candidate\ValueLexemeGenerator::class)]
32#[UsesClass(\SqlFaker\Generation\Candidate\RegisteredLexemeGenerator::class)]
33#[UsesClass(\SqlFaker\Generation\Output\BoundaryCompletion::class)]
34#[UsesClass(\SqlFaker\Generation\Output\CandidateResolver::class)]
35#[UsesClass(\SqlFaker\Generation\Lexeme\OutputPart::class)]
36#[UsesClass(\SqlFaker\Generation\Output\ReverseLexemeGenerator::class)]
37#[UsesClass(\SqlFaker\Generation\Output\CombinedSpacingRule::class)]
38#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeBoundary::class)]
39#[UsesClass(\SqlFaker\Generation\Lexeme\SpacingConstraint::class)]
40#[UsesClass(\SqlFaker\Generation\Token\ProductionOccurrence::class)]
41#[UsesClass(\SqlFaker\Generation\Token\TerminalOccurrence::class)]
42#[UsesClass(\SqlFaker\Generation\Value\CharacterDomain::class)]
43#[UsesClass(\SqlFaker\Generation\Value\ChoiceDomain::class)]
44#[UsesClass(\SqlFaker\MySql\Generation\Value\DollarQuotedDomain::class)]
45#[UsesClass(\SqlFaker\Generation\Value\IntegerDomain::class)]
46#[UsesClass(\SqlFaker\Generation\Value\SequenceDomain::class)]
47#[UsesClass(\SqlFaker\Generation\Value\ValueChoices::class)]
48#[UsesClass(\SqlFaker\Generation\Candidate\VersionCase::class)]
49#[UsesClass(\SqlFaker\Generation\Candidate\VersionedLexemeGenerator::class)]
50#[UsesClass(\SqlFaker\Generation\Exception\LexicalException::class)]
51#[UsesClass(\SqlFaker\Sqlite\Generation\Lexeme\JoinLexemeGenerator::class)]
52#[UsesClass(\SqlFaker\Sqlite\Generation\Lexeme\JoinModifiers::class)]
53#[UsesClass(\SqlFaker\Sqlite\Generation\Lexeme\WindowNameLexemeGenerator::class)]
54#[UsesClass(\SqlFaker\Sqlite\Generation\Value\IdentifierDomain::class)]
55#[UsesClass(\SqlFaker\Sqlite\Generation\Value\QuotedDomain::class)]
56#[UsesClass(\SqlFaker\Generation\Value\RepeatDomain::class)]
57#[UsesClass(\SqlFaker\Generation\Value\WordDomain::class)]
58#[UsesClass(\SqlFaker\Grammar\Resource\SqlVersionRegistry::class)]
59#[UsesClass(\SqlFaker\Grammar\Resource\SqlVersion::class)]
60#[UsesClass(\SqlFaker\Generation\Choice\BytePlanCompiler::class)]
61#[UsesClass(\SqlFaker\Sqlite\Generation\Lexeme\LexicalDefinition::class)]
62final class DefinitionFactoryTest extends TestCase
63{
64    public function testCreateCombinesLexicalOutputAndBoundaryDecisions(): void
65    {
66        $pipeline = (new DefinitionFactory())->create('sqlite-3.47.2')->pipeline;
67        $result = $pipeline->generate(TerminalSequence::fromNames(['INTEGER', 'SEMI']), null, static fn (int $count): int => 0);
68        self::assertSame('1 ;', (new SqlSerializer())->serialize($result->pieces()));
69    }
70
71    public function testLexemesRejectsAnUnreviewedVersion(): void
72    {
73        $this->expectException(RuntimeException::class);
74        (new DefinitionFactory())->create('future-version')->lexemes;
75    }
76
77    public function testSymbolsKeepMultiCharacterOperatorsIndivisible(): void
78    {
79        $result = (new DefinitionFactory())->create('sqlite-3.47.2')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames(['CONCAT']), 0, new ResolvedOutput()));
80        self::assertNotNull($result);
81        self::assertSame('||', [...$result->sequences()][0]->lexemes[0]->text);
82    }
83
84    public function testStrictTypesCoversTheSourceTypeTable(): void
85    {
86        $result = (new DefinitionFactory())->create('sqlite-3.47.2')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames(['STRICT_COLUMN_TYPE']), 0, new ResolvedOutput()));
87        self::assertNotNull($result);
88        self::assertSame(['ANY', 'BLOB', 'INT', 'INTEGER', 'REAL', 'TEXT'], array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$result->sequences()]));
89    }
90
91    /**
92     * @param list<string> $expected
93     */
94    #[DataProvider('providerGeneratedStorage')]
95    public function testLexemesRestrictsGeneratedStorageToTheBuildSourceNames(?string $spelling, array $expected): void
96    {
97        $result = (new DefinitionFactory())->create('sqlite-3.47.2')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames(['GENERATED_STORAGE']), 0, new ResolvedOutput(), $spelling));
98        self::assertNotNull($result);
99        self::assertSame($expected, array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$result->sequences()]));
100    }
101
102    /**
103     * @return iterable<array{?string, list<string>}>
104     */
105    public static function providerGeneratedStorage(): iterable
106    {
107        yield [null, ['VIRTUAL', 'STORED']];
108        yield ['virtual', ['virtual']];
109        yield ['Stored', ['Stored']];
110        yield ['name', []];
111        yield ['"VIRTUAL"', []];
112        yield ['[STORED]', []];
113    }
114
115    public function testValuesLeavesUnknownTerminalsUnclaimed(): void
116    {
117        self::assertNull((new DefinitionFactory())->create('sqlite-3.47.2')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames(['UNKNOWN']), 0, new ResolvedOutput())));
118    }
119
120    public function testNamesChecksSourceDelimiterOrValueBoundariesValue(): void
121    {
122        $generator = (new DefinitionFactory())->create('sqlite-3.47.2')->lexemes;
123        $valid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['ID']), 0, new ResolvedOutput(), '"a""b"'));
124        $invalid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['ID']), 0, new ResolvedOutput(), '"unclosed'));
125        self::assertNotNull($valid);
126        self::assertNotNull($invalid);
127        self::assertSame('"a""b"', [...$valid->sequences()][0]->lexemes[0]->text);
128        self::assertSame([], [...$invalid->sequences()]);
129    }
130
131    public function testStringsChecksSourceDelimiterOrValueBoundariesValue(): void
132    {
133        $generator = (new DefinitionFactory())->create('sqlite-3.47.2')->lexemes;
134        $valid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['BLOB']), 0, new ResolvedOutput(), "X'00'"));
135        $invalid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['BLOB']), 0, new ResolvedOutput(), "X'0'"));
136        self::assertNotNull($valid);
137        self::assertNotNull($invalid);
138        self::assertSame("X'00'", [...$valid->sequences()][0]->lexemes[0]->text);
139        self::assertSame([], [...$invalid->sequences()]);
140    }
141
142    public function testNumbersChecksSourceDelimiterOrValueBoundariesValue(): void
143    {
144        $generator = (new DefinitionFactory())->create('sqlite-3.47.2')->lexemes;
145        $valid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['QNUMBER']), 0, new ResolvedOutput(), '1_2'));
146        $invalid = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['QNUMBER']), 0, new ResolvedOutput(), '1__2'));
147        self::assertNotNull($valid);
148        self::assertNotNull($invalid);
149        self::assertSame('1_2', [...$valid->sequences()][0]->lexemes[0]->text);
150        self::assertSame([], [...$invalid->sequences()]);
151    }
152
153    #[DataProvider('providerValueTokensValue')]
154    public function testCreateOffersACompleteCandidateForEverySourceValueTokenValue(string $terminal): void
155    {
156        $result = (new DefinitionFactory())->create('sqlite-3.47.2')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames([$terminal]), 0, new ResolvedOutput()));
157        self::assertNotNull($result);
158        self::assertNotEmpty([...$result->sequences()]);
159    }
160
161    /**
162     * @return list<array{string}>
163     */
164    public static function providerValueTokensValue(): array
165    {
166        return [['ID'], ['id'], ['idj'], ['ANY'], ['VARIABLE'], ['INTEGER'], ['number'], ['FLOAT'], ['QNUMBER'], ['STRING'], ['ids'], ['BLOB']];
167    }
168
169    /**
170     * @param list<string> $expected
171     */
172    #[DataProvider('providerLexicalFormsValue')]
173    public function testCreateRetainsValidSourceSpellingsAndRejectsMalformedOnesValue(string $terminal, string $spelling, array $expected): void
174    {
175        $result = (new DefinitionFactory())->create('sqlite-3.47.2')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames([$terminal]), 0, new ResolvedOutput(), $spelling));
176        self::assertNotNull($result);
177        self::assertSame($expected, array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$result->sequences()]));
178    }
179
180    /**
181     * @return list<array{string, string, list<string>}>
182     */
183    public static function providerLexicalFormsValue(): array
184    {
185        return [
186            ['STRING', "'ordinary'", ["'ordinary'"]],
187            ['STRING', "'with\0nul'", []],
188            ['ids', "'ordinary'", ["'ordinary'"]],
189            ['ids', "'with\0nul'", []],
190            ['ID', '[a b]', ['[a b]']],
191            ['ID', '[unclosed', []],
192            ['id', '`a``b`', ['`a``b`']],
193            ['id', '`unclosed', []],
194            ['idj', '"a""b"', ['"a""b"']],
195            ['idj', '"unclosed', []],
196            ['ANY', 'alpha_7', ['alpha_7']],
197            ['ANY', 'bad name', []],
198            ['VARIABLE', '?23', ['?23']],
199            ['VARIABLE', '?abc', []],
200            ['INTEGER', '0XFE', ['0XFE']],
201            ['INTEGER', '0xGG', []],
202            ['number', '12', ['12']],
203            ['number', '-12', []],
204            ['FLOAT', '.75e-2', ['.75e-2']],
205            ['FLOAT', '.75e', []],
206            ['QNUMBER', '12_345_6', ['12_345_6']],
207            ['QNUMBER', '12__3', []],
208            ['ids', '\'a\'\'b\'', ['\'a\'\'b\'']],
209            ['ids', '\'unclosed', []],
210            ['BLOB', 'x\'01af\'', ['x\'01af\'']],
211            ['BLOB', 'X\'abc\'', []],
212        ];
213    }
214
215    /**
216     * @param list<int> $decisions
217     * @param list<string> $expected
218     */
219    #[DataProvider('providerConstructedBoundariesValue')]
220    public function testCreateConstructsScannerBoundaryValuesValue(string $terminal, array $decisions, array $expected): void
221    {
222        $values = new \SqlFaker\Generation\Value\ValueChoices(static function (int $count) use (&$decisions): int {
223            return array_shift($decisions) ?? $count - 1;
224        });
225        $result = (new DefinitionFactory())->create('sqlite-3.47.2')->lexemes->generate(new LexemeInput(TerminalSequence::fromNames([$terminal]), 0, new ResolvedOutput(), values: $values));
226        self::assertNotNull($result);
227        self::assertSame($expected, array_map(static fn ($candidate): string => $candidate->lexemes[0]->text, [...$result->sequences()]));
228    }
229
230    /**
231     * @return iterable<array{string, list<int>, list<string>}>
232     */
233    public static function providerConstructedBoundariesValue(): iterable
234    {
235        $minimum = [1, ...array_fill(0, 512, 0)];
236
237        foreach (['ID', 'id', 'idj', 'ANY'] as $terminal) {
238            yield [$terminal, $minimum, ['"a"']];
239            yield [$terminal, [], ['[' . str_repeat('猫', 64) . ']']];
240        }
241        yield ['VARIABLE', $minimum, ['?']];
242        yield ['VARIABLE', [], ['$v' . str_repeat('$', 63)]];
243        yield ['VARIABLE', [1, 1], ['?32766']];
244        yield ['INTEGER', $minimum, ['0']];
245        yield ['INTEGER', [], ['0X' . str_repeat('F', 16)]];
246        yield ['number', $minimum, ['0']];
247        yield ['number', [], [str_repeat('9', 19)]];
248        yield ['FLOAT', $minimum, ['0.']];
249        yield ['FLOAT', [], ['.' . str_repeat('9', 19) . 'E-999']];
250        yield ['FLOAT', [1, 0], ['.' . str_repeat('9', 19)]];
251        yield ['QNUMBER', $minimum, ['0_0']];
252        yield ['QNUMBER', [], [str_repeat('9', 19) . str_repeat('_' . str_repeat('9', 19), 8)]];
253        foreach (['STRING', 'ids'] as $terminal) {
254            yield [$terminal, $minimum, ["''"]];
255            yield [$terminal, [], ["'" . str_repeat('😀', 255) . "'"]];
256        }
257        yield ['BLOB', $minimum, ["X''"]];
258        yield ['BLOB', [], ["x'" . str_repeat('F', 254) . "'"]];
259
260        foreach (range(1, 127) as $byte) {
261            $encoded = str_replace("'", "''", chr($byte));
262            yield ['STRING', [1, 0, 1, $byte - 1], ["'" . $encoded . "'"]];
263            yield ['ids', [1, 0, 1, $byte - 1], ["'" . $encoded . "'"]];
264        }
265    }
266
267    public function testKeywordsKeepsSourceSpellingsBesideTheirTerminals(): void
268    {
269        $keywords = (new DefinitionFactory())->create('sqlite-3.47.2')->keywords;
270        self::assertSame(['SELECT'], $keywords['SELECT']);
271        self::assertSame(['CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP'], $keywords['CTIME_KW']);
272    }
273
274    public function testKeywordLexemesLeavesUnclaimedNamesAvailableToOtherDomains(): void
275    {
276        $factory = new DefinitionFactory();
277        $generator = $factory->create('sqlite-3.47.2')->lexemes;
278        self::assertNull($generator->generate(new LexemeInput(TerminalSequence::fromNames(['UNKNOWN']), 0, new ResolvedOutput())));
279        $result = $generator->generate(new LexemeInput(TerminalSequence::fromNames(['SELECT']), 0, new ResolvedOutput()));
280        self::assertNotNull($result);
281        self::assertSame('SELECT', [...$result->sequences()][0]->lexemes[0]->text);
282    }
283
284    public function testKeywordsRejectsUnsupportedReleases(): void
285    {
286        $this->expectException(RuntimeException::class);
287        (new DefinitionFactory())->create('unknown')->keywords;
288    }
289}
290