packages/sql-parser/tests/Unit/PostgreSql/PostgreSqlParserTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\PostgreSql;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\Small;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use RuntimeException;
12use SqlParser\Lexer\LexicalException;
13use SqlParser\Lexer\Token;
14use SqlParser\Parser\SyntaxException;
15use SqlParser\PostgreSql\PostgreSqlParser;
16use SqlParser\PostgreSql\PostgreSqlVersion;
17
18#[CoversClass(PostgreSqlParser::class)]
19#[UsesClass(PostgreSqlVersion::class)]
20#[UsesClass(SyntaxException::class)]
21#[UsesClass(LexicalException::class)]
22#[UsesClass(Token::class)]
23#[UsesClass(\SqlParser\Grammar\SymbolTable::class)]
24#[UsesClass(\SqlParser\Lexer\Cursor::class)]
25#[UsesClass(\SqlParser\Lexer\Lexeme::class)]
26#[UsesClass(\SqlParser\Lexer\SourcePosition::class)]
27#[UsesClass(\SqlParser\Lexer\TerminalIndex::class)]
28#[UsesClass(\SqlParser\Parser\LrParser::class)]
29#[UsesClass(\SqlParser\Parser\Node::class)]
30#[UsesClass(\SqlParser\PostgreSql\Lexer\KeywordTable::class)]
31#[UsesClass(\SqlParser\PostgreSql\Lexer\LookaheadFilter::class)]
32#[UsesClass(\SqlParser\PostgreSql\Lexer\NumberScanner::class)]
33#[UsesClass(\SqlParser\PostgreSql\Lexer\OperatorScanner::class)]
34#[UsesClass(\SqlParser\PostgreSql\Lexer\PostgreSqlLexer::class)]
35#[UsesClass(\SqlParser\PostgreSql\Lexer\QuotedScanner::class)]
36#[UsesClass(\SqlParser\PostgreSql\Lexer\Scan::class)]
37#[UsesClass(\SqlParser\PostgreSql\Lexer\TriviaScanner::class)]
38#[UsesClass(\SqlParser\PostgreSql\Lexer\WordScanner::class)]
39#[UsesClass(\SqlParser\Resource\SqlVersion::class)]
40#[UsesClass(\SqlParser\Resource\VersionRegistry::class)]
41#[UsesClass(\SqlParser\Table\ActionCode::class)]
42#[UsesClass(\SqlParser\Table\PackedRows::class)]
43#[UsesClass(\SqlParser\Table\ParseTable::class)]
44#[UsesClass(\SqlParser\Table\TableCodec::class)]
45#[UsesClass(\SqlParser\Table\TableFile::class)]
46#[UsesClass(\SqlParser\Table\TableRule::class)]
47#[UsesClass(\SqlParser\Lexer\SourceException::class)]
48#[Small]
49final class PostgreSqlParserTest extends TestCase
50{
51 public function testVersion(): void
52 {
53 self::assertSame('pg-17.2', (new PostgreSqlParser())->version());
54 }
55
56 public function testVersionRejectsAnUnknownRelease(): void
57 {
58 $this->expectException(RuntimeException::class);
59
60 new PostgreSqlParser('pg-9.6');
61 }
62
63 public function testTokenize(): void
64 {
65 $tokens = (new PostgreSqlParser())->tokenize('SELECT "a" FROM t WHERE b IS NOT NULL AND c NOT LIKE $1');
66
67 self::assertSame(['SELECT', 'IDENT', 'FROM', 'IDENT', 'WHERE', 'IDENT', 'IS', 'NOT', 'NULL_P', 'AND', 'IDENT', 'NOT_LA', 'LIKE', 'PARAM', '$end'], array_map(static fn (Token $token): string => $token->name, $tokens));
68 }
69
70 public function testTokenizeRejectsAnUnterminatedString(): void
71 {
72 $this->expectException(LexicalException::class);
73
74 (new PostgreSqlParser())->tokenize("SELECT 'abc");
75 }
76
77 public function testParse(): void
78 {
79 $sql = 'SELECT id FROM users WHERE id = $1; INSERT INTO t (a) VALUES (1) RETURNING a';
80 $tree = (new PostgreSqlParser())->parse($sql);
81
82 self::assertSame('parse_toplevel', $tree->name);
83 self::assertCount(2, array_filter($tree->find('stmt'), static fn ($node): bool => !$node->isEmpty()));
84 self::assertSame('SELECT id FROM users WHERE id = $1', $tree->find('SelectStmt')[0]->text($sql));
85 }
86
87 public function testParseWritesTheTextBackWithItsCommentsAndWhitespace(): void
88 {
89 $sql = " SELECT /* one */ a NOT IN (1,2)\nFROM t ; -- done\n";
90 $unicode = "SELECT U&'d\\0061t' UESCAPE '!' FROM t";
91
92 self::assertSame($sql, (new PostgreSqlParser())->parse($sql)->toString());
93 self::assertSame($unicode, (new PostgreSqlParser())->parse($unicode)->toString());
94 self::assertSame("-- nothing at all\n/* either */", (new PostgreSqlParser())->parse("-- nothing at all\n/* either */")->toString());
95 }
96
97 public function testParseAcceptsAnEmptyInput(): void
98 {
99 self::assertSame('parse_toplevel', (new PostgreSqlParser())->parse('')->name);
100 }
101
102 public function testParseRejectsAChainedComparison(): void
103 {
104 $this->expectException(SyntaxException::class);
105
106 (new PostgreSqlParser())->parse('SELECT 1 = 2 = 3');
107 }
108
109 public function testVersions(): void
110 {
111 self::assertSame(['pg-17.2'], PostgreSqlParser::versions());
112 }
113}
114