packages/sql-parser/tests/Unit/PostgreSql/Lexer/QuotedScannerTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\PostgreSql\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\PostgreSql\Lexer\KeywordTable;
15use SqlParser\PostgreSql\Lexer\QuotedScanner;
16use SqlParser\PostgreSql\Lexer\Scan;
17
18#[CoversClass(QuotedScanner::class)]
19#[UsesClass(Cursor::class)]
20#[UsesClass(Lexeme::class)]
21#[UsesClass(LexicalException::class)]
22#[UsesClass(KeywordTable::class)]
23#[UsesClass(Scan::class)]
24#[UsesClass(\SqlParser\Lexer\SourcePosition::class)]
25#[UsesClass(\SqlParser\Lexer\SourceException::class)]
26#[Small]
27final class QuotedScannerTest extends TestCase
28{
29 public function testScanReadsEverySpellingOfAString(): void
30 {
31 $scanner = new QuotedScanner();
32 $scan = static fn (string $sql): Scan => new Scan(new Cursor($sql), new KeywordTable(['NCHAR' => 'NCHAR']));
33
34 self::assertSame('SCONST', $scanner->scan($scan("'it''s' x"))?->name);
35 self::assertSame("E'a\\'b'", $scanner->scan($scan("E'a\\'b' x"))?->text);
36 self::assertSame('BCONST', $scanner->scan($scan("b'01'"))?->name);
37 self::assertSame('XCONST', $scanner->scan($scan("X'ff'"))?->name);
38 self::assertSame('USCONST', $scanner->scan($scan("U&'d\\0061t'"))?->name);
39 self::assertSame('UIDENT', $scanner->scan($scan('u&"x"'))?->name);
40 self::assertSame('IDENT', $scanner->scan($scan('"x""y"'))?->name);
41 self::assertSame('SCONST', $scanner->scan($scan('$tag$ a $b$ $tag$'))?->name);
42 self::assertNull($scanner->scan($scan('abc')));
43 }
44
45 public function testScanReadsTheNationalPrefixAsAKeyword(): void
46 {
47 $scan = new Scan(new Cursor("n'x'"), new KeywordTable(['NCHAR' => 'NCHAR']));
48 $lexeme = (new QuotedScanner())->scan($scan);
49
50 self::assertNotNull($lexeme);
51 self::assertSame('NCHAR', $lexeme->name);
52 self::assertSame('n', $lexeme->text);
53 self::assertSame("'", $scan->cursor->peek());
54 }
55
56 public function testString(): void
57 {
58 $scan = new Scan(new Cursor("'a' -- c\n 'b' 'c'"), new KeywordTable([]));
59
60 self::assertSame("'a' -- c\n 'b'", (new QuotedScanner())->string($scan, 'SCONST', 0, false)->text);
61 }
62
63 public function testStringRejectsAnUnterminatedString(): void
64 {
65 $this->expectException(LexicalException::class);
66
67 (new QuotedScanner())->string(new Scan(new Cursor("'open"), new KeywordTable([])), 'SCONST', 0, false);
68 }
69
70 public function testBody(): void
71 {
72 $scanner = new QuotedScanner();
73 $doubled = new Scan(new Cursor("a''b' rest"), new KeywordTable([]));
74 $escaped = new Scan(new Cursor("a\\'b' rest"), new KeywordTable([]));
75 $plain = new Scan(new Cursor("a'' rest"), new KeywordTable([]));
76 $scanner->body($doubled, 0, false, true);
77 $scanner->body($escaped, 0, true, true);
78 $scanner->body($plain, 0, false, false);
79
80 self::assertSame(' rest', $doubled->cursor->take(5));
81 self::assertSame(' rest', $escaped->cursor->take(5));
82 self::assertSame("' rest", $plain->cursor->take(6));
83 }
84
85 public function testIdentifier(): void
86 {
87 self::assertSame('"a""b"', (new QuotedScanner())->identifier(new Scan(new Cursor('"a""b" c'), new KeywordTable([])), 'IDENT', 0)->text);
88 }
89
90 public function testIdentifierRejectsAnEmptyIdentifier(): void
91 {
92 $this->expectException(LexicalException::class);
93
94 (new QuotedScanner())->identifier(new Scan(new Cursor('""'), new KeywordTable([])), 'IDENT', 0);
95 }
96
97 public function testIdentifierRejectsAnUnterminatedIdentifier(): void
98 {
99 $this->expectException(LexicalException::class);
100
101 (new QuotedScanner())->identifier(new Scan(new Cursor('"open'), new KeywordTable([])), 'IDENT', 0);
102 }
103
104 public function testDollarQuoted(): void
105 {
106 $scanner = new QuotedScanner();
107
108 self::assertSame('$$a$$', $scanner->dollarQuoted(new Scan(new Cursor('$$a$$ b'), new KeywordTable([])))?->text);
109 self::assertNull($scanner->dollarQuoted(new Scan(new Cursor('$1'), new KeywordTable([]))));
110 self::assertNull($scanner->dollarQuoted(new Scan(new Cursor('$abc x'), new KeywordTable([]))));
111 }
112
113 public function testDollarQuotedRejectsAnUnterminatedString(): void
114 {
115 $this->expectException(LexicalException::class);
116
117 (new QuotedScanner())->dollarQuoted(new Scan(new Cursor('$a$ open $b$'), new KeywordTable([])));
118 }
119}
120