From d1e4c3190a2384e344fcb53b57ee97934bef304d Mon Sep 17 00:00:00 2001 From: Yasuhiro Harada Date: Tue, 22 Jul 2025 13:18:05 +0900 Subject: [PATCH 1/8] Implement SQLSpecialColumns and SQLStatistics functions with query execution --- driver/api/odbc.cpp | 66 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/driver/api/odbc.cpp b/driver/api/odbc.cpp index f12aab820..9be6abfb8 100755 --- a/driver/api/odbc.cpp +++ b/driver/api/odbc.cpp @@ -1266,8 +1266,8 @@ SQLRETURN SQL_API EXPORTED_FUNCTION(SQLGetFunctions)(HDBC connection_handle, SQL SET_EXISTS(SQL_API_SQLSETENVATTR); //SET_EXISTS(SQL_API_SQLSETPOS); SET_EXISTS(SQL_API_SQLSETSTMTATTR); - //SET_EXISTS(SQL_API_SQLSPECIALCOLUMNS); - //SET_EXISTS(SQL_API_SQLSTATISTICS); + SET_EXISTS(SQL_API_SQLSPECIALCOLUMNS); + SET_EXISTS(SQL_API_SQLSTATISTICS); //SET_EXISTS(SQL_API_SQLTABLEPRIVILEGES); SET_EXISTS(SQL_API_SQLTABLES); @@ -1323,8 +1323,23 @@ SQLRETURN SQL_API EXPORTED_FUNCTION_MAYBE_W(SQLSpecialColumns)(HSTMT StatementHa SQLSMALLINT NameLength3, SQLUSMALLINT Scope, SQLUSMALLINT Nullable) { - LOG(__FUNCTION__); - return SQL_ERROR; + std::stringstream query; + query << "SELECT " + "cast(NULL, 'Nullable(Int16)') AS SCOPE, " + "cast(NULL, 'Nullable(String)') AS COLUMN_NAME, " + "cast(NULL, 'Nullable(Int16)') AS DATA_TYPE, " + "cast(NULL, 'Nullable(String)') AS TYPE_NAME, " + "cast(NULL, 'Nullable(Int32)') AS COLUMN_SIZE, " + "cast(NULL, 'Nullable(Int32)') AS BUFFER_LENGTH, " + "cast(NULL, 'Nullable(Int16)') AS DECIMAL_DIGITS, " + "cast(NULL, 'Nullable(Int16)') AS PSEUDO_COLUMN " + "WHERE (1 == 0)"; + auto func = [&](Statement & statement) { + LOG(__FUNCTION__); + statement.executeQuery(query.str()); + return SQL_SUCCESS; + }; + return CALL_WITH_TYPED_HANDLE(SQL_HANDLE_STMT, StatementHandle, func); } SQLRETURN SQL_API EXPORTED_FUNCTION_MAYBE_W(SQLStatistics)(HSTMT StatementHandle, @@ -1337,7 +1352,48 @@ SQLRETURN SQL_API EXPORTED_FUNCTION_MAYBE_W(SQLStatistics)(HSTMT StatementHandle SQLUSMALLINT Unique, SQLUSMALLINT Reserved) { LOG(__FUNCTION__); - return SQL_ERROR; + + auto func = [&](Statement & statement) { + const auto catalog = (CatalogName ? toUTF8(CatalogName, NameLength1) : statement.getParent().database); + const auto schema = (SchemaName ? toUTF8(SchemaName, NameLength2) : ""); + const auto table = (TableName ? toUTF8(TableName, NameLength3) : ""); + + // Build query to retrieve index/statistics information + // ClickHouse has indices which we can report as statistics + std::stringstream query; + query << "SELECT " + "cast(database, 'Nullable(String)') AS TABLE_CAT, " + "cast('', 'Nullable(String)') AS TABLE_SCHEM, " + "cast(table, 'String') AS TABLE_NAME, " + "cast(0, 'Int16') AS NON_UNIQUE, " + "cast(NULL, 'Nullable(String)') AS INDEX_QUALIFIER, " + "cast(name, 'Nullable(String)') AS INDEX_NAME, " + "cast(3, 'Int16') AS TYPE, " + "cast(1, 'Int16') AS ORDINAL_POSITION, " + "cast(NULL, 'Nullable(String)') AS COLUMN_NAME, " + "cast(NULL, 'Nullable(String)') AS ASC_OR_DESC, " + "cast(NULL, 'Nullable(Int32)') AS CARDINALITY, " + "cast(NULL, 'Nullable(Int32)') AS PAGES, " + "cast(NULL, 'Nullable(String)') AS FILTER_CONDITION " + "FROM system.data_skipping_indices " + "WHERE (1 == 1)"; + + // Add filters based on provided parameters + if (!catalog.empty() && catalog != "%") { + query << " AND database = '" << escapeForSQL(catalog) << "'"; + } + + if (!table.empty() && table != "%") { + query << " AND table = '" << escapeForSQL(table) << "'"; + } + + query << " ORDER BY NON_UNIQUE, TYPE, INDEX_NAME, ORDINAL_POSITION"; + statement.executeQuery(query.str()); + + return SQL_SUCCESS; + }; + + return CALL_WITH_TYPED_HANDLE(SQL_HANDLE_STMT, StatementHandle, func); } SQLRETURN SQL_API EXPORTED_FUNCTION_MAYBE_W(SQLColumnPrivileges)(HSTMT hstmt, From 74509eab6996ee281a2cbba747474c8fd7cef0f8 Mon Sep 17 00:00:00 2001 From: Yasuhiro Harada Date: Tue, 22 Jul 2025 13:47:21 +0900 Subject: [PATCH 2/8] Add filtering by database in SQLTables function --- driver/api/odbc.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/driver/api/odbc.cpp b/driver/api/odbc.cpp index 9be6abfb8..267f04435 100755 --- a/driver/api/odbc.cpp +++ b/driver/api/odbc.cpp @@ -948,6 +948,11 @@ SQLRETURN SQL_API EXPORTED_FUNCTION_MAYBE_W(SQLTables)( query << ")"; } } + + if (!catalog.empty() && catalog != "%") { + query << " AND database = '" << escapeForSQL(catalog) << "'"; + } + } query << " ORDER BY TABLE_TYPE, TABLE_CAT, TABLE_SCHEM, TABLE_NAME"; From 1753d5da3a2744a14215cf5a68332d2297e78815 Mon Sep 17 00:00:00 2001 From: Yasuhiro Harada Date: Wed, 23 Jul 2025 08:22:00 +0900 Subject: [PATCH 3/8] Fix test to verify SQLTablePrivileges() instead of SQLStatistics() for unimplemented API call --- driver/test/performance_it.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/driver/test/performance_it.cpp b/driver/test/performance_it.cpp index 488251c97..a813e71e7 100755 --- a/driver/test/performance_it.cpp +++ b/driver/test/performance_it.cpp @@ -60,12 +60,12 @@ TEST_F(PerformanceTest, ENABLE_FOR_OPTIMIZED_BUILDS_ONLY(UnimplementedAPICallOve constexpr std::size_t call_count = 1'000'000; auto tstr = fromUTF8(""); - // Verify that SQLStatistics() is not implemented. Change to something else when implemented. + // Verify that SQLTablePrivileges() is not implemented. Change to something else when implemented. { - const auto rc = SQLStatistics(hstmt, ptcharCast(tstr.data()), 0, ptcharCast(tstr.data()), 0, ptcharCast(tstr.data()), 0, SQL_INDEX_ALL, SQL_ENSURE); + const auto rc = SQLTablePrivileges(hstmt, ptcharCast(tstr.data()), 0, ptcharCast(tstr.data()), 0, ptcharCast(tstr.data()), 0); if (rc != SQL_ERROR) { throw std::runtime_error( - "SQLStatistics return code: " + std::to_string(rc) + + "SQLTablePrivileges return code: " + std::to_string(rc) + ", expected SQL_ERROR (" + std::to_string(SQL_ERROR) + ") - a function that is not implemented by the driver" ); @@ -81,7 +81,7 @@ TEST_F(PerformanceTest, ENABLE_FOR_OPTIMIZED_BUILDS_ONLY(UnimplementedAPICallOve START_MEASURING_TIME(); for (std::size_t i = 0; i < call_count; ++i) { - SQLStatistics(hstmt, ptcharCast(tstr.data()), 0, ptcharCast(tstr.data()), 0, ptcharCast(tstr.data()), 0, SQL_INDEX_ALL, SQL_ENSURE); + SQLTablePrivileges(hstmt, ptcharCast(tstr.data()), 0, ptcharCast(tstr.data()), 0, ptcharCast(tstr.data()), 0); } STOP_MEASURING_TIME_AND_REPORT(call_count); From 6ee4efbd04bb9a5053636688a1216964c2e416d1 Mon Sep 17 00:00:00 2001 From: Yasuhiro Harada Date: Sat, 26 Jul 2025 00:19:39 +0900 Subject: [PATCH 4/8] Add integration tests for SQLSpecialColumns and SQLStatistics functions --- driver/test/CMakeLists.txt | 1 + driver/test/catalog_functions_it.cpp | 415 +++++++++++++++++++++++++++ 2 files changed, 416 insertions(+) create mode 100644 driver/test/catalog_functions_it.cpp diff --git a/driver/test/CMakeLists.txt b/driver/test/CMakeLists.txt index 061e1ea1c..daca0e66b 100644 --- a/driver/test/CMakeLists.txt +++ b/driver/test/CMakeLists.txt @@ -87,6 +87,7 @@ function (declare_odbc_test_targets libname UNICODE) performance_it.cpp type_info_it.cpp authentication_it.cpp + catalog_functions_it.cpp ) if (CH_ODBC_ENABLE_CODE_COVERAGE) diff --git a/driver/test/catalog_functions_it.cpp b/driver/test/catalog_functions_it.cpp new file mode 100644 index 000000000..a658eb5e3 --- /dev/null +++ b/driver/test/catalog_functions_it.cpp @@ -0,0 +1,415 @@ +#include +#include +#include + +#include "driver/test/client_test_base.h" +#include "driver/test/client_utils.h" +#include "driver/test/result_set_reader.hpp" +#include "driver/utils/sql_encoding.h" + +// Integration tests for ODBC catalog functions SQLSpecialColumns and SQLStatistics +class CatalogFunctionsTest + : public ClientTestBase +{ +}; + +// Test SQLSpecialColumns function +TEST_F(CatalogFunctionsTest, SQLSpecialColumns) +{ + SQLRETURN rc = SQL_SUCCESS; + + // Test basic call to SQLSpecialColumns + auto catalog = fromUTF8("default"); + auto schema = fromUTF8(""); + auto table = fromUTF8("system.tables"); + + rc = SQLSpecialColumns(hstmt, + SQL_BEST_ROWID, // IdentifierType + ptcharCast(catalog.data()), SQL_NTS, // CatalogName + ptcharCast(schema.data()), SQL_NTS, // SchemaName + ptcharCast(table.data()), SQL_NTS, // TableName + SQL_SCOPE_CURROW, // Scope + SQL_NULLABLE // Nullable + ); + + EXPECT_EQ(rc, SQL_SUCCESS); + + // Verify the result set structure has the expected columns + SQLSMALLINT column_count = 0; + ODBC_CALL_ON_STMT_THROW(hstmt, SQLNumResultCols(hstmt, &column_count)); + EXPECT_EQ(column_count, 8); + + // Check column names match ODBC specification for SQLSpecialColumns + std::vector expected_columns = { + "SCOPE", "COLUMN_NAME", "DATA_TYPE", "TYPE_NAME", + "COLUMN_SIZE", "BUFFER_LENGTH", "DECIMAL_DIGITS", "PSEUDO_COLUMN" + }; + + for (SQLSMALLINT i = 1; i <= column_count; ++i) { + PTChar column_name[256] = {}; + SQLSMALLINT name_length = 0; + SQLSMALLINT data_type = 0; + SQLULEN column_size = 0; + SQLSMALLINT decimal_digits = 0; + SQLSMALLINT nullable = 0; + + ODBC_CALL_ON_STMT_THROW(hstmt, SQLDescribeCol(hstmt, i, + reinterpret_cast(column_name), sizeof(column_name) / sizeof(PTChar), &name_length, + &data_type, &column_size, &decimal_digits, &nullable)); + + auto actual_name = toUTF8(column_name); + EXPECT_EQ(actual_name, expected_columns[i - 1]); + } + + // The result should be empty since implementation returns WHERE 1 == 0 + rc = SQLFetch(hstmt); + EXPECT_EQ(rc, SQL_NO_DATA); + + ODBC_CALL_ON_STMT_THROW(hstmt, SQLFreeStmt(hstmt, SQL_CLOSE)); +} + +// Test SQLStatistics function +TEST_F(CatalogFunctionsTest, SQLStatistics) +{ + SQLRETURN rc = SQL_SUCCESS; + + // Test basic call to SQLStatistics with system tables + // Using system database since it's guaranteed to exist + auto catalog = fromUTF8("system"); + auto schema = fromUTF8(""); + auto table = fromUTF8("tables"); + + rc = SQLStatistics(hstmt, + ptcharCast(catalog.data()), SQL_NTS, // CatalogName + ptcharCast(schema.data()), SQL_NTS, // SchemaName + ptcharCast(table.data()), SQL_NTS, // TableName + SQL_INDEX_ALL, // Unique + SQL_ENSURE // Reserved + ); + + EXPECT_EQ(rc, SQL_SUCCESS); + + // Verify the result set structure has the expected columns + SQLSMALLINT column_count = 0; + ODBC_CALL_ON_STMT_THROW(hstmt, SQLNumResultCols(hstmt, &column_count)); + EXPECT_EQ(column_count, 13); + + // Check column names + std::vector expected_columns = { + "TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "NON_UNIQUE", + "INDEX_QUALIFIER", "INDEX_NAME", "TYPE", "ORDINAL_POSITION", + "COLUMN_NAME", "ASC_OR_DESC", "CARDINALITY", "PAGES", "FILTER_CONDITION" + }; + + for (SQLSMALLINT i = 1; i <= column_count; ++i) { + PTChar column_name[256] = {}; + SQLSMALLINT name_length = 0; + SQLSMALLINT data_type = 0; + SQLULEN column_size = 0; + SQLSMALLINT decimal_digits = 0; + SQLSMALLINT nullable = 0; + + ODBC_CALL_ON_STMT_THROW(hstmt, SQLDescribeCol(hstmt, i, + reinterpret_cast(column_name), sizeof(column_name) / sizeof(PTChar), &name_length, + &data_type, &column_size, &decimal_digits, &nullable)); + + auto actual_name = toUTF8(column_name); + EXPECT_EQ(actual_name, expected_columns[i - 1]); + } + + // Check if we have any data - this depends on system.data_skipping_indices content + // The result may be empty or contain data, both are valid + ResultSetReader reader{hstmt}; + int row_count = 0; + while (reader.fetch()) { + row_count++; + // Verify basic structure of returned data if any exists + auto table_cat = reader.getData("TABLE_CAT"); + auto table_name = reader.getData("TABLE_NAME"); + auto non_unique = reader.getData("NON_UNIQUE"); + auto type = reader.getData("TYPE"); + + // These fields should always have values when a row is returned + EXPECT_TRUE(table_cat.has_value()); + EXPECT_TRUE(table_name.has_value()); + EXPECT_TRUE(non_unique.has_value()); + EXPECT_TRUE(type.has_value()); + + } + + ODBC_CALL_ON_STMT_THROW(hstmt, SQLFreeStmt(hstmt, SQL_CLOSE)); +} + +// Test SQLStatistics with filtering parameters +TEST_F(CatalogFunctionsTest, SQLStatisticsWithFiltering) +{ + SQLRETURN rc = SQL_SUCCESS; + + // Ensure statement is in a completely clean state + SQLFreeStmt(hstmt, SQL_CLOSE); + SQLFreeStmt(hstmt, SQL_UNBIND); + SQLFreeStmt(hstmt, SQL_RESET_PARAMS); + + // Test with specific catalog filtering + auto catalog = fromUTF8("system"); + auto schema = fromUTF8(""); + auto table = fromUTF8("%"); // Use wildcard to get all tables + + rc = SQLStatistics(hstmt, + ptcharCast(catalog.data()), SQL_NTS, // CatalogName + ptcharCast(schema.data()), SQL_NTS, // SchemaName + ptcharCast(table.data()), SQL_NTS, // TableName + SQL_INDEX_UNIQUE, // Unique + SQL_ENSURE // Reserved + ); + + EXPECT_EQ(rc, SQL_SUCCESS); + + // Verify result set structure + SQLSMALLINT column_count = 0; + ODBC_CALL_ON_STMT_THROW(hstmt, SQLNumResultCols(hstmt, &column_count)); + EXPECT_EQ(column_count, 13); + + // Verify that any returned data has the correct catalog + ResultSetReader reader{hstmt}; + while (reader.fetch()) { + auto table_cat = reader.getData("TABLE_CAT"); + EXPECT_TRUE(table_cat.has_value()); + if (table_cat.has_value()) { + EXPECT_EQ(table_cat.value(), "system"); + } + } + + ODBC_CALL_ON_STMT_THROW(hstmt, SQLFreeStmt(hstmt, SQL_CLOSE)); +} + +// Test SQLStatistics with NULL parameters +TEST_F(CatalogFunctionsTest, SQLStatisticsWithNullParameters) +{ + SQLRETURN rc = SQL_SUCCESS; + + // Create a fresh statement handle to avoid sequence errors + SQLHSTMT temp_hstmt = SQL_NULL_HSTMT; + rc = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &temp_hstmt); + ASSERT_EQ(rc, SQL_SUCCESS) << "Failed to allocate new statement handle"; + + // Test with NULL catalog and schema + rc = SQLStatistics(temp_hstmt, + nullptr, 0, // CatalogName (NULL) + nullptr, 0, // SchemaName (NULL) + nullptr, 0, // TableName (NULL) + SQL_INDEX_ALL, // Unique + SQL_ENSURE // Reserved + ); + + if (rc != SQL_SUCCESS) { + // Get detailed error information + SQLTCHAR sqlstate[6]; + SQLINTEGER native_error; + SQLTCHAR error_msg[256]; + SQLSMALLINT msg_len; + + SQLGetDiagRec(SQL_HANDLE_STMT, temp_hstmt, 1, sqlstate, &native_error, + error_msg, sizeof(error_msg) / sizeof(SQLTCHAR), &msg_len); + + auto sqlstate_str = toUTF8(sqlstate); + auto error_msg_str = toUTF8(error_msg); + + std::cout << "SQLStatistics failed with SQLSTATE: " << sqlstate_str + << ", Native Error: " << native_error + << ", Message: " << error_msg_str << std::endl; + + // HY009 (Invalid use of null pointer) is expected when passing NULL parameters + // This verifies that the driver properly validates input parameters + EXPECT_TRUE(rc == SQL_SUCCESS || rc == SQL_ERROR) + << "SQLStatistics returned unexpected code: " << rc; + + // On some systems, the specific error might be different, but should still be an error + if (rc == SQL_ERROR && !sqlstate_str.empty()) { + // Common SQLSTATE codes for null pointer errors: HY009, HY001, 07009 + EXPECT_TRUE(sqlstate_str == "HY009" || sqlstate_str == "HY001" || sqlstate_str == "07009") + << "Unexpected SQLSTATE for NULL parameter test: " << sqlstate_str; + } + } else { + EXPECT_EQ(rc, SQL_SUCCESS); // Should still return proper result set structure + SQLSMALLINT column_count = 0; + rc = SQLNumResultCols(temp_hstmt, &column_count); + EXPECT_EQ(rc, SQL_SUCCESS); + EXPECT_EQ(column_count, 13); + } + + // Clean up the temporary statement handle + if (temp_hstmt != SQL_NULL_HSTMT) { + SQLFreeStmt(temp_hstmt, SQL_CLOSE); + SQLFreeHandle(SQL_HANDLE_STMT, temp_hstmt); + } +} + +// Test SQLSpecialColumns with different parameters +TEST_F(CatalogFunctionsTest, SQLSpecialColumnsWithDifferentParameters) +{ + SQLRETURN rc = SQL_SUCCESS; + + // Test with SQL_ROWVER instead of SQL_BEST_ROWID + auto catalog = fromUTF8("default"); + auto schema = fromUTF8(""); + auto table = fromUTF8("system.tables"); + + rc = SQLSpecialColumns(hstmt, + SQL_ROWVER, // IdentifierType + ptcharCast(catalog.data()), SQL_NTS, // CatalogName + ptcharCast(schema.data()), SQL_NTS, // SchemaName + ptcharCast(table.data()), SQL_NTS, // TableName + SQL_SCOPE_TRANSACTION, // Scope + SQL_NO_NULLS // Nullable + ); + + EXPECT_EQ(rc, SQL_SUCCESS); + + // Verify the result set structure + SQLSMALLINT column_count = 0; + ODBC_CALL_ON_STMT_THROW(hstmt, SQLNumResultCols(hstmt, &column_count)); + EXPECT_EQ(column_count, 8); + + // The result should be empty (WHERE 1 == 0) + rc = SQLFetch(hstmt); + EXPECT_EQ(rc, SQL_NO_DATA); + + ODBC_CALL_ON_STMT_THROW(hstmt, SQLFreeStmt(hstmt, SQL_CLOSE)); +} + +// Test that SQLGetFunctions reports these functions as supported +TEST_F(CatalogFunctionsTest, SQLGetFunctionsSupport) +{ + SQLRETURN rc = SQL_SUCCESS; + SQLUSMALLINT supported = SQL_FALSE; + + // Test SQLSpecialColumns support + rc = SQLGetFunctions(hdbc, SQL_API_SQLSPECIALCOLUMNS, &supported); + EXPECT_EQ(rc, SQL_SUCCESS); + EXPECT_EQ(supported, SQL_TRUE); + + // Test SQLStatistics support + rc = SQLGetFunctions(hdbc, SQL_API_SQLSTATISTICS, &supported); + EXPECT_EQ(rc, SQL_SUCCESS); + EXPECT_EQ(supported, SQL_TRUE); +} + +// Test SQLStatistics with actual data skipping index +TEST_F(CatalogFunctionsTest, SQLStatisticsWithDataSkippingIndex) +{ + SQLRETURN rc = SQL_SUCCESS; + + // Create a test table with a data skipping index + auto create_table_query = fromUTF8( + "CREATE TABLE IF NOT EXISTS test_index_table (" + "id UInt32, " + "name String, " + "value Float64" + ") ENGINE = MergeTree() " + "ORDER BY id" + ); + + try { + ODBC_CALL_ON_STMT_THROW(hstmt, SQLExecDirect(hstmt, ptcharCast(create_table_query.data()), SQL_NTS)); + ODBC_CALL_ON_STMT_THROW(hstmt, SQLFreeStmt(hstmt, SQL_CLOSE)); + + // Try to create a data skipping index - might fail due to permissions + auto create_index_query = fromUTF8( + "ALTER TABLE test_index_table ADD INDEX idx_name name TYPE bloom_filter() GRANULARITY 1" + ); + + try { + ODBC_CALL_ON_STMT_THROW(hstmt, SQLExecDirect(hstmt, ptcharCast(create_index_query.data()), SQL_NTS)); + ODBC_CALL_ON_STMT_THROW(hstmt, SQLFreeStmt(hstmt, SQL_CLOSE)); + // Index creation succeeded + } catch (const std::exception& e) { + // If index creation fails due to permissions, skip this part + std::cout << "Index creation failed (permissions): skipping index test" << std::endl; + ODBC_CALL_ON_STMT_THROW(hstmt, SQLFreeStmt(hstmt, SQL_CLOSE)); + } + + // Test SQLStatistics on this table regardless of index creation success + auto catalog = fromUTF8("default"); + auto schema = fromUTF8(""); + auto table = fromUTF8("test_index_table"); + + rc = SQLStatistics(hstmt, + ptcharCast(catalog.data()), SQL_NTS, // CatalogName + ptcharCast(schema.data()), SQL_NTS, // SchemaName + ptcharCast(table.data()), SQL_NTS, // TableName + SQL_INDEX_ALL, // Unique + SQL_ENSURE // Reserved + ); + + EXPECT_EQ(rc, SQL_SUCCESS); + + // Verify the result set structure + SQLSMALLINT column_count = 0; + ODBC_CALL_ON_STMT_THROW(hstmt, SQLNumResultCols(hstmt, &column_count)); + EXPECT_EQ(column_count, 13); + + // Check for any indices (may or may not find the one we tried to create) + ResultSetReader reader{hstmt}; + int index_count = 0; + while (reader.fetch()) { + auto table_name = reader.getData("TABLE_NAME"); + auto index_name = reader.getData("INDEX_NAME"); + + if (table_name.has_value() && table_name.value() == "test_index_table") { + index_count++; + if (index_name.has_value()) { + } + } + } + ODBC_CALL_ON_STMT_THROW(hstmt, SQLFreeStmt(hstmt, SQL_CLOSE)); + + // Clean up - always try to drop the table + auto drop_table_query = fromUTF8("DROP TABLE IF EXISTS test_index_table"); + ODBC_CALL_ON_STMT_THROW(hstmt, SQLExecDirect(hstmt, ptcharCast(drop_table_query.data()), SQL_NTS)); + ODBC_CALL_ON_STMT_THROW(hstmt, SQLFreeStmt(hstmt, SQL_CLOSE)); + + } catch (const std::exception& e) { + // If table creation fails, skip the entire test + std::cout << "Table creation failed, skipping test" << std::endl; + ODBC_CALL_ON_STMT_THROW(hstmt, SQLFreeStmt(hstmt, SQL_CLOSE)); + + // Still clean up just in case + try { + auto drop_table_query = fromUTF8("DROP TABLE IF EXISTS test_index_table"); + ODBC_CALL_ON_STMT_THROW(hstmt, SQLExecDirect(hstmt, ptcharCast(drop_table_query.data()), SQL_NTS)); + ODBC_CALL_ON_STMT_THROW(hstmt, SQLFreeStmt(hstmt, SQL_CLOSE)); + } catch (...) { + // Ignore cleanup errors + } + } +} + +// Test error handling with invalid statement handle +TEST_F(CatalogFunctionsTest, InvalidStatementHandleError) +{ + SQLRETURN rc = SQL_SUCCESS; + + // Test SQLSpecialColumns with invalid handle (should return SQL_INVALID_HANDLE) + rc = SQLSpecialColumns(nullptr, + SQL_BEST_ROWID, + nullptr, 0, + nullptr, 0, + nullptr, 0, + SQL_SCOPE_CURROW, + SQL_NULLABLE + ); + + EXPECT_EQ(rc, SQL_INVALID_HANDLE); + + // Test SQLStatistics with invalid handle (should return SQL_INVALID_HANDLE) + rc = SQLStatistics(nullptr, + nullptr, 0, + nullptr, 0, + nullptr, 0, + SQL_INDEX_ALL, + SQL_ENSURE + ); + + EXPECT_EQ(rc, SQL_INVALID_HANDLE); +} From 9a854b5b1aa8f5f65e9216a6770ef3c601ab8a33 Mon Sep 17 00:00:00 2001 From: Yasuhiro Harada <46883658+YasuhiroHarada@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:35:16 +0900 Subject: [PATCH 5/8] Update driver/api/odbc.cpp Co-authored-by: Andrew Slabko --- driver/api/odbc.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/driver/api/odbc.cpp b/driver/api/odbc.cpp index 267f04435..df1289415 100755 --- a/driver/api/odbc.cpp +++ b/driver/api/odbc.cpp @@ -1331,9 +1331,9 @@ SQLRETURN SQL_API EXPORTED_FUNCTION_MAYBE_W(SQLSpecialColumns)(HSTMT StatementHa std::stringstream query; query << "SELECT " "cast(NULL, 'Nullable(Int16)') AS SCOPE, " - "cast(NULL, 'Nullable(String)') AS COLUMN_NAME, " - "cast(NULL, 'Nullable(Int16)') AS DATA_TYPE, " - "cast(NULL, 'Nullable(String)') AS TYPE_NAME, " + "cast('', 'String') AS COLUMN_NAME, " + "cast(0, 'Int16') AS DATA_TYPE, " + "cast('', 'String') AS TYPE_NAME, " "cast(NULL, 'Nullable(Int32)') AS COLUMN_SIZE, " "cast(NULL, 'Nullable(Int32)') AS BUFFER_LENGTH, " "cast(NULL, 'Nullable(Int16)') AS DECIMAL_DIGITS, " From 33d2fc00d12e7e59a456b67d058366dbec67e57b Mon Sep 17 00:00:00 2001 From: Yasuhiro Harada <46883658+YasuhiroHarada@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:35:24 +0900 Subject: [PATCH 6/8] Update driver/api/odbc.cpp Co-authored-by: Andrew Slabko --- driver/api/odbc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver/api/odbc.cpp b/driver/api/odbc.cpp index df1289415..9ee45c281 100755 --- a/driver/api/odbc.cpp +++ b/driver/api/odbc.cpp @@ -1373,7 +1373,7 @@ SQLRETURN SQL_API EXPORTED_FUNCTION_MAYBE_W(SQLStatistics)(HSTMT StatementHandle "cast(0, 'Int16') AS NON_UNIQUE, " "cast(NULL, 'Nullable(String)') AS INDEX_QUALIFIER, " "cast(name, 'Nullable(String)') AS INDEX_NAME, " - "cast(3, 'Int16') AS TYPE, " + "cast(" + toSqlQueryValue(SQL_INDEX_OTHER) + ", 'Int16') AS TYPE, " "cast(1, 'Int16') AS ORDINAL_POSITION, " "cast(NULL, 'Nullable(String)') AS COLUMN_NAME, " "cast(NULL, 'Nullable(String)') AS ASC_OR_DESC, " From 72a24673ed2f127d339241ada0a9771aafdec325 Mon Sep 17 00:00:00 2001 From: Yasuhiro Harada <46883658+YasuhiroHarada@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:35:57 +0900 Subject: [PATCH 7/8] Update driver/api/odbc.cpp Co-authored-by: Andrew Slabko --- driver/api/odbc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver/api/odbc.cpp b/driver/api/odbc.cpp index 9ee45c281..1fdcd5e21 100755 --- a/driver/api/odbc.cpp +++ b/driver/api/odbc.cpp @@ -1370,7 +1370,7 @@ SQLRETURN SQL_API EXPORTED_FUNCTION_MAYBE_W(SQLStatistics)(HSTMT StatementHandle "cast(database, 'Nullable(String)') AS TABLE_CAT, " "cast('', 'Nullable(String)') AS TABLE_SCHEM, " "cast(table, 'String') AS TABLE_NAME, " - "cast(0, 'Int16') AS NON_UNIQUE, " + "cast(" + toSqlQueryValue(SQL_FALSE) + ", 'Int16') AS NON_UNIQUE, " "cast(NULL, 'Nullable(String)') AS INDEX_QUALIFIER, " "cast(name, 'Nullable(String)') AS INDEX_NAME, " "cast(" + toSqlQueryValue(SQL_INDEX_OTHER) + ", 'Int16') AS TYPE, " From 903405703ce2b623eb21ad09b18fd2636e096a26 Mon Sep 17 00:00:00 2001 From: Yasuhiro Harada <46883658+YasuhiroHarada@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:38:11 +0900 Subject: [PATCH 8/8] Update driver/test/catalog_functions_it.cpp Co-authored-by: Andrew Slabko --- driver/test/catalog_functions_it.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver/test/catalog_functions_it.cpp b/driver/test/catalog_functions_it.cpp index a658eb5e3..72e19596c 100644 --- a/driver/test/catalog_functions_it.cpp +++ b/driver/test/catalog_functions_it.cpp @@ -13,7 +13,7 @@ class CatalogFunctionsTest { }; -// Test SQLSpecialColumns function +// Test that `SQLSpecialColumns` returns a dataset in the expected format without any data. TEST_F(CatalogFunctionsTest, SQLSpecialColumns) { SQLRETURN rc = SQL_SUCCESS;