@@ -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 , µ);
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+
169229StatusRecord 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+
486573StatusRecordOr<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);
0 commit comments