Current section
Files
Jump to
Current section
Files
lib/amnesia/table/definition.ex
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# Version 2, December 2004
#
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
#
# 0. You just DO WHAT THE FUCK YOU WANT TO.
defmodule Amnesia.Table.Definition do
@doc false
def where(module, attributes, options) do
spec = { :{}, [], [module | for { key, _ } <- attributes do
if options[:in] && options[:in][key] do
{ :in, [], [{ key, [], nil }, options[:in][key]] }
else
{ key, [], nil }
end
end] }
if options[:qualified] do
qualifier = module |> to_string |> String.split(".") |> List.last
|> String.replace(~r/([A-Z])/, "_\\1") |> String.slice(1, 255)
|> String.downcase |> String.to_atom
quote do: Exquisite.match(unquote({ qualifier, [], nil }) in unquote(spec),
unquote(options))
else
quote do: Exquisite.match(unquote(spec), unquote(options))
end
end
@doc false
def match(module, pattern) do
[module | for { key, _ } <- module.attributes() do
if Keyword.has_key?(pattern, key), do: pattern[key], else: :_
end] |> List.to_tuple
end
@doc false
def autoincrement(module, database, attributes, record) do
alias Amnesia.Metadata, as: M
Enum.reduce attributes, record, fn field, record ->
if record |> Map.get(field) |> is_nil do
record |> Map.put(field, database.metadata() |> M.counter(module, field, +1))
else
record
end
end
end
@doc false
def attributes(attributes, options, index, copying) do
Keyword.merge(options, [
attributes: Keyword.keys(attributes),
copying: copying,
index: index
])
end
@doc false
def define(database, name, attributes, opts \\ []) do
if length(attributes) <= 1 do
raise ArgumentError, message: "the table attributes must be more than 1"
end
block = Keyword.get(opts, :do, nil)
opts = Keyword.delete(opts, :do)
index = Keyword.get(opts, :index, [])
{ autoincrement, attributes } = Enum.reduce attributes, { [], [] }, fn
{ name, { :autoincrement, _, _ } }, { autoincrement, attributes } ->
{ [name | autoincrement], attributes ++ [{ name, nil }] }
{ name, value }, { autoincrement, attributes } ->
{ autoincrement, attributes ++ [{ name, value }] }
name, { autoincrement, attributes } ->
{ autoincrement, attributes ++ [{ name, nil }] }
end
quote do
defmodule unquote(name) do
defstruct unquote(attributes)
use Amnesia.Hooks, write: 1, write!: 1, read: 2, read!: 2
require Exquisite
alias Amnesia.Table, as: T
alias Amnesia.Table.Definition, as: D
alias Amnesia.Selection, as: S
@type autoincrement :: non_neg_integer
@database unquote(database)
@options unquote(opts)
@autoincrement unquote(autoincrement)
@attributes unquote(attributes)
@index unquote(index)
@doc """
Require the needed modules to use the table effectively.
"""
defmacro __using__(_opts) do
quote do
require Exquisite
require unquote(__MODULE__)
end
end
def telemetry_track(table, _action) do
telemetry = :persistent_term.get(:amnesia_table_telemetry, %{})
cond do
telemetry[table] -> :nop
:else ->
telemetry = put_in(telemetry, [Access.key(table)], true)
:persistent_term.put(:amnesia_table_telemetry, telemetry)
end
end
@doc """
Return the database the table belongs to.
"""
@spec database :: module
def database do
@database
end
@doc """
The options passed when the table was defined.
"""
@spec options :: Keyword.t
def options do
@options
end
@doc """
The in order keyword list of attributes passed when the table was defined.
"""
@spec attributes :: Keyword.t
def attributes do
@attributes
end
@doc """
Get the name of the id key.
"""
@spec id :: atom
def id do
unquote(attributes |> Enum.at(0) |> elem(0))
end
@doc false
def coerce(unquote({ :%, [], [{ :__MODULE__, [], nil }, { :%{}, [],
for { key, _ } <- attributes do
{ key, { key, [], nil } }
end
}] })) do
unquote({ :{}, [], [name |
for { key, _ } <- attributes do
{ key, [], nil }
end
] })
end
def coerce(unquote({ :{}, [], [name |
for { key, _ } <- attributes do
{ key, [], nil }
end
] })) do
unquote({ :%, [], [{ :__MODULE__, [], nil }, { :%{}, [],
for { key, _ } <- attributes do
{ key, { key, [], nil } }
end
}] })
end
def coerce(list) when list |> is_list do
Enum.map(list, &coerce/1)
end
def coerce(value) do
value
end
@doc """
Wait for the table optionally with a timeout.
"""
@spec wait :: :ok | { :timeout, [atom] } | { :error, atom }
@spec wait(integer | :infinity) :: :ok | { :timeout, [atom] } | { :error, atom }
def wait(timeout \\ :infinity) do
T.wait([__MODULE__], timeout)
end
@doc """
Force load the table.
"""
@spec force :: :yes | { :error, any }
def force do
T.force(__MODULE__)
end
@doc """
Create the table with the given copying mode and inherent definition.
"""
@spec create :: Amnesia.o
@spec create(T.c) :: Amnesia.o
def create(copying \\ []) do
T.create(__MODULE__, D.attributes(@attributes, @options, @index, copying))
end
@spec create! :: :ok | no_return
@spec create!(T.c) :: :ok | no_return
def create!(copying \\ []) do
T.create!(__MODULE__, D.attributes(@attributes, @options, @index, copying))
end
@doc """
Return the type of the table.
"""
@spec type(t) :: :set | :ordered_set | :bag
def type(self) do
@options[:type]
end
@doc """
Check if the table is a bag.
"""
@spec bag? :: boolean
def bag? do
@options[:type] == :bag
end
@doc """
Check if the table is a set.
"""
@spec set? :: boolean
def set? do
(@options[:type] || :set) == :set
end
@doc """
Check if the table is an ordered set.
"""
@spec ordered_set? :: boolean
def ordered_set? do
@options[:type] == :ordered_set
end
@doc """
Get information about the table, see `mnesia:table_info`.
"""
@spec info(atom) :: any
def info(key) do
T.info(__MODULE__, key)
end
@doc """
Return properties of the table.
"""
@spec properties :: Keyword.t
def properties do
T.properties(__MODULE__)
end
@doc """
Change the access of the table, see `mnesia:change_table_access_mode`.
## Modes
* `:both` sets read and write mode, it's the default.
* `:read!` sets read-only mode.
"""
@spec mode(:both | :read!) :: T.o
def mode(value) do
T.mode(__MODULE__, value)
end
@doc """
Change the copying mode of the table on the given node, see
`mnesia:change_table_copy_type`.
## Modes
* `:disk` sets `:disc_copies` mode
* `:disk!` sets `:disc_only_copies` mode
* `:memory` sets `:ram_copies` mode
"""
@spec copying(node, T.cv) :: T.o
def copying(node, to) do
T.copying(__MODULE__, node, to)
end
@doc """
Change the table loading priority.
"""
@spec priority(integer) :: T.o
def priority(value) do
T.priority(__MODULE__, value)
end
@doc """
Change the table majority.
"""
@spec majority(boolean) :: T.o
def majority(value) do
T.majority(__MODULE__, value)
end
@doc """
Add a copy of the table on the given node with the given mode.
"""
@spec add_copy(node) :: T.o
@spec add_copy(node, T.cv) :: T.o
def add_copy(node, type \\ :disk) do
T.add_copy(__MODULE__, node, type)
end
@doc """
Move a copy of the table from the given node to another given node.
"""
@spec move_copy(node, node) :: T.o
def move_copy(from, to) do
T.move_copy(__MODULE__, from, to)
end
@doc """
Delete a copy of the table from the given node.
"""
@spec delete_copy(node) :: T.o
def delete_copy(node) do
T.delete_copy(__MODULE__, node)
end
@doc """
Add the index in the table for the given attribute.
"""
@spec add_index(atom) :: T.o
def add_index(attribute) do
T.add_index(__MODULE__, attribute)
end
@doc """
Delete the index in the table for the given attribute.
"""
@spec delete_index(atom) :: T.o
def delete_index(attribute) do
T.delete_index(__MODULE__, attribute)
end
@doc """
Set master nodes for the table, see `mnesia:set_master_nodes`.
"""
@spec master_nodes([node]) :: :ok | { :error, any }
def master_nodes(nodes) do
T.master_nodes(__MODULE__, nodes)
end
@doc """
Lock the table with the given lock.
## Locks
* `:write` sets a `:write` lock
* `:write!` sets a `:sticky_write` lock
* `:read` sets a `:read` lock
"""
@spec lock(:write | :write! | :read) :: [node] | :ok | no_return
def lock(mode) do
T.lock(__MODULE__, mode)
end
@doc """
Destroy the table.
"""
@spec destroy :: T.o
def destroy do
T.destroy(__MODULE__)
end
@doc """
Destroy the table, raising if an error occurs.
"""
@spec destroy! :: :ok | no_return
def destroy! do
T.destroy!(__MODULE__)
end
@doc """
Clear the content of the table.
"""
@spec clear :: T.o
def clear do
__MODULE__.telemetry_track(__MODULE__, :clear)
T.clear(__MODULE__)
end
@doc """
Check if the key is present in the table.
"""
@spec member?(any) :: boolean
def member?(key) do
__MODULE__.telemetry_track(__MODULE__, :member?)
T.member?(__MODULE__, key)
end
@doc """
Get the number of records in the table.
"""
@spec count :: non_neg_integer
def count do
__MODULE__.telemetry_track(__MODULE__, :count)
T.count(__MODULE__)
end
if @options[:type] == :bag do
@doc """
Read records from the table with the given key and given lock, see
`mnesia:read`.
## Locks
* `:write` sets a `:write` lock
* `:write!` sets a `:sticky_write` lock
* `:read` sets a `:read` lock
"""
@spec read(any) :: [t] | nil | no_return
@spec read(any, :read | :write | :write!) :: [t] | nil | no_return
def read(key, lock \\ :read) do
__MODULE__.telemetry_track(__MODULE__, :read)
records = coerce(T.read(__MODULE__, key, lock))
hook_read(key, records)
end
@doc """
Read records from the table, see `mnesia:dirty_read`.
"""
@spec read!(any) :: [t] | nil | no_return
def read!(key) do
__MODULE__.telemetry_track(__MODULE__, :read!)
records = coerce(T.read!(__MODULE__, key))
hook_read!(key, records)
end
else
@doc """
Read a record from the table with the given lock, see `mnesia:read`.
Unlike `mnesia:read` this returns either the record or nil.
## Locks
* `:write` sets a `:write` lock
* `:write!` sets a `:sticky_write` lock
* `:read` sets a `:read` lock
"""
@spec read(any) :: t | nil | no_return
@spec read(any, :read | :write | :write!) :: t | nil | no_return
def read(key, lock \\ :read) do
__MODULE__.telemetry_track(__MODULE__, :read)
record = case T.read(__MODULE__, key, lock) do
[r] -> coerce(r)
_ -> nil
end
hook_read(key, record)
end
@doc """
Read a record from the table, see `mnesia:dirty_read`.
Unlike `mnesia:dirty_read` this returns either the record or nil.
"""
@spec read!(any) :: t | nil | no_return
def read!(key) do
__MODULE__.telemetry_track(__MODULE__, :read!)
record = case T.read!(__MODULE__, key) do
[r] -> coerce(r)
_ -> nil
end
hook_read!(key, record)
end
end
@doc """
Read records from the table based on a secondary index given as position,
see `mnesia:index_read`.
"""
@spec read_at(any, integer | atom) :: [t] | nil | no_return
def read_at(key, position) do
coerce(T.read_at(__MODULE__, key, position))
end
@doc """
Read records from the table based on a secondary index given as position,
see `mnesia:dirty_index_read`.
"""
@spec read_at!(any, integer | atom) :: [t] | nil | no_return
def read_at!(key, position) do
coerce(T.read_at!(__MODULE__, key, position))
end
@doc """
Return all the keys in the table, see `mnesia:all_keys`.
"""
@spec keys :: list | no_return
def keys do
__MODULE__.telemetry_track(__MODULE__, :keys)
T.keys(__MODULE__)
end
@doc """
Return all keys in the table, see `mnesia:dirty_all_keys`.
"""
@spec keys! :: list | no_return
def keys! do
__MODULE__.telemetry_track(__MODULE__, :keys!)
T.keys!(__MODULE__)
end
@doc """
Read a record based on a slot, see `mnesia:dirty_slot`.
"""
@spec at!(integer) :: t | nil | no_return
def at!(position) do
T.at!(__MODULE__, position)
end
@doc """
Return the key of the record.
"""
@spec key(t) :: any
def key(%__MODULE__{unquote(Enum.at(attributes, 0) |> elem(0)) => key}) do
key
end
@doc """
Return the first key or record in the table, see `mnesia:first`.
By default it returns the record, if you want only the key pass true as
first parameter.
If the table is a bag, it will return a list of records.
"""
@spec first :: t | nil | no_return
@spec first(boolean) :: any | t | nil | no_return
@spec first(boolean, atom) :: any | t | nil | no_return
def first(key \\ false, lock \\ :read)
def first(true, lock) do
T.first(__MODULE__)
end
def first(false, lock) do
read(T.first(__MODULE__), lock)
end
@doc """
Return the first key or record in the table, see `mnesia:dirty_first`.
By default it returns the record, if you want only the key pass true as
first parameter.
If the table is a bag, it will return a list of records.
"""
@spec first! :: any | t | nil | no_return
@spec first!(boolean) :: any | t | nil | no_return
def first!(key \\ false)
def first!(false) do
read!(T.first!(__MODULE__))
end
def first!(true) do
T.first!(__MODULE__)
end
@doc """
Return the next key or record in the table, see `mnesia:next`.
If you're calling this function from an instance of the table (a record
in it), it will get you the next record, if you're calling it directly
on the module it will treat the argument as key to start from and
return you the next key.
"""
@spec next(any | t) :: any | t | nil | no_return
def next(%__MODULE__{} = self) do
read(T.next(__MODULE__, self |> key))
end
def next(key) do
T.next(__MODULE__, key)
end
@doc """
Return the next key or record in the table, see `mnesia:dirty_next`.
If you're calling this function from an instance of the table (a record
in it), it will get you the next record, if you're calling it directly
on the module it will treat the argument as key to start from and
return you the next key.
"""
@spec next!(any | t) :: any | t | nil | no_return
def next!(%__MODULE__{} = self) do
read!(T.next!(__MODULE__, self |> key))
end
def next!(key) do
T.next!(__MODULE__, key)
end
@doc """
Return the previous key or record in the table, see `mnesia:prev`.
If you're calling this function from an instance of the table (a record
in it), it will get you the previous record, if you're calling it
directly on the module it will treat the argument as key to start from
and return you the previous key.
"""
@spec prev(any | t) :: any | t | nil | no_return
def prev(%__MODULE__{} = self) do
read(T.prev(__MODULE__, self |> key))
end
def prev(key) do
T.prev(__MODULE__, key)
end
@doc """
Return the previous key or record in the table, see `mnesia:dirty_prev`.
If you're calling this function from an instance of the table (a record
in it), it will get you the previous record, if you're calling it
directly on the module it will treat the argument as key to start from
and return you the previous key.
"""
@spec prev!(any | t) :: any | t | nil | no_return
def prev!(%__MODULE__{} = self) do
read!(T.prev!(__MODULE__, self |> key))
end
def prev!(key) do
T.prev!(__MODULE__, key)
end
@doc """
Return the last key or record in the table, see `mnesia:last`.
By default it returns the record, if you want only the key pass true as
first parameter.
If the table is a bag, it will return a list of records.
"""
@spec last :: t | nil | no_return
@spec last(boolean) :: any | t | nil | no_return
@spec last(boolean, atom) :: any | t | nil | no_return
def last(key \\ false, lock \\ :read)
def last(true, lock) do
T.last(__MODULE__)
end
def last(false, lock) do
read(T.last(__MODULE__), lock)
end
@doc """
Return the last key or record in the table, see `mnesia:dirty_last`.
By default it returns the record, if you want only the key pass true as
first parameter.
If the table is a bag, it will return a list of records.
"""
@spec last! :: any | t | nil | no_return
@spec last!(boolean) :: any | t | nil | no_return
def last!(key \\ false)
def last!(false) do
read!(T.last!(__MODULE__))
end
def last!(true) do
T.last!(__MODULE__)
end
@doc """
Select records in the table using a match_spec, see `mnesia:select`.
"""
@spec select(any) :: T.Selection.t | nil | no_return
def select(spec) do
__MODULE__.telemetry_track(__MODULE__, :select)
T.select(__MODULE__, spec)
end
@doc """
Select records in the given table using a match_spec passing a limit or a
lock kind, see `mnesia:select`.
"""
@spec select(integer | :read | :write, any) :: T.Selection.t | nil | no_return
def select(lock_or_limit, spec) do
__MODULE__.telemetry_track(__MODULE__, :select)
T.select(__MODULE__, lock_or_limit, spec)
end
@doc """
Select records in the given table using a match_spec passing a limit and a
lock kind, see `mnesia:select`.
"""
@spec select(integer | :read | :write, integer | :read | :write, integer) :: T.Selection.t | nil | no_return
def select(lock_or_limit, limit_or_lock, spec) do
__MODULE__.telemetry_track(__MODULE__, :select)
T.select(__MODULE__, lock_or_limit, limit_or_lock, spec)
end
@doc """
Select records in the table using a match_spec, see
`mnesia:dirty_select`.
"""
@spec select!(any) :: Selection.t | nil | no_return
def select!(spec) do
__MODULE__.telemetry_track(__MODULE__, :select!)
T.select!(__MODULE__, spec)
end
@doc """
Select records in the table using an Exquisite query, see
`Exquisite.match/2` and `mnesia:select`.
## Options
* `limit` - sets the count of elements to select in every continuation
* `lock` - sets the kind of lock to use
* `select` - Exquisite selector spec
* `qualified` - whether to set a name for the record or not
"""
defmacro where(spec, options \\ []) do
options = Keyword.put(options, :where, spec)
lock = options[:lock]
limit = options[:limit]
cond do
lock || limit ->
quote do: S.coerce(
T.select(unquote(__MODULE__),
unquote(lock || limit),
unquote(D.where(__MODULE__, @attributes, options))),
unquote(__MODULE__))
lock && limit ->
quote do: S.coerce(
T.select(unquote(__MODULE__),
unquote(lock),
unquote(limit),
unquote(D.where(__MODULE__, @attributes, options))),
unquote(__MODULE__))
true ->
quote do: S.coerce(
T.select(unquote(__MODULE__),
unquote(D.where(__MODULE__, @attributes, options))),
unquote(__MODULE__))
end
end
@doc """
Select records in the table using an Exquisite query, see
`Exquisite.match/2` and `mnesia:dirty_select`.
## Options
* `select` - Exquisite selector spec
* `qualified` - whether to set a name for the record or not
"""
defmacro where!(spec, options \\ []) do
options = Keyword.put(options, :where, spec)
quote do
S.coerce(T.select!(unquote(__MODULE__),
unquote(D.where(__MODULE__, @attributes, options))), unquote(__MODULE__))
end
end
@doc """
Select records in the table using simple don't care values, see
`mnesia:match_object`.
"""
@spec match(any) :: [t] | nil | no_return
@spec match(:read | :write, any) :: [t] | nil | no_return
def match(lock \\ :read, pattern) do
__MODULE__.telemetry_track(__MODULE__, :match)
T.match(__MODULE__, lock, D.match(__MODULE__, pattern))
|> S.coerce(__MODULE__)
end
@doc """
Select records in the table using simple don't care values, see
`mnesia:dirty_match_object`.
"""
@spec match!(any) :: [t] | nil | no_return
def match!(pattern) do
__MODULE__.telemetry_track(__MODULE__, :match!)
T.match!(__MODULE__, D.match(__MODULE__, pattern))
|> S.coerce(__MODULE__)
end
@doc """
Fold the whole table from the left, see `mnesia:foldl`.
"""
@spec foldl(any, (t, any -> any)) :: any | no_return
def foldl(acc, fun) do
T.foldl(__MODULE__, acc, fun)
end
@doc """
Fold the whole table from the right, see `mnesia:foldr`.
"""
@spec foldr(any, (t, any -> any)) :: any | no_return
def foldr(acc, fun) do
T.foldr(__MODULE__, acc, fun)
end
@doc """
Return an iterator to use with Enum functions.
"""
@spec stream :: T.Stream.t
@spec stream(:read | :write | :write!) :: T.Stream.t
def stream(lock \\ :read) do
__MODULE__.telemetry_track(__MODULE__, :stream)
T.stream(__MODULE__, lock)
end
@doc """
Return an iterator to use with the Enum functions using dirty
operations to retrieve information.
"""
@spec stream! :: T.Stream.t
def stream! do
__MODULE__.telemetry_track(__MODULE__, :stream!)
T.stream!(__MODULE__)
end
@doc """
Delete the record or the given key from the table, see `mnesia:delete`
and `mnesia:delete_object`.
"""
@spec delete(any | t) :: :ok | no_return
def delete(%__MODULE__{} = self) do
delete(self, :write)
end
def delete(key) do
delete(key, :write)
end
@doc """
Delete the record or the given key from the table with the given lock,
see `mnesia:delete` and `mnesia:delete_object`.
## Locks
* `:write` sets a `:write` lock
* `:write!` sets a `:sticky_write` lock
"""
@spec delete(atom | any, t | atom) :: :ok | no_return
def delete(%__MODULE__{} = self, lock) do
__MODULE__.telemetry_track(__MODULE__, :delete)
:mnesia.delete_object(__MODULE__, coerce(self), case lock do
:write -> :write
:write! -> :sticky_write
end)
end
def delete(key, lock) do
__MODULE__.telemetry_track(__MODULE__, :delete)
:mnesia.delete(__MODULE__, key, case lock do
:write -> :write
:write! -> :sticky_write
end)
end
@doc """
Delete the record or the given key from the table, see
`mnesia:dirty_delete` and `mnesia:dirty_delete_object`.
"""
@spec delete!(t | any) :: :ok | no_return
def delete!(%__MODULE__{} = self) do
__MODULE__.telemetry_track(__MODULE__, :delete!)
:mnesia.dirty_delete_object(__MODULE__, coerce(self))
end
def delete!(key) do
__MODULE__.telemetry_track(__MODULE__, :delete!)
T.delete!(__MODULE__, key)
end
@doc """
Write the record to the table, see `mnesia:write`.
Missing fields tagged as autoincrement will be incremented with the
counter if `nil`.
"""
@spec write(t) :: t | no_return
def write(self, lock \\ :write) do
__MODULE__.telemetry_track(__MODULE__, :write)
self = D.autoincrement(__MODULE__, @database, @autoincrement, self)
self = hook_write(self)
T.write(__MODULE__, coerce(self), lock)
self
end
@doc """
Write the record to the table, see `mnesia:dirty_write`.
Missing fields tagged as autoincrement will be incremented with the
counter if `nil`.
"""
@spec write!(t) :: t | no_return
def write!(self) do
__MODULE__.telemetry_track(__MODULE__, :write!)
self = D.autoincrement(__MODULE__, @database, @autoincrement, self)
self = hook_write!(self)
T.write!(__MODULE__, coerce(self))
self
end
if @options[:extensions] == :enabled do
defoverridable [
read!: 1,
read: 2,
read: 1,
write!: 1,
write: 2,
write: 1,
delete!: 1,
delete: 2,
delete: 1,
clear: 0,
destroy!: 0,
destroy: 0,
delete_index: 1,
add_index: 1,
delete_copy: 1,
move_copy: 2,
add_copy: 1,
add_copy: 2,
copying: 2,
create!: 1,
create!: 0,
create: 1,
create: 0,
wait: 0,
wait: 1,
telemetry_track: 2
]
else
defoverridable [
telemetry_track: 2
]
end
unquote(block)
unless Kernel.Typespec.defines_type?(__MODULE__, {:t, 0}) do
@opaque t :: %__MODULE__{}
end
end
end
end
end