From 43cead1f2d764ff9d20c054da397b2c56d2ad899 Mon Sep 17 00:00:00 2001 From: Bogdan Pintea Date: Wed, 9 May 2018 19:25:45 +0200 Subject: [PATCH 1/5] return current catalog from SYS CATALOGS query The application can get(/set) the current catalog of a connection. 'SYS CATALOGS' query is used now to ask for the list of available catalogs and the first entry is returned as "current". --- driver/catalogue.c | 100 +++++++++++++++++++++++++++++++++++++++++++-- driver/catalogue.h | 6 +++ driver/connect.c | 23 +---------- 3 files changed, 104 insertions(+), 25 deletions(-) diff --git a/driver/catalogue.c b/driver/catalogue.c index 2b5b5d27..c1bf91a6 100644 --- a/driver/catalogue.c +++ b/driver/catalogue.c @@ -28,19 +28,111 @@ #include "info.h" #include "queries.h" -// TODO: add type (and schema, when supported) -#define SQL_TABLES "SYS TABLES" \ - " CATALOG LIKE " ESODBC_STRING_DELIM WPFWP_LDESC ESODBC_STRING_DELIM \ + +#define SYS_CATALOGS \ + "SYS CATALOGS" + +/* SYS TABLES synthax tokens; these need to stay broken down, since this + * query makes a difference between a predicate being '%' or left out */ +// TODO: schema, when supported +#define SQL_TABLES \ + "SYS TABLES" +#define SQL_TABLES_CAT \ + " CATALOG LIKE " ESODBC_STRING_DELIM WPFWP_LDESC ESODBC_STRING_DELIM +#define SQL_TABLES_TAB \ " LIKE " ESODBC_STRING_DELIM WPFWP_LDESC ESODBC_STRING_DELIM +#define SQL_TABLES_TYP \ + " TYPE " WPFWP_LDESC // TODO add schema, when supported #define SQL_COLUMNS(...) "SYS COLUMNS" __VA_ARGS__ \ " TABLE LIKE " ESODBC_STRING_DELIM WPFWP_LDESC ESODBC_STRING_DELIM \ " LIKE " ESODBC_STRING_DELIM WPFWP_LDESC ESODBC_STRING_DELIM -#define SQL_COL_CAT \ +#define SQL_COL_CAT \ " CATALOG " ESODBC_STRING_DELIM WPFWP_LDESC ESODBC_STRING_DELIM \ +/* writes into 'dest', of size 'room', the current catalog of 'dbc'. + * returns negative on error, or the char count written otherwise */ +SQLSMALLINT copy_current_catalog(esodbc_dbc_st *dbc, SQLWCHAR *dest, + SQLSMALLINT room) +{ + esodbc_stmt_st *stmt = NULL; + SQLSMALLINT used = -1; /*failure*/ + SQLLEN row_cnt; + SQLLEN ind_len = SQL_NULL_DATA; + SQLWCHAR buff[ESODBC_MAX_IDENTIFIER_LEN]; + SQLWCHAR *catalog; + + if (! SQL_SUCCEEDED(EsSQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt))) { + ERRH(dbc, "failed to alloc a statement handle."); + return -1; + } + assert(stmt); + + if (! SQL_SUCCEEDED(attach_sql(stmt, MK_WPTR(SYS_CATALOGS), + sizeof(SYS_CATALOGS) - 1))) { + ERRH(dbc, "failed to attach query to statement."); + goto end; + } + if (! SQL_SUCCEEDED(post_statement(stmt))) { + ERRH(dbc, "failed to post query."); + goto end; + } + + /* check that we have received proper number of rows (non-0, less than + * max allowed here) */ + if (! SQL_SUCCEEDED(EsSQLRowCount(stmt, &row_cnt))) { + ERRH(dbc, "failed to get result rows count."); + goto end; + } else if (row_cnt <= 0) { + WARNH(stmt, "Elasticsearch returned no current catalog."); + catalog = MK_WPTR(""); /* empty string, it's not quite an error */ + } else { + DBGH(stmt, "Elasticsearch catalogs rows count: %ld.", row_cnt); + if (1 < row_cnt) { + WARNH(dbc, "Elasticsearch connected to %d clusters, returning " + "the first's name as current catalog.", row_cnt); + } + + if (! SQL_SUCCEEDED(EsSQLBindCol(stmt, /*col#*/1, SQL_C_WCHAR, buff, + sizeof(buff), &ind_len))) { + ERRH(dbc, "failed to bind first column."); + goto end; + } + if (! SQL_SUCCEEDED(EsSQLFetch(stmt))) { + ERRH(stmt, "failed to fetch results."); + goto end; + } + if (ind_len <= 0) { + WARNH(dbc, "NULL catalog received."); /*tho maybe != NULL_DATA */ + catalog = MK_WPTR(""); + } else { + catalog = buff; + DBGH(dbc, "current catalog (first value returned): `" LWPD "`.", + catalog); + } + } + + if (! SQL_SUCCEEDED(write_wptr(&dbc->hdr.diag, dest, catalog, room, + &used))) { + ERRH(dbc, "failed to copy catalog: `" LWPD "`.", catalog); + used = -1; /* write_wptr() can change pointer, and still fail */ + } + +end: + /* safe even if no binding occured */ + if (! SQL_SUCCEEDED(EsSQLFreeStmt(stmt, SQL_UNBIND))) { + ERRH(stmt, "failed to unbind statement"); + used = -1; + } + if (! SQL_SUCCEEDED(EsSQLFreeHandle(SQL_HANDLE_STMT, stmt))) { + ERRH(dbc, "failed to free statement handle!"); + } + return used; +} + + SQLRETURN EsSQLTablesW( SQLHSTMT StatementHandle, _In_reads_opt_(NameLength1) SQLWCHAR *CatalogName, diff --git a/driver/catalogue.h b/driver/catalogue.h index 03e07a0a..7d721223 100644 --- a/driver/catalogue.h +++ b/driver/catalogue.h @@ -19,6 +19,12 @@ #define __CATALOGUE_H__ #include "error.h" +#include "handles.h" + + +SQLSMALLINT copy_current_catalog(esodbc_dbc_st *dbc, SQLWCHAR *dest, + SQLSMALLINT room); + SQLRETURN EsSQLTablesW( SQLHSTMT StatementHandle, diff --git a/driver/connect.c b/driver/connect.c index ea9328b7..949fff91 100644 --- a/driver/connect.c +++ b/driver/connect.c @@ -22,6 +22,7 @@ #include "connect.h" #include "queries.h" +#include "catalogue.h" #include "log.h" #include "info.h" #include "util.h" @@ -2095,26 +2096,6 @@ SQLRETURN EsSQLSetConnectAttrW( return SQL_SUCCESS; } -/* writes into 'dest', of size 'room', the current catalog of 'dbc'. - * returns negative on error, or the char count written otherwise */ -static SQLSMALLINT get_current_catalog(esodbc_dbc_st *dbc, SQLWCHAR *dest, - SQLSMALLINT room) -{ - SQLSMALLINT used; - SQLWCHAR *catalog = MK_WPTR("my_current_catalog"); // FIXME - - // - // TODO: use the new SYS CATALOGS query - // - - DBGH(dbc, "current catalog: `" LWPD "`.", catalog); - if (! SQL_SUCCEEDED(write_wptr(&dbc->hdr.diag, dest, catalog, room, - &used))) { - return -1; - } - return used; -} - SQLRETURN EsSQLGetConnectAttrW( SQLHDBC ConnectionHandle, SQLINTEGER Attribute, @@ -2142,7 +2123,7 @@ SQLRETURN EsSQLGetConnectAttrW( if (! dbc->es_types) { ERRH(dbc, "no connection active."); RET_HDIAGS(dbc, SQL_STATE_08003); - } else if ((used = get_current_catalog(dbc, (SQLWCHAR *)ValuePtr, + } else if ((used = copy_current_catalog(dbc, (SQLWCHAR *)ValuePtr, (SQLSMALLINT)BufferLength)) < 0) { ERRH(dbc, "failed to get current catalog."); RET_STATE(dbc->hdr.diag.state); From 3fcf521b925f78cc79d75ea8dfd4273e905c75b1 Mon Sep 17 00:00:00 2001 From: Bogdan Pintea Date: Wed, 9 May 2018 19:30:34 +0200 Subject: [PATCH 2/5] fix descriptor: ending quote missing must have been removed by mistake, instead of training WS. --- driver/util.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/driver/util.h b/driver/util.h index f96f2630..ec65951a 100644 --- a/driver/util.h +++ b/driver/util.h @@ -197,8 +197,8 @@ size_t json_escape(const char *jin, size_t inlen, char *jout, size_t outlen); #define PFWP_LDESC "%.*S" /* silly M$ */ /* wprintf char pointer descriptor */ -#define WPFCP_DESC L"%s -#define WPFCP_LDESC L"%.*s +#define WPFCP_DESC L"%s" +#define WPFCP_LDESC L"%.*s" /* printf char pointer descriptor */ #define PFCP_DESC "%s" #define PFCP_LDESC "%.*s" From 13f4883cdb706a423d1a763969345cd61f23a64a Mon Sep 17 00:00:00 2001 From: Bogdan Pintea Date: Wed, 9 May 2018 19:31:26 +0200 Subject: [PATCH 3/5] single quote table type arg in SQLTables() The spec allows for this argument to contain a list of table types to filter by. The list of types can be given as quoted or unquoted tokens. ES/SQL only suppots the quoted synthax, so do the quoting in the driver. Note: no sanity check done by the driver, though (i.e. mixed quoted/unquoted or invalid characters). This isn't done for the other arguments either. --- driver/catalogue.c | 155 +++++++++++++++++++++++++++++++++++---------- driver/defs.h | 3 +- driver/util.c | 13 +++- driver/util.h | 11 +++- 4 files changed, 144 insertions(+), 38 deletions(-) diff --git a/driver/catalogue.c b/driver/catalogue.c index c1bf91a6..a6b542f7 100644 --- a/driver/catalogue.c +++ b/driver/catalogue.c @@ -132,6 +132,53 @@ SQLSMALLINT copy_current_catalog(esodbc_dbc_st *dbc, SQLWCHAR *dest, return used; } +/* + * Quote the tokens in a string: "a, b,,c" -> "'a','b',,'c'". + * No string sanity done (garbage in, garbage out). + */ +size_t quote_tokens(SQLWCHAR *src, size_t len, SQLWCHAR *dest) +{ + size_t i; + BOOL copying; + SQLWCHAR *pos; + + DBG("len=%d.", len); + copying = FALSE; + pos = dest; + for (i = 0; i < len; i ++) { + DBG("src[%d]=%C, dest:`" LWPD "`.", i, src[i], dest); + switch (src[i]) { + /* ignore white space */ + case L' ': + case L'\t': + TRACE; + if (copying) { + *pos ++ = L'\''; /* end current token */ + copying = FALSE; + } + continue; /* don't copy WS */ + + case L',': + TRACE; + if (copying) { + *pos ++ = L'\''; /* end current token */ + copying = FALSE; + } /* else continue; -- to remove extra `,` */ + break; + + default: + TRACE; + if (! copying) { + *pos ++ = L'\''; /* start a new token */ + } + copying = TRUE; + } + *pos ++ = src[i]; + } + /* should not overrun */ + assert(i < 2/*see typ_buf below*/ * ESODBC_MAX_IDENTIFIER_LEN); + return pos - dest; +} SQLRETURN EsSQLTablesW( SQLHSTMT StatementHandle, @@ -146,29 +193,47 @@ SQLRETURN EsSQLTablesW( { esodbc_stmt_st *stmt = STMH(StatementHandle); SQLRETURN ret; - SQLWCHAR wbuf[sizeof(SQL_TABLES) + 2 * ESODBC_MAX_IDENTIFIER_LEN]; - SQLWCHAR *table, *schema, *catalog; - size_t cnt_tab, cnt_sch, cnt_cat, pos; + /* b/c declaring an array with a const doesn't work with MSVC's compiler */ + enum wbuf_len { wbuf_len = sizeof(SQL_TABLES) + + sizeof(SQL_TABLES_CAT) + + sizeof(SQL_TABLES_TAB) + + sizeof(SQL_TABLES_TYP) + + 3 * ESODBC_MAX_IDENTIFIER_LEN /* it has 4x 0-term space */ + }; + SQLWCHAR wbuf[wbuf_len]; + SQLWCHAR *table, *schema, *catalog, *type; + size_t cnt_tab, cnt_sch, cnt_cat, cnt_typ, pos; + /* 2x: "a,b,c" -> "'a','b','c'" : each "x," => "'x'," */ + SQLWCHAR typ_buf[2 * ESODBC_MAX_IDENTIFIER_LEN]; if (stmt->metadata_id == SQL_TRUE) FIXME; // FIXME + pos = sizeof(SQL_TABLES) - 1; + wmemcpy(wbuf, MK_WPTR(SQL_TABLES), pos); + if (CatalogName) { catalog = CatalogName; if (NameLength1 == SQL_NTS) { cnt_cat = wcslen(catalog); if (ESODBC_MAX_IDENTIFIER_LEN < cnt_cat) { ERRH(stmt, "catalog identifier name '" LTPDL "' too long " - "(%d. max=%d).", cnt_cat, catalog, cnt_cat, + "(%zd. max=%d).", (int)cnt_cat, catalog, cnt_cat, ESODBC_MAX_IDENTIFIER_LEN); RET_HDIAG(stmt, SQL_STATE_HY090, "catalog name too long", 0); } } else { cnt_cat = NameLength1; } - } else { - catalog = MK_WPTR(SQL_ALL_CATALOGS); - cnt_cat = sizeof(SQL_ALL_CATALOGS) - /*0-term*/1; + + cnt_cat = swprintf(wbuf + pos, wbuf_len - pos, SQL_TABLES_CAT, + (int)cnt_cat, catalog); + if (cnt_cat <= 0) { + ERRH(stmt, "failed to print 'catalog' for tables catalog SQL."); + RET_HDIAGS(stmt, SQL_STATE_HY000); + } else { + pos += cnt_cat; + } } if (SchemaName) { @@ -177,23 +242,20 @@ SQLRETURN EsSQLTablesW( cnt_sch = wcslen(schema); if (ESODBC_MAX_IDENTIFIER_LEN < cnt_sch) { ERRH(stmt, "schema identifier name '" LTPDL "' too long " - "(%d. max=%d).", cnt_sch, schema, cnt_sch, + "(%zd. max=%d).", (int)cnt_sch, schema, cnt_sch, ESODBC_MAX_IDENTIFIER_LEN); RET_HDIAG(stmt, SQL_STATE_HY090, "schema name too long", 0); } } else { cnt_sch = NameLength2; } - } else { - schema = MK_WPTR(SQL_ALL_SCHEMAS); - cnt_sch = sizeof(SQL_ALL_SCHEMAS) - /*0-term*/1; - } - /* TODO: server support needed for sch. name filtering */ - if (cnt_sch && wszmemcmp(schema, MK_WPTR(SQL_ALL_SCHEMAS), - (long)cnt_sch)) { - ERRH(stmt, "filtering by schemas is not supported."); - RET_HDIAG(stmt, SQL_STATE_IM001, "schema filtering not supported", 0); + /* TODO: server support needed for sch. name filtering */ + if (wszmemcmp(schema, MK_WPTR(SQL_ALL_SCHEMAS), (long)cnt_sch)) { + ERRH(stmt, "filtering by schemas is not supported."); + RET_HDIAG(stmt, SQL_STATE_IM001, "schema filtering not supported", + 0); + } } // FIXME: string needs escaping of % \\ _ @@ -203,31 +265,58 @@ SQLRETURN EsSQLTablesW( cnt_tab = wcslen(table); if (ESODBC_MAX_IDENTIFIER_LEN < cnt_tab) { ERRH(stmt, "table identifier name '" LTPDL "' too long " - "(%d. max=%d).", cnt_tab, table, cnt_tab, + "(%zd. max=%d).", (int)cnt_tab, table, cnt_tab, ESODBC_MAX_IDENTIFIER_LEN); RET_HDIAG(stmt, SQL_STATE_HY090, "table name too long", 0); } } else { cnt_tab = NameLength3; } - } else { - table = MK_WPTR(ESODBC_ALL_TABLES); - cnt_tab = sizeof(ESODBC_ALL_TABLES) - /*0-term*/1; - } -#if 1 // TODO: GH#4334 - if (cnt_tab == 0) { - table = MK_WPTR(ESODBC_ALL_TABLES); - cnt_tab = sizeof(ESODBC_ALL_TABLES) - /*0-term*/1; + + cnt_tab = swprintf(wbuf + pos, wbuf_len - pos, SQL_TABLES_TAB, + (int)cnt_tab, table); + if (cnt_tab <= 0) { + ERRH(stmt, "failed to print 'table' for tables catalog SQL."); + RET_HDIAGS(stmt, SQL_STATE_HY000); + } else { + pos += cnt_tab; + } } -#endif // 1 - /* print SQL to send to server */ - pos = swprintf(wbuf, sizeof(wbuf)/sizeof(wbuf[0]), SQL_TABLES, - (int)cnt_cat, catalog, (int)cnt_tab, table); - if (pos <= 0) { - ERRH(stmt, "failed to print 'tables' catalog SQL."); - RET_HDIAGS(stmt, SQL_STATE_HY000); +#if 1 // GH#30398 + if (TableType) { + type = TableType; + if (NameLength4 == SQL_NTS) { + cnt_typ = wcslen(type); + if (ESODBC_MAX_IDENTIFIER_LEN < cnt_typ) { + ERRH(stmt, "type identifier name '" LTPDL "' too long " + "(%zd. max=%d).", (int)cnt_typ, type, cnt_typ, + ESODBC_MAX_IDENTIFIER_LEN); + RET_HDIAG(stmt, SQL_STATE_HY090, "type name too long", 0); + } + } else { + cnt_typ = NameLength4; + } + + /* In this argument, "each value can be enclosed in single quotation + * marks (') or unquoted" => quote if not quoted (see GH#30398). */ + if (! wcsnstr(type, cnt_typ, L'\'')) { + cnt_typ = quote_tokens(type, cnt_typ, typ_buf); + type = typ_buf; + } + + cnt_typ = swprintf(wbuf + pos, wbuf_len - pos, SQL_TABLES_TYP, + (int)cnt_typ, type); + if (cnt_typ <= 0) { + ERRH(stmt, "failed to print 'type' for tables catalog SQL."); + RET_HDIAGS(stmt, SQL_STATE_HY000); + } else { + pos += cnt_typ; + } } +#endif // 0 + + DBGH(stmt, "tables catalog SQL [%d]:`" LWPDL "`.", pos, pos, wbuf); ret = EsSQLFreeStmt(stmt, ESODBC_SQL_CLOSE); assert(SQL_SUCCEEDED(ret)); /* can't return error */ diff --git a/driver/defs.h b/driver/defs.h index 416cfb9f..c45fda9d 100644 --- a/driver/defs.h +++ b/driver/defs.h @@ -59,7 +59,8 @@ #define ESODBC_MAX_CONCURRENT_ACTIVITIES 16 /* maximum identifer length */ /* TODO: review@alpha */ -#define ESODBC_MAX_IDENTIFIER_LEN 128 +/* match 'keyword' ES type lenght */ +#define ESODBC_MAX_IDENTIFIER_LEN 256 /* diff --git a/driver/util.c b/driver/util.c index 8c9f2f0d..07a107da 100644 --- a/driver/util.c +++ b/driver/util.c @@ -103,7 +103,7 @@ int wmemncasecmp(const SQLWCHAR *a, const SQLWCHAR *b, size_t len) if (diff) break; } - //DBG("`" LWPDL "` vs `" LWPDL "` => %d (len=%zd, i=%d).", + //DBG("`" LWPDL "` vs `" LWPDL "` => %d (len=%zd, i=%d).", // len, a, len, b, diff, len, i); return diff; } @@ -124,6 +124,17 @@ int wszmemcmp(const SQLWCHAR *a, const SQLWCHAR *b, long count) return *a - *b; } +const SQLWCHAR* wcsnstr(const SQLWCHAR *hay, size_t len, SQLWCHAR needle) +{ + size_t i; + for (i = 0; i < len; i ++) { + if (hay[i] == needle) { + return hay + i; + } + } + return NULL; +} + /* retuns the length of a buffer to hold the escaped variant of the unescaped * given json object */ static inline size_t json_escaped_len(const char *json, size_t len) diff --git a/driver/util.h b/driver/util.h index ec65951a..5fa0870e 100644 --- a/driver/util.h +++ b/driver/util.h @@ -85,7 +85,7 @@ int wmemncasecmp(const SQLWCHAR *a, const SQLWCHAR *b, size_t len); * either of them or until 'count' characters are evaluated. If 'count' * parameter is negative, it is ignored. * - * This is useful in comparing SQL strings which the API allows to be passed + * This is useful in comparing SQL strings which the API allows to be passed * either as 0-terminated or not (SQL_NTS). * The function does a single pass (no length evaluation of the strings). * wmemcmp() might read over the boundary of one of the objects, if the @@ -93,6 +93,11 @@ int wmemncasecmp(const SQLWCHAR *a, const SQLWCHAR *b, size_t len); */ int wszmemcmp(const SQLWCHAR *a, const SQLWCHAR *b, long count); +/* + * wcsstr() variant for non-NTS. + */ +const SQLWCHAR* wcsnstr(const SQLWCHAR *hay, size_t len, SQLWCHAR needle); + typedef struct wstr { SQLWCHAR *str; size_t cnt; @@ -119,7 +124,7 @@ BOOL wstr2bool(wstr_st *val); BOOL wstr2long(wstr_st *val, long *out); #ifdef _WIN32 -/* +/* * "[D]oes not null-terminate an output string if the input string length is * explicitly specified without a terminating null character. To * null-terminate an output string for this function, the application should @@ -155,7 +160,7 @@ typedef cstr_st tstr_st; #endif /* UNICODE */ -/* +/* * JSON-escapes a string. * If string len is 0, it assumes a NTS. * If output buffer (jout) is NULL, it returns the buffer size needed for From f1c0fb5f677726e0288418ab1f3c83daf7418038 Mon Sep 17 00:00:00 2001 From: Bogdan Pintea Date: Wed, 9 May 2018 19:40:22 +0200 Subject: [PATCH 4/5] remove forgotten debugging tracing --- driver/catalogue.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/driver/catalogue.c b/driver/catalogue.c index a6b542f7..e3473941 100644 --- a/driver/catalogue.c +++ b/driver/catalogue.c @@ -142,16 +142,13 @@ size_t quote_tokens(SQLWCHAR *src, size_t len, SQLWCHAR *dest) BOOL copying; SQLWCHAR *pos; - DBG("len=%d.", len); copying = FALSE; pos = dest; for (i = 0; i < len; i ++) { - DBG("src[%d]=%C, dest:`" LWPD "`.", i, src[i], dest); switch (src[i]) { /* ignore white space */ case L' ': case L'\t': - TRACE; if (copying) { *pos ++ = L'\''; /* end current token */ copying = FALSE; @@ -159,7 +156,6 @@ size_t quote_tokens(SQLWCHAR *src, size_t len, SQLWCHAR *dest) continue; /* don't copy WS */ case L',': - TRACE; if (copying) { *pos ++ = L'\''; /* end current token */ copying = FALSE; @@ -167,7 +163,6 @@ size_t quote_tokens(SQLWCHAR *src, size_t len, SQLWCHAR *dest) break; default: - TRACE; if (! copying) { *pos ++ = L'\''; /* start a new token */ } From 48714fe7b303509dd76ab071c85714cab8c1377f Mon Sep 17 00:00:00 2001 From: Bogdan Pintea Date: Thu, 10 May 2018 17:53:13 +0200 Subject: [PATCH 5/5] comment typo fixed, removed development defs --- driver/catalogue.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/driver/catalogue.c b/driver/catalogue.c index e3473941..7c750f42 100644 --- a/driver/catalogue.c +++ b/driver/catalogue.c @@ -32,7 +32,7 @@ #define SYS_CATALOGS \ "SYS CATALOGS" -/* SYS TABLES synthax tokens; these need to stay broken down, since this +/* SYS TABLES syntax tokens; these need to stay broken down, since this * query makes a difference between a predicate being '%' or left out */ // TODO: schema, when supported #define SQL_TABLES \ @@ -278,7 +278,6 @@ SQLRETURN EsSQLTablesW( } } -#if 1 // GH#30398 if (TableType) { type = TableType; if (NameLength4 == SQL_NTS) { @@ -309,7 +308,6 @@ SQLRETURN EsSQLTablesW( pos += cnt_typ; } } -#endif // 0 DBGH(stmt, "tables catalog SQL [%d]:`" LWPDL "`.", pos, pos, wbuf);