├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── bin ├── hooks │ └── pre-commit.sh └── install-git-hooks.sh ├── config ├── .credo.exs ├── config.exs └── dogma.exs ├── coveralls.json ├── lib └── ecto │ ├── cursors.ex │ ├── paging.ex │ └── repo.ex ├── mix.exs ├── mix.lock ├── priv └── test_repo │ ├── migrations │ └── 20161007140320_add_apis_table.exs │ └── seeds.exs └── test ├── support ├── model_case.ex ├── schema.ex └── test_repo.ex ├── test_helper.exs └── unit └── ecto_paging_test.exs /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | 19 | # Don't commit benchmark snapshots 20 | bench/snapshots 21 | 22 | # Don't commit editor configs 23 | .idea 24 | *.iws 25 | /out/ 26 | atlassian-ide-plugin.xml 27 | *.tmlanguage.cache 28 | *.tmPreferences.cache 29 | *.stTheme.cache 30 | *.sublime-workspace 31 | sftp-config.json 32 | GitHub.sublime-settings 33 | .tags 34 | .tags_sorted_by_file 35 | .vagrant 36 | .DS_Store 37 | 38 | # Ignore released binaries 39 | rel/*/ 40 | !rel/config.exs 41 | .deliver 42 | 43 | # Don't commit file uploads 44 | uploads/ 45 | !uploads/.gitkeep 46 | 47 | # Don't commit test env config 48 | config/test.exs 49 | docker-compose.yml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | cache: 3 | directories: 4 | - deps 5 | services: 6 | - postgresql 7 | addons: 8 | postgresql: "9.4" 9 | elixir: 10 | - 1.4.2 11 | otp_release: 12 | - 19.3 13 | env: 14 | global: 15 | - MIX_ENV=test 16 | script: 17 | # Install dependencies 18 | - "mix deps.get" 19 | # Run all tests except pending ones 20 | - "mix test --exclude pending --trace" 21 | # Submit code coverage report to Coveralls 22 | - "mix coveralls.travis" 23 | # Run static code analysis 24 | - "mix credo" 25 | # Check code style 26 | - "mix dogma" 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nebo #15 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ecto.Paging 2 | 3 | [![Deps Status](https://beta.hexfaktor.org/badge/all/github/Nebo15/ecto_paging.svg)](https://beta.hexfaktor.org/github/Nebo15/ecto_paging) [![Build Status](https://travis-ci.org/Nebo15/ecto_paging.svg?branch=master)](https://travis-ci.org/Nebo15/ecto_paging) [![Coverage Status](https://coveralls.io/repos/github/Nebo15/ecto_paging/badge.svg?branch=master)](https://coveralls.io/github/Nebo15/ecto_paging?branch=master) 4 | 5 | This module provides a easy way to apply cursor-based pagination to your Ecto Queries. 6 | 7 | ## Usage: 8 | 9 | 1. Add macro to your repo 10 | 11 | ```elixir 12 | defmodule MyRepo do 13 | use Ecto.Repo, otp_app: :my_app 14 | use Ecto.Paging.Repo # This string adds `paginate/2` and `page/3` methods. 15 | end 16 | ``` 17 | 18 | 2. Paginate! 19 | 20 | ```elixir 21 | query = from p in Ecto.Paging.Schema 22 | 23 | {res, next_paging} = query 24 | |> Ecto.Paging.TestRepo.page(%Ecto.Paging{limit: 150}) 25 | ``` 26 | 27 | ## Limitations: 28 | 29 | * Right now it works only with schemas that have `:inserted_at` field with auto-generated value. 30 | * You need to be careful with order-by's in your queries, since this feature is not tested yet. 31 | * It doesn't construct `has_more` and `size` counts in `paginate` struct (TODO: add this helpers). 32 | * When both `starting_after` and `ending_before` is set, only `starting_after` is used. 33 | 34 | ## Installation 35 | 36 | 1. Add `ecto_paging` to your list of dependencies in `mix.exs`: 37 | 38 | ```elixir 39 | def deps do 40 | [{:ecto_paging, "~> 0.8.4"}] 41 | end 42 | ``` 43 | 44 | 2. Ensure `ecto_paging` is started before your application: 45 | 46 | ```elixir 47 | def application do 48 | [applications: [:ecto_paging]] 49 | end 50 | ``` 51 | -------------------------------------------------------------------------------- /bin/hooks/pre-commit.sh: -------------------------------------------------------------------------------- 1 | mix test 2 | mix coveralls 3 | mix credo 4 | mix dogma 5 | -------------------------------------------------------------------------------- /bin/install-git-hooks.sh: -------------------------------------------------------------------------------- 1 | ln -s ../../bin/hooks/pre-commit.sh .git/hooks/pre-commit 2 | -------------------------------------------------------------------------------- /config/.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | files: %{ 6 | included: ["lib/", "www/"], 7 | excluded: ["lib/ecto_paging/tasks.ex"] 8 | }, 9 | checks: [ 10 | {Credo.Check.Design.TagTODO, exit_status: 0}, 11 | {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 120} 12 | ] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :ecto_paging, Ecto.Paging.TestRepo, 4 | adapter: Ecto.Adapters.Postgres, 5 | pool: Ecto.Adapters.SQL.Sandbox, 6 | database: "ecto_paging_test", 7 | username: "postgres", 8 | password: "postgres", 9 | hostname: "localhost" 10 | 11 | config :ecto_paging, ecto_repos: [Ecto.Paging.TestRepo] 12 | 13 | config :logger, level: :error 14 | config :ex_unit, capture_log: true 15 | -------------------------------------------------------------------------------- /config/dogma.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | alias Dogma.Rule 3 | 4 | config :dogma, 5 | rule_set: Dogma.RuleSet.All, 6 | exclude: [ 7 | ~r(\Adeps/), 8 | ], 9 | override: [ 10 | %Rule.LineLength{ max_length: 120 }, 11 | %Rule.TakenName{ enabled: false }, # TODO: https://github.com/lpil/dogma/issues/201 12 | %Rule.InfixOperatorPadding{ enabled: false }, 13 | %Rule.FunctionArity{ max: 5 }, 14 | ] 15 | -------------------------------------------------------------------------------- /coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "skip_files": [ 3 | "test/*" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /lib/ecto/cursors.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Paging.Cursors do 2 | @moduledoc false 3 | 4 | @doc """ 5 | This is struct for `Ecto.Paging` that holds cursors. 6 | """ 7 | defstruct starting_after: nil, 8 | ending_before: nil 9 | 10 | @type t :: %{starting_after: any, ending_before: any} 11 | end 12 | -------------------------------------------------------------------------------- /lib/ecto/paging.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Paging do 2 | @moduledoc """ 3 | This module provides a easy way to apply cursor-based pagination to your Ecto Queries. 4 | 5 | ## Usage: 6 | 1. Add macro to your repo 7 | 8 | defmodule MyRepo do 9 | use Ecto.Repo, otp_app: :my_app 10 | use Ecto.Paging.Repo # This string adds `paginate/2` method. 11 | end 12 | 13 | 2. Paginate! 14 | 15 | query = from p in Ecto.Paging.Schema 16 | 17 | query 18 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 150}) 19 | |> Ecto.Paging.TestRepo.all 20 | 21 | ## Limitations: 22 | * Right now it works only with schemas that have `:inserted_at` field with auto-generated value. 23 | * You need to be careful with order-by's in your queries, since this feature is not tested yet. 24 | * It doesn't construct `paginate` struct with `has_more` and `size` counts (TODO: add this helpers). 25 | * When both `starting_after` and `ending_before` is set, only `starting_after` is used. 26 | """ 27 | import Ecto.Query 28 | 29 | @type t :: %{limit: number, cursors: Ecto.Paging.Cursors.t, has_more: number, size: number} 30 | 31 | @doc """ 32 | This struct defines pagination rules. 33 | It can be used in your response API. 34 | """ 35 | defstruct limit: 50, 36 | cursors: %Ecto.Paging.Cursors{}, 37 | has_more: nil, 38 | size: nil 39 | 40 | @doc """ 41 | Convert map into `Ecto.Paging` struct. 42 | """ 43 | def from_map(%Ecto.Paging{cursors: %Ecto.Paging.Cursors{}} = paging), do: paging 44 | 45 | def from_map(%{cursors: %Ecto.Paging.Cursors{} = cursors} = paging) do 46 | Ecto.Paging 47 | |> struct(paging) 48 | |> Map.put(:cursors, cursors) 49 | end 50 | 51 | def from_map(%{cursors: cursors} = paging) when is_map(cursors) do 52 | cursors = struct(Ecto.Paging.Cursors, cursors) 53 | from_map(%{paging | cursors: cursors}) 54 | end 55 | 56 | def from_map(paging) when is_map(paging) do 57 | Ecto.Paging 58 | |> struct(paging) 59 | |> from_map() 60 | end 61 | 62 | @doc """ 63 | Convert `Ecto.Paging` struct into map and drop all nil values and `cursors` property if it's empty. 64 | """ 65 | def to_map(%Ecto.Paging{cursors: cursors} = paging) do 66 | cursors = cursors 67 | |> Map.delete(:__struct__) 68 | |> Enum.filter(fn {_, v} -> v end) 69 | |> Enum.into(%{}) 70 | 71 | paging 72 | |> Map.delete(:__struct__) 73 | |> Map.put(:cursors, cursors) 74 | |> Enum.filter(fn {_, v} -> is_map(v) && v != %{} or not is_map(v) and v end) 75 | |> Enum.into(%{}) 76 | end 77 | 78 | @doc """ 79 | Apply pagination to a `Ecto.Query`. 80 | It can accept either `Ecto.Paging` struct or map that can be converted to it via `from_map/1`. 81 | """ 82 | def paginate(%Ecto.Query{} = query, 83 | %Ecto.Paging{limit: limit, cursors: %Ecto.Paging.Cursors{} = cursors}, 84 | [repo: _, chronological_field: _] = opts) 85 | when is_integer(limit) do 86 | pk = get_primary_key(query) 87 | 88 | query 89 | |> limit(^limit) 90 | |> filter_by_cursors(cursors, pk, opts) 91 | end 92 | 93 | def paginate(%Ecto.Query{} = query, %Ecto.Paging{}, _opts), do: query 94 | def paginate(%Ecto.Query{} = query, paging, opts) when is_map(paging) do 95 | paginate(query, Ecto.Paging.from_map(paging), opts) 96 | end 97 | 98 | def paginate(queriable, paging, opts) when is_atom(queriable) do 99 | queriable 100 | |> Ecto.Queryable.to_query() 101 | |> paginate(paging, opts) 102 | end 103 | 104 | @doc """ 105 | Build a `%Ecto.Paging{}` struct to fetch next page results based on previous `Ecto.Repo.all` result 106 | and previous paging struct. 107 | """ 108 | def get_next_paging(query_result, %Ecto.Paging{limit: nil} = paging) do 109 | get_next_paging(query_result, %{paging | limit: length(query_result)}) 110 | end 111 | 112 | def get_next_paging(query_result, %Ecto.Paging{limit: limit, cursors: cursors}) when is_list(query_result) do 113 | has_more = length(query_result) >= limit 114 | %Ecto.Paging{ 115 | limit: limit, 116 | has_more: has_more, 117 | cursors: get_next_cursors(query_result, cursors) 118 | } 119 | end 120 | 121 | def get_next_paging(query_result, paging) when is_map(paging) do 122 | get_next_paging(query_result, Ecto.Paging.from_map(paging)) 123 | end 124 | 125 | defp get_next_cursors([], _) do 126 | %Ecto.Paging.Cursors{starting_after: nil, ending_before: nil} 127 | end 128 | 129 | defp get_next_cursors(query_result, _) do 130 | %Ecto.Paging.Cursors{starting_after: List.last(query_result).id, 131 | ending_before: List.first(query_result).id} 132 | end 133 | 134 | defp filter_by_cursors(%Ecto.Query{from: {table, schema}, order_bys: order_bys} = query, 135 | %{starting_after: starting_after}, pk, 136 | [repo: repo, chronological_field: chronological_field]) 137 | when not is_nil(starting_after) do 138 | pk_type = schema.__schema__(:type, pk) 139 | case extract_timestamp(repo, table, {pk_type, pk}, starting_after, chronological_field) do 140 | {:ok, ts} -> 141 | order = get_order_from_expression(order_bys) 142 | query 143 | |> find_where_order(chronological_field, ts, :starting_after) 144 | |> set_default_order(order, pk_type, pk, chronological_field) 145 | {:error, :not_found} -> 146 | where(query, [c], false) 147 | end 148 | end 149 | 150 | defp filter_by_cursors(%Ecto.Query{from: {table, schema}} = query, %{ending_before: ending_before}, pk, 151 | [repo: repo, chronological_field: chronological_field]) 152 | when not is_nil(ending_before) do 153 | pk_type = schema.__schema__(:type, pk) 154 | case extract_timestamp(repo, table, {pk_type, pk}, ending_before, chronological_field) do 155 | {:ok, ts} -> 156 | {rev_order, q} = query 157 | |> find_where_order(chronological_field, ts, :ending_before) 158 | |> flip_orders(pk, pk_type, chronological_field) 159 | restore_query_order(rev_order, pk_type, pk, q, chronological_field) 160 | {:error, :not_found} -> 161 | where(query, [c], false) 162 | end 163 | end 164 | 165 | defp filter_by_cursors(query, %{ending_before: nil, starting_after: nil}, _pk, _opts), do: query 166 | 167 | defp extract_timestamp(repo, table, {pk_type, pk}, pk_value, chronological_field) do 168 | start_timestamp_native = 169 | repo.one from r in table, 170 | where: field(r, ^pk) == type(^pk_value, ^pk_type), 171 | select: field(r, ^chronological_field) 172 | 173 | case start_timestamp_native do 174 | nil -> 175 | {:error, :not_found} 176 | timestamp -> 177 | {:ok, start_timestamp} = Ecto.DateTime.load(timestamp) 178 | {:ok, Ecto.DateTime.to_string(start_timestamp)} 179 | end 180 | end 181 | 182 | def find_where_order(%Ecto.Query{order_bys: order_bys} = query, chronological_field, timestamp, :ending_before) 183 | when is_list(order_bys) and length(order_bys) > 0 do 184 | case get_order_from_expression(order_bys) do 185 | :asc -> where(query, [c], field(c, ^chronological_field) < ^timestamp) 186 | :desc -> where(query, [c], field(c, ^chronological_field) > ^timestamp) 187 | end 188 | end 189 | 190 | def find_where_order(%Ecto.Query{order_bys: order_bys} = query, chronological_field, timestamp, :starting_after) 191 | when is_list(order_bys) and length(order_bys) > 0 do 192 | case get_order_from_expression(order_bys) do 193 | :asc -> where(query, [c], field(c, ^chronological_field) > ^timestamp) 194 | :desc -> where(query, [c], field(c, ^chronological_field) < ^timestamp) 195 | end 196 | end 197 | 198 | def find_where_order(%Ecto.Query{} = query, chronological_field, timestamp, :ending_before) do 199 | where(query, [c], field(c, ^chronological_field) < ^timestamp) 200 | end 201 | def find_where_order(%Ecto.Query{} = query, chronological_field, timestamp, :starting_after) do 202 | where(query, [c], field(c, ^chronological_field) > ^timestamp) 203 | end 204 | 205 | defp flip_orders(%Ecto.Query{order_bys: order_bys} = query, _pk, _pk_type, chronological_field) 206 | when is_list(order_bys) and length(order_bys) > 0 do 207 | order = get_order_from_expression(order_bys) 208 | query = case order do 209 | :asc -> query |> exclude(:order_by) |> order_by([c], desc: field(c, ^chronological_field)) 210 | :desc -> exclude(query, :order_by) 211 | end 212 | {order, query} 213 | end 214 | 215 | defp flip_orders(%Ecto.Query{} = query, _pk, :string, chronological_field) do 216 | {:asc, order_by(query, [c], desc: field(c, ^chronological_field))} 217 | end 218 | 219 | defp flip_orders(%Ecto.Query{} = query, _pk, :binary_id, chronological_field) do 220 | {:asc, order_by(query, [c], desc: field(c, ^chronological_field))} 221 | end 222 | 223 | defp flip_orders(%Ecto.Query{} = query, pk, _pk_type, _chronological_field) do 224 | {:asc, order_by(query, [c], desc: field(c, ^pk))} 225 | end 226 | 227 | defp restore_query_order(order, :binary_id, _pk, query, chronological_field) do 228 | from e in subquery(query), order_by: [{^order, ^chronological_field}] 229 | end 230 | 231 | defp restore_query_order(order, :string, _pk, query, chronological_field) do 232 | from e in subquery(query), order_by: [{^order, ^chronological_field}] 233 | end 234 | 235 | defp restore_query_order(order, _pk_type, pk, query, _chronological_field) do 236 | from e in subquery(query), order_by: [{^order, ^pk}] 237 | end 238 | 239 | defp set_default_order(query, order, :binary_id, _pk, chronological_field) do 240 | order_by(query, [{^order, ^chronological_field}]) 241 | end 242 | 243 | defp set_default_order(query, order, :string, _pk, chronological_field) do 244 | order_by(query, [{^order, ^chronological_field}]) 245 | end 246 | 247 | defp set_default_order(query, order, _, pk, _chronological_field) do 248 | order_by(query, [{^order, ^pk}]) 249 | end 250 | 251 | defp get_primary_key(%Ecto.Query{from: {_, model}}) do 252 | :primary_key 253 | |> model.__schema__ 254 | |> List.first 255 | end 256 | 257 | defp get_order_from_expression([]), do: :asc 258 | defp get_order_from_expression(expression) do 259 | [%Ecto.Query.QueryExpr{expr: expr} | _t] = expression 260 | case {Keyword.has_key?(expr, :asc), Keyword.has_key?(expr, :desc)} do 261 | {true, false} -> :asc 262 | {false, true} -> :desc 263 | _ -> :asc 264 | end 265 | end 266 | end 267 | -------------------------------------------------------------------------------- /lib/ecto/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Paging.Repo do 2 | @moduledoc """ 3 | This module provides macro that injects `paginate/2` and `page/3` methods into Ecto.Repo's. 4 | """ 5 | defmacro __using__(_) do 6 | quote location: :keep do 7 | @conf [repo: __MODULE__, chronological_field: :inserted_at] 8 | 9 | @doc """ 10 | Convert queriable to queryable with applied pagination rules from `paging`. 11 | """ 12 | def paginate(queryable, paging) do 13 | Ecto.Paging.paginate(queryable, paging, @conf) 14 | end 15 | 16 | @doc """ 17 | Fetches all entries from the data store matching the given query with applied Paging. 18 | 19 | May raise `Ecto.QueryError` if query validation fails. 20 | 21 | ## Options 22 | 23 | See the "[Shared options](https://hexdocs.pm/ecto/Ecto.Repo.html#module-shared-options)" 24 | section at the `Ecto.Repo` module documentation. 25 | 26 | ## Example 27 | 28 | # Fetch 50 post titles 29 | query = from p in Post, 30 | select: p.title 31 | 32 | MyRepo.all(query, %Ecto.Paging{limit: 50}) 33 | """ 34 | @spec page(queryable :: Ecto.Query.t, paging :: Ecto.Paging.t, opts :: Keyword.t) 35 | :: {[Ecto.Schema.t], Ecto.Paging.t} | no_return 36 | def page(queryable, paging, opts \\ []) do 37 | res = queryable 38 | |> paginate(paging) 39 | |> __MODULE__.all(opts) 40 | 41 | case res do 42 | list when is_list(list) -> {list, Ecto.Paging.get_next_paging(list, paging)} 43 | err -> err 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Paging.Mixfile do 2 | use Mix.Project 3 | 4 | @version "0.8.4" 5 | 6 | def project do 7 | [app: :ecto_paging, 8 | description: "Cursor-based pagination for Ecto.", 9 | package: package(), 10 | version: @version, 11 | elixir: "~> 1.3", 12 | elixirc_paths: elixirc_paths(Mix.env), 13 | compilers: [] ++ Mix.compilers, 14 | build_embedded: Mix.env == :prod, 15 | start_permanent: Mix.env == :prod, 16 | aliases: aliases(), 17 | deps: deps(), 18 | test_coverage: [tool: ExCoveralls], 19 | preferred_cli_env: [coveralls: :test], 20 | docs: [source_ref: "v#\{@version\}", main: "readme", extras: ["README.md"]]] 21 | end 22 | 23 | # Configuration for the OTP application 24 | # 25 | # Type "mix help compile.app" for more information 26 | def application do 27 | [applications: [:logger, :ecto]] 28 | end 29 | 30 | # Specifies which paths to compile per environment. 31 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"] 32 | defp elixirc_paths(_), do: ["lib", "web"] 33 | 34 | # Dependencies can be Hex packages: 35 | # 36 | # {:mydep, "~> 0.3.0"} 37 | # 38 | # Or git/path repositories: 39 | # 40 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 41 | # 42 | # To depend on another app inside the umbrella: 43 | # 44 | # {:myapp, in_umbrella: true} 45 | # 46 | # Type "mix help deps" for more examples and options 47 | defp deps do 48 | [{:ecto, "~> 2.2"}, 49 | {:postgrex, "~> 0.13.2", optional: true}, 50 | {:benchfella, "~> 0.3", only: [:dev, :test]}, 51 | {:ex_doc, ">= 0.0.0", only: [:dev, :test]}, 52 | {:excoveralls, "~> 0.5", only: [:dev, :test]}, 53 | {:dogma, "> 0.1.0", only: [:dev, :test]}, 54 | {:credo, ">= 0.4.8", only: [:dev, :test]}] 55 | end 56 | 57 | # Settings for publishing in Hex package manager: 58 | defp package do 59 | [contributors: ["Nebo #15"], 60 | maintainers: ["Nebo #15"], 61 | licenses: ["LISENSE.md"], 62 | links: %{github: "https://github.com/Nebo15/ecto_paging"}, 63 | files: ~w(lib LICENSE.md mix.exs README.md)] 64 | end 65 | 66 | # Aliases are shortcuts or tasks specific to the current project. 67 | # For example, to create, migrate and run the seeds file at once: 68 | # 69 | # $ mix ecto.setup 70 | # 71 | # See the documentation for `Mix` for more info on aliases. 72 | defp aliases do 73 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 74 | "ecto.reset": ["ecto.drop", "ecto.setup"], 75 | "test": ["ecto.create --quiet", "ecto.migrate", "test"]] 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm"}, 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 3 | "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, 4 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, 5 | "credo": {:hex, :credo, "0.8.6", "335f723772d35da499b5ebfdaf6b426bfb73590b6fcbc8908d476b75f8cbca3f", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "db_connection": {:hex, :db_connection, "1.1.2", "2865c2a4bae0714e2213a0ce60a1b12d76a6efba0c51fbda59c9ab8d1accc7a8", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, 7 | "decimal": {:hex, :decimal, "1.4.0", "fac965ce71a46aab53d3a6ce45662806bdd708a4a95a65cde8a12eb0124a1333", [:mix], [], "hexpm"}, 8 | "dogma": {:hex, :dogma, "0.1.15", "5bceba9054b2b97a4adcb2ab4948ca9245e5258b883946e82d32f785340fd411", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], [], "hexpm"}, 10 | "ecto": {:hex, :ecto, "2.2.1", "ccc6fd304f9bb785f2c3cfd0ee8da6bad6544ab12ca5f7162b20a743d938417c", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, 11 | "ex_doc": {:hex, :ex_doc, "0.16.3", "cd2a4cfe5d26e37502d3ec776702c72efa1adfa24ed9ce723bb565f4c30bd31a", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, 12 | "excoveralls": {:hex, :excoveralls, "0.7.2", "f69ede8c122ccd3b60afc775348a53fc8c39fe4278aee2f538f0d81cc5e7ff3a", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, 13 | "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, 14 | "hackney": {:hex, :hackney, "1.9.0", "51c506afc0a365868469dcfc79a9d0b94d896ec741cfd5bd338f49a5ec515bfe", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 15 | "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 16 | "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], [], "hexpm"}, 17 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 18 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, 19 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 20 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, 21 | "postgrex": {:hex, :postgrex, "0.13.3", "c277cfb2a9c5034d445a722494c13359e361d344ef6f25d604c2353185682bfc", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, 22 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, 23 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}} 24 | -------------------------------------------------------------------------------- /priv/test_repo/migrations/20161007140320_add_apis_table.exs: -------------------------------------------------------------------------------- 1 | defmodule Gateway.DB.Repo.Migrations.AddApisTable do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:apis) do 6 | add :name, :string 7 | add :request, :map 8 | 9 | timestamps() 10 | end 11 | 12 | create table(:apis_utc) do 13 | add :name, :string 14 | add :request, :map 15 | 16 | timestamps(type: :utc_datetime) 17 | end 18 | 19 | create table(:apis_binary, primary_key: false) do 20 | add :id, :uuid, primary_key: true 21 | add :name, :string 22 | add :request, :map 23 | 24 | timestamps() 25 | end 26 | 27 | create table(:apis_string, primary_key: false) do 28 | add :id, :string, primary_key: true 29 | add :name, :string 30 | add :request, :map 31 | 32 | timestamps() 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /priv/test_repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # Ecto.Paging.Repo.insert!(%Ecto.Paging.SomeModel{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will halt execution if something goes wrong. 12 | -------------------------------------------------------------------------------- /test/support/model_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Paging.ModelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | model tests. 5 | You may define functions here to be used as helpers in 6 | your model tests. See `errors_on/2`'s definition as reference. 7 | Finally, if the test case interacts with the database, 8 | it cannot be async. For this reason, every test runs 9 | inside a transaction which is reset at the beginning 10 | of the test unless the test case is marked as async. 11 | """ 12 | 13 | use ExUnit.CaseTemplate 14 | 15 | using do 16 | quote do 17 | alias Ecto.Paging.TestRepo 18 | 19 | import Ecto 20 | import Ecto.Changeset 21 | import Ecto.Query 22 | import Ecto.Paging.ModelCase 23 | end 24 | end 25 | 26 | setup tags do 27 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Ecto.Paging.TestRepo) 28 | 29 | unless tags[:async] do 30 | Ecto.Adapters.SQL.Sandbox.mode(Ecto.Paging.TestRepo, {:shared, self()}) 31 | end 32 | 33 | :ok 34 | end 35 | 36 | @doc """ 37 | Helper for returning list of errors in a struct when given certain data. 38 | ## Examples 39 | Given a User schema that lists `:name` as a required field and validates 40 | `:password` to be safe, it would return: 41 | iex> errors_on(%User{}, %{password: "password"}) 42 | [password: "is unsafe", name: "is blank"] 43 | You could then write your assertion like: 44 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"}) 45 | """ 46 | def errors_on(struct, data) do 47 | data 48 | |> (&struct.__struct__.changeset(struct, &1)).() 49 | |> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/support/schema.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Paging.TestSchema do 2 | @moduledoc false 3 | use Ecto.Schema 4 | 5 | schema "apis" do 6 | field :name, :string 7 | 8 | embeds_one :request, Request, primary_key: false do 9 | field :scheme, :string 10 | field :host, :string 11 | field :port, :integer 12 | field :path, :string 13 | end 14 | 15 | timestamps() 16 | end 17 | end 18 | 19 | defmodule Ecto.Paging.UTCTestSchema do 20 | @moduledoc false 21 | use Ecto.Schema 22 | 23 | schema "apis_utc" do 24 | field :name, :string 25 | 26 | embeds_one :request, Request, primary_key: false do 27 | field :scheme, :string 28 | field :host, :string 29 | field :port, :integer 30 | field :path, :string 31 | end 32 | 33 | timestamps(type: :utc_datetime) 34 | end 35 | end 36 | 37 | defmodule Ecto.Paging.BinaryTestSchema do 38 | @moduledoc false 39 | use Ecto.Schema 40 | 41 | @primary_key {:id, :binary_id, autogenerate: true} 42 | schema "apis_binary" do 43 | field :name, :string 44 | 45 | embeds_one :request, Request, primary_key: false do 46 | field :scheme, :string 47 | field :host, :string 48 | field :port, :integer 49 | field :path, :string 50 | end 51 | 52 | timestamps() 53 | end 54 | end 55 | 56 | defmodule Ecto.Paging.StringTestSchema do 57 | @moduledoc false 58 | use Ecto.Schema 59 | 60 | @primary_key {:id, :string, autogenerate: false} 61 | schema "apis_string" do 62 | field :name, :string 63 | 64 | embeds_one :request, Request, primary_key: false do 65 | field :scheme, :string 66 | field :host, :string 67 | field :port, :integer 68 | field :path, :string 69 | end 70 | 71 | timestamps() 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/support/test_repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Paging.TestRepo do 2 | @moduledoc """ 3 | Repo for Ecto database. 4 | 5 | More info: https://hexdocs.pm/ecto/Ecto.Repo.html 6 | """ 7 | 8 | use Ecto.Repo, otp_app: :ecto_paging 9 | use Ecto.Paging.Repo 10 | end 11 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Ecto.Paging.TestRepo.start_link 2 | ExUnit.start() 3 | Ecto.Adapters.SQL.Sandbox.mode(Ecto.Paging.TestRepo, :manual) 4 | -------------------------------------------------------------------------------- /test/unit/ecto_paging_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.PagingTest do 2 | use Ecto.Paging.ModelCase, async: false 3 | alias Ecto.Paging 4 | doctest Ecto.Paging 5 | 6 | test "works on empty list" do 7 | res = get_query() 8 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 50}) 9 | |> Ecto.Paging.TestRepo.all 10 | 11 | assert res == [] 12 | end 13 | 14 | test "works with corrupted cursors" do 15 | query = 16 | get_query() 17 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{ 18 | limit: 50, 19 | cursors: %Ecto.Paging.Cursors{starting_after: 50, ending_before: 50} 20 | }) 21 | 22 | assert Ecto.Paging.TestRepo.all(query) == [] 23 | 24 | insert_records() 25 | 26 | assert Ecto.Paging.TestRepo.all(query) == [] 27 | end 28 | 29 | test "explicitly defined ASC order works" do 30 | records = insert_records() 31 | {:ok, record} = List.last(records) 32 | {{:ok, penultimate_record}, _list} = List.pop_at(records, length(records) - 2) 33 | res1 = 34 | get_query() 35 | |> Ecto.Query.order_by(asc: :inserted_at) 36 | |> Ecto.Paging.TestRepo.paginate(%{limit: 50, cursors: %{ending_before: record.id}}) 37 | |> Ecto.Paging.TestRepo.all 38 | assert penultimate_record.id == Enum.at(res1, 49).id 39 | # Ordering not influencing pagination 40 | assert length(res1) == 50 41 | end 42 | 43 | describe "converts from map" do 44 | test "with valid root struct" do 45 | assert %Ecto.Paging{limit: 50} = Paging.from_map(%Ecto.Paging{limit: 50}) 46 | end 47 | 48 | test "with valid cursors struct" do 49 | assert %Ecto.Paging{cursors: %Ecto.Paging.Cursors{starting_after: 50}} 50 | = Paging.from_map(%Ecto.Paging{cursors: %Ecto.Paging.Cursors{starting_after: 50}}) 51 | end 52 | 53 | test "with root values" do 54 | assert %Ecto.Paging{limit: 50} = Paging.from_map(%{limit: 50}) 55 | assert %Ecto.Paging{has_more: true} = Paging.from_map(%{has_more: true}) 56 | end 57 | 58 | test "with cursors" do 59 | assert %Ecto.Paging{cursors: %Ecto.Paging.Cursors{}} = Paging.from_map(%{}) 60 | assert %Ecto.Paging{cursors: %Ecto.Paging.Cursors{starting_after: 50}} 61 | = Paging.from_map(%{cursors: %{starting_after: 50}}) 62 | assert %Ecto.Paging{cursors: %Ecto.Paging.Cursors{ending_before: 50}} 63 | = Paging.from_map(%{cursors: %{ending_before: 50}}) 64 | end 65 | 66 | test "with damaged cursors" do 67 | assert %Ecto.Paging{cursors: %Ecto.Paging.Cursors{starting_after: 3}} 68 | = Paging.from_map(%Ecto.Paging{cursors: %{starting_after: 3}}) 69 | end 70 | end 71 | 72 | describe "converts to map" do 73 | test "drops struct" do 74 | assert %Ecto.Paging{cursors: %Ecto.Paging.Cursors{}} 75 | |> Paging.to_map() 76 | |> is_map() 77 | end 78 | 79 | test "keeps raw values" do 80 | assert %{limit: 50} = Paging.to_map(%Ecto.Paging{limit: 50}) 81 | assert %{has_more: true} = Paging.to_map(%Ecto.Paging{has_more: true}) 82 | end 83 | 84 | test "keeps cursors" do 85 | assert %{cursors: %{starting_after: 50}} 86 | = Paging.to_map(%Ecto.Paging{cursors: %Ecto.Paging.Cursors{starting_after: 50}}) 87 | assert %{cursors: %{ending_before: 50}} 88 | = Paging.to_map(%Ecto.Paging{cursors: %Ecto.Paging.Cursors{ending_before: 50}}) 89 | end 90 | end 91 | 92 | describe "paginator on integer id" do 93 | setup do 94 | insert_records() 95 | :ok 96 | end 97 | 98 | test "limits results" do 99 | res1 = 100 | get_query() 101 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 50}) 102 | |> Ecto.Paging.TestRepo.all 103 | 104 | assert length(res1) == 50 105 | 106 | res2 = 107 | get_query() 108 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 101}) 109 | |> Ecto.Paging.TestRepo.all 110 | 111 | {res3, _paging} = 112 | get_query() 113 | |> Ecto.Paging.TestRepo.page(%Ecto.Paging{limit: 101}) 114 | 115 | assert res2 == res3 116 | 117 | assert length(res2) == 101 118 | 119 | assert 0 == 120 | 0..49 121 | |> Enum.filter(fn index -> 122 | Enum.at(res1, index).id != Enum.at(res2, index).id 123 | end) 124 | |> length 125 | end 126 | 127 | test "works with schema" do 128 | {res, _paging} = Ecto.Paging.TestRepo.page(Ecto.Paging.TestSchema, %Ecto.Paging{limit: 101}) 129 | 130 | assert length(res) == 101 131 | end 132 | 133 | test "has default limit" do 134 | {res, _paging} = Ecto.Paging.TestRepo.page(Ecto.Paging.TestSchema, %{}) 135 | 136 | assert length(res) == 50 137 | end 138 | 139 | test "works with dropped limit" do 140 | {res, _paging} = Ecto.Paging.TestRepo.page(Ecto.Paging.TestSchema, %{limit: nil}) 141 | 142 | assert length(res) == 150 143 | end 144 | 145 | test "paginates forward with starting after" do 146 | res1 = 147 | get_query() 148 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 150}) 149 | |> Ecto.Paging.TestRepo.all 150 | 151 | assert length(res1) == 150 152 | 153 | starting_record = Enum.at(res1, 49) 154 | second_record = Enum.at(res1, 50) 155 | 156 | res2 = 157 | get_query() 158 | |> Ecto.Paging.TestRepo.paginate(%{limit: 50, cursors: %{starting_after: starting_record.id}}) 159 | |> Ecto.Paging.TestRepo.all 160 | 161 | {res3, paging} = 162 | get_query() 163 | |> Ecto.Paging.TestRepo.page(%{limit: 50, cursors: %{starting_after: starting_record.id}}) 164 | 165 | %Ecto.Paging{cursors: cursors} = paging 166 | assert cursors.ending_before == Enum.at(res1, 50).id 167 | assert cursors.starting_after == Enum.at(res1, 99).id 168 | 169 | assert res2 == res3 170 | assert second_record in res2 171 | refute starting_record in res2 172 | 173 | assert length(res2) == 50 174 | 175 | # Second query should be subset of first one 176 | assert 0 == 177 | 0..49 178 | |> Enum.filter(fn index -> 179 | Enum.at(res1, index + 50).id != Enum.at(res2, index).id 180 | end) 181 | |> length 182 | 183 | assert List.last(res2).id == paging.cursors.starting_after 184 | assert paging.has_more 185 | end 186 | 187 | test "ending_before is not nil" do 188 | {[head | _] = items, paging} = Ecto.Paging.TestRepo.page(Ecto.Paging.TestSchema, %{limit: 150}) 189 | assert 150 == Enum.count(items) 190 | assert %{cursors: %{ending_before: ending_before}, has_more: true} = paging 191 | assert head.id == ending_before 192 | end 193 | 194 | test "paginates forward with starting after with ordering" do 195 | res1 = 196 | get_query() 197 | |> Ecto.Query.order_by(asc: :inserted_at) 198 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 150}) 199 | |> Ecto.Paging.TestRepo.all 200 | 201 | assert length(res1) == 150 202 | 203 | starting_record = Enum.at(res1, 49) 204 | second_record = Enum.at(res1, 50) 205 | 206 | res2 = 207 | get_query() 208 | |> Ecto.Query.order_by(asc: :inserted_at) 209 | |> Ecto.Paging.TestRepo.paginate(%{limit: 50, cursors: %{starting_after: starting_record.id}}) 210 | |> Ecto.Paging.TestRepo.all 211 | 212 | {res3, paging} = 213 | get_query() 214 | |> Ecto.Query.order_by(asc: :inserted_at) 215 | |> Ecto.Paging.TestRepo.page(%{limit: 50, cursors: %{starting_after: starting_record.id}}) 216 | 217 | %Ecto.Paging{cursors: cursors} = paging 218 | assert cursors.ending_before == Enum.at(res1, 50).id 219 | assert cursors.starting_after == Enum.at(res1, 99).id 220 | 221 | assert res2 == res3 222 | assert second_record in res2 223 | refute starting_record in res2 224 | 225 | assert length(res2) == 50 226 | 227 | # Second query should be subset of first one 228 | assert 0 == 229 | 0..49 230 | |> Enum.filter(fn index -> 231 | Enum.at(res1, index + 50).id != Enum.at(res2, index).id 232 | end) 233 | |> length 234 | 235 | assert List.last(res2).id == paging.cursors.starting_after 236 | assert paging.has_more 237 | end 238 | 239 | test "paginates back with ending before" do 240 | res1 = 241 | get_query() 242 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 150}) 243 | |> Ecto.Paging.TestRepo.all 244 | 245 | assert length(res1) == 150 246 | 247 | res2 = 248 | get_query() 249 | |> Ecto.Paging.TestRepo.paginate(%{limit: 50, cursors: %{ending_before: List.last(res1).id}}) 250 | |> Ecto.Paging.TestRepo.all 251 | 252 | {res3, paging} = 253 | get_query() 254 | |> Ecto.Paging.TestRepo.page(%{limit: 50, cursors: %{ending_before: List.last(res1).id}}) 255 | 256 | %Ecto.Paging{cursors: cursors} = paging 257 | assert cursors.ending_before == Enum.at(res1, 99).id 258 | assert cursors.starting_after == Enum.at(res1, 148).id 259 | 260 | assert res2 == res3 261 | 262 | assert length(res2) == 50 263 | 264 | {penultimate_record, _list} = List.pop_at(res1, length(res1) - 2) 265 | {start_record, _list} = List.pop_at(res1, length(res1) - 51) 266 | assert Enum.any?(res2, &(&1.id == penultimate_record.id)) 267 | assert Enum.any?(res2, &(&1.id == start_record.id)) 268 | refute List.last(res1) in res2 269 | 270 | # Second query should be subset of first one 271 | assert 0 == 272 | 0..49 273 | |> Enum.filter(fn index -> 274 | Enum.at(res1, index + 99).id != Enum.at(res2, index).id 275 | end) 276 | |> length 277 | 278 | assert List.first(res2).id == paging.cursors.ending_before 279 | assert paging.has_more 280 | end 281 | 282 | test "paginate back with ending before, but with order by DESC" do 283 | # Order by desc query and paginate from first elem 284 | res1 = 285 | get_query() 286 | |> Ecto.Query.order_by(desc: :inserted_at) 287 | |> Ecto.Paging.TestRepo.paginate(%{limit: 5}) 288 | |> Ecto.Paging.TestRepo.all 289 | 290 | # Ordering not influencing pagination 291 | assert length(res1) == 5 292 | 293 | res2 = 294 | get_query() 295 | |> Ecto.Query.order_by(desc: :inserted_at) 296 | |> Ecto.Paging.TestRepo.paginate(%{limit: 5, cursors: %{ending_before: List.last(res1).id}}) 297 | |> Ecto.Paging.TestRepo.all 298 | 299 | # %{ending_before} return inversed result properly 300 | assert length(res2) == 4 301 | refute List.last(res1) in res2 302 | {_, paging} = get_query() 303 | |> Ecto.Query.order_by(desc: :inserted_at) 304 | |> Ecto.Paging.TestRepo.page(%{limit: 5, cursors: %{ending_before: List.last(res1).id}}) 305 | 306 | %Ecto.Paging{cursors: cursors} = paging 307 | assert cursors.ending_before == List.first(res1).id 308 | assert cursors.starting_after == Enum.at(res1, 3).id 309 | end 310 | end 311 | 312 | describe "paginator on integer id with UTC chronological fields" do 313 | setup do 314 | insert_records_with_utc_timestamps() 315 | :ok 316 | end 317 | 318 | test "limits results" do 319 | res1 = 320 | get_query_with_utc_timestamps() 321 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 50}) 322 | |> Ecto.Paging.TestRepo.all 323 | 324 | assert length(res1) == 50 325 | 326 | res2 = 327 | get_query_with_utc_timestamps() 328 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 101}) 329 | |> Ecto.Paging.TestRepo.all 330 | 331 | {res3, _paging} = 332 | get_query_with_utc_timestamps() 333 | |> Ecto.Paging.TestRepo.page(%Ecto.Paging{limit: 101}) 334 | 335 | assert res2 == res3 336 | 337 | assert length(res2) == 101 338 | 339 | assert 0 == 340 | 0..49 341 | |> Enum.filter(fn index -> 342 | Enum.at(res1, index).id != Enum.at(res2, index).id 343 | end) 344 | |> length 345 | end 346 | 347 | test "works with schema" do 348 | {res, _paging} = Ecto.Paging.TestRepo.page(Ecto.Paging.UTCTestSchema, %Ecto.Paging{limit: 101}) 349 | 350 | assert length(res) == 101 351 | end 352 | 353 | test "has default limit" do 354 | {res, _paging} = Ecto.Paging.TestRepo.page(Ecto.Paging.UTCTestSchema, %{}) 355 | 356 | assert length(res) == 50 357 | end 358 | 359 | test "works with dropped limit" do 360 | {res, _paging} = Ecto.Paging.TestRepo.page(Ecto.Paging.UTCTestSchema, %{limit: nil}) 361 | 362 | assert length(res) == 150 363 | end 364 | 365 | test "paginates forward with starting after" do 366 | res1 = 367 | get_query_with_utc_timestamps() 368 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 150}) 369 | |> Ecto.Paging.TestRepo.all 370 | 371 | assert length(res1) == 150 372 | 373 | res2 = 374 | get_query_with_utc_timestamps() 375 | |> Ecto.Paging.TestRepo.paginate(%{limit: 50, cursors: %{starting_after: Enum.at(res1, 49).id}}) 376 | |> Ecto.Paging.TestRepo.all 377 | 378 | {res3, paging} = 379 | get_query_with_utc_timestamps() 380 | |> Ecto.Paging.TestRepo.page(%{limit: 50, cursors: %{starting_after: Enum.at(res1, 49).id}}) 381 | 382 | %Ecto.Paging{cursors: cursors} = paging 383 | assert cursors.ending_before == Enum.at(res1, 50).id 384 | assert cursors.starting_after == Enum.at(res1, 99).id 385 | 386 | assert res2 == res3 387 | 388 | assert length(res2) == 50 389 | 390 | # Second query should be subset of first one 391 | assert 0 == 392 | 0..49 393 | |> Enum.filter(fn index -> 394 | Enum.at(res1, index + 50).id != Enum.at(res2, index).id 395 | end) 396 | |> length 397 | 398 | assert List.last(res2).id == paging.cursors.starting_after 399 | assert paging.has_more 400 | end 401 | 402 | test "paginates back with ending before" do 403 | res1 = 404 | get_query_with_utc_timestamps() 405 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 150}) 406 | |> Ecto.Paging.TestRepo.all 407 | 408 | assert length(res1) == 150 409 | 410 | res2 = 411 | get_query_with_utc_timestamps() 412 | |> Ecto.Paging.TestRepo.paginate(%{limit: 50, cursors: %{ending_before: List.last(res1).id}}) 413 | |> Ecto.Paging.TestRepo.all 414 | 415 | {res3, paging} = 416 | get_query_with_utc_timestamps() 417 | |> Ecto.Paging.TestRepo.page(%{limit: 50, cursors: %{ending_before: List.last(res1).id}}) 418 | 419 | %Ecto.Paging{cursors: cursors} = paging 420 | assert cursors.ending_before == Enum.at(res1, 99).id 421 | assert cursors.starting_after == Enum.at(res1, 148).id 422 | 423 | assert res2 == res3 424 | 425 | assert length(res2) == 50 426 | 427 | # Second query should be subset of first one 428 | assert 0 == 429 | 0..49 430 | |> Enum.filter(fn index -> 431 | Enum.at(res1, index + 99).id != Enum.at(res2, index).id 432 | end) 433 | |> length 434 | 435 | assert List.first(res2).id == paging.cursors.ending_before 436 | assert paging.has_more 437 | end 438 | 439 | test "paginate back with ending before, but with order by DESC" do 440 | # Order by desc query and paginate from first elem 441 | res1 = 442 | get_query_with_utc_timestamps() 443 | |> Ecto.Query.order_by(desc: :inserted_at) 444 | |> Ecto.Paging.TestRepo.paginate(%{limit: 50}) 445 | |> Ecto.Paging.TestRepo.all 446 | 447 | # Ordering not influencing pagination 448 | assert length(res1) == 50 449 | 450 | res2 = 451 | get_query_with_utc_timestamps() 452 | |> Ecto.Query.order_by(desc: :inserted_at) 453 | |> Ecto.Paging.TestRepo.paginate(%{limit: 50, cursors: %{ending_before: List.last(res1).id}}) 454 | |> Ecto.Paging.TestRepo.all 455 | 456 | # %{ending_before} return inversed result properly 457 | assert length(res2) == 49 458 | refute List.last(res1) in res2 459 | 460 | {_, paging} = 461 | get_query_with_utc_timestamps() 462 | |> Ecto.Query.order_by(desc: :inserted_at) 463 | |> Ecto.Paging.TestRepo.page(%{limit: 50, cursors: %{ending_before: List.last(res1).id}}) 464 | 465 | %Ecto.Paging{cursors: cursors} = paging 466 | assert cursors.ending_before == List.first(res1).id 467 | assert cursors.starting_after == Enum.at(res1, 48).id 468 | end 469 | end 470 | 471 | describe "paginator on binary id" do 472 | setup do 473 | insert_records_with_binary_id() 474 | :ok 475 | end 476 | 477 | test "limits results" do 478 | res1 = 479 | get_query_with_binary_id() 480 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 50}) 481 | |> Ecto.Paging.TestRepo.all 482 | 483 | assert length(res1) == 50 484 | 485 | res2 = 486 | get_query_with_binary_id() 487 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 101}) 488 | |> Ecto.Paging.TestRepo.all 489 | 490 | {res3, _paging} = 491 | get_query_with_binary_id() 492 | |> Ecto.Paging.TestRepo.page(%Ecto.Paging{limit: 101}) 493 | 494 | assert res2 == res3 495 | 496 | assert length(res2) == 101 497 | 498 | assert 0 == 499 | 0..49 500 | |> Enum.filter(fn index -> 501 | Enum.at(res1, index).id != Enum.at(res2, index).id 502 | end) 503 | |> length 504 | end 505 | 506 | test "works with schema" do 507 | {res, _paging} = Ecto.Paging.TestRepo.page(Ecto.Paging.BinaryTestSchema, %Ecto.Paging{limit: 101}) 508 | 509 | assert length(res) == 101 510 | end 511 | 512 | test "has default limit" do 513 | {res, _paging} = Ecto.Paging.TestRepo.page(Ecto.Paging.BinaryTestSchema, %{}) 514 | assert length(res) == 50 515 | end 516 | 517 | test "works with dropped limit" do 518 | {res, _paging} = Ecto.Paging.TestRepo.page(Ecto.Paging.BinaryTestSchema, %{limit: nil}) 519 | 520 | assert length(res) == 150 521 | end 522 | 523 | test "paginates forward with starting after" do 524 | res1 = 525 | get_query_with_binary_id() 526 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 150}) 527 | |> Ecto.Paging.TestRepo.all 528 | 529 | assert length(res1) == 150 530 | 531 | res2 = 532 | get_query_with_binary_id() 533 | |> Ecto.Paging.TestRepo.paginate(%{limit: 50, cursors: %{starting_after: Enum.at(res1, 49).id}}) 534 | |> Ecto.Paging.TestRepo.all 535 | 536 | {res3, paging} = 537 | get_query_with_binary_id() 538 | |> Ecto.Paging.TestRepo.page(%{limit: 50, cursors: %{starting_after: Enum.at(res1, 49).id}}) 539 | 540 | %Ecto.Paging{cursors: cursors} = paging 541 | assert cursors.ending_before == Enum.at(res1, 50).id 542 | assert cursors.starting_after == Enum.at(res1, 99).id 543 | assert res2 == res3 544 | 545 | assert length(res2) == 50 546 | 547 | # Second query should be subset of first one 548 | assert 0 == 549 | 0..49 550 | |> Enum.filter(fn index -> 551 | Enum.at(res1, index + 50).id != Enum.at(res2, index).id 552 | end) 553 | |> length 554 | 555 | assert List.last(res2).id == paging.cursors.starting_after 556 | assert paging.has_more 557 | end 558 | 559 | test "paginates back with ending before" do 560 | res1 = 561 | get_query_with_binary_id() 562 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 150}) 563 | |> Ecto.Paging.TestRepo.all 564 | 565 | assert length(res1) == 150 566 | 567 | res2 = 568 | get_query_with_binary_id() 569 | |> Ecto.Paging.TestRepo.paginate(%{limit: 50, cursors: %{ending_before: List.last(res1).id}}) 570 | |> Ecto.Paging.TestRepo.all 571 | 572 | {penultimate_record, _list} = List.pop_at(res1, length(res1) - 2) 573 | {start_record, _list} = List.pop_at(res1, length(res1) - 51) 574 | 575 | assert Enum.any?(res2, &(&1.id == penultimate_record.id)) 576 | assert Enum.any?(res2, &(&1.id == start_record.id)) 577 | refute List.last(res1) in res2 578 | 579 | {res3, paging} = 580 | get_query_with_binary_id() 581 | |> Ecto.Paging.TestRepo.page(%{limit: 50, cursors: %{ending_before: List.last(res1).id}}) 582 | 583 | %Ecto.Paging{cursors: cursors} = paging 584 | assert cursors.ending_before == Enum.at(res1, 99).id 585 | assert cursors.starting_after == Enum.at(res1, 148).id 586 | 587 | assert res2 == res3 588 | 589 | assert length(res2) == 50 590 | 591 | assert 0 == 592 | 0..49 593 | |> Enum.filter(fn index -> 594 | Enum.at(res1, index + 99).id != Enum.at(res2, index).id 595 | end) 596 | |> length 597 | 598 | assert List.first(res2).id == paging.cursors.ending_before 599 | assert paging.has_more 600 | end 601 | 602 | test "paginate back with ending before, but with order by DESC" do 603 | # Order by desc query and paginate from first elem 604 | res1 = 605 | get_query_with_binary_id() 606 | |> Ecto.Query.order_by(desc: :inserted_at) 607 | |> Ecto.Paging.TestRepo.paginate(%{limit: 150}) 608 | |> Ecto.Paging.TestRepo.all 609 | 610 | # Ordering not influencing pagination 611 | assert length(res1) == 150 612 | 613 | res2 = 614 | get_query_with_binary_id() 615 | |> Ecto.Query.order_by(desc: :inserted_at) 616 | |> Ecto.Paging.TestRepo.paginate(%{limit: 50, cursors: %{ending_before: List.last(res1).id}}) 617 | |> Ecto.Paging.TestRepo.all 618 | 619 | penultimate_record = Enum.at(res1, 148) 620 | start_record = List.first(res1) 621 | 622 | assert Enum.any?(res2, &(&1.id == penultimate_record.id)) 623 | refute start_record in res2 624 | refute List.last(res1) in res2 625 | 626 | # %{ending_before} return inversed result properly 627 | assert length(res2) == 50 628 | 629 | cursors = %Ecto.Paging.Cursors{starting_after: nil, ending_before: List.last(res1).id} 630 | page = %Ecto.Paging{limit: 50, cursors: cursors} 631 | 632 | query = get_query_with_binary_id() 633 | {res3, _page} = 634 | query 635 | |> Ecto.Query.order_by(desc: :inserted_at) 636 | |> Ecto.Paging.TestRepo.page(page) 637 | assert length(res3) == 50 638 | 639 | assert Enum.any?(res3, &(&1.id == penultimate_record.id)) 640 | refute List.last(res1) in res3 641 | 642 | end 643 | end 644 | 645 | describe "paginator on string id" do 646 | setup do 647 | insert_records_with_string_id() 648 | :ok 649 | end 650 | 651 | test "limits results" do 652 | res1 = 653 | get_query_with_string_id() 654 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 50}) 655 | |> Ecto.Paging.TestRepo.all 656 | 657 | assert length(res1) == 50 658 | 659 | res2 = 660 | get_query_with_string_id() 661 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 101}) 662 | |> Ecto.Paging.TestRepo.all 663 | 664 | {res3, _paging} = 665 | get_query_with_string_id() 666 | |> Ecto.Paging.TestRepo.page(%Ecto.Paging{limit: 101}) 667 | 668 | assert res2 == res3 669 | 670 | assert length(res2) == 101 671 | 672 | assert 0 == 673 | 0..49 674 | |> Enum.filter(fn index -> 675 | Enum.at(res1, index).id != Enum.at(res2, index).id 676 | end) 677 | |> length 678 | end 679 | 680 | test "works with schema" do 681 | {res, _paging} = Ecto.Paging.TestRepo.page(Ecto.Paging.StringTestSchema, %Ecto.Paging{limit: 101}) 682 | 683 | assert length(res) == 101 684 | end 685 | 686 | test "has default limit" do 687 | {res, _paging} = Ecto.Paging.TestRepo.page(Ecto.Paging.StringTestSchema, %{}) 688 | 689 | assert length(res) == 50 690 | end 691 | 692 | test "works with dropped limit" do 693 | {res, _paging} = Ecto.Paging.TestRepo.page(Ecto.Paging.StringTestSchema, %{limit: nil}) 694 | 695 | assert length(res) == 150 696 | end 697 | 698 | test "paginates forward with starting after" do 699 | res1 = 700 | get_query_with_string_id() 701 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 150}) 702 | |> Ecto.Paging.TestRepo.all 703 | 704 | assert length(res1) == 150 705 | 706 | res2 = 707 | get_query_with_string_id() 708 | |> Ecto.Paging.TestRepo.paginate(%{limit: 50, cursors: %{starting_after: Enum.at(res1, 49).id}}) 709 | |> Ecto.Paging.TestRepo.all 710 | 711 | {res3, paging} = 712 | get_query_with_string_id() 713 | |> Ecto.Paging.TestRepo.page(%{limit: 50, cursors: %{starting_after: Enum.at(res1, 49).id}}) 714 | 715 | %Ecto.Paging{cursors: cursors} = paging 716 | assert cursors.ending_before == Enum.at(res1, 50).id 717 | assert cursors.starting_after == Enum.at(res1, 99).id 718 | 719 | assert res2 == res3 720 | 721 | assert length(res2) == 50 722 | 723 | # Second query should be subset of first one 724 | assert 0 == 725 | 0..49 726 | |> Enum.filter(fn index -> 727 | Enum.at(res1, index + 50).id != Enum.at(res2, index).id 728 | end) 729 | |> length 730 | 731 | assert List.last(res2).id == paging.cursors.starting_after 732 | assert paging.has_more 733 | end 734 | 735 | test "paginates forward with starting after with ordering" do 736 | res1 = 737 | get_query_with_string_id() 738 | |> Ecto.Query.order_by(desc: :inserted_at) 739 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 150}) 740 | |> Ecto.Paging.TestRepo.all 741 | 742 | assert length(res1) == 150 743 | 744 | res2 = 745 | get_query_with_string_id() 746 | |> Ecto.Query.order_by(desc: :inserted_at) 747 | |> Ecto.Paging.TestRepo.paginate(%{limit: 50, cursors: %{starting_after: Enum.at(res1, 49).id}}) 748 | |> Ecto.Paging.TestRepo.all 749 | starting_record = Enum.at(res1, 50) 750 | last_record = Enum.at(res1, 99) 751 | 752 | assert starting_record in res2 753 | assert last_record in res2 754 | assert length(res2) == 50 755 | end 756 | 757 | test "paginates back with ending before" do 758 | res1 = 759 | get_query_with_string_id() 760 | |> Ecto.Paging.TestRepo.paginate(%Ecto.Paging{limit: 150}) 761 | |> Ecto.Paging.TestRepo.all 762 | 763 | assert length(res1) == 150 764 | 765 | res2 = 766 | get_query_with_string_id() 767 | |> Ecto.Paging.TestRepo.paginate(%{limit: 50, cursors: %{ending_before: List.last(res1).id}}) 768 | |> Ecto.Paging.TestRepo.all 769 | 770 | {res3, paging} = 771 | get_query_with_string_id() 772 | |> Ecto.Paging.TestRepo.page(%{limit: 50, cursors: %{ending_before: List.last(res1).id}}) 773 | 774 | %Ecto.Paging{cursors: cursors} = paging 775 | assert cursors.ending_before == Enum.at(res1, 99).id 776 | assert cursors.starting_after == Enum.at(res1, 148).id 777 | 778 | assert res2 == res3 779 | 780 | assert length(res2) == 50 781 | 782 | penultimate_record = Enum.at(res1, 148) 783 | start_record = Enum.at(res1, 99) 784 | 785 | assert Enum.any?(res2, &(&1.id == penultimate_record.id)) 786 | assert Enum.any?(res2, &(&1.id == start_record.id)) 787 | 788 | assert List.first(res2).id == paging.cursors.ending_before 789 | assert paging.has_more 790 | end 791 | 792 | test "paginates back with ending before with ORDERING" do 793 | res1 = 794 | get_query_with_string_id() 795 | |> Ecto.Query.order_by(desc: :inserted_at) 796 | |> Ecto.Paging.TestRepo.all 797 | 798 | assert length(res1) == 150 799 | penultimate_record_id = Enum.at(res1, 148).id 800 | 801 | res2 = 802 | get_query_with_string_id() 803 | |> Ecto.Query.order_by(desc: :inserted_at) 804 | |> Ecto.Paging.TestRepo.paginate(%{limit: 50, cursors: %{ending_before: List.last(res1).id}}) 805 | |> Ecto.Paging.TestRepo.all 806 | 807 | assert Enum.any?(res2, &(&1.id == penultimate_record_id)) 808 | end 809 | end 810 | 811 | describe "Default ordering" do 812 | setup do 813 | records_attrs = [ 814 | %{id: "2da21858-e1ae-4d8f-a87d-c3f94b4f433e"}, 815 | %{id: "b36cc9c3-4214-41e3-b12e-03fc2e7f6fa1"}, 816 | %{id: "77a3e1ec-bd4b-443e-bc2c-ca365cc7dc25"}, 817 | %{id: "b7d63e4b-1364-4c6e-8e07-bee8eea8c21f"}, 818 | %{id: "e595eadd-a000-43e5-910b-31fc293d910b"}, 819 | %{id: "86106137-f4cd-483a-9a95-0b90601bf8ba"}, 820 | %{id: "1d27cbab-6192-47ba-9d32-f928b99ed666"}, 821 | %{id: "73a30d5c-42e6-4ba3-a969-cd01d82cdef1"}, 822 | %{id: "a97338cb-0665-42bc-91cc-1de726626553"}, 823 | %{id: "ff591a0e-8773-4b8e-9708-44a75be6f8c8"} 824 | ] 825 | 826 | records = Enum.map(records_attrs, fn item -> 827 | Ecto.Paging.TestRepo.insert!(%Ecto.Paging.StringTestSchema{id: item.id}) 828 | end) 829 | 830 | {:ok, %{records: records}} 831 | end 832 | 833 | test "starting_after:2 + limit:5", %{records: records} do 834 | id = Enum.at(records, 2).id 835 | 836 | expected_records = 837 | records 838 | |> Enum.slice(3, 5) 839 | |> Enum.map(&Map.get(&1, :id)) 840 | 841 | actual_records = 842 | Ecto.Paging.StringTestSchema 843 | |> Ecto.Paging.TestRepo.paginate(%{limit: 5, cursors: %{starting_after: id}}) 844 | |> Ecto.Paging.TestRepo.all() 845 | |> Enum.map(&Map.get(&1, :id)) 846 | 847 | actual_records_with_asc = 848 | Ecto.Paging.StringTestSchema 849 | |> Ecto.Query.order_by(asc: :inserted_at) 850 | |> Ecto.Paging.TestRepo.paginate(%{limit: 5, cursors: %{starting_after: id}}) 851 | |> Ecto.Paging.TestRepo.all() 852 | |> Enum.map(&Map.get(&1, :id)) 853 | 854 | assert expected_records == actual_records 855 | assert expected_records == actual_records_with_asc 856 | end 857 | 858 | test "ending_before:3", %{records: records} do 859 | id = Enum.at(records, 2).id 860 | 861 | expected_records = 862 | records 863 | |> Enum.slice(3, 5) 864 | |> Enum.reverse 865 | |> Enum.map(&Map.get(&1, :id)) 866 | 867 | assert length(expected_records) == 5 868 | 869 | actual_records = 870 | Ecto.Paging.StringTestSchema 871 | |> Ecto.Query.order_by(desc: :inserted_at) 872 | |> Ecto.Paging.TestRepo.paginate(%{limit: 5, cursors: %{ending_before: id}}) 873 | |> Ecto.Paging.TestRepo.all() 874 | |> Enum.map(&Map.get(&1, :id)) 875 | 876 | assert expected_records == actual_records 877 | end 878 | end 879 | 880 | defp get_query do 881 | from p in Ecto.Paging.TestSchema 882 | end 883 | 884 | defp get_query_with_utc_timestamps do 885 | from p in Ecto.Paging.UTCTestSchema 886 | end 887 | 888 | defp insert_records do 889 | for _ <- 1..150, do: Ecto.Paging.TestRepo.insert(%Ecto.Paging.TestSchema{name: "abc"}) 890 | end 891 | 892 | defp insert_records_with_utc_timestamps do 893 | for _ <- 1..150, do: Ecto.Paging.TestRepo.insert(%Ecto.Paging.UTCTestSchema{name: "abc"}) 894 | end 895 | 896 | defp get_query_with_binary_id do 897 | from p in Ecto.Paging.BinaryTestSchema 898 | end 899 | 900 | defp insert_records_with_binary_id do 901 | for _ <- 1..150, do: Ecto.Paging.TestRepo.insert(%Ecto.Paging.BinaryTestSchema{name: "abc"}) 902 | end 903 | 904 | defp get_query_with_string_id do 905 | from p in Ecto.Paging.StringTestSchema 906 | end 907 | 908 | defp insert_records_with_string_id do 909 | for _ <- 1..150, do: Ecto.Paging.TestRepo.insert( 910 | %Ecto.Paging.StringTestSchema{id: Ecto.UUID.generate(), name: "abc"} 911 | ) 912 | end 913 | end 914 | --------------------------------------------------------------------------------