From 024abe5b82d95ee60cb18a77ebf841ad715467fa Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Wed, 26 Sep 2018 20:18:48 +0200 Subject: 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 ++++ 3 files changed, 76 insertions(+) create mode 100644 django/db/backends/oracle/compiler.py (limited to 'django') 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' -- cgit v1.3