Skip to content

Commit fe33ec2

Browse files
committed
Fix handling of size with DATE parameters (#178)
* fix size of DATE parameters This commit fixes the handling of the columns size of the DATE type parameters. The previous implementation always expected the size of an ISO8601 timestamp, since DATE and TIME vals area ultimately parsed as a timestamp. However the column size was passed through unchanged. With this commit, the size is read as passed in by the application and adjusted as needed. * address PR review notes, fixing logging messages - fix missing arguments to logging macros; - improve phrasing. * address PR notes: fix logging format specifier - s/%d/%zu (cherry picked from commit 24fb40e)
1 parent 26fe33c commit fe33ec2

File tree

4 files changed

+124
-42
lines changed

4 files changed

+124
-42
lines changed

driver/convert.c

+96-40
Original file line numberDiff line numberDiff line change
@@ -3118,24 +3118,26 @@ static int print_timestamp(TIMESTAMP_STRUCT *tss, BOOL iso8601,
31183118
tss->hour, tss->minute, tss->second,
31193119
/* fraction is always provided, but only printed if 'decdigits' */
31203120
decdigits, nsec);
3121+
if (n <= 0) {
3122+
return n;
3123+
}
3124+
31213125
if ((int)lim < n) {
31223126
n = (int)lim;
31233127
}
3124-
if (0 < n) {
3125-
if (iso8601) {
3126-
dest[DATE_TEMPLATE_LEN] = L'T';
3127-
/* The SQL column sizes are considered for ISO format too, to
3128-
* allow the case where the client app specifies a timestamp with
3129-
* non-zero seconds, but wants to cut those away in the parameter.
3130-
* The 'Z' would then be on top of the colsize. */
3131-
dest[n] = L'Z';
3132-
n ++;
3133-
dest[n] = L'\0';
3134-
}
3135-
DBG("printed UTC %s timestamp (colsz: %lu, decdig: %hd): "
3136-
"[%d] `" LWPDL "`.", iso8601 ? "ISO8601" : "SQL",
3137-
(SQLUINTEGER)colsize, decdigits, n, n, dest);
3128+
if (iso8601) {
3129+
dest[DATE_TEMPLATE_LEN] = L'T';
3130+
/* The SQL column sizes are considered for ISO format too, to
3131+
* allow the case where the client app specifies a timestamp with
3132+
* non-zero seconds, but wants to cut those away in the parameter.
3133+
* The 'Z' would then be on top of the colsize. */
3134+
dest[n] = L'Z';
3135+
n ++;
31383136
}
3137+
dest[n] = L'\0';
3138+
DBG("printed UTC %s timestamp (colsz: %lu, decdig: %hd): "
3139+
"[%d] `" LWPDL "`.", iso8601 ? "ISO8601" : "SQL",
3140+
(SQLUINTEGER)colsize, decdigits, n, n, dest);
31393141

31403142
return n;
31413143
}
@@ -4252,10 +4254,80 @@ static SQLRETURN struct_to_iso8601_timestamp(esodbc_stmt_st *stmt,
42524254
return SQL_SUCCESS;
42534255
}
42544256

4257+
/* apply corrections depending on the (column) size and decimal digits
4258+
* values given at binding time: nullify or trim the resulted string:
4259+
* https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/column-size
4260+
* */
4261+
static SQLRETURN size_decdigits_for_iso8601(esodbc_rec_st *irec,
4262+
SQLULEN *_colsize, SQLSMALLINT *_decdigits)
4263+
{
4264+
SQLULEN colsize;
4265+
SQLSMALLINT decdigits;
4266+
esodbc_stmt_st *stmt = HDRH(irec->desc)->stmt;
4267+
4268+
colsize = get_param_size(irec);
4269+
DBGH(stmt, "requested column size: %llu.", colsize);
4270+
4271+
decdigits = get_param_decdigits(irec);
4272+
DBGH(stmt, "requested decimal digits: %llu.", decdigits);
4273+
if (ESODBC_MAX_SEC_PRECISION < decdigits) {
4274+
WARNH(stmt, "requested decimal digits adjusted from %hd to %d (max).",
4275+
decdigits, ESODBC_MAX_SEC_PRECISION);
4276+
decdigits = ESODBC_MAX_SEC_PRECISION;
4277+
}
4278+
4279+
switch (irec->es_type->data_type) {
4280+
case SQL_TYPE_TIME:
4281+
if (colsize) {
4282+
if (colsize < TIME_TEMPLATE_LEN(0) ||
4283+
colsize == TIME_TEMPLATE_LEN(1) - 1 /* `:ss.`*/) {
4284+
ERRH(stmt, "invalid column size value: %llu; allowed: "
4285+
"8 or 9 + fractions count.", colsize);
4286+
RET_HDIAGS(stmt, SQL_STATE_HY104);
4287+
}
4288+
colsize += DATE_TEMPLATE_LEN + /* ` `/`T` */1;
4289+
}
4290+
break;
4291+
case SQL_TYPE_DATE:
4292+
/* if origin is a timestamp (struct or string), the time part
4293+
* needs to be zeroed. */
4294+
if (colsize) {
4295+
if (colsize != DATE_TEMPLATE_LEN) {
4296+
ERRH(stmt, "invalid column size value: %llu; allowed: "
4297+
"%zu.", colsize, DATE_TEMPLATE_LEN);
4298+
RET_HDIAGS(stmt, SQL_STATE_HY104);
4299+
}
4300+
colsize += /* ` `/`T` */1 + TIME_TEMPLATE_LEN(0);
4301+
}
4302+
if (decdigits) {
4303+
ERRH(stmt, "invalid decimal digits %hd for TIME type.",
4304+
decdigits);
4305+
RET_HDIAGS(stmt, SQL_STATE_HY104);
4306+
}
4307+
break;
4308+
case SQL_TYPE_TIMESTAMP:
4309+
if (colsize && (colsize < TIMESTAMP_NOSEC_TEMPLATE_LEN ||
4310+
colsize == 17 || colsize == 18)) {
4311+
ERRH(stmt, "invalid column size value: %llu; allowed: "
4312+
"16, 19 or 20 + fractions count.", colsize);
4313+
RET_HDIAGS(stmt, SQL_STATE_HY104);
4314+
}
4315+
break;
4316+
default:
4317+
assert(0);
4318+
}
4319+
4320+
DBGH(stmt, "applying: column size: %llu, decimal digits: %hd.",
4321+
colsize, decdigits);
4322+
*_colsize = colsize;
4323+
*_decdigits = decdigits;
4324+
return SQL_SUCCESS;
4325+
}
4326+
42554327
SQLRETURN c2sql_date_time(esodbc_rec_st *arec, esodbc_rec_st *irec,
42564328
SQLULEN pos, char *dest, size_t *len)
42574329
{
4258-
# define ZERO_TIME_Z "00:00:00Z"
4330+
static const wstr_st time_0_z = WSTR_INIT("00:00:00Z");
42594331
esodbc_stmt_st *stmt;
42604332
void *data_ptr;
42614333
SQLLEN *octet_len_ptr;
@@ -4283,24 +4355,9 @@ SQLRETURN c2sql_date_time(esodbc_rec_st *arec, esodbc_rec_st *irec,
42834355
/* pointer to app's buffer */
42844356
data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec);
42854357

4286-
/* apply corrections depending on the (column) size and decimal digits
4287-
* values given at binding time: nullify or trim the resulted string:
4288-
* https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/column-size
4289-
* */
4290-
colsize = get_param_size(irec);
4291-
DBGH(stmt, "requested column size: %llu.", colsize);
4292-
if (colsize && (colsize < sizeof("yyyy-mm-dd hh:mm") - 1 ||
4293-
colsize == 17 || colsize == 18)) {
4294-
ERRH(stmt, "invalid column size value: %llu; allowed: 16, 19, 20+f.",
4295-
colsize);
4296-
RET_HDIAGS(stmt, SQL_STATE_HY104);
4297-
}
4298-
decdigits = get_param_decdigits(irec);
4299-
DBGH(stmt, "requested decimal digits: %llu.", decdigits);
4300-
if (ESODBC_MAX_SEC_PRECISION < decdigits) {
4301-
WARNH(stmt, "requested decimal digits adjusted from %hd to %d (max).",
4302-
decdigits, ESODBC_MAX_SEC_PRECISION);
4303-
decdigits = ESODBC_MAX_SEC_PRECISION;
4358+
ret = size_decdigits_for_iso8601(irec, &colsize, &decdigits);
4359+
if (! SQL_SUCCEEDED(ret)) {
4360+
return ret;
43044361
}
43054362

43064363
/*INDENT-OFF*/
@@ -4315,7 +4372,7 @@ SQLRETURN c2sql_date_time(esodbc_rec_st *arec, esodbc_rec_st *irec,
43154372
}
43164373
/* disallow DATE <-> TIME conversions */
43174374
if ((irec->es_type->data_type == SQL_C_TYPE_TIME &&
4318-
format == SQL_C_TYPE_DATE) || (format == SQL_C_TYPE_TIME &&
4375+
format == SQL_TYPE_DATE) || (format == SQL_TYPE_TIME &&
43194376
irec->es_type->data_type == SQL_C_TYPE_DATE)) {
43204377
ERRH(stmt, "TIME-DATE conversions are not possible.");
43214378
RET_HDIAGS(stmt, SQL_STATE_22018);
@@ -4351,27 +4408,27 @@ SQLRETURN c2sql_date_time(esodbc_rec_st *arec, esodbc_rec_st *irec,
43514408
* expense. */
43524409
/* Adapt the resulting ISO8601 value to the target data type */
43534410
switch (irec->es_type->data_type) {
4354-
case SQL_C_TYPE_TIME:
4411+
case SQL_TYPE_TIME:
43554412
/* shift value + \0 upwards over the DATE component */
43564413
/* Note: by the book, non-0 fractional seconds in timestamp should
43574414
* lead to 22008 a failure. However, ES/SQL's TIME supports
43584415
* fractions, so will just ignore this provision. */
43594416
cnt -= DATE_TEMPLATE_LEN + /*'T'*/1;
43604417
wmemmove(wbuff, wbuff + DATE_TEMPLATE_LEN + /*'T'*/1, cnt + 1);
43614418
break;
4362-
case SQL_C_TYPE_DATE:
4419+
case SQL_TYPE_DATE:
43634420
/* if origin is a timestamp (struct or string), the time part
43644421
* needs to be zeroed. */
43654422
if (ctype == SQL_C_TYPE_TIMESTAMP ||
4366-
format == SQL_C_TYPE_TIMESTAMP) {
4423+
format == SQL_TYPE_TIMESTAMP) {
43674424
assert(ISO8601_TIMESTAMP_MIN_LEN <= cnt);
43684425
wmemcpy(wbuff + DATE_TEMPLATE_LEN + /*'T'*/1,
4369-
MK_WPTR(ZERO_TIME_Z), sizeof(ZERO_TIME_Z) /*+\0*/);
4426+
(wchar_t *)time_0_z.str, time_0_z.cnt + /*\0*/1);
43704427
cnt = ISO8601_TIMESTAMP_MIN_LEN;
43714428
}
43724429
break;
43734430
default:
4374-
assert(irec->es_type->data_type == SQL_C_TYPE_TIMESTAMP);
4431+
assert(irec->es_type->data_type == SQL_TYPE_TIMESTAMP);
43754432
}
43764433
DBGH(stmt, "converted value: [%zu] `" LWPDL "`.", cnt, cnt, wbuff);
43774434

@@ -4381,7 +4438,6 @@ SQLRETURN c2sql_date_time(esodbc_rec_st *arec, esodbc_rec_st *irec,
43814438

43824439
dest[(*len) ++] = '"';
43834440
return SQL_SUCCESS;
4384-
# undef ZERO_TIME_Z
43854441
}
43864442

43874443
/* parses an interval literal string from app's char/wchar_t buffer */

driver/util.h

+2
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,8 @@ BOOL TEST_API metadata_id_escape(wstr_st *src, wstr_st *dst, BOOL force);
332332
(sizeof("hh:mm:ss") - /*\0*/1 + /*'.'*/!!prec + prec)
333333
#define TIMESTAMP_TEMPLATE_LEN(prec) \
334334
(DATE_TEMPLATE_LEN + /*' '*/1 + TIME_TEMPLATE_LEN(prec))
335+
#define TIMESTAMP_NOSEC_TEMPLATE_LEN \
336+
(DATE_TEMPLATE_LEN + /*' '*/1 + sizeof("hh:mm") - /*\0*/1)
335337

336338
#endif /* __UTIL_H__ */
337339

test/test_conversion_c2sql_date.cc

+24
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ TEST_F(ConvertC2SQL_Date, Date2Date)
3636
"\"value\": \"1234-12-23T00:00:00Z\"}]");
3737
}
3838

39+
TEST_F(ConvertC2SQL_Date, CStr_Date2Date_size10)
40+
{
41+
SQLCHAR val[] = "2000-01-01"; // treated as utc, since apply_tz==FALSE
42+
ret = SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR,
43+
SQL_TYPE_DATE, /*size*/10, /*decdigits*/0, val, sizeof(val),
44+
/*IndLen*/NULL);
45+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
46+
47+
assertRequest("[{\"type\": \"DATE\", "
48+
"\"value\": \"2000-01-01T00:00:00Z\"}]");
49+
}
50+
3951
TEST_F(ConvertC2SQL_Date, CStr_Date2Date)
4052
{
4153
SQLCHAR val[] = "2000-01-01"; // treated as utc, since apply_tz==FALSE
@@ -100,6 +112,18 @@ TEST_F(ConvertC2SQL_Date, WStr_Timestamp2Date)
100112
"\"value\": \"1234-12-23T00:00:00Z\"}]");
101113
}
102114

