packages/sql-parser/tests/Unit/MySql/Lexer/WordScannerTest.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\Scan;
18use SqlParser\MySql\Lexer\WordScanner;
19use SqlParser\MySql\MySqlVersion;
20use SqlParser\MySql\SqlMode;
21
22#[CoversClass(WordScanner::class)]
23#[UsesClass(Charsets::class)]
24#[UsesClass(Cursor::class)]
25#[UsesClass(Lexeme::class)]
26#[UsesClass(LexicalException::class)]
27#[UsesClass(KeywordTable::class)]
28#[UsesClass(MySqlVersion::class)]
29#[UsesClass(Scan::class)]
30#[UsesClass(SqlMode::class)]
31#[UsesClass(\SqlParser\Lexer\SourcePosition::class)]
32#[UsesClass(\SqlParser\Resource\SqlVersion::class)]
33#[UsesClass(\SqlParser\Resource\VersionRegistry::class)]
34#[UsesClass(\SqlParser\Lexer\SourceException::class)]
35#[Small]
36final class WordScannerTest extends TestCase
37{
38    public function testScan(): void
39    {
40        $scanner = new WordScanner();
41        $keywords = new KeywordTable(['SELECT' => 'SELECT_SYM'], ['COUNT' => 'COUNT_SYM']);
42        $scan = static fn (string $sql, ?MySqlVersion $version = null): Scan => new Scan(new Cursor($sql), $keywords, new SqlMode(), $version ?? MySqlVersion::resolve());
43
44        self::assertSame('SELECT_SYM', $scanner->scan($scan('select 1'))?->name);
45        self::assertSame('IDENT', $scanner->scan($scan('count '))?->name);
46        self::assertSame('COUNT_SYM', $scanner->scan($scan('count('))?->name);
47        self::assertSame('UNDERSCORE_CHARSET', $scanner->scan($scan("_utf8mb4'x'"))?->name);
48        self::assertSame('IDENT', $scanner->scan($scan('_unknown'))?->name);
49        self::assertSame('DOLLAR_QUOTED_STRING_SYM', $scanner->scan($scan('$$text$$'))?->name);
50        self::assertSame('IDENT', $scanner->scan($scan('$$text$$', MySqlVersion::resolve('mysql-8.0.44')))?->name);
51        self::assertSame('IDENT', $scanner->scan($scan('$x'))?->name);
52        self::assertNull($scanner->scan($scan('1a')));
53        self::assertNull($scanner->scan($scan('(')));
54    }
55
56    public function testWordKeepsAKeywordBeforeADotAsAnIdentifier(): void
57    {
58        $scan = new Scan(new Cursor('select.x'), new KeywordTable(['SELECT' => 'SELECT_SYM'], []), new SqlMode(), MySqlVersion::resolve());
59        $lexeme = (new WordScanner())->word($scan, 0);
60
61        self::assertSame('IDENT', $lexeme->name);
62        self::assertSame(LexerState::IdentifierSeparator, $scan->next);
63    }
64
65    public function testWordLooksUpFunctionsAcrossSpacesUnderIgnoreSpace(): void
66    {
67        $keywords = new KeywordTable([], ['COUNT' => 'COUNT_SYM']);
68
69        self::assertSame('COUNT_SYM', (new WordScanner())->word(new Scan(new Cursor('count ('), $keywords, new SqlMode(ignoreSpace: true), MySqlVersion::resolve()), 0)->name);
70        self::assertSame('IDENT', (new WordScanner())->word(new Scan(new Cursor('count ('), $keywords, new SqlMode(), MySqlVersion::resolve()), 0)->name);
71    }
72
73    public function testIdentifier(): void
74    {
75        $scan = new Scan(new Cursor('select.x'), new KeywordTable(['SELECT' => 'SELECT_SYM'], []), new SqlMode(), MySqlVersion::resolve());
76        $lexeme = (new WordScanner())->identifier($scan, 0);
77
78        self::assertSame('IDENT', $lexeme->name);
79        self::assertSame('select', $lexeme->text);
80        self::assertSame(LexerState::IdentifierSeparator, $scan->next);
81    }
82
83    public function testConsume(): void
84    {
85        $cursor = new Cursor('ab_1$é rest');
86
87        self::assertSame('ab_1$é', (new WordScanner())->consume($cursor));
88        self::assertSame(' ', $cursor->peek());
89    }
90
91    public function testIdentifierLexeme(): void
92    {
93        $scan = new Scan(new Cursor('猫'), new KeywordTable([], []), new SqlMode(), MySqlVersion::resolve());
94        $scan->cursor->take(3);
95
96        self::assertSame('IDENT_QUOTED', (new WordScanner())->identifierLexeme($scan, 0, '猫')->name);
97        self::assertSame('IDENT', (new WordScanner())->identifierLexeme($scan, 0, 'cat')->name);
98    }
99
100    public function testDollarQuoted(): void
101    {
102        $scanner = new WordScanner();
103
104        self::assertSame('$tag$ a $x$ b $tag$', $scanner->dollarQuoted(new Scan(new Cursor('$tag$ a $x$ b $tag$ c'), new KeywordTable([], []), new SqlMode(), MySqlVersion::resolve()))?->text);
105        self::assertNull($scanner->dollarQuoted(new Scan(new Cursor('$abc'), new KeywordTable([], []), new SqlMode(), MySqlVersion::resolve())));
106    }
107
108    public function testDollarQuotedRejectsAnUnterminatedString(): void
109    {
110        $this->expectException(LexicalException::class);
111
112        (new WordScanner())->dollarQuoted(new Scan(new Cursor('$$ open'), new KeywordTable([], []), new SqlMode(), MySqlVersion::resolve()));
113    }
114}
115