Skip to content

Commit c0ace37

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 8a5ca9d commit c0ace37

File tree

4 files changed

+158
-50
lines changed

4 files changed

+158
-50
lines changed

driver/connect.c

+16-1
Original file line numberDiff line numberDiff line change
@@ -1913,7 +1913,7 @@ static void *copy_types_rows(esodbc_dbc_st *dbc, estype_row_st *type_row,
19131913
SQLWCHAR *pos;
19141914
int c;
19151915
SQLULEN i;
1916-
SQLSMALLINT sql_type;
1916+
SQLSMALLINT sql_type, sec_prec;
19171917

19181918
/* pointer to start position where the strings will be copied in */
19191919
pos = (SQLWCHAR *)&types[rows_fetched];
@@ -1946,6 +1946,7 @@ static void *copy_types_rows(esodbc_dbc_st *dbc, estype_row_st *type_row,
19461946
} \
19471947
} while (0)
19481948

1949+
sec_prec = -1;
19491950
for (i = 0; i < rows_fetched; i ++) {
19501951
/* copy row data */
19511952
ES_TYPES_COPY_WSTR(type_name);
@@ -2028,6 +2029,20 @@ static void *copy_types_rows(esodbc_dbc_st *dbc, estype_row_st *type_row,
20282029
&types[i].sql_datetime_sub);
20292030

20302031
set_display_size(types + i);
2032+
2033+
/* There's no explicit coverage in the docs on how to communicate the
2034+
* interval seconds precision, so will use the scales in
2035+
* SQLGetTypeInfo() for that, just like for Timestamp type;
2036+
* furthermore, will use Timestamp's value, since it's the same
2037+
* seconds precision across all ES/SQL types with seconds component. */
2038+
if (types[i].data_type == SQL_TYPE_TIMESTAMP) {
2039+
assert(sec_prec < 0);
2040+
sec_prec = types[i].maximum_scale;
2041+
} else if (types[i].meta_type == METATYPE_INTERVAL_WSEC) {
2042+
assert(0 < sec_prec);
2043+
types[i].maximum_scale = sec_prec;
2044+
types[i].minimum_scale = types[i].maximum_scale;
2045+
}
20312046
}
20322047

20332048
#undef ES_TYPES_COPY_INT

driver/convert.c

+98-25
Original file line numberDiff line numberDiff line change
@@ -1778,11 +1778,11 @@ static inline SQLRETURN adjust_to_precision(esodbc_rec_st *rec,
17781778
}
17791779
}
17801780

