Current section

Files

Jump to
pointers lib migration.ex
Raw

lib/migration.ex

defmodule Pointers.Migration do
@moduledoc """
Migration helpers for creating tables that participate in the pointers abasction.
"""
import Ecto.Query, only: [from: 2]
import Ecto.Migration
alias Pointers.Config
alias Pointers.Table
defdelegate init_pointers_ulid_extra(), to: Pointers.ULID.Migration
defp table_name(name) when is_atom(name), do: Atom.to_string(name)
defp table_name(name) when is_binary(name), do: name
@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, references(Config.pointer_table(), type: :uuid), 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
Pointers.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_if_exists table(name)
end
@doc "Creates a trait table - one with a ULID primary key and no trigger"
defmacro create_trait_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 trait table. Actually just drop_if_exists table(name)"
@spec drop_trait_table(name :: binary) :: nil
def drop_trait_table(name), do: drop_if_exists 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(Config.table_table(), primary_key: false) do
add_pointer_pk()
add :table, :text, null: false
end
create_if_not_exists table(Config.pointer_table(), primary_key: false) do
add_pointer_pk()
ref = references Config.table_table(),
on_delete: :delete_all, on_update: :update_all, type: :uuid
add :table_id, ref, null: false
end
create_if_not_exists unique_index(Config.table_table(), :table)
create_if_not_exists index(Config.pointer_table(), :table_id)
flush()
insert_table_record(Table.table_id(), Config.table_table())
create_pointer_trigger_function()
flush()
create_pointer_trigger(Config.table_table())
end
def init_pointers(:down) do
drop_pointer_trigger(Config.table_table())
drop_pointer_trigger_function()
drop_if_exists index(Config.pointer_table(), :table_id)
drop_if_exists index(Config.table_table(), :table)
drop_if_exists table(Config.pointer_table())
drop_if_exists table(Config.table_table())
end
@doc false
def create_pointer_trigger_function() do
:ok = execute """
create or replace function #{Config.trigger_function()}() returns trigger as $$
declare table_id uuid;
begin
select id into table_id from #{Config.table_table()}
where #{Config.table_table()}.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 #{Config.pointer_table()} (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 #{Config.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 "#{Config.trigger_prefix()}#{table}"
before insert on "#{table}"
for each row
execute procedure #{Config.trigger_function()}()
"""
end
@doc false
def drop_pointer_trigger(table) do
table = table_name(table)
execute """
drop trigger if exists "#{Config.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
id = Pointers.ULID.cast!(id)
name = table_name(name)
opts = [on_conflict: [set: [id: id]], conflict_target: [:table]]
repo().insert_all(Pointers.Table, [%{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 Config.table_table(), where: t.id == ^id)
end
end