Skip to content
Open
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: 10 additions & 4 deletions cpp/src/arrow/flight/sql/odbc/odbc_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1581,8 +1581,14 @@ SQLRETURN SQLDescribeCol(SQLHSTMT stmt, SQLUSMALLINT column_number, SQLWCHAR* co
case SQL_REAL:
case SQL_FLOAT:
case SQL_DOUBLE: {
ird->GetField(column_number, SQL_DESC_PRECISION, column_size_ptr,
sizeof(SQLULEN), nullptr);
// SQL_DESC_PRECISION is stored as a SQLSMALLINT. Read it into a
// correctly-sized local and widen into the SQLULEN output. Passing the
// SQLULEN* directly would only write the low 2 bytes and leave the
// upper 6 bytes uninitialized.
SQLSMALLINT precision = 0;
ird->GetField(column_number, SQL_DESC_PRECISION, &precision, sizeof(precision),
nullptr);
*column_size_ptr = static_cast<SQLULEN>(precision);
break;
}

Expand All @@ -1604,7 +1610,7 @@ SQLRETURN SQLDescribeCol(SQLHSTMT stmt, SQLUSMALLINT column_number, SQLWCHAR* co
case SQL_DECIMAL:
case SQL_NUMERIC: {
ird->GetField(column_number, SQL_DESC_SCALE, decimal_digits_ptr,
sizeof(SQLULEN), nullptr);
sizeof(SQLSMALLINT), nullptr);
break;
}

Expand All @@ -1622,7 +1628,7 @@ SQLRETURN SQLDescribeCol(SQLHSTMT stmt, SQLUSMALLINT column_number, SQLWCHAR* co
case SQL_INTERVAL_HOUR_TO_SECOND:
case SQL_INTERVAL_DAY_TO_SECOND: {
ird->GetField(column_number, SQL_DESC_PRECISION, decimal_digits_ptr,
sizeof(SQLULEN), nullptr);
sizeof(SQLSMALLINT), nullptr);
break;
}

Expand Down
86 changes: 86 additions & 0 deletions cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2453,6 +2453,92 @@ TEST_F(ColumnsRemoteTest, SQLDescribeColODBCTestTableMetadata) {
}
}

// Sentinel used to pre-fill the SQLULEN column_size output. If SQLDescribeCol
// only writes the low bytes (the width bug guarded against here), the upper
// bytes remain set to this pattern and the value is far outside any legal
// column size.
static constexpr SQLULEN kColumnSizeSentinel =
static_cast<SQLULEN>(0xFFFFFFFFFFFFFFFFULL);

// Verify that SQLDescribeCol fully initializes the SQLULEN column_size output
// for a DECIMAL column. Prior to GH-50560 the numeric path read
// SQL_DESC_PRECISION (a SQLSMALLINT) straight into the SQLULEN* output, writing
// only 2 of the 8 bytes and leaving the upper 6 bytes as uninitialized garbage.
// The mock server reports SQL_WVARCHAR for `SELECT ... AS` columns, so this is
// a remote-only test.
TEST_F(ColumnsRemoteTest, SQLDescribeColDecimalColumnSizeIsFullyWritten) {
std::wstring wsql = this->GetQueryAllDataTypes();
std::vector<SQLWCHAR> sql0(wsql.begin(), wsql.end());

ASSERT_EQ(SQL_SUCCESS,
SQLExecDirect(this->stmt, &sql0[0], static_cast<SQLINTEGER>(sql0.size())));

// decimal_positive column in the all-data-types query.
constexpr SQLUSMALLINT kDecimalColumn = 17;

SQLWCHAR column_name[1024];
SQLSMALLINT buf_char_len = sizeof(column_name) / GetSqlWCharSize();
SQLSMALLINT name_length = 0;
SQLSMALLINT data_type = 0;
SQLULEN column_size = kColumnSizeSentinel;
SQLSMALLINT decimal_digits = -1;
SQLSMALLINT nullable = 0;

ASSERT_EQ(SQL_SUCCESS, SQLDescribeCol(this->stmt, kDecimalColumn, column_name,
buf_char_len, &name_length, &data_type,
&column_size, &decimal_digits, &nullable));

EXPECT_EQ(SQL_DECIMAL, data_type);

// The precision (column size) is a small positive integer. If only the low
// bytes were written, the sentinel's upper bytes would remain set and the
// full 8-byte value would be enormous. Checking the whole SQLULEN (not just a
// truncated view of it) is what catches the short write.
EXPECT_NE(kColumnSizeSentinel, column_size);
EXPECT_GT(column_size, 0u);
EXPECT_LT(column_size, 100u) << "column_size upper bytes look uninitialized: 0x"
<< std::hex << column_size;

EXPECT_GE(decimal_digits, 0);
EXPECT_LE(static_cast<SQLULEN>(decimal_digits), column_size);
}

// Verify SQLDescribeCol reports a fully-written column_size for a TIMESTAMP
// column, exercising the datetime branch. Remote-only for the same reason as
// the DECIMAL test above.
TEST_F(ColumnsRemoteTest, SQLDescribeColTimestampColumnSizeIsFullyWritten) {
std::wstring wsql = this->GetQueryAllDataTypes();
std::vector<SQLWCHAR> sql0(wsql.begin(), wsql.end());

ASSERT_EQ(SQL_SUCCESS,
SQLExecDirect(this->stmt, &sql0[0], static_cast<SQLINTEGER>(sql0.size())));

// timestamp_max column in the all-data-types query.
constexpr SQLUSMALLINT kTimestampColumn = 31;

SQLWCHAR column_name[1024];
SQLSMALLINT buf_char_len = sizeof(column_name) / GetSqlWCharSize();
SQLSMALLINT name_length = 0;
SQLSMALLINT data_type = 0;
SQLULEN column_size = kColumnSizeSentinel;
SQLSMALLINT decimal_digits = -1;
SQLSMALLINT nullable = 0;

ASSERT_EQ(SQL_SUCCESS, SQLDescribeCol(this->stmt, kTimestampColumn, column_name,
buf_char_len, &name_length, &data_type,
&column_size, &decimal_digits, &nullable));

EXPECT_EQ(SQL_TYPE_TIMESTAMP, data_type);

EXPECT_NE(kColumnSizeSentinel, column_size);
EXPECT_GT(column_size, 0u);
EXPECT_LT(column_size, 100u) << "column_size upper bytes look uninitialized: 0x"
<< std::hex << column_size;

EXPECT_GE(decimal_digits, 0);
EXPECT_LE(decimal_digits, 9);
}

TEST_F(ColumnsOdbcV2RemoteTest, SQLDescribeColODBCTestTableMetadataODBCVer2) {
// Test assumes there is a table $scratch.ODBCTest in remote server
SQLWCHAR column_name[1024];
Expand Down