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