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