Skip to content

Commit

Permalink
Remove the dynamic assumption of table field (#8)
Browse files Browse the repository at this point in the history
* Remove the dynamic assumption of table field

- Adds field_or_alias concept to ordering and page break that holds the
  previously held assumption of table column in as expr in the structs
- Utilizes the field_or_alias for the Fob apply_keyset_comparison
  dynamics

* Keep all changes internal for Haste

* Changelog for the feature.

- Assuming we want a minor bump for new functionality? But perhaps we
  call this a patch as all the changes are internal

* Coverage for fragment builder
  • Loading branch information
pmonson711 authored Mar 10, 2022
1 parent 3bb172a commit a6ef115
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 68 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ 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.6.0 - 2022-03-10

### Changed

- Added the ability to paginate with order by fragments, when the same query
fragment is a select. See tests for example.

## 0.5.1 - 2022-02-04

### Fixed
Expand Down
100 changes: 42 additions & 58 deletions lib/fob.ex
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ defmodule Fob do
initial_acc = apply_basic_comparison(id_break, comparison_strictness)

where_clause =
Enum.reduce(remaining_breaks, initial_acc, &apply_keyset_comparison/2)
remaining_breaks
|> PageBreak.wrap_field_or_alias(query)
|> Enum.reduce(initial_acc, &apply_keyset_comparison/2)

where(query, ^where_clause)
end
Expand All @@ -67,98 +69,77 @@ defmodule Fob do
# --- value is nil

defp apply_keyset_comparison(
%PageBreak{
direction: direction,
column: column,
table: table,
value: nil
},
{%PageBreak{
direction: direction,
value: nil
}, field_or_alias},
acc
)
when direction in [:asc, :asc_nulls_last, :desc_nulls_last] do
dynamic([{t, table}], field(t, ^column) |> is_nil() and ^acc)
dynamic(^field_or_alias |> is_nil() and ^acc)
end

defp apply_keyset_comparison(
%PageBreak{
direction: direction,
column: column,
table: table,
value: nil
},
{%PageBreak{
direction: direction,
value: nil
}, field_or_alias},
acc
)
when direction in [:desc, :desc_nulls_first, :asc_nulls_first] do
dynamic(
[{t, table}],
not is_nil(field(t, ^column)) or
(field(t, ^column) |> is_nil() and ^acc)
not is_nil(^field_or_alias) or
(^field_or_alias |> is_nil() and ^acc)
)
end

# --- value is non-nil

defp apply_keyset_comparison(
%PageBreak{
direction: direction,
column: column,
table: table,
value: value
},
{%PageBreak{
direction: direction,
value: value
}, field_or_alias},
acc
)
when direction in [:asc, :asc_nulls_last] do
dynamic(
[{t, table}],
field(t, ^column) > ^value or field(t, ^column) |> is_nil() or
(field(t, ^column) == ^value and ^acc)
^field_or_alias > ^value or ^field_or_alias |> is_nil() or
(^field_or_alias == ^value and ^acc)
)
end

defp apply_keyset_comparison(
%PageBreak{
direction: :asc_nulls_first,
column: column,
table: table,
value: value
},
{%PageBreak{
direction: :asc_nulls_first,
value: value
}, field_or_alias},
acc
) do
dynamic(
[{t, table}],
field(t, ^column) > ^value or (field(t, ^column) == ^value and ^acc)
)
dynamic(^field_or_alias > ^value or (^field_or_alias == ^value and ^acc))
end

defp apply_keyset_comparison(
%PageBreak{
direction: direction,
column: column,
table: table,
value: value
},
{%PageBreak{
direction: direction,
value: value
}, field_or_alias},
acc
)
when direction in [:desc, :desc_nulls_first] do
dynamic(
[{t, table}],
field(t, ^column) < ^value or (field(t, ^column) == ^value and ^acc)
)
dynamic(^field_or_alias < ^value or (^field_or_alias == ^value and ^acc))
end

defp apply_keyset_comparison(
%PageBreak{
direction: :desc_nulls_last,
column: column,
table: table,
value: value
},
{%PageBreak{
direction: :desc_nulls_last,
value: value
}, field_or_alias},
acc
) do
dynamic(
[{t, table}],
field(t, ^column) < ^value or field(t, ^column) |> is_nil() or
(field(t, ^column) == ^value and ^acc)
^field_or_alias < ^value or ^field_or_alias |> is_nil() or
(^field_or_alias == ^value and ^acc)
)
end

Expand Down Expand Up @@ -230,10 +211,13 @@ defmodule Fob do

query
|> Ordering.columns()
|> Enum.map(fn {_table, name} = column ->
key = Map.get(selection_mapping, column, name)
|> Enum.map(fn {table, name} ->
key = Map.get(selection_mapping, {table, name}, name)

%PageBreak{column: name, value: get_in(record, [Access.key(key)])}
%PageBreak{
column: name,
value: get_in(record, [Access.key(key)])
}
end)
end

Expand Down
50 changes: 50 additions & 0 deletions lib/fob/fragment_builder.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
defmodule Fob.FragmentBuilder do
@moduledoc false
@spec build([Macro.t()], Macro.t(), Macro.t(), Macro.Env.t()) :: Macro.t()
def build(binding, expr, params, env) do
quote do
%Ecto.Query.DynamicExpr{
fun: fn query ->
{unquote(expr), unquote(params), []}
end,
binding: unquote(Macro.escape(binding)),
file: unquote(env.file),
line: unquote(env.line)
}
end
end

defmacro build_from_existing(binding \\ [], expr) do
build(binding, expr, [], __CALLER__)
end

def table_for_fragment({:fragment, [], _} = frag) do
Macro.prewalk(frag, nil, fn ast, acc ->
case ast do
{:&, [], [table_idx]} when is_integer(table_idx) ->
{ast, table_idx}

_ ->
{ast, acc}
end
end)
|> elem(1)
end

def column_for_query_fragment(
{:fragment, [], _} = frag,
%Ecto.Query{} = query
) do
query.select.expr
|> Macro.prewalk(nil, fn ast, acc ->
case ast do
{virtual_column, ^frag} when is_atom(virtual_column) ->
{ast, virtual_column}

_ ->
{ast, acc}
end
end)
|> elem(1)
end
end
48 changes: 38 additions & 10 deletions lib/fob/ordering.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,67 @@ defmodule Fob.Ordering do
# in a query

alias Ecto.Query
import Ecto.Query
alias Fob.FragmentBuilder
require Fob.FragmentBuilder

@typep table :: non_neg_integer()
@typep table :: nil | non_neg_integer()

@type t :: %__MODULE__{
table: table(),
column: atom(),
direction: :asc | :desc
direction: :asc | :desc,
field_or_alias: any()
}

defstruct ~w[table column direction]a
defstruct ~w[table column direction field_or_alias]a

@spec config(%Query{}) :: [t()]
def config(%Query{order_bys: orderings}) do
def config(%Query{order_bys: orderings} = query) do
Enum.flat_map(orderings, fn %Query.QueryExpr{expr: exprs} ->
config_from_ordering_expressions(exprs)
config_from_ordering_expressions(exprs, query)
end)
end

defp config_from_ordering_expressions(exprs) when is_list(exprs) do
Enum.map(exprs, &config_from_ordering_expressions/1)
defp config_from_ordering_expressions(exprs, query) when is_list(exprs) do
Enum.map(exprs, &config_from_ordering_expressions(&1, query))
end

defp config_from_ordering_expressions(
{direction, {{:., _, [{:&, _, [table]}, column]}, _, _}}
{direction, {{:., _, [{:&, _, [table]}, column]}, _, _}},
_
) do
%__MODULE__{
direction: direction,
column: column,
table: table
table: table,
field_or_alias: dynamic([{t, table}], field(t, ^column))
}
end

@spec columns(%Query{}) :: [{table(), atom()}]
defp config_from_ordering_expressions(
{direction, {:fragment, [], _} = frag},
query
) do
table = FragmentBuilder.table_for_fragment(frag)

column = FragmentBuilder.column_for_query_fragment(frag, query)

field_or_alias =
Fob.FragmentBuilder.build_from_existing(
[{t, table}],
frag
)

%__MODULE__{
direction: direction,
column: column,
table: table,
field_or_alias: field_or_alias
}
end

@spec columns(%Query{}) :: [{table(), atom(), any()}]
def columns(%Query{} = query) do
query
|> config()
Expand Down
12 changes: 12 additions & 0 deletions lib/fob/page_break.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ defmodule Fob.PageBreak do
%__MODULE__{page_break | table: order.table, direction: order.direction}
end

def wrap_field_or_alias(page_breaks, %Ecto.Query{} = query)
when is_list(page_breaks) do
ordering_config = Ordering.config(query)

Enum.map(page_breaks, &wrap_field_or_alias(&1, ordering_config))
end

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

@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
Loading

0 comments on commit a6ef115

Please sign in to comment.