packages/sql-faker/tests/Unit/Generation/Lexeme/LexicalGrammarTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Generation\Lexeme;
6
7use Faker\Factory;
8use PHPUnit\Framework\Attributes\CoversNothing;
9use PHPUnit\Framework\Attributes\DataProvider;
10use PHPUnit\Framework\Attributes\Medium;
11use PHPUnit\Framework\TestCase;
12use SqlFaker\Generation\Lexeme\LexicalGrammar;
13use SqlFaker\Generation\Plan\GenerationPlan;
14use SqlFaker\MySql\Generation\LexicalGrammar as MySqlLexicalGrammar;
15use SqlFaker\MySqlProvider;
16use SqlFaker\PostgreSql\Generation\LexicalGrammar as PostgreSqlLexicalGrammar;
17use SqlFaker\PostgreSql\StatementType as PostgreSqlStatementType;
18use SqlFaker\PostgreSqlProvider;
19use SqlFaker\Sqlite\Generation\LexicalGrammar as SqliteLexicalGrammar;
20use SqlFaker\SqliteProvider;
21
22#[CoversNothing]
23#[Medium]
24final class LexicalGrammarTest extends TestCase
25{
26    #[DataProvider('providerDialectLexicalGrammar')]
27    public function testVersionReportsTheProfileTheGrammarWasBuiltFor(
28        LexicalGrammar $lexical,
29        string $version,
30        string $terminal,
31    ): void {
32        unset($terminal);
33
34        self::assertSame($version, $lexical->version());
35    }
36
37    #[DataProvider('providerDialectLexicalGrammar')]
38    public function testRealizeTurnsTerminalsIntoConcreteSql(
39        LexicalGrammar $lexical,
40        string $version,
41        string $terminal,
42    ): void {
43        unset($version);
44
45        self::assertNotSame('', $lexical->realize([$terminal]));
46    }
47
48    #[DataProvider('providerDialectLexicalGrammar')]
49    public function testGenerateProducesSqlForALexicalPlan(
50        LexicalGrammar $lexical,
51        string $version,
52        string $terminal,
53    ): void {
54        unset($version, $terminal);
55
56        $plan = GenerationPlan::lexical('quoted_identifier', ['minLength' => 2, 'maxLength' => 8]);
57
58        self::assertNotSame('', $lexical->generate($plan));
59    }
60
61    /**
62     * @return iterable<string, array{LexicalGrammar, string, string}>
63     */
64    public static function providerDialectLexicalGrammar(): iterable
65    {
66        yield 'MySQL' => [
67            new MySqlLexicalGrammar(Factory::create(), 'mysql-8.4.7'),
68            'mysql-8.4.7',
69            'SELECT_SYM',
70        ];
71        yield 'PostgreSQL' => [
72            new PostgreSqlLexicalGrammar(Factory::create(), 'pg-17.2'),
73            'pg-17.2',
74            'SELECT',
75        ];
76        yield 'SQLite' => [
77            new SqliteLexicalGrammar(Factory::create(), 'sqlite-3.47.2'),
78            'sqlite-3.47.2',
79            'SELECT',
80        ];
81    }
82    #[DataProvider('providerSupportedMySqlVersion')]
83    public function testSupportedMySqlVersionBindsGrammarAndLexerProfileTogether(string $version): void
84    {
85        $faker = Factory::create();
86        $faker->seed(20260814);
87        $provider = new MySqlProvider($faker, $version);
88        $statements = array_map(static fn (int $iteration): string => $provider->sql(maxDepth: 20), range(1, 20));
89
90        self::assertNotContains('', $statements, $version);
91    }
92
93    /**
94     * @return iterable<string, array{string}>
95     */
96    public static function providerSupportedMySqlVersion(): iterable
97    {
98        yield 'MySQL 5.6' => ['mysql-5.6.51'];
99        yield 'MySQL 5.7' => ['mysql-5.7.44'];
100        yield 'MySQL 8.0' => ['mysql-8.0.44'];
101        yield 'MySQL 8.1' => ['mysql-8.1.0'];
102        yield 'MySQL 8.2' => ['mysql-8.2.0'];
103        yield 'MySQL 8.3' => ['mysql-8.3.0'];
104        yield 'MySQL 8.4' => ['mysql-8.4.7'];
105        yield 'MySQL 9.0' => ['mysql-9.0.1'];
106        yield 'MySQL 9.1' => ['mysql-9.1.0'];
107    }
108
109    #[DataProvider('providerPostgreSqlStatementType')]
110    public function testSupportedPostgreSqlVersionBindsGrammarAndLexerProfileTogether(PostgreSqlStatementType $type): void
111    {
112        $faker = Factory::create();
113        $faker->seed(20260814);
114        $provider = new PostgreSqlProvider($faker, 'pg-17.2');
115
116        self::assertNotSame('', $provider->sql($type, maxDepth: 20), $type->name);
117    }
118
119    /**
120     * @return iterable<string, array{PostgreSqlStatementType}>
121     */
122    public static function providerPostgreSqlStatementType(): iterable
123    {
124        foreach (PostgreSqlStatementType::cases() as $type) {
125            yield $type->name => [$type];
126        }
127    }
128
129    public function testSupportedSqliteVersionBindsGrammarAndLexerProfileTogether(): void
130    {
131        $faker = Factory::create();
132        $faker->seed(20260814);
133        $provider = new SqliteProvider($faker, 'sqlite-3.47.2');
134        $statements = array_map(static fn (int $iteration): string => $provider->sql(maxDepth: 20), range(1, 20));
135
136        self::assertNotContains('', $statements);
137    }
138    public function testIsNonOutputDistinguishesParserSelectorsFromKeywords(): void
139    {
140        $lexical = new MySqlLexicalGrammar(Factory::create(), 'mysql-8.4.7');
141        self::assertTrue($lexical->isNonOutput('END_OF_INPUT'));
142        self::assertFalse($lexical->isNonOutput('SELECT_SYM'));
143    }
144
145    public function testRealizeSequenceHonorsAnExplicitLexemePlan(): void
146    {
147        $lexical = new MySqlLexicalGrammar(Factory::create(), 'mysql-8.4.7');
148        $sequence = \SqlFaker\Generation\Token\TerminalSequence::fromNames(['NUM']);
149        self::assertSame('42', $lexical->realizeSequence($sequence, GenerationPlan::all()->withLexemes(['NUM' => ['42']])));
150    }
151
152    public function testResolveSequenceUsesTheCommonContractForCandidateInspection(): void
153    {
154        $lexical = new SqliteLexicalGrammar(Factory::create(), 'sqlite-3.47.2');
155        self::assertCount(1, $lexical->resolveSequence(\SqlFaker\Generation\Token\TerminalSequence::fromNames(['INTEGER']), null, static fn (int $count): int => 0)->candidates);
156    }
157}
158