| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| // Integration tests for the brpc MySQL client PREPARED-STATEMENT path, |
| // exercised end to end against a real mysqld through brpc's PUBLIC API |
| // (brpc::Channel + brpc::NewMysqlStatement + brpc::MysqlRequest / |
| // brpc::MysqlResponse). This complements the low-level wire tests in |
| // brpc_mysql_auth_handshake_unittest.cpp, which speak the protocol over a |
| // raw socket; here we drive the actual client stack a user would use. |
| // |
| // Each fat test chains several prepared-statement behaviors (param counting, |
| // binding, typed fetch, re-execution, NULL handling, error paths) so the |
| // test boundaries reflect our own grouping of the client surface. |
| // |
| // HARNESS: Reuses the self-spawned / already-running mysqld pattern |
| // documented in test/README_mysql_auth.md and implemented in |
| // brpc_mysql_auth_handshake_unittest.cpp. When -mysql_use_running_server |
| // is set the tests connect to a server the caller started (neither started |
| // nor stopped here); otherwise the fixture spawns a throwaway mysqld with |
| // an empty-password root. Every test GTEST_SKIP()s when no mysqld is |
| // reachable, so the suite is safe to run in environments without MySQL. |
| |
| #include <gflags/gflags.h> |
| #include <gtest/gtest.h> |
| |
| #include <arpa/inet.h> |
| #include <netinet/in.h> |
| #include <pthread.h> |
| #include <sys/socket.h> |
| #include <unistd.h> |
| |
| #include <cstdio> |
| #include <cstdlib> |
| #include <cstring> |
| #include <memory> |
| #include <string> |
| |
| #include <brpc/channel.h> |
| #include <brpc/controller.h> |
| #include "brpc/policy/mysql/mysql.h" |
| #include "brpc/policy/mysql/mysql_authenticator.h" |
| #include "butil/logging.h" |
| |
| // These flags intentionally mirror the names used by |
| // brpc_mysql_auth_handshake_unittest.cpp so a single command line can drive |
| // both suites against the same running server. gflags forbids registering |
| // the same flag twice in one binary, but each *_unittest.cpp is linked into |
| // its own executable (one gtest_main per glob entry), so there is no clash. |
| DEFINE_bool(mysql_use_running_server, false, |
| "Use an already-running MySQL server instead of spawning a " |
| "throwaway one; the running server is neither started nor " |
| "stopped by the test."); |
| DEFINE_string(mysql_host, "127.0.0.1", |
| "Host of the running MySQL server " |
| "(only with -mysql_use_running_server)."); |
| DEFINE_int32(mysql_port, 13306, |
| "TCP port of the MySQL server (used for both the running " |
| "server and the spawned throwaway server)."); |
| DEFINE_string(mysql_user, "root", |
| "User for the prepared-statement tests against a running " |
| "server."); |
| DEFINE_string(mysql_password, "", |
| "Password for -mysql_user (empty for the spawned server)."); |
| DEFINE_string(mysql_schema, "brpc_ps_test", |
| "Schema/database the tests prepare statements against."); |
| |
| namespace { |
| |
| #define MYSQLD_BIN "mysqld" |
| |
| // The schema the integration tests operate in. On a spawned server we |
| // create it (and a seed table) ourselves over the unix socket; against a |
| // running server the caller must have granted -mysql_user access to it. |
| static const char* kCollation = "utf8mb4_general_ci"; |
| |
| static pthread_once_t g_start_once = PTHREAD_ONCE_INIT; |
| // >0 : we forked a throwaway mysqld with this pid. |
| // -2 : an already-running server is reachable. |
| // -1 : no server available; tests skip. |
| static pid_t g_mysqld_pid = -1; |
| |
| static std::string g_host = "127.0.0.1"; |
| static int g_port = 13306; |
| static std::string g_user = "root"; |
| static std::string g_password; |
| static std::string g_schema; |
| // True once the seed schema/table is known to exist (created on spawn, or |
| // created best-effort against a running server via the channel itself). |
| static bool g_schema_ready = false; |
| |
| static std::string TestDataDir() { |
| char cwd[1024]; |
| if (getcwd(cwd, sizeof(cwd)) == NULL) { |
| return std::string("/tmp/mysql_ps_data_for_test"); |
| } |
| return std::string(cwd) + "/mysql_ps_data_for_test"; |
| } |
| |
| static void RemoveMysqlServer() { |
| if (g_mysqld_pid > 0) { |
| puts("[Stopping mysqld]"); |
| char cmd[1280]; |
| snprintf(cmd, sizeof(cmd), "kill %d", g_mysqld_pid); |
| CHECK(0 == system(cmd)); |
| usleep(500000); |
| snprintf(cmd, sizeof(cmd), "rm -rf '%s'", TestDataDir().c_str()); |
| CHECK(0 == system(cmd)); |
| } |
| } |
| |
| // Opens a raw TCP connection to g_host:g_port purely as a readiness probe; |
| // returns the fd or -1. (The tests themselves talk through brpc, not this.) |
| static int ProbeConnect() { |
| int fd = socket(AF_INET, SOCK_STREAM, 0); |
| if (fd < 0) { |
| return -1; |
| } |
| struct sockaddr_in addr; |
| memset(&addr, 0, sizeof(addr)); |
| addr.sin_family = AF_INET; |
| addr.sin_port = htons(static_cast<uint16_t>(g_port)); |
| addr.sin_addr.s_addr = inet_addr(g_host.c_str()); |
| if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) { |
| close(fd); |
| return -1; |
| } |
| return fd; |
| } |
| |
| static void StartServerOnce() { |
| if (FLAGS_mysql_use_running_server) { |
| g_host = FLAGS_mysql_host; |
| g_port = FLAGS_mysql_port; |
| g_user = FLAGS_mysql_user; |
| g_password = FLAGS_mysql_password; |
| g_schema = FLAGS_mysql_schema; |
| printf("[Using running mysqld at %s:%d as user '%s', schema '%s']\n", |
| g_host.c_str(), g_port, g_user.c_str(), g_schema.c_str()); |
| int fd = ProbeConnect(); |
| if (fd >= 0) { |
| close(fd); |
| g_mysqld_pid = -2; |
| // We create the seed schema/table lazily through the channel in |
| // the fixture (SetUp) so it works even without the mysql CLI. |
| } else { |
| printf("Cannot reach running mysqld at %s:%d, tests will skip\n", |
| g_host.c_str(), g_port); |
| } |
| return; |
| } |
| |
| if (system("which " MYSQLD_BIN) != 0) { |
| puts("Fail to find " MYSQLD_BIN ", tests will be skipped"); |
| return; |
| } |
| g_host = "127.0.0.1"; |
| g_port = FLAGS_mysql_port; |
| g_user = "root"; |
| g_password.clear(); |
| g_schema = FLAGS_mysql_schema; |
| const std::string datadir = TestDataDir(); |
| char cmd[2048]; |
| snprintf(cmd, sizeof(cmd), "rm -rf '%s' && mkdir -p '%s'", |
| datadir.c_str(), datadir.c_str()); |
| if (system(cmd) != 0) { |
| puts("Fail to create datadir, tests will be skipped"); |
| return; |
| } |
| snprintf(cmd, sizeof(cmd), |
| MYSQLD_BIN " --initialize-insecure --datadir='%s'" |
| " --log-error='%s/init.err'", |
| datadir.c_str(), datadir.c_str()); |
| if (system(cmd) != 0) { |
| puts("Fail to initialize mysqld datadir, tests will be skipped"); |
| snprintf(cmd, sizeof(cmd), "rm -rf '%s'", datadir.c_str()); |
| CHECK(0 == system(cmd)); |
| return; |
| } |
| atexit(RemoveMysqlServer); |
| |
| g_mysqld_pid = fork(); |
| if (g_mysqld_pid < 0) { |
| puts("Fail to fork"); |
| exit(1); |
| } else if (g_mysqld_pid == 0) { |
| puts("[Starting mysqld]"); |
| char port_arg[32]; |
| snprintf(port_arg, sizeof(port_arg), "--port=%d", FLAGS_mysql_port); |
| const std::string datadir_arg = "--datadir=" + datadir; |
| const std::string socket_arg = "--socket=" + datadir + "/mysqld.sock"; |
| const std::string pidfile_arg = "--pid-file=" + datadir + "/mysqld.pid"; |
| const std::string logerr_arg = "--log-error=" + datadir + "/mysqld.err"; |
| char* const argv[] = {(char*)MYSQLD_BIN, |
| (char*)datadir_arg.c_str(), |
| (char*)port_arg, |
| (char*)socket_arg.c_str(), |
| (char*)pidfile_arg.c_str(), |
| (char*)logerr_arg.c_str(), |
| (char*)"--mysqlx=OFF", |
| (char*)"--bind-address=127.0.0.1", |
| NULL}; |
| if (execvp(MYSQLD_BIN, argv) < 0) { |
| puts("Fail to run " MYSQLD_BIN); |
| exit(1); |
| } |
| } |
| for (int i = 0; i < 300; ++i) { |
| int fd = ProbeConnect(); |
| if (fd >= 0) { |
| close(fd); |
| // Create the seed schema + table over the unix socket (root has |
| // an empty password there). Best-effort: if the mysql CLI is |
| // missing we fall back to creating it through the channel in |
| // SetUp (DDL also works over the prepared-statement channel via |
| // a plain Query reply, but the CLI keeps the fixture simple). |
| char create[2048]; |
| snprintf(create, sizeof(create), |
| "mysql --socket='%s/mysqld.sock' -u root -e \"" |
| "CREATE DATABASE IF NOT EXISTS %s; \" 2>/dev/null", |
| datadir.c_str(), g_schema.c_str()); |
| (void)system(create); // schema creation is also retried lazily |
| return; |
| } |
| usleep(100000); |
| } |
| puts("mysqld did not become ready, tests will be skipped"); |
| g_mysqld_pid = -1; |
| } |
| |
| // Builds a Channel configured for the prepared-statement protocol against |
| // the active server/schema. Returns 0 on success. |
| static int InitChannel(brpc::Channel* channel) { |
| brpc::ChannelOptions options; |
| options.protocol = brpc::PROTOCOL_MYSQL; |
| options.connection_type = "pooled"; |
| options.timeout_ms = 5000; |
| options.connect_timeout_ms = 5000; |
| options.max_retry = 0; |
| options.auth = new brpc::policy::MysqlAuthenticator( |
| g_user, g_password, g_schema, "", kCollation); |
| return channel->Init(g_host.c_str(), g_port, &options); |
| } |
| |
| // Runs a single plain-text statement (DDL/DML) through |channel| and returns |
| // true when the server answered without an error reply. Used by the fixture |
| // to set up seed tables; not itself one of the prepared-statement scenarios. |
| static bool RunPlainQuery(brpc::Channel* channel, const std::string& sql) { |
| brpc::MysqlRequest request; |
| if (!request.Query(sql)) { |
| return false; |
| } |
| brpc::MysqlResponse response; |
| brpc::Controller cntl; |
| channel->CallMethod(NULL, &cntl, &request, &response, NULL); |
| if (cntl.Failed()) { |
| return false; |
| } |
| if (response.reply_size() < 1) { |
| return false; |
| } |
| return !response.reply(0).is_error(); |
| } |
| |
| class MysqlPreparedTest : public testing::Test { |
| protected: |
| void SetUp() override { |
| pthread_once(&g_start_once, StartServerOnce); |
| if (NoServer()) { |
| return; |
| } |
| // Ensure the schema + the shared seed table exist exactly once. |
| // (Idempotent CREATE IF NOT EXISTS, so re-running is harmless.) |
| if (!g_schema_ready) { |
| brpc::Channel setup; |
| // Connect with an empty schema first so CREATE DATABASE works |
| // even if g_schema does not yet exist on a running server. |
| brpc::ChannelOptions options; |
| options.protocol = brpc::PROTOCOL_MYSQL; |
| options.connection_type = "pooled"; |
| options.timeout_ms = 5000; |
| options.connect_timeout_ms = 5000; |
| options.auth = new brpc::policy::MysqlAuthenticator( |
| g_user, g_password, "", "", kCollation); |
| if (setup.Init(g_host.c_str(), g_port, &options) == 0) { |
| RunPlainQuery(&setup, "CREATE DATABASE IF NOT EXISTS " + g_schema); |
| } |
| // Now (re)connect bound to the schema and create the seed table. |
| brpc::Channel ch; |
| if (InitChannel(&ch) == 0) { |
| RunPlainQuery(&ch, "DROP TABLE IF EXISTS ps_people"); |
| RunPlainQuery(&ch, |
| "CREATE TABLE ps_people(" |
| "id INT, name VARCHAR(50), score BIGINT)"); |
| RunPlainQuery(&ch, |
| "INSERT INTO ps_people VALUES" |
| "(417,'maple',9100),(528,'cobalt',9200),(639,NULL,9300)"); |
| g_schema_ready = true; |
| } |
| } |
| ASSERT_EQ(0, InitChannel(&channel_)) << "channel init failed"; |
| } |
| |
| static bool NoServer() { return g_mysqld_pid == -1; } |
| |
| brpc::Channel channel_; |
| }; |
| |
| #define SKIP_IF_NO_SERVER() \ |
| do { \ |
| if (NoServer()) { \ |
| GTEST_SKIP() << "no mysqld available"; \ |
| } \ |
| } while (0) |
| |
| // Convenience: prepare |sql| against channel_, asserting success and |
| // returning the statement. Returns nullptr on failure (caller asserts). |
| #define PREPARE_OR_FAIL(var, sql) \ |
| auto var = brpc::NewMysqlStatement(channel_, (sql)); \ |
| ASSERT_TRUE((var) != nullptr) << "prepare failed for: " << (sql) |
| |
| // Parameter counting across statement shapes, plus executing a no-parameter |
| // SELECT that returns the full seed result set. |
| TEST_F(MysqlPreparedTest, ParamCountsAndNoParamSelect) { |
| SKIP_IF_NO_SERVER(); |
| |
| // param_count must reflect the placeholders in each shape. |
| { |
| PREPARE_OR_FAIL(s, "INSERT INTO ps_people VALUES(?, ?, ?)"); |
| EXPECT_EQ(3u, s->param_count()); |
| } |
| { |
| PREPARE_OR_FAIL(s, "SELECT * FROM ps_people WHERE id=? AND name=?"); |
| EXPECT_EQ(2u, s->param_count()); |
| } |
| { |
| PREPARE_OR_FAIL(s, "DELETE FROM ps_people WHERE id=417"); |
| EXPECT_EQ(0u, s->param_count()); |
| } |
| { |
| PREPARE_OR_FAIL(s, "DELETE FROM ps_people WHERE id=?"); |
| EXPECT_EQ(1u, s->param_count()); |
| } |
| |
| // A no-parameter SELECT returns a result set covering all three seed rows. |
| PREPARE_OR_FAIL(s, "SELECT id, name, score FROM ps_people ORDER BY id"); |
| EXPECT_EQ(0u, s->param_count()); |
| brpc::MysqlRequest request(s.get()); |
| brpc::MysqlResponse response; |
| brpc::Controller cntl; |
| channel_.CallMethod(NULL, &cntl, &request, &response, NULL); |
| ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); |
| ASSERT_GE(response.reply_size(), 1u); |
| const brpc::MysqlReply& r = response.reply(0); |
| ASSERT_TRUE(r.is_resultset()) << "expected a result set, got: " << r; |
| EXPECT_EQ(3u, r.column_count()); |
| EXPECT_EQ(3u, r.row_count()); |
| } |
| |
| // Bind and execute, all parameter flavors in one place: a single INT bind, a |
| // single STRING bind, and a two-INT arithmetic expression each return their |
| // own correct result. |
| TEST_F(MysqlPreparedTest, BindAndExecuteIntStringAndArithmetic) { |
| SKIP_IF_NO_SERVER(); |
| |
| // (a) bind one INT param -> matching row's name. |
| { |
| PREPARE_OR_FAIL(s, "SELECT name FROM ps_people WHERE id=?"); |
| ASSERT_EQ(1u, s->param_count()); |
| brpc::MysqlRequest request(s.get()); |
| ASSERT_TRUE(request.AddParam((int32_t)417)); |
| brpc::MysqlResponse response; |
| brpc::Controller cntl; |
| channel_.CallMethod(NULL, &cntl, &request, &response, NULL); |
| ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); |
| ASSERT_GE(response.reply_size(), 1u); |
| const brpc::MysqlReply& r = response.reply(0); |
| ASSERT_TRUE(r.is_resultset()) << r; |
| ASSERT_EQ(1u, r.row_count()); |
| const brpc::MysqlReply::Field& f = r.next().field(0); |
| ASSERT_TRUE(f.is_string()); |
| EXPECT_EQ("maple", f.string().as_string()); |
| } |
| |
| // (b) bind one STRING param -> matching row's id. |
| { |
| PREPARE_OR_FAIL(s, "SELECT id FROM ps_people WHERE name=?"); |
| ASSERT_EQ(1u, s->param_count()); |
| brpc::MysqlRequest request(s.get()); |
| ASSERT_TRUE(request.AddParam(butil::StringPiece("cobalt"))); |
| brpc::MysqlResponse response; |
| brpc::Controller cntl; |
| channel_.CallMethod(NULL, &cntl, &request, &response, NULL); |
| ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); |
| ASSERT_GE(response.reply_size(), 1u); |
| const brpc::MysqlReply& r = response.reply(0); |
| ASSERT_TRUE(r.is_resultset()) << r; |
| ASSERT_EQ(1u, r.row_count()); |
| const brpc::MysqlReply::Field& f = r.next().field(0); |
| // id is INT; brpc surfaces a signed INT column as sinteger. |
| ASSERT_TRUE(f.is_sinteger() || f.is_integer()) |
| << "expected an integer id field"; |
| if (f.is_sinteger()) { |
| EXPECT_EQ(528, f.sinteger()); |
| } else { |
| EXPECT_EQ(528u, f.integer()); |
| } |
| } |
| |
| // (c) two INT params in an arithmetic expression -> their sum. |
| { |
| PREPARE_OR_FAIL(s, "SELECT CAST(? AS SIGNED) + CAST(? AS SIGNED)"); |
| ASSERT_EQ(2u, s->param_count()); |
| brpc::MysqlRequest request(s.get()); |
| ASSERT_TRUE(request.AddParam((int32_t)315)); |
| ASSERT_TRUE(request.AddParam((int32_t)28)); |
| brpc::MysqlResponse response; |
| brpc::Controller cntl; |
| channel_.CallMethod(NULL, &cntl, &request, &response, NULL); |
| ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); |
| ASSERT_GE(response.reply_size(), 1u); |
| const brpc::MysqlReply& r = response.reply(0); |
| ASSERT_TRUE(r.is_resultset()) << r; |
| ASSERT_EQ(1u, r.row_count()); |
| const brpc::MysqlReply::Field& f = r.next().field(0); |
| // The sum comes back as a (possibly wide) integer; accept any width. |
| ASSERT_FALSE(f.is_nil()); |
| long long got = 0; |
| if (f.is_sbigint()) got = f.sbigint(); |
| else if (f.is_bigint()) got = (long long)f.bigint(); |
| else if (f.is_sinteger()) got = f.sinteger(); |
| else if (f.is_integer()) got = f.integer(); |
| else if (f.is_string()) got = atoll(f.string().as_string().c_str()); |
| else FAIL() << "unexpected field type for ?+?"; |
| EXPECT_EQ(343, got); |
| } |
| } |
| |
| // Re-execute one statement with new parameters, and fetch every column type |
| // (INT, VARCHAR, BIGINT) of a single matched row through its typed accessor. |
| TEST_F(MysqlPreparedTest, ReExecuteAndTypedColumnFetch) { |
| SKIP_IF_NO_SERVER(); |
| |
| // Re-execute the SAME statement twice with different bound ids. |
| { |
| PREPARE_OR_FAIL(s, "SELECT name FROM ps_people WHERE id=?"); |
| struct Case { int32_t id; const char* name; }; |
| const Case cases[] = {{417, "maple"}, {528, "cobalt"}}; |
| for (const Case& c : cases) { |
| SCOPED_TRACE(c.id); |
| brpc::MysqlRequest request(s.get()); |
| ASSERT_TRUE(request.AddParam(c.id)); |
| brpc::MysqlResponse response; |
| brpc::Controller cntl; |
| channel_.CallMethod(NULL, &cntl, &request, &response, NULL); |
| ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); |
| ASSERT_GE(response.reply_size(), 1u); |
| const brpc::MysqlReply& r = response.reply(0); |
| ASSERT_TRUE(r.is_resultset()) << r; |
| ASSERT_EQ(1u, r.row_count()); |
| const brpc::MysqlReply::Field& f = r.next().field(0); |
| ASSERT_TRUE(f.is_string()); |
| EXPECT_EQ(c.name, f.string().as_string()); |
| } |
| } |
| |
| // Typed fetch: read INT id, VARCHAR name and BIGINT score off one row. |
| { |
| PREPARE_OR_FAIL(s, "SELECT id, name, score FROM ps_people WHERE id=?"); |
| brpc::MysqlRequest request(s.get()); |
| ASSERT_TRUE(request.AddParam((int32_t)528)); |
| brpc::MysqlResponse response; |
| brpc::Controller cntl; |
| channel_.CallMethod(NULL, &cntl, &request, &response, NULL); |
| ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); |
| ASSERT_GE(response.reply_size(), 1u); |
| const brpc::MysqlReply& r = response.reply(0); |
| ASSERT_TRUE(r.is_resultset()) << r; |
| ASSERT_EQ(3u, r.column_count()); |
| ASSERT_EQ(1u, r.row_count()); |
| const brpc::MysqlReply::Row& row = r.next(); |
| |
| // Column 0: INT id == 528. |
| const brpc::MysqlReply::Field& id = row.field(0); |
| ASSERT_TRUE(id.is_sinteger() || id.is_integer()); |
| EXPECT_EQ(528, id.is_sinteger() ? id.sinteger() : (int)id.integer()); |
| |
| // Column 1: VARCHAR name == "cobalt". |
| const brpc::MysqlReply::Field& name = row.field(1); |
| ASSERT_TRUE(name.is_string()); |
| EXPECT_EQ("cobalt", name.string().as_string()); |
| |
| // Column 2: BIGINT score == 9200. |
| const brpc::MysqlReply::Field& score = row.field(2); |
| ASSERT_TRUE(score.is_sbigint() || score.is_bigint() || |
| score.is_sinteger() || score.is_integer()) |
| << "expected an integer score field"; |
| long long sc = 0; |
| if (score.is_sbigint()) sc = score.sbigint(); |
| else if (score.is_bigint()) sc = (long long)score.bigint(); |
| else if (score.is_sinteger()) sc = score.sinteger(); |
| else sc = score.integer(); |
| EXPECT_EQ(9200, sc); |
| } |
| } |
| |
| // NULL handling both ways: a column whose value is SQL NULL (the seed row with |
| // a NULL name) and a literal NULL in the SELECT list both surface as nil. |
| TEST_F(MysqlPreparedTest, NullColumnAndLiteralNullAreNil) { |
| SKIP_IF_NO_SERVER(); |
| |
| // A row with a NULL name column surfaces field(0) as nil. |
| { |
| PREPARE_OR_FAIL(s, "SELECT name FROM ps_people WHERE id=?"); |
| brpc::MysqlRequest request(s.get()); |
| ASSERT_TRUE(request.AddParam((int32_t)639)); |
| brpc::MysqlResponse response; |
| brpc::Controller cntl; |
| channel_.CallMethod(NULL, &cntl, &request, &response, NULL); |
| ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); |
| ASSERT_GE(response.reply_size(), 1u); |
| const brpc::MysqlReply& r = response.reply(0); |
| ASSERT_TRUE(r.is_resultset()) << r; |
| ASSERT_EQ(1u, r.row_count()); |
| EXPECT_TRUE(r.next().field(0).is_nil()); |
| } |
| |
| // A literal NULL in the SELECT list also comes back nil. |
| { |
| PREPARE_OR_FAIL(s, "SELECT NULL"); |
| EXPECT_EQ(0u, s->param_count()); |
| brpc::MysqlRequest request(s.get()); |
| brpc::MysqlResponse response; |
| brpc::Controller cntl; |
| channel_.CallMethod(NULL, &cntl, &request, &response, NULL); |
| ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); |
| ASSERT_GE(response.reply_size(), 1u); |
| const brpc::MysqlReply& r = response.reply(0); |
| ASSERT_TRUE(r.is_resultset()) << r; |
| ASSERT_EQ(1u, r.row_count()); |
| EXPECT_TRUE(r.next().field(0).is_nil()); |
| } |
| } |
| |
| // Error paths must not crash the client: a malformed statement and a |
| // parameter-count mismatch each surface either a failed RPC or an error reply, |
| // never a silent success or a crash. |
| TEST_F(MysqlPreparedTest, MalformedAndParamMismatchSurfaceErrors) { |
| SKIP_IF_NO_SERVER(); |
| |
| // Malformed SQL: dangling WHERE with no predicate. |
| { |
| auto s = brpc::NewMysqlStatement( |
| channel_, "SELECT id FROM ps_people WHERE id=? AND WHERE"); |
| // Acceptable: prepare returns null, OR the first execute reports an |
| // error reply. A crash or a silent success is not. |
| if (s == nullptr) { |
| SUCCEED() << "prepare of malformed SQL returned null as expected"; |
| } else { |
| brpc::MysqlRequest request(s.get()); |
| brpc::MysqlResponse response; |
| brpc::Controller cntl; |
| channel_.CallMethod(NULL, &cntl, &request, &response, NULL); |
| if (cntl.Failed()) { |
| SUCCEED() << "execute of malformed statement failed as expected: " |
| << cntl.ErrorText(); |
| } else { |
| ASSERT_GE(response.reply_size(), 1u); |
| EXPECT_TRUE(response.reply(0).is_error()) |
| << "malformed prepared statement unexpectedly succeeded"; |
| } |
| } |
| } |
| |
| // Bind too few params: one-? statement executed with zero params. |
| { |
| PREPARE_OR_FAIL(s, "SELECT name FROM ps_people WHERE id=?"); |
| ASSERT_EQ(1u, s->param_count()); |
| brpc::MysqlRequest request(s.get()); |
| brpc::MysqlResponse response; |
| brpc::Controller cntl; |
| channel_.CallMethod(NULL, &cntl, &request, &response, NULL); |
| if (cntl.Failed()) { |
| SUCCEED() << "mismatched param count failed the RPC as expected: " |
| << cntl.ErrorText(); |
| } else { |
| ASSERT_GE(response.reply_size(), 1u); |
| EXPECT_TRUE(response.reply(0).is_error()) |
| << "execute with too few params unexpectedly produced a " |
| "non-error reply"; |
| } |
| } |
| } |
| |
| // One statement re-used across executes agrees with itself, and a second, |
| // independent statement on the same channel still works afterward. |
| TEST_F(MysqlPreparedTest, StatementReuseAndIndependentStatement) { |
| SKIP_IF_NO_SERVER(); |
| PREPARE_OR_FAIL(s1, "SELECT COUNT(*) FROM ps_people"); |
| |
| // Execute s1 twice; both must agree. |
| long long first_count = -1; |
| for (int iter = 0; iter < 2; ++iter) { |
| SCOPED_TRACE(iter); |
| brpc::MysqlRequest request(s1.get()); |
| brpc::MysqlResponse response; |
| brpc::Controller cntl; |
| channel_.CallMethod(NULL, &cntl, &request, &response, NULL); |
| ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); |
| ASSERT_GE(response.reply_size(), 1u); |
| const brpc::MysqlReply& r = response.reply(0); |
| ASSERT_TRUE(r.is_resultset()) << r; |
| ASSERT_EQ(1u, r.row_count()); |
| const brpc::MysqlReply::Field& f = r.next().field(0); |
| long long c = 0; |
| if (f.is_sbigint()) c = f.sbigint(); |
| else if (f.is_bigint()) c = (long long)f.bigint(); |
| else if (f.is_sinteger()) c = f.sinteger(); |
| else if (f.is_integer()) c = f.integer(); |
| else if (f.is_string()) c = atoll(f.string().as_string().c_str()); |
| else FAIL() << "unexpected COUNT(*) field type"; |
| if (first_count < 0) first_count = c; |
| EXPECT_EQ(first_count, c); |
| } |
| EXPECT_EQ(3, first_count) << "seed table should hold 3 rows"; |
| |
| // A second, independent statement on the same channel still works after |
| // s1 has been used -- exercises concurrent statement objects / reuse. |
| PREPARE_OR_FAIL(s2, "SELECT id FROM ps_people WHERE id=?"); |
| brpc::MysqlRequest request(s2.get()); |
| ASSERT_TRUE(request.AddParam((int32_t)417)); |
| brpc::MysqlResponse response; |
| brpc::Controller cntl; |
| channel_.CallMethod(NULL, &cntl, &request, &response, NULL); |
| ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); |
| ASSERT_GE(response.reply_size(), 1u); |
| ASSERT_TRUE(response.reply(0).is_resultset()); |
| EXPECT_EQ(1u, response.reply(0).row_count()); |
| } |
| |
| // BINARY-protocol TIME and DATETIME column parsing |
| // (MysqlReply::Field::ParseBinaryTime / ParseBinaryDataTime). |
| // |
| // These code paths are ONLY reached over the prepared-statement (binary) |
| // result protocol -- a plain text Query would return the value pre-formatted |
| // by the server and never touch ParseBinaryTime/ParseBinaryDataTime. So every |
| // case here PREPAREs a SELECT and executes it, forcing brpc to decode the |
| // packed wire bytes itself. |
| // |
| // TIME and DATETIME columns are surfaced as STRINGS: MysqlReply::Field's only |
| // text accessor is string() (returning a butil::StringPiece), and |
| // is_string() returns true for MYSQL_FIELD_TYPE_TIME and |
| // MYSQL_FIELD_TYPE_DATETIME (see mysql_reply.h). The parser writes the |
| // formatted text into _data.str via str.set(ptr, len) with an explicitly |
| // computed length, so comparing the FULL string (length included) against the |
| // exact expected text catches any trailing-garbage / wrong-length bug -- in |
| // particular the variable-width TIME path (optional sign, 2- vs 3+-digit |
| // hour) that has historically mis-sized its output. |
| // |
| // We use CAST(literal AS TIME/DATETIME[(N)]) so the exact value (and the |
| // column's declared fractional-second precision, which drives the wire |
| // length) is fully under our control. |
| TEST_F(MysqlPreparedTest, BinaryTimeAndDateTimeParsing) { |
| SKIP_IF_NO_SERVER(); |
| |
| struct Case { |
| const char* sql; // prepared SELECT producing one TIME/DATETIME field |
| const char* expected; // exact string the field must equal |
| }; |
| const Case cases[] = { |
| // TIME, ordinary 2-digit hour. |
| {"SELECT CAST('12:34:56' AS TIME)", "12:34:56"}, |
| // TIME, 3-digit hour: the variable-width hour path (total_hour >= 100). |
| {"SELECT CAST('300:00:00' AS TIME)", "300:00:00"}, |
| // TIME, the documented maximum magnitude. |
| {"SELECT CAST('838:59:59' AS TIME)", "838:59:59"}, |
| // TIME, negative: leading '-' sign byte on the wire. |
| {"SELECT CAST('-12:30:45' AS TIME)", "-12:30:45"}, |
| // TIME with fractional seconds (decimal=3 -> 12-byte wire packet). |
| {"SELECT CAST('01:02:03.456' AS TIME(3))", "01:02:03.456"}, |
| // DATETIME with no sub-second part (7-byte wire packet). |
| {"SELECT CAST('2021-03-04 05:06:07' AS DATETIME)", "2021-03-04 05:06:07"}, |
| // DATETIME with microseconds (decimal=6 -> 11-byte wire packet). |
| {"SELECT CAST('2021-03-04 05:06:07.123456' AS DATETIME(6))", |
| "2021-03-04 05:06:07.123456"}, |
| // DATETIME at exact midnight: MySQL omits the time-of-day part, so this |
| // arrives as a 4-byte (len==4) wire packet. The parser must emit the |
| // full "YYYY-MM-DD 00:00:00" form and report exactly 19 bytes. |
| {"SELECT CAST('2021-03-04 00:00:00' AS DATETIME)", |
| "2021-03-04 00:00:00"}, |
| // DATE column: only the date part on the wire (len==4) -> "YYYY-MM-DD". |
| {"SELECT CAST('2021-03-04' AS DATE)", "2021-03-04"}, |
| // TIME zero value: encoded with len==0 (no field bytes on the wire). |
| // This must surface as the zero string "00:00:00", NOT as NULL. |
| {"SELECT CAST('00:00:00' AS TIME)", "00:00:00"}, |
| }; |
| |
| for (const Case& c : cases) { |
| SCOPED_TRACE(c.sql); |
| PREPARE_OR_FAIL(s, c.sql); |
| EXPECT_EQ(0u, s->param_count()); |
| brpc::MysqlRequest request(s.get()); |
| brpc::MysqlResponse response; |
| brpc::Controller cntl; |
| channel_.CallMethod(NULL, &cntl, &request, &response, NULL); |
| ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); |
| ASSERT_GE(response.reply_size(), 1u); |
| const brpc::MysqlReply& r = response.reply(0); |
| ASSERT_TRUE(r.is_resultset()) << "expected a result set, got: " << r; |
| ASSERT_EQ(1u, r.column_count()); |
| ASSERT_EQ(1u, r.row_count()); |
| const brpc::MysqlReply::Field& f = r.next().field(0); |
| // The binary TIME/DATETIME value must be surfaced as a string (this is |
| // the ParseBinaryTime / ParseBinaryDataTime output). |
| ASSERT_TRUE(f.is_string()) |
| << "TIME/DATETIME field should be exposed as a string"; |
| // Compare the FULL string, including its length: a trailing-garbage or |
| // off-by-one length bug in the parser would make this exact compare |
| // fail even if the visible prefix looks right. |
| const std::string got = f.string().as_string(); |
| EXPECT_EQ(c.expected, got) |
| << "binary-parsed value mismatch (got length " << got.size() |
| << ", expected length " << strlen(c.expected) << ")"; |
| EXPECT_EQ(strlen(c.expected), got.size()) |
| << "binary-parsed value has wrong length"; |
| } |
| } |
| |
| } // namespace |