Skip to content

Commit 072496c

Browse files
committed
Fix: consider interval's precision. Allow non-aligned period values as interval encoding (#148)
* add the precision value to the interval types The interval types with a seconds component have the seconds precision as a property. With this commit the driver will read the Datetime(/Timestamp) scale and use that as precision for the interval with seconds component data type property, attached to the .precision field of the records. * fix: use IRD as precision src. instead of ARD When handling values received from ES/SQL, use the IRD descriptor instead of the ARD, when reading data characteristics (like the precision). Also, change the integration tests that were setting ARD fields - like precision - on string conversions. (This is wrong since the strings don't have a precision property, so the driver should not - and now will not - take this property into account.) * fix: decouple ES period format from interval type ES/SQL transmits interval values in a ISO8601 period value. An interval-to-period 1:1 mapping is possible, however ES/SQL doesn't adhere to this mapping. For instance: - instead of `P1DT1H` for `INTERVAL '1 1' DAY TO HOUR`, ES/SQL will send `PT25H` (which in a 1:1 mapping would be `INTERVAL '25' HOUR` literal, equivalent, but different from the original); - `INTERVAL '61:59' MINUTE TO SECOND` is encoded as `PT1H1M59S` instead of `PT61M59S`; - `INTERVAL '61' SECOND` is transmitted as `PT1M1S` vs. `PT61S`. Because of this, the driver will now detect the mapping misalignment, convert any interval in the "day/to/second" range to seconds and re-calculate the values for the corresponding interval members from that. (cherry picked from commit 0366a74)
1 parent a6cfca5 commit 072496c

File tree

5 files changed

+160
-50
lines changed

5 files changed

+160
-50
lines changed

driver/connect.c

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2118,7 +2118,7 @@ static void *copy_types_rows(esodbc_dbc_st *dbc, estype_row_st *type_row,
21182118
SQLWCHAR *pos;
21192119
int c;
21202120
SQLULEN i;
2121-
SQLSMALLINT sql_type;
2121+
SQLSMALLINT sql_type, sec_prec;
21222122

21232123
/* pointer to start position where the strings will be copied in */
21242124
pos = (SQLWCHAR *)&types[rows_fetched];
@@ -2151,6 +2151,7 @@ static void *copy_types_rows(esodbc_dbc_st *dbc, estype_row_st *type_row,
21512151
} \
21522152
} while (0)
21532153

2154+
sec_prec = -1;
21542155
for (i = 0; i < rows_fetched; i ++) {
21552156
/* copy row data */
21562157
ES_TYPES_COPY_WSTR(type_name);
@@ -2233,6 +2234,20 @@ static void *copy_types_rows(esodbc_dbc_st *dbc, estype_row_st *type_row,
22332234
&types[i].sql_datetime_sub);
22342235

22352236
set_display_size(types + i);
2237+
2238+
/* There's no explicit coverage in the docs on how to communicate the
2239+
* interval seconds precision, so will use the scales in
2240+
* SQLGetTypeInfo() for that, just like for Timestamp type;
2241+
* furthermore, will use Timestamp's value, since it's the same
2242+
* seconds precision across all ES/SQL types with seconds component. */
2243+
if (types[i].data_type == SQL_TYPE_TIMESTAMP) {
2244+
assert(sec_prec < 0);
2245+
sec_prec = types[i].maximum_scale;
2246+
} else if (types[i].meta_type == METATYPE_INTERVAL_WSEC) {
2247+
assert(0 < sec_prec);
2248+
types[i].maximum_scale = sec_prec;
2249+
types[i].minimum_scale = types[i].maximum_scale;
2250+
}
22362251
}
22372252

22382253
#undef ES_TYPES_COPY_INT

driver/convert.c

Lines changed: 98 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2004,11 +2004,11 @@ static inline SQLRETURN adjust_to_precision(esodbc_rec_st *rec,
20042004
}
20052005
}
20062006

