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