Skip to content

Commit d6e1b34

Browse files
committed
test: broaden coverage for the #56 P0 fixes
Add cases for qualified non-key UPDATE, same-value and multi-row moves, string/int HASH including negatives and leading zeros, empty IN-subquery DML, string IN-list delete, INSERT...SELECT empty source, unknown-table SELECT/UPDATE/DELETE, plan-cache COUNT/SUM twice, composite qualified move, RIGHT JOIN emit, LIST OR prune, and partial vs full composite HASH prune.
1 parent ecde031 commit d6e1b34

3 files changed

Lines changed: 311 additions & 0 deletions

File tree

tests/test_distributed_dml.cpp

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,3 +986,178 @@ TEST_F(DistributedDmlTest, PlanCacheSeesUpdatedShardMap) {
986986
EXPECT_EQ(mock_executor.get_executed_sqls(s).size(), 0u) << s;
987987
}
988988
}
989+
990+
TEST_F(DistributedDmlTest, UpdateQualifiedNonKeyDoesNotMove) {
991+
execute_distributed_dml("INSERT INTO users (id, name, age) VALUES (3, 'Carol', 17)");
992+
const char* home = backend_for_id(3);
993+
auto result = execute_distributed_dml("UPDATE users SET users.age = 40 WHERE id = 3");
994+
EXPECT_TRUE(result.success) << result.error_message;
995+
EXPECT_EQ(row_count_on(home, "users"), 1u);
996+
EXPECT_EQ(mock_executor.total_row_count("users"), 1u);
997+
}
998+
999+
TEST_F(DistributedDmlTest, UpdateShardKeyToSameValue) {
1000+
execute_distributed_dml("INSERT INTO users (id, name, age) VALUES (3, 'Carol', 17)");
1001+
auto result = execute_distributed_dml("UPDATE users SET id = 3 WHERE id = 3");
1002+
EXPECT_TRUE(result.success) << result.error_message;
1003+
EXPECT_EQ(row_count_on(backend_for_id(3), "users"), 1u);
1004+
EXPECT_EQ(mock_executor.total_row_count("users"), 1u);
1005+
}
1006+
1007+
TEST_F(DistributedDmlTest, UpdateMovesThenPointSelectAndDelete) {
1008+
execute_distributed_dml("INSERT INTO users (id, name, age) VALUES (3, 'Carol', 17)");
1009+
ASSERT_STRNE(backend_for_id(3), backend_for_id(9));
1010+
EXPECT_TRUE(execute_distributed_dml("UPDATE users SET id = 9 WHERE id = 3").success);
1011+
EXPECT_EQ(execute_distributed_select("SELECT name FROM users WHERE id = 3").row_count(), 0u);
1012+
EXPECT_EQ(execute_distributed_select("SELECT name FROM users WHERE id = 9").row_count(), 1u);
1013+
EXPECT_TRUE(execute_distributed_dml("DELETE FROM users WHERE id = 9").success);
1014+
EXPECT_EQ(mock_executor.total_row_count("users"), 0u);
1015+
}
1016+
1017+
TEST_F(DistributedDmlTest, UpdateMovesMultipleRowsToOneShard) {
1018+
execute_distributed_dml("INSERT INTO users (id, name, age) VALUES (3, 'A', 1)");
1019+
execute_distributed_dml("INSERT INTO users (id, name, age) VALUES (4, 'B', 2)");
1020+
EXPECT_TRUE(execute_distributed_dml("UPDATE users SET id = 9 WHERE id IN (3, 4)").success);
1021+
EXPECT_EQ(row_count_on(backend_for_id(9), "users"), 2u);
1022+
EXPECT_EQ(mock_executor.total_row_count("users"), 2u);
1023+
}
1024+
1025+
TEST_F(DistributedDmlTest, InsertNegativeStringInt) {
1026+
auto ins = execute_distributed_dml(
1027+
"INSERT INTO users (id, name, age) VALUES ('-7', 'Neg', 1)");
1028+
EXPECT_TRUE(ins.success) << ins.error_message;
1029+
EXPECT_EQ(row_count_on(backend_for_id(-7), "users"), 1u);
1030+
EXPECT_EQ(execute_distributed_select("SELECT name FROM users WHERE id = -7").row_count(), 1u);
1031+
}
1032+
1033+
TEST_F(DistributedDmlTest, InsertLeadingZeroStringInt) {
1034+
auto ins = execute_distributed_dml(
1035+
"INSERT INTO users (id, name, age) VALUES ('03', 'Zed', 1)");
1036+
EXPECT_TRUE(ins.success) << ins.error_message;
1037+
EXPECT_EQ(row_count_on(backend_for_id(3), "users"), 1u);
1038+
EXPECT_EQ(execute_distributed_select("SELECT name FROM users WHERE id = 3").row_count(), 1u);
1039+
}
1040+
1041+
TEST_F(DistributedDmlTest, EmptyInSubqueryDeletesNothing) {
1042+
execute_distributed_dml("INSERT INTO users (id, name, age) VALUES (3, 'Carol', 17)");
1043+
mock_executor.clear_sql_logs();
1044+
auto result = execute_distributed_dml(
1045+
"DELETE FROM users WHERE id IN (SELECT user_id FROM orders)");
1046+
EXPECT_TRUE(result.success) << result.error_message;
1047+
EXPECT_EQ(mock_executor.total_row_count("users"), 1u);
1048+
}
1049+
1050+
TEST_F(DistributedDmlTest, EmptyInSubqueryUpdateTouchesNothing) {
1051+
execute_distributed_dml("INSERT INTO users (id, name, age) VALUES (3, 'Carol', 17)");
1052+
auto result = execute_distributed_dml(
1053+
"UPDATE users SET age = 99 WHERE id IN (SELECT user_id FROM orders)");
1054+
EXPECT_TRUE(result.success) << result.error_message;
1055+
auto got = execute_distributed_select("SELECT age FROM users WHERE id = 3");
1056+
ASSERT_EQ(got.row_count(), 1u);
1057+
EXPECT_EQ(got.rows[0].get(0).int_val, 17);
1058+
}
1059+
1060+
TEST_F(DistributedDmlTest, InSubqueryStringNames) {
1061+
execute_distributed_dml("INSERT INTO users (id, name, age) VALUES (3, 'Carol', 17)");
1062+
execute_distributed_dml("INSERT INTO users (id, name, age) VALUES (4, 'Dave', 18)");
1063+
auto result = execute_distributed_dml(
1064+
"DELETE FROM users WHERE name IN (SELECT name FROM users WHERE id = 3)");
1065+
EXPECT_TRUE(result.success) << result.error_message;
1066+
EXPECT_EQ(mock_executor.total_row_count("users"), 1u);
1067+
EXPECT_EQ(execute_distributed_select("SELECT name FROM users WHERE id = 4").row_count(), 1u);
1068+
}
1069+
1070+
TEST_F(DistributedDmlTest, InsertSelectEmptySourceIsNoop) {
1071+
auto result = execute_distributed_dml(
1072+
"INSERT INTO users (id, name, age) SELECT order_id, 'x', 1 FROM orders WHERE order_id = 999");
1073+
EXPECT_TRUE(result.success) << result.error_message;
1074+
EXPECT_EQ(mock_executor.total_row_count("users"), 0u);
1075+
}
1076+
1077+
TEST_F(DistributedDmlTest, UnknownTableSelectIsEmpty) {
1078+
catalog.add_table("", "ghost", {{"id", SqlType::make_int(), false}});
1079+
Parser<Dialect::MySQL> parser;
1080+
auto pr = parser.parse("SELECT * FROM ghost", 19);
1081+
ASSERT_EQ(pr.status, ParseResult::OK);
1082+
PlanBuilder<Dialect::MySQL> builder(catalog, parser.arena());
1083+
PlanNode* plan = builder.build(pr.ast);
1084+
DistributedPlanner<Dialect::MySQL> dist(shard_map, catalog, parser.arena(),
1085+
&mock_executor, &functions);
1086+
EXPECT_EQ(dist.distribute(plan), nullptr);
1087+
ASSERT_NE(dist.last_error(), nullptr);
1088+
}
1089+
1090+
TEST_F(DistributedDmlTest, UpdateUnknownTableErrors) {
1091+
catalog.add_table("", "ghost", {{"id", SqlType::make_int(), false}});
1092+
auto result = execute_distributed_dml("UPDATE ghost SET id = 1");
1093+
EXPECT_FALSE(result.success);
1094+
EXPECT_NE(result.error_message.find("shard map"), std::string::npos);
1095+
}
1096+
1097+
TEST_F(DistributedDmlTest, DeleteUnknownTableErrors) {
1098+
catalog.add_table("", "ghost", {{"id", SqlType::make_int(), false}});
1099+
auto result = execute_distributed_dml("DELETE FROM ghost");
1100+
EXPECT_FALSE(result.success);
1101+
EXPECT_NE(result.error_message.find("shard map"), std::string::npos);
1102+
}
1103+
1104+
TEST_F(DistributedDmlTest, PlanCacheCountTwice) {
1105+
execute_distributed_dml("INSERT INTO users (id, name, age) VALUES (3, 'Carol', 17)");
1106+
execute_distributed_dml("INSERT INTO users (id, name, age) VALUES (4, 'Dave', 18)");
1107+
LocalTransactionManager txn(data_arena);
1108+
Session<Dialect::MySQL> session(catalog, txn);
1109+
session.set_remote_executor(&mock_executor);
1110+
session.set_shard_map(&shard_map);
1111+
const char* sql = "SELECT COUNT(*) FROM users";
1112+
auto a = session.execute_query(sql);
1113+
auto b = session.execute_query(sql);
1114+
ASSERT_EQ(a.row_count(), 1u);
1115+
ASSERT_EQ(b.row_count(), 1u);
1116+
EXPECT_EQ(a.rows[0].get(0).tag, b.rows[0].get(0).tag);
1117+
EXPECT_EQ(a.rows[0].get(0).to_int64(), 2);
1118+
EXPECT_EQ(b.rows[0].get(0).to_int64(), 2);
1119+
EXPECT_EQ(session.plan_cache_size(), 1u);
1120+
}
1121+
1122+
TEST_F(DistributedDmlTest, PlanCacheSumAndGroupByTwice) {
1123+
execute_distributed_dml("INSERT INTO users (id, name, age) VALUES (3, 'Carol', 17)");
1124+
execute_distributed_dml("INSERT INTO users (id, name, age) VALUES (4, 'Dave', 18)");
1125+
LocalTransactionManager txn(data_arena);
1126+
Session<Dialect::MySQL> session(catalog, txn);
1127+
session.set_remote_executor(&mock_executor);
1128+
session.set_shard_map(&shard_map);
1129+
const char* sql = "SELECT SUM(age) FROM users";
1130+
auto a = session.execute_query(sql);
1131+
auto b = session.execute_query(sql);
1132+
ASSERT_EQ(a.row_count(), 1u);
1133+
ASSERT_EQ(b.row_count(), 1u);
1134+
EXPECT_EQ(a.rows[0].get(0).to_int64(), 35);
1135+
EXPECT_EQ(b.rows[0].get(0).to_int64(), 35);
1136+
}
1137+
1138+
TEST_F(DistributedDmlTest, CompositeQualifiedShardKeyMove) {
1139+
catalog.add_table("", "kv", {
1140+
{"tenant_id", SqlType::make_int(), false},
1141+
{"id", SqlType::make_int(), false},
1142+
{"name", SqlType::make_varchar(255), true},
1143+
});
1144+
TableShardConfig cfg;
1145+
cfg.table_name = "kv";
1146+
cfg.shard_key = "tenant_id+id";
1147+
cfg.shards = {{"shard0"}, {"shard1"}, {"shard2"}};
1148+
shard_map.add_table(cfg);
1149+
mock_executor.add_table_to_all("kv", {
1150+
{"tenant_id", SqlType::make_int(), false},
1151+
{"id", SqlType::make_int(), false},
1152+
{"name", SqlType::make_varchar(255), true},
1153+
});
1154+
EXPECT_TRUE(execute_distributed_dml(
1155+
"INSERT INTO kv (tenant_id, id, name) VALUES (1, 3, 'A')").success);
1156+
auto result = execute_distributed_dml(
1157+
"UPDATE kv SET kv.id = 9 WHERE tenant_id = 1 AND id = 3");
1158+
EXPECT_TRUE(result.success) << result.error_message;
1159+
EXPECT_EQ(execute_distributed_select(
1160+
"SELECT name FROM kv WHERE tenant_id = 1 AND id = 9").row_count(), 1u);
1161+
EXPECT_EQ(execute_distributed_select(
1162+
"SELECT name FROM kv WHERE tenant_id = 1 AND id = 3").row_count(), 0u);
1163+
}

