Chunkr.PaginatedQueries (Chunkr v0.1.0) View Source

Provides a set of macros for generating functions to assist with paginating queries. For example:

defmodule MyApp.PaginatedQueries do
  use Chunkr.PaginatedQueries

  paginate_by :user_created_at do
    sort :desc, as(:user).inserted_at
    sort :desc, as(:user).id, type: :binary_id
  end

  paginate_by :user_name do
    sort :asc, fragment("lower(coalesce(?, 'zzz')"), as(:user).name).inserted_at
    sort :desc, as(:user).id, type: :binary_id
  end
end

The paginate_by/1 macro above takes a query name and sets up the necessary beyond_cursor/4, apply_order/4, and apply_select/2 functions based on the number of sort options passed in the block as well as the sort directions specified.

Each call to sort/3 must include the sort direction, the field to be sorted, and an optional :type keyword. If :type is provided, the cursor value will be cast as that type for the sake of comparisons. See Ecto.Query.API.type/2.

Ordering

In keyset-based pagination, it is essential that results are deterministically ordered, otherwise you may see unexpected results. Therefore, the final column used for sorting must always be unique and non-NULL.

Ordering of paginated results can be based on columns from the primary table, any joined table, any subquery, or any dynamically computed value based on other fields. Regardless of where the column resides, named bindings are always required…

Named bindings

Because these sort/3 clauses must reference bindings that have not yet been established, each sort clause must use :as to take advantage of late binding. A parallel :as must then be used within the query that gets passed to Chunkr.paginate/3 or the query will fail. See Ecto Named bindings for more.

NULL values in sort fields

When using comparison operators in SQL, records involving comparisons against NULL get dropped. This is generally undesirable for pagination, as the goal is usually to work your way through an entire result set in chunks—not just through the part of the result set that doesn't have NULL values in the important fields. For example, when sorting users by [last name, first name, middle name], you most likely don't want to exclude users without a known middle name.

To work around this awkwardness, you'll need to pick a value that is almost sure to come before or after the rest of your results (depending on whether you want NULL values to sort to the beginning or the end of your results). It's not good enough to think you can simply use a strategy like ordering by NULLS LAST because the filtering of values up to the cursor values will use comparison operators—which will cause records with relevant NULL values to be dropped entirely.

The following fragment example sets up names to be compared in a case-insensitive fashion and places records with a NULL name at the end of the list (assuming no names will sort beyond "zzz"!).

sort :asc, fragment("lower(coalesce(?, 'zzz')"), as(:user).name).inserted_at

Limitations

Note that Chunkr limits the number of sort clauses to 4.

Link to this section Summary

Functions

Implements the functions necessary for pagination.

Link to this section Functions

Link to this macro

paginate_by(query_name, list)

View Source (macro)

Implements the functions necessary for pagination.

paginate_by :user_id do
  sort :asc, as(:user).id
end