Django

Code

Ticket #6161: oracle_qsrf_patch_v4.diff

File oracle_qsrf_patch_v4.diff, 14.9 kB (added by jbronn, 10 months ago)

Fixes infinite loop caused by use of default empty_fetchmany_value.

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

    old new  
    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: