packages/sql-parser/src/MySql/MySqlParser.php

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