Skip to content
Merged
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
54 changes: 43 additions & 11 deletions .github/workflows/test-runner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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}'
Expand Down
75 changes: 75 additions & 0 deletions google/cloud/odbc/bq_driver/internal/odbc_sql_execute_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@ StatusRecordOr<std::shared_ptr<arrow::Schema>> 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
Expand Down Expand Up @@ -574,6 +579,76 @@ StatusRecord ProcessRecordBatch(
}
break;
}
case arrow::Type::INTERVAL_MONTHS: {
auto arr = std::static_pointer_cast<arrow::MonthIntervalArray>(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<arrow::DayTimeIntervalArray>(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<arrow::MonthDayNanoIntervalArray>(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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2972,44 +2972,63 @@ void IntervalTestRunner(
std::string const& table_name,
std::vector<SQL_INTERVAL_STRUCT> const& interval_data,
std::function<void(std::shared_ptr<ODBCHandles>, std::string const&)> const&
TestTranslation) {
TestTranslation,
std::string const& connection_string = kDefaultConnectionString) {
auto conn = std::make_shared<ODBCHandles>();
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<bool> {};

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<SQL_INTERVAL_STRUCT> 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<bool> const& info) {
return info.param ? "HTAPI_Enabled"
: "HTAPI_Disabled";
});

std::vector<std::string> GetInputValuesToString(std::string column_name,
StdAllTypesRows input_data) {
std::vector<std::string> input_values;
Expand Down
Loading
Loading