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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions google/cloud/odbc/bq_driver.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
46 changes: 38 additions & 8 deletions google/cloud/odbc/bq_driver/internal/odbc_desc_attr.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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" ||
Expand All @@ -568,18 +572,28 @@ 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 == "PSEUDO_COLUMN";
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 == "ORDINAL_POSITION" ||
col_name == "CARDINALITY" || col_name == "PAGES";

bool const is_integer =
col_name == "COLUMN_SIZE" || col_name == "BUFFER_LENGTH";
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<SQLSMALLINT>(length);
case_sensitive = 0;
searchable = 0;
Expand Down Expand Up @@ -609,13 +623,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;
}
}
Expand Down
18 changes: 18 additions & 0 deletions google/cloud/odbc/bq_driver/internal/odbc_internal_commons.h
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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;
Expand Down
176 changes: 176 additions & 0 deletions google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.cc
Original file line number Diff line number Diff line change
@@ -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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where are we returning SQL_TABLE_STAT row?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

returning it as part of result set

// 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<ResultSet> FetchStatisticsResultSet(
StatementHandle& stmt_handle, std::string const& catalog_name,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

catalog_name, schema_name, table_name and other parameters are not used in the code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated code

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<MaxRetriesOption>(
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<SQLBIGINT>(static_cast<SQLBIGINT>(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<SQLBIGINT>(static_cast<SQLBIGINT>(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
51 changes: 51 additions & 0 deletions google/cloud/odbc/bq_driver/internal/odbc_sql_statistics.h
Original file line number Diff line number Diff line change
@@ -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 <map>
#include <string>

namespace google::cloud::odbc_bq_driver_internal {

static std::map<std::string, ColumnSchema> 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<ResultSet> 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
Loading