Packages
ecto
0.5.0
3.14.1
3.14.0
3.13.6
3.13.5
3.13.4
3.13.3
3.13.2
3.13.1
3.13.0
3.12.6
3.12.5
3.12.4
3.12.3
3.12.2
3.12.1
3.12.0
3.11.2
3.11.1
3.11.0
3.10.3
3.10.2
3.10.1
3.10.0
3.9.6
3.9.5
3.9.4
3.9.3
3.9.2
3.9.1
3.9.0
3.8.4
3.8.3
3.8.2
3.8.1
3.8.0
3.7.2
3.7.1
3.7.0
3.6.2
3.6.1
3.6.0
3.5.8
3.5.7
3.5.6
3.5.5
3.5.4
3.5.3
3.5.2
3.5.1
3.5.0
3.5.0-rc.1
3.5.0-rc.0
3.4.6
3.4.5
3.4.4
3.4.3
3.4.2
3.4.1
3.4.0
3.3.4
3.3.3
3.3.2
3.3.1
3.3.0
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.1
3.0.0-rc.0
2.2.12
2.2.11
2.2.10
2.2.9
2.2.8
2.2.7
2.2.6
2.2.5
2.2.4
2.2.3
2.2.2
2.2.1
2.2.0
2.2.0-rc.1
2.2.0-rc.0
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.1.0-rc.5
2.1.0-rc.4
2.1.0-rc.3
2.1.0-rc.2
2.1.0-rc.1
2.1.0-rc.0
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
2.0.0-beta.2
2.0.0-beta.1
2.0.0-beta.0
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.16.0
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.1
0.13.0
0.12.1
0.12.0
0.12.0-rc
0.11.3
0.11.2
0.11.1
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.0
0.5.1
0.5.0
0.4.0
0.3.0
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
A toolkit for data mapping and language integrated query for Elixir
Current section
Files
Jump to
Current section
Files
lib/ecto/adapters/postgres.ex
if Code.ensure_loaded?(Postgrex.Connection) do
defmodule Ecto.Adapters.Postgres do
@moduledoc """
This is the adapter module for PostgreSQL. It handles and pools the
connections to the postgres database with poolboy.
## Options
The options should be given via the repository configuration:
* `:hostname` - Server hostname
* `:port` - Server port (default: 5432)
* `:username` - Username
* `:password` - User password
* `:size` - The number of connections to keep in the pool
* `:max_overflow` - The maximum overflow of connections (see poolboy docs)
* `:parameters` - Keyword list of connection parameters
* `:ssl` - Set to true if ssl should be used (default: false)
* `:ssl_opts` - A list of ssl options, see ssl docs
* `:lazy` - If false all connections will be started immediately on Repo startup (default: true)
"""
@behaviour Ecto.Adapter
@behaviour Ecto.Adapter.Migrations
@behaviour Ecto.Adapter.Storage
@behaviour Ecto.Adapter.Transactions
@behaviour Ecto.Adapter.TestTransactions
@default_port 5432
@timeout 5000
alias Ecto.Adapters.Postgres.SQL
alias Ecto.Adapters.Postgres.Worker
alias Postgrex.TypeInfo
## Adapter API
@doc false
defmacro __using__(_opts) do
quote do
def __postgres__(:pool_name) do
__MODULE__.Pool
end
end
end
@doc false
def start_link(repo, opts) do
{pool_opts, worker_opts} = prepare_start(repo, opts)
:poolboy.start_link(pool_opts, worker_opts)
end
@doc false
def stop(repo) do
pool = repo_pool(repo)
:poolboy.stop(pool)
end
@doc false
def all(repo, query, params, opts) do
sql = SQL.all(query)
fields = process_fields(query.select.fields, query.sources)
%Postgrex.Result{rows: rows} = query(repo, sql, Map.values(params), opts)
Enum.map(rows, &process_row(&1, fields))
end
@doc false
def update_all(repo, query, values, params, opts) do
sql = SQL.update_all(query, values)
%Postgrex.Result{num_rows: nrows} = query(repo, sql, Map.values(params), opts)
nrows
end
@doc false
def delete_all(repo, query, params, opts) do
sql = SQL.delete_all(query)
%Postgrex.Result{num_rows: nrows} = query(repo, sql, Map.values(params), opts)
nrows
end
@doc false
def insert(repo, source, params, returning, opts) do
{fields, values} = :lists.unzip(params)
sql = SQL.insert(source, fields, returning)
case query(repo, sql, values, opts) do
%Postgrex.Result{num_rows: 1, rows: nil} ->
{:ok, {}}
%Postgrex.Result{num_rows: 1, rows: [values]} ->
{:ok, values}
end
end
@doc false
def update(repo, source, filter, fields, returning, opts) do
{filter, values1} = :lists.unzip(filter)
{fields, values2} = :lists.unzip(fields)
sql = SQL.update(source, filter, fields, returning)
case query(repo, sql, values1 ++ values2, opts) do
%Postgrex.Result{rows: [values]} ->
{:ok, values}
%Postgrex.Result{rows: []} ->
{:error, :stale}
end
end
@doc false
def delete(repo, source, filter, opts) do
{filter, values} = :lists.unzip(filter)
sql = SQL.delete(source, filter)
case query(repo, sql, values, opts) do
%Postgrex.Result{num_rows: 1} ->
:ok
%Postgrex.Result{num_rows: 0} ->
{:error, :stale}
end
end
@doc """
Run custom SQL query on given repo.
## Options
`:timeout` - The time in milliseconds to wait for the call to finish,
`:infinity` will wait indefinitely (default: 5000);
## Examples
iex> Postgres.query(MyRepo, "SELECT $1 + $2", [40, 2])
%Postgrex.Result{command: :select, columns: ["?column?"], rows: [{42}], num_rows: 1}
"""
def query(repo, sql, params, opts \\ []) do
pool = repo_pool(repo)
opts = Keyword.put_new(opts, :timeout, @timeout)
repo.log({:query, sql}, fn ->
use_worker(pool, opts[:timeout], fn worker ->
Worker.query!(worker, sql, params, opts)
end)
end)
end
defp prepare_start(repo, opts) do
pool_name = repo.__postgres__(:pool_name)
{pool_opts, worker_opts} = Dict.split(opts, [:size, :max_overflow])
# TODO: Remove those to integers calls
pool_opts = pool_opts
|> Keyword.update(:size, 5, &to_integer(&1))
|> Keyword.update(:max_overflow, 10, &to_integer(&1))
pool_opts = [
name: {:local, pool_name},
worker_module: Worker ] ++ pool_opts
worker_opts = worker_opts
|> Keyword.put(:formatter, &formatter/1)
|> Keyword.put(:decoder, &decoder/4)
|> Keyword.put(:encoder, &encoder/3)
|> Keyword.put_new(:port, @default_port)
{pool_opts, worker_opts}
end
defp to_integer(int) when is_integer(int), do: int
defp to_integer(bin) when is_binary(bin), do: String.to_integer(bin)
defp repo_pool(repo) do
pid = repo.__postgres__(:pool_name) |> Process.whereis
if is_nil(pid) or not Process.alive?(pid) do
raise ArgumentError, "repo #{inspect repo} is not started"
end
pid
end
## Rows processing
defp process_fields(fields, sources) do
Enum.map fields, fn
{:&, _, [idx]} ->
{_source, model} = elem(sources, idx)
{length(model.__schema__(:fields)), model}
_ ->
{1, nil}
end
end
defp process_row(row, fields) do
Enum.map_reduce(fields, 0, fn
{1, nil}, idx ->
{elem(row, idx), idx + 1}
{count, model}, idx ->
if all_nil?(row, idx, count) do
{nil, idx + count}
else
{model.__schema__(:load, idx, row), idx + count}
end
end) |> elem(0)
end
defp all_nil?(_tuple, _idx, 0), do: true
defp all_nil?(tuple, idx, _count) when elem(tuple, idx) != nil, do: false
defp all_nil?(tuple, idx, count), do: all_nil?(tuple, idx + 1, count - 1)
## Postgrex casting
defp formatter(%TypeInfo{sender: "uuid"}), do: :binary
defp formatter(_), do: nil
defp decoder(%TypeInfo{sender: "uuid"}, :binary, _default, param) do
param
end
defp decoder(_type, _format, default, param) do
default.(param)
end
defp encoder(%TypeInfo{sender: "uuid"}, _default, uuid) do
uuid
end
defp encoder(_type, default, param) do
default.(param)
end
## Transaction API
@doc false
def transaction(repo, opts, fun) do
pool = repo_pool(repo)
opts = Keyword.put_new(opts, :timeout, @timeout)
worker = checkout_worker(pool, opts[:timeout])
try do
do_begin(repo, worker, opts)
value = fun.()
do_commit(repo, worker, opts)
{:ok, value}
catch
:throw, {:ecto_rollback, value} ->
do_rollback(repo, worker, opts)
{:error, value}
type, term ->
stacktrace = System.stacktrace
do_rollback(repo, worker, opts)
:erlang.raise(type, term, stacktrace)
after
checkin_worker(pool)
end
end
@doc false
def rollback(_repo, value) do
throw {:ecto_rollback, value}
end
defp use_worker(pool, timeout, fun) do
key = {:ecto_transaction_pid, pool}
if value = Process.get(key) do
in_transaction = true
worker = elem(value, 0)
else
worker = :poolboy.checkout(pool, true, timeout)
end
try do
fun.(worker)
after
if !in_transaction do
:poolboy.checkin(pool, worker)
end
end
end
defp checkout_worker(pool, timeout) do
key = {:ecto_transaction_pid, pool}
case Process.get(key) do
{worker, counter} ->
Process.put(key, {worker, counter + 1})
worker
nil ->
worker = :poolboy.checkout(pool, true, timeout)
Worker.monitor_me(worker)
Process.put(key, {worker, 1})
worker
end
end
defp checkin_worker(pool) do
key = {:ecto_transaction_pid, pool}
case Process.get(key) do
{worker, 1} ->
Worker.demonitor_me(worker)
:poolboy.checkin(pool, worker)
Process.delete(key)
{worker, counter} ->
Process.put(key, {worker, counter - 1})
end
:ok
end
defp do_begin(repo, worker, opts) do
repo.log(:begin, fn ->
Worker.begin!(worker, opts)
end)
end
defp do_rollback(repo, worker, opts) do
repo.log(:rollback, fn ->
Worker.rollback!(worker, opts)
end)
end
defp do_commit(repo, worker, opts) do
repo.log(:commit, fn ->
Worker.commit!(worker, opts)
end)
end
## Test transaction API
@doc false
def begin_test_transaction(repo, opts \\ []) do
pool = repo_pool(repo)
opts = Keyword.put_new(opts, :timeout, @timeout)
:poolboy.transaction(pool, fn worker ->
do_begin(repo, worker, opts)
end, opts[:timeout])
end
@doc false
def rollback_test_transaction(repo, opts \\ []) do
pool = repo_pool(repo)
opts = Keyword.put_new(opts, :timeout, @timeout)
:poolboy.transaction(pool, fn worker ->
do_rollback(repo, worker, opts)
end, opts[:timeout])
end
## Storage API
@doc false
def storage_up(opts) do
# TODO: allow the user to specify those options either in the Repo or on command line
database_options = ~s(TEMPLATE=template0 ENCODING='UTF8' LC_COLLATE='en_US.UTF-8' LC_CTYPE='en_US.UTF-8')
output = run_with_psql opts, "CREATE DATABASE #{opts[:database]} " <> database_options
cond do
String.length(output) == 0 -> :ok
String.contains?(output, "already exists") -> {:error, :already_up}
true -> {:error, output}
end
end
@doc false
def storage_down(opts) do
output = run_with_psql(opts, "DROP DATABASE #{opts[:database]}")
cond do
String.length(output) == 0 -> :ok
String.contains?(output, "does not exist") -> {:error, :already_down}
true -> {:error, output}
end
end
defp run_with_psql(database, sql_command) do
command = ""
if password = database[:password] do
command = ~s(PGPASSWORD=#{password} )
end
if username = database[:username] do
command = ~s(PGUSER=#{username} ) <> command
end
if port = database[:port] do
command = ~s(PGPORT=#{port} ) <> command
end
command =
command <>
~s(psql --quiet ) <>
~s(template1 ) <>
~s(--host #{database[:hostname]} ) <>
~s(-c "#{sql_command};" )
String.to_char_list(command)
|> :os.cmd
|> List.to_string
end
## Migration API
@migrate_opts [timeout: :infinity]
@doc false
def migrate_up(repo, version, commands) do
case check_migration_version(repo, version) do
%Postgrex.Result{num_rows: 0} ->
transaction(repo, [], fn ->
Enum.each(commands, &query(repo, &1, [], @migrate_opts))
insert_migration_version(repo, version)
end)
:ok
_ ->
:already_up
end
end
@doc false
def migrate_down(repo, version, commands) do
case check_migration_version(repo, version) do
%Postgrex.Result{num_rows: 0} ->
:missing_up
_ ->
transaction(repo, [], fn ->
Enum.each(commands, &query(repo, &1, [], @migrate_opts))
delete_migration_version(repo, version)
end)
:ok
end
end
@doc false
def migrated_versions(repo) do
create_migrations_table(repo)
%Postgrex.Result{rows: rows} = query(repo, "SELECT version FROM schema_migrations", [])
Enum.map(rows, &elem(&1, 0))
end
defp create_migrations_table(repo) do
query(repo, "CREATE TABLE IF NOT EXISTS schema_migrations (id serial primary key, version bigint)", [])
end
defp check_migration_version(repo, version) do
create_migrations_table(repo)
query(repo, "SELECT version FROM schema_migrations WHERE version = #{version}", [])
end
defp insert_migration_version(repo, version) do
query(repo, "INSERT INTO schema_migrations(version) VALUES (#{version})", [])
end
defp delete_migration_version(repo, version) do
query(repo, "DELETE FROM schema_migrations WHERE version = #{version}", [])
end
end
end