From f94656953e17a7579c3e8ac2f1fd3237ab1b1c8a Mon Sep 17 00:00:00 2001 From: Shadab Raza Date: Mon, 14 Sep 2026 20:54:23 +0000 Subject: [PATCH] chore: Remove information schema references from ODBC SQL handlers and add integration tests Remove reliance on INFORMATION_SCHEMA SQL query jobs when retrieving foreign key metadata, switching to direct BigQuery REST API table constraint inspection. - Fast Path: Fetch single-table constraints directly using FetchBQTableData() via tables.get when an exact table name is provided. - Parallel Listing: Enumerate dataset tables using GetFilteredTables() and fetch constraint metadata concurrently using ExecuteParallelTasks() for pattern or schema-wide requests. - ResultSet Conversion: Convert BigQuery ForeignKey metadata into standard ODBC result-set rows via AppendForeignKeyRows(). - Fault Tolerance: Safely skip concurrently deleted tables (HTTP 404) during parallel execution without failing the batch. - Integration Tests: Add integration tests and benchmarks covering single-table and dataset-wide foreign key lookup scenarios. --- .../internal/odbc_sql_foreign_keys.cc | 427 ++++++++++-------- .../internal/odbc_sql_primary_keys.cc | 21 - .../bq_driver/internal/odbc_sql_tables.cc | 63 --- .../odbc/bq_driver/internal/odbc_sql_tables.h | 8 - .../internal/odbc_sql_tables_test.cc | 84 ---- .../odbc_driver_tests/catalog_test.cc | 26 ++ 6 files changed, 262 insertions(+), 367 deletions(-) diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_foreign_keys.cc b/google/cloud/odbc/bq_driver/internal/odbc_sql_foreign_keys.cc index 6257a2d4fc..a9296cb167 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_sql_foreign_keys.cc +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_foreign_keys.cc @@ -13,112 +13,126 @@ // limitations under the License. #include "google/cloud/odbc/bq_driver/internal/odbc_sql_foreign_keys.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/odbc_sql_tables.h" #include "google/cloud/odbc/bq_driver/internal/trace_utils.h" #include "absl/strings/match.h" #include #include +#include #include namespace google::cloud::odbc_bq_driver_internal { using ::google::cloud::bigquery_v2_minimal_internal::ColumnReference; using ::google::cloud::bigquery_v2_minimal_internal::ForeignKey; +using ::google::cloud::bigquery_v2_minimal_internal::Table; using ::google::cloud::odbc_internal::SQLStates; using ::google::cloud::odbc_internal::StatusRecord; using ::google::cloud::odbc_internal::StatusRecordOr; namespace { -std::string const kNamedCatalogParam = "catalog_name"; -std::string const kNamedSchemaParam = "schema_name"; -std::string const kNamedPKTableParam = "pk_table_name"; -std::string const kNamedFKTableParam = "fk_table_name"; - -std::string const kBasicForeignKeysQueryPrefix = - "WITH pk_constraint AS ( " - "SELECT key_column_usage.constraint_catalog as pk_catalog," - "key_column_usage.constraint_schema as pk_dataset, " - "key_column_usage.table_name as pk_table, " - "key_column_usage.column_name as pk_column, " - "key_column_usage.constraint_name as pk_name, " - "key_column_usage.ordinal_position as pk_column_ordinal_position " - "FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE key_column_usage " - "INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS table_constraints " - "ON table_constraints.table_name = key_column_usage.table_name " - "AND table_constraints.constraint_name = key_column_usage.constraint_name " - "AND table_constraints.constraint_schema = " - "key_column_usage.constraint_schema " - "WHERE table_constraints.CONSTRAINT_TYPE = 'PRIMARY KEY' " - "), " - "pk_references AS ( " - "SELECT pk_constraint.*, " - "constraints_column_usage.constraint_schema as fk_constraint_schema, " - "constraints_column_usage.constraint_name as fk_constraint_name " - "FROM pk_constraint " - "JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE constraints_column_usage " - "ON true " - "AND pk_constraint.pk_table = constraints_column_usage.table_name " - "AND pk_constraint.pk_column = constraints_column_usage.column_name " - "AND pk_constraint.pk_dataset = constraints_column_usage.TABLE_SCHEMA " - ") " - "SELECT pk_references.pk_catalog, " - "pk_references.pk_dataset, " - "pk_references.pk_table, " - "pk_references.pk_column, " - "key_column_usage.table_catalog as fk_catalog, " - "key_column_usage.table_schema as fk_dataset, " - "key_column_usage.table_name as fk_table, " - "key_column_usage.column_name as fk_column, " - "key_column_usage.ordinal_position as fk_column_ordinal_position, " - "CAST(NULL AS INT64) AS update_rule, " - "CAST(NULL AS INT64) AS delete_rule, " - "key_column_usage.constraint_name as fk_name, " - "pk_references.pk_name, " - "CAST(2 AS INT64) AS deferrability " - "FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE key_column_usage " - "JOIN pk_references " - "ON pk_references.fk_constraint_name = key_column_usage.constraint_name " - "AND pk_references.fk_constraint_schema = " - "key_column_usage.constraint_schema " - "AND pk_references.pk_column_ordinal_position = " - "key_column_usage.POSITION_IN_UNIQUE_CONSTRAINT "; - -std::string const kBasicForeignKeysQuerySuffix = - "ORDER BY pk_table, pk_column_ordinal_position, pk_column"; - -// BigQuery INFORMATION_SCHEMA reports constraint names as -// ".", where the primary key constraint name is -// always "pk$" (BigQuery primary keys cannot be named) and an unnamed foreign -// key constraint is named "fk$" by order of declaration, which is the order -// tables.get lists them in. These suffixes let the tables.get based path below -// produce the same constraint names as the INFORMATION_SCHEMA query path. + +// BigQuery exposes constraint names in the form ".". +// Primary keys cannot be named explicitly in BigQuery, so they always use +// "pk$". Unnamed foreign keys receive synthetic names "fk$" based on +// declaration order. std::string const kPrimaryKeyNameSuffix = ".pk$"; std::string const kForeignKeyNameSuffix = ".fk$"; -// Fetches the foreign keys of a single, exactly named table by reading the -// table's constraints via the tables.get REST API, the same way -// FetchPKResultSetFromTableMetaData does for SQLPrimaryKeys. This avoids the -// several seconds of fixed latency an INFORMATION_SCHEMA query job carries -// (table names are ordinary identifiers, not search patterns, per the ODBC -// spec). When pk_table_name is non-empty, only foreign keys referencing that -// table are returned. The rows are returned as a ready ResultSet inside -// DSResults, which ProcessQueryResults passes through unchanged. +// Converts BigQuery ForeignKey constraints into standard ODBC SQLForeignKeys +// result set rows. +// +// Arguments: +// result_set: Target result set receiving formatted row data. +// foreign_key: BigQuery foreign key definition containing referenced table +// and columns. catalog_name: Current project/catalog ID. schema_name: +// Current dataset/schema ID. fk_table_name: Table ID containing the foreign +// key constraint. fk_ordinal: Ordinal index of the foreign key in the +// table definition. +void AppendForeignKeyRows(ResultSet& result_set, ForeignKey const& foreign_key, + std::string const& catalog_name, + std::string const& schema_name, + std::string const& fk_table_name, int fk_ordinal) { + // Format the foreign key constraint name to match standard BigQuery naming + // conventions. + DSValue fk_name_value; + if (foreign_key.key_name.empty()) { + StringToDSValue( + fk_table_name + kForeignKeyNameSuffix + std::to_string(fk_ordinal), + fk_name_value); + } else { + StringToDSValue(fk_table_name + "." + foreign_key.key_name, fk_name_value); + } + + // Format the primary key constraint name. + DSValue pk_name_value; + StringToDSValue(foreign_key.referenced_table.table_id + kPrimaryKeyNameSuffix, + pk_name_value); + + SQLBIGINT key_seq = 0; + for (ColumnReference const& column_ref : foreign_key.column_references) { + ++key_seq; + + DSRow row(kForeignKeysMap.size()); + StringToDSValue(foreign_key.referenced_table.project_id, + row[0]); // PKTABLE_CAT + StringToDSValue(foreign_key.referenced_table.dataset_id, + row[1]); // PKTABLE_SCHEM + StringToDSValue(foreign_key.referenced_table.table_id, + row[2]); // PKTABLE_NAME + StringToDSValue(column_ref.referenced_column, row[3]); // PKCOLUMN_NAME + StringToDSValue(catalog_name, row[4]); // FKTABLE_CAT + StringToDSValue(schema_name, row[5]); // FKTABLE_SCHEM + StringToDSValue(fk_table_name, row[6]); // FKTABLE_NAME + StringToDSValue(column_ref.referencing_column, row[7]); // FKCOLUMN_NAME + ArithmeticToDSValue(key_seq, row[8]); // KEY_SEQ + row[9] = kNullValue; // UPDATE_RULE + row[10] = kNullValue; // DELETE_RULE + row[11] = fk_name_value; // FK_NAME + row[12] = pk_name_value; // PK_NAME + ArithmeticToDSValue(2, row[13]); // DEFERRABILITY + + result_set.rows.push_back(std::move(row)); + } +} + +// Sorts SQLForeignKeys result rows to ensure deterministic result ordering. +// Matches the legacy INFORMATION_SCHEMA path ordering: PKTABLE_NAME ascending, +// then KEY_SEQ ascending. +void SortForeignKeyRows(ResultSet& result_set) { + std::stable_sort(result_set.rows.begin(), result_set.rows.end(), + [](DSRow const& a, DSRow const& b) { + if (a[2] != b[2]) return a[2] < b[2]; + DSValue seq_a = a[8]; + DSValue seq_b = b[8]; + return DSValueToInt(seq_a) < DSValueToInt(seq_b); + }); +} + +// FAST PATH: Used when an explicit foreign key table name (without wildcards +// '%') is provided. Fetches table metadata directly via `tables.get` without +// listing all tables in the dataset. StatusRecordOr FetchForeignKeysFromTableMetadata( StatementHandle& stmt_handle, std::string const& catalog_name, std::string const& schema_name, std::string const& pk_table_name, std::string const& fk_table_name) { ConnectionHandle& conn_handle = *(stmt_handle.GetConnectionHandle()); + + // Directly retrieve table metadata using driver helper `FetchBQTableData`. auto bq_table_status = FetchBQTableData(conn_handle, catalog_name, schema_name, fk_table_name); + ResultSet result_set; result_set.row_schema.resize(kForeignKeysMap.size()); for (auto const& [_, schema] : kForeignKeysMap) { result_set.row_schema[schema.col_index] = schema; } + if (!bq_table_status) { auto const& status = bq_table_status.GetStatusRecord(); - // An unknown table produces an empty result set, matching the - // INFORMATION_SCHEMA query path. + // Non-existent tables return an empty result set per ODBC specification. if (status.native_error_code == 404) { LOG(INFO) << "FetchForeignKeysFromTableMetadata:: Table not found: '" << catalog_name << "." << schema_name << "." << fk_table_name @@ -132,69 +146,157 @@ StatusRecordOr FetchForeignKeysFromTableMetadata( stmt_handle.GetDiagnostics().AddStatusRecord(status); return status; } - // Ordinal of the foreign key within the table's constraints, which is what - // BigQuery numbers the unnamed ones by. Counted over all of them, not just - // the ones kept by the pk_table_name filter below. + int fk_ordinal = 0; for (ForeignKey const& foreign_key : bq_table_status->table_constraints.foreign_keys) { ++fk_ordinal; + // Filter out foreign keys that do not match the requested primary key + // table. if (!pk_table_name.empty() && foreign_key.referenced_table.table_id != pk_table_name) { continue; } - // INFORMATION_SCHEMA reports constraint names prefixed with the table id, - // e.g. "my_table.fk$1" / "my_table.my_named_fk"; reproduce that. The name - // of an explicitly named constraint is not available here: tables.get - // reports it in "name", but google-cloud-cpp parses it from "keyName", so - // ForeignKey::key_name always arrives empty and such a constraint is - // reported as "fk$" instead of its declared name. key_name is still - // preferred when present, so this corrects itself if the dependency does. - DSValue fk_name_value; - if (foreign_key.key_name.empty()) { - StringToDSValue( - fk_table_name + kForeignKeyNameSuffix + std::to_string(fk_ordinal), - fk_name_value); - } else { - StringToDSValue(fk_table_name + "." + foreign_key.key_name, - fk_name_value); + AppendForeignKeyRows(result_set, foreign_key, catalog_name, schema_name, + fk_table_name, fk_ordinal); + } + SortForeignKeyRows(result_set); + + DSResults ds_results; + ds_results.data_source_results = std::move(result_set); + return ds_results; +} + +// MULTI-THREADED FALLBACK PATH: Used when `fk_table_name` is empty or contains +// wildcard characters. Lists matching tables in the dataset via `tables.list`, +// then fetches metadata in parallel across thread workers. +StatusRecordOr FetchForeignKeysByListingTables( + StatementHandle& stmt_handle, std::string const& catalog_name, + std::string const& schema_name, std::string const& pk_table_name, + std::string const& fk_table_name) { + ConnectionHandle& conn_handle = *(stmt_handle.GetConnectionHandle()); + + if (!conn_handle.IsConnected()) { + LOG(ERROR) + << "FetchForeignKeysByListingTables:: Connection to the data source " + "is broken."; + return StatusRecord{SQLStates::k_08S01(), + "Connection to the data source is broken"}; + } + + auto bq_client = conn_handle.GetClient(); + if (!bq_client) { + LOG(ERROR) << "FetchForeignKeysByListingTables:: Invalid or null BQ Client " + "within the connection handle."; + return StatusRecord{ + SQLStates::k_HY000(), + "Invalid or null BQ Client within the connection handle"}; + } + + ResultSet result_set; + result_set.row_schema.resize(kForeignKeysMap.size()); + for (auto const& [_, schema] : kForeignKeysMap) { + result_set.row_schema[schema.col_index] = schema; + } + + int const max_retries = conn_handle.GetDsn().max_retries; + std::string const tables_filter = + fk_table_name.empty() ? kMatchAll : fk_table_name; + + // Enumerate candidate tables in the target dataset. + auto tables_status = + GetFilteredTables(*bq_client, catalog_name, schema_name, tables_filter, + "TABLE", SQL_FALSE, max_retries); + + if (!tables_status) { + return tables_status.GetStatusRecord(); + } + + // Determine thread pool concurrency limit based on DSN trace settings or + // hardware concurrency. + std::uint32_t max_threads = 1U; + auto trace_option = TraceOptions::GetTraceOption(); + if (trace_option != nullptr && trace_option->max_threads > 0) { + max_threads = static_cast(trace_option->max_threads); + } else { + max_threads = std::max(1U, std::thread::hardware_concurrency()); + } + + struct ForeignKeyTableResult { + std::string table_name; + Table table; + }; + + // Task lambda passed to `ExecuteParallelTasks`. Wraps `FetchBQTableData` to: + // 1. Capture required catalog and schema parameters from outer scope. + // 2. Handle HTTP 404 gracefully (if a table is deleted mid-execution). + auto fetch_table_task = [&conn_handle, &catalog_name, + &schema_name](FilteredTableResponse const& table) + -> StatusRecordOr> { + auto bq_table_status = FetchBQTableData(conn_handle, catalog_name, + schema_name, table.table_name); + + if (!bq_table_status) { + auto const& status = bq_table_status.GetStatusRecord(); + // If a table is concurrently dropped after `tables.list`, skip it without + // failing the task batch. + if (status.native_error_code == 404) { + return std::optional{}; + } + LOG(ERROR) << "FetchForeignKeysByListingTables::FetchBQTableData:: " + << status.message; + return status; + } + + return std::optional( + ForeignKeyTableResult{table.table_name, std::move(*bq_table_status)}); + }; + + // Execute table metadata fetches concurrently using the shared thread + // manager. + auto table_results_or = + ExecuteParallelTasks>( + max_threads, *tables_status, fetch_table_task); + + if (!table_results_or) { + auto const& status = table_results_or.GetStatusRecord(); + stmt_handle.GetDiagnostics().AddStatusRecord(status); + return status; + } + + // Process parallel task results sequentially and build final result set rows. + for (auto& maybe_table : *table_results_or) { + if (!maybe_table.has_value()) { + continue; } - DSValue pk_name_value; - StringToDSValue( - foreign_key.referenced_table.table_id + kPrimaryKeyNameSuffix, - pk_name_value); - SQLBIGINT key_seq = 0; - for (ColumnReference const& column_ref : foreign_key.column_references) { - ++key_seq; - DSRow row(kForeignKeysMap.size()); - StringToDSValue(foreign_key.referenced_table.project_id, - row[0]); // PKTABLE_CAT - StringToDSValue(foreign_key.referenced_table.dataset_id, - row[1]); // PKTABLE_SCHEM - StringToDSValue(foreign_key.referenced_table.table_id, - row[2]); // PKTABLE_NAME - StringToDSValue(column_ref.referenced_column, row[3]); // PKCOLUMN_NAME - StringToDSValue(catalog_name, row[4]); // FKTABLE_CAT - StringToDSValue(schema_name, row[5]); // FKTABLE_SCHEM - StringToDSValue(fk_table_name, row[6]); // FKTABLE_NAME - StringToDSValue(column_ref.referencing_column, row[7]); // FKCOLUMN_NAME - ArithmeticToDSValue(key_seq, row[8]); // KEY_SEQ - row[9] = kNullValue; // UPDATE_RULE - row[10] = kNullValue; // DELETE_RULE - row[11] = fk_name_value; // FK_NAME - row[12] = pk_name_value; // PK_NAME - ArithmeticToDSValue(2, row[13]); // DEFERRABILITY - result_set.rows.push_back(std::move(row)); + + auto& table_result = *maybe_table; + int fk_ordinal = 0; + + for (ForeignKey const& foreign_key : + table_result.table.table_constraints.foreign_keys) { + ++fk_ordinal; + + // Apply primary key table filtering if `pk_table_name` was specified. + if (!pk_table_name.empty() && + foreign_key.referenced_table.table_id != pk_table_name) { + continue; + } + + // Verify catalog and schema scope match the foreign key definition. + if (foreign_key.referenced_table.project_id != catalog_name || + foreign_key.referenced_table.dataset_id != schema_name) { + continue; + } + + AppendForeignKeyRows(result_set, foreign_key, catalog_name, schema_name, + table_result.table_name, fk_ordinal); } } - // Match the query path ordering: PKTABLE_NAME, then key sequence. - std::stable_sort(result_set.rows.begin(), result_set.rows.end(), - [](DSRow const& a, DSRow const& b) { - if (a[2] != b[2]) return a[2] < b[2]; - DSValue seq_a = a[8]; - DSValue seq_b = b[8]; - return DSValueToInt(seq_a) < DSValueToInt(seq_b); - }); + + SortForeignKeyRows(result_set); + DSResults ds_results; ds_results.data_source_results = std::move(result_set); return ds_results; @@ -202,6 +304,7 @@ StatusRecordOr FetchForeignKeysFromTableMetadata( } // namespace +// Main entry point for `SQLForeignKeys`. odbc_internal::StatusRecordOr FetchForeignKeysFromDataSource( StatementHandle& stmt_handle, std::string const& pk_catalog_name, int pk_catalog_name_len, std::string const& pk_schema_name, @@ -210,7 +313,7 @@ odbc_internal::StatusRecordOr FetchForeignKeysFromDataSource( int fk_catalog_name_len, std::string const& fk_schema_name, int fk_schema_name_len, std::string const& fk_table_name, int fk_table_name_len) { - // Parameter validation. + // 1. Parameter Validation & Fallbacks. std::string catalog_name = (!pk_catalog_name.empty()) ? pk_catalog_name : fk_catalog_name; if (catalog_name.empty() || @@ -276,76 +379,18 @@ odbc_internal::StatusRecordOr FetchForeignKeysFromDataSource( stmt_handle.GetDiagnostics().AddStatusRecord(status_record); return status_record; } - // Fast path: the foreign key table is specified with an exact (non-pattern) - // name — read that table's constraints directly via the tables.get REST API - // instead of running an INFORMATION_SCHEMA query job. This covers both the - // FK-only and the PK+FK cases; the PK-only case cannot use it because the - // referencing tables are not known upfront. Names containing '%' fall back - // to the query below, which preserves the historical LIKE matching. + + // 2. Query Routing: + // If `fk_table_name` is non-empty and contains no wildcard '%' characters, + // use the single-table fast path. Otherwise, fall back to parallel dataset + // table enumeration. if (!fk_table_name.empty() && !absl::StrContains(fk_table_name, '%') && !absl::StrContains(pk_table_name, '%')) { return FetchForeignKeysFromTableMetadata( stmt_handle, catalog_name, schema_name, pk_table_name, fk_table_name); } - // Construct named query for foreign keys. - std::string foreign_keys_query(kBasicForeignKeysQueryPrefix); - foreign_keys_query - .append(" AND pk_catalog = @") // PrimaryKey catalog - .append(kNamedCatalogParam) - .append(" AND pk_dataset = @") // PrimaryKey dataset - .append(kNamedSchemaParam) - .append(" AND key_column_usage.table_catalog = @") // ForeignKey catalog - .append(kNamedCatalogParam) - .append(" AND key_column_usage.table_schema = @") // ForeignKey dataset - .append(kNamedSchemaParam); - if (!pk_table_name.empty()) { - foreign_keys_query.append(" AND pk_references.pk_table LIKE @"); - foreign_keys_query.append(kNamedPKTableParam); - } - if (!fk_table_name.empty()) { - foreign_keys_query.append(" AND key_column_usage.table_name LIKE @"); - foreign_keys_query.append(kNamedFKTableParam); - } - foreign_keys_query.append(" ").append(kBasicForeignKeysQuerySuffix); - // Construct named query params - std::map named_query_params; - named_query_params.insert({kNamedCatalogParam, catalog_name}); - named_query_params.insert({kNamedSchemaParam, schema_name}); - if (!pk_table_name.empty()) { - named_query_params.insert({kNamedPKTableParam, pk_table_name}); - } - if (!fk_table_name.empty()) { - named_query_params.insert({kNamedFKTableParam, fk_table_name}); - } - auto query_param_status = ConstructStringQueryParameters(named_query_params); - if (!query_param_status) { - LOG(ERROR) - << "FetchForeignKeysFromDataSource::ConstructStringQueryParameters:: " - << query_param_status.GetStatusRecord().message; - auto status_record = query_param_status.GetStatusRecord(); - stmt_handle.GetDiagnostics().AddStatusRecord(status_record); - return status_record; - } - // Construct post query request. - auto post_query_request_status = ConstructNamedParametersPostQueryRequest( - catalog_name, schema_name, foreign_keys_query, *query_param_status); - if (!post_query_request_status) { - LOG(ERROR) << "FetchForeignKeysFromDataSource::" - "ConstructNamedParametersPostQueryRequest:: " - << post_query_request_status.GetStatusRecord().message; - auto status_record = post_query_request_status.GetStatusRecord(); - stmt_handle.GetDiagnostics().AddStatusRecord(status_record); - return status_record; - } - // Fetch BQ Data using the post query request above. - auto status_record_or = FetchBQData(stmt_handle, *post_query_request_status); - if (!status_record_or) { - LOG(ERROR) << "FetchForeignKeysFromDataSource::FetchBQData:: " - << status_record_or.GetStatusRecord().message; - stmt_handle.GetDiagnostics().AddStatusRecord( - status_record_or.GetStatusRecord()); - } - return status_record_or; + return FetchForeignKeysByListingTables(stmt_handle, catalog_name, schema_name, + pk_table_name, fk_table_name); } } // namespace google::cloud::odbc_bq_driver_internal diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_primary_keys.cc b/google/cloud/odbc/bq_driver/internal/odbc_sql_primary_keys.cc index 7cb7657c21..99c081e318 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_sql_primary_keys.cc +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_primary_keys.cc @@ -24,27 +24,6 @@ using ::google::cloud::odbc_internal::SQLStates; using ::google::cloud::odbc_internal::StatusRecord; using ::google::cloud::odbc_internal::StatusRecordOr; -namespace { -std::string const kNamedCatalogParam = "catalog_name"; -std::string const kNamedSchemaParam = "schema_name"; -std::string const kNamedTableParam = "table_name"; - -std::string const kBasicPrimaryKeysQuery = - "SELECT kc.table_catalog," - " kc.table_schema," - " kc.table_name," - " kc.column_name," - " kc.ordinal_position," - " kc.constraint_name" - " FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE as kc" - " INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS as tc" - " ON kc.constraint_name = tc.constraint_name AND" - " kc.table_catalog = tc.table_catalog AND" - " kc.table_schema = tc.table_schema AND" - " kc.table_name = tc.table_name " - " WHERE tc.constraint_type = 'PRIMARY KEY'"; -} // namespace - StatusRecordOr CreateResultSetForPrimaryKeys( std::string const& catalog, std::string const& dataset, std::string const& table, TableFieldSchema const& field_schema, diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.cc b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.cc index 5a87bdaee3..b563b73c19 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.cc +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.cc @@ -23,7 +23,6 @@ namespace google::cloud::odbc_bq_driver_internal { using ::google::cloud::bigquery_v2_minimal_internal::ListFormatDataset; using ::google::cloud::bigquery_v2_minimal_internal::Project; -using ::google::cloud::bigquery_v2_minimal_internal::QueryParameter; using google::cloud::odbc_bigquery_client_interface::DatasetFilter; using google::cloud::odbc_bigquery_client_interface::MaxRetriesOption; using google::cloud::odbc_internal::SQLStates; @@ -32,11 +31,6 @@ using google::cloud::odbc_internal::StatusRecordOr; namespace { -std::string const kTableNameParam = "table_name"; -std::string const kTableTypeParam = "table_type"; -std::string const kBasicQuery = - "SELECT table_name, table_type FROM INFORMATION_SCHEMA.TABLES"; - std::string const kBaseTable = "BASE TABLE"; std::string const kTable = "TABLE"; std::string const kClone = "CLONE"; @@ -173,25 +167,6 @@ StatusRecordOr> GetFilteredDatasetIds( return dataset_ids; } -std::string ConstructTableNameWhereClause(std::string const& tables_filter, - SQLULEN metadata_id) { - if (metadata_id == SQL_TRUE) { - return "LOWER(table_name) = LOWER(@" + kTableNameParam + ")"; - } - if (tables_filter != "%") { - return "table_name LIKE @" + kTableNameParam; - } - return ""; -} - -std::string ConstructTableTypeWhereClause(std::string table_types_filter) { - Trim(table_types_filter); - if (table_types_filter != "%") { - return "table_type IN UNNEST (@" + kTableTypeParam + ")"; - } - return ""; -} - std::string ProcessTableTypes(std::string const& table_types_filter) { std::vector types = SplitTableTypes(table_types_filter); for (std::string& type : types) { @@ -214,44 +189,6 @@ std::vector ExtractColumnSchema( return col_schema; } -StatusRecordOr ConstructQuery( - std::string tables_filter, std::string const& table_types_filter, - SQLULEN metadata_id, std::vector& named_query_params) { - if (metadata_id == SQL_TRUE) { - RTrim(tables_filter); - } - std::string table_name_where_clause = - ConstructTableNameWhereClause(tables_filter, metadata_id); - std::string table_type_where_clause = - ConstructTableTypeWhereClause(table_types_filter); - if (!table_name_where_clause.empty()) { - auto query_param = - ConstructStringQueryParameter(kTableNameParam, tables_filter); - if (!query_param) { - return query_param.GetStatusRecord(); - } - named_query_params.push_back(*query_param); - } - if (!table_type_where_clause.empty()) { - std::vector table_types = SplitTableTypes(table_types_filter); - auto query_param = - ConstructStringArrayQueryParameter(kTableTypeParam, table_types); - if (!query_param) { - return query_param.GetStatusRecord(); - } - named_query_params.push_back(*query_param); - } - if (!table_name_where_clause.empty() && !table_type_where_clause.empty()) { - return kBasicQuery + " WHERE " + table_name_where_clause + " AND " + - table_type_where_clause; - } - if (!table_name_where_clause.empty() || !table_type_where_clause.empty()) { - return kBasicQuery + " WHERE " + table_name_where_clause + - table_type_where_clause; - } - return kBasicQuery; -} - std::vector AppendAdditionalProjectsIfMissing( ODBCBQClient& bq_client, SQLULEN metadata_id, std::vector base_projects, diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.h b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.h index 41ce56e93a..c88dd77503 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.h +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.h @@ -83,14 +83,6 @@ odbc_internal::StatusRecordOr> GetFilteredDatasetIds( std::optional LiteralFromOdbcPattern(std::string const& filter, SQLULEN metadata_id); -// Construct a query to INFORMATION_SCHEMA.TABLES table depending on input -// parameters. Populate 'named_query_params' with named parameters if needed. -odbc_internal::StatusRecordOr ConstructQuery( - std::string tables_filter, std::string const& table_types_filter, - SQLULEN metadata_id, - std::vector<::google::cloud::bigquery_v2_minimal_internal::QueryParameter>& - named_query_params); - // Return a list of table names and table types depending on input parameters. // Returns all tables if SQL_ATTR_METADATA_ID == SQL_FALSE and tables_filter == // "%" and table_types_filter == "%". Lists tables via the tables.list REST API diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables_test.cc b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables_test.cc index 7e8e31c31d..ae555da843 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables_test.cc +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables_test.cc @@ -19,7 +19,6 @@ namespace google::cloud::odbc_bq_driver_internal { -using ::google::cloud::bigquery_v2_minimal_internal::QueryParameter; using google::cloud::odbc_internal::SQLStates; using google::cloud::odbc_internal::StatusRecord; using google::cloud::odbc_testing_bq_driver_utils::CastToSQLCHAR; @@ -155,89 +154,6 @@ TEST(LiteralFromOdbcPattern, MetadataIdTrueIsAlwaysLiteral) { EXPECT_EQ(*literal_with_escapes, "my\\_dataset\\%"); } -TEST(ConstructQuery, ConstructWithTwoClausesMetadatafalse) { - std::vector named_query_params; - - auto query = - ConstructQuery("table-1", "BASE TABLE", SQL_FALSE, named_query_params); - - ASSERT_STATUS_RECORD_OK(query); - EXPECT_EQ( - *query, - "SELECT table_name, table_type FROM INFORMATION_SCHEMA.TABLES WHERE " - "table_name LIKE @table_name AND table_type IN UNNEST (@table_type)"); - EXPECT_EQ(2, named_query_params.size()); -} - -TEST(ConstructQuery, ConstructWithTwoClausesMetadatatrue) { - std::vector named_query_params; - - auto query = - ConstructQuery("table-1", "BASE TABLE", SQL_TRUE, named_query_params); - - ASSERT_STATUS_RECORD_OK(query); - EXPECT_EQ(*query, - "SELECT table_name, table_type FROM INFORMATION_SCHEMA.TABLES " - "WHERE LOWER(table_name) = LOWER(@table_name) AND table_type IN " - "UNNEST (@table_type)"); - EXPECT_EQ(2, named_query_params.size()); -} - -TEST(ConstructQuery, ConstructWithTableNameClauseMetadatafalse) { - std::vector named_query_params; - - auto query = ConstructQuery("table-1", " % ", SQL_FALSE, named_query_params); - - ASSERT_STATUS_RECORD_OK(query); - EXPECT_EQ(*query, - "SELECT table_name, table_type FROM INFORMATION_SCHEMA.TABLES " - "WHERE table_name LIKE @table_name"); - EXPECT_EQ(1, named_query_params.size()); -} - -TEST(ConstructQuery, ConstructWithTableNameClauseMetadatatrue) { - std::vector named_query_params; - - auto query = ConstructQuery("table-1", " % ", SQL_TRUE, named_query_params); - - ASSERT_STATUS_RECORD_OK(query); - EXPECT_EQ(*query, - "SELECT table_name, table_type FROM INFORMATION_SCHEMA.TABLES " - "WHERE LOWER(table_name) = LOWER(@table_name)"); - EXPECT_EQ(1, named_query_params.size()); -} - -TEST(ConstructQuery, ConstructWithTableTypeClause) { - std::vector named_query_params; - - auto query = ConstructQuery("%", " ' BASE TABLE ' , ' VIEW ' ", SQL_FALSE, - named_query_params); - - ASSERT_STATUS_RECORD_OK(query); - EXPECT_EQ(*query, - "SELECT table_name, table_type FROM INFORMATION_SCHEMA.TABLES " - "WHERE table_type IN UNNEST (@table_type)"); - EXPECT_EQ(1, named_query_params.size()); - EXPECT_EQ(2, named_query_params[0].parameter_value.array_values.size()); - EXPECT_EQ("BASE TABLE", - named_query_params[0].parameter_value.array_values[0].value); - EXPECT_EQ("VIEW", - named_query_params[0].parameter_value.array_values[1].value); -} - -TEST(ConstructQuery, ConstructWithTwoClausesEmptystrings) { - std::vector named_query_params; - - auto query = ConstructQuery("", "", SQL_FALSE, named_query_params); - - ASSERT_STATUS_RECORD_OK(query); - EXPECT_EQ( - *query, - "SELECT table_name, table_type FROM INFORMATION_SCHEMA.TABLES WHERE " - "table_name LIKE @table_name AND table_type IN UNNEST (@table_type)"); - EXPECT_EQ(2, named_query_params.size()); -} - TEST(CreateResultSetForProjects, CreateResultSetForProjects) { std::vector project_ids = {"id-1", "id-2"}; 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 a272f2f362..c0950948bc 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 @@ -1096,6 +1096,32 @@ TEST(CatalogTest, SQLForeignKeys_With_FkTableName) { EXPECT_EQ(Disconnect(conn), SQL_SUCCESS); } +// "%" forces the listing path:(It is used to test the below function) +// FetchForeignKeysByListingTables() +// → GetFilteredTables(..., "TABLE", ...) +// → FetchBQTableData() +// → Filter by kTableCustomer +// → Verify expected FK +TEST(CatalogTest, SQLForeignKeys_With_FkTablePattern) { + // Use an FK table pattern to exercise FetchForeignKeysByListingTables(). + auto conn = std::make_shared(); + + EXPECT_EQ(Connect(kDefaultConnectionString, conn, true), SQL_SUCCESS); + + CreateTableDirect(conn, kTableCustomerSchema); + CreateTableDirect(conn, kTableOrdersSchema); + CreateTableDirect(conn, kTableLinesSchema); + + // "%" forces the listing-tables path while the PK table filters + // the returned foreign keys. + auto foreign_keys = + Catalog::GetForeignKeys(conn, kDatasetName, kTableCustomer, "%"); + + // Verify that the expected FK metadata is returned. + VerifyRowWiseResults(foreign_keys, kCatalogForeignKeysExpected); + + EXPECT_EQ(Disconnect(conn), SQL_SUCCESS); +} #endif // BQ_DRIVER_INTEGRATION_TESTS struct ExpectedProcedureColumnValues {