packages/sql-faker/tests/Unit/MySql/MySqlProviderTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\MySql;
6
7use Faker\Factory;
8use Override;
9use PHPUnit\Framework\Attributes\CoversClass;
10use PHPUnit\Framework\Attributes\DataProvider;
11use PHPUnit\Framework\Attributes\Medium;
12use PHPUnit\Framework\Attributes\UsesClass;
13use PHPUnit\Framework\TestCase;
14use SqlFaker\Generation\Derivation\TerminationAnalyzer;
15use SqlFaker\Generation\Plan\GenerationPlan;
16use SqlFaker\Generation\Plan\LexemeConstraint;
17use SqlFaker\Generation\Plan\ProductionPattern;
18use SqlFaker\Generation\Plan\RulePlan;
19use SqlFaker\Generation\SqlGenerator;
20use SqlFaker\Grammar\Model\Grammar;
21use SqlFaker\Grammar\Model\NonTerminal;
22use SqlFaker\Grammar\Model\Production;
23use SqlFaker\Grammar\Model\ProductionRule;
24use SqlFaker\Grammar\Model\Terminal;
25use SqlFaker\Grammar\Model\TerminalInventory;
26use SqlFaker\Grammar\Resource\SqlVersion;
27use SqlFaker\MySql\Generation\GenerationPlans;
28use SqlFaker\MySql\Generation\LexicalGrammar;
29use SqlFaker\MySql\Generation\StatementRule;
30use SqlFaker\MySql\Generation\Value\LiteralGenerator;
31use SqlFaker\MySql\MySqlProvider;
32
33#[CoversClass(MySqlProvider::class)]
34#[CoversClass(LiteralGenerator::class)]
35#[CoversClass(SqlGenerator::class)]
36#[CoversClass(Grammar::class)]
37#[CoversClass(NonTerminal::class)]
38#[CoversClass(Production::class)]
39#[CoversClass(ProductionRule::class)]
40#[CoversClass(Terminal::class)]
41#[CoversClass(TerminationAnalyzer::class)]
42#[CoversClass(StatementRule::class)]
43#[CoversClass(LexicalGrammar::class)]
44#[UsesClass(GenerationPlan::class)]
45#[UsesClass(GenerationPlans::class)]
46#[UsesClass(ProductionPattern::class)]
47#[UsesClass(SqlVersion::class)]
48#[UsesClass(TerminalInventory::class)]
49#[Medium]
50#[UsesClass(\SqlFaker\MySql\Generation\SqlGeneratorFactory::class)]
51#[UsesClass(\SqlFaker\Generation\Derivation\CompletionCosts::class)]
52#[UsesClass(\SqlFaker\Generation\Derivation\Derivation::class)]
53#[UsesClass(\SqlFaker\Generation\Derivation\DerivationTrace::class)]
54#[UsesClass(\SqlFaker\Generation\Derivation\TerminationCost::class)]
55#[UsesClass(\SqlFaker\Generation\Exception\GenerationException::class)]
56#[UsesClass(\SqlFaker\Generation\Candidate\ChoiceLexemeGenerator::class)]
57#[UsesClass(\SqlFaker\Generation\Candidate\FixedLexemeGenerator::class)]
58#[UsesClass(\SqlFaker\Generation\Candidate\IntegerLexemeGenerator::class)]
59#[UsesClass(\SqlFaker\Generation\Lexeme\Lexeme::class)]
60#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeCandidates::class)]
61#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeInput::class)]
62#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeSequence::class)]
63#[UsesClass(\SqlFaker\Generation\Candidate\MatchingLexemeGenerator::class)]
64#[UsesClass(\SqlFaker\Generation\Candidate\ValueLexemeGenerator::class)]
65#[UsesClass(\SqlFaker\Generation\Candidate\RegisteredLexemeGenerator::class)]
66#[UsesClass(\SqlFaker\Generation\Candidate\SequenceLexemeGenerator::class)]
67#[UsesClass(\SqlFaker\Generation\Output\CandidateResolver::class)]
68#[UsesClass(\SqlFaker\Generation\Lexeme\OutputPart::class)]
69#[UsesClass(\SqlFaker\Generation\Lexeme\ResolvedOutput::class)]
70#[UsesClass(\SqlFaker\Generation\Output\ReverseLexemeGenerator::class)]
71#[UsesClass(\SqlFaker\Generation\Output\SqlSerializer::class)]
72#[UsesClass(\SqlFaker\Generation\Output\CombinedSpacingRule::class)]
73#[UsesClass(\SqlFaker\MySql\Generation\Spacing\KeywordPhraseSpacingRule::class)]
74#[UsesClass(\SqlFaker\Generation\Lexeme\LexemeBoundary::class)]
75#[UsesClass(\SqlFaker\Generation\Lexeme\SpacingConstraint::class)]
76#[UsesClass(\SqlFaker\Generation\Token\ProductionOccurrence::class)]
77#[UsesClass(\SqlFaker\Generation\Token\TerminalMappingRule::class)]
78#[UsesClass(\SqlFaker\Generation\Token\TerminalOccurrence::class)]
79#[UsesClass(\SqlFaker\Generation\Token\TerminalSequence::class)]
80#[UsesClass(\SqlFaker\Generation\Derivation\TokenGenerator::class)]
81#[UsesClass(\SqlFaker\Generation\Token\TokenRewriter::class)]
82#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Option\UniqueOptionRule::class)]
83#[UsesClass(\SqlFaker\Generation\Candidate\VersionCase::class)]
84#[UsesClass(\SqlFaker\Generation\Candidate\VersionedLexemeGenerator::class)]
85#[UsesClass(\SqlFaker\Generation\Exception\LexicalException::class)]
86#[UsesClass(\SqlFaker\MySql\Generation\Tokenization\KeywordIndex::class)]
87#[UsesClass(\SqlFaker\Generation\Value\RandomCharacters::class)]
88#[UsesClass(\SqlFaker\Grammar\Resource\SqlVersionRegistry::class)]
89#[UsesClass(\SqlFaker\MySql\Generation\GenerationContext::class)]
90#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\DefinitionFactory::class)]
91#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\KeywordLexemeGenerator::class)]
92#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\AlterDatabaseRule::class)]
93#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\ConstraintEnforcementRule::class)]
94#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\FlushExportRule::class)]
95#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\InstanceActionRule::class)]
96#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\IntegerContextRule::class)]
97#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\LoadSourceCountRule::class)]
98#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\RequiredAliasRule::class)]
99#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\RewriteDefinitions::class)]
100#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\RoleGrantRule::class)]
101#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\SetNamesRule::class)]
102#[UsesClass(\SqlFaker\MySql\Generation\Spacing\CloneAddressSpacingRule::class)]
103#[UsesClass(\SqlFaker\MySql\Generation\Spacing\FunctionSpacingRule::class)]
104#[UsesClass(\SqlFaker\MySql\Generation\Spacing\QualifiedNameSpacingRule::class)]
105#[UsesClass(\SqlFaker\MySql\Generation\Spacing\VariableSpacingRule::class)]
106#[UsesClass(\SqlFaker\MySql\Grammar\MySqlGrammar::class)]
107#[UsesClass(\SqlFaker\MySql\Generation\Tokenization\MySqlTokenizer::class)]
108#[UsesClass(\SqlFaker\MySql\Generation\StartRuleResolver::class)]
109#[UsesClass(\SqlFaker\PostgreSql\Generation\GenerationContext::class)]
110#[UsesClass(\SqlFaker\PostgreSql\Generation\Lexeme\DefinitionFactory::class)]
111#[UsesClass(\SqlFaker\PostgreSql\Generation\Lexeme\HashBoundLexemeGenerator::class)]
112#[UsesClass(\SqlFaker\PostgreSql\Generation\Lexeme\KeywordLexemeGenerator::class)]
113#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\CopySourceRule::class)]
114#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\FetchWithTiesRule::class)]
115#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Name\FunctionNameRule::class)]
116#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\HashPartitionBoundRule::class)]
117#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\LimitOffsetRule::class)]
118#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\LookaheadRule::class)]
119#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\OperatorArgumentsRule::class)]
120#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\OverlapsArgumentsRule::class)]
121#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\PublicationObjectRule::class)]
122#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Name\RelationNameRule::class)]
123#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\RewriteDefinitions::class)]
124#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\TimeZoneIntervalRule::class)]
125#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\WindowFrameRule::class)]
126#[UsesClass(\SqlFaker\PostgreSql\Grammar\PgGrammar::class)]
127#[UsesClass(\SqlFaker\PostgreSql\Generation\LexicalGrammar::class)]
128#[UsesClass(\SqlFaker\PostgreSql\Generation\Lookahead\PgLookahead::class)]
129#[UsesClass(\SqlFaker\PostgreSql\Generation\Tokenization\PgTokenizer::class)]
130#[UsesClass(\SqlFaker\Sqlite\Generation\GenerationContext::class)]
131#[UsesClass(\SqlFaker\Sqlite\Generation\Lexeme\DefinitionFactory::class)]
132#[UsesClass(\SqlFaker\Sqlite\Generation\Lexeme\JoinLexemeGenerator::class)]
133#[UsesClass(\SqlFaker\Sqlite\Generation\Lexeme\JoinModifiers::class)]
134#[UsesClass(\SqlFaker\Sqlite\Generation\Rewrite\IdentifierListRule::class)]
135#[UsesClass(\SqlFaker\Sqlite\Generation\Rewrite\JoinRule::class)]
136#[UsesClass(\SqlFaker\Sqlite\Generation\Rewrite\RewriteDefinitions::class)]
137#[UsesClass(\SqlFaker\Sqlite\Generation\Rewrite\StrictTableRule::class)]
138#[UsesClass(\SqlFaker\Sqlite\Generation\Rewrite\TableOptionRule::class)]
139#[UsesClass(\SqlFaker\Sqlite\Generation\Rewrite\WindowFrameRule::class)]
140#[UsesClass(\SqlFaker\Sqlite\Generation\Rewrite\WithoutRowidRule::class)]
141#[UsesClass(\SqlFaker\Sqlite\Generation\GrammarAdaptation::class)]
142#[UsesClass(\SqlFaker\Sqlite\Grammar\SqliteGrammar::class)]
143#[UsesClass(\SqlFaker\Sqlite\Generation\LexicalGrammar::class)]
144#[UsesClass(\SqlFaker\Sqlite\Generation\Tokenization\SqliteTokenizer::class)]
145#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Expression\ExpressionGroupingRule::class)]
146#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\AlterEventRule::class)]
147#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\SubqueryContextRule::class)]
148#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\ConstraintAttributesRule::class)]
149#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Name\AnyRelationNameRule::class)]
150#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Name\IndirectionStarRule::class)]
151#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\IntoClauseRule::class)]
152#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\GeneratedColumnRule::class)]
153#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\TransactionCompletionRule::class)]
154#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\GeneratedColumnRule::class)]
155#[UsesClass(\SqlFaker\Sqlite\Generation\Lexeme\WindowNameLexemeGenerator::class)]
156#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Alter\OrderByRule::class)]
157#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\BoundedIntegerLexemeGenerator::class)]
158#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\ReplicationTablePatternLexemeGenerator::class)]
159#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\SizeNumberLexemeGenerator::class)]
160#[UsesClass(\SqlFaker\Sqlite\Generation\Rewrite\CompoundSelectRule::class)]
161#[UsesClass(\SqlFaker\Sqlite\Generation\Rewrite\UpsertSourceRule::class)]
162#[UsesClass(\SqlFaker\Sqlite\Generation\Rewrite\FunctionArgumentRule::class)]
163#[UsesClass(\SqlFaker\Sqlite\Generation\Rewrite\GeneratedColumnRule::class)]
164#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Replication\StartRule::class)]
165#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Query\SelectOptionsRule::class)]
166#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Routine\TableFunctionRule::class)]
167#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Routine\TypeModifierRule::class)]
168#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Column\IdentityOptionRule::class)]
169#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Query\IntoClauseRule::class)]
170#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Partition\FieldListRule::class)]
171#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Replication\TablePatternRule::class)]
172#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Expression\QuantifiedComparisonRule::class)]
173#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Expression\TableValueConstructorRule::class)]
174#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Query\QueryContextRule::class)]
175#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Query\JoinGroupingRule::class)]
176#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Query\LegacyDerivedTableRule::class)]
177#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Routine\WithinGroupRule::class)]
178#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Column\FieldLengthRule::class)]
179#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Routine\ReturnRule::class)]
180#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Routine\BinlogRule::class)]
181#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Name\ColumnNameRule::class)]
182#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Name\SystemVariableRule::class)]
183#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Column\AutoIncrementRule::class)]
184#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Partition\DefinitionRule::class)]
185#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Partition\ListValueRule::class)]
186#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Routine\LanguageRule::class)]
187#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\FactorLexemeGenerator::class)]
188#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\PrecisionLexemeGenerator::class)]
189#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Name\AliasRule::class)]
190#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Partition\ValueArityRule::class)]
191#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Partition\ValueShape::class)]
192#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Routine\SubstringRule::class)]
193#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Routine\RangeFunctionOrdinalityRule::class)]
194#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Query\WindowFrameRule::class)]
195#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Routine\JsonOptionsRule::class)]
196#[UsesClass(\SqlFaker\Generation\Value\CharacterDomain::class)]
197#[UsesClass(\SqlFaker\Generation\Value\ChoiceDomain::class)]
198#[UsesClass(\SqlFaker\Generation\Value\IntegerDomain::class)]
199#[UsesClass(\SqlFaker\Generation\Value\SequenceDomain::class)]
200#[UsesClass(\SqlFaker\MySql\Generation\Value\DollarQuotedDomain::class)]
201#[UsesClass(\SqlFaker\Generation\Output\BoundaryCompletion::class)]
202#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Column\ConstraintCapabilitiesRule::class)]
203#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Column\ForeignKeyActionRule::class)]
204#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Query\SchemaElementsRule::class)]
205#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Query\ParserOptionsRule::class)]
206#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Routine\JsonTablePathRule::class)]
207#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Routine\AggregateArgumentRule::class)]
208#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Expression\ConcatenationRule::class)]
209#[UsesClass(\SqlFaker\Generation\Derivation\CompletionState::class)]
210#[UsesClass(\SqlFaker\Generation\Derivation\CompletionFrontier::class)]
211#[UsesClass(\SqlFaker\Generation\Derivation\ConstrainedCompletion::class)]
212#[UsesClass(\SqlFaker\Generation\Value\ValueChoices::class)]
213#[UsesClass(\SqlFaker\Generation\Derivation\CompletionMemo::class)]
214#[UsesClass(\SqlFaker\Generation\Derivation\CompletionReduction::class)]
215#[UsesClass(\SqlFaker\Generation\Derivation\ConstraintDependencies::class)]
216#[UsesClass(\SqlFaker\Generation\Choice\BytePlanCompiler::class)]
217#[UsesClass(\SqlFaker\MySql\Generation\Rewrite\Name\HostNameRule::class)]
218#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Name\ParserNameRule::class)]
219#[UsesClass(\SqlFaker\PostgreSql\Generation\Rewrite\Column\NumericContextRule::class)]
220#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\CharsetLexemeGenerator::class)]
221#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\CharsetValueLexemeGenerator::class)]
222#[UsesClass(\SqlFaker\MySql\Generation\Value\IdentifierDomain::class)]
223#[UsesClass(\SqlFaker\MySql\Generation\Value\QuotedDomain::class)]
224#[UsesClass(\SqlFaker\MySql\Generation\Value\RadixDomain::class)]
225#[UsesClass(\SqlFaker\Generation\Value\Utf8::class)]
226#[UsesClass(\SqlFaker\Generation\Value\WordDomain::class)]
227#[UsesClass(\SqlFaker\Generation\Derivation\Completion\PatternProductions::class)]
228#[UsesClass(\SqlFaker\Generation\Derivation\Completion\CompletionWitness::class)]
229#[UsesClass(\SqlFaker\PostgreSql\Generation\Value\OperatorDomain::class)]
230#[UsesClass(\SqlFaker\Generation\Value\RepeatDomain::class)]
231#[UsesClass(\SqlFaker\MySql\Generation\Lexeme\LexicalDefinition::class)]
232#[UsesClass(RulePlan::class)]
233#[UsesClass(LexemeConstraint::class)]
234#[UsesClass(\SqlFaker\Generation\Plan\Compilation\GrammarCompiler::class)]
235#[UsesClass(\SqlFaker\Generation\Plan\Compilation\Scope::class)]
236#[UsesClass(\SqlFaker\Generation\Plan\Compilation\PreparedGrammar::class)]
237#[UsesClass(\SqlFaker\Generation\Plan\Compilation\ScopedGeneration::class)]
238final class MySqlProviderTest extends TestCase
239{
240    #[Override]
241    protected function setUp(): void
242    {
243        parent::setUp();
244        gc_collect_cycles();
245    }
246
247    public function testRegistersItselfWithTheFakerGenerator(): void
248    {
249        $faker = Factory::create();
250        $provider = new MySqlProvider($faker);
251
252        /**
253         * @var list<object> $providers
254         */
255        $providers = $faker->getProviders();
256        self::assertContains($provider, $providers);
257
258        $identifier = $provider->identifier(3);
259        self::assertNotSame('', $identifier);
260    }
261
262    #[DataProvider('providerTargetedGenerationSeed')]
263    public function testInsertFunctionUpsertStatementDerivesConditionalExpressionFromGrammar(int $seed): void
264    {
265        $faker = Factory::create();
266        $provider = new MySqlProvider($faker);
267        $faker->seed($seed);
268        $sql = $provider->insertFunctionUpsertStatement();
269
270        $tokens = (new LexicalGrammar($faker, 'mysql-8.4.7'))
271            ->tokenize($sql);
272        $valueTokens = array_intersect($tokens, ['VALUE_SYM', 'VALUES']);
273        $values = array_key_first($valueTokens);
274        $update = array_search('UPDATE_SYM', $tokens, true);
275        $conditional = array_search('IF', $tokens, true);
276
277        $faker->seed($seed);
278        self::assertSame($sql, $provider->insertFunctionUpsertStatement(40));
279        self::assertIsInt($values);
280        self::assertIsInt($update);
281        self::assertIsInt($conditional);
282        self::assertLessThan($update, $values);
283        self::assertGreaterThan($update, $conditional);
284        self::assertContains('DUPLICATE_SYM', $tokens);
285    }
286
287    #[DataProvider('providerTargetedGenerationSeed')]
288    public function testFullTextSearchStatementDerivesMatchExpressionFromGrammar(int $seed): void
289    {
290        $faker = Factory::create();
291        $faker->seed($seed);
292        $provider = new MySqlProvider($faker);
293        $sql = $provider->fullTextSearchStatement();
294        $faker->seed($seed);
295        $tokens = (new LexicalGrammar($faker, 'mysql-8.4.7'))
296            ->tokenize($sql);
297        $select = array_search('SELECT_SYM', $tokens, true);
298        $from = array_search('FROM', $tokens, true);
299        $where = array_search('WHERE', $tokens, true);
300        $match = array_search('MATCH', $tokens, true);
301
302        self::assertSame($sql, $provider->fullTextSearchStatement(40));
303        self::assertIsInt($select);
304        self::assertIsInt($from);
305        self::assertNotContains('DUAL_SYM', $tokens);
306        self::assertIsInt($where);
307        self::assertIsInt($match);
308        self::assertGreaterThan($select, $from);
309        self::assertGreaterThan($from, $match);
310        self::assertContains('AGAINST', $tokens);
311    }
312
313    #[DataProvider('providerTargetedGenerationSeed')]
314    public function testTemporaryTableStatement(int $seed): void
315    {
316        $faker = Factory::create();
317        $faker->seed($seed);
318        $provider = new MySqlProvider($faker);
319        $sql = $provider->temporaryTableStatement();
320        $faker->seed($seed);
321
322        $tokens = (new LexicalGrammar($faker, 'mysql-8.4.7'))
323            ->tokenize($sql);
324
325        self::assertSame($sql, $provider->temporaryTableStatement(40));
326        self::assertSame('CREATE', $tokens[0]);
327        self::assertContains('TEMPORARY', $tokens);
328        self::assertContains('TABLE_SYM', $tokens);
329    }
330
331    #[DataProvider('providerTargetedGenerationSeed')]
332    public function testViewStatement(int $seed): void
333    {
334        $faker = Factory::create();
335        $faker->seed($seed);
336        $provider = new MySqlProvider($faker);
337        $sql = $provider->viewStatement();
338        $faker->seed($seed);
339        $tokens = (new LexicalGrammar($faker, 'mysql-8.4.7'))
340            ->tokenize($sql);
341
342        self::assertSame($sql, $provider->viewStatement(40));
343        self::assertSame('CREATE', $tokens[0]);
344        self::assertContains('VIEW_SYM', $tokens);
345        self::assertContains('SELECT_SYM', $tokens);
346    }
347
348    #[DataProvider('providerTargetedGenerationSeed')]
349    public function testGeneratedColumnStatement(int $seed): void
350    {
351        $faker = Factory::create();
352        $faker->seed($seed);
353        $provider = new MySqlProvider($faker);
354        $sql = $provider->generatedColumnStatement();
355        $faker->seed($seed);
356        $tokens = (new LexicalGrammar($faker, 'mysql-8.4.7'))
357            ->tokenize($sql);
358
359        self::assertSame($sql, $provider->generatedColumnStatement(40));
360        self::assertContains('GENERATED', $tokens);
361        self::assertContains('ALWAYS_SYM', $tokens);
362        self::assertContains('STORED_SYM', $tokens);
363    }
364
365    #[DataProvider('providerTargetedGenerationSeed')]
366    public function testForeignKeyCascadeStatement(int $seed): void
367    {
368        $faker = Factory::create();
369        $faker->seed($seed);
370        $provider = new MySqlProvider($faker);
371        $sql = $provider->foreignKeyCascadeStatement();
372        $faker->seed($seed);
373        $tokens = (new LexicalGrammar($faker, 'mysql-8.4.7'))
374            ->tokenize($sql);
375
376        self::assertSame($sql, $provider->foreignKeyCascadeStatement(40));
377        self::assertContains('FOREIGN', $tokens);
378        self::assertContains('REFERENCES', $tokens);
379        self::assertStringContainsString('ON_SYM UPDATE_SYM CASCADE', implode(' ', $tokens));
380        self::assertStringContainsString('ON_SYM DELETE_SYM CASCADE', implode(' ', $tokens));
381    }
382
383    #[DataProvider('providerTargetedGenerationSeed')]
384    public function testPartitionSelectStatement(int $seed): void
385    {
386        $faker = Factory::create();
387        $faker->seed($seed);
388        $provider = new MySqlProvider($faker);
389        $sql = $provider->partitionSelectStatement();
390        $faker->seed($seed);
391        $tokens = (new LexicalGrammar($faker, 'mysql-8.4.7'))
392            ->tokenize($sql);
393        $select = array_search('SELECT_SYM', $tokens, true);
394        $from = array_search('FROM', $tokens, true);
395        $partition = array_search('PARTITION_SYM', $tokens, true);
396
397        self::assertSame($sql, $provider->partitionSelectStatement(40));
398        self::assertIsInt($select);
399        self::assertIsInt($from);
400        self::assertIsInt($partition);
401        self::assertGreaterThan($select, $from);
402        self::assertGreaterThan($from, $partition);
403    }
404
405    public function testSql(): void
406    {
407        $faker = Factory::create();
408        $faker->seed(12345);
409        $provider = new MySqlProvider($faker);
410
411        $result = $provider->sql(maxDepth: 10);
412
413        self::assertNotSame('', $result);
414    }
415
416    public function testSqlWithStatementRule(): void
417    {
418        $faker = Factory::create();
419        $faker->seed(12345);
420        $provider = new MySqlProvider($faker);
421
422        $result = $provider->sql(StatementRule::Select, maxDepth: 3);
423
424        self::assertMatchesRegularExpression('/\bSELECT\b/i', $result);
425    }
426
427    public function testSqlWithNullStatementRuleUsesDefault(): void
428    {
429        $faker = Factory::create();
430        $faker->seed(12345);
431        $provider = new MySqlProvider($faker);
432
433        $result = $provider->sql(null, maxDepth: 3);
434
435        self::assertNotSame('', $result);
436    }
437
438    public function testSqlWithMaxDepth(): void
439    {
440        $faker = Factory::create();
441        $faker->seed(12345);
442        $provider = new MySqlProvider($faker);
443
444        $result = $provider->sql(maxDepth: 5);
445
446        self::assertNotSame('', $result);
447    }
448
449    #[DataProvider('providerSupportedMySqlVersion')]
450    public function testSqlWithoutEmptyRowsUsesTheRestrictedGenerationPlan(string $version): void
451    {
452        $faker = Factory::create();
453        $faker->seed(12345);
454        $provider = new MySqlProvider($faker, $version);
455
456        $result = $provider->sqlWithoutEmptyRows(StatementRule::Insert, maxDepth: 10);
457
458        self::assertMatchesRegularExpression('/\bINSERT\b/i', $result);
459        self::assertDoesNotMatchRegularExpression('/\bVALUES?\s*(?:ROW\s*)?\(\s*\)/i', $result);
460    }
461
462    public function testSqlWithoutEmptyRowsCanGenerateFromTheWholeGrammar(): void
463    {
464        $faker = Factory::create();
465        $faker->seed(12345);
466        $provider = new MySqlProvider($faker);
467
468        $result = $provider->sqlWithoutEmptyRows(maxDepth: 3);
469
470        self::assertNotSame('', $result);
471    }
472
473    public function testLoadDataStatementUsesOfficialGrammarRule(): void
474    {
475        $faker = Factory::create();
476        $faker->seed(164);
477        $provider = new MySqlProvider($faker);
478
479        $result = $provider->loadDataStatement(maxDepth: 8);
480
481        self::assertMatchesRegularExpression('/\bLOAD\b.*\bDATA\b/is', $result);
482        self::assertMatchesRegularExpression('/\bINFILE\b/i', $result);
483        self::assertMatchesRegularExpression('/\bINTO\b/i', $result);
484    }
485
486    public function testSelectStatement(): void
487    {
488        $faker = Factory::create();
489        $faker->seed(12345);
490        $provider = new MySqlProvider($faker);
491
492        $result = $provider->selectStatement(maxDepth: 3);
493
494        self::assertMatchesRegularExpression('/\bSELECT\b/i', $result);
495    }
496
497    public function testInsertStatement(): void
498    {
499        $faker = Factory::create();
500        $faker->seed(12345);
501        $provider = new MySqlProvider($faker);
502
503        $result = $provider->insertStatement(maxDepth: 3);
504
505        self::assertMatchesRegularExpression('/\bINSERT\b/i', $result);
506    }
507
508    public function testUpdateStatement(): void
509    {
510        $faker = Factory::create();
511        $faker->seed(12345);
512        $provider = new MySqlProvider($faker);
513
514        $result = $provider->updateStatement(maxDepth: 3);
515
516        self::assertMatchesRegularExpression('/\bUPDATE\b/i', $result);
517    }
518
519    public function testDeleteStatement(): void
520    {
521        $faker = Factory::create();
522        $faker->seed(12345);
523        $provider = new MySqlProvider($faker);
524
525        $result = $provider->deleteStatement(maxDepth: 3);
526
527        self::assertMatchesRegularExpression('/\bDELETE\b/i', $result);
528    }
529
530    #[DataProvider('providerMultiTableGenerationSeed')]
531    public function testMultiTableUpdateStatement(int $seed): void
532    {
533        $faker = Factory::create();
534        $faker->seed($seed);
535        $provider = new MySqlProvider($faker);
536
537        $tokens = (new LexicalGrammar($faker, 'mysql-8.4.7'))
538            ->tokenize($provider->multiTableUpdateStatement(maxDepth: 20));
539
540        self::assertSame('UPDATE_SYM', $tokens[0]);
541        $set = array_search('SET_SYM', $tokens, true);
542        self::assertIsInt($set);
543        self::assertSame(1, count(array_filter(
544            array_slice($tokens, 0, $set),
545            static fn (string $token): bool => $token === ',',
546        )));
547        self::assertContains('SET_SYM', $tokens);
548    }
549
550    #[DataProvider('providerMultiTableGenerationSeed')]
551    public function testMultiTableDeleteStatement(int $seed): void
552    {
553        $faker = Factory::create();
554        $faker->seed($seed);
555        $provider = new MySqlProvider($faker);
556
557        $tokens = (new LexicalGrammar($faker, 'mysql-8.4.7'))
558            ->tokenize($provider->multiTableDeleteStatement(maxDepth: 20));
559
560        self::assertSame('DELETE_SYM', $tokens[0]);
561        self::assertContains('FROM', $tokens);
562        self::assertGreaterThanOrEqual(2, count(array_filter(
563            $tokens,
564            static fn (string $token): bool => in_array($token, ['IDENT', 'IDENT_QUOTED'], true),
565        )));
566    }
567
568    public function testCreateTableStatement(): void
569    {
570        $faker = Factory::create();
571        $faker->seed(12345);
572        $provider = new MySqlProvider($faker);
573
574        $result = $provider->createTableStatement(maxDepth: 3);
575
576        self::assertMatchesRegularExpression('/\bCREATE\b/i', $result);
577        self::assertMatchesRegularExpression('/\bTABLE\b/i', $result);
578    }
579
580    public function testAlterTableStatement(): void
581    {
582        $faker = Factory::create();
583        $faker->seed(12345);
584        $provider = new MySqlProvider($faker);
585
586        $result = $provider->alterTableStatement(maxDepth: 3);
587
588        self::assertMatchesRegularExpression('/\bALTER\b/i', $result);
589        self::assertMatchesRegularExpression('/\bTABLE\b/i', $result);
590    }
591
592    public function testDropTableStatement(): void
593    {
594        $faker = Factory::create();
595        $faker->seed(12345);
596        $provider = new MySqlProvider($faker);
597
598        $result = $provider->dropTableStatement(maxDepth: 3);
599
600        self::assertMatchesRegularExpression('/\bDROP\b/i', $result);
601        self::assertMatchesRegularExpression('/\bTABLE\b/i', $result);
602    }
603
604    public function testSimpleStatement(): void
605    {
606        $faker = Factory::create();
607        $faker->seed(12345);
608        $provider = new MySqlProvider($faker);
609
610        $result = $provider->simpleStatement(maxDepth: 3);
611
612        self::assertNotSame('', $result);
613    }
614
615    public function testReplaceStatement(): void
616    {
617        $faker = Factory::create();
618        $faker->seed(12345);
619        $provider = new MySqlProvider($faker);
620
621        $result = $provider->replaceStatement(maxDepth: 3);
622
623        self::assertMatchesRegularExpression('/\bREPLACE\b/i', $result);
624    }
625
626    public function testTruncateStatement(): void
627    {
628        $faker = Factory::create();
629        $faker->seed(12345);
630        $provider = new MySqlProvider($faker);
631
632        $result = $provider->truncateStatement(maxDepth: 3);
633
634        self::assertMatchesRegularExpression('/\bTRUNCATE\b/i', $result);
635    }
636
637    public function testCreateIndexStatement(): void
638    {
639        $faker = Factory::create();
640        $faker->seed(12345);
641        $provider = new MySqlProvider($faker);
642
643        $result = $provider->createIndexStatement(maxDepth: 3);
644
645        self::assertMatchesRegularExpression('/\bCREATE\b/i', $result);
646        self::assertMatchesRegularExpression('/\bINDEX\b/i', $result);
647    }
648
649    public function testDropIndexStatement(): void
650    {
651        $faker = Factory::create();
652        $faker->seed(12345);
653        $provider = new MySqlProvider($faker);
654
655        $result = $provider->dropIndexStatement(maxDepth: 3);
656
657        self::assertMatchesRegularExpression('/\bDROP\b/i', $result);
658        self::assertMatchesRegularExpression('/\bINDEX\b/i', $result);
659    }
660
661    public function testBeginStatement(): void
662    {
663        $faker = Factory::create();
664        $faker->seed(12345);
665        $provider = new MySqlProvider($faker);
666
667        $result = $provider->beginStatement(maxDepth: 3);
668
669        self::assertMatchesRegularExpression('/\bBEGIN\b/i', $result);
670    }
671
672    public function testCommitStatement(): void
673    {
674        $faker = Factory::create();
675        $faker->seed(12345);
676        $provider = new MySqlProvider($faker);
677
678        $result = $provider->commitStatement(maxDepth: 3);
679
680        self::assertMatchesRegularExpression('/\bCOMMIT\b/i', $result);
681    }
682
683    public function testRollbackStatement(): void
684    {
685        $faker = Factory::create();
686        $faker->seed(12345);
687        $provider = new MySqlProvider($faker);
688
689        $result = $provider->rollbackStatement(maxDepth: 3);
690
691        self::assertMatchesRegularExpression('/\bROLLBACK\b/i', $result);
692    }
693
694    public function testIdentifier(): void
695    {
696        $faker = Factory::create();
697        $faker->seed(12345);
698        $provider = new MySqlProvider($faker);
699
700        $result = $provider->identifier(3);
701
702        self::assertNotSame('', $result);
703    }
704
705    public function testQuotedIdentifier(): void
706    {
707        $faker = Factory::create();
708        $faker->seed(12345);
709        $provider = new MySqlProvider($faker);
710
711        $result = $provider->quotedIdentifier();
712
713        self::assertMatchesRegularExpression('/^`[a-z_][a-z0-9_]*`$/', $result);
714    }
715
716    public function testStringLiteral(): void
717    {
718        $faker = Factory::create();
719        $faker->seed(12345);
720        $provider = new MySqlProvider($faker);
721
722        $result = $provider->stringLiteral();
723
724        self::assertMatchesRegularExpression("/^'[a-zA-Z0-9_]{1,255}'$/", $result);
725    }
726
727    public function testStringLiteralLengthRange(): void
728    {
729        $faker = Factory::create();
730        $faker->seed(12345);
731        $provider = new MySqlProvider($faker);
732
733        $literal = $provider->stringLiteral();
734        $content = substr($literal, 1, -1);
735
736        self::assertGreaterThanOrEqual(1, strlen($content));
737        self::assertLessThanOrEqual(255, strlen($content));
738    }
739
740    public function testNationalStringLiteral(): void
741    {
742        $faker = Factory::create();
743        $faker->seed(12345);
744        $provider = new MySqlProvider($faker);
745
746        $result = $provider->nationalStringLiteral();
747
748        self::assertMatchesRegularExpression("/^N'[a-zA-Z0-9_]{1,255}'$/", $result);
749    }
750
751    public function testDollarQuotedString(): void
752    {
753        $faker = Factory::create();
754        $faker->seed(12345);
755        $provider = new MySqlProvider($faker);
756
757        $result = $provider->dollarQuotedString();
758
759        self::assertMatchesRegularExpression('/^\$\$[a-zA-Z0-9_]{1,255}\$\$$/', $result);
760    }
761
762    public function testIntegerLiteral(): void
763    {
764        $faker = Factory::create();
765        $faker->seed(12345);
766        $provider = new MySqlProvider($faker);
767
768        $result = $provider->integerLiteral();
769
770        self::assertMatchesRegularExpression('/^[1-9]\d*$/', $result);
771    }
772
773    public function testLongIntegerLiteral(): void
774    {
775        $faker = Factory::create();
776        $faker->seed(12345);
777        $provider = new MySqlProvider($faker);
778
779        $result = $provider->longIntegerLiteral();
780
781        self::assertMatchesRegularExpression('/^\d+$/', $result);
782        self::assertGreaterThanOrEqual(0, (int) $result);
783        self::assertLessThanOrEqual(2147483647, (int) $result);
784    }
785
786    public function testUnsignedBigIntLiteral(): void
787    {
788        $faker = Factory::create();
789        $faker->seed(12345);
790        $provider = new MySqlProvider($faker);
791
792        $result = $provider->unsignedBigIntLiteral();
793
794        self::assertMatchesRegularExpression('/^\d+$/', $result);
795    }
796
797    public function testDecimalLiteral(): void
798    {
799        $faker = Factory::create();
800        $faker->seed(12345);
801        $provider = new MySqlProvider($faker);
802
803        $result = $provider->decimalLiteral();
804
805        self::assertMatchesRegularExpression('/^\d+\.\d{2,}$/', $result);
806    }
807
808    public function testFloatLiteral(): void
809    {
810        $faker = Factory::create();
811        $faker->seed(12345);
812        $provider = new MySqlProvider($faker);
813
814        $result = $provider->floatLiteral();
815
816        self::assertMatchesRegularExpression('/^\d+\.\d+e-?\d+$/', $result);
817    }
818
819    public function testHexLiteral(): void
820    {
821        $faker = Factory::create();
822        $faker->seed(12345);
823        $provider = new MySqlProvider($faker);
824
825        $result = $provider->hexLiteral();
826
827        self::assertMatchesRegularExpression('/^0x[0-9a-f]{1,16}$/', $result);
828    }
829
830    public function testQuotedHexLiteral(): void
831    {
832        $faker = Factory::create();
833        $faker->seed(12345);
834        $provider = new MySqlProvider($faker);
835
836        $result = $provider->quotedHexLiteral(4, 4);
837
838        self::assertMatchesRegularExpression("/^X'[0-9a-f]{8}'$/", $result);
839    }
840
841    public function testQuotedHexLiteralDefaultsToOneThroughEightBytes(): void
842    {
843        $faker = Factory::create();
844        $faker->seed(5);
845        $provider = new MySqlProvider($faker);
846
847        $result = $provider->quotedHexLiteral();
848        $faker->seed(5);
849
850        self::assertSame($provider->quotedHexLiteral(1, 8), $result);
851        self::assertMatchesRegularExpression("/^X'[0-9a-f]{2,16}'$/", $result);
852        self::assertSame(0, (strlen($result) - 3) % 2);
853    }
854
855    #[DataProvider('providerMySqlVersion')]
856    public function testForeignKeyConstraint(string $version): void
857    {
858        $faker = Factory::create();
859        $faker->seed(12345);
860        $provider = new MySqlProvider($faker, $version);
861
862        $result = $provider->foreignKeyConstraint(1);
863
864        self::assertSame(
865            ['CONSTRAINT', 'IDENT', 'FOREIGN', 'KEY_SYM', '(', 'IDENT', ')', 'REFERENCES', 'IDENT', '(', 'IDENT', ')'],
866            (new LexicalGrammar($faker, $version))->tokenize($result),
867        );
868    }
869
870    #[DataProvider('providerTargetedGenerationSeed')]
871    public function testUpdateJoinDerivedStatement(int $seed): void
872    {
873        $faker = Factory::create();
874        $faker->seed($seed);
875        $provider = new MySqlProvider($faker);
876        $sql = $provider->updateJoinDerivedStatement();
877        $faker->seed($seed);
878
879        $tokens = (new LexicalGrammar($faker, 'mysql-8.4.7'))->tokenize($sql);
880        $joinTokens = array_intersect($tokens, ['JOIN_SYM', 'STRAIGHT_JOIN']);
881        $join = array_key_first($joinTokens);
882        $select = array_search('SELECT_SYM', $tokens, true);
883
884        self::assertSame($sql, $provider->updateJoinDerivedStatement(40));
885        self::assertSame('UPDATE_SYM', $tokens[0]);
886        self::assertIsInt($join);
887        self::assertIsInt($select);
888        self::assertGreaterThan($join, $select);
889        self::assertContains('FROM', $tokens);
890        self::assertContains('GROUP_SYM', $tokens);
891        self::assertContains('ON_SYM', $tokens);
892        self::assertContains('SET_SYM', $tokens);
893    }
894
895    #[DataProvider('providerTargetedGenerationSeed')]
896    public function testInsertSelectCompoundStatement(int $seed): void
897    {
898        $faker = Factory::create();
899        $faker->seed($seed);
900        $provider = new MySqlProvider($faker);
901        $sql = $provider->insertSelectCompoundStatement();
902        $faker->seed($seed);
903
904        $tokens = (new LexicalGrammar($faker, 'mysql-8.4.7'))->tokenize($sql);
905
906        self::assertSame($sql, $provider->insertSelectCompoundStatement(40));
907        self::assertSame('INSERT_SYM', $tokens[0]);
908        self::assertContains('UNION_SYM', $tokens);
909        self::assertContains('ALL', $tokens);
910        self::assertGreaterThanOrEqual(
911            2,
912            count(array_filter($tokens, static fn (string $token): bool => $token === 'SELECT_SYM')),
913        );
914    }
915
916    #[DataProvider('providerTargetedGenerationSeed')]
917    public function testInsertRowAliasUpsertStatement(int $seed): void
918    {
919        $faker = Factory::create();
920        $faker->seed($seed);
921        $provider = new MySqlProvider($faker);
922        $sql = $provider->insertRowAliasUpsertStatement();
923        $faker->seed($seed);
924
925        $tokens = (new LexicalGrammar($faker, 'mysql-8.4.7'))->tokenize($sql);
926
927        self::assertSame($sql, $provider->insertRowAliasUpsertStatement(40));
928        self::assertSame('INSERT_SYM', $tokens[0]);
929        self::assertContains('VALUES', $tokens);
930        self::assertContains('AS', $tokens);
931        self::assertContains('DUPLICATE_SYM', $tokens);
932        self::assertContains('UPDATE_SYM', $tokens);
933    }
934
935    public function testBinaryLiteral(): void
936    {
937        $faker = Factory::create();
938        $faker->seed(12345);
939        $provider = new MySqlProvider($faker);
940
941        $result = $provider->binaryLiteral();
942
943        self::assertMatchesRegularExpression('/^0b[01]{1,64}$/', $result);
944    }
945
946    public function testHostname(): void
947    {
948        $faker = Factory::create();
949        $faker->seed(12345);
950        $provider = new MySqlProvider($faker);
951
952        $result = $provider->hostname();
953
954        self::assertMatchesRegularExpression('/^[a-z0-9]+(\.[a-z0-9]+)*$/', $result);
955    }
956
957    public function testExpr(): void
958    {
959        $faker = Factory::create();
960        $faker->seed(12345);
961        $provider = new MySqlProvider($faker);
962
963        $result = $provider->expr(maxDepth: 3);
964
965        self::assertNotSame('', $result);
966    }
967
968    public function testSimpleExpr(): void
969    {
970        $faker = Factory::create();
971        $faker->seed(12345);
972        $provider = new MySqlProvider($faker);
973
974        $result = $provider->simpleExpr(maxDepth: 3);
975
976        self::assertNotSame('', $result);
977    }
978
979    public function testLiteral(): void
980    {
981        $faker = Factory::create();
982        $faker->seed(12345);
983        $provider = new MySqlProvider($faker);
984
985        $result = $provider->literal(maxDepth: 3);
986
987        self::assertNotSame('', $result);
988    }
989
990    public function testPredicate(): void
991    {
992        $faker = Factory::create();
993        $faker->seed(12345);
994        $provider = new MySqlProvider($faker);
995
996        $result = $provider->predicate(maxDepth: 3);
997
998        self::assertNotSame('', $result);
999    }
1000
1001    public function testWhereClause(): void
1002    {
1003        $faker = Factory::create();
1004        $faker->seed(12345);
1005        $provider = new MySqlProvider($faker);
1006
1007        $result = $provider->whereClause(maxDepth: 3);
1008
1009        self::assertMatchesRegularExpression('/\bWHERE\b/i', $result);
1010    }
1011
1012    public function testOrderClause(): void
1013    {
1014        $faker = Factory::create();
1015        $faker->seed(12345);
1016        $provider = new MySqlProvider($faker);
1017
1018        $result = $provider->orderClause(maxDepth: 3);
1019
1020        self::assertSame(
1021            ['ORDER_SYM', 'BY'],
1022            array_slice((new LexicalGrammar($faker, 'mysql-8.4.7'))->tokenize($result), 0, 2),
1023        );
1024    }
1025
1026    public function testLimitClause(): void
1027    {
1028        $faker = Factory::create();
1029        $faker->seed(12345);
1030        $provider = new MySqlProvider($faker);
1031
1032        $result = $provider->limitClause(maxDepth: 3);
1033
1034        self::assertMatchesRegularExpression('/\bLIMIT\b/i', $result);
1035    }
1036
1037    public function testTableReference(): void
1038    {
1039        $faker = Factory::create();
1040        $faker->seed(12345);
1041        $provider = new MySqlProvider($faker);
1042
1043        $result = $provider->tableReference(maxDepth: 3);
1044
1045        self::assertNotSame('', $result);
1046    }
1047
1048    public function testJoinedTable(): void
1049    {
1050        $faker = Factory::create();
1051        $faker->seed(12345);
1052        $provider = new MySqlProvider($faker);
1053
1054        $result = $provider->joinedTable(maxDepth: 3);
1055
1056        self::assertMatchesRegularExpression('/\bJOIN\b/i', $result);
1057    }
1058
1059    public function testTableIdent(): void
1060    {
1061        $faker = Factory::create();
1062        $faker->seed(12345);
1063        $provider = new MySqlProvider($faker);
1064
1065        $result = $provider->tableIdent(maxDepth: 3);
1066
1067        self::assertNotSame('', $result);
1068    }
1069
1070    public function testSubquery(): void
1071    {
1072        $faker = Factory::create();
1073        $faker->seed(12345);
1074        $provider = new MySqlProvider($faker);
1075
1076        $result = $provider->subquery(maxDepth: 3);
1077
1078        self::assertStringContainsString('(', $result);
1079        self::assertStringContainsString(')', $result);
1080    }
1081
1082    public function testWithClause(): void
1083    {
1084        $faker = Factory::create();
1085        $faker->seed(12345);
1086        $provider = new MySqlProvider($faker);
1087
1088        $result = $provider->withClause(maxDepth: 3);
1089
1090        self::assertMatchesRegularExpression('/\bWITH\b/i', $result);
1091    }
1092
1093    public function testSeededGenerationIsReproducible(): void
1094    {
1095        $faker1 = Factory::create();
1096        $provider1 = new MySqlProvider($faker1);
1097        $faker1->seed(99999);
1098        $sql1 = $provider1->sql(maxDepth: 6);
1099
1100        $faker2 = Factory::create();
1101        $provider2 = new MySqlProvider($faker2);
1102        $faker2->seed(99999);
1103        $sql2 = $provider2->sql(maxDepth: 6);
1104
1105        self::assertSame($sql1, $sql2, 'Same seed should produce same output');
1106    }
1107
1108    public function testCanBeUsedViaFakerMagicMethod(): void
1109    {
1110        $faker = Factory::create();
1111        $faker->seed(12345);
1112        new MySqlProvider($faker);
1113
1114        $sql = $faker->format('sql', [null, 10]);
1115        self::assertIsString($sql);
1116        self::assertNotSame('', $sql);
1117    }
1118
1119    #[DataProvider('providerStatementRuleValue')]
1120    public function testSqlWithAllStatementRules(StatementRule $type): void
1121    {
1122        $faker = Factory::create();
1123        $faker->seed(12345);
1124        $provider = new MySqlProvider($faker);
1125
1126        $result = $provider->sql($type, maxDepth: 3);
1127
1128        self::assertNotSame('', $result);
1129    }
1130
1131    public function testDefaultMaxDepthIsPhpIntMax(): void
1132    {
1133        $faker = Factory::create();
1134        $faker->seed(12345);
1135        $provider = new MySqlProvider($faker);
1136
1137        $result = $provider->sql(maxDepth: 10);
1138
1139        self::assertNotSame('', $result);
1140    }
1141
1142    public function testSelectStatementMinimalDepthProducesOutput(): void
1143    {
1144        $faker = Factory::create();
1145        $faker->seed(12345);
1146        $provider = new MySqlProvider($faker);
1147
1148        $result = $provider->selectStatement(maxDepth: 1);
1149
1150        self::assertMatchesRegularExpression('/\bSELECT\b/i', $result);
1151    }
1152
1153    #[DataProvider('providerMultipleGenerationSeeds')]
1154    public function testMultipleGenerationsReturnDifferentResults(int $seed1, int $seed2): void
1155    {
1156        $faker1 = Factory::create();
1157        $faker1->seed($seed1);
1158        $provider1 = new MySqlProvider($faker1);
1159
1160        $faker2 = Factory::create();
1161        $faker2->seed($seed2);
1162        $provider2 = new MySqlProvider($faker2);
1163
1164        self::assertNotSame($provider1->selectStatement(maxDepth: 3), $provider2->selectStatement(maxDepth: 3));
1165    }
1166
1167    public function testQuotedIdentifierDefaultMatchesExplicit(): void
1168    {
1169        $faker = Factory::create();
1170        $p = new MySqlProvider($faker);
1171        $faker->seed(42);
1172        $a = $p->quotedIdentifier();
1173        $faker->seed(42);
1174        self::assertSame($a, $p->quotedIdentifier(1, 64));
1175    }
1176
1177    public function testStringLiteralDefaultMatchesExplicit(): void
1178    {
1179        $faker = Factory::create();
1180        $p = new MySqlProvider($faker);
1181        $faker->seed(42);
1182        $a = $p->stringLiteral();
1183        $faker->seed(42);
1184        self::assertSame($a, $p->stringLiteral(1, 255));
1185    }
1186
1187    public function testNationalStringLiteralDefaultMatchesExplicit(): void
1188    {
1189        $faker = Factory::create();
1190        $p = new MySqlProvider($faker);
1191        $faker->seed(42);
1192        $a = $p->nationalStringLiteral();
1193        $faker->seed(42);
1194        self::assertSame($a, $p->nationalStringLiteral(1, 255));
1195    }
1196
1197    public function testDollarQuotedStringDefaultMatchesExplicit(): void
1198    {
1199        $faker = Factory::create();
1200        $p = new MySqlProvider($faker);
1201        $faker->seed(42);
1202        $a = $p->dollarQuotedString();
1203        $faker->seed(42);
1204        self::assertSame($a, $p->dollarQuotedString(1, 255));
1205    }
1206
1207    public function testIntegerLiteralDefaultMatchesExplicit(): void
1208    {
1209        $faker = Factory::create();
1210        $p = new MySqlProvider($faker);
1211        $faker->seed(42);
1212        $a = $p->integerLiteral();
1213        $faker->seed(42);
1214        self::assertSame($a, $p->integerLiteral(1, 2147483647));
1215    }
1216
1217    public function testLongIntegerLiteralDefaultMatchesExplicit(): void
1218    {
1219        $faker = Factory::create();
1220        $p = new MySqlProvider($faker);
1221        $faker->seed(42);
1222        $a = $p->longIntegerLiteral();
1223        $faker->seed(42);
1224        self::assertSame($a, $p->longIntegerLiteral(0, 2147483647));
1225    }
1226
1227    public function testUnsignedBigIntLiteralDefaultMatchesExplicit(): void
1228    {
1229        $faker = Factory::create();
1230        $p = new MySqlProvider($faker);
1231        $faker->seed(42);
1232        $a = $p->unsignedBigIntLiteral();
1233        $faker->seed(42);
1234        self::assertSame($a, $p->unsignedBigIntLiteral(1, 20));
1235    }
1236
1237    public function testDecimalLiteralDefaultMatchesExplicit(): void
1238    {
1239        $faker = Factory::create();
1240        $p = new MySqlProvider($faker);
1241        $faker->seed(42);
1242        $a = $p->decimalLiteral();
1243        $faker->seed(42);
1244        self::assertSame($a, $p->decimalLiteral(10, 2));
1245    }
1246
1247    public function testFloatLiteralDefaultMatchesExplicit(): void
1248    {
1249        $faker = Factory::create();
1250        $p = new MySqlProvider($faker);
1251        $faker->seed(42);
1252        $a = $p->floatLiteral();
1253        $faker->seed(42);
1254        self::assertSame($a, $p->floatLiteral(10, 2, -38, 38));
1255    }
1256
1257    public function testHexLiteralDefaultMatchesExplicit(): void
1258    {
1259        $faker = Factory::create();
1260        $p = new MySqlProvider($faker);
1261        $faker->seed(42);
1262        $a = $p->hexLiteral();
1263        $faker->seed(42);
1264        self::assertSame($a, $p->hexLiteral(1, 16));
1265    }
1266
1267    public function testBinaryLiteralDefaultMatchesExplicit(): void
1268    {
1269        $faker = Factory::create();
1270        $p = new MySqlProvider($faker);
1271        $faker->seed(42);
1272        $a = $p->binaryLiteral();
1273        $faker->seed(42);
1274        self::assertSame($a, $p->binaryLiteral(1, 64));
1275    }
1276
1277    public function testHostnameDefaultMatchesExplicit(): void
1278    {
1279        $faker = Factory::create();
1280        $p = new MySqlProvider($faker);
1281        $faker->seed(42);
1282        $a = $p->hostname();
1283        $faker->seed(42);
1284        self::assertSame($a, $p->hostname(1, 4, 63));
1285    }
1286
1287    public function testQuotedIdentifierCustomLength(): void
1288    {
1289        $faker = Factory::create();
1290        $faker->seed(12345);
1291        $provider = new MySqlProvider($faker);
1292
1293        $result = $provider->quotedIdentifier(5, 10);
1294
1295        self::assertMatchesRegularExpression('/^`[a-z_][a-z0-9_]{4,9}`$/', $result);
1296    }
1297
1298    public function testStringLiteralCustomLength(): void
1299    {
1300        $faker = Factory::create();
1301        $faker->seed(12345);
1302        $provider = new MySqlProvider($faker);
1303
1304        $result = $provider->stringLiteral(3, 8);
1305        $content = substr($result, 1, -1);
1306
1307        self::assertGreaterThanOrEqual(3, strlen($content));
1308        self::assertLessThanOrEqual(8, strlen($content));
1309    }
1310
1311    public function testNationalStringLiteralCustomLength(): void
1312    {
1313        $faker = Factory::create();
1314        $faker->seed(12345);
1315        $provider = new MySqlProvider($faker);
1316
1317        $result = $provider->nationalStringLiteral(2, 5);
1318        $content = substr($result, 2, -1);
1319
1320        self::assertGreaterThanOrEqual(2, strlen($content));
1321        self::assertLessThanOrEqual(5, strlen($content));
1322    }
1323
1324    public function testDollarQuotedStringCustomLength(): void
1325    {
1326        $faker = Factory::create();
1327        $faker->seed(12345);
1328        $provider = new MySqlProvider($faker);
1329
1330        $result = $provider->dollarQuotedString(2, 6);
1331        $content = substr($result, 2, -2);
1332
1333        self::assertGreaterThanOrEqual(2, strlen($content));
1334        self::assertLessThanOrEqual(6, strlen($content));
1335    }
1336
1337    public function testIntegerLiteralCustomRange(): void
1338    {
1339        $faker = Factory::create();
1340        $faker->seed(12345);
1341        $provider = new MySqlProvider($faker);
1342
1343        $result = $provider->integerLiteral(100, 500);
1344
1345        self::assertGreaterThanOrEqual(100, (int) $result);
1346        self::assertLessThanOrEqual(500, (int) $result);
1347    }
1348
1349    public function testLongIntegerLiteralCustomRange(): void
1350    {
1351        $faker = Factory::create();
1352        $faker->seed(12345);
1353        $provider = new MySqlProvider($faker);
1354
1355        $result = $provider->longIntegerLiteral(10, 100);
1356
1357        self::assertGreaterThanOrEqual(10, (int) $result);
1358        self::assertLessThanOrEqual(100, (int) $result);
1359    }
1360
1361    public function testDecimalLiteralCustomPrecision(): void
1362    {
1363        $faker = Factory::create();
1364        $faker->seed(12345);
1365        $provider = new MySqlProvider($faker);
1366
1367        $result = $provider->decimalLiteral(5, 2);
1368
1369        self::assertMatchesRegularExpression('/^\d+\.\d{2,}$/', $result);
1370    }
1371
1372    public function testFloatLiteralCustomParams(): void
1373    {
1374        $faker = Factory::create();
1375        $faker->seed(12345);
1376        $provider = new MySqlProvider($faker);
1377
1378        $result = $provider->floatLiteral(5, 2, -10, 10);
1379
1380        self::assertMatchesRegularExpression('/^\d+\.\d+e-?\d+$/', $result);
1381    }
1382
1383    public function testHexLiteralCustomLength(): void
1384    {
1385        $faker = Factory::create();
1386        $faker->seed(12345);
1387        $provider = new MySqlProvider($faker);
1388
1389        $result = $provider->hexLiteral(4, 8);
1390
1391        self::assertMatchesRegularExpression('/^0x[0-9a-f]{4,8}$/', $result);
1392    }
1393
1394    public function testBinaryLiteralCustomLength(): void
1395    {
1396        $faker = Factory::create();
1397        $faker->seed(12345);
1398        $provider = new MySqlProvider($faker);
1399
1400        $result = $provider->binaryLiteral(8, 16);
1401
1402        self::assertMatchesRegularExpression('/^0b[01]{8,16}$/', $result);
1403    }
1404
1405    public function testHostnameCustomParams(): void
1406    {
1407        $faker = Factory::create();
1408        $faker->seed(12345);
1409        $provider = new MySqlProvider($faker);
1410
1411        $result = $provider->hostname(2, 3, 5);
1412
1413        self::assertMatchesRegularExpression('/^[a-z0-9]+(\.[a-z0-9]+)+$/', $result);
1414    }
1415
1416    /**
1417     * @return iterable<string, array{StatementRule}>
1418     */
1419    public static function providerStatementRuleValue(): iterable
1420    {
1421        yield 'Select' => [StatementRule::Select];
1422        yield 'Insert' => [StatementRule::Insert];
1423        yield 'Update' => [StatementRule::Update];
1424        yield 'Delete' => [StatementRule::Delete];
1425        yield 'CreateTable' => [StatementRule::CreateTable];
1426        yield 'AlterTable' => [StatementRule::AlterTable];
1427        yield 'DropTable' => [StatementRule::DropTable];
1428        yield 'SimpleStatement' => [StatementRule::SimpleStatement];
1429    }
1430
1431    /**
1432     * @return iterable<string, array{string}>
1433     */
1434    public static function providerMySqlVersion(): iterable
1435    {
1436        foreach (SqlVersion::names('mysql') as $version) {
1437            yield $version => [$version];
1438        }
1439    }
1440
1441    /**
1442     * @return iterable<string, array{int, int}>
1443     */
1444    public static function providerMultipleGenerationSeeds(): iterable
1445    {
1446        yield 'seeds 0 and 1' => [0, 1];
1447        yield 'seeds 5 and 10' => [5, 10];
1448    }
1449
1450    /**
1451     * @return iterable<string, array{int}>
1452     */
1453    public static function providerMultiTableGenerationSeed(): iterable
1454    {
1455        foreach (range(0, 31) as $seed) {
1456            yield "seed {$seed}" => [$seed];
1457        }
1458    }
1459
1460    /**
1461     * @return iterable<string, array{string}>
1462     */
1463    public static function providerSupportedMySqlVersion(): iterable
1464    {
1465        yield 'MySQL 5.6' => ['mysql-5.6.51'];
1466        yield 'MySQL 5.7' => ['mysql-5.7.44'];
1467        yield 'MySQL 8.0' => ['mysql-8.0.44'];
1468        yield 'MySQL 8.1' => ['mysql-8.1.0'];
1469        yield 'MySQL 8.2' => ['mysql-8.2.0'];
1470        yield 'MySQL 8.3' => ['mysql-8.3.0'];
1471        yield 'MySQL 8.4' => ['mysql-8.4.7'];
1472        yield 'MySQL 9.0' => ['mysql-9.0.1'];
1473        yield 'MySQL 9.1' => ['mysql-9.1.0'];
1474    }
1475
1476    /**
1477     * @return iterable<string, array{int}>
1478     */
1479    public static function providerTargetedGenerationSeed(): iterable
1480    {
1481        foreach (range(0, 15) as $seed) {
1482            yield "seed {$seed}" => [$seed];
1483        }
1484    }
1485
1486    public function testPlannerCompilesReusableInstructionsWithoutChangingTheDefaultStart(): void
1487    {
1488        $provider = new MySqlProvider(Factory::create(), 'mysql-8.4.7');
1489        $plan = (new \SqlFaker\Generation\Choice\BytePlanCompiler())->compile('', $provider->planner());
1490        self::assertNull($plan->startRule());
1491        self::assertSame($provider->generate($plan), $provider->generate($plan));
1492    }
1493    public function testPlannerFreezesConstructiveValuesFromFuzzBytes(): void
1494    {
1495        $faker = Factory::create();
1496        $provider = new MySqlProvider($faker, 'mysql-8.4.7');
1497        $planner = $provider->planner();
1498        $constraints = GenerationPlan::constrained('ulong_num', [
1499            'ulong_num' => [ProductionPattern::containing('NUM')],
1500        ])->withExpansionBudget(1);
1501        $outputs = array_map(static fn (int $byte): string => $provider->generate((new \SqlFaker\Generation\Choice\BytePlanCompiler())->compile(
1502            "\0\0\0\0" . str_repeat(chr($byte) . chr($byte), 64),
1503            $planner,
1504            $constraints
1505        )), range(1, 31));
1506        self::assertGreaterThan(8, count(array_unique($outputs)));
1507        $plan = (new \SqlFaker\Generation\Choice\BytePlanCompiler())->compile("\0\0\0\0" . str_repeat("\0\xff", 64), $planner, $constraints);
1508        $first = $provider->generate($plan);
1509        self::assertGreaterThan(2, (int) $first);
1510        self::assertSame($first, $plan->lexemeAt('NUM', 0));
1511        self::assertNotNull($plan->candidateKeyAt('NUM', 0));
1512        $faker->seed(913);
1513        $faker->numberBetween(0, 1000);
1514        self::assertSame($first, $provider->generate($plan));
1515    }
1516
1517    /**
1518     * @param GenerationPlan<true> $constraints
1519     */
1520    #[DataProvider('providerScopedInsertPlan')]
1521    public function testGenerateSchemaBoundInsertAndFreezeChoices(GenerationPlan $constraints, int $seed): void
1522    {
1523        $faker = Factory::create();
1524        $faker->seed($seed);
1525        $provider = new MySqlProvider($faker, 'mysql-8.4.7');
1526        self::assertMatchesRegularExpression("/^INSERT(?: INTO)? users VALUES? \( (?:1[0-9]|20) , '(?:Alice|Bob)' \)$/", $provider->generate($constraints));
1527        $frozen = (new \SqlFaker\Generation\Choice\BytePlanCompiler())->compile(str_repeat(chr($seed), 64), $provider->planner(), $constraints);
1528        $sql = $provider->generate($frozen);
1529        $faker->seed(999);
1530        self::assertSame($sql, $provider->generate($frozen));
1531        self::assertMatchesRegularExpression("/^INSERT(?: INTO)? users VALUES? \( (?:1[0-9]|20) , '(?:Alice|Bob)' \)$/", $sql);
1532    }
1533
1534    /**
1535     * @return iterable<string, array{GenerationPlan<true>, int}>
1536     */
1537    public static function providerScopedInsertPlan(): iterable
1538    {
1539        $number = RulePlan::any()->allowing(ProductionPattern::exactly('NUM_literal'))->withRule('NUM_literal', RulePlan::any()->allowing(ProductionPattern::exactly('int64_literal')))->withRule('int64_literal', RulePlan::any()->allowing(ProductionPattern::exactly('NUM')))->withLexeme('NUM', LexemeConstraint::integers(10, 20));
1540        $string = RulePlan::any()->allowing(ProductionPattern::exactly('text_literal'))->withRule('text_literal', RulePlan::any()->allowing(ProductionPattern::exactly('TEXT_STRING')))->withLexeme('TEXT_STRING', LexemeConstraint::oneOf("'Alice'", "'Bob'"));
1541        $plan = GenerationPlan::fromRule('insert_stmt')->withRule('insert_stmt', RulePlan::any()->allowing(ProductionPattern::containing('insert_from_constructor')))
1542         ->withRule('table_ident', RulePlan::any()->allowing(ProductionPattern::exactly('ident'))->withLexeme('IDENT', LexemeConstraint::oneOf('users')))
1543         ->withRule('values', RulePlan::any()->withItems(RulePlan::any()->withRule('literal', $number), RulePlan::any()->withRule('literal', $string)))
1544         ->requiringNonEmpty()->withExpansionBudget(100);
1545        foreach (['insert_lock_option' => [], 'opt_ignore' => [], 'opt_use_partition' => [], 'opt_values_reference' => [], 'opt_insert_update_list' => [], 'ident' => ['IDENT_sys'],'IDENT_sys' => ['IDENT'],'insert_from_constructor' => ['insert_values'],'values_list' => ['row_value'],'opt_values' => ['values'],'expr_or_default' => ['expr'],'expr' => ['bool_pri'],'bool_pri' => ['predicate'],'predicate' => ['bit_expr'],'bit_expr' => ['simple_expr'],'simple_expr' => ['literal_or_null'],'literal_or_null' => ['literal']] as $name => $symbols) {
1546            $plan = $plan->withRule($name, RulePlan::any()->allowing(ProductionPattern::exactly(...$symbols)));
1547        }
1548
1549        foreach ([0, 1, 7, 31] as $seed) {
1550            yield 'seed ' . $seed => [$plan, $seed];
1551        }
1552    }
1553
1554}
1555