From c8704e91584ce2f9b1e0fc2d68051ad75985290d Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 31 Jul 2025 23:43:43 +0100 Subject: [PATCH 01/13] Implement SQLDescribeCol --- cpp/src/arrow/flight/sql/odbc/entry_points.cc | 10 ++ cpp/src/arrow/flight/sql/odbc/odbc.def | 1 + cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 111 ++++++++++++++++++ cpp/src/arrow/flight/sql/odbc/odbc_api.h | 5 + 4 files changed, 127 insertions(+) diff --git a/cpp/src/arrow/flight/sql/odbc/entry_points.cc b/cpp/src/arrow/flight/sql/odbc/entry_points.cc index 63faa57e45b..38b4a1fc8ed 100644 --- a/cpp/src/arrow/flight/sql/odbc/entry_points.cc +++ b/cpp/src/arrow/flight/sql/odbc/entry_points.cc @@ -287,3 +287,13 @@ SQLRETURN SQL_API SQLSetStmtAttr(SQLHSTMT stmt, SQLINTEGER attribute, SQLPOINTER SQLINTEGER stringLength) { return arrow::SQLSetStmtAttr(stmt, attribute, valuePtr, stringLength); } + +SQLRETURN SQL_API SQLDescribeCol(SQLHSTMT statementHandle, SQLUSMALLINT columnNumber, + SQLWCHAR* columnName, SQLSMALLINT bufferLength, + SQLSMALLINT* nameLengthPtr, SQLSMALLINT* dataTypePtr, + SQLULEN* columnSizePtr, SQLSMALLINT* decimalDigitsPtr, + SQLSMALLINT* nullablePtr) { + return arrow::SQLDescribeCol(statementHandle, columnNumber, columnName, bufferLength, + nameLengthPtr, dataTypePtr, columnSizePtr, + decimalDigitsPtr, nullablePtr); +} diff --git a/cpp/src/arrow/flight/sql/odbc/odbc.def b/cpp/src/arrow/flight/sql/odbc/odbc.def index 6a7402ffa90..8ba5b3fff78 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc.def +++ b/cpp/src/arrow/flight/sql/odbc/odbc.def @@ -28,6 +28,7 @@ EXPORTS SQLColAttributeW SQLColumnsW SQLConnectW + SQLDescribeColW SQLDisconnect SQLDriverConnectW SQLExecDirectW diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 461e17fae5f..9c4b55c1dc9 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -1367,4 +1367,115 @@ SQLRETURN SQLNativeSql(SQLHDBC connectionHandle, SQLWCHAR* inStatementText, }); } +SQLRETURN SQLDescribeCol(SQLHSTMT stmt, SQLUSMALLINT columnNumber, SQLWCHAR* columnName, + SQLSMALLINT bufferLength, SQLSMALLINT* nameLengthPtr, + SQLSMALLINT* dataTypePtr, SQLULEN* columnSizePtr, + SQLSMALLINT* decimalDigitsPtr, SQLSMALLINT* nullablePtr) { + LOG_DEBUG( + "SQLDescribeColW called with stmt: {}, columnNumber: {}, " + "columnName: {}, bufferLength: {}, nameLengthPtr: {}, dataTypePtr: {}, " + "columnSizePtr: {}, decimalDigitsPtr: {}, nullablePtr: {}", + stmt, columnNumber, fmt::ptr(columnName), bufferLength, fmt::ptr(nameLengthPtr), + fmt::ptr(dataTypePtr), fmt::ptr(columnSizePtr), fmt::ptr(decimalDigitsPtr), + fmt::ptr(nullablePtr)); + using ODBC::ODBCDescriptor; + using ODBC::ODBCStatement; + + return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { + ODBCStatement* statement = reinterpret_cast(stmt); + ODBCDescriptor* ird = statement->GetIRD(); + + SQLSMALLINT sqlType; + ird->GetField(columnNumber, SQL_DESC_CONCISE_TYPE, &sqlType, sizeof(SQLSMALLINT), + nullptr); + + SQLINTEGER outputLengthInt; + ird->GetField(columnNumber, SQL_DESC_NAME, columnName, bufferLength, + &outputLengthInt); + if (nameLengthPtr) { + *nameLengthPtr = static_cast(outputLengthInt); + } + + if (dataTypePtr) { + *dataTypePtr = sqlType; + } + + // Column Size + if (columnSizePtr) { + switch (sqlType) { + // All numeric types + case SQL_DECIMAL: + case SQL_NUMERIC: + case SQL_TINYINT: + case SQL_SMALLINT: + case SQL_INTEGER: + case SQL_BIGINT: + case SQL_REAL: + case SQL_FLOAT: + case SQL_DOUBLE: { + ird->GetField(columnNumber, SQL_DESC_PRECISION, columnSizePtr, sizeof(SQLULEN), + nullptr); + break; + } + + default: { + ird->GetField(columnNumber, SQL_DESC_LENGTH, columnSizePtr, sizeof(SQLULEN), + nullptr); + } + } + } + + // Decimal Digits + if (decimalDigitsPtr) { + switch (sqlType) { + // All exact numeric types + case SQL_TINYINT: + case SQL_SMALLINT: + case SQL_INTEGER: + case SQL_BIGINT: + case SQL_DECIMAL: + case SQL_NUMERIC: { + ird->GetField(columnNumber, SQL_DESC_SCALE, decimalDigitsPtr, sizeof(SQLULEN), + nullptr); + break; + } + + // All datetime types + case SQL_DATE: + case SQL_TIME: + case SQL_TIMESTAMP: + // TODO THESE ARE UNDEFINED - ODBC 3 Only Datetime Types + // SQL_TYPE_TIMESTAMP_WITH_TIMEZONE and SQL_TYPE_TIME_WITH_TIMEZONE are rarely + // used and not universally supported. + // case SQL_TYPE_TIME_WITH_TIMEZONE: + // case SQL_TYPE_TIMESTAMP_WITH_TIMEZONE: + // + // All interval types with a seconds component + case SQL_INTERVAL_SECOND: + case SQL_INTERVAL_MINUTE_TO_SECOND: + case SQL_INTERVAL_HOUR_TO_SECOND: + case SQL_INTERVAL_DAY_TO_SECOND: { + ird->GetField(columnNumber, SQL_DESC_PRECISION, decimalDigitsPtr, + sizeof(SQLULEN), nullptr); + break; + } + + default: { + // All character and binary types + // SQL_BIT + // All approximate numeric types + // All interval types with no seconds component + *decimalDigitsPtr = static_cast(0); + } + } + } + + // Nullable + ird->GetField(columnNumber, SQL_DESC_NULLABLE, nullablePtr, sizeof(SQLSMALLINT), + nullptr); + + return SQL_SUCCESS; + }); +} + } // namespace arrow diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.h b/cpp/src/arrow/flight/sql/odbc/odbc_api.h index 6aa2ec681af..94a7dc0ec3e 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.h +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.h @@ -96,4 +96,9 @@ SQLRETURN SQLGetTypeInfo(SQLHSTMT stmt, SQLSMALLINT dataType); SQLRETURN SQLNativeSql(SQLHDBC connectionHandle, SQLWCHAR* inStatementText, SQLINTEGER inStatementTextLength, SQLWCHAR* outStatementText, SQLINTEGER bufferLength, SQLINTEGER* outStatementTextLength); +SQLRETURN SQLDescribeCol(SQLHSTMT statementHandle, SQLUSMALLINT columnNumber, + SQLWCHAR* columnName, SQLSMALLINT bufferLength, + SQLSMALLINT* nameLengthPtr, SQLSMALLINT* dataTypePtr, + SQLULEN* columnSizePtr, SQLSMALLINT* decimalDigitsPtr, + SQLSMALLINT* nullablePtr); } // namespace arrow From a51bdba7e866b762eb12b48f45d638dac65855fd Mon Sep 17 00:00:00 2001 From: rscales Date: Wed, 6 Aug 2025 19:13:14 +0100 Subject: [PATCH 02/13] Add initial set of tests for SQLDescribeCol --- .../flight/sql/odbc/tests/columns_test.cc | 453 +++++++++++++++++- 1 file changed, 450 insertions(+), 3 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc index 08a86d4a840..30ca4a14125 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc @@ -26,9 +26,6 @@ #include "gtest/gtest.h" -// TODO: add tests with SQLDescribeCol to check metadata of SQLColumns for ODBC 2 and -// ODBC 3. - namespace arrow::flight::sql::odbc { // Helper functions void checkSQLColumns( @@ -2321,4 +2318,454 @@ TYPED_TEST(FlightSQLODBCTestBase, TestSQLColAttributesUpdatable) { this->disconnect(); } + +TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColValidateInput) { + this->connect(); + this->CreateTestTables(); + + SQLSMALLINT columnCount = 0; + SQLSMALLINT expectedValue = 3; + SQLWCHAR sqlQuery[] = L"SELECT * FROM TestTable LIMIT 1;"; + SQLINTEGER queryLength = static_cast(wcslen(sqlQuery)); + + SQLUSMALLINT bookmarkColumn = 0; + SQLUSMALLINT validColumn = 1; + SQLUSMALLINT outOfRangeColumn = 4; + SQLUSMALLINT negativeColumn = -1; + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT dataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + + SQLRETURN ret = SQLExecDirect(this->stmt, sqlQuery, queryLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + // Invalid descriptor index - Bookmarks are not supported + ret = SQLDescribeCol(this->stmt, bookmarkColumn, columnName, bufCharLen, &nameLength, + &dataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_07009); + + // Invalid descriptor index - index out of range + ret = + SQLDescribeCol(this->stmt, outOfRangeColumn, columnName, bufCharLen, &nameLength, + &dataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_07009); + + // Invalid descriptor index - index out of range + ret = SQLDescribeCol(this->stmt, negativeColumn, columnName, bufCharLen, &nameLength, + &dataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_ERROR); + VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_07009); + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColQueryAllDataTypesMetadataMock) { + this->connect(); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + std::wstring wsql = this->getQueryAllDataTypes(); + std::vector sql0(wsql.begin(), wsql.end()); + + SQLWCHAR* columnNames[] = { + (SQLWCHAR*)L"stiny_int_min", (SQLWCHAR*)L"stiny_int_max", + (SQLWCHAR*)L"utiny_int_min", (SQLWCHAR*)L"utiny_int_max", + (SQLWCHAR*)L"ssmall_int_min", (SQLWCHAR*)L"ssmall_int_max", + (SQLWCHAR*)L"usmall_int_min", (SQLWCHAR*)L"usmall_int_max", + (SQLWCHAR*)L"sinteger_min", (SQLWCHAR*)L"sinteger_max", + (SQLWCHAR*)L"uinteger_min", (SQLWCHAR*)L"uinteger_max", + (SQLWCHAR*)L"sbigint_min", (SQLWCHAR*)L"sbigint_max", + (SQLWCHAR*)L"ubigint_min", (SQLWCHAR*)L"ubigint_max", + (SQLWCHAR*)L"decimal_negative", (SQLWCHAR*)L"decimal_positive", + (SQLWCHAR*)L"float_min", (SQLWCHAR*)L"float_max", + (SQLWCHAR*)L"double_min", (SQLWCHAR*)L"double_max", + (SQLWCHAR*)L"bit_false", (SQLWCHAR*)L"bit_true", + (SQLWCHAR*)L"c_char", (SQLWCHAR*)L"c_wchar", + (SQLWCHAR*)L"c_wvarchar", (SQLWCHAR*)L"c_varchar", + (SQLWCHAR*)L"date_min", (SQLWCHAR*)L"date_max", + (SQLWCHAR*)L"timestamp_min", (SQLWCHAR*)L"timestamp_max"}; + SQLSMALLINT columnDataTypes[] = { + SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_WVARCHAR, SQL_WVARCHAR}; + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, 1024); + EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColQueryAllDataTypesMetadataRemote) { + this->connect(); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + std::wstring wsql = this->getQueryAllDataTypes(); + std::vector sql0(wsql.begin(), wsql.end()); + + SQLWCHAR* columnNames[] = { + (SQLWCHAR*)L"stiny_int_min", (SQLWCHAR*)L"stiny_int_max", + (SQLWCHAR*)L"utiny_int_min", (SQLWCHAR*)L"utiny_int_max", + (SQLWCHAR*)L"ssmall_int_min", (SQLWCHAR*)L"ssmall_int_max", + (SQLWCHAR*)L"usmall_int_min", (SQLWCHAR*)L"usmall_int_max", + (SQLWCHAR*)L"sinteger_min", (SQLWCHAR*)L"sinteger_max", + (SQLWCHAR*)L"uinteger_min", (SQLWCHAR*)L"uinteger_max", + (SQLWCHAR*)L"sbigint_min", (SQLWCHAR*)L"sbigint_max", + (SQLWCHAR*)L"ubigint_min", (SQLWCHAR*)L"ubigint_max", + (SQLWCHAR*)L"decimal_negative", (SQLWCHAR*)L"decimal_positive", + (SQLWCHAR*)L"float_min", (SQLWCHAR*)L"float_max", + (SQLWCHAR*)L"double_min", (SQLWCHAR*)L"double_max", + (SQLWCHAR*)L"bit_false", (SQLWCHAR*)L"bit_true", + (SQLWCHAR*)L"c_char", (SQLWCHAR*)L"c_wchar", + (SQLWCHAR*)L"c_wvarchar", + (SQLWCHAR*)L"c_varchar", (SQLWCHAR*)L"date_min", + (SQLWCHAR*)L"date_max", (SQLWCHAR*)L"timestamp_min", + (SQLWCHAR*)L"timestamp_max"}; + SQLSMALLINT columnDataTypes[] = { + SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, + SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, + SQL_BIGINT, SQL_BIGINT, SQL_BIGINT, SQL_BIGINT, SQL_BIGINT, + SQL_WVARCHAR, SQL_DECIMAL, SQL_DECIMAL, SQL_FLOAT, SQL_FLOAT, + SQL_DOUBLE, SQL_DOUBLE, SQL_BIT, SQL_BIT, SQL_WVARCHAR, + SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_TYPE_DATE, SQL_TYPE_DATE, + SQL_TYPE_TIMESTAMP, SQL_TYPE_TIMESTAMP}; + SQLULEN columnSizes[] = {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 8, + 8, 8, 8, 8, 65536, 19, 19, 8, 8, 8, 8, + 1, 1, 65536, 65536, 65536, 65536, 10, 10, 23, 23}; + + SQLRETURN ret = + SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, columnSizes[i]); + EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadataRemote) { + // Test assumes there is a table $scratch.ODBCTest in remote server + this->connect(); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + SQLWCHAR sqlQuery[] = L"SELECT * from $scratch.ODBCTest LIMIT 1;"; + SQLINTEGER queryLength = static_cast(wcslen(sqlQuery)); + + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"sinteger_max", (SQLWCHAR*)L"sbigint_max", + (SQLWCHAR*)L"decimal_positive", (SQLWCHAR*)L"float_max", + (SQLWCHAR*)L"double_max", (SQLWCHAR*)L"bit_true", + (SQLWCHAR*)L"date_max", (SQLWCHAR*)L"time_max", + (SQLWCHAR*)L"timestamp_max"}; + SQLSMALLINT columnDataTypes[] = {SQL_INTEGER, SQL_BIGINT, SQL_DECIMAL, + SQL_FLOAT, SQL_DOUBLE, SQL_BIT, + SQL_TYPE_DATE, SQL_TYPE_TIME, SQL_TYPE_TIMESTAMP}; + SQLULEN columnSizes[] = {4, 8, 19, 8, 8, 1, 10, 12, 23}; + + SQLRETURN ret = SQLExecDirect(this->stmt, sqlQuery, queryLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, columnSizes[i]); + EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} + +TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadataRemoteODBC2) { + // Test assumes there is a table $scratch.ODBCTest in remote server + this->connect(SQL_OV_ODBC2); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + SQLWCHAR sqlQuery[] = L"SELECT * from $scratch.ODBCTest LIMIT 1;"; + SQLINTEGER queryLength = static_cast(wcslen(sqlQuery)); + + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"sinteger_max", (SQLWCHAR*)L"sbigint_max", + (SQLWCHAR*)L"decimal_positive", (SQLWCHAR*)L"float_max", + (SQLWCHAR*)L"double_max", (SQLWCHAR*)L"bit_true", + (SQLWCHAR*)L"date_max", (SQLWCHAR*)L"time_max", + (SQLWCHAR*)L"timestamp_max"}; + SQLSMALLINT columnDataTypes[] = {SQL_INTEGER, SQL_BIGINT, SQL_DECIMAL, + SQL_FLOAT, SQL_DOUBLE, SQL_BIT, + SQL_DATETIME, SQL_TIME, SQL_TIMESTAMP}; + SQLULEN columnSizes[] = {4, 8, 19, 8, 8, 1, 10, 12, 23}; + SQLULEN columnDecimalDigits[] = {0, 0, 0, 0, 0, 0, 10, 12, 23}; + + SQLRETURN ret = SQLExecDirect(this->stmt, sqlQuery, queryLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, columnSizes[i]); + EXPECT_EQ(decimalDigits, columnDecimalDigits[i]); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColAllTypesTableMetadataMock) { + // Test assumes there is a table $scratch.ODBCTest in remote server + this->connect(); + this->CreateTableAllDataType(); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + SQLWCHAR sqlQuery[] = L"SELECT * from AllTypesTable LIMIT 1;"; + SQLINTEGER queryLength = static_cast(wcslen(sqlQuery)); + + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"bigint_col", (SQLWCHAR*)L"char_col", + (SQLWCHAR*)L"varbinary_col", (SQLWCHAR*)L"double_col"}; + SQLSMALLINT columnDataTypes[] = {SQL_BIGINT, SQL_WVARCHAR, SQL_BINARY, SQL_DOUBLE}; + SQLULEN columnSizes[] = {8, 0, 0, 8}; + + SQLRETURN ret = SQLExecDirect(this->stmt, sqlQuery, queryLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, columnSizes[i]); + EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} + +TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColUnicodeTableMetadataMock) { + // Test assumes there is a table $scratch.ODBCTest in remote server + this->connect(); + this->CreateUnicodeTable(); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 1; + + SQLWCHAR sqlQuery[] = L"SELECT * from 数据 LIMIT 1;"; + SQLINTEGER queryLength = static_cast(wcslen(sqlQuery)); + + SQLWCHAR expectedColumnName[] = L"资料"; + SQLSMALLINT expectedColumnDataType = SQL_WVARCHAR; + SQLULEN expectedColumnSize = 0; + + SQLRETURN ret = SQLExecDirect(this->stmt, sqlQuery, queryLength); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLFetch(this->stmt); + + EXPECT_EQ(ret, SQL_SUCCESS); + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, expectedColumnName); + EXPECT_EQ(columnDataType, expectedColumnDataType); + EXPECT_EQ(columnSize, expectedColumnSize); + EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(nullable, SQL_NULLABLE); + + this->disconnect(); +} + } // namespace arrow::flight::sql::odbc From b17374888cce1684d3fb5665f89a896df18259b8 Mon Sep 17 00:00:00 2001 From: rscales Date: Wed, 6 Aug 2025 19:53:00 +0100 Subject: [PATCH 03/13] Format test cases --- .../flight/sql/odbc/tests/columns_test.cc | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc index 30ca4a14125..36beafe8865 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc @@ -2356,9 +2356,8 @@ TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColValidateInput) { VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_07009); // Invalid descriptor index - index out of range - ret = - SQLDescribeCol(this->stmt, outOfRangeColumn, columnName, bufCharLen, &nameLength, - &dataType, &columnSize, &decimalDigits, &nullable); + ret = SQLDescribeCol(this->stmt, outOfRangeColumn, columnName, bufCharLen, &nameLength, + &dataType, &columnSize, &decimalDigits, &nullable); EXPECT_EQ(ret, SQL_ERROR); VerifyOdbcErrorState(SQL_HANDLE_STMT, this->stmt, error_state_07009); @@ -2479,11 +2478,10 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColQueryAllDataTypesMetadataRemot (SQLWCHAR*)L"float_min", (SQLWCHAR*)L"float_max", (SQLWCHAR*)L"double_min", (SQLWCHAR*)L"double_max", (SQLWCHAR*)L"bit_false", (SQLWCHAR*)L"bit_true", - (SQLWCHAR*)L"c_char", (SQLWCHAR*)L"c_wchar", - (SQLWCHAR*)L"c_wvarchar", - (SQLWCHAR*)L"c_varchar", (SQLWCHAR*)L"date_min", - (SQLWCHAR*)L"date_max", (SQLWCHAR*)L"timestamp_min", - (SQLWCHAR*)L"timestamp_max"}; + (SQLWCHAR*)L"c_char", (SQLWCHAR*)L"c_wchar", + (SQLWCHAR*)L"c_wvarchar", (SQLWCHAR*)L"c_varchar", + (SQLWCHAR*)L"date_min", (SQLWCHAR*)L"date_max", + (SQLWCHAR*)L"timestamp_min", (SQLWCHAR*)L"timestamp_max"}; SQLSMALLINT columnDataTypes[] = { SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, SQL_INTEGER, @@ -2618,8 +2616,8 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadataRemoteODB (SQLWCHAR*)L"double_max", (SQLWCHAR*)L"bit_true", (SQLWCHAR*)L"date_max", (SQLWCHAR*)L"time_max", (SQLWCHAR*)L"timestamp_max"}; - SQLSMALLINT columnDataTypes[] = {SQL_INTEGER, SQL_BIGINT, SQL_DECIMAL, - SQL_FLOAT, SQL_DOUBLE, SQL_BIT, + SQLSMALLINT columnDataTypes[] = {SQL_INTEGER, SQL_BIGINT, SQL_DECIMAL, + SQL_FLOAT, SQL_DOUBLE, SQL_BIT, SQL_DATETIME, SQL_TIME, SQL_TIMESTAMP}; SQLULEN columnSizes[] = {4, 8, 19, 8, 8, 1, 10, 12, 23}; SQLULEN columnDecimalDigits[] = {0, 0, 0, 0, 0, 0, 10, 12, 23}; @@ -2678,7 +2676,7 @@ TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColAllTypesTableMetadataMock) { SQLWCHAR sqlQuery[] = L"SELECT * from AllTypesTable LIMIT 1;"; SQLINTEGER queryLength = static_cast(wcslen(sqlQuery)); - SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"bigint_col", (SQLWCHAR*)L"char_col", + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"bigint_col", (SQLWCHAR*)L"char_col", (SQLWCHAR*)L"varbinary_col", (SQLWCHAR*)L"double_col"}; SQLSMALLINT columnDataTypes[] = {SQL_BIGINT, SQL_WVARCHAR, SQL_BINARY, SQL_DOUBLE}; SQLULEN columnSizes[] = {8, 0, 0, 8}; From 900498d5065324ec942d2aa0a2a6d8a67b0ea6a8 Mon Sep 17 00:00:00 2001 From: rscales Date: Wed, 6 Aug 2025 21:54:02 +0100 Subject: [PATCH 04/13] Update based on comments from draft review --- cpp/src/arrow/flight/sql/odbc/odbc_api.cc | 43 ++++++++++--------- .../flight/sql/odbc/tests/columns_test.cc | 7 ++- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc index 9c4b55c1dc9..3ccde05ca5b 100644 --- a/cpp/src/arrow/flight/sql/odbc/odbc_api.cc +++ b/cpp/src/arrow/flight/sql/odbc/odbc_api.cc @@ -1384,22 +1384,25 @@ SQLRETURN SQLDescribeCol(SQLHSTMT stmt, SQLUSMALLINT columnNumber, SQLWCHAR* col return ODBCStatement::ExecuteWithDiagnostics(stmt, SQL_ERROR, [=]() { ODBCStatement* statement = reinterpret_cast(stmt); ODBCDescriptor* ird = statement->GetIRD(); - + SQLINTEGER outputLengthInt; SQLSMALLINT sqlType; + + // Column SQL Type ird->GetField(columnNumber, SQL_DESC_CONCISE_TYPE, &sqlType, sizeof(SQLSMALLINT), nullptr); - - SQLINTEGER outputLengthInt; - ird->GetField(columnNumber, SQL_DESC_NAME, columnName, bufferLength, - &outputLengthInt); - if (nameLengthPtr) { - *nameLengthPtr = static_cast(outputLengthInt); - } - if (dataTypePtr) { *dataTypePtr = sqlType; } + // Column Name + if (columnName || nameLengthPtr) { + ird->GetField(columnNumber, SQL_DESC_NAME, columnName, bufferLength, + &outputLengthInt); + if (nameLengthPtr) { + *nameLengthPtr = static_cast(outputLengthInt); + } + } + // Column Size if (columnSizePtr) { switch (sqlType) { @@ -1425,7 +1428,7 @@ SQLRETURN SQLDescribeCol(SQLHSTMT stmt, SQLUSMALLINT columnNumber, SQLWCHAR* col } } - // Decimal Digits + // Column Decimal Digits if (decimalDigitsPtr) { switch (sqlType) { // All exact numeric types @@ -1440,16 +1443,14 @@ SQLRETURN SQLDescribeCol(SQLHSTMT stmt, SQLUSMALLINT columnNumber, SQLWCHAR* col break; } - // All datetime types + // All datetime types (ODBC2) case SQL_DATE: case SQL_TIME: case SQL_TIMESTAMP: - // TODO THESE ARE UNDEFINED - ODBC 3 Only Datetime Types - // SQL_TYPE_TIMESTAMP_WITH_TIMEZONE and SQL_TYPE_TIME_WITH_TIMEZONE are rarely - // used and not universally supported. - // case SQL_TYPE_TIME_WITH_TIMEZONE: - // case SQL_TYPE_TIMESTAMP_WITH_TIMEZONE: - // + // All datetime types (ODBC3) + case SQL_TYPE_DATE: + case SQL_TYPE_TIME: + case SQL_TYPE_TIMESTAMP: // All interval types with a seconds component case SQL_INTERVAL_SECOND: case SQL_INTERVAL_MINUTE_TO_SECOND: @@ -1470,9 +1471,11 @@ SQLRETURN SQLDescribeCol(SQLHSTMT stmt, SQLUSMALLINT columnNumber, SQLWCHAR* col } } - // Nullable - ird->GetField(columnNumber, SQL_DESC_NULLABLE, nullablePtr, sizeof(SQLSMALLINT), - nullptr); + // Column Nullable + if (nullablePtr) { + ird->GetField(columnNumber, SQL_DESC_NULLABLE, nullablePtr, sizeof(SQLSMALLINT), + nullptr); + } return SQL_SUCCESS; }); diff --git a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc index 36beafe8865..6b56eb63f49 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc @@ -2493,6 +2493,8 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColQueryAllDataTypesMetadataRemot SQLULEN columnSizes[] = {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 8, 8, 8, 8, 8, 65536, 19, 19, 8, 8, 8, 8, 1, 1, 65536, 65536, 65536, 65536, 10, 10, 23, 23}; + SQLULEN columnDecimalDigits[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 23, 23}; SQLRETURN ret = SQLExecDirect(this->stmt, &sql0[0], static_cast(sql0.size())); @@ -2519,7 +2521,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColQueryAllDataTypesMetadataRemot EXPECT_EQ(returned, columnNames[i]); EXPECT_EQ(columnDataType, columnDataTypes[i]); EXPECT_EQ(columnSize, columnSizes[i]); - EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(decimalDigits, columnDecimalDigits[i]); EXPECT_EQ(nullable, SQL_NULLABLE); nameLength = 0; @@ -2557,6 +2559,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadataRemote) { SQL_FLOAT, SQL_DOUBLE, SQL_BIT, SQL_TYPE_DATE, SQL_TYPE_TIME, SQL_TYPE_TIMESTAMP}; SQLULEN columnSizes[] = {4, 8, 19, 8, 8, 1, 10, 12, 23}; + SQLULEN columnDecimalDigits[] = {0, 0, 0, 0, 0, 0, 10, 12, 23}; SQLRETURN ret = SQLExecDirect(this->stmt, sqlQuery, queryLength); @@ -2582,7 +2585,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadataRemote) { EXPECT_EQ(returned, columnNames[i]); EXPECT_EQ(columnDataType, columnDataTypes[i]); EXPECT_EQ(columnSize, columnSizes[i]); - EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(decimalDigits, columnDecimalDigits[i]); EXPECT_EQ(nullable, SQL_NULLABLE); nameLength = 0; From bfa9edb65edaca4cda2079a7661e320d93446266 Mon Sep 17 00:00:00 2001 From: rscales Date: Wed, 6 Aug 2025 23:50:18 +0100 Subject: [PATCH 05/13] Update code comments based on draft review --- cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc index 6b56eb63f49..bde2e2a9aa4 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc @@ -2373,6 +2373,8 @@ TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColValidateInput) { } TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColQueryAllDataTypesMetadataMock) { + // Mock server has a limitation where only SQL_WVARCHAR column type values are returned + // from SELECT AS queries this->connect(); SQLWCHAR columnName[1024]; @@ -2663,7 +2665,6 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadataRemoteODB } TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColAllTypesTableMetadataMock) { - // Test assumes there is a table $scratch.ODBCTest in remote server this->connect(); this->CreateTableAllDataType(); @@ -2722,7 +2723,6 @@ TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColAllTypesTableMetadataMock) { } TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColUnicodeTableMetadataMock) { - // Test assumes there is a table $scratch.ODBCTest in remote server this->connect(); this->CreateUnicodeTable(); From f4e37bde84b35482abf2f5ae4bb080da52013948 Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 7 Aug 2025 03:47:56 +0100 Subject: [PATCH 06/13] Add tests to get metadata for SQLColumns and SQLTables using SQLDescribeCol --- .../flight/sql/odbc/tests/columns_test.cc | 51 ++++++++++++++++++ .../flight/sql/odbc/tests/tables_test.cc | 52 +++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc index bde2e2a9aa4..acfc6059ca6 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc @@ -2769,4 +2769,55 @@ TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColUnicodeTableMetadataMock) { this->disconnect(); } +TYPED_TEST(FlightSQLODBCTestBase, SQLColumnsGetMetadataBySQLDescribeCol) { + this->connect(); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"TABLE_CAT", (SQLWCHAR*)L"TABLE_SCHEM", + (SQLWCHAR*)L"TABLE_NAME", (SQLWCHAR*)L"COLUMN_NAME", + (SQLWCHAR*)L"DATA_TYPE"}; + SQLSMALLINT columnDataTypes[] = {SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_SMALLINT}; + SQLULEN columnSizes[] = {1024, 1024, 1024, 1024, 2}; + + SQLRETURN ret = SQLColumns(this->stmt, nullptr, 0, nullptr, 0, nullptr, 0, nullptr, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, columnSizes[i]); + EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} } // namespace arrow::flight::sql::odbc diff --git a/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc index 1dc0c90245e..2567a98d131 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc @@ -578,4 +578,56 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLTablesGetSupportedTableTypes) { this->disconnect(); } +TYPED_TEST(FlightSQLODBCTestBase, SQLTablesGetMetadataBySQLDescribeCol) { + this->connect(); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"TABLE_CAT", (SQLWCHAR*)L"TABLE_SCHEM", + (SQLWCHAR*)L"TABLE_NAME", (SQLWCHAR*)L"TABLE_TYPE", + (SQLWCHAR*)L"REMARKS"}; + SQLSMALLINT columnDataTypes[] = {SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_WVARCHAR}; + SQLULEN columnSizes[] = {1024, 1024, 1024, 1024, 1024}; + + SQLRETURN ret = SQLTables(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, nullptr, + SQL_NTS, nullptr, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, columnSizes[i]); + EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} } // namespace arrow::flight::sql::odbc From 52308e0e34322137ad0e7af7ff9ac3ee199bd835 Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 7 Aug 2025 18:36:18 +0100 Subject: [PATCH 07/13] Add ODBC2 tests for SQLColumns and SQLTables using SQLDescribeCol --- .../flight/sql/odbc/tests/columns_test.cc | 52 ++++++++++++++++++ .../flight/sql/odbc/tests/tables_test.cc | 53 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc index acfc6059ca6..4bd70e86919 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc @@ -2820,4 +2820,56 @@ TYPED_TEST(FlightSQLODBCTestBase, SQLColumnsGetMetadataBySQLDescribeCol) { this->disconnect(); } + +TYPED_TEST(FlightSQLODBCTestBase, SQLColumnsGetMetadataBySQLDescribeColODBC2) { + this->connect(SQL_OV_ODBC2); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"TABLE_QUALIFIER", (SQLWCHAR*)L"TABLE_OWNER", + (SQLWCHAR*)L"TABLE_NAME", (SQLWCHAR*)L"COLUMN_NAME", + (SQLWCHAR*)L"DATA_TYPE"}; + SQLSMALLINT columnDataTypes[] = {SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_SMALLINT}; + SQLULEN columnSizes[] = {1024, 1024, 1024, 1024, 2}; + + SQLRETURN ret = SQLColumns(this->stmt, nullptr, 0, nullptr, 0, nullptr, 0, nullptr, 0); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, columnSizes[i]); + EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} } // namespace arrow::flight::sql::odbc diff --git a/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc index 2567a98d131..bd7ea2ab3ad 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc @@ -630,4 +630,57 @@ TYPED_TEST(FlightSQLODBCTestBase, SQLTablesGetMetadataBySQLDescribeCol) { this->disconnect(); } + +TYPED_TEST(FlightSQLODBCTestBase, SQLTablesGetMetadataBySQLDescribeColODBC2) { + this->connect(SQL_OV_ODBC2); + + SQLWCHAR columnName[1024]; + constexpr SQLINTEGER bufCharLen = sizeof(columnName) / ODBC::GetSqlWCharSize(); + SQLSMALLINT nameLength = 0; + SQLSMALLINT columnDataType = 0; + SQLULEN columnSize = 0; + SQLSMALLINT decimalDigits = 0; + SQLSMALLINT nullable = 0; + size_t columnIndex = 0; + + SQLWCHAR* columnNames[] = {(SQLWCHAR*)L"TABLE_QUALIFIER", (SQLWCHAR*)L"TABLE_OWNER", + (SQLWCHAR*)L"TABLE_NAME", (SQLWCHAR*)L"TABLE_TYPE", + (SQLWCHAR*)L"REMARKS"}; + SQLSMALLINT columnDataTypes[] = {SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, SQL_WVARCHAR, + SQL_WVARCHAR}; + SQLULEN columnSizes[] = {1024, 1024, 1024, 1024, 1024}; + + SQLRETURN ret = SQLTables(this->stmt, nullptr, SQL_NTS, nullptr, SQL_NTS, nullptr, + SQL_NTS, nullptr, SQL_NTS); + + EXPECT_EQ(ret, SQL_SUCCESS); + + for (size_t i = 0; i < sizeof(columnNames) / sizeof(*columnNames); ++i) { + columnIndex = i + 1; + + ret = SQLDescribeCol(this->stmt, columnIndex, columnName, bufCharLen, &nameLength, + &columnDataType, &columnSize, &decimalDigits, &nullable); + + EXPECT_EQ(ret, SQL_SUCCESS); + + EXPECT_GT(nameLength, 0); + + // Returned nameLength is in bytes so convert to length in characters + size_t charCount = static_cast(nameLength) / ODBC::GetSqlWCharSize(); + std::wstring returned(columnName, columnName + charCount); + EXPECT_EQ(returned, columnNames[i]); + EXPECT_EQ(columnDataType, columnDataTypes[i]); + EXPECT_EQ(columnSize, columnSizes[i]); + EXPECT_EQ(decimalDigits, 0); + EXPECT_EQ(nullable, SQL_NULLABLE); + + nameLength = 0; + columnDataType = 0; + columnSize = 0; + decimalDigits = 0; + nullable = 0; + } + + this->disconnect(); +} } // namespace arrow::flight::sql::odbc From acdd8a20eac715a2a857fa5bf8eb0384b1ca16cb Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 7 Aug 2025 18:40:00 +0100 Subject: [PATCH 08/13] Empty commit to force running workflows From 98a67b375110a1515e18c2f157e57133dfe893d0 Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 7 Aug 2025 18:56:26 +0100 Subject: [PATCH 09/13] Empty commit to force running workflows after rebase From 0d74b78e33bca54a2af2b1fe624ecfb4955515fd Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 7 Aug 2025 21:49:40 +0100 Subject: [PATCH 10/13] Fix incorrect column type comparison value and remove unnecessary test name suffix --- .../arrow/flight/sql/odbc/tests/columns_test.cc | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc index 4bd70e86919..5ce46dccae7 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc @@ -2372,7 +2372,7 @@ TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColValidateInput) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColQueryAllDataTypesMetadataMock) { +TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColQueryAllDataTypesMetadata) { // Mock server has a limitation where only SQL_WVARCHAR column type values are returned // from SELECT AS queries this->connect(); @@ -2452,7 +2452,7 @@ TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColQueryAllDataTypesMetadataMock) { this->disconnect(); } -TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColQueryAllDataTypesMetadataRemote) { +TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColQueryAllDataTypesMetadata) { this->connect(); SQLWCHAR columnName[1024]; @@ -2536,7 +2536,9 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColQueryAllDataTypesMetadataRemot this->disconnect(); } -TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadataRemote) { +TEST_F( + FlightSQLODBCRemoteTestBase, + SQLDescribeColODBCTestTableMetadatafSQLDescribeColQueryAllDataTypesMetadata) { // Test assumes there is a table $scratch.ODBCTest in remote server this->connect(); @@ -2600,7 +2602,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadataRemote) { this->disconnect(); } -TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadataRemoteODBC2) { +TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadataODBC2) { // Test assumes there is a table $scratch.ODBCTest in remote server this->connect(SQL_OV_ODBC2); @@ -2623,7 +2625,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadataRemoteODB (SQLWCHAR*)L"timestamp_max"}; SQLSMALLINT columnDataTypes[] = {SQL_INTEGER, SQL_BIGINT, SQL_DECIMAL, SQL_FLOAT, SQL_DOUBLE, SQL_BIT, - SQL_DATETIME, SQL_TIME, SQL_TIMESTAMP}; + SQL_DATE, SQL_TIME, SQL_TIMESTAMP}; SQLULEN columnSizes[] = {4, 8, 19, 8, 8, 1, 10, 12, 23}; SQLULEN columnDecimalDigits[] = {0, 0, 0, 0, 0, 0, 10, 12, 23}; @@ -2664,7 +2666,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadataRemoteODB this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColAllTypesTableMetadataMock) { +TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColAllTypesTableMetadata) { this->connect(); this->CreateTableAllDataType(); @@ -2722,7 +2724,7 @@ TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColAllTypesTableMetadataMock) { this->disconnect(); } -TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColUnicodeTableMetadataMock) { +TEST_F(FlightSQLODBCMockTestBase, SQLDescribeColUnicodeTableMetadata) { this->connect(); this->CreateUnicodeTable(); From 67e39c760c7357e9ade69cdf38eabe7a24245ef0 Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 7 Aug 2025 21:55:52 +0100 Subject: [PATCH 11/13] Remove TODO comment as functionality implemented --- cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc | 3 --- 1 file changed, 3 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc index bd7ea2ab3ad..68405c51583 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/tables_test.cc @@ -28,9 +28,6 @@ namespace arrow::flight::sql::odbc { -// TODO: Add tests with SQLDescribeCol to check metadata of SQLColumns for ODBC 2 and -// ODBC 3. - // Helper Functions std::wstring GetStringColumnW(SQLHSTMT stmt, int colId) { From c1df189295339de7a6f0d504d2e26f2162fcf2cf Mon Sep 17 00:00:00 2001 From: rscales Date: Thu, 7 Aug 2025 22:56:06 +0100 Subject: [PATCH 12/13] Fix formatting issues with columns test file --- cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc index 5ce46dccae7..721d8d2a5f2 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc @@ -2536,9 +2536,8 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColQueryAllDataTypesMetadata) { this->disconnect(); } -TEST_F( - FlightSQLODBCRemoteTestBase, - SQLDescribeColODBCTestTableMetadatafSQLDescribeColQueryAllDataTypesMetadata) { +TEST_F(FlightSQLODBCRemoteTestBase, + SQLDescribeColODBCTestTableMetadatafSQLDescribeColQueryAllDataTypesMetadata) { // Test assumes there is a table $scratch.ODBCTest in remote server this->connect(); @@ -2623,9 +2622,9 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadataODBC2) { (SQLWCHAR*)L"double_max", (SQLWCHAR*)L"bit_true", (SQLWCHAR*)L"date_max", (SQLWCHAR*)L"time_max", (SQLWCHAR*)L"timestamp_max"}; - SQLSMALLINT columnDataTypes[] = {SQL_INTEGER, SQL_BIGINT, SQL_DECIMAL, - SQL_FLOAT, SQL_DOUBLE, SQL_BIT, - SQL_DATE, SQL_TIME, SQL_TIMESTAMP}; + SQLSMALLINT columnDataTypes[] = {SQL_INTEGER, SQL_BIGINT, SQL_DECIMAL, + SQL_FLOAT, SQL_DOUBLE, SQL_BIT, + SQL_DATE, SQL_TIME, SQL_TIMESTAMP}; SQLULEN columnSizes[] = {4, 8, 19, 8, 8, 1, 10, 12, 23}; SQLULEN columnDecimalDigits[] = {0, 0, 0, 0, 0, 0, 10, 12, 23}; From 714fef96c506de6fa748b6be217582231c916701 Mon Sep 17 00:00:00 2001 From: rscales Date: Fri, 8 Aug 2025 00:29:11 +0100 Subject: [PATCH 13/13] Fix garbled test case name --- cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc index 721d8d2a5f2..04ca7ec9b96 100644 --- a/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc +++ b/cpp/src/arrow/flight/sql/odbc/tests/columns_test.cc @@ -2536,8 +2536,7 @@ TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColQueryAllDataTypesMetadata) { this->disconnect(); } -TEST_F(FlightSQLODBCRemoteTestBase, - SQLDescribeColODBCTestTableMetadatafSQLDescribeColQueryAllDataTypesMetadata) { +TEST_F(FlightSQLODBCRemoteTestBase, SQLDescribeColODBCTestTableMetadata) { // Test assumes there is a table $scratch.ODBCTest in remote server this->connect();