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