packages/sql-parser/tests/Unit/MySql/Lexer/OperatorScannerTest.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\KeywordTable;
15use SqlParser\MySql\Lexer\LexerState;
16use SqlParser\MySql\Lexer\OperatorScanner;
17use SqlParser\MySql\Lexer\Scan;
18use SqlParser\MySql\MySqlVersion;
19use SqlParser\MySql\SqlMode;
20
21#[CoversClass(OperatorScanner::class)]
22#[UsesClass(Cursor::class)]
23#[UsesClass(Lexeme::class)]
24#[UsesClass(LexicalException::class)]
25#[UsesClass(KeywordTable::class)]
26#[UsesClass(MySqlVersion::class)]
27#[UsesClass(Scan::class)]
28#[UsesClass(SqlMode::class)]
29#[UsesClass(\SqlParser\Lexer\SourcePosition::class)]
30#[UsesClass(\SqlParser\Resource\SqlVersion::class)]
31#[UsesClass(\SqlParser\Resource\VersionRegistry::class)]
32#[UsesClass(\SqlParser\Lexer\SourceException::class)]
33#[Small]
34final class OperatorScannerTest extends TestCase
35{
36 public function testScan(): void
37 {
38 $scanner = new OperatorScanner();
39 $keywords = new KeywordTable(['<=>' => 'EQUAL_SYM', '<=' => 'LE', '<' => 'LT', '!=' => 'NE', '&&' => 'AND_AND_SYM', '||' => 'OR_OR_SYM', '=' => 'EQ'], []);
40 $scan = static fn (string $sql): Scan => new Scan(new Cursor($sql), $keywords, new SqlMode(), MySqlVersion::resolve());
41
42 self::assertSame('EQUAL_SYM', $scanner->scan($scan('<=> 1'))->name);
43 self::assertSame('LE', $scanner->scan($scan('<= 1'))->name);
44 self::assertSame('LT', $scanner->scan($scan('< 1'))->name);
45 self::assertSame('OR2_SYM', $scanner->scan($scan('|| 1'))->name);
46 self::assertSame('|', $scanner->scan($scan('| 1'))->name);
47 self::assertSame('SET_VAR', $scanner->scan($scan(':= 1'))->name);
48 self::assertSame(':', $scanner->scan($scan(': 1'))->name);
49 self::assertSame('PARAM_MARKER', $scanner->scan($scan('? '))->name);
50 self::assertSame('JSON_UNQUOTED_SEPARATOR_SYM', $scanner->scan($scan("->>'$'"))->name);
51 self::assertSame('(', $scanner->scan($scan('('))->name);
52 self::assertSame('!', $scanner->scan($scan('!x'))->name);
53 }
54
55 public function testScanRejectsAParameterMarkerGluedToAName(): void
56 {
57 $this->expectException(LexicalException::class);
58
59 (new OperatorScanner())->scan(new Scan(new Cursor('?x'), new KeywordTable([], []), new SqlMode(), MySqlVersion::resolve()));
60 }
61
62 public function testScanRejectsAForeignCharacter(): void
63 {
64 $this->expectException(LexicalException::class);
65
66 (new OperatorScanner())->scan(new Scan(new Cursor('\\'), new KeywordTable([], []), new SqlMode(), MySqlVersion::resolve()));
67 }
68
69 public function testMultiCharacter(): void
70 {
71 $scanner = new OperatorScanner();
72 $keywords = new KeywordTable(['<<' => 'SHIFT_LEFT'], []);
73 $modern = new Scan(new Cursor(''), $keywords, new SqlMode(), MySqlVersion::resolve('mysql-8.4.7'));
74 $legacy = new Scan(new Cursor(''), $keywords, new SqlMode(), MySqlVersion::resolve('mysql-5.6.51'));
75
76 self::assertSame('SET_VAR', $scanner->multiCharacter($modern, ':='));
77 self::assertSame('JSON_SEPARATOR_SYM', $scanner->multiCharacter($modern, '->'));
78 self::assertNull($scanner->multiCharacter($legacy, '->'));
79 self::assertSame('SHIFT_LEFT', $scanner->multiCharacter($modern, '<<'));
80 self::assertNull($scanner->multiCharacter($modern, '<-'));
81 self::assertNull($scanner->multiCharacter($modern, 'ab'));
82 }
83
84 public function testSeparator(): void
85 {
86 $scanner = new OperatorScanner();
87 $qualified = new Scan(new Cursor('.x'), new KeywordTable([], []), new SqlMode(), MySqlVersion::resolve());
88 $bare = new Scan(new Cursor('. x'), new KeywordTable([], []), new SqlMode(), MySqlVersion::resolve());
89
90 self::assertSame('.', $scanner->separator($qualified)->name);
91 self::assertSame(LexerState::IdentifierStart, $qualified->next);
92 self::assertSame(LexerState::Start, $bare->next === LexerState::Start ? $scanner->separator($bare)->name === '.' ? LexerState::Start : null : null);
93 self::assertSame(LexerState::Start, $bare->next);
94 }
95}
96