Current section
Files
Jump to
Current section
Files
lib/as_nested_set/modifiable.ex
defmodule AsNestedSet.Modifiable do
@type position :: :left | :right | :child | :parent
import Ecto.Query
import AsNestedSet.Helper
@spec create(AsNestedSet.t, AsNestedSet.t, position) :: AsNestedSet.executable
def create(new_model, target \\ nil, position) when is_atom(position) do
fn repo ->
case validate_create(new_model, target, position) do
:ok -> do_safe_create(repo, new_model, do_reload(repo, target), position)
error -> error
end
end
end
@spec reload(AsNestedSet.t) :: AsNestedSet.executable
def reload(model) do
fn repo ->
do_reload(repo, model)
end
end
defp validate_create(new_model, parent, position) do
cond do
parent == nil && position != :root -> {:error, :target_is_required}
position != :root && !AsNestedSet.Scoped.same_scope?(parent, new_model) -> {:error, :not_the_same_scope}
true -> :ok
end
end
defp do_safe_create(repo, %{__struct__: struct} = new_model, target, :left) do
left = get_field(target, :left)
left_column = get_column_name(target, :left)
right_column = get_column_name(target, :right)
# update all the left and right column
from(q in struct,
where: field(q, ^left_column) >= ^left,
update: [inc: ^[{left_column, 2}]]
)
|> AsNestedSet.Scoped.scoped_query(target)
|> repo.update_all([])
from(q in struct,
where: field(q, ^right_column) > ^left,
update: [inc: ^[{right_column, 2}]]
)
|> AsNestedSet.Scoped.scoped_query(target)
|> repo.update_all([])
parent_id_column = get_column_name(target, :parent_id)
parent_id = get_field(target, :parent_id)
# insert the new model
new_model
|> struct.changeset(Map.new([
{left_column, left},
{right_column, left + 1},
{parent_id_column, parent_id}
]))
|> repo.insert!
end
defp do_safe_create(repo, %{__struct__: struct} = new_model, target, :right) do
right = get_field(target, :right)
left_column = get_column_name(target, :left)
# update all the left and right column
from(q in struct,
where: field(q, ^left_column) > ^right,
update: [inc: ^[{left_column, 2}]]
)
|> AsNestedSet.Scoped.scoped_query(target)
|> repo.update_all([])
right_column = get_column_name(target, :right)
from(q in struct,
where: field(q, ^right_column) > ^right,
update: [inc: ^[{right_column, 2}]]
)
|> AsNestedSet.Scoped.scoped_query(target)
|> repo.update_all([])
parent_id_column = get_column_name(target, :parent_id)
parent_id = get_field(target, :parent_id)
# insert new model
new_model
|> struct.changeset(Map.new([
{left_column, right + 1},
{right_column, right + 2},
{parent_id_column, parent_id}
]))
|> repo.insert!
end
defp do_safe_create(repo, %{__struct__: struct} = new_model, target, :child) do
left_column = get_column_name(target, :left)
right = get_field(target, :right)
from(q in struct,
where: field(q, ^left_column) > ^right,
update: [inc: ^[{left_column, 2}]]
)
|> AsNestedSet.Scoped.scoped_query(target)
|> repo.update_all([])
right_column = get_column_name(target, :right)
from(q in struct,
where: field(q, ^right_column) >= ^right,
update: [inc: ^[{right_column, 2}]]
)
|> AsNestedSet.Scoped.scoped_query(target)
|> repo.update_all([])
parent_id_column = get_column_name(target, :parent_id)
node_id = get_field(target, :node_id)
new_model
|> struct.changeset(Map.new([
{left_column, right},
{right_column, right + 1},
{parent_id_column, node_id}
]))
|> repo.insert!
end
defp do_safe_create(repo, %{__struct__: struct} = new_model, _target, :root) do
right_most = AsNestedSet.Queriable.right_most(struct, new_model).(repo) || -1
new_model = new_model
|> set_field(:left, right_most + 1)
|> set_field(:right, right_most + 2)
|> set_field(:parent_id, nil)
|> repo.insert!
new_model
end
defp do_safe_create(repo, %{__struct__: struct} = new_model, target, :parent) do
right = get_field(target, :right)
left = get_field(target, :left)
right_column = get_column_name(target, :right)
from(q in struct,
where: field(q, ^right_column) > ^right,
update: [inc: ^[{right_column, 2}]]
)
|> AsNestedSet.Scoped.scoped_query(target)
|> repo.update_all([])
left_column = get_column_name(target, :left)
from(q in struct,
where: field(q, ^left_column) > ^right,
update: [inc: ^[{left_column, 2}]]
)
|> AsNestedSet.Scoped.scoped_query(target)
|> repo.update_all([])
from(q in struct,
where: field(q, ^left_column) >= ^left and field(q, ^right_column) <= ^right,
update: [inc: ^[{right_column, 1}, {left_column, 1}]]
)
|> AsNestedSet.Scoped.scoped_query(target)
|> repo.update_all([])
parent_id = get_field(target, :parent_id)
new_model = new_model
|> set_field(:left, left)
|> set_field(:right, right + 2)
|> set_field(:parent_id, parent_id)
|> repo.insert!
node_id = get_field(target, :node_id)
node_id_column = get_column_name(target, :node_id)
parent_id_column = get_column_name(target, :parent_id)
new_model_id = get_field(new_model, :node_id)
from(q in struct,
where: field(q, ^node_id_column) == ^node_id,
update: [set: ^[{parent_id_column, new_model_id}]]
)
|> AsNestedSet.Scoped.scoped_query(target)
|> repo.update_all([])
new_model
end
defp do_reload(_repo, nil), do: nil
defp do_reload(repo, %{__struct__: struct} = target) do
node_id = get_field(target, :node_id)
node_id_column = get_column_name(target, :node_id)
from(q in struct,
where: field(q, ^node_id_column) == ^node_id,
limit: 1
)
|> AsNestedSet.Scoped.scoped_query(target)
|> repo.one
end
@spec delete(AsNestedSet.t) :: AsNestedSet.exectuable
def delete(%{__struct__: struct} = model) do
fn repo ->
left = get_field(model, :left)
right = get_field(model, :right)
width = right - left + 1
left_column = get_column_name(model, :left)
right_column = get_column_name(model, :right)
from(q in struct,
where: field(q, ^left_column) >= ^left and field(q, ^left_column) <= ^right
)
|> AsNestedSet.Scoped.scoped_query(model)
|> repo.delete_all([])
from(q in struct,
where: field(q, ^right_column) > ^right,
update: [inc: ^[{right_column, -width}]]
)
|> AsNestedSet.Scoped.scoped_query(model)
|> repo.update_all([])
from(q in struct,
where: field(q, ^left_column) > ^right,
update: [inc: ^[{left_column, -width}]]
)
|> AsNestedSet.Scoped.scoped_query(model)
|> repo.update_all([])
end
end
@spec move(AsNestedSet.t, AsNestedSet.t, position) :: AsNestedSet.executable
@spec move(AsNestedSet.t, :root) :: AsNestedSet.executable
def move(%{__struct__: _} = model, target \\ nil, position) when is_atom(position) do
fn repo ->
model = do_reload(repo, model)
case validate_move(model, target, position) do
:ok -> do_safe_move(repo, model, do_reload(repo, target), position)
error -> error
end
end
end
defp validate_move(model, target, position) do
cond do
target == nil && position != :root -> {:error, :target_is_required}
position == :parent -> {:error, :cannot_move_to_parent}
target != nil && get_field(model, :left) <= get_field(target, :left) && get_field(model, :right) >= get_field(target, :right) -> {:error, :within_the_same_tree}
position != :root && !AsNestedSet.Scoped.same_scope?(target, model) -> {:error, :not_the_same_scope}
true -> :ok
end
end
defp do_safe_move(repo, model, target, position) do
if target != nil && get_field(model, :node_id) == get_field(target, :node_id) do
model
else
target_bound = target_bound(repo, model, target, position)
left = get_field(model, :left)
right = get_field(model, :right)
case get_bounaries(model, target_bound) do
{bound, other_bound} ->
do_switch(repo, model, {left, right, bound, other_bound}, new_parent_id(target, position))
:no_operation ->
model
end
end
end
def target_bound(repo, model, target, position) do
case position do
:child -> get_field(target, :right)
:left -> get_field(target, :left)
:right -> get_field(target, :right) + 1
:root -> AsNestedSet.right_most(model).(repo) + 1
end
end
def get_bounaries(model, target_bound) do
left = get_field(model, :left)
right = get_field(model, :right)
cond do
target_bound - 1 >= right + 1 ->
{right + 1, target_bound - 1}
target_bound <= left - 1 ->
{target_bound, left - 1}
true ->
:no_operation
end
end
defp new_parent_id(target, position) do
case position do
:child -> get_field(target, :node_id)
:left -> get_field(target, :parent_id)
:right -> get_field(target, :parent_id)
:root -> nil
end
end
defp do_switch(repo, %{__struct__: struct} = model, boundaries, new_parent_id) do
# As we checked the boundaries, the two interval is non-overlapping
[a, b, c, d]= boundaries |> Tuple.to_list |> Enum.sort
node_id = get_field(model, :node_id)
node_id_column = get_column_name(model, :node_id)
parent_id_column = get_column_name(model, :parent_id)
# shift the left part to the temporary position (negative space)
do_shift(repo, model, {a, b}, -b - 1)
do_shift(repo, model, {c, d}, a - c)
do_shift(repo, model, {a - b - 1, -1}, d + 1)
from(n in struct, where: field(n, ^node_id_column) == ^node_id, update: [set: ^[{parent_id_column, new_parent_id}]])
|> AsNestedSet.Scoped.scoped_query(model)
|> repo.update_all([])
do_reload(repo, model)
end
defp do_shift(repo, %{__struct__: struct} = model, {left, right}, delta) do
left_column = get_column_name(model, :left)
right_column = get_column_name(model, :right)
from(struct)
|> where([n], field(n, ^left_column) >= ^left and field(n, ^left_column) <= ^right)
|> update([n], [inc: ^[{left_column, delta}]])
|> AsNestedSet.Scoped.scoped_query(model)
|> repo.update_all([])
from(struct)
|> where([n], field(n, ^right_column) >= ^left and field(n, ^right_column) <= ^right)
|> update([n], [inc: ^[{right_column, delta}]])
|> AsNestedSet.Scoped.scoped_query(model)
|> repo.update_all([])
end
end