115+
TEST_F(ConvertC2SQL_Date, WStr_Timestamp2Date_size10)
116+
{
117+
SQLWCHAR val[] = L"1234-12-23T12:34:56.7890123Z";
118+
ret = SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_WCHAR,
119+
SQL_TYPE_DATE, /*size*/10, /*decdigits*/0, val, sizeof(val),
120+
/*IndLen*/NULL);
121+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
122+
123+
assertRequest("[{\"type\": \"DATE\", "
124+
"\"value\": \"1234-12-23T00:00:00Z\"}]");
125+
}
126+
103127
/* note: test name used in test */
104128
TEST_F(ConvertC2SQL_Date, Timestamp2Date)
105129
{

test/test_conversion_c2sql_time.cc

+2-2
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,11 @@ TEST_F(ConvertC2SQL_Time, WStr_Timestamp2Time_colsize_16)
7575
{
7676
SQLWCHAR val[] = L"1234-12-23T12:34:56.7890123Z";
7777
ret = SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_WCHAR,
78-
SQL_TYPE_TIME, /*size*/16, /*decdigits*/0, val, sizeof(val),
78+
SQL_TYPE_TIME, /*size*/8, /*decdigits*/0, val, sizeof(val),
7979
/*IndLen*/NULL);
8080
ASSERT_TRUE(SQL_SUCCEEDED(ret));
8181

82-
assertRequest("[{\"type\": \"TIME\", \"value\": \"12:34Z\"}]");
82+
assertRequest("[{\"type\": \"TIME\", \"value\": \"12:34:56Z\"}]");
8383
}
8484

8585
/* note: test name used in test */

0 commit comments

Comments
 (0)