Current section
Files
Jump to
Current section
Files
lib/migration.ex
defmodule Pointers.Migration do
@moduledoc "Helpers for writing Pointer-aware migrations."
import Ecto.Query, only: [from: 2]
import Ecto.Migration
alias Pointers.{Pointer, Table, ULID}
defdelegate init_pointers_ulid_extra(), to: ULID.Migration
@type pointer_type :: :strong | :weak | :unbreakable
@doc "Creates a strong, weak or unbreakable pointer depending on `type`."
@spec pointer(type :: pointer_type) :: term
@spec pointer(module :: atom, type :: pointer_type) :: term
def pointer(table \\ Pointer, type)
def pointer(table, :strong), do: strong_pointer(table)
def pointer(table, :weak), do: weak_pointer(table)
def pointer(table, :unbreakable), do: unbreakable_pointer(table)
@doc """
A reference to a pointer for use with 'add/3`. A strong pointer will
be deleted when the thing it's pointing to is deleted.
"""
def strong_pointer(table \\ Pointer) do
references table.__schema__(:source),
type: :uuid,
on_update: :update_all,
on_delete: :delete_all
end
@doc """
A reference to a pointer for use with 'add/3`. A weak pointer will
be set null when the thing it's pointing to is deleted.
"""
def weak_pointer(table \\ Pointer) do
references table.__schema__(:source),
type: :uuid,
on_update: :update_all,
on_delete: :nilify_all
end
@doc """
A reference to a pointer for use with 'add/3`. An unbreakable
pointer will prevent the thing it's pointing to from being deleted.
"""
def unbreakable_pointer(table \\ Pointer) do
references table.__schema__(:source),
type: :uuid,
on_update: :update_all,
on_delete: :restrict
end
defp table_name(name) when is_atom(name), do: Atom.to_string(name)
defp table_name(name) when is_binary(name), do: name
config = Application.get_env(:pointers, __MODULE__, [])
@trigger_function Keyword.get(config, :trigger_function, "pointers_trigger")
@trigger_prefix Keyword.get(config, :trigger_prefix, "pointers_trigger_")
@doc """
Adds a pointer primary key to the table.
Not required if you are using `create_pointable_table`
"""
@spec add_pointer_pk() :: nil
def add_pointer_pk(), do: add(:id, :uuid, primary_key: true)
@spec add_pointer_ref_pk() :: nil
def add_pointer_ref_pk(),
do: add(:id, strong_pointer(Pointer), primary_key: true)
@doc "Creates a pointable table along with its trigger."
@spec create_pointable_table(name :: binary, id :: binary, body :: term) :: term
@spec create_pointable_table(name :: binary, id :: binary, opts :: Keyword.t, body :: term) :: term
defmacro create_pointable_table(name, id, opts \\ [], body) do
ULID.cast!(id)
opts = [primary_key: false] ++ opts
quote do
Pointers.Migration.insert_table_record(unquote(id), unquote(name))
table = Ecto.Migration.table(unquote(name), unquote(opts))
Ecto.Migration.create_if_not_exists table do
Pointers.Migration.add_pointer_pk()
unquote(body)
end
Pointers.Migration.create_pointer_trigger(unquote(name))
end
end
@doc "Drops a pointable table"
@spec drop_pointable_table(name :: binary, id :: binary) :: nil
def drop_pointable_table(name, id) do
drop_pointer_trigger(name)
delete_table_record(id)
drop_table(name)
end
@doc "Creates a mixin table - one with a ULID primary key and no trigger"
defmacro create_mixin_table(name, opts \\ [], body) do
opts = [primary_key: false] ++ opts
quote do
table = Ecto.Migration.table(unquote(name), unquote(opts))
Ecto.Migration.create_if_not_exists table do
Pointers.Migration.add_pointer_ref_pk()
unquote(body)
end
end
end
@doc "Drops a mixin table. Actually just a simple cascading drop"
@spec drop_mixin_table(name :: binary) :: nil
def drop_mixin_table(name), do: drop_table(name)
@doc """
When migrating up: initialises the pointers database.
When migrating down: deinitialises the pointers database.
"""
@spec init_pointers() :: nil
def init_pointers(), do: init_pointers(direction())
@doc """
Given `:up`: initialises the pointers database.
Given `:down`: deinitialises the pointers database.
"""
@spec init_pointers(direction :: :up | :down) :: nil
def init_pointers(:up) do
create_if_not_exists table(Table.__schema__(:source), primary_key: false) do
add_pointer_pk()
add :table, :text, null: false
end
create_if_not_exists table(Pointer.__schema__(:source), primary_key: false) do
add_pointer_pk()
ref = references Table.__schema__(:source),
on_delete: :delete_all, on_update: :update_all, type: :uuid
add :table_id, ref, null: false
end
create_if_not_exists unique_index(Table.__schema__(:source), :table)
create_if_not_exists index(Pointer.__schema__(:source), :table_id)
flush()
insert_table_record(Table.__pointable__(:table_id), Table.__schema__(:source))
create_pointer_trigger_function()
flush()
create_pointer_trigger(Table.__schema__(:source))
end
def init_pointers(:down) do
drop_pointer_trigger(Table.__schema__(:source))
drop_pointer_trigger_function()
drop_if_exists index(Pointer.__schema__(:source), :table_id)
drop_if_exists index(Table.__schema__(:source), :table)
drop_table(Pointer.__schema__(:source))
drop_table(Table.__schema__(:source))
end
@doc false
def create_pointer_trigger_function() do
:ok = execute """
create or replace function #{@trigger_function}() returns trigger as $$
declare table_id uuid;
begin
select id into table_id from #{Table.__schema__(:source)}
where #{Table.__schema__(:source)}.table = TG_TABLE_NAME;
if table_id is null then
raise exception 'Table % does not participate in the pointers abstraction', TG_TABLE_NAME;
end if;
insert into #{Pointer.__schema__(:source)} (id, table_id) values (NEW.id, table_id)
on conflict do nothing;
return NEW;
end;
$$ language plpgsql
"""
end
@doc false
def drop_pointer_trigger_function() do
execute "drop function if exists #{@trigger_function}() cascade"
end
@doc false
def create_pointer_trigger(table) do
table = table_name(table)
drop_pointer_trigger(table) # because there is no create trigger if not exists
execute """
create trigger "#{@trigger_prefix}#{table}"
before insert on "#{table}"
for each row
execute procedure #{@trigger_function}()
"""
end
@doc false
def drop_pointer_trigger(table) do
table = table_name(table)
execute """
drop trigger if exists "#{@trigger_prefix}#{table}" on "#{table}"
"""
end
#Insert a Table record. Not required when using `create_pointable_table`
@doc false
def insert_table_record(id, name) do
{:ok, id} = Pointers.ULID.dump(Pointers.ULID.cast!(id))
name = table_name(name)
opts = [on_conflict: [set: [id: id]], conflict_target: [:table]]
repo().insert_all(Table.__schema__(:source), [%{id: id, table: name}], opts)
end
#Delete a Table record. Not required when using `drop_pointable_table`
@doc false
def delete_table_record(id) do
{:ok, id} = Pointers.ULID.dump(Pointers.ULID.cast!(id))
repo().delete_all(from t in Table.__schema__(:source), where: t.id == ^id)
end
def drop_table(name), do: execute "drop table if exists #{name} cascade"
end