packages/sql-parser/src/PostgreSql/PostgreSqlParser.php
1<?php
2
3declare(strict_types=1);
4
5namespace SqlParser\PostgreSql;
6
7use RuntimeException;
8use SqlParser\Lexer\LexicalException;
9use SqlParser\Lexer\TerminalIndex;
10use SqlParser\Lexer\Token;
11use SqlParser\Parser\LrParser;
12use SqlParser\Parser\Node;
13use SqlParser\Parser\SqlParser;
14use SqlParser\Parser\SyntaxException;
15use SqlParser\PostgreSql\Lexer\KeywordTable;
16use SqlParser\PostgreSql\Lexer\PostgreSqlLexer;
17use SqlParser\Resource\VersionRegistry;
18use SqlParser\Table\ParseTable;
19use SqlParser\Table\TableFile;
20
21/**
22 * Parses PostgreSQL statements with the grammar of a chosen server release.
23 *
24 * The parse table is built from the `gram.y` of that release and the lexer
25 * follows its `scan.l`, so the tree names the nonterminals of the official
26 * grammar. Several statements separated by semicolons parse as one tree.
27 *
28 * @visibility public
29 *
30 * @example Parsing with the default release
31 * $parser = new \SqlParser\PostgreSql\PostgreSqlParser();
32 * $tree = $parser->parse('SELECT id FROM users WHERE id = $1');
33 * $tree->name // => 'parse_toplevel'
34 * count($tree->find('relation_expr')) // => 1
35 * @example Writing a parsed statement back
36 * $sql = "SELECT id FROM users -- everyone\n";
37 * $parser = new \SqlParser\PostgreSql\PostgreSqlParser();
38 * $parser->parse($sql)->toString() === $sql // => true
39 * @example Rejecting an unsupported release
40 * new \SqlParser\PostgreSql\PostgreSqlParser('pg-9.6.0') // throws \RuntimeException: Unsupported
41 */
42final class PostgreSqlParser implements SqlParser
43{
44 private readonly PostgreSqlVersion $version;
45
46 private readonly ParseTable $table;
47
48 private readonly PostgreSqlLexer $lexer;
49
50 /**
51 * Loads the parser of one release.
52 *
53 * @param string|null $version Release tag such as `pg-17.2`, or null for the newest shipped
54 * @param VersionRegistry $registry Record of shipped releases
55 *
56 * @throws RuntimeException When the release is not shipped or its resources are missing
57 */
58 public function __construct(?string $version = null, VersionRegistry $registry = new VersionRegistry())
59 {
60 $this->version = PostgreSqlVersion::resolve($version, $registry);
61 $this->table = (new TableFile())->load($this->version->release->tablePath);
62 $this->lexer = new PostgreSqlLexer(KeywordTable::load($this->version->release->keywordPath));
63 }
64
65 /**
66 * Answers the release the parser reads for.
67 *
68 * @return string Release tag such as `pg-17.2`
69 */
70 public function version(): string
71 {
72 return $this->version->name();
73 }
74
75 /**
76 * Reads SQL text into the terminals of the grammar, the end marker last.
77 *
78 * A token carries the whitespace and comments skipped before it and the
79 * end marker what follows the last of them, so the tokens hold every byte
80 * of the text.
81 *
82 * @param string $sql The SQL text
83 *
84 * @return list<Token> The tokens in text order
85 *
86 * @throws LexicalException When the text holds something no token starts with
87 */
88 public function tokenize(string $sql): array
89 {
90 return (new TerminalIndex($this->table->symbols))->tokens($this->lexer->scan($sql), $sql);
91 }
92
93 /**
94 * Parses one or more statements.
95 *
96 * @param string $sql The SQL text
97 *
98 * @return Node The tree, rooted at the grammar's start symbol, holding every byte of the text
99 *
100 * @throws LexicalException When the text holds something no token starts with
101 * @throws SyntaxException When the text is not in the grammar of the release
102 */
103 public function parse(string $sql): Node
104 {
105 return (new LrParser($this->table))->parse($this->tokenize($sql), $sql);
106 }
107
108 /**
109 * Answers every release tag the package ships a PostgreSQL grammar for, oldest first.
110 *
111 * @return list<string> Release tags
112 */
113 public static function versions(): array
114 {
115 return (new VersionRegistry())->names(PostgreSqlVersion::DIALECT);
116 }
117}
118