diff --git a/google/cloud/odbc/bq_client_interface/datasets.cc b/google/cloud/odbc/bq_client_interface/datasets.cc index 834403ef66..7111f39dea 100644 --- a/google/cloud/odbc/bq_client_interface/datasets.cc +++ b/google/cloud/odbc/bq_client_interface/datasets.cc @@ -13,6 +13,7 @@ // limitations under the License. #include "google/cloud/odbc/bq_client_interface/datasets.h" +#include "google/cloud/odbc/bq_client_interface/utils.h" #include "google/cloud/odbc/internal/status_record_or.h" #include "google/cloud/bigquery/v2/minimal/internal/dataset_client.h" #include "absl/log/log.h" @@ -58,6 +59,7 @@ StatusRecordOr> ListAllDatasets( ListDatasetsRequest request; request.set_project_id(project_id); request.set_all_datasets(true); + request.set_max_results(kMetadataPageSize); LOG(INFO) << "ListAllDatasets:: Request body: " << request.DebugString(""); StreamRange datasets_response = dataset_client.ListDatasets(request, options); @@ -86,6 +88,7 @@ StatusRecordOr> FilterDatasets( request.set_project_id(project_id); request.set_all_datasets(dataset_filter.all); request.set_filter(dataset_filter.filter); + request.set_max_results(kMetadataPageSize); LOG(INFO) << "FilterDatasets:: Request body: " << request.DebugString(""); StreamRange datasets_response = diff --git a/google/cloud/odbc/bq_client_interface/projects.cc b/google/cloud/odbc/bq_client_interface/projects.cc index d21cdc2422..9a11afb6f6 100644 --- a/google/cloud/odbc/bq_client_interface/projects.cc +++ b/google/cloud/odbc/bq_client_interface/projects.cc @@ -14,6 +14,7 @@ #include "google/cloud/odbc/bq_client_interface/projects.h" #include "google/cloud/odbc/bq_client_interface/datasets.h" +#include "google/cloud/odbc/bq_client_interface/utils.h" #include "google/cloud/odbc/internal/sql_state_constants.h" #include "google/cloud/odbc/internal/status_record_or.h" #include "google/cloud/resourcemanager/v3/projects.pb.h" @@ -92,6 +93,7 @@ bool IsProjectBQEnabled(std::string const& bq_project_id, StatusRecordOr> ListAllProjects( ProjectClient& project_client, Options const& options) { ListProjectsRequest request; + request.set_max_results(kMetadataPageSize); StreamRange projects_response = project_client.ListProjects(request, options); @@ -112,6 +114,7 @@ StatusRecordOr GetProject(ProjectClient& project_client, std::string const& project_id, Options const& options) { ListProjectsRequest request; + request.set_max_results(kMetadataPageSize); StreamRange projects_response = project_client.ListProjects(request, options); @@ -252,6 +255,7 @@ StatusRecordOr> FilterProjects( ProjectClient& project_client, std::vector const& project_ids, Options const& options) { ListProjectsRequest request; + request.set_max_results(kMetadataPageSize); StreamRange projects_response = project_client.ListProjects(request, options); diff --git a/google/cloud/odbc/bq_client_interface/tables.cc b/google/cloud/odbc/bq_client_interface/tables.cc index d51903cb0c..654881b8f9 100644 --- a/google/cloud/odbc/bq_client_interface/tables.cc +++ b/google/cloud/odbc/bq_client_interface/tables.cc @@ -69,6 +69,7 @@ StatusRecordOr> ListAllTables( ListTablesRequest request; request.set_project_id(project_id); request.set_dataset_id(dataset_id); + request.set_max_results(kMetadataPageSize); LOG(INFO) << "ListAllTables:: Request body: " << request.DebugString(""); StreamRange tables_response = diff --git a/google/cloud/odbc/bq_client_interface/utils.h b/google/cloud/odbc/bq_client_interface/utils.h index 60ae235e9c..a7d30483e5 100644 --- a/google/cloud/odbc/bq_client_interface/utils.h +++ b/google/cloud/odbc/bq_client_interface/utils.h @@ -17,6 +17,7 @@ #include "absl/log/log.h" #include +#include #include #include #include @@ -27,6 +28,11 @@ struct MaxRetriesOption { using Type = int; }; +// Page size requested from paginated metadata list APIs (projects.list, +// datasets.list, tables.list). When maxResults is unset the BigQuery REST API +// returns small pages (50), which multiplies sequential HTTP round trips +constexpr std::int32_t kMetadataPageSize = 1000; + // URL-encodes a value for safe use as a single path segment. inline std::string UrlEncodeSegment(std::string const& value) { std::ostringstream os; 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 88e7f6db84..173366e452 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.cc +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.cc @@ -17,6 +17,7 @@ #include "google/cloud/odbc/bq_driver/internal/trace_utils.h" #include "google/cloud/odbc/bq_driver/internal/utils.h" #include +#include namespace google::cloud::odbc_bq_driver_internal { @@ -54,6 +55,37 @@ std::string NormalizeRestTableType(std::string type) { } } // namespace +std::optional LiteralFromOdbcPattern(std::string const& filter, + SQLULEN metadata_id) { + if (metadata_id == SQL_TRUE) { + std::string literal = filter; + RTrim(literal); + return literal; + } + if (filter.empty()) { + return std::nullopt; + } + std::string literal; + literal.reserve(filter.size()); + for (size_t i = 0; i < filter.size(); ++i) { + char c = filter[i]; + if (c == '\\') { + // Escape: the next char is a literal (consume both). A lone trailing + // backslash is dropped, matching CastOdbcRegexToCppRegex(). + if (i + 1 < filter.size()) { + literal.push_back(filter[i + 1]); + ++i; + } + } else if (c == '%' || c == '_') { + // A genuine wildcard: the filter must be expanded by enumeration. + return std::nullopt; + } else { + literal.push_back(c); + } + } + return literal; +} + StatusRecord ValidateInputParameters( const SQLCHAR* catalog_name, SQLSMALLINT catalog_name_len, const SQLCHAR* schema_name, SQLSMALLINT schema_name_len, @@ -116,9 +148,19 @@ StatusRecordOr> GetFilteredDatasetIds( StatusRecordOr> datasets = bq_client.FilterDatasets(project_id, filter, options); if (!datasets) { - LOG(ERROR) << "GetFilteredDatasetIds::FilterDatasets:: " - << datasets.GetStatusRecord().message; - return datasets.GetStatusRecord(); + auto const& status = datasets.GetStatusRecord(); + // A catalog passed as a literal (no wildcard) is used directly without + // first confirming it via projects.list, so an unknown or inaccessible + // project surfaces here. Treat "not found" as an empty catalog rather than + // failing the whole call: the pattern-based path simply would not have + // matched that project. Mirrors GetFilteredTables' 404 handling. + if (status.native_error_code == 404) { + LOG(WARNING) << "GetFilteredDatasetIds:: Skipping project not found: '" + << project_id << "': " << status.message; + return std::vector{}; + } + LOG(ERROR) << "GetFilteredDatasetIds::FilterDatasets:: " << status.message; + return status; } for (auto const& dataset : *datasets) { if ((!metadata_id && datasets_filter == "%") || @@ -416,12 +458,15 @@ StatusRecordOr GetResultSetForTables( std::string const& project_filter, std::string const& dataset_filter, std::string const& table_filter, std::string const& table_type_filter, SQLULEN metadata_id) { - // When SQL_ATTR_METADATA_ID is true, the catalog argument is an exact project - // identifier (not a pattern). Skip enumerating every accessible project via - // projects.list and use the name directly. + // When the catalog argument is an exact project identifier -- either because + // SQL_ATTR_METADATA_ID is true, or because the LIKE pattern contains no + // wildcard -- skip enumerating every accessible project via projects.list and + // use the name directly. projects.list has no server-side filter, so it + // returns every project the caller can see just to select one by name. std::vector project_list; - if (metadata_id == SQL_TRUE) { - project_list = {project_filter}; + if (auto project_literal = + LiteralFromOdbcPattern(project_filter, metadata_id)) { + project_list = {std::move(*project_literal)}; } else { auto projects_status_record_or = GetFilteredProjectIds(bq_client, project_filter, metadata_id); @@ -444,23 +489,51 @@ StatusRecordOr GetResultSetForTables( }; std::vector tasks; - for (auto const& project_id : project_list) { - // When SQL_ATTR_METADATA_ID is true, the schema argument is an exact - // dataset identifier (not a pattern). Skip listing every dataset in the - // project via datasets.list - if (metadata_id == SQL_TRUE) { - tasks.push_back({project_id, dataset_filter}); - continue; + std::shared_ptr trace_option = TraceOptions::GetTraceOption(); + int max_threads = trace_option->max_threads; + + // The schema argument is an exact dataset identifier when + // SQL_ATTR_METADATA_ID is true, or when the LIKE pattern contains no + // wildcard. Computed once: it does not depend on the project. + auto const dataset_literal = + LiteralFromOdbcPattern(dataset_filter, metadata_id); + + if (dataset_literal) { + // For an exact dataset identifier, skip listing every dataset in the + // project via datasets.list and address the dataset directly. + for (auto const& project_id : project_list) { + tasks.push_back({project_id, *dataset_literal}); } - auto datasets_status_record_or = GetFilteredDatasetIds( - bq_client, project_id, dataset_filter, metadata_id); - if (!datasets_status_record_or) { - LOG(ERROR) << "GetResultSetForTables::GetFilteredDatasetIds:: " - << datasets_status_record_or.GetStatusRecord().message; - return datasets_status_record_or.GetStatusRecord(); + } else { + // Enumerate datasets for each project in parallel. A serial loop here + // issues one datasets.list REST call per project, which dominates + // SQLTables latency when the catalog pattern matches many projects + // (e.g. catalog = "%"). + auto dataset_task = [&](std::string const& project_id) + -> StatusRecordOr> { + auto datasets_status_record_or = GetFilteredDatasetIds( + bq_client, project_id, dataset_filter, metadata_id); + if (!datasets_status_record_or) { + LOG(ERROR) << "GetResultSetForTables::GetFilteredDatasetIds:: " + << datasets_status_record_or.GetStatusRecord().message; + return datasets_status_record_or.GetStatusRecord(); + } + std::vector project_tasks; + project_tasks.reserve(datasets_status_record_or->size()); + for (auto& dataset_id : *datasets_status_record_or) { + project_tasks.push_back({project_id, std::move(dataset_id)}); + } + return project_tasks; + }; + auto dataset_results_or = + ExecuteParallelTasks>( + max_threads, project_list, dataset_task); + if (!dataset_results_or) { + return dataset_results_or.GetStatusRecord(); } - for (auto const& dataset_id : *datasets_status_record_or) { - tasks.push_back({project_id, dataset_id}); + for (auto& project_tasks : *dataset_results_or) { + tasks.insert(tasks.end(), std::make_move_iterator(project_tasks.begin()), + std::make_move_iterator(project_tasks.end())); } } @@ -491,8 +564,6 @@ StatusRecordOr GetResultSetForTables( }; // 3. Execute tasks using the generic utility - std::shared_ptr trace_option = TraceOptions::GetTraceOption(); - int max_threads = trace_option->max_threads; auto parallel_results_or = ExecuteParallelTasks( max_threads, tasks, parallel_func); 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 ae0a45e756..41ce56e93a 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.h +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_tables.h @@ -18,6 +18,7 @@ #include "google/cloud/odbc/bq_driver/internal/odbc_sql_execute_utils.h" #include "google/cloud/odbc/bq_driver/internal/odbc_stmt_handle.h" #include "google/cloud/odbc/internal/status_record_or.h" +#include namespace google::cloud::odbc_bq_driver_internal { @@ -68,6 +69,20 @@ odbc_internal::StatusRecordOr> GetFilteredDatasetIds( ODBCBQClient& bq_client, std::string const& project_id, std::string const& datasets_filter, SQLULEN metadata_id); +// Returns the exact identifier a catalog/schema filter denotes, or nullopt when +// the filter is a genuine search pattern that must be expanded by listing. +// +// When SQL_ATTR_METADATA_ID is SQL_TRUE the argument is an exact identifier and +// is returned verbatim (right-trimmed). Otherwise it is an ODBC LIKE pattern +// and is a literal only when it has no unescaped '%'/'_' metacharacter, in +// which case the unescaped form is returned. Lets SQLTables skip a +// projects.list / datasets.list round trip when a concrete project or dataset +// name was passed -- projects.list in particular has no server-side filter and +// enumerates every visible project. Empty patterns return nullopt to preserve +// the existing (match-nothing) listing path. +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( 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 d29c987c3c..c69d460c48 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 @@ -106,6 +106,49 @@ TEST(ValidateInputParameters, SuccessAllnullsMetadatafalse) { EXPECT_TRUE(status.ok()); } +TEST(LiteralFromOdbcPattern, PlainNameIsLiteral) { + auto literal = LiteralFromOdbcPattern("kirltest", SQL_FALSE); + ASSERT_TRUE(literal.has_value()); + EXPECT_EQ(*literal, "kirltest"); +} + +TEST(LiteralFromOdbcPattern, HyphensAreLiteral) { + auto literal = LiteralFromOdbcPattern("bigquery-devtools-drivers", SQL_FALSE); + ASSERT_TRUE(literal.has_value()); + EXPECT_EQ(*literal, "bigquery-devtools-drivers"); +} + +TEST(LiteralFromOdbcPattern, PercentWildcardIsPattern) { + EXPECT_FALSE(LiteralFromOdbcPattern("%", SQL_FALSE).has_value()); + EXPECT_FALSE(LiteralFromOdbcPattern("%timestamp%", SQL_FALSE).has_value()); +} + +TEST(LiteralFromOdbcPattern, UnderscoreWildcardIsPattern) { + // '_' is an ODBC single-character wildcard, so a name containing it must be + // expanded by listing rather than used as an exact identifier. + EXPECT_FALSE( + LiteralFromOdbcPattern("ODBC_TEST_DATASET", SQL_FALSE).has_value()); +} + +TEST(LiteralFromOdbcPattern, EscapedWildcardsAreLiteral) { + auto literal = LiteralFromOdbcPattern("my\\_dataset\\%", SQL_FALSE); + ASSERT_TRUE(literal.has_value()); + EXPECT_EQ(*literal, "my_dataset%"); +} + +TEST(LiteralFromOdbcPattern, EmptyPatternIsNotLiteral) { + // Preserve the prior (match-nothing) listing path for empty filters. + EXPECT_FALSE(LiteralFromOdbcPattern("", SQL_FALSE).has_value()); +} + +TEST(LiteralFromOdbcPattern, MetadataIdTrueIsAlwaysLiteral) { + // With SQL_ATTR_METADATA_ID true, arguments are exact identifiers even when + // they contain characters that would otherwise be wildcards. + auto literal = LiteralFromOdbcPattern("ODBC_TEST_DATASET ", SQL_TRUE); + ASSERT_TRUE(literal.has_value()); + EXPECT_EQ(*literal, "ODBC_TEST_DATASET"); +} + TEST(ConstructQuery, ConstructWithTwoClausesMetadatafalse) { std::vector named_query_params; diff --git a/google/cloud/odbc/bq_driver/internal/trace_utils.cc b/google/cloud/odbc/bq_driver/internal/trace_utils.cc index 9d9ee8aded..75e6090a2c 100644 --- a/google/cloud/odbc/bq_driver/internal/trace_utils.cc +++ b/google/cloud/odbc/bq_driver/internal/trace_utils.cc @@ -283,7 +283,9 @@ TraceOptions::CreateTraceOptionsFile( int log_level = 0; int log_file_count; int log_file_size; - std::uint32_t max_threads = 8; // default max_threads + // Must track kDefaultMaxThreads: a literal here silently overrides it + // whenever the driver config key has no MaxThreads value. + std::uint32_t max_threads = kDefaultMaxThreads; for (auto const& s : trace_sections) { if (s.first == kLogLevel && !s.second.empty()) { log_level = std::strtol(s.second.c_str(), nullptr, 10); diff --git a/google/cloud/odbc/bq_driver/internal/trace_utils.h b/google/cloud/odbc/bq_driver/internal/trace_utils.h index 6124b12495..76f9847d30 100644 --- a/google/cloud/odbc/bq_driver/internal/trace_utils.h +++ b/google/cloud/odbc/bq_driver/internal/trace_utils.h @@ -41,7 +41,13 @@ inline std::string const kLogPath = "LogPath"; inline std::string const kLogFileCount = "LogFileCount"; inline std::string const kLogFileSize = "LogFileSize"; inline std::string const kMaxThreadsParam = "MaxThreads"; -inline std::uint32_t const kDefaultMaxThreads = 8; +// Concurrency for the catalog metadata fan-out (SQLTables / SQLColumns issue +// one datasets.list / tables.list REST call per project / dataset through +// ExecuteParallelTasks). These threads spend essentially all their time waiting +// on network I/O, not CPU, so full-catalog enumeration wall time is +// ceil(calls / kDefaultMaxThreads) * per_call_latency and the previous value of +// 8 bounded throughput well below what the service will serve. +inline std::uint32_t const kDefaultMaxThreads = 16; inline std::string const kDefaultMaxFiles = "50"; inline std::string const kDefaultMaxSize = "2000"; diff --git a/google/cloud/odbc/bq_driver/internal/utils.cc b/google/cloud/odbc/bq_driver/internal/utils.cc index b9fb984502..b36366020e 100644 --- a/google/cloud/odbc/bq_driver/internal/utils.cc +++ b/google/cloud/odbc/bq_driver/internal/utils.cc @@ -979,6 +979,19 @@ std::string CastOdbcRegexToCppRegex(std::string const& str) { return result; } +std::string EscapeOdbcPattern(std::string const& identifier) { + std::string escaped; + // Worst case: every character needs a backslash. + escaped.reserve(identifier.size() * 2); + for (char c : identifier) { + if (c == '%' || c == '_' || c == '\\') { + escaped.push_back('\\'); + } + escaped.push_back(c); + } + return escaped; +} + std::unique_ptr BuildRegex(std::string filter_pattern, SQLULEN metadata_id) { if (metadata_id == SQL_TRUE) { diff --git a/google/cloud/odbc/bq_driver/internal/utils.h b/google/cloud/odbc/bq_driver/internal/utils.h index a557ce6725..70214a0ec9 100644 --- a/google/cloud/odbc/bq_driver/internal/utils.h +++ b/google/cloud/odbc/bq_driver/internal/utils.h @@ -131,6 +131,11 @@ odbc_internal::StatusRecordOr> ExecuteParallelTasks( } }; + // A max_threads of 0 (a misconfigured MaxThreads) would make the slot-wait + // loop below spin forever: the condition 0 >= 0 holds while there is no + // future to drain. Treat it as serial execution. + if (max_threads == 0) max_threads = 1; + for (auto const& input : inputs) { // If we have hit the thread limit, wait for at least one thread to finish while (active_futures.size() >= max_threads) { @@ -367,6 +372,17 @@ std::string GetDefaultPemFile(); // on some hosts (e.g. SAP HANA with libstdc++/libc++). std::string CastOdbcRegexToCppRegex(std::string const& str); +// Escapes the ODBC LIKE metacharacters in `identifier` -- '%', '_', and the +// escape character '\' itself -- so that it matches only itself when the result +// is interpreted as a search pattern. +// +// Required wherever an exact identifier that did not come from the application +// (e.g. the DSN's configured default dataset) is substituted into an argument +// the ODBC spec defines as a pattern. BigQuery dataset and table names +// routinely contain '_', which would otherwise act as a single-character +// wildcard and match objects the user never configured. +std::string EscapeOdbcPattern(std::string const& identifier); + std::vector SplitTableTypes(std::string const& table_types); std::unique_ptr BuildRegex(std::string filter_pattern, diff --git a/google/cloud/odbc/bq_driver/internal/utils_test.cc b/google/cloud/odbc/bq_driver/internal/utils_test.cc index f134d0a0ca..49351bf86d 100644 --- a/google/cloud/odbc/bq_driver/internal/utils_test.cc +++ b/google/cloud/odbc/bq_driver/internal/utils_test.cc @@ -27,6 +27,7 @@ using ::google::cloud::odbc_internal::SQLStates; using ::google::cloud::odbc_internal::StatusRecord; using ::google::cloud::odbc_internal::StatusRecordOr; using google::cloud::odbc_testing_utils::StatusRecordIs; +using ::testing::ElementsAre; using ::testing::HasSubstr; using ::testing::IsEmpty; using ::testing::UnorderedElementsAre; @@ -1016,4 +1017,47 @@ TEST(ExecuteParallelTasksTest, RespectsSlidingWindow) { EXPECT_GE(duration, (task_count / max_threads) * min_sleep_ms); } + +TEST(ExecuteParallelTasksTest, ZeroMaxThreadsRunsSerially) { + // A misconfigured MaxThreads of 0 must not hang; it degrades to serial + // execution. + std::vector inputs = {1, 2, 3}; + auto task = [](int input) -> StatusRecordOr { return input * 2; }; + + auto result = ExecuteParallelTasks(0, inputs, task); + + ASSERT_STATUS_RECORD_OK(result); + EXPECT_THAT(*result, ElementsAre(2, 4, 6)); +} + + +TEST(EscapeOdbcPattern, PlainNameUnchanged) { + EXPECT_EQ(EscapeOdbcPattern("kirltest"), "kirltest"); +} + +TEST(EscapeOdbcPattern, EscapesUnderscores) { + // '_' is an ODBC single-character wildcard; a configured dataset name + // containing it must match only itself. + EXPECT_EQ(EscapeOdbcPattern("ODBC_TEST_DATASET"), + "ODBC\\_TEST\\_DATASET"); +} + +TEST(EscapeOdbcPattern, EscapesPercentAndBackslash) { + EXPECT_EQ(EscapeOdbcPattern("a%b"), "a\\%b"); + EXPECT_EQ(EscapeOdbcPattern("a\\b"), "a\\\\b"); +} + +TEST(EscapeOdbcPattern, EscapedNameMatchesOnlyItself) { + // The escaped form must compile to a regex that matches the literal name and + // rejects the wildcard expansions the unescaped form would have accepted. + auto regex = BuildRegex(EscapeOdbcPattern("ODBC_TEST_DATASET"), SQL_FALSE); + EXPECT_TRUE(re2::RE2::FullMatch("ODBC_TEST_DATASET", *regex)); + EXPECT_FALSE(re2::RE2::FullMatch("ODBCxTESTyDATASET", *regex)); + EXPECT_FALSE(re2::RE2::FullMatch("ODBC-TEST-DATASET", *regex)); + + // Without escaping, '_' acts as a wildcard -- the behaviour being fixed. + auto unescaped = BuildRegex("ODBC_TEST_DATASET", SQL_FALSE); + EXPECT_TRUE(re2::RE2::FullMatch("ODBCxTESTyDATASET", *unescaped)); +} + } // namespace google::cloud::odbc_bq_driver_internal diff --git a/google/cloud/odbc/bq_driver/odbc_driver_metadata.cc b/google/cloud/odbc/bq_driver/odbc_driver_metadata.cc index 5d17ce2085..03c27915bc 100644 --- a/google/cloud/odbc/bq_driver/odbc_driver_metadata.cc +++ b/google/cloud/odbc/bq_driver/odbc_driver_metadata.cc @@ -32,6 +32,7 @@ using google::cloud::odbc_bq_driver_internal::ConnectionHandle; using google::cloud::odbc_bq_driver_internal::CreateResultSetForTableTypes; using google::cloud::odbc_bq_driver_internal::DescriptorHandle; using google::cloud::odbc_bq_driver_internal::DescriptorType; +using google::cloud::odbc_bq_driver_internal::EscapeOdbcPattern; using google::cloud::odbc_bq_driver_internal::DSResults; using google::cloud::odbc_bq_driver_internal::FetchBQSQLProceduresData; using google::cloud::odbc_bq_driver_internal::FetchBQTablesData; @@ -457,7 +458,14 @@ SQLRETURN SQLTablesInternal(SQLHSTMT stmt_handle, SQLCHAR* catalog_name, if (!metadata_id && dataset_filter == kMatchAll) { auto const dsn = conn_handle.GetDsn(); if (dsn.filter_tables_on_default_dataset && !dsn.default_dataset.empty()) { - dataset_filter = dsn.default_dataset; + // The configured default dataset is an exact identifier, not something + // the application asked us to pattern-match, so escape any LIKE + // metacharacters in it. Without this a name like "ODBC_TEST_DATASET" + // would treat each '_' as a single-character wildcard: it would report + // tables from datasets that were never configured (e.g. + // "ODBCxTESTyDATASET"), and would force a full datasets.list plus + // client-side matching instead of addressing the dataset directly. + dataset_filter = EscapeOdbcPattern(dsn.default_dataset); } } if (!conn_handle.IsConnected()) { diff --git a/google/cloud/odbc/bq_driver/odbc_windows.h b/google/cloud/odbc/bq_driver/odbc_windows.h index 16a565904c..ebfac96cb1 100644 --- a/google/cloud/odbc/bq_driver/odbc_windows.h +++ b/google/cloud/odbc/bq_driver/odbc_windows.h @@ -73,7 +73,9 @@ std::string const kDefaultLargeDatasetName = "_odbc_temp_tables"; std::string const kDefaultRowsPerBlock = "100000"; std::string const kDefaultStringLength = "16384"; std::string const kDefaultTempExpiration = "3600000"; -std::string const kDefaultMaxThreads = "8"; +// Keep in sync with odbc_bq_driver_internal::kDefaultMaxThreads; this is the +// value pre-filled in the Windows DSN setup dialog. +std::string const kDefaultMaxThreads = "32"; std::string const kDefaultMaxRetries = "6"; } // namespace google::cloud::odbc_bq_driver