packages/sql-faker/tests/Unit/MySql/Generation/GenerationPlansTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\MySql\Generation;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use SqlFaker\Generation\Plan\GenerationPlan;
11use SqlFaker\Generation\Plan\ProductionPattern;
12use SqlFaker\Grammar\Model\Grammar;
13use SqlFaker\Grammar\Model\ProductionRule;
14use SqlFaker\MySql\Generation\GenerationPlans;
15
16#[CoversClass(GenerationPlans::class)]
17#[UsesClass(GenerationPlan::class)]
18#[UsesClass(ProductionPattern::class)]
19#[UsesClass(Grammar::class)]
20#[UsesClass(ProductionRule::class)]
21#[UsesClass(\SqlFaker\Generation\Choice\BytePlanCompiler::class)]
22final class GenerationPlansTest extends TestCase
23{
24    public function testWithoutEmptyRowsConstrainsEveryOptionalValuesOccurrence(): void
25    {
26        $all = GenerationPlans::withoutEmptyRows();
27        $insert = GenerationPlans::withoutEmptyRows('insert_stmt');
28        $firstPattern = $all->patternAt('opt_values', 0);
29        $laterPattern = $all->patternAt('opt_values', 100);
30        $insertPattern = $insert->patternAt('opt_values', 100);
31
32        self::assertNull($all->startRule());
33        self::assertSame('insert_stmt', $insert->startRule());
34        self::assertNotNull($firstPattern);
35        self::assertNotNull($laterPattern);
36        self::assertNotNull($insertPattern);
37        self::assertFalse($firstPattern->matches([]));
38        self::assertTrue($firstPattern->matches(['values']));
39        self::assertFalse($laterPattern->matches([]));
40        self::assertTrue($insertPattern->matches(['values']));
41    }
42
43    public function testMultiTableUpdateStatementRestrictsTheUpdateGrammar(): void
44    {
45        $plan = GenerationPlans::multiTableUpdateStatement();
46
47        self::assertSame('update_stmt', $plan->startRule());
48        self::assertTrue($plan->patternAt('opt_with_clause', 0)?->matches([]) ?? false);
49        self::assertTrue(
50            $plan->patternAt('table_reference_list', 0)?->matches([
51                'table_reference_list',
52                ',',
53                'table_reference',
54            ]) ?? false,
55        );
56        self::assertTrue($plan->patternAt('table_reference_list', 1)?->matches(['table_reference']) ?? false);
57        self::assertTrue($plan->patternAt('table_reference', 0)?->matches(['table_factor']) ?? false);
58        self::assertTrue($plan->patternAt('table_reference', 1)?->matches(['table_factor']) ?? false);
59        self::assertTrue($plan->patternAt('table_factor', 0)?->matches(['single_table']) ?? false);
60        self::assertTrue($plan->patternAt('table_factor', 1)?->matches(['single_table']) ?? false);
61        self::assertTrue($plan->patternAt('opt_use_partition', 0)?->matches([]) ?? false);
62        self::assertTrue($plan->patternAt('opt_use_partition', 1)?->matches([]) ?? false);
63        self::assertTrue($plan->patternAt('update_list', 0)?->matches(['update_list', ',', 'update_elem']) ?? false);
64        self::assertTrue($plan->patternAt('update_list', 1)?->matches(['update_elem']) ?? false);
65    }
66
67    public function testMultiTableDeleteStatementRestrictsTheDeleteGrammar(): void
68    {
69        $plan = GenerationPlans::multiTableDeleteStatement();
70
71        self::assertSame('delete_stmt', $plan->startRule());
72        self::assertTrue(
73            $plan->patternAt('delete_stmt', 0)?->matches(['table_alias_ref_list', 'table_reference_list']) ?? false,
74        );
75        self::assertTrue($plan->patternAt('opt_with_clause', 0)?->matches([]) ?? false);
76        self::assertTrue(
77            $plan->patternAt('table_alias_ref_list', 0)?->matches([
78                'table_alias_ref_list',
79                ',',
80                'table_ident_opt_wild',
81            ]) ?? false,
82        );
83        self::assertTrue(
84            $plan->patternAt('table_alias_ref_list', 1)?->matches(['table_ident_opt_wild']) ?? false,
85        );
86        self::assertTrue(
87            $plan->patternAt('table_reference_list', 0)?->matches([
88                'table_reference_list',
89                ',',
90                'table_reference',
91            ]) ?? false,
92        );
93        self::assertTrue($plan->patternAt('table_reference_list', 1)?->matches(['table_reference']) ?? false);
94        self::assertTrue($plan->patternAt('table_reference', 0)?->matches(['table_factor']) ?? false);
95        self::assertTrue($plan->patternAt('table_reference', 1)?->matches(['table_factor']) ?? false);
96        self::assertTrue($plan->patternAt('table_factor', 0)?->matches(['single_table']) ?? false);
97        self::assertTrue($plan->patternAt('table_factor', 1)?->matches(['single_table']) ?? false);
98        self::assertTrue($plan->patternAt('opt_use_partition', 0)?->matches([]) ?? false);
99        self::assertTrue($plan->patternAt('opt_use_partition', 1)?->matches([]) ?? false);
100    }
101
102    public function testForeignKeyConstraintAdaptsToTheGrammarVersion(): void
103    {
104        $modernGrammar = new Grammar('table_constraint_def', [
105            'table_constraint_def' => new ProductionRule('table_constraint_def', []),
106            'opt_constraint_name' => new ProductionRule('opt_constraint_name', []),
107        ]);
108        $legacyGrammar = new Grammar('key_def', []);
109
110        $modern = GenerationPlans::foreignKeyConstraint($modernGrammar);
111        $legacy = GenerationPlans::foreignKeyConstraint($legacyGrammar);
112
113        self::assertSame('table_constraint_def', $modern->startRule());
114        self::assertTrue(
115            $modern->patternAt('table_constraint_def', 0)?->matches(['FOREIGN', 'KEY_SYM']) ?? false,
116        );
117        self::assertTrue($modern->patternAt('opt_constraint_name', 0)?->matches(['CONSTRAINT']) ?? false);
118        self::assertTrue($modern->patternAt('opt_ident', 0)?->matches(['ident']) ?? false);
119        self::assertTrue($modern->patternAt('opt_ref_list', 0)?->matches(['reference']) ?? false);
120        self::assertSame('key_def', $legacy->startRule());
121        self::assertTrue($legacy->patternAt('key_def', 0)?->matches(['FOREIGN', 'KEY_SYM']) ?? false);
122        self::assertTrue($legacy->patternAt('opt_constraint', 0)?->matches(['constraint']) ?? false);
123        self::assertTrue($legacy->patternAt('opt_ident', 0)?->matches(['ident']) ?? false);
124        self::assertTrue($legacy->patternAt('opt_ref_list', 0)?->matches(['reference']) ?? false);
125    }
126
127    public function testUpdateJoinDerivedStatementRestrictsTheDerivedTableGrammar(): void
128    {
129        $plan = GenerationPlans::updateJoinDerivedStatement();
130
131        self::assertSame('update_stmt', $plan->startRule());
132        self::assertTrue($plan->patternAt('opt_with_clause', 0)?->matches([]) ?? false);
133        self::assertTrue($plan->patternAt('table_reference', 0)?->matches(['joined_table']) ?? false);
134        self::assertTrue($plan->patternAt('table_reference', 1)?->matches(['table_factor']) ?? false);
135        self::assertTrue($plan->patternAt('table_reference', 2)?->matches(['table_factor']) ?? false);
136        self::assertTrue($plan->patternAt('table_reference', 3)?->matches(['table_factor']) ?? false);
137        self::assertTrue($plan->patternAt('joined_table', 0)?->matches(['ON_SYM']) ?? false);
138        self::assertTrue($plan->patternAt('table_factor', 0)?->matches(['single_table']) ?? false);
139        self::assertTrue($plan->patternAt('table_factor', 1)?->matches(['derived_table']) ?? false);
140        self::assertTrue($plan->patternAt('table_factor', 2)?->matches(['single_table']) ?? false);
141        self::assertTrue($plan->patternAt('opt_from_clause', 0)?->matches(['table_reference_list']) ?? false);
142        self::assertTrue($plan->patternAt('opt_group_clause', 0)?->matches(['GROUP_SYM']) ?? false);
143    }
144
145    public function testInsertSelectCompoundStatementRestrictsTheUnionGrammar(): void
146    {
147        $plan = GenerationPlans::insertSelectCompoundStatement();
148
149        self::assertSame('insert_stmt', $plan->startRule());
150        self::assertTrue($plan->patternAt('insert_stmt', 0)?->matches(['insert_query_expression']) ?? false);
151        self::assertTrue($plan->patternAt('query_expression_body', 0)?->matches(['UNION_SYM']) ?? false);
152        self::assertTrue($plan->patternAt('query_expression_body', 1)?->matches(['query_primary']) ?? false);
153        self::assertTrue($plan->patternAt('query_expression_body', 2)?->matches(['query_primary']) ?? false);
154        self::assertTrue($plan->patternAt('query_primary', 0)?->matches(['query_specification']) ?? false);
155        self::assertTrue($plan->patternAt('union_option', 0)?->matches(['ALL']) ?? false);
156    }
157
158    public function testInsertRowAliasUpsertStatementRestrictsTheValuesGrammar(): void
159    {
160        $plan = GenerationPlans::insertRowAliasUpsertStatement();
161
162        self::assertSame('insert_stmt', $plan->startRule());
163        self::assertTrue($plan->patternAt('insert_stmt', 0)?->matches(['insert_from_constructor']) ?? false);
164        self::assertTrue(
165            $plan->patternAt('insert_from_constructor', 0)?->matches(['insert_values']) ?? false,
166        );
167        self::assertTrue($plan->patternAt('value_or_values', 0)?->matches(['VALUES']) ?? false);
168        self::assertTrue($plan->patternAt('opt_values_reference', 0)?->matches(['alias']) ?? false);
169        self::assertTrue($plan->patternAt('opt_insert_update_list', 0)?->matches(['update']) ?? false);
170    }
171
172    public function testInsertFunctionUpsertStatementRestrictsTheConflictFunctionGrammar(): void
173    {
174        $plan = GenerationPlans::insertFunctionUpsertStatement();
175
176        self::assertSame('insert_stmt', $plan->startRule());
177        self::assertTrue($plan->patternAt('insert_stmt', 0)?->matches(['insert_from_constructor']) ?? false);
178        self::assertTrue(
179            $plan->patternAt('insert_from_constructor', 0)?->matches(['insert_values']) ?? false,
180        );
181        self::assertTrue($plan->patternAt('values_list', 0)?->matches(['row_value']) ?? false);
182        self::assertTrue($plan->patternAt('opt_values', 0)?->matches(['values']) ?? false);
183        self::assertTrue($plan->patternAt('values', 0)?->matches(['expr_or_default']) ?? false);
184        self::assertTrue($plan->patternAt('expr_or_default', 0)?->matches(['DEFAULT_SYM']) ?? false);
185        self::assertTrue($plan->patternAt('expr_or_default', 1)?->matches(['expr']) ?? false);
186        self::assertTrue($plan->patternAt('opt_insert_update_list', 0)?->matches(['update']) ?? false);
187        self::assertTrue($plan->patternAt('simple_expr', 0)?->matches(['function_call_conflict']) ?? false);
188        self::assertTrue($plan->patternAt('function_call_conflict', 0)?->matches(['IF']) ?? false);
189    }
190
191    public function testTemporaryTableStatementRequiresTheTemporaryProduction(): void
192    {
193        $plan = GenerationPlans::temporaryTableStatement();
194
195        self::assertSame('create_table_stmt', $plan->startRule());
196        self::assertTrue($plan->patternAt('opt_temporary', 0)?->matches(['TEMPORARY']) ?? false);
197    }
198
199    public function testViewStatementRestrictsTheCreateGrammar(): void
200    {
201        $plan = GenerationPlans::viewStatement();
202
203        self::assertSame('create', $plan->startRule());
204        self::assertTrue(
205            $plan->patternAt('create', 0)?->matches(['CREATE', 'view_or_trigger_or_sp_or_event']) ?? false,
206        );
207        self::assertTrue(
208            $plan->patternAt('view_or_trigger_or_sp_or_event', 0)?->matches([
209                'no_definer',
210                'init_lex_create_info',
211                'no_definer_tail',
212            ]) ?? false,
213        );
214        self::assertTrue($plan->patternAt('no_definer_tail', 0)?->matches(['view_tail']) ?? false);
215        self::assertTrue($plan->patternAt('query_primary', 0)?->matches(['query_specification']) ?? false);
216    }
217
218    public function testGeneratedColumnStatementRequiresTheGeneratedAttribute(): void
219    {
220        $plan = GenerationPlans::generatedColumnStatement();
221
222        self::assertSame('create_table_stmt', $plan->startRule());
223        self::assertTrue($plan->patternAt('create_table_stmt', 0)?->matches(['table_element_list']) ?? false);
224        self::assertTrue($plan->patternAt('table_element', 0)?->matches(['column_def']) ?? false);
225        self::assertTrue($plan->patternAt('field_def', 0)?->matches(['opt_generated_always', 'expr']) ?? false);
226        self::assertTrue($plan->patternAt('opt_generated_always', 0)?->matches(['GENERATED']) ?? false);
227        self::assertTrue($plan->patternAt('opt_stored_attribute', 0)?->matches(['STORED_SYM']) ?? false);
228    }
229
230    public function testForeignKeyCascadeStatementRequiresBothCascadeActions(): void
231    {
232        $plan = GenerationPlans::foreignKeyCascadeStatement();
233
234        self::assertSame('create_table_stmt', $plan->startRule());
235        self::assertTrue($plan->patternAt('create_table_stmt', 0)?->matches(['table_element_list']) ?? false);
236        self::assertTrue($plan->patternAt('table_element', 0)?->matches(['table_constraint_def']) ?? false);
237        self::assertTrue(
238            $plan->patternAt('table_constraint_def', 0)?->matches(['FOREIGN', 'KEY_SYM', 'references']) ?? false,
239        );
240        self::assertTrue($plan->patternAt('opt_ref_list', 0)?->matches(['reference']) ?? false);
241        self::assertTrue(
242            $plan->patternAt('opt_on_update_delete', 0)?->matches(['UPDATE_SYM', 'DELETE_SYM']) ?? false,
243        );
244        self::assertTrue($plan->patternAt('delete_option', 0)?->matches(['CASCADE']) ?? false);
245        self::assertTrue($plan->patternAt('delete_option', 1)?->matches(['CASCADE']) ?? false);
246    }
247
248    public function testPartitionSelectStatementRequiresThePartitionClause(): void
249    {
250        $plan = GenerationPlans::partitionSelectStatement();
251
252        self::assertSame('select_stmt', $plan->startRule());
253        self::assertTrue(
254            $plan->patternAt('query_expression', 0)?->matches([
255                'query_expression_body',
256                'opt_order_clause',
257                'opt_limit_clause',
258            ]) ?? false,
259        );
260        self::assertTrue($plan->patternAt('query_expression_body', 0)?->matches(['query_primary']) ?? false);
261        self::assertTrue($plan->patternAt('query_primary', 0)?->matches(['query_specification']) ?? false);
262        self::assertTrue($plan->patternAt('opt_from_clause', 0)?->matches(['from_tables']) ?? false);
263        self::assertTrue($plan->patternAt('from_tables', 0)?->matches(['table_reference_list']) ?? false);
264        self::assertTrue($plan->patternAt('table_factor', 0)?->matches(['single_table']) ?? false);
265        self::assertTrue($plan->patternAt('opt_use_partition', 0)?->matches(['PARTITION_SYM']) ?? false);
266    }
267
268    public function testLoadDataStatementStartsFromTheLoadGrammar(): void
269    {
270        $plan = GenerationPlans::loadDataStatement();
271
272        self::assertSame('load_stmt', $plan->startRule());
273    }
274
275    public function testFullTextSearchStatementRequiresMatchAgainstGrammar(): void
276    {
277        $plan = GenerationPlans::fullTextSearchStatement();
278
279        self::assertSame('select_stmt', $plan->startRule());
280        self::assertTrue($plan->patternAt('query_primary', 0)?->matches(['query_specification']) ?? false);
281        self::assertTrue($plan->patternAt('select_item_list', 0)?->matches(['*']) ?? false);
282        self::assertTrue($plan->patternAt('opt_from_clause', 0)?->matches(['from_tables']) ?? false);
283        self::assertTrue($plan->patternAt('from_tables', 0)?->matches(['table_reference_list']) ?? false);
284        self::assertTrue($plan->patternAt('opt_where_clause', 0)?->matches(['expr']) ?? false);
285        self::assertTrue($plan->patternAt('simple_expr', 0)?->matches(['MATCH', 'AGAINST']) ?? false);
286    }
287
288    public function testQuotedIdentifierPlansThatLexeme(): void
289    {
290        $plan = GenerationPlans::quotedIdentifier(1, 2);
291
292        self::assertSame('quoted_identifier', $plan->lexicalTarget());
293        self::assertSame(['minLength' => 1, 'maxLength' => 2], $plan->parameters());
294    }
295
296    public function testStringLiteralPlansThatLexeme(): void
297    {
298        $plan = GenerationPlans::stringLiteral(1, 2);
299
300        self::assertSame('string_literal', $plan->lexicalTarget());
301        self::assertSame(['minLength' => 1, 'maxLength' => 2], $plan->parameters());
302    }
303
304    public function testNationalStringLiteralPlansThatLexeme(): void
305    {
306        $plan = GenerationPlans::nationalStringLiteral(1, 2);
307
308        self::assertSame('national_string_literal', $plan->lexicalTarget());
309        self::assertSame(['minLength' => 1, 'maxLength' => 2], $plan->parameters());
310    }
311
312    public function testDollarQuotedStringPlansThatLexeme(): void
313    {
314        $plan = GenerationPlans::dollarQuotedString(1, 2);
315
316        self::assertSame('dollar_quoted_string', $plan->lexicalTarget());
317        self::assertSame(['minLength' => 1, 'maxLength' => 2], $plan->parameters());
318    }
319
320    public function testIntegerLiteralPlansThatLexeme(): void
321    {
322        $plan = GenerationPlans::integerLiteral(1, 2);
323
324        self::assertSame('integer_literal', $plan->lexicalTarget());
325        self::assertSame(['min' => 1, 'max' => 2], $plan->parameters());
326    }
327
328    public function testLongIntegerLiteralPlansThatLexeme(): void
329    {
330        $plan = GenerationPlans::longIntegerLiteral(1, 2);
331
332        self::assertSame('long_integer_literal', $plan->lexicalTarget());
333        self::assertSame(['min' => 1, 'max' => 2], $plan->parameters());
334    }
335
336    public function testUnsignedBigIntLiteralPlansThatLexeme(): void
337    {
338        $plan = GenerationPlans::unsignedBigIntLiteral(1, 2);
339
340        self::assertSame('unsigned_big_int_literal', $plan->lexicalTarget());
341        self::assertSame(['minLength' => 1, 'maxLength' => 2], $plan->parameters());
342    }
343
344    public function testDecimalLiteralPlansThatLexeme(): void
345    {
346        $plan = GenerationPlans::decimalLiteral(1, 2);
347
348        self::assertSame('decimal_literal', $plan->lexicalTarget());
349        self::assertSame(['precision' => 1, 'scale' => 2], $plan->parameters());
350    }
351
352    public function testFloatLiteralPlansThatLexeme(): void
353    {
354        $plan = GenerationPlans::floatLiteral(1, 2, 3, 4);
355
356        self::assertSame('float_literal', $plan->lexicalTarget());
357        self::assertSame(['precision' => 1, 'scale' => 2, 'minExponent' => 3, 'maxExponent' => 4], $plan->parameters());
358    }
359
360    public function testHexLiteralPlansThatLexeme(): void
361    {
362        $plan = GenerationPlans::hexLiteral(1, 2);
363
364        self::assertSame('hex_literal', $plan->lexicalTarget());
365        self::assertSame(['minLength' => 1, 'maxLength' => 2], $plan->parameters());
366    }
367
368    public function testQuotedHexLiteralPlansThatLexeme(): void
369    {
370        $plan = GenerationPlans::quotedHexLiteral(1, 2);
371
372        self::assertSame('quoted_hex_literal', $plan->lexicalTarget());
373        self::assertSame(['minBytes' => 1, 'maxBytes' => 2], $plan->parameters());
374    }
375
376    public function testBinaryLiteralPlansThatLexeme(): void
377    {
378        $plan = GenerationPlans::binaryLiteral(1, 2);
379
380        self::assertSame('binary_literal', $plan->lexicalTarget());
381        self::assertSame(['minLength' => 1, 'maxLength' => 2], $plan->parameters());
382    }
383
384    public function testHostnamePlansThatLexeme(): void
385    {
386        $plan = GenerationPlans::hostname(1, 2, 3);
387
388        self::assertSame('hostname', $plan->lexicalTarget());
389        self::assertSame(['minParts' => 1, 'maxParts' => 2, 'maxPartLength' => 3], $plan->parameters());
390    }
391}
392