Skip to content

Commit 4f1414d

Browse files
committed
SQLNumParams now counts parameter markers (#174)
This commit adds an simple counter of the parameter markers within an attached SQL statement. While a correct and complete implementation would require in our case sending the statement to the server for parameter analysis, this simple implementation should work for most cases where the application simply wants to validate the number of user-provided paramters number against the number of markers in the statement. (cherry picked from commit cb5be6f)
1 parent 616a641 commit 4f1414d

File tree

9 files changed

+249
-6
lines changed

9 files changed

+249
-6
lines changed

driver/defs.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
#define ESODBC_CATALOG_TERM "catalog"
5656
#define ESODBC_TABLE_TERM "table"
5757
#define ESODBC_SCHEMA_TERM "schema"
58-
#define ESODBC_PARAM_MARKER "?"
58+
#define ESODBC_PARAM_MARKER '?'
5959

6060
/* maximum identifer length: match ES/Lucene byte max */
6161
#define ESODBC_MAX_IDENTIFIER_LEN SHRT_MAX

driver/odbc.c

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
#include "catalogue.h"
1515
#include "tinycbor.h"
1616

17-
//#include "elasticodbc_export.h"
18-
//#define SQL_API ELASTICODBC_EXPORT SQL_API
19-
2017

2118
#define RET_NOT_IMPLEMENTED(hnd) \
2219
do { \

driver/queries.c

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3921,6 +3921,85 @@ SQLRETURN EsSQLColAttributeW(
39213921
#undef PNUMATTR_ASSIGN
39223922
}
39233923

3924+
/* very simple counter of non-quoted, not-escaped single question marks.
3925+
* no statement validation done */
3926+
static SQLRETURN count_param_markers(esodbc_stmt_st *stmt, SQLSMALLINT *p_cnt)
3927+
{
3928+
SQLSMALLINT cnt;
3929+
wstr_st u16sql;
3930+
SQLWCHAR *pos, *end, *sav;
3931+
BOOL quoting, escaping;
3932+
SQLWCHAR crr, qchar; /* char that starting the quote (`'` or `"`) */
3933+
3934+
/* The SQL query is received originally as UTF-16 SQLWCHAR, but converted
3935+
* to UTF-8 MB right away and stored like that, for conveience. Easiest to
3936+
* parse it is in its original SQLWCHAR format, though and since that's
3937+
* only needed (assumably) rarely, we'll convert it back here, instead of
3938+
* storing also the original. */
3939+
if (&u16sql != utf8_to_wstr(&stmt->u8sql, &u16sql)) {
3940+
ERRH(stmt, "failed to convert stmt `" LCPDL "` back to UTF-16 WC",
3941+
LCSTR(&stmt->u8sql));
3942+
RET_HDIAGS(stmt, SQL_STATE_HY000);
3943+
}
3944+
pos = u16sql.str;
3945+
end = pos + u16sql.cnt;
3946+
cnt = 0;
3947+
quoting = FALSE;
3948+
escaping = FALSE;
3949+
crr = 0;
3950+
while (pos < end) {
3951+
switch((crr = *pos ++)) {
3952+
case MK_WPTR(ESODBC_PARAM_MARKER): /* ~ L'?' */
3953+
if (escaping || quoting) {
3954+
break;
3955+
}
3956+
/* skip groups `???` */
3957+
sav = pos - 1; /* position of first `?` */
3958+
while (pos < end && *pos == crr) {
3959+
pos ++;
3960+
}
3961+
/* only count if single */
3962+
if (sav + 1 == pos) {
3963+
cnt ++;
3964+
}
3965+
break;
3966+
case L'"':
3967+
case L'\'':
3968+
if (escaping) {
3969+
break;
3970+
}
3971+
if (! quoting) {
3972+
quoting = TRUE;
3973+
qchar = crr;
3974+
} else if (qchar == crr) {
3975+
quoting = FALSE;
3976+
} /* else: sequence is: `"..'` or `'.."` -> ignore */
3977+
break;
3978+
case MK_WPTR(ESODBC_CHAR_ESCAPE): /* ~ L'\\' */
3979+
if (escaping) {
3980+
break;
3981+
}
3982+
escaping = TRUE;
3983+
continue;
3984+
}
3985+
escaping = FALSE;
3986+
}
3987+
3988+
if (escaping || quoting) {
3989+
ERRH(stmt, "invalid SQL statement: [%zu] `" LWPDL "`.", u16sql.cnt,
3990+
LWSTR(&u16sql));
3991+
free(u16sql.str);
3992+
RET_HDIAG(stmt, SQL_STATE_HY000, "failed to parse statement", 0);
3993+
}
3994+
3995+
DBGH(stmt, "counted %hd param marker(s) in SQL `" LWPDL "`.", cnt,
3996+
LWSTR(&u16sql));
3997+
free(u16sql.str);
3998+
3999+
*p_cnt = cnt;
4000+
return SQL_SUCCESS;
4001+
}
4002+
39244003
/* function implementation is correct, but it can't really be used as
39254004
* intended, since the driver's "preparation" doesn't really involve sending
39264005
* it to ES or even parameter marker counting; the later would be doable now,
@@ -3938,8 +4017,16 @@ SQLRETURN EsSQLNumParams(
39384017
RET_HDIAGS(stmt, SQL_STATE_HY010);
39394018
}
39404019

4020+
# if 0
4021+
/* The correct implementation, once a statement trully is prepared. */
39414022
return EsSQLGetDescFieldW(stmt->ipd, NO_REC_NR,
39424023
SQL_DESC_COUNT, ParameterCountPtr, SQL_IS_SMALLINT, NULL);
4024+
# else /* 0 */
4025+
/* Only count params on-demand */
4026+
/* some apps (like pyodbc) will verify the user-provided parameters number
4027+
* against the result of this function -> implement a crude parser. */
4028+
return count_param_markers(stmt, ParameterCountPtr);
4029+
# endif /* 0 */
39434030
}
39444031

39454032
SQLRETURN EsSQLRowCount(_In_ SQLHSTMT StatementHandle, _Out_ SQLLEN *RowCount)

driver/util.c

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -588,7 +588,7 @@ cstr_st TEST_API *wstr_to_utf8(wstr_st *src, cstr_st *dst)
588588
/* eval the needed space for conversion */
589589
len = U16WC_TO_MBU8(src->str, src->cnt, NULL, 0);
590590
if (! len) {
591-
ERRN("failed to evaluate UTF-8 conversion space necessary for [%zu] "
591+
ERRN("failed to eval UTF-8 conversion space necessary for [%zu] "
592592
"`" LWPDL "`.", src->cnt, LWSTR(src));
593593
return NULL;
594594
}
@@ -634,6 +634,65 @@ cstr_st TEST_API *wstr_to_utf8(wstr_st *src, cstr_st *dst)
634634
return dst;
635635
}
636636

637+
wstr_st TEST_API *utf8_to_wstr(cstr_st *src, wstr_st *dst)
638+
{
639+
int len;
640+
size_t cnt;
641+
void *addr;
642+
BOOL nts; /* is the \0 present and counted in source string? */
643+
644+
if (0 < src->cnt) {
645+
nts = !src->str[src->cnt - 1];
646+
647+
/* eval the needed space for conversion */
648+
len = U8MB_TO_U16WC(src->str, src->cnt, NULL, 0);
649+
if (! len) {
650+
ERRN("failed to eval UTF-16 conversion space necessary for [%zu] "
651+
"`" LCPDL "`.", src->cnt, LCSTR(src));
652+
return NULL;
653+
}
654+
} else {
655+
nts = FALSE;
656+
len = 0;
657+
}
658+
659+
assert(0 <= len);
660+
/* explicitely allocate the \0 if not present&counted */
661+
cnt = len + /*0-term?*/!nts;
662+
if (! dst) { /* if null destination, allocate that as well */
663+
cnt += sizeof(wstr_st);
664+
}
665+
666+
if (! (addr = malloc(cnt * sizeof(SQLWCHAR)))) {
667+
ERRN("OOM for size: %zuB.", cnt);
668+
return NULL;
669+
}
670+
if (! dst) {
671+
dst = (wstr_st *)addr;
672+
dst->str = (SQLWCHAR *)((uint8_t *)addr + sizeof(wstr_st));
673+
} else {
674+
dst->str = (SQLWCHAR *)addr;
675+
}
676+
677+
if (0 < src->cnt) {
678+
/* convert the string */
679+
len = U8MB_TO_U16WC(src->str, src->cnt, dst->str, len);
680+
if (! len) {
681+
/* should not happen, since a first scan already happened */
682+
ERRN("failed to UTF-16 convert `" LCPDL "`.", LCSTR(src));
683+
free(addr);
684+
return NULL;
685+
}
686+
}
687+
688+
if (! nts) {
689+
dst->str[len] = 0;
690+
}
691+
dst->cnt = len;
692+
693+
return dst;
694+
}
695+
637696
/* Escape `%`, `_`, `\` characters in 'src'.
638697
* If not 'force'-d, the escaping will stop on detection of pre-existing
639698
* escaping(*), OR if the chars to be escaped are stand-alone.

driver/util.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ SQLRETURN write_wstr(SQLHANDLE hnd, SQLWCHAR *dest, wstr_st *src,
274274
SQLSMALLINT /*B*/avail, SQLSMALLINT /*B*/*usedp);
275275

