Fixed #29630 -- Fixed crash of sliced queries with multiple columns with the same name on Oracle 12.1.
Regression in 0899d583bd
.
Thanks Tim Graham for the review and Jani Tiainen for help.
This commit is contained in:
parent
51da347c32
commit
024abe5b82
|
@ -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
|
|
@ -1,5 +1,6 @@
|
||||||
from django.db.backends.base.features import BaseDatabaseFeatures
|
from django.db.backends.base.features import BaseDatabaseFeatures
|
||||||
from django.db.utils import InterfaceError
|
from django.db.utils import InterfaceError
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
|
||||||
class DatabaseFeatures(BaseDatabaseFeatures):
|
class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
|
@ -55,3 +56,11 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
supports_over_clause = True
|
supports_over_clause = True
|
||||||
supports_ignore_conflicts = False
|
supports_ignore_conflicts = False
|
||||||
max_query_params = 2**16 - 1
|
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
|
||||||
|
|
|
@ -8,6 +8,7 @@ from django.db.backends.utils import strip_quotes, truncate_name
|
||||||
from django.db.utils import DatabaseError
|
from django.db.utils import DatabaseError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.encoding import force_bytes
|
from django.utils.encoding import force_bytes
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
from .base import Database
|
from .base import Database
|
||||||
from .utils import BulkInsertMapper, InsertIdVar, Oracle_datetime
|
from .utils import BulkInsertMapper, InsertIdVar, Oracle_datetime
|
||||||
|
@ -579,3 +580,9 @@ END;
|
||||||
if fields:
|
if fields:
|
||||||
return self.connection.features.max_query_params // len(fields)
|
return self.connection.features.max_query_params // len(fields)
|
||||||
return len(objs)
|
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'
|
||||||
|
|
|
@ -22,3 +22,6 @@ Bugfixes
|
||||||
|
|
||||||
* Fixed a regression in Django 2.0 where unique index names weren't quoted
|
* Fixed a regression in Django 2.0 where unique index names weren't quoted
|
||||||
(:ticket:`29778`).
|
(:ticket:`29778`).
|
||||||
|
|
||||||
|
* Fixed a regression where sliced queries with multiple columns with the same
|
||||||
|
name crashed on Oracle 12.1 (:ticket:`29630`).
|
||||||
|
|
|
@ -1836,15 +1836,15 @@ class Queries6Tests(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
generic = NamedCategory.objects.create(name="Generic")
|
generic = NamedCategory.objects.create(name="Generic")
|
||||||
t1 = Tag.objects.create(name='t1', category=generic)
|
cls.t1 = Tag.objects.create(name='t1', category=generic)
|
||||||
Tag.objects.create(name='t2', parent=t1, category=generic)
|
cls.t2 = Tag.objects.create(name='t2', parent=cls.t1, category=generic)
|
||||||
t3 = Tag.objects.create(name='t3', parent=t1)
|
cls.t3 = Tag.objects.create(name='t3', parent=cls.t1)
|
||||||
t4 = Tag.objects.create(name='t4', parent=t3)
|
cls.t4 = Tag.objects.create(name='t4', parent=cls.t3)
|
||||||
Tag.objects.create(name='t5', parent=t3)
|
cls.t5 = Tag.objects.create(name='t5', parent=cls.t3)
|
||||||
n1 = Note.objects.create(note='n1', misc='foo', id=1)
|
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)
|
ann1.notes.add(n1)
|
||||||
Annotation.objects.create(name='a2', tag=t4)
|
Annotation.objects.create(name='a2', tag=cls.t4)
|
||||||
|
|
||||||
def test_parallel_iterators(self):
|
def test_parallel_iterators(self):
|
||||||
# Parallel iterators work.
|
# Parallel iterators work.
|
||||||
|
@ -1923,6 +1923,24 @@ class Queries6Tests(TestCase):
|
||||||
def test_distinct_ordered_sliced_subquery_aggregation(self):
|
def test_distinct_ordered_sliced_subquery_aggregation(self):
|
||||||
self.assertEqual(Tag.objects.distinct().order_by('category__name')[:3].count(), 3)
|
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):
|
class RawQueriesTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
Loading…
Reference in New Issue