Skip to content

Commit 16f79b1

Browse files
authored
Merge pull request #27 from poyrazK/test/distributed-executor-coverage
test: add distributed_executor and ShardManager unit tests
2 parents b7fb93d + 81a27a0 commit 16f79b1

2 files changed

Lines changed: 357 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ if(BUILD_TESTS)
138138
add_cloudsql_test(rpc_server_tests tests/rpc_server_tests.cpp)
139139
add_cloudsql_test(operator_tests tests/operator_tests.cpp)
140140
add_cloudsql_test(query_executor_tests tests/query_executor_tests.cpp)
141+
add_cloudsql_test(distributed_executor_tests tests/distributed_executor_tests.cpp)
141142

142143
add_custom_target(run-tests
143144
COMMAND ${CMAKE_CTEST_COMMAND}
Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
/**
2+
* @file distributed_executor_tests.cpp
3+
* @brief Unit tests for DistributedExecutor and ShardManager utilities
4+
*/
5+
6+
#include <gtest/gtest.h>
7+
8+
#include <memory>
9+
#include <string>
10+
#include <vector>
11+
12+
#include "catalog/catalog.hpp"
13+
#include "common/cluster_manager.hpp"
14+
#include "common/config.hpp"
15+
#include "common/value.hpp"
16+
#include "distributed/distributed_executor.hpp"
17+
#include "distributed/shard_manager.hpp"
18+
#include "parser/expression.hpp"
19+
#include "parser/lexer.hpp"
20+
#include "parser/parser.hpp"
21+
22+
using namespace cloudsql;
23+
using namespace cloudsql::executor;
24+
using namespace cloudsql::cluster;
25+
using namespace cloudsql::parser;
26+
using namespace cloudsql::common;
27+
28+
namespace {
29+
30+
// ============= ShardManager Tests =============
31+
32+
TEST(ShardManagerTests, StableHashConsistency) {
33+
// Same string should always produce same hash
34+
uint32_t h1 = ShardManager::stable_hash("test_key");
35+
uint32_t h2 = ShardManager::stable_hash("test_key");
36+
uint32_t h3 = ShardManager::stable_hash("test_key");
37+
EXPECT_EQ(h1, h2);
38+
EXPECT_EQ(h2, h3);
39+
}
40+
41+
TEST(ShardManagerTests, StableHashDifferentStrings) {
42+
// Different strings should likely produce different hashes
43+
uint32_t h1 = ShardManager::stable_hash("key1");
44+
uint32_t h2 = ShardManager::stable_hash("key2");
45+
EXPECT_NE(h1, h2);
46+
}
47+
48+
TEST(ShardManagerTests, StableHashEmptyString) {
49+
uint32_t hash = ShardManager::stable_hash("");
50+
// Empty string should have a defined hash value (DJB2 algorithm)
51+
EXPECT_EQ(hash, 5381u); // hash starts at 5381
52+
}
53+
54+
TEST(ShardManagerTests, ComputeShardWithNumShards) {
55+
Value key = Value::make_int64(42);
56+
EXPECT_EQ(ShardManager::compute_shard(key, 4), ShardManager::compute_shard(key, 4));
57+
}
58+
59+
TEST(ShardManagerTests, ComputeShardZeroShards) {
60+
Value key = Value::make_int64(100);
61+
// Should return 0 (not crash) when num_shards is 0
62+
EXPECT_EQ(ShardManager::compute_shard(key, 0), 0u);
63+
}
64+
65+
TEST(ShardManagerTests, ComputeShardDeterministic) {
66+
Value key1 = Value::make_int64(1000);
67+
Value key2 = Value::make_int64(1000);
68+
uint32_t shard1 = ShardManager::compute_shard(key1, 8);
69+
uint32_t shard2 = ShardManager::compute_shard(key2, 8);
70+
EXPECT_EQ(shard1, shard2);
71+
}
72+
73+
TEST(ShardManagerTests, ComputeShardInRange) {
74+
Value key = Value::make_int64(999);
75+
uint32_t num_shards = 16;
76+
uint32_t shard = ShardManager::compute_shard(key, num_shards);
77+
EXPECT_LT(shard, num_shards);
78+
}
79+
80+
TEST(ShardManagerTests, GetTargetNodeEmptyShards) {
81+
TableInfo info;
82+
info.shards = {};
83+
auto result = ShardManager::get_target_node(info, 0);
84+
EXPECT_FALSE(result.has_value());
85+
}
86+
87+
TEST(ShardManagerTests, GetTargetNodeFound) {
88+
ShardInfo shard;
89+
shard.shard_id = 5;
90+
shard.node_address = "127.0.0.1";
91+
shard.port = 7000;
92+
93+
TableInfo info;
94+
info.shards = {shard};
95+
96+
auto result = ShardManager::get_target_node(info, 5);
97+
EXPECT_TRUE(result.has_value());
98+
EXPECT_EQ(result->node_address, "127.0.0.1");
99+
}
100+
101+
TEST(ShardManagerTests, GetTargetNodeNotFound) {
102+
ShardInfo shard;
103+
shard.shard_id = 3;
104+
shard.node_address = "127.0.0.1";
105+
shard.port = 7000;
106+
107+
TableInfo info;
108+
info.shards = {shard};
109+
110+
auto result = ShardManager::get_target_node(info, 99); // Different shard_id
111+
EXPECT_FALSE(result.has_value());
112+
}
113+
114+
// ============= DistributedExecutor Basic Tests =============
115+
116+
class DistributedExecutorTests : public ::testing::Test {
117+
protected:
118+
void SetUp() override {
119+
catalog_ = Catalog::create();
120+
config_.mode = config::RunMode::Coordinator;
121+
cm_ = std::make_unique<ClusterManager>(&config_);
122+
exec_ = std::make_unique<DistributedExecutor>(*catalog_, *cm_);
123+
}
124+
125+
std::shared_ptr<Catalog> catalog_;
126+
config::Config config_;
127+
std::unique_ptr<ClusterManager> cm_;
128+
std::unique_ptr<DistributedExecutor> exec_;
129+
};
130+
131+
TEST_F(DistributedExecutorTests, ConstructorBasic) {
132+
EXPECT_NE(exec_, nullptr);
133+
}
134+
135+
// DDL operations succeed because they update the local catalog
136+
// (no distributed coordination needed for schema changes)
137+
TEST_F(DistributedExecutorTests, ExecuteDDLWithoutNodes) {
138+
auto lexer = std::make_unique<Lexer>("CREATE TABLE test_table (id INT, name TEXT)");
139+
Parser parser(std::move(lexer));
140+
auto stmt = parser.parse_statement();
141+
ASSERT_NE(stmt, nullptr);
142+
143+
auto res = exec_->execute(*stmt, "CREATE TABLE test_table (id INT, name TEXT)");
144+
EXPECT_TRUE(res.success());
145+
}
146+
147+
// DDL without nodes succeeds (local catalog update only)
148+
TEST_F(DistributedExecutorTests, ExecuteDDLNoNodesDropTable) {
149+
auto lexer = std::make_unique<Lexer>("DROP TABLE test_table");
150+
Parser parser(std::move(lexer));
151+
auto stmt = parser.parse_statement();
152+
ASSERT_NE(stmt, nullptr);
153+
154+
auto res = exec_->execute(*stmt, "DROP TABLE test_table");
155+
EXPECT_TRUE(res.success());
156+
}
157+
158+
// DML fails when no nodes because it needs shard routing
159+
TEST_F(DistributedExecutorTests, ExecuteDMLWithoutNodes) {
160+
auto lexer = std::make_unique<Lexer>("INSERT INTO test_table VALUES (1, 'test')");
161+
Parser parser(std::move(lexer));
162+
auto stmt = parser.parse_statement();
163+
ASSERT_NE(stmt, nullptr);
164+
165+
auto res = exec_->execute(*stmt, "INSERT INTO test_table VALUES (1, 'test')");
166+
EXPECT_FALSE(res.success());
167+
EXPECT_STREQ(res.error().c_str(), "No active data nodes in cluster");
168+
}
169+
170+
// SELECT fails when no nodes available
171+
TEST_F(DistributedExecutorTests, ExecuteSELECTWithoutNodes) {
172+
auto lexer = std::make_unique<Lexer>("SELECT * FROM test_table");
173+
Parser parser(std::move(lexer));
174+
auto stmt = parser.parse_statement();
175+
ASSERT_NE(stmt, nullptr);
176+
177+
auto res = exec_->execute(*stmt, "SELECT * FROM test_table");
178+
EXPECT_FALSE(res.success());
179+
EXPECT_STREQ(res.error().c_str(), "No active data nodes in cluster");
180+
}
181+
182+
// Transaction control fails when no nodes
183+
TEST_F(DistributedExecutorTests, ExecuteBEGINWithoutNodes) {
184+
auto lexer = std::make_unique<Lexer>("BEGIN");
185+
Parser parser(std::move(lexer));
186+
auto stmt = parser.parse_statement();
187+
ASSERT_NE(stmt, nullptr);
188+
189+
auto res = exec_->execute(*stmt, "BEGIN");
190+
EXPECT_FALSE(res.success());
191+
EXPECT_STREQ(res.error().c_str(), "No active data nodes in cluster");
192+
}
193+
194+
TEST_F(DistributedExecutorTests, ExecuteCOMMITWithoutNodes) {
195+
auto lexer = std::make_unique<Lexer>("COMMIT");
196+
Parser parser(std::move(lexer));
197+
auto stmt = parser.parse_statement();
198+
ASSERT_NE(stmt, nullptr);
199+
200+
auto res = exec_->execute(*stmt, "COMMIT");
201+
EXPECT_FALSE(res.success());
202+
EXPECT_STREQ(res.error().c_str(), "No active data nodes in cluster");
203+
}
204+
205+
TEST_F(DistributedExecutorTests, ExecuteROLLBACKWithoutNodes) {
206+
auto lexer = std::make_unique<Lexer>("ROLLBACK");
207+
Parser parser(std::move(lexer));
208+
auto stmt = parser.parse_statement();
209+
ASSERT_NE(stmt, nullptr);
210+
211+
auto res = exec_->execute(*stmt, "ROLLBACK");
212+
EXPECT_FALSE(res.success());
213+
EXPECT_STREQ(res.error().c_str(), "No active data nodes in cluster");
214+
}
215+
216+
// SELECT without FROM clause - parser error
217+
TEST_F(DistributedExecutorTests, ParseRejectsSelectWithoutFrom) {
218+
// SELECT * without FROM is not valid SQL in this parser
219+
auto lexer = std::make_unique<Lexer>("SELECT *");
220+
Parser parser(std::move(lexer));
221+
auto stmt = parser.parse_statement();
222+
// Parser should fail on "SELECT *" without table
223+
ASSERT_EQ(stmt, nullptr);
224+
}
225+
226+
// ============= Expression Sharding Key Extraction Tests =============
227+
228+
class ShardingKeyExtractionTests : public ::testing::Test {
229+
protected:
230+
void SetUp() override {}
231+
};
232+
233+
TEST_F(ShardingKeyExtractionTests, ExtractShardingKeySimpleEq) {
234+
// Test: id = 42
235+
auto lexer = std::make_unique<Lexer>("SELECT * FROM test WHERE id = 42");
236+
Parser parser(std::move(lexer));
237+
auto stmt = parser.parse_statement();
238+
ASSERT_NE(stmt, nullptr);
239+
240+
auto* select_stmt = dynamic_cast<const SelectStatement*>(stmt.get());
241+
ASSERT_NE(select_stmt, nullptr);
242+
auto* where_expr = dynamic_cast<const BinaryExpr*>(select_stmt->where());
243+
ASSERT_NE(where_expr, nullptr);
244+
245+
// Verify it's: id = 42
246+
auto* left_col = dynamic_cast<const ColumnExpr*>(&where_expr->left());
247+
ASSERT_NE(left_col, nullptr);
248+
EXPECT_EQ(left_col->name(), "id");
249+
250+
auto* right_const = dynamic_cast<const ConstantExpr*>(&where_expr->right());
251+
ASSERT_NE(right_const, nullptr);
252+
EXPECT_EQ(right_const->value(), Value::make_int64(42));
253+
254+
EXPECT_EQ(where_expr->op(), TokenType::Eq);
255+
}
256+
257+
TEST_F(ShardingKeyExtractionTests, NoWHEREClause) {
258+
auto lexer = std::make_unique<Lexer>("SELECT * FROM test");
259+
Parser parser(std::move(lexer));
260+
auto stmt = parser.parse_statement();
261+
ASSERT_NE(stmt, nullptr);
262+
263+
auto* select_stmt = dynamic_cast<const SelectStatement*>(stmt.get());
264+
ASSERT_NE(select_stmt, nullptr);
265+
EXPECT_EQ(select_stmt->where(), nullptr);
266+
}
267+
268+
TEST_F(ShardingKeyExtractionTests, NonEqCondition) {
269+
// WHERE id > 42 uses Greater operator, not equality - no valid sharding key
270+
auto lexer = std::make_unique<Lexer>("SELECT * FROM test WHERE id > 42");
271+
Parser parser(std::move(lexer));
272+
auto stmt = parser.parse_statement();
273+
ASSERT_NE(stmt, nullptr);
274+
275+
auto* select_stmt = dynamic_cast<const SelectStatement*>(stmt.get());
276+
ASSERT_NE(select_stmt, nullptr);
277+
auto* where_expr = dynamic_cast<const BinaryExpr*>(select_stmt->where());
278+
ASSERT_NE(where_expr, nullptr);
279+
280+
// Verify it's: id > 42 (Greater, not Eq)
281+
auto* left_col = dynamic_cast<const ColumnExpr*>(&where_expr->left());
282+
ASSERT_NE(left_col, nullptr);
283+
EXPECT_EQ(left_col->name(), "id");
284+
285+
// op should be Gt, not Eq - cannot extract sharding key from inequality
286+
EXPECT_EQ(where_expr->op(), TokenType::Gt);
287+
}
288+
289+
// ============= Helper Function Tests =============
290+
291+
TEST(HelperTests, StableHashAlgorithm) {
292+
// DJB2 hash algorithm verification
293+
std::string input = "hello";
294+
uint32_t hash = ShardManager::stable_hash(input);
295+
296+
// Manually verify DJB2: hash = hash * 33 + c for each char
297+
uint32_t expected = 5381;
298+
for (char c : input) {
299+
expected = ((expected << 5) + expected) + static_cast<uint8_t>(c);
300+
}
301+
EXPECT_EQ(hash, expected);
302+
}
303+
304+
TEST(HelperTests, ComputeShardModuloProperties) {
305+
// Verify compute_shard uses modulo correctly
306+
Value key = Value::make_int64(12345);
307+
uint32_t shard1 = ShardManager::compute_shard(key, 10);
308+
uint32_t shard2 = ShardManager::compute_shard(key, 10);
309+
310+
// Same key, same num_shards should always give same result
311+
EXPECT_EQ(shard1, shard2);
312+
313+
// Should be in range [0, 10)
314+
EXPECT_LT(shard1, 10);
315+
}
316+
317+
TEST(HelperTests, ComputeShardStringKey) {
318+
// Test with string value key
319+
Value key = Value::make_text("primary_key_value");
320+
uint32_t shard = ShardManager::compute_shard(key, 8);
321+
322+
// Should be in range [0, 8)
323+
EXPECT_LT(shard, 8);
324+
}
325+
326+
// ============= Null Safety Tests =============
327+
328+
TEST(NullSafetyTests, ExecuteWithEmptyCluster) {
329+
auto catalog = Catalog::create();
330+
config::Config config;
331+
ClusterManager cm(&config);
332+
DistributedExecutor exec(*catalog, cm);
333+
334+
// DDL succeeds (local catalog update), DML/SELECT fail
335+
std::vector<std::pair<std::string, bool>> statements = {
336+
{"CREATE TABLE t (id INT)", true}, // succeeds - local catalog
337+
{"DROP TABLE t", true}, // succeeds - local catalog
338+
{"INSERT INTO t VALUES (1)", false}, // fails - needs nodes
339+
{"SELECT * FROM t", false}, // fails - needs nodes
340+
{"UPDATE t SET id = 1", false}, // fails - needs nodes
341+
{"DELETE FROM t", false}, // fails - needs nodes
342+
{"BEGIN", false}, // fails - needs nodes
343+
{"COMMIT", false}, // fails - needs nodes
344+
{"ROLLBACK", false}}; // fails - needs nodes
345+
346+
for (const auto& [sql, expected_success] : statements) {
347+
auto lexer = std::make_unique<Lexer>(sql);
348+
Parser parser(std::move(lexer));
349+
auto stmt = parser.parse_statement();
350+
ASSERT_TRUE(stmt) << "Parse failed for: " << sql;
351+
auto res = exec.execute(*stmt, sql);
352+
EXPECT_EQ(res.success(), expected_success) << "Failed for: " << sql;
353+
}
354+
}
355+
356+
} // namespace

0 commit comments

Comments
 (0)