packages/sql-parser/tests/Unit/MySql/Lexer/MySqlLexerTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\MySql\Lexer;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\Small;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use SqlParser\Lexer\Cursor;
12use SqlParser\Lexer\Lexeme;
13use SqlParser\Lexer\LexicalException;
14use SqlParser\MySql\Lexer\Charsets;
15use SqlParser\MySql\Lexer\KeywordTable;
16use SqlParser\MySql\Lexer\LexerState;
17use SqlParser\MySql\Lexer\MySqlLexer;
18use SqlParser\MySql\Lexer\NumberScanner;
19use SqlParser\MySql\Lexer\OperatorScanner;
20use SqlParser\MySql\Lexer\QuotedScanner;
21use SqlParser\MySql\Lexer\Scan;
22use SqlParser\MySql\Lexer\TriviaScanner;
23use SqlParser\MySql\Lexer\VariableScanner;
24use SqlParser\MySql\Lexer\WordScanner;
25use SqlParser\MySql\MySqlVersion;
26use SqlParser\MySql\SqlMode;
27
28#[CoversClass(MySqlLexer::class)]
29#[UsesClass(Charsets::class)]
30#[UsesClass(Cursor::class)]
31#[UsesClass(Lexeme::class)]
32#[UsesClass(LexicalException::class)]
33#[UsesClass(KeywordTable::class)]
34#[UsesClass(MySqlVersion::class)]
35#[UsesClass(NumberScanner::class)]
36#[UsesClass(OperatorScanner::class)]
37#[UsesClass(QuotedScanner::class)]
38#[UsesClass(Scan::class)]
39#[UsesClass(SqlMode::class)]
40#[UsesClass(TriviaScanner::class)]
41#[UsesClass(VariableScanner::class)]
42#[UsesClass(WordScanner::class)]
43#[UsesClass(\SqlParser\Lexer\SourcePosition::class)]
44#[UsesClass(\SqlParser\Resource\SqlVersion::class)]
45#[UsesClass(\SqlParser\Resource\VersionRegistry::class)]
46#[UsesClass(\SqlParser\Lexer\SourceException::class)]
47#[Small]
48final class MySqlLexerTest extends TestCase
49{
50    public function testScanReadsAStatementIntoTerminals(): void
51    {
52        $version = MySqlVersion::resolve('mysql-8.4.7');
53        $lexer = new MySqlLexer(KeywordTable::load($version->release->keywordPath), $version);
54        $names = array_map(static fn (Lexeme $lexeme): string => $lexeme->name, $lexer->scan("SELECT t.select, @v := 1, @@session.sql_mode, u@localhost FROM db.t WHERE a <=> ? -- x\n"));
55
56        self::assertSame(['SELECT_SYM', 'IDENT', '.', 'IDENT', ',', '@', 'LEX_HOSTNAME', 'SET_VAR', 'NUM', ',', '@', '@', 'SESSION_SYM', '.', 'IDENT', ',', 'IDENT', '@', 'LEX_HOSTNAME', 'FROM', 'IDENT', '.', 'IDENT', 'WHERE', 'IDENT', 'EQUAL_SYM', 'PARAM_MARKER', 'END_OF_INPUT'], $names);
57    }
58
59    public function testScanReadsAnEmptyHostNameAsTheServerDoes(): void
60    {
61        $version = MySqlVersion::resolve('mysql-8.4.7');
62        $lexer = new MySqlLexer(KeywordTable::load($version->release->keywordPath), $version);
63        $lexemes = $lexer->scan('SELECT @ x');
64
65        self::assertSame(['SELECT_SYM', '@', 'LEX_HOSTNAME', 'IDENT', 'END_OF_INPUT'], array_map(static fn (Lexeme $lexeme): string => $lexeme->name, $lexemes));
66        self::assertSame('', $lexemes[2]->text);
67    }
68
69    public function testScanRejectsAnUnreadableCharacter(): void
70    {
71        $version = MySqlVersion::resolve();
72
73        $this->expectException(LexicalException::class);
74
75        (new MySqlLexer(KeywordTable::load($version->release->keywordPath), $version))->scan('SELECT \\');
76    }
77
78    public function testNext(): void
79    {
80        $version = MySqlVersion::resolve();
81        $lexer = new MySqlLexer(KeywordTable::load($version->release->keywordPath), $version);
82        $scan = new Scan(new Cursor('x'), KeywordTable::load($version->release->keywordPath), new SqlMode(), $version);
83        $scan->next = LexerState::IdentifierStart;
84
85        self::assertSame('IDENT', $lexer->next($scan)->name);
86        self::assertSame(LexerState::Start, $scan->next);
87    }
88
89    public function testMerged(): void
90    {
91        $modern = MySqlVersion::resolve('mysql-8.4.7');
92        $legacy = MySqlVersion::resolve('mysql-5.7.44');
93        $sql = 'WITH ROLLUP WITH CUBE WITH x';
94        $lexemes = [new Lexeme('WITH', 'WITH', 0), new Lexeme('ROLLUP_SYM', 'ROLLUP', 5), new Lexeme('WITH', 'WITH', 12), new Lexeme('CUBE_SYM', 'CUBE', 17), new Lexeme('WITH', 'WITH', 22), new Lexeme('IDENT', 'x', 27)];
95        $modernNames = array_map(static fn (Lexeme $lexeme): string => $lexeme->name, (new MySqlLexer(new KeywordTable([], []), $modern))->merged($lexemes, $sql));
96        $legacyMerged = (new MySqlLexer(new KeywordTable([], []), $legacy))->merged($lexemes, $sql);
97
98        self::assertSame(['WITH_ROLLUP_SYM', 'WITH', 'CUBE_SYM', 'WITH', 'IDENT'], $modernNames);
99        self::assertSame(['WITH_ROLLUP_SYM', 'WITH_CUBE_SYM', 'WITH', 'IDENT'], array_map(static fn (Lexeme $lexeme): string => $lexeme->name, $legacyMerged));
100        self::assertSame('WITH ROLLUP', $legacyMerged[0]->text);
101    }
102}
103