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

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\PostgreSql\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\PostgreSql\Generation\GenerationPlans;
13
14#[CoversClass(GenerationPlans::class)]
15#[UsesClass(GenerationPlan::class)]
16#[UsesClass(ProductionPattern::class)]
17#[UsesClass(\SqlFaker\Generation\Choice\BytePlanCompiler::class)]
18final class GenerationPlansTest extends TestCase
19{
20    public function testForeignKeyConstraintRestrictsTheTableConstraintGrammar(): void
21    {
22        $plan = GenerationPlans::foreignKeyConstraint();
23
24        self::assertSame('TableConstraint', $plan->startRule());
25        self::assertTrue($plan->patternAt('TableConstraint', 0)?->matches(['CONSTRAINT']) ?? false);
26        self::assertTrue($plan->patternAt('ConstraintElem', 0)?->matches(['FOREIGN', 'KEY']) ?? false);
27        self::assertTrue($plan->patternAt('opt_column_list', 0)?->matches(['column']) ?? false);
28    }
29
30    public function testInsertFunctionUpsertStatementRestrictsTheConflictFunctionGrammar(): void
31    {
32        $plan = GenerationPlans::insertFunctionUpsertStatement();
33
34        self::assertSame('InsertStmt', $plan->startRule());
35        self::assertTrue($plan->patternAt('opt_with_clause', 0)?->matches([]) ?? false);
36        self::assertTrue($plan->patternAt('insert_target', 0)?->matches(['qualified_name']) ?? false);
37        self::assertTrue($plan->patternAt('qualified_name', 0)?->matches(['ColId']) ?? false);
38        self::assertTrue(
39            $plan->patternAt('insert_rest', 0)?->matches(['insert_column_list', 'SelectStmt']) ?? false,
40        );
41        self::assertTrue($plan->patternAt('insert_column_list', 0)?->matches(['insert_column_item']) ?? false);
42        self::assertTrue($plan->patternAt('select_no_parens', 0)?->matches(['simple_select']) ?? false);
43        self::assertTrue($plan->patternAt('simple_select', 0)?->matches(['values_clause']) ?? false);
44        self::assertTrue(
45            $plan->patternAt('values_clause', 0)?->matches(['VALUES', '(', 'expr_list', ')']) ?? false,
46        );
47        self::assertTrue($plan->patternAt('expr_list', 0)?->matches(['a_expr']) ?? false);
48        self::assertTrue($plan->patternAt('opt_on_conflict', 0)?->matches(['DO', 'UPDATE']) ?? false);
49        self::assertTrue($plan->patternAt('opt_conf_expr', 0)?->matches([]) ?? false);
50        self::assertTrue($plan->patternAt('opt_indirection', 0)?->matches([]) ?? false);
51        self::assertTrue($plan->patternAt('opt_indirection', 1)?->matches([]) ?? false);
52        self::assertTrue($plan->patternAt('a_expr', 0)?->matches(['c_expr']) ?? false);
53        self::assertTrue($plan->patternAt('a_expr', 1)?->matches(['c_expr']) ?? false);
54        self::assertTrue($plan->patternAt('c_expr', 0)?->matches(['AexprConst']) ?? false);
55        self::assertTrue($plan->patternAt('c_expr', 1)?->matches(['func_expr']) ?? false);
56        self::assertTrue($plan->patternAt('AexprConst', 0)?->matches(['Iconst']) ?? false);
57        self::assertTrue($plan->patternAt('func_expr', 0)?->matches(['func_application']) ?? false);
58    }
59
60    public function testTemporaryTableStatementRequiresTheTemporaryProduction(): void
61    {
62        $plan = GenerationPlans::temporaryTableStatement();
63
64        self::assertSame('CreateStmt', $plan->startRule());
65        self::assertTrue($plan->patternAt('OptTemp', 0)?->matches(['TEMP']) ?? false);
66    }
67
68    public function testViewStatementStartsFromTheViewGrammar(): void
69    {
70        self::assertSame('ViewStmt', GenerationPlans::viewStatement()->startRule());
71    }
72
73    public function testGeneratedColumnStatementRequiresTheGeneratedConstraint(): void
74    {
75        $plan = GenerationPlans::generatedColumnStatement();
76
77        self::assertSame('CreateStmt', $plan->startRule());
78        self::assertTrue($plan->patternAt('CreateStmt', 0)?->matches(['OptTableElementList']) ?? false);
79        self::assertTrue($plan->patternAt('OptTableElementList', 0)?->matches(['TableElement']) ?? false);
80        self::assertTrue($plan->patternAt('TableElement', 0)?->matches(['columnDef']) ?? false);
81        self::assertTrue(
82            $plan->patternAt('ColQualList', 0)?->matches(['ColQualList', 'ColConstraint']) ?? false,
83        );
84        self::assertTrue($plan->patternAt('ColQualList', 1)?->matches([]) ?? false);
85        self::assertTrue($plan->patternAt('ColConstraint', 0)?->matches(['ColConstraintElem']) ?? false);
86        self::assertTrue($plan->patternAt('ColConstraintElem', 0)?->matches(['GENERATED', 'STORED']) ?? false);
87    }
88
89    public function testForeignKeyCascadeStatementRequiresBothCascadeActions(): void
90    {
91        $plan = GenerationPlans::foreignKeyCascadeStatement();
92
93        self::assertSame('CreateStmt', $plan->startRule());
94        self::assertTrue($plan->patternAt('CreateStmt', 0)?->matches(['OptTableElementList']) ?? false);
95        self::assertTrue($plan->patternAt('OptTableElementList', 0)?->matches(['TableElement']) ?? false);
96        self::assertTrue($plan->patternAt('TableElement', 0)?->matches(['TableConstraint']) ?? false);
97        self::assertTrue(
98            $plan->patternAt('ConstraintElem', 0)?->matches(['FOREIGN', 'KEY', 'REFERENCES']) ?? false,
99        );
100        self::assertTrue($plan->patternAt('key_actions', 0)?->matches(['key_update', 'key_delete']) ?? false);
101        self::assertTrue($plan->patternAt('key_action', 0)?->matches(['CASCADE']) ?? false);
102        self::assertTrue($plan->patternAt('key_action', 1)?->matches(['CASCADE']) ?? false);
103    }
104
105    public function testPartitionOfStatementRequiresARangeBound(): void
106    {
107        $plan = GenerationPlans::partitionOfStatement();
108
109        self::assertSame('CreateStmt', $plan->startRule());
110        self::assertTrue(
111            $plan->patternAt('CreateStmt', 0)?->matches(['PARTITION', 'OF', 'PartitionBoundSpec']) ?? false,
112        );
113        self::assertTrue($plan->patternAt('PartitionBoundSpec', 0)?->matches(['FROM', 'TO']) ?? false);
114    }
115
116    public function testTableSampleStatementRequiresASamplingClause(): void
117    {
118        $plan = GenerationPlans::tableSampleStatement();
119
120        self::assertSame('SelectStmt', $plan->startRule());
121        self::assertTrue($plan->patternAt('SelectStmt', 0)?->matches(['select_no_parens']) ?? false);
122        self::assertTrue($plan->patternAt('select_no_parens', 0)?->matches(['simple_select']) ?? false);
123        self::assertTrue(
124            $plan->patternAt('simple_select', 0)?->matches(['SELECT', 'opt_target_list', 'from_clause']) ?? false,
125        );
126        self::assertTrue($plan->patternAt('from_clause', 0)?->matches(['table_ref']) ?? false);
127        self::assertTrue(
128            $plan->patternAt('table_ref', 0)?->matches(['relation_expr', 'tablesample_clause']) ?? false,
129        );
130    }
131
132    public function testDoStatementRequiresASingleStringBlock(): void
133    {
134        $plan = GenerationPlans::doStatement();
135
136        self::assertSame('DoStmt', $plan->startRule());
137        self::assertTrue($plan->patternAt('dostmt_opt_list', 0)?->matches(['dostmt_opt_item']) ?? false);
138        self::assertTrue($plan->patternAt('dostmt_opt_item', 0)?->matches(['Sconst']) ?? false);
139    }
140
141    public function testMergeStatementRequiresAllMutationBranches(): void
142    {
143        $plan = GenerationPlans::mergeStatement();
144
145        self::assertSame('MergeStmt', $plan->startRule());
146        self::assertTrue(
147            $plan->patternAt('merge_when_list', 0)?->matches(['merge_when_list', 'merge_when_clause']) ?? false,
148        );
149        self::assertTrue(
150            $plan->patternAt('merge_when_list', 1)?->matches(['merge_when_list', 'merge_when_clause']) ?? false,
151        );
152        self::assertTrue(
153            $plan->patternAt('merge_when_list', 2)?->matches(['merge_when_list', 'merge_when_clause']) ?? false,
154        );
155        self::assertTrue($plan->patternAt('merge_when_list', 3)?->matches(['merge_when_clause']) ?? false);
156        self::assertTrue(
157            $plan->patternAt('merge_when_clause', 0)?->matches(['merge_when_tgt_matched', 'merge_delete']) ?? false,
158        );
159        self::assertTrue(
160            $plan->patternAt('merge_when_clause', 1)?->matches(['merge_when_tgt_matched', 'DO', 'NOTHING']) ?? false,
161        );
162        self::assertTrue(
163            $plan->patternAt('merge_when_clause', 2)?->matches(['merge_when_tgt_matched', 'merge_update']) ?? false,
164        );
165        self::assertTrue(
166            $plan->patternAt('merge_when_clause', 3)?->matches(['merge_when_tgt_not_matched', 'merge_insert']) ?? false,
167        );
168    }
169
170    public function testCopyStatementStartsFromTheCopyGrammar(): void
171    {
172        $plan = GenerationPlans::copyStatement();
173
174        self::assertSame('CopyStmt', $plan->startRule());
175    }
176
177    public function testPartialIndexUpsertStatementRequiresAConflictPredicate(): void
178    {
179        $plan = GenerationPlans::partialIndexUpsertStatement();
180
181        self::assertSame('InsertStmt', $plan->startRule());
182        self::assertTrue($plan->patternAt('opt_with_clause', 0)?->matches([]) ?? false);
183        self::assertTrue($plan->patternAt('qualified_name', 0)?->matches(['ColId']) ?? false);
184        self::assertTrue($plan->patternAt('insert_rest', 0)?->matches(['DEFAULT', 'VALUES']) ?? false);
185        self::assertTrue($plan->patternAt('opt_on_conflict', 0)?->matches(['DO', 'UPDATE']) ?? false);
186        self::assertTrue(
187            $plan->patternAt('opt_conf_expr', 0)?->matches(['index_params', 'where_clause']) ?? false,
188        );
189        self::assertTrue($plan->patternAt('where_clause', 0)?->matches(['a_expr']) ?? false);
190    }
191
192    public function testDomainDmlStatementsCoverAllMutationGrammars(): void
193    {
194        $plans = GenerationPlans::domainDmlStatements();
195
196        self::assertSame(
197            ['InsertStmt', 'UpdateStmt', 'DeleteStmt'],
198            array_map(static fn (GenerationPlan $plan): ?string => $plan->startRule(), $plans),
199        );
200        self::assertTrue($plans[0]->patternAt('opt_with_clause', 0)?->matches([]) ?? false);
201        self::assertTrue($plans[1]->patternAt('opt_with_clause', 0)?->matches([]) ?? false);
202        self::assertTrue($plans[2]->patternAt('opt_with_clause', 0)?->matches([]) ?? false);
203    }
204
205    public function testFullTextSearchStatementRequiresTheMatchOperator(): void
206    {
207        $plan = GenerationPlans::fullTextSearchStatement();
208
209        self::assertSame('SelectStmt', $plan->startRule());
210        self::assertTrue($plan->patternAt('SelectStmt', 0)?->matches(['select_no_parens']) ?? false);
211        self::assertTrue($plan->patternAt('select_no_parens', 0)?->matches(['simple_select']) ?? false);
212        self::assertTrue(
213            $plan->patternAt('simple_select', 0)?->matches([
214                'SELECT',
215                'opt_target_list',
216                'from_clause',
217                'where_clause',
218            ]) ?? false,
219        );
220        self::assertTrue($plan->patternAt('opt_target_list', 0)?->matches(['target_list']) ?? false);
221        self::assertTrue($plan->patternAt('target_list', 0)?->matches(['target_el']) ?? false);
222        self::assertTrue($plan->patternAt('target_el', 0)?->matches(['*']) ?? false);
223        self::assertTrue($plan->patternAt('into_clause', 0)?->matches([]) ?? false);
224        self::assertTrue($plan->patternAt('from_clause', 0)?->matches(['from_list']) ?? false);
225        self::assertTrue($plan->patternAt('from_list', 0)?->matches(['table_ref']) ?? false);
226        self::assertTrue(
227            $plan->patternAt('table_ref', 0)?->matches(['relation_expr', 'opt_alias_clause']) ?? false,
228        );
229        self::assertTrue($plan->patternAt('relation_expr', 0)?->matches(['qualified_name']) ?? false);
230        self::assertTrue($plan->patternAt('qualified_name', 0)?->matches(['ColId']) ?? false);
231        self::assertTrue($plan->patternAt('opt_alias_clause', 0)?->matches([]) ?? false);
232        self::assertTrue($plan->patternAt('where_clause', 0)?->matches(['a_expr']) ?? false);
233        self::assertTrue($plan->patternAt('group_clause', 0)?->matches([]) ?? false);
234        self::assertTrue($plan->patternAt('having_clause', 0)?->matches([]) ?? false);
235        self::assertTrue($plan->patternAt('window_clause', 0)?->matches([]) ?? false);
236        self::assertTrue($plan->patternAt('a_expr', 0)?->matches(['a_expr', 'qual_Op', 'a_expr']) ?? false);
237        self::assertTrue($plan->patternAt('a_expr', 1)?->matches(['c_expr']) ?? false);
238        self::assertTrue($plan->patternAt('a_expr', 2)?->matches(['c_expr']) ?? false);
239        self::assertTrue($plan->patternAt('qual_Op', 0)?->matches(['Op']) ?? false);
240        self::assertTrue($plan->patternAt('c_expr', 0)?->matches(['columnref']) ?? false);
241        self::assertTrue($plan->patternAt('c_expr', 1)?->matches(['columnref']) ?? false);
242        self::assertTrue($plan->patternAt('columnref', 0)?->matches(['ColId']) ?? false);
243        self::assertTrue($plan->patternAt('columnref', 1)?->matches(['ColId']) ?? false);
244        self::assertTrue($plan->patternAt('ColId', 0)?->matches(['IDENT']) ?? false);
245        self::assertTrue($plan->patternAt('ColId', 1)?->matches(['IDENT']) ?? false);
246        self::assertTrue($plan->patternAt('ColId', 2)?->matches(['IDENT']) ?? false);
247        self::assertSame('@@', $plan->lexemeAt('Op', 0));
248    }
249
250    public function testQuotedIdentifierPlansThatLexeme(): void
251    {
252        $plan = GenerationPlans::quotedIdentifier(1, 2);
253
254        self::assertSame('quoted_identifier', $plan->lexicalTarget());
255        self::assertSame(['minLength' => 1, 'maxLength' => 2], $plan->parameters());
256    }
257
258    public function testStringLiteralPlansThatLexeme(): void
259    {
260        $plan = GenerationPlans::stringLiteral(1, 2);
261
262        self::assertSame('string_literal', $plan->lexicalTarget());
263        self::assertSame(['minLength' => 1, 'maxLength' => 2], $plan->parameters());
264    }
265
266    public function testIntegerLiteralPlansThatLexeme(): void
267    {
268        $plan = GenerationPlans::integerLiteral(1, 2);
269
270        self::assertSame('integer_literal', $plan->lexicalTarget());
271        self::assertSame(['min' => 1, 'max' => 2], $plan->parameters());
272    }
273
274    public function testDecimalLiteralPlansThatLexeme(): void
275    {
276        $plan = GenerationPlans::decimalLiteral(1, 2);
277
278        self::assertSame('decimal_literal', $plan->lexicalTarget());
279        self::assertSame(['precision' => 1, 'scale' => 2], $plan->parameters());
280    }
281
282    public function testFloatLiteralPlansThatLexeme(): void
283    {
284        $plan = GenerationPlans::floatLiteral(1, 2, 3, 4);
285
286        self::assertSame('float_literal', $plan->lexicalTarget());
287        self::assertSame(['precision' => 1, 'scale' => 2, 'minExponent' => 3, 'maxExponent' => 4], $plan->parameters());
288    }
289
290    public function testHexLiteralPlansThatLexeme(): void
291    {
292        $plan = GenerationPlans::hexLiteral(1, 2);
293
294        self::assertSame('hex_literal', $plan->lexicalTarget());
295        self::assertSame(['minLength' => 1, 'maxLength' => 2], $plan->parameters());
296    }
297
298    public function testBinaryLiteralPlansThatLexeme(): void
299    {
300        $plan = GenerationPlans::binaryLiteral(1, 2);
301
302        self::assertSame('binary_literal', $plan->lexicalTarget());
303        self::assertSame(['minLength' => 1, 'maxLength' => 2], $plan->parameters());
304    }
305
306    public function testDollarQuotedStringPlansThatLexeme(): void
307    {
308        $plan = GenerationPlans::dollarQuotedString(1, 2);
309
310        self::assertSame('dollar_quoted_string', $plan->lexicalTarget());
311        self::assertSame(['minLength' => 1, 'maxLength' => 2], $plan->parameters());
312    }
313
314    public function testParameterMarkerPlansThatLexeme(): void
315    {
316        $plan = GenerationPlans::parameterMarker(1, 2);
317
318        self::assertSame('parameter_marker', $plan->lexicalTarget());
319        self::assertSame(['min' => 1, 'max' => 2], $plan->parameters());
320    }
321
322    public function testStatementOfTypePreservesAnExplicitTypeWithoutConsumingRandomness(): void
323    {
324        $faker = \Faker\Factory::create();
325        $faker->seed(17);
326        $plan = GenerationPlans::statementOfType($faker, \SqlFaker\PostgreSql\StatementType::Select, 6);
327        $next = $faker->randomNumber();
328        $faker->seed(17);
329
330        self::assertSame($faker->randomNumber(), $next);
331        self::assertSame(\SqlFaker\PostgreSql\StatementType::Select->value, $plan->startRule());
332        self::assertSame(6, $plan->maxDepth());
333        self::assertSame(false, $plan->usesStepBudget());
334    }
335
336    public function testStatementOfTypeUsesTheSeededFakerChoiceWhenNoTypeIsSpecified(): void
337    {
338        $faker = \Faker\Factory::create();
339        $faker->seed(17);
340        $plan = GenerationPlans::statementOfType($faker, null, 8);
341        $faker->seed(17);
342        /** @var \SqlFaker\PostgreSql\StatementType $expected */
343        $expected = $faker->randomElement(\SqlFaker\PostgreSql\StatementType::cases());
344
345        self::assertSame($expected->value, $plan->startRule());
346        self::assertSame(8, $plan->maxDepth());
347    }
348    public function testTableSampleStatementKeepsTheConstraintBeyondTheFirstOccurrence(): void
349    {
350        $plan = GenerationPlans::tableSampleStatement();
351        $from = $plan->patternAt('from_clause', 1);
352        $table = $plan->patternAt('table_ref', 1);
353        self::assertNotNull($from);
354        self::assertNotNull($table);
355        self::assertFalse($from->matches([]));
356        self::assertTrue($table->matches(['relation_expr', 'tablesample_clause']));
357        self::assertFalse($table->matches(['relation_expr']));
358    }
359
360}
361