Skip to content
Permalink
Browse files
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)
  • Loading branch information
plamut committed Apr 16, 2021
1 parent e0b373d commit 6502a602337ae562652a20b20270949f2c9d5073
Showing with 56 additions and 3 deletions.
  1. +3 −3 google/cloud/bigquery/dbapi/cursor.py
  2. +53 −0 tests/unit/test_dbapi_cursor.py
@@ -393,7 +393,7 @@ def _format_operation_list(operation, parameters):

try:
return operation % tuple(formatted_params)
except TypeError as exc:
except (TypeError, ValueError) as exc:
raise exceptions.ProgrammingError(exc)


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

try:
return operation % formatted_params
except KeyError as exc:
except (KeyError, ValueError, TypeError) as exc:
raise exceptions.ProgrammingError(exc)


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

if isinstance(parameters, collections_abc.Mapping):
return _format_operation_dict(operation, parameters)
@@ -657,6 +657,14 @@ def test__format_operation_w_wrong_dict(self):
{"somevalue-not-here": "hi", "othervalue": "world"},
)

def test__format_operation_w_redundant_dict_key(self):
from google.cloud.bigquery.dbapi import cursor

formatted_operation = cursor._format_operation(
"SELECT %(somevalue)s;", {"somevalue": "foo", "value-not-used": "bar"}
)
self.assertEqual(formatted_operation, "SELECT @`somevalue`;")

def test__format_operation_w_sequence(self):
from google.cloud.bigquery.dbapi import cursor

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

def test__format_operation_w_too_long_sequence(self):
from google.cloud.bigquery import dbapi
from google.cloud.bigquery.dbapi import cursor

self.assertRaises(
dbapi.ProgrammingError,
cursor._format_operation,
"SELECT %s, %s;",
("hello", "world", "everyone"),
)

def test__format_operation_w_empty_dict(self):
from google.cloud.bigquery.dbapi import cursor

formatted_operation = cursor._format_operation("SELECT '%f'", {})
self.assertEqual(formatted_operation, "SELECT '%f'")

def test__format_operation_wo_params_single_percent(self):
from google.cloud.bigquery.dbapi import cursor

formatted_operation = cursor._format_operation("SELECT '%'", {})
self.assertEqual(formatted_operation, "SELECT '%'")

def test__format_operation_wo_params_double_percents(self):
from google.cloud.bigquery.dbapi import cursor

formatted_operation = cursor._format_operation("SELECT '%%'", {})
self.assertEqual(formatted_operation, "SELECT '%'")

def test__format_operation_unescaped_percent_w_dict_param(self):
from google.cloud.bigquery import dbapi
from google.cloud.bigquery.dbapi import cursor

self.assertRaises(
dbapi.ProgrammingError,
cursor._format_operation,
"SELECT %(foo)s, '100 %';",
{"foo": "bar"},
)

def test__format_operation_unescaped_percent_w_list_param(self):
from google.cloud.bigquery import dbapi
from google.cloud.bigquery.dbapi import cursor

self.assertRaises(
dbapi.ProgrammingError,
cursor._format_operation,
"SELECT %s, %s, '100 %';",
["foo", "bar"],
)

0 comments on commit 6502a60

Please sign in to comment.