packages/sql-parser/tests/Unit/MySql/MySqlParserTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\MySql;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\Small;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use RuntimeException;
12use SqlParser\Lexer\LexicalException;
13use SqlParser\Lexer\Token;
14use SqlParser\MySql\MySqlParser;
15use SqlParser\MySql\MySqlVersion;
16use SqlParser\MySql\SqlMode;
17use SqlParser\Parser\SyntaxException;
18
19#[CoversClass(MySqlParser::class)]
20#[UsesClass(MySqlVersion::class)]
21#[UsesClass(SqlMode::class)]
22#[UsesClass(SyntaxException::class)]
23#[UsesClass(LexicalException::class)]
24#[UsesClass(Token::class)]
25#[UsesClass(\SqlParser\Grammar\SymbolTable::class)]
26#[UsesClass(\SqlParser\Lexer\Cursor::class)]
27#[UsesClass(\SqlParser\Lexer\Lexeme::class)]
28#[UsesClass(\SqlParser\Lexer\SourcePosition::class)]
29#[UsesClass(\SqlParser\Lexer\TerminalIndex::class)]
30#[UsesClass(\SqlParser\MySql\Lexer\KeywordTable::class)]
31#[UsesClass(\SqlParser\MySql\Lexer\MySqlLexer::class)]
32#[UsesClass(\SqlParser\MySql\Lexer\NumberScanner::class)]
33#[UsesClass(\SqlParser\MySql\Lexer\OperatorScanner::class)]
34#[UsesClass(\SqlParser\MySql\Lexer\QuotedScanner::class)]
35#[UsesClass(\SqlParser\MySql\Lexer\Scan::class)]
36#[UsesClass(\SqlParser\MySql\Lexer\TriviaScanner::class)]
37#[UsesClass(\SqlParser\MySql\Lexer\VariableScanner::class)]
38#[UsesClass(\SqlParser\MySql\Lexer\WordScanner::class)]
39#[UsesClass(\SqlParser\Parser\LrParser::class)]
40#[UsesClass(\SqlParser\Parser\Node::class)]
41#[UsesClass(\SqlParser\Resource\SqlVersion::class)]
42#[UsesClass(\SqlParser\Resource\VersionRegistry::class)]
43#[UsesClass(\SqlParser\Table\ActionCode::class)]
44#[UsesClass(\SqlParser\Table\PackedRows::class)]
45#[UsesClass(\SqlParser\Table\ParseTable::class)]
46#[UsesClass(\SqlParser\Table\TableCodec::class)]
47#[UsesClass(\SqlParser\Table\TableFile::class)]
48#[UsesClass(\SqlParser\Table\TableRule::class)]
49#[UsesClass(\SqlParser\Lexer\SourceException::class)]
50#[Small]
51final class MySqlParserTest extends TestCase
52{
53 public function testVersion(): void
54 {
55 self::assertSame('mysql-8.4.7', (new MySqlParser())->version());
56 self::assertSame('mysql-5.7.44', (new MySqlParser('mysql-5.7.44'))->version());
57 }
58
59 public function testVersionRejectsAnUnknownRelease(): void
60 {
61 $this->expectException(RuntimeException::class);
62
63 new MySqlParser('mysql-4.1.0');
64 }
65
66 public function testTokenize(): void
67 {
68 $tokens = (new MySqlParser())->tokenize('SELECT `a` FROM t');
69
70 self::assertSame(['SELECT_SYM', 'IDENT_QUOTED', 'FROM', 'IDENT', 'END_OF_INPUT', '$end'], array_map(static fn (Token $token): string => $token->name, $tokens));
71 self::assertSame('`a`', $tokens[1]->text);
72 }
73
74 public function testTokenizeHonoursTheMode(): void
75 {
76 self::assertSame('IDENT_QUOTED', (new MySqlParser(mode: new SqlMode(ansiQuotes: true)))->tokenize('SELECT "x"')[1]->name);
77 self::assertSame('TEXT_STRING', (new MySqlParser())->tokenize('SELECT "x"')[1]->name);
78 }
79
80 public function testTokenizeRejectsAnUnterminatedString(): void
81 {
82 $this->expectException(LexicalException::class);
83
84 (new MySqlParser())->tokenize("SELECT 'abc");
85 }
86
87 public function testParse(): void
88 {
89 $sql = 'SELECT id, name FROM users u WHERE u.id = ? AND name LIKE \'a%\' ORDER BY id DESC LIMIT 10';
90 $tree = (new MySqlParser())->parse($sql);
91
92 self::assertSame('start_entry', $tree->name);
93 self::assertSame($sql, $tree->text($sql));
94 self::assertCount(1, $tree->find('where_clause'));
95 self::assertSame('WHERE u.id = ? AND name LIKE \'a%\'', $tree->find('where_clause')[0]->text($sql));
96 }
97
98 public function testParseWritesTheTextBackWithItsCommentsAndWhitespace(): void
99 {
100 $sql = " SELECT /* one */ 1 , 2 # two\nFROM t ;\n\n-- done\n";
101 $versioned = 'SELECT 1 /*!40101 , 2 */ FROM t';
102
103 $rollup = 'SELECT a FROM t GROUP BY a WITH ROLLUP';
104
105 self::assertSame($sql, (new MySqlParser())->parse($sql)->toString());
106 self::assertSame($versioned, (new MySqlParser())->parse($versioned)->toString());
107 self::assertSame($rollup, (new MySqlParser())->parse($rollup)->toString());
108 self::assertSame('-- nothing at all', (new MySqlParser())->parse('-- nothing at all')->toString());
109 }
110
111 public function testParseFollowsTheChosenRelease(): void
112 {
113 self::assertSame('query', (new MySqlParser('mysql-5.6.51'))->parse('SELECT 1')->name);
114 self::assertCount(1, (new MySqlParser('mysql-8.0.44'))->parse('WITH c AS (SELECT 1) SELECT * FROM c')->find('with_clause'));
115 }
116
117 public function testParseRejectsSyntaxTheReleaseLacks(): void
118 {
119 $this->expectException(SyntaxException::class);
120 $this->expectExceptionMessage("Unexpected 'WITH' at line 1, column 1");
121
122 (new MySqlParser('mysql-5.6.51'))->parse('WITH c AS (SELECT 1) SELECT * FROM c');
123 }
124
125 public function testParseRejectsAStatementThatEndsEarly(): void
126 {
127 $this->expectException(SyntaxException::class);
128
129 (new MySqlParser())->parse('SELECT 1 FROM');
130 }
131
132 public function testVersions(): void
133 {
134 $versions = MySqlParser::versions();
135
136 self::assertSame('mysql-5.6.51', $versions[0]);
137 self::assertContains('mysql-8.4.7', $versions);
138 self::assertCount(9, $versions);
139 }
140}
141