Skip to content
This repository was archived by the owner on Mar 20, 2026. It is now read-only.

Commit af5d8e3

Browse files
author
Ilya Gurov
authored
feat: Add dummy WHERE clause to certain statements (#516)
1 parent 196c449 commit af5d8e3

6 files changed

Lines changed: 50 additions & 34 deletions

File tree

django_spanner/compiler.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
SQLInsertCompiler as BaseSQLInsertCompiler,
1313
SQLUpdateCompiler as BaseSQLUpdateCompiler,
1414
)
15-
from django.db.utils import DatabaseError
15+
from django.db.utils import DatabaseError, add_dummy_where
1616

1717

1818
class SQLCompiler(BaseSQLCompiler):
@@ -90,6 +90,8 @@ def get_combinator_sql(self, combinator, all):
9090
params = []
9191
for part in args_parts:
9292
params.extend(part)
93+
94+
result = add_dummy_where(result)
9395
return result, params
9496

9597

django_spanner/utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1+
# Copyright 2020 Google LLC
2+
#
3+
# Use of this source code is governed by a BSD-style
4+
# license that can be found in the LICENSE file or at
5+
# https://developers.google.com/open-source/licenses/bsd
6+
17
import django
8+
import sqlparse
29
from django.core.exceptions import ImproperlyConfigured
310
from django.utils.version import get_version_tuple
411

@@ -18,3 +25,17 @@ def check_django_compatability():
1825
A=django.VERSION[0], B=django.VERSION[1], C=__version__
1926
)
2027
)
28+
29+
30+
def add_dummy_where(sql):
31+
"""
32+
Cloud Spanner requires a WHERE clause on UPDATE and DELETE statements.
33+
Add a dummy WHERE clause if necessary.
34+
"""
35+
if any(
36+
isinstance(token, sqlparse.sql.Where)
37+
for token in sqlparse.parse(sql)[0]
38+
):
39+
return sql
40+
41+
return sql + " WHERE 1=1"

google/cloud/spanner_dbapi/cursor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def close(self):
117117
self._is_closed = True
118118

119119
def _do_execute_update(self, transaction, sql, params, param_types=None):
120-
sql = parse_utils.ensure_where_clause(sql)
120+
parse_utils.ensure_where_clause(sql)
121121
sql, params = parse_utils.sql_pyformat_args_to_spanner(sql, params)
122122

123123
result = transaction.execute_update(

google/cloud/spanner_dbapi/parse_utils.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -515,15 +515,18 @@ def get_param_types(params):
515515

516516
def ensure_where_clause(sql):
517517
"""
518-
Cloud Spanner requires a WHERE clause on UPDATE and DELETE statements.
519-
Add a dummy WHERE clause if necessary.
518+
Raise unless `sql` includes a WHERE clause.
519+
520+
:type sql: str
521+
:param sql: SQL statement to check.
520522
"""
521-
if any(
523+
if not any(
522524
isinstance(token, sqlparse.sql.Where)
523525
for token in sqlparse.parse(sql)[0]
524526
):
525-
return sql
526-
return sql + " WHERE 1=1"
527+
raise ProgrammingError(
528+
"Cloud Spanner requires a WHERE clause in UPDATE and DELETE statements"
529+
)
527530

528531

529532
def escape_name(name):

tests/unit/spanner_dbapi/test_cursor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def test_do_execute_update(self):
9494
def run_helper(ret_value):
9595
transaction.execute_update.return_value = ret_value
9696
res = cursor._do_execute_update(
97-
transaction=transaction, sql="sql", params=None,
97+
transaction=transaction, sql="SELECT * WHERE true", params={},
9898
)
9999
return res
100100

tests/unit/spanner_dbapi/test_parse_utils.py

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -407,36 +407,26 @@ def test_get_param_types_none(self):
407407
self.assertEqual(get_param_types(None), None)
408408

409409
def test_ensure_where_clause(self):
410+
from google.cloud.spanner_dbapi.exceptions import ProgrammingError
410411
from google.cloud.spanner_dbapi.parse_utils import ensure_where_clause
411412

412-
cases = [
413-
(
414-
"UPDATE a SET a.b=10 FROM articles a JOIN d c ON a.ai = c.ai WHERE c.ci = 1",
415-
"UPDATE a SET a.b=10 FROM articles a JOIN d c ON a.ai = c.ai WHERE c.ci = 1",
416-
),
417-
(
418-
"UPDATE (SELECT * FROM A JOIN c ON ai.id = c.id WHERE cl.ci = 1) SET d=5",
419-
"UPDATE (SELECT * FROM A JOIN c ON ai.id = c.id WHERE cl.ci = 1) SET d=5 WHERE 1=1",
420-
),
421-
(
422-
"UPDATE T SET A = 1 WHERE C1 = 1 AND C2 = 2",
423-
"UPDATE T SET A = 1 WHERE C1 = 1 AND C2 = 2",
424-
),
425-
(
426-
"UPDATE T SET r=r*0.9 WHERE id IN (SELECT id FROM items WHERE r / w >= 1.3 AND q > 100)",
427-
"UPDATE T SET r=r*0.9 WHERE id IN (SELECT id FROM items WHERE r / w >= 1.3 AND q > 100)",
428-
),
429-
(
430-
"UPDATE T SET r=r*0.9 WHERE id IN (SELECT id FROM items WHERE r / w >= 1.3 AND q > 100)",
431-
"UPDATE T SET r=r*0.9 WHERE id IN (SELECT id FROM items WHERE r / w >= 1.3 AND q > 100)",
432-
),
433-
("DELETE * FROM TABLE", "DELETE * FROM TABLE WHERE 1=1"),
434-
]
413+
cases = (
414+
"UPDATE a SET a.b=10 FROM articles a JOIN d c ON a.ai = c.ai WHERE c.ci = 1",
415+
"UPDATE T SET A = 1 WHERE C1 = 1 AND C2 = 2",
416+
"UPDATE T SET r=r*0.9 WHERE id IN (SELECT id FROM items WHERE r / w >= 1.3 AND q > 100)",
417+
)
418+
err_cases = (
419+
"UPDATE (SELECT * FROM A JOIN c ON ai.id = c.id WHERE cl.ci = 1) SET d=5",
420+
"DELETE * FROM TABLE",
421+
)
422+
for sql in cases:
423+
with self.subTest(sql=sql):
424+
ensure_where_clause(sql)
435425

436-
for sql, want in cases:
426+
for sql in err_cases:
437427
with self.subTest(sql=sql):
438-
got = ensure_where_clause(sql)
439-
self.assertEqual(got, want)
428+
with self.assertRaises(ProgrammingError):
429+
ensure_where_clause(sql)
440430

441431
def test_escape_name(self):
442432
from google.cloud.spanner_dbapi.parse_utils import escape_name

0 commit comments

Comments
 (0)