tests/test_distributed_planner.cpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1358,6 +1358,86 @@ TEST_F(DistributedPlannerTest, CompositeRangePrunesOnFirstKey) {
13581358
EXPECT_EQ(count_remotes("SELECT * FROM users WHERE id BETWEEN 6 AND 10"), 1u);
13591359
}
13601360

1361+
TEST_F(DistributedPlannerTest, ColocatedRightJoinEmitsRightJoin) {
1362+
shard_map.add_table(TableShardConfig{
1363+
"orders", "user_id",
1364+
{ShardInfo{"shard_1"}, ShardInfo{"shard_2"}, ShardInfo{"shard_3"}}
1365+
});
1366+
Parser<Dialect::MySQL> parser;
1367+
const char* sql = "SELECT * FROM users RIGHT JOIN orders ON users.id = orders.user_id";
1368+
auto pr = parser.parse(sql, std::strlen(sql));
1369+
ASSERT_EQ(pr.status, ParseResult::OK);
1370+
PlanBuilder<Dialect::MySQL> builder(catalog, parser.arena());
1371+
PlanNode* plan = builder.build(pr.ast);
1372+
DistributedPlanner<Dialect::MySQL> dp(shard_map, catalog, parser.arena());
1373+
PlanNode* dist = dp.distribute(plan);
1374+
std::vector<PlanNode*> remotes;
1375+
find_nodes(dist, PlanNodeType::REMOTE_SCAN, remotes);
1376+
ASSERT_FALSE(remotes.empty());
1377+
bool saw_right = false;
1378+
for (auto* rs : remotes) {
1379+
std::string remote(rs->remote_scan.remote_sql, rs->remote_scan.remote_sql_len);
1380+
if (remote.find("RIGHT JOIN") != std::string::npos) saw_right = true;
1381+
}
1382+
EXPECT_TRUE(saw_right);
1383+
}
1384+
1385+
TEST_F(DistributedPlannerTest, OrEqualityPrunesUnionOfShards) {
1386+
TableShardConfig cfg;
1387+
cfg.table_name = "users";
1388+
cfg.shard_key = "id";
1389+
cfg.shards = {{"shard_1"}, {"shard_2"}, {"shard_3"}};
1390+
cfg.strategy = RoutingStrategy::LIST;
1391+
cfg.list = {{true, 1, "", 0}, {true, 6, "", 1}, {true, 15, "", 2}};
1392+
shard_map.add_table(cfg);
1393+
1394+
Parser<Dialect::MySQL> parser;
1395+
auto pr = parser.parse("SELECT * FROM users WHERE id = 1 OR id = 6", 42);
1396+
PlanBuilder<Dialect::MySQL> builder(catalog, parser.arena());
1397+
PlanNode* plan = builder.build(pr.ast);
1398+
DistributedPlanner<Dialect::MySQL> dp(shard_map, catalog, parser.arena());
1399+
PlanNode* dist = dp.distribute(plan);
1400+
std::vector<PlanNode*> remotes;
1401+
find_nodes(dist, PlanNodeType::REMOTE_SCAN, remotes);
1402+
EXPECT_EQ(remotes.size(), 2u);
1403+
}
1404+
1405+
TEST_F(DistributedPlannerTest, CompositeHashPartialWhereScatters) {
1406+
TableShardConfig cfg;
1407+
cfg.table_name = "users";
1408+
cfg.shard_key = "id+name";
1409+
cfg.shards = {{"shard_1"}, {"shard_2"}, {"shard_3"}};
1410+
shard_map.add_table(cfg);
1411+
1412+
Parser<Dialect::MySQL> parser;
1413+
auto pr = parser.parse("SELECT * FROM users WHERE id = 3", 32);
1414+
PlanBuilder<Dialect::MySQL> builder(catalog, parser.arena());
1415+
PlanNode* plan = builder.build(pr.ast);
1416+
DistributedPlanner<Dialect::MySQL> dp(shard_map, catalog, parser.arena());
1417+
PlanNode* dist = dp.distribute(plan);
1418+
std::vector<PlanNode*> remotes;
1419+
find_nodes(dist, PlanNodeType::REMOTE_SCAN, remotes);
1420+
EXPECT_EQ(remotes.size(), 3u);
1421+
}
1422+
1423+
TEST_F(DistributedPlannerTest, CompositeHashBothKeysPrune) {
1424+
TableShardConfig cfg;
1425+
cfg.table_name = "users";
1426+
cfg.shard_key = "id+age";
1427+
cfg.shards = {{"shard_1"}, {"shard_2"}, {"shard_3"}};
1428+
shard_map.add_table(cfg);
1429+
1430+
Parser<Dialect::MySQL> parser;
1431+
auto pr = parser.parse("SELECT * FROM users WHERE id = 3 AND age = 17", 45);
1432+
PlanBuilder<Dialect::MySQL> builder(catalog, parser.arena());
1433+
PlanNode* plan = builder.build(pr.ast);
1434+
DistributedPlanner<Dialect::MySQL> dp(shard_map, catalog, parser.arena());
1435+
PlanNode* dist = dp.distribute(plan);
1436+
std::vector<PlanNode*> remotes;
1437+
find_nodes(dist, PlanNodeType::REMOTE_SCAN, remotes);
1438+
EXPECT_EQ(remotes.size(), 1u);
1439+
}
1440+
13611441
TEST_F(DistributedPlannerTest, SemiJoinPrunesProbeShards) {
13621442
Parser<Dialect::MySQL> parser;
13631443
const char* sql = "SELECT * FROM users JOIN orders ON users.id = orders.user_id";

tests/test_shard_map.cpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,62 @@ TEST(ShardMapHashTest, IsDeterministic) {
4848
}
4949
}
5050

