Skip to content

Commit 06cb959

Browse files
committed
Add structured MySQL CREATE and ALTER partition grammar
Parse HASH, LINEAR HASH, KEY, LINEAR KEY, RANGE and LIST methods, including COLUMNS variants, KEY ALGORITHM, partition counts, HASH/KEY subpartitioning, partition value lists and recognized definition options. Represent methods, expressions, names and definitions as traversable AST nodes and emit canonical SQL without preserving unchecked clause tails. Add standalone ALTER partition maintenance and EXCHANGE operations with native modifier placement and action-list boundaries. Parse repartitioning and REMOVE PARTITIONING tails separately from comma-separated actions. Preserve destination routing metadata when EXCHANGE names a source table. Enforce native bit_expr boundaries and numeric-token rules, including wide hexadecimal metadata. Reject opaque legacy expression fragments and PostgreSQL array/postfix syntax in partition operands. Treat ARRAY as a normal MySQL identifier/function token while retaining PostgreSQL behavior. Append node kinds without changing the 48-byte AST and retain conservative local-planner and parameterization guards. Register nine regression groups covering methods, malformed boundaries, canonical round trips, AST clone/walk/edit behavior and metadata guards. Remove the obsolete blanket rejection of partitioned CREATE TABLE. Validation: forced full rebuild and make test passed 1,683 tests with 37 external-database skips; ASan/UBSan with leak detection passed 167 focused MySQL tests. PostgreSQL harness passed 87 tests and all 5,122 CI cases. Independent review verified malformed inputs, stable valid emission and 6,138 constrained-arena sanitizer parses. Seven parser/emitter benchmark smoke cases ran; no benchmark sources or results are included.
1 parent 066baeb commit 06cb959

10 files changed

Lines changed: 658 additions & 8 deletions

‎Makefile‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ TEST_SRCS = $(TEST_DIR)/test_main.cpp \
7777
$(TEST_DIR)/test_mysql_optimizer_hints.cpp \
7878
$(TEST_DIR)/test_mysql_json_table.cpp \
7979
$(TEST_DIR)/test_mysql_table_ddl.cpp \
80+
$(TEST_DIR)/test_mysql_partition.cpp \
81+
$(TEST_DIR)/test_mysql_partition_alter.cpp \
8082
$(TEST_DIR)/test_mysql_procedure.cpp \
8183
$(TEST_DIR)/test_mysql_grouping.cpp \
8284
$(TEST_DIR)/test_mysql_explain.cpp \

‎include/sql_engine/plan_builder.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ class PlanBuilder {
199199
case NodeType::NODE_MYSQL_JSON_AGG_ARGUMENT:
200200
case NodeType::NODE_MYSQL_CREATE_LIKE:
201201
case NodeType::NODE_MYSQL_CREATE_QUERY:
202+
case NodeType::NODE_MYSQL_PARTITION_CLAUSE:
203+
case NodeType::NODE_MYSQL_PARTITION_DEF:
204+
case NodeType::NODE_MYSQL_PARTITION_ACTION:
205+
case NodeType::NODE_MYSQL_PARTITION_NAMES:
202206
case NodeType::NODE_MYSQL_CREATE_TABLE:
203207
case NodeType::NODE_MYSQL_ALTER_TABLE:
204208
case NodeType::NODE_MYSQL_COLUMN_DEF:

‎include/sql_parser/common.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,11 @@ enum class NodeType : uint16_t {
391391
NODE_MYSQL_CREATE_LIKE, // LIKE; structured source relation child
392392
NODE_MYSQL_CREATE_QUERY, // AS / IGNORE AS / REPLACE AS; query AST child
393393

394+
NODE_MYSQL_PARTITION_CLAUSE, // PARTITION BY; method, counts and partition definitions
395+
NODE_MYSQL_PARTITION_DEF, // PARTITION/SUBPARTITION; name, values, options and subpartitions
396+
NODE_MYSQL_PARTITION_ACTION, // standalone ALTER partition command; structured operands
397+
NODE_MYSQL_PARTITION_NAMES, // comma-separated partition identifiers
398+
394399
};
395400

396401
} // namespace sql_parser

