diff --git a/AUTHORS b/AUTHORS index 5760128368..ddd3cf8566 100644 --- a/AUTHORS +++ b/AUTHORS @@ -60,6 +60,7 @@ answer newbie questions, and generally made Django that much better: atlithorn Jökull Sólberg Auðunsson Arthur + Aymeric Augustin av0000@mail.ru David Avsajanishvili Mike Axiak diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 16067c6807..6d02aa771c 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -191,6 +191,12 @@ class DatabaseOperations(BaseDatabaseOperations): def fulltext_search_sql(self, field_name): return 'MATCH (%s) AGAINST (%%s IN BOOLEAN MODE)' % field_name + def last_executed_query(self, cursor, sql, params): + # With MySQLdb, cursor objects have an (undocumented) "_last_executed" + # attribute where the exact query sent to the database is saved. + # See MySQLdb/cursors.py in the source distribution. + return cursor._last_executed + def no_limit_value(self): # 2**64 - 1, as recommended by the MySQL documentation return 18446744073709551615L diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index bcb906762b..c024e6d7d0 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -210,6 +210,11 @@ WHEN (new.%(col_name)s IS NULL) else: return "%s" + def last_executed_query(self, cursor, sql, params): + # http://cx-oracle.sourceforge.net/html/cursor.html#Cursor.statement + # The DB API definition does not define this attribute. + return cursor.statement + def last_insert_id(self, cursor, table_name, pk_name): sq_name = self._get_sequence_name(table_name) cursor.execute('SELECT "%s".currval FROM dual' % sq_name) diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py index 4efdb8fa3e..33159133d6 100644 --- a/django/db/backends/postgresql_psycopg2/operations.py +++ b/django/db/backends/postgresql_psycopg2/operations.py @@ -202,9 +202,8 @@ class DatabaseOperations(BaseDatabaseOperations): return 63 def last_executed_query(self, cursor, sql, params): - # With psycopg2, cursor objects have a "query" attribute that is the - # exact query sent to the database. See docs here: - # http://www.initd.org/tracker/psycopg/wiki/psycopg2_documentation#postgresql-status-message-and-executed-query + # http://initd.org/psycopg/docs/cursor.html#cursor.query + # The query attribute is a Psycopg extension to the DB API 2.0. return cursor.query def return_insert_id(self): diff --git a/docs/faq/models.txt b/docs/faq/models.txt index f00d453d88..5911d9f487 100644 --- a/docs/faq/models.txt +++ b/docs/faq/models.txt @@ -22,9 +22,8 @@ of dictionaries in order of query execution. Each dictionary has 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 raw SQL logged in ``connection.queries`` may not include -parameter quoting. Parameter quoting is performed by the database-specific -backend, and not all backends provide a way to retrieve the SQL after quoting. +Note that the SQL recorded here may be :ref:`incorrectly quoted under SQLite +`. .. versionadded:: 1.2 diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index fb97c9f045..82c780cb0f 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -465,7 +465,7 @@ itself to versions newer than the ones included with your particular Python binary distribution, if needed. "Database is locked" errors ------------------------------------------------ +--------------------------- SQLite is meant to be a lightweight database, and thus can't support a high level of concurrency. ``OperationalError: database is locked`` errors indicate @@ -506,6 +506,16 @@ If you're getting this error, you can solve it by: SQLite does not support the ``SELECT ... FOR UPDATE`` syntax. Calling it will have no effect. +.. _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 a SQLite shell. + .. _oracle-notes: Oracle notes diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py index a6bac5a009..3191ef0c85 100644 --- a/tests/regressiontests/backends/tests.py +++ b/tests/regressiontests/backends/tests.py @@ -2,6 +2,7 @@ # Unit and doctests for specific database backends. import datetime +from django.conf import settings from django.core.management.color import no_style from django.db import backend, connection, connections, DEFAULT_DB_ALIAS, IntegrityError from django.db.backends.signals import connection_created @@ -85,6 +86,35 @@ class DateQuotingTest(TestCase): classes = models.SchoolClass.objects.filter(last_updated__day=20) self.assertEqual(len(classes), 1) +class LastExecutedQueryTest(TestCase): + + def setUp(self): + # connection.queries will not be filled in without this + settings.DEBUG = True + + def tearDown(self): + settings.DEBUG = False + + # There are no tests for the sqlite backend because it does not + # implement paramater escaping. See #14091. + + @unittest.skipUnless(connection.vendor in ('oracle', 'postgresql'), + "These backends use the standard parameter escaping rules") + def test_parameter_escaping(self): + # check that both numbers and string are properly quoted + list(models.Tag.objects.filter(name="special:\\\"':", object_id=12)) + sql = connection.queries[-1]['sql'] + self.assertTrue("= 'special:\\\"'':' " in sql) + self.assertTrue("= 12 " in sql) + + @unittest.skipUnless(connection.vendor == 'mysql', + "MySQL uses backslashes to escape parameters.") + def test_parameter_escaping(self): + list(models.Tag.objects.filter(name="special:\\\"':", object_id=12)) + sql = connection.queries[-1]['sql'] + # only this line is different from the test above + self.assertTrue("= 'special:\\\\\\\"\\':' " in sql) + self.assertTrue("= 12 " in sql) class ParameterHandlingTest(TestCase): def test_bad_parameter_count(self):