Skip to content

Fast row values pagination #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bench/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.7.2-alpine
FROM python:3.8.6-alpine3.12

ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
Expand Down
56 changes: 28 additions & 28 deletions infinite_scroll_pagination/paginator.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def prepare_order(self, has_pk, move_to):
return result

# q = X<=? & ~(X=? & ~(Y<?))
def _apply_filter(self, i, fields, values, move_to):
def _apply_filter2(self, i, fields, values, move_to):
assert i < len(fields)
f, d = fields[i]
v = values[i]
Expand All @@ -88,6 +88,29 @@ def _apply_filter(self, i, fields, values, move_to):
q = self._apply_filter(i+1, fields, values, move_to)
return Q(**{lf + 'e': v}) & ~(Q(**{f: v}) & ~q)

def _apply_filter(self, q, fields, values, move_to):
assert len(set(d for _, d in fields)) == 1, (
"Mixing sort fields direction is not allowed")
assert all('__' not in f for f, _ in fields), (
"Field relationships are not allowed")
sign = '>'
_, d = fields[0]
if ((d == DESC and move_to == NEXT_PAGE) or
(d == ASC and move_to == PREV_PAGE)):
sign = '<'
table = q.model._meta.db_table
where = '({lhs}) {sign} ({rhs})'.format(
lhs=', '.join(
'"{}"."id"'.format(table)
if f == 'pk'
else '"{}"."{}"'.format(table, f)
for f, _ in fields),
sign=sign,
rhs=', '.join(['%s']*len(values)))
return q.extra(
where=[where],
params=values)

def apply_filter(self, value, pk, move_to):
assert len(value) == len(self.lookup_fields)
fields = list(self.fields_direction)
Expand All @@ -96,8 +119,10 @@ def apply_filter(self, value, pk, move_to):
values.append(pk)
fields.append(
('pk', fields[-1][1]))
q = self._apply_filter(0, fields, values, move_to)
return self.query_set.filter(q)
#q = self._apply_filter(0, fields, values, move_to)
#return self.query_set.filter(q)
return self._apply_filter(
self.query_set, fields, values, move_to)

def seek(self, value, pk, move_to):
"""
Expand All @@ -111,31 +136,6 @@ def seek(self, value, pk, move_to):
AND NOT (date = ? AND id >= ?)
ORDER BY date DESC, id DESC

Multi field lookup. Note how it produces nesting,
and how I removed it using boolean logic simplification::

X <= ?
AND NOT (X = ? AND (date <= ? AND NOT (date = ? AND id >= ?)))
<--->
X <= ?
AND (NOT X = ? OR NOT date <= ? OR (date = ? AND id >= ?))
<--->
X <= ?
AND (NOT X = ? OR NOT date <= ? OR date = ?)
AND (NOT X = ? OR NOT date <= ? OR id >= ?)

A * ~(B * (C * ~(D * F)))
-> (D + ~B + ~C) * (F + ~B + ~C) * A
A * ~(B * (C * ~(D * (F * ~(G * H)))))
-> (D + ~B + ~C) * (F + ~B + ~C) * (~B + ~C + ~G + ~H) * A
A * ~(B * (C * ~(D * (F * ~(G * (X * ~(Y * Z)))))))
-> (D + ~B + ~C) * (F + ~B + ~C) * (Y + ~B + ~C + ~G + ~X) * (Z + ~B + ~C + ~G + ~X) * A

Addendum::

X <= ?
AND NOT (X = ? AND NOT (date <= ? AND NOT (date = ? AND id >= ?)))

"""
query_set = self.query_set
if value is not None:
Expand Down
21 changes: 21 additions & 0 deletions tests/migrations/0004_auto_20201009_2348.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 3.1.1 on 2020-10-09 23:48

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('tests', '0003_article_is_sticky'),
]

operations = [
migrations.AddIndex(
model_name='article',
index=models.Index(fields=['is_pinned', 'is_sticky', 'date', 'id'], name='tests_artic_is_pinn_819aa7_idx'),
),
migrations.AddIndex(
model_name='article',
index=models.Index(fields=['-is_pinned', '-is_sticky', '-date', '-id'], name='tests_artic_is_pinn_603b8c_idx'),
),
]
14 changes: 5 additions & 9 deletions tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,11 @@ class Article(models.Model):
is_pinned = models.BooleanField(default=False)
is_sticky = models.BooleanField(default=False)

#class Meta:
# The benchmarks show index are not used, see
# https://github.com/nitely/django-infinite-scroll-pagination/pull/8
#indexes = [
# models.Index(fields=['is_pinned', 'is_sticky', 'date', 'id']),
# models.Index(fields=['-is_pinned', '-is_sticky', '-date', '-id'])]
#indexes = [
# models.Index(fields=['date', 'id']),
# models.Index(fields=['-date', '-id'])]
class Meta:
# This is only used on the "row values" variation
indexes = [
models.Index(fields=['is_pinned', 'is_sticky', 'date', 'id']),
models.Index(fields=['-is_pinned', '-is_sticky', '-date', '-id'])]

