chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,742 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user