Skip to content

Commit db67498

Browse files
authored
Fixed numeric parameter substitution bug (cloudera#508)
1 parent 0f2be72 commit db67498

File tree

2 files changed

+60
-50
lines changed

2 files changed

+60
-50
lines changed

impala/interface.py

Lines changed: 50 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -181,57 +181,60 @@ def __exit__(self, exc_type, exc_val, exc_tb):
181181

182182
def _replace_numeric_markers(operation, string_parameters, paramstyle):
183183
"""
184-
Replaces qname, format, and numeric markers in the given operation, from
184+
Replaces qmark, format, and numeric markers in the given operation, from
185185
the string_parameters list.
186186
187-
Raises ProgrammingError on wrong number of parameters or bindings
188-
when using qmark. There is no error checking on numeric parameters.
187+
Raises ProgrammingError on wrong number of parameters or markers.
188+
For numeric markers there has to be enough parameters to satisfy
189+
every marker and there has to bo no unused parameter.
189190
"""
190-
def replace_markers(marker, op, parameters):
191-
param_count = len(parameters)
192-
marker_index = 0
193-
start_offset = 0
194-
while True:
195-
found_offset = op.find(marker, start_offset)
196-
if not found_offset > -1:
197-
break
198-
if marker_index < param_count:
199-
op = op[:found_offset]+op[found_offset:].replace(marker, parameters[marker_index], 1)
200-
start_offset = found_offset + len(parameters[marker_index])
201-
marker_index += 1
202-
else:
203-
raise ProgrammingError("Incorrect number of bindings "
204-
"supplied. The current statement uses "
205-
"%d or more, and there are %d "
206-
"supplied." % (marker_index + 1,
207-
param_count))
208-
if marker_index != 0 and marker_index != param_count:
209-
raise ProgrammingError("Incorrect number of bindings "
210-
"supplied. The current statement uses "
211-
"%d or more, and there are %d supplied." %
212-
(marker_index + 1, param_count))
213-
return op
214-
215-
# replace qmark parameters and format parameters
216-
# If paramstyle is explicitly specified don't try to substitue them all
217-
if paramstyle == 'qmark' or paramstyle is None:
218-
operation = replace_markers('?', operation, string_parameters)
219-
if paramstyle == 'format' or paramstyle is None:
220-
operation = replace_markers(r'%s', operation, string_parameters)
221-
222-
# replace numbered parameters
223-
if paramstyle == 'numeric' or paramstyle is None:
224-
operation = re.sub(r'(:)(\d+)', r'{\2}', operation)
225-
# offset by one
226-
operation = operation.format(*[''] + string_parameters)
227-
228-
if paramstyle in ['named', 'pyformat']:
191+
RE_QMARK = r'(?P<qmark>\?)'
192+
RE_FORMAT = r'(?P<format>%s)'
193+
RE_NUMERIC = r'(?P<numeric>:(?P<index>\d+))'
194+
RE_ALL = '|'.join([RE_QMARK, RE_FORMAT, RE_NUMERIC])
195+
196+
if paramstyle is not None:
197+
if paramstyle in ['named', 'pyformat']:
198+
raise ProgrammingError(
199+
"Paramstyle '%s' is not compatible with parameters passed as "
200+
"list. Please use a dict for your parameters instead "
201+
"or specify a different paramstyle" % paramstyle)
202+
203+
if paramstyle not in ['qmark', 'format', 'numeric']:
204+
raise ProgrammingError(
205+
"Paramstyle '%s' is not supported. Please use a different one")
206+
207+
param_count = len(string_parameters)
208+
used_positional_indexes = set()
209+
used_numeric_indexes = set()
210+
211+
def replace_marker(match):
212+
if paramstyle is not None and match.group(paramstyle) is None:
213+
return match.group(0)
214+
215+
if match.group('index') is not None:
216+
param_index = int(match.group('index')) - 1
217+
used_numeric_indexes.add(param_index)
218+
else:
219+
param_index = len(used_positional_indexes)
220+
used_positional_indexes.add(param_index)
221+
222+
if param_index >= param_count:
223+
raise ProgrammingError(
224+
"Incorrect number of bindings supplied. The current statement "
225+
"uses %d or more, and there are %d supplied." % (
226+
param_index, param_count))
227+
228+
return string_parameters[param_index]
229+
230+
operation = re.sub(RE_ALL, replace_marker, operation)
231+
232+
marker_count = len(used_numeric_indexes | used_positional_indexes)
233+
if marker_count < param_count:
229234
raise ProgrammingError(
230-
"paramstyle '%s' is not compatible with parameters passed as List."
231-
"please use a dict for you parameters instead or specify"
232-
" a different paramstyle",
233-
paramstyle
234-
)
235+
"Incorrect number of bindings supplied. The current statement "
236+
"uses %d, and there are %d supplied." % (
237+
marker_count, param_count))
235238

236239
return operation
237240

impala/tests/test_query_parameters.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,15 @@ def test_numeric():
155155
"d=:4 and e=:5 and f=:6 and g=:7 and h=:8 and i=:9 and " +
156156
"j=:10 and k=:11",
157157
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'])
158+
bad_bindings = [
159+
("select * from test where int = :1", []),
160+
("select * from test where int = :1 or int = :2", [1]),
161+
("select * from test where int = :1", [1, 2]),
162+
("select * from test where int = :1 or float = :1", [1, 2])
163+
]
164+
for q in bad_bindings:
165+
with raises(ProgrammingError):
166+
dt("should have raised exception", q[0], q[1])
158167

159168

160169
def test_qmark():
@@ -284,9 +293,7 @@ def test_avoid_substitution():
284293
"select * from test where a=:x and b=:y",
285294
{'x': 'string :2 ? %s :named', 'y':'42'}, paramstyle='named')
286295

287-
# BUG: if a parameter is substituted with a string containing another parameter
288-
# specifier, double substitution can occur.
289-
dt("select * from test where a='string '42'' and b='42'",
296+
dt("select * from test where a='string :2' and b='42'",
290297
"select * from test where a=%s and b=%s",
291298
['string :2', '42'])
292299

0 commit comments

Comments
 (0)