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