Skip to content

Commit 895f3fe

Browse files
impl(bq_driver): Picosecond support
1 parent 7d6187d commit 895f3fe

11 files changed

Lines changed: 452 additions & 112 deletions

File tree

google/cloud/odbc/bq_driver/internal/data_translation.cc

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -935,26 +935,79 @@ odbc_internal::StatusRecord ConvertFromTimeDSValue(DSValue const& src_dsval,
935935
return status_record;
936936
}
937937

938+
odbc_internal::StatusRecord ConvertTimestampStringToChar(
939+
const std::string& timestamp_src_str,
940+
void* dest_buf,
941+
SQLLEN buffer_length,
942+
SQLLEN* res_len) {
943+
944+
constexpr SQLLEN k_timestamp_src_len = 64; // adjust if needed
945+
auto* dest = reinterpret_cast<char*>(dest_buf);
946+
StatusRecord status_record;
947+
948+
if (buffer_length > k_timestamp_src_len) {
949+
if (res_len) *res_len = k_timestamp_src_len;
950+
std::strncpy(dest, timestamp_src_str.c_str(), k_timestamp_src_len);
951+
dest[k_timestamp_src_len] = '\0';
952+
} else if (20 <= buffer_length && buffer_length <= k_timestamp_src_len) {
953+
if (res_len) *res_len = buffer_length;
954+
std::strncpy(dest, timestamp_src_str.c_str(), buffer_length - 1);
955+
dest[buffer_length - 1] = '\0';
956+
status_record = StatusRecord{SQLStates::k_01004(), "Data truncated"};
957+
} else {
958+
status_record =
959+
StatusRecord{SQLStates::k_22003(), "Buffer length is insufficient"};
960+
}
961+
return status_record;
962+
}
963+
964+
odbc_internal::StatusRecord ConvertTimestampStringToWChar(
965+
const std::string& timestamp_src_str,
966+
void* dest_buf,
967+
SQLLEN buffer_length,
968+
SQLLEN* res_len) {
969+
970+
StatusRecord status_record;
971+
auto wstr_or = Utf8ToUtf16(timestamp_src_str);
972+
if (!wstr_or) {
973+
return StatusRecord{SQLStates::k_HY000(), "DSValueToWchar Conversion Failed"};
974+
}
975+
976+
std::vector<SQLWCHAR> wstr_data(wstr_or->begin(), wstr_or->end());
977+
wstr_data.emplace_back(L'\0');
978+
979+
auto* dest = reinterpret_cast<SQLWCHAR*>(dest_buf);
980+
981+
if (buffer_length > static_cast<SQLLEN>(wstr_or->size())) {
982+
if (res_len) *res_len = wstr_or->size() * sizeof(SQLWCHAR);
983+
std::memcpy(dest, wstr_data.data(), wstr_or->size() * sizeof(SQLWCHAR));
984+
dest[wstr_or->size()] = L'\0';
985+
} else if (20 <= buffer_length && buffer_length <= static_cast<SQLLEN>(wstr_or->size())) {
986+
if (res_len) *res_len = buffer_length * sizeof(SQLWCHAR);
987+
std::memcpy(dest, wstr_data.data(), buffer_length * sizeof(SQLWCHAR));
988+
dest[buffer_length - 1] = L'\0';
989+
status_record = StatusRecord{SQLStates::k_01004(), "Data truncated"};
990+
} else {
991+
status_record =
992+
StatusRecord{SQLStates::k_22003(), "Buffer length is insufficient"};
993+
}
994+
995+
return status_record;
996+
}
938997
odbc_internal::StatusRecord ConvertFromTimestampDSValue(
939998
DSValue const& src_dsval, DataBuffer& dest_data) {
940-
using odbc_internal::SQLStates;
941-
using odbc_internal::StatusRecord;
942-
using odbc_internal::StatusRecordOr;
943999

944-
SQL_TIMESTAMP_STRUCT timestamp_src_struct;
945-
DSValueToTimestamp(src_dsval, timestamp_src_struct);
1000+
using odbc_internal::SQLStates;
1001+
using odbc_internal::StatusRecord;
9461002

947-
std::string timestamp_src_str;
948-
timestamp_src_str = FormatTimestampToString(timestamp_src_struct);
1003+
std::string str_val;
1004+
DSValueToString(src_dsval, str_val);
1005+
std::string timestamp_src_str;
9491006

950-
SQLSMALLINT dest_type = dest_data.type;
951-
SQLPOINTER dest_buf = dest_data.buf;
952-
SQLLEN buffer_length = dest_data.buflen;
953-
SQLLEN* res_len = dest_data.result_len;
954-
955-
// Define length variables
956-
int k_timestamp_src_len = timestamp_src_str.length();
957-
constexpr int kTimestampBinaryLength = sizeof(SQL_TIMESTAMP_STRUCT);
1007+
SQLSMALLINT dest_type = dest_data.type;
1008+
SQLPOINTER dest_buf = dest_data.buf;
1009+
SQLLEN buffer_length = dest_data.buflen;
1010+
SQLLEN* res_len = dest_data.result_len;
9581011

9591012
if (!dest_buf) {
9601013
return StatusRecord::Ok();
@@ -1040,7 +1093,7 @@ odbc_internal::StatusRecord ConvertFromTimestampDSValue(
10401093
if (res_len) {
10411094
*res_len = kTimestampBinaryLength;
10421095
}
1043-
timestamp_src_struct.fraction = timestamp_src_struct.fraction * 1000;
1096+
timestamp_src_struct.fraction = timestamp_src_struct.fraction;
10441097
std::memcpy(dest_buf, &timestamp_src_struct, kTimestampBinaryLength);
10451098

10461099
} else {

google/cloud/odbc/bq_driver/internal/odbc_conn_handle.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ void ConnectionHandle::SetUp(Section& dsn_section,
160160
std::string job_creation_mode = dsn_section["JOBCREATIONMODE"];
161161
dsn_.is_job_creation_required = (job_creation_mode == "1");
162162

163+
std::string timestamp_output_format =dsn_section["TIMESTAMPOUTPUTFORMAT"];
164+
if (!timestamp_output_format.empty()) {
165+
dsn_.format_options.timestamp_output_format = timestamp_output_format;
166+
}
167+
168+
163169
if (attribute_str_values_.count(SQL_ATTR_CURRENT_CATALOG) == 0) {
164170
attribute_str_values_.insert({SQL_ATTR_CURRENT_CATALOG, dsn_.catalog});
165171
}

google/cloud/odbc/bq_driver/internal/odbc_conn_handle.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ struct Dsn {
7373
std::vector<ConnectionProperty> connection_properties;
7474
std::uint32_t row_fetched_per_block = 100000;
7575
std::uint32_t default_string_column_length = 16384;
76+
google::cloud::bigquery_v2_minimal_internal::DataFormatOptions format_options;
7677
/////////////////////////////////////////////////////////////////
7778
// Optional Properties needed for HTAPI.
7879
/////////////////////////////////////////////////////////////////

google/cloud/odbc/bq_driver/internal/odbc_internal_commons.cc

Lines changed: 139 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,66 @@ int DaysInMonth(int year, int month) {
166166
return kDaysInMonth[month - 1];
167167
}
168168

169+
std::string FloatTimestampToString(const std::string& ts) {
170+
double seconds_since_epoch = std::stod(ts);
171+
time_t sec = static_cast<time_t>(seconds_since_epoch);
172+
double fractional = seconds_since_epoch - static_cast<double>(sec);
173+
std::tm tm{};
174+
#ifdef _WIN32
175+
gmtime_s(&tm, &sec);
176+
#else
177+
gmtime_r(&sec, &tm);
178+
#endif
179+
180+
int microseconds = static_cast<int>(std::round(fractional * 1'000'000));
181+
if (microseconds == 1'000'000) {
182+
microseconds = 0;
183+
sec += 1;
184+
#ifdef _WIN32
185+
gmtime_s(&tm, &sec);
186+
#else
187+
gmtime_r(&sec, &tm);
188+
#endif
189+
}
190+
191+
std::ostringstream out;
192+
out << std::put_time(&tm, "%Y-%m-%d %H:%M:%S")
193+
<< "." << std::setw(6) << std::setfill('0') << microseconds;
194+
195+
return out.str();
196+
}
197+
198+
SQL_TIMESTAMP_STRUCT ConvertStrToTimestampStruct(std::string const& str) {
199+
SQL_TIMESTAMP_STRUCT ts = {};
200+
int micro = 0;
201+
int matched = 0;
202+
203+
static constexpr const char* kFormats[] = {
204+
// ISO 8601: YYYY-MM-DDTHH:MM:SS[.ffffff]
205+
"%4hd-%2hd-%2hdT%2hd:%2hd:%2hd.%6d",
206+
// Space-separated: YYYY-MM-DD HH:MM:SS[.ffffff]
207+
"%4hd-%2hd-%2hd %2hd:%2hd:%2hd.%6d",
208+
// Compact (legacy): YYYY-MM-DDHH:MM:SS[.ffffff]
209+
"%4hd-%2hd-%2hd%2hd:%2hd:%2hd.%6d",
210+
};
211+
212+
for (const char* fmt : kFormats) {
213+
matched = sscanf(str.c_str(), fmt,
214+
&ts.year, &ts.month, &ts.day,
215+
&ts.hour, &ts.minute, &ts.second, &micro);
216+
if (matched >= 6) {
217+
break;
218+
}
219+
}
220+
221+
if (matched < 6) {
222+
throw std::invalid_argument("Invalid timestamp: " + str);
223+
}
224+
225+
ts.fraction = (matched == 7) ? micro : 0;
226+
return ts;
227+
}
228+
169229
StatusRecord ConvertUnixTimestampToTimestampStruct(
170230
double unix_timestamp, SQL_TIMESTAMP_STRUCT& timestamp_struct) {
171231
// Check for invalid timestamp (e.g., negative or non-finite)
@@ -483,9 +543,54 @@ std::string FormatNumericToString(SQL_NUMERIC_STRUCT numeric) {
483543
return result;
484544
}
485545

546+
SQL_TIMESTAMP_STRUCT ConvertEpochStringToTimestamp(
547+
const std::string& epoch_str) {
548+
long double epoch = std::stold(epoch_str);
549+
long long epoch_seconds = static_cast<long long>(epoch);
550+
long double fractional_part = epoch - epoch_seconds;
551+
long long fraction_ns =
552+
static_cast<long long>(fractional_part * 1'000'000'000.0L);
553+
if (fraction_ns < 0) {
554+
fraction_ns += 1'000'000'000;
555+
epoch_seconds -= 1;
556+
}
557+
558+
time_t raw_time = static_cast<time_t>(epoch_seconds);
559+
std::tm* timeinfo = std::gmtime(&raw_time);
560+
561+
SQL_TIMESTAMP_STRUCT ts{};
562+
ts.year = timeinfo->tm_year + 1900;
563+
ts.month = timeinfo->tm_mon + 1;
564+
ts.day = timeinfo->tm_mday;
565+
ts.hour = timeinfo->tm_hour;
566+
ts.minute = timeinfo->tm_min;
567+
ts.second = timeinfo->tm_sec;
568+
ts.fraction = static_cast<SQLUINTEGER>(fraction_ns); // ✅ exact fraction
569+
570+
return ts;
571+
}
572+
486573
StatusRecordOr<SQL_TIMESTAMP_STRUCT> ConvertStringToTimestampStruct(
487574
std::string const& date_str) {
575+
using odbc_internal::SQLStates;
576+
using odbc_internal::StatusRecord;
488577
std::string cleaned_date_str = date_str;
578+
bool looks_like_epoch =
579+
cleaned_date_str.find('-') == std::string::npos &&
580+
cleaned_date_str.find(':') == std::string::npos;
581+
582+
if (looks_like_epoch) {
583+
try {
584+
auto ts = ConvertEpochStringToTimestamp(cleaned_date_str);
585+
return ts;
586+
} catch (const std::exception& ex) {
587+
return StatusRecord{SQLStates::k_HY000(),
588+
"Epoch conversion failed"};
589+
}
590+
}
591+
if (!cleaned_date_str.empty() && cleaned_date_str.back() == 'Z') {
592+
cleaned_date_str.pop_back();
593+
}
489594
std::replace(cleaned_date_str.begin(), cleaned_date_str.end(), 'T', ' ');
490595

491596
SQL_TIMESTAMP_STRUCT date_struct = {};
@@ -495,10 +600,10 @@ StatusRecordOr<SQL_TIMESTAMP_STRUCT> ConvertStringToTimestampStruct(
495600
int hour;
496601
int minute;
497602
int second;
498-
char fraction_str[10] = "0";
603+
char fraction_str[13] = "0";
499604

500605
int matched =
501-
std::sscanf(cleaned_date_str.c_str(), "%4d-%2d-%2d %2d:%2d:%2d.%6s",
606+
std::sscanf(cleaned_date_str.c_str(), "%4d-%2d-%2d %2d:%2d:%2d.%12s",
502607
&year, &month, &day, &hour, &minute, &second, fraction_str);
503608

504609
if (matched < 6) {
@@ -520,12 +625,16 @@ StatusRecordOr<SQL_TIMESTAMP_STRUCT> ConvertStringToTimestampStruct(
520625
return StatusRecord{SQLStates::k_HY000(),
521626
"Fractional part is not a valid number"};
522627
}
523-
fraction = fraction * 10 + (ch - '0');
524-
++len;
525628
}
526-
for (; len < 6; ++len) {
527-
fraction *= 10;
629+
std::string frac_str(fraction_str);
630+
// Truncate or pad to 9 digits (nanoseconds)
631+
if (frac_str.size() > 7) {
632+
frac_str.resize(6); // truncate picoseconds
633+
} else {
634+
frac_str.append(6 - frac_str.size(), '0'); // pad to nanoseconds
528635
}
636+
637+
fraction = static_cast<SQLUINTEGER>(std::stoul(frac_str));
529638
}
530639

531640
date_struct.year = static_cast<SQLSMALLINT>(year);
@@ -632,16 +741,27 @@ StatusRecordOr<ResultSet> ProcessResultSetRows(
632741
break;
633742
}
634743
case BQDataType::kTimeStamp: {
635-
double unix_timestamp;
636-
try {
637-
unix_timestamp = std::stod(data);
638-
} catch (std::exception const& ex) {
639-
return StatusRecord{SQLStates::k_HY000(),
640-
"data cannot be parsed as double"};
744+
// Check if the string has a fraction part longer than 9 digits
745+
auto dot_pos = data.find('.');
746+
bool long_fraction = false;
747+
if (dot_pos != std::string::npos) {
748+
std::size_t fraction_length = data.size() - dot_pos - 1;
749+
if (fraction_length > 9) {
750+
long_fraction = true;
751+
}
641752
}
642-
SQL_TIMESTAMP_STRUCT time_struct;
643-
ConvertUnixTimestampToTimestampStruct(unix_timestamp, time_struct);
644-
TimestampToDSValue(time_struct, row_val);
753+
754+
if (long_fraction) {
755+
StringToDSValue(data,row_val);
756+
break;
757+
}
758+
// Normal timestamp parsing
759+
StatusRecordOr<SQL_TIMESTAMP_STRUCT> ts_status = ConvertStringToTimestampStruct(data);
760+
if (!ts_status) {
761+
return ts_status.GetStatusRecord();
762+
}
763+
const SQL_TIMESTAMP_STRUCT& ts = *ts_status;
764+
TimestampToDSValue(ts, row_val);
645765
break;
646766
}
647767
case BQDataType::kInterval: {
@@ -1052,7 +1172,10 @@ PostQueryRequest ConstructBasicPostQueryRequest(
10521172
bool is_query_cache = conn_handle.GetDsn().is_query_cache;
10531173
PostQueryRequest post_request;
10541174
QueryRequest query_request;
1055-
// Construct query request.
1175+
auto format_options = conn_handle.GetDsn().format_options;
1176+
if (!format_options.timestamp_output_format.empty()) {
1177+
query_request.set_format_options(format_options);
1178+
}
10561179
query_request.set_dry_run(false);
10571180
query_request.set_location(std::move(location));
10581181
query_request.set_query(query_str);

google/cloud/odbc/bq_driver/internal/odbc_internal_commons.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,10 @@ inline std::string FormatDatetimeToString(
317317
return buffer;
318318
}
319319

320+
std::string FloatTimestampToString(const std::string& ts);
321+
322+
SQL_TIMESTAMP_STRUCT ConvertStrToTimestampStruct(std::string const& str);
323+
320324
inline void TimestampToDSValue(const SQL_TIMESTAMP_STRUCT& timestamp,
321325
DSValue& value) {
322326
value.resize(sizeof(SQL_TIMESTAMP_STRUCT));

google/cloud/odbc/bq_driver/internal/odbc_sql_type_info.cc

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,32 @@
1616

1717
namespace google::cloud::odbc_bq_driver_internal {
1818

19-
DSRow CreateDSRowFromTypeInfo(TypeInfoRow const& type_info) {
19+
bool IsTimeRelated(TypeInfoRow const& type_info) {
20+
return type_info.sql_data_type == SQL_TYPE_TIMESTAMP ||
21+
type_info.sql_data_type == SQL_TIMESTAMP ;
22+
}
23+
24+
DSRow CreateDSRowFromTypeInfo(TypeInfoRow const& type_info, bool timestamp_format) {
25+
TypeInfoRow adjusted = type_info;
26+
if (timestamp_format &&
27+
IsTimeRelated(type_info)) {
28+
adjusted.sql_data_type = SQL_VARCHAR;
29+
adjusted.data_type = SQL_VARCHAR;
30+
adjusted.col_size = 16384;
31+
}
2032
DSRow ds_row;
2133

2234
DSValue type_name;
2335
StringToDSValue(type_info.type_name, type_name);
2436
ds_row.emplace_back(type_name);
2537

2638
DSValue data_type;
27-
ArithmeticToDSValue<SQLBIGINT>(static_cast<SQLBIGINT>(type_info.data_type),
39+
ArithmeticToDSValue<SQLBIGINT>(static_cast<SQLBIGINT>(adjusted.data_type),
2840
data_type);
2941
ds_row.emplace_back(data_type);
3042

3143
DSValue col_size;
32-
ArithmeticToDSValue<SQLBIGINT>(static_cast<SQLBIGINT>(type_info.col_size),
44+
ArithmeticToDSValue<SQLBIGINT>(static_cast<SQLBIGINT>(adjusted.col_size),
3345
col_size);
3446
ds_row.emplace_back(col_size);
3547

@@ -96,7 +108,7 @@ DSRow CreateDSRowFromTypeInfo(TypeInfoRow const& type_info) {
96108
ds_row.emplace_back(maximum_scale);
97109

98110
DSValue sql_data_type;
99-
ArithmeticToDSValue<SQLBIGINT>(type_info.sql_data_type, sql_data_type);
111+
ArithmeticToDSValue<SQLBIGINT>(adjusted.sql_data_type, sql_data_type);
100112
ds_row.emplace_back(sql_data_type);
101113

102114
DSValue sql_datetime_sub;

google/cloud/odbc/bq_driver/internal/odbc_sql_type_info.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ std::map<SQLSMALLINT, std::map<std::string, TypeInfoRow>> const
527527
{"BIGNUMERIC", kBqBignumericTypeInfoRow},
528528
}}};
529529

530-
DSRow CreateDSRowFromTypeInfo(TypeInfoRow const& type_info);
530+
DSRow CreateDSRowFromTypeInfo(TypeInfoRow const& type_info, bool timestamp_format = false);
531531

532532
void CreateTypeInfoRowSchema(ResultSet& result_set);
533533

0 commit comments

Comments
 (0)