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