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