Skip to content

Commit f46b8c5

Browse files
committed
Apply DST (#142)
* fix: find if DST is in effect and apply offset The system timezone offset doesn't account for daylight saving. This commit adds the check if DST is in effect, calculates its offset and adds it to the timezone offset for a total time offset. * enable per-request time offset calculation This commit will enable the calculation of the time offset (timezone plus DST) for every request (done when serializing a statement). This should allow correct operation of the driver also while DST turns into efect. The commit also switches back the calculation of the time offset to using POSIX functions. A warning is now logged if TZ environment variable is used and the DST timezone acronym part is set since the Win CRT "assumes the United States' rules", applied also when the timezone is not a US one. * minor: remove testing decorator - convert_init() no longer used by unit tests * local-UTC time conversions done now with C lib fns - convert timestamps using the CRT available functions, instead of the incorrect use of start-time UTC-offset; - add unit tests for the DST conversions * minor headers and comments fixes (cherry picked from commit 0cf97aa)
1 parent a0f60cc commit f46b8c5

File tree

11 files changed

+528
-123
lines changed

11 files changed

+528
-123
lines changed

driver/convert.c

Lines changed: 124 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,15 @@
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"
@@ -39,6 +28,9 @@
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'. */
16211642
static 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

Comments
 (0)