From 6449f7f633b09ece4092f4c2d003c36bb869e8f6 Mon Sep 17 00:00:00 2001 From: Anshu6250 Date: Tue, 18 Aug 2026 10:40:35 +0530 Subject: [PATCH 1/3] fix(bq_driver): fix Unsupported arrow data type (0) crash for INTERVAL types in HTAPI --- .../internal/odbc_sql_execute_utils.cc | 75 +++++++++++++++++++ .../examples/catalog_performance_example.cc | 19 +++-- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/google/cloud/odbc/bq_driver/internal/odbc_sql_execute_utils.cc b/google/cloud/odbc/bq_driver/internal/odbc_sql_execute_utils.cc index e304737658..9f9ba23355 100644 --- a/google/cloud/odbc/bq_driver/internal/odbc_sql_execute_utils.cc +++ b/google/cloud/odbc/bq_driver/internal/odbc_sql_execute_utils.cc @@ -321,6 +321,11 @@ StatusRecordOr> GetArrowSchema( case arrow::Type::DECIMAL256: col_schema.col_type = BQDataType::kBigNumeric; break; + case arrow::Type::INTERVAL_MONTHS: + case arrow::Type::INTERVAL_DAY_TIME: + case arrow::Type::INTERVAL_MONTH_DAY_NANO: + col_schema.col_type = BQDataType::kInterval; + break; case arrow::Type::LIST: // For other datatypes within an array, we don't have any special // handling. Setting 'is_mode_repeated' is enough @@ -574,6 +579,76 @@ StatusRecord ProcessRecordBatch( } break; } + case arrow::Type::INTERVAL_MONTHS: { + auto arr = std::static_pointer_cast(column); + for (int64_t row = 0; row < num_rows; ++row) { + if (arr->IsNull(row)) { + result_set.rows[row][col_i] = kNullValue; + } else { + int32_t m = arr->Value(row); + char buf[64]; + snprintf(buf, sizeof(buf), "%d-%d 0 0:0:0", m / 12, m % 12); + StringToDSValue(std::string(buf), result_set.rows[row][col_i]); + } + } + break; + } + case arrow::Type::INTERVAL_DAY_TIME: { + auto arr = + std::static_pointer_cast(column); + for (int64_t row = 0; row < num_rows; ++row) { + if (arr->IsNull(row)) { + result_set.rows[row][col_i] = kNullValue; + } else { + auto val = arr->Value(row); + int32_t days = val.days; + int64_t total_sec = val.milliseconds / 1000; + int32_t fraction = (val.milliseconds % 1000) * 1000000; + int32_t hours = total_sec / 3600; + int32_t minutes = (total_sec / 60) % 60; + int32_t seconds = total_sec % 60; + char buf[64]; + if (fraction > 0) { + snprintf(buf, sizeof(buf), "0-0 %d %d:%d:%d.%09d", days, hours, + minutes, seconds, fraction); + } else { + snprintf(buf, sizeof(buf), "0-0 %d %d:%d:%d", days, hours, + minutes, seconds); + } + StringToDSValue(std::string(buf), result_set.rows[row][col_i]); + } + } + break; + } + case arrow::Type::INTERVAL_MONTH_DAY_NANO: { + auto arr = + std::static_pointer_cast(column); + for (int64_t row = 0; row < num_rows; ++row) { + if (arr->IsNull(row)) { + result_set.rows[row][col_i] = kNullValue; + } else { + auto val = arr->Value(row); + int32_t years = val.months / 12; + int32_t months = val.months % 12; + int32_t days = val.days; + int64_t total_sec = val.nanoseconds / 1000000000LL; + int32_t nanos = val.nanoseconds % 1000000000LL; + int32_t hours = total_sec / 3600; + int32_t minutes = (total_sec / 60) % 60; + int32_t seconds = total_sec % 60; + char buf[128]; + if (nanos > 0) { + snprintf(buf, sizeof(buf), "%d-%d %d %d:%d:%d.%09d", years, + months, days, hours, minutes, seconds, nanos); + } else { + snprintf(buf, sizeof(buf), "%d-%d %d %d:%d:%d", years, months, + days, hours, minutes, seconds); + } + StringToDSValue(std::string(buf), result_set.rows[row][col_i]); + } + } + break; + } // For complex types, we fall back to the existing logic but apply it // column-wise. We still avoid the GetScalar() overhead where possible, // but use ToString() to maintain compatibility with the existing parsing diff --git a/google/cloud/odbc/integration_tests/odbc_driver_tests/examples/catalog_performance_example.cc b/google/cloud/odbc/integration_tests/odbc_driver_tests/examples/catalog_performance_example.cc index 3e442a03a1..1f2229b864 100644 --- a/google/cloud/odbc/integration_tests/odbc_driver_tests/examples/catalog_performance_example.cc +++ b/google/cloud/odbc/integration_tests/odbc_driver_tests/examples/catalog_performance_example.cc @@ -412,16 +412,15 @@ INSTANTIATE_TEST_SUITE_P( "CAST(nyc311.closed_date AS STRING) AS V22 FROM " "`bigquery-public-data.new_york_311.311_service_requests` AS " "nyc311 " - "LIMIT 1000000;") - // TODO: Re-enable this benchmark once HTAPI Arrow supports all data - // types. Currently SQLExecDirect fails with: - // "[Google][ODBC BigQuery Driver] Internal Error: Unsupported arrow - // data type (0)" - // , std::make_tuple("AllDataTypes_2", - // "SELECT * FROM " - // "`bigquery-devtools-drivers.DATATYPERANGETEST." - // "AllDataTypes_2` LIMIT 100000") - ), + "LIMIT 1000000;"), + std::make_tuple("AllDataTypes_2", + "SELECT * FROM " + "`bigquery-devtools-drivers.DATATYPERANGETEST." + "AllDataTypes_2` LIMIT 1000000"), + std::make_tuple("RangeIntervalTestTable_2", + "SELECT * FROM " + "`bigquery-devtools-drivers.DATATYPERANGETEST." + "RangeIntervalTestTable_2` LIMIT 1000000")), [](::testing::TestParamInfo const& info) { return std::get<0>(info.param); }); From a0856087e876df5525dea1bf6027415270bceb15 Mon Sep 17 00:00:00 2001 From: Sachin Purohit Date: Wed, 26 Aug 2026 11:23:49 +0000 Subject: [PATCH 2/3] test: enabling HTAPI for interval tests --- .../data_translation_test.cc | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/google/cloud/odbc/integration_tests/odbc_driver_tests/data_translation_test.cc b/google/cloud/odbc/integration_tests/odbc_driver_tests/data_translation_test.cc index 0042cfd779..82631a9e55 100644 --- a/google/cloud/odbc/integration_tests/odbc_driver_tests/data_translation_test.cc +++ b/google/cloud/odbc/integration_tests/odbc_driver_tests/data_translation_test.cc @@ -2972,44 +2972,63 @@ void IntervalTestRunner( std::string const& table_name, std::vector const& interval_data, std::function, std::string const&)> const& - TestTranslation) { + TestTranslation, + std::string const& connection_string = kDefaultConnectionString) { auto conn = std::make_shared(); Table table(table_name); // Create Table - EXPECT_EQ(Connect(kDefaultConnectionString, conn), SQL_SUCCESS); + EXPECT_EQ(Connect(connection_string, conn), SQL_SUCCESS); table.CreateWithPrepare(conn, "(index INT64, IntervalField INTERVAL)"); EXPECT_EQ(Disconnect(conn), SQL_SUCCESS); // Insert data to read - EXPECT_EQ(Connect(kDefaultConnectionString, conn), SQL_SUCCESS); + EXPECT_EQ(Connect(connection_string, conn), SQL_SUCCESS); table.InsertIntervalData(conn, interval_data); EXPECT_EQ(Disconnect(conn), SQL_SUCCESS); // Read data - EXPECT_EQ(Connect(kDefaultConnectionString, conn), SQL_SUCCESS); + EXPECT_EQ(Connect(connection_string, conn), SQL_SUCCESS); std::string qry = "SELECT IntervalField FROM " + table_name + " ORDER BY index;"; TestTranslation(conn, qry); EXPECT_EQ(Disconnect(conn), SQL_SUCCESS); // Drop table - EXPECT_EQ(Connect(kDefaultConnectionString, conn), SQL_SUCCESS); + EXPECT_EQ(Connect(connection_string, conn), SQL_SUCCESS); table.DropWithPrepare(conn); EXPECT_EQ(Disconnect(conn), SQL_SUCCESS); } -TEST(DataTranslationTest, From_Interval_Year_Month) { - auto const table_name = - kDatasetWithTablePrefix + "ODBC_DATA_TRANSLATION_SQL_INTERVAL_YEAR_MONTH"; +class IntervalDataTranslationTest : public ::testing::TestWithParam {}; + +TEST_P(IntervalDataTranslationTest, From_Interval_Year_Month) { + bool is_htapi = GetParam(); + std::string connection_string = kDefaultConnectionString; + if (is_htapi) { + connection_string += + ";AllowHtapiForLargeResults=1;HTAPI_ActivationThreshold=0;"; + } else { + connection_string += ";AllowHtapiForLargeResults=0;"; + } + auto const table_name = kDatasetWithTablePrefix + + "ODBC_DATA_TRANSLATION_SQL_INTERVAL_YEAR_MONTH" + + (is_htapi ? "_HTAPI" : "_REST"); std::vector interval_data; for (auto const& test_data : kConversionYearMonthIntervalTestData) { interval_data.push_back(test_data.interval_value); } IntervalTestRunner(table_name, interval_data, - TestTranslationFromIntervalYearMonth); + TestTranslationFromIntervalYearMonth, connection_string); } +INSTANTIATE_TEST_SUITE_P(HtapiEnabled, IntervalDataTranslationTest, + ::testing::Values(false, true), + [](::testing::TestParamInfo const& info) { + return info.param ? "HTAPI_Enabled" + : "HTAPI_Disabled"; + }); + std::vector GetInputValuesToString(std::string column_name, StdAllTypesRows input_data) { std::vector input_values; From 596164091aa8d3275526d9f0cf639011cc434df3 Mon Sep 17 00:00:00 2001 From: Sachin Purohit Date: Wed, 26 Aug 2026 12:32:52 +0000 Subject: [PATCH 3/3] perf: enhanced perf pipeline --- .github/workflows/test-runner.yml | 54 +++++++-- .../examples/catalog_performance_example.cc | 113 ++++++++++++------ 2 files changed, 121 insertions(+), 46 deletions(-) diff --git a/.github/workflows/test-runner.yml b/.github/workflows/test-runner.yml index a53c94b8ba..b6ed114604 100644 --- a/.github/workflows/test-runner.yml +++ b/.github/workflows/test-runner.yml @@ -188,28 +188,47 @@ jobs: if not os.path.exists(filepath): return {} - pattern = re.compile(r'\[\s+OK\s+\]\s+(\S+)\s+\(([^)]+)\)') + ok_pattern = re.compile(r'\[\s+OK\s+\]\s+(\S+)\s+\(([^)]+)\)') + metric_pattern = re.compile(r'\[\s+METRIC\s+\]\s+(\S+)\s+\(([^)]+)\):\s*([0-9.]+\s*ms)') try: with open(filepath, 'r') as f: for line in f: - match = pattern.search(line) - if match: - test_name = clean_test_name(match.group(1)) - ms = parse_time_to_ms(match.group(2)) + m_match = metric_pattern.search(line) + if m_match: + test_name = m_match.group(1) + metric_name = m_match.group(2) + entry_name = f'{test_name} - {metric_name}' + ms = parse_time_to_ms(m_match.group(3)) if ms is not None: - samples.setdefault(test_name, []).append(ms) + samples.setdefault(entry_name, []).append(ms) + continue + + ok_match = ok_pattern.search(line) + if ok_match: + raw_name = ok_match.group(1) + test_name = clean_test_name(raw_name) + if 'DataFetchPerformanceParamTest' in raw_name or test_name.startswith('Benchmark/'): + base_name = test_name.split('Benchmark/', 1)[-1] + if not re.search(r'_(?:10k|100k|1M)$', base_name): + base_name = f'{base_name}_1M' + entry_name = f'{base_name} - Total execution time' + else: + entry_name = test_name + ms = parse_time_to_ms(ok_match.group(2)) + if ms is not None: + samples.setdefault(entry_name, []).append(ms) except Exception as e: print(f'Error reading {filepath}: {e}') results = {} - for test_name, values in samples.items(): + for entry_name, values in samples.items(): values.sort() n = len(values) median = (values[n // 2] if n % 2 == 1 else (values[n // 2 - 1] + values[n // 2]) / 2.0) - results[test_name] = f'{median}ms' + results[entry_name] = f'{median}ms' if n > 1: - print(f'{test_name}: median={median:.0f}ms of {n} runs ' + print(f'{entry_name}: median={median:.0f}ms of {n} runs ' f'(min={values[0]:.0f}ms max={values[-1]:.0f}ms)') return results @@ -243,12 +262,25 @@ jobs: else: return ' (0%)' + def sort_key(test_name): + match = re.match(r'^(.+?)_(10k|100k|1M) - (.+)$', test_name) + if match: + table_name, limit_label, metric_name = match.groups() + limit_order = {'10k': 1, '100k': 2, '1M': 3}.get(limit_label, 4) + metric_order = { + 'Time to first byte': 1, + 'Iteration time': 2, + 'Total execution time': 3, + }.get(metric_name, 4) + return (0, table_name, limit_order, metric_order) + return (1, test_name, 0, 0) + existing_data = parse_gtest_output('./benchmark_results/current_core.txt') current_bq_data = parse_gtest_output('./benchmark_results/current_bq.txt') main_bq_data = parse_gtest_output('./benchmark_results/main_bq.txt') all_tests = set(existing_data.keys()).union(set(current_bq_data.keys())).union(set(main_bq_data.keys())) - sorted_tests = sorted(list(all_tests)) + sorted_tests = sorted(list(all_tests), key=sort_key) rows = [] for test in sorted_tests: @@ -261,7 +293,7 @@ jobs: main_bq_ms = parse_time_to_ms(main_bq_raw) cur_bq_pct = get_percentage_str(cur_bq_ms, existing_ms) if cur_bq_raw != 'N/A' else '' - main_bq_pct = get_percentage_str(main_bq_ms, cur_bq_ms) if main_bq_raw != 'N/A' else '' + main_bq_pct = get_percentage_str(main_bq_ms, cur_bq_ms) if (main_bq_raw != 'N/A' and cur_bq_raw != 'N/A') else ' (N/A)' if main_bq_raw == 'N/A' else '' cur_bq_val = f'{cur_bq_raw}{cur_bq_pct}' main_bq_val = f'{main_bq_raw}{main_bq_pct}' diff --git a/google/cloud/odbc/integration_tests/odbc_driver_tests/examples/catalog_performance_example.cc b/google/cloud/odbc/integration_tests/odbc_driver_tests/examples/catalog_performance_example.cc index 1f2229b864..6daddcd00f 100644 --- a/google/cloud/odbc/integration_tests/odbc_driver_tests/examples/catalog_performance_example.cc +++ b/google/cloud/odbc/integration_tests/odbc_driver_tests/examples/catalog_performance_example.cc @@ -349,10 +349,16 @@ TEST_P(DataFetchPerformanceParamTest, Benchmark) { << "Failed to connect to the database."; auto const& params = GetParam(); + std::string test_name = std::get<0>(params); std::string query = std::get<1>(params); + auto ttfb_start = std::chrono::high_resolution_clock::now(); SQLRETURN ret = SQLExecDirect(conn->hstmt, ToSqlChar(query.c_str()), SQL_NTS); + auto ttfb_end = std::chrono::high_resolution_clock::now(); CheckError(ret, "SQLExecDirect", conn); + auto ttfb_duration_ms = std::chrono::duration_cast( + ttfb_end - ttfb_start) + .count(); SQLSMALLINT num_cols; ret = SQLNumResultCols(conn->hstmt, &num_cols); @@ -374,56 +380,93 @@ TEST_P(DataFetchPerformanceParamTest, Benchmark) { } int row_count = 0; + auto fetch_start = std::chrono::high_resolution_clock::now(); while ((ret = SQLFetch(conn->hstmt)) == SQL_SUCCESS || ret == SQL_SUCCESS_WITH_INFO) { row_count++; } + auto fetch_end = std::chrono::high_resolution_clock::now(); + auto fetch_duration_ms = + std::chrono::duration_cast(fetch_end - + fetch_start) + .count(); + EXPECT_EQ(ret, SQL_NO_DATA) << "Fetch ended unexpectedly with return code: " << ret; + std::cout << "[ METRIC ] " << test_name + << " (Time to first byte): " << ttfb_duration_ms << "ms" + << std::endl; + std::cout << "[ METRIC ] " << test_name + << " (Iteration time): " << fetch_duration_ms << "ms" << std::endl; + EXPECT_EQ(Disconnect(conn), SQL_SUCCESS); } +struct BenchmarkConfig { + std::string name; + std::string base_query; + std::vector> limits; +}; + +inline std::vector GetDataFetchBenchmarkParams() { + std::vector const benchmark_configs = { + {"new_timestamp_table", + "SELECT * FROM " + "`bigquery-devtools-drivers.kirltest.new_timestamp_table`", + {{"10k", 10000}, {"100k", 100000}, {"1M", 1000000}}}, + + {"all_bq_types_2", + "SELECT * FROM " + "`bigquery-devtools-drivers.INTEGRATION_TEST_FORMAT.all_bq_types_2`", + {{"10k", 10000}, {"100k", 100000}, {"1M", 1000000}}}, + + {"nyc311_service_requests", + "SELECT nyc311.unique_key AS V1, nyc311.descriptor AS V2, " + "nyc311.open_data_channel_type AS V3, nyc311.status AS V4, " + "nyc311.incident_address AS V5, nyc311.street_name AS V7, " + "nyc311.city AS V8, nyc311.incident_zip AS V9, nyc311.borough AS V10, " + "nyc311.x_coordinate AS V11, nyc311.y_coordinate AS V12, " + "nyc311.latitude AS V13, nyc311.longitude AS V14, nyc311.location AS " + "V15, " + "nyc311.community_board AS V16, NULL AS V17, NULL AS V18, " + "CAST(nyc311.resolution_action_updated_date AS STRING) AS V19, " + "CAST(nyc311.created_date AS STRING) AS V20, " + "CAST(nyc311.resolution_action_updated_date AS STRING) AS V21, " + "CAST(nyc311.closed_date AS STRING) AS V22 FROM " + "`bigquery-public-data.new_york_311.311_service_requests` AS nyc311", + {{"10k", 10000}, {"100k", 100000}, {"1M", 1000000}}}, + + {"AllDataTypes_2", + "SELECT * FROM " + "`bigquery-devtools-drivers.DATATYPERANGETEST.AllDataTypes_2`", + {{"10k", 10000}, {"100k", 100000}, {"1M", 1000000}}}, + + {"RangeIntervalTestTable_2", + "SELECT * FROM " + "`bigquery-devtools-drivers.DATATYPERANGETEST.RangeIntervalTestTable_2`", + {{"10k", 10000}, {"100k", 100000}, {"1M", 1000000}}}, + }; + + std::vector params; + for (auto const& config : benchmark_configs) { + for (auto const& [label, limit] : config.limits) { + std::string test_name = config.name + "_" + label; + std::string query = + config.base_query + " LIMIT " + std::to_string(limit) + ";"; + params.emplace_back(test_name, query); + } + } + return params; +} + INSTANTIATE_TEST_SUITE_P( , DataFetchPerformanceParamTest, - ::testing::Values( - std::make_tuple("new_timestamp_table", - "SELECT * FROM " - "`bigquery-devtools-drivers.kirltest.new_timestamp_" - "table` LIMIT 1000000"), - std::make_tuple("all_bq_types_2", - "SELECT * FROM " - "`bigquery-devtools-drivers.INTEGRATION_TEST_FORMAT." - "all_bq_types_2` LIMIT 1000000"), - std::make_tuple( - "nyc311_service_requests", - "SELECT nyc311.unique_key AS V1, nyc311.descriptor AS V2, " - "nyc311.open_data_channel_type AS V3, nyc311.status AS V4, " - "nyc311.incident_address AS V5, nyc311.street_name AS V7, " - "nyc311.city AS " - "V8, nyc311.incident_zip AS V9, nyc311.borough AS V10, " - "nyc311.x_coordinate AS V11, nyc311.y_coordinate AS V12, " - "nyc311.latitude " - "AS V13, nyc311.longitude AS V14, nyc311.location AS V15, " - "nyc311.community_board AS V16, NULL AS V17, NULL AS V18, " - "CAST(nyc311.resolution_action_updated_date AS STRING) AS V19, " - "CAST(nyc311.created_date AS STRING) AS V20, " - "CAST(nyc311.resolution_action_updated_date AS STRING) AS V21, " - "CAST(nyc311.closed_date AS STRING) AS V22 FROM " - "`bigquery-public-data.new_york_311.311_service_requests` AS " - "nyc311 " - "LIMIT 1000000;"), - std::make_tuple("AllDataTypes_2", - "SELECT * FROM " - "`bigquery-devtools-drivers.DATATYPERANGETEST." - "AllDataTypes_2` LIMIT 1000000"), - std::make_tuple("RangeIntervalTestTable_2", - "SELECT * FROM " - "`bigquery-devtools-drivers.DATATYPERANGETEST." - "RangeIntervalTestTable_2` LIMIT 1000000")), + ::testing::ValuesIn(GetDataFetchBenchmarkParams()), [](::testing::TestParamInfo const& info) { return std::get<0>(info.param); }); + } // namespace google::cloud::odbc_tests int main(int argc, char* argv[]) {