Browse Source

Refs #14091 -- Fixed connection.queries on SQLite.

Aymeric Augustin 9 years ago
parent
commit
4f6a7663bc

+ 33 - 0
django/db/backends/sqlite3/operations.py

@@ -103,6 +103,39 @@ class DatabaseOperations(BaseDatabaseOperations):
     def pk_default_value(self):
         return "NULL"
 
+    def _quote_params_for_last_executed_query(self, params):
+        """
+        Only for last_executed_query! Don't use this to execute SQL queries!
+        """
+        sql = 'SELECT ' + ', '.join(['QUOTE(?)'] * len(params))
+        # Bypass Django's wrappers and use the underlying sqlite3 connection
+        # to avoid logging this query - it would trigger infinite recursion.
+        cursor = self.connection.connection.cursor()
+        # Native sqlite3 cursors cannot be used as context managers.
+        try:
+            return cursor.execute(sql, params).fetchone()
+        finally:
+            cursor.close()
+
+    def last_executed_query(self, cursor, sql, params):
+        # Python substitutes parameters in Modules/_sqlite/cursor.c with:
+        # pysqlite_statement_bind_parameters(self->statement, parameters, allow_8bit_chars);
+        # Unfortunately there is no way to reach self->statement from Python,
+        # so we quote and substitute parameters manually.
+        if params:
+            if isinstance(params, (list, tuple)):
+                params = self._quote_params_for_last_executed_query(params)
+            else:
+                keys = params.keys()
+                values = tuple(params.values())
+                values = self._quote_params_for_last_executed_query(values)
+                params = dict(zip(keys, values))
+            return sql % params
+        # For consistency with SQLiteCursorWrapper.execute(), just return sql
+        # when there are no parameters. See #13648 and #17158.
+        else:
+            return sql
+
     def quote_name(self, name):
         if name.startswith('"') and name.endswith('"'):
             return name  # Quoting once is enough.

+ 0 - 2
docs/faq/models.txt

@@ -23,8 +23,6 @@ the following::
 
 ``connection.queries`` includes all SQL statements -- INSERTs, UPDATES,
 SELECTs, etc. Each time your app hits the database, the query will be recorded.
-Note that the SQL recorded here may be :ref:`incorrectly quoted under SQLite
-<sqlite-connection-queries>`.
 
 If you are using :doc:`multiple databases</topics/db/multi-db>`, you can use the
 same interface on each member of the ``connections`` dictionary::

+ 0 - 10
docs/ref/databases.txt

@@ -704,16 +704,6 @@ can use the "pyformat" parameter style, where placeholders in the query
 are given as ``'%(name)s'`` and the parameters are passed as a dictionary
 rather than a list. SQLite does not support this.
 
-.. _sqlite-connection-queries:
-
-Parameters not quoted in ``connection.queries``
------------------------------------------------
-
-``sqlite3`` does not provide a way to retrieve the SQL after quoting and
-substituting the parameters. Instead, the SQL in ``connection.queries`` is
-rebuilt with a simple string interpolation. It may be incorrect. Make sure
-you add quotes where necessary before copying a query into an SQLite shell.
-
 .. _oracle-notes:
 
 Oracle notes

+ 2 - 0
docs/releases/1.9.txt

@@ -510,6 +510,8 @@ Models
 
 * Added support for referencing annotations in ``QuerySet.distinct()``.
 
+* ``connection.queries`` shows queries with substituted parameters on SQLite.
+
 CSRF
 ^^^^
 

+ 13 - 3
tests/backends/tests.py

@@ -26,7 +26,6 @@ from django.test import (
     SimpleTestCase, TestCase, TransactionTestCase, mock, override_settings,
     skipIfDBFeature, skipUnlessDBFeature,
 )
-from django.test.utils import str_prefix
 from django.utils import six
 from django.utils.six.moves import range
 
@@ -388,8 +387,19 @@ class LastExecutedQueryTest(TestCase):
         # This shouldn't raise an exception
         query = "SELECT strftime('%Y', 'now');"
         connection.cursor().execute(query)
