Skip to content
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

n+1 pagination #27

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
115 changes: 115 additions & 0 deletions graphql_relay/connection/arrayconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,121 @@ def connection_from_list_slice(list_slice, args=None, connection_type=None,
)


def connection_from_list_slice_lazy(list_slice, args=None, connection_type=None,
edge_type=None, pageinfo_type=None,
slice_start=0, list_length=0, list_slice_length=None):
'''
Given an iterator it consumes the needed amount based on the pagination
params, and also tries to figure out if there are more results to be
consumed. We do so by trying to fetch one more element than the specified
amount, if we were able to fetch n+1 it means there are more to be consumed.

This spares the caller passing the total count of results, which
usually means making an extra query just to find out that number.
'''
connection_type = connection_type or Connection
edge_type = edge_type or Edge
pageinfo_type = pageinfo_type or PageInfo

args = args or {}

before = args.get('before')
after = args.get('after')
first = args.get('first')
last = args.get('last')

first_is_negative = type(first) == int and first < 0
last_is_negative = type(last) == int and last < 0

start_offset = slice_start
end_offset = None

# Validations
if first_is_negative or last_is_negative:
raise ValueError("first and last can't be negative values")

if first and last:
raise ValueError(
"Including a value for both first and last is strongly discouraged,"
" as it is likely to lead to confusing queries and results"
)

if before and last is None:
raise ValueError(
"before without last is not supported"
)

if after and first is None:
raise ValueError(
"after without first is not supported"
)

if before and after:
raise ValueError(
"Mixing before and after is not supported"
)

if type(first) == int:
after_offset = get_offset_with_default(after, -1)
start_offset = after_offset + 1
end_offset = start_offset + first

elif type(last) == int:
if before:
before_offset = get_offset_with_default(before)
else:
try:
before_offset = int(list_slice.count())
Copy link

@lucas-bremond lucas-bremond May 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this defeating the purpose of this PR, since providing last would trigger a count?

There is likely no workaround, but I believe this should at least be made explicit that first and last do not exhibit symmetrical behaviors.

except (TypeError, AttributeError):
before_offset = len(list_slice)
end_offset = before_offset
start_offset = before_offset - last if before_offset - last > 0 else 0

# Slice with an extra item to figure out if there's a next page
_slice = list_slice[start_offset : end_offset + 1]
has_next_page = False

if (first and len(_slice) > first) or (last and len(_slice) > last):
has_next_page = True
del _slice[-1]

edges = [
edge_type(
node=node,
cursor=offset_to_cursor(start_offset + index)
)
for index, node in enumerate(_slice)
]

# To know if there are more pages we just check if there is n+1 element
# In this case it would just be the end_offset element because array indexes
# start from zero
extra_item = None

try:
extra_item = list_slice[end_offset]
has_next_page = True

except (IndexError, TypeError):
has_next_page = False

if extra_item is None:
has_next_page = False

has_previous_page = start_offset > 0
first_edge_cursor = edges[0].cursor if edges else None
last_edge_cursor = edges[-1].cursor if edges else None

return connection_type(
edges=edges,
page_info=pageinfo_type(
start_cursor=first_edge_cursor,
end_cursor=last_edge_cursor,
has_previous_page=has_previous_page,
has_next_page=has_next_page
)
)

PREFIX = 'arrayconnection:'


Expand Down