Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #9055 -- Standardized behaviour of parameter escaping in db cur…

…sors

Previously, depending on the database backend or the cursor type,
you'd need to double the percent signs in the query before passing
it to cursor.execute. Now cursor.execute consistently need percent
doubling whenever params argument is not None (placeholder substitution
will happen).
Thanks Thomas Güttler for the report and Walter Doekes for his work
on the patch.
  • Loading branch information...
commit 76aecfbc4b49f5ab0613cccff1df6fab03253fab 1 parent e7514e4
Claude Paroz authored March 23, 2013
2  django/db/backends/__init__.py
@@ -815,6 +815,8 @@ def last_executed_query(self, cursor, sql, params):
815 815
         to_unicode = lambda s: force_text(s, strings_only=True, errors='replace')
816 816
         if isinstance(params, (list, tuple)):
817 817
             u_params = tuple(to_unicode(val) for val in params)
  818
+        elif params is None:
  819
+            u_params = ()
818 820
         else:
819 821
             u_params = dict((to_unicode(k), to_unicode(v)) for k, v in params.items())
820 822
 
1  django/db/backends/mysql/base.py
@@ -115,6 +115,7 @@ def __init__(self, cursor):
115 115
 
116 116
     def execute(self, query, args=None):
117 117
         try:
  118
+            # args is None means no string interpolation
118 119
             return self.cursor.execute(query, args)
119 120
         except Database.OperationalError as e:
120 121
             # Map some error codes to IntegrityError, since they seem to be
13  django/db/backends/oracle/base.py
@@ -757,18 +757,19 @@ def _param_generator(self, params):
757 757
         return [p.force_bytes for p in params]
758 758
 
759 759
     def execute(self, query, params=None):
760  
-        if params is None:
761  
-            params = []
762  
-        else:
763  
-            params = self._format_params(params)
764  
-        args = [(':arg%d' % i) for i in range(len(params))]
765 760
         # cx_Oracle wants no trailing ';' for SQL statements.  For PL/SQL, it
766 761
         # it does want a trailing ';' but not a trailing '/'.  However, these
767 762
         # characters must be included in the original query in case the query
768 763
         # is being passed to SQL*Plus.
769 764
         if query.endswith(';') or query.endswith('/'):
770 765
             query = query[:-1]
771  
-        query = convert_unicode(query % tuple(args), self.charset)
  766
+        if params is None:
  767
+            params = []
  768
+            query = convert_unicode(query, self.charset)
  769
+        else:
  770
+            params = self._format_params(params)
  771
+            args = [(':arg%d' % i) for i in range(len(params))]
  772
+            query = convert_unicode(query % tuple(args), self.charset)
772 773
         self._guess_input_sizes([params])
773 774
         try:
774 775
             return self.cursor.execute(query, self._param_generator(params))
4  django/db/backends/sqlite3/base.py
@@ -433,7 +433,9 @@ class SQLiteCursorWrapper(Database.Cursor):
433 433
     This fixes it -- but note that if you want to use a literal "%s" in a query,
434 434
     you'll need to use "%%s".
435 435
     """
436  
-    def execute(self, query, params=()):
  436
+    def execute(self, query, params=None):
  437
+        if params is None:
  438
+            return Database.Cursor.execute(self, query)
437 439
         query = self.convert_query(query)
438 440
         return Database.Cursor.execute(self, query, params)
439 441
 
5  django/db/backends/util.py
@@ -35,11 +35,14 @@ def __iter__(self):
35 35
 
36 36
 class CursorDebugWrapper(CursorWrapper):
37 37
 
38  
-    def execute(self, sql, params=()):
  38
+    def execute(self, sql, params=None):
39 39
         self.db.set_dirty()
40 40
         start = time()
41 41
         try:
42 42
             with self.db.wrap_database_errors():
  43
+                if params is None:
  44
+                    # params default might be backend specific
  45
+                    return self.cursor.execute(sql)
43 46
                 return self.cursor.execute(sql, params)
44 47
         finally:
45 48
             stop = time()
6  docs/topics/db/sql.txt
@@ -227,6 +227,12 @@ For example::
227 227
     were committed to the database. Since Django now defaults to database-level
228 228
     autocommit, this isn't necessary any longer.
229 229
 
  230
+Note that if you want to include literal percent signs in the query, you have to
  231
+double them in the case you are passing parameters::
  232
+
  233
+     cursor.execute("SELECT foo FROM bar WHERE baz = '30%'")
  234
+     cursor.execute("SELECT foo FROM bar WHERE baz = '30%%' and id = %s", [self.id])
  235
+
230 236
 If you are using :doc:`more than one database </topics/db/multi-db>`, you can
231 237
 use ``django.db.connections`` to obtain the connection (and cursor) for a
232 238
 specific database. ``django.db.connections`` is a dictionary-like
24  tests/backends/tests.py
@@ -361,18 +361,34 @@ def receiver(sender, connection, **kwargs):
361 361
 
362 362
 
363 363
 class EscapingChecks(TestCase):
  364
+    """
  365
+    All tests in this test case are also run with settings.DEBUG=True in
  366
+    EscapingChecksDebug test case, to also test CursorDebugWrapper.
  367
+    """
  368
+    def test_paramless_no_escaping(self):
  369
+        cursor = connection.cursor()
  370
+        cursor.execute("SELECT '%s'")
  371
+        self.assertEqual(cursor.fetchall()[0][0], '%s')
  372
+
  373
+    def test_parameter_escaping(self):
  374
+        cursor = connection.cursor()
  375
+        cursor.execute("SELECT '%%', %s", ('%d',))
  376
+        self.assertEqual(cursor.fetchall()[0], ('%', '%d'))
364 377
 
365 378
     @unittest.skipUnless(connection.vendor == 'sqlite',
366 379
                          "This is a sqlite-specific issue")
367  
-    def test_parameter_escaping(self):
  380
+    def test_sqlite_parameter_escaping(self):
368 381
         #13648: '%s' escaping support for sqlite3
369 382
         cursor = connection.cursor()
370  
-        response = cursor.execute(
371  
-            "select strftime('%%s', date('now'))").fetchall()[0][0]
372  
-        self.assertNotEqual(response, None)
  383
+        cursor.execute("select strftime('%s', date('now'))")
  384
+        response = cursor.fetchall()[0][0]
373 385
         # response should be an non-zero integer
374 386
         self.assertTrue(int(response))
375 387
 
  388
+@override_settings(DEBUG=True)
  389
+class EscapingChecksDebug(EscapingChecks):
  390
+    pass
  391
+
376 392
 
377 393
 class SqlliteAggregationTests(TestCase):
378 394
     """

0 notes on commit 76aecfb

Please sign in to comment.
Something went wrong with that request. Please try again.