Skip to content

Commit

Permalink
cast page-break values from iso8601 (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
the-mikedavis authored Mar 16, 2022
1 parent ebfb7f6 commit 6bbe637
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 4 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format is based on [Keep a
Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.7.0 - 2022-03-16

### Addded

- When ording by a fragment, Fob may now cast dates from binaries into
`Date` structs
- This fixes errors arising from fetching the next page of data when
ordering by a fragment
- See the new `test/fob/fragment_casting_test.exs` test cases

## 0.6.1 - 2022-03-14

### Fixed
Expand Down
53 changes: 49 additions & 4 deletions lib/fob/page_break.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,23 @@ defmodule Fob.PageBreak do
when is_list(page_breaks) do
ordering_config = Ordering.config(query)

Enum.map(page_breaks, &add_query_info(&1, ordering_config))
Enum.map(page_breaks, &add_query_info(&1, ordering_config, query))
end

def add_query_info(%{column: column} = page_break, ordering_config) do
def add_query_info(
%{column: column, value: value} = page_break,
ordering_config,
query
) do
order = Enum.find(ordering_config, fn order -> column == order.column end)

%__MODULE__{page_break | table: order.table, direction: order.direction}
casted_value = cast_type(query, column, value)

%__MODULE__{
page_break
| table: order.table,
direction: order.direction,
value: casted_value
}
end

def wrap_to_routeable(page_breaks, %Ecto.Query{} = query)
Expand All @@ -43,6 +53,41 @@ defmodule Fob.PageBreak do
{page_break, order.maybe_expression}
end

defp cast_type(query, column, value) do
with {_table_name, schema} <- query.from.source,
true <- function_exported?(schema, :__changeset__, 0),
%{^column => type} <- schema.__changeset__() do
do_cast_type(type, value)
else
_ -> value
end
end

@iso_8601_modules %{
:date => Date,
:time => Time,
:naive_datetime => NaiveDateTime,
:naive_datetime_usec => NaiveDateTime,
:utc_datetime => DateTime,
:utc_datetime_usec => DateTime
}

for {type, module} <- @iso_8601_modules do
defp do_cast_type(unquote(type), string) when is_binary(string) do
case unquote(module).from_iso8601(string) do
{:ok, casted_value} ->
casted_value

# chaps-ignore-start
_ ->
string
# chaps-ignore-stop
end
end
end

defp do_cast_type(_type, value), do: value

@doc since: "0.2.0"
def compare(a, b, query) when is_list(a) and is_list(b) do
compare(add_query_info(a, query), add_query_info(b, query))
Expand Down
11 changes: 11 additions & 0 deletions priv/repo/migrations/20220316162350_create_choose_field_schema.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule Fob.Repo.Migrations.CreateChooseFieldSchema do
use Ecto.Migration

def change do
create table(:choose_field_schema) do
add :date_a, :date
add :date_b, :date
add :mode, :string
end
end
end
67 changes: 67 additions & 0 deletions test/fob/fragment_casting_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
defmodule Fob.FragmentCastingTest do
use Fob.RepoCase

@moduledoc """
Tests that the Fob.between_bounds/3 function works as expected and respects
nils
"""
@moduledoc since: "0.2.0"

alias Ecto.Multi
alias Fob.Cursor

setup do
[schema: ChooseFieldSchema, repo: Fob.Repo]
end

setup c do
Multi.new()
|> Multi.insert_all(:seeds, c.schema, c.schema.seed())
|> c.repo.transaction()

import ChooseFieldSchema, only: [date: 1]

query =
from t in c.schema,
order_by: [desc: date(t), asc: :id],
select_merge: %{date: date(t)}

[query: query]
end

test "the dataset is fetched in the expected order", c do
cursor = Cursor.new(c.query, c.repo, nil, 5)

assert run_cursor(cursor) |> ids() == c.repo.all(c.query) |> ids()
end

test "next pages may be fetched even when the page break value is an ISO8601 date",
c do
expected_ids = c.query |> c.repo.all() |> ids()

records = Fob.next_page(c.query, nil, 6) |> c.repo.all()
assert records |> ids() == Enum.slice(expected_ids, 0..5)
page_breaks = Fob.page_breaks(c.query, records |> List.last())

records = Fob.next_page(c.query, page_breaks, 3) |> c.repo.all()
assert records |> ids() == Enum.slice(expected_ids, 6..8)

page_breaks =
Fob.page_breaks(c.query, records |> List.last())
|> update_in([Access.at(0), Access.key(:value)], fn %Date{} = date ->
Date.to_iso8601(date)
end)

records = Fob.next_page(c.query, page_breaks, 3) |> c.repo.all()
assert records |> ids() == Enum.slice(expected_ids, 9..11)
end

defp run_cursor(cursor, acc \\ []) do
case Cursor.next(cursor) do
{[], _cursor} -> acc
{records, cursor} -> run_cursor(cursor, acc ++ records)
end
end

defp ids(records), do: Enum.map(records, & &1.id)
end
55 changes: 55 additions & 0 deletions test/support/choose_field_schema.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
defmodule ChooseFieldSchema do
@moduledoc """
This schema is meant to test the casting of page break values
when ordering by a fragment
"""

use Ecto.Schema

defmacro date(schema) do
quote do
fragment(
"case ? when 'a' then ? when 'b' then ? else null end",
unquote(schema).mode,
unquote(schema).date_a,
unquote(schema).date_b
)
end
end

schema "choose_field_schema" do
field :date, :date, virtual: true
field :date_a, :date
field :date_b, :date
field :mode, :string
end

def seed do
[
{0, "a", ~D[2020-12-15]},
{1, "a", ~D[2020-12-16]},
{2, "a", ~D[2020-12-17]},
{3, "a", ~D[2020-12-18]},
{4, "b", ~D[2020-12-15]},
{5, "b", ~D[2020-12-16]},
{6, "b", ~D[2020-12-17]},
{7, "b", ~D[2020-12-18]},
{8, "b", ~D[2020-12-15]},
{9, "b", ~D[2020-12-16]},
{10, "b", ~D[2020-12-17]},
{11, "b", ~D[2020-12-19]},
{12, "b", ~D[2020-12-20]},
{13, "c", ~D[2020-12-21]},
{14, "c", ~D[2020-12-22]},
{15, "c", nil},
{16, "c", nil},
{17, "c", nil},
{18, "c", nil},
{19, "c", nil}
]
|> Enum.map(fn {index, mode, date} ->
# it's not very important to this test case that
%{id: index, mode: mode, date_a: date, date_b: date}
end)
end
end

0 comments on commit 6bbe637

Please sign in to comment.