-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add diff for many associations without primary keys #144
base: master
Are you sure you want to change the base?
Changes from all commits
c5967e2
2b0f3d3
1f00ebf
42b9fc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -303,37 +303,47 @@ defmodule EctoDiff do | |
defp diff_association(previous, current, %{cardinality: :many, field: field, related: struct}, acc) do | ||
primary_key_fields = struct.__schema__(:primary_key) | ||
|
||
if primary_key_fields == [], | ||
do: raise("cannot determine difference in many association with no primary key for `#{struct}`") | ||
|
||
{previous_map, keys} = | ||
Enum.reduce(previous, {%{}, []}, fn x, {map, keys} -> | ||
key = Map.take(x, primary_key_fields) | ||
{Map.put(map, key, x), [key | keys]} | ||
end) | ||
|
||
{current_map, keys} = | ||
Enum.reduce(current, {%{}, keys}, fn x, {map, keys} -> | ||
key = Map.take(x, primary_key_fields) | ||
{Map.put(map, key, x), [key | keys]} | ||
end) | ||
|
||
keys = keys |> Enum.reverse() |> Enum.uniq() | ||
|
||
diffs = | ||
keys | ||
|> Enum.map(fn key -> | ||
prev_child = Map.get(previous_map, key) | ||
current_child = Map.get(current_map, key) | ||
if primary_key_fields == [] do | ||
from = max(length(previous), length(current)) - 1 | ||
|
||
do_diff(prev_child, current_child) | ||
end) | ||
|> Enum.reject(&no_changes?/1) | ||
from | ||
|> Range.new(0) | ||
|> Enum.map(fn i -> | ||
prev_child = Enum.at(previous, i) | ||
current_child = Enum.at(current, i) | ||
|
||
if diffs == [] do | ||
acc | ||
else | ||
[{field, diffs} | acc] | ||
do_diff(prev_child, current_child) | ||
end) | ||
Comment on lines
+308
to
+317
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Along with doughsay's suggestion, once you bring in that zip_longest(previous, current)
|> Enum.map(&do_diff(&1, &2)) |
||
else | ||
{previous_map, keys} = | ||
Enum.reduce(previous, {%{}, []}, fn x, {map, keys} -> | ||
key = Map.take(x, primary_key_fields) | ||
{Map.put(map, key, x), [key | keys]} | ||
end) | ||
|
||
{current_map, keys} = | ||
Enum.reduce(current, {%{}, keys}, fn x, {map, keys} -> | ||
key = Map.take(x, primary_key_fields) | ||
{Map.put(map, key, x), [key | keys]} | ||
end) | ||
|
||
keys | ||
|> Enum.reverse() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. However, once you make the above change, you'll need to pull this reverse back out to the end of this whole function body, in the pipeline after |
||
|> Enum.uniq() | ||
|> Enum.map(fn key -> | ||
prev_child = Map.get(previous_map, key) | ||
current_child = Map.get(current_map, key) | ||
|
||
do_diff(prev_child, current_child) | ||
end) | ||
end | ||
|
||
diffs | ||
|> Enum.reject(&no_changes?/1) | ||
|> case do | ||
[] -> acc | ||
changed -> [{field, changed} | acc] | ||
end | ||
end | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
defmodule InventoryCore.Repo.Migrations.CreateBoxes do | ||
use Ecto.Migration | ||
|
||
def change do | ||
create table(:boxes) do | ||
add :shapes, {:array, :map}, default: [] | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
defmodule EctoDiff.Box do | ||
@moduledoc false | ||
|
||
use Ecto.Schema | ||
import Ecto.Changeset | ||
|
||
schema("boxes") do | ||
embeds_many :shapes, EctoDiff.Shape, on_replace: :delete | ||
end | ||
|
||
def new(params), do: changeset(%__MODULE__{}, params) | ||
def update(struct, params), do: changeset(struct, params) | ||
|
||
defp changeset(struct, params) do | ||
struct | ||
|> cast(params, []) | ||
|> cast_embed(:shapes) | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
defmodule EctoDiff.Shape do | ||
@moduledoc false | ||
|
||
use Ecto.Schema | ||
import Ecto.Changeset | ||
|
||
@primary_key false | ||
embedded_schema do | ||
field :angles, :integer | ||
end | ||
|
||
def changeset(struct, params), do: cast(struct, params, [:angles]) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The performance of this isn't great, because
Enum.at
traverses the whole list each time. It would be better to zip the two lists together first and traverse the zipped list instead. The problem of course is thatEnum.zip
stops at the shortest list. Here's a simple implementation of a zip that zips all the way to the longest list, defaulting missing elements tonil
: https://gist.github.com/nathan-cruz77/586092fa2487da2bdd5603934e7db4adI would recommend adding this util function somewhere and using it here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, good idea