// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #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(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