‎include/sql_parser/emitter.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@ class Emitter {
203203
sb_.append("ALL "); emit_node(node->first_child); break;
204204
case NodeType::NODE_MYSQL_CREATE_LIKE:
205205
case NodeType::NODE_MYSQL_CREATE_QUERY:
206+
case NodeType::NODE_MYSQL_PARTITION_CLAUSE:
207+
case NodeType::NODE_MYSQL_PARTITION_DEF:
208+
case NodeType::NODE_MYSQL_PARTITION_ACTION:
206209
case NodeType::NODE_MYSQL_CREATE_TABLE:
207210
case NodeType::NODE_MYSQL_ALTER_TABLE:
208211
case NodeType::NODE_MYSQL_COLUMN_DEF:
@@ -219,6 +222,7 @@ class Emitter {
219222
case NodeType::NODE_MYSQL_DDL_LIST:
220223
case NodeType::NODE_MYSQL_PROCEDURE_PARAMS:
221224
sb_.append_char('('); emit_list(node, ", "); sb_.append_char(')'); break;
225+
case NodeType::NODE_MYSQL_PARTITION_NAMES:
222226
case NodeType::NODE_MYSQL_ALTER_ACTIONS:
223227
emit_list(node, ", "); break;
224228
case NodeType::NODE_MYSQL_JSON_TABLE: {

‎include/sql_parser/expression_parser.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,8 @@ class ExpressionParser {
652652
}
653653
case TokenType::TK_ARRAY: {
654654
tok_.skip();
655+
// ARRAY is a nonreserved MySQL identifier/function name.
656+
if constexpr (D == Dialect::MySQL) return parse_identifier_or_function(t);
655657
return parse_array_constructor();
656658
}
657659
case TokenType::TK_ROW: {

‎include/sql_parser/mysql_ddl_parser.h‎

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33

44
#include "sql_parser/expression_parser.h"
55
#include "sql_parser/mysql_type_parser.h"
6+
#include "sql_parser/mysql_partition_parser.h"
67
#include "sql_parser/mysql_value_syntax.h"
78
#include "sql_parser/parse_result.h"
89
#include "sql_parser/subquery_parse_callback.h"
910

1011
namespace sql_parser {
1112

12-
// Bounded native CREATE/ALTER TABLE grammar. Unsupported partitioning and
13-
// specialized index options remain explicit errors.
13+
// Bounded native CREATE/ALTER TABLE grammar. Unsupported specialized table
14+
// and index options remain explicit errors.
1415
// Lists, columns, expressions, constraints and options have separate AST nodes;
1516
// syntax leaves are only emitted after matching their native production.
1617
class MySQLDdlParser {
@@ -36,9 +37,7 @@ class MySQLDdlParser {
3637
if (create) {
3738
create_body(root);
3839
} else {
39-
auto* actions = node(NodeType::NODE_MYSQL_ALTER_ACTIONS);
40-
do { add(actions, action()); } while (!failed_ && take(TokenType::TK_COMMA));
41-
add(root, actions);
40+
alter_body(root);
4241
}
4342
if (!terminal()) fail();
4443
ParseResult result;
@@ -144,11 +143,13 @@ class MySQLDdlParser {
144143
require(TokenType::TK_RPAREN);
145144
add(root, definitions);
146145
}
147-
while (!failed_ && !terminal() && !query_suffix()) {
146+
while (!failed_ && !terminal() && !query_suffix() && !is("PARTITION")) {
148147
add(root, table_option());
149148
// A comma separates table options, never the following query.
150-
if (take(TokenType::TK_COMMA) && (terminal() || query_suffix())) fail();
149+
if (take(TokenType::TK_COMMA) && (terminal() || query_suffix() || is("PARTITION"))) fail();
151150
}
151+
if (!failed_ && is("PARTITION"))
152+
add(root, MySQLPartitionParser(tok_, arena_, callback_).parse());
152153
if (failed_ || terminal()) return;
153154
const char* prefix = "AS";
154155
if (take("IGNORE")) prefix = "IGNORE AS";
@@ -165,6 +166,87 @@ class MySQLDdlParser {
165166
add(root, source);
166167
}
167168

169+
bool partition_tail() { return is("PARTITION") || is("REMOVE"); }
170+
bool partition_action_start() {
171+
if (!one_of({"ADD", "DROP", "REBUILD", "OPTIMIZE", "ANALYZE", "CHECK",
172+
"REPAIR", "COALESCE", "TRUNCATE", "REORGANIZE", "EXCHANGE"})) return false;
173+
auto look = tok_; look.skip();
174+
return word(look.peek(), "PARTITION");
175+
}
176+
bool alter_modifier() { return one_of({"ALGORITHM", "LOCK", "WITH", "WITHOUT"}); }
177+
AstNode* validation() {
178+
auto* result = clause();
179+
if (!take("WITH", result) && !take("WITHOUT", result)) fail();
180+
require("VALIDATION", result); return result;
181+
}
182+
AstNode* partition_names(bool allow_all) {
183+
if (allow_all && is("ALL")) return token_node(NodeType::NODE_MYSQL_DDL_SYNTAX);
184+
auto* names = node(NodeType::NODE_MYSQL_PARTITION_NAMES);
185+
do { add(names, identifier()); } while (!failed_ && take(TokenType::TK_COMMA));
186+
return names;
187+
}
188+
AstNode* partition_action() {
189+
const Token first = tok_.peek();
190+
auto* result = token_node(NodeType::NODE_MYSQL_PARTITION_ACTION);
191+
require("PARTITION", result);
192+
const bool binlog = word(first, "ADD") || word(first, "REBUILD") || word(first, "OPTIMIZE") ||
193+
word(first, "ANALYZE") || word(first, "REPAIR") || word(first, "COALESCE") ||
194+
word(first, "REORGANIZE");
195+
if (binlog && !take("NO_WRITE_TO_BINLOG", result)) take("LOCAL", result);
196+
MySQLPartitionParser partition(tok_, arena_, callback_);
197+
if (word(first, "ADD")) {
198+
if (take("PARTITIONS", result)) add(result, partition.number());
199+
else if (at(TokenType::TK_LPAREN)) add(result, partition.definitions());
200+
} else if (word(first, "COALESCE")) add(result, partition.number());
201+
else if (word(first, "REORGANIZE")) {
202+
if (!terminal()) {
203+
add(result, partition_names(false)); require("INTO", result);
204+
add(result, partition.definitions());
205+
}
206+
} else if (word(first, "EXCHANGE")) {
207+
add(result, identifier()); require("WITH", result); require("TABLE", result);
208+
add(result, name());
209+
if (one_of({"WITH", "WITHOUT"})) add(result, validation());
210+
} else {
211+
add(result, partition_names(!word(first, "DROP")));
212+
if (word(first, "CHECK")) {
213+
while (!failed_ && one_of({"QUICK", "FAST", "MEDIUM", "EXTENDED", "CHANGED", "FOR"})) {
214+
if (take("FOR", result)) require("UPGRADE", result); else syntax(result);
215+
}
216+
} else if (word(first, "REPAIR")) {
217+
while (!failed_ && one_of({"QUICK", "EXTENDED", "USE_FRM"})) syntax(result);
218+
}
219+
}
220+
return result;
221+
}
222+
void alter_body(AstNode* root) {
223+
auto* actions = node(NodeType::NODE_MYSQL_ALTER_ACTIONS);
224+
bool modifiers_only = true;
225+
while (!failed_ && !partition_tail()) {
226+
if (partition_action_start()) {
227+
if (!modifiers_only) { fail(); break; }
228+
add(actions, partition_action());
229+
// Standalone partition actions cannot have trailing modifiers
230+
// or ordinary column/index actions.
231+
if (!terminal()) fail();
232+
break;
233+
}
234+
const bool modifier = alter_modifier();
235+
add(actions, one_of({"WITH", "WITHOUT"}) ? validation() : action());
236+
modifiers_only = modifiers_only && modifier;
237+
if (!take(TokenType::TK_COMMA)) break;
238+
if (partition_tail()) { fail(); break; }
239+
}
240+
if (actions && actions->first_child) add(root, actions);
241+
if (failed_) return;
242+
// Native repartition/removal follows ordinary actions without a comma.
243+
if (is("PARTITION")) add(root, MySQLPartitionParser(tok_, arena_, callback_).parse());
244+
else if (is("REMOVE")) {
245+
auto* remove = clause(); require("REMOVE", remove); require("PARTITIONING", remove);
246+
add(root, remove);
247+
}
248+
}
249+
168250
AstNode* expression() {
169251
ExpressionParser<Dialect::MySQL> parser(tok_, arena_, true);
170252
if (callback_) parser.set_subquery_callback(callback_);

0 commit comments

Comments
 (0)