276276
/*
277-
* Converts a wide string to a UTF-8 MB, allocating the necessary space.
277+
* Converts a UTF-16 wide string to a UTF-8 MB, allocating the necessary space.
278278
* The \0 is allocated and written, even if not present in source string, but
279279
* only counted in output string if counted in input one.
280280
* If 'dst' is null, the destination is also going to be allocate (collated
@@ -284,6 +284,9 @@ SQLRETURN write_wstr(SQLHANDLE hnd, SQLWCHAR *dest, wstr_st *src,
284284
*/
285285
cstr_st TEST_API *wstr_to_utf8(wstr_st *src, cstr_st *dst);
286286

287+
/* the inverse of wstr_to_utf8(). */
288+
wstr_st TEST_API *utf8_to_wstr(cstr_st *src, wstr_st *dst);
289+
287290
/* Escape `%`, `_`, `\` characters in 'src'.
288291
* If not 'force'-d, the escaping will stop on detection of pre-existing
289292
* escaping(*), OR if the chars to be escaped are stand-alone.

test/connected_dbc.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,12 @@ void ConnectedDBC::prepareStatement()
213213
ASSERT_TRUE(SQL_SUCCEEDED(ret));
214214
}
215215

216+
void ConnectedDBC::prepareStatement(const SQLWCHAR *sql)
217+
{
218+
ret = ATTACH_SQL(stmt, sql, wcslen(sql));
219+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
220+
}
221+
216222
void ConnectedDBC::prepareStatement(const SQLWCHAR *sql,
217223
const char *jsonAnswer)
218224
{

test/connected_dbc.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ class ConnectedDBC {
5757
// use the test name as SQL (for faster logs lookup)
5858
void prepareStatement();
5959
// use an actual SQL statement (if it might be processed)
60+
void prepareStatement(const SQLWCHAR *sql);
61+
// use an actual SQL statement (if it might be processed)
6062
void prepareStatement(const SQLWCHAR *sql, const char *jsonAnswer);
6163
// use test name as SQL and attach given answer
6264
void prepareStatement(const char *jsonAnswer);

test/test_queries.cc

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,77 @@ TEST_F(Queries, SQLNativeSql_truncate) {
117117
ASSERT_TRUE(wcsncmp(buff, src, sizeof(buff)/sizeof(*buff) - 1) == 0);
118118
}
119119

120+
TEST_F(Queries, SQLNumParams_no_markers) {
121+
prepareStatement(MK_WPTR("SELECT 1"));
122+
SQLRETURN ret;
123+
SQLSMALLINT params = -1;
124+
ret = SQLNumParams(stmt, &params);
125+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
126+
ASSERT_EQ(params, 0);
127+
}
128+
129+
TEST_F(Queries, SQLNumParams_markers) {
130+
prepareStatement(MK_WPTR("SELECT * FROM foo WHERE bar = ? AND baz = ?"));
131+
SQLRETURN ret;
132+
SQLSMALLINT params = -1;
133+
ret = SQLNumParams(stmt, &params);
134+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
135+
ASSERT_EQ(params, 2);
136+
}
137+
138+
TEST_F(Queries, SQLNumParams_quoted) {
139+
prepareStatement(MK_WPTR("foo ' ?' ?"));
140+
SQLRETURN ret;
141+
SQLSMALLINT params = -1;
142+
ret = SQLNumParams(stmt, &params);
143+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
144+
ASSERT_EQ(params, 1);
145+
}
146+
147+
TEST_F(Queries, SQLNumParams_quoted_escaped) {
148+
prepareStatement(MK_WPTR("foo ' \\?' ? ?"));
149+
SQLRETURN ret;
150+
SQLSMALLINT params = -1;
151+
ret = SQLNumParams(stmt, &params);
152+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
153+
ASSERT_EQ(params, 2);
154+
}
155+
156+
TEST_F(Queries, SQLNumParams_escaped) {
157+
prepareStatement(MK_WPTR("foo \\? ?"));
158+
SQLRETURN ret;
159+
SQLSMALLINT params = -1;
160+
ret = SQLNumParams(stmt, &params);
161+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
162+
ASSERT_EQ(params, 1);
163+
}
164+
165+
TEST_F(Queries, SQLNumParams_invalid) {
166+
prepareStatement(MK_WPTR("foo '\\? ?"));
167+
SQLRETURN ret;
168+
SQLSMALLINT params;
169+
ret = SQLNumParams(stmt, &params);
170+
ASSERT_FALSE(SQL_SUCCEEDED(ret));
171+
}
172+
173+
TEST_F(Queries, SQLNumParams_duplicates) {
174+
prepareStatement(MK_WPTR("foo ??? ?"));
175+
SQLRETURN ret;
176+
SQLSMALLINT params = -1;
177+
ret = SQLNumParams(stmt, &params);
178+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
179+
ASSERT_EQ(params, 1);
180+
}
181+
182+
TEST_F(Queries, SQLNumParams_duplicates_escape) {
183+
prepareStatement(MK_WPTR("foo ? \\??? ??? ? ??"));
184+
SQLRETURN ret;
185+
SQLSMALLINT params = -1;
186+
ret = SQLNumParams(stmt, &params);
187+
ASSERT_TRUE(SQL_SUCCEEDED(ret));
188+
ASSERT_EQ(params, 2);
189+
}
190+
120191
} // test namespace
121192

122193
/* vim: set noet fenc=utf-8 ff=dos sts=0 sw=4 ts=4 : */

test/test_util.cc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,24 @@ TEST_F(Util, wstr_to_utf8_no_nts) {
105105
free(dst.str);
106106
}
107107

108+
TEST_F(Util, utf8_to_wstr_unicode) {
109+
#undef SRC_STR
110+
#undef SRC_AID
111+
#define SRC_STR "XäXüXßX"
112+
#define SRC_AID "X\xC3\xA4X\xC3\xBCX\xC3\x9FX"
113+
wstr_st src = WSTR_INIT(SRC_STR);
114+
cstr_st dst_mb;
115+
wstr_st dst_wc;
116+
117+
ASSERT_EQ(&dst_mb, wstr_to_utf8(&src, &dst_mb));
118+
ASSERT_EQ(&dst_wc, utf8_to_wstr(&dst_mb, &dst_wc));
119+
ASSERT_STREQ((wchar_t *)src.str, (wchar_t *)dst_wc.str);
120+
free(dst_mb.str);
121+
free(dst_wc.str);
122+
}
123+
124+
125+
108126
TEST_F(Util, ascii_c2w2c)
109127
{
110128
#undef SRC_STR

0 commit comments

Comments
 (0)