Packages

A toolkit for data mapping and language integrated query for Elixir

Current section

Files

Jump to
ecto_tc lib ecto repo queryable.ex
Raw

lib/ecto/repo/queryable.ex

defmodule Ecto.Repo.Queryable do
@moduledoc false
alias Ecto.Queryable
alias Ecto.Query
alias Ecto.Query.Planner
alias Ecto.Query.SelectExpr
import Ecto.Query.Planner, only: [attach_prefix: 2]
require Ecto.Query
def all(name, queryable, opts) when is_list(opts) do
query =
queryable
|> Ecto.Queryable.to_query()
|> Ecto.Query.Planner.ensure_select(true)
execute(:all, name, query, opts) |> elem(1)
end
def stream(name, queryable, opts) when is_list(opts) do
query =
queryable
|> Ecto.Queryable.to_query()
|> Ecto.Query.Planner.ensure_select(true)
{adapter, %{cache: cache, repo: repo} = adapter_meta} = Ecto.Repo.Registry.lookup(name)
{query, opts} = repo.prepare_query(:stream, query, opts)
query = attach_prefix(query, opts)
{query_meta, prepared, params} = Planner.query(query, :all, cache, adapter, 0)
case query_meta do
%{select: nil} ->
adapter_meta
|> adapter.stream(query_meta, prepared, params, opts)
|> Stream.flat_map(fn {_, nil} -> [] end)
%{select: select, preloads: preloads} ->
%{
assocs: assocs,
preprocess: preprocess,
postprocess: postprocess,
take: take,
from: from
} = select
if preloads != [] or assocs != [] do
raise Ecto.QueryError, query: query, message: "preloads are not supported on streams"
end
preprocessor = preprocessor(from, preprocess, adapter)
stream = adapter.stream(adapter_meta, query_meta, prepared, params, opts)
postprocessor = postprocessor(from, postprocess, take, adapter)
stream
|> Stream.flat_map(fn {_, rows} -> rows end)
|> Stream.map(preprocessor)
|> Stream.map(postprocessor)
end
end
def get(name, queryable, id, opts) do
one(name, query_for_get(queryable, id), opts)
end
def get!(name, queryable, id, opts) do
one!(name, query_for_get(queryable, id), opts)
end
def get_by(name, queryable, clauses, opts) do
one(name, query_for_get_by(queryable, clauses), opts)
end
def get_by!(name, queryable, clauses, opts) do
one!(name, query_for_get_by(queryable, clauses), opts)
end
def reload(name, [head | _] = structs, opts) when is_list(structs) do
results = all(name, query_for_reload(structs), opts)
[pk] = head.__struct__.__schema__(:primary_key)
for struct <- structs do
struct_pk = Map.fetch!(struct, pk)
Enum.find(results, &Map.fetch!(&1, pk) == struct_pk)
end
end
def reload(name, struct, opts) do
one(name, query_for_reload([struct]), opts)
end
def reload!(name, [head | _] = structs, opts) when is_list(structs) do
query = query_for_reload(structs)
results = all(name, query, opts)
[pk] = head.__struct__.__schema__(:primary_key)
for struct <- structs do
struct_pk = Map.fetch!(struct, pk)
Enum.find(results, &Map.fetch!(&1, pk) == struct_pk) || raise "could not reload #{inspect(struct)}, maybe it doesn't exist or was deleted"
end
end
def reload!(name, struct, opts) do
query = query_for_reload([struct])
one!(name, query, opts)
end
def aggregate(name, queryable, aggregate, opts) do
one!(name, query_for_aggregate(queryable, aggregate), opts)
end
def aggregate(name, queryable, aggregate, field, opts) do
one!(name, query_for_aggregate(queryable, aggregate, field), opts)
end
def exists?(name, queryable, opts) do
queryable =
Query.exclude(queryable, :select)
|> Query.exclude(:preload)
|> Query.exclude(:order_by)
|> Query.exclude(:distinct)
|> Query.select(1)
|> Query.limit(1)
|> rewrite_combinations()
case all(name, queryable, opts) do
[1] -> true
[] -> false
end
end
defp rewrite_combinations(%{combinations: []} = query), do: query
defp rewrite_combinations(%{combinations: combinations} = query) do
combinations = Enum.map(combinations, fn {type, query} ->
{type, query |> Query.exclude(:select) |> Query.select(1)}
end)
%{query | combinations: combinations}
end
def one(name, queryable, opts) do
case all(name, queryable, opts) do
[one] -> one
[] -> nil
other -> raise Ecto.MultipleResultsError, queryable: queryable, count: length(other)
end
end
def one!(name, queryable, opts) do
case all(name, queryable, opts) do
[one] -> one
[] -> raise Ecto.NoResultsError, queryable: queryable
other -> raise Ecto.MultipleResultsError, queryable: queryable, count: length(other)
end
end
def update_all(name, queryable, [], opts) when is_list(opts) do
update_all(name, queryable, opts)
end
def update_all(name, queryable, updates, opts) when is_list(opts) do
query = Query.from(queryable, update: ^updates)
update_all(name, query, opts)
end
defp update_all(name, queryable, opts) do
query = Ecto.Queryable.to_query(queryable)
execute(:update_all, name, query, opts)
end
def delete_all(name, queryable, opts) when is_list(opts) do
query = Ecto.Queryable.to_query(queryable)
execute(:delete_all, name, query, opts)
end
@doc """
Load structs from query.
"""
def struct_load!([{field, type} | types], [value | values], acc, all_nil?, struct, adapter) do
all_nil? = all_nil? and value == nil
value = load!(type, value, field, struct, adapter)
struct_load!(types, values, [{field, value} | acc], all_nil?, struct, adapter)
end
def struct_load!([], values, _acc, true, _struct, _adapter) do
{nil, values}
end
def struct_load!([], values, acc, false, struct, _adapter) do
{Map.merge(struct, Map.new(acc)), values}
end
## Helpers
defp execute(operation, name, query, opts) when is_list(opts) do
{adapter, %{cache: cache, repo: repo} = adapter_meta} = Ecto.Repo.Registry.lookup(name)
{query, opts} = repo.prepare_query(operation, query, opts)
query = attach_prefix(query, opts)
{query_meta, prepared, params} = Planner.query(query, operation, cache, adapter, 0)
case query_meta do
%{select: nil} ->
adapter.execute(adapter_meta, query_meta, prepared, params, opts)
%{select: select, sources: sources, preloads: preloads} ->
%{
preprocess: preprocess,
postprocess: postprocess,
take: take,
assocs: assocs,
from: from
} = select
preprocessor = preprocessor(from, preprocess, adapter)
{count, rows} = adapter.execute(adapter_meta, query_meta, prepared, params, opts)
postprocessor = postprocessor(from, postprocess, take, adapter)
{count,
rows
|> Ecto.Repo.Assoc.query(assocs, sources, preprocessor)
|> Ecto.Repo.Preloader.query(name, preloads, take, postprocessor, opts)}
end
end
defp preprocessor({_, {:source, {source, schema}, prefix, types}}, preprocess, adapter) do
struct = Ecto.Schema.Loader.load_struct(schema, prefix, source)
fn row ->
{entry, rest} = struct_load!(types, row, [], false, struct, adapter)
preprocess(rest, preprocess, entry, adapter)
end
end
defp preprocessor({_, from}, preprocess, adapter) do
fn row ->
{entry, rest} = process(row, from, nil, adapter)
preprocess(rest, preprocess, entry, adapter)
end
end
defp preprocessor(:none, preprocess, adapter) do
fn row ->
preprocess(row, preprocess, nil, adapter)
end
end
defp preprocess(row, [], _from, _adapter) do
row
end
defp preprocess(row, [source | sources], from, adapter) do
{entry, rest} = process(row, source, from, adapter)
[entry | preprocess(rest, sources, from, adapter)]
end
defp postprocessor({:any, _}, postprocess, _take, adapter) do
fn [from | row] ->
row |> process(postprocess, from, adapter) |> elem(0)
end
end
defp postprocessor({:map, _}, postprocess, take, adapter) do
fn [from | row] ->
row |> process(postprocess, to_map(from, take), adapter) |> elem(0)
end
end
defp postprocessor(:none, postprocess, _take, adapter) do
fn row -> row |> process(postprocess, nil, adapter) |> elem(0) end
end
defp process(row, {:source, :from}, from, _adapter) do
{from, row}
end
defp process(row, {:source, {source, schema}, prefix, types}, _from, adapter) do
struct = Ecto.Schema.Loader.load_struct(schema, prefix, source)
struct_load!(types, row, [], true, struct, adapter)
end
defp process(row, {:merge, left, right}, from, adapter) do
{left, row} = process(row, left, from, adapter)
{right, row} = process(row, right, from, adapter)
data =
case {left, right} do
{%{__struct__: s}, %{__struct__: s}} ->
Map.merge(left, right)
{%{__struct__: left_struct}, %{__struct__: right_struct}} ->
raise ArgumentError,
"cannot merge structs of different types, " <>
"got: #{inspect(left_struct)} and #{inspect(right_struct)}"
{%{__struct__: name}, %{}} ->
for {key, _} <- right, not Map.has_key?(left, key) do
raise ArgumentError, "struct #{inspect(name)} does not have the key #{inspect(key)}"
end
Map.merge(left, right)
{%{}, %{}} ->
Map.merge(left, right)
{%{}, nil} ->
left
{_, %{}} ->
raise ArgumentError,
"cannot merge because the left side is not a map, got: #{inspect(left)}"
{%{}, _} ->
raise ArgumentError,
"cannot merge because the right side is not a map, got: #{inspect(right)}"
end
{data, row}
end
defp process(row, {:struct, struct, data, args}, from, adapter) do
case process(row, data, from, adapter) do
{%{__struct__: ^struct} = data, row} ->
process_update(data, args, row, from, adapter)
{data, _row} ->
raise BadStructError, struct: struct, term: data
end
end
defp process(row, {:struct, struct, args}, from, adapter) do
{fields, row} = process_kv(args, row, from, adapter)
case Map.merge(struct.__struct__(), Map.new(fields)) do
%{__meta__: %Ecto.Schema.Metadata{state: state} = metadata} = struct
when state != :loaded ->
{Map.replace!(struct, :__meta__, %{metadata | state: :loaded}), row}
map ->
{map, row}
end
end
defp process(row, {:map, data, args}, from, adapter) do
{data, row} = process(row, data, from, adapter)
process_update(data, args, row, from, adapter)
end
defp process(row, {:map, args}, from, adapter) do
{args, row} = process_kv(args, row, from, adapter)
{Map.new(args), row}
end
defp process(row, {:list, args}, from, adapter) do
process_args(args, row, from, adapter)
end
defp process(row, {:tuple, args}, from, adapter) do
{args, row} = process_args(args, row, from, adapter)
{List.to_tuple(args), row}
end
defp process([value | row], {:value, :any}, _from, _adapter) do
{value, row}
end
defp process([value | row], {:value, type}, _from, adapter) do
{load!(type, value, nil, nil, adapter), row}
end
defp process(row, value, _from, _adapter)
when is_binary(value) or is_number(value) or is_atom(value) do
{value, row}
end
defp process_update(data, args, row, from, adapter) do
{args, row} = process_kv(args, row, from, adapter)
data = Enum.reduce(args, data, fn {key, value}, acc -> %{acc | key => value} end)
{data, row}
end
defp process_args(args, row, from, adapter) do
Enum.map_reduce(args, row, fn arg, row ->
process(row, arg, from, adapter)
end)
end
defp process_kv(kv, row, from, adapter) do
Enum.map_reduce(kv, row, fn {key, value}, row ->
{key, row} = process(row, key, from, adapter)
{value, row} = process(row, value, from, adapter)
{{key, value}, row}
end)
end
@compile {:inline, load!: 5}
defp load!(type, value, field, struct, adapter) do
case Ecto.Type.adapter_load(adapter, type, value) do
{:ok, value} ->
value
:error ->
field = field && " for field #{inspect(field)}"
struct = struct && " in #{inspect(struct)}"
raise ArgumentError,
"cannot load `#{inspect(value)}` as type #{inspect(type)}#{field}#{struct}"
end
end
defp to_map(nil, _fields) do
nil
end
defp to_map(value, fields) when is_list(value) do
Enum.map(value, &to_map(&1, fields))
end
defp to_map(value, fields) do
for field <- fields, into: %{} do
case field do
{k, v} -> {k, to_map(Map.fetch!(value, k), List.wrap(v))}
k -> {k, Map.fetch!(value, k)}
end
end
end
defp query_for_get(_queryable, nil) do
raise ArgumentError, "cannot perform Ecto.Repo.get/2 because the given value is nil"
end
defp query_for_get(queryable, id) do
query = Queryable.to_query(queryable)
schema = assert_schema!(query)
case schema.__schema__(:primary_key) do
[pk] ->
Query.from(x in query, where: field(x, ^pk) == ^id)
pks ->
raise ArgumentError,
"Ecto.Repo.get/2 requires the schema #{inspect(schema)} " <>
"to have exactly one primary key, got: #{inspect(pks)}"
end
end
defp query_for_get_by(queryable, clauses) do
Query.where(queryable, [], ^Enum.to_list(clauses))
end
defp query_for_reload([head| _] = structs) do
assert_structs!(structs)
schema = head.__struct__
prefix = head.__meta__.prefix
case schema.__schema__(:primary_key) do
[pk] ->
keys = Enum.map(structs, &get_pk!(&1, pk))
query = Query.from(x in schema, where: field(x, ^pk) in ^keys)
%{query | prefix: prefix}
pks ->
raise ArgumentError,
"Ecto.Repo.reload/2 requires the schema #{inspect(schema)} " <>
"to have exactly one primary key, got: #{inspect(pks)}"
end
end
defp query_for_aggregate(queryable, aggregate) do
query =
case prepare_for_aggregate(queryable) do
%{distinct: nil, limit: nil, offset: nil, combinations: []} = query ->
%{query | order_bys: []}
query ->
query
|> Query.subquery()
|> Queryable.Ecto.SubQuery.to_query()
end
select = %SelectExpr{expr: {aggregate, [], []}, file: __ENV__.file, line: __ENV__.line}
%{query | select: select}
end
defp query_for_aggregate(queryable, aggregate, field) do
ast = field(0, field)
query =
case prepare_for_aggregate(queryable) do
%{distinct: nil, limit: nil, offset: nil, combinations: []} = query ->
%{query | order_bys: []}
query ->
select = %SelectExpr{expr: ast, file: __ENV__.file, line: __ENV__.line}
%{query | select: select}
|> Query.subquery()
|> Queryable.Ecto.SubQuery.to_query()
end
select = %SelectExpr{expr: {aggregate, [], [ast]}, file: __ENV__.file, line: __ENV__.line}
%{query | select: select}
end
defp prepare_for_aggregate(queryable) do
case %{Queryable.to_query(queryable) | preloads: [], assocs: []} do
%{group_bys: [_ | _]} = query ->
raise Ecto.QueryError, message: "cannot aggregate on query with group_by", query: query
%{} = query ->
query
end
end
defp field(ix, field) when is_integer(ix) and is_atom(field) do
{{:., [], [{:&, [], [ix]}, field]}, [], []}
end
defp assert_schema!(%{from: %{source: {_source, schema}}}) when schema != nil, do: schema
defp assert_schema!(query) do
raise Ecto.QueryError,
query: query,
message: "expected a from expression with a schema"
end
defp assert_structs!([head | _] = structs) when is_list(structs) do
unless Enum.all?(structs, &schema?/1) do
raise ArgumentError, "expected a struct or a list of structs, received #{inspect(structs)}"
end
unless Enum.all?(structs, &(&1.__struct__ == head.__struct__)) do
raise ArgumentError, "expected an homogenous list, received different struct types"
end
:ok
end
defp schema?(%{__meta__: _}), do: true
defp schema?(_), do: false
defp get_pk!(struct, pk) do
struct
|> Map.fetch!(pk)
|> case do
nil ->
raise ArgumentError, "Ecto.Repo.reload/2 expects existent structs, found a `nil` primary key"
key ->
key
end
end
end