From 17df72114e222d63c2af9ed9780583f4cb0689eb Mon Sep 17 00:00:00 2001 From: Arsa Date: Mon, 6 Dec 2021 16:55:29 +0100 Subject: [PATCH] Fixed #33340 -- Fixed unquoted column names in queries used by DatabaseCache. --- AUTHORS | 1 + django/core/cache/backends/db.py | 14 ++++++++++---- django/db/backends/base/operations.py | 3 ++- django/db/backends/oracle/operations.py | 7 ++++++- tests/cache/tests.py | 18 +++++++++++++++++- 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/AUTHORS b/AUTHORS index e13d13d4ae..272a530132 100644 --- a/AUTHORS +++ b/AUTHORS @@ -97,6 +97,7 @@ answer newbie questions, and generally made Django that much better: arien Armin Ronacher Aron Podrigal + Arsalan Ghassemi Artem Gnilov Arthur Arthur Jovart diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py index ef95b1dd16..5bb1c5aec5 100644 --- a/django/core/cache/backends/db.py +++ b/django/core/cache/backends/db.py @@ -228,10 +228,11 @@ class DatabaseCache(BaseDatabaseCache): with connection.cursor() as cursor: cursor.execute( - 'SELECT %s FROM %s WHERE %s = %%s and expires > %%s' % ( + 'SELECT %s FROM %s WHERE %s = %%s and %s > %%s' % ( quote_name('cache_key'), quote_name(self._table), quote_name('cache_key'), + quote_name('expires'), ), [key, connection.ops.adapt_datetimefield_value(now)] ) @@ -243,8 +244,10 @@ class DatabaseCache(BaseDatabaseCache): else: connection = connections[db] table = connection.ops.quote_name(self._table) - cursor.execute("DELETE FROM %s WHERE expires < %%s" % table, - [connection.ops.adapt_datetimefield_value(now)]) + cursor.execute('DELETE FROM %s WHERE %s < %%s' % ( + table, + connection.ops.quote_name('expires'), + ), [connection.ops.adapt_datetimefield_value(now)]) deleted_count = cursor.rowcount remaining_num = num - deleted_count if remaining_num > self._max_entries: @@ -255,7 +258,10 @@ class DatabaseCache(BaseDatabaseCache): last_cache_key = cursor.fetchone() if last_cache_key: cursor.execute( - 'DELETE FROM %s WHERE cache_key < %%s' % table, + 'DELETE FROM %s WHERE %s < %%s' % ( + table, + connection.ops.quote_name('cache_key'), + ), [last_cache_key[0]], ) diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py index 27fe486e2f..66bb009175 100644 --- a/django/db/backends/base/operations.py +++ b/django/db/backends/base/operations.py @@ -88,7 +88,8 @@ class BaseDatabaseOperations: This is used by the 'db' cache backend to determine where to start culling. """ - return "SELECT cache_key FROM %s ORDER BY cache_key LIMIT 1 OFFSET %%s" + cache_key = self.quote_name('cache_key') + return f'SELECT {cache_key} FROM %s ORDER BY {cache_key} LIMIT 1 OFFSET %%s' def unification_cast_sql(self, output_field): """ diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index f497390bea..c357c75351 100644 --- a/django/db/backends/oracle/operations.py +++ b/django/db/backends/oracle/operations.py @@ -72,7 +72,12 @@ END; } def cache_key_culling_sql(self): - return 'SELECT cache_key FROM %s ORDER BY cache_key OFFSET %%s ROWS FETCH FIRST 1 ROWS ONLY' + cache_key = self.quote_name('cache_key') + return ( + f'SELECT {cache_key} ' + f'FROM %s ' + f'ORDER BY {cache_key} OFFSET %%s ROWS FETCH FIRST 1 ROWS ONLY' + ) def date_extract_sql(self, lookup_type, field_name): if lookup_type == 'week_day': diff --git a/tests/cache/tests.py b/tests/cache/tests.py index ddfe9ddfe6..7a98fc4ba1 100644 --- a/tests/cache/tests.py +++ b/tests/cache/tests.py @@ -1113,7 +1113,7 @@ class DBCacheTests(BaseCacheTests, TransactionTestCase): with self.assertNumQueries(1): cache.delete_many(['a', 'b', 'c']) - def test_cull_count_queries(self): + def test_cull_queries(self): old_max_entries = cache._max_entries # Force _cull to delete on first cached record. cache._max_entries = -1 @@ -1124,6 +1124,13 @@ class DBCacheTests(BaseCacheTests, TransactionTestCase): cache._max_entries = old_max_entries num_count_queries = sum('COUNT' in query['sql'] for query in captured_queries) self.assertEqual(num_count_queries, 1) + # Column names are quoted. + for query in captured_queries: + sql = query['sql'] + if 'expires' in sql: + self.assertIn(connection.ops.quote_name('expires'), sql) + if 'cache_key' in sql: + self.assertIn(connection.ops.quote_name('cache_key'), sql) def test_delete_cursor_rowcount(self): """ @@ -1180,6 +1187,15 @@ class DBCacheTests(BaseCacheTests, TransactionTestCase): ) self.assertEqual(out.getvalue(), "Cache table 'test cache table' created.\n") + def test_has_key_query_columns_quoted(self): + with CaptureQueriesContext(connection) as captured_queries: + cache.has_key('key') + self.assertEqual(len(captured_queries), 1) + sql = captured_queries[0]['sql'] + # Column names are quoted. + self.assertIn(connection.ops.quote_name('expires'), sql) + self.assertIn(connection.ops.quote_name('cache_key'), sql) + @override_settings(USE_TZ=True) class DBCacheWithTimeZoneTests(DBCacheTests):