From 0e8ad44a1556525b71293813af27ab0a9a605daf Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Tue, 10 Jun 2014 13:35:05 +0300 Subject: [PATCH] Rewrite the conversion functions between strings and SQL_NUMERIC_STRUCT. Aside from making the functions more readable and faster, this fixes a number of bugs: 1. When converting a large number from decimal to binary, the least significant byte might be wrong (reported by Walter Couto) 2. In binary->decimal conversion, a number with small precision but large scale caused a "Floating point exception". 3. In binary->decimal conversion, building the final string was wrong, picking up "digits" from outside the reserved stack space, if scale was larger than 40 4. The param_string buffer allocated for the result of binary->decimal conversion was too small. Also add a test cases for all of these bugs. --- convert.c | 360 ++++++++++++++++++++------------------ test/Makefile.in | 2 +- test/expected/numeric.out | 61 +++++++ test/src/numeric-test.c | 223 +++++++++++++++++++++++ 4 files changed, 470 insertions(+), 176 deletions(-) create mode 100644 test/expected/numeric.out create mode 100644 test/src/numeric-test.c diff --git a/convert.c b/convert.c index 8871216..ae18250 100644 --- a/convert.c +++ b/convert.c @@ -215,6 +215,8 @@ static unsigned ODBCINT64 ATOI64U(const char *val) #endif /* WIN32 */ #endif /* ODBCINT64 */ +static void parse_to_numeric_struct(const char *wv, SQL_NUMERIC_STRUCT *ns, BOOL *overflow); + /* * TIMESTAMP <-----> SIMPLE_TIME * precision support since 7.2. @@ -1595,80 +1597,21 @@ inolog("2stime fr=%d\n", std_time.fr); break; #if (ODBCVER >= 0x0300) - case SQL_C_NUMERIC: - { - SQL_NUMERIC_STRUCT *ns; - int i, nlen, bit, hval, tv, dig, sta, olen; - char calv[SQL_MAX_NUMERIC_LEN * 3]; - const char *wv; - BOOL dot_exist; - - len = sizeof(SQL_NUMERIC_STRUCT); - if (bind_size > 0) - ns = (SQL_NUMERIC_STRUCT *) rgbValueBindRow; - else - ns = (SQL_NUMERIC_STRUCT *) rgbValue + bind_row; - for (wv = neut_str; *wv && isspace((unsigned char) *wv); wv++) - ; - ns->sign = 1; - if (*wv == '-') - { - ns->sign = 0; - wv++; - } - else if (*wv == '+') - wv++; - while (*wv == '0') wv++; - ns->precision = 0; - ns->scale = 0; - for (nlen = 0, dot_exist = FALSE;; wv++) - { - if (*wv == '.') - { - if (dot_exist) - break; - dot_exist = TRUE; - } - else if (*wv == '\0' || !isdigit((unsigned char) *wv)) - break; - else - { - if (dot_exist) - ns->scale++; - ns->precision++; - calv[nlen++] = *wv; - } - } - memset(ns->val, 0, sizeof(ns->val)); - for (hval = 0, bit = 1L, sta = 0, olen = 0; sta < nlen;) - { - for (dig = 0, i = sta; i < nlen; i++) + case SQL_C_NUMERIC: { - tv = dig * 10 + calv[i] - '0'; - dig = tv % 2; - calv[i] = tv / 2 + '0'; - if (i == sta && tv < 2) - sta++; - } - if (dig > 0) - hval |= bit; - bit <<= 1; - if (bit >= (1L << 8)) - { - ns->val[olen++] = hval; - hval = 0; - bit = 1L; - if (olen >= SQL_MAX_NUMERIC_LEN - 1) - { - ns->scale = sta - ns->precision; - break; - } + SQL_NUMERIC_STRUCT *ns; + BOOL overflowed; + + if (bind_size > 0) + ns = (SQL_NUMERIC_STRUCT *) rgbValueBindRow; + else + ns = (SQL_NUMERIC_STRUCT *) rgbValue + bind_row; + + parse_to_numeric_struct(neut_str, ns, &overflowed); + if (overflowed) + result = COPY_RESULT_TRUNCATED; } - } - if (hval && olen < SQL_MAX_NUMERIC_LEN - 1) - ns->val[olen++] = hval; - } - break; + break; #endif /* ODBCVER */ case SQL_C_SSHORT: @@ -3829,136 +3772,201 @@ cleanup: } #if (ODBCVER >= 0x0300) -static BOOL + +/* + * With SQL_MAX_NUMERIC_LEN = 16, the highest representable number is + * 2^128 - 1, which fits in 39 digits. + */ +#define MAX_NUMERIC_DIGITS 39 + +/* + * Convert a SQL_NUMERIC_STRUCT into string representation. + */ +static void ResolveNumericParam(const SQL_NUMERIC_STRUCT *ns, char *chrform) { - static const int prec[] = {1, 3, 5, 8, 10, 13, 15, 17, 20, 22, 25, 27, 29, 32, 34, 37, 39}; - Int4 i, j, k, ival, vlen, len, newlen; - UCHAR calv[40]; + Int4 i, vlen, len, newlen; const UCHAR *val = (const UCHAR *) ns->val; - BOOL next_figure; + UCHAR vals[SQL_MAX_NUMERIC_LEN]; + int lastnonzero; + UCHAR calv[MAX_NUMERIC_DIGITS]; + int precision; inolog("C_NUMERIC [prec=%d scale=%d]", ns->precision, ns->scale); + if (0 == ns->precision) { strcpy(chrform, "0"); - return TRUE; - } - else if (ns->precision < prec[sizeof(Int4)]) - { - for (i = 0, ival = 0; i < sizeof(Int4) && prec[i] <= ns->precision; i++) - { -inolog("(%d)", val[i]); - ival += (val[i] << (8 * i)); /* ns->val is little endian */ - } -inolog(" ival=%d,%d", ival, (val[3] << 24) | (val[2] << 16) | (val[1] << 8) | val[0]); - if (0 == ns->scale) - { - if (0 == ns->sign) - ival *= -1; - sprintf(chrform, "%d", ival); - } - else if (ns->scale > 0) - { - Int4 i, div, o1val, o2val; - - for (i = 0, div = 1; i < ns->scale; i++) - div *= 10; - o1val = ival / div; - o2val = ival % div; - if (0 == ns->sign) - sprintf(chrform, "-%d.%0*d", o1val, ns->scale, o2val); - else - sprintf(chrform, "%d.%0*d", o1val, ns->scale, o2val); - } -inolog(" convval=%s\n", chrform); - return TRUE; + return; } - for (i = 0; i < SQL_MAX_NUMERIC_LEN && prec[i] <= ns->precision; i++) - ; - vlen = i; + precision = ns->precision; + if (precision > MAX_NUMERIC_DIGITS) + precision = MAX_NUMERIC_DIGITS; + + /* + * The representation in SQL_NUMERIC_STRUCT is 16 bytes with most + * significant byte first. Make a working copy. + */ + memcpy(vals, val, SQL_MAX_NUMERIC_LEN); + + vlen = SQL_MAX_NUMERIC_LEN; len = 0; - memset(calv, 0, sizeof(calv)); -inolog(" len1=%d", vlen); - for (i = vlen - 1; i >= 0; i--) + do { - for (j = len - 1; j >= 0; j--) - { - if (!calv[j]) - continue; - ival = (((Int4)calv[j]) << 8); - calv[j] = (ival % 10); - ival /= 10; - calv[j + 1] += (ival % 10); - ival /= 10; - calv[j + 2] += (ival % 10); - ival /= 10; - calv[j + 3] += ival; - for (k = j;; k++) - { - next_figure = FALSE; - if (calv[k] > 0) - { - if (k >= len) - len = k + 1; - while (calv[k] > 9) - { - calv[k + 1]++; - calv[k] -= 10; - next_figure = TRUE; - } - } - if (k >= j + 3 && !next_figure) - break; - } - } - ival = val[i]; - if (!ival) - continue; - calv[0] += (ival % 10); - ival /= 10; - calv[1] += (ival % 10); - ival /= 10; - calv[2] += ival; - for (j = 0;; j++) + UInt2 d, r; + + /* + * Divide the number by 10, and output the reminder as the next digit. + * + * Begin from the most-significant byte (last in the array), and at + * each step, carry the remainder to the prev byte. + */ + r = 0; + lastnonzero = -1; + for (i = vlen - 1; i >= 0; i--) { - next_figure = FALSE; - if (calv[j] > 0) - { - if (j >= len) - len = j + 1; - while (calv[j] > 9) - { - calv[j + 1]++; - calv[j] -= 10; - next_figure = TRUE; - } - } - if (j >= 2 && !next_figure) - break; + UInt2 v; + + v = ((UInt2) vals[i]) + (r << 8); + d = v / 10; r = v % 10; + vals[i] = (UCHAR) d; + + if (d != 0 && lastnonzero == -1) + lastnonzero = i; } - } + + /* output the remainder */ + calv[len++] = r; + + vlen = lastnonzero + 1; + } while(lastnonzero >= 0 && len < precision); + + /* + * calv now contains the digits in reverse order, i.e. least significant + * digit is at calv[0] + */ + inolog(" len2=%d", len); + + /* build the final output string. */ newlen = 0; if (0 == ns->sign) chrform[newlen++] = '-'; - if (i = len - 1, i < ns->scale) + + i = len - 1; + if (i < ns->scale) i = ns->scale; for (; i >= ns->scale; i--) - chrform[newlen++] = calv[i] + '0'; + { + if (i >= len) + chrform[newlen++] = '0'; + else + chrform[newlen++] = calv[i] + '0'; + } if (ns->scale > 0) { chrform[newlen++] = '.'; for (; i >= 0; i--) - chrform[newlen++] = calv[i] + '0'; + { + if (i >= len) + chrform[newlen++] = '0'; + else + chrform[newlen++] = calv[i] + '0'; + } } if (0 == len) chrform[newlen++] = '0'; chrform[newlen] = '\0'; inolog(" convval(2) len=%d %s\n", newlen, chrform); - return TRUE; } + +/* + * Convert a string representation of a numeric into SQL_NUMERIC_STRUCT. + */ +static void +parse_to_numeric_struct(const char *wv, SQL_NUMERIC_STRUCT *ns, BOOL *overflow) +{ + int i, nlen, dig; + char calv[SQL_MAX_NUMERIC_LEN * 3]; + BOOL dot_exist; + + *overflow = FALSE; + + /* skip leading space */ + while (*wv && isspace((unsigned char) *wv)) + wv++; + + /* sign */ + ns->sign = 1; + if (*wv == '-') + { + ns->sign = 0; + wv++; + } + else if (*wv == '+') + wv++; + + /* skip leading zeros */ + while (*wv == '0') + wv++; + + /* read the digits into calv */ + ns->precision = 0; + ns->scale = 0; + for (nlen = 0, dot_exist = FALSE;; wv++) + { + if (*wv == '.') + { + if (dot_exist) + break; + dot_exist = TRUE; + } + else if (*wv == '\0' || !isdigit((unsigned char) *wv)) + break; + else + { + if (nlen >= sizeof(calv)) + { + if (dot_exist) + break; + else + { + ns->scale--; + *overflow = TRUE; + continue; + } + } + if (dot_exist) + ns->scale++; + calv[nlen++] = *wv; + } + } + ns->precision = nlen; + + /* Convert the decimal digits to binary */ + memset(ns->val, 0, sizeof(ns->val)); + for (dig = 0; dig < nlen; dig++) + { + UInt4 carry; + + /* multiply the current value by 10, and add the next digit */ + carry = calv[dig] - '0'; + for (i = 0; i < sizeof(ns->val); i++) + { + UInt4 t; + + t = ((UInt4) ns->val[i]) * 10 + carry; + ns->val[i] = (unsigned char) (t & 0xFF); + carry = (t >> 8); + } + + if (carry != 0) + *overflow = TRUE; + } +} + + #endif /* ODBCVER */ /* @@ -3975,7 +3983,7 @@ ResolveOneParam(QueryBuild *qb, QueryParse *qp, BOOL *isnull) PutDataInfo *pdata = qb->pdata; int param_number; - char param_string[128], + char param_string[150], tmp[256]; char cbuf[PG_NUMERIC_MAX_PRECISION * 2]; /* seems big enough to handle the data in this function */ OID param_pgtype; @@ -4403,8 +4411,10 @@ mylog("C_WCHAR=%s(%d)\n", buffer, used); } #if (ODBCVER >= 0x0300) case SQL_C_NUMERIC: - if (ResolveNumericParam((SQL_NUMERIC_STRUCT *) buffer, param_string)) - break; + { + ResolveNumericParam((SQL_NUMERIC_STRUCT *) buffer, param_string); + break; + } case SQL_C_INTERVAL_YEAR: ivsign = ivstruct->interval_sign ? "-" : ""; sprintf(param_string, "%s%d years", ivsign, ivstruct->intval.year_month.year); diff --git a/test/Makefile.in b/test/Makefile.in index 4bb5149..8bc3419 100644 --- a/test/Makefile.in +++ b/test/Makefile.in @@ -2,7 +2,7 @@ TESTS = connect stmthandles select commands multistmt getresult prepare \ params notice arraybinding insertreturning dataatexecution \ boolsaschar cvtnulldate alter quotes cursors positioned-update \ catalogfunctions bindcol lfconversion cte deprecated error-rollback \ - diagnostic + diagnostic numeric TESTBINS = $(patsubst %,src/%-test, $(TESTS)) TESTSQLS = $(patsubst %,sql/%.sql, $(TESTS)) diff --git a/test/expected/numeric.out b/test/expected/numeric.out new file mode 100644 index 0000000..e01c6d2 --- /dev/null +++ b/test/expected/numeric.out @@ -0,0 +1,61 @@ +\! ./src/numeric-test +connected +Testing SQL_NUMERIC_STRUCT params... + +sign 1 prec 5 scale 3 val 7C62: + 25.212 +sign 1 prec 41 scale 0 val 78563412 78563412 78563412 78563412: + 24197857161011715162171839636988778104 +sign 1 prec 38 scale 0 val 4EF338DE509049C4133302F0F6B04909: + 12345678901234567890123456789012345678 +sign 1 prec 50 scale 0 val FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF: + 340282366920938463463374607431768211455 +sign 1 prec 1 scale 0 val 00: + 0 +sign 0 prec 1 scale 0 val 00: + 0 +sign 1 prec 3 scale 2 val 0203: + 7.70 +sign 1 prec 1 scale 0 val 3930: + 1 +sign 1 prec 2 scale 0 val 3930: + 12 +sign 1 prec 3 scale 0 val 3930: + 123 +sign 1 prec 4 scale 0 val 3930: + 1234 +sign 1 prec 5 scale 0 val 3930: + 12345 +sign 1 prec 6 scale 0 val 3930: + 12345 +sign 1 prec 3 scale 50 val 0203: + 0.00000000000000000000000000000000000000000000000770 +sign 1 prec 25 scale 80 val 0203: + 0.00000000000000000000000000000000000000000000000000000000000000000000000000000770 +sign 0 prec 25 scale 127 val FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF: + -0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003402823669209384634633746 +sign 1 prec 3 scale 50 val 0203: + 0.00000000000000000000000000000000000000000000000770 +Testing SQL_NUMERIC_STRUCT results... + +25.212: + sign 1 prec 5 scale 3 val 7C620000000000000000000000000000 +24197857161011715162171839636988778104: + sign 1 prec 38 scale 0 val 78563412785634127856341278563412 +12345678901234567890123456789012345678: + sign 1 prec 38 scale 0 val 4EF338DE509049C4133302F0F6B04909 +340282366920938463463374607431768211455: + sign 1 prec 39 scale 0 val FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +340282366920938463463374607431768211456: + sign 1 prec 39 scale 0 val 00000000000000000000000000000000 +340282366920938463463374607431768211457: + sign 1 prec 39 scale 0 val 01000000000000000000000000000000 +-0: + sign 1 prec 0 scale 0 val 00000000000000000000000000000000 +0: + sign 1 prec 0 scale 0 val 00000000000000000000000000000000 +-7.70: + sign 0 prec 3 scale 2 val 02030000000000000000000000000000 +999999999999: + sign 1 prec 12 scale 0 val FF0FA5D4E80000000000000000000000 +disconnecting diff --git a/test/src/numeric-test.c b/test/src/numeric-test.c new file mode 100644 index 0000000..3f34bdc --- /dev/null +++ b/test/src/numeric-test.c @@ -0,0 +1,223 @@ +/* + * Test cases for dealing with SQL_NUMERIC_STRUCT + */ +#include +#include + +#include "common.h" + +static unsigned char +hex_to_int(char c) +{ + char result; + + if (c >= '0' && c <= '9') + result = c - '0'; + else if (c >= 'a' && c <= 'f') + result = c - 'a' + 10; + else if (c >= 'A' && c <= 'F') + result = c - 'A' + 10; + else + { + fprintf(stderr, "invalid hex-encoded numeric value\n"); + exit(1); + } + return (unsigned char) result; +} + +static void +build_numeric_struct(SQL_NUMERIC_STRUCT *numericparam, + unsigned char sign, char *hexstr, + unsigned char precision, unsigned char scale) +{ + int len; + + /* parse the hex-encoded value */ + memset(numericparam, 0, sizeof(SQL_NUMERIC_STRUCT)); + + numericparam->sign = sign; + numericparam->precision = precision; + numericparam->scale = scale; + + len = 0; + while (*hexstr) + { + if (*hexstr == ' ') + { + hexstr++; + continue; + } + if (len >= SQL_MAX_NUMERIC_LEN) + { + fprintf(stderr, "hex-encoded numeric value too long\n"); + exit(1); + } + numericparam->val[len] = + hex_to_int(*hexstr) << 4 | hex_to_int(*(hexstr + 1)); + hexstr += 2; + len++; + } +} + +static void +test_numeric_param(HSTMT hstmt, unsigned char sign, char *hexval, + unsigned char precision, unsigned char scale) +{ + SQL_NUMERIC_STRUCT numericparam; + SQLLEN cbParam1; + SQLRETURN rc; + char buf[200]; + + build_numeric_struct(&numericparam, sign, hexval, precision, scale); + + cbParam1 = sizeof(numericparam); + rc = SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, + SQL_C_NUMERIC, /* value type */ + SQL_NUMERIC, /* param type */ + 0, /* column size (ignored for SQL_INTERVAL_SECOND) */ + 0, /* dec digits */ + &numericparam, /* param value ptr */ + sizeof(numericparam), /* buffer len (ignored for SQL_C_INTERVAL_SECOND) */ + &cbParam1 /* StrLen_or_IndPtr (ignored for SQL_C_INTERVAL_SECOND) */); + CHECK_STMT_RESULT(rc, "SQLBindParameter failed", hstmt); + + /* Execute */ + rc = SQLExecute(hstmt); + if (!SQL_SUCCEEDED(rc)) + { + print_diag("SQLExecute failed", SQL_HANDLE_STMT, hstmt); + } + else + { + /* print result */ + rc = SQLFetch(hstmt); + CHECK_STMT_RESULT(rc, "SQLFetch failed", hstmt); + + rc = SQLGetData(hstmt, 1, SQL_C_CHAR, buf, sizeof(buf), NULL); + CHECK_STMT_RESULT(rc, "SQLGetData failed", hstmt); + printf("sign %u prec %u scale %d val %s:\n %s\n", + sign, precision, scale, hexval, buf); + } + + rc = SQLFreeStmt(hstmt, SQL_CLOSE); + CHECK_STMT_RESULT(rc, "SQLFreeStmt failed", hstmt); +} + +static void +test_numeric_result(HSTMT hstmt, char *numstr) +{ + char sql[100]; + SQL_NUMERIC_STRUCT numericres; + SQLRETURN rc; + + /* + * assume 'numstr' param to be well-formed (we're testing how the + * results come out, not the input handling) + */ + snprintf(sql, sizeof(sql), "SELECT '%s'::numeric", numstr); + rc = SQLExecDirect(hstmt, (SQLCHAR *) sql, SQL_NTS); + CHECK_STMT_RESULT(rc, "SQLExecDirect failed", hstmt); + + rc = SQLFetch(hstmt); + CHECK_STMT_RESULT(rc, "SQLFetch failed", hstmt); + + rc = SQLGetData(hstmt, 1, SQL_C_NUMERIC, &numericres, sizeof(numericres), NULL); + CHECK_STMT_RESULT(rc, "SQLGetData failed", hstmt); + printf("%s:\n sign %u prec %u scale %d val %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X\n", + numstr, numericres.sign, numericres.precision, numericres.scale, + numericres.val[0], numericres.val[1], + numericres.val[2], numericres.val[3], + numericres.val[4], numericres.val[5], + numericres.val[6], numericres.val[7], + numericres.val[8], numericres.val[9], + numericres.val[10], numericres.val[11], + numericres.val[12], numericres.val[13], + numericres.val[14], numericres.val[15]); + + rc = SQLFreeStmt(hstmt, SQL_CLOSE); + CHECK_STMT_RESULT(rc, "SQLFreeStmt failed", hstmt); +} + +int main(int argc, char **argv) +{ + SQLRETURN rc; + HSTMT hstmt = SQL_NULL_HSTMT; + + test_connect(); + + rc = SQLAllocHandle(SQL_HANDLE_STMT, conn, &hstmt); + if (!SQL_SUCCEEDED(rc)) + { + print_diag("failed to allocate stmt handle", SQL_HANDLE_DBC, conn); + exit(1); + } + + /**** Test binding SQL_NUMERIC_STRUCT params (SQL_C_NUMERIC) ****/ + + printf("Testing SQL_NUMERIC_STRUCT params...\n\n"); + + rc = SQLPrepare(hstmt, (SQLCHAR *) "SELECT ?::numeric", SQL_NTS); + CHECK_STMT_RESULT(rc, "SQLPrepare failed", hstmt); + + /* 25.212 (per Microsoft KB 22831) */ + test_numeric_param(hstmt, 1, "7C62", 5, 3); + + /* 24197857161011715162171839636988778104 */ + test_numeric_param(hstmt, 1, "78563412 78563412 78563412 78563412", 41, 0); + + /* 12345678901234567890123456789012345678 */ + test_numeric_param(hstmt, 1, "4EF338DE509049C4133302F0F6B04909", 38, 0); + + /* highest possible non-scaled: 340282366920938463463374607431768211455 */ + test_numeric_param(hstmt, 1, "FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF", 50, 0); + + /* positive and negative zero */ + test_numeric_param(hstmt, 1, "00", 1, 0); + test_numeric_param(hstmt, 0, "00", 1, 0); + + /* -7.70 */ + test_numeric_param(hstmt, 1, "0203", 3, 2); + + /* 0.12345, with 1-6 digit precision: */ + test_numeric_param(hstmt, 1, "3930", 1, 5); + test_numeric_param(hstmt, 1, "3930", 2, 5); + test_numeric_param(hstmt, 1, "3930", 3, 5); + test_numeric_param(hstmt, 1, "3930", 4, 5); + test_numeric_param(hstmt, 1, "3930", 5, 5); + test_numeric_param(hstmt, 1, "3930", 6, 5); + + /* large scale with small value */ + test_numeric_param(hstmt, 1, "0203", 3, 50); + + /* medium-sized scale and precision */ + test_numeric_param(hstmt, 1, "0203", 25, 80); + + /* max length output; negative with max scale and decimal dot */ + test_numeric_param(hstmt, 0, "FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF", 25, 127); + + /* large scale with small value */ + test_numeric_param(hstmt, 1, "0203", 3, 50); + + + /**** Test fetching SQL_NUMERIC_STRUCT results ****/ + printf("Testing SQL_NUMERIC_STRUCT results...\n\n"); + + test_numeric_result(hstmt, "25.212"); + test_numeric_result(hstmt, "24197857161011715162171839636988778104"); + test_numeric_result(hstmt, "12345678901234567890123456789012345678"); + /* highest number */ + test_numeric_result(hstmt, "340282366920938463463374607431768211455"); + /* overflow */ + test_numeric_result(hstmt, "340282366920938463463374607431768211456"); + test_numeric_result(hstmt, "340282366920938463463374607431768211457"); + + test_numeric_result(hstmt, "-0"); + test_numeric_result(hstmt, "0"); + test_numeric_result(hstmt, "-7.70"); + test_numeric_result(hstmt, "999999999999"); + + /* Clean up */ + test_disconnect(); + + return 0; +} -- 2.39.5