88#include <stdlib.h>
99#include <float.h>
1010#include <math.h>
11- #include <time.h>
1211
1312#include <timestamp.h>
1413
15- #include "handles .h"
14+ #include "convert .h"
1615
1716#define JSON_VAL_NULL "null"
1817#define JSON_VAL_TRUE "true"
1918#define JSON_VAL_FALSE "false"
2019
21- #define TM_TO_TIMESTAMP_STRUCT (_tmp /*src*/ , _tsp /*dst*/ ) \
22- do { \
23- (_tsp)->year = (_tmp)->tm_year + 1900; \
24- (_tsp)->month = (_tmp)->tm_mon + 1; \
25- (_tsp)->day = (_tmp)->tm_mday; \
26- (_tsp)->hour = (_tmp)->tm_hour; \
27- (_tsp)->minute = (_tmp)->tm_min; \
28- (_tsp)->second = (_tmp)->tm_sec; \
29- } while (0)
30-
3120#ifdef _WIN32
3221# ifndef _USE_32BIT_TIME_T
3322# define MKTIME_YEAR_RANGE "1970-3000"
3928#endif /* _WIN32 */
4029#define MKTIME_FAIL_MSG "Outside of the " MKTIME_YEAR_RANGE "year range?"
4130
31+ /* see "recent" meaning in parse_date_time_ts() */
32+ static thread_local SQLCHAR recent_date [] = "1970-01-01" ;
33+
4234/* For fixed size (destination) types, the target buffer can't be NULL. */
4335#define REJECT_IF_NULL_DEST_BUFF (_s /*tatement*/ , _p /*ointer*/ ) \
4436 do { \
@@ -160,8 +152,6 @@ void convert_init()
160152 SQL_C_TYPE_TIMESTAMP
161153 };
162154
163- char * tz ;
164-
165155 /* fill the compact block of TRUEs (growing from the upper left corner) */
166156 for (i = 0 ; i < sizeof (block_idx_sql )/sizeof (* block_idx_sql ); i ++ ) {
167157 for (j = 0 ; j < sizeof (block_idx_csql )/sizeof (* block_idx_csql ); j ++ ) {
@@ -257,13 +247,6 @@ void convert_init()
257247 sql = SQL_GUID ;
258248 csql = SQL_C_GUID ;
259249 compat_matrix [ESSQL_TYPE_IDX (sql )][ESSQL_C_TYPE_IDX (csql )] = TRUE;
260-
261- /* TZ conversions */
262- tzset ();
263- tz = getenv (ESODBC_TZ_ENV_VAR );
264- INFO ("TZ: `%s`, timezone offset: %ld seconds, daylight saving: "
265- "%sapplicable, standard: %s, daylight: %s." , tz ? tz : "<not set>" ,
266- _timezone , _daylight ? "" : "not " , _tzname [0 ], _tzname [1 ]);
267250}
268251
269252
@@ -1550,6 +1533,8 @@ static inline SQLRETURN wstr_to_wstr(esodbc_rec_st *arec, esodbc_rec_st *irec,
15501533}
15511534
15521535/* Parses an ISO8601 timestamp and returns the result into a TIMESTAMP_STRUCT.
1536+ * The time is adjusted to UTC timezone or kept "local", depending on 'to_utc'
1537+ * value (TIMESTAMP_STRUCT lacks any timezone indicator).
15531538 * Returns:
15541539 * - SQL_STATE_0000 for success,
15551540 * - _22007 if the string is not a timestamp and
@@ -1561,7 +1546,8 @@ static SQLRETURN parse_iso8601_timestamp(esodbc_stmt_st *stmt, xstr_st *xstr,
15611546 char buff [ISO8601_TIMESTAMP_MAX_LEN + /*\0*/ 1 ];
15621547 cstr_st ts_str ;
15631548 timestamp_t tsp ;
1564- struct tm tm ;
1549+ struct tm tm , * tmp ;
1550+ time_t utc ;
15651551
15661552 if (xstr -> wide ) {
15671553 DBGH (stmt , "parsing `" LWPDL "` as ISO timestamp." , LWSTR (& xstr -> w ));
@@ -1592,21 +1578,19 @@ static SQLRETURN parse_iso8601_timestamp(esodbc_stmt_st *stmt, xstr_st *xstr,
15921578 }
15931579
15941580 if (! to_utc ) {
1595- /* apply local offset */
1596- tm .tm_sec -= _timezone ; /* : time.h externs */
1597- tm .tm_isdst = -1 ; /* let the system determine the daylight saving */
1598- /* rebalance member values */
1599- if (mktime (& tm ) == (time_t )- 1 ) {
1600- ERRH (stmt , "failed to adjust timestamp `" LCPDL "` by TZ (%ld). "
1601- MKTIME_FAIL_MSG , LCSTR (& ts_str ), _timezone );
1602- RET_HDIAG (stmt , SQL_STATE_22008 , "Timestamp timezone adjustment "
1603- "failed. " MKTIME_FAIL_MSG , 0 );
1581+ /* convert UTC to localtime */
1582+ if ((utc = timegm (& tm )) == (time_t )- 1 || ! (tmp = localtime (& utc ))) {
1583+ ERRNH (stmt , "failed to convert timestamp `" LCPDL "` to UTC. "
1584+ MKTIME_FAIL_MSG , LCSTR (& ts_str ));
1585+ RET_HDIAG (stmt , SQL_STATE_22008 , "Timestamp conversion failed. "
1586+ MKTIME_FAIL_MSG , 0 );
1587+ } else {
1588+ tm = * tmp ;
16041589 }
16051590 }
16061591
1607- TM_TO_TIMESTAMP_STRUCT (& tm , tss );
16081592 /* "the fraction field is the number of billionths of a second" */
1609- tss -> fraction = tsp .nsec ;
1593+ TM_TO_TIMESTAMP_STRUCT ( & tm , tss , tsp .nsec ) ;
16101594
16111595 DBGH (stmt , "parsed %s timestamp: %04d-%02d-%02d %02d:%02d:%02d.%u." ,
16121596 to_utc ? "UTC" : "local" , tss -> year , tss -> month , tss -> day ,
@@ -1615,23 +1599,64 @@ static SQLRETURN parse_iso8601_timestamp(esodbc_stmt_st *stmt, xstr_st *xstr,
16151599 return SQL_SUCCESS ;
16161600}
16171601
1602+ BOOL update_dst_date (struct tm * now )
1603+ {
1604+ int n ;
1605+
1606+ assert (sizeof (SQLCHAR ) == sizeof (char ));
1607+ assert (sizeof (recent_date ) - /*\0*/ 1 == DATE_TEMPLATE_LEN );
1608+ n = snprintf (recent_date , DATE_TEMPLATE_LEN + 1 , "%04d-%02d-%02d" ,
1609+ now -> tm_year + 1900 , now -> tm_mon + 1 , now -> tm_mday );
1610+ if (n <= 0 || DATE_TEMPLATE_LEN < n ) {
1611+ ERRN ("failed to update the DST date." );
1612+ return FALSE;
1613+ }
1614+ return TRUE;
1615+ }
1616+
1617+ static SQLRETURN tss_local_to_utc (esodbc_stmt_st * stmt , TIMESTAMP_STRUCT * tss )
1618+ {
1619+ struct tm * tmp , tm ;
1620+ time_t utc ;
1621+
1622+ TIMESTAMP_STRUCT_TO_TM (tss , & tm );
1623+ tm .tm_isdst = -1 ;
1624+ if (((utc = mktime (& tm )) == (time_t )- 1 ) || (! (tmp = gmtime (& utc )))) {
1625+ ERRNH (stmt , "failed to convert local timestamp "
1626+ "%04hd-%02hu-%02hu %02hu:%02hu:%02hu..%lu to UTC. "
1627+ MKTIME_FAIL_MSG , tss -> year , tss -> month , tss -> day ,
1628+ tss -> hour , tss -> minute , tss -> second , tss -> fraction );
1629+ RET_HDIAG (stmt , SQL_STATE_22008 , "Timestamp timezone adjustment "
1630+ "failed. " MKTIME_FAIL_MSG , errno );
1631+ }
1632+ TM_TO_TIMESTAMP_STRUCT (tmp , tss , tss -> fraction );
1633+
1634+ DBGH (stmt , "UTC: `%04hd-%02hu-%02hu %02hu:%02hu:%02hu..%lu`." ,
1635+ tss -> year , tss -> month , tss -> day ,
1636+ tss -> hour , tss -> minute , tss -> second , tss -> fraction );
1637+ return SQL_SUCCESS ;
1638+ }
16181639
16191640/* Analyzes the received string as time/date/timestamp(timedate) and parses it
16201641 * into received 'tss' struct, indicating detected format in 'format'. */
16211642static SQLRETURN parse_date_time_ts (esodbc_stmt_st * stmt , xstr_st * xstr ,
16221643 BOOL sql2c , TIMESTAMP_STRUCT * tss , SQLSMALLINT * format )
16231644{
1624- esodbc_dbc_st * dbc ;
16251645 SQLRETURN ret ;
1626- /* template buffer: date or time values will be copied in place and
1627- * evaluated as a timestamp (needs to be valid) */
1628- SQLCHAR templ [] = "0001-01-01T00:00:00.000000000+00:00" ;
1646+ /* Template buffer: date or time values will be copied in place and
1647+ * evaluated as a timestamp.
1648+ * It needs to be a "recent" date, since UTC-local_time conversions will
1649+ * need to make use of the correct DST setting.
1650+ * "recent": date within the same DST interval with the wall-clock time. */
1651+ SQLCHAR templ [] = "1970-01-01T00:00:00.000000000+00:00" ;
1652+ SQLCHAR sign ;
16291653 /* conversion Wide to C-string buffer */
16301654 SQLCHAR w2c [ISO8601_TIMESTAMP_MAX_LEN ];
16311655 cstr_st td ;/* timedate string */
16321656 xstr_st xtd ;
1633- long hours , mins , n ;
1634- BOOL to_utc ;
1657+ esodbc_dbc_st * dbc = HDRH (stmt )-> dbc ;
1658+ BOOL to_utc = sql2c ? (! dbc -> apply_tz ) : TRUE;
1659+ BOOL local_to_utc ;
16351660
16361661 /* W-strings will eventually require convertion to C-string for TS
16371662 * conversion => do it now to simplify string analysis */
@@ -1647,60 +1672,63 @@ static SQLRETURN parse_date_time_ts(esodbc_stmt_st *stmt, xstr_st *xstr,
16471672 if (TIMESTAMP_TEMPLATE_LEN (0 ) <= td .cnt ) {
16481673 assert (TIMESTAMP_TEMPLATE_LEN (0 ) < ISO8601_TIMESTAMP_MIN_LEN );
16491674
1650- dbc = HDRH ( stmt ) -> dbc ;
1675+ local_to_utc = FALSE ;
16511676 /* is this a SQL-format timestamp? (vs ISO8601) */
16521677 if (td .str [DATE_TEMPLATE_LEN ] == ' ' ) { /* vs. 'T' */
16531678 td .str [DATE_TEMPLATE_LEN ] = 'T' ;
1654-
1655- /* if c2sql, apply_tz will tell if time is UTC already or not */
1656- if ((! sql2c ) && dbc -> apply_tz ) {
1657- /* TODO: does _timezone account for DST?? */
1658- hours = - _timezone / 3600 ;
1659- mins = _timezone < 0 ? - _timezone : _timezone ;
1660- mins = mins % 3600 ;
1661- assert (mins % 60 == 0 );
1662- mins /= 60 ;
1663- n = snprintf (td .str + td .cnt , /*±00:00\0*/ 7 , "%+03ld:%02ld" ,
1664- hours , mins );
1665- if (n <= 0 ) {
1666- ERRNH (stmt , "failed to print TZ offset (%ld)." , _timezone );
1667- RET_HDIAGS (stmt , SQL_STATE_HY000 );
1668- }
1669- td .cnt += (size_t )n ;
1670- } else {
1671- /* TZ not applicable (UTC is used) or it's an ES-originated
1672- * value => assume UTC: the query can construct a SQL
1673- * timestamp that the app then wants translated to timestamp */
1674- td .str [td .cnt ] = 'Z' ;
1675- td .cnt ++ ;
1676- }
1677- DBGH (stmt , "SQL format translated to ISO: [%zu] `" LCPDL "`." ,
1678- td .cnt , LCSTR (& td ));
1679+ td .str [td .cnt ++ ] = 'Z' ;
1680+
1681+ /* If the received string is a local timestamp, it needs to be
1682+ * first parsed (possibly as if it were a UTC value already) and
1683+ * once that's done and the TSS is available, shifted to the
1684+ * actual UTC value. */
1685+ /* If c2sql, apply_tz will tell if time is local (apply_tz==TRUE)
1686+ * or UTC (apply_tz==FALSE);
1687+ * If local_to_utc==FALSE: TZ not applicable (UTC is used) or it's
1688+ * an ES-originated value (sql2c==TRUE) => assume UTC: the query
1689+ * can construct a SQL timestamp (i.e. the result data type is a
1690+ * text/KW) that the app then wants translated to a timestamp. */
1691+ local_to_utc = (! sql2c ) && dbc -> apply_tz ;
1692+
1693+ DBGH (stmt , "SQL format translated to ISO: [%zu] `" LCPDL "`. "
1694+ "Local to UTC: %d." , td .cnt , LCSTR (& td ), local_to_utc );
16791695 } /* else: already in ISO8601 format */
16801696
16811697 xtd .c = td ;
1682- to_utc = sql2c ? (! dbc -> apply_tz ) : TRUE;
16831698 ret = parse_iso8601_timestamp (stmt , & xtd , to_utc , tss );
1684- if (SQL_SUCCEEDED (ret ) && format ) {
1685- * format = SQL_TYPE_TIMESTAMP ;
1699+ if (SQL_SUCCEEDED (ret )) {
1700+ if (format ) {
1701+ * format = SQL_TYPE_TIMESTAMP ;
1702+ }
1703+ if (local_to_utc ) {
1704+ ret = tss_local_to_utc (stmt , tss );
1705+ }
16861706 } else {
16871707 ERRH (stmt , "`" LCPDL "` is not a TIMESTAMP value." , LCSTR (& td ));
16881708 }
16891709 return ret ;
16901710 }
16911711
1692- /* could this be a TIME value? */
1712+ /* could this be a TIME value? hh:mm:ss / hh:mm:ssZ / hh:mm:ss+HH:MM */
16931713 if (TIME_TEMPLATE_LEN (0 ) <= td .cnt &&
16941714 td .str [2 ] == ':' && td .str [5 ] == ':' ) {
1715+ memcpy (templ , recent_date , sizeof (recent_date ) - /*\0*/ 1 );
16951716 /* copy active value in template and parse it as TS */
16961717 /* copy is safe: cnt <= [time template] < [templ] */
16971718 memcpy (templ + DATE_TEMPLATE_LEN + /*'T'*/ 1 , td .str , td .cnt );
1698- /* there could be a varying number of fractional digits */
1699- templ [DATE_TEMPLATE_LEN + /*'T'*/ 1 + td .cnt ] = 'Z' ;
1700- xtd .c .cnt = td .cnt + DATE_TEMPLATE_LEN + /*'T'*/ 1 + /*Z*/ 1 ;
1719+ assert (sizeof ("+HH:MM" ) - 1 < TIME_TEMPLATE_LEN (0 ));
1720+ sign = td .str [td .cnt - (sizeof ("+HH:MM" ) - /*\0*/ 1 )];
1721+ if (td .str [td .cnt - 1 ] != 'Z' && /* not a Zulu time */
1722+ (sign != '+' && sign != '-' )) { /* not in hh:mm:ss±HH:MM format */
1723+ /* there could be a varying number of fractional digits */
1724+ templ [DATE_TEMPLATE_LEN + /*'T'*/ 1 + td .cnt ] = 'Z' ;
1725+ xtd .c .cnt = DATE_TEMPLATE_LEN + /*'T'*/ 1 + td .cnt + /*Z*/ 1 ;
1726+ } else {
1727+ xtd .c .cnt = DATE_TEMPLATE_LEN + /*'T'*/ 1 + td .cnt ;
1728+ }
17011729 xtd .c .str = templ ;
17021730
1703- ret = parse_iso8601_timestamp (stmt , & xtd , /*to UTC*/ TRUE , tss );
1731+ ret = parse_iso8601_timestamp (stmt , & xtd , to_utc , tss );
17041732 if (SQL_SUCCEEDED (ret )) {
17051733 tss -> year = tss -> month = tss -> day = 0 ;
17061734 if (format ) {
@@ -1712,7 +1740,7 @@ static SQLRETURN parse_date_time_ts(esodbc_stmt_st *stmt, xstr_st *xstr,
17121740 return ret ;
17131741 }
17141742
1715- /* could this be a DATE value? */
1743+ /* could this be a DATE value? YYYY-MM-DD */
17161744 if (DATE_TEMPLATE_LEN <= td .cnt && td .str [4 ] == '-' && td .str [7 ] == '-' ) {
17171745 /* copy active value in template and parse it as TS */
17181746 /* copy is safe: cnt <= [time template] < [templ] */
@@ -1884,6 +1912,7 @@ static SQLRETURN wstr_to_time_struct(esodbc_rec_st *arec, esodbc_rec_st *irec,
18841912 }
18851913
18861914 if (data_ptr ) {
1915+ fmt = 0 ;
18871916 ret = wstr_to_timestamp_struct (arec , irec , & tss , NULL , wstr , chars_0 ,
18881917 & fmt );
18891918 if (! SQL_SUCCEEDED (ret )) {
@@ -2942,22 +2971,23 @@ static int print_timestamp(TIMESTAMP_STRUCT *tss, BOOL iso8601,
29422971{
29432972 int n ;
29442973 size_t lim ;
2945- wchar_t * fmt ;
2974+ const wchar_t * fmt ;
29462975 SQLUINTEGER nsec ; /* "fraction" */
2947- # define FMT_TIMESTAMP_MILLIS "%04d-%02d-%02d %02d:%02d:%02d.%lu"
2948- # define FMT_TIMESTAMP_NOMILLIS "%04d-%02d-%02d %02d:%02d:%02d"
2976+ const static wchar_t * fmt_millis = L "%04d-%02d-%02d %02d:%02d:%02d.%.* lu";
2977+ const static wchar_t * fmt_nomillis = L "%04d-%02d-%02d %02d:%02d:%02d";
29492978
29502979 /* see c2sql_timestamp() for an explanation of these values */
29512980 assert ((! colsize ) || (colsize == 16 || colsize == 19 || 20 < colsize ));
29522981 lim = colsize ? colsize : TIMESTAMP_TEMPLATE_LEN (ESODBC_MAX_SEC_PRECISION );
29532982
29542983 nsec = tss -> fraction ;
29552984 if (0 < decdigits ) {
2956- fmt = MK_WPTR (FMT_TIMESTAMP_MILLIS );
29572985 assert (decdigits <= ESODBC_MAX_SEC_PRECISION );
29582986 nsec /= (SQLUINTEGER )pow10 (ESODBC_MAX_SEC_PRECISION - decdigits );
2987+ /* only print millis if they are non-null */
2988+ fmt = nsec ? fmt_millis : fmt_nomillis ;
29592989 } else {
2960- fmt = MK_WPTR ( FMT_TIMESTAMP_NOMILLIS ) ;
2990+ fmt = fmt_nomillis ;
29612991 }
29622992 /* swprintf and now (=14.15.26706) also _snwprintf() both fail instead of
29632993 * truncating, despite the documentation indicating otherwise => give full
@@ -2966,7 +2996,7 @@ static int print_timestamp(TIMESTAMP_STRUCT *tss, BOOL iso8601,
29662996 fmt , tss -> year , tss -> month , tss -> day ,
29672997 tss -> hour , tss -> minute , tss -> second ,
29682998 /* fraction is always provided, but only printed if 'decdigits' */
2969- nsec );
2999+ decdigits , nsec );
29703000 if ((int )lim < n ) {
29713001 n = (int )lim ;
29723002 }
@@ -2980,14 +3010,12 @@ static int print_timestamp(TIMESTAMP_STRUCT *tss, BOOL iso8601,
29803010 dest [n ] = L'Z' ;
29813011 n ++ ;
29823012 }
2983- DBG ("printed UTC %s timestamp: [%d] `" LWPDL "`." ,
2984- iso8601 ? "ISO8601" : "SQL" , n , n , dest );
3013+ DBG ("printed UTC %s timestamp (colsz: %lu, decdig: %hd): "
3014+ "[%d] `" LWPDL "`." , iso8601 ? "ISO8601" : "SQL" ,
3015+ (SQLUINTEGER )colsize , decdigits , n , n , dest );
29853016 }
29863017
2987-
29883018 return n ;
2989- # undef FMT_TIMESTAMP_MILLIS
2990- # undef FMT_TIMESTAMP_NOMILLIS
29913019}
29923020
29933021/* transform an ISO8601 timestamp str. to SQL/ODBC timestamp str. */
@@ -3964,6 +3992,7 @@ static SQLRETURN struct_to_iso8601_timestamp(esodbc_stmt_st *stmt,
39643992 DATE_STRUCT * ds ;
39653993 int n ;
39663994 size_t osize ;
3995+ SQLRETURN ret ;
39673996
39683997 switch (ctype ) {
39693998 case SQL_C_TYPE_DATE :
@@ -3997,6 +4026,16 @@ static SQLRETURN struct_to_iso8601_timestamp(esodbc_stmt_st *stmt,
39974026 RET_HDIAG (stmt , SQL_STATE_HY000 , "param conversion bug" , 0 );
39984027 }
39994028
4029+ if (HDRH (stmt )-> dbc -> apply_tz ) {
4030+ /* used for C2SQL conversions only: localtime to UTC */
4031+ buff = * tss ;
4032+ ret = tss_local_to_utc (stmt , & buff );
4033+ if (! SQL_SUCCEEDED (ret )) {
4034+ return ret ;
4035+ }
4036+ tss = & buff ;
4037+ }
4038+
40004039 n = print_timestamp (tss , /*ISO?*/ TRUE, colsize , decdigits , dest );
40014040 if (n <= 0 ) {
40024041 ERRNH (stmt , "printing TIMESTAMP failed." );
0 commit comments