Current section

Files

Jump to
moebius lib moebius database.ex
Raw

lib/moebius/database.ex

defmodule Moebius.Database do
defmacro __using__(_opts) do
quote location: :keep do
@name __MODULE__
alias __MODULE__
def start_link(opts) do
opts
|> prepare_extensions
|> Moebius.Database.start_link()
end
def child_spec([]), do: child_spec(Moebius.get_connection())
def child_spec(arg) do
%{
id: @name,
start: {@name, :start_link, [arg]}
}
end
def prepare_extensions(opts) do
# make sure we convert a tuple list, which will happen if our db is a worker
opts =
cond do
Keyword.keyword?(opts) -> opts
true -> Keyword.new([opts])
end
opts
|> Keyword.put_new(:name, @name)
|> Keyword.put_new(:types, PostgresTypes)
end
def run(sql) when is_binary(sql), do: run(sql, [])
def run(sql, params) when is_binary(sql) and is_list(params),
do: %Moebius.QueryCommand{sql: sql, params: params} |> run
def run(sql, %DBConnection{} = conn) when is_binary(sql),
do: %Moebius.QueryCommand{sql: sql, params: []} |> run(conn)
def run(sql, %DBConnection{} = conn, params) when is_binary(sql),
do: %Moebius.QueryCommand{sql: sql, params: params} |> run(conn)
def run(%Moebius.QueryCommand{type: :insert} = cmd),
do: execute(cmd) |> Moebius.Transformer.to_single()
def run(%Moebius.QueryCommand{type: :update} = cmd),
do: execute(cmd) |> Moebius.Transformer.to_single()
def run(%Moebius.QueryCommand{type: :delete} = cmd),
do: execute(cmd) |> Moebius.Transformer.to_single()
def run(%Moebius.QueryCommand{type: :count} = cmd),
do: execute(cmd) |> Moebius.Transformer.to_single()
def run(%Moebius.QueryCommand{} = cmd), do: execute(cmd) |> Moebius.Transformer.to_list()
def run(%Moebius.QueryCommand{type: :insert} = cmd, %DBConnection{} = conn),
do: execute(cmd, conn) |> Moebius.Transformer.to_single()
def run(%Moebius.QueryCommand{type: :update} = cmd, %DBConnection{} = conn),
do: execute(cmd, conn) |> Moebius.Transformer.to_single()
def run(%Moebius.QueryCommand{type: :delete} = cmd, %DBConnection{} = conn),
do: execute(cmd, conn) |> Moebius.Transformer.to_single()
def run(%Moebius.QueryCommand{} = cmd, %DBConnection{} = conn),
do: execute(cmd, conn) |> Moebius.Transformer.to_list()
defdelegate all(table), to: __MODULE__, as: :run
def run_batch(%Moebius.CommandBatch{} = batch) do
batch.commands
|> Enum.map(fn cmd -> execute(cmd) end)
end
def transact_batch(%Moebius.CommandBatch{} = batch) do
transaction(fn tx ->
batch.commands
|> Enum.map(fn cmd -> execute(cmd, tx) end)
end)
end
def run(%Moebius.DocumentCommand{sql: nil} = cmd) do
res =
%{cmd | conn: @name}
|> Moebius.DocumentQuery.select()
|> Moebius.Database.execute()
|> Moebius.Transformer.from_json()
end
def run(%Moebius.DocumentCommand{} = cmd) do
execute(cmd)
|> Moebius.Transformer.from_json()
end
def first(%Moebius.DocumentCommand{} = cmd) do
Moebius.DocumentQuery.select(cmd)
|> execute
|> Moebius.Transformer.from_json(:single)
end
def first(%Moebius.QueryCommand{sql: nil} = cmd) do
Moebius.Query.select(cmd)
|> execute
|> Moebius.Transformer.to_single()
end
def first(%Moebius.QueryCommand{} = cmd) do
cmd
|> execute
|> Moebius.Transformer.to_single()
end
defdelegate one(table), to: __MODULE__, as: :first
def find(%Moebius.QueryCommand{} = cmd, id) do
sql = "select * from #{cmd.table_name} where id=#{id}"
%{cmd | sql: sql}
|> execute
|> Moebius.Transformer.to_single()
end
def find(%Moebius.DocumentCommand{} = cmd, id) do
sql =
"select id, #{cmd.json_field}::text, created_at, updated_at from #{cmd.table_name} where id=$1"
%{cmd | sql: sql, params: [id]}
|> execute
|> Moebius.Transformer.from_json(:single)
end
def transaction(fun) do
try do
{:ok, conn} = Postgrex.transaction(@name, fun, Moebius.get_connection())
conn
catch
e, %{message: message} -> {:error, message}
e, {:error, message} -> {:error, message}
end
end
def save(%Moebius.DocumentCommand{} = cmd, doc) when is_list(doc),
do: save(cmd, Enum.into(doc, %{}))
def save(%Moebius.DocumentCommand{} = cmd, doc) when is_struct(doc) do
case save(%Moebius.DocumentCommand{} = cmd, Map.from_struct(doc)) do
{:error, err} -> {:error, err}
{:ok, res} -> {:ok, Map.put_new(res, :__struct__, doc.__struct__)}
end
end
def save(%Moebius.DocumentCommand{} = cmd, doc) when is_map(doc) do
res =
%{cmd | conn: @name}
|> Moebius.DocumentQuery.decide_command(doc)
|> Moebius.Database.execute()
|> Moebius.Transformer.from_json(:single)
|> handle_save_result(cmd, doc)
|> check_struct(doc)
end
def save(%Moebius.DocumentCommand{} = cmd, doc, %DBConnection{} = conn) when is_map(doc) do
%{cmd | conn: @name}
|> Moebius.DocumentQuery.decide_command(doc)
|> Moebius.Database.execute(conn)
|> Moebius.Transformer.from_json(:single)
|> handle_save_result(cmd, doc)
|> check_struct(doc)
end
def create_document_table(name) when is_atom(name) do
case Moebius.DocumentQuery.db(name) |> create_document_table(nil) do
{:error, err} -> {:error, err}
%Moebius.DocumentCommand{} = cmd -> {:ok, "Table created"}
end
end
def create_document_table(%Moebius.DocumentCommand{} = cmd, _) do
sql = """
create table #{cmd.table_name}(
id serial primary key not null,
body jsonb not null,
search tsvector,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
"""
%Moebius.QueryCommand{conn: @name, sql: sql} |> execute
%Moebius.QueryCommand{
conn: @name,
sql: "create index idx_#{cmd.table_name}_search on #{cmd.table_name} using GIN(search);"
}
|> execute
%Moebius.QueryCommand{
conn: @name,
sql:
"create index idx_#{cmd.table_name} on #{cmd.table_name} using GIN(body jsonb_path_ops);"
}
|> execute
cmd
end
defp check_struct({:ok, query_result} = res, original) do
res =
cond do
Map.has_key?(original, :__struct__) ->
Map.put_new(query_result, :__struct__, original.__struct__)
true ->
query_result
end
{:ok, res}
end
defp handle_save_result({:ok, save_result} = res, cmd, doc) when is_map(save_result),
do: update_search(res, cmd) && res
defp handle_save_result({:error, err}, cmd, doc) do
table = cmd.table_name
cond do
String.contains?(err, "column") ->
raise err
String.contains?(err, "does not exist") ->
create_document_table(cmd, doc) |> save(Map.delete(doc, :id))
true ->
{:error, err}
end
end
defp execute(%Moebius.DocumentCommand{sql: nil} = cmd) do
%{cmd | conn: @name}
|> Moebius.DocumentQuery.select()
|> Moebius.Database.execute()
end
defp execute(%Moebius.DocumentCommand{} = cmd) do
res =
%{cmd | conn: @name}
|> Moebius.Database.execute()
case res do
{:error, err} -> create_document_table(cmd, nil) && execute(cmd)
res -> res
end
end
defp execute(%Moebius.QueryCommand{sql: nil} = cmd) do
%{cmd | conn: @name}
|> Moebius.Query.select()
|> Moebius.Database.execute()
end
defp execute(%Moebius.QueryCommand{} = cmd) do
%{cmd | conn: @name}
|> Moebius.Database.execute()
end
defp execute(%Moebius.QueryCommand{} = cmd, %DBConnection{} = conn),
do: Moebius.Database.execute(cmd, conn)
defp update_search({:error, err}, cmd), do: {:error, err}
defp update_search([], _), do: []
defp update_search({:ok, query_result} = res, cmd) do
if length(cmd.search_fields) > 0 do
terms = Enum.map_join(cmd.search_fields, ", ' ', ", &"body -> '#{Atom.to_string(&1)}'")
sql =
"update #{cmd.table_name} set search = to_tsvector(concat(#{terms})) where id=#{query_result.id}"
%Moebius.QueryCommand{sql: sql}
|> execute
end
res
end
end
end
def start_link(opts) do
Postgrex.start_link(opts)
end
def execute(cmd) do
case Postgrex.query(cmd.conn, cmd.sql, cmd.params, Moebius.pool_opts()) do
{:ok, result} ->
{:ok, result}
{:error, err} ->
{:error, err.postgres.message}
end
end
@doc """
Executes a command for a given transaction specified with `pid`. If the execution fails,
it will be caught in `Query.transaction/1` and reported back using `{:error, err}`.
"""
def execute(cmd, %DBConnection{} = conn) do
case Postgrex.query(conn, cmd.sql, cmd.params, Moebius.pool_opts()) do
{:ok, result} ->
{:ok, result}
{:error, err} ->
Postgrex.query(conn, "ROLLBACK", []) && raise err.postgres.message
end
end
end