Ticket #6161: oracle_qsrf_patch_v4.diff

File oracle_qsrf_patch_v4.diff, 14.9 KB (added by jbronn, 16 years ago)

Fixes infinite loop caused by use of default empty_fetchmany_value.

  • django/db/backends/oracle/base.py

     
    44Requires cx_Oracle: http://www.python.net/crew/atuining/cx_Oracle/
    55"""
    66
    7 import datetime
    8 import os
    9 
    107from django.db.backends import BaseDatabaseWrapper, BaseDatabaseFeatures, BaseDatabaseOperations, util
    118from django.utils.datastructures import SortedDict
    129from django.utils.encoding import smart_str, force_unicode
     10import datetime
     11import os
    1312
    1413# Oracle takes client-side character set encoding from the environment.
    1514os.environ['NLS_LANG'] = '.UTF8'
     
    2524class DatabaseFeatures(BaseDatabaseFeatures):
    2625    allows_group_by_ordinal = False
    2726    allows_unique_and_pk = False        # Suppress UNIQUE/PK for Oracle (ORA-02259)
     27    empty_fetchmany_value = ()
    2828    needs_datetime_string_cast = False
    2929    needs_upper_for_iops = True
    3030    supports_tablespaces = True
     
    9090        # Instead, they are handled in django/db/backends/oracle/query.py.
    9191        return ""
    9292
    93     def lookup_cast(self, lookup_type):
    94         if lookup_type in ('iexact', 'icontains', 'istartswith', 'iendswith'):
    95             return "UPPER(%s)"
    96         return "%s"
    97 
    9893    def max_name_length(self):
    9994        return 30
    10095
    10196    def query_set_class(self, DefaultQuerySet):
    102         from django.db import connection
    103         from django.db.models.query import EmptyResultSet, GET_ITERATOR_CHUNK_SIZE, quote_only_if_word
     97        # Getting the base default `Query` object.
     98        DefaultQuery = DefaultQuerySet().query.__class__
    10499
    105         class OracleQuerySet(DefaultQuerySet):
    106 
    107             def iterator(self):
    108                 "Performs the SELECT database lookup of this QuerySet."
    109 
    110                 from django.db.models.query import get_cached_row
    111 
    112                 # self._select is a dictionary, and dictionaries' key order is
    113                 # undefined, so we convert it to a list of tuples.
    114                 extra_select = self._select.items()
    115 
    116                 full_query = None
    117 
    118                 try:
    119                     try:
    120                         select, sql, params, full_query = self._get_sql_clause(get_full_query=True)
    121                     except TypeError:
    122                         select, sql, params = self._get_sql_clause()
    123                 except EmptyResultSet:
    124                     raise StopIteration
    125                 if not full_query:
    126                     full_query = "SELECT %s%s\n%s" % ((self._distinct and "DISTINCT " or ""), ', '.join(select), sql)
    127 
    128                 cursor = connection.cursor()
    129                 cursor.execute(full_query, params)
    130 
    131                 fill_cache = self._select_related
    132                 fields = self.model._meta.fields
    133                 index_end = len(fields)
    134 
    135                 # so here's the logic;
    136                 # 1. retrieve each row in turn
    137                 # 2. convert NCLOBs
    138 
    139                 while 1:
    140                     rows = cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE)
    141                     if not rows:
    142                         raise StopIteration
    143                     for row in rows:
    144                         row = self.resolve_columns(row, fields)
    145                         if fill_cache:
    146                             obj, index_end = get_cached_row(klass=self.model, row=row,
    147                                                             index_start=0, max_depth=self._max_related_depth)
    148                         else:
    149                             obj = self.model(*row[:index_end])
    150                         for i, k in enumerate(extra_select):
    151                             setattr(obj, k[0], row[index_end+i])
    152                         yield obj
    153 
    154 
    155             def _get_sql_clause(self, get_full_query=False):
    156                 from django.db.models.query import fill_table_cache, \
    157                     handle_legacy_orderlist, orderfield2column
    158 
    159                 opts = self.model._meta
    160                 qn = connection.ops.quote_name
    161 
    162                 # Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z.
    163                 select = ["%s.%s" % (qn(opts.db_table), qn(f.column)) for f in opts.fields]
    164                 tables = [quote_only_if_word(t) for t in self._tables]
    165                 joins = SortedDict()
    166                 where = self._where[:]
    167                 params = self._params[:]
    168 
    169                 # Convert self._filters into SQL.
    170                 joins2, where2, params2 = self._filters.get_sql(opts)
    171                 joins.update(joins2)
    172                 where.extend(where2)
    173                 params.extend(params2)
    174 
    175                 # Add additional tables and WHERE clauses based on select_related.
    176                 if self._select_related:
    177                     fill_table_cache(opts, select, tables, where, opts.db_table, [opts.db_table])
    178 
    179                 # Add any additional SELECTs.
    180                 if self._select:
    181                     select.extend(['(%s) AS %s' % (quote_only_if_word(s[1]), qn(s[0])) for s in self._select.items()])
    182 
    183                 # Start composing the body of the SQL statement.
    184                 sql = [" FROM", qn(opts.db_table)]
    185 
    186                 # Compose the join dictionary into SQL describing the joins.
    187                 if joins:
    188                     sql.append(" ".join(["%s %s %s ON %s" % (join_type, table, alias, condition)
    189                                     for (alias, (table, join_type, condition)) in joins.items()]))
    190 
    191                 # Compose the tables clause into SQL.
    192                 if tables:
    193                     sql.append(", " + ", ".join(tables))
    194 
    195                 # Compose the where clause into SQL.
    196                 if where:
    197                     sql.append(where and "WHERE " + " AND ".join(where))
    198 
    199                 # ORDER BY clause
    200                 order_by = []
    201                 if self._order_by is not None:
    202                     ordering_to_use = self._order_by
    203                 else:
    204                     ordering_to_use = opts.ordering
    205                 for f in handle_legacy_orderlist(ordering_to_use):
    206                     if f == '?': # Special case.
    207                         order_by.append(DatabaseOperations().random_function_sql())
    208                     else:
    209                         if f.startswith('-'):
    210                             col_name = f[1:]
    211                             order = "DESC"
    212                         else:
    213                             col_name = f
    214                             order = "ASC"
    215                         if "." in col_name:
    216                             table_prefix, col_name = col_name.split('.', 1)
    217                             table_prefix = qn(table_prefix) + '.'
    218                         else:
    219                             # Use the database table as a column prefix if it wasn't given,
    220                             # and if the requested column isn't a custom SELECT.
    221                             if "." not in col_name and col_name not in (self._select or ()):
    222                                 table_prefix = qn(opts.db_table) + '.'
    223                             else:
    224                                 table_prefix = ''
    225                         order_by.append('%s%s %s' % (table_prefix, qn(orderfield2column(col_name, opts)), order))
    226                 if order_by:
    227                     sql.append("ORDER BY " + ", ".join(order_by))
    228 
    229                 # Look for column name collisions in the select elements
    230                 # and fix them with an AS alias.  This allows us to do a
    231                 # SELECT * later in the paging query.
    232                 cols = [clause.split('.')[-1] for clause in select]
    233                 for index, col in enumerate(cols):
    234                     if cols.count(col) > 1:
    235                         col = '%s%d' % (col.replace('"', ''), index)
    236                         cols[index] = col
    237                         select[index] = '%s AS %s' % (select[index], col)
    238 
    239                 # LIMIT and OFFSET clauses
    240                 # To support limits and offsets, Oracle requires some funky rewriting of an otherwise normal looking query.
    241                 select_clause = ",".join(select)
    242                 distinct = (self._distinct and "DISTINCT " or "")
    243 
    244                 if order_by:
    245                     order_by_clause = " OVER (ORDER BY %s )" % (", ".join(order_by))
    246                 else:
    247                     #Oracle's row_number() function always requires an order-by clause.
    248                     #So we need to define a default order-by, since none was provided.
    249                     order_by_clause = " OVER (ORDER BY %s.%s)" % \
    250                         (qn(opts.db_table), qn(opts.fields[0].db_column or opts.fields[0].column))
    251                 # limit_and_offset_clause
    252                 if self._limit is None:
    253                     assert self._offset is None, "'offset' is not allowed without 'limit'"
    254 
    255                 if self._offset is not None:
    256                     offset = int(self._offset)
    257                 else:
    258                     offset = 0
    259                 if self._limit is not None:
    260                     limit = int(self._limit)
    261                 else:
    262                     limit = None
    263 
    264                 limit_and_offset_clause = ''
    265                 if limit is not None:
    266                     limit_and_offset_clause = "WHERE rn > %s AND rn <= %s" % (offset, limit+offset)
    267                 elif offset:
    268                     limit_and_offset_clause = "WHERE rn > %s" % (offset)
    269 
    270                 if len(limit_and_offset_clause) > 0:
    271                     fmt = \
    272     """SELECT * FROM
    273       (SELECT %s%s,
    274               ROW_NUMBER()%s AS rn
    275        %s)
    276     %s"""
    277                     full_query = fmt % (distinct, select_clause,
    278                                         order_by_clause, ' '.join(sql).strip(),
    279                                         limit_and_offset_clause)
    280                 else:
    281                     full_query = None
    282 
    283                 if get_full_query:
    284                     return select, " ".join(sql), params, full_query
    285                 else:
    286                     return select, " ".join(sql), params
    287 
     100        class OracleQuery(DefaultQuery):
    288101            def resolve_columns(self, row, fields=()):
    289102                from django.db.models.fields import DateField, DateTimeField, \
    290                     TimeField, BooleanField, NullBooleanField, DecimalField, Field
     103                     TimeField, BooleanField, NullBooleanField, DecimalField, Field
    291104                values = []
    292105                for value, field in map(None, row, fields):
    293106                    if isinstance(value, Database.LOB):
     
    331144                    values.append(value)
    332145                return values
    333146
     147            def as_sql(self, with_limits=True):
     148                """
     149                Creates the SQL for this query. Returns the SQL string and list of
     150                parameters.  This is overriden from the original Query class to
     151                accommodate Oracle's limit/offset SQL.
     152               
     153                If 'with_limits' is False, any limit/offset information is not included
     154                in the query.
     155                """
     156                # The `do_offset` flag indicates whether we need to construct the
     157                # SQL needed to use limit/offset w/Oracle.
     158                do_offset = with_limits and (self.high_mark or self.low_mark)
     159
     160                # If no offsets, just return the result of the base class `as_sql`.
     161                if not do_offset:
     162                    return super(OracleQuery, self).as_sql(with_limits=False)
     163
     164                # `get_columns` needs to be called before `get_ordering` to populate
     165                # `_select_alias`.
     166                self.pre_sql_setup()
     167                out_cols = self.get_columns()
     168                ordering = self.get_ordering()
     169
     170                # Getting the "ORDER BY" SQL for the ROW_NUMBER() result.
     171                if ordering:
     172                    rn_orderby = ', '.join(ordering)
     173                else:
     174                    # Oracle's ROW_NUMBER() function always requires an order-by clause.
     175                    # So we need to define a default order-by, since none was provided.
     176                    qn = self.quote_name_unless_alias
     177                    opts = self.model._meta
     178                    rn_orderby = '%s.%s' % (qn(opts.db_table), qn(opts.fields[0].db_column or opts.fields[0].column))
     179
     180                # Getting the selection SQL and the params, which has the `rn`
     181                # extra selection SQL; we pop `rn` after this completes so we do
     182                # not get the attribute on the returned models.
     183                self.extra_select['rn'] = 'ROW_NUMBER() OVER (ORDER BY %s )' % rn_orderby
     184                sql, params= super(OracleQuery, self).as_sql(with_limits=False)
     185                self.extra_select.pop('rn')
     186
     187                # Constructing the result SQL, using the initial select SQL
     188                # obtained above.
     189                result = ['SELECT * FROM (%s)' % sql]
     190               
     191                # Place WHERE condition on `rn` for the desired range.
     192                result.append('WHERE rn > %d' % self.low_mark)
     193                if self.high_mark:
     194                    result.append('AND rn <= %d' % self.high_mark)
     195
     196                # Returning the SQL w/params.
     197                return ' '.join(result), params
     198
     199        from django.db import connection
     200        class OracleQuerySet(DefaultQuerySet):
     201            "The OracleQuerySet is overriden to use OracleQuery."
     202            def __init__(self, model=None, query=None):
     203                super(OracleQuerySet, self).__init__(model=model, query=query)
     204                self.query = query or OracleQuery(self.model, connection)
    334205        return OracleQuerySet
    335206
    336207    def quote_name(self, name):
     
    345216    def random_function_sql(self):
    346217        return "DBMS_RANDOM.RANDOM"
    347218
    348     def regex_lookup_9(self, lookup_type):
    349         raise NotImplementedError("Regexes are not supported in Oracle before version 10g.")
    350 
    351     def regex_lookup_10(self, lookup_type):
    352         if lookup_type == 'regex':
    353             match_option = 'c'
    354         else:
    355             match_option = 'i'
    356         return 'REGEXP_LIKE(%%s %%s %s)' % match_option
    357 
    358219    def sql_flush(self, style, tables, sequences):
    359220        # Return a list of 'TRUNCATE x;', 'TRUNCATE y;',
    360221        # 'TRUNCATE z;'... style SQL statements
     
    446307                           "NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF'")
    447308            try:
    448309                self.oracle_version = int(self.connection.version.split('.')[0])
    449                 # There's no way for the DatabaseOperations class to know the
    450                 # currently active Oracle version, so we do some setups here.
    451                 # TODO: Multi-db support will need a better solution (a way to
    452                 # communicate the current version).
    453                 if self.oracle_version <= 9:
    454                     self.ops.regex_lookup = self.ops.regex_lookup_9
    455                 else:
    456                     self.ops.regex_lookup = self.ops.regex_lookup_10
    457310            except ValueError:
    458311                pass
    459312            try:
Back to Top