Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/develop' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
heywhy committed Jun 20, 2022
2 parents eb26a57 + 371d3dc commit 8e0b164
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 10 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Using Etso is a two-step process. First, include it in your application’s depe
```elixir
defp deps do
[
{:etso, "~> 0.1.6"}
{:etso, "~> 1.0.1"}
]
end
```
Expand Down Expand Up @@ -105,6 +105,10 @@ The Author also wishes to thank the following individuals:

- [William Martins][wmartins], for [fixing primary key unicity check issues][gh-7].

- [Soichiro Nishizawa][soichiro-nishizawa], for [providing the implementation of sorted results][gh-12].

- [Doug W.][harmon25], for [providing insights into parallel preloads][gh-13].

[erlang-ets]: http://erlang.org/doc/man/ets.html
[northwind]: https://github.com/evadne/etso/tree/master/test/support/northwind
[northwind-importer]: https://github.com/evadne/etso/tree/master/test/support/northwind/importer.ex
Expand All @@ -118,3 +122,7 @@ The Author also wishes to thank the following individuals:
[pr-10]: https://github.com/evadne/etso/pull/10
[wmartins]: https://github.com/wmartins
[gh-7]: https://github.com/evadne/etso/issues/7
[soichiro-nishizawa]: https://github.com/soichiro-nishizawa
[gh-12]: https://github.com/evadne/etso/issues/12
[harmon25]: https://github.com/harmon25
[gh-13]: https://github.com/evadne/etso/issues/13
17 changes: 14 additions & 3 deletions lib/etso/adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ defmodule Etso.Adapter do
@behaviour Ecto.Adapter.Schema
@behaviour Ecto.Adapter.Queryable

@impl Ecto.Adapter
defmacro __before_compile__(_opts), do: :ok

@doc false
@impl Ecto.Adapter
def ensure_all_started(_config, _type), do: {:ok, []}

@doc false
@impl Ecto.Adapter
def init(config) do
{:ok, repo} = Keyword.fetch(config, :repo)
child_spec = __MODULE__.Supervisor.child_spec(repo)
Expand All @@ -33,17 +36,21 @@ defmodule Etso.Adapter do
end

@doc false
@impl Ecto.Adapter
def checkout(_, _, fun), do: fun.()

@doc false
@impl Ecto.Adapter
def checked_out?(_), do: false

@doc false
@impl Ecto.Adapter
def loaders(:binary_id, type), do: [Ecto.UUID, type]
def loaders(:embed_id, type), do: [Ecto.UUID, type]
def loaders(_, type), do: [type]

@doc false
@impl Ecto.Adapter
def dumpers(:binary_id, type), do: [type, Ecto.UUID]
def dumpers(:embed_id, type), do: [type, Ecto.UUID]
def dumpers(:map, type), do: [type, Etso.Ecto.MapType]
Expand All @@ -59,13 +66,17 @@ defmodule Etso.Adapter do
|> Enum.each(& :ets.delete_all_objects(&1) == true)
end

for module <- [__MODULE__.Behaviour.Schema, __MODULE__.Behaviour.Queryable] do
for {name, arity} <- module.__info__(:functions) do
for {implementation_module, behaviour_module} <- [
{__MODULE__.Behaviour.Schema, Ecto.Adapter.Schema},
{__MODULE__.Behaviour.Queryable, Ecto.Adapter.Queryable}
] do
for {name, arity} <- implementation_module.__info__(:functions) do
args = Enum.map(1..arity, &{:"arg_#{&1}", [], Elixir})

@doc false
@impl behaviour_module
def unquote(name)(unquote_splicing(args)) do
unquote(module).unquote(name)(unquote_splicing(args))
unquote(implementation_module).unquote(name)(unquote_splicing(args))
end
end
end
Expand Down
7 changes: 6 additions & 1 deletion lib/etso/adapter/behaviour/queryable.ex
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
defmodule Etso.Adapter.Behaviour.Queryable do
@moduledoc false
@behaviour Ecto.Adapter.Queryable

alias Etso.Adapter.TableRegistry
alias Etso.ETS.MatchSpecification
alias Etso.ETS.ObjectsSorter

@impl Ecto.Adapter.Queryable
def prepare(:all, query) do
{:nocache, query}
end

