@@ -1778,11 +1778,11 @@ static inline SQLRETURN adjust_to_precision(esodbc_rec_st *rec,
1778
1778
}
1779
1779
}
1780
1780
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 ,
1782
1782
SQLUINTEGER * uint , char * sign ,
1783
1783
SQLUINTEGER * fraction , BOOL * has_fraction )
1784
1784
{
1785
- esodbc_stmt_st * stmt = arec -> desc -> hdr .stmt ;
1785
+ esodbc_stmt_st * stmt = rec -> desc -> hdr .stmt ;
1786
1786
char inc ;
1787
1787
wstr_st nr ;
1788
1788
int digits , fdigits ;
@@ -1815,7 +1815,7 @@ static SQLRETURN parse_iso8601_number(esodbc_rec_st *arec, wstr_st *wstr,
1815
1815
ERRH (stmt , "fraction value too large (%llu)." , ubint );
1816
1816
return SQL_ERROR ;
1817
1817
} else {
1818
- ret = adjust_to_precision (arec , & ubint , fdigits );
1818
+ ret = adjust_to_precision (rec , & ubint , fdigits );
1819
1819
assert (ubint < ULONG_MAX ); /* due to previous sanity checks */
1820
1820
* fraction = (SQLUINTEGER )ubint ;
1821
1821
}
@@ -1847,13 +1847,19 @@ static SQLRETURN parse_iso8601_number(esodbc_rec_st *arec, wstr_st *wstr,
1847
1847
return ret ;
1848
1848
}
1849
1849
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 ,
1854
1860
SQLSMALLINT ctype , wstr_st * wstr , SQL_INTERVAL_STRUCT * ivl )
1855
1861
{
1856
- esodbc_stmt_st * stmt = arec -> desc -> hdr .stmt ;
1862
+ esodbc_stmt_st * stmt = rec -> desc -> hdr .stmt ;
1857
1863
char sign ;
1858
1864
SQLWCHAR * crr , * end ;
1859
1865
wstr_st nr ;
@@ -1877,7 +1883,9 @@ static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
1877
1883
(1 << SQL_IS_HOUR ) | (1 << SQL_IS_MINUTE ) | (1 << SQL_IS_SECOND ),
1878
1884
(1 << SQL_IS_MINUTE ) | (1 << SQL_IS_SECOND ),
1879
1885
};
1886
+ uint16_t type2bm_ivl ; /* the type bit mask for the interval */
1880
1887
SQLRETURN ret ;
1888
+ SQLUINTEGER secs ; /* ~ond~ */
1881
1889
1882
1890
/* Sets a bit in a bitmask corresponding to one interval field, given
1883
1891
* `_ivl`, or errs if already set.
@@ -1910,6 +1918,15 @@ static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
1910
1918
DBGH(stmt, "field %d assigned value %lu.", _ivl_type, uint); \
1911
1919
state = saved; \
1912
1920
} 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)
1913
1930
1914
1931
/* the interval type will be used as bitmask indexes */
1915
1932
assert (0 < SQL_IS_YEAR && SQL_IS_MINUTE_TO_SECOND < 16 );
@@ -1954,7 +1971,7 @@ static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
1954
1971
}
1955
1972
nr .str = crr ;
1956
1973
nr .cnt = end - crr ;
1957
- ret = parse_iso8601_number (arec , & nr , & uint , & sign ,
1974
+ ret = parse_iso8601_number (rec , & nr , & uint , & sign ,
1958
1975
& fraction , & has_fraction );
1959
1976
if (! SQL_SUCCEEDED (ret )) {
1960
1977
goto err_format ;
@@ -2017,28 +2034,82 @@ static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
2017
2034
2018
2035
assert (0 < /*starts at 1*/ ivl -> interval_type &&
2019
2036
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
+ }
2030
2096
}
2031
2097
2032
2098
/* Check that the ISO value has no fields set other than those allowed
2033
2099
* for the advertised type. Since the year_month and day_second form a
2034
2100
* 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 ) {
2036
2102
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 );
2038
2104
goto err_format ;
2039
2105
}
2040
2106
2041
2107
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 );
2042
2113
err_parse :
2043
2114
ERRH (stmt , "unexpected current char `%c` in state %d." , * crr , state );
2044
2115
err_format :
@@ -2048,6 +2119,7 @@ static SQLRETURN parse_interval_iso8601(esodbc_rec_st *arec,
2048
2119
2049
2120
# undef ASSIGN_FIELD
2050
2121
# undef SET_BITMASK_OR_ERR
2122
+ # undef ULONG_SAFE_ADD
2051
2123
}
2052
2124
2053
2125
/* Parse one field of the value.
@@ -2095,6 +2167,9 @@ static SQLRETURN parse_interval_field(esodbc_rec_st *rec, SQLUINTEGER limit,
2095
2167
return SQL_SUCCESS ;
2096
2168
}
2097
2169
2170
+ /* Interval precision:
2171
+ * https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/interval-data-type-precision
2172
+ */
2098
2173
static SQLRETURN parse_interval_second (esodbc_rec_st * rec , SQLUINTEGER limit ,
2099
2174
wstr_st * wstr , SQL_INTERVAL_STRUCT * ivl )
2100
2175
{
@@ -2695,8 +2770,6 @@ static size_t print_interval_iso8601(esodbc_rec_st *rec,
2695
2770
case SQL_IS_HOUR_TO_MINUTE :
2696
2771
case SQL_IS_HOUR_TO_SECOND :
2697
2772
case SQL_IS_MINUTE_TO_SECOND :
2698
- // TODO: compoound year to hour, ES/SQL-style?
2699
- // (see parse_interval_iso8601 note)
2700
2773
PRINT_FIELD (day_second .day , 'D' , /* is time comp. */ FALSE);
2701
2774
t_added = FALSE;
2702
2775
PRINT_FIELD (day_second .hour , 'H' , /*is time*/ TRUE);
@@ -2749,12 +2822,12 @@ static SQLRETURN interval_iso8601_to_sql(esodbc_rec_st *arec,
2749
2822
2750
2823
ivl_wstr .str = (SQLWCHAR * )wstr ;
2751
2824
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 ,
2753
2826
& ivl );
2754
2827
if (! SQL_SUCCEEDED (ret )) {
2755
2828
return ret ;
2756
2829
}
2757
- cnt = print_interval_sql (arec , & ivl , (SQLWCHAR * )lit );
2830
+ cnt = print_interval_sql (irec , & ivl , (SQLWCHAR * )lit );
2758
2831
if (cnt <= 0 ) {
2759
2832
ERRH (stmt , "sql interval printing failed for ISO8601`" LWPDL "`." ,
2760
2833
chars_0 - 1 , wstr );
0 commit comments