-        self.assertEqual(connection.queries[-1]['sql'],
-            str_prefix("QUERY = %(_)s\"SELECT strftime('%%Y', 'now');\" - PARAMS = ()"))
+        self.assertEqual(connection.queries[-1]['sql'], query)
+
+    @unittest.skipUnless(connection.vendor == 'sqlite',
+                         "This test is specific to SQLite.")
+    def test_parameter_quoting_on_sqlite(self):
+        # The implementation of last_executed_queries isn't optimal. It's
+        # worth testing that parameters are quoted. See #14091.
+        query = "SELECT %s"
+        params = ["\"'\\"]
+        connection.cursor().execute(query, params)
+        # Note that the single quote is repeated
+        substituted = "SELECT '\"''\\'"
+        self.assertEqual(connection.queries[-1]['sql'], substituted)
 
 
 class ParameterHandlingTest(TestCase):

+ 12 - 36
tests/test_runner/test_debug_sql.py

@@ -61,28 +61,14 @@ class TestDebugSQL(unittest.TestCase):
         for output in self.verbose_expected_outputs:
             self.assertIn(output, full_output)
 
-    if six.PY3:
-        expected_outputs = [
-            ('''QUERY = 'SELECT COUNT(*) AS "__count" '''
-                '''FROM "test_runner_person" WHERE '''
-                '''"test_runner_person"."first_name" = %s' '''
-                '''- PARAMS = ('error',);'''),
-            ('''QUERY = 'SELECT COUNT(*) AS "__count" '''
-                '''FROM "test_runner_person" WHERE '''
-                '''"test_runner_person"."first_name" = %s' '''
-                '''- PARAMS = ('fail',);'''),
-        ]
-    else:
-        expected_outputs = [
-            ('''QUERY = u'SELECT COUNT(*) AS "__count" '''
-                '''FROM "test_runner_person" WHERE '''
-                '''"test_runner_person"."first_name" = %s' '''
-                '''- PARAMS = (u'error',);'''),
-            ('''QUERY = u'SELECT COUNT(*) AS "__count" '''
-                '''FROM "test_runner_person" WHERE '''
-                '''"test_runner_person"."first_name" = %s' '''
-                '''- PARAMS = (u'fail',);'''),
-        ]
+    expected_outputs = [
+        ('''SELECT COUNT(*) AS "__count" '''
+            '''FROM "test_runner_person" WHERE '''
+            '''"test_runner_person"."first_name" = 'error';'''),
+        ('''SELECT COUNT(*) AS "__count" '''
+            '''FROM "test_runner_person" WHERE '''
+            '''"test_runner_person"."first_name" = 'fail';'''),
+    ]
 
     verbose_expected_outputs = [
         # Output format changed in Python 3.5+
@@ -91,18 +77,8 @@ class TestDebugSQL(unittest.TestCase):
             'runTest (test_runner.test_debug_sql.{}ErrorTest) ... ERROR',
             'runTest (test_runner.test_debug_sql.{}PassingTest) ... ok',
         ]
+    ] + [
+        ('''SELECT COUNT(*) AS "__count" '''
+            '''FROM "test_runner_person" WHERE '''
+            '''"test_runner_person"."first_name" = 'pass';'''),
     ]
-    if six.PY3:
-        verbose_expected_outputs += [
-            ('''QUERY = 'SELECT COUNT(*) AS "__count" '''
-                '''FROM "test_runner_person" WHERE '''
-                '''"test_runner_person"."first_name" = %s' '''
-                '''- PARAMS = ('pass',);'''),
-        ]
-    else:
-        verbose_expected_outputs += [
-            ('''QUERY = u'SELECT COUNT(*) AS "__count" '''
-                '''FROM "test_runner_person" WHERE '''
-                '''"test_runner_person"."first_name" = %s' '''
-                '''- PARAMS = (u'pass',);'''),
-        ]