Django

Code

Changeset 7321

Show
Ignore:
Timestamp:
03/19/08 10:46:20 (2 months ago)
Author:
mtredinnick
Message:

queryset-refactor: Initial pass at fixing the Oracle support. Thanks, Justin Bronn. Fixed #6161

This is untested (by me) and is a slight modification on Justin's original
patch, so feedback and bug reports are welcome.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/branches/queryset-refactor/django/db/backends/oracle/base.py

    r7087 r7321  
    2626    allows_group_by_ordinal = False 
    2727    allows_unique_and_pk = False        # Suppress UNIQUE/PK for Oracle (ORA-02259) 
     28    empty_fetchmany_value = () 
    2829    needs_datetime_string_cast = False 
    2930    needs_upper_for_iops = True 
     
    100101 
    101102    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 
    104  
    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  
     103        # Getting the base default `Query` object. 
     104        DefaultQuery = DefaultQuerySet().query.__class__ 
     105 
     106        class OracleQuery(DefaultQuery): 
    288107            def resolve_columns(self, row, fields=()): 
    289108                from django.db.models.fields import DateField, DateTimeField, \ 
    290                     TimeField, BooleanField, NullBooleanField, DecimalField, Field 
     109                    TimeField, BooleanField, NullBooleanField, DecimalField, Field 
    291110                values = [] 
    292111                for value, field in map(None, row, fields): 
     
    332151                return values 
    333152 
     153            def as_sql(self, with_limits=True): 
     154                """ 
     155                Creates the SQL for this query. Returns the SQL string and list 
     156                of parameters.  This is overriden from the original Query class 
     157                to accommodate Oracle's limit/offset SQL. 
     158 
     159                If 'with_limits' is False, any limit/offset information is not 
     160                included in the query. 
     161                """ 
     162                # The `do_offset` flag indicates whether we need to construct 
     163                # the SQL needed to use limit/offset w/Oracle. 
     164                do_offset = with_limits and (self.high_mark or self.low_mark) 
     165 
     166                # If no offsets, just return the result of the base class 
     167                # `as_sql`. 
     168                if not do_offset: 
     169                    return super(OracleQuery, self).as_sql(with_limits=False) 
     170 
     171                # `get_columns` needs to be called before `get_ordering` to 
     172                # populate `_select_alias`. 
     173                self.pre_sql_setup() 
     174                out_cols = self.get_columns() 
     175                ordering = self.get_ordering() 
     176 
     177                # Getting the "ORDER BY" SQL for the ROW_NUMBER() result. 
     178                if ordering: 
     179                    rn_orderby = ', '.join(ordering) 
     180                else: 
     181                    # Oracle's ROW_NUMBER() function always requires an 
     182                    # order-by clause.  So we need to define a default 
     183                    # order-by, since none was provided. 
     184                    qn = self.quote_name_unless_alias 
     185                    opts = self.model._meta 
     186                    rn_orderby = '%s.%s' % (qn(opts.db_table), qn(opts.fields[0].db_column or opts.fields[0].column)) 
     187 
     188                # Getting the selection SQL and the params, which has the `rn` 
     189                # extra selection SQL; we pop `rn` after this completes so we do 
     190                # not get the attribute on the returned models. 
     191                self.extra_select['rn'] = 'ROW_NUMBER() OVER (ORDER BY %s )' % rn_orderby 
     192                sql, params= super(OracleQuery, self).as_sql(with_limits=False) 
     193                self.extra_select.pop('rn') 
     194 
     195                # Constructing the result SQL, using the initial select SQL 
     196                # obtained above. 
     197                result = ['SELECT * FROM (%s)' % sql] 
     198 
     199                # Place WHERE condition on `rn` for the desired range. 
     200                result.append('WHERE rn > %d' % self.low_mark) 
     201                if self.high_mark: 
     202                    result.append('AND rn <= %d' % self.high_mark) 
     203 
     204                # Returning the SQL w/params. 
     205                return ' '.join(result), params 
     206 
     207        from django.db import connection 
     208        class OracleQuerySet(DefaultQuerySet): 
     209            "The OracleQuerySet is overriden to use OracleQuery." 
     210            def __init__(self, model=None, query=None): 
     211                super(OracleQuerySet, self).__init__(model=model, query=query) 
     212                self.query = query or OracleQuery(self.model, connection) 
    334213        return OracleQuerySet 
    335214