From f4652e863ff0377df216f682e9d2411c8a1648a1 Mon Sep 17 00:00:00 2001 From: Kanchan Shukla Date: Sun, 13 Sep 2026 19:53:52 +0000 Subject: [PATCH 1/4] impl(bq_driver): SQLStatistics Implementation --- google/cloud/odbc/bq_driver.cmake | 3 + .../odbc/bq_driver/internal/odbc_desc_attr.cc | 43 ++++- .../internal/odbc_internal_commons.h | 18 ++ .../bq_driver/internal/odbc_sql_statistics.cc | 176 ++++++++++++++++++ .../bq_driver/internal/odbc_sql_statistics.h | 51 +++++ .../internal/odbc_sql_statistics_test.cc | 71 +++++++ google/cloud/odbc/bq_driver/odbc_api.cc | 12 +- .../odbc/bq_driver/odbc_driver_metadata.cc | 127 +++++++++++++ .../odbc/bq_driver/odbc_driver_metadata.h | 14 ++ .../google_cloud_odbc_bq_driver_internal.bzl | 2 + .../odbc_driver_tests/catalog_test.cc | 13 +- 11 files changed, 518 insertions(+), 12 deletions(-) create mode 100644 google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.cc create mode 100644 google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.h create mode 100644 google/cloud/odbc/bq_driver/internal/odbc_sql_statistics_test.cc diff --git a/google/cloud/odbc/bq_driver.cmake b/google/cloud/odbc/bq_driver.cmake index a8306afa15..46073f0477 100644 --- a/google/cloud/odbc/bq_driver.cmake +++ b/google/cloud/odbc/bq_driver.cmake @@ -63,6 +63,8 @@ set(COMMON_SOURCES bq_driver/internal/odbc_sql_primary_keys.h bq_driver/internal/odbc_sql_special_columns.cc bq_driver/internal/odbc_sql_special_columns.h + bq_driver/internal/odbc_sql_statistics.cc + bq_driver/internal/odbc_sql_statistics.h bq_driver/internal/odbc_sql_tables.cc bq_driver/internal/odbc_sql_tables.h bq_driver/internal/odbc_sql_type_info.cc @@ -282,6 +284,7 @@ function (bq_driver_define_unit_tests) bq_driver/internal/odbc_sql_info_test.cc bq_driver/internal/odbc_sql_primary_keys_test.cc bq_driver/internal/odbc_sql_special_columns_test.cc + bq_driver/internal/odbc_sql_statistics_test.cc bq_driver/internal/odbc_sql_tables_test.cc bq_driver/internal/odbc_sql_type_info_test.cc bq_driver/internal/odbc_stmt_attr_test.cc diff --git a/google/cloud/odbc/bq_driver/internal/odbc_desc_attr.cc b/google/cloud/odbc/bq_driver/internal/odbc_desc_attr.cc index c9701326b2..38209c531a 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_desc_attr.cc +++ b/google/cloud/odbc/bq_driver/internal/odbc_desc_attr.cc @@ -556,7 +556,11 @@ void DescriptorRecord::ApplyMetadataIrdOverrides(std::string const& col_name) { col_name == "TABLE_CAT" || col_name == "COLUMN_NAME" || col_name == "PKCOLUMN_NAME" || col_name == "PKTABLE_CAT" || col_name == "FKTABLE_CAT" || col_name == "FKCOLUMN_NAME" || - col_name == "FK_NAME" || col_name == "PK_NAME" || col_name == "TYPE_NAME"; + col_name == "FK_NAME" || col_name == "PK_NAME" || + col_name == "TYPE_NAME" || col_name == "INDEX_NAME" || + col_name == "FILTER_CONDITION"; + + bool const is_medium_wvarchar = col_name == "INDEX_QUALIFIER"; bool const is_long_wvarchar = col_name == "TABLE_SCHEM" || col_name == "TABLE_NAME" || @@ -568,18 +572,29 @@ void DescriptorRecord::ApplyMetadataIrdOverrides(std::string const& col_name) { col_name == "NULLABLE" || col_name == "SQL_DATA_TYPE" || col_name == "SQL_DATETIME_SUB" || col_name == "KEY_SEQ" || col_name == "UPDATE_RULE" || col_name == "DELETE_RULE" || - col_name == "DEFERRABILITY" || col_name == "SCOPE" || + col_name == "DEFERRABILITY" || col_name == "NON_UNIQUE" || + col_name == "TYPE" || col_name == "SCOPE" || col_name == "PSEUDO_COLUMN"; bool const is_integer = - col_name == "COLUMN_SIZE" || col_name == "BUFFER_LENGTH"; + col_name == "COLUMN_SIZE" || col_name == "BUFFER_LENGTH" || + col_name == "ORDINAL_POSITION" || col_name == "CARDINALITY" || + col_name == "PAGES"; + + bool const is_wchar_one = col_name == "ASC_OR_DESC"; - if (is_short_wvarchar || is_long_wvarchar) { + if (is_short_wvarchar || is_medium_wvarchar || is_long_wvarchar) { type_name = "WVARCHAR"; local_type_name = "WVARCHAR"; SetConciseType(SQL_WVARCHAR, DescriptorType::kIRD); - length = is_short_wvarchar ? 128 : 1024; + if (is_short_wvarchar) + length = 128; + else if (is_medium_wvarchar) + length = 255; + else + length = 1024; + precision = static_cast(length); case_sensitive = 0; searchable = 0; @@ -609,13 +624,29 @@ void DescriptorRecord::ApplyMetadataIrdOverrides(std::string const& col_name) { SetDisplaySize(SQL_INTEGER, 10, 10); SetOctetLength(SQL_INTEGER, 10, 10); + } else if (is_wchar_one) { + type_name = "WCHAR"; + local_type_name = "WCHAR"; + SetConciseType(SQL_WCHAR, DescriptorType::kIRD); + + searchable = 0; + length = 1; + precision = 1; + scale = 0; + + SetDisplaySize(SQL_WCHAR, 1, 1); + SetOctetLength(SQL_WCHAR, 1, 1); } + // Existing Driver returns TABLE_NAME as SQL_NULLABLE for SQLStatistics but + // SQL_NO_NULLS for others. We'll keep it as SQL_NO_NULLS because that + // conforms to the ODBC standard. if (col_name == "TABLE_NAME" || col_name == "COLUMN_NAME" || col_name == "PKTABLE_NAME" || col_name == "PKCOLUMN_NAME" || col_name == "FKTABLE_NAME" || col_name == "FKCOLUMN_NAME" || col_name == "KEY_SEQ" || col_name == "DATA_TYPE" || - col_name == "TYPE_NAME") { + col_name == "TYPE_NAME" || col_name == "TYPE" || + col_name == "ORDINAL_POSITION") { nullable = SQL_NO_NULLS; } } diff --git a/google/cloud/odbc/bq_driver/internal/odbc_internal_commons.h b/google/cloud/odbc/bq_driver/internal/odbc_internal_commons.h index 6cc3e4a6e6..53160d8f28 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_internal_commons.h +++ b/google/cloud/odbc/bq_driver/internal/odbc_internal_commons.h @@ -126,6 +126,15 @@ inline constexpr char const* kRemarksColName = "REMARKS"; inline constexpr char const* kColumnNameColName = "COLUMN_NAME"; inline constexpr char const* kKeySeqColName = "KEY_SEQ"; inline constexpr char const* kPkNameColName = "PK_NAME"; +inline constexpr char const* kNonUniqueColName = "NON_UNIQUE"; +inline constexpr char const* kIndexQualifierColName = "INDEX_QUALIFIER"; +inline constexpr char const* kIndexNameColName = "INDEX_NAME"; +inline constexpr char const* kTypeColName = "TYPE"; +inline constexpr char const* kOrdinalPositionColName = "ORDINAL_POSITION"; +inline constexpr char const* kAscOrDescColName = "ASC_OR_DESC"; +inline constexpr char const* kCardinalityColName = "CARDINALITY"; +inline constexpr char const* kPagesColName = "PAGES"; +inline constexpr char const* kFilterConditionColName = "FILTER_CONDITION"; inline constexpr ColumnSchema kTableCatSchema{0, BQDataType::kString}; inline constexpr ColumnSchema kTableSchemaSchema{0, BQDataType::kString}; @@ -134,6 +143,15 @@ inline constexpr ColumnSchema kRemarksSchema{0, BQDataType::kString}; inline constexpr ColumnSchema kColumnNameSchema{0, BQDataType::kString}; inline constexpr ColumnSchema kKeySeqSchema{0, BQDataType::kInt64}; inline constexpr ColumnSchema kPkNameSchema{0, BQDataType::kString}; +inline constexpr ColumnSchema kNonUniqueSchema{0, BQDataType::kInt64}; +inline constexpr ColumnSchema kIndexQualifierSchema{0, BQDataType::kString}; +inline constexpr ColumnSchema kIndexNameSchema{0, BQDataType::kString}; +inline constexpr ColumnSchema kTypeSchema{0, BQDataType::kInt64}; +inline constexpr ColumnSchema kOrdinalPositionSchema{0, BQDataType::kInt64}; +inline constexpr ColumnSchema kAscOrDescSchema{0, BQDataType::kString}; +inline constexpr ColumnSchema kCardinalitySchema{0, BQDataType::kInt64}; +inline constexpr ColumnSchema kPagesSchema{0, BQDataType::kInt64}; +inline constexpr ColumnSchema kFilterConditionSchema{0, BQDataType::kString}; inline ColumnSchema WithIndex(int col_index, ColumnSchema base) { base.col_index = col_index; diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.cc b/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.cc new file mode 100644 index 0000000000..a58454f918 --- /dev/null +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.cc @@ -0,0 +1,176 @@ +// Copyright 2026 Google LLC +// +// Licensed 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 +// +// https://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. + +#include "google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.h" +#include "google/cloud/odbc/bq_client_interface/utils.h" +#include "google/cloud/odbc/bq_driver/internal/odbc_sql_columns.h" +#include "google/cloud/odbc/bq_driver/internal/trace_utils.h" +#include "google/cloud/odbc/bq_driver/internal/utils.h" + +namespace google::cloud::odbc_bq_driver_internal { + +using ::google::cloud::Options; +using ::google::cloud::bigquery_v2_minimal_internal::TableMetadataView; +using ::google::cloud::odbc_bigquery_client_interface::MaxRetriesOption; +using ::google::cloud::odbc_bigquery_client_interface::TableFilter; +using ::google::cloud::odbc_internal::SQLStates; +using ::google::cloud::odbc_internal::StatusRecord; +using ::google::cloud::odbc_internal::StatusRecordOr; + +// Returns a ResultSet containing table-level statistics for the given BigQuery +// table. BigQuery does not support traditional indexes, so only a +// SQL_TABLE_STAT row (TYPE = 0) is returned with the row count in CARDINALITY. +// All index-specific columns are set to NULL. +// +// Per the ODBC spec: +// - If the table does not exist or catalog/schema arguments do not identify a +// table, the function returns SQL_SUCCESS with an empty result set. +// - CARDINALITY is the number of rows in the table (num_rows from BQ +// metadata). +// - The unique argument is ignored since BigQuery has no traditional indexes. +// - When reserved = SQL_QUICK, CARDINALITY and PAGES may be NULL; we always +// return num_rows from BQ metadata since it is cheap to retrieve. +// +// See: +// https://learn.microsoft.com/en-us/sql/odbc/reference/syntax/sqlstatistics-function +StatusRecordOr FetchStatisticsResultSet( + StatementHandle& stmt_handle, std::string const& catalog_name, + std::string const& schema_name, std::string const& table_name, + SQLUSMALLINT unique, SQLUSMALLINT reserved) { + if (unique != SQL_INDEX_UNIQUE && unique != SQL_INDEX_ALL) { + LOG(ERROR) << "FetchStatisticsResultSet:: Invalid fUnique option: " + << unique; + return StatusRecord{SQLStates::k_HY100(), "Invalid fUnique option."}; + } + + if (reserved != SQL_ENSURE && reserved != SQL_QUICK) { + LOG(ERROR) << "FetchStatisticsResultSet:: Invalid fAccuracy option: " + << reserved; + return StatusRecord{SQLStates::k_HY101(), "Invalid fAccuracy option."}; + } + + if (!stmt_handle.GetConnectionHandle()) { + LOG(ERROR) << "FetchStatisticsResultSet:: Connection handle is null."; + return StatusRecord{SQLStates::k_HY013(), + "Internal connection handle is null."}; + } + + // Initialize the result set schema. + // Per the spec, SQLStatistics always returns the 13-column schema. + ResultSet result_set; + result_set.row_schema.resize(kStatisticsMap.size()); + for (auto const& [_, schema] : kStatisticsMap) { + result_set.row_schema[schema.col_index] = schema; + } + + // Per the ODBC spec, table_name is a required identifier (not a search + // pattern). If it is empty or contains wildcard characters, return an empty + // result set. + if (table_name.empty() || absl::StrContains(table_name, "%") || + absl::StrContains(table_name, "\\")) { + return result_set; + } + + // Fetch table metadata from BigQuery to populate the SQL_TABLE_STAT row. + auto bq_client = stmt_handle.GetConnectionHandle()->GetClient(); + if (!bq_client) { + LOG(ERROR) << "FetchStatisticsResultSet:: Invalid or null BQ Client."; + return StatusRecord{SQLStates::k_HY000(), "Invalid or null BQ Client."}; + } + + Options options; + options.set( + stmt_handle.GetConnectionHandle()->GetDsn().max_retries); + + TableFilter filter{{}, TableMetadataView::Full()}; + + auto table_status = bq_client->GetTable(catalog_name, schema_name, table_name, + filter, options); + if (!table_status) { + // Per the ODBC spec: if the table is not found, return SQL_SUCCESS with + // an empty result set. + if (table_status.GetStatusRecord().native_error_code == 404) { + LOG(INFO) << "FetchStatisticsResultSet:: Table not found, returning " + "empty result set."; + return result_set; + } + LOG(ERROR) << "FetchStatisticsResultSet::GetTable:: " + << table_status.GetStatusRecord().message; + return table_status.GetStatusRecord(); + } + + auto const& table = *table_status; + + // Build the single SQL_TABLE_STAT row per the ODBC spec. + // 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 + DSRow ds_row; + + // 1: TABLE_CAT + DSValue ds_table_cat = kNullValue; + if (!catalog_name.empty()) StringToDSValue(catalog_name, ds_table_cat); + ds_row.push_back(ds_table_cat); + + // 2: TABLE_SCHEM + DSValue ds_table_schema = kNullValue; + if (!schema_name.empty()) StringToDSValue(schema_name, ds_table_schema); + ds_row.push_back(ds_table_schema); + + // 3: TABLE_NAME + DSValue ds_table_name = kNullValue; + if (!table_name.empty()) StringToDSValue(table_name, ds_table_name); + ds_row.push_back(ds_table_name); + + // 4: NON_UNIQUE — NULL for SQL_TABLE_STAT rows + ds_row.push_back(kNullValue); + + // 5: INDEX_QUALIFIER — NULL for SQL_TABLE_STAT rows + ds_row.push_back(kNullValue); + + // 6: INDEX_NAME — NULL for SQL_TABLE_STAT rows + ds_row.push_back(kNullValue); + + // 7: TYPE — SQL_TABLE_STAT (0) indicates this is a table statistics row + DSValue ds_type; + ArithmeticToDSValue(static_cast(SQL_TABLE_STAT), + ds_type); + ds_row.push_back(ds_type); + + // 8: ORDINAL_POSITION — NULL for SQL_TABLE_STAT rows + ds_row.push_back(kNullValue); + + // 9: COLUMN_NAME — NULL for SQL_TABLE_STAT rows + ds_row.push_back(kNullValue); + + // 10: ASC_OR_DESC — NULL for SQL_TABLE_STAT rows + ds_row.push_back(kNullValue); + + // 11: CARDINALITY — number of rows in the table + DSValue ds_cardinality; + ArithmeticToDSValue(static_cast(table.num_rows), + ds_cardinality); + ds_row.push_back(ds_cardinality); + + // 12: PAGES — NULL (BigQuery has no concept of pages) + ds_row.push_back(kNullValue); + + // 13: FILTER_CONDITION — NULL (only for filtered indexes) + ds_row.push_back(kNullValue); + + result_set.rows.push_back(std::move(ds_row)); + return result_set; +} + +} // namespace google::cloud::odbc_bq_driver_internal diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.h b/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.h new file mode 100644 index 0000000000..e58c859fc7 --- /dev/null +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.h @@ -0,0 +1,51 @@ +// Copyright 2026 Google LLC +// +// Licensed 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 +// +// https://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. + +#ifndef CPP_BIGQUERY_ODBC_GOOGLE_CLOUD_ODBC_BQ_DRIVER_INTERNAL_ODBC_SQL_STATISTICS_H +#define CPP_BIGQUERY_ODBC_GOOGLE_CLOUD_ODBC_BQ_DRIVER_INTERNAL_ODBC_SQL_STATISTICS_H + +#include "google/cloud/odbc/bq_client_interface/odbc_bq_client.h" +#include "google/cloud/odbc/bq_driver/internal/odbc_conn_handle.h" +#include "google/cloud/odbc/bq_driver/internal/odbc_internal_commons.h" +#include "google/cloud/odbc/bq_driver/internal/odbc_stmt_handle.h" +#include "google/cloud/odbc/internal/odbc_includes.h" +#include +#include + +namespace google::cloud::odbc_bq_driver_internal { + +static std::map const kStatisticsMap = { + {kTableCatColName, WithIndex(0, kTableCatSchema)}, + {kTableSchemaColName, WithIndex(1, kTableSchemaSchema)}, + {kTableNameColName, WithIndex(2, kTableNameSchema)}, + {kNonUniqueColName, WithIndex(3, kNonUniqueSchema)}, + {kIndexQualifierColName, WithIndex(4, kIndexQualifierSchema)}, + {kIndexNameColName, WithIndex(5, kIndexNameSchema)}, + {kTypeColName, WithIndex(6, kTypeSchema)}, + {kOrdinalPositionColName, WithIndex(7, kOrdinalPositionSchema)}, + {kColumnNameColName, WithIndex(8, kColumnNameSchema)}, + {kAscOrDescColName, WithIndex(9, kAscOrDescSchema)}, + {kCardinalityColName, WithIndex(10, kCardinalitySchema)}, + {kPagesColName, WithIndex(11, kPagesSchema)}, + {kFilterConditionColName, WithIndex(12, kFilterConditionSchema)}, +}; + +StatusRecordOr FetchStatisticsResultSet( + StatementHandle& stmt_handle, std::string const& catalog_name, + std::string const& schema_name, std::string const& table_name, + SQLUSMALLINT unique, SQLUSMALLINT reserved); + +} // namespace google::cloud::odbc_bq_driver_internal + +#endif // CPP_BIGQUERY_ODBC_GOOGLE_CLOUD_ODBC_BQ_DRIVER_INTERNAL_ODBC_SQL_STATISTICS_H diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics_test.cc b/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics_test.cc new file mode 100644 index 0000000000..66a37e7134 --- /dev/null +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics_test.cc @@ -0,0 +1,71 @@ +// Copyright 2026 Google LLC +// +// Licensed 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 +// +// https://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. + +#include "google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.h" +#include "google/cloud/odbc/testing/bq_driver_utils/handles.h" +#include +#include + +namespace google::cloud::odbc_bq_driver_internal { +namespace { + +using google::cloud::odbc_bq_driver_internal::ConnectionHandle; +using google::cloud::odbc_bq_driver_internal::DescriptorHandle; +using google::cloud::odbc_bq_driver_internal::StatementHandle; +using google::cloud::odbc_internal::SQLStates; +using google::cloud::odbc_testing_bq_driver_utils::CreateConnectionHandle; + +TEST(OdbcSqlStatisticsTest, EmptyTableNameReturnsEmptyResultSet) { + ConnectionHandle conn_handle = CreateConnectionHandle(true); + + DescriptorHandle impl_desc; + + auto* stmt_handle = new StatementHandle( + &conn_handle, {impl_desc, impl_desc, impl_desc, impl_desc}); + + conn_handle.GetStatementHandles().emplace(stmt_handle); + + auto result = FetchStatisticsResultSet(*stmt_handle, "catalog", "schema", "", + SQL_INDEX_ALL, SQL_QUICK); + + ASSERT_TRUE(result.Ok()); + EXPECT_TRUE(result->rows.empty()); +} + +TEST(OdbcSqlStatisticsTest, InvalidUniqueOption) { + StatementHandle handle; + auto result = FetchStatisticsResultSet(handle, "catalog", "schema", "table", + 999, SQL_QUICK); + ASSERT_FALSE(result.Ok()); + EXPECT_EQ(result.GetStatusRecord().sql_state, SQLStates::k_HY100()); +} + +TEST(OdbcSqlStatisticsTest, InvalidReservedOption) { + StatementHandle handle; + auto result = FetchStatisticsResultSet(handle, "catalog", "schema", "table", + SQL_INDEX_ALL, 999); + ASSERT_FALSE(result.Ok()); + EXPECT_EQ(result.GetStatusRecord().sql_state, SQLStates::k_HY101()); +} + +TEST(OdbcSqlStatisticsTest, NullConnectionHandle) { + StatementHandle handle; + auto result = FetchStatisticsResultSet(handle, "catalog", "schema", "table", + SQL_INDEX_ALL, SQL_QUICK); + ASSERT_FALSE(result.Ok()); + EXPECT_EQ(result.GetStatusRecord().sql_state, SQLStates::k_HY013()); +} + +} // namespace +} // namespace google::cloud::odbc_bq_driver_internal diff --git a/google/cloud/odbc/bq_driver/odbc_api.cc b/google/cloud/odbc/bq_driver/odbc_api.cc index eb58aefde5..4a89486ff9 100644 --- a/google/cloud/odbc/bq_driver/odbc_api.cc +++ b/google/cloud/odbc/bq_driver/odbc_api.cc @@ -3312,18 +3312,18 @@ SQLRETURN SQL_API SQLStatistics(SQLHSTMT statementHandle, SQLCHAR* catalogName, SQLSMALLINT tableNameLen, SQLUSMALLINT indexType, SQLUSMALLINT reserved) { SQLRETURN rc = SQL_SUCCESS; + InitializeTracing("SQLStatistics"); HandleLock lock(statementHandle, SQL_HANDLE_STMT); if (!lock.isLocked()) { HandleLockError(SQL_HANDLE_STMT, statementHandle, "SQLStatistics"); return SQL_ERROR; } - // Call to Trace function entry in odbc_trace.h if tracing is enabled. - // Call to common internal function for SQLStatistics and SQLStatisticsW // in odbc_driver_metadata.h. - - // Call to Trace function exit in odbc_trace.h if tracing is enabled. + rc = google::cloud::odbc_bq_driver::SQLStatisticsInternal( + statementHandle, catalogName, catalogNameLen, schemaName, schemaNameLen, + tableName, tableNameLen, indexType, reserved); return rc; } @@ -3382,6 +3382,10 @@ SQLRETURN SQL_API SQLStatisticsW( // Call to common internal function for SQLStatistics and SQLStatisticsW // in odbc_driver_metadata.h. + rc = google::cloud::odbc_bq_driver::SQLStatisticsInternal( + statementHandle, sqlchar_category_name, catalogNameLen, + sqlchar_schema_name, schemaNameLen, sqlchar_table_name, tableNameLen, + indexType, reserved); // Handle Unicode conversion of output parameters. return rc; diff --git a/google/cloud/odbc/bq_driver/odbc_driver_metadata.cc b/google/cloud/odbc/bq_driver/odbc_driver_metadata.cc index ae1c7406c9..d3c88c5810 100644 --- a/google/cloud/odbc/bq_driver/odbc_driver_metadata.cc +++ b/google/cloud/odbc/bq_driver/odbc_driver_metadata.cc @@ -21,6 +21,7 @@ #include "google/cloud/odbc/bq_driver/internal/odbc_sql_info.h" #include "google/cloud/odbc/bq_driver/internal/odbc_sql_primary_keys.h" #include "google/cloud/odbc/bq_driver/internal/odbc_sql_special_columns.h" +#include "google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.h" #include "google/cloud/odbc/bq_driver/internal/odbc_sql_tables.h" #include "google/cloud/odbc/bq_driver/internal/trace_utils.h" #include "google/cloud/odbc/bq_driver/odbc_utils.h" @@ -28,7 +29,9 @@ namespace google::cloud::odbc_bq_driver { +using google::cloud::bigquery_v2_minimal_internal::TableReference; using google::cloud::odbc_bigquery_client_interface::ODBCBQClient; +using google::cloud::odbc_bq_driver_internal::BuildTableSchemaFromRowSchema; using google::cloud::odbc_bq_driver_internal::ConnectionHandle; using google::cloud::odbc_bq_driver_internal::CreateResultSetForTableTypes; using google::cloud::odbc_bq_driver_internal::DescriptorHandle; @@ -38,6 +41,7 @@ using google::cloud::odbc_bq_driver_internal::EscapeOdbcPattern; using google::cloud::odbc_bq_driver_internal::FetchBQSQLProceduresData; using google::cloud::odbc_bq_driver_internal::FetchBQTablesData; using google::cloud::odbc_bq_driver_internal::FetchForeignKeysFromDataSource; +using google::cloud::odbc_bq_driver_internal::FetchStatisticsResultSet; using google::cloud::odbc_bq_driver_internal::GetResultSetForDatasets; using google::cloud::odbc_bq_driver_internal::GetResultSetForProjects; using google::cloud::odbc_bq_driver_internal::GetResultSetForTables; @@ -50,6 +54,7 @@ using google::cloud::odbc_bq_driver_internal::kODBCColumnsMap; using google::cloud::odbc_bq_driver_internal::kPrimaryKeysMap; using google::cloud::odbc_bq_driver_internal::kSchema; using google::cloud::odbc_bq_driver_internal::kSqlApiAllFuncsSize; +using google::cloud::odbc_bq_driver_internal::kStatisticsMap; using google::cloud::odbc_bq_driver_internal::LogAndReturnCode; using google::cloud::odbc_bq_driver_internal::PopulateSupportedODBC2Functions; using google::cloud::odbc_bq_driver_internal::PopulateSupportedODBC3Functions; @@ -1059,4 +1064,126 @@ SQLRETURN SQLProcedureColumnsInternal( return SQL_SUCCESS; } +SQLRETURN SQLStatisticsInternal(SQLHSTMT stmt_handle, + SQLCHAR const* catalog_name, + SQLSMALLINT catalog_name_len, + SQLCHAR const* schema_name, + SQLSMALLINT /*schema_name_len*/, + SQLCHAR const* table_name, + SQLSMALLINT /*table_name_len*/, + SQLUSMALLINT unique, SQLUSMALLINT reserved) { + StatusRecordOr handle_result = + ValidateStatementHandle(stmt_handle); + if (!handle_result) { + LOG(ERROR) << "SQLStatistics::ValidateStatementHandle:: " + << handle_result.GetStatusRecord().message; + return handle_result.GetCalculatedReturnCode(); + } + StatementHandle& handle = *(*handle_result); + + if (handle.GetConnectionHandle() == nullptr) { + LOG(ERROR) << "SQLStatistics:: Internal connection handle is null"; + return LogAndReturnCode(handle, + StatusRecord{SQLStates::k_HY013(), + "Internal connection handle is null"}); + } + ConnectionHandle& conn_handle = *(handle.GetConnectionHandle()); + + // Resolve catalog (project). Per the spec, catalog_name is not a search + // pattern. When null/empty, use the connection's current catalog. + std::string s_catalog_name; + if (catalog_name == nullptr || catalog_name_len == 0 || + catalog_name_len == SQL_NULL_DATA) { + SQLINTEGER catalog_len = 0; + SQLCHAR current_catalog[256] = {0}; + conn_handle.GetAttribute(SQL_ATTR_CURRENT_CATALOG, current_catalog, + sizeof(current_catalog), &catalog_len); + s_catalog_name = ToCharStr(current_catalog); + } else { + s_catalog_name = ToCharStr(catalog_name); + } + + // Per the spec, table_name is a required identifier argument (not a search + // pattern). Applications may pass "dataset.table" in table_name with a null + // schema_name; we split on '.' to extract the schema in that case. + std::string s_schema_name = ToCharStr(schema_name, ""); + std::string s_table_name = ToCharStr(table_name, ""); + + if (s_schema_name.empty()) { + // Try to parse "dataset.table" notation from table_name. + auto const dot_pos = s_table_name.find('.'); + if (dot_pos != std::string::npos) { + s_schema_name = s_table_name.substr(0, dot_pos); + s_table_name = s_table_name.substr(dot_pos + 1); + } + } + + // If schema is still empty, fall back to the DSN's default dataset when + // configured (matching the behaviour of SQLColumns). + if (s_schema_name.empty()) { + auto const dsn = conn_handle.GetDsn(); + if (dsn.filter_tables_on_default_dataset && !dsn.default_dataset.empty()) { + s_schema_name = dsn.default_dataset; + } + } + + // Per the ODBC spec, table_name is a required argument — a null pointer + // means "no table name specified" and is an error (HY009). + // An empty string is treated as a valid (if unresolvable) identifier and + // returns SQL_SUCCESS with an empty result set, consistent with Existing + // driver. + if (table_name == nullptr) { + LOG(ERROR) + << "SQLStatistics:: table_name is required (null pointer passed)"; + return LogAndReturnCode( + handle, StatusRecord{SQLStates::k_HY009(), + "TableName is required for SQLStatistics"}); + } + + auto result_set_status = FetchStatisticsResultSet( + handle, s_catalog_name, s_schema_name, s_table_name, unique, reserved); + + if (!result_set_status) { + LOG(ERROR) << "SQLStatistics::FetchStatisticsResultSet:: " + << result_set_status.GetStatusRecord().message; + return LogAndReturnCode(handle, result_set_status); + } + + ResultSet result_set = std::move(*result_set_status); + + // Apply SQL_ATTR_MAX_ROWS limit. + auto max_rows_status = handle.GetAttribute(SQL_ATTR_MAX_ROWS); + SQLULEN max_rows = 0; + if (max_rows_status) { + max_rows = *max_rows_status; + } + if (max_rows != 0 && result_set.rows.size() > max_rows) { + result_set.rows.resize(max_rows); + } + + // Populate the IRD so SQLNumResultCols / SQLDescribeCol work correctly. + DescriptorHandle& ird = handle.GetDescriptorHandle(DescriptorType::kIRD); + ird.SetConnectionHandle(&conn_handle); + + auto table_schema = + BuildTableSchemaFromRowSchema(result_set.row_schema, kStatisticsMap); + if (!table_schema) { + LOG(ERROR) << "SQLStatistics::BuildTableSchemaFromRowSchema:: " + << table_schema.GetStatusRecord().message; + return LogAndReturnCode(handle, table_schema); + } + + TableReference table_fields; + auto populate_status = + StatementHandle::PopulateIrd(ird, *table_schema, table_fields, true); + if (!populate_status.ok()) { + LOG(ERROR) << "SQLStatistics::PopulateIrd:: " << populate_status.message; + return LogAndReturnCode(handle, populate_status); + } + + handle.SetResultSet(result_set); + handle.SetStmtState(StmtStates::kStatementExecutedWithRs); + return SQL_SUCCESS; +} + } // namespace google::cloud::odbc_bq_driver diff --git a/google/cloud/odbc/bq_driver/odbc_driver_metadata.h b/google/cloud/odbc/bq_driver/odbc_driver_metadata.h index 84284af647..1433ccb3a8 100644 --- a/google/cloud/odbc/bq_driver/odbc_driver_metadata.h +++ b/google/cloud/odbc/bq_driver/odbc_driver_metadata.h @@ -175,6 +175,20 @@ SQLRETURN SQLProcedureColumnsInternal( SQLSMALLINT proc_name_len, SQLCHAR* column_name, SQLSMALLINT column_name_len); +// Implements the semantics for SQLStatistics ODBC API +// as per the ODBC 3.8 spec and the design doc. +// +// For details on the implementation semantics please refer to +// the following: +// +// ODBC Spec: +// https://learn.microsoft.com/en-us/sql/odbc/reference/syntax/sqlstatistics-function +SQLRETURN SQLStatisticsInternal( + SQLHSTMT stmt_handle, SQLCHAR const* catalog_name, + SQLSMALLINT catalog_name_len, SQLCHAR const* schema_name, + SQLSMALLINT schema_name_len, SQLCHAR const* table_name, + SQLSMALLINT table_name_len, SQLUSMALLINT unique, SQLUSMALLINT reserved); + } // namespace google::cloud::odbc_bq_driver #endif // CPP_BIGQUERY_ODBC_GOOGLE_CLOUD_ODBC_BQ_DRIVER_ODBC_DRIVER_METADATA_H diff --git a/google/cloud/odbc/google_cloud_odbc_bq_driver_internal.bzl b/google/cloud/odbc/google_cloud_odbc_bq_driver_internal.bzl index 4b6e04e09c..eec790655b 100644 --- a/google/cloud/odbc/google_cloud_odbc_bq_driver_internal.bzl +++ b/google/cloud/odbc/google_cloud_odbc_bq_driver_internal.bzl @@ -38,6 +38,7 @@ google_cloud_odbc_bq_driver_internal_hdrs = [ "bq_driver/internal/odbc_sql_info.h", "bq_driver/internal/odbc_sql_primary_keys.h", "bq_driver/internal/odbc_sql_special_columns.h", + "bq_driver/internal/odbc_sql_statistics.h", "bq_driver/internal/odbc_sql_tables.h", "bq_driver/internal/odbc_sql_type_info.h", "bq_driver/internal/odbc_stmt_attr.h", @@ -70,6 +71,7 @@ google_cloud_odbc_bq_driver_internal_srcs = [ "bq_driver/internal/odbc_sql_info.cc", "bq_driver/internal/odbc_sql_primary_keys.cc", "bq_driver/internal/odbc_sql_special_columns.cc", + "bq_driver/internal/odbc_sql_statistics.cc", "bq_driver/internal/odbc_sql_tables.cc", "bq_driver/internal/odbc_sql_type_info.cc", "bq_driver/internal/odbc_stmt_attr.cc", diff --git a/google/cloud/odbc/integration_tests/odbc_driver_tests/catalog_test.cc b/google/cloud/odbc/integration_tests/odbc_driver_tests/catalog_test.cc index f4c607350b..d28f702a21 100644 --- a/google/cloud/odbc/integration_tests/odbc_driver_tests/catalog_test.cc +++ b/google/cloud/odbc/integration_tests/odbc_driver_tests/catalog_test.cc @@ -2219,7 +2219,6 @@ TEST(CatalogTest, SQLTables_NullCatalogFiltersToCurrentProject) { EXPECT_EQ(Disconnect(conn), SQL_SUCCESS); } -#ifndef BQ_DRIVER_INTEGRATION_TESTS TEST(CatalogTest, SQLStatistics_ValidTableRows) { auto conn = std::make_shared(); std::string table_name = kDatasetWithTablePrefix + "ODBC_SQLSTATISTICS_TEST"; @@ -2241,10 +2240,16 @@ TEST(CatalogTest, SQLStatistics_ValidTableRows) { ASSERT_TRUE(status == SQL_SUCCESS || status == SQL_SUCCESS_WITH_INFO); // Verify expected ODBC result-set schema +#ifndef BQ_DRIVER_INTEGRATION_TESTS + SQLSMALLINT const table_name_nullable = SQL_NULLABLE; +#else + SQLSMALLINT const table_name_nullable = SQL_NO_NULLS; +#endif + ExpectedColMetadata expected[] = { {"TABLE_CAT", SQL_WVARCHAR, 128, 0, SQL_NULLABLE}, {"TABLE_SCHEM", SQL_WVARCHAR, 1024, 0, SQL_NULLABLE}, - {"TABLE_NAME", SQL_WVARCHAR, 1024, 0, SQL_NULLABLE}, + {"TABLE_NAME", SQL_WVARCHAR, 1024, 0, table_name_nullable}, {"NON_UNIQUE", SQL_SMALLINT, 5, 0, SQL_NULLABLE}, {"INDEX_QUALIFIER", SQL_WVARCHAR, 255, 0, SQL_NULLABLE}, {"INDEX_NAME", SQL_WVARCHAR, 128, 0, SQL_NULLABLE}, @@ -2272,7 +2277,11 @@ TEST(CatalogTest, SQLStatistics_ValidTableRows) { ASSERT_TRUE(status == SQL_SUCCESS || status == SQL_SUCCESS_WITH_INFO); ++row_count; } +#ifndef BQ_DRIVER_INTEGRATION_TESTS EXPECT_EQ(0, row_count); +#else + EXPECT_EQ(1, row_count); +#endif /* BQ_DRIVER_INTEGRATION_TESTS */ EXPECT_EQ(Disconnect(conn), SQL_SUCCESS); From 52305ea71759449cf1c72b71ec581e41a4bd2bcc Mon Sep 17 00:00:00 2001 From: Kanchan Shukla Date: Mon, 14 Sep 2026 08:12:26 +0000 Subject: [PATCH 2/4] fixed review comments --- google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.cc b/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.cc index a58454f918..8d049053b7 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.cc +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.cc @@ -144,8 +144,8 @@ StatusRecordOr FetchStatisticsResultSet( // 7: TYPE — SQL_TABLE_STAT (0) indicates this is a table statistics row DSValue ds_type; - ArithmeticToDSValue(static_cast(SQL_TABLE_STAT), - ds_type); + ArithmeticToDSValue(static_cast(SQL_TABLE_STAT), + ds_type); ds_row.push_back(ds_type); // 8: ORDINAL_POSITION — NULL for SQL_TABLE_STAT rows From 9e584ff3fd45b4959c367488203dae90a8bc2865 Mon Sep 17 00:00:00 2001 From: Kanchan Shukla Date: Tue, 15 Sep 2026 14:32:53 +0000 Subject: [PATCH 3/4] fixed checkers --- .../cloud/odbc/bq_driver/internal/odbc_desc_attr.cc | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/google/cloud/odbc/bq_driver/internal/odbc_desc_attr.cc b/google/cloud/odbc/bq_driver/internal/odbc_desc_attr.cc index 38209c531a..c9f50d5ad7 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_desc_attr.cc +++ b/google/cloud/odbc/bq_driver/internal/odbc_desc_attr.cc @@ -573,13 +573,12 @@ void DescriptorRecord::ApplyMetadataIrdOverrides(std::string const& col_name) { col_name == "SQL_DATETIME_SUB" || col_name == "KEY_SEQ" || col_name == "UPDATE_RULE" || col_name == "DELETE_RULE" || col_name == "DEFERRABILITY" || col_name == "NON_UNIQUE" || - col_name == "TYPE" || col_name == "SCOPE" || - col_name == "PSEUDO_COLUMN"; + col_name == "TYPE" || col_name == "SCOPE" || col_name == "PSEUDO_COLUMN"; - bool const is_integer = - col_name == "COLUMN_SIZE" || col_name == "BUFFER_LENGTH" || - col_name == "ORDINAL_POSITION" || col_name == "CARDINALITY" || - col_name == "PAGES"; + bool const is_integer = col_name == "COLUMN_SIZE" || + col_name == "BUFFER_LENGTH" || + col_name == "ORDINAL_POSITION" || + col_name == "CARDINALITY" || col_name == "PAGES"; bool const is_wchar_one = col_name == "ASC_OR_DESC"; From ec5dcf661898a56f2deb0ca8bea982272658cb19 Mon Sep 17 00:00:00 2001 From: Kanchan Shukla Date: Thu, 17 Sep 2026 10:20:41 +0000 Subject: [PATCH 4/4] fixed git check issues --- .../odbc/bq_driver/internal/odbc_sql_statistics_test.cc | 8 +++++--- .../integration_tests/odbc_driver_tests/catalog_test.cc | 1 - 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics_test.cc b/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics_test.cc index 66a37e7134..b5b8b37f1f 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics_test.cc +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_statistics_test.cc @@ -31,16 +31,18 @@ TEST(OdbcSqlStatisticsTest, EmptyTableNameReturnsEmptyResultSet) { DescriptorHandle impl_desc; - auto* stmt_handle = new StatementHandle( - &conn_handle, {impl_desc, impl_desc, impl_desc, impl_desc}); + auto stmt_handle = std::make_unique( + &conn_handle, Descriptors{impl_desc, impl_desc, impl_desc, impl_desc}); - conn_handle.GetStatementHandles().emplace(stmt_handle); + conn_handle.GetStatementHandles().emplace(stmt_handle.get()); auto result = FetchStatisticsResultSet(*stmt_handle, "catalog", "schema", "", SQL_INDEX_ALL, SQL_QUICK); ASSERT_TRUE(result.Ok()); EXPECT_TRUE(result->rows.empty()); + + conn_handle.GetStatementHandles().erase(stmt_handle.get()); } TEST(OdbcSqlStatisticsTest, InvalidUniqueOption) { diff --git a/google/cloud/odbc/integration_tests/odbc_driver_tests/catalog_test.cc b/google/cloud/odbc/integration_tests/odbc_driver_tests/catalog_test.cc index d28f702a21..2870269430 100644 --- a/google/cloud/odbc/integration_tests/odbc_driver_tests/catalog_test.cc +++ b/google/cloud/odbc/integration_tests/odbc_driver_tests/catalog_test.cc @@ -2377,7 +2377,6 @@ TEST(CatalogTest, SQLStatisticsW_ValidTable) { table.DropWithPrepare(conn); EXPECT_EQ(Disconnect(conn), SQL_SUCCESS); } -#endif // BQ_DRIVER_INTEGRATION_TESTS // NOTE: We do not call CreateTableDirect() in the SQLSpecialColumns tests // below because doing so frequently exceeds BigQuery's rate limits for table