Ticket #5012: negative_sliced_querysets.patch
File negative_sliced_querysets.patch, 16.6 KB (added by , 17 years ago) |
---|
-
django/db/backends/mysql/base.py
177 177 def get_datetime_cast_sql(): 178 178 return None 179 179 180 def get_limit_offset_sql(limit, offset=None): 180 def get_limit_offset_sql(limit=None, offset=None): 181 if not limit and not offset: 182 return '' 181 183 sql = "LIMIT " 182 if offset and offset != 0: 184 if not limit: 185 # Per mySQL manual: 186 # To retrieve all rows from a certain offset up to the end of the result 187 # set, you can use some large number for the second parameter. 188 limit = 18446744073709551615 189 if offset: 183 190 sql += "%s," % offset 184 191 return sql + str(limit) 185 192 -
django/db/backends/mysql_old/base.py
192 192 def get_datetime_cast_sql(): 193 193 return None 194 194 195 def get_limit_offset_sql(limit, offset=None): 195 def get_limit_offset_sql(limit=None, offset=None): 196 if not limit and not offset: 197 return '' 196 198 sql = "LIMIT " 197 if offset and offset != 0: 199 if not limit: 200 # Per mySQL manual: 201 # To retrieve all rows from a certain offset up to the end of the result 202 # set, you can use some large number for the second parameter. 203 limit = 18446744073709551615 204 if offset: 198 205 sql += "%s," % offset 199 206 return sql + str(limit) 200 207 -
django/db/backends/postgresql/base.py
158 158 def get_datetime_cast_sql(): 159 159 return None 160 160 161 def get_limit_offset_sql(limit, offset=None): 162 sql = "LIMIT %s" % limit 163 if offset and offset != 0: 161 def get_limit_offset_sql(limit=None, offset=None): 162 if not limit and not offset: 163 return '' 164 # From PostgreSQL manual: "LIMIT { count | ALL } OFFSET start" 165 sql = "LIMIT %s" % (limit or 'ALL') 166 if offset: 164 167 sql += " OFFSET %s" % offset 165 168 return sql 166 169 -
django/db/backends/postgresql_psycopg2/base.py
112 112 def get_datetime_cast_sql(): 113 113 return None 114 114 115 def get_limit_offset_sql(limit, offset=None): 116 sql = "LIMIT %s" % limit 117 if offset and offset != 0: 115 def get_limit_offset_sql(limit=None, offset=None): 116 if not limit and not offset: 117 return '' 118 sql = "LIMIT %s" % (limit or 'ALL') 119 if offset: 118 120 sql += " OFFSET %s" % offset 119 121 return sql 120 122 -
django/db/backends/sqlite3/base.py
142 142 def get_datetime_cast_sql(): 143 143 return None 144 144 145 def get_limit_offset_sql(limit, offset=None): 146 sql = "LIMIT %s" % limit 145 def get_limit_offset_sql(limit=None, offset=None): 146 if not limit and not offset: 147 return '' 148 # From sqlite manual: "A negative LIMIT indicates no upper bound." 149 sql = "LIMIT %s" % (limit or -1) 147 150 if offset and offset != 0: 148 151 sql += " OFFSET %s" % offset 149 152 return sql -
django/db/models/query.py
15 15 except NameError: 16 16 from sets import Set as set # Python 2.3 fallback 17 17 18 try: 19 reversed 20 except NameError: 21 from django.utils.itercompat import reversed # Python 2.3 fallback 22 18 23 # The string constant used to separate query parts 19 24 LOOKUP_SEPARATOR = '__' 20 25 … … 82 87 else: 83 88 return backend.quote_name(word) 84 89 90 def negative_slice(k): 91 return (k.start and k.start < 0) or (k.stop and k.stop < 0) 92 93 def invert_slice(k): 94 """ 95 Reverse the slice, inverting the negation and swapping the start and stop 96 slice positions. 97 """ 98 new_start = k.stop and -k.stop or None 99 if k.stop == 0: 100 # Edge case of [-x:0] should remain empty. 101 new_stop = 0 102 else: 103 new_stop = k.start and -k.start or None 104 return slice(new_start, new_stop) 105 106 def invert_item(k): 107 "Reverse the __getitem__ integer item position." 108 return -k - 1 109 85 110 class _QuerySet(object): 86 111 "Represents a lazy database lookup for a set of objects" 87 112 def __init__(self, model=None): … … 97 122 self._tables = [] # List of extra tables to use. 98 123 self._offset = None # OFFSET clause. 99 124 self._limit = None # LIMIT clause. 125 self._reversed = None # Reverse the db results (used for negative slicing) 100 126 self._result_cache = None 101 127 102 128 ######################## … … 116 142 "Retrieve an item or slice from the set of results." 117 143 if not isinstance(k, (slice, int)): 118 144 raise TypeError 119 assert (not isinstance(k, slice) and (k >= 0)) \ 120 or (isinstance(k, slice) and (k.start is None or k.start >= 0) and (k.stop is None or k.stop >= 0)), \ 121 "Negative indexing is not supported." 145 if isinstance(k, slice): 146 if k.start and k.stop: 147 assert not ((k.start < 0) ^ (k.stop < 0)), \ 148 "Slicing with both positive and negative indexes is not supported." 149 if k.start and k.start < 0 or k.stop and k.stop < 0: 150 assert not k.step or k.step == 1, \ 151 "Slicing with a negative index and a step is not supported." 122 152 if self._result_cache is None: 153 extra_clone_args = {} 123 154 if isinstance(k, slice): 155 if self._reversed: 156 # Because we will be dealing with a reversed QuerySet, the 157 # slice should be inverted. 158 if not negative_slice(k): 159 # Can't negative slice an already reversed QuerySet, so 160 # just slice it as a list. Note, we haven't done the 161 # inversion yet, hence reverse logic in the condition. 162 return self._get_data()[k] 163 k = invert_slice(k) 164 elif negative_slice(k): 165 extra_clone_args['_reversed'] = True 166 k = invert_slice(k) 124 167 # Offset: 125 168 if self._offset is None: 126 169 offset = k.start … … 137 180 # Limit: 138 181 if k.stop is not None and k.start is not None: 139 182 if limit is None: 140 limit = k.stop - k.start183 limit = max(k.stop - k.start, 0) 141 184 else: 142 185 limit = min((k.stop - k.start), limit) 143 186 else: … … 147 190 if k.stop is not None: 148 191 limit = min(k.stop, limit) 149 192 150 if k.step is None: 151 return self._clone(_offset=offset, _limit=limit) 193 if k.step is None or k.step == 1: 194 return self._clone(_offset=offset, _limit=limit, 195 **extra_clone_args) 152 196 else: 153 return list(self._clone(_offset=offset, _limit=limit))[::k.step] 197 return list(self._clone(_offset=offset, _limit=limit, 198 **extra_clone_args))[::k.step] 154 199 else: 200 if self._reversed: 201 # Because we will be dealing with a reversed QuerySet, the 202 # item position should be inverted. 203 if k >= 0: 204 # Can't negate already offset reverse slice via the 205 # database way, just have to get it as a list. Note, we 206 # haven't done the inversion yet, hence reverse logic in 207 # the condition. 208 return self._get_data()[k] 209 # Reversed queryset lookups need to take the current offset 210 # into consideration. 211 k += self._offset 212 k = invert_item(k) 213 elif k < 0: 214 k = invert_item(k) 215 extra_clone_args['_reversed'] = True 155 216 try: 156 return list(self._clone(_offset=k, _limit=1))[0] 217 return list(self._clone(_offset=k, _limit=1, 218 **extra_clone_args))[0] 157 219 except self.model.DoesNotExist, e: 158 220 raise IndexError, e.args 159 221 else: … … 174 236 #################################### 175 237 176 238 def iterator(self): 239 if self._reversed: 240 # Being a reverse slice, the order will be reversed from what the 241 # user expects. 242 return reversed(list(self._iterator())) 243 return self._iterator() 244 def _iterator(self): 177 245 "Performs the SELECT database lookup of this QuerySet." 178 246 try: 179 247 select, sql, params = self._get_sql_clause() 180 248 except EmptyResultSet: 181 249 raise StopIteration 250 if self._limit == 0: 251 raise StopIteration 182 252 183 253 # self._select is a dictionary, and dictionaries' key order is 184 254 # undefined, so we convert it to a list of tuples. … … 452 522 c._tables = self._tables[:] 453 523 c._offset = self._offset 454 524 c._limit = self._limit 525 c._reversed = self._reversed 455 526 c.__dict__.update(kwargs) 456 527 return c 457 528 … … 525 596 526 597 # ORDER BY clause 527 598 order_by = [] 528 if self._order_by is not None: 529 ordering_to_use = self._order_by 530 else: 531 ordering_to_use = opts.ordering 532 for f in handle_legacy_orderlist(ordering_to_use): 599 for f in handle_legacy_orderlist(self._get_ordering()): 533 600 if f == '?': # Special case. 534 601 order_by.append(backend.get_random_function_sql()) 535 602 else: … … 554 621 sql.append("ORDER BY " + ", ".join(order_by)) 555 622 556 623 # LIMIT and OFFSET clauses 557 if self._limit is not None :624 if self._limit is not None or self._offset is not None: 558 625 sql.append("%s " % backend.get_limit_offset_sql(self._limit, self._offset)) 559 else:560 assert self._offset is None, "'offset' is not allowed without 'limit'"561 626 562 627 return select, " ".join(sql), params 563 628 629 def _get_ordering(self): 630 if self._order_by is not None: 631 ordering = self._order_by 632 else: 633 ordering = self.model._meta.ordering 634 if not self._reversed: 635 return ordering 636 # Negative indexing requires inverting the ordering. 637 assert ordering, 'Negative indexing requires explicit ordering' 638 reverse_ordering = lambda f: (f.startswith('-') and f[1:] 639 or '-%s' % f) 640 return [reverse_ordering(f) for f in ordering] 641 564 642 # Use the backend's QuerySet class if it defines one, otherwise use _QuerySet. 565 643 if hasattr(backend, 'get_query_set_class'): 566 644 QuerySet = backend.get_query_set_class(_QuerySet) … … 573 651 # select_related isn't supported in values(). 574 652 self._select_related = False 575 653 576 def iterator(self):654 def _iterator(self): 577 655 try: 578 656 select, sql, params = self._get_sql_clause() 579 657 except EmptyResultSet: … … 621 699 return c 622 700 623 701 class DateQuerySet(QuerySet): 624 def iterator(self):702 def _iterator(self): 625 703 from django.db.backends.util import typecast_timestamp 626 704 from django.db.models.fields import DateTimeField 627 705 self._order_by = () # Clear this because it'll mess things up otherwise. -
docs/db-api.txt
434 434 Note, however, that the first of these will raise ``IndexError`` while the 435 435 second will raise ``DoesNotExist`` if no objects match the given criteria. 436 436 437 **New in Django development version** 438 439 You can also use negative slicing and negative indexes, provided the 440 ``QuerySet``s has an explicit order (either via ``.order_by()`` or the model's 441 meta ``ordering`` option). For example:: 442 443 entries_by_date_order = Entry.objects.order_by('pub_date') 444 oldest_entry = entries_by_date_order[0] 445 latest_entry = entries_by_date_order[-1] 446 latest_three_entries = entries_by_date_order[:-3] 447 437 448 QuerySet methods that return new QuerySets 438 449 ------------------------------------------ 439 450 -
tests/modeltests/basic/models.py
246 246 >>> s3 = Article.objects.filter(id__exact=3) 247 247 >>> (s1 | s2 | s3)[::2] 248 248 [<Article: Area woman programs in Python>, <Article: Third article>] 249 >>> Article.objects.all()[2:] 250 [<Article: Third article>, <Article: Article 6>, <Article: Default headline>, <Article: Fourth article>, <Article: Article 7>, <Article: Updated article 8>] 249 251 252 250 253 # Slices (without step) are lazy: 251 254 >>> Article.objects.all()[0:5].filter() 252 255 [<Article: Area woman programs in Python>, <Article: Second article>, <Article: Third article>, <Article: Article 6>, <Article: Default headline>] … … 268 271 [<Article: Third article>, <Article: Article 6>] 269 272 >>> Article.objects.all()[2:][2:3] 270 273 [<Article: Default headline>] 274 >>> Article.objects.all()[-5:-2] 275 [<Article: Article 6>, <Article: Default headline>, <Article: Fourth article>] 276 >>> Article.objects.all()[-5:-2][1] 277 <Article: Default headline> 278 >>> Article.objects.all()[-5:-2][-1] 279 <Article: Fourth article> 280 >>> Article.objects.all()[-5:-2][1:] 281 [<Article: Default headline>, <Article: Fourth article>] 282 >>> Article.objects.all()[-5:-2][-2:] 283 [<Article: Default headline>, <Article: Fourth article>] 284 >>> Article.objects.all()[-5:-2][:2] 285 [<Article: Article 6>, <Article: Default headline>] 286 >>> Article.objects.all()[-5:-2][:-2] 287 [<Article: Article 6>] 288 >>> Article.objects.all()[-5:-2][:-10] 289 [] 290 >>> Article.objects.all()[-5:-2][-10:] 291 [<Article: Article 6>, <Article: Default headline>, <Article: Fourth article>] 271 292 272 # Note that you can't use 'offset' without 'limit' (on some dbs), so this doesn't work: 273 >>> Article.objects.all()[2:] 274 Traceback (most recent call last): 275 ... 276 AssertionError: 'offset' is not allowed without 'limit' 277 278 # Also, once you have sliced you can't filter, re-order or combine 293 # Once you have sliced you can't filter, re-order or combine 279 294 >>> Article.objects.all()[0:5].filter(id=1) 280 295 Traceback (most recent call last): 281 296 ... … … 291 306 ... 292 307 AssertionError: Cannot combine queries once a slice has been taken. 293 308 294 # Negative slices are not supported, due to database constraints.295 # ( hint: inverting your ordering might do what you need).309 # Negative slices are possible, provided if the QuerySet has explicit ordering. 310 # (done by internally inverting the ordering). 296 311 >>> Article.objects.all()[-1] 312 <Article: Updated article 8> 313 >>> Article.objects.all()[0:-5] 314 [<Article: Area woman programs in Python>, <Article: Second article>, <Article: Third article>] 315 >>> Article.objects.all()[-2:] 316 [<Article: Article 7>, <Article: Updated article 8>] 317 >>> Article.objects.all()[:-7] 318 [<Article: Area woman programs in Python>] 319 >>> Article.objects.all()[-2:-1] 320 [<Article: Article 7>] 321 >>> Article.objects.all()[-2:0] 322 [] 323 >>> Article.objects.all()[-2:-3] 324 [] 325 326 # Can't mix negative and positive positions when slicing. 327 >>> Article.objects.all()[1:-1] 297 328 Traceback (most recent call last): 298 329 ... 299 AssertionError: Negative indexingis not supported.300 >>> Article.objects.all()[ 0:-5]330 AssertionError: Slicing with both positive and negative indexes is not supported. 331 >>> Article.objects.all()[-5:2] 301 332 Traceback (most recent call last): 302 333 ... 303 AssertionError: Negative indexingis not supported.334 AssertionError: Slicing with both positive and negative indexes is not supported. 304 335 305 336 # An Article instance doesn't have access to the "objects" attribute. 306 337 # That's only available on the class.