Skip to content

Commit 43f4c39

Browse files
committed
feat: multi-table JOIN UPDATE/DELETE distributed execution (#32)
Detect multi-table UPDATE/DELETE in DmlPlanBuilder (via FROM_CLAUSE or USING clause presence), store original AST in plan node, and emit full SQL from AST in distributed planner for shard routing.
1 parent cc1aa85 commit 43f4c39

5 files changed

Lines changed: 121 additions & 0 deletions

File tree

include/sql_engine/distributed_planner.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,16 @@ class DistributedPlanner {
10181018
const TableInfo* table = up.table;
10191019
if (!table || !shards_.has_table(table->table_name)) return plan;
10201020

1021+
// Multi-table UPDATE: emit full SQL from AST, route to primary table's backend
1022+
if (up.original_ast) {
1023+
sql_parser::StringRef sql = qb_.build_update_from_ast(up.original_ast);
1024+
if (!shards_.is_sharded(table->table_name)) {
1025+
return make_remote_scan(shards_.get_backend(table->table_name), sql, table);
1026+
}
1027+
const auto& shard_list = shards_.get_shards(table->table_name);
1028+
return scatter_dml_to_shards(table, shard_list, [&]() { return sql; });
1029+
}
1030+
10211031
// Check for cross-shard subqueries in WHERE and rewrite
10221032
const sql_parser::AstNode* where_expr = up.where_expr;
10231033
if (where_expr && has_subquery(where_expr) && remote_executor_) {
@@ -1057,6 +1067,16 @@ class DistributedPlanner {
10571067
const TableInfo* table = dp.table;
10581068
if (!table || !shards_.has_table(table->table_name)) return plan;
10591069

1070+
// Multi-table DELETE: emit full SQL from AST, route to primary table's backend
1071+
if (dp.original_ast) {
1072+
sql_parser::StringRef sql = qb_.build_delete_from_ast(dp.original_ast);
1073+
if (!shards_.is_sharded(table->table_name)) {
1074+
return make_remote_scan(shards_.get_backend(table->table_name), sql, table);
1075+
}
1076+
const auto& shard_list = shards_.get_shards(table->table_name);
1077+
return scatter_dml_to_shards(table, shard_list, [&]() { return sql; });
1078+
}
1079+
10601080
// Check for cross-shard subqueries in WHERE and rewrite
10611081
const sql_parser::AstNode* where_expr = dp.where_expr;
10621082
if (where_expr && has_subquery(where_expr) && remote_executor_) {

include/sql_engine/dml_plan_builder.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ class DmlPlanBuilder {
115115
node->update_plan.table = resolve_table(table_ref);
116116
}
117117

118+
// Detect multi-table UPDATE: presence of FROM_CLAUSE child means JOINs
119+
const sql_parser::AstNode* from_clause = find_child(update_ast, sql_parser::NodeType::NODE_FROM_CLAUSE);
120+
if (from_clause) {
121+
node->update_plan.original_ast = update_ast;
122+
}
123+
118124
// Find UPDATE_SET_CLAUSE -> extract SET items
119125
const sql_parser::AstNode* set_clause = find_child(update_ast, sql_parser::NodeType::NODE_UPDATE_SET_CLAUSE);
120126
if (set_clause) {
@@ -166,6 +172,23 @@ class DmlPlanBuilder {
166172
node->delete_plan.table = resolve_table(table_ref);
167173
}
168174

175+
// Detect multi-table DELETE: FROM_CLAUSE (MySQL multi-table) or USING clause
176+
const sql_parser::AstNode* from_clause = find_child(delete_ast, sql_parser::NodeType::NODE_FROM_CLAUSE);
177+
const sql_parser::AstNode* using_clause = find_child(delete_ast, sql_parser::NodeType::NODE_DELETE_USING_CLAUSE);
178+
if (using_clause) {
179+
// USING always indicates multi-table
180+
node->delete_plan.original_ast = delete_ast;
181+
} else if (from_clause) {
182+
// FROM_CLAUSE with multiple children indicates multi-table
183+
uint16_t child_count = 0;
184+
for (const sql_parser::AstNode* c = from_clause->first_child; c; c = c->next_sibling) {
185+
++child_count;
186+
}
187+
if (child_count > 1) {
188+
node->delete_plan.original_ast = delete_ast;
189+
}
190+
}
191+
169192
// Find WHERE_CLAUSE
170193
const sql_parser::AstNode* where = find_child(delete_ast, sql_parser::NodeType::NODE_WHERE_CLAUSE);
171194
if (where && where->first_child) {

include/sql_engine/plan_node.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,13 @@ struct PlanNode {
152152
const sql_parser::AstNode** set_exprs; // new value expression AST nodes (parallel array)
153153
uint16_t set_count;
154154
const sql_parser::AstNode* where_expr; // WHERE condition (nullable = update all)
155+
const sql_parser::AstNode* original_ast; // non-null for multi-table UPDATE
155156
} update_plan;
156157

157158
struct {
158159
const TableInfo* table;
159160
const sql_parser::AstNode* where_expr; // WHERE condition (nullable = delete all)
161+
const sql_parser::AstNode* original_ast; // non-null for multi-table DELETE
160162
} delete_plan;
161163
};
162164
};

include/sql_engine/remote_query_builder.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,18 @@ class RemoteQueryBuilder {
188188
return sb.finish();
189189
}
190190

191+
sql_parser::StringRef build_update_from_ast(const sql_parser::AstNode* ast) {
192+
sql_parser::Emitter<D> emitter(arena_);
193+
emitter.emit(ast);
194+
return emitter.result();
195+
}
196+
197+
sql_parser::StringRef build_delete_from_ast(const sql_parser::AstNode* ast) {
198+
sql_parser::Emitter<D> emitter(arena_);
199+
emitter.emit(ast);
200+
return emitter.result();
201+
}
202+
191203
private:
192204
sql_parser::Arena& arena_;
193205

tests/test_dml.cpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,67 @@ TEST_F(DmlTest, InsertWithoutColumnList) {
251251
auto rs = run_select("SELECT * FROM users");
252252
EXPECT_EQ(rs.rows[3].get(0).int_val, 6);
253253
}
254+
255+
// ---- Multi-table UPDATE/DELETE plan node tests ----
256+
257+
// Multi-table UPDATE sets original_ast in the plan node
258+
TEST_F(DmlTest, MultiTableUpdateSetsOriginalAst) {
259+
const char* sql = "UPDATE users u JOIN orders o ON u.id = o.user_id SET u.age = 30 WHERE o.total > 100";
260+
parser.reset();
261+
auto r = parser.parse(sql, std::strlen(sql));
262+
ASSERT_EQ(r.status, ParseResult::OK);
263+
ASSERT_NE(r.ast, nullptr);
264+
265+
DmlPlanBuilder<Dialect::MySQL> dml_builder(catalog, parser.arena());
266+
PlanNode* plan = dml_builder.build(r.ast);
267+
ASSERT_NE(plan, nullptr);
268+
EXPECT_EQ(plan->type, PlanNodeType::UPDATE_PLAN);
269+
EXPECT_NE(plan->update_plan.original_ast, nullptr);
270+
EXPECT_EQ(plan->update_plan.original_ast, r.ast);
271+
}
272+
273+
// Single-table UPDATE leaves original_ast as nullptr
274+
TEST_F(DmlTest, SingleTableUpdateOriginalAstNull) {
275+
const char* sql = "UPDATE users SET age = 30 WHERE id = 1";
276+
parser.reset();
277+
auto r = parser.parse(sql, std::strlen(sql));
278+
ASSERT_EQ(r.status, ParseResult::OK);
279+
ASSERT_NE(r.ast, nullptr);
280+
281+
DmlPlanBuilder<Dialect::MySQL> dml_builder(catalog, parser.arena());
282+
PlanNode* plan = dml_builder.build(r.ast);
283+
ASSERT_NE(plan, nullptr);
284+
EXPECT_EQ(plan->type, PlanNodeType::UPDATE_PLAN);
285+
EXPECT_EQ(plan->update_plan.original_ast, nullptr);
286+
}
287+
288+
// Multi-table DELETE (MySQL syntax) sets original_ast
289+
TEST_F(DmlTest, MultiTableDeleteSetsOriginalAst) {
290+
const char* sql = "DELETE u FROM users u JOIN orders o ON u.id = o.user_id WHERE o.total > 100";
291+
parser.reset();
292+
auto r = parser.parse(sql, std::strlen(sql));
293+
ASSERT_EQ(r.status, ParseResult::OK);
294+
ASSERT_NE(r.ast, nullptr);
295+
296+
DmlPlanBuilder<Dialect::MySQL> dml_builder(catalog, parser.arena());
297+
PlanNode* plan = dml_builder.build(r.ast);
298+
ASSERT_NE(plan, nullptr);
299+
EXPECT_EQ(plan->type, PlanNodeType::DELETE_PLAN);
300+
EXPECT_NE(plan->delete_plan.original_ast, nullptr);
301+
EXPECT_EQ(plan->delete_plan.original_ast, r.ast);
302+
}
303+
304+
// Single-table DELETE leaves original_ast as nullptr
305+
TEST_F(DmlTest, SingleTableDeleteOriginalAstNull) {
306+
const char* sql = "DELETE FROM users WHERE id = 1";
307+
parser.reset();
308+
auto r = parser.parse(sql, std::strlen(sql));
309+
ASSERT_EQ(r.status, ParseResult::OK);
310+
ASSERT_NE(r.ast, nullptr);
311+
312+
DmlPlanBuilder<Dialect::MySQL> dml_builder(catalog, parser.arena());
313+
PlanNode* plan = dml_builder.build(r.ast);
314+
ASSERT_NE(plan, nullptr);
315+
EXPECT_EQ(plan->type, PlanNodeType::DELETE_PLAN);
316+
EXPECT_EQ(plan->delete_plan.original_ast, nullptr);
317+
}

0 commit comments

Comments
 (0)