51+
TEST(ShardMapHashTest, LeadingZerosRouteLikeInt) {
52+
ShardMap map;
53+
map.add_table(make_two_shards(RoutingStrategy::HASH));
54+
size_t a = map.shard_index_for_int(sref("users"), 3);
55+
size_t b = 99;
56+
ASSERT_TRUE(map.try_shard_index_for_string(sref("users"), "03", 2, b));
57+
EXPECT_EQ(a, b);
58+
}
59+
60+
TEST(ShardMapHashTest, NonNumericStringDoesNotCoerce) {
61+
TableShardConfig cfg;
62+
cfg.table_name = "users";
63+
cfg.shard_key = "id";
64+
cfg.shards = {ShardInfo{"a"}, ShardInfo{"b"}, ShardInfo{"c"}, ShardInfo{"d"}};
65+
ShardMap map;
66+
map.add_table(cfg);
67+
size_t as_int = map.shard_index_for_int(sref("users"), 3);
68+
bool differs = false;
69+
const char* samples[] = {"3x", "3.0", " 3", "3 "};
70+
for (const char* s : samples) {
71+
size_t idx = 99;
72+
ASSERT_TRUE(map.try_shard_index_for_string(
73+
sref("users"), s, static_cast<uint32_t>(std::strlen(s)), idx));
74+
if (idx != as_int) differs = true;
75+
}
76+
EXPECT_TRUE(differs);
77+
}
78+
79+
TEST(ShardMapHashTest, EmptyStringIsUnparsed) {
80+
ShardMap map;
81+
map.add_table(make_two_shards(RoutingStrategy::HASH));
82+
size_t idx = 99;
83+
EXPECT_TRUE(map.try_shard_index_for_string(sref("users"), "", 0, idx));
84+
EXPECT_LT(idx, 2u);
85+
}
86+
87+
TEST(ShardMapListTest, StringIntegerDoesNotCoerceToIntKey) {
88+
TableShardConfig cfg = make_two_shards(RoutingStrategy::LIST);
89+
cfg.list = {ShardListEntry{true, 3, "", 1}};
90+
ShardMap map;
91+
map.add_table(cfg);
92+
size_t idx = 99;
93+
EXPECT_FALSE(map.try_shard_index_for_string(sref("users"), "3", 1, idx));
94+
EXPECT_TRUE(map.try_shard_index_for_int(sref("users"), 3, idx));
95+
EXPECT_EQ(idx, 1u);
96+
}
97+
98+
TEST(ShardMapRangeTest, StringIntegerIsUnroutable) {
99+
TableShardConfig cfg = make_two_shards(RoutingStrategy::RANGE);
100+
cfg.ranges = {ShardRange{5, 0}, ShardRange{100, 1}};
101+
ShardMap map;
102+
map.add_table(cfg);
103+
size_t idx = 99;
104+
EXPECT_FALSE(map.try_shard_index_for_string(sref("users"), "3", 1, idx));
105+
}
106+
51107
TEST(ShardMapHashTest, StringIntegerRoutesLikeInt) {
52108
ShardMap map;
53109
map.add_table(make_two_shards(RoutingStrategy::HASH));

0 commit comments

Comments
 (0)