@impl Ecto.Adapter.Queryable
def execute(%{repo: repo}, _, {:nocache, query}, params, _) do
{_, schema} = query.from.source
{:ok, ets_table} = TableRegistry.get_table(repo, schema)
ets_match = MatchSpecification.build(query, params)
ets_objects = :ets.select(ets_table, [ets_match])
ets_objects = :ets.select(ets_table, [ets_match]) |> ObjectsSorter.sort(query)
{length(ets_objects), ets_objects}
end

@impl Ecto.Adapter.Queryable
def stream(%{repo: repo}, _, {:nocache, query}, params, options) do
{_, schema} = query.from.source
{:ok, ets_table} = TableRegistry.get_table(repo, schema)
Expand Down
6 changes: 6 additions & 0 deletions lib/etso/adapter/behaviour/schema.ex
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
defmodule Etso.Adapter.Behaviour.Schema do
@moduledoc false
@behaviour Ecto.Adapter.Schema

alias Etso.Adapter.TableRegistry
alias Etso.ETS.TableStructure

@impl Ecto.Adapter.Schema
def autogenerate(:id), do: :erlang.unique_integer()
def autogenerate(:binary_id), do: Ecto.UUID.bingenerate()
def autogenerate(:embed_id), do: Ecto.UUID.bingenerate()

@impl Ecto.Adapter.Schema
def insert_all(%{repo: repo}, %{schema: schema}, _, entries, _, _, _, _) do
{:ok, ets_table} = TableRegistry.get_table(repo, schema)
ets_field_names = TableStructure.field_names(schema)
Expand All @@ -16,6 +19,7 @@ defmodule Etso.Adapter.Behaviour.Schema do
if ets_result, do: {length(ets_changes), nil}, else: {0, nil}
end

@impl Ecto.Adapter.Schema
def insert(%{repo: repo}, %{schema: schema}, fields, _, _, _) do
{:ok, ets_table} = TableRegistry.get_table(repo, schema)
ets_field_names = TableStructure.field_names(schema)
Expand All @@ -24,6 +28,7 @@ defmodule Etso.Adapter.Behaviour.Schema do
if ets_result, do: {:ok, []}, else: {:invalid, [unique: "primary_key"]}
end

@impl Ecto.Adapter.Schema
def update(%{repo: repo}, %{schema: schema}, fields, filters, [], _) do
{:ok, ets_table} = TableRegistry.get_table(repo, schema)
[key_name] = schema.__schema__(:primary_key)
Expand All @@ -33,6 +38,7 @@ defmodule Etso.Adapter.Behaviour.Schema do
if ets_result, do: {:ok, []}, else: {:error, :stale}
end

@impl Ecto.Adapter.Schema
def delete(%{repo: repo}, %{schema: schema}, filters, _) do
{:ok, ets_table} = TableRegistry.get_table(repo, schema)
[key_name] = schema.__schema__(:primary_key)
Expand Down
16 changes: 14 additions & 2 deletions lib/etso/adapter/meta.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
defmodule Etso.Adapter.Meta do
@moduledoc false

@type t :: %__MODULE__{repo: Ecto.Repo.t()}
@type t :: %__MODULE__{
repo: Ecto.Repo.t(),
cache: :ets.tab() | nil,
pid: pid() | nil,
stacktrace: true | false
}

@enforce_keys ~w(repo)a
defstruct repo: nil
defstruct repo: nil, cache: nil, pid: nil, stacktrace: false

@behaviour Access
defdelegate get(v, key, default), to: Map
defdelegate fetch(v, key), to: Map
defdelegate get_and_update(v, key, func), to: Map
defdelegate pop(v, key), to: Map
end
35 changes: 35 additions & 0 deletions lib/etso/ets/objects_sorter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
defmodule Etso.ETS.ObjectsSorter do
@moduledoc """
The ETS Objects Sorter module is responsible for sorting results returned from ETS according to
the sort predicates provided in the query.
"""

def sort(ets_objects, %Ecto.Query{order_bys: []}) do
ets_objects
end

def sort(ets_objects, %Ecto.Query{} = query) do
sort_predicates = build_sort_predicates(query)
Enum.sort_by(ets_objects, & &1, &compare(&1, &2, sort_predicates))
end

defp build_sort_predicates(%Ecto.Query{} = query) do
Enum.flat_map(query.order_bys, fn %Ecto.Query.QueryExpr{expr: list} ->
Enum.map(list, fn {direction, field} ->
{direction, Enum.find_index(query.select.fields, &(&1 == field))}
end)
end)
end

