diff --git a/.gitignore b/.gitignore index 5e260e24..3204ad08 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.swp driver/*.swp +builds/ cscope.out +.vs diff --git a/CMakeLists.txt b/CMakeLists.txt index b739b35e..4231e5e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,6 +82,12 @@ if (${WIN32}) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /D_CRT_SECURE_NO_WARNINGS") # don't complain about non-constant aggregate initializer (C4204) # set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /Ze") + # parallel building + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP") + + # 2MB stack size + # set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /STACK:2097152") + # set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /F 2097152") if (${IS_UNICODE}) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /DUNICODE /D_UNICODE") @@ -208,7 +214,6 @@ add_custom_target(curlclean WORKING_DIRECTORY "${LIBCURL_PATH_SRC}/winbuild" ) - message("Driver source files: ${DRV_SRC} .") message("Driver include paths: " ${ODBC_INC} ${DRV_SRC_DIR} ${LIBCURL_INC_PATH} ${UJSON4C_INC} ${CTIMESTAMP_PATH_SRC}) @@ -223,7 +228,7 @@ target_compile_definitions(${DRV_NAME} PRIVATE "DRIVER_BUILD") include_directories(${ODBC_INC} ${DRV_SRC_DIR} ${LIBCURL_INC_PATH} ${UJSON4C_INC} ${CTIMESTAMP_PATH_SRC}) -target_link_libraries(${DRV_NAME} libcurl) +target_link_libraries(${DRV_NAME} odbccp32 legacy_stdio_definitions libcurl) # add testing project/target enable_testing() diff --git a/build.bat b/build.bat index 75016f91..614dc6c0 100644 --- a/build.bat +++ b/build.bat @@ -44,16 +44,16 @@ REM REM presence of 'help'/'?': invoke USAGE "function" and exit if /i not [%ARG:help=%] == [%ARG%] ( call:USAGE %0 - goto end + goto END ) else if not [%ARG:?=%] == [%ARG%] ( call:USAGE %0 - goto end + goto END ) REM presence of 'proper' or 'clean': invoke respective "functions" if /i not [%ARG:proper=%] == [%ARG%] ( call:PROPER - goto end + goto END ) else if /i not [%ARG:clean=%] == [%ARG%] ( call:CLEAN ) @@ -69,7 +69,7 @@ if /i not [%ARG:setup=%] == [%ARG%] ( echo. echo ERROR: building environment not set. Run with /? to see options. echo. - goto end + goto END ) ) @@ -136,10 +136,6 @@ if /i not [%ARG:regdel=%] == [%ARG%] ( REM Invoked without 'regadd': registry adding skipped. ) -:end -exit /b 0 - - REM REM "Functions" @@ -211,7 +207,7 @@ REM function to check and set cmake binary (if installed) echo ERROR: needed cmake executable not found: when installed, echo either set it in path or in environment variable CMAKE echo. - goto end + goto END ) ) else ( set CMAKE=cmake.exe @@ -231,7 +227,7 @@ REM USAGE function: output a usage message echo setup : invoke MSVC's build environment setup script before echo building (requires 2017 version or later^). echo clean : remove all the files in the build dir. - echo proper : clean both the libs and builds dirs and exit. + echo proper : clean libs, builds and project dirs and exit. echo nobuild : skip project building (the default is to build^). echo type=T : selects the build type; T can be one of Debug/Release/ echo RelWithDebInfo/MinSizeRel^); defaults to Debug. @@ -276,6 +272,8 @@ REM PROPER function: clean up the build and libs dir. MSBuild %BUILD_DIR%\curlclean.vcxproj ) call:CLEAN + REM delete VisualStudio files + rmdir /S /Q .vs goto:eof @@ -347,19 +345,18 @@ REM BUILD function: build various targets ) else if /i not [%ARG:all=%] == [%ARG%] ( echo Building all the project. MSBuild ALL_BUILD.vcxproj %MSBUILD_ARGS% - ) else if /i not [%ARG:suites=%] == [%ARG%] ( + ) else if /i not [%ARG:suiteS=%] == [%ARG%] ( echo Building the test projects only. for %%i in (test\test_*.vcxproj) do ( MSBuild %%~fi %MSBUILD_ARGS% - if not ERRORLEVEL 1 ( + if ERRORLEVEL 1 ( + goto END + ) else ( echo Running test\%CFG_INTDIR%\%%~ni.exe : test\%CFG_INTDIR%\%%~ni.exe if ERRORLEVEL 1 ( - goto:eof + goto END ) - ) else ( - echo Building %%~fi failed. - goto:eof ) ) ) else if /i not [%ARG:suite==%] == [%ARG%] ( @@ -372,7 +369,9 @@ REM BUILD function: build various targets echo Building and running one suite: !SUITE! echo MSBuild test\%%a.vcxproj %MSBUILD_ARGS% MSBuild test\%%a.vcxproj %MSBUILD_ARGS% - if not ERRORLEVEL 1 ( + if ERRORLEVEL 1 ( + goto END + ) else ( test\%CFG_INTDIR%\%%a.exe ) goto:eof @@ -385,7 +384,7 @@ REM BUILD function: build various targets REM file name expansion, cmd style... for /f %%i in ("%DRIVER_BASE_NAME%*.vcxproj") do MSBuild %%~nxi %MSBUILD_ARGS% - if not ERRORLEVEL 1 if /i not [%ARG:symbols=%] == [%ARG%] ( + if ERRORLEVEL 0 if /i not [%ARG:exports=%] == [%ARG%] ( dumpbin /exports %CFG_INTDIR%\%DRIVER_BASE_NAME%*.dll ) ) @@ -440,7 +439,7 @@ REM REGADD function: add driver into the registry REM check if driver exists, otherwise the filename is unknown if not exist %BUILD_DIR%\%CFG_INTDIR%\%DRIVER_BASE_NAME%*.dll ( echo Error: Driver can only be added into the registry once built. - goto end + goto END ) for /f %%i in ("%BUILD_DIR%\%CFG_INTDIR%\%DRIVER_BASE_NAME%*.dll") do set DRVNAME=%%~nxi @@ -472,4 +471,9 @@ REM REGDEL function: remove driver from the registry goto:eof + + +:END + exit /b %ERRORLEVEL% + endlocal diff --git a/builds/.gitignore b/builds/.gitignore deleted file mode 100644 index d6b7ef32..00000000 --- a/builds/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/devtools/astyle-c.sh b/devtools/astyle-c.sh index 3c3984e1..300af222 100755 --- a/devtools/astyle-c.sh +++ b/devtools/astyle-c.sh @@ -20,7 +20,7 @@ fi # Ensure astyle is available and is of the right version to have all below # parameters supported. # -if ! which -s astyle ; then +if ! which astyle &>/dev/null ; then echo "ERROR: The astyle code formatter is not available. Exiting." exit 1 fi diff --git a/driver/build_def.bat b/driver/build_def.bat index becdd8d7..d32eb914 100644 --- a/driver/build_def.bat +++ b/driver/build_def.bat @@ -3,7 +3,7 @@ REM Helper script to generate the definition file of symbols to export in DLL @echo off REM TODO: find out a smart way for this filtering -set FILTER="Select-String -Pattern 'SQLRETURN\s+SQL_API\s+\w+' *.c | %%{ [regex]::split($_, '\s+')[2]; } | %%{ [regex]::split($_, '\(')[0]; }" +set FILTER="Select-String -Pattern '^\w+\s+SQL_API\s+\w+(\(|$)' *.c | %%{ [regex]::split($_, '\s+')[2]; } | %%{ [regex]::split($_, '\(')[0]; }" set DRV_NAME=%1 set OUTFILE=%2 diff --git a/driver/catalogue.c b/driver/catalogue.c index 21404093..6bacae79 100644 --- a/driver/catalogue.c +++ b/driver/catalogue.c @@ -35,6 +35,7 @@ // TODO add schema, when supported #define SQL_COLUMNS(...) "SYS COLUMNS" __VA_ARGS__ \ " TABLE LIKE " ESODBC_STRING_DELIM WPFWP_LDESC ESODBC_STRING_DELIM \ + " ESCAPE '" ESODBC_PATTERN_ESCAPE "' " \ " LIKE " ESODBC_STRING_DELIM WPFWP_LDESC ESODBC_STRING_DELIM #define SQL_COL_CAT \ " CATALOG " ESODBC_STRING_DELIM WPFWP_LDESC ESODBC_STRING_DELIM \ @@ -159,6 +160,9 @@ size_t quote_tokens(SQLWCHAR *src, size_t len, SQLWCHAR *dest) } *pos ++ = src[i]; } + if (copying) { + *pos ++ = L'\''; /* end last token */ + } /* should not overrun */ assert(i < 2/*see typ_buf below*/ * ESODBC_MAX_IDENTIFIER_LEN); return pos - dest; diff --git a/driver/connect.c b/driver/connect.c index 563884f5..43b95126 100644 --- a/driver/connect.c +++ b/driver/connect.c @@ -15,6 +15,7 @@ #include "log.h" #include "info.h" #include "util.h" +#include "dsn.h" /* HTTP headers default for every request */ #define HTTP_ACCEPT_JSON "Accept: application/json" @@ -47,54 +48,6 @@ #define JSON_COL_SCALED_FLOAT "scaled_float" -/* attribute keywords used in connection strings */ -#define CONNSTR_KW_DRIVER "Driver" -#define CONNSTR_KW_DSN "DSN" -#define CONNSTR_KW_PWD "PWD" -#define CONNSTR_KW_UID "UID" -#define CONNSTR_KW_SAVEFILE "SAVEFILE" -#define CONNSTR_KW_FILEDSN "FILEDSN" -#define CONNSTR_KW_SERVER "Server" -#define CONNSTR_KW_PORT "Port" -#define CONNSTR_KW_SECURE "Secure" -#define CONNSTR_KW_TIMEOUT "Timeout" -#define CONNSTR_KW_FOLLOW "Follow" -#define CONNSTR_KW_CATALOG "Catalog" -#define CONNSTR_KW_PACKING "Packing" -#define CONNSTR_KW_MAX_FETCH_SIZE "MaxFetchSize" -#define CONNSTR_KW_MAX_BODY_SIZE_MB "MaxBodySizeMB" - -#define ODBC_REG_SUBKEY_PATH "SOFTWARE\\ODBC\\ODBC.INI" -#define REG_HKLM "HKEY_LOCAL_MACHINE" -#define REG_HKCU "HKEY_CURRENT_USER" - -/* max length of a registry key value name */ -#define MAX_REG_VAL_NAME 1024 -/* max size of a registry key data */ -#define MAX_REG_DATA_SIZE 4096 - - -/* stucture to collect all attributes in a connection string */ -typedef struct { - wstr_st driver; - wstr_st dsn; - wstr_st pwd; - wstr_st uid; - wstr_st savefile; - wstr_st filedsn; - wstr_st server; - wstr_st port; - wstr_st secure; - wstr_st timeout; - wstr_st follow; - wstr_st catalog; - wstr_st packing; - wstr_st max_fetch_size; - wstr_st max_body_size; - wstr_st trace_file; - wstr_st trace_level; -} config_attrs_st; - /* structure for one row returned by the ES. * This is a mirror of elasticsearch_type, with length-or-indicator fields * for each of the members in elasticsearch_type */ @@ -473,361 +426,10 @@ static SQLRETURN test_connect(CURL *curl) return SQL_SUCCESS; } -static BOOL assign_config_attr(config_attrs_st *attrs, - wstr_st *keyword, wstr_st *value, BOOL overwrite) -{ - struct { - wstr_st *kw; - wstr_st *val; - } *iter, map[] = { - {&MK_WSTR(CONNSTR_KW_DRIVER), &attrs->driver}, - {&MK_WSTR(CONNSTR_KW_DSN), &attrs->dsn}, - {&MK_WSTR(CONNSTR_KW_PWD), &attrs->pwd}, - {&MK_WSTR(CONNSTR_KW_UID), &attrs->uid}, - {&MK_WSTR(CONNSTR_KW_SAVEFILE), &attrs->savefile}, - {&MK_WSTR(CONNSTR_KW_FILEDSN), &attrs->filedsn}, - {&MK_WSTR(CONNSTR_KW_SERVER), &attrs->server}, - {&MK_WSTR(CONNSTR_KW_PORT), &attrs->port}, - {&MK_WSTR(CONNSTR_KW_SECURE), &attrs->secure}, - {&MK_WSTR(CONNSTR_KW_TIMEOUT), &attrs->timeout}, - {&MK_WSTR(CONNSTR_KW_FOLLOW), &attrs->follow}, - {&MK_WSTR(CONNSTR_KW_CATALOG), &attrs->catalog}, - {&MK_WSTR(CONNSTR_KW_PACKING), &attrs->packing}, - {&MK_WSTR(CONNSTR_KW_MAX_FETCH_SIZE), &attrs->max_fetch_size}, - {&MK_WSTR(CONNSTR_KW_MAX_BODY_SIZE_MB), &attrs->max_body_size}, - {NULL, NULL} - }; - - for (iter = &map[0]; iter->kw; iter ++) { - if (! EQ_CASE_WSTR(iter->kw, keyword)) { - continue; - } - /* it's a match: has it been assigned already? */ - if (iter->val->cnt) { - if (! overwrite) { - INFO("multiple occurances of keyword '" LWPDL "'; " - "ignoring new `" LWPDL "`, keeping `" LWPDL "`.", - LWSTR(iter->kw), LWSTR(value), LWSTR(iter->val)); - continue; - } - INFO("multiple occurances of keyword '" LWPDL "'; " - "overwriting old `" LWPDL "` with new `" LWPDL "`.", - LWSTR(iter->kw), LWSTR(iter->val), LWSTR(value)); - } - *iter->val = *value; - return TRUE; - } - - return FALSE; -} - -/* - * Advance position in string, skipping white space. - * if exended is true, `;` will be treated as white space too. - */ -static SQLWCHAR *skip_ws(SQLWCHAR **pos, SQLWCHAR *end, BOOL extended) -{ - while (*pos < end) { - switch(**pos) { - case ' ': - case '\t': - case '\r': - case '\n': - (*pos)++; - break; - - case '\0': - return NULL; - - case ';': - if (extended) { - (*pos)++; - break; - } - // no break; - - default: - return *pos; - } - } - - /* end of string reached */ - return NULL; -} - -/* - * Parse a keyword or a value. - * Within braces, any character is allowed, safe for \0 (i.e. no "bla{bla\0" - * is supported as keyword or value). - * Braces within braces are allowed. - */ -static BOOL parse_token(BOOL is_value, SQLWCHAR **pos, SQLWCHAR *end, - wstr_st *token) -{ - BOOL brace_escaped = FALSE; - int open_braces = 0; - SQLWCHAR *start = *pos; - BOOL stop = FALSE; - - while (*pos < end && (! stop)) { - switch (**pos) { - case '\\': - if (! is_value) { - ERR("keywords and data source names cannot contain " - "the backslash."); - return FALSE; - } - (*pos)++; - break; - - case ' ': - case '\t': - case '\r': - case '\n': - if (open_braces || is_value) { - (*pos)++; - } else { - stop = TRUE; - } - break; - - case '=': - if (open_braces || is_value) { - (*pos)++; - } else { - stop = TRUE; - } - break; - - case ';': - if (open_braces) { - (*pos)++; - } else if (is_value) { - stop = TRUE; - } else { - ERR("';' found while parsing keyword"); - return FALSE; - } - break; - - case '\0': - if (open_braces) { - ERR("null terminator found while within braces"); - return FALSE; - } else if (! is_value) { - ERR("null terminator found while parsing keyword."); - return FALSE; - } /* else: \0 used as delimiter of value string */ - stop = TRUE; - break; - - case '{': - if (*pos == start) { - open_braces ++; - } else if (open_braces) { - ERR("token started with opening brace, so can't use " - "inner braces"); - /* b/c: `{foo{ = }}bar} = val`; `foo{bar}baz` is fine */ - return FALSE; - } - (*pos)++; - break; - - case '}': - if (open_braces) { - open_braces --; - brace_escaped = TRUE; - stop = TRUE; - } - (*pos)++; - break; - - default: - (*pos)++; - } - } - - if (open_braces) { - ERR("string finished with open braces."); - return FALSE; - } - - token->str = start + (brace_escaped ? 1 : 0); - token->cnt = (*pos - start) - (brace_escaped ? 2 : 0); - return TRUE; -} - -static SQLWCHAR *parse_separator(SQLWCHAR **pos, SQLWCHAR *end) -{ - if (*pos < end) { - if (**pos == '=') { - (*pos)++; - return *pos; - } - } - return NULL; -} - -/* - * - "keywords and attribute values that contain the characters []{}(),;?*=!@ - * not enclosed with braces should be avoided"; => allowed. - * - "value of the DSN keyword cannot consist only of blanks and should not - * contain leading blanks"; - * - "keywords and data source names cannot contain the backslash (\) - * character."; - * - "value enclosed with braces ({}) containing any of the characters - * []{}(),;?*=!@ is passed intact to the driver."; - * - * foo{bar}=baz=foo; => "foo{bar}" = "baz=foo" - * - * * `=` is delimiter, unless within {} - * * `{` and `}` allowed within {} - * * brances need to be returned to out-str; - */ -static BOOL parse_connection_string(config_attrs_st *attrs, - SQLWCHAR *szConnStrIn, SQLSMALLINT cchConnStrIn) -{ - - SQLWCHAR *pos; - SQLWCHAR *end; - wstr_st keyword, value; - - /* parse and assign attributes in connection string */ - pos = szConnStrIn; - end = pos + (cchConnStrIn == SQL_NTS ? SHRT_MAX : cchConnStrIn); - - while (skip_ws(&pos, end, TRUE)) { - if (! parse_token(FALSE, &pos, end, &keyword)) { - ERR("failed to parse keyword at position %zd", - pos - szConnStrIn); - return FALSE; - } - - if (! skip_ws(&pos, end, FALSE)) { - return FALSE; - } - - if (! parse_separator(&pos, end)) { - ERR("failed to parse separator (`=`) at position %zd", - pos - szConnStrIn); - return FALSE; - } - - if (! skip_ws(&pos, end, FALSE)) { - return FALSE; - } - - if (! parse_token(TRUE, &pos, end, &value)) { - ERR("failed to parse value at position %zd", - pos - szConnStrIn); - return FALSE; - } - - DBG("read connection string attribute: `" LWPDL "` = `" LWPDL - "`.", LWSTR(&keyword), LWSTR(&value)); - if (! assign_config_attr(attrs, &keyword, &value, TRUE)) - WARN("keyword '" LWPDL "' is unknown, ignoring it.", - LWSTR(&keyword)); - } - - return TRUE; -} - -static inline BOOL needs_braces(wstr_st *token) -{ - int i; - - for (i = 0; i < token->cnt; i ++) { - switch(token->str[i]) { - case ' ': - case '\t': - case '\r': - case '\n': - case '=': - case ';': - return TRUE; - } - } - return FALSE; -} - -/* build a connection string to be written in the DSN files */ -static BOOL write_connection_string(config_attrs_st *attrs, - SQLWCHAR *szConnStrOut, SQLSMALLINT cchConnStrOutMax, - SQLSMALLINT *pcchConnStrOut) -{ - int n, braces; - size_t pos; - wchar_t *format; - struct { - wstr_st *val; - char *kw; - } *iter, map[] = { - {&attrs->driver, CONNSTR_KW_DRIVER}, - {&attrs->dsn, CONNSTR_KW_DSN}, - {&attrs->pwd, CONNSTR_KW_PWD}, - {&attrs->uid, CONNSTR_KW_UID}, - {&attrs->savefile, CONNSTR_KW_SAVEFILE}, - {&attrs->filedsn, CONNSTR_KW_FILEDSN}, - {&attrs->server, CONNSTR_KW_SERVER}, - {&attrs->port, CONNSTR_KW_PORT}, - {&attrs->secure, CONNSTR_KW_SECURE}, - {&attrs->timeout, CONNSTR_KW_TIMEOUT}, - {&attrs->follow, CONNSTR_KW_FOLLOW}, - {&attrs->catalog, CONNSTR_KW_CATALOG}, - {&attrs->packing, CONNSTR_KW_PACKING}, - {&attrs->max_fetch_size, CONNSTR_KW_MAX_FETCH_SIZE}, - {&attrs->max_body_size, CONNSTR_KW_MAX_BODY_SIZE_MB}, - {NULL, NULL} - }; - - for (iter = &map[0], pos = 0; iter->val; iter ++) { - if (iter->val->cnt) { - braces = needs_braces(iter->val) ? 2 : 0; - if (cchConnStrOutMax && szConnStrOut) { - /* swprintf will fail if formated string would overrun the - * buffer size */ - if (cchConnStrOutMax - pos < iter->val->cnt + braces) { - /* indicate that we've reached buffer limits: only account - * for how long the string would be */ - cchConnStrOutMax = 0; - pos += iter->val->cnt + braces; - continue; - } - if (braces) { - format = WPFCP_DESC "={" WPFWP_LDESC "};"; - } else { - format = WPFCP_DESC "=" WPFWP_LDESC ";"; - } - n = swprintf(szConnStrOut + pos, cchConnStrOutMax - pos, - format, iter->kw, LWSTR(iter->val)); - if (n < 0) { - ERRN("failed to outprint connection string (space " - "left: %d; needed: %d).", cchConnStrOutMax - pos, - iter->val->cnt); - return FALSE; - } else { - pos += n; - } - } else { - /* simply increment the counter, since the untruncated length - * need to be returned to the app */ - pos += iter->val->cnt + braces; - } - } - } - - *pcchConnStrOut = (SQLSMALLINT)pos; - - DBG("Output connection string: `" LWPD "`; out len: %d.", - szConnStrOut, pos); - return TRUE; -} - /* * init dbc from configured attributes */ -static SQLRETURN process_config(esodbc_dbc_st *dbc, config_attrs_st *attrs) +static SQLRETURN process_config(esodbc_dbc_st *dbc, esodbc_dsn_attrs_st *attrs) { esodbc_state_et state = SQL_STATE_HY000; int n, cnt; @@ -839,6 +441,7 @@ static SQLRETURN process_config(esodbc_dbc_st *dbc, config_attrs_st *attrs) * build connection URL */ secure = wstr2bool(&attrs->secure); + INFOH(dbc, "connect secure: %s.", secure ? "true" : "false"); cnt = swprintf(urlw, sizeof(urlw)/sizeof(urlw[0]), L"http" WPFCP_DESC "://" WPFWP_LDESC ":" WPFWP_LDESC ELASTIC_SQL_PATH, secure ? "s" : "", LWSTR(&attrs->server), @@ -879,8 +482,8 @@ static SQLRETURN process_config(esodbc_dbc_st *dbc, config_attrs_st *attrs) * request timeout for liburl: negative reset to 0 */ if (! str2bigint(&attrs->timeout, /*wide?*/TRUE, (SQLBIGINT *)&timeout)) { - ERRH(dbc, "failed to convert '" LWPDL "' to big int.", - LWSTR(&attrs->timeout)); + ERRH(dbc, "failed to convert `" LWPDL "` [%zu] to big int.", + LWSTR(&attrs->timeout), attrs->timeout.cnt); goto err; } if (timeout < 0) { @@ -895,16 +498,16 @@ static SQLRETURN process_config(esodbc_dbc_st *dbc, config_attrs_st *attrs) */ if (! str2bigint(&attrs->max_body_size, /*wide?*/TRUE, (SQLBIGINT *)&max_body_size)) { - ERRH(dbc, "failed to convert '" LWPDL "' to long long.", - LWSTR(&attrs->max_body_size)); + ERRH(dbc, "failed to convert `" LWPDL "` [%zu] to long long.", + LWSTR(&attrs->max_body_size), attrs->max_body_size.cnt); goto err; } if (max_body_size < 0) { ERRH(dbc, "'%s' setting can't be negative (%ld).", - CONNSTR_KW_MAX_BODY_SIZE_MB, max_body_size); + ESODBC_DSN_MAX_BODY_SIZE_MB, max_body_size); goto err; } else { - dbc->amax = max_body_size * 1024 * 1024; + dbc->amax = (size_t)max_body_size * 1024 * 1024; } INFOH(dbc, "max body size: %zd.", dbc->amax); @@ -913,16 +516,16 @@ static SQLRETURN process_config(esodbc_dbc_st *dbc, config_attrs_st *attrs) */ if (! str2bigint(&attrs->max_fetch_size, /*wide?*/TRUE, (SQLBIGINT *)&max_fetch_size)) { - ERRH(dbc, "failed to convert '" LWPDL "' to long long.", - LWSTR(&attrs->max_fetch_size)); + ERRH(dbc, "failed to convert `" LWPDL "` [%zu] to long long.", + LWSTR(&attrs->max_fetch_size), attrs->max_fetch_size.cnt); goto err; } if (max_fetch_size < 0) { ERRH(dbc, "'%s' setting can't be negative (%ld).", - CONNSTR_KW_MAX_FETCH_SIZE, max_fetch_size); + ESODBC_DSN_MAX_FETCH_SIZE, max_fetch_size); goto err; } else { - dbc->fetch.max = max_fetch_size; + dbc->fetch.max = (size_t)max_fetch_size; } /* set the string representation of fetch_size, once for all STMTs */ if (dbc->fetch.max) { @@ -961,43 +564,6 @@ static SQLRETURN process_config(esodbc_dbc_st *dbc, config_attrs_st *attrs) RET_HDIAGS(dbc, state); } -static inline void assign_defaults(config_attrs_st *attrs) -{ - /* assign defaults where not assigned and applicable */ - if (! attrs->server.cnt) { - attrs->server = MK_WSTR(ESODBC_DEF_SERVER); - } - if (! attrs->port.cnt) { - attrs->port = MK_WSTR(ESODBC_DEF_PORT); - } - if (! attrs->secure.cnt) { - attrs->secure = MK_WSTR(ESODBC_DEF_SECURE); - } - if (! attrs->timeout.cnt) { - attrs->timeout = MK_WSTR(ESODBC_DEF_TIMEOUT); - } - if (! attrs->follow.cnt) { - attrs->follow = MK_WSTR(ESODBC_DEF_FOLLOW); - } - - /* no default packing */ - - if (! attrs->packing.cnt) { - attrs->packing = MK_WSTR(ESODBC_DEF_PACKING); - } - if (! attrs->max_fetch_size.cnt) { - attrs->max_fetch_size = MK_WSTR(ESODBC_DEF_FETCH_SIZE); - } - if (! attrs->max_body_size.cnt) { - attrs->max_body_size = MK_WSTR(ESODBC_DEF_MAX_BODY_SIZE_MB); - } - - /* default: no trace file */ - - if (! attrs->trace_level.cnt) { - attrs->trace_level = MK_WSTR(ESODBC_DEF_TRACE_LEVEL); - } -} /* release all resources, except the handler itself */ void cleanup_dbc(esodbc_dbc_st *dbc) @@ -1032,7 +598,7 @@ void cleanup_dbc(esodbc_dbc_st *dbc) cleanup_curl(dbc); } -static SQLRETURN do_connect(esodbc_dbc_st *dbc, config_attrs_st *attrs) +static SQLRETURN do_connect(esodbc_dbc_st *dbc, esodbc_dsn_attrs_st *attrs) { SQLRETURN ret; char *url = NULL; @@ -1391,7 +957,8 @@ static void *copy_types_rows(estype_row_st *type_row, SQLULEN rows_fetched, esodbc_estype_st *types) { SQLWCHAR *pos; - int i, c; + int c; + SQLULEN i; SQLSMALLINT sql_type; /* pointer to start position where the strings will be copied in */ @@ -1548,15 +1115,24 @@ static BOOL load_es_types(esodbc_dbc_st *dbc) SQLLEN row_cnt; /* both arrays below must use ESODBC_MAX_ROW_ARRAY_SIZE since no SQLFetch() * looping is implemented (see check after SQLFetch() below). */ - estype_row_st type_row[ESODBC_MAX_ROW_ARRAY_SIZE]; SQLUSMALLINT row_status[ESODBC_MAX_ROW_ARRAY_SIZE]; + /* a static estype_row_st array is over 350KB and too big for the default + * stack size in certain cases: needs allocation on heap */ + estype_row_st *type_row = NULL; SQLULEN rows_fetched, i, strs_len; size_t size; esodbc_estype_st *types = NULL; void *pos; + type_row = calloc(ESODBC_MAX_ROW_ARRAY_SIZE, sizeof(estype_row_st)); + if (! type_row) { + ERRNH(dbc, "OOM"); + return FALSE; + } + if (! SQL_SUCCEEDED(EsSQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt))) { ERRH(dbc, "failed to alloc a statement handle."); + free(type_row); return FALSE; } assert(stmt); @@ -1728,123 +1304,13 @@ static BOOL load_es_types(esodbc_dbc_st *dbc) free(types); types = NULL; } - - return ret; -} - -#if defined(_WIN32) || defined (WIN32) -/* - * Reads system registry for ODBC DSN subkey named in attrs->dsn. - */ -static BOOL read_system_info(config_attrs_st *attrs, TCHAR *buff) -{ - HKEY hkey; - BOOL ret = FALSE; - const char *ktree; - DWORD valsno, i, j; /* number of values in subkey */ - DWORD maxvallen; /* len of longest value name */ - DWORD maxdatalen; /* len of longest data (buffer) */ - TCHAR val[MAX_REG_VAL_NAME]; - DWORD vallen; - DWORD valtype; - BYTE *d; - DWORD datalen; - tstr_st tval, tdata; - - if (swprintf(val, sizeof(val)/sizeof(val[0]), WPFCP_DESC "\\" WPFWP_LDESC, - ODBC_REG_SUBKEY_PATH, LWSTR(&attrs->dsn)) < 0) { - ERRN("failed to print registry key path."); - return FALSE; - } - /* try accessing local user's config first, if that fails, systems' */ - if (RegOpenKeyEx(HKEY_CURRENT_USER, val, /*options*/0, KEY_READ, - &hkey) != ERROR_SUCCESS) { - INFO("failed to open registry key `" REG_HKCU "\\" LWPD "`.", - val); - if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, val, /*options*/0, KEY_READ, - &hkey) != ERROR_SUCCESS) { - INFO("failed to open registry key `" REG_HKLM "\\" LWPD "`.", - val); - goto end; - } else { - ktree = REG_HKLM; - } - } else { - ktree = REG_HKCU; - } - - if (RegQueryInfoKey(hkey, /*key class*/NULL, /*len of key class*/NULL, - /*reserved*/NULL, /*no subkeys*/NULL, /*longest subkey*/NULL, - /*longest subkey class name*/NULL, &valsno, &maxvallen, - &maxdatalen, /*sec descr*/NULL, - /*update time */NULL) != ERROR_SUCCESS) { - ERRN("Failed to query registery key info for path `%s\\" LWPD - "`.", ktree, val); - goto end; - } else { - DBG("Subkey '%s\\" LWPD "': vals: %d, lengthiest name: %d, " - "lengthiest data: %d.", ktree, val, valsno, maxvallen, - maxdatalen); - // malloc buffers? - if (MAX_REG_VAL_NAME < maxvallen) - BUG("value name buffer too small (%d), needed: %dB.", - MAX_REG_VAL_NAME, maxvallen); - if (MAX_REG_DATA_SIZE < maxdatalen) - BUG("value data buffer too small (%d), needed: %dB.", - MAX_REG_DATA_SIZE, maxdatalen); - /* connection could still succeeded, so carry on */ - } - - for (i = 0, j = 0; i < valsno; i ++) { - vallen = sizeof(val) / sizeof(val[0]); - datalen = MAX_REG_DATA_SIZE; - d = (BYTE *)&buff[j * datalen]; - if (RegEnumValue(hkey, i, val, &vallen, /*reserved*/NULL, &valtype, - d, &datalen) != ERROR_SUCCESS) { - ERR("failed to read register subkey value."); - goto end; - } - if (valtype != REG_SZ) { - INFO("unused register values of type %d -- skipping.", - valtype); - continue; - } - tval = (tstr_st) { - val, vallen - }; - tdata = (tstr_st) { - (SQLTCHAR *)d, datalen - }; - if (assign_config_attr(attrs, &tval, &tdata, FALSE)) { - j ++; - DBG("reg entry`" LTPDL "`: `" LTPDL "` assigned.", - LTSTR(&tval), LTSTR(&tdata)); - } else { - INFO("ignoring reg entry `" LTPDL "`: `" LTPDL "`.", - LTSTR(&tval), LTSTR(&tdata)); - /* entry not directly relevant to driver config */ - } + if (type_row) { + free(type_row); + type_row = NULL; } - - ret = TRUE; -end: - RegCloseKey(hkey); - return ret; } -#else /* defined(_WIN32) || defined (WIN32) */ -#error "unsupported platform" /* TODO */ -#endif /* defined(_WIN32) || defined (WIN32) */ - -static BOOL prompt_user(config_attrs_st *attrs, BOOL disable_conn) -{ - // - // TODO: display settings dialog box; - // An error message (if popping it more than once) might be needed. - // - return FALSE; -} SQLRETURN EsSQLDriverConnectW ( @@ -1873,14 +1339,14 @@ SQLRETURN EsSQLDriverConnectW { esodbc_dbc_st *dbc = DBCH(hdbc); SQLRETURN ret; - config_attrs_st attrs; - wstr_st orig_dsn; - BOOL disable_conn = FALSE; - BOOL user_canceled = FALSE; - TCHAR buff[(sizeof(config_attrs_st)/sizeof(wstr_st)) * MAX_REG_DATA_SIZE]; - + esodbc_dsn_attrs_st attrs; + SQLWCHAR buff_dsn[ESODBC_DSN_MAX_ATTR_LEN]; + wstr_st orig_dsn = {buff_dsn, 0}; + BOOL disable_nonconn = FALSE; + BOOL prompt_user = TRUE; + int res; - memset(&attrs, 0, sizeof(attrs)); + init_dsn_attrs(&attrs); if (szConnStrIn) { DBGH(dbc, "Input connection string: '"LWPD"'[%d].", szConnStrIn, @@ -1893,7 +1359,9 @@ SQLRETURN EsSQLDriverConnectW RET_HDIAGS(dbc, SQL_STATE_HY000); } /* original received DSN saved for later query by the app */ - orig_dsn = attrs.dsn; + memcpy(orig_dsn.str, attrs.dsn.str, + attrs.dsn.cnt * sizeof(*attrs.dsn.str)); + orig_dsn.cnt = attrs.dsn.cnt; /* set DSN (to DEFAULT) only if both DSN and Driver kw are missing */ if ((! attrs.driver.cnt) && (! attrs.dsn.cnt)) { @@ -1903,11 +1371,15 @@ SQLRETURN EsSQLDriverConnectW * Default data source. */ INFOH(dbc, "no DRIVER or DSN keyword found in connection string: " "using the \"DEFAULT\" DSN."); - attrs.dsn = MK_WSTR("DEFAULT"); + res = assign_dsn_attr(&attrs, &MK_WSTR(ESODBC_DSN_DSN), + &MK_WSTR("DEFAULT"), /*overwrite?*/TRUE); + assert(0 < res); } } else { INFOH(dbc, "empty connection string: using the \"DEFAULT\" DSN."); - attrs.dsn = MK_WSTR("DEFAULT"); + res = assign_dsn_attr(&attrs, &MK_WSTR(ESODBC_DSN_DSN), + &MK_WSTR("DEFAULT"), /*overwrite?*/TRUE); + assert(0 < res); } assert(attrs.driver.cnt || attrs.dsn.cnt); @@ -1919,14 +1391,16 @@ SQLRETURN EsSQLDriverConnectW * the information in the connection string." */ INFOH(dbc, "configuring the driver by DSN '" LWPDL "'.", LWSTR(&attrs.dsn)); - if (! read_system_info(&attrs, buff)) { + if (! read_system_info(&attrs)) { /* warn, but try to carry on */ WARNH(dbc, "failed to read system info for DSN '" LWPDL "' data.", LWSTR(&attrs.dsn)); /* DM should take care of this, but just in case */ if (! EQ_WSTR(&attrs.dsn, &MK_WSTR("DEFAULT"))) { - attrs.dsn = MK_WSTR("DEFAULT"); - if (! read_system_info(&attrs, buff)) { + res = assign_dsn_attr(&attrs, &MK_WSTR(ESODBC_DSN_DSN), + &MK_WSTR("DEFAULT"), /*overwrite?*/TRUE); + assert(0 < res); + if (! read_system_info(&attrs)) { ERRH(dbc, "failed to read system info for default DSN."); RET_HDIAGS(dbc, SQL_STATE_IM002); } @@ -1936,11 +1410,12 @@ SQLRETURN EsSQLDriverConnectW /* "If the connection string contains the DRIVER keyword, the driver * cannot retrieve information about the data source from the system * information." */ - INFOH(dbc, "configuring the driver '" LWPDL "'.", LWSTR(&attrs.driver)); + INFOH(dbc, "configuring the driver '" LWPDL "'.", + LWSTR(&attrs.driver)); } /* whatever attributes haven't yet been set, init them with defaults */ - assign_defaults(&attrs); + assign_dsn_defaults(&attrs); switch (fDriverCompletion) { case SQL_DRIVER_NOPROMPT: @@ -1951,27 +1426,25 @@ SQLRETURN EsSQLDriverConnectW break; case SQL_DRIVER_COMPLETE_REQUIRED: - disable_conn = TRUE; - //no break; + disable_nonconn = TRUE; + /* no break */ case SQL_DRIVER_COMPLETE: /* try connect first, then, if that fails, prompt user */ - while (! SQL_SUCCEEDED(do_connect(dbc, &attrs))) { - if (! prompt_user(&attrs, disable_conn)) - /* user canceled */ - { - return SQL_NO_DATA; - } - } - break; - - case SQL_DRIVER_PROMPT: + prompt_user = FALSE; + /* no break; */ + case SQL_DRIVER_PROMPT: /* prompt user first, then try connect */ do { - /* prompt user first, then try connect */ - if (! prompt_user(&attrs, FALSE)) + res = prompt_user ? + prompt_user_config(hdbc, &attrs, FALSE) : 1; + if (res < 0) { + ERRH(dbc, "user interaction failed."); + RET_HDIAGS(dbc, SQL_STATE_IM008); + } else if (! res) { /* user canceled */ - { return SQL_NO_DATA; } + /* promt user on next iteration */ + prompt_user = TRUE; } while (! SQL_SUCCEEDED(do_connect(dbc, &attrs))); break; @@ -2008,7 +1481,9 @@ SQLRETURN EsSQLDriverConnectW /* return the final connection string */ if (szConnStrOut || pcchConnStrOut) { /* might have been reset to DEFAULT, if orig was not found */ - attrs.dsn = orig_dsn; + res = assign_dsn_attr(&attrs, &MK_WSTR(ESODBC_DSN_DSN), &orig_dsn, + /*overwrite?*/TRUE); + assert(0 < res); if (! write_connection_string(&attrs, szConnStrOut, cchConnStrOutMax, pcchConnStrOut)) { ERRH(dbc, "failed to build output connection string."); diff --git a/driver/convert.c b/driver/convert.c new file mode 100644 index 00000000..a9ebf19e --- /dev/null +++ b/driver/convert.c @@ -0,0 +1,2888 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +#include +#include +#include +#include + +#include + +#include "log.h" + +#define JSON_VAL_NULL "null" +#define JSON_VAL_TRUE "true" +#define JSON_VAL_FALSE "false" + +#define TM_TO_TIMESTAMP_STRUCT(_tmp/*src*/, _tsp/*dst*/) \ + do { \ + (_tsp)->year = (_tmp)->tm_year + 1900; \ + (_tsp)->month = (_tmp)->tm_mon + 1; \ + (_tsp)->day = (_tmp)->tm_mday; \ + (_tsp)->hour = (_tmp)->tm_hour; \ + (_tsp)->minute = (_tmp)->tm_min; \ + (_tsp)->second = (_tmp)->tm_sec; \ + } while (0) + +/* For fixed size (destination) types, the target buffer can't be NULL. */ +#define REJECT_IF_NULL_DEST_BUFF(_s/*tatement*/, _p/*ointer*/) \ + do { \ + if (! _p) { \ + ERRH(_s, "destination buffer can't be NULL."); \ + RET_HDIAGS(stmt, SQL_STATE_HY009); \ + } \ + } while (0) +#define REJECT_AS_OOR(_stmt, _val, _fix_val, _target) /* Out Of Range */ \ + do { \ + if (_fix_val) { \ + ERRH(_stmt, "can't convert value %llx to %s: out of range.", \ + _val, STR(_target)); \ + } else { \ + ERRH(_stmt, "can't convert value %f to %s: out of range.", \ + _val, STR(_target)); \ + } \ + RET_HDIAGS(_stmt, SQL_STATE_22003); \ + } while (0) + + +#if (0x0300 <= ODBCVER) +# define ESSQL_TYPE_MIN SQL_GUID +# define ESSQL_TYPE_MAX SQL_INTERVAL_MINUTE_TO_SECOND +# define ESSQL_C_TYPE_MIN SQL_C_UTINYINT +# define ESSQL_C_TYPE_MAX SQL_C_INTERVAL_MINUTE_TO_SECOND +#else /* ODBCVER < 0x0300 */ +/* would need to adjust the limits */ +# error "ODBC version not supported; must be 3.0 (0x0300) or higher" +#endif /* 0x0300 <= ODBCVER */ + +#define ESSQL_NORM_RANGE (ESSQL_TYPE_MAX - ESSQL_TYPE_MIN + 1) +#define ESSQL_C_NORM_RANGE (ESSQL_C_TYPE_MAX - ESSQL_C_TYPE_MIN + 1) + +/* conversion matrix SQL indexer */ +#define ESSQL_TYPE_IDX(_t) (_t - ESSQL_TYPE_MIN) +/* conversion matrix C SQL indexer */ +#define ESSQL_C_TYPE_IDX(_t) (_t - ESSQL_C_TYPE_MIN) + +/* sparse SQL-C_SQL types conversion matrix, used for quick compatiblity check + * on columns and parameters binding */ +static BOOL compat_matrix[ESSQL_NORM_RANGE][ESSQL_C_NORM_RANGE] = {FALSE}; + +/* Note: check is array-access unsafe: types IDs must be validated prior to + * checking compatibility (ex. meta type setting) */ +#define ESODBC_TYPES_COMPATIBLE(_sql, _csql) \ + /* if not within the ODBC range, it can only by a binary conversion;.. */ \ + ((ESSQL_TYPE_MAX < _sql && _csql == SQL_C_BINARY) || \ + /* ..otheriwse use the conversion matrix */ \ + compat_matrix[ESSQL_TYPE_IDX(_sql)][ESSQL_C_TYPE_IDX(_csql)]) + +/* populates the compat_matrix as required in: + * https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/converting-data-from-c-to-sql-data-types */ +void convert_init() +{ + SQLSMALLINT i, j, sql, csql; + /*INDENT-OFF*/ + SQLSMALLINT block_idx_sql[] = {SQL_CHAR, SQL_VARCHAR, SQL_LONGVARCHAR, + SQL_WCHAR, SQL_WVARCHAR, SQL_WLONGVARCHAR, SQL_DECIMAL, + SQL_NUMERIC, SQL_BIT, ESODBC_SQL_BOOLEAN, SQL_TINYINT, + SQL_SMALLINT, SQL_INTEGER, SQL_BIGINT, SQL_REAL, SQL_FLOAT, + SQL_DOUBLE + }; + /*INDENT-ON*/ + SQLSMALLINT block_idx_csql[] = {SQL_C_CHAR, SQL_C_WCHAR, + SQL_C_BIT, SQL_C_NUMERIC, SQL_C_STINYINT, SQL_C_UTINYINT, + SQL_C_TINYINT, SQL_C_SBIGINT, SQL_C_UBIGINT, SQL_C_SSHORT, + SQL_C_USHORT, SQL_C_SHORT, SQL_C_SLONG, SQL_C_ULONG, + SQL_C_LONG, SQL_C_FLOAT, SQL_C_DOUBLE, SQL_C_BINARY + }; + SQLSMALLINT to_csql_interval[] = {SQL_CHAR, SQL_VARCHAR, SQL_LONGVARCHAR, + SQL_WCHAR, SQL_WVARCHAR, SQL_WLONGVARCHAR, SQL_DECIMAL, + SQL_NUMERIC, SQL_TINYINT, SQL_SMALLINT, SQL_INTEGER, SQL_BIGINT + }; + SQLSMALLINT from_sql_interval[] = {SQL_C_CHAR, SQL_C_WCHAR, + SQL_C_BIT,SQL_C_NUMERIC, SQL_C_STINYINT, SQL_C_UTINYINT, + SQL_C_TINYINT, SQL_C_SBIGINT, SQL_C_UBIGINT, SQL_C_SSHORT, + SQL_C_USHORT, SQL_C_SHORT, SQL_C_SLONG, SQL_C_ULONG, + SQL_C_LONG + }; + SQLSMALLINT sql_interval[] = {SQL_INTERVAL_MONTH, SQL_INTERVAL_YEAR, + SQL_INTERVAL_YEAR_TO_MONTH, SQL_INTERVAL_DAY, + SQL_INTERVAL_HOUR, SQL_INTERVAL_MINUTE, SQL_INTERVAL_SECOND, + SQL_INTERVAL_DAY_TO_HOUR, SQL_INTERVAL_DAY_TO_MINUTE, + SQL_INTERVAL_DAY_TO_SECOND, SQL_INTERVAL_HOUR_TO_MINUTE, + SQL_INTERVAL_HOUR_TO_SECOND, SQL_INTERVAL_MINUTE_TO_SECOND + }; + SQLSMALLINT csql_interval[] = {SQL_C_INTERVAL_DAY, SQL_C_INTERVAL_HOUR, + SQL_C_INTERVAL_MINUTE, SQL_C_INTERVAL_SECOND, + SQL_C_INTERVAL_DAY_TO_HOUR, SQL_C_INTERVAL_DAY_TO_MINUTE, + SQL_C_INTERVAL_DAY_TO_SECOND, SQL_C_INTERVAL_HOUR_TO_MINUTE, + SQL_C_INTERVAL_HOUR_TO_SECOND, + SQL_C_INTERVAL_MINUTE_TO_SECOND, SQL_C_INTERVAL_MONTH, + SQL_C_INTERVAL_YEAR, SQL_C_INTERVAL_YEAR_TO_MONTH + }; + SQLSMALLINT to_csql_datetime[] = {SQL_CHAR, SQL_VARCHAR, SQL_LONGVARCHAR, + SQL_WCHAR, SQL_WVARCHAR, SQL_WLONGVARCHAR, SQL_TYPE_DATE, + SQL_TYPE_TIME, SQL_TYPE_TIMESTAMP + }; + SQLSMALLINT csql_datetime[] = {SQL_C_TYPE_DATE, SQL_C_TYPE_TIME, + SQL_C_TYPE_TIMESTAMP + }; + + /* fill the compact block of TRUEs (growing from the upper left corner) */ + for (i = 0; i < sizeof(block_idx_sql)/sizeof(*block_idx_sql) ; i ++) { + for (j = 0; j < sizeof(block_idx_csql)/sizeof(*block_idx_csql) ; j ++) { + sql = block_idx_sql[i]; + csql = block_idx_csql[j]; + compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; + } + } + + /* SQL_C_ BINARY, CHAR, WCHAR and DEFAULT are comatible with all SQL types; + * this will set also non-ODBC intersections (but it's convenient) */ + for (sql = 0; sql < ESSQL_NORM_RANGE; sql ++) { + compat_matrix[sql][ESSQL_C_TYPE_IDX(SQL_C_CHAR)] = TRUE; + compat_matrix[sql][ESSQL_C_TYPE_IDX(SQL_C_WCHAR)] = TRUE; + compat_matrix[sql][ESSQL_C_TYPE_IDX(SQL_C_BINARY)] = TRUE; + compat_matrix[sql][ESSQL_C_TYPE_IDX(SQL_C_DEFAULT)] = TRUE; + } + + /* ESODBC_SQL_NULL (NULL) is compabitle with all SQL_C types */ + for (csql = 0; csql < ESSQL_C_NORM_RANGE; csql ++) { + compat_matrix[ESSQL_TYPE_IDX(ESODBC_SQL_NULL)][csql] = TRUE; + } + + /* set conversions to INTERVAL_C */ + for (i = 0; i < sizeof(to_csql_interval)/sizeof(*to_csql_interval); i ++) { + for (j = 0; j < sizeof(csql_interval)/sizeof(*csql_interval); j ++ ) { + sql = to_csql_interval[i]; + csql = csql_interval[j]; + compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; + } + } + + /* set conversions from INTERVAL_SQL */ + for (i = 0; i < sizeof(sql_interval)/sizeof(*sql_interval); i ++) { + for (j = 0; j < sizeof(from_sql_interval)/sizeof(*from_sql_interval); + j ++ ) { + sql = sql_interval[i]; + csql = from_sql_interval[j]; + compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; + } + } + + /* set conversions between date-time types */ + for (i = 0; i < sizeof(to_csql_datetime)/sizeof(*to_csql_datetime); i ++) { + for (j = 0; j < sizeof(csql_datetime)/sizeof(*csql_datetime); j ++ ) { + sql = to_csql_datetime[i]; + csql = csql_datetime[j]; + if (sql == SQL_TYPE_DATE && csql == SQL_C_TYPE_TIME) { + continue; + } + if (sql == SQL_TYPE_TIME && csql == SQL_C_TYPE_DATE) { + continue; + } + compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; + } + } + + /* GUID conversion */ + sql = SQL_GUID; + csql = SQL_C_GUID; + compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; +} + + +SQLRETURN set_param_decdigits(esodbc_rec_st *irec, + SQLUSMALLINT param_no, SQLSMALLINT decdigits) +{ + assert(irec->desc->type == DESC_TYPE_IPD); + + switch (irec->meta_type) { + /* for "SQL_TYPE_TIME, SQL_TYPE_TIMESTAMP, SQL_INTERVAL_SECOND, + * SQL_INTERVAL_DAY_TO_SECOND, SQL_INTERVAL_HOUR_TO_SECOND, or + * SQL_INTERVAL_MINUTE_TO_SECOND, the SQL_DESC_PRECISION field of the + * IPD is set to DecimalDigits." */ + case METATYPE_DATETIME: + if (irec->concise_type == SQL_TYPE_DATE) { + break; + } + case METATYPE_INTERVAL_WSEC: + if (decdigits < 0) { + ERRH(irec->desc, "can't set negative (%hd) as fractional " + "second precision.", decdigits); + RET_HDIAGS(irec->desc, SQL_STATE_HY104); + } + return EsSQLSetDescFieldW(irec->desc, param_no, SQL_DESC_PRECISION, + (SQLPOINTER)(intptr_t)decdigits, SQL_IS_SMALLINT); + + /* for " SQL_NUMERIC or SQL_DECIMAL, the SQL_DESC_SCALE field of the + * IPD is set to DecimalDigits." */ + case METATYPE_EXACT_NUMERIC: + if (irec->concise_type == SQL_DECIMAL || + irec->concise_type == SQL_NUMERIC) { + return EsSQLSetDescFieldW(irec->desc, param_no, SQL_DESC_SCALE, + (SQLPOINTER)(intptr_t)decdigits, SQL_IS_SMALLINT); + } + break; // formal + + default: + /* "For all other data types, the DecimalDigits argument is + * ignored." */ + ; + } + + return SQL_SUCCESS; +} + +SQLSMALLINT get_param_decdigits(esodbc_rec_st *irec) +{ + assert(irec->desc->type == DESC_TYPE_IPD); + + switch(irec->meta_type) { + case METATYPE_DATETIME: + if (irec->concise_type == SQL_TYPE_DATE) { + break; + } + case METATYPE_INTERVAL_WSEC: + return irec->precision; + + case METATYPE_EXACT_NUMERIC: + if (irec->concise_type == SQL_DECIMAL || + irec->concise_type == SQL_NUMERIC) { + return irec->scale; + } + break; + + default: + WARNH(irec->desc, "retriving decdigits for IPD metatype: %d.", + irec->meta_type); + } + + return 0; +} + +SQLRETURN set_param_size(esodbc_rec_st *irec, + SQLUSMALLINT param_no, SQLULEN size) +{ + assert(irec->desc->type == DESC_TYPE_IPD); + + switch (irec->meta_type) { + /* for "SQL_CHAR, SQL_VARCHAR, SQL_LONGVARCHAR, SQL_BINARY, + * SQL_VARBINARY, SQL_LONGVARBINARY, or one of the concise SQL + * datetime or interval data types, the SQL_DESC_LENGTH field of the + * IPD is set to the value of [s]ize." */ + case METATYPE_STRING: + case METATYPE_BIN: + case METATYPE_DATETIME: + case METATYPE_INTERVAL_WSEC: + case METATYPE_INTERVAL_WOSEC: + return EsSQLSetDescFieldW(irec->desc, param_no, SQL_DESC_LENGTH, + (SQLPOINTER)(uintptr_t)size, SQL_IS_UINTEGER); + + /* for "SQL_DECIMAL, SQL_NUMERIC, SQL_FLOAT, SQL_REAL, or SQL_DOUBLE, + * the SQL_DESC_PRECISION field of the IPD is set to the value of + * [s]ize." */ + case METATYPE_EXACT_NUMERIC: + if (irec->concise_type != SQL_DECIMAL && + irec->concise_type != SQL_NUMERIC) { + break; + } + /* no break */ + case METATYPE_FLOAT_NUMERIC: + // TODO: https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/column-size : + // "The ColumnSize argument of SQLBindParameter is ignored for + // this data type." (floats included): ???? + return EsSQLSetDescFieldW(irec->desc, param_no, SQL_DESC_PRECISION, + /* cast: ULEN -> SMALLINT; XXX: range check? */ + (SQLPOINTER)(uintptr_t)size, SQL_IS_SMALLINT); + + default: + ;/* "For other data types, the [s]ize argument is ignored." */ + } + return SQL_SUCCESS; +} + +SQLULEN get_param_size(esodbc_rec_st *irec) +{ + assert(irec->desc->type == DESC_TYPE_IPD); + + switch (irec->meta_type) { + case METATYPE_STRING: + case METATYPE_BIN: + case METATYPE_DATETIME: + case METATYPE_INTERVAL_WSEC: + case METATYPE_INTERVAL_WOSEC: + return irec->length; + + case METATYPE_EXACT_NUMERIC: + // TODO: make DEC, NUM a floating meta? + if (irec->concise_type != SQL_DECIMAL && + irec->concise_type != SQL_NUMERIC) { + assert(irec->es_type); + return irec->es_type->column_size; + } + + case METATYPE_FLOAT_NUMERIC: + return irec->precision; + + default: + WARNH(irec->desc, "retriving colsize for IPD metatype: %d.", + irec->meta_type); + } + return 0; +} + + +/* + * field: SQL_DESC_: DATA_PTR / INDICATOR_PTR / OCTET_LENGTH_PTR + * pos: position in array/row_set (not result_set) + */ +inline void *deferred_address(SQLSMALLINT field_id, size_t pos, + esodbc_rec_st *rec) +{ + size_t elem_size; + SQLLEN offt; + void *base; + esodbc_desc_st *desc = rec->desc; + +#define ROW_OFFSETS \ + do { \ + elem_size = desc->bind_type; \ + offt = desc->bind_offset_ptr ? *(desc->bind_offset_ptr) : 0; \ + } while (0) + + switch (field_id) { + case SQL_DESC_DATA_PTR: + base = rec->data_ptr; + if (desc->bind_type == SQL_BIND_BY_COLUMN) { + elem_size = (size_t)rec->octet_length; + offt = 0; + } else { /* by row */ + ROW_OFFSETS; + } + break; + case SQL_DESC_INDICATOR_PTR: + base = rec->indicator_ptr; + if (desc->bind_type == SQL_BIND_BY_COLUMN) { + elem_size = sizeof(*rec->indicator_ptr); + offt = 0; + } else { /* by row */ + ROW_OFFSETS; + } + break; + case SQL_DESC_OCTET_LENGTH_PTR: + base = rec->octet_length_ptr; + if (desc->bind_type == SQL_BIND_BY_COLUMN) { + elem_size = sizeof(*rec->octet_length_ptr); + offt = 0; + } else { /* by row */ + ROW_OFFSETS; + } + break; + default: + BUG("can't calculate the deferred address of field type %d.", + field_id); + return NULL; + } +#undef ROW_OFFSETS + + DBGH(desc->hdr.stmt, "rec@0x%p, field_id:%hd, pos: %zu : base@0x%p, " + "offset=%lld, elem_size=%zu", rec, field_id, pos, base, offt, + elem_size); + + return base ? (char *)base + offt + pos * elem_size : NULL; +} + +/* + * Handles the lengths of the data to copy out to the application: + * (1) returns the max amount of bytes to copy (in the data_ptr), taking into + * account size of data and of buffer, relevant statement attribute and + * buffer type; + * (2) indicates if truncation occured into 'state'. + * WARN: only to be used with ARD.meta_type == STR || BIN (as it can indicate + * a size to copy smaller than the original -- truncating). + */ +static size_t buff_octet_size( + size_t avail, /* how many bytes are there to copy out */ + size_t unit_size, /* the unit size of the buffer (i.e. sizeof(wchar_t)) */ + esodbc_rec_st *arec, esodbc_rec_st *irec, + esodbc_state_et *state /* out param: only written when truncating */) +{ + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + /* how large (bytes) is the buffer to copy into*/ + size_t room = (size_t)arec->octet_length; + /* statement attribute SQL_ATTR_MAX_LENGTH value */ + size_t attr_max = stmt->max_length; + /* meta type of IRD */ + esodbc_metatype_et ird_mt = irec->meta_type; + size_t max_copy, max; + + /* type is signed, driver should not allow a negative to this point: + * making sure the cast above is sane. */ + assert(0 <= arec->octet_length); + + /* truncate to statment max bytes, only if "the column contains character + * or binary data" */ + max = (ird_mt == METATYPE_STRING || ird_mt == METATYPE_BIN) ? attr_max : 0; + + /* apply "network" truncation first, if need to */ + if (0 < max && max < avail) { + INFO("applying 'network' truncation %zd -> %zd.", avail, max); + max_copy = max; + /* no truncation indicated for this case */ + } else { + max_copy = avail; + } + + /* is target buffer to small? adjust size if so and indicate truncation */ + /* Note: this should only be tested/applied if ARD.meta_type == STR||BIN */ + // FIXME: check note above + if (room < max_copy) { + INFO("applying buffer truncation %zd -> %zd.", max_copy, room); + max_copy = room; + *state = SQL_STATE_01004; + } + + /* adjust to align to target buffer unit */ + if (max_copy % unit_size) { + max_copy -= max_copy % unit_size; + } + + DBG("avail=%zd, room=%zd, attr_max=%zd, metatype:%d => " + "max_copy=%zd, state=%d.", + avail, room, attr_max, ird_mt, max_copy, *state); + return max_copy; +} + +/* + * Indicate the amount of data available to the application, taking into + * account: the type of data, should truncation - due to max length attr + * setting - need to be indicated, since original length is indicated, w/o + * possible buffer truncation, but with possible 'network' truncation. + */ +static inline void write_out_octets( + SQLLEN *octet_len_ptr, /* buffer to write the avail octets into */ + size_t avail, /* amount of bytes avail */ + esodbc_rec_st *irec) +{ + esodbc_stmt_st *stmt = irec->desc->hdr.stmt; + /* statement attribute SQL_ATTR_MAX_LENGTH value */ + size_t attr_max = stmt->max_length; + /* meta type of IRD */ + esodbc_metatype_et ird_mt = irec->meta_type; + size_t max; + + if (! octet_len_ptr) { + DBG("NULL octet len pointer, length (%zd) not indicated.", avail); + return; + } + + /* truncate to statment max bytes, only if "the column contains character + * or binary data" */ + max = (ird_mt == METATYPE_STRING || ird_mt == METATYPE_BIN) ? attr_max : 0; + + if (0 < max) { + /* put the value of SQL_ATTR_MAX_LENGTH attribute.. even + * if this would be larger than what the data actually + * occupies after conversion: "the driver has no way of + * figuring out what the actual length is" */ + *octet_len_ptr = max; + DBG("max length (%zd) attribute enforced.", max); + } else { + /* if no "network" truncation done, indicate data's length, no + * matter if truncated to buffer's size or not */ + *octet_len_ptr = avail; + } + + DBG("length of data available for transfer: %ld", *octet_len_ptr); +} + +/* if an application doesn't specify the conversion, use column's type */ +static inline SQLSMALLINT get_rec_c_type(esodbc_rec_st *arec, + esodbc_rec_st *irec) +{ + SQLSMALLINT ctype; + /* "To use the default mapping, an application specifies the SQL_C_DEFAULT + * type identifier." */ + if (arec->concise_type != SQL_C_DEFAULT) { + ctype = arec->concise_type; + } else { + ctype = irec->es_type->c_concise_type; + } + DBGH(arec->desc, "target data C type: %hd.", ctype); + return ctype; +} + + +static inline void gd_offset_apply(esodbc_stmt_st *stmt, xstr_st *xstr) +{ + if (! STMT_GD_CALLING(stmt)) { + return; + } + + assert(0 <= stmt->gd_offt); /* negative means "data exhausted" */ + assert(stmt->gd_offt < (SQLLEN)xstr->w.cnt + /*\0*/1); + + if (xstr->wide) { + xstr->w.str += stmt->gd_offt; + xstr->w.cnt -= stmt->gd_offt; + } else { + xstr->c.str += stmt->gd_offt; + xstr->c.cnt -= stmt->gd_offt; + } + + DBGH(stmt, "applied an offset of %lld.", stmt->gd_offt); +} + +/* + * cnt: character count of string/bin to transfer, excluding \0 + * xfed: char count of copied data. + */ +static inline void gd_offset_update(esodbc_stmt_st *stmt, size_t cnt, + size_t xfed) +{ + if (! STMT_GD_CALLING(stmt)) { + return; + } + + if (cnt <= xfed) { + /* if all has been transfered, indicate so in the gd_offt */ + stmt->gd_offt = -1; + } else { + stmt->gd_offt += xfed; + } + + DBGH(stmt, "offset updated with %zu to new value of %lld.", xfed, + stmt->gd_offt); +} + + + +/* transfer to the application a 0-terminated (but unaccounted for) cstr_st */ +static SQLRETURN transfer_xstr0(esodbc_rec_st *arec, esodbc_rec_st *irec, + xstr_st *xsrc, void *data_ptr, SQLLEN *octet_len_ptr) +{ + size_t in_bytes, in_chars; + SQLCHAR *dst_c; + SQLWCHAR *dst_w; + size_t cnt, char_sz; + esodbc_state_et state = SQL_STATE_00000; + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + + /* apply source offset, if this is a SQLGetData() call */ + gd_offset_apply(stmt, xsrc); + + cnt = xsrc->w.cnt; /*==->c.cnt*/ + if (xsrc->wide) { + /* the source string must be 0-term'd (buff_octet_size param below + * counts it) */ + assert(xsrc->w.str[cnt] == 0); + char_sz = sizeof(*xsrc->w.str); + } else { + assert(xsrc->c.str[cnt] == 0); + char_sz = sizeof(*xsrc->c.str); + } + + /* always return the app the untruncated number of bytes */ + write_out_octets(octet_len_ptr, cnt * char_sz, irec); + + if (data_ptr) { + in_bytes = buff_octet_size((cnt + /*\0*/1) * char_sz, char_sz, + arec, irec, &state); + if (in_bytes) { + in_chars = in_bytes / char_sz; + /* deduct the \0 added above; which is needed, since we need to + * copy it too out to the app (or truncate the data, but still not + * count the \0) */ + in_chars --; + + if (xsrc->wide) { + dst_w = (SQLWCHAR *)data_ptr; + memcpy(dst_w, xsrc->w.str, in_bytes); + /* TODO: should the left be filled with spaces? : + * https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/rules-for-conversions */ + + if (state != SQL_STATE_00000) { + /* 0-term the buffer */ + dst_w[in_chars] = 0; + DBGH(stmt, "aREC@0x%p: `" LWPDL "` transfered truncated " + "as `" LWPDL "` at data_ptr@0x%p.", arec, + LWSTR(&xsrc->w), in_chars, dst_w, dst_w); + } else { + assert(dst_w[in_chars] == 0); + DBGH(stmt, "aREC@0x%p: `" LWPDL "` transfered at " + "data_ptr@0x%p.", arec, LWSTR(&xsrc->w), dst_w); + } + } else { + dst_c = (SQLCHAR *)data_ptr; + memcpy(dst_c, xsrc->c.str, in_bytes); + + if (state != SQL_STATE_00000) { + /* 0-term the buffer */ + dst_c[in_chars] = 0; + DBGH(stmt, "aREC@0x%p: `" LCPDL "` transfered truncated " + "as `" LCPDL "` at data_ptr@0x%p.", arec, + LCSTR(&xsrc->w), in_chars, dst_c, dst_c); + } else { + assert(dst_c[in_chars] == 0); + DBGH(stmt, "aREC@0x%p: `" LCPDL "` transfered at " + "data_ptr@0x%p.", arec, LCSTR(&xsrc->c), dst_c); + } + } + + /* only update offset if data is copied out */ + gd_offset_update(stmt, xsrc->w.cnt, in_chars); /*==->c.cnt*/ + } + } else { + DBGH(stmt, "aREC@0x%p: NULL transfer buffer.", arec); + } + + if (state != SQL_STATE_00000) { + RET_HDIAGS(stmt, state); + } + return SQL_SUCCESS; +} + +/* 10^n */ +static inline unsigned long long pow10(unsigned n) +{ + unsigned long long pow = 1; + pow <<= n; + while (n--) { + pow += pow << 2; + } + return pow; +} + +static SQLRETURN double_to_numeric(esodbc_rec_st *arec, double src, void *dst) +{ + SQL_NUMERIC_STRUCT *numeric; + esodbc_stmt_st *stmt; + SQLSMALLINT prec/*..ision*/; + unsigned long long ullng, pow; + long long llng; + + stmt = arec->desc->hdr.stmt; + numeric = (SQL_NUMERIC_STRUCT *)dst; + assert(numeric); + + numeric->scale = (SQLCHAR)arec->scale; + numeric->sign = 0 <= src; + + ullng = numeric->sign ? (unsigned long long)src : (unsigned long long)-src; + /* =~ log10(abs(src)) */ + for (prec = 0; ullng; prec ++) { + ullng /= 10; + } + if (arec->scale < 0) { + pow = pow10(-arec->scale); + llng = (long long)(src / pow); + prec += arec->scale; /* prec lowers */ + } else if (0 < arec->scale) { + pow = pow10(arec->scale); + if (DBL_MAX / pow < src) { + // TODO: numeric.val is 16 octets long -> expand + ERRH(stmt, "max numeric conversion scale reached."); + RET_HDIAGS(stmt, SQL_STATE_22003); + } + llng = (long long)(src * pow); + prec += arec->scale; /* prec grows */ + } else { + llng = (long long)src; + } + ullng = numeric->sign ? (unsigned long long)llng : + (unsigned long long)-llng; + + DBGH(stmt, "arec@0x%p: precision=%hd, scale=%hd; src.precision=%hd", + arec, arec->precision, arec->scale, prec); + if ((UCHAR_MAX < prec) || (0 < arec->precision && arec->precision < prec)) { + /* precision of source is higher than requested => overflow */ + ERRH(stmt, "conversion overflow. source: %.6e; requested: " + "precisions: %d, scale: %d.", src, arec->precision, arec->scale); + RET_HDIAGS(stmt, SQL_STATE_22003); + } else if (prec < 0) { + prec = 0; + assert(ullng == 0); + } + numeric->precision = (SQLCHAR)prec; + + +#if REG_DWORD != REG_DWORD_LITTLE_ENDIAN + ullng = _byteswap_ulong(ullng); +#endif /* LE */ + assert(sizeof(ullng) <= sizeof(numeric->val)); + memcpy(numeric->val, (char *)&ullng, sizeof(ullng)); + memset(numeric->val+sizeof(ullng), 0, sizeof(numeric->val)-sizeof(ullng)); + + DBGH(stmt, "double %.6e converted to numeric: .sign=%d, precision=%d " + "(req: %d), .scale=%d (req: %d), .val:`" LCPDL "` (0x%lx).", src, + numeric->sign, numeric->precision, arec->precision, + numeric->scale, arec->scale, (int)sizeof(numeric->val), numeric->val, + ullng); + return SQL_SUCCESS; +} + +static SQLRETURN numeric_to_double(esodbc_rec_st *irec, void *src, double *dst) +{ + unsigned long long ullng, pow; + double dbl; + SQLSMALLINT prec/*..ision*/; + SQL_NUMERIC_STRUCT *numeric; + esodbc_stmt_st *stmt = irec->desc->hdr.stmt; + + assert(src); + numeric = (SQL_NUMERIC_STRUCT *)src; + + assert(2 * sizeof(ullng) == sizeof(numeric->val)); + ullng = *(unsigned long long *)&numeric->val[sizeof(ullng)]; + if (ullng) { + // TODO: shift down with scale + ERRH(stmt, "max numeric precision scale reached."); + goto erange; + } + ullng = *(unsigned long long *)&numeric->val[0]; +#if REG_DWORD != REG_DWORD_LITTLE_ENDIAN + ullng = _byteswap_ulong(ullng); +#endif /* LE */ + + /* =~ log10(abs(ullng)) */ + for (prec = 0, pow = ullng; pow; prec ++) { + pow /= 10; + } + + if (DBL_MAX < ullng) { + goto erange; + } else { + dbl = (double)ullng; + } + + if (numeric->scale < 0) { + pow = pow10(-numeric->scale); + if (DBL_MAX / pow < dbl) { + goto erange; + } + dbl *= pow; + prec -= numeric->scale; /* prec grows */ + } else if (0 < numeric->scale) { + pow = pow10(numeric->scale); + dbl /= pow; + prec -= numeric->scale; /* prec lowers */ + } + + DBGH(stmt, "irec@0x%p: precision=%hd, scale=%hd; src.precision=%hd", + irec, irec->precision, irec->scale, prec); + if ((UCHAR_MAX < prec) || (0 < irec->precision && irec->precision < prec)) { + ERRH(stmt, "source precision (%hd) larger than requested (%hd)", + prec, irec->precision); + goto erange; + } else { + if (! numeric->sign) { + dbl = -dbl; + } + } + + DBGH(stmt, "VAL: %f", dbl); + DBGH(stmt, "numeric val: %llu, scale: %hhd, precision: %hhu converted to " + "double %.6e.", ullng, numeric->scale, numeric->precision, dbl); + + *dst = dbl; + return SQL_SUCCESS; +erange: + ERRH(stmt, "can't convert numeric val: %llu, scale: %hhd, precision: %hhu" + " to double.", ullng, numeric->scale, numeric->precision); + RET_HDIAGS(stmt, SQL_STATE_22003); +} + +/* + * https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/transferring-data-in-its-binary-form + */ +static SQLRETURN llong_to_binary(esodbc_rec_st *arec, esodbc_rec_st *irec, + long long src, void *dst, SQLLEN *src_len) +{ + size_t cnt; + char *s = (char *)&src; + esodbc_state_et state = SQL_STATE_00000; + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + unsigned long long ull = src < 0 ? -src : src; + + /* UJ4C uses long long for any integer type -> find out the + * smallest type that would accomodate the value (since fixed negatives + * would take more space then minimally required). */ + if (ull < CHAR_MAX) { + cnt = sizeof(char); + } else if (ull < SHRT_MAX) { + cnt = sizeof(short); + } else if (ull < INT_MAX) { + cnt = sizeof(int); + } else if (ull < LONG_MAX) { + cnt = sizeof(long); + } else { /* definetely ull < LLONG_MAX */ + cnt = sizeof(long long); + } + + cnt = buff_octet_size(cnt, sizeof(*s), arec, irec, &state); + if (state) { /* has it been shrunk? */ + REJECT_AS_OOR(stmt, src, /*fixed?*/TRUE, "[BINARY]<[value]"); + } + + if (dst) { + /* copy bytes as-are: the reverse conversion need to take place on + * "same DBMS and hardare platform". */ + memcpy(dst, s, cnt); + //TODO: should the driver clear all the received buffer?? Cfg option? + //memset((char *)dst + cnt, 0, arec->octet_length - cnt); + } + write_out_octets(src_len, cnt, irec); + DBGH(stmt, "long long value %lld, converted on %zd octets.", src, cnt); + + return SQL_SUCCESS; +} + +static SQLRETURN longlong_to_str(esodbc_rec_st *arec, esodbc_rec_st *irec, + long long ll, void *data_ptr, SQLLEN *octet_len_ptr, BOOL wide) +{ + SQLRETURN ret; + /* buffer is overprovisioned for !wide, but avoids double declaration */ + SQLCHAR buff[(ESODBC_PRECISION_INT64 + /*0-term*/1 + /*+/-*/1) + * sizeof(SQLWCHAR)]; + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + xstr_st xsrc = (xstr_st) { + .wide = wide, + .c = (cstr_st) { + buff, 0 + } + }; + + xsrc.w.cnt = i64tot((int64_t)ll, buff, wide); /*==.c.cnt*/ + + if (wide) { + DBGH(stmt, "long long %lld convertible to w-string `" LWPD "` on " + "%zd octets.", ll, xsrc.w.str, xsrc.w.cnt); + } else { + DBGH(stmt, "long long %lld convertible to string `" LCPD "` on " + "%zd octets.", ll, xsrc.c.str, xsrc.c.cnt); + } + ret = transfer_xstr0(arec, irec, &xsrc, data_ptr, octet_len_ptr); + + /* need to change the error code from truncation to "out of + * range", since "whole digits" are truncated */ + if (ret == SQL_SUCCESS_WITH_INFO && + HDRH(stmt)->diag.state == SQL_STATE_01004) { + if (STMT_GD_CALLING(stmt)) { + return ret; + } else { + REJECT_AS_OOR(stmt, ll, /*fixed?*/TRUE, "[STRING]<[value]"); + } + } + return SQL_SUCCESS; +} + +SQLRETURN sql2c_longlong(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, long long ll) +{ + esodbc_stmt_st *stmt; + void *data_ptr; + SQLLEN *octet_len_ptr; + esodbc_desc_st *ard, *ird; + SQLSMALLINT ctype; + SQLRETURN ret; + + stmt = arec->desc->hdr.stmt; + ird = stmt->ird; + ard = stmt->ard; + + /* pointer where to write how many characters we will/would use */ + octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); + /* pointer to app's buffer */ + data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); + + /* Assume a C type behind an SQL C type, but check size representation. + * Note: won't work if _min==0 is a legit limit */ +# define REJECT_IF_OOR(_stmt, _ll, _min, _max, _sqlctype, _ctype) \ + do { \ + assert(sizeof(_sqlctype) == sizeof(_ctype)); \ + if ((_min && _ll < _min) || _max < _ll) { \ + REJECT_AS_OOR(_stmt, _ll, /*fixed int*/TRUE, _ctype); \ + } \ + } while (0) + /* Transfer a long long to an SQL integer type. + * Uses local vars: stmt, data_ptr, irec, octet_len_ptr. */ +# define TRANSFER_LL(_ll, _min, _max, _sqlctype, _ctype) \ + do { \ + REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); \ + REJECT_IF_OOR(stmt, _ll, _min, _max, _sqlctype, _ctype); \ + *(_sqlctype *)data_ptr = (_sqlctype)_ll; \ + write_out_octets(octet_len_ptr, sizeof(_sqlctype), irec); \ + DBGH(stmt, "converted long long %lld to " STR(_sqlctype) " 0x%llx.", \ + _ll, (intptr_t)*(_sqlctype *)data_ptr); \ + } while (0) + + switch ((ctype = get_rec_c_type(arec, irec))) { + case SQL_C_CHAR: + case SQL_C_WCHAR: + return longlong_to_str(arec, irec, ll, data_ptr, octet_len_ptr, + ctype == SQL_C_WCHAR); + + case SQL_C_TINYINT: + case SQL_C_STINYINT: + TRANSFER_LL(ll, CHAR_MIN, CHAR_MAX, SQLSCHAR, char); + break; + case SQL_C_UTINYINT: + TRANSFER_LL(ll, 0, UCHAR_MAX, SQLCHAR, unsigned char); + break; + case SQL_C_SHORT: + case SQL_C_SSHORT: + TRANSFER_LL(ll, SHRT_MIN, SHRT_MAX, SQLSMALLINT, short); + break; + case SQL_C_USHORT: + TRANSFER_LL(ll, 0, USHRT_MAX, SQLUSMALLINT, unsigned short); + break; + case SQL_C_LONG: + case SQL_C_SLONG: + TRANSFER_LL(ll, LONG_MIN, LONG_MAX, SQLINTEGER, long); + break; + case SQL_C_ULONG: + TRANSFER_LL(ll, 0, ULONG_MAX, SQLUINTEGER, unsigned long); + break; + case SQL_C_SBIGINT: + TRANSFER_LL(ll, LLONG_MIN, LLONG_MAX, SQLBIGINT, long long); + break; + case SQL_C_UBIGINT: + TRANSFER_LL(ll, 0, ULLONG_MAX, SQLUBIGINT, unsigned long long); + break; + + case SQL_C_BIT: + REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); + if (ll < 0 || 2 <= ll) { + REJECT_AS_OOR(stmt, ll, /*fixed int*/TRUE, SQL_C_BIT); + } else { /* 0 or 1 */ + *(SQLCHAR *)data_ptr = (SQLCHAR)ll; + } + write_out_octets(octet_len_ptr, sizeof(SQLSCHAR), irec); + break; + + case SQL_C_NUMERIC: + REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); + ret = double_to_numeric(arec, (double)ll, data_ptr); + if (! SQL_SUCCEEDED(ret)) { + return ret; + } + write_out_octets(octet_len_ptr, sizeof(SQL_NUMERIC_STRUCT), irec); + break; + + case SQL_C_FLOAT: + REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); + REJECT_IF_OOR(stmt, ll, -FLT_MAX, FLT_MAX, SQLREAL, float); + *(SQLREAL *)data_ptr = (SQLREAL)ll; + write_out_octets(octet_len_ptr, sizeof(SQLREAL), irec); + break; + + case SQL_C_DOUBLE: + REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); + REJECT_IF_OOR(stmt, ll, -DBL_MAX, DBL_MAX, SQLDOUBLE, double); + *(SQLDOUBLE *)data_ptr = (SQLDOUBLE)ll; + write_out_octets(octet_len_ptr, sizeof(SQLDOUBLE), irec); + break; + + case SQL_C_BINARY: + return llong_to_binary(arec, irec, ll, data_ptr, octet_len_ptr); + + default: + BUGH(stmt, "unexpected unhanlded data type: %d.", + get_rec_c_type(arec, irec)); + return SQL_ERROR; + } + DBGH(stmt, "REC@0x%p, data_ptr@0x%p, copied long long: %lld.", arec, + data_ptr, ll); + + return SQL_SUCCESS; + +# undef REJECT_IF_OOR +# undef TRANSFER_LL +} + +static SQLRETURN double_to_bit(esodbc_rec_st *arec, esodbc_rec_st *irec, + double src, void *data_ptr, SQLLEN *octet_len_ptr) +{ + esodbc_state_et state = SQL_STATE_00000; + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + + REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); + + write_out_octets(octet_len_ptr, sizeof(SQLCHAR), irec); + + if (src < 0. || 2. <= src) { + REJECT_AS_OOR(stmt, src, /*fixed?*/FALSE, SQL_C_BIT); + } else if (0. < src && src < 1.) { + *(SQLCHAR *)data_ptr = 0; + state = SQL_STATE_01S07; + } else if (1. < src && src < 2.) { + *(SQLCHAR *)data_ptr = 1; + state = SQL_STATE_01S07; + } else { /* 0 or 1 */ + *(SQLCHAR *)data_ptr = (SQLCHAR)src; + } + if (state != SQL_STATE_00000) { + INFOH(stmt, "truncating when converting %f as %d.", src, + *(SQLCHAR *)data_ptr); + RET_HDIAGS(stmt, state); + } + + DBGH(stmt, "double %f converted to bit %d.", src, *(SQLCHAR *)data_ptr); + + return SQL_SUCCESS; +} + +static SQLRETURN double_to_binary(esodbc_rec_st *arec, esodbc_rec_st *irec, + double dbl, void *data_ptr, SQLLEN *octet_len_ptr) +{ + size_t cnt; + double udbl = dbl < 0. ? -dbl : dbl; + float flt; + char *ptr; + esodbc_state_et state = SQL_STATE_00000; + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + + if (udbl < FLT_MIN || FLT_MAX < udbl) { + /* value's precision/scale requires a double */ + cnt = sizeof(dbl); + ptr = (char *)&dbl; + } else { + flt = (float)dbl; + cnt = sizeof(flt); + ptr = (char *)&flt; + } + + cnt = buff_octet_size(cnt, sizeof(*ptr), arec, irec, &state); + if (state) { + REJECT_AS_OOR(stmt, dbl, /*fixed?*/FALSE, "[BINARY]<[floating]"); + } + write_out_octets(octet_len_ptr, cnt, irec); + if (data_ptr) { + memcpy(data_ptr, ptr, cnt); + //TODO: should the driver clear all the received buffer?? Cfg option? + //memset((char *)data_ptr + cnt, 0, arec->octet_length - cnt); + } + + DBGH(stmt, "converted double %f to binary on %zd octets.", dbl, cnt); + + return SQL_SUCCESS; +} + +/* + * TODO!!! + * 1. use default precision + * 2. config for scientific notation. + * 3. sprintf (for now) + */ +static SQLRETURN double_to_str(esodbc_rec_st *arec, esodbc_rec_st *irec, + double dbl, void *data_ptr, SQLLEN *octet_len_ptr, BOOL wide) +{ + long long whole; + unsigned long long fraction; + double rest; + SQLSMALLINT scale; + size_t pos, octets; + /* buffer unit size */ + size_t usize = wide ? sizeof(SQLWCHAR) : sizeof(SQLCHAR); + esodbc_state_et state = SQL_STATE_00000; + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + /* buffer is overprovisioned for !wide, but avoids double declaration */ + SQLCHAR buff[(2 * ESODBC_PRECISION_INT64 + /*.*/1 + /*\0*/1) + * sizeof(SQLWCHAR)]; + xstr_st xstr = (xstr_st) { + .wide = wide, + .c = (cstr_st) { + /* same vals for both wide and C strings */ + .str = buff, + .cnt = 0 + } + }; + + /* + * split the whole and fractional parts + */ + assert(sizeof(dbl) == sizeof(whole)); /* [double]==[long long] */ + whole = (long long)dbl; + rest = dbl - whole; + + /* retain user defined or data source default number of fraction digits */ + scale = 0 < arec->scale ? arec->scale : irec->es_type->maximum_scale; + rest *= pow10(scale); + rest = round(rest); + fraction = rest < 0 ? (unsigned long long) -rest + : (unsigned long long)rest; + + /* copy integer part into work buffer */ + pos = i64tot((int64_t)whole, buff, wide); + /* would writing just the whole part + \0 fit into the buffer? */ + octets = buff_octet_size((pos + 1) * usize, usize, arec, irec, &state); + if (state) { + REJECT_AS_OOR(stmt, dbl, /*fixed?*/FALSE, "[STRING]<[floating.whole]"); + } else { + assert(octets == (pos + 1) * usize); + } + + if (wide) { + ((SQLWCHAR *)buff)[pos ++] = L'.'; + } else { + ((SQLCHAR *)buff)[pos ++] = '.'; + } + + /* copy fractional part into work buffer */ + pos += ui64tot((uint64_t)fraction, (char *)buff + pos * usize, wide); + + xstr.w.cnt = pos; /*==.c.cnt*/ + + return transfer_xstr0(arec, irec, &xstr, data_ptr, octet_len_ptr); +} + +SQLRETURN sql2c_double(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, double dbl) +{ + esodbc_stmt_st *stmt; + void *data_ptr; + SQLLEN *octet_len_ptr; + esodbc_desc_st *ard, *ird; + SQLSMALLINT ctype; + SQLRETURN ret; + double udbl; + + stmt = arec->desc->hdr.stmt; + ird = stmt->ird; + ard = stmt->ard; + + /* pointer where to write how many characters we will/would use */ + octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); + /* pointer to app's buffer */ + data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); + + /* Transfer a double to an SQL integer type. + * Uses local vars: stmt, data_ptr, irec, octet_len_ptr. + * Returns - RET_ - 01S07 on success (due to truncation of fractionals). */ +# define RET_TRANSFER_DBL(_dbl, _min, _max, _sqlctype, _ctype) \ + do { \ + /* using C type limits, so check C and SQL C type precision */ \ + assert(sizeof(_sqlctype) == sizeof(_ctype)); \ + if (_dbl) { \ + if ((_sqlctype)_dbl < _min || _max < (_sqlctype)_dbl) { \ + REJECT_AS_OOR(stmt, _dbl, /*fixed?*/FALSE, _sqlctype); \ + } \ + } else { \ + double __udbl = dbl < 0 ? -dbl : dbl; \ + if (_max < (_sqlctype)__udbl) { \ + REJECT_AS_OOR(stmt, _dbl, /*fixed?*/FALSE, _sqlctype); \ + } \ + } \ + *(_sqlctype *)data_ptr = (_sqlctype)_dbl; \ + write_out_octets(octet_len_ptr, sizeof(_sqlctype), irec); \ + DBGH(stmt, "converted double %f to " STR(_sqlctype) " 0x%llx.", _dbl, \ + (intptr_t)*(_sqlctype *)data_ptr); \ + RET_HDIAGS(stmt, SQL_STATE_01S07); \ + } while (0) + + switch ((ctype = get_rec_c_type(arec, irec))) { + case SQL_C_CHAR: + case SQL_C_WCHAR: + return double_to_str(arec, irec, dbl, data_ptr, octet_len_ptr, + ctype == SQL_C_WCHAR); + + case SQL_C_TINYINT: + case SQL_C_STINYINT: + RET_TRANSFER_DBL(dbl, CHAR_MIN, CHAR_MAX, SQLSCHAR, char); + case SQL_C_UTINYINT: + RET_TRANSFER_DBL(dbl, 0, UCHAR_MAX, SQLCHAR, unsigned char); + case SQL_C_SBIGINT: + RET_TRANSFER_DBL(dbl, LLONG_MIN, LLONG_MAX, SQLBIGINT, long long); + case SQL_C_UBIGINT: + RET_TRANSFER_DBL(dbl, 0, LLONG_MAX, SQLUBIGINT, long long); + case SQL_C_SHORT: + case SQL_C_SSHORT: + RET_TRANSFER_DBL(dbl, SHRT_MIN, SHRT_MAX, SQLSMALLINT, short); + case SQL_C_USHORT: + RET_TRANSFER_DBL(dbl, 0, USHRT_MAX, SQLUSMALLINT, unsigned short); + case SQL_C_LONG: + case SQL_C_SLONG: + RET_TRANSFER_DBL(dbl, LONG_MIN, LONG_MAX, SQLINTEGER, long); + case SQL_C_ULONG: + RET_TRANSFER_DBL(dbl, 0, ULONG_MAX, SQLINTEGER, unsigned long); + + case SQL_C_NUMERIC: + REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); + ret = double_to_numeric(arec, dbl, data_ptr); + if (! SQL_SUCCEEDED(ret)) { + return ret; + } + write_out_octets(octet_len_ptr, sizeof(SQL_NUMERIC_STRUCT), irec); + break; + + case SQL_C_FLOAT: + REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); + udbl = dbl < 0 ? -dbl : dbl; + if (udbl < FLT_MIN || FLT_MAX < udbl) { + REJECT_AS_OOR(stmt, dbl, /* is fixed */FALSE, SQLREAL); + } + *(SQLREAL *)data_ptr = (SQLREAL)dbl; + write_out_octets(octet_len_ptr, sizeof(SQLREAL), irec); + break; + case SQL_C_DOUBLE: + REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); + *(SQLDOUBLE *)data_ptr = dbl; + write_out_octets(octet_len_ptr, sizeof(SQLDOUBLE), irec); + break; + + case SQL_C_BIT: + return double_to_bit(arec, irec, dbl, data_ptr, octet_len_ptr); + + case SQL_C_BINARY: + return double_to_binary(arec, irec, dbl, data_ptr, octet_len_ptr); + + default: + BUGH(stmt, "unexpected unhanlded data type: %d.", + get_rec_c_type(arec, irec)); + return SQL_ERROR; + } + + DBGH(stmt, "REC@0x%p, data_ptr@0x%p, copied double: %.6e.", arec, + data_ptr, dbl); + + return SQL_SUCCESS; + +# undef RET_TRANSFER_DBL +} + +static SQLRETURN wstr_to_cstr(esodbc_rec_st *arec, esodbc_rec_st *irec, + void *data_ptr, SQLLEN *octet_len_ptr, + const wchar_t *wstr, size_t chars_0) +{ + esodbc_state_et state = SQL_STATE_00000; + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + char *charp; + int in_bytes, out_bytes, c; + xstr_st xstr = (xstr_st) { + .wide = TRUE, + .w = (wstr_st) { + (SQLWCHAR *)wstr, chars_0 - 1 + } + }; + + gd_offset_apply(stmt, &xstr); + + if (data_ptr) { + charp = (char *)data_ptr; + + in_bytes = (int)buff_octet_size((xstr.w.cnt + 1) * sizeof(SQLWCHAR), + sizeof(SQLCHAR), arec, irec, &state); + /* trim the original string until it fits in output buffer, with given + * length limitation */ + for (c = (int)xstr.w.cnt + 1; 0 < c; c --) { + out_bytes = WCS2U8(xstr.w.str, c, charp, in_bytes); + if (out_bytes <= 0) { + if (WCS2U8_BUFF_INSUFFICIENT) { + continue; + } + ERRNH(stmt, "failed to convert wchar_t* to char* for string `" + LWPDL "`.", c, xstr.w.str); + RET_HDIAGS(stmt, SQL_STATE_22018); + } else { + /* conversion succeeded */ + break; + } + } + + /* if 0's present => 0 < out_bytes */ + assert(xstr.w.str[xstr.w.cnt] == L'\0'); + assert(0 < out_bytes); + /* if user gives 0 as buffer size, out_bytes will also be 0 */ + if (charp[out_bytes - 1]) { + /* ran out of buffer => not 0-terminated and truncated already */ + charp[out_bytes - 1] = 0; + state = SQL_STATE_01004; /* indicate truncation */ + c --; /* last char was overwritten with 0 -> dec xfed count */ + } + + /* only update offset if data is copied out */ + gd_offset_update(stmt, xstr.w.cnt, c); + + DBGH(stmt, "REC@0x%p, data_ptr@0x%p, copied %zd bytes: `" LWPD "`.", + arec, data_ptr, out_bytes, charp); + } else { + DBGH(stmt, "REC@0x%p, NULL data_ptr.", arec); + } + + /* if length needs to be given, calculate it (not truncated) & converted */ + if (octet_len_ptr) { + out_bytes = (size_t)WCS2U8(xstr.w.str, (int)xstr.w.cnt + 1, NULL, 0); + if (out_bytes <= 0) { + ERRNH(stmt, "failed to convert wchar* to char* for string `" + LWPDL "`.", LWSTR(&xstr.w)); + RET_HDIAGS(stmt, SQL_STATE_22018); + } else { + /* chars_0 accounts for 0-terminator, so WCS2U8 will count that in + * the output as well => trim it, since we must not count it when + * indicating the length to the application */ + out_bytes --; + } + write_out_octets(octet_len_ptr, out_bytes, irec); + } else { + DBGH(stmt, "REC@0x%p, NULL octet_len_ptr.", arec); + } + + if (state != SQL_STATE_00000) { + RET_HDIAGS(stmt, state); + } + return SQL_SUCCESS; +} + +/* + * -> SQL_C_WCHAR + * Note: chars_0 accounts for 0-term, but length indicated back to the + * application must not. + */ +static SQLRETURN wstr_to_wstr(esodbc_rec_st *arec, esodbc_rec_st *irec, + void *data_ptr, SQLLEN *octet_len_ptr, + const wchar_t *wstr, size_t chars_0) +{ + xstr_st xsrc = (xstr_st) { + .wide = TRUE, + .w = (wstr_st) { + (SQLWCHAR *)wstr, chars_0 - 1 + } + }; + return transfer_xstr0(arec, irec, &xsrc, data_ptr, octet_len_ptr); +} + +/* Converts an xstr to a TS. + * xstr needs to be trimmed to exact data (no padding, no 0-term counted). + * If ts_buff is non-NULL, the xstr will be copied (possibly W-to-C converted) + * into it. */ +static BOOL xstr_to_timestamp_struct(xstr_st *xstr, TIMESTAMP_STRUCT *tss, + cstr_st *ts_buff) +{ + /* need the 0-term in the buff, since ansi_w2c will write it */ + char buff[sizeof(ESODBC_ISO8601_TEMPLATE)/*+\0*/]; + cstr_st ts_str, *ts_ptr; + timestamp_t tsp; + struct tm tmp; + + if (ts_buff) { + assert(sizeof(ESODBC_ISO8601_TEMPLATE) - 1 <= ts_buff->cnt); + ts_ptr = ts_buff; + } else { + ts_str.str = buff; + ts_str.cnt = sizeof(buff); + ts_ptr = &ts_str; + } + + if (xstr->wide) { + DBG("converting ISO 8601 `" LWPDL "` to timestamp.", LWSTR(&xstr->w)); + if (sizeof(ESODBC_ISO8601_TEMPLATE) - 1 < xstr->w.cnt) { + ERR("`" LWPDL "` not a TIMESTAMP.", LWSTR(&xstr->w)); + return FALSE; + } + /* convert the W-string to C-string; also, copy it directly into out + * ts_buff, if given (thus saving one extra copying) */ + ts_ptr->cnt = ansi_w2c(xstr->w.str, ts_ptr->str, xstr->w.cnt) - 1; + } else { + DBG("converting ISO 8601 `" LCPDL "` to timestamp.", LCSTR(&xstr->c)); + if (sizeof(ESODBC_ISO8601_TEMPLATE) - 1 < xstr->c.cnt) { + ERR("`" LCPDL "` not a TIMESTAMP.", LCSTR(&xstr->c)); + return FALSE; + } + /* no conversion needed; but copying to the out ts_buff, if given */ + if (ts_buff) { + memcpy(ts_ptr->str, xstr->c.str, xstr->c.cnt); + ts_ptr->cnt = xstr->c.cnt; + } else { + ts_ptr = &xstr->c; + } + } + + /* len counts the 0-term */ + if (ts_ptr->cnt <= 1 || timestamp_parse(ts_ptr->str, ts_ptr->cnt, &tsp) || + (! timestamp_to_tm_local(&tsp, &tmp))) { + ERR("data `" LCPDL "` not an ANSI ISO 8601 format.", LCSTR(ts_ptr)); + return FALSE; + } + TM_TO_TIMESTAMP_STRUCT(&tmp, tss); + tss->fraction = tsp.nsec / 1000000; + + DBG("parsed ISO 8601: `%04d-%02d-%02dT%02d:%02d:%02d.%u+%dm`.", + tss->year, tss->month, tss->day, + tss->hour, tss->minute, tss->second, tss->fraction, + tsp.offset); + + return TRUE; +} + + +static BOOL parse_timedate(xstr_st *xstr, TIMESTAMP_STRUCT *tss, + SQLSMALLINT *format, cstr_st *ts_buff) +{ + /* template buffer: date or time values will be copied in place and + * evaluated as a timestamp (needs to be valid) */ + SQLCHAR templ[] = "0001-01-01T00:00:00.0000000Z"; + /* conversion Wide to C-string buffer */ + SQLCHAR w2c[sizeof(ESODBC_ISO8601_TEMPLATE)/*+\0*/]; + cstr_st td;/* timedate string */ + xstr_st xtd; + + /* is this a TIMESTAMP? */ + if (sizeof(ESODBC_TIME_TEMPLATE) - 1 < XSTR_LEN(xstr)) { + /* longer than a date-value -> try a timestamp */ + if (! xstr_to_timestamp_struct(xstr, tss, ts_buff)) { + return FALSE; + } + if (format) { + *format = SQL_TYPE_TIMESTAMP; + } + return TRUE; + } + + /* W-strings will eventually require convertion to C-string for TS + * conversion => do it now to simplify string analysis */ + if (xstr->wide) { + td.cnt = ansi_w2c(xstr->w.str, w2c, xstr->w.cnt) - 1; + td.str = w2c; + } else { + td = xstr->c; + } + xtd.wide = FALSE; + + /* could this be a TIME-val? */ + if (/*hh:mm:ss*/8 <= td.cnt && td.str[2] == ':' && td.str[5] == ':') { + /* copy active value in template and parse it as TS */ + /* copy is safe: cnt <= [time template] < [templ] */ + memcpy(templ + sizeof(ESODBC_DATE_TEMPLATE) - 1, td.str, td.cnt); + /* there could be a varying number of fractional digits */ + templ[sizeof(ESODBC_DATE_TEMPLATE) - 1 + td.cnt] = 'Z'; + xtd.c.str = templ; + xtd.c.cnt = td.cnt + sizeof(ESODBC_DATE_TEMPLATE); + if (! xstr_to_timestamp_struct(&xtd, tss, ts_buff)) { + ERR("`" LCPDL "` not a TIME.", LCSTR(&td)); + return FALSE; + } else { + tss->year = tss->month = tss->day = 0; + if (format) { + *format = SQL_TYPE_TIME; + } + } + return TRUE; + } + + /* could this be a DATE-val? */ + if (/*yyyy-mm-dd*/10 <= td.cnt && td.str[4] == '-' && td.str[7] == '-') { + /* copy active value in template and parse it as TS */ + /* copy is safe: cnt <= [time template] < [templ] */ + memcpy(templ, td.str, td.cnt); + xtd.c.str = templ; + xtd.c.cnt = sizeof(templ)/sizeof(templ[0]) - 1; + if (! xstr_to_timestamp_struct(&xtd, tss, ts_buff)) { + ERR("`" LCPDL "` not a DATE.", LCSTR(&td)); + return FALSE; + } else { + tss->hour = tss->minute = tss->second = 0; + tss->fraction = 0; + if (format) { + *format = SQL_TYPE_DATE; + } + } + return TRUE; + } + + ERR("`" LCPDL "` not a Time/Date/Timestamp.", LCSTR(&td)); + return FALSE; +} + +/* + * -> SQL_C_TYPE_TIMESTAMP + * + * Conversts an ES/SQL 'date' or a text representation of a + * timestamp/date/time value into a TIMESTAMP_STRUCT (indicates the detected + * input format into the "format" parameter). + */ +static SQLRETURN wstr_to_timestamp(esodbc_rec_st *arec, esodbc_rec_st *irec, + void *data_ptr, SQLLEN *octet_len_ptr, + const wchar_t *w_str, size_t chars_0, SQLSMALLINT *format) +{ + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + TIMESTAMP_STRUCT *tss = (TIMESTAMP_STRUCT *)data_ptr; + xstr_st xstr = (xstr_st) { + .wide = TRUE, + .w = (wstr_st) { + (SQLWCHAR *)w_str, chars_0 - 1 + } + }; + + if (octet_len_ptr) { + *octet_len_ptr = sizeof(*tss); + } + + if (data_ptr) { + /* right & left trim the data before attempting conversion */ + wtrim_ws(&xstr.w); + + switch (irec->concise_type) { + case SQL_TYPE_TIMESTAMP: + if (! xstr_to_timestamp_struct(&xstr, tss, NULL)) { + RET_HDIAGS(stmt, SQL_STATE_22018); + } + if (format) { + *format = SQL_TYPE_TIMESTAMP; + } + break; + case SQL_VARCHAR: + if (! parse_timedate(&xstr, tss, format, NULL)) { + RET_HDIAGS(stmt, SQL_STATE_22018); + } + break; + + case SQL_CHAR: + case SQL_LONGVARCHAR: + case SQL_WCHAR: + case SQL_WLONGVARCHAR: + case SQL_TYPE_DATE: + case SQL_TYPE_TIME: + BUGH(stmt, "unexpected (but permitted) SQL type."); + RET_HDIAGS(stmt, SQL_STATE_HY004); + default: + BUGH(stmt, "uncought invalid conversion."); + RET_HDIAGS(stmt, SQL_STATE_07006); + } + } else { + DBGH(stmt, "REC@0x%p, NULL data_ptr", arec); + } + + return SQL_SUCCESS; +} + +/* + * -> SQL_C_TYPE_DATE + */ +static SQLRETURN wstr_to_date(esodbc_rec_st *arec, esodbc_rec_st *irec, + void *data_ptr, SQLLEN *octet_len_ptr, + const wchar_t *wstr, size_t chars_0) +{ + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + DATE_STRUCT *ds = (DATE_STRUCT *)data_ptr; + TIMESTAMP_STRUCT tss; + SQLRETURN ret; + SQLSMALLINT fmt; + + if (octet_len_ptr) { + *octet_len_ptr = sizeof(*ds); + } + + if (data_ptr) { + ret = wstr_to_timestamp(arec, irec, &tss, NULL, wstr, chars_0, &fmt); + if (! SQL_SUCCEEDED(ret)) { + return ret; + } + if (fmt == SQL_TYPE_TIME) { + /* it's a time-value */ + RET_HDIAGS(stmt, SQL_STATE_22018); + } + ds->year = tss.year; + ds->month = tss.month; + ds->day = tss.day; + if (tss.hour || tss.minute || tss.second || tss.fraction) { + /* value's truncated */ + RET_HDIAGS(stmt, SQL_STATE_01S07); + } + } else { + DBGH(stmt, "REC@0x%p, NULL data_ptr", arec); + } + + return SQL_SUCCESS; +} + +/* + * -> SQL_C_TYPE_TIME + */ +static SQLRETURN wstr_to_time(esodbc_rec_st *arec, esodbc_rec_st *irec, + void *data_ptr, SQLLEN *octet_len_ptr, + const wchar_t *wstr, size_t chars_0) +{ + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + TIME_STRUCT *ts = (TIME_STRUCT *)data_ptr; + TIMESTAMP_STRUCT tss; + SQLRETURN ret; + SQLSMALLINT fmt; + + if (octet_len_ptr) { + *octet_len_ptr = sizeof(*ts); + } + + if (data_ptr) { + ret = wstr_to_timestamp(arec, irec, &tss, NULL, wstr, chars_0, &fmt); + if (! SQL_SUCCEEDED(ret)) { + return ret; + } + /* need to differentiate between: + * - 1234-12-34T00:00:00Z : valid and + * - 1234-12-34 : invalid */ + if (fmt == SQL_TYPE_DATE) { + RET_HDIAGS(stmt, SQL_STATE_22018); + } + ts->hour = tss.hour; + ts->minute = tss.minute; + ts->second = tss.second; + if (tss.fraction) { + /* value's truncated */ + RET_HDIAGS(stmt, SQL_STATE_01S07); + } + } else { + DBGH(stmt, "REC@0x%p, NULL data_ptr", arec); + } + + return SQL_SUCCESS; +} + +/* + * wstr: is 0-terminated and terminator is counted in 'chars_0'. + * However: "[w]hen C strings are used to hold character data, the + * null-termination character is not considered to be part of the data and is + * not counted as part of its byte length." + * "If the data was converted to a variable-length data type, such as + * character or binary [...][i]t then null-terminates the data." + */ +SQLRETURN sql2c_string(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, const wchar_t *wstr, size_t chars_0) +{ + esodbc_stmt_st *stmt; + void *data_ptr; + SQLLEN *octet_len_ptr; + esodbc_desc_st *ard, *ird; + SQLSMALLINT ctarget; + long long ll; + unsigned long long ull; + wstr_st wval; + double dbl; + SQLWCHAR *endp; + + stmt = arec->desc->hdr.stmt; + ird = stmt->ird; + ard = stmt->ard; + + /* pointer where to write how many characters we will/would use */ + octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); + /* pointer to app's buffer */ + data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); + + switch ((ctarget = get_rec_c_type(arec, irec))) { + case SQL_C_CHAR: + return wstr_to_cstr(arec, irec, data_ptr, octet_len_ptr, + wstr, chars_0); + case SQL_C_BINARY: /* treat binary as WCHAR */ // TODO: add \0??? + case SQL_C_WCHAR: + return wstr_to_wstr(arec, irec, data_ptr, octet_len_ptr, + wstr, chars_0); + + case SQL_C_TYPE_TIMESTAMP: + return wstr_to_timestamp(arec, irec, data_ptr, octet_len_ptr, + wstr, chars_0, NULL); + case SQL_C_TYPE_DATE: + return wstr_to_date(arec, irec, data_ptr, octet_len_ptr, + wstr, chars_0); + case SQL_C_TYPE_TIME: + return wstr_to_time(arec, irec, data_ptr, octet_len_ptr, + wstr, chars_0); + + case SQL_C_TINYINT: + case SQL_C_STINYINT: + case SQL_C_SHORT: + case SQL_C_SSHORT: + case SQL_C_LONG: + case SQL_C_SLONG: + case SQL_C_SBIGINT: + wval = (wstr_st) { + (SQLWCHAR *)wstr, chars_0 - 1 + }; + /* trim any white spaces */ + wtrim_ws(&wval); + /* convert to integer type */ + errno = 0; + if (! str2bigint(&wval, /*wide?*/TRUE, (SQLBIGINT *)&ll)) { + ERRH(stmt, "can't convert `" LWPD "` to long long.", wstr); + RET_HDIAGS(stmt, errno == ERANGE ? SQL_STATE_22003 : + SQL_STATE_22018); + } + DBGH(stmt, "string `" LWPD "` converted to LL=%lld.", wstr, ll); + /* delegate to existing functionality */ + return sql2c_longlong(arec, irec, pos, ll); + + case SQL_C_UTINYINT: + case SQL_C_USHORT: + case SQL_C_ULONG: + case SQL_C_UBIGINT: + wval = (wstr_st) { + (SQLWCHAR *)wstr, chars_0 - 1 + }; + /* trim any white spaces */ + wtrim_ws(&wval); + /* convert to integer type */ + errno = 0; + if (! str2ubigint(&wval, /*wide?*/TRUE, (SQLUBIGINT *)&ull)) { + ERRH(stmt, "can't convert `" LWPD "` to unsigned long long.", + wstr); + RET_HDIAGS(stmt, errno == ERANGE ? SQL_STATE_22003 : + SQL_STATE_22018); + } + DBGH(stmt, "string `" LWPD "` converted to ULL=%llu.", wstr, ull); + if (ull <= LLONG_MAX) { + /* the cast is safe, delegate to existing functionality */ + return sql2c_longlong(arec, irec, pos, (long long)ull); + } + /* value is larger than what long long can hold: can only convert + * to SQLUBIGINT (and SQLULONG, if it has the same size), or fail + * as out-of-range */ + assert(sizeof(SQLUBIGINT) == sizeof(unsigned long long)); + if ((ctarget == SQL_C_UBIGINT) || (ctarget == SQL_C_ULONG && + sizeof(SQLUINTEGER) == sizeof(SQLUBIGINT))) { + /* write out the converted value */ + REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); + *(SQLUBIGINT *)data_ptr = (SQLUBIGINT)ull; + write_out_octets(octet_len_ptr, sizeof(SQLUBIGINT), irec); + DBGH(stmt, "converted string `" LWPD "` to " + "unsigned long long %llu.", wstr, ull); + } else { + REJECT_AS_OOR(stmt, ull, /*fixed?*/TRUE, "non-ULL"); + } + break; + + case SQL_C_FLOAT: + case SQL_C_DOUBLE: + case SQL_C_NUMERIC: + case SQL_C_BIT: + wval = (wstr_st) { + (SQLWCHAR *)wstr, chars_0 - 1 + }; + /* trim any white spaces */ + wtrim_ws(&wval); + /* convert to double */ + errno = 0; + dbl = wcstod((wchar_t *)wval.str, (wchar_t **)&endp); + DBGH(stmt, "string `" LWPD "` converted to dbl=%.6e.", wstr, dbl); + /* if empty string, non-numeric or under/over-flow, bail out */ + if ((! wval.cnt) || (wval.str + wval.cnt != endp) || errno) { + ERRH(stmt, "can't convert `" LWPD "` to double.", wstr); + RET_HDIAGS(stmt, errno == ERANGE ? SQL_STATE_22003 : + SQL_STATE_22018); + } + /* delegate to existing functionality */ + return sql2c_double(arec, irec, pos, dbl); + + + default: + BUGH(stmt, "unexpected unhandled data type: %d.", + get_rec_c_type(arec, irec)); + return SQL_ERROR; + } + + return SQL_SUCCESS; +} + + +/* TODO: implementation for the below */ +static inline BOOL conv_implemented(SQLSMALLINT sqltype, SQLSMALLINT ctype) +{ + switch (ctype) { + case SQL_C_GUID: + + case SQL_C_INTERVAL_DAY: + case SQL_C_INTERVAL_HOUR: + case SQL_C_INTERVAL_MINUTE: + case SQL_C_INTERVAL_SECOND: + case SQL_C_INTERVAL_DAY_TO_HOUR: + case SQL_C_INTERVAL_DAY_TO_MINUTE: + case SQL_C_INTERVAL_DAY_TO_SECOND: + case SQL_C_INTERVAL_HOUR_TO_MINUTE: + case SQL_C_INTERVAL_HOUR_TO_SECOND: + case SQL_C_INTERVAL_MINUTE_TO_SECOND: + case SQL_C_INTERVAL_MONTH: + case SQL_C_INTERVAL_YEAR: + case SQL_C_INTERVAL_YEAR_TO_MONTH: + // case SQL_C_TYPE_TIMESTAMP_WITH_TIMEZONE: + // case SQL_C_TYPE_TIME_WITH_TIMEZONE: + return FALSE; + } + + switch (sqltype) { + case SQL_C_GUID: + + case SQL_INTERVAL_DAY: + case SQL_INTERVAL_HOUR: + case SQL_INTERVAL_MINUTE: + case SQL_INTERVAL_SECOND: + case SQL_INTERVAL_DAY_TO_HOUR: + case SQL_INTERVAL_DAY_TO_MINUTE: + case SQL_INTERVAL_DAY_TO_SECOND: + case SQL_INTERVAL_HOUR_TO_MINUTE: + case SQL_INTERVAL_HOUR_TO_SECOND: + case SQL_INTERVAL_MINUTE_TO_SECOND: + case SQL_INTERVAL_MONTH: + case SQL_INTERVAL_YEAR: + case SQL_INTERVAL_YEAR_TO_MONTH: + // case SQL_TYPE_TIMESTAMP_WITH_TIMEZONE: + // case SQL_TYPE_TIME_WITH_TIMEZONE: + return FALSE; + } + + return TRUE; +} + + +/* check if data types in returned columns are compabile with buffer types + * bound for those columns + * idx: + * if >= 0: parameter number (0,) for parameter binding; + * if < 0: indicator for bound columns check. + * */ +SQLRETURN convertability_check(esodbc_stmt_st *stmt, SQLINTEGER idx, + int *conv_code) +{ + SQLINTEGER i, start, stop; + esodbc_desc_st *axd, *ixd; + esodbc_rec_st *arec, *irec; + + if (idx < 0) { + /* + * bound columns check + */ + assert(stmt->hdr.dbc->es_types); + assert(STMT_HAS_RESULTSET(stmt)); + + axd = stmt->ard; + ixd = stmt->ird; + + start = 0; + stop = axd->count < ixd->count ? axd->count : ixd->count; + } else { + /* + * binding paramter check + */ + assert(0 < idx); + start = idx - 1; + stop = idx; + + axd = stmt->apd; + ixd = stmt->ipd; + } + + for (i = start; i < stop; i ++) { + assert(i < axd->count); + arec = &axd->recs[i]; + if ((idx < 0) && (! REC_IS_BOUND(arec))) { + /* skip not bound columns */ + continue; + } + assert(i < ixd->count); + irec = &ixd->recs[i]; + + assert(arec && irec); + + if (! ESODBC_TYPES_COMPATIBLE(irec->concise_type,arec->concise_type)) { + ERRH(stmt, "conversion not possible on ordinal #%d: IRD: %hd, " + "ARD: %hd.", i + 1, irec->concise_type, arec->concise_type); + if (conv_code) { + *conv_code = CONVERSION_VIOLATION; + } + RET_HDIAGS(stmt, SQL_STATE_07006); + } + if (! conv_implemented(irec->concise_type, arec->concise_type)) { + ERRH(stmt, "conversion not supported on ordinal #%d : IRD: %hd, " + "ARD: %hd.", i + 1, irec->concise_type, arec->concise_type); + if (conv_code) { + *conv_code = CONVERSION_UNSUPPORTED; + } + RET_HDIAGS(stmt, SQL_STATE_HYC00); + } + } + + if (conv_code) { + *conv_code = CONVERSION_SUPPORTED; + } + DBGH(stmt, "convertibility check: OK."); + return SQL_SUCCESS; +} + +#if 0 +/* check if data types in returned columns are compabile with buffer types + * bound for those columns */ +SQLRETURN sql2c_convertible(esodbc_stmt_st *stmt) +{ + SQLSMALLINT i, min, ret; + esodbc_desc_st *ard, *ird; + esodbc_rec_st *arec, *irec; + + assert(stmt->hdr.dbc->es_types); + assert(STMT_HAS_RESULTSET(stmt)); + + ard = stmt->ard; + ird = stmt->ird; + + min = ard->count < ird->count ? ard->count : ird->count; + for (i = 0; i < min; i ++) { + arec = &ard->recs[i]; + if (! REC_IS_BOUND(arec)) { + /* skip not bound columns */ + continue; + } + irec = &ird->recs[i]; + + if (! ESODBC_TYPES_COMPATIBLE(irec->concise_type,arec->concise_type)) { + ERRH(stmt, "type conversion not possible on column %d: IRD: %hd, " + "ARD: %hd.", i + 1, irec->concise_type, arec->concise_type); + stmt->sql2c_conversion = CONVERSION_VIOLATION; + RET_HDIAGS(stmt, SQL_STATE_07006); + } + if (! conv_implemented(irec->concise_type, arec->concise_type)) { + ERRH(stmt, "conversion not supported on column %d types: IRD: %hd," + " ARD: %hd.", i + 1, irec->concise_type, arec->concise_type); + stmt->sql2c_conversion = CONVERSION_UNSUPPORTED; + RET_HDIAGS(stmt, SQL_STATE_HYC00); + } + } + + stmt->sql2c_conversion = CONVERSION_SUPPORTED; + DBGH(stmt, "convertibility check: OK."); + return SQL_SUCCESS; +} +#endif + +/* Converts a C/W-string to a u/llong or double (dest_type?); the xstr->wide + * needs to be set; + * Returns success of conversion and pointer to trimmed number str + * representation. */ +static BOOL xstr_to_number(void *data_ptr, SQLLEN *octet_len_ptr, + xstr_st *xstr, SQLSMALLINT dest_type, void *dest) +{ + BOOL res; + + if (xstr->wide) { + xstr->w.str = (SQLWCHAR *)data_ptr; + if ((octet_len_ptr && *octet_len_ptr == SQL_NTSL) || !octet_len_ptr) { + xstr->w.cnt = wcslen(xstr->w.str); + } else { + xstr->w.cnt = (size_t)(*octet_len_ptr / sizeof(*xstr->w.str)); + xstr->w.cnt -= /*0-term*/1; + } + } else { + xstr->c.str = (SQLCHAR *)data_ptr; + if ((octet_len_ptr && *octet_len_ptr == SQL_NTSL) || !octet_len_ptr) { + xstr->c.cnt = strlen(xstr->c.str); + } else { + xstr->c.cnt = (size_t)(*octet_len_ptr - /*\0*/1); + } + } + + if (! dest) { + return TRUE; + } + + if (xstr->wide) { + wtrim_ws(&xstr->w); + DBG("converting paramter value `" LWPDL "` to number.", + LWSTR(&xstr->w)); + switch (dest_type) { + case SQL_C_SBIGINT: + res = str2bigint(&xstr->w, /*wide?*/TRUE, (SQLBIGINT *)dest); + break; + case SQL_C_UBIGINT: + res = str2bigint(&xstr->w, /*wide?*/TRUE, (SQLUBIGINT *)dest); + break; + case SQL_C_DOUBLE: + res = str2double(&xstr->w, /*wide?*/TRUE, (SQLDOUBLE *)dest); + break; + default: + assert(0); + } + } else { + trim_ws(&xstr->c); + DBG("converting paramter value `" LCPDL "` to number.", + LCSTR(&xstr->c)); + switch (dest_type) { + case SQL_C_SBIGINT: + res = str2bigint(&xstr->c, /*wide?*/FALSE, (SQLBIGINT *)dest); + break; + case SQL_C_UBIGINT: + res = str2bigint(&xstr->c, /*wide?*/FALSE, (SQLUBIGINT *)dest); + break; + case SQL_C_DOUBLE: + res = str2double(&xstr->c, /*wide?*/FALSE, (SQLDOUBLE *)dest); + break; + default: + assert(0); + } + } + + if (! res) { + if (xstr->wide) { + ERR("can't convert `" LWPDL "` to type %hd number.", + LWSTR(&xstr->w), dest_type); + } else { + ERR("can't convert `" LCPDL "` to type %hd number.", + LCSTR(&xstr->c), dest_type); + } + return FALSE; + } else { + return TRUE; + } +} + + +SQLRETURN c2sql_null(esodbc_rec_st *arec, + esodbc_rec_st *irec, char *dest, size_t *len) +{ + assert(irec->concise_type == ESODBC_SQL_NULL); + if (dest) { + memcpy(dest, JSON_VAL_NULL, sizeof(JSON_VAL_NULL) - /*\0*/1); + } + *len = sizeof(JSON_VAL_NULL) - /*\0*/1; + return SQL_SUCCESS; +} + +static SQLRETURN double_to_bool(esodbc_stmt_st *stmt, double dbl, BOOL *val) +{ + DBGH(stmt, "converting double %.6e to bool.", dbl); +#ifdef BOOLEAN_IS_BIT + if (dbl < 0. && 2. < dbl) { + ERRH(stmt, "double %.6e out of range.", dbl); + RET_HDIAGS(stmt, SQL_STATE_22003); + } + if (0. < dbl && dbl < 2. && dbl != 1.) { + /* it's a failure, since SUCCESS_WITH_INFO would be returned only + * after data is sent to the server. */ + ERRH(stmt, "double %.6e right truncated.", dbl); + RET_HDIAGS(stmt, SQL_STATE_22003); + } +#endif /* BOOLEAN_IS_BIT */ + *val = dbl != 0.; + return SQL_SUCCESS; +} + +SQLRETURN c2sql_boolean(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, char *dest, size_t *len) +{ + BOOL val; + SQLSMALLINT ctype; + SQLRETURN ret; + void *data_ptr; + SQLLEN *octet_len_ptr; + xstr_st xstr; + double dbl; + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + + if (! dest) { + /* return the "worst case" len (and only convert data at copy time) */ + *len = sizeof(JSON_VAL_FALSE) - 1; + return SQL_SUCCESS; + } + + /* pointer to app's buffer */ + data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); + + /*INDENT-OFF*/ + switch ((ctype = get_rec_c_type(arec, irec))) { + case SQL_C_CHAR: + case SQL_C_WCHAR: + /* pointer to read from how many bytes we have */ + octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, + arec); + xstr.wide = ctype == SQL_C_WCHAR; + if (! xstr_to_number(data_ptr, octet_len_ptr, &xstr, SQL_C_DOUBLE, + &dbl)) { + RET_HDIAGS(stmt, SQL_STATE_22018); + } + ret = double_to_bool(stmt, dbl, &val); + if (! SQL_SUCCEEDED(ret)) { + return ret; + } + break; + + do { + case SQL_C_BINARY: + /* pointer to read from how many bytes we have */ + octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, + pos, arec); + if (! octet_len_ptr) { + if (((char *)data_ptr)[0]) { + RET_HDIAGS(stmt, SQL_STATE_22003); + } + } else if (*octet_len_ptr != sizeof(SQLCHAR)) { + RET_HDIAGS(stmt, SQL_STATE_22003); + } + /* no break */ + case SQL_C_BIT: + case SQL_C_UTINYINT: dbl = (double)*(SQLCHAR *)data_ptr; break; + case SQL_C_TINYINT: + case SQL_C_STINYINT: dbl = (double)*(SQLSCHAR *)data_ptr; break; + case SQL_C_SHORT: + case SQL_C_SSHORT: dbl = (double)*(SQLSMALLINT *)data_ptr; break; + case SQL_C_USHORT: dbl = (double)*(SQLUSMALLINT *)data_ptr; break; + case SQL_C_LONG: + case SQL_C_SLONG: dbl = (double)*(SQLINTEGER *)data_ptr; break; + case SQL_C_ULONG: dbl = (double)*(SQLUINTEGER *)data_ptr; break; + case SQL_C_SBIGINT: dbl = (double)*(SQLBIGINT *)data_ptr; break; + case SQL_C_UBIGINT: dbl = (double)*(SQLUBIGINT *)data_ptr; break; + case SQL_C_FLOAT: dbl = (double)*(SQLREAL *)data_ptr; break; + case SQL_C_DOUBLE: dbl = (double)*(SQLDOUBLE *)data_ptr; break; + + case SQL_C_NUMERIC: + // TODO: better? *(uul *)val[0] != 0 && *[uul *]val[8] != 0 */ + ret = numeric_to_double(irec, data_ptr, &dbl); + if (! SQL_SUCCEEDED(ret)) { + return ret; + } + break; + } while (0); + ret = double_to_bool(stmt, dbl, &val); + if (! SQL_SUCCEEDED(ret)) { + return ret; + } + break; + + //case SQL_C_BOOKMARK: + //case SQL_C_VARBOOKMARK: + default: + BUGH(stmt, "can't convert SQL C type %hd to boolean.", + get_rec_c_type(arec, irec)); + RET_HDIAG(stmt, SQL_STATE_HY000, "bug converting parameter", 0); + } + /*INDENT-ON*/ + + DBGH(stmt, "parameter (pos#%lld) converted to boolean: %d.", pos, val); + + if (val) { + memcpy(dest, JSON_VAL_TRUE, sizeof(JSON_VAL_TRUE) - /*\0*/1); + *len = sizeof(JSON_VAL_TRUE) - 1; + } else { + memcpy(dest, JSON_VAL_FALSE, sizeof(JSON_VAL_FALSE) - /*\0*/1); + *len = sizeof(JSON_VAL_FALSE) - 1; + } + return SQL_SUCCESS; +} + +/* + * Copies a C/W-string representing a number out to send buffer. + * wide: type of string; + * min, max: target SQL numeric type's limits; + * fixed: target SQL numberic type (integer or floating); + * dest: buffer's pointer; can be null (when eval'ing) needed space; + * len: how much of the buffer is needed or has been used. + */ +static SQLRETURN string_to_number(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, BOOL wide, double *min, double *max, BOOL fixed, + char *dest, size_t *len) +{ + void *data_ptr; + SQLLEN *octet_len_ptr; + xstr_st xstr; + SQLDOUBLE dbl, abs_dbl; + int ret; + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + + /* pointer to app's buffer */ + data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); + /* pointer to read from how many bytes we have */ + octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); + + xstr.wide = wide; + /* do a conversion check: use double, as a capture all cases + * value: ES/SQL will accept a float for an INTEGER param */ + if (! xstr_to_number(data_ptr, octet_len_ptr, &xstr, + SQL_C_DOUBLE, dest ? &dbl : NULL)) { + ERRH(stmt, "failed to convert param value to a double."); + RET_HDIAGS(stmt, SQL_STATE_22018); + } + + if (! dest) { + /* check is superfluous, but safer */ + *len = wide ? xstr.w.cnt : xstr.c.cnt; + return SQL_SUCCESS; + } + + /* check against truncation, limits and any given precision */ + abs_dbl = dbl < 0 ? -dbl : dbl; + if (fixed) { + if (0 < abs_dbl - (SQLUBIGINT)abs_dbl) { + ERRH(stmt, "conversion of double %.6e to fixed would" + " truncate fractional digits", dbl); + RET_HDIAGS(stmt, SQL_STATE_22001); + } + if ((min && dbl < *min) || (max && *max < dbl)) { + ERRH(stmt, "converted double %.6e out of bounds " + "[%.6e, %.6e]", dbl, min, *max); + /* spec requires 22001 here, but that is wrong? */ + RET_HDIAGS(stmt, SQL_STATE_22003); + } + } else { + if ((min && abs_dbl < *min) || (max && *max < abs_dbl)) { + ERRH(stmt, "converted abs double %.6e out of bounds " + "[%.6e, %.6e]", abs_dbl, min, *max); + RET_HDIAGS(stmt, SQL_STATE_22003); + } + } + // + // TODO: check IRD precision against avail value? + // + + /* copy values from app's buffer directly */ + if (wide) { /* need a conversion to ANSI */ + *len = xstr.w.cnt; + ret = ansi_w2c((SQLWCHAR *)data_ptr, dest, *len); + assert(0 < ret); /* it converted to a float already */ + } else { + *len = xstr.c.cnt; + memcpy(dest, data_ptr, *len); + } + return SQL_SUCCESS; +} + +static SQLRETURN sfixed_to_number(esodbc_stmt_st *stmt, SQLBIGINT src, + double *min, double *max, char *dest, size_t *len) +{ + if (! dest) { + /* largest space it could occupy */ + *len = ESODBC_PRECISION_INT64; + return SQL_SUCCESS; + } + + DBGH(stmt, "converting paramter value %lld as number.", src); + + if ((min && src < *min) || (max && *max < src)) { + ERRH(stmt, "source value %lld out of range [%e, %e] for dest type", + src, *min, *max); + RET_HDIAGS(stmt, SQL_STATE_22003); + } + + assert(sizeof(SQLBIGINT) == sizeof(int64_t)); + *len = i64tot(src, dest, /*wide?*/FALSE); + assert(*len <= ESODBC_PRECISION_INT64); + + return SQL_SUCCESS; +} + +static SQLRETURN ufixed_to_number(esodbc_stmt_st *stmt, SQLUBIGINT src, + double *max, char *dest, size_t *len) +{ + if (! dest) { + /* largest space it could occupy */ + *len = ESODBC_PRECISION_UINT64; + return SQL_SUCCESS; + } + + DBGH(stmt, "converting paramter value %llu as number.", src); + + if (src < 0 || (max && *max < src)) { + ERRH(stmt, "source value %llu out of range [0, %e] for dest type", + src, *max); + RET_HDIAGS(stmt, SQL_STATE_22003); + } + + assert(sizeof(SQLBIGINT) == sizeof(int64_t)); + *len = ui64tot(src, dest, /*wide?*/FALSE); + assert(*len <= ESODBC_PRECISION_UINT64); + + return SQL_SUCCESS; +} + +static SQLRETURN floating_to_number(esodbc_rec_st *irec, SQLDOUBLE src, + double *min, double *max, char *dest, size_t *len) +{ + /* format fixed length in scientific notation, -1.23E-45 */ + //const static size_t ff_len = /*-1.*/3 + /*prec*/0 + /*E-*/ 2 + + // sizeof(STR(ESODBC_PRECISION_DOUBLE)) - 1; + size_t maxlen, width; + int cnt; + SQLDOUBLE abs_src; + esodbc_stmt_st *stmt = irec->desc->hdr.stmt; + + //maxlen = get_param_size(irec) + ff_len; + maxlen = get_param_size(irec); + if (! dest) { + /* largest space it could occupy */ + *len = maxlen + /*0-term, for printf*/1; + return SQL_SUCCESS; + } + + abs_src = src < 0 ? -src : src; + if ((min && abs_src < *min) || (max && *max < abs_src)) { + ERRH(stmt, "source value %e out of range [%e, %e] for dest type", + src, *min, *max); + RET_HDIAGS(stmt, SQL_STATE_22003); + } + + width = maxlen; + width -= /*sign*/(src < 0); + width -= /*1.*/2; + width -= /*e+00*/4 + /*3rd digit*/(abs_src < 1e-100 || 1e100 < src); + if (width < 0) { + ERRH(stmt, "parameter size (%zu) to low for floating point.", maxlen); + RET_HDIAGS(stmt, SQL_STATE_HY104); + } + DBGH(stmt, "converting double param %.6e with precision/width: %zu/%d.", + src, maxlen, width); + cnt = snprintf(dest, maxlen + /*\0*/1, "%.*e", (int)width, src); + if (cnt < 0) { + ERRH(stmt, "failed to print double %e.", src); + RET_HDIAGS(stmt, SQL_STATE_HY000); + } else { + *len = cnt; + DBGH(stmt, "value %.6e printed as `" LCPDL "`.", src, *len, dest); + } + + return SQL_SUCCESS; +} + +static SQLRETURN binary_to_number(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, char *dest, size_t *len) +{ + void *data_ptr; + SQLLEN *octet_len_ptr, osize /*octet~*/; + SQLBIGINT llng; + SQLDOUBLE dbl; + esodbc_stmt_st *stmt = irec->desc->hdr.stmt; + + if (! dest) { + if (irec->meta_type == METATYPE_EXACT_NUMERIC) { + return sfixed_to_number(stmt, 0LL, NULL, NULL, NULL, len); + } else { + assert(irec->meta_type == METATYPE_FLOAT_NUMERIC); + return floating_to_number(irec, 0., NULL, NULL, NULL, len); + } + } + + /* pointer to read from how many bytes we have */ + octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); + /* pointer to app's buffer */ + data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); + + if (! octet_len_ptr) { + /* "If [...] is a null pointer, the driver assumes [...] that + * character and binary data is null-terminated." */ + WARNH(stmt, "no length information provided for binary type: " + "calculating it as a C-string!"); + osize = strlen((char *)data_ptr); + } else { + osize = *octet_len_ptr; + } + +# define CHK_SIZES(_sqlc_type) \ + do { \ + if (osize != sizeof(_sqlc_type)) { \ + ERRH(stmt, "binary data length (%zu) misaligned with target" \ + " data type (%hd) size (%lld)", sizeof(_sqlc_type), \ + irec->es_type->data_type, osize); \ + RET_HDIAGS(stmt, SQL_STATE_HY090); \ + } \ + } while (0) +# define BIN_TO_LLNG(_sqlc_type) \ + do { \ + CHK_SIZES(_sqlc_type); \ + llng = (SQLBIGINT)*(_sqlc_type *)data_ptr; \ + } while (0) +# define BIN_TO_DBL(_sqlc_type) \ + do { \ + CHK_SIZES(_sqlc_type); \ + dbl = (SQLDOUBLE)*(_sqlc_type *)data_ptr; \ + } while (0) + + /*INDENT-OFF*/ + switch (irec->es_type->data_type) { + do { + /* JSON long */ + case SQL_BIGINT: BIN_TO_LLNG(SQLBIGINT); break; /* LONG */ + case SQL_INTEGER: BIN_TO_LLNG(SQLINTEGER); break; /* INTEGER */ + case SQL_SMALLINT: BIN_TO_LLNG(SQLSMALLINT); break; /* SHORT */ + case SQL_TINYINT: BIN_TO_LLNG(SQLSCHAR); break; /* BYTE */ + } while (0); + return sfixed_to_number(stmt, llng, NULL, NULL, dest, len); + + /* JSON double */ + do { + // TODO: check accurate limits for floats in ES/SQL + case SQL_FLOAT: /* HALF_FLOAT, SCALED_FLOAT */ + case SQL_REAL: BIN_TO_DBL(SQLREAL); break; /* FLOAT */ + case SQL_DOUBLE: BIN_TO_DBL(SQLDOUBLE); break; /* DOUBLE */ + } while (0); + return floating_to_number(irec, dbl, NULL, NULL, dest, len); + } + /*INDENT-ON*/ + +# undef BIN_TO_LLNG +# undef BIN_TO_DBL +# undef CHK_SIZES + + BUGH(arec->desc->hdr.stmt, "unexpected ES/SQL type %hd.", + irec->es_type->data_type); + RET_HDIAG(arec->desc->hdr.stmt, SQL_STATE_HY000, + "parameter conversion bug", 0); +} + +static SQLRETURN numeric_to_number(esodbc_rec_st *irec, void *data_ptr, + char *dest, size_t *len) +{ + SQLDOUBLE dbl; + SQLRETURN ret; + + if (! dest) { + return floating_to_number(irec, 0., NULL, NULL, NULL, len); + } + + ret = numeric_to_double(irec, data_ptr, &dbl); + if (! SQL_SUCCEEDED(ret)) { + return ret; + } + return floating_to_number(irec, dbl, NULL, NULL, dest, len); +} + +SQLRETURN c2sql_number(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, double *min, double *max, BOOL fixed, char *dest, size_t *len) +{ + void *data_ptr; + SQLSMALLINT ctype; + SQLBIGINT llng; + SQLUBIGINT ullng; + SQLDOUBLE dbl; + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + + /* pointer to app's buffer */ + data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); + + /*INDENT-OFF*/ + switch ((ctype = get_rec_c_type(arec, irec))) { + case SQL_C_WCHAR: + case SQL_C_CHAR: + return string_to_number(arec, irec, pos, ctype == SQL_C_WCHAR, + min, max, fixed, dest, len); + + case SQL_C_BINARY: + return binary_to_number(arec, irec, pos, dest, len); + + do { + case SQL_C_TINYINT: + case SQL_C_STINYINT: llng = (SQLBIGINT)*(SQLSCHAR *)data_ptr; break; + case SQL_C_SHORT: + case SQL_C_SSHORT: llng = (SQLBIGINT)*(SQLSMALLINT *)data_ptr; break; + case SQL_C_LONG: + case SQL_C_SLONG: llng = (SQLBIGINT)*(SQLINTEGER *)data_ptr; break; + case SQL_C_SBIGINT: llng = *(SQLBIGINT *)data_ptr; break; + } while (0); + return sfixed_to_number(stmt, llng, min, max, dest, len); + + do { + case SQL_C_BIT: // XXX: check if 0/1? + case SQL_C_UTINYINT: ullng = (SQLUBIGINT)*(SQLCHAR *)data_ptr; break; + case SQL_C_USHORT: ullng = (SQLUBIGINT)*(SQLUSMALLINT *)data_ptr;break; + case SQL_C_ULONG: ullng = (SQLUBIGINT)*(SQLUINTEGER *)data_ptr; break; + case SQL_C_UBIGINT: ullng = *(SQLUBIGINT *)data_ptr; break; + } while (0); + return ufixed_to_number(stmt, ullng, max, dest, len); + + do { + case SQL_C_FLOAT: dbl = (SQLDOUBLE)*(SQLREAL *)data_ptr; break; + case SQL_C_DOUBLE: dbl = *(SQLDOUBLE *)data_ptr; break; + } while (0); + return floating_to_number(irec, dbl, min, max, dest, len); + + case SQL_C_NUMERIC: + return numeric_to_number(irec, data_ptr, dest, len); + + //case SQL_C_BOOKMARK: + //case SQL_C_VARBOOKMARK: + default: + BUGH(stmt, "can't convert SQL C type %hd to long long.", + get_rec_c_type(arec, irec)); + RET_HDIAG(stmt, SQL_STATE_HY000, "bug converting parameter", 0); + } + /*INDENT-ON*/ + + return SQL_SUCCESS; +} + +static SQLRETURN convert_str_to_timestamp(esodbc_stmt_st *stmt, + SQLLEN *octet_len_ptr, void *data_ptr, BOOL wide, + char *dest, size_t *len) +{ + xstr_st xstr; + TIMESTAMP_STRUCT tss; + SQLSMALLINT format; + cstr_st ts_buff; + + xstr.wide = wide; + + if (wide) { + xstr.w.str = (SQLWCHAR *)data_ptr; + if (octet_len_ptr) { + xstr.w.cnt = *octet_len_ptr / sizeof(SQLWCHAR) - /*\0*/1; + } else { + xstr.w.cnt = wcslen(xstr.w.str); + } + wtrim_ws(&xstr.w); + } else { + xstr.c.str = (SQLCHAR *)data_ptr; + if (octet_len_ptr) { + xstr.c.cnt = *octet_len_ptr / sizeof(SQLCHAR) - /*\0*/1; + } else { + xstr.c.cnt = strlen(xstr.c.str); + } + trim_ws(&xstr.c); + } + + assert(dest); + ts_buff.str = dest; + ts_buff.cnt = sizeof(ESODBC_ISO8601_TEMPLATE) - 1; + if (! parse_timedate(&xstr, &tss, &format, &ts_buff)) { + ERRH(stmt, "failed to parse input as Time/Date/Timestamp"); + RET_HDIAGS(stmt, SQL_STATE_22008); + } else if (format == SQL_TYPE_TIME) { + ERRH(stmt, "can not convert a Time to a Timestamp value"); + RET_HDIAGS(stmt, SQL_STATE_22018); + } else { + /* conversion from TIME to TIMESTAMP should have been deined earlier */ + assert(format != SQL_TYPE_TIME); + *len += ts_buff.cnt; + } + + return SQL_SUCCESS; +} + +static SQLRETURN convert_ts_to_timestamp(esodbc_stmt_st *stmt, + SQLLEN *octet_len_ptr, void *data_ptr, SQLSMALLINT ctype, + char *dest, size_t *len) +{ + TIMESTAMP_STRUCT *tss, buff; + DATE_STRUCT *ds; + int cnt; + size_t osize; + + switch (ctype) { + case SQL_C_TYPE_DATE: + ds = (DATE_STRUCT *)data_ptr; + memset(&buff, 0, sizeof(buff)); + buff.year = ds->year; + buff.month = ds->month; + buff.day = ds->day; + tss = &buff; + break; + case SQL_C_BINARY: + if (! octet_len_ptr) { + WARNH(stmt, "no length information provided for binary type: " + "calculating it as a C-string!"); + osize = strlen((char *)data_ptr); + } else { + osize = *octet_len_ptr; + } + if (osize != sizeof(TIMESTAMP_STRUCT)) { + ERRH(stmt, "incorrect binary object size: %zu; expected: %zu.", + osize, sizeof(TIMESTAMP_STRUCT)); + RET_HDIAGS(stmt, SQL_STATE_22003); + } + /* no break */ + case SQL_C_TYPE_TIMESTAMP: + tss = (TIMESTAMP_STRUCT *)data_ptr; + break; + + default: + BUGH(stmt, "unexpected SQL C type %hd.", ctype); + RET_HDIAG(stmt, SQL_STATE_HY000, "param conversion bug", 0); + } + + assert(dest); + cnt = snprintf(dest, sizeof(ESODBC_ISO8601_TEMPLATE) - 1, + "%04d-%02d-%02dT%02d:%02d:%02d.%03uZ", + tss->year, tss->month, tss->day, + tss->hour, tss->minute, tss->second, tss->fraction); + if (cnt < 0) { + ERRH(stmt, "failed printing timestamp struct: %s.", strerror(errno)); + SET_HDIAG(stmt, SQL_STATE_HY000, "C runtime error", 0); + } + *len = cnt; + + return SQL_SUCCESS; +} + +SQLRETURN c2sql_timestamp(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, char *dest, size_t *len) +{ + void *data_ptr; + SQLLEN *octet_len_ptr; + SQLSMALLINT ctype; + SQLRETURN ret; + SQLULEN colsize, offt, decdigits; + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + + if (! dest) { + /* maximum possible space it can take */ + *len = /*2x `"`*/2 + sizeof(ESODBC_ISO8601_TEMPLATE) - 1; + return SQL_SUCCESS; + } else { + *dest = '"'; + } + + /* pointer to read from how many bytes we have */ + octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); + /* pointer to app's buffer */ + data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); + + switch ((ctype = get_rec_c_type(arec, irec))) { + case SQL_C_CHAR: + case SQL_C_WCHAR: + ret = convert_str_to_timestamp(stmt, octet_len_ptr, data_ptr, + ctype == SQL_C_WCHAR, dest + /*`"`*/1, len); + if (! SQL_SUCCEEDED(ret)) { + return ret; + } + break; + + case SQL_C_TYPE_TIME: + // TODO + ERRH(stmt, "conversion from time to timestamp not implemented."); + RET_HDIAG(stmt, SQL_STATE_HYC00, "conversion time to timestamp " + "not yet supported", 0); + + case SQL_C_TYPE_DATE: + case SQL_C_BINARY: + case SQL_C_TYPE_TIMESTAMP: + ret = convert_ts_to_timestamp(stmt, octet_len_ptr, data_ptr, + ctype, dest + /*`"`*/1, len); + if (! SQL_SUCCEEDED(ret)) { + return ret; + } + break; + + default: + BUGH(stmt, "can't convert SQL C type %hd to timestamp.", + get_rec_c_type(arec, irec)); + RET_HDIAG(stmt, SQL_STATE_HY000, "bug converting parameter", 0); + } + + /* apply corrections depending on the (column) size and decimal digits + * values given at binding time: nullify or trim the resulted string: + * https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/column-size + * */ + colsize = get_param_size(irec); + DBGH(stmt, "requested column size: %llu.", colsize); + if (colsize) { + if (colsize < sizeof("yyyy-mm-dd hh:mm") - 1 || + colsize == 17 || colsize == 18 ) { + ERRH(stmt, "invalid column size value: %llu; " + "allowed: 16, 19, 20+f.", colsize); + RET_HDIAGS(stmt, SQL_STATE_HY104); + } else if (colsize == sizeof("yyyy-mm-dd hh:mm") - 1) { + offt = sizeof("yyyy-mm-ddThh:mm:") - 1; + offt += /*leading `"`*/1; + dest[offt ++] = '0'; + dest[offt ++] = '0'; + dest[offt ++] = 'Z'; + *len = offt; + } else if (colsize == sizeof("yyyy-mm-dd hh:mm:ss") - 1) { + offt = sizeof("yyyy-mm-ddThh:mm:ss") - 1; + offt += /*leading `"`*/1; + dest[offt ++] = 'Z'; + *len = offt; + } else { + assert(20 < colsize); + decdigits = get_param_decdigits(irec); + DBGH(stmt, "requested decimal digits: %llu.", decdigits); + if (/*count of fractions in ISO8601 template*/7 < decdigits) { + INFOH(stmt, "decimal digits value (%hd) reset to 7."); + decdigits = 7; + } else if (decdigits == 0) { + decdigits = -1; /* shave the `.` away */ + } + if (colsize < decdigits + sizeof("yyyy-mm-ddThh:mm:ss.") - 1) { + decdigits = colsize - (sizeof("yyyy-mm-ddThh:mm:ss.") - 1); + WARNH(stmt, "column size adjusted to %hd to fit into a %llu" + " columns size.", decdigits, colsize); + } + offt = sizeof("yyyy-mm-ddThh:mm:ss.") - 1; + offt += /*leading `"`*/1; + offt += decdigits; + dest[offt ++] = 'Z'; + *len = offt; + } + } else { + WARNH(stmt, "column size given 0 -- column size check skipped"); + (*len) ++; /* initial `"` */ + } + + dest[(*len) ++] = '"'; + return SQL_SUCCESS; +} + + +static SQLRETURN c2sql_cstr2qstr(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, char *dest, size_t *len) +{ + void *data_ptr; + SQLLEN *octet_len_ptr, cnt; + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + + /* pointer to read from how many bytes we have */ + octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); + /* pointer to app's buffer */ + data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); + + cnt = octet_len_ptr ? *octet_len_ptr : strlen((char *)data_ptr); + + if (dest) { + *dest = '"'; + } else if ((SQLLEN)get_param_size(irec) < cnt) { + ERRH(stmt, "string's length (%lld) longer than parameter size (%llu).", + cnt, get_param_size(irec)); + RET_HDIAGS(stmt, SQL_STATE_22001); + } + + *len = json_escape((char *)data_ptr, cnt, dest + !!dest, SIZE_MAX); + + if (dest) { + dest[*len + /*1st `"`*/1] = '"'; + } + + *len += /*`"`*/2; + + return SQL_SUCCESS; +} + +static SQLRETURN c2sql_wstr2qstr(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, char *dest, size_t *len) +{ + void *data_ptr; + SQLLEN *octet_len_ptr, cnt, octets; + int err; + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + + /* pointer to read from how many bytes we have */ + octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); + /* pointer to app's buffer */ + data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); + + cnt = octet_len_ptr ? *octet_len_ptr : wcslen((wchar_t *)data_ptr); + + if (dest) { + *dest = '"'; + } else { + if ((SQLLEN)get_param_size(irec) < cnt) { + ERRH(stmt, "string's length (%lld) longer than parameter " + "size (%llu).", cnt, get_param_size(irec)); + RET_HDIAGS(stmt, SQL_STATE_22001); + } + } + + DBGH(stmt, "converting w-string [%lld] `" LWPDL "`; target@0x%p.", + cnt, cnt, (wchar_t *)data_ptr, dest); + if (cnt) { /* WCS2U8 will fail with empty string */ + SetLastError(0); + octets = WCS2U8((wchar_t *)data_ptr, (int)cnt, dest + !!dest, + dest ? INT_MAX : 0); + if ((err = GetLastError())) { + ERRH(stmt, "converting to multibyte string failed: %d", err); + RET_HDIAGS(stmt, SQL_STATE_HY000); + } + } else { + octets = 0; + } + assert(0 <= octets); /* buffer might be empty, so 0 is valid */ + *len = (size_t)octets; + + if (dest) { + /* last param - buffer len - is calculated as if !dest */ + *len = json_escape_overlapping(dest + /*1st `"`*/1, octets, + JSON_ESC_SEQ_SZ * octets); + dest[*len + /*1st `"`*/1] = '"'; + } else { + /* UCS*-to-UTF8 converted buffer is not yet available, so an accurate + * estimation of how long the JSON-escaping would take is not possible + * => estimate a worst case: 6x */ + *len *= JSON_ESC_SEQ_SZ; + } + + *len += /*2x `"`*/2; + + return SQL_SUCCESS; +} + +static SQLRETURN c2sql_number2qstr(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, char *dest, size_t *len) +{ + SQLRETURN ret; + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + + if (dest) { + *dest = '"'; + } + + ret = c2sql_number(arec, irec, pos, NULL,NULL, 0, dest + !!dest, len); + + if (dest) { + /* compare lengths only once number has actually been converted */ + if (get_param_size(irec) < *len) { + ERRH(stmt, "converted number length (%zu) larger than parameter " + "size (%llu)", *len, get_param_size(irec)); + RET_HDIAGS(stmt, SQL_STATE_22003); + } + dest[*len + /*1st `"`*/1] = '"'; + } + *len += /*2x `"`*/2; + + return ret; +} + +SQLRETURN c2sql_varchar(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, char *dest, size_t *len) +{ + SQLSMALLINT ctype; + esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + + switch ((ctype = get_rec_c_type(arec, irec))) { + case SQL_C_CHAR: + return c2sql_cstr2qstr(arec, irec, pos, dest, len); + case SQL_C_WCHAR: + return c2sql_wstr2qstr(arec, irec, pos, dest, len); + + case SQL_C_BINARY: + // XXX: json_escape + ERRH(stmt, "conversion from SQL C BINARY not implemented."); + RET_HDIAG(stmt, SQL_STATE_HYC00, "conversion from SQL C BINARY " + "not yet supported", 0); + break; + + case SQL_C_TINYINT: + case SQL_C_STINYINT: + case SQL_C_SHORT: + case SQL_C_SSHORT: + case SQL_C_LONG: + case SQL_C_SLONG: + case SQL_C_SBIGINT: + + case SQL_C_BIT: + case SQL_C_UTINYINT: + case SQL_C_USHORT: + case SQL_C_ULONG: + case SQL_C_UBIGINT: + + case SQL_C_FLOAT: + case SQL_C_DOUBLE: + case SQL_C_NUMERIC: + return c2sql_number2qstr(arec, irec, pos, dest, len); + + case SQL_C_TYPE_DATE: + case SQL_C_TYPE_TIME: + case SQL_C_TYPE_TIMESTAMP: + // TODO: leave it a timestamp, or actual DATE/TIME/TS? + return c2sql_timestamp(arec, irec, pos, dest, len); + + // case SQL_C_GUID: + default: + BUGH(stmt, "can't convert SQL C type %hd to timestamp.", + get_rec_c_type(arec, irec)); + RET_HDIAG(stmt, SQL_STATE_HY000, "bug converting parameter", 0); + } +} + + +/* vim: set noet fenc=utf-8 ff=dos sts=0 sw=4 ts=4 : */ diff --git a/driver/convert.h b/driver/convert.h new file mode 100644 index 00000000..52abc15e --- /dev/null +++ b/driver/convert.h @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +#ifndef __CONVERT_H__ +#define __CONVERT_H__ + +#include "error.h" +#include "handles.h" + +void convert_init(); + +SQLRETURN set_param_decdigits(esodbc_rec_st *irec, + SQLUSMALLINT param_no, SQLSMALLINT decdigits); +SQLSMALLINT get_param_decdigits(esodbc_rec_st *irec); +SQLRETURN set_param_size(esodbc_rec_st *irec, + SQLUSMALLINT param_no, SQLULEN size); +SQLULEN get_param_size(esodbc_rec_st *irec); + +inline void *deferred_address(SQLSMALLINT field_id, size_t pos, + esodbc_rec_st *rec); + +SQLRETURN convertability_check(esodbc_stmt_st *stmt, SQLINTEGER idx, + int *conv_code); + +/* + * SQL -> C SQL + */ + +SQLRETURN sql2c_string(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, const wchar_t *wstr, size_t chars_0); +SQLRETURN sql2c_longlong(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, long long ll); +SQLRETURN sql2c_double(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, double dbl); +/* + * SQL C -> SQL + */ +inline SQLRETURN c2sql_null(esodbc_rec_st *arec, + esodbc_rec_st *irec, char *dest, size_t *len); +SQLRETURN c2sql_boolean(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, char *dest, size_t *len); +SQLRETURN c2sql_number(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, double *min, double *max, BOOL fixed, char *dest, + size_t *len); +SQLRETURN c2sql_varchar(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, char *dest, size_t *len); +SQLRETURN c2sql_timestamp(esodbc_rec_st *arec, esodbc_rec_st *irec, + SQLULEN pos, char *dest, size_t *len); + +#endif /* __CONVERT_H__ */ diff --git a/driver/defs.h b/driver/defs.h index e6ea67e8..78294572 100644 --- a/driver/defs.h +++ b/driver/defs.h @@ -25,6 +25,8 @@ #define ESODBC_DEF_ARRAY_SIZE 1 /* max cols or args to bind; needs to stay <= SHRT_MAX */ #define ESODBC_MAX_DESC_COUNT 128 +/* number of static records for SQLGetData() */ +#define ESODBC_GD_DESC_COUNT ESODBC_MAX_DESC_COUNT /* values for SQL_ATTR_MAX_LENGTH statement attribute */ #define ESODBC_UP_MAX_LENGTH 0 // USHORT_MAX #define ESODBC_LO_MAX_LENGTH 0 @@ -43,6 +45,7 @@ #define ESODBC_QUOTE_CHAR "\"" #define ESODBC_PATTERN_ESCAPE "\\" #define ESODBC_CATALOG_SEPARATOR ":" +#define ESODBC_CATALOG_LOCATION SQL_CL_START /* TODO: 2x check! */ #define ESODBC_CATALOG_TERM "clusterName" #define ESODBC_TABLE_TERM "type" // TODO: or table? #define ESODBC_SCHEMA_TERM "schema" @@ -56,6 +59,20 @@ /* TODO: review@alpha */ /* match 'keyword' ES type length */ #define ESODBC_MAX_IDENTIFIER_LEN 256 +/* the following lifted/derived from ES/SQL's JdbcDatabaseMetaData.java */ +/* max columns in ORDER BY; 0 - no limit / unknown */ +#define ESODBC_MAX_COLUMNS_IN_ORDER_BY 0 +/* max columns in GROUP BY; 0 - no limit / unknown */ +#define ESODBC_MAX_COLUMNS_IN_GROUP_BY 0 +/* max columns in SELECT; 0 - no limit / unknown */ +#define ESODBC_MAX_COLUMNS_IN_SELECT 0 +/* "if the columns in the ORDER BY clause must be in the select list" */ +#define ESODBC_ORDER_BY_COLUMNS_IN_SELECT "Y" +/* "the relationship between the columns in the GROUP BY clause and the + * nonaggregated columns in the select list" */ +#define ESODBC_GROUP_BY SQL_GB_NO_RELATION +/* "how the data source handles the concatenation of NULL [.] with non-NULL" */ +#define ESODBC_CONCAT_NULL_BEHAVIOR SQL_CB_NULL /* 20 = len("18446744073709551616"), 1 << (sizeof(uint64_t) * 8bits) */ #define ESODBC_PRECISION_UINT64 20 @@ -100,9 +117,11 @@ */ /* maximum URL size */ #define ESODBC_MAX_URL_LEN 2048 -/* maximum DNS name */ -/* SQL_MAX_DSN_LENGTH=32 < IPv6 len */ -#define ESODBC_MAX_DNS_LEN 255 +/* maximum DNS attribute value lenght (should be long enought to accomodate a + * decently long FQ file path name) */ +#define ESODBC_DSN_MAX_ATTR_LEN 1024 +/* sample DSN name provisioned with the installation */ +#define ESODBC_DSN_SAMPLE_NAME "Elasticsearch ODBC Sample DSN" /* SQL plugin's REST endpoint for SQL */ #define ELASTIC_SQL_PATH "/_xpack/sql" @@ -144,7 +163,6 @@ /* default tracing level */ #define ESODBC_DEF_TRACE_LEVEL "WARN" - /* * * Driver/Elasticsearch capabilities @@ -159,6 +177,8 @@ /* Driver conformance level: CORE. * No scrollabe cursors et al. just yet */ #define ESODBC_ODBC_INTERFACE_CONFORMANCE SQL_OIC_CORE +#define ESODBC_GETDATA_EXTENSIONS (0 | \ + SQL_GD_ANY_COLUMN | SQL_GD_ANY_ORDER | SQL_GD_BOUND) /* Read-only queries supported only */ #define ESODBC_DATA_SOURCE_READ_ONLY "Y" /* no support for transactions */ @@ -199,6 +219,8 @@ #define ESODBC_BATCH_SUPPORT 0UL /* no driver support array of parameters */ #define ESODBC_PARAM_ARRAY_SELECTS SQL_PAS_NO_SELECT +/* no connection pooling or distributed transactions */ +#define ESODBC_DTC_TRANSITION_COST 0 /* diff --git a/driver/dsn.c b/driver/dsn.c new file mode 100644 index 00000000..403f7503 --- /dev/null +++ b/driver/dsn.c @@ -0,0 +1,849 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +#include "util.h" /* includes windows.h, needed for odbcinst.h */ +#include + +#include "dsn.h" +#include "log.h" + +#define ODBC_REG_SUBKEY_PATH "SOFTWARE\\ODBC\\ODBC.INI" +#define REG_HKLM "HKEY_LOCAL_MACHINE" +#define REG_HKCU "HKEY_CURRENT_USER" + + +void init_dsn_attrs(esodbc_dsn_attrs_st *attrs) +{ + size_t i; + wstr_st *wstr; + + memset(attrs, 0, sizeof(*attrs)); + + for (i = 0; i < ESODBC_DSN_ATTRS_COUNT; i ++) { + wstr = &((wstr_st *)attrs)[i]; + wstr->str = &attrs->buff[i * ESODBC_DSN_MAX_ATTR_LEN]; + } +} + +#define DSN_NOT_MATCHED 0 +#define DSN_NOT_OVERWRITTEN 1 +#define DSN_ASSIGNED 2 +#define DSN_OVERWRITTEN 3 +/* + * returns: + * positive on keyword match: + * DSN_ASSIGNED for assignment on blank, + * DSN_OVERWRITTEN for assignment over value, + * DSN_NOT_OVERWRITTEN for skipped assignment due to !overwrite; + * 0 on keyword mismatch; + * negative on error; + */ +int assign_dsn_attr(esodbc_dsn_attrs_st *attrs, + wstr_st *keyword, wstr_st *value, BOOL overwrite) +{ + struct { + wstr_st *kw; + wstr_st *val; + } *iter, map[] = { + {&MK_WSTR(ESODBC_DSN_DRIVER), &attrs->driver}, + {&MK_WSTR(ESODBC_DSN_DESCRIPTION), &attrs->description}, + {&MK_WSTR(ESODBC_DSN_DSN), &attrs->dsn}, + {&MK_WSTR(ESODBC_DSN_PWD), &attrs->pwd}, + {&MK_WSTR(ESODBC_DSN_UID), &attrs->uid}, + {&MK_WSTR(ESODBC_DSN_SAVEFILE), &attrs->savefile}, + {&MK_WSTR(ESODBC_DSN_FILEDSN), &attrs->filedsn}, + {&MK_WSTR(ESODBC_DSN_SERVER), &attrs->server}, + {&MK_WSTR(ESODBC_DSN_PORT), &attrs->port}, + {&MK_WSTR(ESODBC_DSN_SECURE), &attrs->secure}, + {&MK_WSTR(ESODBC_DSN_TIMEOUT), &attrs->timeout}, + {&MK_WSTR(ESODBC_DSN_FOLLOW), &attrs->follow}, + {&MK_WSTR(ESODBC_DSN_CATALOG), &attrs->catalog}, + {&MK_WSTR(ESODBC_DSN_PACKING), &attrs->packing}, + {&MK_WSTR(ESODBC_DSN_MAX_FETCH_SIZE), &attrs->max_fetch_size}, + {&MK_WSTR(ESODBC_DSN_MAX_BODY_SIZE_MB), &attrs->max_body_size}, + {&MK_WSTR(ESODBC_DSN_TRACE_FILE), &attrs->trace_file}, + {&MK_WSTR(ESODBC_DSN_TRACE_LEVEL), &attrs->trace_level}, + {NULL, NULL} + }; + int ret; + + if (ESODBC_DSN_MAX_ATTR_LEN < value->cnt) { + ERR("attribute value lenght too large: %zu; max=%zu.", value->cnt, + ESODBC_DSN_MAX_ATTR_LEN); + return -1; + } + + for (iter = &map[0]; iter->kw; iter ++) { + if (! EQ_CASE_WSTR(iter->kw, keyword)) { + continue; + } + /* it's a match: has it been assigned already? */ + if (iter->val->cnt) { + if (! overwrite) { + INFO("keyword '" LWPDL "' already assigned; " + "ignoring new `" LWPDL "`, keeping previous `" LWPDL "`.", + LWSTR(iter->kw), LWSTR(value), LWSTR(iter->val)); + return DSN_NOT_OVERWRITTEN; + } + INFO("keyword '" LWPDL "' already assigned: " + "overwriting previous `" LWPDL "` with new `" LWPDL "`.", + LWSTR(iter->kw), LWSTR(iter->val), LWSTR(value)); + ret = DSN_OVERWRITTEN; + } else { + INFO("keyword '" LWPDL "' new assignment: `" LWPDL "`.", + LWSTR(iter->kw), LWSTR(value)); + ret = DSN_ASSIGNED; + } + memcpy(iter->val->str, value->str, value->cnt * sizeof(*value->str)); + iter->val->cnt = value->cnt; + return ret; + } + + /* entry not directly relevant to driver config */ + WARN("keyword `" LWPDL "` (with value `" LWPDL "`) not DSN config " + "specific, so not assigned.", LWSTR(keyword), LWSTR(value)); + return DSN_NOT_MATCHED; +} + +/* + * Advance position in string, skipping white space. + * if exended is true, `;` will be treated as white space too. + */ +static SQLWCHAR *skip_ws(SQLWCHAR **pos, SQLWCHAR *end, BOOL extended) +{ + while (*pos < end) { + switch(**pos) { + case ' ': + case '\t': + case '\r': + case '\n': + (*pos)++; + break; + + case '\0': + return NULL; + + case ';': + if (extended) { + (*pos)++; + break; + } + // no break; + + default: + return *pos; + } + } + + /* end of string reached */ + return NULL; +} + +/* + * Parse a keyword or a value. + * Within braces, any character is allowed, safe for \0 (i.e. no "bla{bla\0" + * is supported as keyword or value). + * Braces within braces are allowed. + */ +static BOOL parse_token(BOOL is_value, SQLWCHAR **pos, SQLWCHAR *end, + wstr_st *token) +{ + BOOL brace_escaped = FALSE; + int open_braces = 0; + SQLWCHAR *start = *pos; + BOOL stop = FALSE; + + while (*pos < end && (! stop)) { + switch (**pos) { + case '\\': + if (! is_value) { + ERR("keywords and data source names cannot contain " + "the backslash."); + return FALSE; + } + (*pos)++; + break; + + case ' ': + case '\t': + case '\r': + case '\n': + if (open_braces || is_value) { + (*pos)++; + } else { + stop = TRUE; + } + break; + + case '=': + if (open_braces || is_value) { + (*pos)++; + } else { + stop = TRUE; + } + break; + + case ';': + if (open_braces) { + (*pos)++; + } else if (is_value) { + stop = TRUE; + } else { + ERR("';' found while parsing keyword"); + return FALSE; + } + break; + + case '\0': + if (open_braces) { + ERR("null terminator found while within braces"); + return FALSE; + } else if (! is_value) { + ERR("null terminator found while parsing keyword."); + return FALSE; + } /* else: \0 used as delimiter of value string */ + stop = TRUE; + break; + + case '{': + if (*pos == start) { + open_braces ++; + } else if (open_braces) { + ERR("token started with opening brace, so can't use " + "inner braces"); + /* b/c: `{foo{ = }}bar} = val`; `foo{bar}baz` is fine */ + return FALSE; + } + (*pos)++; + break; + + case '}': + if (open_braces) { + open_braces --; + brace_escaped = TRUE; + stop = TRUE; + } + (*pos)++; + break; + + default: + (*pos)++; + } + } + + if (open_braces) { + ERR("string finished with open braces."); + return FALSE; + } + + token->str = start + (brace_escaped ? 1 : 0); + token->cnt = (*pos - start) - (brace_escaped ? 2 : 0); + return TRUE; +} + +static SQLWCHAR *parse_separator(SQLWCHAR **pos, SQLWCHAR *end) +{ + if (*pos < end) { + if (**pos == '=') { + (*pos)++; + return *pos; + } + } + return NULL; +} + +/* + * - "keywords and attribute values that contain the characters []{}(),;?*=!@ + * not enclosed with braces should be avoided"; => allowed. + * - "value of the DSN keyword cannot consist only of blanks and should not + * contain leading blanks"; + * - "keywords and data source names cannot contain the backslash (\) + * character."; + * - "value enclosed with braces ({}) containing any of the characters + * []{}(),;?*=!@ is passed intact to the driver."; + * + * foo{bar}=baz=foo; => "foo{bar}" = "baz=foo" + * + * * `=` is delimiter, unless within {} + * * `{` and `}` allowed within {} + * * brances need to be returned to out-str; + */ +BOOL parse_connection_string(esodbc_dsn_attrs_st *attrs, + SQLWCHAR *szConnStrIn, SQLSMALLINT cchConnStrIn) +{ + + SQLWCHAR *pos; + SQLWCHAR *end; + wstr_st keyword, value; + + /* parse and assign attributes in connection string */ + pos = szConnStrIn; + end = pos + (cchConnStrIn == SQL_NTS ? SHRT_MAX : cchConnStrIn); + + while (skip_ws(&pos, end, TRUE)) { + if (! parse_token(FALSE, &pos, end, &keyword)) { + ERR("failed to parse keyword at position %zd", + pos - szConnStrIn); + return FALSE; + } + + if (! skip_ws(&pos, end, FALSE)) { + return FALSE; + } + + if (! parse_separator(&pos, end)) { + ERR("failed to parse separator (`=`) at position %zd", + pos - szConnStrIn); + return FALSE; + } + + if (! skip_ws(&pos, end, FALSE)) { + return FALSE; + } + + if (! parse_token(TRUE, &pos, end, &value)) { + ERR("failed to parse value at position %zd", + pos - szConnStrIn); + return FALSE; + } + + DBG("read connection string attribute: `" LWPDL "` = `" LWPDL "`.", + LWSTR(&keyword), LWSTR(&value)); + if (assign_dsn_attr(attrs, &keyword, &value, TRUE) < 0) { + ERRN("failed to assign keyword `" LWPDL "` with val `" LWPDL "`.", + LWSTR(&keyword), LWSTR(&value)); + return FALSE; + } + } + + return TRUE; +} + +BOOL parse_00_list(esodbc_dsn_attrs_st *attrs, SQLWCHAR *list00) +{ + SQLWCHAR *pos; + size_t cnt; + + for (pos = (SQLWCHAR *)list00; *pos; pos += cnt + 1) { + cnt = wcslen(pos); + + if (SHRT_MAX < cnt) { + ERR("invalid list lenght (%zu).", cnt); + return FALSE; + } + if (! parse_connection_string(attrs, pos, (SQLSMALLINT)cnt)) { + ERR("failed parsing list entry `" LWPDL "`.", cnt, pos); + return FALSE; + } + } + return TRUE; +} + +static void log_installer_err() +{ + DWORD ecode; + SQLWCHAR buff[SQL_MAX_MESSAGE_LENGTH]; + WORD msg_len; + int i = 0; + + while (SQL_SUCCEEDED(SQLInstallerError(++ i, &ecode, buff, + sizeof(buff)/sizeof(buff[0]), &msg_len))) { + ERR("#%i: errcode=%d: " LWPDL ".", i, ecode, msg_len, buff); + } +} + +/* + * Checks if a DSN entry with the given name exists already. + * Returns: + * . negative on failure + * . 0 on false + * . positive on true. + */ +int system_dsn_exists(wstr_st *dsn) +{ + int res; + SQLWCHAR kbuff[ESODBC_DSN_MAX_ATTR_LEN]; + + /* '\' can't be a key name */ + res = SQLGetPrivateProfileStringW(dsn->str, NULL, MK_WPTR("\\"), + kbuff, sizeof(kbuff)/sizeof(kbuff[0]), MK_WPTR(SUBKEY_ODBC)); + if (res < 0) { + ERR("failed to query for DSN registry keys."); + log_installer_err(); + return -1; + } + /* subkey can be found, but have nothing beneath => res == 0 */ + return (! res) || (kbuff[0] != MK_WPTR('\\')); + +} +/* + * Reads the system entries for a DSN given into a doubly null-terminated + * attributes list. + * + * The defaults are always filled in. + * + * The list - as received by ConfigDSN() - seems to only contain the 'DSN' + * keyword, though the documentation mentions a full list. However, if a full + * is provided, the values are going to be taken into account, but possibly + * overwritten by registry entries (which theoretically should be the same + * anyways). + */ +BOOL load_system_dsn(esodbc_dsn_attrs_st *attrs, SQLWCHAR *list00) +{ + int res; + SQLWCHAR buff[ESODBC_DSN_MAX_ATTR_LEN], *pos; + SQLWCHAR kbuff[sizeof(attrs->buff)/sizeof(attrs->buff[0])]; + wstr_st keyword, value; + + if (! parse_00_list(attrs, list00)) { + ERR("failed to parse doubly null-terminated attributes " + "list `" LWPD "`.", list00); + return FALSE; + } + /* ConfigDSN() requirement */ + if (attrs->driver.cnt) { + ERR("function can not accept '" ESODBC_DSN_DRIVER "' keyword."); + return FALSE; + } + + if (attrs->dsn.cnt) { + DBG("loading attributes for DSN `" LWPDL "`.", LWSTR(&attrs->dsn)); + + /* load available key *names* first; + * it's another doubly null-terminated list (w/o values). */ + res = SQLGetPrivateProfileStringW(attrs->dsn.str, NULL, MK_WPTR(""), + kbuff, sizeof(kbuff)/sizeof(kbuff[0]), MK_WPTR(SUBKEY_ODBC)); + if (res < 0) { + ERR("failed to query for DSN registry keys."); + log_installer_err(); + return FALSE; + } + /* for each key name, read its value and add it to 'attrs' */ + for (pos = kbuff; *pos; pos += keyword.cnt + 1) { + keyword.str = pos; + keyword.cnt = wcslen(pos); + + if (EQ_CASE_WSTR(&keyword, &MK_WSTR(ESODBC_DSN_DRIVER))) { + /* skip the 'Driver' keyword */ + continue; + } + + res = SQLGetPrivateProfileStringW(attrs->dsn.str, pos, MK_WPTR(""), + buff, sizeof(buff)/sizeof(buff[0]), MK_WPTR(SUBKEY_ODBC)); + if (res < 0) { + ERR("failed to query value for DSN registry key `" LWPDL "`.", + LWSTR(&keyword)); + log_installer_err(); + return FALSE; + } else { + value.cnt = (size_t)res; + value.str = buff; + } + /* assign it to the config */ + DBG("read DSN attribute: `" LWPDL "` = `" LWPDL "`.", + LWSTR(&keyword), LWSTR(&value)); + /* assign attributes not yet given in the 00-list */ + if (assign_dsn_attr(attrs, &keyword, &value, /*over?*/FALSE) < 0) { + ERR("keyword '" LWPDL "' couldn't be assigned.", + LWSTR(&keyword)); + return FALSE; + } + } + } + + if (! assign_dsn_defaults(attrs)) { + ERR("OOM assigning defaults"); + return FALSE; + } + + return TRUE; +} + +BOOL write_system_dsn(esodbc_dsn_attrs_st *attrs, BOOL create_new) +{ + struct { + wstr_st *kw; + wstr_st *val; + } *iter, map[] = { + /* Driver */ + {&MK_WSTR(ESODBC_DSN_DESCRIPTION), &attrs->description}, + /* DSN */ + {&MK_WSTR(ESODBC_DSN_PWD), &attrs->pwd}, + {&MK_WSTR(ESODBC_DSN_UID), &attrs->uid}, + /* SAVEILE */ + /* FILEDSN */ + {&MK_WSTR(ESODBC_DSN_SERVER), &attrs->server}, + {&MK_WSTR(ESODBC_DSN_PORT), &attrs->port}, + {&MK_WSTR(ESODBC_DSN_SECURE), &attrs->secure}, + {&MK_WSTR(ESODBC_DSN_TIMEOUT), &attrs->timeout}, + {&MK_WSTR(ESODBC_DSN_FOLLOW), &attrs->follow}, + {&MK_WSTR(ESODBC_DSN_CATALOG), &attrs->catalog}, + {&MK_WSTR(ESODBC_DSN_PACKING), &attrs->packing}, + {&MK_WSTR(ESODBC_DSN_MAX_FETCH_SIZE), &attrs->max_fetch_size}, + {&MK_WSTR(ESODBC_DSN_MAX_BODY_SIZE_MB), &attrs->max_body_size}, + {&MK_WSTR(ESODBC_DSN_TRACE_FILE), &attrs->trace_file}, + {&MK_WSTR(ESODBC_DSN_TRACE_LEVEL), &attrs->trace_level}, + {NULL, NULL} + }; + + if (create_new) { + if (! SQLValidDSNW(attrs->dsn.str)) { + ERR("invalid DSN value `" LWPDL "`.", LWSTR(&attrs->dsn)); + return FALSE; + } + INFO("creating new DSN `" LWPDL "` for driver ` " LWPDL " `.", + LWSTR(&attrs->dsn), LWSTR(&attrs->driver)); + if (! SQLWriteDSNToIniW(attrs->dsn.str, attrs->driver.str)) { + ERR("failed to add DSN `" LWPDL "` for driver ` " LWPDL " ` to " + ".INI.", LWSTR(&attrs->dsn), LWSTR(&attrs->driver)); + return FALSE; + } + } + + for (iter = &map[0]; iter->kw; iter ++) { + if (! iter->val->cnt) { + DBG("value `" LWPDL "` not provisioned.", LWSTR(iter->kw)); + continue; + } + if (! SQLWritePrivateProfileStringW(attrs->dsn.str, + iter->kw->str, iter->val->str, MK_WPTR(SUBKEY_ODBC))) { + ERR("failed to write key `" LWPDL "` with value `" LWPDL "`.", + LWSTR(iter->kw), LWSTR(iter->val)); + return FALSE; + } + DBG("key `" LWPDL "` with value `" LWPDL "` written to " SUBKEY_ODBC + ".", LWSTR(iter->kw), LWSTR(iter->val)); + } + return TRUE; +} + +static inline BOOL needs_braces(wstr_st *token) +{ + size_t i; + + for (i = 0; i < token->cnt; i ++) { + switch(token->str[i]) { + case ' ': + case '\t': + case '\r': + case '\n': + case '=': + case ';': + return TRUE; + } + } + return FALSE; +} + +/* build a connection string to be written in the DSN files */ +BOOL write_connection_string(esodbc_dsn_attrs_st *attrs, + SQLWCHAR *szConnStrOut, SQLSMALLINT cchConnStrOutMax, + SQLSMALLINT *pcchConnStrOut) +{ + int n, braces; + size_t pos; + wchar_t *format; + struct { + wstr_st *val; + char *kw; + } *iter, map[] = { + {&attrs->driver, ESODBC_DSN_DRIVER}, + /* Description */ + {&attrs->dsn, ESODBC_DSN_DSN}, + {&attrs->pwd, ESODBC_DSN_PWD}, + {&attrs->uid, ESODBC_DSN_UID}, + {&attrs->savefile, ESODBC_DSN_SAVEFILE}, + {&attrs->filedsn, ESODBC_DSN_FILEDSN}, + {&attrs->server, ESODBC_DSN_SERVER}, + {&attrs->port, ESODBC_DSN_PORT}, + {&attrs->secure, ESODBC_DSN_SECURE}, + {&attrs->timeout, ESODBC_DSN_TIMEOUT}, + {&attrs->follow, ESODBC_DSN_FOLLOW}, + {&attrs->catalog, ESODBC_DSN_CATALOG}, + {&attrs->packing, ESODBC_DSN_PACKING}, + {&attrs->max_fetch_size, ESODBC_DSN_MAX_FETCH_SIZE}, + {&attrs->max_body_size, ESODBC_DSN_MAX_BODY_SIZE_MB}, + {&attrs->trace_file, ESODBC_DSN_TRACE_FILE}, + {&attrs->trace_level, ESODBC_DSN_TRACE_LEVEL}, + {NULL, NULL} + }; + + for (iter = &map[0], pos = 0; iter->val; iter ++) { + if (iter->val->cnt) { + braces = needs_braces(iter->val) ? 2 : 0; + if (cchConnStrOutMax && szConnStrOut) { + /* swprintf will fail if formated string would overrun the + * buffer size */ + if (cchConnStrOutMax - pos < iter->val->cnt + braces) { + /* indicate that we've reached buffer limits: only account + * for how long the string would be */ + cchConnStrOutMax = 0; + pos += iter->val->cnt + braces; + continue; + } + if (braces) { + format = WPFCP_DESC "={" WPFWP_LDESC "};"; + } else { + format = WPFCP_DESC "=" WPFWP_LDESC ";"; + } + n = swprintf(szConnStrOut + pos, cchConnStrOutMax - pos, + format, iter->kw, LWSTR(iter->val)); + if (n < 0) { + ERRN("failed to outprint connection string (space " + "left: %d; needed: %d).", cchConnStrOutMax - pos, + iter->val->cnt); + return FALSE; + } else { + pos += n; + } + } else { + /* simply increment the counter, since the untruncated length + * need to be returned to the app */ + pos += iter->val->cnt + braces; + } + } + } + + *pcchConnStrOut = (SQLSMALLINT)pos; + + DBG("Output connection string: `" LWPD "`; out len: %d.", + szConnStrOut, pos); + return TRUE; +} + +BOOL assign_dsn_defaults(esodbc_dsn_attrs_st *attrs) +{ + /* assign defaults where not assigned and applicable */ + if (assign_dsn_attr(attrs, + &MK_WSTR(ESODBC_DSN_SERVER), &MK_WSTR(ESODBC_DEF_SERVER), + /*overwrite?*/FALSE) < 0) { + return FALSE; + } + if (assign_dsn_attr(attrs, + &MK_WSTR(ESODBC_DSN_PORT), &MK_WSTR(ESODBC_DEF_PORT), + /*overwrite?*/FALSE) < 0) { + return FALSE; + } + if (assign_dsn_attr(attrs, + &MK_WSTR(ESODBC_DSN_SECURE), &MK_WSTR(ESODBC_DEF_SECURE), + /*overwrite?*/FALSE) < 0) { + return FALSE; + } + if (assign_dsn_attr(attrs, + &MK_WSTR(ESODBC_DSN_TIMEOUT), &MK_WSTR(ESODBC_DEF_TIMEOUT), + /*overwrite?*/FALSE) < 0) { + return FALSE; + } + if (assign_dsn_attr(attrs, + &MK_WSTR(ESODBC_DSN_FOLLOW), &MK_WSTR(ESODBC_DEF_FOLLOW), + /*overwrite?*/FALSE) < 0) { + return FALSE; + } + + if (assign_dsn_attr(attrs, + &MK_WSTR(ESODBC_DSN_PACKING), &MK_WSTR(ESODBC_DEF_PACKING), + /*overwrite?*/FALSE) < 0) { + return FALSE; + } + if (assign_dsn_attr(attrs, + &MK_WSTR(ESODBC_DSN_MAX_FETCH_SIZE), + &MK_WSTR(ESODBC_DEF_FETCH_SIZE), + /*overwrite?*/FALSE) < 0) { + return FALSE; + } + if (assign_dsn_attr(attrs, &MK_WSTR(ESODBC_DSN_MAX_BODY_SIZE_MB), + &MK_WSTR(ESODBC_DEF_MAX_BODY_SIZE_MB), + /*overwrite?*/FALSE) < 0) { + return FALSE; + } + + /* default: no trace file */ + if (assign_dsn_attr(attrs, + &MK_WSTR(ESODBC_DSN_TRACE_LEVEL), &MK_WSTR(ESODBC_DEF_TRACE_LEVEL), + /*overwrite?*/FALSE) < 0) { + return FALSE; + } + + return TRUE; +} + + +#if defined(_WIN32) || defined (WIN32) +/* + * Reads system registry for ODBC DSN subkey named in attrs->dsn. + * TODO: use odbccp32.dll's SQLGetPrivateProfileString() & co. instead of + * direct registry access: + * https://docs.microsoft.com/en-us/sql/odbc/reference/syntax/sqlgetprivateprofilestring-function + */ +BOOL read_system_info(esodbc_dsn_attrs_st *attrs) +{ + HKEY hkey; + BOOL ret = FALSE; + const char *ktree; + DWORD valsno/* number of values in subkey */, i, res; + DWORD maxvallen; /* len of longest value name */ + DWORD maxdatalen; /* len of longest data (buffer) */ + DWORD vallen; + DWORD valtype; + DWORD datalen; + SQLWCHAR val_buff[ESODBC_DSN_MAX_ATTR_LEN]; + wstr_st val_name = {val_buff, 0}; + SQLWCHAR data_buff[ESODBC_DSN_MAX_ATTR_LEN]; + wstr_st val_data = {data_buff, 0}; + + if (swprintf(val_buff, sizeof(val_buff)/sizeof(val_buff[0]), + WPFCP_DESC "\\" WPFWP_LDESC, + ODBC_REG_SUBKEY_PATH, LWSTR(&attrs->dsn)) < 0) { + ERRN("failed to print registry key path."); + return FALSE; + } + /* try accessing local user's config first, if that fails, systems' */ + if (RegOpenKeyExW(HKEY_CURRENT_USER, val_buff, /*options*/0, KEY_READ, + &hkey) != ERROR_SUCCESS) { + INFO("failed to open registry key `" REG_HKCU "\\" LWPD "`.", + val_buff); + if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, val_buff, /*options*/0, KEY_READ, + &hkey) != ERROR_SUCCESS) { + INFO("failed to open registry key `" REG_HKLM "\\" LWPD "`.", + val_buff); + goto end; + } else { + ktree = REG_HKLM; + } + } else { + ktree = REG_HKCU; + } + + if (RegQueryInfoKeyW(hkey, /*key class*/NULL, /*len of key class*/NULL, + /*reserved*/NULL, /*no subkeys*/NULL, /*longest subkey*/NULL, + /*longest subkey class name*/NULL, &valsno, &maxvallen, + &maxdatalen, /*sec descr*/NULL, + /*update time */NULL) != ERROR_SUCCESS) { + ERRN("Failed to query registery key info for path `%s\\" LWPD + "`.", ktree, val_buff); + goto end; + } else { + DBG("Subkey '%s\\" LWPD "': vals: %d, lengthiest name: %d, " + "lengthiest data: %d.", ktree, val_buff, valsno, maxvallen, + maxdatalen); + if (sizeof(val_buff)/sizeof(val_buff[0]) < maxvallen) { + WARN("value name buffer too small (%d), needed: %dB.", + sizeof(val_buff)/sizeof(val_buff[0]), maxvallen); + } + if (sizeof(data_buff)/sizeof(data_buff[0]) < maxdatalen) { + WARN("value data buffer too small (%d), needed: %dB.", + sizeof(data_buff)/sizeof(data_buff[0]), maxdatalen); + } + /* the registry might contain other, non connection-related strings, + * so these conditions are not necearily an error. */ + } + + for (i = 0; i < valsno; i ++) { + vallen = sizeof(val_buff) / sizeof(val_buff[0]); + datalen = sizeof(data_buff); + + if (RegEnumValueW(hkey, i, val_buff, &vallen, /*reserved*/NULL, &valtype, + (BYTE *)data_buff, &datalen) != ERROR_SUCCESS) { + ERR("failed to read register subkey value."); + goto end; + } + /* vallen doesn't count the \0 */ + val_name.cnt = vallen; + if (valtype != REG_SZ) { + INFO("skipping register value `" LWPDL "` of type %d.", + LWSTR(&val_name), valtype); + continue; + } + if (datalen <= 0) { + INFO("skipping value `" LWPDL "` with empty data.", + LWSTR(&val_name)); + continue; + } + assert(datalen % sizeof(SQLWCHAR) == 0); + /* datalen counts all bytes returned, so including the \0 */ + val_data.cnt = datalen/sizeof(SQLWCHAR) - /*\0*/1; + + if ((res = assign_dsn_attr(attrs, &val_name, &val_data, + /*overwrite?*/FALSE)) < 0) { + ERR("failed to assign reg entry `" LTPDL "`: `" LTPDL "`.", + LTSTR(&val_name), LTSTR(&val_data)); + goto end; + } else if (res == DSN_ASSIGNED) { + DBG("reg entry`" LTPDL "`: `" LTPDL "` assigned.", + LTSTR(&val_name), LTSTR(&val_data)); + } + /* if == 0: entry not directly relevant to driver config + * if == DSN_NOT_OVERWRITTEN: entry provisioned in the conn str. */ + } + + ret = TRUE; +end: + RegCloseKey(hkey); + + return ret; +} +#else /* defined(_WIN32) || defined (WIN32) */ +#error "unsupported platform" /* TODO */ +#endif /* defined(_WIN32) || defined (WIN32) */ + + +// asks user for the config data +// returns: +// . negative: on failure +// . 0: user canceled +// . positive: user provided input +int prompt_user_config(HWND hwndParent, esodbc_dsn_attrs_st *attrs, + /* disable non-connect-related controls? */ + BOOL disable_nonconn) +{ + static int attempts = 0; + + if (! hwndParent) { + INFO("no window handler provided -- configuration skipped."); + return 1; + } + TRACE; + + if (assign_dsn_attr(attrs, &MK_WSTR(ESODBC_DSN_DSN), + &MK_WSTR("My Elasticsearch ODBC DSN"), FALSE) <= 0) { + //&MK_WSTR("My Elasticsearch ODBC DSN"), TRUE, TRUE) <= 0) { + return -1; + } +#if 1 + if (assign_dsn_attr(attrs, &MK_WSTR(ESODBC_DSN_TRACE_LEVEL), + &MK_WSTR("INFO"), FALSE) <= 0) { + return -1; + } +#endif + if (1 < attempts ++) { + /* prevent infinite loops */ + return 0; + } + + if (SQL_MAX_DSN_LENGTH < attrs->dsn.cnt) { + ERR("DSN name longer than max (%d).", SQL_MAX_DSN_LENGTH); + } + + return 1; +} + +// asks user if we should overwrite the existing DSN +// returns: +// . negative on failure +// . 0 on false +// . positive on true +int prompt_user_overwrite(HWND hwndParent, wstr_st *dsn) +{ + if (! hwndParent) { + INFO("no window handler provided -- forcing overwrite."); + return 1; + } + TRACE; + return 1; +} + + +/* vim: set noet fenc=utf-8 ff=dos sts=0 sw=4 ts=4 : */ diff --git a/driver/dsn.h b/driver/dsn.h new file mode 100644 index 00000000..d7b0fe56 --- /dev/null +++ b/driver/dsn.h @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +#ifndef __DSN_H__ +#define __DSN_H__ + +#include "util.h" +#include "defs.h" + +#define SUBKEY_ODBCINST "ODBCINST.INI" +#define SUBKEY_ODBC "ODBC.INI" + +/* attribute keywords used in connection strings */ +#define ESODBC_DSN_DRIVER "Driver" +#define ESODBC_DSN_DESCRIPTION "Description" +#define ESODBC_DSN_DSN "DSN" +#define ESODBC_DSN_PWD "PWD" +#define ESODBC_DSN_UID "UID" +#define ESODBC_DSN_SAVEFILE "SAVEFILE" +#define ESODBC_DSN_FILEDSN "FILEDSN" +#define ESODBC_DSN_SERVER "Server" +#define ESODBC_DSN_PORT "Port" +#define ESODBC_DSN_SECURE "Secure" +#define ESODBC_DSN_TIMEOUT "Timeout" +#define ESODBC_DSN_FOLLOW "Follow" +#define ESODBC_DSN_CATALOG "Catalog" +#define ESODBC_DSN_PACKING "Packing" +#define ESODBC_DSN_MAX_FETCH_SIZE "MaxFetchSize" +#define ESODBC_DSN_MAX_BODY_SIZE_MB "MaxBodySizeMB" +#define ESODBC_DSN_TRACE_FILE "TraceFile" +#define ESODBC_DSN_TRACE_LEVEL "TraceLevel" + +/* stucture to collect all attributes in a connection string */ +typedef struct { + wstr_st driver; + wstr_st description; + wstr_st dsn; + wstr_st pwd; + wstr_st uid; + wstr_st savefile; + wstr_st filedsn; + wstr_st server; + wstr_st port; + wstr_st secure; + wstr_st timeout; + wstr_st follow; + wstr_st catalog; + wstr_st packing; + wstr_st max_fetch_size; + wstr_st max_body_size; + wstr_st trace_file; + wstr_st trace_level; +#define ESODBC_DSN_ATTRS_COUNT 18 + SQLWCHAR buff[ESODBC_DSN_ATTRS_COUNT * ESODBC_DSN_MAX_ATTR_LEN]; +} esodbc_dsn_attrs_st; + +void init_dsn_attrs(esodbc_dsn_attrs_st *attrs); +BOOL assign_dsn_defaults(esodbc_dsn_attrs_st *attrs); +BOOL assign_dsn_attr(esodbc_dsn_attrs_st *attrs, + wstr_st *keyword, wstr_st *value, BOOL overwrite); + +BOOL read_system_info(esodbc_dsn_attrs_st *attrs); +int system_dsn_exists(wstr_st *dsn); +BOOL load_system_dsn(esodbc_dsn_attrs_st *attrs, SQLWCHAR *list00); +BOOL write_system_dsn(esodbc_dsn_attrs_st *attrs, BOOL create_new); + +BOOL parse_connection_string(esodbc_dsn_attrs_st *attrs, + SQLWCHAR *szConnStrIn, SQLSMALLINT cchConnStrIn); +BOOL write_connection_string(esodbc_dsn_attrs_st *attrs, + SQLWCHAR *szConnStrOut, SQLSMALLINT cchConnStrOutMax, + SQLSMALLINT *pcchConnStrOut); + +BOOL prompt_user_config(HWND hwndParent, esodbc_dsn_attrs_st *attrs, + BOOL disable_nonconn); +int prompt_user_overwrite(HWND hwndParent, wstr_st *dsn); + +#endif /* __DSN_H__ */ + + +/* vim: set noet fenc=utf-8 ff=dos sts=0 sw=4 ts=4 : */ diff --git a/driver/error.c b/driver/error.c index 31fe28da..ec18dcde 100644 --- a/driver/error.c +++ b/driver/error.c @@ -40,10 +40,11 @@ SQLRETURN post_diagnostic(esodbc_diag_st *dest, esodbc_state_et state, if (ebufsz <= pos + tcnt) { wcsncpy(dest->text + pos, text, ebufsz - (pos + 1)); dest->text[ebufsz - 1] = 0; - dest->text_len = (int)ebufsz - 1; + assert(1 < ebufsz && ebufsz < USHRT_MAX); + dest->text_len = (SQLUSMALLINT)ebufsz - 1; } else { wcsncpy(dest->text + pos, text, tcnt + /* 0-term */1); - dest->text_len = (int)(pos + tcnt); + dest->text_len = (SQLUSMALLINT)(pos + tcnt); } DBG("diagnostic message: `" LWPD "` [%d], native code: %d.", dest->text, dest->text_len, dest->native_code); diff --git a/driver/handles.c b/driver/handles.c index 25abce18..069af88f 100644 --- a/driver/handles.c +++ b/driver/handles.c @@ -26,8 +26,8 @@ static void free_rec_fields(esodbc_rec_st *rec) &rec->table_name.str, }; for (i = 0; i < sizeof(wptr)/sizeof(wptr[0]); i ++) { - DBGH(rec->desc, "freeing field #%d = 0x%p.", i, *wptr[i]); if (*wptr[i]) { + DBGH(rec->desc, "freeing field #%d = 0x%p.", i, *wptr[i]); free(*wptr[i]); *wptr[i] = NULL; } @@ -63,11 +63,9 @@ static void init_desc(esodbc_desc_st *desc, esodbc_stmt_st *stmt, init_hheader(&desc->hdr, SQL_HANDLE_DESC, stmt); - desc->type = type; desc->alloc_type = alloc_type; - /* a user can only alloc an anon type -> can't have IxD on _USER type */ - assert((alloc_type == SQL_DESC_ALLOC_USER && type == DESC_TYPE_ANON) || - (alloc_type == SQL_DESC_ALLOC_AUTO)); + /* user allocated descriptors are reset to ANON */ + desc->type = alloc_type == SQL_DESC_ALLOC_USER ? DESC_TYPE_ANON : type; desc->array_size = ESODBC_DEF_ARRAY_SIZE; if (DESC_TYPE_IS_APPLICATION(type)) { @@ -75,28 +73,23 @@ static void init_desc(esodbc_desc_st *desc, esodbc_stmt_st *stmt, } } -static void clear_desc(esodbc_stmt_st *stmt, desc_type_et dtype, BOOL reinit) +static void clear_desc(esodbc_desc_st *desc, BOOL reinit) { - esodbc_desc_st *desc; - DBGH(stmt, "clearing desc type %d.", dtype); - switch (dtype) { + DBGH(desc, "clearing desc type %d.", desc->type); + switch (desc->type) { + case DESC_TYPE_ANON: case DESC_TYPE_ARD: - desc = stmt->ard; + case DESC_TYPE_APD: + case DESC_TYPE_IPD: break; + case DESC_TYPE_IRD: - if (STMT_HAS_RESULTSET(stmt)) { - clear_resultset(stmt); + if (STMT_HAS_RESULTSET(desc->hdr.stmt)) { + clear_resultset(desc->hdr.stmt); } - desc = stmt->ird; - break; - case DESC_TYPE_APD: - desc = stmt->apd; - break; - case DESC_TYPE_IPD: - desc = stmt->ipd; break; default: - BUG("no such descriptor of type %d.", dtype); + BUG("no such descriptor of type %d.", desc->type); return; } if (desc->recs) { @@ -151,6 +144,7 @@ void dump_record(esodbc_rec_st *rec) #undef DUMP_FIELD } + /* * The Driver Manager does not call the driver-level environment handle * allocation function until the application calls SQLConnect, @@ -172,6 +166,7 @@ void dump_record(esodbc_rec_st *rec) SQLRETURN EsSQLAllocHandle(SQLSMALLINT HandleType, SQLHANDLE InputHandle, _Out_ SQLHANDLE *OutputHandle) { + esodbc_env_st *env; esodbc_dbc_st *dbc; esodbc_stmt_st *stmt; esodbc_hhdr_st *hdr; @@ -216,16 +211,17 @@ SQLRETURN EsSQLAllocHandle(SQLSMALLINT HandleType, * return SQLSTATE HY010 (Function sequence error). * """ */ - if (! ENVH(InputHandle)->version) { - ERR("no version set in env when allocating DBC."); - RET_HDIAG(ENVH(InputHandle), SQL_STATE_HY010, + env = ENVH(InputHandle); + if (! env->version) { + ERRH(env, "no version set in env when allocating DBC."); + RET_HDIAG(env, SQL_STATE_HY010, "enviornment has no version set yet", 0); } dbc = (esodbc_dbc_st *)calloc(1, sizeof(esodbc_dbc_st)); *OutputHandle = (SQLHANDLE *)dbc; if (! dbc) { - ERRN("failed to callocate connection handle."); - RET_HDIAGS(ENVH(InputHandle), SQL_STATE_HY001); + ERRNH(env, "failed to callocate connection handle."); + RET_HDIAGS(env, SQL_STATE_HY001); } dbc->dsn.str = MK_WPTR(""); /* see explanation in cleanup_dbc() */ dbc->metadata_id = SQL_FALSE; @@ -234,7 +230,7 @@ SQLRETURN EsSQLAllocHandle(SQLSMALLINT HandleType, /* rest of initialization done at connect time */ - DBG("new Connection handle allocated @0x%p.", *OutputHandle); + DBGH(env, "new Connection handle allocated @0x%p.", *OutputHandle); break; case SQL_HANDLE_STMT: /* Statement Handle */ @@ -242,7 +238,7 @@ SQLRETURN EsSQLAllocHandle(SQLSMALLINT HandleType, stmt = (esodbc_stmt_st *)calloc(1, sizeof(esodbc_stmt_st)); *OutputHandle = stmt; if (! stmt) { - ERRN("failed to callocate statement handle."); + ERRNH(dbc, "failed to callocate statement handle."); RET_HDIAGS(dbc, SQL_STATE_HY001); } @@ -259,6 +255,7 @@ SQLRETURN EsSQLAllocHandle(SQLSMALLINT HandleType, stmt->ipd = &stmt->i_ipd; /* set option defaults */ + /* TODO: change to SQL_UB_DEFAULT when supporting bookmarks */ stmt->bookmarks = SQL_UB_OFF; /* inherit this connection-statement attributes * Note: these attributes won't propagate at statement level when @@ -267,18 +264,19 @@ SQLRETURN EsSQLAllocHandle(SQLSMALLINT HandleType, stmt->async_enable = dbc->async_enable; stmt->sql2c_conversion = CONVERSION_UNCHECKED; - DBG("new Statement handle allocated @0x%p.", *OutputHandle); + DBGH(dbc, "new Statement handle allocated @0x%p.", *OutputHandle); break; case SQL_HANDLE_DESC: + stmt = STMH(InputHandle); *OutputHandle = (SQLHANDLE *)calloc(1, sizeof(esodbc_desc_st)); if (! *OutputHandle) { - ERRN("failed to callocate descriptor handle."); - RET_HDIAGS(STMH(InputHandle), SQL_STATE_HY001); + ERRNH(stmt, "failed to callocate descriptor handle."); + RET_HDIAGS(stmt, SQL_STATE_HY001); } init_desc(*OutputHandle, InputHandle, DESC_TYPE_ANON, SQL_DESC_ALLOC_USER); - DBG("new Descriptor handle allocated @0x%p.", *OutputHandle); + DBGH(stmt, "new Descriptor handle allocated @0x%p.",*OutputHandle); // FIXME: assign/chain to statement? break; @@ -306,6 +304,7 @@ SQLRETURN EsSQLAllocHandle(SQLSMALLINT HandleType, SQLRETURN EsSQLFreeHandle(SQLSMALLINT HandleType, SQLHANDLE Handle) { esodbc_stmt_st *stmt; + esodbc_desc_st *desc; if (! Handle) { ERR("provided null Handle."); @@ -326,21 +325,24 @@ SQLRETURN EsSQLFreeHandle(SQLSMALLINT HandleType, SQLHANDLE Handle) case SQL_HANDLE_STMT: // TODO: remove from (potential) list? stmt = STMH(Handle); + detach_sql(stmt); - clear_desc(stmt, DESC_TYPE_ARD, FALSE); - clear_desc(stmt, DESC_TYPE_IRD, FALSE); - clear_desc(stmt, DESC_TYPE_APD, FALSE); - clear_desc(stmt, DESC_TYPE_IPD, FALSE); + + clear_desc(stmt->ard, FALSE); + clear_desc(stmt->ird, FALSE); + clear_desc(stmt->apd, FALSE); + clear_desc(stmt->ipd, FALSE); free(stmt); break; + // FIXME: /* "When an explicitly allocated descriptor is freed, all statement * handles to which the freed descriptor applied automatically revert * to the descriptors implicitly allocated for them." */ case SQL_HANDLE_DESC: - //break; - // FIXME - FIXME; + desc = DSCH(Handle); + clear_desc(desc, FALSE); + break; case SQL_HANDLE_SENV: /* Shared Environment Handle */ // TODO: do I need to set the state into the Handle? @@ -413,7 +415,7 @@ SQLRETURN EsSQLFreeStmt(SQLHSTMT StatementHandle, SQLUSMALLINT Option) * pending results." */ case SQL_CLOSE: DBGH(stmt, "closing."); - clear_desc(stmt, DESC_TYPE_IRD, FALSE /*keep the header values*/); + clear_desc(stmt->ird, FALSE /*keep the header values*/); // TODO: /_xpack/sql/close ? if still pending data? break; @@ -562,8 +564,8 @@ SQLRETURN EsSQLSetStmtAttrW( case SQL_ATTR_USE_BOOKMARKS: DBGH(stmt, "setting use-bookmarks to: %u.", (SQLULEN)ValuePtr); if ((SQLULEN)ValuePtr != SQL_UB_OFF) { - WARNH(stmt, "bookmarks are not supported by driver."); - RET_HDIAG(stmt, SQL_STATE_01000, + ERRH(stmt, "bookmarks are not supported by driver."); + RET_HDIAG(stmt, SQL_STATE_HYC00, "bookmarks are not supported by driver", 0); } break; @@ -704,32 +706,48 @@ SQLRETURN EsSQLSetStmtAttrW( return ret; case SQL_ATTR_APP_ROW_DESC: - if ((ValuePtr == (SQLPOINTER *)&stmt->i_ard) || - (ValuePtr == SQL_NULL_HDESC)) { - if (stmt->ard) { - DBGH(stmt, "unbinding ARD 0x%p.", stmt->ard); - // FIXME: unbind - FIXME; - } + desc = DSCH(ValuePtr); + if (desc == stmt->ard) { + WARNH(stmt, "trying to overwrite ARD with same value (@0x%p).", + ValuePtr); + break; /* nop */ + } + if (desc == &stmt->i_ard || desc == SQL_NULL_HDESC) { + DBGH(stmt, "unbinding current ARD (@0x%p), rebinding with " + "implicit value (@0x%p).", stmt->ard, &stmt->i_ard); + /* re-anonymize the descriptor, makingit re-usable */ stmt->ard = &stmt->i_ard; - FIXME; - } else if (FALSE) { + } else { /* "This attribute cannot be set to a descriptor handle that * was implicitly allocated for another statement or to * another descriptor handle that was implicitly set on the * same statement; implicitly allocated descriptor handles * cannot be associated with more than one statement or * descriptor handle." */ - /* TODO: check if this is implicitely allocated in all - statements??? */ - ERRH(stmt, "trying to set AxD (%d) descriptor to the wrong " - "implicit descriptor @0x%p.", Attribute, ValuePtr); - RET_HDIAGS(stmt, SQL_STATE_HY017); - } else { - stmt->ard = (esodbc_desc_st *)ValuePtr; - // FIXME: bind: re-init - FIXME; + if (desc->alloc_type == SQL_DESC_ALLOC_AUTO) { + ERRH(stmt, "source ARD (@0x%p) is implicit (alloc: %d).", + desc, desc->alloc_type); + RET_HDIAGS(stmt, SQL_STATE_HY017); + } else { + switch (desc->type) { + case DESC_TYPE_ANON: + desc->type = DESC_TYPE_ARD; + case DESC_TYPE_ARD: + break; + default: + // TODO: should this be allowed? + /* this means a descriptor can not be changed from + * APD to ARD (IxD is also ruled out) */ + ERRH(stmt, "can't convert descriptor from type %d" + " to ARD.", desc->type); + RET_HDIAGS(stmt, SQL_STATE_HY024); + } + DBGH(stmt, "overwritting current ARD (@0x%p) with new " + "value (@0x%p).", stmt->ard, desc); + stmt->ard = desc; + } } + break; case SQL_ATTR_APP_PARAM_DESC: // FIXME: same logic for ARD as above (part of params passing) FIXME; @@ -854,8 +872,10 @@ SQLRETURN EsSQLGetStmtAttrW( desc = stmt->apd; break; } while (0); + /* " If Attribute is an ODBC-defined attribute and *ValuePtr is an + * integer, BufferLength is ignored." */ ret = EsSQLGetDescFieldW(desc, NO_REC_NR, SQL_DESC_ARRAY_SIZE, - ValuePtr, BufferLength, NULL); + ValuePtr, SQL_IS_UINTEGER, NULL); if (ret != SQL_SUCCESS) { /* _WITH_INFO wud be "error" here */ /* if SetDescField() fails, DM will check statement's diag */ HDIAG_COPY(desc, stmt); @@ -1340,6 +1360,56 @@ static SQLSMALLINT recount_bound(esodbc_desc_st *desc) return i; } +esodbc_desc_st *getdata_set_ard(esodbc_stmt_st *stmt, esodbc_desc_st *gd_ard, + SQLUSMALLINT colno, esodbc_rec_st *recs, SQLUSMALLINT count) +{ + SQLRETURN ret; + SQLUSMALLINT i; + esodbc_desc_st *ard = stmt->ard; + + init_desc(gd_ard, stmt, DESC_TYPE_ARD, SQL_DESC_ALLOC_USER); + ret = EsSQLSetStmtAttrW(stmt, SQL_ATTR_APP_ROW_DESC, gd_ard, + SQL_IS_POINTER); + if (! SQL_SUCCEEDED(ret)) { + stmt->ard = ard; + return NULL; + } + + if (colno < count) { /* can the static recs be used? */ + /* need to init all records, not only the single one that will be + * bound, since data compat. check will run against all bound recs. */ + for (i = 0; i < count; i ++) { + init_rec(&recs[i], gd_ard); + } + + gd_ard->count = count; + gd_ard->recs = recs; + } + + DBGH(stmt, "GD ARD @0x%p, records allocated %s.", gd_ard, + colno < count ? "statically" : "dynamically"); + return ard; +} + +void getdata_reset_ard(esodbc_stmt_st *stmt, esodbc_desc_st *ard, + SQLUSMALLINT colno, esodbc_rec_st *recs, SQLUSMALLINT count) +{ + SQLRETURN ret; + + if (stmt->ard->recs != recs) { /* the recs are allocated */ + ret = update_rec_count(stmt->ard, 0); /* free all */ + assert(SQL_SUCCEEDED(ret)); + } else { /* recs are on stack */ + /* only the fields of the used rec */ + free_rec_fields(&recs[colno - 1]); + } + + /* re-instate old ARD value */ + stmt->ard = ard; + DBGH(stmt, "ARD reset @0x%p.", stmt->ard); +} + + /* * "Even when freed, an implicitly allocated descriptor remains valid, and @@ -2180,7 +2250,7 @@ SQLRETURN EsSQLSetDescFieldW( return update_rec_count(desc, (SQLSMALLINT)(intptr_t)ValuePtr); case SQL_DESC_ROWS_PROCESSED_PTR: - DBGH(desc, "setting desc rwos processed ptr to: 0x%p.", ValuePtr); + DBGH(desc, "setting desc rows processed ptr to: 0x%p.", ValuePtr); desc->rows_processed_ptr = (SQLULEN *)ValuePtr; return SQL_SUCCESS; } @@ -2329,7 +2399,7 @@ SQLRETURN EsSQLSetDescFieldW( case SQL_DESC_NAME: WARNH(desc, "stored procedure params (to set to `"LWPD"`) not " - "supported.", ValuePtr ? (SQLWCHAR *)ValuePtr : TS_NULL); + "supported.", ValuePtr ? (SQLWCHAR *)ValuePtr : TWS_NULL); RET_HDIAG(desc, SQL_STATE_HYC00, "stored procedure params not supported", 0); diff --git a/driver/handles.h b/driver/handles.h index 7508902f..0355d31a 100644 --- a/driver/handles.h +++ b/driver/handles.h @@ -7,7 +7,8 @@ #ifndef __HANDLES_H__ #define __HANDLES_H__ -#include +#include +#include #include "error.h" #include "defs.h" @@ -265,16 +266,17 @@ typedef struct struct_resultset { char *buff; /* buffer containing the answer to the last request in a STM */ size_t blen; /* length of the answer */ + wstr_st ecurs; /* Elastic's cursor object */ void *state; /* top UJSON decoder state */ - void *rows_iter; /* UJSON array with the result set */ - const wchar_t *ecurs; /* Elastic's cursor object */ - size_t eccnt; /* cursor char count */ + void *rows_iter; /* UJSON iterator with the rows in result set */ + UJObject row_array; /* UJSON object for current row */ size_t nrows; /* (count of) rows in current result set */ size_t vrows; /* (count of) visited rows in current result set */ size_t frows; /* (count of) fetched rows across *entire* result set */ - SQLULEN array_pos; /* position in ARD array to write_in/resume_at */ + /* position in ARD array to write_in/resume_at with cursors */ + SQLULEN array_pos; } resultset_st; /* @@ -323,14 +325,34 @@ typedef struct struct_stmt { CONVERSION_SUPPORTED, CONVERSION_SKIPPED, /* used with driver's meta queries */ } sql2c_conversion; + + /* SQLGetData state members */ + SQLINTEGER gd_col; /* current column to get from, if positive */ + SQLINTEGER gd_ctype; /* current target type */ + SQLLEN gd_offt; /* position in source buffer */ + } esodbc_stmt_st; +/* SQLGetData() state reset */ +#define STMT_GD_RESET(_stmt) \ + do { \ + _stmt->gd_col = -1; \ + _stmt->gd_ctype = 0; \ + _stmt->gd_offt = 0; \ + } while (0) +/* is currently a SQLGetData() call being serviced? */ +#define STMT_GD_CALLING(_stmt) (0 <= _stmt->gd_col) SQLRETURN update_rec_count(esodbc_desc_st *desc, SQLSMALLINT new_count); esodbc_rec_st *get_record(esodbc_desc_st *desc, SQLSMALLINT rec_no, BOOL grow); void dump_record(esodbc_rec_st *rec); +esodbc_desc_st *getdata_set_ard(esodbc_stmt_st *stmt, esodbc_desc_st *gd_ard, + SQLUSMALLINT colno, esodbc_rec_st *recs, SQLUSMALLINT count); +void getdata_reset_ard(esodbc_stmt_st *stmt, esodbc_desc_st *ard, + SQLUSMALLINT colno, esodbc_rec_st *recs, SQLUSMALLINT count); + /* TODO: move to some utils.h */ void concise_to_type_code(SQLSMALLINT concise, SQLSMALLINT *type, SQLSMALLINT *code); diff --git a/driver/info.c b/driver/info.c index aefbcceb..c4e12292 100644 --- a/driver/info.c +++ b/driver/info.c @@ -12,13 +12,7 @@ #include "handles.h" #include "queries.h" #include "connect.h" - -#if ODBCVER == 0x0380 -/* String constant for supported ODBC version */ -#define ESODBC_SQL_SPEC_STRING "03.80" -#else /* ver==3.8 */ -#error "unsupported ODBC version" -#endif /* ver==3.8 */ +#include "info.h" #define ORIG_DISCRIM "IM" #define ORIG_CLASS_ISO "ISO 9075" @@ -167,7 +161,7 @@ SQLRETURN EsSQLGetInfoW(SQLHDBC ConnectionHandle, DBGH(dbc, "requested: GetData extentions."); // FIXME: review@alpha // TODO: GetData review - *(SQLUINTEGER *)InfoValue = 0; + *(SQLUINTEGER *)InfoValue = ESODBC_GETDATA_EXTENSIONS; break; case SQL_DATA_SOURCE_NAME: @@ -223,6 +217,12 @@ SQLRETURN EsSQLGetInfoW(SQLHDBC ConnectionHandle, &MK_WSTR(ESODBC_CATALOG_SEPARATOR), BufferLength, StringLengthPtr); + case SQL_CATALOG_LOCATION: + DBGH(dbc, "requested: catalogue location (`%d`).", + ESODBC_CATALOG_LOCATION); + *(SQLUSMALLINT *)InfoValue = ESODBC_CATALOG_LOCATION; + break; + case SQL_FILE_USAGE: /* JDBC[0]: usesLocalFilePerTable() */ DBGH(dbc, "requested: file usage: table."); @@ -448,6 +448,43 @@ SQLRETURN EsSQLGetInfoW(SQLHDBC ConnectionHandle, return write_wstr(dbc, InfoValue, &MK_WSTR(ESODBC_COLUMN_ALIAS), BufferLength, StringLengthPtr); + case SQL_MAX_COLUMNS_IN_ORDER_BY: + DBGH(dbc, "requested: max columns in ORDER BY (%u).", + ESODBC_MAX_COLUMNS_IN_ORDER_BY); + *(SQLUSMALLINT *)InfoValue = ESODBC_MAX_COLUMNS_IN_ORDER_BY; + break; + + case SQL_MAX_COLUMNS_IN_GROUP_BY: + DBGH(dbc, "requested: max columns in GROUP BY (%u).", + ESODBC_MAX_COLUMNS_IN_GROUP_BY); + *(SQLUSMALLINT *)InfoValue = ESODBC_MAX_COLUMNS_IN_GROUP_BY; + break; + + case SQL_MAX_COLUMNS_IN_SELECT: + DBGH(dbc, "requested: max columns in SELECT (%u).", + ESODBC_MAX_COLUMNS_IN_SELECT); + *(SQLUSMALLINT *)InfoValue = ESODBC_MAX_COLUMNS_IN_SELECT; + break; + + case SQL_ORDER_BY_COLUMNS_IN_SELECT: + DBGH(dbc, "requested: requirement for ORDER BY columns in SELECT " + "(%s).", ESODBC_ORDER_BY_COLUMNS_IN_SELECT); + return write_wstr(dbc, InfoValue, + &MK_WSTR(ESODBC_ORDER_BY_COLUMNS_IN_SELECT), + BufferLength, StringLengthPtr); + + case SQL_GROUP_BY: + DBGH(dbc, "requested: requirement for GROUP BY (%u).", + ESODBC_GROUP_BY); + *(SQLUSMALLINT *)InfoValue = ESODBC_GROUP_BY; + break; + + case SQL_CONCAT_NULL_BEHAVIOR: + DBGH(dbc, "requested: concat NULL behavior (%u).", + ESODBC_CONCAT_NULL_BEHAVIOR); + *(SQLUSMALLINT *)InfoValue = ESODBC_CONCAT_NULL_BEHAVIOR; + break; + case SQL_SQL_CONFORMANCE: DBGH(dbc, "requested: SQL conformance (%lu).", ESODBC_SQL_CONFORMANCE); @@ -488,6 +525,7 @@ SQLRETURN EsSQLGetInfoW(SQLHDBC ConnectionHandle, case SQL_CONVERT_BIGINT: case SQL_CONVERT_BINARY: case SQL_CONVERT_BIT: + case SQL_CONVERT_GUID: case SQL_CONVERT_CHAR: case SQL_CONVERT_DATE: case SQL_CONVERT_DECIMAL: @@ -506,6 +544,9 @@ SQLRETURN EsSQLGetInfoW(SQLHDBC ConnectionHandle, case SQL_CONVERT_TINYINT: case SQL_CONVERT_VARBINARY: case SQL_CONVERT_VARCHAR: + case SQL_CONVERT_WCHAR: + case SQL_CONVERT_WLONGVARCHAR: + case SQL_CONVERT_WVARCHAR: DBGH(dbc, "requested: convert data-type support (0)."); INFOH(dbc, "no CONVERT scalar function support."); *(SQLUINTEGER *)InfoValue = 0; @@ -529,6 +570,25 @@ SQLRETURN EsSQLGetInfoW(SQLHDBC ConnectionHandle, *(SQLUINTEGER *)InfoValue = ESODBC_PARAM_ARRAY_SELECTS; break; + case SQL_DTC_TRANSITION_COST: + INFOH(dbc, "no connection pooling / DTC support."); + DBGH(dbc, "requested: DTC transition cost (%d).", + ESODBC_DTC_TRANSITION_COST); + *(SQLUINTEGER *)InfoValue = ESODBC_DTC_TRANSITION_COST; + break; + +#if (ODBCVER < 0x0400) + /* this is an ODBC 4.0, but Excel seems to asks for it anyways in + * certain cases with a 3.80 driver */ + case 180: /* = SQL_RETURN_ESCAPE_CLAUSE */ +#else + case SQL_RETURN_ESCAPE_CLAUSE: +#endif /* SQL_RETURN_ESCAPE_CLAUSE */ + /* actually an error, but po */ + INFOH(dbc, "SQL_RETURN_ESCAPE_CLAUSE not supported"); + *(SQLUINTEGER *)InfoValue = 0; /* = SQL_RC_NONE */ + break; + default: BUGH(dbc, "unimplemented InfoType: %u.", InfoType); //FIXME ERRH(dbc, "unknown InfoType: %u.", InfoType); diff --git a/driver/info.h b/driver/info.h index 80da76df..e80f614c 100644 --- a/driver/info.h +++ b/driver/info.h @@ -9,6 +9,12 @@ #include "sqlext.h" +#if ODBCVER == 0x0380 +/* String constant for supported ODBC version */ +#define ESODBC_SQL_SPEC_STRING "03.80" +#else /* ver==3.8 */ +#error "unsupported ODBC version" +#endif /* ver==3.8 */ SQLRETURN EsSQLGetInfoW(SQLHDBC ConnectionHandle, SQLUSMALLINT InfoType, diff --git a/driver/log.c b/driver/log.c index 4904ccc0..4dbd4513 100644 --- a/driver/log.c +++ b/driver/log.c @@ -169,7 +169,7 @@ static void log_file_write(char *buff, size_t pos) } else { #ifndef NDEBUG #ifdef _WIN32 - FlushFileBuffers(log_handle); + //FlushFileBuffers(log_handle); #endif /* _WIN32 */ #endif /* NDEBUG */ } diff --git a/driver/log.h b/driver/log.h index 4dba7989..67bde196 100644 --- a/driver/log.h +++ b/driver/log.h @@ -94,7 +94,9 @@ extern int _esodbc_log_level; #define FIXME BUG("not yet implemented") #define TRACE DBG("===== TR4C3 ====="); -#define TS_NULL MK_WPTR("") +/* NULL as ("to Wide") string */ +#define TWS_NULL MK_WPTR("") +#define TS_NULL MK_CPTR("") /* diff --git a/driver/odbc.c b/driver/odbc.c index 94dc461d..a423a813 100644 --- a/driver/odbc.c +++ b/driver/odbc.c @@ -10,6 +10,7 @@ #include "info.h" #include "connect.h" #include "queries.h" +#include "convert.h" #include "catalogue.h" //#include "elasticodbc_export.h" @@ -29,8 +30,8 @@ static BOOL driver_init() { if (log_init()) { - queries_init(); INFO("initializing driver %s.", ESODBC_DRIVER_VER); + convert_init(); return connect_init(); } return FALSE; @@ -821,16 +822,23 @@ SQLRETURN SQL_API SQLFetchScroll(SQLHSTMT StatementHandle, { RET_NOT_IMPLEMENTED; } +#endif /* WITH_EMPTY */ -SQLRETURN SQL_API SQLGetData(SQLHSTMT StatementHandle, +SQLRETURN SQL_API SQLGetData(SQLHSTMT StatementHandle, SQLUSMALLINT ColumnNumber, SQLSMALLINT TargetType, - _Out_writes_opt_(_Inexpressible_(BufferLength)) SQLPOINTER TargetValue, + _Out_writes_opt_(_Inexpressible_(BufferLength)) SQLPOINTER TargetValuePtr, SQLLEN BufferLength, _Out_opt_ SQLLEN *StrLen_or_IndPtr) { - RET_NOT_IMPLEMENTED; + SQLRETURN ret; + TRACE6(_IN, "pHhplp", StatementHandle, ColumnNumber, TargetType, + TargetValuePtr, BufferLength, StrLen_or_IndPtr); + ret = EsSQLGetData(StatementHandle, ColumnNumber, TargetType, + TargetValuePtr, BufferLength, StrLen_or_IndPtr); + TRACE7(_OUT, "dpHhpln", ret, StatementHandle, ColumnNumber, TargetType, + TargetValuePtr, BufferLength, StrLen_or_IndPtr); + return ret; } -#endif /* WITH_EMPTY */ SQLRETURN SQL_API SQLSetPos( SQLHSTMT StatementHandle, @@ -856,13 +864,14 @@ SQLRETURN SQL_API SQLBulkOperations( return ret; } -#if WITH_EMPTY -SQLRETURN SQL_API SQLMoreResults( - SQLHSTMT hstmt) +SQLRETURN SQL_API SQLMoreResults(SQLHSTMT StatementHandle) { - RET_NOT_IMPLEMENTED; + SQLRETURN ret; + TRACE1(_IN, "p", StatementHandle); + ret = EsSQLMoreResults(StatementHandle); + TRACE2(_OUT, "dp", ret, StatementHandle); + return ret; } -#endif /* WITH_EMPTY */ /* TODO: see error.h: esodbc_errors definition note (2.x apps support) */ SQLRETURN SQL_API SQLGetDiagFieldW( diff --git a/driver/queries.c b/driver/queries.c index 8144ec85..2b526c77 100644 --- a/driver/queries.c +++ b/driver/queries.c @@ -4,24 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -#include -#include -#include /* WideCharToMultiByte() */ #include -#include -#include - -#include "ujdecode.h" -#include "timestamp.h" #include "queries.h" #include "log.h" #include "connect.h" #include "info.h" - -#define JSON_VAL_NULL "null" -#define JSON_VAL_TRUE "true" -#define JSON_VAL_FALSE "false" +#include "convert.h" /* key names used in Elastic/SQL REST/JSON answers */ #define JSON_ANSWER_COLUMNS "columns" @@ -37,181 +26,6 @@ #define MSG_INV_SRV_ANS "Invalid server answer" -#define TM_TO_TIMESTAMP_STRUCT(_tmp/*src*/, _tsp/*dst*/) \ - do { \ - (_tsp)->year = (_tmp)->tm_year + 1900; \ - (_tsp)->month = (_tmp)->tm_mon + 1; \ - (_tsp)->day = (_tmp)->tm_mday; \ - (_tsp)->hour = (_tmp)->tm_hour; \ - (_tsp)->minute = (_tmp)->tm_min; \ - (_tsp)->second = (_tmp)->tm_sec; \ - } while (0) - -/* For fixed size (destination) types, the target buffer can't be NULL. */ -#define REJECT_IF_NULL_DEST_BUFF(_s/*tatement*/, _p/*ointer*/) \ - do { \ - if (! _p) { \ - ERRH(_s, "destination buffer can't be NULL."); \ - RET_HDIAGS(stmt, SQL_STATE_HY009); \ - } \ - } while (0) -#define REJECT_AS_OOR(_stmt, _val, _fix_val, _target) /* Out Of Range */ \ - do { \ - if (_fix_val) { \ - ERRH(_stmt, "can't convert value %llx to %s: out of range", \ - _val, STR(_target)); \ - } else { \ - ERRH(_stmt, "can't convert value %f to %s: out of range", \ - _val, STR(_target)); \ - } \ - RET_HDIAGS(_stmt, SQL_STATE_22003); \ - } while (0) - - -#if (0x0300 <= ODBCVER) -# define ESSQL_TYPE_MIN SQL_GUID -# define ESSQL_TYPE_MAX SQL_INTERVAL_MINUTE_TO_SECOND -# define ESSQL_C_TYPE_MIN SQL_C_UTINYINT -# define ESSQL_C_TYPE_MAX SQL_C_INTERVAL_MINUTE_TO_SECOND -#else /* ODBCVER < 0x0300 */ -/* would need to adjust the limits */ -# error "ODBC version not supported; must be 3.0 (0x0300) or higher" -#endif /* 0x0300 <= ODBCVER */ - -#define ESSQL_NORM_RANGE (ESSQL_TYPE_MAX - ESSQL_TYPE_MIN + 1) -#define ESSQL_C_NORM_RANGE (ESSQL_C_TYPE_MAX - ESSQL_C_TYPE_MIN + 1) - -/* conversion matrix SQL indexer */ -#define ESSQL_TYPE_IDX(_t) (_t - ESSQL_TYPE_MIN) -/* conversion matrix C SQL indexer */ -#define ESSQL_C_TYPE_IDX(_t) (_t - ESSQL_C_TYPE_MIN) - -/* sparse SQL-C_SQL types conversion matrix, used for quick compatiblity check - * on columns and parameters binding */ -static BOOL compat_matrix[ESSQL_NORM_RANGE][ESSQL_C_NORM_RANGE] = {FALSE}; - -/* Note: check is array-access unsafe: types IDs must be validated prior to - * checking compatibility (ex. meta type setting) */ -#define ESODBC_TYPES_COMPATIBLE(_sql, _csql) \ - /* if not within the ODBC range, it can only by a binary conversion;.. */ \ - ((ESSQL_TYPE_MAX < _sql && _csql == SQL_C_BINARY) || \ - /* ..otheriwse use the conversion matrix */ \ - compat_matrix[ESSQL_TYPE_IDX(_sql)][ESSQL_C_TYPE_IDX(_csql)]) - -/* populates the compat_matrix as required in: - * https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/converting-data-from-c-to-sql-data-types */ -void queries_init() -{ - SQLSMALLINT i, j, sql, csql; - /*INDENT-OFF*/ - SQLSMALLINT block_idx_sql[] = {SQL_CHAR, SQL_VARCHAR, SQL_LONGVARCHAR, - SQL_WCHAR, SQL_WVARCHAR, SQL_WLONGVARCHAR, SQL_DECIMAL, - SQL_NUMERIC, SQL_BIT, ESODBC_SQL_BOOLEAN, SQL_TINYINT, - SQL_SMALLINT, SQL_INTEGER, SQL_BIGINT, SQL_REAL, SQL_FLOAT, - SQL_DOUBLE - }; - /*INDENT-ON*/ - SQLSMALLINT block_idx_csql[] = {SQL_C_CHAR, SQL_C_WCHAR, - SQL_C_BIT, SQL_C_NUMERIC, SQL_C_STINYINT, SQL_C_UTINYINT, - SQL_C_TINYINT, SQL_C_SBIGINT, SQL_C_UBIGINT, SQL_C_SSHORT, - SQL_C_USHORT, SQL_C_SHORT, SQL_C_SLONG, SQL_C_ULONG, - SQL_C_LONG, SQL_C_FLOAT, SQL_C_DOUBLE, SQL_C_BINARY - }; - SQLSMALLINT to_csql_interval[] = {SQL_CHAR, SQL_VARCHAR, SQL_LONGVARCHAR, - SQL_WCHAR, SQL_WVARCHAR, SQL_WLONGVARCHAR, SQL_DECIMAL, - SQL_NUMERIC, SQL_TINYINT, SQL_SMALLINT, SQL_INTEGER, SQL_BIGINT - }; - SQLSMALLINT from_sql_interval[] = {SQL_C_CHAR, SQL_C_WCHAR, - SQL_C_BIT,SQL_C_NUMERIC, SQL_C_STINYINT, SQL_C_UTINYINT, - SQL_C_TINYINT, SQL_C_SBIGINT, SQL_C_UBIGINT, SQL_C_SSHORT, - SQL_C_USHORT, SQL_C_SHORT, SQL_C_SLONG, SQL_C_ULONG, - SQL_C_LONG - }; - SQLSMALLINT sql_interval[] = {SQL_INTERVAL_MONTH, SQL_INTERVAL_YEAR, - SQL_INTERVAL_YEAR_TO_MONTH, SQL_INTERVAL_DAY, - SQL_INTERVAL_HOUR, SQL_INTERVAL_MINUTE, SQL_INTERVAL_SECOND, - SQL_INTERVAL_DAY_TO_HOUR, SQL_INTERVAL_DAY_TO_MINUTE, - SQL_INTERVAL_DAY_TO_SECOND, SQL_INTERVAL_HOUR_TO_MINUTE, - SQL_INTERVAL_HOUR_TO_SECOND, SQL_INTERVAL_MINUTE_TO_SECOND - }; - SQLSMALLINT csql_interval[] = {SQL_C_INTERVAL_DAY, SQL_C_INTERVAL_HOUR, - SQL_C_INTERVAL_MINUTE, SQL_C_INTERVAL_SECOND, - SQL_C_INTERVAL_DAY_TO_HOUR, SQL_C_INTERVAL_DAY_TO_MINUTE, - SQL_C_INTERVAL_DAY_TO_SECOND, SQL_C_INTERVAL_HOUR_TO_MINUTE, - SQL_C_INTERVAL_HOUR_TO_SECOND, - SQL_C_INTERVAL_MINUTE_TO_SECOND, SQL_C_INTERVAL_MONTH, - SQL_C_INTERVAL_YEAR, SQL_C_INTERVAL_YEAR_TO_MONTH - }; - SQLSMALLINT to_csql_datetime[] = {SQL_CHAR, SQL_VARCHAR, SQL_LONGVARCHAR, - SQL_WCHAR, SQL_WVARCHAR, SQL_WLONGVARCHAR, SQL_TYPE_DATE, - SQL_TYPE_TIME, SQL_TYPE_TIMESTAMP - }; - SQLSMALLINT csql_datetime[] = {SQL_C_TYPE_DATE, SQL_C_TYPE_TIME, - SQL_C_TYPE_TIMESTAMP - }; - - /* fill the compact block of TRUEs (growing from the upper left corner) */ - for (i = 0; i < sizeof(block_idx_sql)/sizeof(*block_idx_sql) ; i ++) { - for (j = 0; j < sizeof(block_idx_csql)/sizeof(*block_idx_csql) ; j ++) { - sql = block_idx_sql[i]; - csql = block_idx_csql[j]; - compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; - } - } - - /* SQL_C_ BINARY, CHAR, WCHAR and DEFAULT are comatible with all SQL types; - * this will set also non-ODBC intersections (but it's convenient) */ - for (sql = 0; sql < ESSQL_NORM_RANGE; sql ++) { - compat_matrix[sql][ESSQL_C_TYPE_IDX(SQL_C_CHAR)] = TRUE; - compat_matrix[sql][ESSQL_C_TYPE_IDX(SQL_C_WCHAR)] = TRUE; - compat_matrix[sql][ESSQL_C_TYPE_IDX(SQL_C_BINARY)] = TRUE; - compat_matrix[sql][ESSQL_C_TYPE_IDX(SQL_C_DEFAULT)] = TRUE; - } - - /* ESODBC_SQL_NULL (NULL) is compabitle with all SQL_C types */ - for (csql = 0; csql < ESSQL_C_NORM_RANGE; csql ++) { - compat_matrix[ESSQL_TYPE_IDX(ESODBC_SQL_NULL)][csql] = TRUE; - } - - /* set conversions to INTERVAL_C */ - for (i = 0; i < sizeof(to_csql_interval)/sizeof(*to_csql_interval); i ++) { - for (j = 0; j < sizeof(csql_interval)/sizeof(*csql_interval); j ++ ) { - sql = to_csql_interval[i]; - csql = csql_interval[j]; - compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; - } - } - - /* set conversions from INTERVAL_SQL */ - for (i = 0; i < sizeof(sql_interval)/sizeof(*sql_interval); i ++) { - for (j = 0; j < sizeof(from_sql_interval)/sizeof(*from_sql_interval); - j ++ ) { - sql = sql_interval[i]; - csql = from_sql_interval[j]; - compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; - } - } - - /* set conversions between date-time types */ - for (i = 0; i < sizeof(to_csql_datetime)/sizeof(*to_csql_datetime); i ++) { - for (j = 0; j < sizeof(csql_datetime)/sizeof(*csql_datetime); j ++ ) { - sql = to_csql_datetime[i]; - csql = csql_datetime[j]; - if (sql == SQL_TYPE_DATE && csql == SQL_C_TYPE_TIME) { - continue; - } - if (sql == SQL_TYPE_TIME && csql == SQL_C_TYPE_DATE) { - continue; - } - compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; - } - } - - /* GUID conversion */ - sql = SQL_GUID; - csql = SQL_C_GUID; - compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; -} void clear_resultset(esodbc_stmt_st *stmt) @@ -225,6 +39,9 @@ void clear_resultset(esodbc_stmt_st *stmt) UJFree(stmt->rset.state); } memset(&stmt->rset, 0, sizeof(stmt->rset)); + + /* reset SQLGetData state to detect sequence "SQLExec*(); SQLGetData();" */ + STMT_GD_RESET(stmt); } /* Set the desriptor fields associated with "size". This step is needed since @@ -367,7 +184,7 @@ static SQLRETURN attach_columns(esodbc_stmt_st *stmt, UJObject columns) recno ++; } - /* new columsn attached, need to check compatiblity */ + /* new columns attached, need to check compatiblity */ stmt->sql2c_conversion = CONVERSION_UNCHECKED; return SQL_SUCCESS; @@ -438,6 +255,7 @@ SQLRETURN TEST_API attach_answer(esodbc_stmt_st *stmt, char *buff, size_t blen) stmt->rset.nrows = 0; #endif /*0*/ } else { + /* the cast is made safe by the decoding format indicator for array */ stmt->rset.nrows = (size_t)UJLengthArray(rows); } DBGH(stmt, "rows received in result set: %zd.", stmt->rset.nrows); @@ -452,20 +270,22 @@ SQLRETURN TEST_API attach_answer(esodbc_stmt_st *stmt, char *buff, size_t blen) if (! stmt->hdr.dbc->fetch.max) { INFOH(stmt, "no fetch size defined, but cursor returned."); } - if (stmt->rset.ecurs) + if (stmt->rset.ecurs.cnt) { DBGH(stmt, "replacing old cursor `" LWPDL "`.", - stmt->rset.eccnt, stmt->rset.ecurs); + LWSTR(&stmt->rset.ecurs)); + } /* store new cursor vals */ - stmt->rset.ecurs = wcurs; - stmt->rset.eccnt = eccnt; + stmt->rset.ecurs = (wstr_st) { + (SQLWCHAR *)wcurs, eccnt + }; DBGH(stmt, "new elastic cursor: `" LWPDL "`[%zd].", - stmt->rset.eccnt, stmt->rset.ecurs, stmt->rset.eccnt); + LWSTR(&stmt->rset.ecurs), stmt->rset.ecurs.cnt); } else { WARNH(stmt, "empty cursor found in the answer."); } } else { /* should have been cleared by now */ - assert(! stmt->rset.eccnt); + assert(! stmt->rset.ecurs.cnt); } /* @@ -686,13 +506,13 @@ SQLRETURN EsSQLBindCol( RET_HDIAGS(STMH(StatementHandle), SQL_STATE_HY090); } - if ((STMH(StatementHandle)->bookmarks != SQL_UB_OFF) || (! ColumnNumber)) { + if (! ColumnNumber) { /* "The statement attribute SQL_ATTR_USE_BOOKMARKS should always be * set before binding a column to column 0. This is not required but * is strongly recommended." */ - //RET_HDIAGS(STMH(StatementHandle), SQL_STATE_IM001); - /* TODO: implement bookmarks? */ - FIXME; + assert(stmt->bookmarks == SQL_UB_OFF); // TODO: bookmarks + ERRH(stmt, "bookmarks use turned off."); + RET_HDIAGS(stmt, SQL_STATE_07009); } ard_prev_count = ard->count; @@ -777,3395 +597,949 @@ SQLRETURN EsSQLBindCol( } /* - * field: SQL_DESC_: DATA_PTR / INDICATOR_PTR / OCTET_LENGTH_PTR - * pos: position in array/row_set (not result_set) + * Copy one row from IRD to ARD. + * pos: row number in the rowset + * Returns: ... */ -static inline void *deferred_address(SQLSMALLINT field_id, size_t pos, - esodbc_rec_st *rec) +SQLRETURN copy_one_row(esodbc_stmt_st *stmt, SQLULEN pos) { - size_t elem_size; - SQLLEN offt; - void *base; - esodbc_desc_st *desc = rec->desc; + SQLSMALLINT i; + SQLLEN rowno; + SQLRETURN ret; + UJObject obj; + void *iter_row; + SQLLEN *ind_len; + long long ll; + double dbl; + const wchar_t *wstr; + BOOL boolval; + size_t len; + BOOL with_info; + esodbc_desc_st *ard, *ird; + esodbc_rec_st *arec, *irec; + + rowno = stmt->rset.frows + pos + /*1-based*/1; + ard = stmt->ard; + ird = stmt->ird; -#define ROW_OFFSETS \ +#define RET_ROW_DIAG(_state, _message, _colno) \ do { \ - elem_size = desc->bind_type; \ - offt = desc->bind_offset_ptr ? *(desc->bind_offset_ptr) : 0; \ + if (ard->array_status_ptr) \ + ard->array_status_ptr[pos] = SQL_ROW_ERROR; \ + return post_row_diagnostic(&stmt->hdr.diag, _state, MK_WPTR(_message),\ + 0, rowno, _colno); \ + } while (0) +#define SET_ROW_DIAG(_rowno, _colno) \ + do { \ + stmt->hdr.diag.row_number = _rowno; \ + stmt->hdr.diag.column_number = _colno; \ } while (0) - switch (field_id) { - case SQL_DESC_DATA_PTR: - base = rec->data_ptr; - if (desc->bind_type == SQL_BIND_BY_COLUMN) { - elem_size = (size_t)rec->octet_length; - offt = 0; - } else { /* by row */ - ROW_OFFSETS; - } - break; - case SQL_DESC_INDICATOR_PTR: - base = rec->indicator_ptr; - if (desc->bind_type == SQL_BIND_BY_COLUMN) { - elem_size = sizeof(*rec->indicator_ptr); - offt = 0; - } else { /* by row */ - ROW_OFFSETS; - } - break; - case SQL_DESC_OCTET_LENGTH_PTR: - base = rec->octet_length_ptr; - if (desc->bind_type == SQL_BIND_BY_COLUMN) { - elem_size = sizeof(*rec->octet_length_ptr); - offt = 0; - } else { /* by row */ - ROW_OFFSETS; - } - break; - default: - BUG("can't calculate the deferred address of field type %d.", - field_id); - return NULL; - } -#undef ROW_OFFSETS - - DBGH(desc->hdr.stmt, "rec@0x%p, field_id:%hd, pos: %zu : base@0x%p, " - "offset=%lld, elem_size=%zu", rec, field_id, pos, base, offt, - elem_size); - - return base ? (char *)base + offt + pos * elem_size : NULL; -} - -/* - * Handles the lengths of the data to copy out to the application: - * (1) returns the max amount of bytes to copy (in the data_ptr), taking into - * account size of data and of buffer, relevant statement attribute and - * buffer type; - * (2) indicates if truncation occured into 'state'. - * WARN: only to be used with ARD.meta_type == STR || BIN (as it can indicate - * a size to copy smaller than the original -- truncating). - */ -static size_t buff_octet_size( - size_t avail, /* how many bytes are there to copy out */ - size_t unit_size, /* the unit size of the buffer (i.e. sizeof(wchar_t)) */ - esodbc_rec_st *arec, esodbc_rec_st *irec, - esodbc_state_et *state /* out param: only written when truncating */) -{ - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; - /* how large (bytes) is the buffer to copy into*/ - size_t room = (size_t)arec->octet_length; - /* statement attribute SQL_ATTR_MAX_LENGTH value */ - size_t attr_max = stmt->max_length; - /* meta type of IRD */ - esodbc_metatype_et ird_mt = irec->meta_type; - size_t max_copy, max; - - /* type is signed, driver should not allow a negative to this point: - * making sure the cast above is sane. */ - assert(0 <= arec->octet_length); - - /* truncate to statment max bytes, only if "the column contains character - * or binary data" */ - max = (ird_mt == METATYPE_STRING || ird_mt == METATYPE_BIN) ? attr_max : 0; - - /* apply "network" truncation first, if need to */ - if (0 < max && max < avail) { - INFO("applying 'network' truncation %zd -> %zd.", avail, max); - max_copy = max; - /* no truncation indicated for this case */ - } else { - max_copy = avail; - } - - /* is target buffer to small? adjust size if so and indicate truncation */ - /* Note: this should only be tested/applied if ARD.meta_type == STR||BIN */ - // FIXME: check note above - if (room < max_copy) { - INFO("applying buffer truncation %zd -> %zd.", max_copy, room); - max_copy = room; - *state = SQL_STATE_01004; - } - - /* adjust to align to target buffer unit */ - if (max_copy % unit_size) { - max_copy -= max_copy % unit_size; - } - - DBG("avail=%zd, room=%zd, attr_max=%zd, metatype:%d => " - "max_copy=%zd, state=%d.", - avail, room, attr_max, ird_mt, max_copy, *state); - return max_copy; -} - -/* - * Indicate the amount of data available to the application, taking into - * account: the type of data, should truncation - due to max length attr - * setting - need to be indicated, since original length is indicated, w/o - * possible buffer truncation, but with possible 'network' truncation. - */ -static inline void write_out_octets( - SQLLEN *octet_len_ptr, /* buffer to write the avail octets into */ - size_t avail, /* amount of bytes avail */ - esodbc_rec_st *irec) -{ - esodbc_stmt_st *stmt = irec->desc->hdr.stmt; - /* statement attribute SQL_ATTR_MAX_LENGTH value */ - size_t attr_max = stmt->max_length; - /* meta type of IRD */ - esodbc_metatype_et ird_mt = irec->meta_type; - size_t max; - - if (! octet_len_ptr) { - DBG("NULL octet len pointer, length (%zd) not indicated.", avail); - return; - } - - /* truncate to statment max bytes, only if "the column contains character - * or binary data" */ - max = (ird_mt == METATYPE_STRING || ird_mt == METATYPE_BIN) ? attr_max : 0; - - if (0 < max) { - /* put the value of SQL_ATTR_MAX_LENGTH attribute.. even - * if this would be larger than what the data actually - * occupies after conversion: "the driver has no way of - * figuring out what the actual length is" */ - *octet_len_ptr = max; - DBG("max length (%zd) attribute enforced.", max); - } else { - /* if no "network" truncation done, indicate data's length, no - * matter if truncated to buffer's size or not */ - *octet_len_ptr = avail; - } - - DBG("length of data available for transfer: %ld", *octet_len_ptr); -} - -/* if an application doesn't specify the conversion, use column's type */ -static inline SQLSMALLINT get_rec_c_type(esodbc_rec_st *arec, - esodbc_rec_st *irec) -{ - SQLSMALLINT ctype; - /* "To use the default mapping, an application specifies the SQL_C_DEFAULT - * type identifier." */ - if (arec->concise_type != SQL_C_DEFAULT) { - ctype = arec->concise_type; - } else { - ctype = irec->es_type->c_concise_type; + /* is current object an array? */ + if (! UJIsArray(stmt->rset.row_array)) { + ERRH(stmt, "one '%s' element (#%zd) in result set not an array; type:" + " %d.", JSON_ANSWER_ROWS, stmt->rset.vrows, + UJGetType(stmt->rset.row_array)); + RET_ROW_DIAG(SQL_STATE_01S01, MSG_INV_SRV_ANS, SQL_NO_COLUMN_NUMBER); + } + /* are there elements in this row array to at least match the number of + * columns? */ + if (UJLengthArray(stmt->rset.row_array) < ird->count) { + ERRH(stmt, "current row counts less elements (%d) than columns (%hd)", + UJLengthArray(stmt->rset.row_array), ird->count); + RET_ROW_DIAG(SQL_STATE_01S01, MSG_INV_SRV_ANS, SQL_NO_COLUMN_NUMBER); + } else if (ird->count < UJLengthArray(stmt->rset.row_array)) { + WARNH(stmt, "current row counts more elements (%d) than columns (%hd)", + UJLengthArray(stmt->rset.row_array), ird->count); + } + /* get an iterator over the row array */ + if (! (iter_row = UJBeginArray(stmt->rset.row_array))) { + ERRH(stmt, "Failed to obtain iterator on row (#%zd): %s.", rowno, + UJGetError(stmt->rset.state)); + RET_ROW_DIAG(SQL_STATE_01S01, MSG_INV_SRV_ANS, SQL_NO_COLUMN_NUMBER); } - DBGH(arec->desc, "target data C type: %hd.", ctype); - return ctype; -} -/* transfer to the application a 0-terminated (but unaccounted for) wstr_st */ -static SQLRETURN transfer_wstr0(esodbc_rec_st *arec, esodbc_rec_st *irec, - wstr_st *src, void *data_ptr, SQLLEN *octet_len_ptr) -{ - size_t in_bytes; - esodbc_state_et state; - SQLWCHAR *dst; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; - - /* the source string must be 0-term'd (since this needs to be transfered - * out to the application) */ - assert(src->str[src->cnt] == 0); - - /* always return the app the untruncated number of bytes */ - write_out_octets(octet_len_ptr, src->cnt * sizeof(*src->str), irec); - - if (data_ptr) { - dst = (SQLWCHAR *)data_ptr; - state = SQL_STATE_00000; - in_bytes = buff_octet_size((src->cnt + 1) * sizeof(*src->str), - sizeof(*src->str), arec, irec, &state); - - if (in_bytes) { - memcpy(dst, src->str, in_bytes); - /* TODO: should the left be filled with spaces? : - * https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/rules-for-conversions */ - - if (state != SQL_STATE_00000) { - /* 0-term the buffer */ - ((SQLWCHAR *)data_ptr)[(in_bytes/sizeof(SQLWCHAR)) - 1] = 0; - DBGH(stmt, "aREC@0x%p: `" LWPDL "` transfered truncated as " - "`%s`.", arec, LWSTR(src), dst); - RET_HDIAGS(stmt, state); - } else { - assert(((SQLWCHAR *)data_ptr)[(in_bytes/sizeof(SQLWCHAR))-1] - == 0); - DBGH(stmt, "aREC@0x%p: `" LWPDL "` transfered @ " - "data_ptr@0x%p.", arec, LWSTR(src), dst); - } + with_info = FALSE; + /* iterate over the bound cols and contents of one (table) row */ + for (i = 0; i < ard->count && UJIterArray(&iter_row, &obj); i ++) { + arec = &ard->recs[i]; /* access safe if 'i < ard->count' */ + /* if record not bound skip it */ + if (! REC_IS_BOUND(arec)) { + DBGH(stmt, "column #%d not bound, skipping it.", i + 1); + continue; } - } else { - DBGH(stmt, "aREC@0x%p: NULL transfer buffer.", arec); - } - - return SQL_SUCCESS; -} - -/* transfer to the application a 0-terminated (but unaccounted for) cstr_st */ -static SQLRETURN transfer_cstr0(esodbc_rec_st *arec, esodbc_rec_st *irec, - cstr_st *src, void *data_ptr, SQLLEN *octet_len_ptr) -{ - size_t in_bytes; - esodbc_state_et state; - SQLCHAR *dst = (SQLCHAR *)data_ptr; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; - /* the source string must be 0-term'd (since this needs to be transfered - * out to the application) */ - assert(src->str[src->cnt] == 0); - - /* always return the app the untruncated number of bytes */ - write_out_octets(octet_len_ptr, src->cnt * sizeof(*src->str), irec); - - if (data_ptr) { - dst = (SQLCHAR *)data_ptr; - state = SQL_STATE_00000; - in_bytes = buff_octet_size((src->cnt + 1) * sizeof(*src->str), - sizeof(*src->str), arec, irec, &state); - - if (in_bytes) { - memcpy(dst, src->str, in_bytes); - /* TODO: should the left be filled with spaces? : - * https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/rules-for-conversions */ - - if (state != SQL_STATE_00000) { - /* 0-term the buffer */ - dst[(in_bytes/sizeof(SQLCHAR)) - 1] = 0; - DBGH(stmt, "aREC@0x%p: `" LCPDL "` transfered truncated as " - "`%s`.", arec, LCSTR(src), dst); - RET_HDIAGS(stmt, state); - } else { - assert(dst[(in_bytes/sizeof(SQLCHAR)) - 1] == 0); - DBGH(stmt, "aREC@0x%p: `" LCPDL "` transfered @ " - "data_ptr@0x%p.", arec, LCSTR(src), dst); - } - } - } else { - DBGH(stmt, "aREC@0x%p: NULL transfer buffer.", arec); - } + /* access made safe by ird->count match against array len above */ + irec = &ird->recs[i]; - return SQL_SUCCESS; -} + switch (UJGetType(obj)) { + default: + ERRH(stmt, "unexpected object of type %d in row L#%zd/T#%zd.", + UJGetType(obj), stmt->rset.vrows, stmt->rset.frows); + RET_ROW_DIAG(SQL_STATE_01S01, MSG_INV_SRV_ANS, i + 1); + /* RET_.. returns */ -/* 10^n */ -static inline unsigned long long pow10(unsigned n) -{ - unsigned long long pow = 1; - pow <<= n; - while (n--) { - pow += pow << 2; - } - return pow; -} + case UJT_Null: + DBGH(stmt, "value [%zd, %d] is NULL.", rowno, i + 1); + /* Note: if ever causing an issue, check + * arec->es_type->nullable before returning NULL to app */ + ind_len = deferred_address(SQL_DESC_INDICATOR_PTR, pos, arec); + if (! ind_len) { + ERRH(stmt, "no buffer to signal NULL value."); + RET_ROW_DIAG(SQL_STATE_22002, "Indicator variable required" + " but not supplied", i + 1); + } + *ind_len = SQL_NULL_DATA; + continue; /* instead of break! no 'ret' processing to do. */ -static SQLRETURN double_to_numeric(esodbc_rec_st *arec, double src, void *dst) -{ - SQL_NUMERIC_STRUCT *numeric; - esodbc_stmt_st *stmt; - SQLSMALLINT prec/*..ision*/; - unsigned long long ullng, pow; - long long llng; + case UJT_String: + wstr = UJReadString(obj, &len); + DBGH(stmt, "value [%zd, %d] is string [%d]:`" LWPDL "`.", + rowno, i + 1, len, len, wstr); + /* UJSON4C returns chars count, but 0-terminates w/o counting + * the terminator */ + assert(wstr[len] == 0); + /* "When character data is returned from the driver to the + * application, the driver must always null-terminate it." */ + ret = sql2c_string(arec, irec, pos, wstr, len + /*\0*/1); + break; - stmt = arec->desc->hdr.stmt; - numeric = (SQL_NUMERIC_STRUCT *)dst; - assert(numeric); + case UJT_Long: + case UJT_LongLong: + ll = UJNumericLongLong(obj); + DBGH(stmt, "value [%zd, %d] is numeric: %lld.", rowno, i + 1, + ll); + ret = sql2c_longlong(arec, irec, pos, ll); + break; - numeric->scale = (SQLCHAR)arec->scale; - numeric->sign = 0 <= src; + case UJT_Double: + dbl = UJNumericFloat(obj); + DBGH(stmt, "value [%zd, %d] is double: %f.", rowno, i + 1, + dbl); + ret = sql2c_double(arec, irec, pos, dbl); + break; - ullng = numeric->sign ? (unsigned long long)src : (unsigned long long)-src; - /* =~ log10(abs(src)) */ - for (prec = 0; ullng; prec ++) { - ullng /= 10; - } - if (arec->scale < 0) { - pow = pow10(-arec->scale); - llng = (long long)(src / pow); - prec += arec->scale; /* prec lowers */ - } else if (0 < arec->scale) { - pow = pow10(arec->scale); - if (DBL_MAX / pow < src) { - // TODO: numeric.val is 16 octets long -> expand - ERRH(stmt, "max numeric conversion scale reached."); - RET_HDIAGS(stmt, SQL_STATE_22003); + case UJT_True: + case UJT_False: + boolval = UJGetType(obj) == UJT_True ? TRUE : FALSE; + DBGH(stmt, "value [%zd, %d] is boolean: %d.", rowno, i + 1, + boolval); + /* "When bit SQL data is converted to character C data, the + * possible values are "0" and "1"." */ + ret = sql2c_longlong(arec, irec, pos, boolval ? 1LL : 0LL); + break; } - llng = (long long)(src * pow); - prec += arec->scale; /* prec grows */ - } else { - llng = (long long)src; - } - ullng = numeric->sign ? (unsigned long long)llng : - (unsigned long long)-llng; - - DBGH(stmt, "arec@0x%p: precision=%hd, scale=%hd; src.precision=%hd", - arec, arec->precision, arec->scale, prec); - if ((UCHAR_MAX < prec) || (0 < arec->precision && arec->precision < prec)) { - /* precision of source is higher than requested => overflow */ - ERRH(stmt, "conversion overflow. source: %.6e; requested: " - "precisions: %d, scale: %d.", src, arec->precision, arec->scale); - RET_HDIAGS(stmt, SQL_STATE_22003); - } else if (prec < 0) { - prec = 0; - assert(ullng == 0); - } - numeric->precision = (SQLCHAR)prec; - - -#if REG_DWORD != REG_DWORD_LITTLE_ENDIAN - ullng = _byteswap_ulong(ullng); -#endif /* LE */ - assert(sizeof(ullng) <= sizeof(numeric->val)); - memcpy(numeric->val, (char *)&ullng, sizeof(ullng)); - memset(numeric->val+sizeof(ullng), 0, sizeof(numeric->val)-sizeof(ullng)); - - DBGH(stmt, "double %.6e converted to numeric: .sign=%d, precision=%d " - "(req: %d), .scale=%d (req: %d), .val:`" LCPDL "` (0x%lx).", src, - numeric->sign, numeric->precision, arec->precision, - numeric->scale, arec->scale, (int)sizeof(numeric->val), numeric->val, - ullng); - return SQL_SUCCESS; -} - -static SQLRETURN numeric_to_double(esodbc_rec_st *irec, void *src, double *dst) -{ - unsigned long long ullng, pow; - double dbl; - SQLSMALLINT prec/*..ision*/; - SQL_NUMERIC_STRUCT *numeric; - esodbc_stmt_st *stmt = irec->desc->hdr.stmt; - - assert(src); - numeric = (SQL_NUMERIC_STRUCT *)src; - - assert(2 * sizeof(ullng) == sizeof(numeric->val)); - ullng = *(unsigned long long *)&numeric->val[sizeof(ullng)]; - if (ullng) { - // TODO: shift down with scale - ERRH(stmt, "max numeric precision scale reached."); - goto erange; - } - ullng = *(unsigned long long *)&numeric->val[0]; -#if REG_DWORD != REG_DWORD_LITTLE_ENDIAN - ullng = _byteswap_ulong(ullng); -#endif /* LE */ - - /* =~ log10(abs(ullng)) */ - for (prec = 0, pow = ullng; pow; prec ++) { - pow /= 10; - } - if (DBL_MAX < ullng) { - goto erange; - } else { - dbl = (double)ullng; - } - - if (numeric->scale < 0) { - pow = pow10(-numeric->scale); - if (DBL_MAX / pow < dbl) { - goto erange; + switch (ret) { + case SQL_SUCCESS_WITH_INFO: + with_info = TRUE; + SET_ROW_DIAG(rowno, i + 1); + case SQL_SUCCESS: + break; + default: /* error */ + SET_ROW_DIAG(rowno, i + 1); + return ret; } - dbl *= pow; - prec -= numeric->scale; /* prec grows */ - } else if (0 < numeric->scale) { - pow = pow10(numeric->scale); - dbl /= pow; - prec -= numeric->scale; /* prec lowers */ } - DBGH(stmt, "irec@0x%p: precision=%hd, scale=%hd; src.precision=%hd", - irec, irec->precision, irec->scale, prec); - if ((UCHAR_MAX < prec) || (0 < irec->precision && irec->precision < prec)) { - ERRH(stmt, "source precision (%hd) larger than requested (%hd)", - prec, irec->precision); - goto erange; - } else { - if (! numeric->sign) { - dbl = -dbl; - } + if (ird->array_status_ptr) { + ird->array_status_ptr[pos] = with_info ? SQL_ROW_SUCCESS_WITH_INFO : + SQL_ROW_SUCCESS; + DBGH(stmt, "status array @0x%p#%d set to %d.", ird->array_status_ptr, + pos, ird->array_status_ptr[pos]); } - DBGH(stmt, "VAL: %f", dbl); - DBGH(stmt, "numeric val: %llu, scale: %hhd, precision: %hhu converted to " - "double %.6e.", ullng, numeric->scale, numeric->precision, dbl); + return with_info ? SQL_SUCCESS_WITH_INFO : SQL_SUCCESS; - *dst = dbl; - return SQL_SUCCESS; -erange: - ERRH(stmt, "can't convert numeric val: %llu, scale: %hhd, precision: %hhu" - " to double.", ullng, numeric->scale, numeric->precision); - RET_HDIAGS(stmt, SQL_STATE_22003); +#undef RET_ROW_DIAG +#undef SET_ROW_DIAG } /* - * https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/transferring-data-in-its-binary-form - */ -static SQLRETURN llong_to_binary(esodbc_rec_st *arec, esodbc_rec_st *irec, - long long src, void *dst, SQLLEN *src_len) -{ - size_t cnt; - char *s = (char *)&src; - esodbc_state_et state = SQL_STATE_00000; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; - unsigned long long ull = src < 0 ? -src : src; - - /* UJ4C uses long long for any integer type -> find out the - * smallest type that would accomodate the value (since fixed negatives - * would take more space then minimally required). */ - if (ull < CHAR_MAX) { - cnt = sizeof(char); - } else if (ull < SHRT_MAX) { - cnt = sizeof(short); - } else if (ull < INT_MAX) { - cnt = sizeof(int); - } else if (ull < LONG_MAX) { - cnt = sizeof(long); - } else { /* definetely ull < LLONG_MAX */ - cnt = sizeof(long long); - } - - cnt = buff_octet_size(cnt, sizeof(*s), arec, irec, &state); - if (state) { /* has it been shrunk? */ - REJECT_AS_OOR(stmt, src, /*fixed?*/TRUE, "[BINARY]<[value]"); - } - - if (dst) { - /* copy bytes as-are: the reverse conversion need to take place on - * "same DBMS and hardare platform". */ - memcpy(dst, s, cnt); - //TODO: should the driver clear all the received buffer?? Cfg option? - //memset((char *)dst + cnt, 0, arec->octet_length - cnt); - } - write_out_octets(src_len, cnt, irec); - DBGH(stmt, "long long value %lld, converted on %zd octets.", src, cnt); - - return SQL_SUCCESS; -} - -static SQLRETURN longlong_to_str(esodbc_rec_st *arec, esodbc_rec_st *irec, - long long ll, void *data_ptr, SQLLEN *octet_len_ptr, BOOL wide) -{ - /* buffer is overprovisioned for !wide, but avoids double declaration */ - SQLCHAR buff[(ESODBC_PRECISION_INT64 + /*0-term*/1 + /*+/-*/1) - * sizeof(SQLWCHAR)]; - size_t cnt; - SQLRETURN ret; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; - - cnt = i64tot((int64_t)ll, buff, wide); - - if (wide) { - wstr_st llwstr = {.str = (SQLWCHAR *)buff, .cnt = cnt}; - ret = transfer_wstr0(arec, irec, &llwstr, data_ptr, octet_len_ptr); - DBGH(stmt, "long long %lld convertible to w-string `" LWPD "` on " - "%zd octets.", ll, (SQLWCHAR *)buff, cnt); - } else { - cstr_st llcstr = {.str = buff, .cnt = cnt}; - ret = transfer_cstr0(arec, irec, &llcstr, data_ptr, octet_len_ptr); - DBGH(stmt, "long long %lld convertible to string `" LCPD "` on " - "%zd octets.", ll, (SQLCHAR *)buff, cnt); - } - - /* need to change the error code from truncation to "out of - * range", since "whole digits" are truncated */ - if (ret == SQL_SUCCESS_WITH_INFO && - HDRH(stmt)->diag.state == SQL_STATE_01004) { - REJECT_AS_OOR(stmt, ll, /*fixed?*/TRUE, "[STRING]<[value]"); - } - return SQL_SUCCESS; -} - -static SQLRETURN copy_longlong(esodbc_rec_st *arec, esodbc_rec_st *irec, - SQLULEN pos, long long ll) -{ - esodbc_stmt_st *stmt; - void *data_ptr; - SQLLEN *octet_len_ptr; - esodbc_desc_st *ard, *ird; - SQLRETURN ret; - - stmt = arec->desc->hdr.stmt; - ird = stmt->ird; + * "SQLFetch and SQLFetchScroll use the rowset size at the time of the call to + * determine how many rows to fetch." + * + * "If SQLFetch or SQLFetchScroll encounters an error while retrieving one row + * of a multirow rowset, or if SQLBulkOperations with an Operation argument of + * SQL_FETCH_BY_BOOKMARK encounters an error while performing a bulk fetch, it + * sets the corresponding value in the row status array to SQL_ROW_ERROR, + * continues fetching rows, and returns SQL_SUCCESS_WITH_INFO." + * + * "SQLFetch can be used only for multirow fetches when called in ODBC 3.x; if + * an ODBC 2.x application calls SQLFetch, it will open only a single-row, + * forward-only cursor." + * + * "The application can change the rowset size and bind new rowset buffers (by + * calling SQLBindCol or specifying a bind offset) even after rows have been + * fetched." + * + * "SQLFetch returns bookmarks if column 0 is bound." Otherwise, "return more + * than one row" (if avail). + * + * "The driver does not return SQLSTATE 01S01 (Error in row) to indicate that + * an error has occurred while rows were fetched by a call to SQLFetch." (same + * for SQLFetchScroll). + * + * "SQL_ROW_NOROW: The rowset overlapped the end of the result set, and no row + * was returned that corresponded to this element of the row status array." + * + * "If the bound address is 0, no data value is returned" (also for row/column + * binding) + * + * "In the IRD, this header field points to a row status array containing + * status values after a call to SQLBulkOperations, SQLFetch, SQLFetchScroll, + * or SQLSetPos." = row status array of IRD (.array_status_ptr); can be NULL. + * + * "The binding offset is always added directly to the values in the + * SQL_DESC_DATA_PTR, SQL_DESC_INDICATOR_PTR, and SQL_DESC_OCTET_LENGTH_PTR + * fields." (.bind_offset.ptr) + * + * "In ARDs, this field specifies the binding orientation when SQLFetchScroll + * or SQLFetch is called on the associated statement handle." (.bind_type) + * + * "In an IRD, this SQLULEN * header field points to a buffer containing the + * number of rows fetched after a call to SQLFetch or SQLFetchScroll, or the + * number of rows affected in a bulk operation performed by a call to + * SQLBulkOperations or SQLSetPos, including error rows." + * (.rows_processed_ptr) + * + * "The variable that the StrLen_or_Ind argument refers to is used for both + * indicator and length information. If a fetch encounters a null value for + * the column, it stores SQL_NULL_DATA in this variable; otherwise, it stores + * the data length in this variable. Passing a null pointer as StrLen_or_Ind + * keeps the fetch operation from returning the data length but makes the + * fetch fail if it encounters a null value and has no way to return + * SQL_NULL_DATA." (.indicator_ptr) + */ +SQLRETURN EsSQLFetch(SQLHSTMT StatementHandle) +{ + esodbc_stmt_st *stmt; + esodbc_desc_st *ard, *ird; + SQLULEN i, j, errors; + SQLRETURN ret; + + stmt = STMH(StatementHandle); ard = stmt->ard; + ird = stmt->ird; - /* pointer where to write how many characters we will/would use */ - octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); - /* pointer to app's buffer */ - data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); + if (! STMT_HAS_RESULTSET(stmt)) { + if (STMT_NODATA_FORCED(stmt)) { + DBGH(stmt, "empty result set flag set - returning no data."); + return SQL_NO_DATA; + } + ERRH(stmt, "no resultset available on statement."); + RET_HDIAGS(stmt, SQL_STATE_HY010); + } - /* Assume a C type behind an SQL C type, but check size representation. - * Note: won't work if _min==0 is a legit limit */ -# define REJECT_IF_OOR(_stmt, _ll, _min, _max, _sqlctype, _ctype) \ - do { \ - assert(sizeof(_sqlctype) == sizeof(_ctype)); \ - if ((_min && _ll < _min) || _max < _ll) { \ - REJECT_AS_OOR(_stmt, _ll, /*fixed int*/TRUE, _ctype); \ - } \ - } while (0) - /* Transfer a long long to an SQL integer type. - * Uses local vars: stmt, data_ptr, irec, octet_len_ptr. */ -# define TRANSFER_LL(_ll, _min, _max, _sqlctype, _ctype) \ - do { \ - REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); \ - REJECT_IF_OOR(stmt, _ll, _min, _max, _sqlctype, _ctype); \ - *(_sqlctype *)data_ptr = (_sqlctype)_ll; \ - write_out_octets(octet_len_ptr, sizeof(_sqlctype), irec); \ - DBGH(stmt, "converted long long %lld to " STR(_sqlctype) " 0x%llx.", \ - _ll, (intptr_t)*(_sqlctype *)data_ptr); \ - } while (0) + /* Check if the data [type] stored in DB is compatiblie with the buffer + * [type] the application provides. This test can only be done at + * fetch-time, since the application can unbind/rebind columns at any time + * (i.e. also in-between consecutive fetches). */ + switch (stmt->sql2c_conversion) { + case CONVERSION_VIOLATION: + ERRH(stmt, "types compibility check had failed already " + "(violation)."); + RET_HDIAGS(stmt, SQL_STATE_07006); + /* RET_ returns */ - switch (get_rec_c_type(arec, irec)) { - case SQL_C_CHAR: - return longlong_to_str(arec, irec, ll, data_ptr, octet_len_ptr, - FALSE); - case SQL_C_WCHAR: - return longlong_to_str(arec, irec, ll, data_ptr, octet_len_ptr, - TRUE); - - case SQL_C_TINYINT: - case SQL_C_STINYINT: - TRANSFER_LL(ll, CHAR_MIN, CHAR_MAX, SQLSCHAR, char); - break; - case SQL_C_UTINYINT: - TRANSFER_LL(ll, 0, UCHAR_MAX, SQLCHAR, unsigned char); - break; - case SQL_C_SHORT: - case SQL_C_SSHORT: - TRANSFER_LL(ll, SHRT_MIN, SHRT_MAX, SQLSMALLINT, short); - break; - case SQL_C_USHORT: - TRANSFER_LL(ll, 0, USHRT_MAX, SQLUSMALLINT, unsigned short); - break; - case SQL_C_LONG: - case SQL_C_SLONG: - TRANSFER_LL(ll, LONG_MIN, LONG_MAX, SQLINTEGER, long); - break; - case SQL_C_ULONG: - TRANSFER_LL(ll, 0, ULONG_MAX, SQLUINTEGER, unsigned long); - break; - case SQL_C_SBIGINT: - TRANSFER_LL(ll, LLONG_MIN, LLONG_MAX, SQLBIGINT, long long); - break; - case SQL_C_UBIGINT: - TRANSFER_LL(ll, 0, ULLONG_MAX, SQLUBIGINT, unsigned long long); - break; + case CONVERSION_UNSUPPORTED: + ERRH(stmt, "types compibility check had failed already " + "(unsupported)."); + RET_HDIAG(stmt, SQL_STATE_HYC00, "Conversion target type not" + " supported", 0); + /* RET_ returns */ - case SQL_C_BIT: - REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); - if (ll < 0 || 2 <= ll) { - REJECT_AS_OOR(stmt, ll, /*fixed int*/TRUE, SQL_C_BIT); - } else { /* 0 or 1 */ - *(SQLCHAR *)data_ptr = (SQLCHAR)ll; - } - write_out_octets(octet_len_ptr, sizeof(SQLSCHAR), irec); + case CONVERSION_SKIPPED: + DBGH(stmt, "types compatibility skipped."); + /* check unnecessary (SYS TYPES, possiblity other metas) */ break; - case SQL_C_NUMERIC: - REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); - ret = double_to_numeric(arec, (double)ll, data_ptr); + case CONVERSION_UNCHECKED: + ret = convertability_check(stmt, /*col bind check*/-1, + (int *)&stmt->sql2c_conversion); if (! SQL_SUCCEEDED(ret)) { return ret; } - write_out_octets(octet_len_ptr, sizeof(SQL_NUMERIC_STRUCT), irec); - break; - - case SQL_C_FLOAT: - REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); - REJECT_IF_OOR(stmt, ll, -FLT_MAX, FLT_MAX, SQLREAL, float); - *(SQLREAL *)data_ptr = (SQLREAL)ll; - write_out_octets(octet_len_ptr, sizeof(SQLREAL), irec); - break; - - case SQL_C_DOUBLE: - REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); - REJECT_IF_OOR(stmt, ll, -DBL_MAX, DBL_MAX, SQLDOUBLE, double); - *(SQLDOUBLE *)data_ptr = (SQLDOUBLE)ll; - write_out_octets(octet_len_ptr, sizeof(SQLDOUBLE), irec); - break; - - case SQL_C_BINARY: - return llong_to_binary(arec, irec, ll, data_ptr, octet_len_ptr); + /* no break; */ default: - BUGH(stmt, "unexpected unhanlded data type: %d.", - get_rec_c_type(arec, irec)); - return SQL_ERROR; + DBGH(stmt, "ES/app data/buffer types found compatible."); } - DBGH(stmt, "REC@0x%p, data_ptr@0x%p, copied long long: %lld.", arec, - data_ptr, ll); - - return SQL_SUCCESS; -# undef REJECT_IF_OOR -# undef TRANSFER_LL -} + DBGH(stmt, "(`" LCPDL "`); cursor @ %zd / %zd.", LCSTR(&stmt->u8sql), + stmt->rset.vrows, stmt->rset.nrows); -static SQLRETURN double_to_bit(esodbc_rec_st *arec, esodbc_rec_st *irec, - double src, void *data_ptr, SQLLEN *octet_len_ptr) -{ - esodbc_state_et state = SQL_STATE_00000; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + /* reset SQLGetData state, to reset fetch position */ + STMT_GD_RESET(stmt); - REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); + DBGH(stmt, "rowset max size: %d.", ard->array_size); + errors = 0; + /* for all rows in rowset/array, iterate over rows in current resultset */ + for (i = stmt->rset.array_pos; i < ard->array_size; i ++) { + if (! UJIterArray(&stmt->rset.rows_iter, &stmt->rset.row_array)) { + DBGH(stmt, "ran out of rows in current result set: nrows=%zd, " + "vrows=%zd.", stmt->rset.nrows, stmt->rset.vrows); + if (stmt->rset.ecurs.cnt) { /* is there an Elastic cursor? */ + stmt->rset.array_pos = i; + ret = EsSQLExecute(stmt); + if (! SQL_SUCCEEDED(ret)) { + ERRH(stmt, "failed to fetch next results."); + return ret; + } + return EsSQLFetch(StatementHandle); + } else { + DBGH(stmt, "reached end of entire result set. fetched=%zd.", + stmt->rset.frows); + /* indicate the non-processed rows in rowset */ + if (ard->array_status_ptr) + for (j = i; j < ard->array_size; j ++) { + ard->array_status_ptr[j] = SQL_ROW_NOROW; + } + } + break; + } + ret = copy_one_row(stmt, i); + if (! SQL_SUCCEEDED(ret)) { + ERRH(stmt, "copying row %zd failed.", stmt->rset.vrows + i + 1); + errors ++; + } + } + stmt->rset.array_pos = 0; - write_out_octets(octet_len_ptr, sizeof(SQLCHAR), irec); + /* account for processed rows */ + stmt->rset.vrows += i; + stmt->rset.frows += i; - if (src < 0. || 2. <= src) { - REJECT_AS_OOR(stmt, src, /*fixed?*/FALSE, SQL_C_BIT); - } else if (0. < src && src < 1.) { - *(SQLCHAR *)data_ptr = 0; - state = SQL_STATE_01S07; - } else if (1. < src && src < 2.) { - *(SQLCHAR *)data_ptr = 1; - state = SQL_STATE_01S07; - } else { /* 0 or 1 */ - *(SQLCHAR *)data_ptr = (SQLCHAR)src; + /* return number of processed rows (even if 0) */ + if (ird->rows_processed_ptr) { + DBGH(stmt, "setting number of processed rows to: %u.", i); + *ird->rows_processed_ptr = i; } - if (state != SQL_STATE_00000) { - INFOH(stmt, "truncating when converting %f as %d.", src, - *(SQLCHAR *)data_ptr); - RET_HDIAGS(stmt, state); + + /* no data has been copied out */ + if (i <= 0) { + /* ard->array_size is in interval [1, ESODBC_MAX_ROW_ARRAY_SIZE] */ + DBGH(stmt, "no data %sto return.", stmt->rset.vrows ? "left ": ""); + return SQL_NO_DATA; } - DBGH(stmt, "double %f converted to bit %d.", src, *(SQLCHAR *)data_ptr); + if (errors && i <= errors) { + ERRH(stmt, "processing failed for all rows [%llu].", errors); + return SQL_ERROR; + } + /* only failures need stmt.diag defer'ing */ return SQL_SUCCESS; } -static SQLRETURN double_to_binary(esodbc_rec_st *arec, esodbc_rec_st *irec, - double dbl, void *data_ptr, SQLLEN *octet_len_ptr) -{ - size_t cnt; - double udbl = dbl < 0. ? -dbl : dbl; - float flt; - char *ptr; - esodbc_state_et state = SQL_STATE_00000; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; - if (udbl < FLT_MIN || FLT_MAX < udbl) { - /* value's precision/scale requires a double */ - cnt = sizeof(dbl); - ptr = (char *)&dbl; - } else { - flt = (float)dbl; - cnt = sizeof(flt); - ptr = (char *)&flt; +/* + * data availability, call sanity checks and init'ing + */ +static SQLRETURN gd_checks(esodbc_stmt_st *stmt, SQLUSMALLINT colno) +{ + /* is there a result set? */ + if (! STMT_HAS_RESULTSET(stmt)) { + if (STMT_NODATA_FORCED(stmt)) { + DBGH(stmt, "empty result flag set - returning no data."); + return SQL_NO_DATA; + } + ERRH(stmt, "no resultset available on statement."); + RET_HDIAGS(stmt, SQL_STATE_HY010); } - - cnt = buff_octet_size(cnt, sizeof(*ptr), arec, irec, &state); - if (state) { - REJECT_AS_OOR(stmt, dbl, /*fixed?*/FALSE, "[BINARY]<[floating]"); + /* is there a block cursor bound? */ + if (1 < stmt->ard->array_size) { + ERRH(stmt, "can't use function with block cursor " + "(array_size=%llu).", stmt->ard->array_size); + RET_HDIAGS(stmt, SQL_STATE_HYC00); } - write_out_octets(octet_len_ptr, cnt, irec); - if (data_ptr) { - memcpy(data_ptr, ptr, cnt); - //TODO: should the driver clear all the received buffer?? Cfg option? - //memset((char *)data_ptr + cnt, 0, arec->octet_length - cnt); + /* has SQLFetch() been called? rset is reset with every new result */ + if (! stmt->rset.row_array) { + /* DM should have detected this case */ + ERRH(stmt, "SQLFetch() hasn't yet been called on result set."); + RET_HDIAGS(stmt, SQL_STATE_24000); } - DBGH(stmt, "converted double %f to binary on %zd octets.", dbl, cnt); - return SQL_SUCCESS; } -/* - * TODO!!! - * 1. use default precision - * 2. config for scientific notation. - * 3. sprintf (for now) - */ -static SQLRETURN double_to_str(esodbc_rec_st *arec, esodbc_rec_st *irec, - double dbl, void *data_ptr, SQLLEN *octet_len_ptr, BOOL wide) +static SQLRETURN gd_bind_col( + esodbc_stmt_st *stmt, + esodbc_desc_st *ard, + SQLUSMALLINT colno, + SQLSMALLINT target_type, + SQLPOINTER buff_ptr, + SQLLEN buff_len, + SQLLEN *len_ind) { - long long whole; - unsigned long long fraction; - double rest; - SQLSMALLINT scale; - size_t pos, octets; - /* buffer is overprovisioned for !wide, but avoids double declaration */ - SQLCHAR buff[(2 * ESODBC_PRECISION_INT64 + /*.*/1 + /*\0*/1) - * sizeof(SQLWCHAR)]; - /* buffer unit size */ - size_t usize = wide ? sizeof(SQLWCHAR) : sizeof(SQLCHAR); - esodbc_state_et state = SQL_STATE_00000; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; - - /* - * split the whole and fractional parts - */ - assert(sizeof(dbl) == sizeof(whole)); /* [double]==[long long] */ - whole = (long long)dbl; - rest = dbl - whole; - - /* retain user defined or data source default number of fraction digits */ - scale = 0 < arec->scale ? arec->scale : irec->es_type->maximum_scale; - rest *= pow10(scale); - rest = round(rest); - fraction = rest < 0 ? (unsigned long long) -rest - : (unsigned long long)rest; - - /* copy integer part into work buffer */ - pos = i64tot((int64_t)whole, buff, wide); - /* would writing just the whole part + \0 fit into the buffer? */ - octets = buff_octet_size((pos + 1) * usize, usize, arec, irec, &state); - if (state) { - REJECT_AS_OOR(stmt, dbl, /*fixed?*/FALSE, "[STRING]<[floating.whole]"); - } else { - assert(octets == (pos + 1) * usize); + SQLRETURN ret; + SQLSMALLINT ctype; + esodbc_rec_st *arec, *gd_arec; + + switch (target_type) { + case SQL_ARD_TYPE: + if (! (arec = get_record(ard, colno, /*grow?*/FALSE))) { + ERRH(stmt, "no bound column #%hu to copy its concise type.", + colno); + RET_HDIAGS(stmt, SQL_STATE_07009); + } + ctype = arec->concise_type; + case SQL_APD_TYPE: + ERRH(stmt, "procedure parameters not unsupported."); + RET_HDIAGS(stmt, SQL_STATE_HYC00); + default: + arec = NULL; + ctype = target_type; } - if (wide) { - ((SQLWCHAR *)buff)[pos ++] = L'.'; - } else { - ((SQLCHAR *)buff)[pos ++] = '.'; + /* bind the column */ + ret = EsSQLBindCol(stmt, colno, ctype, buff_ptr, buff_len, len_ind); + if (! SQL_SUCCEEDED(ret)) { + return ret; } - /* copy fractional part into work buffer */ - pos += ui64tot((uint64_t)fraction, (char *)buff + pos * usize, wide); - - /* write how many bytes (w/o \0) we'd write if buffer is large enough */ - write_out_octets(octet_len_ptr, pos * usize, irec); - /* compute how many bytes we can actually transfer, including \0 */ - octets = buff_octet_size((pos + 1) * usize, usize, arec, irec, &state); - - if (data_ptr) { - /* transfer the bytes out */ - memcpy(data_ptr, buff, octets); - if (state) { - /* usize < octets, since user input is checked above for OOR */ - if (wide) { - ((SQLWCHAR *)data_ptr)[octets/usize - 1] = L'\0'; - } else { - ((SQLCHAR *)data_ptr)[octets/usize - 1] = '\0'; - } + /* If target is SQL_ARD_TYPE, use "SQL_DESC_DATETIME_INTERVAL_PRECISION, + * SQL_DESC_PRECISION, and SQL_DESC_SCALE fields of the ARD" */ + if (arec) { + if (! (gd_arec = get_record(stmt->ard, colno, FALSE))) { + BUGH(stmt, "can't fetch just set record."); + RET_HDIAG(stmt, SQL_STATE_HY000, "BUG: failed to prepare " + "SQLGetData call", 0); } + gd_arec->datetime_interval_precision = + arec->datetime_interval_precision; + gd_arec->precision = arec->precision; + gd_arec->scale = arec->scale; } - if (wide) { - DBGH(stmt, "double %.6e converted to w-string `" LWPD "` on %zd " - "octets (state: %d; scale: %d).", dbl, buff, octets, state, scale); - } else { - DBGH(stmt, "double %.6e converted to string `" LCPD "` on %zd " - "octets (state: %d; scale: %d).", dbl, buff, octets, state, scale); - } - - if (state) { - RET_HDIAGS(stmt, state); - } else { - return SQL_SUCCESS; - } + return SQL_SUCCESS; } -static SQLRETURN copy_double(esodbc_rec_st *arec, esodbc_rec_st *irec, - SQLULEN pos, double dbl) +SQLRETURN EsSQLGetData( + SQLHSTMT StatementHandle, + SQLUSMALLINT ColumnNumber, + SQLSMALLINT TargetType, + _Out_writes_opt_(_Inexpressible_(BufferLength)) SQLPOINTER TargetValuePtr, + SQLLEN BufferLength, + _Out_opt_ SQLLEN *StrLen_or_IndPtr) { - esodbc_stmt_st *stmt; - void *data_ptr; - SQLLEN *octet_len_ptr; - esodbc_desc_st *ard, *ird; SQLRETURN ret; - double udbl; - - stmt = arec->desc->hdr.stmt; - ird = stmt->ird; - ard = stmt->ard; - - /* pointer where to write how many characters we will/would use */ - octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); - /* pointer to app's buffer */ - data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); - - /* Transfer a double to an SQL integer type. - * Uses local vars: stmt, data_ptr, irec, octet_len_ptr. - * Returns - RET_ - 01S07 on success (due to truncation of fractionals). */ -# define RET_TRANSFER_DBL(_dbl, _min, _max, _sqlctype, _ctype) \ - do { \ - /* using C type limits, so check C and SQL C type precision */ \ - assert(sizeof(_sqlctype) == sizeof(_ctype)); \ - if (_dbl) { \ - if ((_sqlctype)_dbl < _min || _max < (_sqlctype)_dbl) { \ - REJECT_AS_OOR(stmt, _dbl, /*fixed?*/FALSE, _sqlctype); \ - } \ - } else { \ - double __udbl = dbl < 0 ? -dbl : dbl; \ - if (_max < (_sqlctype)__udbl) { \ - REJECT_AS_OOR(stmt, _dbl, /*fixed?*/FALSE, _sqlctype); \ - } \ - } \ - *(_sqlctype *)data_ptr = (_sqlctype)_dbl; \ - write_out_octets(octet_len_ptr, sizeof(_sqlctype), irec); \ - DBGH(stmt, "converted double %f to " STR(_sqlctype) " 0x%llx.", _dbl, \ - (intptr_t)*(_sqlctype *)data_ptr); \ - RET_HDIAGS(stmt, SQL_STATE_01S07); \ - } while (0) - - switch (get_rec_c_type(arec, irec)) { - case SQL_C_CHAR: - return double_to_str(arec, irec, dbl, data_ptr, octet_len_ptr, - FALSE); - case SQL_C_WCHAR: - return double_to_str(arec, irec, dbl, data_ptr, octet_len_ptr, - TRUE); - - case SQL_C_TINYINT: - case SQL_C_STINYINT: - RET_TRANSFER_DBL(dbl, CHAR_MIN, CHAR_MAX, SQLSCHAR, char); - case SQL_C_UTINYINT: - RET_TRANSFER_DBL(dbl, 0, UCHAR_MAX, SQLCHAR, unsigned char); - case SQL_C_SBIGINT: - RET_TRANSFER_DBL(dbl, LLONG_MIN, LLONG_MAX, SQLBIGINT, long long); - case SQL_C_UBIGINT: - RET_TRANSFER_DBL(dbl, 0, LLONG_MAX, SQLUBIGINT, long long); - case SQL_C_SHORT: - case SQL_C_SSHORT: - RET_TRANSFER_DBL(dbl, SHRT_MIN, SHRT_MAX, SQLSMALLINT, short); - case SQL_C_USHORT: - RET_TRANSFER_DBL(dbl, 0, USHRT_MAX, SQLUSMALLINT, unsigned short); - case SQL_C_LONG: - case SQL_C_SLONG: - RET_TRANSFER_DBL(dbl, LONG_MIN, LONG_MAX, SQLINTEGER, long); - case SQL_C_ULONG: - RET_TRANSFER_DBL(dbl, 0, ULONG_MAX, SQLINTEGER, unsigned long); - - case SQL_C_NUMERIC: - REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); - ret = double_to_numeric(arec, dbl, data_ptr); - if (! SQL_SUCCEEDED(ret)) { - return ret; - } - write_out_octets(octet_len_ptr, sizeof(SQL_NUMERIC_STRUCT), irec); - break; + esodbc_desc_st *ard, gd_ard; + esodbc_rec_st arecs[ESODBC_GD_DESC_COUNT]; + esodbc_stmt_st *stmt = STMH(StatementHandle); - case SQL_C_FLOAT: - REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); - udbl = dbl < 0 ? -dbl : dbl; - if (udbl < FLT_MIN || FLT_MAX < udbl) { - REJECT_AS_OOR(stmt, dbl, /* is fixed */FALSE, SQLREAL); - } - *(SQLREAL *)data_ptr = (SQLREAL)dbl; - write_out_octets(octet_len_ptr, sizeof(SQLREAL), irec); - break; - case SQL_C_DOUBLE: - REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); - *(SQLDOUBLE *)data_ptr = dbl; - write_out_octets(octet_len_ptr, sizeof(SQLDOUBLE), irec); - break; + if (stmt->gd_col == ColumnNumber && stmt->gd_ctype == TargetType) { + DBGH(stmt, "resuming get on column #%hu (pos @ %lld).", + stmt->gd_col, stmt->gd_offt); + if (stmt->gd_offt < 0) { + WARNH(stmt, "data for current column exhausted."); + return SQL_NO_DATA; + } + } else { + if (0 <= stmt->gd_col) { + INFOH(stmt, "previous source column #%hu (pos @ %lld), SQL C %hd " + "abandoned for new #%hu, SQL C %hd.", stmt->gd_col, + stmt->gd_offt, stmt->gd_ctype, ColumnNumber, TargetType); + /* reset fields now, should the call eventually fail */ + STMT_GD_RESET(stmt); + } else { + DBGH(stmt, "prep. column #%hu as the new data src.", ColumnNumber); + } + ret = gd_checks(stmt, ColumnNumber); + if (! SQL_SUCCEEDED(ret)) { + return ret; + } - case SQL_C_BIT: - return double_to_bit(arec, irec, dbl, data_ptr, octet_len_ptr); + stmt->gd_col = ColumnNumber; + stmt->gd_ctype = TargetType; + } + /* data is available */ - case SQL_C_BINARY: - return double_to_binary(arec, irec, dbl, data_ptr, octet_len_ptr); + /* save stmt current ARD before overwriting it */ + ard = getdata_set_ard(stmt, &gd_ard, ColumnNumber, arecs, + sizeof(arecs)/sizeof(arecs[0])); + if (! ard) { + BUGH(stmt, "failed to prepare GD ARD."); + RET_HDIAG(stmt, SQL_STATE_HY000, + "BUG: failed to prepare SQLGetData call", 0); + } - default: - BUGH(stmt, "unexpected unhanlded data type: %d.", - get_rec_c_type(arec, irec)); - return SQL_ERROR; + ret = gd_bind_col(stmt, ard, ColumnNumber, TargetType, TargetValuePtr, + BufferLength, StrLen_or_IndPtr); + if (! SQL_SUCCEEDED(ret)) { + goto end; } - DBGH(stmt, "REC@0x%p, data_ptr@0x%p, copied double: %.6e.", arec, - data_ptr, dbl); + /* check if data types are compatible/convertible */ + ret = convertability_check(stmt, /*col bind check*/-1, NULL); + if (! SQL_SUCCEEDED(ret)) { + goto end; + } - return SQL_SUCCESS; + /* copy the data */ + ret = copy_one_row(stmt, 0); + if (! SQL_SUCCEEDED(ret)) { + goto end; + } -# undef RET_TRANSFER_DBL -} - -/* https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/sql-to-c-bit */ -static SQLRETURN copy_boolean(esodbc_rec_st *arec, esodbc_rec_st *irec, - SQLULEN pos, BOOL boolval) -{ - esodbc_stmt_st *stmt; - void *data_ptr; - SQLLEN *octet_len_ptr; - wstr_st wbool; - cstr_st cbool; - - stmt = arec->desc->hdr.stmt; - - /* pointer where to write how many bytes we will/would use */ - octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); - /* pointer to app's buffer */ - data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); - - switch (get_rec_c_type(arec, irec)) { - case SQL_C_WCHAR: - if (arec->octet_length < 1) { /* can't inquiry needed buffer len */ - REJECT_AS_OOR(stmt, boolval, /*fixed int*/TRUE, NULL WCHAR); - } - /* "When bit SQL data is converted to character C data, the - * possible values are "0" and "1"." */ - wbool = boolval ? MK_WSTR("1") : MK_WSTR("0"); - return transfer_wstr0(arec, irec, &wbool, data_ptr, octet_len_ptr); - case SQL_C_CHAR: - if (arec->octet_length < 1) { /* can't inquiry needed buffer len */ - REJECT_AS_OOR(stmt, boolval, /*fixed int*/TRUE, NULL CHAR); - } - cbool = boolval ? MK_CSTR("1") : MK_CSTR("0"); - return transfer_cstr0(arec, irec, &cbool, data_ptr, octet_len_ptr); - default: - return copy_longlong(arec, irec, pos, boolval ? 1LL : 0LL); - } - - DBGH(stmt, "REC@0x%p, data_ptr@0x%p, copied boolean: `%d`.", arec, - data_ptr, boolval); - return SQL_SUCCESS; + DBGH(stmt, "succesfully copied data from column #%hu (pos @ %lld), " + "SQL C %hd.", ColumnNumber, stmt->gd_offt, TargetType); +end: + /* XXX: if get_record(gd_ard, ColumnNumber)->meta_type != string/bin, + * should stmt->gd_offt bet set to -1 ?? */ + /* reinstate previous ARD */ + getdata_reset_ard(stmt, ard, ColumnNumber, arecs, + sizeof(arecs)/sizeof(arecs[0])); + if (! SQL_SUCCEEDED(ret)) { + /* if function call failed, reset GD state */ + STMT_GD_RESET(stmt); + } + return ret; } /* - * -> SQL_C_CHAR - * Note: chars_0 param accounts for 0-term, but length indicated back to the - * application must not. + * "SQLSetPos uses the rowset size that is in effect as of the preceding call + * to SQLFetch or SQLFetchScroll, because SQLSetPos operates on a rowset that + * has already been set. SQLSetPos also will pick up the new rowset size if + * SQLBulkOperations has been called after the rowset size was changed." + * + * "When a block cursor first returns a rowset, the current row is the first + * row of the rowset. To change the current row, the application calls + * SQLSetPos or SQLBulkOperations (to update by bookmark)." + * + * "The driver returns SQLSTATE 01S01 (Error in row) only to indicate that an + * error has occurred while rows were fetched by a call to SQLSetPos to + * perform a bulk operation when the function is called in state S7." (not + * supported currently, with RO operation) + * + * "In the IRD, this header field points to a row status array containing + * status values after a call to SQLBulkOperations, SQLFetch, SQLFetchScroll, + * or SQLSetPos." = row status array of IRD (.array_status_ptr) + * + * "In the ARD, this header field points to a row operation array of values + * that can be set by the application to indicate whether this row is to be + * ignored for SQLSetPos operations." .array_status_ptr + * "If the value in the SQL_DESC_ARRAY_STATUS_PTR field of the ARD is a null + * pointer, all rows are included in the bulk operation" */ -static SQLRETURN wstr_to_cstr(esodbc_rec_st *arec, esodbc_rec_st *irec, - void *data_ptr, SQLLEN *octet_len_ptr, - const wchar_t *wstr, size_t chars_0) +SQLRETURN EsSQLSetPos( + SQLHSTMT StatementHandle, + SQLSETPOSIROW RowNumber, + SQLUSMALLINT Operation, + SQLUSMALLINT LockType) { - esodbc_state_et state = SQL_STATE_00000; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; - char *charp; - int in_bytes, out_bytes, c; - - if (data_ptr) { - charp = (char *)data_ptr; - - in_bytes = (int)buff_octet_size(chars_0 * sizeof(*wstr), - sizeof(*charp), arec, irec, &state); - /* trim the original string until it fits in output buffer, with given - * length limitation */ - for (c = (int)chars_0; 0 < c; c --) { - out_bytes = WCS2U8(wstr, c, charp, in_bytes); - if (out_bytes <= 0) { - if (WCS2U8_BUFF_INSUFFICIENT) { - continue; - } - ERRNH(stmt, "failed to convert wchar* to char* for string `" - LWPDL "`.", chars_0, wstr); - RET_HDIAGS(stmt, SQL_STATE_22018); - } else { - /* conversion succeeded */ - break; - } - } - - /* if 0's present => 0 < out_bytes */ - assert(wstr[chars_0 - 1] == L'\0'); - assert(0 < out_bytes); - /* is user gives 0 as buffer size, out_bytes will also be 0 */ - if (charp[out_bytes - 1]) { - /* ran out of buffer => not 0-terminated and truncated already */ - charp[out_bytes - 1] = 0; - state = SQL_STATE_01004; /* indicate truncation */ - } - - DBGH(stmt, "REC@0x%p, data_ptr@0x%p, copied %zd bytes: `" LWPD "`.", - arec, data_ptr, out_bytes, charp); - } else { - DBGH(stmt, "REC@0x%p, NULL data_ptr.", arec); - } - - /* if length needs to be given, calculate it (not truncated) & converted */ - if (octet_len_ptr) { - out_bytes = (size_t)WCS2U8(wstr, (int)chars_0, NULL, 0); - if (out_bytes <= 0) { - ERRNH(stmt, "failed to convert wchar* to char* for string `" - LWPDL "`.", chars_0, wstr); - RET_HDIAGS(stmt, SQL_STATE_22018); - } else { - /* chars_0 accounts for 0-terminator, so WCS2U8 will count that in - * the output as well => trim it, since we must not count it when - * indicating the length to the application */ - out_bytes --; - } - write_out_octets(octet_len_ptr, out_bytes, irec); - } else { - DBGH(stmt, "REC@0x%p, NULL octet_len_ptr.", arec); - } + switch(Operation) { + case SQL_POSITION: + // FIXME + // check ESODBC_GETDATA_EXTENSIONS (GD_BLOCK) when implementing + FIXME; + break; - if (state != SQL_STATE_00000) { - RET_HDIAGS(stmt, state); + case SQL_REFRESH: + case SQL_UPDATE: + case SQL_DELETE: + ERRH(StatementHandle, "operation %d not supported.", Operation); + RET_HDIAGS(STMH(StatementHandle), SQL_STATE_HYC00); + default: + ERRH(StatementHandle, "unknown operation type: %d.", Operation); + RET_HDIAGS(STMH(StatementHandle), SQL_STATE_HY092); } return SQL_SUCCESS; } /* - * -> SQL_C_WCHAR - * Note: chars_0 accounts for 0-term, but length indicated back to the - * application must not. + * == JDBC's Jdbc/PreparedStatement.executeLargeUpdate() + * "SQLBulkOperations uses the rowset size in effect at the time of the call, + * because it performs operations on a table independent of any fetched + * rowset." + * "In the IRD, this header field points to a row status array containing + * status values after a call to SQLBulkOperations, SQLFetch, SQLFetchScroll, + * or SQLSetPos." = row status array of IRD (.array_status_ptr) */ -static SQLRETURN wstr_to_wstr(esodbc_rec_st *arec, esodbc_rec_st *irec, - void *data_ptr, SQLLEN *octet_len_ptr, - const wchar_t *wstr, size_t chars_0) +SQLRETURN EsSQLBulkOperations( + SQLHSTMT StatementHandle, + SQLSMALLINT Operation) { - wstr_st wsrc = {(SQLWCHAR *)wstr, chars_0 - 1}; - return transfer_wstr0(arec, irec, &wsrc, data_ptr, octet_len_ptr); + ERRH(StatementHandle, "data update functions not supported"); + RET_HDIAGS(STMH(StatementHandle), SQL_STATE_IM001); } -/* Converts an xstr to a TS. - * xstr needs to be trimmed to exact data (no padding, no 0-term counted). - * If ts_buff is non-NULL, the xstr will be copied (possibly W-to-C converted) - * into it. */ -static BOOL xstr_to_timestamp_struct(xstr_st *xstr, TIMESTAMP_STRUCT *tss, - cstr_st *ts_buff) +SQLRETURN EsSQLMoreResults(SQLHSTMT hstmt) { - /* need the 0-term in the buff, since ansi_w2c will write it */ - char buff[sizeof(ESODBC_ISO8601_TEMPLATE)/*+\0*/]; - cstr_st ts_str, *ts_ptr; - timestamp_t tsp; - struct tm tmp; - - if (ts_buff) { - assert(sizeof(ESODBC_ISO8601_TEMPLATE) - 1 <= ts_buff->cnt); - ts_ptr = ts_buff; - } else { - ts_str.str = buff; - ts_str.cnt = sizeof(buff); - ts_ptr = &ts_str; - } - - if (xstr->wide) { - DBG("converting ISO 8601 `" LWPDL "` to timestamp.", LWSTR(&xstr->w)); - if (sizeof(ESODBC_ISO8601_TEMPLATE) - 1 < xstr->w.cnt) { - ERR("`" LWPDL "` not a TIMESTAMP.", LWSTR(&xstr->w)); - return FALSE; - } - /* convert the W-string to C-string; also, copy it directly into out - * ts_buff, if given (thus saving one extra copying) */ - ts_ptr->cnt = ansi_w2c(xstr->w.str, ts_ptr->str, xstr->w.cnt) - 1; - } else { - DBG("converting ISO 8601 `" LCPDL "` to timestamp.", LCSTR(&xstr->c)); - if (sizeof(ESODBC_ISO8601_TEMPLATE) - 1 < xstr->c.cnt) { - ERR("`" LCPDL "` not a TIMESTAMP.", LCSTR(&xstr->c)); - return FALSE; - } - /* no conversion needed; but copying to the out ts_buff, if given */ - if (ts_buff) { - memcpy(ts_ptr->str, xstr->c.str, xstr->c.cnt); - ts_ptr->cnt = xstr->c.cnt; - } else { - ts_ptr = &xstr->c; - } - } - - /* len counts the 0-term */ - if (ts_ptr->cnt <= 1 || timestamp_parse(ts_ptr->str, ts_ptr->cnt, &tsp) || - (! timestamp_to_tm_local(&tsp, &tmp))) { - ERR("data `" LCPDL "` not an ANSI ISO 8601 format.", LCSTR(ts_ptr)); - return FALSE; - } - TM_TO_TIMESTAMP_STRUCT(&tmp, tss); - tss->fraction = tsp.nsec / 1000000; - - DBG("parsed ISO 8601: `%04d-%02d-%02dT%02d:%02d:%02d.%u+%dm`.", - tss->year, tss->month, tss->day, - tss->hour, tss->minute, tss->second, tss->fraction, - tsp.offset); - - return TRUE; + INFOH(hstmt, "multiple result sets not supported."); // TODO? + return SQL_NO_DATA; } - -static BOOL parse_timedate(xstr_st *xstr, TIMESTAMP_STRUCT *tss, - SQLSMALLINT *format, cstr_st *ts_buff) +SQLRETURN EsSQLCloseCursor(SQLHSTMT StatementHandle) { - /* template buffer: date or time values will be copied in place and - * evaluated as a timestamp (needs to be valid) */ - SQLCHAR templ[] = "0001-01-01T00:00:00.0000000Z"; - /* conversion Wide to C-string buffer */ - SQLCHAR w2c[sizeof(ESODBC_ISO8601_TEMPLATE)/*+\0*/]; - cstr_st td;/* timedate string */ - xstr_st xtd; - - /* is this a TIMESTAMP? */ - if (sizeof(ESODBC_TIME_TEMPLATE) - 1 < XSTR_LEN(xstr)) { - /* longer than a date-value -> try a timestamp */ - if (! xstr_to_timestamp_struct(xstr, tss, ts_buff)) { - return FALSE; - } - if (format) { - *format = SQL_TYPE_TIMESTAMP; - } - return TRUE; - } - - /* W-strings will eventually require convertion to C-string for TS - * conversion => do it now to simplify string analysis */ - if (xstr->wide) { - td.cnt = ansi_w2c(xstr->w.str, w2c, xstr->w.cnt) - 1; - td.str = w2c; - } else { - td = xstr->c; - } - xtd.wide = FALSE; - - /* could this be a TIME-val? */ - if (/*hh:mm:ss*/8 <= td.cnt && td.str[2] == ':' && td.str[5] == ':') { - /* copy active value in template and parse it as TS */ - /* copy is safe: cnt <= [time template] < [templ] */ - memcpy(templ + sizeof(ESODBC_DATE_TEMPLATE) - 1, td.str, td.cnt); - /* there could be a varying number of fractional digits */ - templ[sizeof(ESODBC_DATE_TEMPLATE) - 1 + td.cnt] = 'Z'; - xtd.c.str = templ; - xtd.c.cnt = td.cnt + sizeof(ESODBC_DATE_TEMPLATE); - if (! xstr_to_timestamp_struct(&xtd, tss, ts_buff)) { - ERR("`" LCPDL "` not a TIME.", LCSTR(&td)); - return FALSE; - } else { - tss->year = tss->month = tss->day = 0; - if (format) { - *format = SQL_TYPE_TIME; - } - } - return TRUE; - } - - /* could this be a DATE-val? */ - if (/*yyyy-mm-dd*/10 <= td.cnt && td.str[4] == '-' && td.str[7] == '-') { - /* copy active value in template and parse it as TS */ - /* copy is safe: cnt <= [time template] < [templ] */ - memcpy(templ, td.str, td.cnt); - xtd.c.str = templ; - xtd.c.cnt = sizeof(templ)/sizeof(templ[0]) - 1; - if (! xstr_to_timestamp_struct(&xtd, tss, ts_buff)) { - ERR("`" LCPDL "` not a DATE.", LCSTR(&td)); - return FALSE; - } else { - tss->hour = tss->minute = tss->second = 0; - tss->fraction = 0; - if (format) { - *format = SQL_TYPE_DATE; - } - } - return TRUE; + esodbc_stmt_st *stmt = STMH(StatementHandle); + if (! STMT_HAS_RESULTSET(stmt)) { + ERRH(stmt, "no open cursor for statement"); + RET_HDIAGS(stmt, SQL_STATE_24000); } - - ERR("`" LCPDL "` not a Time/Date/Timestamp.", LCSTR(&td)); - return FALSE; + return EsSQLFreeStmt(StatementHandle, SQL_CLOSE); } -/* - * -> SQL_C_TYPE_TIMESTAMP - * - * Conversts an ES/SQL 'date' or a text representation of a - * timestamp/date/time value into a TIMESTAMP_STRUCT (indicates the detected - * input format into the "format" parameter). - */ -static SQLRETURN wstr_to_timestamp(esodbc_rec_st *arec, esodbc_rec_st *irec, - void *data_ptr, SQLLEN *octet_len_ptr, - const wchar_t *w_str, size_t chars_0, SQLSMALLINT *format) +SQLRETURN EsSQLNumResultCols(SQLHSTMT StatementHandle, + _Out_ SQLSMALLINT *ColumnCount) { - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; - TIMESTAMP_STRUCT *tss = (TIMESTAMP_STRUCT *)data_ptr; - xstr_st xstr = (xstr_st) { - .wide = TRUE, - .w = (wstr_st) { - (SQLWCHAR *)w_str, chars_0 - 1 - } - }; - - if (octet_len_ptr) { - *octet_len_ptr = sizeof(*tss); - } - - if (data_ptr) { - /* right & left trim the data before attempting conversion */ - wtrim_ws(&xstr.w); - - switch (irec->concise_type) { - case SQL_TYPE_TIMESTAMP: - if (! xstr_to_timestamp_struct(&xstr, tss, NULL)) { - RET_HDIAGS(stmt, SQL_STATE_22018); - } - if (format) { - *format = SQL_TYPE_TIMESTAMP; - } - break; - case SQL_VARCHAR: - if (! parse_timedate(&xstr, tss, format, NULL)) { - RET_HDIAGS(stmt, SQL_STATE_22018); - } - break; - - case SQL_CHAR: - case SQL_LONGVARCHAR: - case SQL_WCHAR: - case SQL_WLONGVARCHAR: - case SQL_TYPE_DATE: - case SQL_TYPE_TIME: - BUGH(stmt, "unexpected (but permitted) SQL type."); - RET_HDIAGS(stmt, SQL_STATE_HY004); - default: - BUGH(stmt, "uncought invalid conversion."); - RET_HDIAGS(stmt, SQL_STATE_07006); - } - } else { - DBGH(stmt, "REC@0x%p, NULL data_ptr", arec); - } - - return SQL_SUCCESS; + return EsSQLGetDescFieldW(STMH(StatementHandle)->ird, NO_REC_NR, + SQL_DESC_COUNT, ColumnCount, SQL_IS_SMALLINT, NULL); } /* - * -> SQL_C_TYPE_DATE + * "The prepared statement associated with the statement handle can be + * re-executed by calling SQLExecute until the application frees the statement + * with a call to SQLFreeStmt with the SQL_DROP option or until the statement + * handle is used in a call to SQLPrepare, SQLExecDirect, or one of the + * catalog functions (SQLColumns, SQLTables, and so on)." */ -static SQLRETURN wstr_to_date(esodbc_rec_st *arec, esodbc_rec_st *irec, - void *data_ptr, SQLLEN *octet_len_ptr, - const wchar_t *wstr, size_t chars_0) +SQLRETURN EsSQLPrepareW +( + SQLHSTMT hstmt, + _In_reads_(cchSqlStr) SQLWCHAR *szSqlStr, + SQLINTEGER cchSqlStr +) { - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; - DATE_STRUCT *ds = (DATE_STRUCT *)data_ptr; - TIMESTAMP_STRUCT tss; + esodbc_stmt_st *stmt = STMH(hstmt); SQLRETURN ret; - SQLSMALLINT fmt; - if (octet_len_ptr) { - *octet_len_ptr = sizeof(*ds); + if (cchSqlStr == SQL_NTS) { + cchSqlStr = (SQLINTEGER)wcslen(szSqlStr); + } else if (cchSqlStr <= 0) { + ERRH(stmt, "invalid statment length: %d.", cchSqlStr); + RET_HDIAGS(stmt, SQL_STATE_HY090); } + DBGH(stmt, "preparing `" LWPDL "` [%d]", cchSqlStr, szSqlStr, + cchSqlStr); - if (data_ptr) { - ret = wstr_to_timestamp(arec, irec, &tss, NULL, wstr, chars_0, &fmt); - if (! SQL_SUCCEEDED(ret)) { - return ret; - } - if (fmt == SQL_TYPE_TIME) { - /* it's a time-value */ - RET_HDIAGS(stmt, SQL_STATE_22018); - } - ds->year = tss.year; - ds->month = tss.month; - ds->day = tss.day; - if (tss.hour || tss.minute || tss.second || tss.fraction) { - /* value's truncated */ - RET_HDIAGS(stmt, SQL_STATE_01S07); - } - } else { - DBGH(stmt, "REC@0x%p, NULL data_ptr", arec); - } + ret = EsSQLFreeStmt(stmt, ESODBC_SQL_CLOSE); + assert(SQL_SUCCEEDED(ret)); /* can't return error */ - return SQL_SUCCESS; + return attach_sql(stmt, szSqlStr, cchSqlStr); } -/* - * -> SQL_C_TYPE_TIME - */ -static SQLRETURN wstr_to_time(esodbc_rec_st *arec, esodbc_rec_st *irec, - void *data_ptr, SQLLEN *octet_len_ptr, - const wchar_t *wstr, size_t chars_0) -{ - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; - TIME_STRUCT *ts = (TIME_STRUCT *)data_ptr; - TIMESTAMP_STRUCT tss; - SQLRETURN ret; - SQLSMALLINT fmt; - if (octet_len_ptr) { - *octet_len_ptr = sizeof(*ts); - } +/* Find the ES/SQL type given in es_type; for ID matching multiple types + * (scaled/half_float and keyword/text) use the best matching col_size, which + * is the smallest, that's still matching (<=) the given one. This assumes the + * types are ordered by it (as per the spec). */ +static esodbc_estype_st *lookup_es_type(esodbc_dbc_st *dbc, + SQLSMALLINT es_type, SQLULEN col_size) +{ + SQLULEN i; - if (data_ptr) { - ret = wstr_to_timestamp(arec, irec, &tss, NULL, wstr, chars_0, &fmt); - if (! SQL_SUCCEEDED(ret)) { - return ret; - } - /* need to differentiate between: - * - 1234-12-34T00:00:00Z : valid and - * - 1234-12-34 : invalid */ - if (fmt == SQL_TYPE_DATE) { - RET_HDIAGS(stmt, SQL_STATE_22018); - } - ts->hour = tss.hour; - ts->minute = tss.minute; - ts->second = tss.second; - if (tss.fraction) { - /* value's truncated */ - RET_HDIAGS(stmt, SQL_STATE_01S07); + for (i = 0; i < dbc->no_types; i ++) { + if (dbc->es_types[i].data_type == es_type) { + if (col_size <= 0) { + return &dbc->es_types[i]; + } else { + assert(col_size < LONG_MAX); + if ((SQLINTEGER)col_size <= dbc->es_types[i].column_size) { + return &dbc->es_types[i]; + } + if (es_type == SQL_VARCHAR && + dbc->es_types[i].column_size == dbc->max_varchar_size) { + return &dbc->es_types[i]; + } + if (es_type == SQL_FLOAT && + dbc->es_types[i].column_size == dbc->max_float_size) { + return &dbc->es_types[i]; + } + } } - } else { - DBGH(stmt, "REC@0x%p, NULL data_ptr", arec); } - - return SQL_SUCCESS; + WARNH(dbc, "no ES/SQL type found for ID %hd and column size %hd.", + es_type, col_size); + return NULL; } -/* - * wstr: is 0-terminated and terminator is counted in 'chars_0'. - * However: "[w]hen C strings are used to hold character data, the - * null-termination character is not considered to be part of the data and is - * not counted as part of its byte length." - * "If the data was converted to a variable-length data type, such as - * character or binary [...][i]t then null-terminates the data." - */ -static SQLRETURN copy_string(esodbc_rec_st *arec, esodbc_rec_st *irec, - SQLULEN pos, const wchar_t *wstr, size_t chars_0) +/* find the matching ES/SQL type for app's SQL type, which can be an exact + * math against ES/SQL types, but also some other valid SQL type. */ +static esodbc_estype_st *match_es_type(esodbc_rec_st *arec, + esodbc_rec_st *irec) { - esodbc_stmt_st *stmt; - void *data_ptr; - SQLLEN *octet_len_ptr; - esodbc_desc_st *ard, *ird; - SQLSMALLINT ctarget; - long long ll; - unsigned long long ull; - wstr_st wval; - double dbl; - SQLWCHAR *endp; - - stmt = arec->desc->hdr.stmt; - ird = stmt->ird; - ard = stmt->ard; - - /* pointer where to write how many characters we will/would use */ - octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); - /* pointer to app's buffer */ - data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); - - switch ((ctarget = get_rec_c_type(arec, irec))) { - case SQL_C_CHAR: - return wstr_to_cstr(arec, irec, data_ptr, octet_len_ptr, - wstr, chars_0); - case SQL_C_BINARY: /* treat binary as WCHAR */ - case SQL_C_WCHAR: - return wstr_to_wstr(arec, irec, data_ptr, octet_len_ptr, - wstr, chars_0); - - case SQL_C_TYPE_TIMESTAMP: - return wstr_to_timestamp(arec, irec, data_ptr, octet_len_ptr, - wstr, chars_0, NULL); - case SQL_C_TYPE_DATE: - return wstr_to_date(arec, irec, data_ptr, octet_len_ptr, - wstr, chars_0); - case SQL_C_TYPE_TIME: - return wstr_to_time(arec, irec, data_ptr, octet_len_ptr, - wstr, chars_0); - - case SQL_C_TINYINT: - case SQL_C_STINYINT: - case SQL_C_SHORT: - case SQL_C_SSHORT: - case SQL_C_LONG: - case SQL_C_SLONG: - case SQL_C_SBIGINT: - wval = (wstr_st) { - (SQLWCHAR *)wstr, chars_0 - 1 - }; - /* trim any white spaces */ - wtrim_ws(&wval); - /* convert to integer type */ - errno = 0; - if (! str2bigint(&wval, /*wide?*/TRUE, (SQLBIGINT *)&ll)) { - ERRH(stmt, "can't convert `" LWPD "` to long long.", wstr); - RET_HDIAGS(stmt, errno == ERANGE ? SQL_STATE_22003 : - SQL_STATE_22018); - } - DBGH(stmt, "string `" LWPD "` converted to LL=%lld.", wstr, ll); - /* delegate to existing functionality */ - return copy_longlong(arec, irec, pos, ll); - - case SQL_C_UTINYINT: - case SQL_C_USHORT: - case SQL_C_ULONG: - case SQL_C_UBIGINT: - wval = (wstr_st) { - (SQLWCHAR *)wstr, chars_0 - 1 - }; - /* trim any white spaces */ - wtrim_ws(&wval); - /* convert to integer type */ - errno = 0; - if (! str2ubigint(&wval, /*wide?*/TRUE, (SQLUBIGINT *)&ull)) { - ERRH(stmt, "can't convert `" LWPD "` to unsigned long long.", - wstr); - RET_HDIAGS(stmt, errno == ERANGE ? SQL_STATE_22003 : - SQL_STATE_22018); - } - DBGH(stmt, "string `" LWPD "` converted to ULL=%llu.", wstr, ull); - if (ull <= LLONG_MAX) { - /* the cast is safe, delegate to existing functionality */ - return copy_longlong(arec, irec, pos, (long long)ull); - } - /* value is larger than what long long can hold: can only convert - * to SQLUBIGINT (and SQLULONG, if it has the same size), or fail - * as out-of-range */ - assert(sizeof(SQLUBIGINT) == sizeof(unsigned long long)); - if ((ctarget == SQL_C_UBIGINT) || (ctarget == SQL_C_ULONG && - sizeof(SQLUINTEGER) == sizeof(SQLUBIGINT))) { - /* write out the converted value */ - REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); - *(SQLUBIGINT *)data_ptr = (SQLUBIGINT)ull; - write_out_octets(octet_len_ptr, sizeof(SQLUBIGINT), irec); - DBGH(stmt, "converted string `" LWPD "` to " - "unsigned long long %llu.", wstr, ull); - } else { - REJECT_AS_OOR(stmt, ull, /*fixed?*/TRUE, "non-ULL"); - } - break; + SQLULEN i, length; + esodbc_dbc_st *dbc = arec->desc->hdr.stmt->hdr.dbc; - case SQL_C_FLOAT: - case SQL_C_DOUBLE: - case SQL_C_NUMERIC: - case SQL_C_BIT: - wval = (wstr_st) { - (SQLWCHAR *)wstr, chars_0 - 1 - }; - /* trim any white spaces */ - wtrim_ws(&wval); - /* convert to double */ - errno = 0; - dbl = wcstod((wchar_t *)wval.str, (wchar_t **)&endp); - DBGH(stmt, "string `" LWPD "` converted to dbl=%.6e.", wstr, dbl); - /* if empty string, non-numeric or under/over-flow, bail out */ - if ((! wval.cnt) || (wval.str + wval.cnt != endp) || errno) { - ERRH(stmt, "can't convert `" LWPD "` to double.", wstr); - RET_HDIAGS(stmt, errno == ERANGE ? SQL_STATE_22003 : - SQL_STATE_22018); + for (i = 0; i < dbc->no_types; i ++) { + if (dbc->es_types[i].data_type == irec->concise_type) { + switch (irec->concise_type) { + /* For SQL types mappign to more than one ES/SQL type, choose + * the ES/SQL type with smallest "size" that covers user given + * precision OR that has maximum precision (in case user's is + * larger than max ES/SQL offers. */ + case SQL_FLOAT: /* HALF_FLOAT, SCALED_FLOAT */ + if (irec->precision <= dbc->es_types[i].column_size || + dbc->es_types[i].column_size == dbc->max_float_size) { + return &dbc->es_types[i]; + } + break; + case SQL_VARCHAR: /* KEYWORD, TEXT */ + length = irec->length ? irec->length : arec->octet_length; + assert(length < LONG_MAX); + if ((SQLINTEGER)length <= dbc->es_types[i].column_size || + dbc->es_types[i].column_size==dbc->max_varchar_size) { + return &dbc->es_types[i]; + } + break; + default: + /* unequivocal match */ + return &dbc->es_types[i]; } - /* delegate to existing functionality */ - return copy_double(arec, irec, pos, dbl); - + } + } - default: - BUGH(stmt, "unexpected unhandled data type: %d.", - get_rec_c_type(arec, irec)); - return SQL_ERROR; - } - - return SQL_SUCCESS; -} - -/* - * Copy one row from IRD to ARD. - * pos: row number in the rowset - * Returns: ... - */ -static SQLRETURN copy_one_row(esodbc_stmt_st *stmt, SQLULEN pos, UJObject row) -{ - SQLSMALLINT i; - SQLLEN rowno; - SQLRETURN ret; - UJObject obj; - void *iter_row; - SQLLEN *ind_len; - long long ll; - double dbl; - const wchar_t *wstr; - BOOL boolval; - size_t len; - BOOL with_info; - esodbc_desc_st *ard, *ird; - esodbc_rec_st *arec, *irec; - - rowno = stmt->rset.frows + pos + /*1-based*/1; - ard = stmt->ard; - ird = stmt->ird; - -#define RET_ROW_DIAG(_state, _message, _colno) \ - do { \ - if (ard->array_status_ptr) \ - ard->array_status_ptr[pos] = SQL_ROW_ERROR; \ - return post_row_diagnostic(&stmt->hdr.diag, _state, MK_WPTR(_message),\ - 0, rowno, _colno); \ - } while (0) -#define SET_ROW_DIAG(_rowno, _colno) \ - do { \ - stmt->hdr.diag.row_number = _rowno; \ - stmt->hdr.diag.column_number = _colno; \ - } while (0) - - if (! UJIsArray(row)) { - ERRH(stmt, "one '%s' (#%zd) element in result set not array; type:" - " %d.", JSON_ANSWER_ROWS, stmt->rset.vrows, UJGetType(row)); - RET_ROW_DIAG(SQL_STATE_01S01, MSG_INV_SRV_ANS, - SQL_NO_COLUMN_NUMBER); - } - iter_row = UJBeginArray(row); - if (! iter_row) { - ERRH(stmt, "Failed to obtain iterator on row (#%zd): %s.", rowno, - UJGetError(stmt->rset.state)); - RET_ROW_DIAG(SQL_STATE_01S01, MSG_INV_SRV_ANS, - SQL_NO_COLUMN_NUMBER); - } - - with_info = FALSE; - /* iterate over the contents of one table row */ - for (i = 0; i < ard->count && UJIterArray(&iter_row, &obj); i ++) { - arec = &ard->recs[i]; /* access safe if 'i < ard->count' */ - /* if record not bound skip it */ - if (! REC_IS_BOUND(arec)) { - DBGH(stmt, "column #%d not bound, skipping it.", i + 1); - continue; - } - - irec = &ird->recs[i]; /* access checked by UJIterArray() condition */ - - switch (UJGetType(obj)) { - default: - ERRH(stmt, "unexpected object of type %d in row L#%zd/T#%zd.", - UJGetType(obj), stmt->rset.vrows, stmt->rset.frows); - RET_ROW_DIAG(SQL_STATE_01S01, MSG_INV_SRV_ANS, i + 1); - /* RET_.. returns */ - - case UJT_Null: - DBGH(stmt, "value [%zd, %d] is NULL.", rowno, i + 1); - /* Note: if ever causing an issue, check - * arec->es_type->nullable before returning NULL to app */ - ind_len = deferred_address(SQL_DESC_INDICATOR_PTR, pos, arec); - if (! ind_len) { - ERRH(stmt, "no buffer to signal NULL value."); - RET_ROW_DIAG(SQL_STATE_22002, "Indicator variable required" - " but not supplied", i + 1); - } - *ind_len = SQL_NULL_DATA; - continue; /* instead of break! no 'ret' processing to do. */ - - case UJT_String: - wstr = UJReadString(obj, &len); - DBGH(stmt, "value [%zd, %d] is string [%d]:`" LWPDL "`.", - rowno, i + 1, len, len, wstr); - /* UJSON4C returns chars count, but 0-terminates w/o counting - * the terminator */ - assert(wstr[len] == 0); - /* "When character data is returned from the driver to the - * application, the driver must always null-terminate it." */ - ret = copy_string(arec, irec, pos, wstr, len + /*\0*/1); - break; - - case UJT_Long: - case UJT_LongLong: - ll = UJNumericLongLong(obj); - DBGH(stmt, "value [%zd, %d] is numeric: %lld.", rowno, i + 1, - ll); - ret = copy_longlong(arec, irec, pos, ll); - break; - - case UJT_Double: - dbl = UJNumericFloat(obj); - DBGH(stmt, "value [%zd, %d] is double: %f.", rowno, i + 1, - dbl); - ret = copy_double(arec, irec, pos, dbl); - break; - - case UJT_True: - case UJT_False: - boolval = UJGetType(obj) == UJT_True ? TRUE : FALSE; - DBGH(stmt, "value [%zd, %d] is boolean: %d.", rowno, i + 1, - boolval); - ret = copy_boolean(arec, irec, pos, boolval); - break; - } - - switch (ret) { - case SQL_SUCCESS_WITH_INFO: - with_info = TRUE; - SET_ROW_DIAG(rowno, i + 1); - case SQL_SUCCESS: - break; - default: /* error */ - SET_ROW_DIAG(rowno, i + 1); - return ret; - } - } - - if (ird->array_status_ptr) { - ird->array_status_ptr[pos] = with_info ? SQL_ROW_SUCCESS_WITH_INFO : - SQL_ROW_SUCCESS; - DBGH(stmt, "status array @0x%p#%d set to %d.", ird->array_status_ptr, - pos, ird->array_status_ptr[pos]); - } - - return with_info ? SQL_SUCCESS_WITH_INFO : SQL_SUCCESS; - -#undef RET_ROW_DIAG -#undef SET_ROW_DIAG -} - -/* TODO: implementation for the below */ -static BOOL conv_implemented(SQLSMALLINT sqltype, SQLSMALLINT ctype) -{ - switch (ctype) { - case SQL_C_GUID: - - case SQL_C_INTERVAL_DAY: - case SQL_C_INTERVAL_HOUR: - case SQL_C_INTERVAL_MINUTE: - case SQL_C_INTERVAL_SECOND: - case SQL_C_INTERVAL_DAY_TO_HOUR: - case SQL_C_INTERVAL_DAY_TO_MINUTE: - case SQL_C_INTERVAL_DAY_TO_SECOND: - case SQL_C_INTERVAL_HOUR_TO_MINUTE: - case SQL_C_INTERVAL_HOUR_TO_SECOND: - case SQL_C_INTERVAL_MINUTE_TO_SECOND: - case SQL_C_INTERVAL_MONTH: - case SQL_C_INTERVAL_YEAR: - case SQL_C_INTERVAL_YEAR_TO_MONTH: - // case SQL_C_TYPE_TIMESTAMP_WITH_TIMEZONE: - // case SQL_C_TYPE_TIME_WITH_TIMEZONE: - return FALSE; - } - - switch (sqltype) { - case SQL_C_GUID: - - case SQL_INTERVAL_DAY: - case SQL_INTERVAL_HOUR: - case SQL_INTERVAL_MINUTE: - case SQL_INTERVAL_SECOND: - case SQL_INTERVAL_DAY_TO_HOUR: - case SQL_INTERVAL_DAY_TO_MINUTE: - case SQL_INTERVAL_DAY_TO_SECOND: - case SQL_INTERVAL_HOUR_TO_MINUTE: - case SQL_INTERVAL_HOUR_TO_SECOND: - case SQL_INTERVAL_MINUTE_TO_SECOND: - case SQL_INTERVAL_MONTH: - case SQL_INTERVAL_YEAR: - case SQL_INTERVAL_YEAR_TO_MONTH: - // case SQL_TYPE_TIMESTAMP_WITH_TIMEZONE: - // case SQL_TYPE_TIME_WITH_TIMEZONE: - return FALSE; - } - - return TRUE; -} - - -/* check if data types in returned columns are compabile with buffer types - * bound for those columns */ -static int sql2c_convertible(esodbc_stmt_st *stmt) -{ - SQLSMALLINT i, min; - esodbc_desc_st *ard, *ird; - esodbc_rec_st *arec, *irec; - - assert(stmt->hdr.dbc->es_types); - assert(STMT_HAS_RESULTSET(stmt)); - - ard = stmt->ard; - ird = stmt->ird; - - min = ard->count < ird->count ? ard->count : ird->count; - for (i = 0; i < min; i ++) { - arec = &ard->recs[i]; - if (! REC_IS_BOUND(arec)) { - /* skip not bound columns */ - continue; - } - irec = &ird->recs[i]; - - if (! ESODBC_TYPES_COMPATIBLE(irec->concise_type, - arec->concise_type)) { - ERRH(stmt, "type conversion not possible on column %d: IRD: %hd, " - "ARD: %hd.", i, irec->concise_type, arec->concise_type); - return CONVERSION_VIOLATION; - } - if (! conv_implemented(irec->concise_type, arec->concise_type)) { - ERRH(stmt, "conversion not supported on column %d types: IRD: %hd," - " ARD: %hd.", i, irec->concise_type, arec->concise_type); - return CONVERSION_UNSUPPORTED; - } - } - - return CONVERSION_SUPPORTED; -} - -/* - * "SQLFetch and SQLFetchScroll use the rowset size at the time of the call to - * determine how many rows to fetch." - * - * "If SQLFetch or SQLFetchScroll encounters an error while retrieving one row - * of a multirow rowset, or if SQLBulkOperations with an Operation argument of - * SQL_FETCH_BY_BOOKMARK encounters an error while performing a bulk fetch, it - * sets the corresponding value in the row status array to SQL_ROW_ERROR, - * continues fetching rows, and returns SQL_SUCCESS_WITH_INFO." - * - * "SQLFetch can be used only for multirow fetches when called in ODBC 3.x; if - * an ODBC 2.x application calls SQLFetch, it will open only a single-row, - * forward-only cursor." - * - * "The application can change the rowset size and bind new rowset buffers (by - * calling SQLBindCol or specifying a bind offset) even after rows have been - * fetched." - * - * "SQLFetch returns bookmarks if column 0 is bound." Otherwise, "return more - * than one row" (if avail). - * - * "The driver does not return SQLSTATE 01S01 (Error in row) to indicate that - * an error has occurred while rows were fetched by a call to SQLFetch." (same - * for SQLFetchScroll). - * - * "SQL_ROW_NOROW: The rowset overlapped the end of the result set, and no row - * was returned that corresponded to this element of the row status array." - * - * "If the bound address is 0, no data value is returned" (also for row/column - * binding) - * - * "In the IRD, this header field points to a row status array containing - * status values after a call to SQLBulkOperations, SQLFetch, SQLFetchScroll, - * or SQLSetPos." = row status array of IRD (.array_status_ptr); can be NULL. - * - * "The binding offset is always added directly to the values in the - * SQL_DESC_DATA_PTR, SQL_DESC_INDICATOR_PTR, and SQL_DESC_OCTET_LENGTH_PTR - * fields." (.bind_offset.ptr) - * - * "In ARDs, this field specifies the binding orientation when SQLFetchScroll - * or SQLFetch is called on the associated statement handle." (.bind_type) - * - * "In an IRD, this SQLULEN * header field points to a buffer containing the - * number of rows fetched after a call to SQLFetch or SQLFetchScroll, or the - * number of rows affected in a bulk operation performed by a call to - * SQLBulkOperations or SQLSetPos, including error rows." - * (.rows_processed_ptr) - * - * "The variable that the StrLen_or_Ind argument refers to is used for both - * indicator and length information. If a fetch encounters a null value for - * the column, it stores SQL_NULL_DATA in this variable; otherwise, it stores - * the data length in this variable. Passing a null pointer as StrLen_or_Ind - * keeps the fetch operation from returning the data length but makes the - * fetch fail if it encounters a null value and has no way to return - * SQL_NULL_DATA." (.indicator_ptr) - */ -SQLRETURN EsSQLFetch(SQLHSTMT StatementHandle) -{ - esodbc_stmt_st *stmt; - esodbc_desc_st *ard, *ird; - SQLULEN i, j; - UJObject row; - SQLRETURN ret; - int errors; - - stmt = STMH(StatementHandle); - ard = stmt->ard; - ird = stmt->ird; - - if (! STMT_HAS_RESULTSET(stmt)) { - if (STMT_NODATA_FORCED(stmt)) { - DBGH(stmt, "empty result set flag set - returning no data."); - return SQL_NO_DATA; - } - ERRH(stmt, "no resultset available on statement."); - RET_HDIAGS(stmt, SQL_STATE_HY010); - } - - /* Check if the data [type] stored in DB is compatiblie with the buffer - * [type] the application provides. This test can only be done at - * fetch-time, since the application can unbind/rebind columns at any time - * (i.e. also in-between consecutive fetches). */ - switch (stmt->sql2c_conversion) { - case CONVERSION_VIOLATION: - ERRH(stmt, "types compibility check had failed already " - "(violation)."); - RET_HDIAGS(stmt, SQL_STATE_07006); - /* RET_ returns */ - - case CONVERSION_UNSUPPORTED: - ERRH(stmt, "types compibility check had failed already " - "(unsupported)."); - RET_HDIAG(stmt, SQL_STATE_HYC00, "Conversion target type not" - " supported", 0); - /* RET_ returns */ - - case CONVERSION_SKIPPED: - DBGH(stmt, "types compatibility skipped."); - /* check unnecessary (SYS TYPES, possiblity other metas) */ - break; - - case CONVERSION_UNCHECKED: - stmt->sql2c_conversion = sql2c_convertible(stmt); - if (stmt->sql2c_conversion < 0) { - ERRH(stmt, "convertibility check: failed!"); - RET_HDIAGS(stmt, - stmt->sql2c_conversion == CONVERSION_VIOLATION ? - SQL_STATE_07006 : SQL_STATE_HYC00); - } - DBGH(stmt, "convertibility check: OK."); - /* no break; */ - - default: - DBGH(stmt, "ES/app data/buffer types found compatible."); - } - - DBGH(stmt, "(`" LCPDL "`); cursor @ %zd / %zd.", LCSTR(&stmt->u8sql), - stmt->rset.vrows, stmt->rset.nrows); - - DBGH(stmt, "rowset max size: %d.", ard->array_size); - errors = 0; - /* for all rows in rowset/array, iterate over rows in current resultset */ - for (i = stmt->rset.array_pos; i < ard->array_size; i ++) { - if (! UJIterArray(&stmt->rset.rows_iter, &row)) { - DBGH(stmt, "ran out of rows in current result set: nrows=%zd, " - "vrows=%zd.", stmt->rset.nrows, stmt->rset.vrows); - if (stmt->rset.eccnt) { /*do I have an Elastic cursor? */ - stmt->rset.array_pos = i; - ret = EsSQLExecute(stmt); - if (! SQL_SUCCEEDED(ret)) { - ERRH(stmt, "failed to fetch next results."); - return ret; - } - return EsSQLFetch(StatementHandle); - } else { - DBGH(stmt, "reached end of entire result set. fetched=%zd.", - stmt->rset.frows); - /* indicate the non-processed rows in rowset */ - if (ard->array_status_ptr) - for (j = i; j < ard->array_size; j ++) { - ard->array_status_ptr[j] = SQL_ROW_NOROW; - } - } - break; - } - ret = copy_one_row(stmt, i, row); - if (! SQL_SUCCEEDED(ret)) { - ERRH(stmt, "copying row %zd failed.", stmt->rset.vrows + i + 1); - errors ++; - } - } - stmt->rset.array_pos = 0; - - /* account for processed rows */ - stmt->rset.vrows += i; - stmt->rset.frows += i; - - /* return number of processed rows (even if 0) */ - if (ird->rows_processed_ptr) { - DBGH(stmt, "setting number of processed rows to: %u.", i); - *ird->rows_processed_ptr = i; - } - - if (i <= 0) { - DBGH(stmt, "no data %sto return.", stmt->rset.vrows ? "left ": ""); - return SQL_NO_DATA; - } - - if (errors && i <= errors) { - ERRH(stmt, "processing failed for all rows [%d].", errors); - return SQL_ERROR; - } - - /* only failures need stmt.diag defer'ing */ - return SQL_SUCCESS; -} - -/* - * "SQLSetPos uses the rowset size that is in effect as of the preceding call - * to SQLFetch or SQLFetchScroll, because SQLSetPos operates on a rowset that - * has already been set. SQLSetPos also will pick up the new rowset size if - * SQLBulkOperations has been called after the rowset size was changed." - * - * "When a block cursor first returns a rowset, the current row is the first - * row of the rowset. To change the current row, the application calls - * SQLSetPos or SQLBulkOperations (to update by bookmark)." - * - * "The driver returns SQLSTATE 01S01 (Error in row) only to indicate that an - * error has occurred while rows were fetched by a call to SQLSetPos to - * perform a bulk operation when the function is called in state S7." (not - * supported currently, with RO operation) - * - * "In the IRD, this header field points to a row status array containing - * status values after a call to SQLBulkOperations, SQLFetch, SQLFetchScroll, - * or SQLSetPos." = row status array of IRD (.array_status_ptr) - * - * "In the ARD, this header field points to a row operation array of values - * that can be set by the application to indicate whether this row is to be - * ignored for SQLSetPos operations." .array_status_ptr - * "If the value in the SQL_DESC_ARRAY_STATUS_PTR field of the ARD is a null - * pointer, all rows are included in the bulk operation" - */ -SQLRETURN EsSQLSetPos( - SQLHSTMT StatementHandle, - SQLSETPOSIROW RowNumber, - SQLUSMALLINT Operation, - SQLUSMALLINT LockType) -{ - switch(Operation) { - case SQL_POSITION: - // FIXME - FIXME; - break; - - case SQL_REFRESH: - case SQL_UPDATE: - case SQL_DELETE: - ERRH(StatementHandle, "operation %d not supported.", Operation); - RET_HDIAGS(STMH(StatementHandle), SQL_STATE_HYC00); - default: - ERRH(StatementHandle, "unknown operation type: %d.", Operation); - RET_HDIAGS(STMH(StatementHandle), SQL_STATE_HY092); - } - return SQL_SUCCESS; -} - -/* - * == JDBC's Jdbc/PreparedStatement.executeLargeUpdate() - * "SQLBulkOperations uses the rowset size in effect at the time of the call, - * because it performs operations on a table independent of any fetched - * rowset." - * "In the IRD, this header field points to a row status array containing - * status values after a call to SQLBulkOperations, SQLFetch, SQLFetchScroll, - * or SQLSetPos." = row status array of IRD (.array_status_ptr) - */ -SQLRETURN EsSQLBulkOperations( - SQLHSTMT StatementHandle, - SQLSMALLINT Operation) -{ - ERRH(StatementHandle, "data update functions not supported"); - RET_HDIAGS(STMH(StatementHandle), SQL_STATE_IM001); -} - -SQLRETURN EsSQLCloseCursor(SQLHSTMT StatementHandle) -{ - esodbc_stmt_st *stmt = STMH(StatementHandle); - if (! STMT_HAS_RESULTSET(stmt)) { - ERRH(stmt, "no open cursor for statement"); - RET_HDIAGS(stmt, SQL_STATE_24000); - } - return EsSQLFreeStmt(StatementHandle, SQL_CLOSE); -} - -SQLRETURN EsSQLNumResultCols(SQLHSTMT StatementHandle, - _Out_ SQLSMALLINT *ColumnCount) -{ - return EsSQLGetDescFieldW(STMH(StatementHandle)->ird, NO_REC_NR, - SQL_DESC_COUNT, ColumnCount, SQL_IS_SMALLINT, NULL); -} - -/* - * "The prepared statement associated with the statement handle can be - * re-executed by calling SQLExecute until the application frees the statement - * with a call to SQLFreeStmt with the SQL_DROP option or until the statement - * handle is used in a call to SQLPrepare, SQLExecDirect, or one of the - * catalog functions (SQLColumns, SQLTables, and so on)." - */ -SQLRETURN EsSQLPrepareW -( - SQLHSTMT hstmt, - _In_reads_(cchSqlStr) SQLWCHAR *szSqlStr, - SQLINTEGER cchSqlStr -) -{ - esodbc_stmt_st *stmt = STMH(hstmt); - SQLRETURN ret; - - if (cchSqlStr == SQL_NTS) { - cchSqlStr = (SQLINTEGER)wcslen(szSqlStr); - } else if (cchSqlStr <= 0) { - ERRH(stmt, "invalid statment length: %d.", cchSqlStr); - RET_HDIAGS(stmt, SQL_STATE_HY090); - } - DBGH(stmt, "preparing `" LWPDL "` [%d]", cchSqlStr, szSqlStr, - cchSqlStr); - - ret = EsSQLFreeStmt(stmt, ESODBC_SQL_CLOSE); - assert(SQL_SUCCEEDED(ret)); /* can't return error */ - - return attach_sql(stmt, szSqlStr, cchSqlStr); -} - - -static SQLRETURN set_param_decdigits(esodbc_rec_st *irec, - SQLUSMALLINT param_no, SQLSMALLINT decdigits) -{ - assert(irec->desc->type == DESC_TYPE_IPD); - - switch (irec->meta_type) { - /* for "SQL_TYPE_TIME, SQL_TYPE_TIMESTAMP, SQL_INTERVAL_SECOND, - * SQL_INTERVAL_DAY_TO_SECOND, SQL_INTERVAL_HOUR_TO_SECOND, or - * SQL_INTERVAL_MINUTE_TO_SECOND, the SQL_DESC_PRECISION field of the - * IPD is set to DecimalDigits." */ - case METATYPE_DATETIME: - if (irec->concise_type == SQL_TYPE_DATE) { - break; - } - case METATYPE_INTERVAL_WSEC: - if (decdigits < 0) { - ERRH(irec->desc, "can't set negative (%hd) as fractional " - "second precision.", decdigits); - RET_HDIAGS(irec->desc, SQL_STATE_HY104); - } - return EsSQLSetDescFieldW(irec->desc, param_no, SQL_DESC_PRECISION, - (SQLPOINTER)(intptr_t)decdigits, SQL_IS_SMALLINT); - - /* for " SQL_NUMERIC or SQL_DECIMAL, the SQL_DESC_SCALE field of the - * IPD is set to DecimalDigits." */ - case METATYPE_EXACT_NUMERIC: - if (irec->concise_type == SQL_DECIMAL || - irec->concise_type == SQL_NUMERIC) { - return EsSQLSetDescFieldW(irec->desc, param_no, SQL_DESC_SCALE, - (SQLPOINTER)(intptr_t)decdigits, SQL_IS_SMALLINT); - } - break; // formal - - default: - /* "For all other data types, the DecimalDigits argument is - * ignored." */ - ; - } - - return SQL_SUCCESS; -} - -static SQLSMALLINT get_param_decdigits(esodbc_rec_st *irec) -{ - assert(irec->desc->type == DESC_TYPE_IPD); - - switch(irec->meta_type) { - case METATYPE_DATETIME: - if (irec->concise_type == SQL_TYPE_DATE) { - break; - } - case METATYPE_INTERVAL_WSEC: - return irec->precision; - - case METATYPE_EXACT_NUMERIC: - if (irec->concise_type == SQL_DECIMAL || - irec->concise_type == SQL_NUMERIC) { - return irec->scale; - } - break; - - default: - WARNH(irec->desc, "retriving decdigits for IPD metatype: %d.", - irec->meta_type); - } - - return 0; -} - -static SQLRETURN set_param_size(esodbc_rec_st *irec, - SQLUSMALLINT param_no, SQLULEN size) -{ - assert(irec->desc->type == DESC_TYPE_IPD); - - switch (irec->meta_type) { - /* for "SQL_CHAR, SQL_VARCHAR, SQL_LONGVARCHAR, SQL_BINARY, - * SQL_VARBINARY, SQL_LONGVARBINARY, or one of the concise SQL - * datetime or interval data types, the SQL_DESC_LENGTH field of the - * IPD is set to the value of [s]ize." */ - case METATYPE_STRING: - case METATYPE_BIN: - case METATYPE_DATETIME: - case METATYPE_INTERVAL_WSEC: - case METATYPE_INTERVAL_WOSEC: - return EsSQLSetDescFieldW(irec->desc, param_no, SQL_DESC_LENGTH, - (SQLPOINTER)(uintptr_t)size, SQL_IS_UINTEGER); - - /* for "SQL_DECIMAL, SQL_NUMERIC, SQL_FLOAT, SQL_REAL, or SQL_DOUBLE, - * the SQL_DESC_PRECISION field of the IPD is set to the value of - * [s]ize." */ - case METATYPE_EXACT_NUMERIC: - if (irec->concise_type != SQL_DECIMAL && - irec->concise_type != SQL_NUMERIC) { - break; - } - /* no break */ - case METATYPE_FLOAT_NUMERIC: - // TODO: https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/column-size : - // "The ColumnSize argument of SQLBindParameter is ignored for - // this data type." (floats included): ???? - return EsSQLSetDescFieldW(irec->desc, param_no, SQL_DESC_PRECISION, - /* cast: ULEN -> SMALLINT; XXX: range check? */ - (SQLPOINTER)(uintptr_t)size, SQL_IS_SMALLINT); - - default: - ;/* "For other data types, the [s]ize argument is ignored." */ - } - return SQL_SUCCESS; -} - -static SQLULEN get_param_size(esodbc_rec_st *irec) -{ - assert(irec->desc->type == DESC_TYPE_IPD); - - switch (irec->meta_type) { - case METATYPE_STRING: - case METATYPE_BIN: - case METATYPE_DATETIME: - case METATYPE_INTERVAL_WSEC: - case METATYPE_INTERVAL_WOSEC: - return irec->length; - - case METATYPE_EXACT_NUMERIC: - // TODO: make DEC, NUM a floating meta? - if (irec->concise_type != SQL_DECIMAL && - irec->concise_type != SQL_NUMERIC) { - assert(irec->es_type); - return irec->es_type->column_size; - } - - case METATYPE_FLOAT_NUMERIC: - return irec->precision; - - default: - WARNH(irec->desc, "retriving colsize for IPD metatype: %d.", - irec->meta_type); - } - return 0; -} - -/* Find the ES/SQL type given in es_type; for ID matching multiple types - * (scaled/half_float and keyword/text) use the best matching col_size, which - * is the smallest, that's still matching (<=) the given one. This assumes the - * types are ordered by it (as per the spec). */ -static esodbc_estype_st *lookup_es_type(esodbc_dbc_st *dbc, - SQLSMALLINT es_type, SQLULEN col_size) -{ - SQLULEN i; - - for (i = 0; i < dbc->no_types; i ++) { - if (dbc->es_types[i].data_type == es_type) { - if (col_size <= 0) { - return &dbc->es_types[i]; - } else { - if (col_size <= dbc->es_types[i].column_size) { - return &dbc->es_types[i]; - } - if (es_type == SQL_VARCHAR && - dbc->es_types[i].column_size == dbc->max_varchar_size) { - return &dbc->es_types[i]; - } - if (es_type == SQL_FLOAT && - dbc->es_types[i].column_size == dbc->max_float_size) { - return &dbc->es_types[i]; - } - } - } - } - WARNH(dbc, "no ES/SQL type found for ID %hd and column size %hd.", - es_type, col_size); - return NULL; -} - -/* find the matching ES/SQL type for app's SQL type, which can be an exact - * math against ES/SQL types, but also some other valid SQL type. */ -static esodbc_estype_st *match_es_type(esodbc_rec_st *arec, - esodbc_rec_st *irec) -{ - SQLULEN i, length; - esodbc_dbc_st *dbc = arec->desc->hdr.stmt->hdr.dbc; - - for (i = 0; i < dbc->no_types; i ++) { - if (dbc->es_types[i].data_type == irec->concise_type) { - switch (irec->concise_type) { - /* For SQL types mappign to more than one ES/SQL type, choose - * the ES/SQL type with smallest "size" that covers user given - * precision OR that has maximum precision (in case user's is - * larger than max ES/SQL offers. */ - case SQL_FLOAT: /* HALF_FLOAT, SCALED_FLOAT */ - if (irec->precision <= dbc->es_types[i].column_size || - dbc->es_types[i].column_size == dbc->max_float_size) { - return &dbc->es_types[i]; - } - break; - case SQL_VARCHAR: /* KEYWORD, TEXT */ - length = irec->length ? irec->length : arec->octet_length; - if (length <= dbc->es_types[i].column_size || - dbc->es_types[i].column_size==dbc->max_varchar_size) { - return &dbc->es_types[i]; - } - break; - default: - /* unequivocal match */ - return &dbc->es_types[i]; - } - } - } - - /* app specified an SQL type with no direct mapping to an ES/SQL type */ - switch (irec->meta_type) { - case METATYPE_EXACT_NUMERIC: - assert(irec->concise_type == SQL_DECIMAL || - irec->concise_type == SQL_NUMERIC); - return lookup_es_type(dbc, SQL_FLOAT, irec->precision); - - case METATYPE_STRING: - length = irec->length ? irec->length : arec->octet_length; - return lookup_es_type(dbc, SQL_VARCHAR, length); - case METATYPE_BIN: - /* SQL_VARBINARY == -3 == ES/SQL BINARY */ - return lookup_es_type(dbc, SQL_VARBINARY, /*no prec*/0); - case METATYPE_DATETIME: - assert(irec->concise_type == SQL_TYPE_DATE || - irec->concise_type == SQL_TYPE_TIME); - return lookup_es_type(dbc, SQL_TYPE_TIMESTAMP, /*no prec*/0); - case METATYPE_BIT: - return lookup_es_type(dbc, ESODBC_SQL_BOOLEAN, /*no prec*/0); - case METATYPE_UID: - return lookup_es_type(dbc, SQL_VARCHAR, /*no prec: TEXT*/0); - - case METATYPE_INTERVAL_WSEC: - case METATYPE_INTERVAL_WOSEC: - /* TODO: implement them once avail in ES */ - - case METATYPE_FLOAT_NUMERIC: /* should have matched already */ - case METATYPE_MAX: - /* -> SQL_C_DEFAULT, ESODBC_SQL_NULL, should've matched already */ - case METATYPE_UNKNOWN: - default: - BUGH(irec->desc, "unexpected meta type: %d.", irec->meta_type); - SET_HDIAG(irec->desc, SQL_STATE_HY000, - "bug converting parameters", 0); - } - - return NULL; -} - -/* - * https://docs.microsoft.com/en-us/sql/odbc/reference/develop-app/sending-long-data - * Note: must use EsSQLSetDescFieldW() for param data-type setting, to call - * set_defaults_from_type(), to meet the "Other fields implicitly set" - * requirements from the page linked in set_defaults_from_type() comments. - * - * "Bindings remain in effect until the application calls SQLBindParameter - * again, calls SQLFreeStmt with the SQL_RESET_PARAMS option, or calls - * SQLSetDescField to set the SQL_DESC_COUNT header field of the APD to 0." - */ -SQLRETURN EsSQLBindParameter( - SQLHSTMT StatementHandle, - SQLUSMALLINT ParameterNumber, - SQLSMALLINT InputOutputType, - SQLSMALLINT ValueType, - SQLSMALLINT ParameterType, - SQLULEN ColumnSize, - SQLSMALLINT DecimalDigits, - SQLPOINTER ParameterValuePtr, - SQLLEN BufferLength, - SQLLEN *StrLen_or_IndPtr) -{ - SQLRETURN ret; - esodbc_stmt_st *stmt = STMH(StatementHandle); - esodbc_desc_st *desc; - SQLSMALLINT apd_prev_count, ipd_prev_count; - esodbc_rec_st *irec, *arec; - - if (InputOutputType != SQL_PARAM_INPUT) { - ERRH(stmt, "parameter IO-type (%hd) not supported.", InputOutputType); - RET_HDIAG(stmt, SQL_STATE_HYC00, "parameter IO-type not supported", 0); - } - - /* Note: "If StrLen_or_IndPtr is a null pointer, the driver assumes that - * all input parameter values are non-NULL and that character and binary - * data is null-terminated." */ - if (StrLen_or_IndPtr) { - if (*StrLen_or_IndPtr == SQL_DATA_AT_EXEC || - *StrLen_or_IndPtr < SQL_NTSL) { - ERRH(stmt, "data-at-exec not supported (LenInd=%lld).", - *StrLen_or_IndPtr); - RET_HDIAG(stmt, SQL_STATE_HYC00, "data-at-exec not supported", 0); - } - } else { - /* "If the ParameterValuePtr and StrLen_or_IndPtr arguments specified - * in SQLBindParameter are both null pointers, that function returns - * SQLSTATE HY009". */ - if (! ParameterValuePtr) { - ERRH(stmt, "both value pointer and indicator are NULL."); - RET_HDIAGS(stmt, SQL_STATE_HY009); - } - } - - apd_prev_count = stmt->apd->count; - ipd_prev_count = stmt->ipd->count; - - /* - * APD descriptor setting. - */ - desc = stmt->apd; - - if (apd_prev_count < ParameterNumber) { - ret = EsSQLSetDescFieldW(desc, NO_REC_NR, SQL_DESC_COUNT, - /* ParameterNumber is unsigned, but SQL_DESC_COUNT is signed */ - (SQLPOINTER)(uintptr_t)ParameterNumber, SQL_IS_SMALLINT); - if (! SQL_SUCCEEDED(ret)) { - goto copy_diag; - } - } - - /* set types (or verbose for datetime/interval types) */ - ret = EsSQLSetDescFieldW(desc, ParameterNumber, SQL_DESC_CONCISE_TYPE, - (SQLPOINTER)(intptr_t)ValueType, SQL_IS_SMALLINT); - if (! SQL_SUCCEEDED(ret)) { - goto copy_diag; - } - - /* "Sets the SQL_DESC_OCTET_LENGTH field to the value of BufferLength." */ - ret = EsSQLSetDescFieldW(desc, ParameterNumber, SQL_DESC_OCTET_LENGTH, - (SQLPOINTER)(intptr_t)BufferLength, SQL_IS_INTEGER); - if (! SQL_SUCCEEDED(ret)) { - goto copy_diag; - } - - /* "Sets the SQL_DESC_OCTET_LENGTH_PTR field to the value of - * StrLen_or_Ind." */ - ret = EsSQLSetDescFieldW(desc, ParameterNumber, SQL_DESC_OCTET_LENGTH_PTR, - StrLen_or_IndPtr, - SQL_LEN_BINARY_ATTR((SQLINTEGER)sizeof(StrLen_or_IndPtr))); - if (! SQL_SUCCEEDED(ret)) { - goto copy_diag; - } - - /* "Sets the SQL_DESC_INDICATOR_PTR field to the value of - * StrLen_or_Ind." - * "The SQL_DESC_ARRAY_STATUS_PTR header field in the APD is used to - * ignore parameters." (IPD's indicate execution error status.) */ - ret = EsSQLSetDescFieldW(desc, ParameterNumber, SQL_DESC_INDICATOR_PTR, - StrLen_or_IndPtr, - SQL_LEN_BINARY_ATTR((SQLINTEGER)sizeof(StrLen_or_IndPtr))); - if (! SQL_SUCCEEDED(ret)) { - goto copy_diag; - } - - /* "Sets the SQL_DESC_DATA_PTR field to the value of ParameterValue." - * Note: needs to be last set field, as setting other fields unbinds. */ - ret = EsSQLSetDescFieldW(desc, ParameterNumber, SQL_DESC_DATA_PTR, - ParameterValuePtr, SQL_IS_POINTER); - if (! SQL_SUCCEEDED(ret)) { - goto copy_diag; - } - - /* - * IPD descriptor setting. - */ - desc = stmt->ipd; - - if (ipd_prev_count < ParameterNumber) { - ret = EsSQLSetDescFieldW(desc, NO_REC_NR, SQL_DESC_COUNT, - /* ParameterNumber is unsigned, but SQL_DESC_COUNT is signed */ - (SQLPOINTER)(uintptr_t)ParameterNumber, SQL_IS_SMALLINT); - if (! SQL_SUCCEEDED(ret)) { - goto copy_diag; - } - } - - /* set types (or verbose for datetime/interval types) */ - ret = EsSQLSetDescFieldW(desc, ParameterNumber, SQL_DESC_CONCISE_TYPE, - (SQLPOINTER)(intptr_t)ParameterType, SQL_IS_SMALLINT); - if (! SQL_SUCCEEDED(ret)) { - goto copy_diag; - } - - /* "sets the SQL_DESC_PARAMETER_TYPE field of the IPD." */ - ret = EsSQLSetDescFieldW(desc, ParameterNumber, SQL_DESC_PARAMETER_TYPE, - (SQLPOINTER)(intptr_t)InputOutputType, SQL_IS_SMALLINT); - if (! SQL_SUCCEEDED(ret)) { - goto copy_diag; - } - - irec = get_record(desc, ParameterNumber, /*grow?*/FALSE); - assert(irec); - - ret = set_param_decdigits(irec, ParameterNumber, DecimalDigits); - if (! SQL_SUCCEEDED(ret)) { - goto copy_diag; - } - - ret = set_param_size(irec, ParameterNumber, ColumnSize); - if (! SQL_SUCCEEDED(ret)) { - goto copy_diag; - } - - /* - * data conversion settings - */ - - arec = get_record(stmt->apd, ParameterNumber, /*grow?*/FALSE); - assert(arec); - - if (! conv_implemented(irec->concise_type, arec->concise_type)) { - ERRH(stmt, "type conversion not supported on parameter #%hd:" - " IPD: %hd, APD: %hd.", ParameterNumber, - irec->concise_type, arec->concise_type); - SET_HDIAG(desc, SQL_STATE_HYC00, - "Optional feature not implemented", 0); - goto copy_diag; - } - - irec->es_type = match_es_type(arec, irec); - if (! irec->es_type) { - /* validation shoudl have been done earlier on meta type setting - * (SQL_DESC_CONCISE_TYPE) */ - BUGH(stmt, "failed to match valid SQL type %hd.", irec->concise_type); - SET_HDIAG(desc, SQL_STATE_HY000, "parameter binding bug", 0); - goto copy_diag; - } - DBGH(stmt, "SQL type %hd matched to `" LCPDL "` ES/SQL type.", - ParameterType, LCSTR(&irec->es_type->type_name_c)); - - /* check types compatibility once types have been set and validated */ - if (! ESODBC_TYPES_COMPATIBLE(irec->concise_type, arec->concise_type)) { - ERRH(desc, "type conversion not possible on parameter #%hd: " - "IPD: %hd, APD: %hd.", ParameterNumber, - irec->concise_type, arec->concise_type); - SET_HDIAG(desc, SQL_STATE_07006, "Restricted data type " - "attribute violation", 0); - goto copy_diag; - } - - DBGH(stmt, "succesfully bound parameter #%hu, IO-type: %hd, " - "SQL C type: %hd, SQL type: %hd, size: %llu, decdigits: %hd, " - "buffer@0x%p, length: %lld, LenInd@0x%p.", ParameterNumber, - InputOutputType, ValueType, ParameterType, ColumnSize, DecimalDigits, - ParameterValuePtr, BufferLength, StrLen_or_IndPtr); - - return SQL_SUCCESS; - -copy_diag: - /* copy initial error at top handle level, where it's going to be " - * "inquired from (more might occur below) */ - HDIAG_COPY(desc, stmt); - - ERRH(stmt, "binding parameter failed -- resetting xPD counts."); - /* "If the call to SQLBindParameter fails, [...] the SQL_DESC_COUNT field - * of the APD" [...] "and the SQL_DESC_COUNT field of the IPD is - * unchanged." */ - if (apd_prev_count != stmt->apd->count) { - ret = EsSQLSetDescFieldW(stmt->apd, NO_REC_NR, SQL_DESC_COUNT, - (SQLPOINTER)(uintptr_t)-apd_prev_count, SQL_IS_SMALLINT); - if (! SQL_SUCCEEDED(ret)) { - ERRH(stmt, "failed to reset APD count back to %hd - descriptor " - "might be left in inconsistent state!", -apd_prev_count); - } - } - if (ipd_prev_count != stmt->ipd->count) { - ret = EsSQLSetDescFieldW(stmt->ipd, NO_REC_NR, SQL_DESC_COUNT, - (SQLPOINTER)(uintptr_t)-ipd_prev_count, SQL_IS_SMALLINT); - if (! SQL_SUCCEEDED(ret)) { - ERRH(stmt, "failed to reset IPD count back to %hd - descriptor " - "might be left in inconsistent state!", -ipd_prev_count); - } - } - /* return original failure code */ - RET_STATE(stmt->hdr.diag.state); -} - -/* Converts a C/W-string to a u/llong or double (dest_type?); the xstr->wide - * needs to be set; - * Returns success of conversion and pointer to trimmed number str - * representation. */ -static BOOL xstr_to_number(void *data_ptr, SQLLEN *octet_len_ptr, - xstr_st *xstr, SQLSMALLINT dest_type, void *dest) -{ - BOOL res; - - if (xstr->wide) { - xstr->w.str = (SQLWCHAR *)data_ptr; - if ((octet_len_ptr && *octet_len_ptr == SQL_NTSL) || !octet_len_ptr) { - xstr->w.cnt = wcslen(xstr->w.str); - } else { - xstr->w.cnt = (size_t)(*octet_len_ptr / sizeof(*xstr->w.str)); - xstr->w.cnt -= /*0-term*/1; - } - } else { - xstr->c.str = (SQLCHAR *)data_ptr; - if ((octet_len_ptr && *octet_len_ptr == SQL_NTSL) || !octet_len_ptr) { - xstr->c.cnt = strlen(xstr->c.str); - } else { - xstr->c.cnt = (size_t)(*octet_len_ptr - /*\0*/1); - } - } - - if (! dest) { - return TRUE; - } - - if (xstr->wide) { - wtrim_ws(&xstr->w); - DBG("converting paramter value `" LWPDL "` to number.", - LWSTR(&xstr->w)); - switch (dest_type) { - case SQL_C_SBIGINT: - res = str2bigint(&xstr->w, /*wide?*/TRUE, (SQLBIGINT *)dest); - break; - case SQL_C_UBIGINT: - res = str2bigint(&xstr->w, /*wide?*/TRUE, (SQLUBIGINT *)dest); - break; - case SQL_C_DOUBLE: - res = str2double(&xstr->w, /*wide?*/TRUE, (SQLDOUBLE *)dest); - break; - default: - assert(0); - } - } else { - trim_ws(&xstr->c); - DBG("converting paramter value `" LCPDL "` to number.", - LCSTR(&xstr->c)); - switch (dest_type) { - case SQL_C_SBIGINT: - res = str2bigint(&xstr->c, /*wide?*/FALSE, (SQLBIGINT *)dest); - break; - case SQL_C_UBIGINT: - res = str2bigint(&xstr->c, /*wide?*/FALSE, (SQLUBIGINT *)dest); - break; - case SQL_C_DOUBLE: - res = str2double(&xstr->c, /*wide?*/FALSE, (SQLDOUBLE *)dest); - break; - default: - assert(0); - } - } - - if (! res) { - if (xstr->wide) { - ERR("can't convert `" LWPDL "` to type %hd number.", - LWSTR(&xstr->w), dest_type); - } else { - ERR("can't convert `" LCPDL "` to type %hd number.", - LCSTR(&xstr->c), dest_type); - } - return FALSE; - } else { - return TRUE; - } -} - - -static inline SQLRETURN c2sql_null(esodbc_rec_st *arec, - esodbc_rec_st *irec, char *dest, size_t *len) -{ - assert(irec->concise_type == ESODBC_SQL_NULL); - if (dest) { - memcpy(dest, JSON_VAL_NULL, sizeof(JSON_VAL_NULL) - /*\0*/1); - } - *len = sizeof(JSON_VAL_NULL) - /*\0*/1; - return SQL_SUCCESS; -} - -static SQLRETURN double_to_bool(esodbc_stmt_st *stmt, double dbl, BOOL *val) -{ - DBGH(stmt, "converting double %.6e to bool.", dbl); -#ifdef BOOLEAN_IS_BIT - if (dbl < 0. && 2. < dbl) { - ERRH(stmt, "double %.6e out of range.", dbl); - RET_HDIAGS(stmt, SQL_STATE_22003); - } - if (0. < dbl && dbl < 2. && dbl != 1.) { - /* it's a failure, since SUCCESS_WITH_INFO would be returned only - * after data is sent to the server. */ - ERRH(stmt, "double %.6e right truncated.", dbl); - RET_HDIAGS(stmt, SQL_STATE_22003); - } -#endif /* BOOLEAN_IS_BIT */ - *val = dbl != 0.; - return SQL_SUCCESS; -} - -static SQLRETURN c2sql_boolean(esodbc_rec_st *arec, esodbc_rec_st *irec, - SQLULEN pos, char *dest, size_t *len) -{ - BOOL val; - SQLSMALLINT ctype; - SQLRETURN ret; - void *data_ptr; - SQLLEN *octet_len_ptr; - xstr_st xstr; - double dbl; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; - - if (! dest) { - /* return the "worst case" len (and only convert data at copy time) */ - *len = sizeof(JSON_VAL_FALSE) - 1; - return SQL_SUCCESS; - } - - /* pointer to app's buffer */ - data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); - - /*INDENT-OFF*/ - switch ((ctype = get_rec_c_type(arec, irec))) { - case SQL_C_CHAR: - case SQL_C_WCHAR: - /* pointer to read from how many bytes we have */ - octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, - arec); - xstr.wide = ctype == SQL_C_WCHAR; - if (! xstr_to_number(data_ptr, octet_len_ptr, &xstr, SQL_C_DOUBLE, - &dbl)) { - RET_HDIAGS(stmt, SQL_STATE_22018); - } - ret = double_to_bool(stmt, dbl, &val); - if (! SQL_SUCCEEDED(ret)) { - return ret; - } - break; - - do { - case SQL_C_BINARY: - /* pointer to read from how many bytes we have */ - octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, - pos, arec); - if (! octet_len_ptr) { - if (((char *)data_ptr)[0]) { - RET_HDIAGS(stmt, SQL_STATE_22003); - } - } else if (*octet_len_ptr != sizeof(SQLCHAR)) { - RET_HDIAGS(stmt, SQL_STATE_22003); - } - /* no break */ - case SQL_C_BIT: - case SQL_C_UTINYINT: dbl = (double)*(SQLCHAR *)data_ptr; break; - case SQL_C_TINYINT: - case SQL_C_STINYINT: dbl = (double)*(SQLSCHAR *)data_ptr; break; - case SQL_C_SHORT: - case SQL_C_SSHORT: dbl = (double)*(SQLSMALLINT *)data_ptr; break; - case SQL_C_USHORT: dbl = (double)*(SQLUSMALLINT *)data_ptr; break; - case SQL_C_LONG: - case SQL_C_SLONG: dbl = (double)*(SQLINTEGER *)data_ptr; break; - case SQL_C_ULONG: dbl = (double)*(SQLUINTEGER *)data_ptr; break; - case SQL_C_SBIGINT: dbl = (double)*(SQLBIGINT *)data_ptr; break; - case SQL_C_UBIGINT: dbl = (double)*(SQLUBIGINT *)data_ptr; break; - case SQL_C_FLOAT: dbl = (double)*(SQLREAL *)data_ptr; break; - case SQL_C_DOUBLE: dbl = (double)*(SQLDOUBLE *)data_ptr; break; - - case SQL_C_NUMERIC: - // TODO: better? *(uul *)val[0] != 0 && *[uul *]val[8] != 0 */ - ret = numeric_to_double(irec, data_ptr, &dbl); - if (! SQL_SUCCEEDED(ret)) { - return ret; - } - break; - } while (0); - ret = double_to_bool(stmt, dbl, &val); - if (! SQL_SUCCEEDED(ret)) { - return ret; - } - break; - - //case SQL_C_BOOKMARK: - //case SQL_C_VARBOOKMARK: - default: - BUGH(stmt, "can't convert SQL C type %hd to boolean.", - get_rec_c_type(arec, irec)); - RET_HDIAG(stmt, SQL_STATE_HY000, "bug converting parameter", 0); - } - /*INDENT-ON*/ - - DBGH(stmt, "parameter (pos#%lld) converted to boolean: %d.", pos, val); - - if (val) { - memcpy(dest, JSON_VAL_TRUE, sizeof(JSON_VAL_TRUE) - /*\0*/1); - *len = sizeof(JSON_VAL_TRUE) - 1; - } else { - memcpy(dest, JSON_VAL_FALSE, sizeof(JSON_VAL_FALSE) - /*\0*/1); - *len = sizeof(JSON_VAL_FALSE) - 1; - } - return SQL_SUCCESS; -} - -/* - * Copies a C/W-string representing a number out to send buffer. - * wide: type of string; - * min, max: target SQL numeric type's limits; - * fixed: target SQL numberic type (integer or floating); - * dest: buffer's pointer; can be null (when eval'ing) needed space; - * len: how much of the buffer is needed or has been used. - */ -static SQLRETURN string_to_number(esodbc_rec_st *arec, esodbc_rec_st *irec, - SQLULEN pos, BOOL wide, double *min, double *max, BOOL fixed, - char *dest, size_t *len) -{ - void *data_ptr; - SQLLEN *octet_len_ptr; - xstr_st xstr; - SQLDOUBLE dbl, abs_dbl; - int ret; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; - - /* pointer to app's buffer */ - data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); - /* pointer to read from how many bytes we have */ - octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); - - xstr.wide = wide; - /* do a conversion check: use double, as a capture all cases - * value: ES/SQL will accept a float for an INTEGER param */ - if (! xstr_to_number(data_ptr, octet_len_ptr, &xstr, - SQL_C_DOUBLE, dest ? &dbl : NULL)) { - ERRH(stmt, "failed to convert param value to a double."); - RET_HDIAGS(stmt, SQL_STATE_22018); - } - - if (! dest) { - /* check is superfluous, but safer */ - *len = wide ? xstr.w.cnt : xstr.c.cnt; - return SQL_SUCCESS; - } - - /* check against truncation, limits and any given precision */ - abs_dbl = dbl < 0 ? -dbl : dbl; - if (fixed) { - if (0 < abs_dbl - (SQLUBIGINT)abs_dbl) { - ERRH(stmt, "conversion of double %.6e to fixed would" - " truncate fractional digits", dbl); - RET_HDIAGS(stmt, SQL_STATE_22001); - } - if ((min && dbl < *min) || (max && *max < dbl)) { - ERRH(stmt, "converted double %.6e out of bounds " - "[%.6e, %.6e]", dbl, min, *max); - /* spec requires 22001 here, but that is wrong? */ - RET_HDIAGS(stmt, SQL_STATE_22003); - } - } else { - if ((min && abs_dbl < *min) || (max && *max < abs_dbl)) { - ERRH(stmt, "converted abs double %.6e out of bounds " - "[%.6e, %.6e]", abs_dbl, min, *max); - RET_HDIAGS(stmt, SQL_STATE_22003); - } - } - // - // TODO: check IRD precision against avail value? - // - - /* copy values from app's buffer directly */ - if (wide) { /* need a conversion to ANSI */ - *len = xstr.w.cnt; - ret = ansi_w2c((SQLWCHAR *)data_ptr, dest, *len); - assert(0 < ret); /* it converted to a float already */ - } else { - *len = xstr.c.cnt; - memcpy(dest, data_ptr, *len); - } - return SQL_SUCCESS; -} - -static SQLRETURN sfixed_to_number(esodbc_stmt_st *stmt, SQLBIGINT src, - double *min, double *max, char *dest, size_t *len) -{ - if (! dest) { - /* largest space it could occupy */ - *len = ESODBC_PRECISION_INT64; - return SQL_SUCCESS; - } - - DBGH(stmt, "converting paramter value %lld as number.", src); - - if ((min && src < *min) || (max && *max < src)) { - ERRH(stmt, "source value %lld out of range [%e, %e] for dest type", - src, *min, *max); - RET_HDIAGS(stmt, SQL_STATE_22003); - } - - assert(sizeof(SQLBIGINT) == sizeof(int64_t)); - *len = i64tot(src, dest, /*wide?*/FALSE); - assert(*len <= ESODBC_PRECISION_INT64); - - return SQL_SUCCESS; -} - -static SQLRETURN ufixed_to_number(esodbc_stmt_st *stmt, SQLUBIGINT src, - double *max, char *dest, size_t *len) -{ - if (! dest) { - /* largest space it could occupy */ - *len = ESODBC_PRECISION_UINT64; - return SQL_SUCCESS; - } - - DBGH(stmt, "converting paramter value %llu as number.", src); - - if (src < 0 || (max && *max < src)) { - ERRH(stmt, "source value %llu out of range [0, %e] for dest type", - src, *max); - RET_HDIAGS(stmt, SQL_STATE_22003); - } - - assert(sizeof(SQLBIGINT) == sizeof(int64_t)); - *len = ui64tot(src, dest, /*wide?*/FALSE); - assert(*len <= ESODBC_PRECISION_UINT64); - - return SQL_SUCCESS; -} - -static SQLRETURN floating_to_number(esodbc_rec_st *irec, SQLDOUBLE src, - double *min, double *max, char *dest, size_t *len) -{ - /* format fixed length in scientific notation, -1.23E-45 */ - //const static size_t ff_len = /*-1.*/3 + /*prec*/0 + /*E-*/ 2 + - // sizeof(STR(ESODBC_PRECISION_DOUBLE)) - 1; - size_t maxlen, width; - int cnt; - SQLDOUBLE abs_src; - esodbc_stmt_st *stmt = irec->desc->hdr.stmt; - - //maxlen = get_param_size(irec) + ff_len; - maxlen = get_param_size(irec); - if (! dest) { - /* largest space it could occupy */ - *len = maxlen + /*0-term, for printf*/1; - return SQL_SUCCESS; - } - - abs_src = src < 0 ? -src : src; - if ((min && abs_src < *min) || (max && *max < abs_src)) { - ERRH(stmt, "source value %e out of range [%e, %e] for dest type", - src, *min, *max); - RET_HDIAGS(stmt, SQL_STATE_22003); - } - - width = maxlen; - width -= /*sign*/(src < 0); - width -= /*1.*/2; - width -= /*e+00*/4 + /*3rd digit*/(abs_src < 1e-100 || 1e100 < src); - if (width < 0) { - ERRH(stmt, "parameter size (%zu) to low for floating point.", maxlen); - RET_HDIAGS(stmt, SQL_STATE_HY104); - } - DBGH(stmt, "converting double param %.6e with precision/width: %zu/%d.", - src, maxlen, width); - cnt = snprintf(dest, maxlen + /*\0*/1, "%.*e", (int)width, src); - if (cnt < 0) { - ERRH(stmt, "failed to print double %e.", src); - RET_HDIAGS(stmt, SQL_STATE_HY000); - } else { - *len = cnt; - DBGH(stmt, "value %.6e printed as `" LCPDL "`.", src, *len, dest); - } - - return SQL_SUCCESS; -} - -static SQLRETURN binary_to_number(esodbc_rec_st *arec, esodbc_rec_st *irec, - SQLULEN pos, char *dest, size_t *len) -{ - void *data_ptr; - SQLLEN *octet_len_ptr, osize /*octet~*/; - SQLBIGINT llng; - SQLDOUBLE dbl; - esodbc_stmt_st *stmt = irec->desc->hdr.stmt; - - if (! dest) { - if (irec->meta_type == METATYPE_EXACT_NUMERIC) { - return sfixed_to_number(stmt, 0LL, NULL, NULL, NULL, len); - } else { - assert(irec->meta_type == METATYPE_FLOAT_NUMERIC); - return floating_to_number(irec, 0., NULL, NULL, NULL, len); - } - } - - /* pointer to read from how many bytes we have */ - octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); - /* pointer to app's buffer */ - data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); - - if (! octet_len_ptr) { - /* "If [...] is a null pointer, the driver assumes [...] that - * character and binary data is null-terminated." */ - WARNH(stmt, "no length information provided for binary type: " - "calculating it as a C-string!"); - osize = strlen((char *)data_ptr); - } else { - osize = *octet_len_ptr; - } - -# define CHK_SIZES(_sqlc_type) \ - do { \ - if (osize != sizeof(_sqlc_type)) { \ - ERRH(stmt, "binary data length (%zu) misaligned with target" \ - " data type (%hd) size (%lld)", sizeof(_sqlc_type), \ - irec->es_type->data_type, osize); \ - RET_HDIAGS(stmt, SQL_STATE_HY090); \ - } \ - } while (0) -# define BIN_TO_LLNG(_sqlc_type) \ - do { \ - CHK_SIZES(_sqlc_type); \ - llng = (SQLBIGINT)*(_sqlc_type *)data_ptr; \ - } while (0) -# define BIN_TO_DBL(_sqlc_type) \ - do { \ - CHK_SIZES(_sqlc_type); \ - dbl = (SQLDOUBLE)*(_sqlc_type *)data_ptr; \ - } while (0) - - /*INDENT-OFF*/ - switch (irec->es_type->data_type) { - do { - /* JSON long */ - case SQL_BIGINT: BIN_TO_LLNG(SQLBIGINT); break; /* LONG */ - case SQL_INTEGER: BIN_TO_LLNG(SQLINTEGER); break; /* INTEGER */ - case SQL_SMALLINT: BIN_TO_LLNG(SQLSMALLINT); break; /* SHORT */ - case SQL_TINYINT: BIN_TO_LLNG(SQLSCHAR); break; /* BYTE */ - } while (0); - return sfixed_to_number(stmt, llng, NULL, NULL, dest, len); - - /* JSON double */ - do { - // TODO: check accurate limits for floats in ES/SQL - case SQL_FLOAT: /* HALF_FLOAT, SCALED_FLOAT */ - case SQL_REAL: BIN_TO_DBL(SQLREAL); break; /* FLOAT */ - case SQL_DOUBLE: BIN_TO_DBL(SQLDOUBLE); break; /* DOUBLE */ - } while (0); - return floating_to_number(irec, dbl, NULL, NULL, dest, len); - } - /*INDENT-ON*/ - -# undef BIN_TO_LLNG -# undef BIN_TO_DBL -# undef CHK_SIZES - - BUGH(arec->desc->hdr.stmt, "unexpected ES/SQL type %hd.", - irec->es_type->data_type); - RET_HDIAG(arec->desc->hdr.stmt, SQL_STATE_HY000, - "parameter conversion bug", 0); -} - -static SQLRETURN numeric_to_number(esodbc_rec_st *irec, void *data_ptr, - char *dest, size_t *len) -{ - SQLDOUBLE dbl; - SQLRETURN ret; - - if (! dest) { - return floating_to_number(irec, 0., NULL, NULL, NULL, len); - } - - ret = numeric_to_double(irec, data_ptr, &dbl); - if (! SQL_SUCCEEDED(ret)) { - return ret; - } - return floating_to_number(irec, dbl, NULL, NULL, dest, len); -} - -static SQLRETURN c2sql_number(esodbc_rec_st *arec, esodbc_rec_st *irec, - SQLULEN pos, double *min, double *max, BOOL fixed, char *dest, size_t *len) -{ - void *data_ptr; - SQLSMALLINT ctype; - SQLBIGINT llng; - SQLUBIGINT ullng; - SQLDOUBLE dbl; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; - - /* pointer to app's buffer */ - data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); - - /*INDENT-OFF*/ - switch ((ctype = get_rec_c_type(arec, irec))) { - case SQL_C_WCHAR: - case SQL_C_CHAR: - return string_to_number(arec, irec, pos, ctype == SQL_C_WCHAR, - min, max, fixed, dest, len); - - case SQL_C_BINARY: - return binary_to_number(arec, irec, pos, dest, len); - - do { - case SQL_C_TINYINT: - case SQL_C_STINYINT: llng = (SQLBIGINT)*(SQLSCHAR *)data_ptr; break; - case SQL_C_SHORT: - case SQL_C_SSHORT: llng = (SQLBIGINT)*(SQLSMALLINT *)data_ptr; break; - case SQL_C_LONG: - case SQL_C_SLONG: llng = (SQLBIGINT)*(SQLINTEGER *)data_ptr; break; - case SQL_C_SBIGINT: llng = *(SQLBIGINT *)data_ptr; break; - } while (0); - return sfixed_to_number(stmt, llng, min, max, dest, len); - - do { - case SQL_C_BIT: // XXX: check if 0/1? - case SQL_C_UTINYINT: ullng = (SQLUBIGINT)*(SQLCHAR *)data_ptr; break; - case SQL_C_USHORT: ullng = (SQLUBIGINT)*(SQLUSMALLINT *)data_ptr;break; - case SQL_C_ULONG: ullng = (SQLUBIGINT)*(SQLUINTEGER *)data_ptr; break; - case SQL_C_UBIGINT: ullng = *(SQLUBIGINT *)data_ptr; break; - } while (0); - return ufixed_to_number(stmt, ullng, max, dest, len); - - do { - case SQL_C_FLOAT: dbl = (SQLDOUBLE)*(SQLREAL *)data_ptr; break; - case SQL_C_DOUBLE: dbl = *(SQLDOUBLE *)data_ptr; break; - } while (0); - return floating_to_number(irec, dbl, min, max, dest, len); - - case SQL_C_NUMERIC: - return numeric_to_number(irec, data_ptr, dest, len); - - //case SQL_C_BOOKMARK: - //case SQL_C_VARBOOKMARK: - default: - BUGH(stmt, "can't convert SQL C type %hd to long long.", - get_rec_c_type(arec, irec)); - RET_HDIAG(stmt, SQL_STATE_HY000, "bug converting parameter", 0); - } - /*INDENT-ON*/ - - return SQL_SUCCESS; -} - -static SQLRETURN convert_str_to_timestamp(esodbc_stmt_st *stmt, - SQLLEN *octet_len_ptr, void *data_ptr, BOOL wide, - char *dest, size_t *len) -{ - xstr_st xstr; - TIMESTAMP_STRUCT tss; - SQLSMALLINT format; - cstr_st ts_buff; - - xstr.wide = wide; - - if (wide) { - xstr.w.str = (SQLWCHAR *)data_ptr; - if (octet_len_ptr) { - xstr.w.cnt = *octet_len_ptr / sizeof(SQLWCHAR) - /*\0*/1; - } else { - xstr.w.cnt = wcslen(xstr.w.str); - } - wtrim_ws(&xstr.w); - } else { - xstr.c.str = (SQLCHAR *)data_ptr; - if (octet_len_ptr) { - xstr.c.cnt = *octet_len_ptr / sizeof(SQLCHAR) - /*\0*/1; - } else { - xstr.c.cnt = strlen(xstr.c.str); - } - trim_ws(&xstr.c); - } - - assert(dest); - ts_buff.str = dest; - ts_buff.cnt = sizeof(ESODBC_ISO8601_TEMPLATE) - 1; - if (! parse_timedate(&xstr, &tss, &format, &ts_buff)) { - ERRH(stmt, "failed to parse input as Time/Date/Timestamp"); - RET_HDIAGS(stmt, SQL_STATE_22008); - } else if (format == SQL_TYPE_TIME) { - ERRH(stmt, "can not convert a Time to a Timestamp value"); - RET_HDIAGS(stmt, SQL_STATE_22018); - } else { - /* conversion from TIME to TIMESTAMP should have been deined earlier */ - assert(format != SQL_TYPE_TIME); - *len += ts_buff.cnt; - } - - return SQL_SUCCESS; -} + /* app specified an SQL type with no direct mapping to an ES/SQL type */ + switch (irec->meta_type) { + case METATYPE_EXACT_NUMERIC: + assert(irec->concise_type == SQL_DECIMAL || + irec->concise_type == SQL_NUMERIC); + return lookup_es_type(dbc, SQL_FLOAT, irec->precision); -static SQLRETURN convert_ts_to_timestamp(esodbc_stmt_st *stmt, - SQLLEN *octet_len_ptr, void *data_ptr, SQLSMALLINT ctype, - char *dest, size_t *len) -{ - TIMESTAMP_STRUCT *tss, buff; - DATE_STRUCT *ds; - int cnt; - size_t osize; - - switch (ctype) { - case SQL_C_TYPE_DATE: - ds = (DATE_STRUCT *)data_ptr; - memset(&buff, 0, sizeof(buff)); - buff.year = ds->year; - buff.month = ds->month; - buff.day = ds->day; - tss = &buff; - break; - case SQL_C_BINARY: - if (! octet_len_ptr) { - WARNH(stmt, "no length information provided for binary type: " - "calculating it as a C-string!"); - osize = strlen((char *)data_ptr); - } else { - osize = *octet_len_ptr; - } - if (osize != sizeof(TIMESTAMP_STRUCT)) { - ERRH(stmt, "incorrect binary object size: %zu; expected: %zu.", - osize, sizeof(TIMESTAMP_STRUCT)); - RET_HDIAGS(stmt, SQL_STATE_22003); - } - /* no break */ - case SQL_C_TYPE_TIMESTAMP: - tss = (TIMESTAMP_STRUCT *)data_ptr; - break; + case METATYPE_STRING: + length = irec->length ? irec->length : arec->octet_length; + return lookup_es_type(dbc, SQL_VARCHAR, length); + case METATYPE_BIN: + /* SQL_VARBINARY == -3 == ES/SQL BINARY */ + return lookup_es_type(dbc, SQL_VARBINARY, /*no prec*/0); + case METATYPE_DATETIME: + assert(irec->concise_type == SQL_TYPE_DATE || + irec->concise_type == SQL_TYPE_TIME); + return lookup_es_type(dbc, SQL_TYPE_TIMESTAMP, /*no prec*/0); + case METATYPE_BIT: + return lookup_es_type(dbc, ESODBC_SQL_BOOLEAN, /*no prec*/0); + case METATYPE_UID: + return lookup_es_type(dbc, SQL_VARCHAR, /*no prec: TEXT*/0); - default: - BUGH(stmt, "unexpected SQL C type %hd.", ctype); - RET_HDIAG(stmt, SQL_STATE_HY000, "param conversion bug", 0); - } + case METATYPE_INTERVAL_WSEC: + case METATYPE_INTERVAL_WOSEC: + /* TODO: implement them once avail in ES */ - assert(dest); - cnt = snprintf(dest, sizeof(ESODBC_ISO8601_TEMPLATE) - 1, - "%04d-%02d-%02dT%02d:%02d:%02d.%03uZ", - tss->year, tss->month, tss->day, - tss->hour, tss->minute, tss->second, tss->fraction); - if (cnt < 0) { - ERRH(stmt, "failed printing timestamp struct: %s.", strerror(errno)); - SET_HDIAG(stmt, SQL_STATE_HY000, "C runtime error", 0); + case METATYPE_FLOAT_NUMERIC: /* should have matched already */ + case METATYPE_MAX: + /* -> SQL_C_DEFAULT, ESODBC_SQL_NULL, should've matched already */ + case METATYPE_UNKNOWN: + default: + BUGH(irec->desc, "unexpected meta type: %d.", irec->meta_type); + SET_HDIAG(irec->desc, SQL_STATE_HY000, + "bug converting parameters", 0); } - *len = cnt; - return SQL_SUCCESS; + return NULL; } -static SQLRETURN c2sql_timestamp(esodbc_rec_st *arec, esodbc_rec_st *irec, - SQLULEN pos, char *dest, size_t *len) +/* + * https://docs.microsoft.com/en-us/sql/odbc/reference/develop-app/sending-long-data + * Note: must use EsSQLSetDescFieldW() for param data-type setting, to call + * set_defaults_from_type(), to meet the "Other fields implicitly set" + * requirements from the page linked in set_defaults_from_type() comments. + * + * "Bindings remain in effect until the application calls SQLBindParameter + * again, calls SQLFreeStmt with the SQL_RESET_PARAMS option, or calls + * SQLSetDescField to set the SQL_DESC_COUNT header field of the APD to 0." + */ +SQLRETURN EsSQLBindParameter( + SQLHSTMT StatementHandle, + SQLUSMALLINT ParameterNumber, + SQLSMALLINT InputOutputType, + SQLSMALLINT ValueType, + SQLSMALLINT ParameterType, + SQLULEN ColumnSize, + SQLSMALLINT DecimalDigits, + SQLPOINTER ParameterValuePtr, + SQLLEN BufferLength, + SQLLEN *StrLen_or_IndPtr) { - void *data_ptr; - SQLLEN *octet_len_ptr; - SQLSMALLINT ctype; SQLRETURN ret; - SQLULEN colsize, offt, decdigits; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; - - if (! dest) { - /* maximum possible space it can take */ - *len = /*2x `"`*/2 + sizeof(ESODBC_ISO8601_TEMPLATE) - 1; - return SQL_SUCCESS; - } else { - *dest = '"'; - } - - /* pointer to read from how many bytes we have */ - octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); - /* pointer to app's buffer */ - data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); - - switch ((ctype = get_rec_c_type(arec, irec))) { - case SQL_C_CHAR: - case SQL_C_WCHAR: - ret = convert_str_to_timestamp(stmt, octet_len_ptr, data_ptr, - ctype == SQL_C_WCHAR, dest + /*`"`*/1, len); - if (! SQL_SUCCEEDED(ret)) { - return ret; - } - break; - - case SQL_C_TYPE_TIME: - // TODO - ERRH(stmt, "conversion from time to timestamp not implemented."); - RET_HDIAG(stmt, SQL_STATE_HYC00, "conversion time to timestamp " - "not yet supported", 0); - - case SQL_C_TYPE_DATE: - case SQL_C_BINARY: - case SQL_C_TYPE_TIMESTAMP: - ret = convert_ts_to_timestamp(stmt, octet_len_ptr, data_ptr, - ctype, dest + /*`"`*/1, len); - if (! SQL_SUCCEEDED(ret)) { - return ret; - } - break; + esodbc_stmt_st *stmt = STMH(StatementHandle); + esodbc_desc_st *desc; + SQLSMALLINT apd_prev_count, ipd_prev_count; + esodbc_rec_st *irec, *arec; - default: - BUGH(stmt, "can't convert SQL C type %hd to timestamp.", - get_rec_c_type(arec, irec)); - RET_HDIAG(stmt, SQL_STATE_HY000, "bug converting parameter", 0); + if (InputOutputType != SQL_PARAM_INPUT) { + ERRH(stmt, "parameter IO-type (%hd) not supported.", InputOutputType); + RET_HDIAG(stmt, SQL_STATE_HYC00, "parameter IO-type not supported", 0); } - /* apply corrections depending on the (column) size and decimal digits - * values given at binding time: nullify or trim the resulted string: - * https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/column-size - * */ - colsize = get_param_size(irec); - DBGH(stmt, "requested column size: %llu.", colsize); - if (colsize) { - if (colsize < sizeof("yyyy-mm-dd hh:mm") - 1 || - colsize == 17 || colsize == 18 ) { - ERRH(stmt, "invalid column size value: %llu; " - "allowed: 16, 19, 20+f.", colsize); - RET_HDIAGS(stmt, SQL_STATE_HY104); - } else if (colsize == sizeof("yyyy-mm-dd hh:mm") - 1) { - offt = sizeof("yyyy-mm-ddThh:mm:") - 1; - offt += /*leading `"`*/1; - dest[offt ++] = '0'; - dest[offt ++] = '0'; - dest[offt ++] = 'Z'; - *len = offt; - } else if (colsize == sizeof("yyyy-mm-dd hh:mm:ss") - 1) { - offt = sizeof("yyyy-mm-ddThh:mm:ss") - 1; - offt += /*leading `"`*/1; - dest[offt ++] = 'Z'; - *len = offt; - } else { - assert(20 < colsize); - decdigits = get_param_decdigits(irec); - DBGH(stmt, "requested decimal digits: %llu.", decdigits); - if (/*count of fractions in ISO8601 template*/7 < decdigits) { - INFOH(stmt, "decimal digits value (%hd) reset to 7."); - decdigits = 7; - } else if (decdigits == 0) { - decdigits = -1; /* shave the `.` away */ - } - if (colsize < decdigits + sizeof("yyyy-mm-ddThh:mm:ss.") - 1) { - decdigits = colsize - (sizeof("yyyy-mm-ddThh:mm:ss.") - 1); - WARNH(stmt, "column size adjusted to %hd to fit into a %llu" - " columns size.", decdigits, colsize); - } - offt = sizeof("yyyy-mm-ddThh:mm:ss.") - 1; - offt += /*leading `"`*/1; - offt += decdigits; - dest[offt ++] = 'Z'; - *len = offt; + /* Note: "If StrLen_or_IndPtr is a null pointer, the driver assumes that + * all input parameter values are non-NULL and that character and binary + * data is null-terminated." */ + if (StrLen_or_IndPtr) { + if (*StrLen_or_IndPtr == SQL_DATA_AT_EXEC || + *StrLen_or_IndPtr < SQL_NTSL) { + ERRH(stmt, "data-at-exec not supported (LenInd=%lld).", + *StrLen_or_IndPtr); + RET_HDIAG(stmt, SQL_STATE_HYC00, "data-at-exec not supported", 0); } } else { - WARNH(stmt, "column size given 0 -- column size check skipped"); - (*len) ++; /* initial `"` */ + /* "If the ParameterValuePtr and StrLen_or_IndPtr arguments specified + * in SQLBindParameter are both null pointers, that function returns + * SQLSTATE HY009". */ + if (! ParameterValuePtr) { + ERRH(stmt, "both value pointer and indicator are NULL."); + RET_HDIAGS(stmt, SQL_STATE_HY009); + } } - dest[(*len) ++] = '"'; - return SQL_SUCCESS; -} - - -static SQLRETURN c2sql_cstr2qstr(esodbc_rec_st *arec, esodbc_rec_st *irec, - SQLULEN pos, char *dest, size_t *len) -{ - void *data_ptr; - SQLLEN *octet_len_ptr, cnt; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; - - /* pointer to read from how many bytes we have */ - octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); - /* pointer to app's buffer */ - data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); + apd_prev_count = stmt->apd->count; + ipd_prev_count = stmt->ipd->count; - cnt = octet_len_ptr ? *octet_len_ptr : strlen((char *)data_ptr); + /* + * APD descriptor setting. + */ + desc = stmt->apd; - if (dest) { - *dest = '"'; - } else if ((SQLLEN)get_param_size(irec) < cnt) { - ERRH(stmt, "string's length (%lld) longer than parameter size (%llu).", - cnt, get_param_size(irec)); - RET_HDIAGS(stmt, SQL_STATE_22001); + if (apd_prev_count < ParameterNumber) { + ret = EsSQLSetDescFieldW(desc, NO_REC_NR, SQL_DESC_COUNT, + /* ParameterNumber is unsigned, but SQL_DESC_COUNT is signed */ + (SQLPOINTER)(uintptr_t)ParameterNumber, SQL_IS_SMALLINT); + if (! SQL_SUCCEEDED(ret)) { + goto copy_diag; + } } - *len = json_escape((char *)data_ptr, cnt, dest + !!dest, SIZE_MAX); - - if (dest) { - dest[*len + /*1st `"`*/1] = '"'; + /* set types (or verbose for datetime/interval types) */ + ret = EsSQLSetDescFieldW(desc, ParameterNumber, SQL_DESC_CONCISE_TYPE, + (SQLPOINTER)(intptr_t)ValueType, SQL_IS_SMALLINT); + if (! SQL_SUCCEEDED(ret)) { + goto copy_diag; } - *len += /*`"`*/2; + /* "Sets the SQL_DESC_OCTET_LENGTH field to the value of BufferLength." */ + ret = EsSQLSetDescFieldW(desc, ParameterNumber, SQL_DESC_OCTET_LENGTH, + (SQLPOINTER)(intptr_t)BufferLength, SQL_IS_INTEGER); + if (! SQL_SUCCEEDED(ret)) { + goto copy_diag; + } - return SQL_SUCCESS; -} + /* "Sets the SQL_DESC_OCTET_LENGTH_PTR field to the value of + * StrLen_or_Ind." */ + ret = EsSQLSetDescFieldW(desc, ParameterNumber, SQL_DESC_OCTET_LENGTH_PTR, + StrLen_or_IndPtr, + SQL_LEN_BINARY_ATTR((SQLINTEGER)sizeof(StrLen_or_IndPtr))); + if (! SQL_SUCCEEDED(ret)) { + goto copy_diag; + } -static SQLRETURN c2sql_wstr2qstr(esodbc_rec_st *arec, esodbc_rec_st *irec, - SQLULEN pos, char *dest, size_t *len) -{ - void *data_ptr; - SQLLEN *octet_len_ptr, cnt, octets; - int err; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + /* "Sets the SQL_DESC_INDICATOR_PTR field to the value of + * StrLen_or_Ind." + * "The SQL_DESC_ARRAY_STATUS_PTR header field in the APD is used to + * ignore parameters." (IPD's indicate execution error status.) */ + ret = EsSQLSetDescFieldW(desc, ParameterNumber, SQL_DESC_INDICATOR_PTR, + StrLen_or_IndPtr, + SQL_LEN_BINARY_ATTR((SQLINTEGER)sizeof(StrLen_or_IndPtr))); + if (! SQL_SUCCEEDED(ret)) { + goto copy_diag; + } - /* pointer to read from how many bytes we have */ - octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); - /* pointer to app's buffer */ - data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); + /* "Sets the SQL_DESC_DATA_PTR field to the value of ParameterValue." + * Note: needs to be last set field, as setting other fields unbinds. */ + ret = EsSQLSetDescFieldW(desc, ParameterNumber, SQL_DESC_DATA_PTR, + ParameterValuePtr, SQL_IS_POINTER); + if (! SQL_SUCCEEDED(ret)) { + goto copy_diag; + } - cnt = octet_len_ptr ? *octet_len_ptr : wcslen((wchar_t *)data_ptr); + /* + * IPD descriptor setting. + */ + desc = stmt->ipd; - if (dest) { - *dest = '"'; - } else { - if ((SQLLEN)get_param_size(irec) < cnt) { - ERRH(stmt, "string's length (%lld) longer than parameter " - "size (%llu).", cnt, get_param_size(irec)); - RET_HDIAGS(stmt, SQL_STATE_22001); + if (ipd_prev_count < ParameterNumber) { + ret = EsSQLSetDescFieldW(desc, NO_REC_NR, SQL_DESC_COUNT, + /* ParameterNumber is unsigned, but SQL_DESC_COUNT is signed */ + (SQLPOINTER)(uintptr_t)ParameterNumber, SQL_IS_SMALLINT); + if (! SQL_SUCCEEDED(ret)) { + goto copy_diag; } } - DBGH(stmt, "converting w-string [%lld] `" LWPDL "`; target@0x%p.", - cnt, cnt, (wchar_t *)data_ptr, dest); - if (cnt) { /* WCS2U8 will fail with empty string */ - SetLastError(0); - octets = WCS2U8((wchar_t *)data_ptr, (int)cnt, dest + !!dest, - dest ? INT_MAX : 0); - if ((err = GetLastError())) { - ERRH(stmt, "converting to multibyte string failed: %d", err); - RET_HDIAGS(stmt, SQL_STATE_HY000); - } - } else { - octets = 0; + /* set types (or verbose for datetime/interval types) */ + ret = EsSQLSetDescFieldW(desc, ParameterNumber, SQL_DESC_CONCISE_TYPE, + (SQLPOINTER)(intptr_t)ParameterType, SQL_IS_SMALLINT); + if (! SQL_SUCCEEDED(ret)) { + goto copy_diag; } - assert(0 <= octets); /* buffer might be empty, so 0 is valid */ - *len = (size_t)octets; - if (dest) { - /* last param - buffer len - is calculated as if !dest */ - *len = json_escape_overlapping(dest + /*1st `"`*/1, octets, - JSON_ESC_SEQ_SZ * octets); - dest[*len + /*1st `"`*/1] = '"'; - } else { - /* UCS*-to-UTF8 converted buffer is not yet available, so an accurate - * estimation of how long the JSON-escaping would take is not possible - * => estimate a worst case: 6x */ - *len *= JSON_ESC_SEQ_SZ; + /* "sets the SQL_DESC_PARAMETER_TYPE field of the IPD." */ + ret = EsSQLSetDescFieldW(desc, ParameterNumber, SQL_DESC_PARAMETER_TYPE, + (SQLPOINTER)(intptr_t)InputOutputType, SQL_IS_SMALLINT); + if (! SQL_SUCCEEDED(ret)) { + goto copy_diag; } - *len += /*2x `"`*/2; - - return SQL_SUCCESS; -} - -static SQLRETURN c2sql_number2qstr(esodbc_rec_st *arec, esodbc_rec_st *irec, - SQLULEN pos, char *dest, size_t *len) -{ - SQLRETURN ret; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + irec = get_record(desc, ParameterNumber, /*grow?*/FALSE); + assert(irec); - if (dest) { - *dest = '"'; + ret = set_param_decdigits(irec, ParameterNumber, DecimalDigits); + if (! SQL_SUCCEEDED(ret)) { + goto copy_diag; } - ret = c2sql_number(arec, irec, pos, NULL,NULL, 0, dest + !!dest, len); - - if (dest) { - /* compare lengths only once number has actually been converted */ - if (get_param_size(irec) < *len) { - ERRH(stmt, "converted number length (%zu) larger than parameter " - "size (%llu)", *len, get_param_size(irec)); - RET_HDIAGS(stmt, SQL_STATE_22003); - } - dest[*len + /*1st `"`*/1] = '"'; + ret = set_param_size(irec, ParameterNumber, ColumnSize); + if (! SQL_SUCCEEDED(ret)) { + goto copy_diag; } - *len += /*2x `"`*/2; - - return ret; -} -static SQLRETURN c2sql_varchar(esodbc_rec_st *arec, esodbc_rec_st *irec, - SQLULEN pos, char *dest, size_t *len) -{ - SQLSMALLINT ctype; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + /* + * data conversion settings + */ + ret = convertability_check(stmt, (SQLINTEGER)ParameterNumber, NULL); + if (! SQL_SUCCEEDED(ret)) { + goto err; + } - switch ((ctype = get_rec_c_type(arec, irec))) { - case SQL_C_CHAR: - return c2sql_cstr2qstr(arec, irec, pos, dest, len); - case SQL_C_WCHAR: - return c2sql_wstr2qstr(arec, irec, pos, dest, len); + arec = get_record(stmt->apd, ParameterNumber, /*grow?*/FALSE); + irec->es_type = match_es_type(arec, irec); + if (! irec->es_type) { + /* validation shoudl have been done earlier on meta type setting + * (SQL_DESC_CONCISE_TYPE) */ + BUGH(stmt, "failed to match valid SQL type %hd.", irec->concise_type); + SET_HDIAG(desc, SQL_STATE_HY000, "parameter binding bug", 0); + goto copy_diag; + } + DBGH(stmt, "SQL type %hd matched to `" LCPDL "` ES/SQL type.", + ParameterType, LCSTR(&irec->es_type->type_name_c)); - case SQL_C_BINARY: - // XXX: json_escape - ERRH(stmt, "conversion from SQL C BINARY not implemented."); - RET_HDIAG(stmt, SQL_STATE_HYC00, "conversion from SQL C BINARY " - "not yet supported", 0); - break; + DBGH(stmt, "succesfully bound parameter #%hu, IO-type: %hd, " + "SQL C type: %hd, SQL type: %hd, size: %llu, decdigits: %hd, " + "buffer@0x%p, length: %lld, LenInd@0x%p.", ParameterNumber, + InputOutputType, ValueType, ParameterType, ColumnSize, DecimalDigits, + ParameterValuePtr, BufferLength, StrLen_or_IndPtr); - case SQL_C_TINYINT: - case SQL_C_STINYINT: - case SQL_C_SHORT: - case SQL_C_SSHORT: - case SQL_C_LONG: - case SQL_C_SLONG: - case SQL_C_SBIGINT: - - case SQL_C_BIT: - case SQL_C_UTINYINT: - case SQL_C_USHORT: - case SQL_C_ULONG: - case SQL_C_UBIGINT: - - case SQL_C_FLOAT: - case SQL_C_DOUBLE: - case SQL_C_NUMERIC: - return c2sql_number2qstr(arec, irec, pos, dest, len); - - case SQL_C_TYPE_DATE: - case SQL_C_TYPE_TIME: - case SQL_C_TYPE_TIMESTAMP: - // TODO: leave it a timestamp, or actual DATE/TIME/TS? - return c2sql_timestamp(arec, irec, pos, dest, len); + return SQL_SUCCESS; - // case SQL_C_GUID: - default: - BUGH(stmt, "can't convert SQL C type %hd to timestamp.", - get_rec_c_type(arec, irec)); - RET_HDIAG(stmt, SQL_STATE_HY000, "bug converting parameter", 0); +copy_diag: + /* copy initial error at top handle level, where it's going to be " + * "inquired from (more might occur below) */ + HDIAG_COPY(desc, stmt); +err: + ERRH(stmt, "binding parameter failed -- resetting xPD counts."); + /* "If the call to SQLBindParameter fails, [...] the SQL_DESC_COUNT field + * of the APD" [...] "and the SQL_DESC_COUNT field of the IPD is + * unchanged." */ + if (apd_prev_count != stmt->apd->count) { + ret = EsSQLSetDescFieldW(stmt->apd, NO_REC_NR, SQL_DESC_COUNT, + (SQLPOINTER)(uintptr_t)-apd_prev_count, SQL_IS_SMALLINT); + if (! SQL_SUCCEEDED(ret)) { + ERRH(stmt, "failed to reset APD count back to %hd - descriptor " + "might be left in inconsistent state!", -apd_prev_count); + } + } + if (ipd_prev_count != stmt->ipd->count) { + ret = EsSQLSetDescFieldW(stmt->ipd, NO_REC_NR, SQL_DESC_COUNT, + (SQLPOINTER)(uintptr_t)-ipd_prev_count, SQL_IS_SMALLINT); + if (! SQL_SUCCEEDED(ret)) { + ERRH(stmt, "failed to reset IPD count back to %hd - descriptor " + "might be left in inconsistent state!", -ipd_prev_count); + } } + /* return original failure code */ + RET_STATE(stmt->hdr.diag.state); } + /* Converts parameter values to string, JSON-escaping where necessary * The conversion is actually left to the server: the driver will use the * C source data type as value (at least for numbers) and specify what ES/SQL @@ -4258,7 +1632,6 @@ static SQLRETURN convert_param_val(esodbc_rec_st *arec, esodbc_rec_st *irec, return SQL_SUCCESS; } - /* Forms the JSON array with params: * [{"type": "", "value": }(,etc)*] */ static SQLRETURN serialize_params(esodbc_stmt_st *stmt, char *dest, @@ -4367,14 +1740,14 @@ SQLRETURN TEST_API serialize_statement(esodbc_stmt_st *stmt, cstr_st *buff) bodylen = 1; /* { */ /* evaluate how long the stringified REST object will be */ - if (stmt->rset.eccnt) { /* eval CURSOR object length */ + if (stmt->rset.ecurs.cnt) { /* eval CURSOR object length */ /* convert cursor to C [mb]string. */ /* TODO: ansi_w2c() fits better for Base64 encoded cursors. */ - u8len = WCS2U8(stmt->rset.ecurs, (int)stmt->rset.eccnt, u8curs, + u8len = WCS2U8(stmt->rset.ecurs.str, (int)stmt->rset.ecurs.cnt, u8curs, sizeof(u8curs)); if (u8len <= 0) { ERRH(stmt, "failed to convert cursor `" LWPDL "` to UTF8: %d.", - stmt->rset.eccnt, stmt->rset.ecurs, WCS2U8_ERRNO()); + LWSTR(&stmt->rset.ecurs), WCS2U8_ERRNO()); RET_HDIAGS(stmt, SQL_STATE_24000); } @@ -4426,7 +1799,7 @@ SQLRETURN TEST_API serialize_statement(esodbc_stmt_st *stmt, cstr_st *buff) pos = 0; body[pos ++] = '{'; /* build the actual stringified JSON object */ - if (stmt->rset.eccnt) { /* copy CURSOR object */ + if (stmt->rset.ecurs.cnt) { /* copy CURSOR object */ memcpy(body + pos, JSON_KEY_CURSOR, sizeof(JSON_KEY_CURSOR) - 1); pos += sizeof(JSON_KEY_CURSOR) - 1; body[pos ++] = '"'; @@ -4872,7 +2245,8 @@ SQLRETURN EsSQLColAttributeW( /* function implementation is correct, but it can't really be used as * intended, since the driver's "preparation" doesn't really involve sending * it to ES or even parameter marker counting. - * TODO: marker counting? (SQLDescribeParam would need ES/SQL support, tho) */ + * TODO: marker counting? (SQLDescribeParam would need ES/SQL support, tho); + * or execute with fetch_size 0 or 1. */ SQLRETURN EsSQLNumParams( SQLHSTMT StatementHandle, _Out_opt_ @@ -4901,7 +2275,7 @@ SQLRETURN EsSQLRowCount(_In_ SQLHSTMT StatementHandle, _Out_ SQLLEN *RowCount) DBGH(stmt, "current resultset rows count: %zd.", stmt->rset.nrows); *RowCount = (SQLLEN)stmt->rset.nrows; - if (stmt->rset.eccnt) { + if (stmt->rset.ecurs.cnt) { /* fetch_size or scroller size chunks the result */ WARNH(stmt, "this function will only return the row count of the " "partial result set available."); diff --git a/driver/queries.h b/driver/queries.h index 11b40f49..ea018450 100644 --- a/driver/queries.h +++ b/driver/queries.h @@ -9,7 +9,6 @@ #include "error.h" #include "handles.h" -void queries_init(); void clear_resultset(esodbc_stmt_st *stmt); SQLRETURN TEST_API attach_answer(esodbc_stmt_st *stmt, char *buff, size_t blen); @@ -29,6 +28,13 @@ SQLRETURN EsSQLBindCol( SQLLEN BufferLength, _Inout_opt_ SQLLEN *StrLen_or_Ind); SQLRETURN EsSQLFetch(SQLHSTMT StatementHandle); +SQLRETURN EsSQLGetData( + SQLHSTMT StatementHandle, + SQLUSMALLINT ColumnNumber, + SQLSMALLINT TargetType, + _Out_writes_opt_(_Inexpressible_(BufferLength)) SQLPOINTER TargetValue, + SQLLEN BufferLength, + _Out_opt_ SQLLEN *StrLen_or_IndPtr); SQLRETURN EsSQLSetPos( SQLHSTMT StatementHandle, SQLSETPOSIROW RowNumber, @@ -37,6 +43,7 @@ SQLRETURN EsSQLSetPos( SQLRETURN EsSQLBulkOperations( SQLHSTMT StatementHandle, SQLSMALLINT Operation); +SQLRETURN EsSQLMoreResults(SQLHSTMT hstmt); SQLRETURN EsSQLCloseCursor(SQLHSTMT StatementHandle); SQLRETURN EsSQLNumResultCols(SQLHSTMT StatementHandle, _Out_ SQLSMALLINT *ColumnCount); diff --git a/driver/setup.c b/driver/setup.c new file mode 100644 index 00000000..a69e22f4 --- /dev/null +++ b/driver/setup.c @@ -0,0 +1,263 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +#include "util.h" /* includes windows.h, needed for odbcinst.h */ +#include + +#include "defs.h" +#include "tracing.h" +#include "log.h" +#include "info.h" +#include "dsn.h" + +#define VAL_NAME_APILEVEL "APILevel" +#define VAL_NAME_CONNECTFN "ConnectFunctions" +#define VAL_NAME_ODBCVER "DriverODBCVer" +#define VAL_NAME_SQLLEVEL "SQLLevel" + +/* + * Adds vals from: + * https://docs.microsoft.com/en-us/sql/odbc/reference/install/driver-specification-subkeys + */ +static BOOL add_subkey_values(LPCWSTR lpszDriver) +{ + SQLWCHAR buff[sizeof("YYY")] = {0}; + esodbc_dbc_st dbc; + SQLRETURN ret = SQL_SUCCESS; + SQLSMALLINT supported; + + assert(ESODBC_ODBC_INTERFACE_CONFORMANCE <= 9); + memset(buff, 0, sizeof(buff)); + buff[0] = MK_WPTR('0') + ESODBC_ODBC_INTERFACE_CONFORMANCE; + if (! SQLWritePrivateProfileStringW(lpszDriver, + MK_WPTR(VAL_NAME_APILEVEL), + buff, MK_WPTR(SUBKEY_ODBCINST))) { + ERR("failed to add value `" VAL_NAME_APILEVEL "`."); + return FALSE; + } + + memset(buff, 0, sizeof(buff)); + memset(&dbc, 0, sizeof(dbc)); + dbc.hdr.type = SQL_HANDLE_DBC; + ret |= EsSQLGetFunctions(&dbc, SQL_API_SQLCONNECT, &supported); + buff[0] = supported ? MK_WPTR('Y') : MK_WPTR('N'); + ret |= EsSQLGetFunctions(&dbc, SQL_API_SQLDRIVERCONNECT, &supported); + buff[1] = supported ? MK_WPTR('Y') : MK_WPTR('N'); + ret |= EsSQLGetFunctions(&dbc, SQL_API_SQLBROWSECONNECT, &supported); + buff[2] = supported ? MK_WPTR('Y') : MK_WPTR('N'); + if (! SQL_SUCCEEDED(ret)) { + BUG("failed to read connect functions support."); + return FALSE; + } + if (! SQLWritePrivateProfileStringW(lpszDriver, + MK_WPTR(VAL_NAME_CONNECTFN), + buff, MK_WPTR(SUBKEY_ODBCINST))) { + ERR("failed to add value `" VAL_NAME_CONNECTFN "`."); + return FALSE; + } + + if (! SQLWritePrivateProfileStringW(lpszDriver, + MK_WPTR(VAL_NAME_ODBCVER), + MK_WPTR(ESODBC_SQL_SPEC_STRING), MK_WPTR(SUBKEY_ODBCINST))) { + ERR("failed to add value `" VAL_NAME_APILEVEL "`."); + return FALSE; + } + + assert(ESODBC_SQL_CONFORMANCE <= 9); + memset(buff, 0, sizeof(buff)); + buff[0] = MK_WPTR('0') + ESODBC_SQL_CONFORMANCE; + if (! SQLWritePrivateProfileStringW(lpszDriver, + MK_WPTR(VAL_NAME_SQLLEVEL), + buff, MK_WPTR(SUBKEY_ODBCINST))) { + ERR("failed to add value `" VAL_NAME_APILEVEL "`."); + return FALSE; + } + + return TRUE; +} + +/* called for every installation and for last removal */ +BOOL SQL_API ConfigDriverW( + HWND hwndParent, + WORD fRequest, + LPCWSTR lpszDriver, + LPCWSTR lpszArgs, + LPWSTR lpszMsg, + WORD cbMsgMax, + WORD *pcbMsgOut) +{ + BOOL ret = FALSE; +# define _DSN_END_MARKER "\\" + SQLWCHAR *sample_dsn = MK_WPTR("DSN=" + ESODBC_DSN_SAMPLE_NAME + _DSN_END_MARKER); + SQLWCHAR *pos; + + TRACE7(_IN, "phWWpht", hwndParent, fRequest, lpszDriver, lpszArgs, + lpszMsg, cbMsgMax, pcbMsgOut); + + switch (fRequest) { + case ODBC_INSTALL_DRIVER: + ret = add_subkey_values(lpszDriver); + if (! ret) { + SQLPostInstallerError(ODBC_ERROR_REQUEST_FAILED, NULL); + } + /* add the 2nd \0 to have a 00-list */ + pos = wcschr(sample_dsn, _DSN_END_MARKER[0]); + assert(pos); + *pos = _MK_WPTR('\0'); + /* add a sample DSN */ + if (! ConfigDSNW(NULL, ODBC_ADD_DSN, lpszDriver, sample_dsn)) { + WARN("failed to provision a sample DSN."); + /* no further error indication though, install succeedes */ + } + break; + case ODBC_REMOVE_DRIVER: + /* nothing to do, the vales in ODBCINST.INI are removed along with + * the subkey by caller, when needed and same happens with DSNs + * under ODBC.INI */ + ret = TRUE; + break; + default: + ERR("unexpected configuration request type: %hd.", fRequest); + SQLPostInstallerError(ODBC_ERROR_INVALID_REQUEST_TYPE, NULL); + goto end; + } +end: + TRACE8(_OUT, "dphWWpht", ret, hwndParent, fRequest, lpszDriver, lpszArgs, + lpszMsg, cbMsgMax, pcbMsgOut); + return ret; +# undef _DSN_END_MARKER +} + +BOOL SQL_API ConfigDSNW( + HWND hwndParent, + WORD fRequest, + LPCWSTR lpszDriver, + LPCWSTR lpszAttributes) +{ + esodbc_dsn_attrs_st attrs; + wstr_st driver; + SQLWCHAR buff[ESODBC_DSN_MAX_ATTR_LEN] = {0}; + wstr_st old_dsn = {buff, 0}; + BOOL create_new; + int res; + BOOL ret = FALSE; + + TRACE4(_IN, "phWW", hwndParent, fRequest, lpszDriver, lpszAttributes); + + init_dsn_attrs(&attrs); + + /* If there's a DSN in reveived attributes, load the config from the + * registry. Otherwise, populate a new config with defaults. */ + if (! load_system_dsn(&attrs, (SQLWCHAR *)lpszAttributes)) { + ERR("failed to load system DSN for driver ` " LWPD " ` and " + "attributes `" LWPD "`.", lpszDriver, lpszAttributes); + return FALSE;; + } + /* assign the Driver name; this is not the value of the Driver key in the + * registry (i.e. the path to the DLL), which is actually skipped when + * loading the config. */ + driver = (wstr_st) { + (SQLWCHAR *)lpszDriver, + wcslen(lpszDriver) + }; + res = assign_dsn_attr(&attrs, &MK_WSTR(ESODBC_DSN_DRIVER), &driver, + /*overwrite?*/FALSE); + assert(0 < res); + + switch (fRequest) { + case ODBC_CONFIG_DSN: + /* save the DSN naming, since this might be changed by the user */ + if (attrs.dsn.cnt) { + /* attrs.dsn.cnt < ESODBC_DSN_MAX_ATTR_LEN due to + * load_sys_dsn() */ + wcscpy(old_dsn.str, attrs.dsn.str); + old_dsn.cnt = attrs.dsn.cnt; + } + case ODBC_ADD_DSN: + /* user-interraction loop */ + while (TRUE) { + res = prompt_user_config(hwndParent, &attrs, FALSE); + if (res < 0) { + ERR("failed getting user values."); + goto err; + } else if (! res) { + INFO("user canceled the dialog."); + goto end; + } + /* is it a brand new DSN or has the DSN name changed? */ + DBG("old DSN: `" LWPDL "`, new DSN: `" LWPDL "`.", + LWSTR(&old_dsn), LWSTR(&attrs.dsn)); + if ((! old_dsn.cnt) || + (! EQ_CASE_WSTR(&old_dsn, &attrs.dsn))) { + /* check if target DSN (new or old) already exists */ + res = system_dsn_exists(&attrs.dsn); + if (res < 0) { + ERR("failed to check if DSN `" LWPDL "` already " + "exists.", LWSTR(&attrs.dsn)); + goto err; + } else if (res) { + res = prompt_user_overwrite(hwndParent, &attrs.dsn); + if (res < 0) { + ERR("failed to get user input."); + goto err; + } else if (! res) { + /* let user chose a different DSN name */ + continue; + } + } + /* if an old DSN exists, delete it */ + if (old_dsn.cnt) { + if (! SQLRemoveDSNFromIniW(old_dsn.str)) { + ERR("failed to remove old DSN ` " LWPDL " `.", + LWSTR(&old_dsn)); + goto err; + } + DBG("removed now renamed DSN `" LWPDL "`.", + LWSTR(&old_dsn)); + } + create_new = TRUE; + } else { + create_new = FALSE; + } + break; + } + /* create or update the DSN */ + if (! write_system_dsn(&attrs, create_new)) { + ERR("failed to add DSN to the system."); + } else { + ret = TRUE; + } + break; + + case ODBC_REMOVE_DSN: + if (! SQLRemoveDSNFromIniW(attrs.dsn.str)) { + ERR("failed to remove driver ` " LWPD " ` with " + "attributes `" LWPD "`.", lpszDriver, lpszAttributes); + } else { + INFO("removed DSN `" LWPDL "` from the system.", + LWSTR(&attrs.dsn)); + ret = TRUE; + } + break; + + default: + ERR("unexpected configuration request type: %hd.", fRequest); + SQLPostInstallerError(ODBC_ERROR_INVALID_REQUEST_TYPE, NULL); + goto end; + } + +err: + if (! ret) { + SQLPostInstallerError(ODBC_ERROR_REQUEST_FAILED, NULL); + } +end: + TRACE5(_OUT, "dphWW", ret, hwndParent, fRequest, lpszDriver, + lpszAttributes); + return ret; +} + +/* vim: set noet fenc=utf-8 ff=dos sts=0 sw=4 ts=4 : */ diff --git a/driver/tracing.h b/driver/tracing.h index 57900525..0e3e2dac 100644 --- a/driver/tracing.h +++ b/driver/tracing.h @@ -14,11 +14,14 @@ #define _AVAIL sizeof(_bf) - _ps +/*INDENT-OFF*/ /* TODO: the SQL[U]LEN for _WIN32 */ #define _PRINT_PARAM_VAL(type, val) \ do { \ switch(type) { \ - /* numeric pointers */ \ + /* + * numeric pointers + */ \ /* SQLNUMERIC/SQLDATE/SQLCHAR/etc. = unsigned char */ \ /* SQLSCHAR = char */ \ case 'c': /* char signed */ \ @@ -56,18 +59,29 @@ _n = snprintf(_bf + _ps, _AVAIL, "%llu", \ val ? *(uint64_t *)(uintptr_t)val : 0); \ break; \ - /* non-numeric pointers */ \ + /* + * non-numeric pointers + */ \ case 'p': /* void* */ \ _n = snprintf(_bf + _ps, _AVAIL, "@0x%p", \ (void *)(uintptr_t)val); \ break; \ case 'W': /* wchar_t* */ \ - /* TODO: problematic for untouched buffs: add len! */ \ + /* TODO: scan for unsafe usages with uninit'ed buffers */ \ + /* Note: problematic for untouched buffs: 'p' for unsure. */ \ _n = snprintf(_bf + _ps, _AVAIL, "`" LWPD "`[%zd]", \ - val ? (wchar_t *)(uintptr_t)val : TS_NULL, \ + val ? (wchar_t *)(uintptr_t)val : TWS_NULL, \ val ? wcslen((wchar_t *)(uintptr_t)val) : 0); \ break; \ - /* imediat values */ \ + case 's': /* char* */ \ + /* Note: problematic for untouched buffs: 'p' for unsure. */ \ + _n = snprintf(_bf + _ps, _AVAIL, "`" LCPD "`[%zd]", \ + val ? (char *)(uintptr_t)val : TS_NULL, \ + val ? strlen((char *)(uintptr_t)val) : 0); \ + break; \ + /* + * imediat values + */ \ /* longs */ \ case 'l': /* long signed */ \ _n = snprintf(_bf + _ps, _AVAIL, "%ld", \ @@ -86,6 +100,14 @@ _n = snprintf(_bf + _ps, _AVAIL, "%u", \ (unsigned)(uintptr_t)val); \ break;\ + case 'h': /* short signed */ \ + _n = snprintf(_bf + _ps, _AVAIL, "%hd", \ + (short)(intptr_t)val); \ + break;\ + case 'H': /* short unsigned */ \ + _n = snprintf(_bf + _ps, _AVAIL, "%hu", \ + (unsigned short)(uintptr_t)val); \ + break;\ default: \ _n = snprintf(_bf+_ps, _AVAIL, "BUG! unknown type: %d",type); \ break; \ @@ -93,6 +115,7 @@ if (0 < _n) \ _ps += _n; \ } while (0) +/*INDENT-ON*/ #define _IS_PTR(type, _is_ptr) \ do {\ @@ -101,6 +124,8 @@ case 'L': \ case 'd': \ case 'u': \ + case 'h': \ + case 'H': \ _is_ptr = FALSE; \ break; \ default: \ diff --git a/driver/util.c b/driver/util.c index 947846ca..e61dfa09 100644 --- a/driver/util.c +++ b/driver/util.c @@ -85,7 +85,7 @@ BOOL str2ubigint(void *val, const BOOL wide, SQLUBIGINT *out) BOOL str2bigint(void *val, const BOOL wide, SQLBIGINT *out) { SQLUBIGINT ull; /* unsigned long long */ - size_t i, at0; + size_t i; BOOL negative, ret; cstr_st cstr; wstr_st wstr; @@ -93,18 +93,16 @@ BOOL str2bigint(void *val, const BOOL wide, SQLBIGINT *out) if (wide) { wstr = *(wstr_st *)val; i = wstr.cnt; - at0 = wstr.str[0]; } else { cstr = *(cstr_st *)val; i = cstr.cnt; - at0 = cstr.str[0]; } if (i < 1) { errno = EINVAL; return FALSE; } else { - switch (at0) { + switch (wide ? wstr.str[0] : cstr.str[0]) { case '-': /* L'-' =(size_t)= '-' */ negative = TRUE; i = 1; @@ -267,15 +265,16 @@ void trim_ws(cstr_st *cstr) */ int ansi_w2c(const SQLWCHAR *src, char *dst, size_t chars) { - int i = 0; + size_t i = 0; if (chars < 1) { return -1; } + assert(chars < INT_MAX); do { if (CHAR_MAX < src[i]) { - return -(i + 1); + return -((int)i + 1); } dst[i] = (char)src[i]; } while (src[i] && (++i < chars)); @@ -284,7 +283,7 @@ int ansi_w2c(const SQLWCHAR *src, char *dst, size_t chars) /* loop stopped b/c of length -> src is not 0-term'd */ dst[i] = 0; /* chars + 1 <= [dst] */ } - return i + 1; + return (int)i + 1; } int wmemncasecmp(const SQLWCHAR *a, const SQLWCHAR *b, size_t len) diff --git a/test/test_conversion_sql2c_boolean.cc b/test/test_conversion_sql2c_boolean.cc index 18040c4e..4b6be948 100644 --- a/test/test_conversion_sql2c_boolean.cc +++ b/test/test_conversion_sql2c_boolean.cc @@ -204,5 +204,35 @@ TEST_F(ConvertSQL2C_Boolean, Boolean2WString) { EXPECT_STREQ(wbuff, MK_WPTR("1")); } +TEST_F(ConvertSQL2C_Boolean, Boolean2WString_empty) { + +#undef SQL_VAL +#undef SQL +#define SQL_VAL "true" +#define SQL "CAST(" SQL_VAL " AS W-TEXT)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"boolean\"}\ + ],\ + \"rows\": [\ + [" SQL_VAL "]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLWCHAR wbuff[1] = {0}; + ret = SQLBindCol(stmt, /*col#*/1, SQL_C_WCHAR, &wbuff, sizeof(wbuff), + &ind_len); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLFetch(stmt); + ASSERT_FALSE(SQL_SUCCEEDED(ret)); + assertState(MK_WPTR("22003")); +} + + } // test namespace diff --git a/test/test_conversion_sql2c_ints.cc b/test/test_conversion_sql2c_ints.cc index 47567fb3..860417fb 100644 --- a/test/test_conversion_sql2c_ints.cc +++ b/test/test_conversion_sql2c_ints.cc @@ -159,13 +159,16 @@ TEST_F(ConvertSQL2C_Ints, Long2Char_zero_copy) { prepareStatement(json_answer); SQLCHAR buff[sizeof(SQL_VAL)] = {0}; - ret = SQLBindCol(stmt, /*col#*/1, SQL_C_CHAR, &buff, 0, &ind_len); + // test correction: if destionation buffer is non-NULL, truncation + // indication is the expected result even with a given len of 0 + //ret = SQLBindCol(stmt, /*col#*/1, SQL_C_CHAR, &buff, 0, &ind_len); + ret = SQLBindCol(stmt, /*col#*/1, SQL_C_CHAR, NULL, 0, &ind_len); ASSERT_TRUE(SQL_SUCCEEDED(ret)); ret = SQLFetch(stmt); ASSERT_TRUE(SQL_SUCCEEDED(ret)); EXPECT_EQ(ind_len, sizeof(SQL_VAL) - 1); - EXPECT_EQ(buff[0], 0); /* nothing copied, since 0 buff size indicated */ + //EXPECT_EQ(buff[0], 0); /* nothing copied, since 0 buff size indicated */ } diff --git a/test/test_sqlgetdata.cc b/test/test_sqlgetdata.cc new file mode 100644 index 00000000..bd4eaaa5 --- /dev/null +++ b/test/test_sqlgetdata.cc @@ -0,0 +1,643 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +#include +#include "connected_dbc.h" + +#include + +/* placeholders; will be undef'd and redef'd */ +#define SQL_RAW +#define SQL_VAL +#define SQL /* attached for troubleshooting purposes */ + +namespace test { + +class GetData : public ::testing::Test, public ConnectedDBC { +}; + + +TEST_F(GetData, String2Char_chunked) { + +#undef SQL_VAL +#undef SQL +#define SQL_VAL "12345678901234567890" +#define SQL "CAST(" SQL_VAL " AS TEXT)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"text\"}\ + ],\ + \"rows\": [\ + [\"" SQL_VAL "\"]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLCHAR buff[7]; + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS_WITH_INFO); + EXPECT_EQ(ind_len, sizeof(SQL_VAL) - /*\0*/1 - + 0 * (sizeof(buff) - /*truncation \0*/1)); + EXPECT_STREQ((char *)buff, "123456"); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS_WITH_INFO); + EXPECT_EQ(ind_len, sizeof(SQL_VAL) - /*\0*/1 - + 1 * (sizeof(buff) - /*truncation \0*/1)); + EXPECT_STREQ((char *)buff, "789012"); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS_WITH_INFO); + EXPECT_EQ(ind_len, sizeof(SQL_VAL) - /*\0*/1 - + 2 * (sizeof(buff) - /*truncation \0*/1)); + EXPECT_STREQ((char *)buff, "345678"); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ind_len, sizeof(SQL_VAL) - /*\0*/1 - + 3 * (sizeof(buff) - /*truncation \0*/1)); + EXPECT_STREQ((char *)buff, "90"); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), &ind_len); + ASSERT_EQ(ret, SQL_NO_DATA); +} + +TEST_F(GetData, String2Char_whole) { + +#undef SQL_VAL +#undef SQL +#define SQL_VAL "12345678901234567890" +#define SQL "CAST(" SQL_VAL " AS TEXT)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"text\"}\ + ],\ + \"rows\": [\ + [\"" SQL_VAL "\"]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLCHAR buff[2 * sizeof(SQL_VAL)]; + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), &ind_len); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + EXPECT_EQ(ind_len, sizeof(SQL_VAL) - /*\0*/1); + EXPECT_STREQ((char *)buff, SQL_VAL); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), &ind_len); + ASSERT_EQ(ret, SQL_NO_DATA); +} + + +TEST_F(GetData, String2Char_zero_copy) { + +#undef SQL_VAL +#undef SQL +#define SQL_VAL "12345678901234567890" +#define SQL "CAST(" SQL_VAL " AS TEXT)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"text\"}\ + ],\ + \"rows\": [\ + [\"" SQL_VAL "\"]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLCHAR buff[2 * sizeof(SQL_VAL)]; + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, 0, &ind_len); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + EXPECT_EQ(ind_len, sizeof(SQL_VAL) - /*\0*/1); +} + + +TEST_F(GetData, String2WChar_chunked) { + +#undef SQL_VAL +#undef SQL +#define SQL_VAL "12345678901234567890" +#define SQL "CAST(" SQL_VAL " AS TEXT)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"text\"}\ + ],\ + \"rows\": [\ + [\"" SQL_VAL "\"]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLWCHAR buff[7]; + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS_WITH_INFO); + EXPECT_EQ(ind_len/sizeof(*buff), sizeof(SQL_VAL) - /*\0*/1 - + 0 * (sizeof(buff)/sizeof(*buff) - /*truncation \0*/1)); + EXPECT_STREQ((wchar_t *)buff, MK_WPTR("123456")); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS_WITH_INFO); + EXPECT_EQ(ind_len/sizeof(*buff), sizeof(SQL_VAL) - /*\0*/1 - + 1 * (sizeof(buff)/sizeof(*buff) - /*truncation \0*/1)); + EXPECT_STREQ((wchar_t *)buff, MK_WPTR("789012")); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS_WITH_INFO); + EXPECT_EQ(ind_len/sizeof(*buff), sizeof(SQL_VAL) - /*\0*/1 - + 2 * (sizeof(buff)/sizeof(*buff) - /*truncation \0*/1)); + EXPECT_STREQ((wchar_t *)buff, MK_WPTR("345678")); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ind_len/sizeof(*buff), sizeof(SQL_VAL) - /*\0*/1 - + 3 * (sizeof(buff)/sizeof(*buff) - /*truncation \0*/1)); + EXPECT_STREQ((wchar_t *)buff, MK_WPTR("90")); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_FALSE(SQL_SUCCEEDED(ret)); +} + + +TEST_F(GetData, String2WChar_whole) { + +#undef SQL_VAL +#undef SQL +#define SQL_VAL "12345678901234567890" +#define SQL "CAST(" SQL_VAL " AS TEXT)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"text\"}\ + ],\ + \"rows\": [\ + [\"" SQL_VAL "\"]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLWCHAR buff[2 * sizeof(SQL_VAL)]; + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + EXPECT_EQ(ind_len/sizeof(*buff), sizeof(SQL_VAL) - /*\0*/1); + EXPECT_STREQ((wchar_t *)buff, MK_WPTR(SQL_VAL)); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_NO_DATA); +} + + +TEST_F(GetData, String2WChar_zero_copy) { + +#undef SQL_VAL +#undef SQL +#define SQL_VAL "12345678901234567890" +#define SQL "CAST(" SQL_VAL " AS TEXT)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"text\"}\ + ],\ + \"rows\": [\ + [\"" SQL_VAL "\"]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLWCHAR buff[2 * sizeof(SQL_VAL)]; + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, 0, &ind_len); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + EXPECT_EQ(ind_len/sizeof(*buff), sizeof(SQL_VAL) - /*\0*/1); +} + +TEST_F(GetData, String2SLong) { + +#undef SQL_RAW +#undef SQL_VAL +#undef SQL +#define SQL_RAW 123 +#define SQL_VAL "+" STR(SQL_RAW) +#define SQL "CAST(" SQL_VAL " AS KEYWORD)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"keyword\"}\ + ],\ + \"rows\": [\ + [\"" SQL_VAL "\"]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLINTEGER val; + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_SLONG, &val, /*ignored*/0, + &ind_len); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + EXPECT_EQ(ind_len, sizeof(val)); + EXPECT_EQ(val, SQL_RAW); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_SLONG, &val, /*ignored*/0, + &ind_len); + /* XXX: not sure what should actually be returned in this fixed-data + * subsequent call case -- for now the driver services the request + * succesfully every time */ + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + //ASSERT_EQ(ret, SQL_NO_DATA); +} + + +TEST_F(GetData, SLong2WChar_chunked) { + +#undef SQL_VAL +#undef SQL +#define SQL_VAL "-1234567890" +#define SQL "CAST(" SQL_VAL " AS INTEGER)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"long\"}\ + ],\ + \"rows\": [\ + [" SQL_VAL "]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLWCHAR buff[5]; + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS_WITH_INFO); + EXPECT_EQ(ind_len, (sizeof(SQL_VAL) - 1) * sizeof(*buff)); + EXPECT_STREQ((wchar_t *)buff, MK_WPTR("-123")); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS_WITH_INFO); + EXPECT_EQ(ind_len, (sizeof(SQL_VAL) - 1) * sizeof(*buff) - + 1 * (sizeof(buff) - /*\0*/sizeof(*buff))); + EXPECT_STREQ((wchar_t *)buff, MK_WPTR("4567")); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ind_len, (sizeof(SQL_VAL) - 1) * sizeof(*buff) - + 2 * (sizeof(buff) - /*\0*/sizeof(*buff))); + EXPECT_STREQ((wchar_t *)buff, MK_WPTR("890")); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_NO_DATA); +} + + +TEST_F(GetData, SLong2WChar_whole) { + +#undef SQL_VAL +#undef SQL +#define SQL_VAL "-1234567890" +#define SQL "CAST(" SQL_VAL " AS INTEGER)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"long\"}\ + ],\ + \"rows\": [\ + [" SQL_VAL "]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLWCHAR buff[2 * sizeof(SQL_VAL)]; + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ind_len, (sizeof(SQL_VAL) - 1) * sizeof(*buff)); + EXPECT_STREQ((wchar_t *)buff, MK_WPTR(SQL_VAL)); +} + + +TEST_F(GetData, SLong2Char_chunked) { + +#undef SQL_VAL +#undef SQL +#define SQL_VAL "-1234567890" +#define SQL "CAST(" SQL_VAL " AS INTEGER)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"long\"}\ + ],\ + \"rows\": [\ + [" SQL_VAL "]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLCHAR buff[5]; + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS_WITH_INFO); + EXPECT_EQ(ind_len, (sizeof(SQL_VAL) - 1) * sizeof(*buff)); + EXPECT_STREQ((char *)buff, MK_CPTR("-123")); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS_WITH_INFO); + EXPECT_EQ(ind_len, (sizeof(SQL_VAL) - 1) * sizeof(*buff) - + 1 * (sizeof(buff) - /*\0*/sizeof(*buff))); + EXPECT_STREQ((char *)buff, MK_CPTR("4567")); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ind_len, (sizeof(SQL_VAL) - 1) * sizeof(*buff) - + 2 * (sizeof(buff) - /*\0*/sizeof(*buff))); + EXPECT_STREQ((char *)buff, MK_CPTR("890")); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_NO_DATA); +} + +TEST_F(GetData, SLong2Char_whole) { + +#undef SQL_VAL +#undef SQL +#define SQL_VAL "-1234567890" +#define SQL "CAST(" SQL_VAL " AS INTEGER)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"long\"}\ + ],\ + \"rows\": [\ + [" SQL_VAL "]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLCHAR buff[sizeof(SQL_VAL)]; + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ind_len, sizeof(SQL_VAL) - 1); + EXPECT_STREQ((char *)buff, SQL_VAL); +} + + +TEST_F(GetData, ScaledFloat2WChar_chunked) { + +#undef SQL_VAL +#undef SQL +#define SQL_VAL "0.98765432100123456789" //20 fractional digits +#define SQL "CAST(" SQL_VAL " AS SCALED_FLOAT)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"scaled_float\"}\ + ],\ + \"rows\": [\ + [" SQL_VAL "]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLWCHAR buff[7]; + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS_WITH_INFO); + EXPECT_EQ(ind_len , (/*0.*/2 + /* max ES/SQL double scale */19 + - 0 * (sizeof(buff)/sizeof(SQLWCHAR) - 1)) * sizeof(SQLWCHAR)); + EXPECT_STREQ((wchar_t *)buff, MK_WPTR("0.9876")); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS_WITH_INFO); + EXPECT_EQ(ind_len , (/*0.*/2 + /* max ES/SQL double scale */19 + - 1 * (sizeof(buff)/sizeof(SQLWCHAR) - 1)) * sizeof(SQLWCHAR)); + EXPECT_STREQ((wchar_t *)buff, MK_WPTR("543210")); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS_WITH_INFO); + EXPECT_EQ(ind_len , (/*0.*/2 + /* max ES/SQL double scale */19 + - 2 * (sizeof(buff)/sizeof(SQLWCHAR) - 1)) * sizeof(SQLWCHAR)); + EXPECT_STREQ((wchar_t *)buff, MK_WPTR("012340")); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ind_len , (/*0.*/2 + /* max ES/SQL double scale */19 + - 3 * (sizeof(buff)/sizeof(SQLWCHAR) - 1)) * sizeof(SQLWCHAR)); + /* conversion value of SQL_VAL on x64 is 0.9876543210012340224 */ + EXPECT_STREQ((wchar_t *)buff, MK_WPTR("224")); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_NO_DATA); +} + +TEST_F(GetData, ScaledFloat2WChar_whole) { + +#undef SQL_VAL +#undef SQL +#define SQL_VAL "0.98765432100123456789" //20 fractional digits +#define SQL "CAST(" SQL_VAL " AS SCALED_FLOAT)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"scaled_float\"}\ + ],\ + \"rows\": [\ + [" SQL_VAL "]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLWCHAR buff[sizeof(SQL_VAL)]; + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_WCHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ind_len , (/*0.*/2 + /* max ES/SQL double scale */19) + * sizeof(SQLWCHAR)); + /* TODO: convert the value in the test and use it for comparison. + * The below value is what the SQL_VAL converts to */ + EXPECT_STREQ((wchar_t *)buff, MK_WPTR("0.9876543210012340224")); +} + + +TEST_F(GetData, ScaledFloat2Char_chunked) { + +#undef SQL_VAL +#undef SQL +#define SQL_VAL "0.98765432100123456789" //20 fractional digits +#define SQL "CAST(" SQL_VAL " AS SCALED_FLOAT)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"scaled_float\"}\ + ],\ + \"rows\": [\ + [" SQL_VAL "]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLCHAR buff[7]; + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS_WITH_INFO); + EXPECT_EQ(ind_len , /*0.*/2 + /* max ES/SQL double scale */19 + - 0 * (sizeof(buff) - 1)); + EXPECT_STREQ((char *)buff, "0.9876"); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS_WITH_INFO); + EXPECT_EQ(ind_len , /*0.*/2 + /* max ES/SQL double scale */19 + - 1 * (sizeof(buff) - 1)); + EXPECT_STREQ((char *)buff, "543210"); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS_WITH_INFO); + EXPECT_EQ(ind_len , /*0.*/2 + /* max ES/SQL double scale */19 + - 2 * (sizeof(buff) - 1)); + EXPECT_STREQ((char *)buff, "012340"); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ind_len , /*0.*/2 + /* max ES/SQL double scale */19 + - 3 * (sizeof(buff) - 1)); + /* conversion value of SQL_VAL on x64 is 0.9876543210012340224 */ + EXPECT_STREQ((char *)buff, "224"); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_NO_DATA); +} + +TEST_F(GetData, ScaledFloat2Char_whole) { + +#undef SQL_VAL +#undef SQL +#define SQL_VAL "0.98765432100123456789" //20 fractional digits +#define SQL "CAST(" SQL_VAL " AS SCALED_FLOAT)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"scaled_float\"}\ + ],\ + \"rows\": [\ + [" SQL_VAL "]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLCHAR buff[sizeof(SQL_VAL)]; + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLGetData(stmt, /*col*/1, SQL_C_CHAR, buff, sizeof(buff), + &ind_len); + ASSERT_EQ(ret, SQL_SUCCESS); + EXPECT_EQ(ind_len , /*0.*/2 + /* max ES/SQL double scale */19); + //std::cerr << buff << std::endl; + EXPECT_EQ(memcmp(buff, SQL_VAL, /*0.*/2+/*x64 dbl precision*/15), 0); +} + + +} // test namespace +