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

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Sqlite\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\Sqlite\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 testMultiDmlStatementDirectsBothStatementOccurrences(): void
21    {
22        $plan = GenerationPlans::multiDmlStatement(0, 2);
23
24        self::assertSame('input', $plan->startRule());
25        self::assertTrue($plan->patternAt('cmdlist', 0)?->matches(['cmdlist', 'ecmd']) ?? false);
26        self::assertTrue($plan->patternAt('cmdlist', 1)?->matches(['ecmd']) ?? false);
27        self::assertTrue($plan->patternAt('ecmd', 0)?->matches(['cmdx', 'SEMI']) ?? false);
28        self::assertTrue($plan->patternAt('ecmd', 1)?->matches(['cmdx', 'SEMI']) ?? false);
29        self::assertTrue($plan->patternAt('cmd', 0)?->matches(['insert_cmd']) ?? false);
30        self::assertTrue($plan->patternAt('cmd', 1)?->matches(['DELETE']) ?? false);
31        self::assertTrue($plan->patternAt('insert_cmd', 0)?->matches(['INSERT']) ?? false);
32        self::assertTrue($plan->patternAt('insert_cmd', 1)?->matches(['INSERT']) ?? false);
33        self::assertTrue($plan->patternAt('with', 0)?->matches([]) ?? false);
34        self::assertTrue($plan->patternAt('with', 1)?->matches([]) ?? false);
35    }
36
37
38
39    public function testFullTextSearchStatementRequiresMatchGrammar(): void
40    {
41        $plan = GenerationPlans::fullTextSearchStatement();
42
43        self::assertSame('select', $plan->startRule());
44        self::assertTrue($plan->patternAt('select', 0)?->matches(['selectnowith']) ?? false);
45        self::assertTrue(
46            $plan->patternAt('oneselect', 0)?->matches(['SELECT', 'selcollist', 'from', 'where_opt']) ?? false,
47        );
48        self::assertTrue($plan->patternAt('selcollist', 0)?->matches(['sclp', 'scanpt', 'STAR']) ?? false);
49        self::assertTrue($plan->patternAt('sclp', 0)?->matches([]) ?? false);
50        self::assertTrue($plan->patternAt('from', 0)?->matches(['seltablist']) ?? false);
51        self::assertTrue(
52            $plan->patternAt('seltablist', 0)?->matches(['stl_prefix', 'nm', 'dbnm', 'as', 'on_using']) ?? false,
53        );
54        self::assertTrue($plan->patternAt('stl_prefix', 0)?->matches([]) ?? false);
55        self::assertTrue($plan->patternAt('on_using', 0)?->matches([]) ?? false);
56        self::assertTrue($plan->patternAt('where_opt', 0)?->matches(['expr']) ?? false);
57        self::assertTrue($plan->patternAt('expr', 0)?->matches(['expr', 'likeop', 'expr']) ?? false);
58        self::assertTrue($plan->patternAt('expr', 1)?->matches(['term']) ?? false);
59        self::assertTrue($plan->patternAt('expr', 2)?->matches(['term']) ?? false);
60        self::assertTrue($plan->patternAt('likeop', 0)?->matches(['MATCH']) ?? false);
61    }
62
63    public function testForeignKeyConstraintRestrictsTheTableConstraintGrammar(): void
64    {
65        $plan = GenerationPlans::foreignKeyConstraint();
66
67        self::assertSame('conslist', $plan->startRule());
68        self::assertTrue(
69            $plan->patternAt('conslist', 0)?->matches(['conslist', 'tconscomma', 'tcons']) ?? false,
70        );
71        self::assertTrue($plan->patternAt('conslist', 1)?->matches(['tcons']) ?? false);
72        self::assertTrue($plan->patternAt('tcons', 0)?->matches(['CONSTRAINT']) ?? false);
73        self::assertTrue($plan->patternAt('tcons', 1)?->matches(['FOREIGN', 'KEY']) ?? false);
74        self::assertTrue($plan->patternAt('tconscomma', 0)?->matches([]) ?? false);
75        self::assertTrue($plan->patternAt('eidlist_opt', 0)?->matches(['column']) ?? false);
76    }
77
78    public function testInsertFunctionUpsertStatementRestrictsTheConflictFunctionGrammar(): void
79    {
80        $plan = GenerationPlans::insertFunctionUpsertStatement();
81
82        self::assertSame('cmd', $plan->startRule());
83        self::assertTrue($plan->patternAt('cmd', 0)?->matches(['insert_cmd', 'select', 'upsert']) ?? false);
84        self::assertTrue($plan->patternAt('with', 0)?->matches([]) ?? false);
85        self::assertTrue($plan->patternAt('insert_cmd', 0)?->matches(['INSERT']) ?? false);
86        self::assertTrue($plan->patternAt('select', 0)?->matches(['selectnowith']) ?? false);
87        self::assertTrue($plan->patternAt('selectnowith', 0)?->matches(['oneselect']) ?? false);
88        self::assertTrue($plan->patternAt('oneselect', 0)?->matches(['values']) ?? false);
89        self::assertTrue(
90            $plan->patternAt('values', 0)?->matches(['VALUES', 'LP', 'nexprlist', 'RP']) ?? false,
91        );
92        self::assertTrue($plan->patternAt('nexprlist', 0)?->matches(['expr']) ?? false);
93        self::assertTrue(
94            $plan->patternAt('upsert', 0)?->matches([
95                'ON',
96                'CONFLICT',
97                'DO',
98                'UPDATE',
99                'SET',
100                'setlist',
101                'where_opt',
102                'returning',
103            ]) ?? false,
104        );
105        self::assertTrue($plan->patternAt('expr', 0)?->matches(['term']) ?? false);
106        self::assertTrue($plan->patternAt('expr', 1)?->matches(['idj', 'LP', 'RP']) ?? false);
107    }
108
109    public function testTemporaryTableStatementRequiresTheTemporaryProduction(): void
110    {
111        $plan = GenerationPlans::temporaryTableStatement();
112
113        self::assertSame('cmd', $plan->startRule());
114        self::assertTrue($plan->patternAt('cmd', 0)?->matches(['create_table', 'create_table_args']) ?? false);
115        self::assertTrue($plan->patternAt('temp', 0)?->matches(['TEMP']) ?? false);
116    }
117
118    public function testViewStatementRestrictsTheCreateGrammar(): void
119    {
120        $plan = GenerationPlans::viewStatement();
121
122        self::assertSame('cmd', $plan->startRule());
123        self::assertTrue($plan->patternAt('cmd', 0)?->matches(['createkw', 'VIEW', 'select']) ?? false);
124        self::assertTrue($plan->patternAt('oneselect', 0)?->matches(['SELECT']) ?? false);
125    }
126
127    public function testGeneratedColumnStatementRequiresTheGeneratedConstraint(): void
128    {
129        $plan = GenerationPlans::generatedColumnStatement();
130
131        self::assertSame('cmd', $plan->startRule());
132        self::assertTrue($plan->patternAt('cmd', 0)?->matches(['create_table', 'create_table_args']) ?? false);
133        self::assertTrue($plan->patternAt('create_table_args', 0)?->matches(['columnlist']) ?? false);
134        self::assertTrue($plan->patternAt('carglist', 0)?->matches(['carglist', 'ccons']) ?? false);
135        self::assertTrue($plan->patternAt('carglist', 1)?->matches([]) ?? false);
136        self::assertTrue($plan->patternAt('ccons', 0)?->matches(['GENERATED', 'generated']) ?? false);
137    }
138
139    public function testForeignKeyCascadeStatementRequiresBothCascadeActions(): void
140    {
141        $plan = GenerationPlans::foreignKeyCascadeStatement();
142
143        self::assertSame('cmd', $plan->startRule());
144        self::assertTrue($plan->patternAt('cmd', 0)?->matches(['create_table', 'create_table_args']) ?? false);
145        self::assertTrue(
146            $plan->patternAt('create_table_args', 0)?->matches(['columnlist', 'conslist_opt']) ?? false,
147        );
148        self::assertTrue($plan->patternAt('conslist_opt', 0)?->matches(['tcons']) ?? false);
149        self::assertTrue($plan->patternAt('tcons', 0)?->matches(['FOREIGN', 'KEY', 'REFERENCES']) ?? false);
150        self::assertTrue($plan->patternAt('refargs', 0)?->matches(['refargs', 'refarg']) ?? false);
151        self::assertTrue($plan->patternAt('refargs', 1)?->matches(['refargs', 'refarg']) ?? false);
152        self::assertTrue($plan->patternAt('refargs', 2)?->matches([]) ?? false);
153        self::assertTrue($plan->patternAt('refarg', 0)?->matches(['ON', 'DELETE']) ?? false);
154        self::assertTrue($plan->patternAt('refarg', 1)?->matches(['ON', 'UPDATE']) ?? false);
155        self::assertTrue($plan->patternAt('refact', 0)?->matches(['CASCADE']) ?? false);
156        self::assertTrue($plan->patternAt('refact', 1)?->matches(['CASCADE']) ?? false);
157    }
158
159    public function testQuotedIdentifierPlansThatLexeme(): void
160    {
161        $plan = GenerationPlans::quotedIdentifier(1, 2);
162
163        self::assertSame('quoted_identifier', $plan->lexicalTarget());
164        self::assertSame(['minLength' => 1, 'maxLength' => 2], $plan->parameters());
165    }
166
167    public function testStringLiteralPlansThatLexeme(): void
168    {
169        $plan = GenerationPlans::stringLiteral(1, 2);
170
171        self::assertSame('string_literal', $plan->lexicalTarget());
172        self::assertSame(['minLength' => 1, 'maxLength' => 2], $plan->parameters());
173    }
174
175    public function testIntegerLiteralPlansThatLexeme(): void
176    {
177        $plan = GenerationPlans::integerLiteral(1, 2);
178
179        self::assertSame('integer_literal', $plan->lexicalTarget());
180        self::assertSame(['min' => 1, 'max' => 2], $plan->parameters());
181    }
182
183    public function testDecimalLiteralPlansThatLexeme(): void
184    {
185        $plan = GenerationPlans::decimalLiteral(1, 2);
186
187        self::assertSame('decimal_literal', $plan->lexicalTarget());
188        self::assertSame(['precision' => 1, 'scale' => 2], $plan->parameters());
189    }
190
191    public function testStatementBoundsTheWalkAtTheRuleItIsGrownFrom(): void
192    {
193        $plan = GenerationPlans::statement('select_stmt', 12);
194
195        self::assertSame('select_stmt', $plan->startRule());
196        self::assertSame(12, $plan->maxDepth());
197    }
198
199    public function testStatementOfTypePreservesAnExplicitTypeWithoutConsumingRandomness(): void
200    {
201        $faker = \Faker\Factory::create();
202        $faker->seed(17);
203        $plan = GenerationPlans::statementOfType($faker, \SqlFaker\Sqlite\StatementType::Select, 6);
204        $next = $faker->randomNumber();
205        $faker->seed(17);
206
207        self::assertSame($faker->randomNumber(), $next);
208        self::assertSame(\SqlFaker\Sqlite\StatementType::Select->value, $plan->startRule());
209        self::assertSame(6, $plan->maxDepth());
210        self::assertSame(true, $plan->usesStepBudget());
211    }
212
213    public function testStatementOfTypeUsesTheSeededFakerChoiceWhenNoTypeIsSpecified(): void
214    {
215        $faker = \Faker\Factory::create();
216        $faker->seed(17);
217        $plan = GenerationPlans::statementOfType($faker, null, 8);
218        $faker->seed(17);
219        /** @var \SqlFaker\Sqlite\StatementType $expected */
220        $expected = $faker->randomElement(\SqlFaker\Sqlite\StatementType::cases());
221
222        self::assertSame($expected->value, $plan->startRule());
223        self::assertSame(8, $plan->maxDepth());
224    }
225}
226