defp compare(lhs, rhs, [{direction, index} | predicates]) do
case {direction, Enum.at(lhs, index), Enum.at(rhs, index)} do
{_, lhs, rhs} when lhs == rhs -> compare(lhs, rhs, predicates)
{:asc, lhs, rhs} -> lhs < rhs
{:desc, lhs, rhs} -> lhs > rhs
end
end

defp compare(_lhs, _rhs, []) do
true
end
end
5 changes: 3 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Etso.MixProject do
def project do
[
app: :etso,
version: "0.1.6",
version: "1.0.1",
elixir: "~> 1.8",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
Expand All @@ -27,7 +27,7 @@ defmodule Etso.MixProject do

defp deps do
[
{:ecto, "~> 3.0"},
{:ecto, "~> 3.8.3"},
{:dialyxir, "~> 1.0.0-rc.6", only: :dev, runtime: false},
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
{:jason, "~> 1.1", only: :test, runtime: false}
Expand Down Expand Up @@ -62,6 +62,7 @@ defmodule Etso.MixProject do
defp package_files do
~w(
lib/etso/*
lib/etso.ex
mix.exs
)
end
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
"dialyxir": {:hex, :dialyxir, "1.0.0-rc.6", "78e97d9c0ff1b5521dd68041193891aebebce52fc3b93463c0a6806874557d7d", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "49496d63267bc1a4614ffd5f67c45d9fc3ea62701a6797975bc98bc156d2763f"},
"earmark": {:hex, :earmark, "1.3.6", "ce1d0675e10a5bb46b007549362bd3f5f08908843957687d8484fe7f37466b19", [:mix], [], "hexpm", "1476378df80982302d5a7857b6a11dd0230865057dec6d16544afecc6bc6b4c2"},
"ecto": {:hex, :ecto, "3.8.1", "35e0bd8c8eb772e14a5191a538cd079706ecb45164ea08a7523b4fc69ab70f56", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f1b68f8d5fe3ab89e24f57c03db5b5d0aed3602077972098b3a6006a1be4b69b"},
"ecto": {:hex, :ecto, "3.8.3", "5e681d35bc2cbb46dcca1e2675837c7d666316e5ada14eca6c9c609b6232817c", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "af92dd7815967bcaea0daaaccf31c3b23165432b1c7a475d84144efbc703d105"},
"erlex": {:hex, :erlex, "0.2.4", "23791959df45fe8f01f388c6f7eb733cc361668cbeedd801bf491c55a029917b", [:mix], [], "hexpm", "4a12ebc7cd8f24f2d0fce93d279fa34eb5068e0e885bb841d558c4d83c52c439"},
"ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f1155337ae17ff7a1255217b4c1ceefcd1860b7ceb1a1874031e7a861b052e39"},
"jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
Expand Down
35 changes: 35 additions & 0 deletions test/northwind/repo_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ defmodule Northwind.RepoTest do
Repo.delete(employee)
end

test "Insert Employees" do
changes = [%{first_name: "Fred", employee_id: 100}, %{first_name: "Steven", employee_id: 200}]
nil = Repo.get(Model.Employee, 100)

Repo.insert_all(Model.Employee, changes)
%{first_name: "Fred"} = Repo.get(Model.Employee, 100)
end

test "List all Employees Again" do
Repo.all(Model.Employee)
end
Expand Down Expand Up @@ -145,4 +153,31 @@ defmodule Northwind.RepoTest do
|> where([e], e.metadata["twitter"] == "@andrew_fuller")
|> Repo.one!()
end

test "Order / Shipper + Employee Preloading" do
Model.Order
|> Repo.all()
|> Repo.preload([[shipper: :orders], :employee, :customer], in_parallel: true)
end

test "Order / Shipper / Orders Preloading before all()" do
Model.Order
|> preload([_], shipper: :orders)
|> Repo.all()
end

test "Order By Desc company_name, Asc phone" do
sorted_etso =
Model.Shipper
|> order_by([x], desc: x.company_name, asc: x.phone)
|> Repo.all()

sorted_code =
Model.Shipper
|> Repo.all()
|> Enum.sort_by(& &1.company_name, :desc)
|> Enum.sort_by(& &1.phone)

assert sorted_etso == sorted_code
end
end

0 comments on commit 8e0b164

Please sign in to comment.