Skip to content

Commit f1e896f

Browse files
authored
Merge pull request #59 from ProxySQL/fix/shard-having-join-2pc
fix: close remaining sharding correctness holes
2 parents e742dfd + 886f741 commit f1e896f

12 files changed

Lines changed: 448 additions & 29 deletions

include/sql_engine/distributed_planner.h

Lines changed: 130 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -999,12 +999,72 @@ class DistributedPlanner {
999999
return result;
10001000
}
10011001

1002-
// Case 5: Cross-backend join
1002+
bool join_on_shard_keys(const sql_parser::AstNode* cond,
1003+
sql_parser::StringRef left_key,
1004+
sql_parser::StringRef right_key) const {
1005+
if (!cond || cond->type != sql_parser::NodeType::NODE_BINARY_OP) return false;
1006+
sql_parser::StringRef op = cond->value();
1007+
if (op.len != 1 || op.ptr[0] != '=') return false;
1008+
const sql_parser::AstNode* l = cond->first_child;
1009+
const sql_parser::AstNode* r = l ? l->next_sibling : nullptr;
1010+
if (!l || !r) return false;
1011+
return (is_shard_key_ref(l, left_key) && is_shard_key_ref(r, right_key)) ||
1012+
(is_shard_key_ref(l, right_key) && is_shard_key_ref(r, left_key));
1013+
}
1014+
1015+
PlanNode* distribute_colocated_join(PlanNode* join_node,
1016+
const TableInfo* left_table,
1017+
const TableInfo* right_table) {
1018+
ScanContext lctx = extract_scan_context(join_node->left);
1019+
ScanContext rctx = extract_scan_context(join_node->right);
1020+
const sql_parser::AstNode* where_expr = nullptr;
1021+
if (lctx.where_expr && rctx.where_expr) {
1022+
sql_parser::AstNode* and_node = sql_parser::make_node(
1023+
arena_, sql_parser::NodeType::NODE_BINARY_OP,
1024+
sql_parser::StringRef{"AND", 3});
1025+
and_node->add_child(const_cast<sql_parser::AstNode*>(lctx.where_expr));
1026+
and_node->add_child(const_cast<sql_parser::AstNode*>(rctx.where_expr));
1027+
where_expr = and_node;
1028+
} else if (lctx.where_expr) {
1029+
where_expr = lctx.where_expr;
1030+
} else {
1031+
where_expr = rctx.where_expr;
1032+
}
1033+
1034+
const auto& shard_list = shards_.get_shards(left_table->table_name);
1035+
PlanNode* current = nullptr;
1036+
for (const auto& shard : shard_list) {
1037+
sql_parser::StringRef sql = qb_.build_select_join(
1038+
left_table, right_table, join_node->join.condition, where_expr);
1039+
PlanNode* rs = make_remote_scan(shard.backend_name.c_str(), sql, left_table);
1040+
if (!current) {
1041+
current = rs;
1042+
} else {
1043+
PlanNode* union_node = make_plan_node(arena_, PlanNodeType::SET_OP);
1044+
union_node->set_op.op = SET_OP_UNION;
1045+
union_node->set_op.all = true;
1046+
union_node->left = current;
1047+
union_node->right = rs;
1048+
current = union_node;
1049+
}
1050+
}
1051+
return current ? current : join_node;
1052+
}
1053+
10031054
PlanNode* distribute_join(PlanNode* join_node) {
1004-
// Get tables from each side
10051055
const TableInfo* left_table = find_table(join_node->left);
10061056
const TableInfo* right_table = find_table(join_node->right);
10071057

1058+
if (left_table && right_table &&
1059+
shards_.is_sharded(left_table->table_name) &&
1060+
shards_.is_sharded(right_table->table_name) &&
1061+
shards_.same_routing(left_table->table_name, right_table->table_name) &&
1062+
join_on_shard_keys(join_node->join.condition,
1063+
shards_.get_shard_key(left_table->table_name),
1064+
shards_.get_shard_key(right_table->table_name))) {
1065+
return distribute_colocated_join(join_node, left_table, right_table);
1066+
}
1067+
10081068
PlanNode* left_dist = nullptr;
10091069
PlanNode* right_dist = nullptr;
10101070

@@ -1172,18 +1232,13 @@ class DistributedPlanner {
11721232
PlanNode* distribute_update(PlanNode* plan) {
11731233
const auto& up = plan->update_plan;
11741234
const TableInfo* table = up.table;
1175-
if (!table || !shards_.has_table(table->table_name)) return plan;
11761235

1177-
// Multi-table UPDATE: emit full SQL from AST, route to primary table's backend
11781236
if (up.original_ast) {
1179-
sql_parser::StringRef sql = qb_.build_update_from_ast(up.original_ast);
1180-
if (!shards_.is_sharded(table->table_name)) {
1181-
return make_remote_scan(shards_.get_backend(table->table_name), sql, table);
1182-
}
1183-
const auto& shard_list = shards_.get_shards(table->table_name);
1184-
return scatter_dml_to_shards(table, shard_list, [&]() { return sql; });
1237+
return distribute_multi_table_dml(up.original_ast, table, true);
11851238
}
11861239

1240+
if (!table || !shards_.has_table(table->table_name)) return plan;
1241+
11871242
// Check for cross-shard subqueries in WHERE and rewrite
11881243
const sql_parser::AstNode* where_expr = up.where_expr;
11891244
if (where_expr && has_subquery(where_expr) && remote_executor_) {
@@ -1220,18 +1275,13 @@ class DistributedPlanner {
12201275
PlanNode* distribute_delete(PlanNode* plan) {
12211276
const auto& dp = plan->delete_plan;
12221277
const TableInfo* table = dp.table;
1223-
if (!table || !shards_.has_table(table->table_name)) return plan;
12241278

1225-
// Multi-table DELETE: emit full SQL from AST, route to primary table's backend
12261279
if (dp.original_ast) {
1227-
sql_parser::StringRef sql = qb_.build_delete_from_ast(dp.original_ast);
1228-
if (!shards_.is_sharded(table->table_name)) {
1229-
return make_remote_scan(shards_.get_backend(table->table_name), sql, table);
1230-
}
1231-
const auto& shard_list = shards_.get_shards(table->table_name);
1232-
return scatter_dml_to_shards(table, shard_list, [&]() { return sql; });
1280+
return distribute_multi_table_dml(dp.original_ast, table, false);
12331281
}
12341282

1283+
if (!table || !shards_.has_table(table->table_name)) return plan;
1284+
12351285
// Check for cross-shard subqueries in WHERE and rewrite
12361286
const sql_parser::AstNode* where_expr = dp.where_expr;
12371287
if (where_expr && has_subquery(where_expr) && remote_executor_) {
@@ -1318,7 +1368,68 @@ class DistributedPlanner {
13181368
return false;
13191369
}
13201370

1321-
// Scatter DML SQL to all shards, combining results via UNION ALL
1371+
void collect_ast_table_names(const sql_parser::AstNode* n,
1372+
std::vector<sql_parser::StringRef>& out) const {
1373+
if (!n) return;
1374+
if (n->type == sql_parser::NodeType::NODE_TABLE_REF && n->first_child) {
1375+
const sql_parser::AstNode* name = n->first_child;
1376+
if (name->type == sql_parser::NodeType::NODE_IDENTIFIER) {
1377+
out.push_back(name->value());
1378+
} else if (name->type == sql_parser::NodeType::NODE_QUALIFIED_NAME) {
1379+
const sql_parser::AstNode* schema = name->first_child;
1380+
const sql_parser::AstNode* table = schema ? schema->next_sibling : nullptr;
1381+
if (table) out.push_back(table->value());
1382+
else if (schema) out.push_back(schema->value());
1383+
}
1384+
}
1385+
for (const sql_parser::AstNode* c = n->first_child; c; c = c->next_sibling) {
1386+
collect_ast_table_names(c, out);
1387+
}
1388+
}
1389+
1390+
PlanNode* distribute_multi_table_dml(const sql_parser::AstNode* ast,
1391+
const TableInfo* primary,
1392+
bool is_update) {
1393+
std::vector<sql_parser::StringRef> names;
1394+
collect_ast_table_names(ast, names);
1395+
const char* backend = nullptr;
1396+
bool saw_mapped = false;
1397+
for (sql_parser::StringRef name : names) {
1398+
if (!shards_.has_table(name)) continue;
1399+
saw_mapped = true;
1400+
if (shards_.is_sharded(name)) {
1401+
return fail_dml(is_update
1402+
? "multi-table UPDATE is not supported on sharded tables"
1403+
: "multi-table DELETE is not supported on sharded tables");
1404+
}
1405+
const char* b = shards_.get_backend(name);
1406+
if (backend && b && std::strcmp(backend, b) != 0) {
1407+
return fail_dml(is_update
1408+
? "multi-table UPDATE spans multiple backends"
1409+
: "multi-table DELETE spans multiple backends");
1410+
}
1411+
if (b) backend = b;
1412+
}
1413+
if (!backend && primary && shards_.has_table(primary->table_name)) {
1414+
if (shards_.is_sharded(primary->table_name)) {
1415+
return fail_dml(is_update
1416+
? "multi-table UPDATE is not supported on sharded tables"
1417+
: "multi-table DELETE is not supported on sharded tables");
1418+
}
1419+
backend = shards_.get_backend(primary->table_name);
1420+
saw_mapped = true;
1421+
}
1422+
if (!backend || !saw_mapped) {
1423+
return fail_dml(is_update
1424+
? "multi-table UPDATE is not supported on sharded tables"
1425+
: "multi-table DELETE is not supported on sharded tables");
1426+
}
1427+
sql_parser::StringRef sql = is_update
1428+
? qb_.build_update_from_ast(ast)
1429+
: qb_.build_delete_from_ast(ast);
1430+
return make_remote_scan(backend, sql, primary);
1431+
}
1432+
13221433
PlanNode* scatter_dml_to_shards(const TableInfo* table,
13231434
const std::vector<ShardInfo>& shard_list,
13241435
std::function<sql_parser::StringRef()> build_sql) {

include/sql_engine/distributed_txn.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,18 @@ class DistributedTransactionManager : public TransactionManager {
178178
return executor_.execute_dml(backend_name, sql);
179179
}
180180

181+
ResultSet route_query(const char* backend_name,
182+
sql_parser::StringRef sql) override {
183+
if (!active_) return executor_.execute(backend_name, sql);
184+
auto it = sessions_.find(backend_name);
185+
if (it != sessions_.end() && it->second) {
186+
return it->second->execute(sql);
187+
}
188+
return executor_.execute(backend_name, sql);
189+
}
190+
191+
bool route_query_supported() const override { return true; }
192+
181193
bool commit() override {
182194
if (!active_) return false;
183195
if (participants_.empty()) {

include/sql_engine/plan_executor.h

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
#include <vector>
5858
#include <memory>
5959
#include <cstdlib>
60+
#include <cstring>
6061

6162
namespace sql_engine {
6263

@@ -847,15 +848,128 @@ class PlanExecutor {
847848
return ptr;
848849
}
849850

851+
static bool same_agg_call(const sql_parser::AstNode* a, const sql_parser::AstNode* b) {
852+
if (!a || !b) return false;
853+
if (a->type != sql_parser::NodeType::NODE_FUNCTION_CALL ||
854+
b->type != sql_parser::NodeType::NODE_FUNCTION_CALL) return false;
855+
return a->value().equals_ci(b->value().ptr, b->value().len);
856+
}
857+
858+
const sql_parser::AstNode* rewrite_having_expr(const sql_parser::AstNode* expr,
859+
PlanNode* agg_node) {
860+
if (!expr || !agg_node) return expr;
861+
uint16_t group_count = 0;
862+
uint16_t agg_count = 0;
863+
const sql_parser::AstNode** agg_exprs = nullptr;
864+
const sql_parser::AstNode** group_by = nullptr;
865+
if (agg_node->type == PlanNodeType::AGGREGATE) {
866+
group_count = agg_node->aggregate.group_count;
867+
agg_count = agg_node->aggregate.agg_count;
868+
agg_exprs = agg_node->aggregate.agg_exprs;
869+
group_by = agg_node->aggregate.group_by;
870+
} else if (agg_node->type == PlanNodeType::MERGE_AGGREGATE) {
871+
group_count = agg_node->merge_aggregate.group_key_count;
872+
if (agg_node->merge_aggregate.output_exprs &&
873+
agg_node->merge_aggregate.output_expr_count > group_count) {
874+
agg_exprs = agg_node->merge_aggregate.output_exprs + group_count;
875+
agg_count = static_cast<uint16_t>(
876+
agg_node->merge_aggregate.output_expr_count - group_count);
877+
group_by = agg_node->merge_aggregate.output_exprs;
878+
}
879+
} else {
880+
return expr;
881+
}
882+
883+
if (expr->type == sql_parser::NodeType::NODE_FUNCTION_CALL && agg_exprs) {
884+
for (uint16_t i = 0; i < agg_count; ++i) {
885+
if (same_agg_call(expr, agg_exprs[i])) {
886+
sql_parser::StringRef name = expr->value();
887+
return sql_parser::make_node(
888+
arena_, sql_parser::NodeType::NODE_IDENTIFIER, name);
889+
}
890+
}
891+
}
892+
893+
bool changed = false;
894+
sql_parser::AstNode* clone = sql_parser::make_node(
895+
arena_, expr->type, expr->value(), expr->flags);
896+
for (const sql_parser::AstNode* c = expr->first_child; c; c = c->next_sibling) {
897+
const sql_parser::AstNode* rw = rewrite_having_expr(c, agg_node);
898+
if (rw != c) changed = true;
899+
if (rw) clone->add_child(const_cast<sql_parser::AstNode*>(rw));
900+
}
901+
(void)group_count;
902+
(void)group_by;
903+
return changed ? clone : expr;
904+
}
905+
906+
const TableInfo* make_agg_output_table(PlanNode* agg_node) {
907+
if (!agg_node) return nullptr;
908+
uint16_t group_count = 0;
909+
uint16_t agg_count = 0;
910+
const sql_parser::AstNode** group_by = nullptr;
911+
const sql_parser::AstNode** agg_exprs = nullptr;
912+
if (agg_node->type == PlanNodeType::AGGREGATE) {
913+
group_count = agg_node->aggregate.group_count;
914+
agg_count = agg_node->aggregate.agg_count;
915+
group_by = agg_node->aggregate.group_by;
916+
agg_exprs = agg_node->aggregate.agg_exprs;
917+
} else if (agg_node->type == PlanNodeType::MERGE_AGGREGATE &&
918+
agg_node->merge_aggregate.output_exprs) {
919+
group_count = agg_node->merge_aggregate.group_key_count;
920+
group_by = agg_node->merge_aggregate.output_exprs;
921+
if (agg_node->merge_aggregate.output_expr_count > group_count) {
922+
agg_exprs = agg_node->merge_aggregate.output_exprs + group_count;
923+
agg_count = static_cast<uint16_t>(
924+
agg_node->merge_aggregate.output_expr_count - group_count);
925+
}
926+
} else {
927+
return nullptr;
928+
}
929+
930+
uint16_t n = static_cast<uint16_t>(group_count + agg_count);
931+
if (n == 0) return nullptr;
932+
auto* cols = static_cast<ColumnInfo*>(arena_.allocate(sizeof(ColumnInfo) * n));
933+
if (!cols) return nullptr;
934+
for (uint16_t i = 0; i < group_count; ++i) {
935+
cols[i].ordinal = i;
936+
cols[i].nullable = true;
937+
cols[i].type = SqlType::make_int();
938+
cols[i].name = (group_by && group_by[i]) ? group_by[i]->value()
939+
: sql_parser::StringRef{};
940+
}
941+
for (uint16_t i = 0; i < agg_count; ++i) {
942+
cols[group_count + i].ordinal = static_cast<uint16_t>(group_count + i);
943+
cols[group_count + i].nullable = true;
944+
cols[group_count + i].type = SqlType::make_int();
945+
cols[group_count + i].name = (agg_exprs && agg_exprs[i])
946+
? agg_exprs[i]->value() : sql_parser::StringRef{};
947+
}
948+
auto* ti = static_cast<TableInfo*>(arena_.allocate(sizeof(TableInfo)));
949+
if (!ti) return nullptr;
950+
std::memset(ti, 0, sizeof(TableInfo));
951+
ti->columns = cols;
952+
ti->column_count = n;
953+
return ti;
954+
}
955+
850956
Operator* build_filter(PlanNode* node) {
851957
Operator* child = build_operator(node->left);
852958
if (!child && node->left) return nullptr;
853959

854960
std::vector<const TableInfo*> tables;
855-
collect_tables(node->left, tables);
961+
const sql_parser::AstNode* expr = node->filter.expr;
962+
if (node->left && (node->left->type == PlanNodeType::AGGREGATE ||
963+
node->left->type == PlanNodeType::MERGE_AGGREGATE)) {
964+
expr = rewrite_having_expr(expr, node->left);
965+
const TableInfo* synth = make_agg_output_table(node->left);
966+
if (synth) tables.push_back(synth);
967+
} else {
968+
collect_tables(node->left, tables);
969+
}
856970

857971
auto op = std::make_unique<FilterOperator<D>>(
858-
child, node->filter.expr, catalog_, tables, functions_, arena_,
972+
child, expr, catalog_, tables, functions_, arena_,
859973
&subquery_exec_, outer_resolver_);
860974
Operator* ptr = op.get();
861975
operators_.push_back(std::move(op));

include/sql_engine/remote_query_builder.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,28 @@ class RemoteQueryBuilder {
9494
return sb.finish();
9595
}
9696

97+
sql_parser::StringRef build_select_join(
98+
const TableInfo* left,
99+
const TableInfo* right,
100+
const sql_parser::AstNode* on_expr,
101+
const sql_parser::AstNode* where_expr)
102+
{
103+
sql_parser::StringBuilder sb(arena_, 512);
104+
sb.append("SELECT * FROM ");
105+
if (left) sb.append(left->table_name.ptr, left->table_name.len);
106+
sb.append(" JOIN ");
107+
if (right) sb.append(right->table_name.ptr, right->table_name.len);
108+
if (on_expr) {
109+
sb.append(" ON ");
110+
emit_expr(on_expr, sb);
111+
}
112+
if (where_expr) {
113+
sb.append(" WHERE ");
114+
emit_expr(where_expr, sb);
115+
}
116+
return sb.finish();
117+
}
118+
97119
// Build an INSERT statement string.
98120
sql_parser::StringRef build_insert(
99121
const TableInfo* table,

0 commit comments

Comments
 (0)