1781-
static SQLRETURN parse_iso8601_number(esodbc_rec_st *arec, wstr_st *wstr,
1781+
static SQLRETURN parse_iso8601_number(esodbc_rec_st *rec, wstr_st *wstr,
17821782
SQLUINTEGER *uint, char *sign,
17831783
SQLUINTEGER *fraction, BOOL *has_fraction)
17841784
{
1785-
esodbc_stmt_st *stmt = arec->desc->hdr.stmt;
1785+
esodbc_stmt_st *stmt = rec->desc->hdr.stmt;
17861786
char inc;
17871787
wstr_st nr;
17881788
int digits, fdigits;
@@ -1815,7 +1815,7 @@ static SQLRETURN parse_iso8601_number(esodbc_rec_st *arec, wstr_st *wstr,
18151815
ERRH(stmt, "fraction value too large (%llu).", ubint);
18161816
return SQL_ERROR;
18171817
} else {
1818-
ret = adjust_to_precision(arec, &ubint, fdigits);
1818+
ret = adjust_to_precision(rec, &ubint, fdigits);
18191819
assert(ubint < ULONG_MAX); /* due to previous sanity checks */
18201820
*fraction = (SQLUINTEGER)ubint;
18211821
}
@@ -1847,13 +1847,19 @@ static SQLRETURN parse_iso8601_number(esodbc_rec_st *arec, wstr_st *wstr,
18471847
return ret;
18481848
}
18491849

1850-
/* parse an ISO8601 period value
1851-
* Elasticsearch'es implementation deviates slightly, hiding the day field:
1852-
* `INTERVAL '1 1' DAY TO HOUR` -> `PT25H` instead of `P1DT1H`. */
1853-
static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
1850+
/* Parse an ISO8601 period value.
1851+
* Elasticsearch'es implementation deviates from the standard by, for example:
1852+
* - demoting the day field: `INTERVAL '1 1' DAY TO HOUR` ->
1853+
* `PT25H` instead of `P1DT1H`; `INTERVAL '1' DAY` -> `PT24H` vs. `P1D`;
1854+
* - promoting the hour field: `INTERVAL '61:59' MINUTE TO SECOND` ->
1855+
* `PT1H1M59S` instead of `PT61M59S`; `INTERVAL '60' MINUTE` -> `PT1H` vs.
1856+
* `PT60M`.
1857+
* - promoting the minute field: `INTERVAL '61' SECOND` -> `PT1M1S` vs.
1858+
* `PT61S` */
1859+
static SQLRETURN parse_interval_iso8601(esodbc_rec_st *rec,
18541860
SQLSMALLINT ctype, wstr_st *wstr, SQL_INTERVAL_STRUCT *ivl)
18551861
{
1856-
esodbc_stmt_st *stmt = arec->desc->hdr.stmt;
1862+
esodbc_stmt_st *stmt = rec->desc->hdr.stmt;
18571863
char sign;
18581864
SQLWCHAR *crr, *end;
18591865
wstr_st nr;
@@ -1877,7 +1883,9 @@ static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
18771883
(1 << SQL_IS_HOUR) | (1 << SQL_IS_MINUTE) | (1 << SQL_IS_SECOND),
18781884
(1 << SQL_IS_MINUTE) | (1 << SQL_IS_SECOND),
18791885
};
1886+
uint16_t type2bm_ivl; /* the type bit mask for the interval */
18801887
SQLRETURN ret;
1888+
SQLUINTEGER secs; /* ~ond~ */
18811889

18821890
/* Sets a bit in a bitmask corresponding to one interval field, given
18831891
* `_ivl`, or errs if already set.
@@ -1910,6 +1918,15 @@ static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
19101918
DBGH(stmt, "field %d assigned value %lu.", _ivl_type, uint); \
19111919
state = saved; \
19121920
} while (0)
1921+
/* Safe ulong addition */
1922+
# define ULONG_SAFE_ADD(_to, _from) \
1923+
do { \
1924+
if (ULONG_MAX - (_from) < _to) { \
1925+
goto err_overflow; \
1926+
} else { \
1927+
_to += _from; \
1928+
} \
1929+
} while (0)
19131930

19141931
/* the interval type will be used as bitmask indexes */
19151932
assert(0 < SQL_IS_YEAR && SQL_IS_MINUTE_TO_SECOND < 16);
@@ -1954,7 +1971,7 @@ static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
19541971
}
19551972
nr.str = crr;
19561973
nr.cnt = end - crr;
1957-
ret = parse_iso8601_number(arec, &nr, &uint, &sign,
1974+
ret = parse_iso8601_number(rec, &nr, &uint, &sign,
19581975
&fraction, &has_fraction);
19591976
if (! SQL_SUCCEEDED(ret)) {
19601977
goto err_format;
@@ -2017,28 +2034,82 @@ static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
20172034

20182035
assert(0 < /*starts at 1*/ ivl->interval_type &&
20192036
ivl->interval_type < 8 * sizeof(type2bm)/sizeof(type2bm[0]));
2020-
/* reinstate the day field merged by ES in the hour field when:
2021-
* - the day field hasn't been set;
2022-
* - it is an interval with day component;
2023-
* - the hour field overflows a day/24h */
2024-
if (((1 << SQL_IS_DAY) & fields_bm) == 0 &&
2025-
(type2bm[ivl->interval_type - 1] & (1 << SQL_IS_DAY)) &&
2026-
24 <= ivl->intval.day_second.hour) {
2027-
ivl->intval.day_second.day = ivl->intval.day_second.hour / 24;
2028-
ivl->intval.day_second.hour = ivl->intval.day_second.hour % 24;
2029-
fields_bm |= 1 << SQL_IS_DAY;
2037+
2038+
type2bm_ivl = type2bm[ivl->interval_type - 1];
2039+
/* If the expression set fields not directly relevant to the interval AND
2040+
* the interval is not of type year/-/month one (which does seem to
2041+
* conform to the standard), rebalance the member values as expected by
2042+
* the interval type. */
2043+
if ((type2bm_ivl != fields_bm) && ((type2bm_ivl &
2044+
((1 << SQL_CODE_YEAR) | (1 << SQL_CODE_MONTH))) == 0)) {
2045+
secs = ivl->intval.day_second.second;
2046+
ULONG_SAFE_ADD(secs, 60 * ivl->intval.day_second.minute); //...
2047+
ULONG_SAFE_ADD(secs, 3600 * ivl->intval.day_second.hour);
2048+
ULONG_SAFE_ADD(secs, 24 * 3600 * ivl->intval.day_second.day);
2049+
/* clear everything set, but reinstate any set fractions */
2050+
fields_bm = 0;
2051+
memset(&ivl->intval.day_second, 0, sizeof(ivl->intval.day_second));
2052+
ivl->intval.day_second.fraction = fraction;
2053+
2054+
if (type2bm_ivl & (1 << SQL_CODE_SECOND)) {
2055+
ivl->intval.day_second.second = secs;
2056+
fields_bm |= (1 << SQL_CODE_SECOND);
2057+
} else if (has_fraction) {
2058+
/* fraction val itself is truncated away due to precision
2059+
* zero/null for intervals with no seconds component */
2060+
ERRH(stmt, "fraction in interval with no second component");
2061+
goto err_format;
2062+
}
2063+
if (type2bm_ivl & (1 << SQL_CODE_MINUTE)) {
2064+
if (ivl->intval.day_second.second) {
2065+
ivl->intval.day_second.minute =
2066+
ivl->intval.day_second.second / 60;
2067+
ivl->intval.day_second.second = secs % 60;
2068+
} else {
2069+
ivl->intval.day_second.minute = secs / 60;
2070+
assert(secs % 60 == 0);
2071+
}
2072+
fields_bm |= (1 << SQL_CODE_MINUTE);
2073+
}
2074+
if (type2bm_ivl & (1 << SQL_CODE_HOUR)) {
2075+
if (ivl->intval.day_second.minute) {
2076+
ivl->intval.day_second.hour =
2077+
ivl->intval.day_second.minute / 60;
2078+
ivl->intval.day_second.minute %= 60;
2079+
} else {
2080+
ivl->intval.day_second.hour = secs / 3600;
2081+
assert(secs % 3600 == 0);
2082+
}
2083+
fields_bm |= (1 << SQL_CODE_HOUR);
2084+
}
2085+
if (type2bm_ivl & (1 << SQL_CODE_DAY)) {
2086+
if (ivl->intval.day_second.hour) {
2087+
ivl->intval.day_second.day =
2088+
ivl->intval.day_second.hour / 24;
2089+
ivl->intval.day_second.hour %= 24;
2090+
} else {
2091+
ivl->intval.day_second.day = secs / (24 * 3600);
2092+
assert(secs % (24 * 3600) == 0);
2093+
}
2094+
fields_bm |= (1 << SQL_CODE_DAY);
2095+
}
20302096
}
20312097

20322098
/* Check that the ISO value has no fields set other than those allowed
20332099
* for the advertised type. Since the year_month and day_second form a
20342100
* union, this can't be done by checks against field values. */
2035-
if ((~type2bm[ivl->interval_type - 1]) & fields_bm) {
2101+
if (~type2bm_ivl & fields_bm) {
20362102
ERRH(stmt, "illegal fields (0x%hx) for interval type %hd (0x%hx).",
2037-
fields_bm, ctype, type2bm[ivl->interval_type - 1]);
2103+
fields_bm, ctype, type2bm_ivl);
20382104
goto err_format;
20392105
}
20402106

20412107
return ret;
2108+
2109+
err_overflow:
2110+
ERRH(stmt, "integer overflow while normalizing ISO8601 format [%zu] `"
2111+
LWPDL "`.", wstr->cnt, LWSTR(wstr));
2112+
RET_HDIAGS(stmt, SQL_STATE_22015);
20422113
err_parse:
20432114
ERRH(stmt, "unexpected current char `%c` in state %d.", *crr, state);
20442115
err_format:
@@ -2048,6 +2119,7 @@ static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
20482119

20492120
# undef ASSIGN_FIELD
20502121
# undef SET_BITMASK_OR_ERR
2122+
# undef ULONG_SAFE_ADD
20512123
}
20522124

20532125
/* Parse one field of the value.
@@ -2095,6 +2167,9 @@ static SQLRETURN parse_interval_field(esodbc_rec_st *rec, SQLUINTEGER limit,
20952167
return SQL_SUCCESS;
20962168
}
20972169

2170+
/* Interval precision:
2171+
* https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/interval-data-type-precision
2172+
*/
20982173
static SQLRETURN parse_interval_second(esodbc_rec_st *rec, SQLUINTEGER limit,
20992174
wstr_st *wstr, SQL_INTERVAL_STRUCT *ivl)
21002175
{
@@ -2695,8 +2770,6 @@ static size_t print_interval_iso8601(esodbc_rec_st *rec,
26952770
case SQL_IS_HOUR_TO_MINUTE:
26962771
case SQL_IS_HOUR_TO_SECOND:
26972772
case SQL_IS_MINUTE_TO_SECOND:
2698-
// TODO: compoound year to hour, ES/SQL-style?
2699-
// (see parse_interval_iso8601 note)
27002773
PRINT_FIELD(day_second.day, 'D', /* is time comp. */FALSE);
27012774
t_added = FALSE;
27022775
PRINT_FIELD(day_second.hour, 'H', /*is time*/TRUE);
@@ -2749,12 +2822,12 @@ static SQLRETURN interval_iso8601_to_sql(esodbc_rec_st *arec,
27492822

27502823
ivl_wstr.str = (SQLWCHAR *)wstr;
27512824
ivl_wstr.cnt = *chars_0 - 1;
2752-
ret = parse_interval_iso8601(arec, irec->es_type->data_type, &ivl_wstr,
2825+
ret = parse_interval_iso8601(irec, irec->es_type->data_type, &ivl_wstr,
27532826
&ivl);
27542827
if (! SQL_SUCCEEDED(ret)) {
27552828
return ret;
27562829
}
2757-
cnt = print_interval_sql(arec, &ivl, (SQLWCHAR *)lit);
2830+
cnt = print_interval_sql(irec, &ivl, (SQLWCHAR *)lit);
27582831
if (cnt <= 0) {
27592832
ERRH(stmt, "sql interval printing failed for ISO8601`" LWPDL "`.",
27602833
chars_0 - 1, wstr);

driver/queries.c

+5
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ static SQLRETURN attach_columns(esodbc_stmt_st *stmt, UJObject columns)
159159
rec->type = rec->es_type->sql_data_type;
160160
rec->datetime_interval_code = rec->es_type->sql_datetime_sub;
161161
rec->meta_type = rec->es_type->meta_type;
162+
/* set INTERVAL record's seconds precision */
163+
if (rec->meta_type == METATYPE_INTERVAL_WSEC) {
164+
assert(rec->precision == 0);
165+
rec->precision = rec->es_type->maximum_scale;
166+
}
162167
} else if (! dbc->no_types) {
163168
/* the connection doesn't have yet the types cached (this is the
164169
* caching call) and don't have access to the data itself either,

test/test_conversion_sql2c_interval.cc

+39-24
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)