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:
Mariusz Felisiak 2018-09-26 20:18:48 +02:00 committed by GitHub
parent 51da347c32
commit 024abe5b82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 104 additions and 7 deletions

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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`).

View File

@ -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):