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