Skip to content

Commit 24fb40e

Browse files
authored
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
1 parent 29376ec commit 24fb40e

File tree

4 files changed

+124
-42
lines changed

4 files changed

+124
-42
lines changed

driver/convert.c

Lines changed: 96 additions & 40 deletions
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
}
@@ -4254,10 +4256,80 @@ static SQLRETURN struct_to_iso8601_timestamp(esodbc_stmt_st *stmt,
42544256
return SQL_SUCCESS;
42554257
}
42564258

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

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

43084365
/*INDENT-OFF*/
@@ -4317,7 +4374,7 @@ SQLRETURN c2sql_date_time(esodbc_rec_st *arec, esodbc_rec_st *irec,
43174374
}
43184375
/* disallow DATE <-> TIME conversions */
43194376
if ((irec->es_type->data_type == SQL_C_TYPE_TIME &&
4320-
format == SQL_C_TYPE_DATE) || (format == SQL_C_TYPE_TIME &&
4377+
format == SQL_TYPE_DATE) || (format == SQL_TYPE_TIME &&
43214378
irec->es_type->data_type == SQL_C_TYPE_DATE)) {
43224379
ERRH(stmt, "TIME-DATE conversions are not possible.");
43234380
RET_HDIAGS(stmt, SQL_STATE_22018);
@@ -4353,27 +4410,27 @@ SQLRETURN c2sql_date_time(esodbc_rec_st *arec, esodbc_rec_st *irec,
43534410
* expense. */
43544411
/* Adapt the resulting ISO8601 value to the target data type */
43554412
switch (irec->es_type->data_type) {
4356-
case SQL_C_TYPE_TIME:
4413+
case SQL_TYPE_TIME:
43574414
/* shift value + \0 upwards over the DATE component */
43584415
/* Note: by the book, non-0 fractional seconds in timestamp should
43594416
* lead to 22008 a failure. However, ES/SQL's TIME supports
43604417
* fractions, so will just ignore this provision. */
43614418
cnt -= DATE_TEMPLATE_LEN + /*'T'*/1;
43624419
wmemmove(wbuff, wbuff + DATE_TEMPLATE_LEN + /*'T'*/1, cnt + 1);
43634420
break;
4364-
case SQL_C_TYPE_DATE:
4421+
case SQL_TYPE_DATE:
43654422
/* if origin is a timestamp (struct or string), the time part
43664423
* needs to be zeroed. */
43674424
if (ctype == SQL_C_TYPE_TIMESTAMP ||
4368-
format == SQL_C_TYPE_TIMESTAMP) {
4425+
format == SQL_TYPE_TIMESTAMP) {
43694426
assert(ISO8601_TIMESTAMP_MIN_LEN <= cnt);
43704427
wmemcpy(wbuff + DATE_TEMPLATE_LEN + /*'T'*/1,
4371-
MK_WPTR(ZERO_TIME_Z), sizeof(ZERO_TIME_Z) /*+\0*/);
4428+
(wchar_t *)time_0_z.str, time_0_z.cnt + /*\0*/1);
43724429
cnt = ISO8601_TIMESTAMP_MIN_LEN;
43734430
}
43744431
break;
43754432
default:
4376-
assert(irec->es_type->data_type == SQL_C_TYPE_TIMESTAMP);
4433+
assert(irec->es_type->data_type == SQL_TYPE_TIMESTAMP);
43774434
}
43784435
DBGH(stmt, "converted value: [%zu] `" LWPDL "`.", cnt, cnt, wbuff);
43794436

@@ -4383,7 +4440,6 @@ SQLRETURN c2sql_date_time(esodbc_rec_st *arec, esodbc_rec_st *irec,
43834440

43844441
dest[(*len) ++] = '"';
43854442
return SQL_SUCCESS;
4386-
# undef ZERO_TIME_Z
43874443
}
43884444

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

driver/util.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,8 @@ char *cstr_hex_dump(const cstr_st *buff);
350350
(sizeof("hh:mm:ss") - /*\0*/1 + /*'.'*/!!prec + prec)
351351
#define TIMESTAMP_TEMPLATE_LEN(prec) \
352352
(DATE_TEMPLATE_LEN + /*' '*/1 + TIME_TEMPLATE_LEN(prec))
353+
#define TIMESTAMP_NOSEC_TEMPLATE_LEN \
354+
(DATE_TEMPLATE_LEN + /*' '*/1 + sizeof("hh:mm") - /*\0*/1)
353355

354356

355357
#endif /* __UTIL_H__ */

test/test_conversion_c2sql_date.cc

Lines changed: 24 additions & 0 deletions
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

Lines changed: 2 additions & 2 deletions
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)