From 024abe5b82d95ee60cb18a77ebf841ad715467fa Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Wed, 26 Sep 2018 20:18:48 +0200 Subject: [PATCH] Fixed #29630 -- Fixed crash of sliced queries with multiple columns with the same name on Oracle 12.1. Regression in 0899d583bdb140910698d00d17f5f1abc8774b07. Thanks Tim Graham for the review and Jani Tiainen for help. --- django/db/backends/oracle/compiler.py | 60 +++++++++++++++++++++++++ django/db/backends/oracle/features.py | 9 ++++ django/db/backends/oracle/operations.py | 7 +++ docs/releases/2.1.2.txt | 3 ++ tests/queries/tests.py | 32 ++++++++++--- 5 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 django/db/backends/oracle/compiler.py diff --git a/django/db/backends/oracle/compiler.py b/django/db/backends/oracle/compiler.py new file mode 100644 index 0000000000..819f241f80 --- /dev/null +++ b/django/db/backends/oracle/compiler.py @@ -0,0 +1,60 @@ +from django.db import NotSupportedError +from django.db.models.sql import compiler + + +class SQLCompiler(compiler.SQLCompiler): + def as_sql(self, with_limits=True, with_col_aliases=False): + """ + Create the SQL for this query. Return the SQL string and list of + parameters. This is overridden from the original Query class to handle + the restriction in Oracle 12.1 and emulate LIMIT and OFFSET with + a subquery. + + If 'with_limits' is False, any limit/offset information is not included + in the query. + """ + # Whether the query must be constructed using limit/offset. + do_offset = with_limits and (self.query.high_mark is not None or self.query.low_mark) + if not do_offset: + sql, params = super().as_sql(with_limits=False, with_col_aliases=with_col_aliases) + elif not self.connection.features.supports_select_for_update_with_limit and self.query.select_for_update: + raise NotSupportedError( + 'LIMIT/OFFSET is not supported with select_for_update on this ' + 'database backend.' + ) + else: + sql, params = super().as_sql(with_limits=False, with_col_aliases=True) + # Wrap the base query in an outer SELECT * with boundaries on + # the "_RN" column. This is the canonical way to emulate LIMIT + # and OFFSET on Oracle. + high_where = '' + if self.query.high_mark is not None: + high_where = 'WHERE ROWNUM <= %d' % (self.query.high_mark,) + + if self.query.low_mark: + sql = ( + 'SELECT * FROM (SELECT "_SUB".*, ROWNUM AS "_RN" FROM (%s) ' + '"_SUB" %s) WHERE "_RN" > %d' % (sql, high_where, self.query.low_mark) + ) + else: + # Simplify the query to support subqueries if there's no offset. + sql = ( + 'SELECT * FROM (SELECT "_SUB".* FROM (%s) "_SUB" %s)' % (sql, high_where) + ) + return sql, params + + +class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler): + pass + + +class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler): + pass + + +class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler): + pass + + +class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler): + pass diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py index 81eb03f2b5..c44b5041e7 100644 --- a/django/db/backends/oracle/features.py +++ b/django/db/backends/oracle/features.py @@ -1,5 +1,6 @@ from django.db.backends.base.features import BaseDatabaseFeatures from django.db.utils import InterfaceError +from django.utils.functional import cached_property class DatabaseFeatures(BaseDatabaseFeatures): @@ -55,3 +56,11 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_over_clause = True supports_ignore_conflicts = False max_query_params = 2**16 - 1 + + @cached_property + def has_fetch_offset_support(self): + return self.connection.oracle_version >= (12, 2) + + @cached_property + def allow_sliced_subqueries_with_in(self): + return self.has_fetch_offset_support diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index 9cfee5897d..dcbcb5e47f 100644 --- a/django/db/backends/oracle/operations.py +++ b/django/db/backends/oracle/operations.py @@ -8,6 +8,7 @@ from django.db.backends.utils import strip_quotes, truncate_name from django.db.utils import DatabaseError from django.utils import timezone from django.utils.encoding import force_bytes +from django.utils.functional import cached_property from .base import Database from .utils import BulkInsertMapper, InsertIdVar, Oracle_datetime @@ -579,3 +580,9 @@ END; if fields: return self.connection.features.max_query_params // len(fields) return len(objs) + + @cached_property + def compiler_module(self): + if self.connection.features.has_fetch_offset_support: + return super().compiler_module + return 'django.db.backends.oracle.compiler' diff --git a/docs/releases/2.1.2.txt b/docs/releases/2.1.2.txt index 3651440bc6..cfa5b57f22 100644 --- a/docs/releases/2.1.2.txt +++ b/docs/releases/2.1.2.txt @@ -22,3 +22,6 @@ Bugfixes * Fixed a regression in Django 2.0 where unique index names weren't quoted (:ticket:`29778`). + +* Fixed a regression where sliced queries with multiple columns with the same + name crashed on Oracle 12.1 (:ticket:`29630`). diff --git a/tests/queries/tests.py b/tests/queries/tests.py index 65917f84fb..25e3b283ec 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -1836,15 +1836,15 @@ class Queries6Tests(TestCase): @classmethod def setUpTestData(cls): generic = NamedCategory.objects.create(name="Generic") - t1 = Tag.objects.create(name='t1', category=generic) - Tag.objects.create(name='t2', parent=t1, category=generic) - t3 = Tag.objects.create(name='t3', parent=t1) - t4 = Tag.objects.create(name='t4', parent=t3) - Tag.objects.create(name='t5', parent=t3) + cls.t1 = Tag.objects.create(name='t1', category=generic) + cls.t2 = Tag.objects.create(name='t2', parent=cls.t1, category=generic) + cls.t3 = Tag.objects.create(name='t3', parent=cls.t1) + cls.t4 = Tag.objects.create(name='t4', parent=cls.t3) + cls.t5 = Tag.objects.create(name='t5', parent=cls.t3) n1 = Note.objects.create(note='n1', misc='foo', id=1) - ann1 = Annotation.objects.create(name='a1', tag=t1) + ann1 = Annotation.objects.create(name='a1', tag=cls.t1) ann1.notes.add(n1) - Annotation.objects.create(name='a2', tag=t4) + Annotation.objects.create(name='a2', tag=cls.t4) def test_parallel_iterators(self): # Parallel iterators work. @@ -1923,6 +1923,24 @@ class Queries6Tests(TestCase): def test_distinct_ordered_sliced_subquery_aggregation(self): self.assertEqual(Tag.objects.distinct().order_by('category__name')[:3].count(), 3) + def test_multiple_columns_with_the_same_name_slice(self): + self.assertEqual( + list(Tag.objects.order_by('name').values_list('name', 'category__name')[:2]), + [('t1', 'Generic'), ('t2', 'Generic')], + ) + self.assertSequenceEqual( + Tag.objects.order_by('name').select_related('category')[:2], + [self.t1, self.t2], + ) + self.assertEqual( + list(Tag.objects.order_by('-name').values_list('name', 'parent__name')[:2]), + [('t5', 't3'), ('t4', 't3')], + ) + self.assertSequenceEqual( + Tag.objects.order_by('-name').select_related('parent')[:2], + [self.t5, self.t4], + ) + class RawQueriesTests(TestCase): def setUp(self):