Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions driver/statement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@

static const char exception_code_header_name[] = "X-ClickHouse-Exception-Code";

// The length/indicator buffer decides whether the parameter is NULL: an application
// is free to keep a value buffer bound and signal NULL by setting the indicator to
// SQL_NULL_DATA, which is what reusing the same buffer for every row amounts to.
static bool isNullParamValue(const ParamBindingInfo & binding_info) noexcept {
return (
binding_info.value == nullptr ||
(binding_info.indicator != nullptr && *binding_info.indicator == SQL_NULL_DATA)
);
}

Statement::Statement(Connection & connection)
: ChildType(connection)
{
Expand Down Expand Up @@ -84,7 +94,7 @@ Statement::HttpRequestData Statement::prepareHttpRequest()
if (!isInputParam(binding_info.io_type) || isStreamParam(binding_info.io_type))
throw std::runtime_error("Unable to extract data from bound param buffer: param IO type is not supported");

if (binding_info.value == nullptr)
if (isNullParamValue(binding_info))
value = "\\N";
else {
readReadyDataTo(binding_info, value);
Expand Down Expand Up @@ -333,7 +343,7 @@ std::string Statement::buildFinalQuery(const std::vector<ParamBindingInfo>& para
type_info.value_max_size = binding_info.value_max_size;
type_info.precision = binding_info.precision;
type_info.scale = binding_info.scale;
type_info.is_nullable = (binding_info.is_nullable || binding_info.value == nullptr);
type_info.is_nullable = (binding_info.is_nullable || isNullParamValue(binding_info));

param_type = convertSQLOrCTypeToDataSourceType(type_info);
}
Expand Down
29 changes: 27 additions & 2 deletions driver/test/statement_parameter_binding_ut.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -254,15 +254,40 @@ TEST_F(StatementBindingTest, FromCGUID) {

TEST_F(StatementBindingTest, NullValueBinding) {
prepare("select ?");
SQLINTEGER null_value;
SQLINTEGER null_value = 12345;
SQLLEN null_ind = SQL_NULL_DATA;

bind(1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, &null_value, 0, &null_ind);

auto [query, params] = execute();
ASSERT_EQ(query, "select {odbc_positional_1:Nullable(Int32)}");
ASSERT_EQ(params.size(), 1);
ASSERT_TRUE(params["param_odbc_positional_1"].empty());
ASSERT_EQ(params["param_odbc_positional_1"], "\\N");
}

TEST_F(StatementBindingTest, NullValueBindingWithoutBuffer) {
prepare("select ?");
SQLLEN null_ind = SQL_NULL_DATA;

bind(1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, nullptr, 0, &null_ind);

auto [query, params] = execute();
ASSERT_EQ(query, "select {odbc_positional_1:Nullable(Int32)}");
ASSERT_EQ(params.size(), 1);
ASSERT_EQ(params["param_odbc_positional_1"], "\\N");
}

TEST_F(StatementBindingTest, NullValueBindingForString) {
prepare("select ?");
char char_value[] = "Test String";
SQLLEN null_ind = SQL_NULL_DATA;

bind(1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 20, 0, char_value, sizeof(char_value), &null_ind);

auto [query, params] = execute();
ASSERT_EQ(query, "select {odbc_positional_1:LowCardinality(Nullable(String))}");
ASSERT_EQ(params.size(), 1);
ASSERT_EQ(params["param_odbc_positional_1"], "\\N");
}

TEST_F(StatementBindingTest, FunctionLocate) {
Expand Down
90 changes: 90 additions & 0 deletions driver/test/statement_parameter_bindings_it.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,96 @@ TEST_F(StatementParameterBindingsTest, NullValueForString) {
ASSERT_EQ(SQLFetch(hstmt), SQL_NO_DATA);
}

TEST_F(StatementParameterBindingsTest, NullValueForIntegerWithBoundBuffer) {
auto query = fromUTF8<PTChar>("SELECT isNull(?)");

SQLINTEGER param = 12345;
SQLLEN param_ind = SQL_NULL_DATA;

ODBC_CALL_ON_STMT_THROW(hstmt, SQLPrepare(hstmt, ptcharCast(query.data()), SQL_NTS));
ODBC_CALL_ON_STMT_THROW(hstmt,
SQLBindParameter(
hstmt,
1,
SQL_PARAM_INPUT,
getCTypeFor<decltype(param)>(),
SQL_INTEGER,
0,
0,
&param, // N.B.: the value buffer stays bound, the indicator says NULL.
sizeof(param),
&param_ind
)
);

ODBC_CALL_ON_STMT_THROW(hstmt, SQLExecute(hstmt));
ODBC_CALL_ON_STMT_THROW(hstmt, SQLFetch(hstmt));

SQLINTEGER col = 0;
SQLLEN col_ind = 0;

ODBC_CALL_ON_STMT_THROW(hstmt,
SQLGetData(
hstmt,
1,
getCTypeFor<decltype(col)>(),
&col,
sizeof(col),
&col_ind
)
);

ASSERT_TRUE(col_ind >= 0 || col_ind == SQL_NTS);
ASSERT_EQ(col, 1);

ASSERT_EQ(SQLFetch(hstmt), SQL_NO_DATA);
}

TEST_F(StatementParameterBindingsTest, NullValueForStringWithBoundBuffer) {
auto query = fromUTF8<PTChar>("SELECT isNull(?)");

auto param = fromUTF8<PTChar>("Test String");
SQLLEN param_ind = SQL_NULL_DATA;

ODBC_CALL_ON_STMT_THROW(hstmt, SQLPrepare(hstmt, ptcharCast(query.data()), SQL_NTS));
ODBC_CALL_ON_STMT_THROW(hstmt,
SQLBindParameter(
hstmt,
1,
SQL_PARAM_INPUT,
SQL_C_TCHAR,
SQL_VARCHAR,
0,
0,
ptcharCast(param.data()), // N.B.: the value buffer stays bound, the indicator says NULL.
param.size() * sizeof(PTChar),
&param_ind
)
);

ODBC_CALL_ON_STMT_THROW(hstmt, SQLExecute(hstmt));
ODBC_CALL_ON_STMT_THROW(hstmt, SQLFetch(hstmt));

SQLINTEGER col = 0;
SQLLEN col_ind = 0;

ODBC_CALL_ON_STMT_THROW(hstmt,
SQLGetData(
hstmt,
1,
getCTypeFor<decltype(col)>(),
&col,
sizeof(col),
&col_ind
)
);

ASSERT_TRUE(col_ind >= 0 || col_ind == SQL_NTS);
ASSERT_EQ(col, 1);

ASSERT_EQ(SQLFetch(hstmt), SQL_NO_DATA);
}

class StatementParameterArrayBindingsTest
: public StatementParameterBindingsTest
, public ::testing::WithParamInterface<std::size_t>
Expand Down
Loading