def __unicode__(self):
return self.title
13 changes: 13 additions & 0 deletions tests/test_multi_fields.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#-*- coding: utf-8 -*-

import datetime
from unittest import skip

from django.test import TestCase
from django.utils import timezone
Expand Down Expand Up @@ -97,6 +98,7 @@ def test_prev_desc_is_pinned(self):
move_to=inf_paginator.PREV_PAGE)
self.assertListEqual(list(page_1), list(articles[:10]))

@skip
def test_next_asc_desc(self):
articles = Article.objects.all().order_by('is_pinned', "-date_unique")
for a in articles[:5]:
Expand All @@ -121,6 +123,7 @@ def test_next_asc_desc(self):
value=(page_2[-1].is_pinned, page_2[-1].date_unique))
self.assertListEqual(list(page_3), list(articles[20:]))

@skip
def test_next_desc_asc(self):
articles = Article.objects.all().order_by('-is_pinned', "date_unique")
for a in articles[:5]:
Expand All @@ -145,6 +148,7 @@ def test_next_desc_asc(self):
value=(page_2[-1].is_pinned, page_2[-1].date_unique))
self.assertListEqual(list(page_3), list(articles[20:]))

@skip
def test_prev_asc_desc(self):
articles = Article.objects.all().order_by('is_pinned', "-date_unique")
for a in articles[:5]:
Expand All @@ -169,6 +173,7 @@ def test_prev_asc_desc(self):
move_to=inf_paginator.PREV_PAGE)
self.assertListEqual(list(page_1), list(articles[:10]))

@skip
def test_prev_desc_asc(self):
articles = Article.objects.all().order_by('-is_pinned', "date_unique")
for a in articles[:5]:
Expand Down Expand Up @@ -279,6 +284,7 @@ def test_prev_desc_non_unique_is_pinned(self):
move_to=inf_paginator.PREV_PAGE)
self.assertListEqual(list(page_1), list(articles[:10]))

@skip
def test_next_asc_desc_non_unique(self):
articles = Article.objects.all().order_by('is_pinned', "-date", "-pk")
for a in articles[:5]:
Expand All @@ -305,6 +311,7 @@ def test_next_asc_desc_non_unique(self):
pk=page_2[-1].pk)
self.assertListEqual(list(page_3), list(articles[20:]))

@skip
def test_next_desc_asc_non_unique(self):
articles = Article.objects.all().order_by('-is_pinned', "date", "pk")
for a in articles[:5]:
Expand All @@ -331,6 +338,7 @@ def test_next_desc_asc_non_unique(self):
pk=page_2[-1].pk)
self.assertListEqual(list(page_3), list(articles[20:]))

@skip
def test_prev_asc_desc_non_unique(self):
articles = Article.objects.all().order_by('is_pinned', "-date", "-pk")
for a in articles[:5]:
Expand All @@ -357,6 +365,7 @@ def test_prev_asc_desc_non_unique(self):
move_to=inf_paginator.PREV_PAGE)
self.assertListEqual(list(page_1), list(articles[:10]))

@skip
def test_prev_desc_asc_non_unique(self):
articles = Article.objects.all().order_by('-is_pinned', "date", "pk")
for a in articles[:5]:
Expand Down Expand Up @@ -461,6 +470,7 @@ def test_next_desc_sticky_pinned(self):
value=(page_2[-1].is_pinned, page_2[-1].is_sticky, page_2[-1].date_unique))
self.assertListEqual(list(page_3), list(articles[20:]))

@skip
def test_next_desc_asc_sticky_pinned(self):
articles = Article.objects.all().order_by(
'-is_pinned', 'is_sticky', "-date_unique")
Expand Down Expand Up @@ -574,6 +584,7 @@ def test_prev_desc_sticky_pinned(self):
move_to=inf_paginator.PREV_PAGE)
self.assertListEqual(list(page_1), list(articles[:10]))

@skip
def test_prev_desc_asc_sticky_pinned(self):
articles = Article.objects.all().order_by(
'-is_pinned', 'is_sticky', "-date_unique")
Expand Down Expand Up @@ -681,6 +692,7 @@ def test_next_desc_sticky_pinned_non_unique(self):
pk=page_2[-1].pk)
self.assertListEqual(list(page_3), list(articles[20:]))

@skip
def test_next_desc_asc_sticky_pinned_non_unique(self):
articles = Article.objects.all().order_by(
'-is_pinned', 'is_sticky', "-date", '-pk')
Expand Down Expand Up @@ -802,6 +814,7 @@ def test_prev_desc_sticky_pinned_non_unique(self):
move_to=inf_paginator.PREV_PAGE)
self.assertListEqual(list(page_1), list(articles[:10]))

@skip
def test_prev_desc_asc_sticky_pinned_non_unique(self):
articles = Article.objects.all().order_by(
'-is_pinned', 'is_sticky', "-date", '-pk')
Expand Down
1 change: 0 additions & 1 deletion tests/tests.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#-*- coding: utf-8 -*-

from __future__ import unicode_literals
import datetime
import json

Expand Down