Skip to content

Commit 6502a60

Browse files
authored
fix: consistent percents handling in DB API query (#619)
Fixes #608. Percents in the query string are now always de-escaped, regardless of whether any query parameters are passed or not. In addition, misformatting placeholders that don't match parameter values now consistently raise `ProgrammingError`. **PR checklist:** - [x] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/python-bigquery/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [x] Ensure the tests and linter pass - [x] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary)
1 parent e0b373d commit 6502a60

File tree

2 files changed

+56
-3
lines changed

2 files changed

+56
-3
lines changed

google/cloud/bigquery/dbapi/cursor.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ def _format_operation_list(operation, parameters):
393393

394394
try:
395395
return operation % tuple(formatted_params)
396-
except TypeError as exc:
396+
except (TypeError, ValueError) as exc:
397397
raise exceptions.ProgrammingError(exc)
398398

399399

@@ -423,7 +423,7 @@ def _format_operation_dict(operation, parameters):
423423

424424
try:
425425
return operation % formatted_params
426-
except KeyError as exc:
426+
except (KeyError, ValueError, TypeError) as exc:
427427
raise exceptions.ProgrammingError(exc)
428428

429429

@@ -445,7 +445,7 @@ def _format_operation(operation, parameters=None):
445445
``parameters`` argument.
446446
"""
447447
if parameters is None or len(parameters) == 0:
448-
return operation
448+
return operation.replace("%%", "%") # Still do percent de-escaping.
449449

450450
if isinstance(parameters, collections_abc.Mapping):
451451
return _format_operation_dict(operation, parameters)

tests/unit/test_dbapi_cursor.py

+53
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,14 @@ def test__format_operation_w_wrong_dict(self):
657657
{"somevalue-not-here": "hi", "othervalue": "world"},
658658
)
659659

660+
def test__format_operation_w_redundant_dict_key(self):
661+
from google.cloud.bigquery.dbapi import cursor
662+
663+
formatted_operation = cursor._format_operation(
664+
"SELECT %(somevalue)s;", {"somevalue": "foo", "value-not-used": "bar"}
665+
)
666+
self.assertEqual(formatted_operation, "SELECT @`somevalue`;")
667+
660668
def test__format_operation_w_sequence(self):
661669
from google.cloud.bigquery.dbapi import cursor
662670

@@ -676,8 +684,53 @@ def test__format_operation_w_too_short_sequence(self):
676684
("hello",),
677685
)
678686

687+
def test__format_operation_w_too_long_sequence(self):
688+
from google.cloud.bigquery import dbapi
689+
from google.cloud.bigquery.dbapi import cursor
690+
691+
self.assertRaises(
692+
dbapi.ProgrammingError,
693+
cursor._format_operation,
694+
"SELECT %s, %s;",
695+
("hello", "world", "everyone"),
696+
)
697+
679698
def test__format_operation_w_empty_dict(self):
680699
from google.cloud.bigquery.dbapi import cursor
681700

682701
formatted_operation = cursor._format_operation("SELECT '%f'", {})
683702
self.assertEqual(formatted_operation, "SELECT '%f'")
703+
704+
def test__format_operation_wo_params_single_percent(self):
705+
from google.cloud.bigquery.dbapi import cursor
706+
707+
formatted_operation = cursor._format_operation("SELECT '%'", {})
708+
self.assertEqual(formatted_operation, "SELECT '%'")
709+
710+
def test__format_operation_wo_params_double_percents(self):
711+
from google.cloud.bigquery.dbapi import cursor
712+
713+
formatted_operation = cursor._format_operation("SELECT '%%'", {})
714+
self.assertEqual(formatted_operation, "SELECT '%'")
715+
716+
def test__format_operation_unescaped_percent_w_dict_param(self):
717+
from google.cloud.bigquery import dbapi
718+
from google.cloud.bigquery.dbapi import cursor
719+
720+
self.assertRaises(
721+
dbapi.ProgrammingError,
722+
cursor._format_operation,
723+
"SELECT %(foo)s, '100 %';",
724+
{"foo": "bar"},
725+
)
726+
727+
def test__format_operation_unescaped_percent_w_list_param(self):
728+
from google.cloud.bigquery import dbapi
729+
from google.cloud.bigquery.dbapi import cursor
730+
731+
self.assertRaises(
732+
dbapi.ProgrammingError,
733+
cursor._format_operation,
734+
"SELECT %s, %s, '100 %';",
735+
["foo", "bar"],
736+
)

0 commit comments

Comments
 (0)