Skip to content

Commit 80cb726

Browse files
committed
dbapi: unescape all %% before SQL upload to server
Ensures that every site that transforms sql will unescape escaped SQL format literals %% into plain %. Escaped SQL format literals exist because by default dbapi.Cursor.execute escapes %s as %%s, because %s is used as the format specifier. We transform '%s' into '@nAmed' variables so we should finish up the escaping before uploading the SQL to Cloud Spanner's server. Fixes #252 Fixes #347
1 parent 5d9cd5d commit 80cb726

File tree

4 files changed

+21
-12
lines changed

4 files changed

+21
-12
lines changed

packages/django-google-spanner/.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ env:
4848
- DJANGO_TEST_APPS="queries.test_q queries.test_qs_combinators queries.test_query"
4949
# Commented out because they take longer 2hr and TravisCI unconditionally terminates them.
5050
# - DJANGO_TEST_APPS="queries.tests"
51-
- DJANGO_TEST_APPS="raw_query redirects_tests reserved_names reverse_lookup"
51+
- DJANGO_TEST_APPS="queries.tests.Queries5Tests.test_extra_select_literal_percent_s raw_query redirects_tests reserved_names reverse_lookup"
5252
- DJANGO_TEST_APPS="save_delete_hooks select_related"
5353
- DJANGO_TEST_APPS="select_related_onetoone signing sitemaps_tests"
5454
- DJANGO_TEST_APPS="string_lookup signals"

packages/django-google-spanner/django_spanner/features.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -270,9 +270,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
270270
# Cloud Spanner limit: "Number of functions exceeds the maximum
271271
# allowed limit of 1000."
272272
'queries.test_bulk_update.BulkUpdateTests.test_large_batch',
273-
# QuerySet.extra() with select literal percent doesn't work:
274-
# https://github.com/orijtech/spanner-orm/issues/252
275-
'queries.tests.Queries5Tests.test_extra_select_literal_percent_s',
276273
# Spanner doesn't support random ordering.
277274
'ordering.tests.OrderingTests.test_random_ordering',
278275
# No matching signature for function MOD for argument types: FLOAT64,
@@ -307,10 +304,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
307304
# DatabaseIntrospection.get_relations() isn't implemented:
308305
# https://github.com/orijtech/django-spanner/issues/311
309306
'introspection.tests.IntrospectionTests.test_get_relations',
310-
# parameter escaping of % not working correctly:
311-
# https://github.com/orijtech/django-spanner/issues/347
312-
'backends.tests.EscapingChecks.test_parameter_escaping',
313-
'backends.tests.EscapingChecksDebug.test_parameter_escaping',
314307
# Non-ascii SELECT alias crashes "Syntax error: Illegal input character"
315308
# https://github.com/orijtech/django-spanner/issues/341
316309
'backends.tests.LastExecutedQueryTest.test_query_encoding',

packages/django-google-spanner/spanner/dbapi/parse_utils.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .exceptions import Error, ProgrammingError
1616
from .parser import parse_values
1717
from .types import DateStr, TimestampStr
18+
from .utils import unescape_percent_literals
1819

1920
STMT_DDL = 'DDL'
2021
STMT_NON_UPDATING = 'NON_UPDATING'
@@ -143,6 +144,7 @@ def parse_insert(insert_sql, params):
143144
after_values_sql = re_VALUES_TILL_END.findall(insert_sql)
144145
if not after_values_sql:
145146
# Case b)
147+
insert_sql = unescape_percent_literals(insert_sql)
146148
return {
147149
'sql_params_list': [(insert_sql, None,)],
148150
}
@@ -154,6 +156,7 @@ def parse_insert(insert_sql, params):
154156
if pyformat_str_count > 0:
155157
raise ProgrammingError('no params yet there are %d "%s" tokens' % pyformat_str_count)
156158

159+
insert_sql = unescape_percent_literals(insert_sql)
157160
# Confirmed case of:
158161
# SQL: INSERT INTO T (a1, a2) VALUES (1, 2)
159162
# Params: None
@@ -177,6 +180,7 @@ def parse_insert(insert_sql, params):
177180
)
178181
values_pyformat = [str(arg) for arg in values.argv]
179182
rows_list = rows_for_insert_or_update(columns, params, values_pyformat)
183+
insert_sql_preamble = unescape_percent_literals(insert_sql_preamble)
180184
for row in rows_list:
181185
sql_params_list.append((insert_sql_preamble, row,))
182186

@@ -202,6 +206,7 @@ def parse_insert(insert_sql, params):
202206
sql_param_tuples = []
203207
for token_arg in values.argv:
204208
row_sql = before_values_sql + ' VALUES%s' % token_arg
209+
row_sql = unescape_percent_literals(row_sql)
205210
row_params, params = tuple(params[0:len(token_arg)]), params[len(token_arg):]
206211
sql_param_tuples.append((row_sql, row_params,))
207212

@@ -297,6 +302,8 @@ def rows_for_insert_or_update(columns, params, pyformat_args=None):
297302
def sql_pyformat_args_to_spanner(sql, params):
298303
"""
299304
Transform pyformat set SQL to named arguments for Cloud Spanner.
305+
It will also unescape previously escaped format specifiers
306+
like %%s to %s.
300307
For example:
301308
SQL: 'SELECT * from t where f1=%s, f2=%s, f3=%s'
302309
Params: ('a', 23, '888***')
@@ -312,14 +319,14 @@ def sql_pyformat_args_to_spanner(sql, params):
312319
Params: {'a0': 'a', 'a1': 23, 'a2': '888***'}
313320
"""
314321
if not params:
315-
return sql, params
322+
return unescape_percent_literals(sql), params
316323

317324
found_pyformat_placeholders = re_PYFORMAT.findall(sql)
318325
params_is_dict = isinstance(params, dict)
319326

320327
if params_is_dict:
321328
if not found_pyformat_placeholders:
322-
return sql, params
329+
return unescape_percent_literals(sql), params
323330
else:
324331
n_params = len(params) if params else 0
325332
n_matches = len(found_pyformat_placeholders)
@@ -329,7 +336,7 @@ def sql_pyformat_args_to_spanner(sql, params):
329336
'want %d args in %s' % (n_matches, found_pyformat_placeholders, n_params, params))
330337

331338
if len(params) == 0:
332-
return sql, params
339+
return unescape_percent_literals(sql), params
333340

334341
named_args = {}
335342
# We've now got for example:
@@ -347,7 +354,7 @@ def sql_pyformat_args_to_spanner(sql, params):
347354
else:
348355
named_args[key] = cast_for_spanner(params[i])
349356

350-
return sql, named_args
357+
return unescape_percent_literals(sql), named_args
351358

352359

353360
def cast_for_spanner(param):

packages/django-google-spanner/spanner/dbapi/utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,12 @@ def get_table_column_schema(spanner_db, table_name):
7373
)
7474

7575
return column_details
76+
77+
78+
def unescape_percent_literals(s):
79+
"""
80+
Convert %% (escaped percent literals) to %. Percent signs must be escaped when
81+
values like %s are used as SQL parameter placeholders but Spanner's query language
82+
uses placeholders like @a0 and doesn't expect percent signs to be escaped.
83+
"""
84+
return s.replace('%%', '%')

0 commit comments

Comments
 (0)