Packages
ecto
0.1.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
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 `Ecto.Repo.conf/0`.
`: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 Ecto.Associations.Assoc
alias Ecto.Query.Query
alias Ecto.Query.QueryExpr
alias Ecto.Query.Util
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_name = repo.__postgres__(:pool_name)
:poolboy.stop(pool_name)
end
@doc false
def all(repo, Query[] = query, opts) do
pg_query = Query[] = query.select |> normalize_select |> query.select
%Postgrex.Result{rows: rows} = query(repo, SQL.select(pg_query), [], opts)
# Transform each row based on select expression
transformed =
Enum.map(rows, fn row ->
values = tuple_to_list(row)
transform_row(pg_query.select.expr, values, pg_query.sources) |> elem(0)
end)
transformed
|> Ecto.Associations.Assoc.run(query)
|> preload(repo, query)
end
@doc false
def insert(repo, entity, opts) do
module = elem(entity, 0)
returning = module.__entity__(:keywords, entity)
|> Enum.filter(fn { _, val } -> val == nil end)
|> Keyword.keys
case query(repo, SQL.insert(entity, returning), [], opts) do
%Postgrex.Result{rows: [values]} ->
Enum.zip(returning, tuple_to_list(values))
_ ->
[]
end
end
@doc false
def update(repo, entity, opts) do
%Postgrex.Result{num_rows: nrows} = query(repo, SQL.update(entity), [], opts)
nrows
end
@doc false
def update_all(repo, query, values, opts) do
%Postgrex.Result{num_rows: nrows} = query(repo, SQL.update_all(query, values), [], opts)
nrows
end
@doc false
def delete(repo, entity, opts) do
%Postgrex.Result{num_rows: nrows} = query(repo, SQL.delete(entity), [], opts)
nrows
end
@doc false
def delete_all(repo, query, opts) do
%Postgrex.Result{num_rows: nrows} = query(repo, SQL.delete_all(query), [], opts)
nrows
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
timeout = opts[:timeout] || @timeout
repo.log({ :query, sql }, fn ->
use_worker(repo, timeout, fn worker ->
Worker.query!(worker, sql, params, timeout)
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])
pool_opts = pool_opts
|> Keyword.update(:size, 5, &binary_to_integer(&1))
|> Keyword.update(:max_overflow, 10, &binary_to_integer(&1))
pool_opts = [
name: { :local, pool_name },
worker_module: Worker ] ++ pool_opts
worker_opts = worker_opts
|> Keyword.put(:decoder, &decoder/4)
|> Keyword.put_new(:port, @default_port)
{ pool_opts, worker_opts }
end
@doc false
def normalize_select(QueryExpr[expr: { :assoc, _, [_, _] } = assoc] = expr) do
normalize_assoc(assoc) |> expr.expr
end
def normalize_select(QueryExpr[expr: _] = expr), do: expr
defp normalize_assoc({ :assoc, _, [_, _] } = assoc) do
{ var, fields } = Assoc.decompose_assoc(assoc)
normalize_assoc(var, fields)
end
defp normalize_assoc(var, fields) do
nested = Enum.map(fields, fn { _field, nested } ->
{ var, fields } = Assoc.decompose_assoc(nested)
normalize_assoc(var, fields)
end)
{ var, nested }
end
## Result set transformation
defp transform_row({ :{}, _, list }, values, sources) do
{ result, values } = transform_row(list, values, sources)
{ list_to_tuple(result), values }
end
defp transform_row({ :&, _, [_] } = var, values, sources) do
entity = Util.find_source(sources, var) |> Util.entity
entity_size = length(entity.__entity__(:field_names))
{ entity_values, values } = Enum.split(values, entity_size)
if Enum.all?(entity_values, &(nil?(&1))) do
{ nil, values }
else
{ entity.__entity__(:allocate, entity_values), values }
end
end
# Skip records
defp transform_row({ first, _ } = tuple, values, sources) when not is_atom(first) do
{ result, values } = transform_row(tuple_to_list(tuple), values, sources)
{ list_to_tuple(result), values }
end
defp transform_row(list, values, sources) when is_list(list) do
{ result, values } = Enum.reduce(list, { [], values }, fn elem, { res, values } ->
{ result, values } = transform_row(elem, values, sources)
{ [result|res], values }
end)
{ Enum.reverse(result), values }
end
defp transform_row(_, values, _entities) do
[value|values] = values
{ value, values }
end
defp preload(results, repo, Query[] = query) do
pos = Util.locate_var(query.select.expr, { :&, [], [0] })
fields = Enum.map(query.preloads, &(&1.expr)) |> Enum.concat
Ecto.Associations.Preloader.run(results, repo, fields, pos)
end
## Postgrex casting
defp decoder(%TypeInfo{sender: "interval"}, :binary, default, param) do
{ mon, day, sec } = default.(param)
Ecto.Interval[year: 0, month: mon, day: day, hour: 0, min: 0, sec: sec]
end
defp decoder(%TypeInfo{sender: sender}, :binary, default, param) when sender in ["timestamp", "timestamptz"] do
{ { year, mon, day }, { hour, min, sec } } = default.(param)
Ecto.DateTime[year: year, month: mon, day: day, hour: hour, min: min, sec: sec]
end
defp decoder(%TypeInfo{sender: "date"}, :binary, default, param) do
{ year, mon, day } = default.(param)
Ecto.Date[year: year, month: mon, day: day]
end
defp decoder(%TypeInfo{sender: sender}, :binary, default, param) when sender in ["time", "timetz"] do
{ hour, min, sec } = default.(param)
Ecto.Time[hour: hour, min: min, sec: sec]
end
defp decoder(_type, _format, default, param) do
default.(param)
end
## Transaction API
@doc false
def transaction(repo, opts, fun) do
timeout = opts[:timout] || @timeout
worker = checkout_worker(repo, timeout)
try do
do_begin(repo, worker, timeout)
value = fun.()
do_commit(repo, worker, timeout)
{ :ok, value }
catch
:throw, { :ecto_rollback, value } ->
do_rollback(repo, worker, timeout)
{ :error, value }
type, term ->
do_rollback(repo, worker, timeout)
:erlang.raise(type, term, System.stacktrace)
after
checkin_worker(repo)
end
end
@doc false
def rollback(_repo, value) do
throw { :ecto_rollback, value }
end
defp use_worker(repo, timeout, fun) do
pool = repo.__postgres__(:pool_name)
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(repo, timeout) do
pool = repo.__postgres__(:pool_name)
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(repo) do
pool = repo.__postgres__(:pool_name)
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, timeout) do
repo.log(:begin, fn ->
Worker.begin!(worker, timeout)
end)
end
defp do_rollback(repo, worker, timeout) do
repo.log(:rollback, fn ->
Worker.rollback!(worker, timeout)
end)
end
defp do_commit(repo, worker, timeout) do
repo.log(:commit, fn ->
Worker.commit!(worker, timeout)
end)
end
## Test transaction API
@doc false
def begin_test_transaction(repo, opts \\ []) do
timeout = opts[:timeout] || @timeout
pool = repo.__postgres__(:pool_name)
:poolboy.transaction(pool, fn worker ->
do_begin(repo, worker, timeout)
end, timeout)
end
@doc false
def rollback_test_transaction(repo, opts \\ []) do
timeout = opts[:timeout] || @timeout
pool = repo.__postgres__(:pool_name)
:poolboy.transaction(pool, fn worker ->
do_rollback(repo, worker, timeout)
end, 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
command =
command <>
~s(psql --quiet -U #{ database[:username] } ) <>
~s(--host #{ database[:hostname] } ) <>
~s(-c "#{ sql_command };" )
System.cmd command
end
## Migration API
@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, []))
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, []))
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