2007-
static SQLRETURN parse_iso8601_number(esodbc_rec_st *arec, wstr_st *wstr,
2007+
static SQLRETURN parse_iso8601_number(esodbc_rec_st *rec, wstr_st *wstr,
20082008
SQLUINTEGER *uint, char *sign,
20092009
SQLUINTEGER *fraction, BOOL *has_fraction)
20102010
{
2011-
esodbc_stmt_st *stmt = arec->desc->hdr.stmt;
2011+
esodbc_stmt_st *stmt = rec->desc->hdr.stmt;
20122012
char inc;
20132013
wstr_st nr;
20142014
int digits, fdigits;
@@ -2041,7 +2041,7 @@ static SQLRETURN parse_iso8601_number(esodbc_rec_st *arec, wstr_st *wstr,
20412041
ERRH(stmt, "fraction value too large (%llu).", ubint);
20422042
return SQL_ERROR;
20432043
} else {
2044-
ret = adjust_to_precision(arec, &ubint, fdigits);
2044+
ret = adjust_to_precision(rec, &ubint, fdigits);
20452045
assert(ubint < ULONG_MAX); /* due to previous sanity checks */
20462046
*fraction = (SQLUINTEGER)ubint;
20472047
}
@@ -2073,13 +2073,19 @@ static SQLRETURN parse_iso8601_number(esodbc_rec_st *arec, wstr_st *wstr,
20732073
return ret;
20742074
}
20752075

2076-
/* parse an ISO8601 period value
2077-
* Elasticsearch'es implementation deviates slightly, hiding the day field:
2078-
* `INTERVAL '1 1' DAY TO HOUR` -> `PT25H` instead of `P1DT1H`. */
2079-
static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
2076+
/* Parse an ISO8601 period value.
2077+
* Elasticsearch'es implementation deviates from the standard by, for example:
2078+
* - demoting the day field: `INTERVAL '1 1' DAY TO HOUR` ->
2079+
* `PT25H` instead of `P1DT1H`; `INTERVAL '1' DAY` -> `PT24H` vs. `P1D`;
2080+
* - promoting the hour field: `INTERVAL '61:59' MINUTE TO SECOND` ->
2081+
* `PT1H1M59S` instead of `PT61M59S`; `INTERVAL '60' MINUTE` -> `PT1H` vs.
2082+
* `PT60M`.
2083+
* - promoting the minute field: `INTERVAL '61' SECOND` -> `PT1M1S` vs.
2084+
* `PT61S` */
2085+
static SQLRETURN parse_interval_iso8601(esodbc_rec_st *rec,
20802086
SQLSMALLINT ctype, wstr_st *wstr, SQL_INTERVAL_STRUCT *ivl)
20812087
{
2082-
esodbc_stmt_st *stmt = arec->desc->hdr.stmt;
2088+
esodbc_stmt_st *stmt = rec->desc->hdr.stmt;
20832089
char sign;
20842090
SQLWCHAR *crr, *end;
20852091
wstr_st nr;
@@ -2103,7 +2109,9 @@ static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
21032109
(1 << SQL_IS_HOUR) | (1 << SQL_IS_MINUTE) | (1 << SQL_IS_SECOND),
21042110
(1 << SQL_IS_MINUTE) | (1 << SQL_IS_SECOND),
21052111
};
2112+
uint16_t type2bm_ivl; /* the type bit mask for the interval */
21062113
SQLRETURN ret;
2114+
SQLUINTEGER secs; /* ~ond~ */
21072115

21082116
/* Sets a bit in a bitmask corresponding to one interval field, given
21092117
* `_ivl`, or errs if already set.
@@ -2136,6 +2144,15 @@ static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
21362144
DBGH(stmt, "field %d assigned value %lu.", _ivl_type, uint); \
21372145
state = saved; \
21382146
} while (0)
2147+
/* Safe ulong addition */
2148+
# define ULONG_SAFE_ADD(_to, _from) \
2149+
do { \
2150+
if (ULONG_MAX - (_from) < _to) { \
2151+
goto err_overflow; \
2152+
} else { \
2153+
_to += _from; \
2154+
} \
2155+
} while (0)
21392156

21402157
/* the interval type will be used as bitmask indexes */
21412158
assert(0 < SQL_IS_YEAR && SQL_IS_MINUTE_TO_SECOND < 16);
@@ -2180,7 +2197,7 @@ static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
21802197
}
21812198
nr.str = crr;
21822199
nr.cnt = end - crr;
2183-
ret = parse_iso8601_number(arec, &nr, &uint, &sign,
2200+
ret = parse_iso8601_number(rec, &nr, &uint, &sign,
21842201
&fraction, &has_fraction);
21852202
if (! SQL_SUCCEEDED(ret)) {
21862203
goto err_format;
@@ -2243,28 +2260,82 @@ static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
22432260

22442261
assert(0 < /*starts at 1*/ ivl->interval_type &&
22452262
ivl->interval_type < 8 * sizeof(type2bm)/sizeof(type2bm[0]));
2246-
/* reinstate the day field merged by ES in the hour field when:
2247-
* - the day field hasn't been set;
2248-
* - it is an interval with day component;
2249-
* - the hour field overflows a day/24h */
2250-
if (((1 << SQL_IS_DAY) & fields_bm) == 0 &&
2251-
(type2bm[ivl->interval_type - 1] & (1 << SQL_IS_DAY)) &&
2252-
24 <= ivl->intval.day_second.hour) {
2253-
ivl->intval.day_second.day = ivl->intval.day_second.hour / 24;
2254-
ivl->intval.day_second.hour = ivl->intval.day_second.hour % 24;
2255-
fields_bm |= 1 << SQL_IS_DAY;
2263+
2264+
type2bm_ivl = type2bm[ivl->interval_type - 1];
2265+
/* If the expression set fields not directly relevant to the interval AND
2266+
* the interval is not of type year/-/month one (which does seem to
2267+
* conform to the standard), rebalance the member values as expected by
2268+
* the interval type. */
2269+
if ((type2bm_ivl != fields_bm) && ((type2bm_ivl &
2270+
((1 << SQL_CODE_YEAR) | (1 << SQL_CODE_MONTH))) == 0)) {
2271+
secs = ivl->intval.day_second.second;
2272+
ULONG_SAFE_ADD(secs, 60 * ivl->intval.day_second.minute); //...
2273+
ULONG_SAFE_ADD(secs, 3600 * ivl->intval.day_second.hour);
2274+
ULONG_SAFE_ADD(secs, 24 * 3600 * ivl->intval.day_second.day);
2275+
/* clear everything set, but reinstate any set fractions */
2276+
fields_bm = 0;
2277+
memset(&ivl->intval.day_second, 0, sizeof(ivl->intval.day_second));
2278+
ivl->intval.day_second.fraction = fraction;
2279+
2280+
if (type2bm_ivl & (1 << SQL_CODE_SECOND)) {
2281+
ivl->intval.day_second.second = secs;
2282+
fields_bm |= (1 << SQL_CODE_SECOND);
2283+
} else if (has_fraction) {
2284+
/* fraction val itself is truncated away due to precision
2285+
* zero/null for intervals with no seconds component */
2286+
ERRH(stmt, "fraction in interval with no second component");
2287+
goto err_format;
2288+
}
2289+
if (type2bm_ivl & (1 << SQL_CODE_MINUTE)) {
2290+
if (ivl->intval.day_second.second) {
2291+
ivl->intval.day_second.minute =
2292+
ivl->intval.day_second.second / 60;
2293+
ivl->intval.day_second.second = secs % 60;
2294+
} else {
2295+
ivl->intval.day_second.minute = secs / 60;
2296+
assert(secs % 60 == 0);
2297+
}
2298+
fields_bm |= (1 << SQL_CODE_MINUTE);
2299+
}
2300+
if (type2bm_ivl & (1 << SQL_CODE_HOUR)) {
2301+
if (ivl->intval.day_second.minute) {
2302+
ivl->intval.day_second.hour =
2303+
ivl->intval.day_second.minute / 60;
2304+
ivl->intval.day_second.minute %= 60;
2305+
} else {
2306+
ivl->intval.day_second.hour = secs / 3600;
2307+
assert(secs % 3600 == 0);
2308+
}
2309+
fields_bm |= (1 << SQL_CODE_HOUR);
2310+
}
2311+
if (type2bm_ivl & (1 << SQL_CODE_DAY)) {
2312+
if (ivl->intval.day_second.hour) {
2313+
ivl->intval.day_second.day =
2314+
ivl->intval.day_second.hour / 24;
2315+
ivl->intval.day_second.hour %= 24;
2316+
} else {
2317+
ivl->intval.day_second.day = secs / (24 * 3600);
2318+
assert(secs % (24 * 3600) == 0);
2319+
}
2320+
fields_bm |= (1 << SQL_CODE_DAY);
2321+
}
22562322
}
22572323

22582324
/* Check that the ISO value has no fields set other than those allowed
22592325
* for the advertised type. Since the year_month and day_second form a
22602326
* union, this can't be done by checks against field values. */
2261-
if ((~type2bm[ivl->interval_type - 1]) & fields_bm) {
2327+
if (~type2bm_ivl & fields_bm) {
22622328
ERRH(stmt, "illegal fields (0x%hx) for interval type %hd (0x%hx).",
2263-
fields_bm, ctype, type2bm[ivl->interval_type - 1]);
2329+
fields_bm, ctype, type2bm_ivl);
22642330
goto err_format;
22652331
}
22662332

22672333
return ret;
2334+
2335+
err_overflow:
2336+
ERRH(stmt, "integer overflow while normalizing ISO8601 format [%zu] `"
2337+
LWPDL "`.", wstr->cnt, LWSTR(wstr));
2338+
RET_HDIAGS(stmt, SQL_STATE_22015);
22682339
err_parse:
22692340
ERRH(stmt, "unexpected current char `%c` in state %d.", *crr, state);
22702341
err_format:
@@ -2274,6 +2345,7 @@ static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
22742345

22752346
# undef ASSIGN_FIELD
22762347
# undef SET_BITMASK_OR_ERR
2348+
# undef ULONG_SAFE_ADD
22772349
}
22782350

22792351
/* Parse one field of the value.
@@ -2321,6 +2393,9 @@ static SQLRETURN parse_interval_field(esodbc_rec_st *rec, SQLUINTEGER limit,
23212393
return SQL_SUCCESS;
23222394
}
23232395

2396+
/* Interval precision:
2397+
* https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/interval-data-type-precision
2398+
*/
23242399
static SQLRETURN parse_interval_second(esodbc_rec_st *rec, SQLUINTEGER limit,
23252400
wstr_st *wstr, SQL_INTERVAL_STRUCT *ivl)
23262401
{
@@ -2921,8 +2996,6 @@ static size_t print_interval_iso8601(esodbc_rec_st *rec,
29212996
case SQL_IS_HOUR_TO_MINUTE:
29222997
case SQL_IS_HOUR_TO_SECOND:
29232998
case SQL_IS_MINUTE_TO_SECOND:
2924-
// TODO: compoound year to hour, ES/SQL-style?
2925-
// (see parse_interval_iso8601 note)
29262999
PRINT_FIELD(day_second.day, 'D', /* is time comp. */FALSE);
29273000
t_added = FALSE;
29283001
PRINT_FIELD(day_second.hour, 'H', /*is time*/TRUE);
@@ -2975,12 +3048,12 @@ static SQLRETURN interval_iso8601_to_sql(esodbc_rec_st *arec,
29753048

29763049
ivl_wstr.str = (SQLWCHAR *)wstr;
29773050
ivl_wstr.cnt = *chars_0 - 1;
2978-
ret = parse_interval_iso8601(arec, irec->es_type->data_type, &ivl_wstr,
3051+
ret = parse_interval_iso8601(irec, irec->es_type->data_type, &ivl_wstr,
29793052
&ivl);
29803053
if (! SQL_SUCCEEDED(ret)) {
29813054
return ret;
29823055
}
2983-
cnt = print_interval_sql(arec, &ivl, (SQLWCHAR *)lit);
3056+
cnt = print_interval_sql(irec, &ivl, (SQLWCHAR *)lit);
29843057
if (cnt <= 0) {
29853058
ERRH(stmt, "sql interval printing failed for ISO8601`" LWPDL "`.",
29863059
chars_0 - 1, wstr);

driver/handles.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ typedef struct desc_rec {
239239
/* "number of digits for an exact numeric type, the number of bits in the
240240
* mantissa (binary precision) for an approximate numeric type, or the
241241
* numbers of digits in the fractional seconds component "*/
242+
/* Intervals: "the number of decimal digits allowed in the fractional part
243+
* of the seconds value" */
242244
SQLSMALLINT precision;
243245
SQLSMALLINT rowver;
244246
SQLSMALLINT scale;

driver/queries.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,11 @@ static SQLRETURN attach_columns(esodbc_stmt_st *stmt, UJObject columns)
264264
rec->type = rec->es_type->sql_data_type;
265265
rec->datetime_interval_code = rec->es_type->sql_datetime_sub;
266266
rec->meta_type = rec->es_type->meta_type;
267+
/* set INTERVAL record's seconds precision */
268+
if (rec->meta_type == METATYPE_INTERVAL_WSEC) {
269+
assert(rec->precision == 0);
270+
rec->precision = rec->es_type->maximum_scale;
271+
}
267272
} else if (! dbc->no_types) {
268273
/* the connection doesn't have yet the types cached (this is the
269274
* caching call) and don't have access to the data itself either,

test/test_conversion_sql2c_interval.cc

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,7 +1260,7 @@ TEST_F(ConvertSQL2C_Interval, Iso86012Interval_minute_to_second)
12601260
ASSERT_TRUE(is.intval.day_second.fraction == 666000); // def prec: 6
12611261
}
12621262

1263-
TEST_F(ConvertSQL2C_Interval, Iso86012Interval_extra_field_22018)
1263+
TEST_F(ConvertSQL2C_Interval, Iso86012Interval_H_in_Min2Sec)
12641264
{
12651265
# undef SQL_VAL
12661266
# undef SQL
@@ -1284,8 +1284,12 @@ TEST_F(ConvertSQL2C_Interval, Iso86012Interval_extra_field_22018)
12841284
ASSERT_TRUE(SQL_SUCCEEDED(ret));
12851285

12861286
ret = SQLFetch(stmt);
1287-
ASSERT_FALSE(SQL_SUCCEEDED(ret));
1288-
assertState(L"22018");
1287+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
1288+
ASSERT_TRUE(is.interval_type == SQL_IS_MINUTE_TO_SECOND);
1289+
ASSERT_TRUE(is.interval_sign == SQL_FALSE);
1290+
ASSERT_TRUE(is.intval.day_second.minute == 104);
1291+
ASSERT_TRUE(is.intval.day_second.second == 55);
1292+
ASSERT_TRUE(is.intval.day_second.fraction == 666000); // def prec: 6
12891293
}
12901294

12911295
TEST_F(ConvertSQL2C_Interval, Iso86012Interval_invalid_char_22018)
@@ -1344,6 +1348,34 @@ TEST_F(ConvertSQL2C_Interval, Iso86012Interval_repeated_valid_22018)
13441348
assertState(L"22018");
13451349
}
13461350

1351+
TEST_F(ConvertSQL2C_Interval, Iso86012Interval_dangling_fraction_22018)
1352+
{
1353+
# undef SQL_VAL
1354+
# undef SQL
1355+
# define SQL_VAL "PT60.666S" // non-minute convertible
1356+
# define SQL "SELECT " SQL_VAL
1357+
1358+
const char json_answer[] = "\
1359+
{\
1360+
\"columns\": [\
1361+
{\"name\": \"" SQL "\", \"type\": \"INTERVAL_MINUTE\"}\
1362+
],\
1363+
\"rows\": [\
1364+
[\"" SQL_VAL "\"]\
1365+
]\
1366+
}\
1367+
";
1368+
prepareStatement(json_answer);
1369+
1370+
ret = SQLBindCol(stmt, /*col#*/1, SQL_C_INTERVAL_MINUTE, &is,
1371+
sizeof(is), &ind_len);
1372+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
1373+
1374+
ret = SQLFetch(stmt);
1375+
ASSERT_FALSE(SQL_SUCCEEDED(ret));
1376+
assertState(L"22018");
1377+
}
1378+
13471379
TEST_F(ConvertSQL2C_Interval, Iso86012Interval_plus_minus_22018)
13481380
{
13491381
# undef SQL_VAL
@@ -1540,15 +1572,6 @@ TEST_F(ConvertSQL2C_Interval, Iso8601_hour_to_second2WChar)
15401572
&ind_len);
15411573
ASSERT_TRUE(SQL_SUCCEEDED(ret));
15421574

1543-
SQLHDESC ard;
1544-
ret = SQLGetStmtAttr(stmt, SQL_ATTR_APP_ROW_DESC, &ard, 0, NULL);
1545-
ASSERT_TRUE(SQL_SUCCEEDED(ret));
1546-
ret = SQLSetDescField(ard, 1, SQL_DESC_PRECISION, (SQLPOINTER)3, 0);
1547-
ASSERT_TRUE(SQL_SUCCEEDED(ret));
1548-
// data ptr is reset by direct desc field setting
1549-
ret = SQLSetDescField(ard, 1, SQL_DESC_DATA_PTR, (SQLPOINTER)wbuff, 0);
1550-
ASSERT_TRUE(SQL_SUCCEEDED(ret));
1551-
15521575
ret = SQLFetch(stmt);
15531576
ASSERT_TRUE(SQL_SUCCEEDED(ret));
15541577
EXPECT_EQ(ind_len, sizeof(SQLWCHAR) * (sizeof("2:3:4.555") - /*\0*/1));
@@ -1578,19 +1601,11 @@ TEST_F(ConvertSQL2C_Interval, Iso8601_minute_to_second2Char)
15781601
&ind_len);
15791602
ASSERT_TRUE(SQL_SUCCEEDED(ret));
15801603

1581-
SQLHDESC ard;
1582-
ret = SQLGetStmtAttr(stmt, SQL_ATTR_APP_ROW_DESC, &ard, 0, NULL);
1583-
ASSERT_TRUE(SQL_SUCCEEDED(ret));
1584-
ret = SQLSetDescField(ard, 1, SQL_DESC_PRECISION, (SQLPOINTER)4, 0);
1585-
ASSERT_TRUE(SQL_SUCCEEDED(ret));
1586-
// data ptr is reset by direct desc field setting
1587-
ret = SQLSetDescField(ard, 1, SQL_DESC_DATA_PTR, (SQLPOINTER)buff, 0);
1588-
ASSERT_TRUE(SQL_SUCCEEDED(ret));
1589-
15901604
ret = SQLFetch(stmt);
1591-
ASSERT_TRUE(SQL_SUCCEEDED(ret));
1592-
EXPECT_EQ(ind_len, sizeof("3:4.5555") - /*\0*/1);
1593-
ASSERT_STREQ((char *)buff, "3:4.5555");
1605+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
1606+
// driver truncates it to default (current) ES/SQL seconds precision
1607+
EXPECT_EQ(ind_len, sizeof("3:4.555") - /*\0*/1);
1608+
ASSERT_STREQ((char *)buff, "3:4.555");
15941609
}
15951610

15961611
TEST_F(ConvertSQL2C_Interval, Iso8601_hour_to_minute2Char)

0 commit comments

Comments
 (0)