summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMariusz Felisiak <felisiak.mariusz@gmail.com>2018-09-26 20:18:48 +0200
committerGitHub <noreply@github.com>2018-09-26 20:18:48 +0200
commit024abe5b82d95ee60cb18a77ebf841ad715467fa (patch)
tree58375f803fde96f2badcd21e6af717fe0f063df0
parent51da347c32f5af1e8de4be77c15d6a35b58d4059 (diff)
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.
-rw-r--r--django/db/backends/oracle/compiler.py60
-rw-r--r--django/db/backends/oracle/features.py9
-rw-r--r--django/db/backends/oracle/operations.py7
-rw-r--r--docs/releases/2.1.2.txt3
-rw-r--r--tests/queries/tests.py32
5 files changed, 104 insertions, 7 deletions
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):