This commit is contained in:
2026-07-14 22:50:46 +04:00
parent 27143319e3
commit bd19e0682b
3116 changed files with 467189 additions and 0 deletions

View File

@@ -0,0 +1,754 @@
defmodule Ecto do
@moduledoc ~S"""
Ecto is split into 4 main components:
* `Ecto.Repo` - repositories are wrappers around the data store.
Via the repository, we can create, update, destroy and query
existing entries. A repository needs an adapter and credentials
to communicate to the database
* `Ecto.Schema` - schemas are used to map external data into Elixir
structs. We often use them to map database tables to Elixir data but
they have many other use cases
* `Ecto.Query` - written in Elixir syntax, queries are used to retrieve
information from a given repository. Ecto queries are secure and composable
* `Ecto.Changeset` - changesets provide a way to track and validate changes
before they are applied to the data
In summary:
* `Ecto.Repo` - **where** the data is
* `Ecto.Schema` - **what** the data is
* `Ecto.Query` - **how to read** the data
* `Ecto.Changeset` - **how to change** the data
Besides the four components above, most developers use Ecto to interact
with SQL databases, such as PostgreSQL and MySQL via the
[`ecto_sql`](https://hexdocs.pm/ecto_sql) project. `ecto_sql` provides many
conveniences for working with SQL databases as well as the ability to version
how your database changes through time via
[database migrations](https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.html#module-migrations).
If you want to quickly check a sample application using Ecto, please check
the [getting started guide](https://hexdocs.pm/ecto/getting-started.html) and
the accompanying sample application. [Ecto's README](https://github.com/elixir-ecto/ecto)
also links to other resources.
In the following sections, we will provide an overview of those components and
how they interact with each other. Feel free to access their respective module
documentation for more specific examples, options and configuration.
## Repositories
`Ecto.Repo` is a wrapper around the database. We can define a
repository as follows:
defmodule Repo do
use Ecto.Repo,
otp_app: :my_app,
adapter: Ecto.Adapters.Postgres
end
Where the configuration for the Repo must be in your application
environment, usually defined in your `config/config.exs`:
config :my_app, Repo,
database: "ecto_simple",
username: "postgres",
password: "postgres",
hostname: "localhost",
# OR use a URL to connect instead
url: "postgres://postgres:postgres@localhost/ecto_simple"
Each repository in Ecto defines a `start_link/0` function that needs to be invoked
before using the repository. In general, this function is not called directly,
but is used as part of your application supervision tree.
If your application was generated with a supervisor (by passing `--sup` to `mix new`)
you will have a `lib/my_app/application.ex` file containing the application start
callback that defines and starts your supervisor. You just need to edit the `start/2`
function to start the repo as a supervisor on your application's supervisor:
def start(_type, _args) do
children = [
MyApp.Repo,
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
## Schema
Schemas allow developers to define the shape of their data.
Let's see an example:
defmodule Weather do
use Ecto.Schema
# weather is the DB table
schema "weather" do
field :city, :string
field :temp_lo, :integer
field :temp_hi, :integer
field :prcp, :float, default: 0.0
end
end
By defining a schema, Ecto automatically defines a struct with
the schema fields:
iex> weather = %Weather{temp_lo: 30}
iex> weather.temp_lo
30
The schema also allows us to interact with a repository:
iex> weather = %Weather{temp_lo: 0, temp_hi: 23}
iex> Repo.insert!(weather)
%Weather{...}
After persisting `weather` to the database, it will return a new copy of
`%Weather{}` with the primary key (the `id`) set. We can use this value
to read a struct back from the repository:
# Get the struct back
iex> weather = Repo.get Weather, 1
%Weather{id: 1, ...}
# Delete it
iex> Repo.delete!(weather)
%Weather{...}
> NOTE: by using `Ecto.Schema`, an `:id` field with type `:id` (:id means :integer) is
> generated by default, which is the primary key of the schema. If you want
> to use a different primary key, you can declare custom `@primary_key`
> before the `schema/2` call. Consult the `Ecto.Schema` documentation
> for more information.
Notice how the storage (repository) and the data are decoupled. This provides
two main benefits:
* By having structs as data, we guarantee they are light-weight,
serializable structures. In many languages, the data is often represented
by large, complex objects, with entwined state transactions, which makes
serialization, maintenance and understanding hard;
* You do not need to define schemas in order to interact with repositories,
operations like `all`, `insert_all` and so on allow developers to directly
access and modify the data, keeping the database at your fingertips when
necessary;
## Changesets
Although in the example above we have directly inserted and deleted the
struct in the repository, operations on top of schemas are done through
changesets so Ecto can efficiently track changes.
Changesets allow developers to filter, cast, and validate changes before
we apply them to the data. Imagine the given schema:
defmodule User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :name
field :email
field :age, :integer
end
def changeset(user, params \\ %{}) do
user
|> cast(params, [:name, :email, :age])
|> validate_required([:name, :email])
|> validate_format(:email, ~r/@/)
|> validate_inclusion(:age, 18..100)
end
end
The `changeset/2` function first invokes `Ecto.Changeset.cast/4` with
the struct, the parameters and a list of allowed fields; this returns a changeset.
The parameters is a map with binary keys and values that will be cast based
on the type defined by the schema.
Any parameter that was not explicitly listed in the fields list will be ignored.
After casting, the changeset is given to many `Ecto.Changeset.validate_*`
functions that validate only the **changed fields**. In other words:
if a field was not given as a parameter, it won't be validated at all.
For example, if the params map contain only the "name" and "email" keys,
the "age" validation won't run.
Once a changeset is built, it can be given to functions like `insert` and
`update` in the repository that will return an `:ok` or `:error` tuple:
case Repo.update(changeset) do
{:ok, user} ->
# user updated
{:error, changeset} ->
# an error occurred
end
The benefit of having explicit changesets is that we can easily provide
different changesets for different use cases. For example, one
could easily provide specific changesets for registering and updating
users:
def registration_changeset(user, params) do
# Changeset on create
end
def update_changeset(user, params) do
# Changeset on update
end
Changesets are also capable of transforming database constraints,
like unique indexes and foreign key checks, into errors. Allowing
developers to keep their database consistent while still providing
proper feedback to end users. Check `Ecto.Changeset.unique_constraint/3`
for some examples as well as the other `_constraint` functions.
## Query
Last but not least, Ecto allows you to write queries in Elixir and send
them to the repository, which translates them to the underlying database.
Let's see an example:
import Ecto.Query, only: [from: 2]
query = from u in User,
where: u.age > 18 or is_nil(u.email),
select: u
# Returns %User{} structs matching the query
Repo.all(query)
In the example above we relied on our schema but queries can also be
made directly against a table by giving the table name as a string. In
such cases, the data to be fetched must be explicitly outlined:
query = from u in "users",
where: u.age > 18 or is_nil(u.email),
select: %{name: u.name, age: u.age}
# Returns maps as defined in select
Repo.all(query)
Queries are defined and extended with the [`from`](`Ecto.Query.from/2`) macro. The supported
keywords are:
* `:distinct`
* `:where`
* `:order_by`
* `:offset`
* `:limit`
* `:lock`
* `:group_by`
* `:having`
* `:join`
* `:select`
* `:preload`
Examples and detailed documentation for each of those are available
in the `Ecto.Query` module. Functions supported in queries are listed
in `Ecto.Query.API`.
When writing a query, you are inside Ecto's query syntax. In order to
access params values or invoke Elixir functions, you need to use the `^`
operator, which is overloaded by Ecto:
def min_age(min) do
from u in User, where: u.age > ^min
end
Besides [`Repo.all/1`](`c:Ecto.Repo.all/2`) which returns all entries, repositories
also provide [`Repo.one/1`](`c:Ecto.Repo.one/2`) which returns one entry or `nil`,
[`Repo.one!/1`](`c:Ecto.Repo.one!/2`)) which returns one entry or raises,
[`Repo.get/2`](`c:Ecto.Repo.get/3`) which fetches entries for a particular ID and more.
Finally, if you need an escape hatch, Ecto provides fragments
(see `Ecto.Query.API.fragment/1`) to inject SQL (and non-SQL)
fragments into queries. Also, most adapters provide direct
APIs for queries, like `Ecto.Adapters.SQL.query/4`, allowing
developers to completely bypass Ecto queries.
## Other topics
### Associations
Ecto supports defining associations on schemas:
defmodule Post do
use Ecto.Schema
schema "posts" do
has_many :comments, Comment
end
end
defmodule Comment do
use Ecto.Schema
schema "comments" do
field :title, :string
belongs_to :post, Post
end
end
When an association is defined, Ecto also defines a field in the schema
with the association name. By default, associations are not loaded into
this field:
iex> post = Repo.get(Post, 42)
iex> post.comments
#Ecto.Association.NotLoaded<...>
However, developers can use the preload functionality in queries to
automatically pre-populate the field:
Repo.all from p in Post, preload: [:comments]
Preloading can also be done with a pre-defined join value:
Repo.all from p in Post,
join: c in assoc(p, :comments),
where: c.votes > p.votes,
preload: [comments: c]
Finally, for the simple cases, preloading can also be done after
a collection was fetched:
posts = Repo.all(Post) |> Repo.preload(:comments)
The `Ecto` module also provides conveniences for working
with associations. For example, `Ecto.assoc/3` returns a query
with all associated data to a given struct:
import Ecto
# Get all comments for the given post
Repo.all assoc(post, :comments)
# Or build a query on top of the associated comments
query = from c in assoc(post, :comments), where: not is_nil(c.title)
Repo.all(query)
Another function in `Ecto` is `build_assoc/3`, which allows
someone to build an associated struct with the proper fields:
Repo.transact(fn ->
post = Repo.insert!(%Post{title: "Hello", body: "world"})
# Build a comment from post
comment = Ecto.build_assoc(post, :comments, body: "Excellent!")
Repo.insert!(comment)
end)
In the example above, `Ecto.build_assoc/3` is equivalent to:
%Comment{post_id: post.id, body: "Excellent!"}
You can find more information about defining associations and each
respective association module in `Ecto.Schema` docs.
> NOTE: Ecto does not lazy load associations. While lazily loading
> associations may sound convenient at first, in the long run it
> becomes a source of confusion and performance issues.
### Embeds
Ecto also supports embeds. While associations keep parent and child
entries in different tables, embeds stores the child along side the
parent.
Databases like MongoDB have native support for embeds. Databases
like PostgreSQL uses a mixture of JSONB (`embeds_one/3`) and ARRAY
columns to provide this functionality.
Check `Ecto.Schema.embeds_one/3` and `Ecto.Schema.embeds_many/3`
for more information.
### Mix tasks and generators
Ecto provides many tasks to help your workflow as well as code generators.
You can find all available tasks by typing `mix help` inside a project
with Ecto listed as a dependency.
Ecto generators will automatically open the generated files if you have
`ECTO_EDITOR` set in your environment variable.
#### Repo resolution
Ecto requires developers to specify the key `:ecto_repos` in their
application configuration before using tasks like `ecto.create` and
`ecto.migrate`. For example:
config :my_app, :ecto_repos, [MyApp.Repo]
config :my_app, MyApp.Repo,
database: "ecto_simple",
username: "postgres",
password: "postgres",
hostname: "localhost"
"""
@doc """
Returns the schema primary keys as a keyword list.
"""
@spec primary_key(Ecto.Schema.t()) :: Keyword.t()
def primary_key(%{__struct__: schema} = struct) do
Enum.map(schema.__schema__(:primary_key), fn field ->
{field, Map.fetch!(struct, field)}
end)
end
@doc """
Returns the schema primary keys as a keyword list.
Raises `Ecto.NoPrimaryKeyFieldError` if the schema has no
primary key field.
"""
@spec primary_key!(Ecto.Schema.t()) :: Keyword.t()
def primary_key!(%{__struct__: schema} = struct) do
case primary_key(struct) do
[] -> raise Ecto.NoPrimaryKeyFieldError, schema: schema
pk -> pk
end
end
@doc """
Builds a struct from the given `assoc` in `struct`.
## Examples
If the relationship is a `has_one` or `has_many` and
the primary key is set in the parent struct, the key will
automatically be set in the built association:
iex> post = Repo.get(Post, 13)
%Post{id: 13}
iex> build_assoc(post, :comments)
%Comment{id: nil, post_id: 13}
Note though it doesn't happen with `belongs_to` cases, as the
key is often the primary key and such is usually generated
dynamically:
iex> comment = Repo.get(Comment, 13)
%Comment{id: 13, post_id: 25}
iex> build_assoc(comment, :post)
%Post{id: nil}
You can also pass the attributes, which can be a map or
a keyword list, to set the struct's fields except the
association key.
iex> build_assoc(post, :comments, text: "cool")
%Comment{id: nil, post_id: 13, text: "cool"}
iex> build_assoc(post, :comments, %{text: "cool"})
%Comment{id: nil, post_id: 13, text: "cool"}
iex> build_assoc(post, :comments, post_id: 1)
%Comment{id: nil, post_id: 13}
The given attributes are expected to be structured data.
If you want to build an association with external data,
such as a request parameters, you can use `Ecto.Changeset.cast/3`
after `build_assoc/3`:
parent
|> Ecto.build_assoc(:child)
|> Ecto.Changeset.cast(params, [:field1, :field2])
"""
def build_assoc(%{__struct__: schema} = struct, assoc, attributes \\ %{}) do
assoc = Ecto.Association.association_from_schema!(schema, assoc)
assoc.__struct__.build(assoc, struct, drop_meta(attributes))
end
defp drop_meta(%{} = attrs), do: Map.drop(attrs, [:__struct__, :__meta__])
defp drop_meta([_ | _] = attrs), do: Keyword.drop(attrs, [:__struct__, :__meta__])
@doc """
Builds a query for the association in the given struct or structs.
## Examples
In the example below, we get all comments associated to the given
post:
post = Repo.get Post, 1
Repo.all Ecto.assoc(post, :comments)
`assoc/3` can also receive a list of posts, as long as the posts are
not empty:
posts = Repo.all from p in Post, where: is_nil(p.published_at)
Repo.all Ecto.assoc(posts, :comments)
This function can also be used to dynamically load through associations
by giving it a list. For example, to get all authors for all comments for
the given posts, do:
posts = Repo.all from p in Post, where: is_nil(p.published_at)
Repo.all Ecto.assoc(posts, [:comments, :author])
## Options
* `:prefix` - the prefix to fetch assocs from. By default, queries
will use the same prefix as the first struct in the given collection.
This option allows the prefix to be changed.
"""
def assoc(struct_or_structs, assocs, opts \\ []) do
[assoc | assocs] = List.wrap(assocs)
structs =
case struct_or_structs do
nil -> raise ArgumentError, "cannot retrieve association #{inspect(assoc)} for nil"
[] -> raise ArgumentError, "cannot retrieve association #{inspect(assoc)} for empty list"
struct_or_structs -> List.wrap(struct_or_structs)
end
sample = hd(structs)
prefix = assoc_prefix(sample, opts)
schema = sample.__struct__
refl = %{owner_key: owner_key} = Ecto.Association.association_from_schema!(schema, assoc)
values =
Enum.uniq(
for(
struct <- structs,
assert_struct!(schema, struct),
key = Map.fetch!(struct, owner_key),
do: key
)
)
case assocs do
[] ->
%module{} = refl
%{module.assoc_query(refl, nil, values) | prefix: prefix}
assocs ->
%{
Ecto.Association.filter_through_chain(schema, [assoc | assocs], values)
| prefix: prefix
}
end
end
defp assoc_prefix(sample, opts) do
case Keyword.fetch(opts, :prefix) do
{:ok, prefix} ->
prefix
:error ->
case sample do
%{__meta__: %{prefix: prefix}} -> prefix
# Must be an embedded schema
_ -> nil
end
end
end
@doc """
Checks if an association is loaded.
## Examples
iex> post = Repo.get(Post, 1)
iex> Ecto.assoc_loaded?(post.comments)
false
iex> post = post |> Repo.preload(:comments)
iex> Ecto.assoc_loaded?(post.comments)
true
"""
def assoc_loaded?(%Ecto.Association.NotLoaded{}), do: false
def assoc_loaded?(list) when is_list(list), do: true
def assoc_loaded?(%_{}), do: true
def assoc_loaded?(nil), do: true
@doc """
Resets fields in a struct to their default values.
## Examples
iex> post = post |> Repo.preload(:author)
%Post{title: "hello world", author: %Author{}}
iex> Ecto.reset_fields(post, [:title, :author])
%Post{
title: "default title",
author: #Ecto.Association.NotLoaded<association :author is not loaded>
}
"""
@spec reset_fields(Ecto.Schema.t(), list()) :: Ecto.Schema.t()
def reset_fields(struct, []), do: struct
def reset_fields(%{__struct__: schema} = struct, fields) do
default_struct = schema.__struct__()
default_fields = Map.take(default_struct, fields)
Map.merge(struct, default_fields)
end
@doc """
Gets the metadata from the given struct.
For example, to check whether it has been persisted:
iex> Ecto.get_meta(changeset.data, :state)
:built
See `Ecto.Schema.Metadata`.
"""
def get_meta(struct, :context),
do: struct.__meta__.context
def get_meta(struct, :state),
do: struct.__meta__.state
def get_meta(struct, :source),
do: struct.__meta__.source
def get_meta(struct, :prefix),
do: struct.__meta__.prefix
@doc """
Returns a new struct with updated metadata.
It is possible to set:
* `:source` - changes the struct query source
* `:prefix` - changes the struct query prefix
* `:context` - changes the struct meta context
* `:state` - changes the struct state
See `Ecto.Schema.Metadata`.
"""
@spec put_meta(Ecto.Schema.schema(), meta) :: Ecto.Schema.schema()
when meta: [
source: Ecto.Schema.source(),
prefix: Ecto.Schema.prefix(),
context: Ecto.Schema.Metadata.context(),
state: Ecto.Schema.Metadata.state()
]
def put_meta(%{__meta__: meta} = struct, opts) do
case put_or_noop_meta(opts, meta, false) do
:noop -> struct
meta -> %{struct | __meta__: meta}
end
end
defp put_or_noop_meta([{key, value} | t], meta, updated?) do
case meta do
%{^key => ^value} -> put_or_noop_meta(t, meta, updated?)
_ -> put_or_noop_meta(t, put_meta(meta, key, value), true)
end
end
defp put_or_noop_meta([], meta, true), do: meta
defp put_or_noop_meta([], _meta, false), do: :noop
defp put_meta(meta, :state, state) do
if state in [:built, :loaded, :deleted] do
%{meta | state: state}
else
raise ArgumentError, "invalid state #{inspect(state)}"
end
end
defp put_meta(meta, :source, source) do
%{meta | source: source}
end
defp put_meta(meta, :prefix, prefix) do
%{meta | prefix: prefix}
end
defp put_meta(meta, :context, context) do
%{meta | context: context}
end
defp put_meta(_meta, key, _value) do
raise ArgumentError, "unknown meta key #{inspect(key)}"
end
defp assert_struct!(module, %{__struct__: struct}) do
if struct != module do
raise ArgumentError,
"expected a homogeneous list containing the same struct, " <>
"got: #{inspect(module)} and #{inspect(struct)}"
else
true
end
end
@doc """
Loads previously dumped `data` in the given `format` into a schema.
The first argument can be an embedded schema module, or a map (of types) and
determines the return value: a struct or a map, respectively.
The second argument `data` specifies fields and values that are to be loaded.
It can be a map, a keyword list, or a `{fields, values}` tuple. Fields can be
atoms or strings.
The third argument `format` is the format the data has been dumped as. For
example, databases may dump embedded to `:json`, this function allows such
dumped data to be put back into the schemas. If custom types are used,
Ecto will invoke the `c:Ecto.Type.embed_as/1` callback to decide if the data
should be loaded using `cast` or `load`.
Fields that are not present in the schema (or `types` map) are ignored.
If any of the values has invalid type, an error is raised.
Note that if you want to load data into a non-embedded schema that was
directly persisted into a given repository, then use `c:Ecto.Repo.load/2`.
## Examples
iex> result = Ecto.Adapters.SQL.query!(MyRepo, "SELECT users.settings FROM users", [])
iex> Enum.map(result.rows, fn [settings] -> Ecto.embedded_load(Setting, Jason.decode!(settings), :json) end)
[%Setting{...}, ...]
"""
@spec embedded_load(
module_or_map :: module | map(),
data :: map(),
format :: atom()
) :: Ecto.Schema.t() | map()
def embedded_load(schema_or_types, data, format) do
Ecto.Schema.Loader.unsafe_load(
schema_or_types,
data,
&Ecto.Type.embedded_load(&1, &2, format)
)
end
@doc """
Dumps the given struct defined by an embedded schema.
This converts the given embedded schema to a map to be serialized
with the given format. For example:
iex> Ecto.embedded_dump(%Post{}, :json)
%{title: "hello"}
"""
@spec embedded_dump(Ecto.Schema.t(), format :: atom()) :: map()
def embedded_dump(%schema{} = data, format) do
Ecto.Schema.Loader.safe_dump(
data,
schema.__schema__(:dump),
&Ecto.Type.embedded_dump(&1, &2, format)
)
end
end

View File

@@ -0,0 +1,142 @@
defmodule Ecto.Adapter do
@moduledoc """
Specifies the minimal API required from adapters.
"""
@type t :: module
@typedoc """
The metadata returned by the adapter `c:init/1`.
It must be a map and Ecto itself will always inject
two keys into the meta:
* the `:cache` key, which as ETS table that can be used as a cache (if available)
* the `:pid` key, which is the PID returned by the child spec returned in `c:init/1`
"""
@type adapter_meta :: %{optional(:stacktrace) => boolean(), optional(any()) => any()}
@doc """
The callback invoked in case the adapter needs to inject code.
"""
@macrocallback __before_compile__(env :: Macro.Env.t()) :: Macro.t()
@doc """
Ensure all applications necessary to run the adapter are started.
"""
@callback ensure_all_started(
config :: Keyword.t(),
type :: :permanent | :transient | :temporary
) ::
{:ok, [atom]} | {:error, atom}
@doc """
Initializes the adapter supervision tree by returning the children and adapter metadata.
"""
@callback init(config :: Keyword.t()) :: {:ok, :supervisor.child_spec(), adapter_meta}
@doc """
Checks out a connection for the duration of the given function.
In case the adapter provides a pool, this guarantees all of the code
inside the given `fun` runs against the same connection, which
might improve performance by for instance allowing multiple related
calls to the datastore to share cache information:
Repo.checkout(fn ->
for _ <- 1..100 do
Repo.insert!(%Post{})
end
end)
If the adapter does not provide a pool, just calling the passed function
and returning its result are enough.
If the adapter provides a pool, it is supposed to "check out" one of the
pool connections for the duration of the function call. Which connection
is checked out is not passed to the calling function, so it should be done
using a stateful method like using the current process' dictionary, process
tracking, or some kind of other lookup method. Make sure that this stored
connection is then used in the other callbacks implementations, such as
`Ecto.Adapter.Queryable` and `Ecto.Adapter.Schema`.
"""
@callback checkout(adapter_meta, config :: Keyword.t(), (-> result)) :: result when result: var
@doc """
Returns true if a connection has been checked out.
"""
@callback checked_out?(adapter_meta) :: boolean
@doc """
Returns the loaders for a given type.
It receives the primitive type and the Ecto type (which may be
primitive as well). It returns a list of loaders with the given
type usually at the end.
This allows developers to properly translate values coming from
the adapters into Ecto ones. For example, if the database does not
support booleans but instead returns 0 and 1 for them, you could
add:
def loaders(:boolean, type), do: [&bool_decode/1, type]
def loaders(_primitive, type), do: [type]
defp bool_decode(0), do: {:ok, false}
defp bool_decode(1), do: {:ok, true}
All adapters are required to implement a clause for `:binary_id` types,
since they are adapter specific. If your adapter does not provide binary
ids, you may simply use `Ecto.UUID`:
def loaders(:binary_id, type), do: [Ecto.UUID, type]
def loaders(_primitive, type), do: [type]
"""
@callback loaders(primitive_type :: Ecto.Type.primitive(), ecto_type :: Ecto.Type.t()) ::
[(term -> {:ok, term} | :error) | Ecto.Type.t()]
@doc """
Returns the dumpers for a given type.
It receives the primitive type and the Ecto type (which may be
primitive as well). It returns a list of dumpers with the given
type usually at the beginning.
This allows developers to properly translate values coming from
the Ecto into adapter ones. For example, if the database does not
support booleans but instead returns 0 and 1 for them, you could
add:
def dumpers(:boolean, type), do: [type, &bool_encode/1]
def dumpers(_primitive, type), do: [type]
defp bool_encode(false), do: {:ok, 0}
defp bool_encode(true), do: {:ok, 1}
All adapters are required to implement a clause for :binary_id types,
since they are adapter specific. If your adapter does not provide
binary ids, you may simply use `Ecto.UUID`:
def dumpers(:binary_id, type), do: [type, Ecto.UUID]
def dumpers(_primitive, type), do: [type]
"""
@callback dumpers(primitive_type :: Ecto.Type.primitive(), ecto_type :: Ecto.Type.t()) ::
[(term -> {:ok, term} | :error) | Ecto.Type.t()]
@doc """
Returns the adapter metadata from its `c:init/1` callback.
It expects a process name of a repository. The name is either
an atom or a PID. For a given repository, you often want to
call this function based on the repository dynamic repo:
Ecto.Adapter.lookup_meta(repo.get_dynamic_repo())
"""
def lookup_meta(repo_name_or_pid) do
Ecto.Repo.Registry.lookup(repo_name_or_pid)
end
end

View File

@@ -0,0 +1,127 @@
defmodule Ecto.Adapter.Queryable do
@moduledoc """
Specifies the query API required from adapters.
If your adapter is only able to respond to one or a couple of the query functions,
add custom implementations of those functions directly to the Repo
by using `c:Ecto.Adapter.__before_compile__/1` instead.
"""
@typedoc "Proxy type to the adapter meta"
@type adapter_meta :: Ecto.Adapter.adapter_meta()
@typedoc "Ecto.Query metadata fields (stored in cache)"
@type query_meta :: %{sources: tuple, preloads: term, select: map}
@typedoc """
Cache query metadata that is passed to `c:execute/5`.
The cache can be in 3 states, documented below.
If `{:nocache, prepared}` is given, it means the query was
not and cannot be cached. The `prepared` value is the value
returned by `c:prepare/2`.
If `{:cache, cache_function, prepared}` is given, it means
the query can be cached and it must be cached by calling
the `cache_function` function with the cache entry of your
choice. Once `cache_function` is called, the next time the
same query is given to `c:execute/5`, it will receive the
`:cached` tuple.
If `{:cached, update_function, reset_function, cached}` is
given, it means the query has been cached. You may call
`update_function/1` if you want to update the cached result.
Or you may call `reset_function/1`, with a new prepared query,
to force the query to be cached again. If `reset_function/1`
is called, the next time the same query is given to
`c:execute/5`, it will receive the `:cache` tuple.
"""
@type query_cache :: {:nocache, prepared}
| {:cache, cache_function :: (cached -> :ok), prepared}
| {:cached, update_function :: (cached -> :ok), reset_function :: (prepared -> :ok), cached}
@type prepared :: term
@type cached :: term
@type options :: Keyword.t()
@type selected :: term
@doc """
Commands invoked to prepare a query.
It is used on `c:Ecto.Repo.all/2`, `c:Ecto.Repo.update_all/3`,
and `c:Ecto.Repo.delete_all/2`. It returns a tuple, indicating if
this query can be cached or not, and the `prepared` query.
The `prepared` query is any term that will be passed to the
adapter's `c:execute/5`.
"""
@callback prepare(atom :: :all | :update_all | :delete_all, query :: Ecto.Query.t()) ::
{:cache, prepared} | {:nocache, prepared}
@doc """
Executes a previously prepared query.
The `query_meta` field is a map containing some of the fields
found in the `Ecto.Query` struct, after they have been normalized.
For example, the values `selected` by the query, which then have
to be returned, can be found in `query_meta`.
The `query_cache` and its state is documented in `t:query_cache/0`.
The `params` is the list of query parameters. For example, for
a query such as `from Post, where: [id: ^123]`, `params` will be
`[123]`.
Finally, `options` is a keyword list of options given to the
`Repo` operation that triggered the adapter call. Any option is
allowed, as this is a mechanism to allow users of Ecto to customize
how the adapter behaves per operation.
It must return a tuple containing the number of entries and
the result set as a list of lists. The entries in the actual
list will depend on what has been selected by the query. The
result set may also be `nil`, if no value is being selected.
"""
@callback execute(adapter_meta, query_meta, query_cache, params :: list(), options) ::
{non_neg_integer, [[selected]] | nil}
@doc """
Streams a previously prepared query.
See `c:execute/5` for a description of arguments.
It returns a stream of values.
"""
@callback stream(adapter_meta, query_meta, query_cache, params :: list(), options) ::
Enumerable.t
@doc """
Plans and prepares a query for the given repo, leveraging its query cache.
This operation uses the query cache if one is available.
"""
def prepare_query(operation, repo_name_or_pid, queryable) do
%{adapter: adapter, cache: cache} = Ecto.Repo.Registry.lookup(repo_name_or_pid)
{_meta, prepared, _cast_params, dump_params} =
queryable
|> Ecto.Queryable.to_query()
|> Ecto.Query.Planner.ensure_select(operation == :all)
|> Ecto.Query.Planner.query(operation, cache, adapter, 0)
{prepared, dump_params}
end
@doc """
Plans a query using the given adapter.
This does not expect the repository and therefore does not leverage the cache.
"""
def plan_query(operation, adapter, queryable) do
query = Ecto.Queryable.to_query(queryable)
{query, params, _key} = Ecto.Query.Planner.plan(query, operation, adapter)
{cast_params, dump_params} = Enum.unzip(params)
{query, _} = Ecto.Query.Planner.normalize(query, operation, adapter, 0)
{query, cast_params, dump_params}
end
end

View File

@@ -0,0 +1,92 @@
defmodule Ecto.Adapter.Schema do
@moduledoc """
Specifies the schema API required from adapters.
"""
@typedoc "Proxy type to the adapter meta"
@type adapter_meta :: Ecto.Adapter.adapter_meta()
@typedoc "Ecto.Schema metadata fields"
@type schema_meta :: %{
autogenerate_id: {schema_field :: atom, source_field :: atom, Ecto.Type.t()},
context: term,
prefix: binary | nil,
schema: atom,
source: binary
}
@type fields :: Keyword.t()
@type filters :: Keyword.t()
@type constraints :: Keyword.t()
@type returning :: [atom]
@type placeholders :: [term]
@type options :: Keyword.t()
@type on_conflict ::
{:raise, list(), []}
| {:nothing, list(), [atom]}
| {[atom], list(), [atom]}
| {Ecto.Query.t(), list(), [atom]}
@doc """
Called to autogenerate a value for id/embed_id/binary_id.
Returns the autogenerated value, or nil if it must be
autogenerated inside the storage or raise if not supported.
"""
@callback autogenerate(field_type :: :id | :binary_id | :embed_id) :: term | nil
@doc """
Inserts multiple entries into the data store.
In case an `Ecto.Query` given as any of the field values by the user,
it will be sent to the adapter as a tuple with in the shape of
`{query, params}`.
"""
@callback insert_all(
adapter_meta,
schema_meta,
header :: [atom],
[[{atom, term | {Ecto.Query.t(), list()}}]],
on_conflict,
returning,
placeholders,
options
) :: {non_neg_integer, [[term]] | nil}
@doc """
Inserts a single new struct in the data store.
## Autogenerate
The primary key will be automatically included in `returning` if the
field has type `:id` or `:binary_id` and no value was set by the
developer or none was autogenerated by the adapter.
"""
@callback insert(adapter_meta, schema_meta, fields, on_conflict, returning, options) ::
{:ok, fields} | {:invalid, constraints}
@doc """
Updates a single struct with the given filters.
While `filters` can be any record column, it is expected that
at least the primary key (or any other key that uniquely
identifies an existing record) be given as a filter. Therefore,
in case there is no record matching the given filters,
`{:error, :stale}` is returned.
"""
@callback update(adapter_meta, schema_meta, fields, filters, returning, options) ::
{:ok, fields} | {:invalid, constraints} | {:error, :stale}
@doc """
Deletes a single struct with the given filters.
While `filters` can be any record column, it is expected that
at least the primary key (or any other key that uniquely
identifies an existing record) be given as a filter. Therefore,
in case there is no record matching the given filters,
`{:error, :stale}` is returned.
"""
@callback delete(adapter_meta, schema_meta, filters, returning, options) ::
{:ok, fields} | {:invalid, constraints} | {:error, :stale}
end

View File

@@ -0,0 +1,53 @@
defmodule Ecto.Adapter.Storage do
@moduledoc """
Specifies the adapter storage API.
"""
@doc """
Creates the storage given by options.
Returns `:ok` if it was created successfully.
Returns `{:error, :already_up}` if the storage has already been created or
`{:error, term}` in case anything else goes wrong.
## Examples
storage_up(username: "postgres",
database: "ecto_test",
hostname: "localhost")
"""
@callback storage_up(options :: Keyword.t) :: :ok | {:error, :already_up} | {:error, term}
@doc """
Drops the storage given by options.
Returns `:ok` if it was dropped successfully.
Returns `{:error, :already_down}` if the storage has already been dropped or
`{:error, term}` in case anything else goes wrong.
## Examples
storage_down(username: "postgres",
database: "ecto_test",
hostname: "localhost")
"""
@callback storage_down(options :: Keyword.t) :: :ok | {:error, :already_down} | {:error, term}
@doc """
Returns the status of a storage given by options.
Can return `:up`, `:down` or `{:error, term}` in case anything goes wrong.
## Examples
storage_status(username: "postgres",
database: "ecto_test",
hostname: "localhost")
"""
@callback storage_status(options :: Keyword.t()) :: :up | :down | {:error, term()}
end

View File

@@ -0,0 +1,31 @@
defmodule Ecto.Adapter.Transaction do
@moduledoc """
Specifies the adapter transactions API.
"""
@type adapter_meta :: Ecto.Adapter.adapter_meta()
@doc """
Runs the given function inside a transaction.
Returns `{:ok, value}` if the transaction was successful where `value`
is the value returned by the function or `{:error, value}` if the transaction
was rolled back where `value` is the value given to `rollback/1`.
"""
@callback transaction(adapter_meta, options :: Keyword.t(), function :: fun) ::
{:ok, any} | {:error, any}
@doc """
Returns true if the given process is inside a transaction.
"""
@callback in_transaction?(adapter_meta) :: boolean
@doc """
Rolls back the current transaction.
The transaction will return the value given as `{:error, value}`.
See `c:Ecto.Repo.rollback/1`.
"""
@callback rollback(adapter_meta, value :: any) :: no_return
end

View File

@@ -0,0 +1,13 @@
defmodule Ecto.Application do
@moduledoc false
use Application
def start(_type, _args) do
children = [
Ecto.Repo.Registry
]
opts = [strategy: :one_for_one, name: Ecto.Supervisor]
Supervisor.start_link(children, opts)
end
end

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,586 @@
defmodule Ecto.Changeset.Relation do
@moduledoc false
require Logger
alias Ecto.Changeset
alias Ecto.Association.NotLoaded
@type t :: %{required(:__struct__) => atom(),
required(:cardinality) => :one | :many,
required(:on_replace) => :raise | :mark_as_invalid | atom,
required(:relationship) => :parent | :child,
required(:ordered) => boolean,
required(:owner) => atom,
required(:related) => atom,
required(:field) => atom,
optional(atom()) => any()}
@doc """
Builds the related data.
"""
@callback build(t, owner :: Ecto.Schema.t) :: Ecto.Schema.t
@doc """
Returns empty container for relation.
"""
def empty(%{cardinality: cardinality}), do: cardinality_to_empty(cardinality)
defp cardinality_to_empty(:one), do: nil
defp cardinality_to_empty(:many), do: []
@doc """
Checks if the container can be considered empty.
"""
def empty?(%{cardinality: _}, %NotLoaded{}), do: true
def empty?(%{cardinality: :many}, []), do: true
def empty?(%{cardinality: :many}, changes), do: filter_empty(changes) == []
def empty?(%{cardinality: :one}, nil), do: true
def empty?(%{}, _), do: false
@doc """
Filter empty changes
"""
def filter_empty(changes) do
Enum.filter(changes, fn
%Changeset{action: action} when action in [:replace, :delete] -> false
_ -> true
end)
end
@doc """
Applies related changeset changes
"""
def apply_changes(%{cardinality: :one}, nil) do
nil
end
def apply_changes(%{cardinality: :one}, changeset) do
apply_changes(changeset)
end
def apply_changes(%{cardinality: :many}, changesets) do
for changeset <- changesets,
struct = apply_changes(changeset),
do: struct
end
defp apply_changes(%Changeset{action: :delete}), do: nil
defp apply_changes(%Changeset{action: :replace}), do: nil
defp apply_changes(changeset), do: Changeset.apply_changes(changeset)
@doc """
Loads the relation with the given struct.
Loading will fail if the association is not loaded but the struct is.
"""
def load!(%{__meta__: %{state: :built}}, %NotLoaded{__cardinality__: cardinality}) do
cardinality_to_empty(cardinality)
end
def load!(struct, %NotLoaded{__field__: field}) do
raise "attempting to cast or change association `#{field}` " <>
"from `#{inspect struct.__struct__}` that was not loaded. Please preload your " <>
"associations before manipulating them through changesets"
end
def load!(_struct, loaded), do: loaded
@doc """
Casts related according to the `on_cast` function.
"""
def cast(%{cardinality: :one} = relation, _owner, nil, current, _on_cast) do
case current && on_replace(relation, current) do
:error -> {:error, {"is invalid", [type: expected_type(relation)]}}
_ -> {:ok, nil, true}
end
end
def cast(%{cardinality: :one} = relation, owner, params, current, on_cast) when is_list(params) do
if Keyword.keyword?(params) do
cast(relation, owner, Map.new(params), current, on_cast)
else
{:error, {"is invalid", [type: expected_type(relation)]}}
end
end
def cast(%{related: mod} = relation, owner, params, current, on_cast) do
pks = mod.__schema__(:primary_key)
fun = &do_cast(relation, owner, &1, &2, &3, &4, on_cast)
data_pk = data_pk(pks)
param_pk = param_pk(mod, pks)
with :error <- cast_or_change(relation, params, current, data_pk, param_pk, fun) do
{:error, {"is invalid", [type: expected_type(relation)]}}
end
end
defp do_cast(meta, owner, params, struct, allowed_actions, idx, {module, fun, args})
when is_atom(module) and is_atom(fun) and is_list(args) do
IO.warn "passing a MFA to :with in cast_assoc/cast_embed is deprecated, please pass an anonymous function instead"
on_cast = fn changeset, attrs ->
apply(module, fun, [changeset, attrs | args])
end
do_cast(meta, owner, params, struct, allowed_actions, idx, on_cast)
end
defp do_cast(relation, owner, params, nil = _struct, allowed_actions, idx, on_cast) do
{:ok,
relation
|> apply_on_cast(on_cast, relation.__struct__.build(relation, owner), params, idx)
|> put_new_action(:insert)
|> check_action!(allowed_actions)}
end
defp do_cast(relation, _owner, nil = _params, current, _allowed_actions, _idx, _on_cast) do
on_replace(relation, current)
end
defp do_cast(relation, _owner, params, struct, allowed_actions, idx, on_cast) do
{:ok,
relation
|> apply_on_cast(on_cast, struct, params, idx)
|> put_new_action(:update)
|> check_action!(allowed_actions)}
end
defp apply_on_cast(%{cardinality: :many}, on_cast, struct, params, idx) when is_function(on_cast, 3) do
on_cast.(struct, params, idx)
end
defp apply_on_cast(%{cardinality: :one, field: field}, on_cast, _struct, _params, _idx) when is_function(on_cast, 3) do
raise ArgumentError, "invalid :with function for relation #{inspect(field)} " <>
"of cardinality one. Expected a function of arity 2"
end
defp apply_on_cast(_relation, on_cast, struct, params, _idx) when is_function(on_cast, 2) do
on_cast.(struct, params)
end
@doc """
Wraps related structs in changesets.
"""
def change(%{cardinality: :one} = relation, nil, current) do
case current && on_replace(relation, current) do
:error -> {:error, {"is invalid", [type: expected_type(relation)]}}
_ -> {:ok, nil, true}
end
end
def change(%{related: mod} = relation, value, current) do
get_pks = data_pk(mod.__schema__(:primary_key))
with :error <- cast_or_change(relation, value, current, get_pks, get_pks,
&do_change(relation, &1, &2, &3, &4)) do
{:error, {"is invalid", [type: expected_type(relation)]}}
end
end
# This may be an insert or an update, get all fields.
defp do_change(relation, %{__struct__: _} = changeset_or_struct, nil, _allowed_actions, _idx) do
changeset = Changeset.change(changeset_or_struct)
{:ok,
changeset
|> assert_changeset_struct!(relation)
|> put_new_action(action_from_changeset(changeset, nil))}
end
defp do_change(relation, nil, current, _allowed_actions, _idx) do
on_replace(relation, current)
end
defp do_change(relation, %Changeset{} = changeset, _current, allowed_actions, _idx) do
{:ok,
changeset
|> assert_changeset_struct!(relation)
|> put_new_action(:update)
|> check_action!(allowed_actions)}
end
defp do_change(_relation, %{__struct__: _} = struct, _current, allowed_actions, _idx) do
{:ok,
struct
|> Ecto.Changeset.change
|> put_new_action(:update)
|> check_action!(allowed_actions)}
end
defp do_change(relation, changes, current, allowed_actions, idx)
when is_list(changes) or is_map(changes) do
changeset = Ecto.Changeset.change(current || relation.__struct__.build(relation, nil), changes)
changeset = put_new_action(changeset, action_from_changeset(changeset, current))
do_change(relation, changeset, current, allowed_actions, idx)
end
defp action_from_changeset(%{data: %{__meta__: %{state: state}}}, _current) do
case state do
:built -> :insert
:loaded -> :update
:deleted -> :delete
end
end
defp action_from_changeset(_, nil) do
:insert
end
defp action_from_changeset(_, _current) do
:update
end
defp assert_changeset_struct!(%{data: %{__struct__: mod}} = changeset, %{related: mod}) do
changeset
end
defp assert_changeset_struct!(%{data: data}, %{related: mod}) do
raise ArgumentError, "expected changeset data to be a #{mod} struct, got: #{inspect data}"
end
@doc """
Handles the changeset or struct when being replaced.
"""
def on_replace(%{on_replace: :mark_as_invalid}, _changeset_or_struct) do
:error
end
def on_replace(%{on_replace: :raise, field: name, owner: owner}, _) do
raise """
you are attempting to change relation #{inspect name} of
#{inspect owner} but the `:on_replace` option of this relation
is set to `:raise`.
By default it is not possible to replace or delete embeds and
associations during `cast`. Therefore Ecto requires the parameters
given to `cast` to have IDs matching the data currently associated
to #{inspect owner}. Failing to do so results in this error message.
If you want to replace data or automatically delete any data
not sent to `cast`, please set the appropriate `:on_replace`
option when defining the relation. The docs for `Ecto.Changeset`
covers the supported options in the "Associations, embeds and on
replace" section.
However, if you don't want to allow data to be replaced or
deleted, only updated, make sure that:
* If you are attempting to update an existing entry, you
are including the entry primary key (ID) in the data.
* If you have a relationship with many children, all children
must be given on update.
"""
end
def on_replace(_relation, changeset_or_struct) do
{:ok, Changeset.change(changeset_or_struct) |> put_new_action(:replace)}
end
defp raise_if_updating_with_struct!(%{field: name, owner: owner}, %{__struct__: _} = new) do
raise """
you have set that the relation #{inspect name} of #{inspect owner}
has `:on_replace` set to `:update` but you are giving it a struct/
changeset to put_assoc/put_change.
Since you have set `:on_replace` to `:update`, you are only allowed
to update the existing entry by giving updated fields as a map or
keyword list or set it to nil.
If you indeed want to replace the existing #{inspect name}, you have
to change the foreign key field directly.
Got:
#{inspect(new, pretty: true)}
"""
end
defp raise_if_updating_with_struct!(_, _) do
true
end
defp cast_or_change(%{cardinality: :one} = relation, value, current, current_pks_fun, new_pks_fun, fun)
when is_map(value) or is_list(value) or is_nil(value) do
single_change(relation, value, current_pks_fun, new_pks_fun, fun, current)
end
defp cast_or_change(%{cardinality: :many}, [], [], _current_pks, _new_pks, _fun) do
{:ok, [], true}
end
defp cast_or_change(%{cardinality: :many} = relation, value, current, current_pks_fun, new_pks_fun, fun)
when is_list(value) do
{current_pks, current_map} = process_current(current, current_pks_fun, relation)
%{unique: unique, ordered: ordered, related: mod} = relation
change_pks_fun = change_pk(mod.__schema__(:primary_key))
ordered = if ordered, do: current_pks, else: []
map_changes(value, new_pks_fun, change_pks_fun, fun, current_map, [], true, true, unique && %{}, 0, ordered)
end
defp cast_or_change(_, _, _, _, _, _), do: :error
# single change
defp single_change(_relation, nil, _current_pks_fun, _new_pks_fun, fun, current) do
single_change(nil, current, fun, [:update, :delete], false)
end
defp single_change(_relation, new, _current_pks_fun, _new_pks_fun, fun, nil) do
single_change(new, nil, fun, [:insert], false)
end
defp single_change(%{on_replace: on_replace} = relation, new, current_pks_fun, new_pks_fun, fun, current) do
pk_values = new_pks_fun.(new)
if (pk_values == current_pks_fun.(current) and pk_values != []) or
(on_replace == :update and raise_if_updating_with_struct!(relation, new)) do
single_change(new, current, fun, allowed_actions(pk_values), true)
else
case on_replace(relation, current) do
{:ok, _changeset} -> single_change(new, nil, fun, [:insert], false)
:error -> :error
end
end
end
defp single_change(new, current, fun, allowed_actions, skippable?) do
case fun.(new, current, allowed_actions, nil) do
{:ok, %{action: :ignore}} ->
:ignore
{:ok, changeset} ->
if skippable? and skip?(changeset) do
:ignore
else
{:ok, changeset, changeset.valid?}
end
:error ->
:error
end
end
# map changes
defp map_changes([changes | rest], new_pks, change_pks, fun, current, acc, valid?, skip?, unique, idx, ordered)
when is_map(changes) or is_list(changes) do
pk_values = new_pks.(changes)
{struct, current, allowed_actions} = pop_current(current, pk_values)
case fun.(changes, struct, allowed_actions, idx) do
{:ok, %{action: :ignore}} ->
ordered = pop_ordered(pk_values, ordered)
map_changes(rest, new_pks, change_pks, fun, current, acc, valid?, skip?, unique, idx + 1, ordered)
{:ok, changeset} ->
pk_values = change_pks.(changeset)
changeset = maybe_add_error_on_pk(changeset, pk_values, unique)
acc = [changeset | acc]
valid? = valid? and changeset.valid?
skip? = (struct != nil) and skip? and skip?(changeset)
unique = unique && Map.put(unique, pk_values, true)
ordered = pop_ordered(pk_values, ordered)
map_changes(rest, new_pks, change_pks, fun, current, acc, valid?, skip?, unique, idx + 1, ordered)
:error ->
:error
end
end
defp map_changes([], _new_pks, _change_pks, fun, current, acc, valid?, skip?, _unique, _idx, ordered) do
current_structs = Enum.map(current, &elem(&1, 1))
skip? = skip? and ordered == []
reduce_delete_changesets(current_structs, fun, Enum.reverse(acc), valid?, skip?)
end
defp map_changes(_params, _new_pks, _change_pks, _fun, _current, _acc, _valid?, _skip?, _unique, _idx, _ordered) do
:error
end
defp pop_ordered(pk_values, [pk_values | tail]), do: tail
defp pop_ordered(_pk_values, tail), do: tail
defp maybe_add_error_on_pk(%{data: %{__struct__: schema}} = changeset, pk_values, unique) do
if is_map(unique) and not missing_pks?(pk_values) and Map.has_key?(unique, pk_values) do
Enum.reduce(schema.__schema__(:primary_key), changeset, fn pk, acc ->
Changeset.add_error(acc, pk, "has already been taken")
end)
else
changeset
end
end
defp missing_pks?(pk_values) do
pk_values == [] or Enum.any?(pk_values, &is_nil/1)
end
defp allowed_actions(pk_values) do
if Enum.all?(pk_values, &is_nil/1) do
[:insert, :update, :delete]
else
[:update, :delete]
end
end
defp reduce_delete_changesets([struct | rest], fun, acc, valid?, _skip?) do
case fun.(nil, struct, [:update, :delete], nil) do
{:ok, changeset} ->
valid? = valid? and changeset.valid?
reduce_delete_changesets(rest, fun, [changeset | acc], valid?, false)
:error ->
:error
end
end
defp reduce_delete_changesets([], _fun, _acc, _valid?, true), do: :ignore
defp reduce_delete_changesets([], _fun, acc, valid?, false), do: {:ok, acc, valid?}
# helpers
defp check_action!(changeset, allowed_actions) do
action = changeset.action
cond do
action in allowed_actions ->
changeset
action == :ignore ->
changeset
action == :insert ->
raise "cannot insert related #{inspect changeset.data} " <>
"because it is already associated with the given struct"
action == :replace ->
raise "cannot replace related #{inspect changeset.data}. " <>
"This typically happens when you are calling put_assoc/put_embed " <>
"with the results of a previous put_assoc/put_embed/cast_assoc/cast_embed " <>
"operation, which is not supported. You must call such operations only once " <>
"per embed/assoc, in order for Ecto to track changes efficiently"
true ->
raise "cannot #{action} related #{inspect changeset.data} because " <>
"it already exists and it is not currently associated with the " <>
"given struct. Ecto forbids casting existing records through " <>
"the association field for security reasons. Instead, set " <>
"the foreign key value accordingly"
end
end
defp process_current(nil, _get_pks, _relation),
do: {[], %{}}
defp process_current(current, get_pks, relation) do
{pks, {map, counter}} =
Enum.map_reduce(current, {%{}, 0}, fn struct, {acc, counter} ->
pks = get_pks.(struct)
key = if pks == [], do: map_size(acc), else: pks
{pks, {Map.put(acc, key, struct), counter + 1}}
end)
if map_size(map) != counter do
Logger.warning """
found duplicate primary keys for association/embed `#{inspect(relation.field)}` \
in `#{inspect(relation.owner)}`. In case of duplicate IDs, only the last entry \
with the same ID will be kept. Make sure that all entries in `#{inspect(relation.field)}` \
have an ID and the IDs are unique between them
"""
end
{pks, map}
end
defp pop_current(current, pk_values) do
case Map.pop(current, pk_values) do
{nil, current} -> {nil, current, [:insert]}
{struct, current} -> {struct, current, allowed_actions(pk_values)}
end
end
defp data_pk(pks) do
fn
%Changeset{data: data} -> Enum.map(pks, &Map.get(data, &1))
map when is_map(map) -> Enum.map(pks, &Map.get(map, &1))
list when is_list(list) -> Enum.map(pks, &Keyword.get(list, &1))
end
end
defp param_pk(mod, pks) do
pks = Enum.map(pks, &{&1, Atom.to_string(&1), mod.__schema__(:type, &1)})
fn params ->
Enum.map pks, fn {atom_key, string_key, type} ->
original = Map.get(params, string_key) || Map.get(params, atom_key)
case Ecto.Type.cast(type, original) do
{:ok, value} -> value
_ -> original
end
end
end
end
defp change_pk(pks) do
fn %Changeset{} = cs ->
Enum.map(pks, fn pk ->
case cs.changes do
%{^pk => pk_value} -> pk_value
_ -> Map.get(cs.data, pk)
end
end)
end
end
defp put_new_action(%{action: action} = changeset, new_action) when is_nil(action),
do: Map.put(changeset, :action, new_action)
defp put_new_action(changeset, _new_action),
do: changeset
defp skip?(%{valid?: true, changes: empty, action: :update}) when empty == %{},
do: true
defp skip?(_changeset),
do: false
defp expected_type(%{cardinality: :one}), do: :map
defp expected_type(%{cardinality: :many}), do: {:array, :map}
## Surface changes on insert
def surface_changes(%{changes: changes, types: types} = changeset, struct, fields) do
{changes, errors} =
Enum.reduce fields, {changes, []}, fn field, {changes, errors} ->
case {struct, changes, types} do
# User has explicitly changed it
{_, %{^field => _}, _} ->
{changes, errors}
# Handle associations specially
{_, _, %{^field => {tag, embed_or_assoc}}} when tag in [:assoc, :embed] ->
# This is partly reimplementing the logic behind put_relation
# in Ecto.Changeset but we need to do it in a way where we have
# control over the current value.
value = not_loaded_to_empty(Map.get(struct, field))
empty = empty(embed_or_assoc)
case change(embed_or_assoc, value, empty) do
{:ok, change, _} when change != empty ->
{Map.put(changes, field, change), errors}
{:error, error} ->
{changes, [{field, error}]}
_ -> # :ignore or ok with change == empty
{changes, errors}
end
# Struct has a non nil value
{%{^field => value}, _, %{^field => _}} when value != nil ->
{Map.put(changes, field, value), errors}
{_, _, _} ->
{changes, errors}
end
end
case errors do
[] -> %{changeset | changes: changes}
_ -> %{changeset | errors: errors ++ changeset.errors, valid?: false, changes: changes}
end
end
defp not_loaded_to_empty(%NotLoaded{__cardinality__: cardinality}),
do: cardinality_to_empty(cardinality)
defp not_loaded_to_empty(loaded), do: loaded
end

View File

@@ -0,0 +1,301 @@
defmodule Ecto.Embedded do
@moduledoc """
The embedding struct for `embeds_one` and `embeds_many`.
Its fields are:
* `cardinality` - The association cardinality
* `field` - The name of the association field on the schema
* `owner` - The schema where the association was defined
* `related` - The schema that is embedded
* `on_cast` - Function name to call by default when casting embeds
* `on_replace` - The action taken on associations when schema is replaced
"""
alias __MODULE__
alias Ecto.Changeset
alias Ecto.Changeset.Relation
use Ecto.ParameterizedType
@behaviour Relation
@on_replace_opts [:raise, :mark_as_invalid, :delete]
@embeds_one_on_replace_opts @on_replace_opts ++ [:update]
defstruct [
:cardinality,
:field,
:owner,
:related,
:on_cast,
on_replace: :raise,
unique: true,
ordered: true
]
## Parameterized API
# We treat even embed_many as maps, as that's often the
# most efficient format to encode them in the database.
@impl Ecto.ParameterizedType
def type(_), do: {:map, :any}
@impl Ecto.ParameterizedType
def init(opts) do
opts = Keyword.put_new(opts, :on_replace, :raise)
cardinality = Keyword.fetch!(opts, :cardinality)
on_replace_opts =
if cardinality == :one, do: @embeds_one_on_replace_opts, else: @on_replace_opts
unless opts[:on_replace] in on_replace_opts do
raise ArgumentError,
"invalid `:on_replace` option for #{inspect(Keyword.fetch!(opts, :field))}. " <>
"The only valid options are: " <>
Enum.map_join(on_replace_opts, ", ", &"`#{inspect(&1)}`")
end
struct(%Embedded{}, opts)
end
@impl Ecto.ParameterizedType
def load(nil, _fun, %{cardinality: :one}), do: {:ok, nil}
def load(value, fun, %{cardinality: :one, related: schema, field: field}) when is_map(value) do
{:ok, load_field(field, schema, value, fun)}
end
def load(nil, _fun, %{cardinality: :many}), do: {:ok, []}
def load(value, fun, %{cardinality: :many, related: schema, field: field})
when is_list(value) do
{:ok, Enum.map(value, &load_field(field, schema, &1, fun))}
end
def load(_value, _fun, _embed) do
:error
end
defp load_field(_field, schema, value, loader) when is_map(value) do
Ecto.Schema.Loader.unsafe_load(schema, value, loader)
end
defp load_field(field, _schema, value, _fun) do
raise ArgumentError, "cannot load embed `#{field}`, expected a map but got: #{inspect(value)}"
end
@impl Ecto.ParameterizedType
def dump(nil, _, _), do: {:ok, nil}
def dump(value, fun, %{cardinality: :one, related: schema, field: field}) when is_map(value) do
{:ok, dump_field(field, schema, value, schema.__schema__(:dump), fun, _one_embed? = true)}
end
def dump(value, fun, %{cardinality: :many, related: schema, field: field})
when is_list(value) do
types = schema.__schema__(:dump)
{:ok, Enum.map(value, &dump_field(field, schema, &1, types, fun, _one_embed? = false))}
end
def dump(_value, _fun, _embed) do
:error
end
defp dump_field(_field, schema, %{__struct__: schema} = struct, types, dumper, _one_embed?) do
Ecto.Schema.Loader.safe_dump(struct, types, dumper)
end
defp dump_field(field, schema, value, _types, _dumper, one_embed?) do
one_or_many =
if one_embed?,
do: "a struct #{inspect(schema)} value",
else: "a list of #{inspect(schema)} struct values"
raise ArgumentError,
"cannot dump embed `#{field}`, expected #{one_or_many} but got: #{inspect(value)}"
end
@impl Ecto.ParameterizedType
def cast(nil, %{cardinality: :one}), do: {:ok, nil}
def cast(%{__struct__: schema} = struct, %{cardinality: :one, related: schema}) do
{:ok, struct}
end
def cast(nil, %{cardinality: :many}), do: {:ok, []}
def cast(value, %{cardinality: :many, related: schema}) when is_list(value) do
if Enum.all?(value, &Kernel.match?(%{__struct__: ^schema}, &1)) do
{:ok, value}
else
:error
end
end
def cast(_value, _embed) do
:error
end
@impl Ecto.ParameterizedType
def embed_as(_, _), do: :dump
## End of parameterized API
# Callback invoked by repository to prepare embeds.
#
# It replaces the changesets for embeds inside changes
# by actual structs so it can be dumped by adapters and
# loaded into the schema struct afterwards.
@doc false
def prepare(changeset, embeds, adapter, repo_action) do
%{changes: changes, types: types, repo: repo} = changeset
prepare(Map.take(changes, embeds), types, adapter, repo, repo_action)
end
defp prepare(embeds, _types, _adapter, _repo, _repo_action) when embeds == %{} do
embeds
end
defp prepare(embeds, types, adapter, repo, repo_action) do
Enum.reduce(embeds, embeds, fn {name, changeset_or_changesets}, acc ->
{:embed, embed} = Map.get(types, name)
Map.put(acc, name, prepare_each(embed, changeset_or_changesets, adapter, repo, repo_action))
end)
end
defp prepare_each(%{cardinality: :one}, nil, _adapter, _repo, _repo_action) do
nil
end
defp prepare_each(%{cardinality: :one} = embed, changeset, adapter, repo, repo_action) do
action = normalize_action(changeset.action, repo_action, embed)
changeset = run_prepare(changeset, repo)
to_struct(changeset, action, embed, adapter)
end
defp prepare_each(%{cardinality: :many} = embed, changesets, adapter, repo, repo_action) do
for changeset <- changesets,
action = normalize_action(changeset.action, repo_action, embed),
changeset = run_prepare(changeset, repo),
prepared = to_struct(changeset, action, embed, adapter),
do: prepared
end
defp to_struct(%Changeset{valid?: false}, _action, %{related: schema}, _adapter) do
raise ArgumentError,
"changeset for embedded #{inspect(schema)} is invalid, " <>
"but the parent changeset was not marked as invalid"
end
defp to_struct(%Changeset{data: %{__struct__: actual}}, _action, %{related: expected}, _adapter)
when actual != expected do
raise ArgumentError,
"expected changeset for embedded schema `#{inspect(expected)}`, " <>
"got: #{inspect(actual)}"
end
defp to_struct(%Changeset{changes: changes, data: schema}, :update, _embed, _adapter)
when changes == %{} do
schema
end
defp to_struct(%Changeset{}, :delete, _embed, _adapter) do
nil
end
defp to_struct(%Changeset{data: data} = changeset, action, %{related: schema}, adapter) do
%{data: struct, changes: changes} =
changeset =
maybe_surface_changes(changeset, data, schema, action)
embeds = prepare(changeset, schema.__schema__(:embeds), adapter, action)
changes
|> Map.merge(embeds)
|> autogenerate_id(struct, action, schema, adapter)
|> autogenerate(action, schema)
|> apply_embeds(struct)
end
defp maybe_surface_changes(changeset, data, schema, :insert) do
Relation.surface_changes(changeset, data, schema.__schema__(:fields))
end
defp maybe_surface_changes(changeset, _data, _schema, _action) do
changeset
end
defp run_prepare(changeset, repo) do
changeset = %{changeset | repo: repo}
Enum.reduce(Enum.reverse(changeset.prepare), changeset, fn fun, acc ->
case fun.(acc) do
%Ecto.Changeset{} = acc ->
acc
other ->
raise "expected function #{inspect(fun)} given to Ecto.Changeset.prepare_changes/2 " <>
"to return an Ecto.Changeset, got: `#{inspect(other)}`"
end
end)
end
defp apply_embeds(changes, struct) do
struct(struct, changes)
end
defp normalize_action(:replace, _, %{on_replace: :delete}), do: :delete
defp normalize_action(:update, :insert, _), do: :insert
defp normalize_action(action, _, _), do: action
defp autogenerate_id(changes, _struct, :insert, schema, adapter) do
case schema.__schema__(:autogenerate_id) do
{key, _source, :binary_id} ->
Map.put_new_lazy(changes, key, fn -> adapter.autogenerate(:embed_id) end)
{_key, _source, :id} ->
raise ArgumentError,
"embedded schema `#{inspect(schema)}` cannot autogenerate `:id` primary keys, " <>
"those are typically used for auto-incrementing constraints. " <>
"Maybe you meant to use `:binary_id` instead?"
nil ->
changes
end
end
defp autogenerate_id(changes, struct, :update, _schema, _adapter) do
for {_, nil} <- Ecto.primary_key(struct) do
raise Ecto.NoPrimaryKeyValueError, struct: struct
end
changes
end
defp autogenerate(changes, action, schema) do
autogen_fields = action |> action_to_auto() |> schema.__schema__()
Enum.reduce(autogen_fields, changes, fn {fields, {mod, fun, args}}, acc ->
case Enum.reject(fields, &Map.has_key?(changes, &1)) do
[] ->
acc
fields ->
generated = apply(mod, fun, args)
Enum.reduce(fields, acc, &Map.put(&2, &1, generated))
end
end)
end
defp action_to_auto(:insert), do: :autogenerate
defp action_to_auto(:update), do: :autoupdate
@impl Relation
def build(%Embedded{related: related}, _owner) do
related.__struct__()
end
def preload_info(_embed) do
:embed
end
end

View File

@@ -0,0 +1,393 @@
defmodule Ecto.Enum do
@moduledoc """
A custom type that maps atoms to strings or integers.
`Ecto.Enum` must be used whenever you want to keep atom values in a field.
Since atoms cannot be persisted to the database, `Ecto.Enum` converts them
to strings or integers when writing to the database and converts them back
to atoms when loading data. It can be used in your schemas as follows:
# Stored as strings
field :status, Ecto.Enum, values: [:foo, :bar, :baz]
or
# Stored as integers
field :status, Ecto.Enum, values: [foo: 1, bar: 2, baz: 5]
Therefore, the type to be used in your migrations for enum fields depends
on the choice above. For the cases above, one would do, respectively:
add :status, :string
or
add :status, :integer
Some databases also support enum types, which you could use in combination
with the above.
Composite types, such as `:array`, are also supported which allow selecting
multiple values per record:
field :roles, {:array, Ecto.Enum}, values: [:author, :editor, :admin]
Overall, `:values` must be a list of atoms or a keyword list. Values will be
cast to atoms safely and only if the atom exists in the list (otherwise an
error will be raised). Attempting to load any string/integer not represented
by an atom in the list will be invalid.
The helper function `mappings/2` returns the mappings for a given schema and
field, which can be used in places like form drop-downs. See `mappings/2` for
examples.
If you want the values only, you can use `values/2`, and if you want
the "dump-able" values only, you can use `dump_values/2`.
## Embeds
`Ecto.Enum` allows to customize how fields are dumped within embeds through the
`:embed_as` option. Two alternatives are supported: `:values`, which will save
the enum keys (and not their respective mapping), and `:dumped`, which will save
the dumped value. The default is `:values`. For example, assuming the following
schema:
defmodule EnumSchema do
use Ecto.Schema
schema "my_schema" do
embeds_one :embed, Embed do
field :embed_as_values, Ecto.Enum, values: [foo: 1, bar: 2], embed_as: :values
field :embed_as_dump, Ecto.Enum, values: [foo: 1, bar: 2], embed_as: :dumped
end
end
end
The `:embed_as_values` field value will save `:foo` or `:bar`, while the
`:embed_as_dump` field value will save `1` or `2`.
"""
use Ecto.ParameterizedType
@impl true
def type(params), do: params.type
@impl true
def init(opts) do
values = opts[:values]
{type, mappings} =
cond do
is_list(values) and Enum.all?(values, &is_atom/1) ->
validate_unique!(values)
{:string, Enum.map(values, fn atom -> {atom, to_string(atom)} end)}
type = Keyword.keyword?(values) and infer_type(Keyword.values(values)) ->
validate_unique!(Keyword.keys(values))
validate_unique!(Keyword.values(values))
{type, values}
true ->
raise ArgumentError, """
Ecto.Enum types must have a values option specified as a list of atoms or a
keyword list with a mapping from atoms to either integer or string values.
For example:
field :my_field, Ecto.Enum, values: [:foo, :bar]
or
field :my_field, Ecto.Enum, values: [foo: 1, bar: 2, baz: 5]
"""
end
on_load = Map.new(mappings, fn {key, val} -> {val, key} end)
on_dump = Map.new(mappings)
on_cast = Map.new(mappings, fn {key, _} -> {Atom.to_string(key), key} end)
embed_as =
case Keyword.get(opts, :embed_as, :values) do
:values ->
:self
:dumped ->
:dump
other ->
raise ArgumentError, """
the `:embed_as` option for `Ecto.Enum` accepts either `:values` or `:dumped`,
received: `#{inspect(other)}`
"""
end
%{
on_load: on_load,
on_dump: on_dump,
on_cast: on_cast,
mappings: mappings,
embed_as: embed_as,
type: type
}
end
defp validate_unique!(values) do
if length(Enum.uniq(values)) != length(values) do
raise ArgumentError, """
Ecto.Enum type values must be unique.
For example:
field :my_field, Ecto.Enum, values: [:foo, :bar, :foo]
is invalid, while
field :my_field, Ecto.Enum, values: [:foo, :bar, :baz]
is valid
"""
end
end
defp infer_type(values) do
cond do
Enum.all?(values, &is_integer/1) -> :integer
Enum.all?(values, &is_binary/1) -> :string
true -> nil
end
end
@impl true
def cast(nil, _params), do: {:ok, nil}
def cast(data, params) do
case params do
%{on_load: %{^data => as_atom}} ->
{:ok, as_atom}
%{on_dump: %{^data => _}} ->
{:ok, data}
%{on_cast: %{^data => as_atom}} ->
{:ok, as_atom}
params ->
{:error, validation: :inclusion, enum: Map.keys(params.on_cast)}
end
end
@impl true
def load(nil, _, _), do: {:ok, nil}
def load(data, _loader, %{on_load: on_load}) do
case on_load do
%{^data => as_atom} -> {:ok, as_atom}
_ -> :error
end
end
@impl true
def dump(nil, _, _), do: {:ok, nil}
def dump(data, _dumper, %{on_dump: on_dump}) do
case on_dump do
%{^data => as_string} -> {:ok, as_string}
_ -> :error
end
end
@impl true
def equal?(a, b, _params), do: a == b
@impl true
def embed_as(_, %{embed_as: embed_as}), do: embed_as
@impl true
def format(%{mappings: mappings}) do
"#Ecto.Enum<values: #{inspect(Keyword.keys(mappings))}>"
end
@doc """
Returns the possible values for a given schema or types map and field.
These values are the atoms that represent the different possible values
of the field.
## Examples
Assuming this schema:
defmodule MySchema do
use Ecto.Schema
schema "my_schema" do
field :my_string_enum, Ecto.Enum, values: [:foo, :bar, :baz]
field :my_integer_enum, Ecto.Enum, values: [foo: 1, bar: 2, baz: 5]
end
end
Then:
Ecto.Enum.values(MySchema, :my_string_enum)
#=> [:foo, :bar, :baz]
Ecto.Enum.values(MySchema, :my_integer_enum)
#=> [:foo, :bar, :baz]
"""
@spec values(module | map, atom) :: [atom()]
def values(schema_or_types, field) do
schema_or_types
|> mappings(field)
|> Keyword.keys()
end
@doc """
Returns the possible dump values for a given schema or types map and field
"Dump values" are the values that can be dumped in the database. For enums stored
as strings, these are the strings that will be dumped in the database. For enums
stored as integers, these are the integers that will be dumped in the database.
## Examples
Assuming this schema:
defmodule MySchema do
use Ecto.Schema
schema "my_schema" do
field :my_string_enum, Ecto.Enum, values: [:foo, :bar, :baz]
field :my_integer_enum, Ecto.Enum, values: [foo: 1, bar: 2, baz: 5]
end
end
Then:
Ecto.Enum.dump_values(MySchema, :my_string_enum)
#=> ["foo", "bar", "baz"]
Ecto.Enum.dump_values(MySchema, :my_integer_enum)
#=> [1, 2, 5]
`schema_or_types` can also be a types map. See `mappings/2` for more information.
"""
@spec dump_values(module | map, atom) :: [String.t()] | [integer()]
def dump_values(schema_or_types, field) do
schema_or_types
|> mappings(field)
|> Keyword.values()
end
@doc """
Casts a value from the given `schema` and `field`.
## Examples
Assuming this schema:
defmodule MySchema do
use Ecto.Schema
schema "my_schema" do
field :my_string_enum, Ecto.Enum, values: [:foo, :bar, :baz]
field :my_integer_enum, Ecto.Enum, values: [foo: 1, bar: 2, baz: 5]
end
end
Then:
Ecto.Enum.cast_value(MySchema, :my_string_enum, "foo")
#=> {:ok, :foo}
Ecto.Enum.cast_value(MySchema, :my_string_enum, :foo)
#=> {:ok, :foo}
Ecto.Enum.cast_value(MySchema, :my_string_enum, "qux")
#=> :error
Ecto.Enum.cast_value(MySchema, :my_integer_enum, 1)
#=> {:ok, :foo}
Ecto.Enum.cast_value(MySchema, :my_integer_enum, :foo)
#=> {:ok, :foo}
Ecto.Enum.cast_value(MySchema, :my_integer_enum, 6)
#=> :error
`schema_or_types` can also be a types map. See `mappings/2` for more information.
"""
@spec cast_value(module | map, atom, binary | atom | integer) :: {:ok, atom} | :error
def cast_value(schema_or_types, field, value) do
params = get_params(schema_or_types, field)
case cast(value, params) do
{:ok, casted_value} -> {:ok, casted_value}
{:error, _reason} -> :error
end
end
@doc """
Returns the mappings between values and dumped values.
## Examples
Assuming this schema:
defmodule MySchema do
use Ecto.Schema
schema "my_schema" do
field :my_string_enum, Ecto.Enum, values: [:foo, :bar, :baz]
field :my_integer_enum, Ecto.Enum, values: [foo: 1, bar: 2, baz: 5]
end
end
Here are some examples of using `mappings/2` with it:
Ecto.Enum.mappings(MySchema, :my_string_enum)
#=> [foo: "foo", bar: "bar", baz: "baz"]
Ecto.Enum.mappings(MySchema, :my_integer_enum)
#=> [foo: 1, bar: 2, baz: 5]
Examples of calling `mappings/2` with a types map:
schemaless_types = %{
my_enum: Ecto.ParameterizedType.init(Ecto.Enum, values: [:foo, :bar, :baz]),
my_integer_enum: Ecto.ParameterizedType.init(Ecto.Enum, values: [foo: 1, bar: 2, baz: 5])
}
Ecto.Enum.mappings(schemaless_types, :my_enum)
#=> [foo: "foo", bar: "bar", baz: "baz"]
Ecto.Enum.mappings(schemaless_types, :my_integer_enum)
#=> [foo: 1, bar: 2, baz: 5]
"""
@spec mappings(module | map, atom) :: keyword(String.t() | integer())
def mappings(schema_or_types, field) do
get_params(schema_or_types, field)
|> Map.fetch!(:mappings)
end
defp get_params(schema_or_types, field)
defp get_params(schema, field) when is_atom(schema) do
try do
schema.__changeset__()
rescue
_ in UndefinedFunctionError ->
raise ArgumentError, "#{inspect(schema)} is not an Ecto schema or types map"
else
%{} = types -> get_params(types, field)
end
end
defp get_params(types, field) when is_map(types) do
case types do
%{^field => {:parameterized, {Ecto.Enum, params}}} -> params
%{^field => {_, {:parameterized, {Ecto.Enum, params}}}} -> params
%{^field => _} -> raise ArgumentError, "#{field} is not an Ecto.Enum field"
%{} -> raise ArgumentError, "#{field} does not exist"
end
end
end

View File

@@ -0,0 +1,321 @@
defmodule Ecto.Query.CompileError do
@moduledoc """
Raised at compilation time when the query cannot be compiled.
"""
defexception [:message]
end
defmodule Ecto.Query.CastError do
@moduledoc """
Raised at runtime when a value cannot be cast.
"""
defexception [:type, :value, :message]
def exception(opts) do
value = Keyword.fetch!(opts, :value)
type = Keyword.fetch!(opts, :type)
msg = Keyword.fetch!(opts, :message)
%__MODULE__{value: value, type: type, message: msg}
end
end
defmodule Ecto.QueryError do
@moduledoc """
Raised at runtime when the query is invalid.
"""
defexception [:message]
def exception(opts) do
message = Keyword.fetch!(opts, :message)
query = Keyword.fetch!(opts, :query)
hint = Keyword.get(opts, :hint)
message = """
#{message} in query:
#{Inspect.Ecto.Query.to_string(query)}
"""
file = opts[:file]
line = opts[:line]
message =
if file && line do
relative = Path.relative_to_cwd(file)
Exception.format_file_line(relative, line) <> " " <> message
else
message
end
message =
if hint do
message <> "\n" <> hint <> "\n"
else
message
end
%__MODULE__{message: message}
end
end
defmodule Ecto.SubQueryError do
@moduledoc """
Raised at runtime when a subquery is invalid.
"""
defexception [:message, :exception]
def exception(opts) do
exception = Keyword.fetch!(opts, :exception)
query = Keyword.fetch!(opts, :query)
message = """
the following exception happened when compiling a subquery.
#{Exception.format(:error, exception, []) |> String.replace("\n", "\n ")}
The subquery originated from the following query:
#{Inspect.Ecto.Query.to_string(query)}
"""
%__MODULE__{message: message, exception: exception}
end
end
defmodule Ecto.InvalidChangesetError do
@moduledoc """
Raised when we cannot perform an action because the
changeset is invalid.
"""
defexception [:action, :changeset]
def message(%{action: action, changeset: changeset}) do
changes = extract_changes(changeset)
errors = Ecto.Changeset.traverse_errors(changeset, & &1)
"""
could not perform #{action} because changeset is invalid.
Errors
#{pretty(errors)}
Applied changes
#{pretty(changes)}
Params
#{pretty(changeset.params)}
Changeset
#{pretty(changeset)}
"""
end
defp pretty(term) do
inspect(term, pretty: true)
|> String.split("\n")
|> Enum.map_join("\n", &(" " <> &1))
end
defp extract_changes(%Ecto.Changeset{changes: changes}) do
Enum.reduce(changes, %{}, fn {key, value}, acc ->
case value do
%Ecto.Changeset{action: :delete} -> acc
_ -> Map.put(acc, key, extract_changes(value))
end
end)
end
defp extract_changes([%Ecto.Changeset{action: :delete} | tail]),
do: extract_changes(tail)
defp extract_changes([%Ecto.Changeset{} = changeset | tail]),
do: [extract_changes(changeset) | extract_changes(tail)]
defp extract_changes(other),
do: other
end
defmodule Ecto.CastError do
@moduledoc """
Raised when a changeset can't cast a value.
"""
defexception [:message, :type, :value]
def exception(opts) do
type = Keyword.fetch!(opts, :type)
value = Keyword.fetch!(opts, :value)
msg = opts[:message] || "cannot cast #{inspect(value)} to #{Ecto.Type.format(type)}"
%__MODULE__{message: msg, type: type, value: value}
end
end
defmodule Ecto.InvalidURLError do
defexception [:message, :url]
def exception(opts) do
url = Keyword.fetch!(opts, :url)
msg = Keyword.fetch!(opts, :message)
msg = "invalid URL #{url}, #{msg}. The parsed URL is: #{inspect(URI.parse(url))}"
%__MODULE__{message: msg, url: url}
end
end
defmodule Ecto.NoPrimaryKeyFieldError do
@moduledoc """
Raised at runtime when an operation that requires a primary key is invoked
with a schema that does not define a primary key by using `@primary_key false`
"""
defexception [:message, :schema]
def exception(opts) do
schema = Keyword.fetch!(opts, :schema)
message = "schema `#{inspect(schema)}` has no primary key"
%__MODULE__{message: message, schema: schema}
end
end
defmodule Ecto.NoPrimaryKeyValueError do
@moduledoc """
Raised at runtime when an operation that requires a primary key is invoked
with a schema missing value for its primary key
"""
defexception [:message, :struct]
def exception(opts) do
struct = Keyword.fetch!(opts, :struct)
message = "struct `#{inspect(struct)}` is missing primary key value"
%__MODULE__{message: message, struct: struct}
end
end
defmodule Ecto.ChangeError do
defexception [:message]
end
defmodule Ecto.NoResultsError do
defexception [:message]
def exception(opts) do
query = Keyword.fetch!(opts, :queryable) |> Ecto.Queryable.to_query()
msg = """
expected at least one result but got none in query:
#{Inspect.Ecto.Query.to_string(query)}
"""
%__MODULE__{message: msg}
end
end
defmodule Ecto.MultipleResultsError do
defexception [:message]
def exception(opts) do
query = Keyword.fetch!(opts, :queryable) |> Ecto.Queryable.to_query()
count = Keyword.fetch!(opts, :count)
msg = """
expected at most one result but got #{count} in query:
#{Inspect.Ecto.Query.to_string(query)}
"""
%__MODULE__{message: msg}
end
end
defmodule Ecto.MultiplePrimaryKeyError do
defexception [:message]
def exception(opts) do
operation = Keyword.fetch!(opts, :operation)
source = Keyword.fetch!(opts, :source)
params = Keyword.fetch!(opts, :params)
count = Keyword.fetch!(opts, :count)
msg = """
expected #{operation} on #{source} to return at most one entry but got #{count} entries.
This typically means the field(s) set as primary_key in your schema/source
are not enough to uniquely identify entries in the repository.
Those are the parameters sent to the repository:
#{inspect(params)}
"""
%__MODULE__{message: msg}
end
end
defmodule Ecto.MigrationError do
defexception [:message]
end
defmodule Ecto.StaleEntryError do
defexception [:message, :changeset]
def exception(opts) do
action = Keyword.fetch!(opts, :action)
changeset = Keyword.fetch!(opts, :changeset)
msg = """
attempted to #{action} a stale struct:
#{inspect(changeset.data)}
This typically happens when the struct no longer exists in the database \
or a database trigger/rule has forbidden the action. If stale entries are \
expected, you may use `:stale_error_field` to convert this into a changeset \
error, or set `:allow_stale` to true if you would like stale operations to \
be considered a success (such as a stale deletion)
"""
%__MODULE__{message: msg, changeset: changeset}
end
end
defmodule Ecto.ConstraintError do
defexception [:type, :constraint, :message]
def exception(opts) do
type = Keyword.fetch!(opts, :type)
constraint = Keyword.fetch!(opts, :constraint)
changeset = Keyword.fetch!(opts, :changeset)
action = Keyword.fetch!(opts, :action)
constraints =
case changeset.constraints do
[] ->
"The changeset has not defined any constraint."
constraints ->
"The changeset defined the following constraints:\n\n" <>
Enum.map_join(
constraints,
"\n",
&" * #{inspect(&1.constraint)} (#{&1.type}_constraint)"
)
end
msg = """
constraint error when attempting to #{action} struct:
* #{inspect(constraint)} (#{type}_constraint)
If you would like to stop this constraint violation from raising an
exception and instead add it as an error to your changeset, please
call `#{type}_constraint/3` on your changeset with the constraint
`:name` as an option.
#{constraints}
"""
%__MODULE__{message: msg, type: type, constraint: constraint}
end
end

View File

@@ -0,0 +1,52 @@
for encoder <- [Jason.Encoder, JSON.Encoder] do
module = Macro.inspect_atom(:literal, encoder)
if Code.ensure_loaded?(encoder) do
defimpl encoder, for: Ecto.Association.NotLoaded do
def encode(%{__owner__: owner, __field__: field}, _) do
raise """
cannot encode association #{inspect(field)} from #{inspect(owner)} to \
JSON because the association was not loaded.
You can either preload the association:
Repo.preload(#{inspect(owner)}, #{inspect(field)})
Or choose to not encode the association when converting the struct \
to JSON by explicitly listing the JSON fields in your schema:
defmodule #{inspect(owner)} do
# ...
@derive {#{unquote(module)}, only: [:name, :title, ...]}
schema ... do
You can also use the :except option instead of :only if you would \
prefer to skip some fields.
"""
end
end
defimpl encoder, for: Ecto.Schema.Metadata do
def encode(%{schema: schema}, _) do
raise """
cannot encode metadata from the :__meta__ field for #{inspect(schema)} \
to JSON. This metadata is used internally by Ecto and should never be \
exposed externally.
You can either map the schemas to remove the :__meta__ field before \
encoding or explicitly list the JSON fields in your schema:
defmodule #{inspect(schema)} do
# ...
@derive {#{unquote(module)}, only: [:name, :title, ...]}
schema ... do
You can also use the :except option instead of :only if you would \
prefer to skip some fields.
"""
end
end
end
end

View File

@@ -0,0 +1,998 @@
defmodule Ecto.Multi do
@moduledoc """
`Ecto.Multi` is a data structure for grouping multiple Repo operations.
`Ecto.Multi` makes it possible to pack operations that should be
performed in a single database transaction and provides a way to introspect
the queued operations without actually performing them. Each operation
is given a name that is unique and will identify its result in case of
either success or failure.
If a Multi is valid (i.e. all the changesets in it are valid),
all operations will be executed in the order they were added.
The `Ecto.Multi` structure should be considered opaque. You can use
`%Ecto.Multi{}` to pattern match the type, but accessing fields or
directly modifying them is not advised.
`Ecto.Multi.to_list/1` returns a canonical representation of the
structure that can be used for introspection.
> #### When to use Ecto.Multi? {: .info}
>
> `Ecto.Multi` is particularly useful when the set of operations to perform
> is dynamic. For most other use cases, using regular control flow within
> [`Repo.transact(fun)`](`c:Ecto.Repo.transact/2`) and returning
> `{:ok, result}` or `{:error, reason}` is more straightforward.
## Changesets
If a Multi contains operations that accept changesets (like `insert/4`,
`update/4` or `delete/4`), they will be checked before starting the
transaction. If any changeset has errors, the transaction will not be
started and the error will immediately be returned.
Note: `insert/4`, `update/4`, `insert_or_update/4` and `delete/4`
variants that accept a function do not perform these checks since
the functions are executed after the transaction has started.
## Run
`Multi` allows you to run arbitrary functions as part of your transaction
via `run/3` and `run/5`. This is especially useful when an operation
depends on the value of a previous operation. For this reason, the
function given as a callback to `run/3` and `run/5` will receive the repo
as the first argument, and all changes performed by the Multi so far as a
map as the second argument.
The function given to `run` must return `{:ok, value}` or `{:error, value}`
as its result. Returning an error will abort any further operations
and make the Multi fail.
## Example
Let's look at an example definition and usage: resetting a password. We need
to update the account with proper information, log the request and remove
all current sessions:
defmodule PasswordManager do
alias Ecto.Multi
def reset(account, params) do
Multi.new()
|> Multi.update(:account, Account.password_reset_changeset(account, params))
|> Multi.insert(:log, Log.password_reset_changeset(account, params))
|> Multi.delete_all(:sessions, Ecto.assoc(account, :sessions))
end
end
We can later execute it in the integration layer using Repo:
Repo.transact(PasswordManager.reset(account, params))
By pattern matching on the result we can differentiate various conditions:
case result do
{:ok, %{account: account, log: log, sessions: sessions}} ->
# The Multi was successful. We can access results , which are as
# we would get from running the corresponding Repo functions, under
# keys we used for naming the operations.
{:error, failed_operation, failed_value, changes_so_far} ->
# One of the operations failed. We can access the operation's failure
# value (such as a changeset for operations on changesets) to prepare a
# proper response. We also get access to the results of any operations
# that succeeded before the indicated operation failed. (However,
# successful operations were rolled back.)
end
We can also easily unit test our transaction without actually running it.
Since changesets can use in-memory data, we can use an account that is
constructed in memory as well, without persisting it to the database:
test "dry run password reset" do
account = %Account{password: "letmein"}
multi = PasswordManager.reset(account, params)
assert [
{:account, {:update, account_changeset, []}},
{:log, {:insert, log_changeset, []}},
{:sessions, {:delete_all, query, []}}
] = Ecto.Multi.to_list(multi)
# We can introspect changesets and query to see if everything
# is as expected, for example:
assert account_changeset.valid?
assert log_changeset.valid?
assert inspect(query) == "#Ecto.Query<from a in Session>"
end
The name of each operation does not have to be an atom. This can be particularly
useful when you wish to update a collection of changesets at once, and track their
errors individually:
accounts = [%Account{id: 1}, %Account{id: 2}]
Enum.reduce(accounts, Multi.new(), fn account, multi ->
Multi.update(
multi,
{:account, account.id},
Account.password_reset_changeset(account, params)
)
end)
"""
alias __MODULE__
alias Ecto.Changeset
defstruct operations: [], names: MapSet.new()
@typedoc """
Map of changes made so far during the current transaction. For any Multi
which returns `{:ok, result}`, its `t:name/0` is added as a key and its
result as the value.
"""
@type changes :: map
@type run :: (Ecto.Repo.t(), changes -> {:ok | :error, any})
@type fun(result) :: (changes -> result)
@type merge :: (changes -> t) | {module, atom, [any]}
@typep schema_or_source :: binary | {binary, module} | module
@typep operation ::
{:changeset, Changeset.t(), Keyword.t()}
| {:run, run}
| {:put, any}
| {:inspect, Keyword.t()}
| {:merge, merge}
| {:update_all, Ecto.Query.t(), Keyword.t()}
| {:delete_all, Ecto.Query.t(), Keyword.t()}
| {:insert_all, schema_or_source, [map | Keyword.t()], Keyword.t()}
@typep operations :: [{name, operation}]
@typep names :: MapSet.t()
@typedoc """
Name of an operation in the Multi. Can be any term, as long as it is unique
within the list of operations; for example, `:insert_post` or `{:delete_post,
5}`.
"""
@type name :: any
@typedoc """
Result of a failed transaction using a Multi.
"""
@type failure ::
{:error, failed_operation :: Ecto.Multi.name(), failed_value :: any(),
changes_so_far :: %{Ecto.Multi.name() => any}}
@type t :: %__MODULE__{operations: operations, names: names}
@doc """
Returns an empty `Ecto.Multi` struct.
## Example
iex> Ecto.Multi.new() |> Ecto.Multi.to_list()
[]
"""
@spec new :: t
def new do
%Multi{}
end
@doc """
Appends the second Multi to the first.
All names must be unique within both structures.
## Example
iex> lhs = Ecto.Multi.new() |> Ecto.Multi.run(:left, fn _, changes -> {:ok, changes} end)
iex> rhs = Ecto.Multi.new() |> Ecto.Multi.run(:right, fn _, changes -> {:error, changes} end)
iex> Ecto.Multi.append(lhs, rhs) |> Ecto.Multi.to_list |> Keyword.keys
[:left, :right]
"""
@spec append(t, t) :: t
def append(lhs, rhs) do
merge_structs(lhs, rhs, &(&2 ++ &1))
end
@doc """
Prepends the second Multi to the first.
All names must be unique within both structures.
## Example
iex> lhs = Ecto.Multi.new() |> Ecto.Multi.run(:left, fn _, changes -> {:ok, changes} end)
iex> rhs = Ecto.Multi.new() |> Ecto.Multi.run(:right, fn _, changes -> {:error, changes} end)
iex> Ecto.Multi.prepend(lhs, rhs) |> Ecto.Multi.to_list |> Keyword.keys
[:right, :left]
"""
@spec prepend(t, t) :: t
def prepend(lhs, rhs) do
merge_structs(lhs, rhs, &(&1 ++ &2))
end
defp merge_structs(%Multi{} = lhs, %Multi{} = rhs, joiner) do
%{names: lhs_names, operations: lhs_ops} = lhs
%{names: rhs_names, operations: rhs_ops} = rhs
case MapSet.intersection(lhs_names, rhs_names) |> MapSet.to_list() do
[] ->
%Multi{names: MapSet.union(lhs_names, rhs_names), operations: joiner.(lhs_ops, rhs_ops)}
common ->
raise ArgumentError, """
error when merging the following Ecto.Multi structs:
#{Kernel.inspect(lhs)}
#{Kernel.inspect(rhs)}
both declared operations: #{Kernel.inspect(common)}
"""
end
end
@doc """
Merges a Multi returned dynamically by an anonymous function.
This function is useful when the Multi to be merged requires information
from the original Multi. The second argument is an anonymous function
that receives the Multi changes so far. The anonymous function must return
another Multi.
If you would prefer to simply merge two Multis together, see `append/2` or
`prepend/2`.
Duplicated operations are not allowed.
## Example
multi =
Ecto.Multi.new()
|> Ecto.Multi.insert(:post, %Post{title: "first"})
multi
|> Ecto.Multi.merge(fn %{post: post} ->
Ecto.Multi.new()
|> Ecto.Multi.insert(:comment, Ecto.build_assoc(post, :comments))
end)
|> MyApp.Repo.transact()
"""
@spec merge(t, (changes -> t)) :: t
def merge(%Multi{} = multi, merge) when is_function(merge, 1) do
Map.update!(multi, :operations, &[{:merge, {:merge, merge}} | &1])
end
@doc """
Merges a Multi returned dynamically by calling `module` and `function` with `args`.
Similar to `merge/2` but allows passing of module name, function and
arguments. The function should return an `Ecto.Multi`, and receives changes so far
as the first argument (prepended to those passed in the call to the function).
Duplicated operations are not allowed.
"""
@spec merge(t, module, function, args) :: t when function: atom, args: [any]
def merge(%Multi{} = multi, mod, fun, args)
when is_atom(mod) and is_atom(fun) and is_list(args) do
Map.update!(multi, :operations, &[{:merge, {:merge, {mod, fun, args}}} | &1])
end
@doc """
Adds an insert operation to the Multi.
The `name` must be unique within the Multi.
The remaining arguments and options are the same as in `c:Ecto.Repo.insert/2`.
## Example
Ecto.Multi.new()
|> Ecto.Multi.insert(:insert, %Post{title: "first"})
|> MyApp.Repo.transact()
Ecto.Multi.new()
|> Ecto.Multi.insert(:post, %Post{title: "first"})
|> Ecto.Multi.insert(:comment, fn %{post: post} ->
Ecto.build_assoc(post, :comments)
end)
|> MyApp.Repo.transact()
"""
@spec insert(
t,
name,
Changeset.t() | Ecto.Schema.t() | (changes -> Changeset.t() | Ecto.Schema.t()),
Keyword.t()
) :: t
def insert(multi, name, changeset_or_struct_or_fun, opts \\ [])
def insert(multi, name, %Changeset{} = changeset, opts) do
add_changeset(multi, :insert, name, changeset, opts)
end
def insert(multi, name, %_{} = struct, opts) do
insert(multi, name, Changeset.change(struct), opts)
end
def insert(multi, name, fun, opts) when is_function(fun, 1) do
run(multi, name, operation_fun({:insert, fun}, opts))
end
@doc """
Adds an update operation to the Multi.
The `name` must be unique within the Multi.
The remaining arguments and options are the same as in `c:Ecto.Repo.update/2`.
## Example
post = MyApp.Repo.get!(Post, 1)
changeset = Ecto.Changeset.change(post, title: "New title")
Ecto.Multi.new()
|> Ecto.Multi.update(:update, changeset)
|> MyApp.Repo.transact()
Ecto.Multi.new()
|> Ecto.Multi.insert(:post, %Post{title: "first"})
|> Ecto.Multi.update(:fun, fn %{post: post} ->
Ecto.Changeset.change(post, title: "New title")
end)
|> MyApp.Repo.transact()
"""
@spec update(t, name, Changeset.t() | (changes -> Changeset.t()), Keyword.t()) :: t
def update(multi, name, changeset_or_fun, opts \\ [])
def update(multi, name, %Changeset{} = changeset, opts) do
add_changeset(multi, :update, name, changeset, opts)
end
def update(multi, name, fun, opts) when is_function(fun, 1) do
run(multi, name, operation_fun({:update, fun}, opts))
end
@doc """
Inserts or updates a changeset depending on whether or not the changeset was persisted.
The `name` must be unique within the Multi.
The remaining arguments and options are the same as in `c:Ecto.Repo.insert_or_update/2`.
## Example
changeset = Post.changeset(%Post{}, %{title: "New title"})
Ecto.Multi.new()
|> Ecto.Multi.insert_or_update(:insert_or_update, changeset)
|> MyApp.Repo.transact()
Ecto.Multi.new()
|> Ecto.Multi.run(:post, fn repo, _changes ->
{:ok, repo.get(Post, 1) || %Post{}}
end)
|> Ecto.Multi.insert_or_update(:update, fn %{post: post} ->
Ecto.Changeset.change(post, title: "New title")
end)
|> MyApp.Repo.transact()
"""
@spec insert_or_update(t, name, Changeset.t() | (changes -> Changeset.t()), Keyword.t()) :: t
def insert_or_update(multi, name, changeset_or_fun, opts \\ [])
def insert_or_update(
multi,
name,
%Changeset{data: %{__meta__: %{state: :loaded}}} = changeset,
opts
) do
add_changeset(multi, :update, name, changeset, opts)
end
def insert_or_update(multi, name, %Changeset{} = changeset, opts) do
add_changeset(multi, :insert, name, changeset, opts)
end
def insert_or_update(multi, name, fun, opts) when is_function(fun, 1) do
run(multi, name, operation_fun({:insert_or_update, fun}, opts))
end
@doc """
Adds a delete operation to the Multi.
The `name` must be unique within the Multi.
The remaining arguments and options are the same as in `c:Ecto.Repo.delete/2`.
## Example
post = MyApp.Repo.get!(Post, 1)
Ecto.Multi.new()
|> Ecto.Multi.delete(:delete, post)
|> MyApp.Repo.transact()
Ecto.Multi.new()
|> Ecto.Multi.run(:post, fn repo, _changes ->
case repo.get(Post, 1) do
nil -> {:error, :not_found}
post -> {:ok, post}
end
end)
|> Ecto.Multi.delete(:delete, fn %{post: post} ->
# Others validations
post
end)
|> MyApp.Repo.transact()
"""
@spec delete(
t,
name,
Changeset.t() | Ecto.Schema.t() | (changes -> Changeset.t() | Ecto.Schema.t()),
Keyword.t()
) :: t
def delete(multi, name, changeset_or_struct_fun, opts \\ [])
def delete(multi, name, %Changeset{} = changeset, opts) do
add_changeset(multi, :delete, name, changeset, opts)
end
def delete(multi, name, %_{} = struct, opts) do
delete(multi, name, Changeset.change(struct), opts)
end
def delete(multi, name, fun, opts) when is_function(fun, 1) do
run(multi, name, operation_fun({:delete, fun}, opts))
end
@doc """
Runs a query expecting one result and stores the result in the Multi.
The `name` must be unique within the Multi.
The remaining arguments and options are the same as in `c:Ecto.Repo.one/2`.
## Example
Ecto.Multi.new()
|> Ecto.Multi.one(:post, Post)
|> Ecto.Multi.one(:author, fn %{post: post} ->
from(a in Author, where: a.id == ^post.author_id)
end)
|> MyApp.Repo.transact()
"""
@spec one(
t,
name,
queryable :: Ecto.Queryable.t() | (changes -> Ecto.Queryable.t()),
opts :: Keyword.t()
) :: t
def one(multi, name, queryable_or_fun, opts \\ [])
def one(multi, name, fun, opts) when is_function(fun, 1) do
run(multi, name, operation_fun({:one, fun}, opts))
end
def one(multi, name, queryable, opts) do
run(multi, name, operation_fun({:one, fn _ -> queryable end}, opts))
end
@doc """
Runs a query and stores all results in the Multi.
The `name` must be unique within the Multi.
The remaining arguments and options are the same as in `c:Ecto.Repo.all/2`.
## Example
Ecto.Multi.new()
|> Ecto.Multi.all(:all, Post)
|> MyApp.Repo.transact()
Ecto.Multi.new()
|> Ecto.Multi.all(:all, fn _changes -> Post end)
|> MyApp.Repo.transact()
"""
@spec all(
t,
name,
queryable :: Ecto.Queryable.t() | (changes -> Ecto.Queryable.t()),
opts :: Keyword.t()
) :: t
def all(multi, name, queryable_or_fun, opts \\ [])
def all(multi, name, fun, opts) when is_function(fun, 1) do
run(multi, name, operation_fun({:all, fun}, opts))
end
def all(multi, name, queryable, opts) do
run(multi, name, operation_fun({:all, fn _ -> queryable end}, opts))
end
@doc """
Checks if an entry matching the given query exists and stores a boolean in the Multi.
The `name` must be unique within the Multi.
The remaining arguments and options are the same as in `c:Ecto.Repo.exists?/2`.
## Example
Ecto.Multi.new()
|> Ecto.Multi.exists?(:post, Post)
|> MyApp.Repo.transact()
Ecto.Multi.new()
|> Ecto.Multi.exists?(:post, fn _changes -> Post end)
|> MyApp.Repo.transact()
"""
@spec exists?(
t,
name,
queryable :: Ecto.Queryable.t() | (changes -> Ecto.Queryable.t()),
opts :: Keyword.t()
) :: t
def exists?(multi, name, queryable_or_fun, opts \\ [])
def exists?(multi, name, fun, opts) when is_function(fun, 1) do
run(multi, name, operation_fun({:exists?, fun}, opts))
end
def exists?(multi, name, queryable, opts) do
run(multi, name, operation_fun({:exists?, fn _ -> queryable end}, opts))
end
defp add_changeset(multi, action, name, changeset, opts) when is_list(opts) do
add_operation(multi, name, {:changeset, put_action(changeset, action), opts})
end
defp put_action(%{action: nil} = changeset, action) do
%{changeset | action: action}
end
defp put_action(%{action: action} = changeset, action) do
changeset
end
defp put_action(%{action: original}, action) do
raise ArgumentError,
"you provided a changeset with an action already set " <>
"to #{Kernel.inspect(original)} when trying to #{action} it"
end
@doc """
Causes the Multi to fail with the given value.
Running the Multi in a transaction will execute
no previous steps and return the value of the first
error added.
"""
@spec error(t, name, error :: term) :: t
def error(multi, name, value) do
add_operation(multi, name, {:error, value})
end
@doc """
Adds a function to run as part of the Multi.
The function should return either `{:ok, value}` or `{:error, value}`,
and receives the repo as the first argument and the changes so far
as the second argument.
## Example
Ecto.Multi.run(multi, :write, fn _repo, %{image: image} ->
with :ok <- File.write(image.name, image.contents) do
{:ok, nil}
end
end)
"""
@spec run(t, name, run) :: t
def run(multi, name, run) when is_function(run, 2) do
add_operation(multi, name, {:run, run})
end
@doc """
Adds a function to run as part of the Multi.
Similar to `run/3`, but allows passing of module name, function and arguments.
The function should return either `{:ok, value}` or `{:error, value}`, and
receives the repo as the first argument and the changes so far as the
second argument (prepended to those passed in the call to the function).
"""
@spec run(t, name, module, function, args) :: t when function: atom, args: [any]
def run(multi, name, mod, fun, args)
when is_atom(mod) and is_atom(fun) and is_list(args) do
add_operation(multi, name, {:run, {mod, fun, args}})
end
@doc """
Adds an `insert_all` operation to the Multi.
Accepts the same arguments and options as `c:Ecto.Repo.insert_all/3`.
## Example
posts = [%{title: "My first post"}, %{title: "My second post"}]
Ecto.Multi.new()
|> Ecto.Multi.insert_all(:insert_all, Post, posts)
|> MyApp.Repo.transact()
Ecto.Multi.new()
|> Ecto.Multi.run(:post, fn repo, _changes ->
case repo.get(Post, 1) do
nil -> {:error, :not_found}
post -> {:ok, post}
end
end)
|> Ecto.Multi.insert_all(:insert_all, Comment, fn %{post: post} ->
# Others validations
entries
|> Enum.map(fn comment ->
Map.put(comment, :post_id, post.id)
end)
end)
|> MyApp.Repo.transact()
"""
@spec insert_all(
t,
name,
schema_or_source,
entries_or_query_or_fun ::
[map | Keyword.t()] | (changes -> [map | Keyword.t()]) | Ecto.Query.t(),
Keyword.t()
) :: t
def insert_all(multi, name, schema_or_source, entries_or_query_or_fun, opts \\ [])
def insert_all(multi, name, schema_or_source, entries_fun, opts)
when is_function(entries_fun, 1) and is_list(opts) do
run(multi, name, operation_fun({:insert_all, schema_or_source, entries_fun}, opts))
end
def insert_all(multi, name, schema_or_source, entries_or_query, opts) when is_list(opts) do
add_operation(multi, name, {:insert_all, schema_or_source, entries_or_query, opts})
end
@doc """
Adds an `update_all` operation to the Multi.
Accepts the same arguments and options as `c:Ecto.Repo.update_all/3`.
## Example
Ecto.Multi.new()
|> Ecto.Multi.update_all(:update_all, Post, set: [title: "New title"])
|> MyApp.Repo.transact()
Ecto.Multi.new()
|> Ecto.Multi.run(:post, fn repo, _changes ->
case repo.get(Post, 1) do
nil -> {:error, :not_found}
post -> {:ok, post}
end
end)
|> Ecto.Multi.update_all(:update_all, fn %{post: post} ->
# Others validations
from(c in Comment, where: c.post_id == ^post.id, update: [set: [title: "New title"]])
end, [])
|> MyApp.Repo.transact()
"""
@spec update_all(
t,
name,
Ecto.Queryable.t() | (changes -> Ecto.Queryable.t()),
Keyword.t(),
Keyword.t()
) :: t
def update_all(multi, name, queryable_or_fun, updates, opts \\ [])
def update_all(multi, name, queryable_fun, updates, opts)
when is_function(queryable_fun, 1) and is_list(opts) do
run(multi, name, operation_fun({:update_all, queryable_fun, updates}, opts))
end
def update_all(multi, name, queryable, updates, opts) when is_list(opts) do
query = Ecto.Queryable.to_query(queryable)
add_operation(multi, name, {:update_all, query, updates, opts})
end
@doc """
Adds a `delete_all` operation to the Multi.
Accepts the same arguments and options as `c:Ecto.Repo.delete_all/2`.
## Example
queryable = from(p in Post, where: p.id < 5)
Ecto.Multi.new()
|> Ecto.Multi.delete_all(:delete_all, queryable)
|> MyApp.Repo.transact()
Ecto.Multi.new()
|> Ecto.Multi.run(:post, fn repo, _changes ->
case repo.get(Post, 1) do
nil -> {:error, :not_found}
post -> {:ok, post}
end
end)
|> Ecto.Multi.delete_all(:delete_all, fn %{post: post} ->
# Others validations
from(c in Comment, where: c.post_id == ^post.id)
end)
|> MyApp.Repo.transact()
"""
@spec delete_all(t, name, Ecto.Queryable.t() | (changes -> Ecto.Queryable.t()), Keyword.t()) ::
t
def delete_all(multi, name, queryable_or_fun, opts \\ [])
def delete_all(multi, name, fun, opts) when is_function(fun, 1) and is_list(opts) do
run(multi, name, operation_fun({:delete_all, fun}, opts))
end
def delete_all(multi, name, queryable, opts) when is_list(opts) do
query = Ecto.Queryable.to_query(queryable)
add_operation(multi, name, {:delete_all, query, opts})
end
defp add_operation(%Multi{} = multi, name, operation) do
%{operations: operations, names: names} = multi
if MapSet.member?(names, name) do
raise "#{Kernel.inspect(name)} is already a member of the Ecto.Multi: \n#{Kernel.inspect(multi)}"
else
%{multi | operations: [{name, operation} | operations], names: MapSet.put(names, name)}
end
end
@doc """
Returns the list of operations stored in the Multi.
Always use this function when you need to access the operations you
have defined in `Ecto.Multi`. Inspecting the `Ecto.Multi` struct internals
directly is discouraged.
"""
@spec to_list(t) :: [{name, term}]
def to_list(%Multi{operations: operations}) do
operations
|> Enum.reverse()
|> Enum.map(&format_operation/1)
end
defp format_operation({name, {:changeset, changeset, opts}}),
do: {name, {changeset.action, changeset, opts}}
defp format_operation(other),
do: other
@doc """
Adds a value to the changes so far under the given name.
The given `value` is added to the Multi before the transaction starts.
If you would like to run arbitrary functions as part of your transaction,
see `run/3` or `run/5`.
## Example
Imagine there is an existing company schema that you retrieved from
the database. You can insert it as a change in the Multi using `put/3`:
Ecto.Multi.new()
|> Ecto.Multi.put(:company, company)
|> Ecto.Multi.insert(:user, fn changes -> User.changeset(changes.company) end)
|> Ecto.Multi.insert(:person, fn changes -> Person.changeset(changes.user, changes.company) end)
|> MyApp.Repo.transact()
In the example above, there isn't a significant benefit in putting
the `company` in the Multi because you could also access the
`company` variable directly inside the anonymous function.
However, the benefit of `put/3` is seen when composing `Ecto.Multi`s.
If the insert operations above were defined in another module,
you could use `put(:company, company)` to inject changes that
will be accessed by other functions down the chain, removing
the need to pass both `multi` and `company` values around.
"""
@spec put(t, name, any) :: t
def put(multi, name, value) do
add_operation(multi, name, {:put, value})
end
@doc """
Inspects results from a Multi.
By default, the name is shown as a label to the inspect. Custom labels are
supported through the `IO.inspect/2` `label` option.
## Options
All options for IO.inspect/2 are supported, as well as:
* `:only` - A field or a list of fields to inspect, will print the entire
map by default.
## Examples
Ecto.Multi.new()
|> Ecto.Multi.insert(:person_a, changeset)
|> Ecto.Multi.insert(:person_b, changeset)
|> Ecto.Multi.inspect()
|> MyApp.Repo.transact()
Prints:
%{person_a: %Person{...}, person_b: %Person{...}}
We can use the `:only` option to limit which fields will be printed:
Ecto.Multi.new()
|> Ecto.Multi.insert(:person_a, changeset)
|> Ecto.Multi.insert(:person_b, changeset)
|> Ecto.Multi.inspect(only: :person_a)
|> MyApp.Repo.transact()
Prints:
%{person_a: %Person{...}}
"""
@spec inspect(t, Keyword.t()) :: t
def inspect(multi, opts \\ []) do
Map.update!(multi, :operations, &[{:inspect, {:inspect, opts}} | &1])
end
@doc false
@spec __apply__(t, Ecto.Repo.t(), fun, (term -> no_return)) :: {:ok, term} | {:error, term}
def __apply__(%Multi{} = multi, repo, wrap, return) do
operations = Enum.reverse(multi.operations)
with {:ok, operations} <- check_operations_valid(operations) do
apply_operations(operations, multi.names, repo, wrap, return)
end
end
defp check_operations_valid(operations) do
Enum.find_value(operations, &invalid_operation/1) || {:ok, operations}
end
defp invalid_operation({name, {:changeset, %{valid?: false} = changeset, _}}),
do: {:error, {name, changeset, %{}}}
defp invalid_operation({name, {:error, value}}),
do: {:error, {name, value, %{}}}
defp invalid_operation(_operation),
do: nil
defp apply_operations([], _names, _repo, _wrap, _return), do: {:ok, %{}}
defp apply_operations(operations, names, repo, wrap, return) do
wrap.(fn ->
operations
|> Enum.reduce({%{}, names}, &apply_operation(&1, repo, wrap, return, &2))
|> elem(0)
end)
end
defp apply_operation({_, {:merge, merge}}, repo, wrap, return, {acc, names}) do
case __apply__(apply_merge_fun(merge, acc), repo, wrap, return) do
{:ok, value} ->
merge_results(acc, value, names)
{:error, {name, value, nested_acc}} ->
{acc, _names} = merge_results(acc, nested_acc, names)
return.({name, value, acc})
end
end
defp apply_operation({_name, {:inspect, opts}}, _repo, _wrap_, _return, {acc, names}) do
if opts[:only] do
acc |> Map.take(List.wrap(opts[:only])) |> IO.inspect(opts)
else
IO.inspect(acc, opts)
end
{acc, names}
end
defp apply_operation({name, operation}, repo, wrap, return, {acc, names}) do
case apply_operation(operation, acc, {wrap, return}, repo) do
{:ok, value} ->
{Map.put(acc, name, value), names}
{:error, value} ->
return.({name, value, acc})
other ->
raise "expected Ecto.Multi callback named `#{Kernel.inspect(name)}` to return either {:ok, value} or {:error, value}, got: #{Kernel.inspect(other)}"
end
end
defp apply_operation({:changeset, changeset, opts}, _acc, _apply_args, repo),
do: apply(repo, changeset.action, [changeset, opts])
defp apply_operation({:run, run}, acc, _apply_args, repo),
do: apply_run_fun(run, repo, acc)
defp apply_operation({:error, value}, _acc, _apply_args, _repo),
do: {:error, value}
defp apply_operation({:insert_all, source, entries, opts}, _acc, _apply_args, repo),
do: {:ok, repo.insert_all(source, entries, opts)}
defp apply_operation({:update_all, query, updates, opts}, _acc, _apply_args, repo),
do: {:ok, repo.update_all(query, updates, opts)}
defp apply_operation({:delete_all, query, opts}, _acc, _apply_args, repo),
do: {:ok, repo.delete_all(query, opts)}
defp apply_operation({:put, value}, _acc, _apply_args, _repo),
do: {:ok, value}
defp apply_merge_fun({mod, fun, args}, acc), do: apply(mod, fun, [acc | args])
defp apply_merge_fun(fun, acc), do: apply(fun, [acc])
defp apply_run_fun({mod, fun, args}, repo, acc), do: apply(mod, fun, [repo, acc | args])
defp apply_run_fun(fun, repo, acc), do: apply(fun, [repo, acc])
defp merge_results(changes, new_changes, names) do
new_names = new_changes |> Map.keys() |> MapSet.new()
case MapSet.intersection(names, new_names) |> MapSet.to_list() do
[] ->
{Map.merge(changes, new_changes), MapSet.union(names, new_names)}
common ->
raise "cannot merge Multi; the following operations were found in " <>
"both Ecto.Multi: #{Kernel.inspect(common)}"
end
end
defp operation_fun({:update_all, queryable_fun, updates}, opts) do
fn repo, changes ->
{:ok, repo.update_all(queryable_fun.(changes), updates, opts)}
end
end
defp operation_fun({:insert_all, schema_or_source, entries_fun}, opts) do
fn repo, changes ->
{:ok, repo.insert_all(schema_or_source, entries_fun.(changes), opts)}
end
end
defp operation_fun({:delete_all, fun}, opts) do
fn repo, changes ->
{:ok, repo.delete_all(fun.(changes), opts)}
end
end
defp operation_fun({:one, fun}, opts) do
fn repo, changes ->
{:ok, repo.one(fun.(changes), opts)}
end
end
defp operation_fun({:all, fun}, opts) do
fn repo, changes ->
{:ok, repo.all(fun.(changes), opts)}
end
end
defp operation_fun({:exists?, fun}, opts) do
fn repo, changes ->
{:ok, repo.exists?(fun.(changes), opts)}
end
end
defp operation_fun({operation, fun}, opts) do
fn repo, changes ->
apply(repo, operation, [fun.(changes), opts])
end
end
end

View File

@@ -0,0 +1,220 @@
defmodule Ecto.ParameterizedType do
@moduledoc """
Parameterized types are Ecto types that can be customized per field.
Parameterized types allow a set of options to be specified in the schema
which are initialized on compilation and passed to the callback functions
as the last argument.
For example, `field :foo, :string` behaves the same for every field.
On the other hand, `field :foo, Ecto.Enum, values: [:foo, :bar, :baz]`
will likely have a different set of values per field.
Note that options are specified as a keyword, but it is idiomatic to
convert them to maps inside `c:init/1` for easier pattern matching in
other callbacks.
Parameterized types are a superset of regular types. In other words,
with parameterized types you can do everything a regular type does,
and more. For example, parameterized types can handle `nil` values
in both `load` and `dump` callbacks, they can customize `cast` behavior
per query and per changeset, and also control how values are embedded.
However, parameterized types are also more complex. Therefore, if
everything you need to achieve can be done with basic types, they
should be preferred to parameterized ones.
## Examples
To create a parameterized type, create a module as shown below:
defmodule MyApp.MyType do
use Ecto.ParameterizedType
def type(_params), do: :string
def init(opts) do
validate_opts(opts)
Enum.into(opts, %{})
end
def cast(data, params) do
...
{:ok, cast_data}
end
def load(data, _loader, params) do
...
{:ok, loaded_data}
end
def dump(data, dumper, params) do
...
{:ok, dumped_data}
end
def equal?(a, b, _params) do
a == b
end
end
To use this type in a schema field, specify the type and parameters like this:
schema "foo" do
field :bar, MyApp.MyType, opt1: :baz, opt2: :boo
end
To use this type in a schema field with a composite type, specify the type in a tuple
and opts afterwards.
schema "foo" do
field :bars, {:array, MyApp.MyType}, opt1: :baz, opt2: :boo
end
To use this type in places where you need it to be initialized (for example,
schemaless changesets), you can use `init/2`.
> #### `use Ecto.ParameterizedType` {: .info}
>
> When you `use Ecto.ParameterizedType`, it will set
> `@behaviour Ecto.ParameterizedType` and define default, overridable
> implementations for `c:embed_as/2` and `c:equal?/3`.
"""
@typedoc """
The keyword options passed from the Schema's field macro into `c:init/1`
"""
@type opts :: keyword()
@typedoc """
The parameters for the ParameterizedType
This is the value passed back from `c:init/1` and subsequently passed
as the last argument to all callbacks. Idiomatically it is a map.
"""
@type params :: term()
@doc """
Callback to convert the options specified in the field macro into parameters
to be used in other callbacks.
This function is called at compile time, and should raise if invalid values are
specified. It is idiomatic that the parameters returned from this are a map.
`field` and `schema` will be injected into the options automatically.
For example, this schema specification
schema "my_table" do
field :my_field, MyParameterizedType, opt1: :foo, opt2: nil
end
will result in the call:
MyParameterizedType.init([schema: "my_table", field: :my_field, opt1: :foo, opt2: nil])
"""
@callback init(opts :: opts()) :: params()
@doc """
Casts the given input to the ParameterizedType with the given parameters.
If the parameterized type is also a composite type,
the inner type can be cast by calling `Ecto.Type.cast/2`
directly.
For more information on casting, see `c:Ecto.Type.cast/1`.
"""
@callback cast(data :: term, params()) ::
{:ok, term} | :error | {:error, keyword()}
@doc """
Loads the given term into a ParameterizedType.
It receives a `loader` function in case the parameterized
type is also a composite type. In order to load the inner
type, the `loader` must be called with the inner type and
the inner value as argument.
For more information on loading, see `c:Ecto.Type.load/1`.
Note that this callback *will* be called when loading a `nil`
value, unlike `c:Ecto.Type.load/1`.
"""
@callback load(value :: any(), loader :: function(), params()) :: {:ok, value :: any()} | :error
@doc """
Dumps the given term into an Ecto native type.
It receives a `dumper` function in case the parameterized
type is also a composite type. In order to dump the inner
type, the `dumper` must be called with the inner type and
the inner value as argument.
For more information on dumping, see `c:Ecto.Type.dump/1`.
Note that this callback *will* be called when dumping a `nil`
value, unlike `c:Ecto.Type.dump/1`.
"""
@callback dump(value :: any(), dumper :: function(), params()) :: {:ok, value :: any()} | :error
@doc """
Returns the underlying schema type for the ParameterizedType.
For more information on schema types, see `c:Ecto.Type.type/0`
"""
@callback type(params()) :: Ecto.Type.t()
@doc """
Checks if two terms are semantically equal.
"""
@callback equal?(value1 :: any(), value2 :: any(), params()) :: boolean()
@doc """
Dictates how the type should be treated inside embeds.
For more information on embedding, see `c:Ecto.Type.embed_as/1`
"""
@callback embed_as(format :: atom(), params()) :: :self | :dump
@doc """
Generates a loaded version of the data.
This callback is invoked when a parameterized type is given
to `field` with the `:autogenerate` flag.
"""
@callback autogenerate(params()) :: term()
@doc """
Formats output when a ParameterizedType is printed in exceptions and
other logs.
Note this callback is not used when constructing `Ecto.Changeset` validation
errors. See the `:message` option of most `Ecto.Changeset` validation
functions for how to customize error messaging on a per `Ecto.Changeset` basis.
"""
@callback format(params()) :: String.t()
@optional_callbacks autogenerate: 1, format: 1
@doc """
Inits a parameterized type given by `type` with `opts`.
Useful when manually initializing a type for schemaless changesets.
"""
def init(type, opts) do
{:parameterized, {type, type.init(opts)}}
end
@doc false
defmacro __using__(_) do
quote location: :keep do
@behaviour Ecto.ParameterizedType
@doc false
def embed_as(_, _), do: :self
@doc false
def equal?(term1, term2, _params), do: term1 == term2
defoverridable embed_as: 2, equal?: 3
end
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,974 @@
defmodule Ecto.Query.API do
@moduledoc """
Lists all functions allowed in the query API.
* Comparison operators: `==`, `!=`, `<=`, `>=`, `<`, `>`
* Arithmetic operators: `+`, `-`, `*`, `/`
* Boolean operators: `and`, `or`, `not`
* Inclusion operator: `in/2`
* Subquery operators: `any`, `all` and `exists`
* Search functions: `like/2` and `ilike/2`
* Null check functions: `is_nil/1`
* Aggregates: `count/0`, `count/1`, `avg/1`, `sum/1`, `min/1`, `max/1`
* Date/time intervals: `datetime_add/3`, `date_add/3`, `from_now/2`, `ago/2`
* Inside select: `struct/2`, `map/2`, `merge/2`, `selected_as/2` and literals (map, tuples, lists, etc)
* General: `fragment/1`, `field/2`, `type/2`, `as/1`, `parent_as/1`, `selected_as/1`
Note the functions in this module exist for documentation
purposes and one should never need to invoke them directly.
Furthermore, it is possible to define your own macros and
use them in Ecto queries (see docs for `fragment/1`).
## Intervals
Ecto supports following values for `interval` option: `"year"`, `"month"`,
`"week"`, `"day"`, `"hour"`, `"minute"`, `"second"`, `"millisecond"`, and
`"microsecond"`.
`Date`/`Time` functions like `datetime_add/3`, `date_add/3`, `from_now/2`,
`ago/2` take `interval` as an argument.
## Window API
Ecto also supports many of the windows functions found
in SQL databases. See `Ecto.Query.WindowAPI` for more
information.
## About the arithmetic operators
The Ecto implementation of these operators provide only
a thin layer above the adapters. So if your adapter allows you
to use them in a certain way (like adding a date and an
interval in PostgreSQL), it should work just fine in Ecto
queries.
"""
@dialyzer :no_return
@doc """
Binary `==` operation.
"""
def left == right, do: doc!([left, right])
@doc """
Binary `!=` operation.
"""
def left != right, do: doc!([left, right])
@doc """
Binary `<=` operation.
"""
def left <= right, do: doc!([left, right])
@doc """
Binary `>=` operation.
"""
def left >= right, do: doc!([left, right])
@doc """
Binary `<` operation.
"""
def left < right, do: doc!([left, right])
@doc """
Binary `>` operation.
"""
def left > right, do: doc!([left, right])
@doc """
Binary `+` operation.
"""
def left + right, do: doc!([left, right])
@doc """
Binary `-` operation.
"""
def left - right, do: doc!([left, right])
@doc """
Binary `*` operation.
"""
def left * right, do: doc!([left, right])
@doc """
Binary `/` operation.
"""
def left / right, do: doc!([left, right])
@doc """
Binary `and` operation.
"""
def left and right, do: doc!([left, right])
@doc """
Binary `or` operation.
"""
def left or right, do: doc!([left, right])
@doc """
Unary `not` operation.
It is used to negate values in `:where`. It is also used to match
the assert the opposite of `in/2`, `is_nil/1`, and `exists/1`.
For example:
from p in Post, where: p.id not in [1, 2, 3]
from p in Post, where: not is_nil(p.title)
# Retrieve all the posts that doesn't have comments.
from p in Post,
as: :post,
where:
not exists(
from(
c in Comment,
where: parent_as(:post).id == c.post_id
)
)
"""
def not value, do: doc!([value])
@doc """
Checks if the left-value is included in the right one.
from p in Post, where: p.id in [1, 2, 3]
The right side may either be a literal list, an interpolated list,
any struct that implements the `Enumerable` protocol, or even a
column in the database with array type:
from p in Post, where: "elixir" in p.tags
Additionally, the right side may also be a subquery, which should return
a single column:
from c in Comment, where: c.post_id in subquery(
from(p in Post, where: p.created_at > ^since, select: p.id)
)
"""
def left in right, do: doc!([left, right])
@doc """
Evaluates to true if the provided subquery returns 1 or more rows.
from p in Post,
as: :post,
where:
exists(
from(
c in Comment,
where: parent_as(:post).id == c.post_id and c.replies_count > 5,
select: 1
)
)
This is best used in conjunction with `parent_as/1` to correlate the subquery
with the parent query to test some condition on related rows in a different table.
In the above example the query returns posts which have at least one comment that
has more than 5 replies.
"""
def exists(subquery), do: doc!([subquery])
@doc """
Tests whether one or more values returned from the provided subquery match in a comparison operation.
from p in Product, where: p.id == any(
from(li in LineItem, select: [li.product_id], where: li.created_at > ^since and li.qty >= 10)
)
A product matches in the above example if a line item was created since the provided date where the customer purchased
at least 10 units.
Both `any` and `all` must be given a subquery as an argument, and they must be used on the right hand side of a comparison.
Both can be used with every comparison operator: `==`, `!=`, `>`, `>=`, `<`, `<=`.
"""
def any(subquery), do: doc!([subquery])
@doc """
Evaluates whether all values returned from the provided subquery match in a comparison operation.
from p in Post, where: p.visits >= all(
from(p in Post, select: avg(p.visits), group_by: [p.category_id])
)
For a post to match in the above example it must be visited at least as much as the average post in all categories.
from p in Post, where: p.visits == all(
from(p in Post, select: max(p.visits))
)
The above example matches all the posts which are tied for being the most visited.
Both `any` and `all` must be given a subquery as an argument, and they must be used on the right hand side of a comparison.
Both can be used with every comparison operator: `==`, `!=`, `>`, `>=`, `<`, `<=`.
"""
def all(subquery), do: doc!([subquery])
@doc """
Searches for `search` in `string`.
from p in Post, where: like(p.body, "Chapter%")
Translates to the underlying SQL LIKE query, therefore
its behaviour is dependent on the database. In particular,
PostgreSQL will do a case-sensitive operation, while the
majority of other databases will be case-insensitive. For
performing a case-insensitive `like` in PostgreSQL, see `ilike/2`.
You should be very careful when allowing user sent data to be used
as part of LIKE query, since they allow to perform
[LIKE-injections](https://githubengineering.com/like-injection/).
"""
def like(string, search), do: doc!([string, search])
@doc """
Searches for `search` in `string` in a case insensitive fashion.
from p in Post, where: ilike(p.body, "Chapter%")
Translates to the underlying SQL ILIKE query. This operation is
only available on PostgreSQL.
"""
def ilike(string, search), do: doc!([string, search])
@doc """
Checks if the given value is nil.
from p in Post, where: is_nil(p.published_at)
To check if a given value is not nil use:
from p in Post, where: not is_nil(p.published_at)
"""
def is_nil(value), do: doc!([value])
@doc """
Counts the entries in the table.
from p in Post, select: count()
"""
def count, do: doc!([])
@doc """
Counts the given entry.
from p in Post, select: count(p.id)
"""
def count(value), do: doc!([value])
@doc """
Counts the distinct values in given entry.
from p in Post, select: count(p.id, :distinct)
"""
def count(value, :distinct), do: doc!([value, :distinct])
@doc """
Takes the first value which is not null, or null if they both are.
In SQL, COALESCE takes any number of arguments, but in ecto
it only takes two, so it must be chained to achieve the same
effect.
from p in Payment, select: p.value |> coalesce(p.backup_value) |> coalesce(0)
"""
def coalesce(value, expr), do: doc!([value, expr])
@doc """
Applies the given expression as a FILTER clause against an
aggregate. This is currently only supported by Postgres.
from p in Payment, select: filter(avg(p.value), p.value > 0 and p.value < 100)
from p in Payment, select: avg(p.value) |> filter(p.value < 0)
"""
def filter(value, filter), do: doc!([value, filter])
@doc """
Calculates the average for the given entry.
from p in Payment, select: avg(p.value)
"""
def avg(value), do: doc!([value])
@doc """
Calculates the sum for the given entry.
from p in Payment, select: sum(p.value)
"""
def sum(value), do: doc!([value])
@doc """
Calculates the minimum for the given entry.
from p in Payment, select: min(p.value)
"""
def min(value), do: doc!([value])
@doc """
Calculates the maximum for the given entry.
from p in Payment, select: max(p.value)
"""
def max(value), do: doc!([value])
@doc """
Adds a given interval to a datetime.
The first argument is a `datetime`, the second one is the count
for the interval, which may be either positive or negative and
the interval value:
# Get all items published since the last month
from p in Post, where: p.published_at >
datetime_add(^NaiveDateTime.utc_now(), -1, "month")
In the example above, we used `datetime_add/3` to subtract one month
from the current datetime and compared it with the `p.published_at`.
If you want to perform operations on date, `date_add/3` could be used.
See [Intervals](#module-intervals) for supported `interval` values.
"""
def datetime_add(datetime, count, interval), do: doc!([datetime, count, interval])
@doc """
Adds a given interval to a date.
See `datetime_add/3` for more information.
See [Intervals](#module-intervals) for supported `interval` values.
"""
def date_add(date, count, interval), do: doc!([date, count, interval])
@doc """
Adds the given interval to the current time in UTC.
The current time in UTC is retrieved from Elixir and
not from the database.
See [Intervals](#module-intervals) for supported `interval` values.
## Examples
from a in Account, where: a.expires_at < from_now(3, "month")
"""
def from_now(count, interval), do: doc!([count, interval])
@doc """
Subtracts the given interval from the current time in UTC.
The current time in UTC is retrieved from Elixir and
not from the database.
See [Intervals](#module-intervals) for supported `interval` values.
## Examples
from p in Post, where: p.published_at > ago(3, "month")
"""
def ago(count, interval), do: doc!([count, interval])
@doc """
Send fragments directly to the database.
It is not possible to represent all possible database queries using
Ecto's query syntax. When such is required, it is possible to use
fragments to send any expression to the database:
def unpublished_by_title(title) do
from p in Post,
where: is_nil(p.published_at) and
fragment("lower(?)", p.title) == ^title
end
Every occurrence of the `?` character will be interpreted as a place
for parameters, which must be given as additional arguments to
`fragment`. If the literal character `?` is required as part of the
fragment, it can be escaped with `\\\\?` (one escape for strings,
another for fragment).
In the example above, we are using the lower procedure in the
database to downcase the title column.
It is very important to keep in mind that Ecto is unable to do any
type casting when fragments are used. Therefore it may be necessary
to explicitly cast parameters via `type/2`:
fragment("lower(?)", p.title) == type(^title, :string)
## Identifiers and Constants
Sometimes you need to interpolate an identifier or a constant value into a fragment,
instead of a query parameter. The latter can happen if your database does not allow
parameterizing certain clauses. For example:
collation = "es_ES"
fragment("? COLLATE ?", ^name, ^collation)
limit = "10"
"posts" |> select([p], p.title) |> limit(fragment("?", ^limit))
The first example above won't work because `collation` needs to be quoted as an identifier.
The second example won't work on databases that do not allow passing query parameters
as part of `limit`.
You can address this by telling Ecto to treat these values differently than a query parameter:
fragment("? COLLATE ?", ^name, identifier(^collation))
"posts" |> select([p], p.title) |> limit(fragment("?", ^constant(limit))
Ecto will make these values directly part of the query, handling quoting and escaping where necessary.
> #### Query caching {: .warning}
>
> Because identifiers and constants are made part of the query, each different
> value will generate a separate query, with its own cache.
## Splicing
Sometimes you may need to interpolate a variable number of arguments
into the same fragment. For example, when overriding Ecto's default
`where` behaviour for Postgres:
from p in Post, where: fragment("? in (?, ?)", p.id, val1, val2)
The example above will only work if you know the number of arguments
upfront. If it can vary, the above will not work.
You can address this by telling Ecto to splice a list argument into
the fragment:
from p in Post, where: fragment("? in (?)", p.id, splice(^val_list))
This will let Ecto know it should expand the values of the list into
separate fragment arguments. For example:
from p in Post, where: fragment("? in (?)", p.id, splice(^[1, 2, 3]))
would be expanded into
from p in Post, where: fragment("? in (?,?,?)", p.id, ^1, ^2, ^3)
## Defining custom functions using macros and fragment
You can add a custom Ecto query function using macros. For example
to expose SQL's coalesce function you can define this macro:
defmodule CustomFunctions do
defmacro coalesce(left, right) do
quote do
fragment("coalesce(?, ?)", unquote(left), unquote(right))
end
end
end
To have coalesce/2 available, just import the module that defines it.
import CustomFunctions
The only downside is that it will show up as a fragment when
inspecting the Elixir query. Other than that, it should be
equivalent to a built-in Ecto query function.
## Keyword fragments
In order to support databases that do not have string-based
queries, like MongoDB, fragments also allow keywords to be given:
from p in Post,
where: fragment(title: ["$eq": ^some_value])
"""
def fragment(fragments), do: doc!([fragments])
@doc """
Allows a dynamic identifier to be injected into a fragment:
collation = "es_ES"
select("posts", [p], fragment("? COLLATE ?", p.title, identifier(^collation)))
The example above will inject the value of `collation` directly
into the query instead of treating it as a query parameter. It will
generate a query such as `SELECT p0.title COLLATE "es_ES" FROM "posts" AS p0`
as opposed to `SELECT p0.title COLLATE $1 FROM "posts" AS p0`.
Note that each different value of `collation` will emit a different query,
which will be independently prepared and cached.
"""
def identifier(binary), do: doc!([binary])
@doc """
Allows a dynamic string or number to be injected into a fragment:
limit = 10
"posts" |> select([p], p.title) |> limit(fragment("?", constant(^limit)))
The example above will inject the value of `limit` directly
into the query instead of treating it as a query parameter. It will
generate a query such as `SELECT p0.title FROM "posts" AS p0 LIMIT 1`
as opposed to `SELECT p0.title FROM "posts" AS p0 LIMIT $1`.
Note that each different value of `limit` will emit a different query,
which will be independently prepared and cached.
"""
def constant(value), do: doc!([value])
@doc """
Allows a list argument to be spliced into a fragment.
from p in Post, where: fragment("? in (?)", p.id, splice(^[1, 2, 3]))
The example above will be transformed at runtime into the following:
from p in Post, where: fragment("? in (?,?,?)", p.id, ^1, ^2, ^3)
You may only splice runtime values. For example, this would not work because
query bindings are compile-time constructs:
from p in Post, where: fragment("concat(?)", splice(^[p.count, " ", "count"]))
"""
def splice(list), do: doc!([list])
@doc """
Creates a values list/constant table.
A values list can be used as a source in a query, both in `Ecto.Query.from/2`
and `Ecto.Query.join/5`.
The first argument is a list of maps representing the values of the constant table.
An error is raised if the list is empty or if every map does not have exactly the
same fields.
The second argument is either a map of types or an Ecto schema containing all the
fields in the first argument.
Each field must be given a type or an error is raised. Any type that can be specified in
a schema may be used.
Queries using a values list are not cacheable by Ecto.
## Select with map types example
values = [%{id: 1, text: "abc"}, %{id: 2, text: "xyz"}]
types = %{id: :integer, text: :string}
query =
from v1 in values(values, types),
join: v2 in values(values, types),
on: v1.id == v2.id
Repo.all(query)
## Select with schema types example
values = [%{id: 1, text: "abc"}, %{id: 2, text: "xyz"}]
types = ValuesSchema
query =
from v1 in values(values, types),
join: v2 in values(values, types),
on: v1.id == v2.id
Repo.all(query)
## Delete example
values = [%{id: 1, text: "abc"}, %{id: 2, text: "xyz"}]
types = %{id: :integer, text: :string}
query =
from p in Post,
join: v in values(values, types),
on: p.id == v.id,
where: p.counter == ^0
Repo.delete_all(query)
## Update example
values = [%{id: 1, text: "abc"}, %{id: 2, text: "xyz"}]
types = %{id: :integer, text: :string}
query =
from p in Post,
join: v in values(values, types),
on: p.id == v.id,
update: [set: [text: v.text]]
Repo.update_all(query, [])
"""
def values(values, types), do: doc!([values, types])
@doc """
Allows a field to be dynamically accessed.
The source name can be a binding (`p` in `from p in Post`) or a named binding
using `as/1` or `parent_as/1`. The named binding maybe a literal atom or an
interpolation.
The field name can be given as either an atom or a string. In a schemaless
query, the two types of names behave the same. However, when referencing
a field from a schema the behaviours are different.
Using an atom to reference a schema field will inherit all the properties from
the schema. For example, the field name will be changed to the value of `:source`
before generating the final query and its type behaviour will be dictated by the
one specified in the schema.
Using a string to reference a schema field is equivalent to bypassing all of the
above and accessing the field directly from the source (i.e. the underlying table).
This means the name will not be changed to the value of `:source` and the type
behaviour will be dictated by the underlying driver (e.g. Postgrex or MyXQL).
Take the following schema and query:
defmodule Car do
use Ecto.Schema
schema "cars" do
field :doors, source: :num_doors
field :tires, source: :num_tires
end
end
def at_least_four(doors_or_tires) do
from c in Car,
where: field(c, ^doors_or_tires) >= 4
end
def at_least_four(query, doors_or_tires) do
from q in query,
where: field(as(:car), ^doors_or_tires) >= 4
end
def at_least_four(query, binding, doors_or_tires) do
from q in query,
where: field(as(^binding), ^doors_or_tires) >= 4
end
In the example above, `at_least_four(:doors)` and `at_least_four("num_doors")`
would be valid ways to return the set of cars having at least 4 doors.
String names can be particularly useful when your application is dynamically
generating many schemaless queries at runtime and you want to avoid creating
a large number of atoms.
"""
def field(source, field), do: doc!([source, field])
@doc """
Used in `select` to specify which struct fields should be returned.
For example, if you don't need all fields to be returned
as part of a struct, you can filter it to include only certain
fields by using `struct/2`:
from p in Post,
select: struct(p, [:title, :body])
`struct/2` can also be used to dynamically select fields:
fields = [:title, :body]
from p in Post, select: struct(p, ^fields)
As a convenience, `select` allows developers to take fields
without an explicit call to `struct/2`:
from p in Post, select: [:title, :body]
Or even dynamically:
fields = [:title, :body]
from p in Post, select: ^fields
For preloads, the selected fields may be specified from the parent:
from(city in City, preload: :country,
select: struct(city, [:country_id, :name, country: [:id, :population]]))
If the same source is selected multiple times with a `struct`,
the fields are merged in order to avoid fetching multiple copies
from the database. In other words, the expression below:
from(city in City, preload: :country,
select: {struct(city, [:country_id]), struct(city, [:name])})
is expanded to:
from(city in City, preload: :country,
select: {struct(city, [:country_id, :name]), struct(city, [:country_id, :name])})
**IMPORTANT**: When filtering fields for associations, you
MUST include the foreign keys used in the relationship,
otherwise Ecto will be unable to find associated records.
"""
def struct(source, fields), do: doc!([source, fields])
@doc """
Used in `select` to specify which fields should be returned as a map.
For example, if you don't need all fields to be returned or
neither need a struct, you can use `map/2` to achieve both:
from p in Post,
select: map(p, [:title, :body])
`map/2` can also be used to dynamically select fields:
fields = [:title, :body]
from p in Post, select: map(p, ^fields)
If the same source is selected multiple times with a `map`,
the fields are merged in order to avoid fetching multiple copies
from the database. In other words, the expression below:
from(city in City, preload: :country,
select: {map(city, [:country_id]), map(city, [:name])})
is expanded to:
from(city in City, preload: :country,
select: {map(city, [:country_id, :name]), map(city, [:country_id, :name])})
For preloads, the selected fields may be specified from the parent:
from(city in City, preload: :country,
select: map(city, [:country_id, :name, country: [:id, :population]]))
It's also possible to select a struct from one source but only a subset of
fields from one of its associations:
from(city in City, preload: :country,
select: %{city | country: map(country: [:id, :population])})
**IMPORTANT**: When filtering fields for associations, you
MUST include the foreign keys used in the relationship,
otherwise Ecto will be unable to find associated records.
"""
def map(source, fields), do: doc!([source, fields])
@doc """
Merges the map on the right over the map on the left.
If the map on the left side is a struct, Ecto will check
all of the field on the right previously exist on the left
before merging.
from(city in City, select: merge(city, %{virtual_field: "some_value"}))
This function is primarily used by `Ecto.Query.select_merge/3`
to merge different select clauses.
"""
def merge(left_map, right_map), do: doc!([left_map, right_map])
@doc """
Returns value from the `json_field` pointed to by `path`.
from(post in Post, select: json_extract_path(post.meta, ["author", "name"]))
The path can be dynamic:
path = ["author", "name"]
from(post in Post, select: json_extract_path(post.meta, ^path))
And the field can also be dynamic in combination with it:
path = ["author", "name"]
from(post in Post, select: json_extract_path(field(post, :meta), ^path))
The query can be also rewritten as:
from(post in Post, select: post.meta["author"]["name"])
Path elements can be integers to access values in JSON arrays:
from(post in Post, select: post.meta["tags"][0]["name"])
Some adapters allow path elements to be references to query source fields
from(post in Post, select: post.meta[p.title])
from(p in Post, join: u in User, on: p.user_id == u.id, select: p.meta[u.name])
Any element of the path can be dynamic:
field = "name"
from(post in Post, select: post.meta["author"][^field])
source_field = :source_column
from(post in Post, select: post.meta["author"][field(p, ^source_field)])
## Warning: indexes on PostgreSQL
PostgreSQL supports indexing on jsonb columns via GIN indexes.
Whenever comparing the value of a jsonb field against a string
or integer, Ecto will use the containment operator @> which
is optimized. You can even use the more efficient `jsonb_path_ops`
GIN index variant. For more information, consult PostgreSQL's docs
on [JSON indexing](https://www.postgresql.org/docs/current/datatype-json.html#JSON-INDEXING).
## Warning: return types
The underlying data in the JSON column is returned without any
additional decoding. This means "null" JSON values are not the
same as SQL's "null". For example, the `Repo.all` operation below
returns an empty list because `p.meta["author"]` returns JSON's
null and therefore `is_nil` does not succeed:
Repo.insert!(%Post{meta: %{author: nil}})
Repo.all(from(post in Post, where: is_nil(p.meta["author"])))
Similarly, other types, such as datetimes, are returned as strings.
This means conditions like `post.meta["published_at"] > from_now(-1, "day")`
may return incorrect results or fail as the underlying database
tries to compare incompatible types. You can, however, use `type/2`
to force the types on the database level.
"""
def json_extract_path(json_field, path), do: doc!([json_field, path])
@doc """
Casts the given value to the given type at the database level.
Most of the times, Ecto is able to proper cast interpolated
values due to its type checking mechanism. In some situations
though, you may want to tell Ecto that a parameter has some
particular type:
type(^title, :string)
It is also possible to say the type must match the same of a column:
type(^title, p.title)
Or a parameterized type, which must be previously initialized
with `Ecto.ParameterizedType.init/2`:
@my_enum Ecto.ParameterizedType.init(Ecto.Enum, values: [:foo, :bar, :baz])
type(^title, ^@my_enum)
Ecto will ensure `^title` is cast to the given type and enforce such
type at the database level. If the value is returned in a `select`,
Ecto will also enforce the proper type throughout.
When performing arithmetic operations, `type/2` can be used to cast
all the parameters in the operation to the same type:
from p in Post,
select: type(p.visits + ^a_float + ^a_integer, :decimal)
Inside `select`, `type/2` can also be used to cast fragments:
type(fragment("NOW"), :naive_datetime)
Or to type fields from schemaless queries:
from p in "posts", select: type(p.cost, :decimal)
Or to type aggregation results:
from p in Post, select: type(avg(p.cost), :integer)
from p in Post, select: type(filter(avg(p.cost), p.cost > 0), :integer)
Or to type comparison expression results:
from p in Post, select: type(coalesce(p.cost, 0), :integer)
Or to type fields from a parent query using `parent_as/1`:
child = from c in Comment, where: type(parent_as(:posts).id, :string) == c.text
from Post, as: :posts, inner_lateral_join: c in subquery(child), select: c.text
## `type` vs `fragment`
`type/2` is all about Ecto types. Therefore, you can perform `type(expr, :string)`
but not `type(expr, :text)`, because `:text` is not an actual Ecto type. If you want
to perform casting exclusively at the database level, you can use fragment. For example,
in PostgreSQL, you might do `fragment("?::text", p.column)`.
"""
def type(interpolated_value, type), do: doc!([interpolated_value, type])
@doc """
Refer to a named atom binding.
See [Named Bindings](Ecto.Query.html#module-named-bindings) for more information.
"""
def as(binding), do: doc!([binding])
@doc """
Refer to a named atom binding in the parent query.
This is available only inside subqueries.
See [Named Bindings](Ecto.Query.html#module-named-bindings) for more information.
"""
def parent_as(binding), do: doc!([binding])
@doc """
Refer to an alias of a selected value.
This can be used to refer to aliases created using `selected_as/2`. If
the alias hasn't been created using `selected_as/2`, an error will be raised.
Each database has its own rules governing which clauses can reference these aliases.
If an error is raised mentioning an unknown column, most likely the alias is being
referenced somewhere that is not allowed. Consult the documentation for the database
to ensure the alias is being referenced correctly.
"""
def selected_as(name), do: doc!([name])
@doc """
Creates an alias for the given selected value.
When working with calculated values, an alias can be used to simplify
the query. Otherwise, the entire expression would need to be copied when
referencing it outside of select statements.
This comes in handy when, for instance, you would like to use the calculated
value in `Ecto.Query.group_by/3` or `Ecto.Query.order_by/3`:
from p in Post,
select: %{
posted: selected_as(p.posted, :date),
sum_visits: p.visits |> coalesce(0) |> sum() |> selected_as(:sum_visits)
},
group_by: selected_as(:date),
order_by: selected_as(:sum_visits)
The name of the alias must be an atom and it can only be used in the outer most
select expression, otherwise an error is raised. Please note that the alias name
does not have to match the key when `select` returns a map, struct or keyword list.
Using this in conjunction with `selected_as/1` is recommended to ensure only defined aliases
are referenced.
## Subqueries and CTEs
Subqueries and CTEs automatically alias the selected fields, for example, one can write:
# Subquery
s = from p in Post, select: %{visits: coalesce(p.visits, 0)}
from(s in subquery(s), select: s.visits)
# CTE
cte_query = from p in Post, select: %{visits: coalesce(p.visits, 0)}
Post |> with_cte("cte", as: ^cte_query) |> join(:inner, [p], c in "cte") |> select([p, c], c.visits)
However, one can also use `selected_as` to override the default naming:
# Subquery
s = from p in Post, select: %{visits: coalesce(p.visits, 0) |> selected_as(:num_visits)}
from(s in subquery(s), select: s.num_visits)
# CTE
cte_query = from p in Post, select: %{visits: coalesce(p.visits, 0) |> selected_as(:num_visits)}
Post |> with_cte("cte", as: ^cte_query) |> join(:inner, [p], c in "cte") |> select([p, c], c.num_visits)
The name given to `selected_as/2` can also be referenced in `selected_as/1`,
as in regular queries.
"""
def selected_as(selected_value, name), do: doc!([selected_value, name])
defp doc!(_) do
raise "the functions in Ecto.Query.API should not be invoked directly, " <>
"they serve for documentation purposes only"
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
import Kernel, except: [apply: 2]
defmodule Ecto.Query.Builder.Combination do
@moduledoc false
alias Ecto.Query.Builder
@doc """
Builds a quoted expression.
The quoted expression should evaluate to a query at runtime.
If possible, it does all calculations at compile time to avoid
runtime work.
"""
@spec build(atom, Macro.t, Macro.t, Macro.Env.t) :: Macro.t
def build(kind, query, {:^, _, [expr]}, env) do
expr = quote do: Ecto.Queryable.to_query(unquote(expr))
Builder.apply_query(query, __MODULE__, [[{kind, expr}]], env)
end
def build(kind, _query, other, _env) do
Builder.error! "`#{Macro.to_string(other)}` is not a valid #{kind}. " <>
"#{kind} must always be an interpolated query, such as ^existing_query"
end
@doc """
The callback applied by `build/4` to build the query.
"""
@spec apply(Ecto.Queryable.t, term) :: Ecto.Query.t
def apply(%Ecto.Query{combinations: combinations} = query, value) do
%{query | combinations: combinations ++ value}
end
def apply(query, value) do
apply(Ecto.Queryable.to_query(query), value)
end
end

View File

@@ -0,0 +1,121 @@
import Kernel, except: [apply: 3]
defmodule Ecto.Query.Builder.CTE do
@moduledoc false
alias Ecto.Query.Builder
@doc """
Escapes the CTE name.
iex> escape(quote(do: "FOO"), __ENV__)
"FOO"
"""
@spec escape(Macro.t, Macro.Env.t) :: Macro.t
def escape(name, _env) when is_bitstring(name), do: name
def escape({:^, _, [expr]}, _env), do: expr
def escape(expr, env) do
case Macro.expand_once(expr, env) do
^expr ->
Builder.error! "`#{Macro.to_string(expr)}` is not a valid CTE name. " <>
"It must be a literal string or an interpolated variable."
expr ->
escape(expr, env)
end
end
@doc """
Builds a quoted expression.
The quoted expression should evaluate to a query at runtime.
If possible, it does all calculations at compile time to avoid
runtime work.
"""
@spec build(Macro.t, Macro.t, Macro.t, nil | boolean(), nil | :all | :update_all | :delete_all , Macro.Env.t) :: Macro.t
def build(query, name, cte, materialized, operation, env) do
Builder.apply_query(query, __MODULE__, [escape(name, env), build_cte(name, cte, env), materialized, operation], env)
end
@spec build_cte(Macro.t, Macro.t, Macro.Env.t) :: Macro.t
def build_cte(_name, {:^, _, [expr]}, _env) do
quote do: Ecto.Queryable.to_query(unquote(expr))
end
def build_cte(_name, {:fragment, _, _} = fragment, env) do
{expr, {params, _acc}} = Builder.escape(fragment, :any, {[], %{}}, [], env)
params = Builder.escape_params(params)
quote do
%Ecto.Query.QueryExpr{
expr: unquote(expr),
params: unquote(params),
file: unquote(env.file),
line: unquote(env.line)
}
end
end
def build_cte(name, cte, env) do
case Macro.expand_once(cte, env) do
^cte ->
Builder.error! "`#{Macro.to_string(cte)}` is not a valid CTE (named: #{Macro.to_string(name)}). " <>
"The CTE must be an interpolated query, such as ^existing_query or a fragment."
cte ->
build_cte(name, cte, env)
end
end
@doc """
The callback applied by `build/4` to build the query.
"""
@spec apply(Ecto.Queryable.t, bitstring, Ecto.Queryable.t, nil | boolean(), nil | :all | :update_all | :delete_all) :: Ecto.Query.t
# Runtime
def apply(query, name, with_query, materialized, nil) do
apply(query, name, with_query, materialized, :all)
end
def apply(_query, _name, _with_query, _materialized, operation)
when operation not in [:all, :update_all, :delete_all] do
Builder.error!("`operation` option must be one of :all, :update_all, or :delete_all")
end
def apply(%Ecto.Query{with_ctes: with_expr} = query, name, %_{} = with_query, materialized, operation) do
%{query | with_ctes: apply_cte(with_expr, name, with_query, materialized, operation)}
end
# Compile
def apply(%Ecto.Query{with_ctes: with_expr} = query, name, with_query, materialized, operation) do
update = quote do
Ecto.Query.Builder.CTE.apply_cte(unquote(with_expr), unquote(name), unquote(with_query), unquote(materialized), unquote(operation))
end
%{query | with_ctes: update}
end
# Runtime catch-all
def apply(query, name, with_query, materialized, operation) do
apply(Ecto.Queryable.to_query(query), name, with_query, materialized, operation)
end
@doc false
def apply_cte(nil, name, with_query, materialized, operation) when is_boolean(materialized) do
%Ecto.Query.WithExpr{queries: [{name, %{materialized: materialized, operation: operation}, with_query}]}
end
def apply_cte(nil, name, with_query, _materialized, operation) do
%Ecto.Query.WithExpr{queries: [{name, %{operation: operation}, with_query}]}
end
def apply_cte(%Ecto.Query.WithExpr{queries: queries} = with_expr, name, with_query, materialized, operation) when is_boolean(materialized) do
%{with_expr | queries: List.keystore(queries, name, 0, {name, %{materialized: materialized, operation: operation}, with_query})}
end
def apply_cte(%Ecto.Query.WithExpr{queries: queries} = with_expr, name, with_query, _materialized, operation) do
%{with_expr | queries: List.keystore(queries, name, 0, {name, %{operation: operation}, with_query})}
end
end

View File

@@ -0,0 +1,91 @@
import Kernel, except: [apply: 2]
defmodule Ecto.Query.Builder.Distinct do
@moduledoc false
alias Ecto.Query.Builder
@doc """
Escapes a list of quoted expressions.
iex> escape(quote do true end, {[], %{}}, [], __ENV__)
{true, {[], %{}}}
iex> escape(quote do [x.x, 13] end, {[], %{}}, [x: 0], __ENV__)
{[asc: {:{}, [], [{:{}, [], [:., [], [{:{}, [], [:&, [], [0]]}, :x]]}, [], []]},
asc: 13],
{[], %{}}}
"""
@spec escape(Macro.t, {list, term}, Keyword.t, Macro.Env.t) :: {Macro.t, {list, term}}
def escape(expr, params_acc, _vars, _env) when is_boolean(expr) do
{expr, params_acc}
end
def escape(expr, params_acc, vars, env) do
Builder.OrderBy.escape(:distinct, expr, params_acc, vars, env)
end
@doc """
Called at runtime to verify distinct.
"""
def distinct!(query, distinct, file, line) when is_boolean(distinct) do
apply(query, %Ecto.Query.ByExpr{expr: distinct, params: [], line: line, file: file})
end
def distinct!(query, distinct, file, line) do
{expr, params, subqueries} =
Builder.OrderBy.order_by_or_distinct!(:distinct, query, distinct, [])
expr = %Ecto.Query.ByExpr{
expr: expr,
params: Enum.reverse(params),
line: line,
file: file,
subqueries: subqueries
}
apply(query, expr)
end
@doc """
Builds a quoted expression.
The quoted expression should evaluate to a query at runtime.
If possible, it does all calculations at compile time to avoid
runtime work.
"""
@spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t
def build(query, _binding, {:^, _, [var]}, env) do
quote do
Ecto.Query.Builder.Distinct.distinct!(unquote(query), unquote(var), unquote(env.file), unquote(env.line))
end
end
def build(query, binding, expr, env) do
{query, binding} = Builder.escape_binding(query, binding, env)
{expr, {params, acc}} = escape(expr, {[], %{subqueries: []}}, binding, env)
params = Builder.escape_params(params)
distinct = quote do: %Ecto.Query.ByExpr{
expr: unquote(expr),
params: unquote(params),
subqueries: unquote(acc.subqueries),
file: unquote(env.file),
line: unquote(env.line)}
Builder.apply_query(query, __MODULE__, [distinct], env)
end
@doc """
The callback applied by `build/4` to build the query.
"""
@spec apply(Ecto.Queryable.t, term) :: Ecto.Query.t
def apply(%Ecto.Query{distinct: nil} = query, expr) do
%{query | distinct: expr}
end
def apply(%Ecto.Query{}, _expr) do
Builder.error! "only one distinct expression is allowed in query"
end
def apply(query, expr) do
apply(Ecto.Queryable.to_query(query), expr)
end
end

View File

@@ -0,0 +1,112 @@
import Kernel, except: [apply: 2]
defmodule Ecto.Query.Builder.Dynamic do
@moduledoc false
alias Ecto.Query.Builder
alias Ecto.Query.Builder.Select
@doc """
Builds a dynamic expression.
"""
@spec build([Macro.t], Macro.t, Macro.Env.t) :: Macro.t
def build(binding, expr, env) do
{query, vars} = Builder.escape_binding(quote(do: query), binding, env)
{expr, {params, acc}} = escape(expr, {[], %{subqueries: [], aliases: %{}}}, vars, env)
aliases = escape_select_aliases(acc.aliases)
params = Builder.escape_params(params)
quote do
%Ecto.Query.DynamicExpr{fun: fn query ->
_ = unquote(query)
{unquote(expr), unquote(params), unquote(Enum.reverse(acc.subqueries)), unquote(aliases)}
end,
binding: unquote(Macro.escape(binding)),
file: unquote(env.file),
line: unquote(env.line)}
end
end
defp escape({:selected_as, _, [_, _]} = expr, _params_acc, vars, env) do
Select.escape(expr, vars, env)
end
defp escape({:%{}, _, _} = expr, _params_acc, vars, env) do
Select.escape(expr, vars, env)
end
defp escape(expr, params_acc, vars, env) do
Builder.escape(expr, :any, params_acc, vars, {env, &escape_expansion/5})
end
defp escape_expansion(expr, _type, params_acc, vars, env) do
escape(expr, params_acc, vars, env)
end
defp escape_select_aliases(%{} = aliases), do: Builder.escape_select_aliases(aliases)
defp escape_select_aliases(aliases), do: aliases
@doc """
Expands a dynamic expression for insertion into the given query.
"""
def fully_expand(query, %{file: file, line: line, binding: binding} = dynamic) do
{expr, {binding, params, subqueries, _aliases, _count}} = expand(query, dynamic, {binding, [], [], %{}, 0})
{expr, binding, Enum.reverse(params), Enum.reverse(subqueries), file, line}
end
@doc """
Expands a dynamic expression as part of an existing expression.
Any dynamic expression parameter is prepended and the parameters
list is not reversed. This is useful when the dynamic expression
is given in the middle of an expression.
"""
def partially_expand(query, %{binding: binding} = dynamic, params, subqueries, aliases, count) do
{expr, {_binding, params, subqueries, aliases, count}} =
expand(query, dynamic, {binding, params, subqueries, aliases, count})
{expr, params, subqueries, aliases, count}
end
def partially_expand(kind, query, %{binding: binding} = dynamic, params, count) do
{expr, {_binding, params, subqueries, _aliases, count}} =
expand(query, dynamic, {binding, params, [], %{}, count})
if subqueries != [] do
raise ArgumentError, "subqueries are not allowed in `#{kind}` expressions"
end
{expr, params, count}
end
defp expand(query, %{fun: fun}, {binding, params, subqueries, aliases, count}) do
{dynamic_expr, dynamic_params, dynamic_subqueries, dynamic_aliases} = fun.(query)
aliases = merge_aliases(aliases, dynamic_aliases)
Macro.postwalk(dynamic_expr, {binding, params, subqueries, aliases, count}, fn
{:^, meta, [ix]}, {binding, params, subqueries, aliases, count} ->
case Enum.fetch!(dynamic_params, ix) do
{%Ecto.Query.DynamicExpr{binding: new_binding} = dynamic, _} ->
binding = if length(new_binding) > length(binding), do: new_binding, else: binding
expand(query, dynamic, {binding, params, subqueries, aliases, count})
param ->
{{:^, meta, [count]}, {binding, [param | params], subqueries, aliases, count + 1}}
end
{:subquery, i}, {binding, params, subqueries, aliases, count} ->
subquery = Enum.fetch!(dynamic_subqueries, i)
ix = length(subqueries)
{{:subquery, ix}, {binding, [{:subquery, ix} | params], [subquery | subqueries], aliases, count + 1}}
expr, acc ->
{expr, acc}
end)
end
defp merge_aliases(old_aliases, new_aliases) do
Enum.reduce(new_aliases, old_aliases, fn {alias, _}, aliases ->
Builder.add_select_alias(aliases, alias)
end)
end
end

View File

@@ -0,0 +1,196 @@
import Kernel, except: [apply: 3]
defmodule Ecto.Query.Builder.Filter do
@moduledoc false
alias Ecto.Query.Builder
@doc """
Escapes a where or having clause.
It allows query expressions that evaluate to a boolean
or a keyword list of field names and values. In a keyword
list multiple key value pairs will be joined with "and".
Returned is `{expression, {params, %{subqueries: subqueries}}}` which is
a valid escaped expression, see `Macro.escape/2`. Both `params`
and `subqueries` are reversed.
"""
@spec escape(:where | :having | :on, Macro.t, non_neg_integer, Keyword.t, Macro.Env.t) :: {Macro.t, {list, Builder.acc()}}
def escape(_kind, [], _binding, _vars, _env) do
{true, {[], %{subqueries: []}}}
end
def escape(kind, expr, binding, vars, env) when is_list(expr) do
{parts, params_acc} =
Enum.map_reduce(expr, {[], %{subqueries: []}}, fn
{field, nil}, _params_acc ->
Builder.error! "nil given for `#{field}`. Comparison with nil is forbidden as it is unsafe. " <>
"Instead write a query with is_nil/1, for example: is_nil(s.#{field})"
{field, value}, params_acc when is_atom(field) ->
value = check_for_nils(value, field)
{value, params_acc} = Builder.escape(value, {binding, field}, params_acc, vars, env)
{{:{}, [], [:==, [], [to_escaped_field(binding, field), value]]}, params_acc}
_, _params_acc ->
Builder.error! "expected a keyword list at compile time in #{kind}, " <>
"got: `#{Macro.to_string expr}`. If you would like to " <>
"pass a list dynamically, please interpolate the whole list with ^"
end)
expr = Enum.reduce parts, &{:{}, [], [:and, [], [&2, &1]]}
{expr, params_acc}
end
def escape(_kind, expr, _binding, vars, env) do
Builder.escape(expr, :boolean, {[], %{subqueries: []}}, vars, env)
end
@doc """
Builds a quoted expression.
The quoted expression should evaluate to a query at runtime.
If possible, it does all calculations at compile time to avoid
runtime work.
"""
@spec build(:where | :having, :and | :or, Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t
def build(kind, op, query, _binding, {:^, _, [var]}, env) do
quote do
Ecto.Query.Builder.Filter.filter!(unquote(kind), unquote(op), unquote(query),
unquote(var), 0, unquote(env.file), unquote(env.line))
end
end
def build(kind, op, query, binding, expr, env) do
{query, binding} = Builder.escape_binding(query, binding, env)
{expr, {params, acc}} = escape(kind, expr, 0, binding, env)
params = Builder.escape_params(params)
subqueries = Enum.reverse(acc.subqueries)
expr = quote do: %Ecto.Query.BooleanExpr{
expr: unquote(expr),
op: unquote(op),
params: unquote(params),
subqueries: unquote(subqueries),
file: unquote(env.file),
line: unquote(env.line)}
Builder.apply_query(query, __MODULE__, [kind, expr], env)
end
@doc """
The callback applied by `build/4` to build the query.
"""
@spec apply(Ecto.Queryable.t, :where | :having, term) :: Ecto.Query.t
def apply(query, _, %{expr: true}) do
query
end
def apply(%Ecto.Query{wheres: wheres} = query, :where, expr) do
%{query | wheres: wheres ++ [expr]}
end
def apply(%Ecto.Query{havings: havings} = query, :having, expr) do
%{query | havings: havings ++ [expr]}
end
def apply(query, kind, expr) do
apply(Ecto.Queryable.to_query(query), kind, expr)
end
@doc """
Builds a filter based on the given arguments.
This is shared by having, where and join's on expressions.
"""
def filter!(kind, query, %Ecto.Query.DynamicExpr{} = dynamic, _binding, _file, _line) do
{expr, _binding, params, subqueries, file, line} =
Ecto.Query.Builder.Dynamic.fully_expand(query, dynamic)
if subqueries != [] do
raise ArgumentError, "subqueries are not allowed in `#{kind}` expressions"
end
{expr, params, file, line}
end
def filter!(_kind, _query, bool, _binding, file, line) when is_boolean(bool) do
{bool, [], file, line}
end
def filter!(kind, _query, kw, binding, file, line) when is_list(kw) do
{expr, params} = kw!(kind, kw, binding)
{expr, params, file, line}
end
def filter!(kind, _query, other, _binding, _file, _line) do
raise ArgumentError, "expected a keyword list or dynamic expression in `#{kind}`, got: `#{inspect other}`"
end
@doc """
Builds the filter and applies it to the given query as boolean operator.
"""
def filter!(:where, op, query, %Ecto.Query.DynamicExpr{} = dynamic, _binding, _file, _line) do
{expr, _binding, params, subqueries, file, line} =
Ecto.Query.Builder.Dynamic.fully_expand(query, dynamic)
boolean = %Ecto.Query.BooleanExpr{
expr: expr,
params: params,
line: line,
file: file,
op: op,
subqueries: subqueries
}
apply(query, :where, boolean)
end
def filter!(kind, op, query, expr, binding, file, line) do
{expr, params, file, line} = filter!(kind, query, expr, binding, file, line)
boolean = %Ecto.Query.BooleanExpr{expr: expr, params: params, line: line, file: file, op: op}
apply(query, kind, boolean)
end
defp kw!(kind, kw, binding) do
case kw!(kw, binding, 0, [], [], kind, kw) do
{[], params} -> {true, params}
{parts, params} -> {Enum.reduce(parts, &{:and, [], [&2, &1]}), params}
end
end
defp kw!([{field, nil}|_], _binding, _counter, _exprs, _params, _kind, _original) when is_atom(field) do
raise ArgumentError, "nil given for #{inspect field}. Comparison with nil is forbidden as it is unsafe. " <>
"Instead write a query with is_nil/1, for example: is_nil(s.#{field})"
end
defp kw!([{field, value}|t], binding, counter, exprs, params, kind, original) when is_atom(field) do
kw!(t, binding, counter + 1,
[{:==, [], [to_field(binding, field), {:^, [], [counter]}]}|exprs],
[{value, {binding, field}}|params],
kind, original)
end
defp kw!([], _binding, _counter, exprs, params, _kind, _original) do
{Enum.reverse(exprs), Enum.reverse(params)}
end
defp kw!(_, _binding, _counter, _exprs, _params, kind, original) do
raise ArgumentError, "expected a keyword list in `#{kind}`, got: `#{inspect original}`"
end
defp to_field(binding, field),
do: {{:., [], [{:&, [], [binding]}, field]}, [], []}
defp to_escaped_field(binding, field),
do: {:{}, [], [{:{}, [], [:., [], [{:{}, [], [:&, [], [binding]]}, field]]}, [], []]}
defp check_for_nils({:^, _, [var]}, field) do
quote do
^Ecto.Query.Builder.Filter.not_nil!(unquote(var), unquote(field))
end
end
defp check_for_nils(value, _field), do: value
def not_nil!(nil, field) do
raise ArgumentError, "nil given for `#{field}`. comparison with nil is forbidden as it is unsafe. " <>
"Instead write a query with is_nil/1, for example: is_nil(s.#{field})"
end
def not_nil!(other, _field), do: other
end

View File

@@ -0,0 +1,237 @@
defmodule Ecto.Query.Builder.From do
@moduledoc false
alias Ecto.Query.Builder
@doc """
Handles from expressions.
The expressions may either contain an `in` expression or not.
The right side is always expected to Queryable.
## Examples
iex> escape(quote(do: MySchema), __ENV__)
{MySchema, []}
iex> escape(quote(do: p in posts), __ENV__)
{quote(do: posts), [p: 0]}
iex> escape(quote(do: p in {"posts", MySchema}), __ENV__)
{{"posts", MySchema}, [p: 0]}
iex> escape(quote(do: [p, q] in posts), __ENV__)
{quote(do: posts), [p: 0, q: 1]}
iex> escape(quote(do: [_, _] in abc), __ENV__)
{quote(do: abc), [_: 0, _: 1]}
iex> escape(quote(do: other), __ENV__)
{quote(do: other), []}
iex> escape(quote(do: x() in other), __ENV__)
** (Ecto.Query.CompileError) binding list should contain only variables or `{as, var}` tuples, got: x()
"""
@spec escape(Macro.t(), Macro.Env.t()) :: {Macro.t(), Keyword.t()}
def escape({:in, _, [var, query]}, env) do
query = escape_source(query, env)
Builder.escape_binding(query, List.wrap(var), env)
end
def escape(query, env) do
query = escape_source(query, env)
{query, []}
end
defp escape_source(query, env) do
case Macro.expand_once(query, env) do
{:fragment, _, _} = fragment ->
{fragment, {params, _acc}} = Builder.escape(fragment, :any, {[], %{}}, [], env)
{fragment, Builder.escape_params(params)}
{:values, _, [values_list, types]} ->
prelude = quote do: values = Ecto.Query.Values.new(unquote(values_list), unquote(types))
types = quote do: values.types
num_rows = quote do: values.num_rows
params = quote do: Ecto.Query.Builder.escape_params(values.params)
{{:{}, [], [:values, [], [types, num_rows]]}, prelude, params}
^query ->
case query do
{left, right} -> {left, Macro.expand(right, env)}
_ -> query
end
other ->
escape_source(other, env)
end
end
@typep hints :: [String.t() | Macro.t()]
@doc """
Builds a quoted expression.
The quoted expression should evaluate to a query at runtime.
If possible, it does all calculations at compile time to avoid
runtime work.
"""
@spec build(Macro.t(), Macro.Env.t(), atom, {:ok, Ecto.Schema.prefix | nil} | nil, hints) ::
{Macro.t(), Keyword.t(), non_neg_integer | nil}
def build(query, env, as, prefix, hints) do
hints = Enum.map(hints, &hint!(&1))
prefix = case prefix do
nil -> nil
{:ok, prefix} when is_binary(prefix) or is_nil(prefix) -> {:ok, prefix}
{:ok, {:^, _, [prefix]}} -> {:ok, prefix}
{:ok, prefix} -> Builder.error!("`prefix` must be a compile time string or an interpolated value using ^, got: #{Macro.to_string(prefix)}")
end
as = case as do
{:^, _, [as]} -> as
as when is_atom(as) -> as
as -> Builder.error!("`as` must be a compile time atom or an interpolated value using ^, got: #{Macro.to_string(as)}")
end
{query, binds} = escape(query, env)
case query do
schema when is_atom(schema) ->
# Get the source at runtime so no unnecessary compile time
# dependencies between modules are added
source = quote(do: unquote(schema).__schema__(:source))
{:ok, prefix} = prefix || {:ok, quote(do: unquote(schema).__schema__(:prefix))}
{query(prefix, {source, schema}, [], as, hints, env.file, env.line), binds, 1}
source when is_binary(source) ->
{:ok, prefix} = prefix || {:ok, nil}
# When a binary is used, there is no schema
{query(prefix, {source, nil}, [], as, hints, env.file, env.line), binds, 1}
{source, schema} when is_binary(source) and is_atom(schema) ->
{:ok, prefix} = prefix || {:ok, quote(do: unquote(schema).__schema__(:prefix))}
{query(prefix, {source, schema}, [], as, hints, env.file, env.line), binds, 1}
{{:{}, _, [:fragment, _, _]} = fragment, params} ->
{:ok, prefix} = prefix || {:ok, nil}
{query(prefix, fragment, params, as, hints, env.file, env.line), binds, 1}
{{:{}, _, [:values, _, _]} = values, prelude, params} ->
{:ok, prefix} = prefix || {:ok, nil}
query = query(prefix, values, params, as, hints, env.file, env.line)
quoted =
quote do
unquote(prelude)
unquote(query)
end
{quoted, binds, 1}
_other ->
quoted =
quote do
Ecto.Query.Builder.From.apply(unquote(query), unquote(length(binds)), unquote(as), unquote(prefix), unquote(hints))
end
{quoted, binds, nil}
end
end
defp query(prefix, source, params, as, hints, file, line) do
aliases = if as, do: [{as, 0}], else: []
from_fields = [source: source, params: params, as: as, prefix: prefix, hints: hints, file: file, line: line]
query_fields = [
from: {:%, [], [Ecto.Query.FromExpr, {:%{}, [], from_fields}]},
aliases: {:%{}, [], aliases}
]
{:%, [], [Ecto.Query, {:%{}, [], query_fields}]}
end
@doc """
Validates hints at compile time and runtime
"""
def hint!(hint) when is_binary(hint), do: hint
def hint!({:unsafe_fragment, _, [fragment]}) do
case fragment do
{:^, _, [value]} ->
quote do: Ecto.Query.Builder.From.hint!(unquote(value))
_ ->
Builder.error!(
"`unsafe_fragment/1` in `hints` expects an interpolated value, such as " <>
"unsafe_fragment(^value), got: `#{Macro.to_string(fragment)}`"
)
end
end
def hint!(other) do
Builder.error!(
"`hints` must be a compile time string, unsafe fragment of the form `unsafe_fragment(^...)`, " <>
"or list containing either, got: `#{Macro.to_string(other)}`"
)
end
@doc """
The callback applied by `build/2` to build the query.
"""
@spec apply(Ecto.Queryable.t(), non_neg_integer, Macro.t(), {:ok, Ecto.Schema.prefix} | nil, hints) :: Ecto.Query.t()
def apply(query, binds, as, prefix, hints) do
query =
query
|> Ecto.Queryable.to_query()
|> maybe_apply_as(as)
|> maybe_apply_prefix(prefix)
|> maybe_apply_hints(hints)
check_binds(query, binds)
query
end
defp maybe_apply_as(query, nil), do: query
defp maybe_apply_as(%{from: %{as: from_as}}, as) when not is_nil(from_as) do
Builder.error!(
"can't apply alias `#{inspect(as)}`, binding in `from` is already aliased to `#{inspect(from_as)}`"
)
end
defp maybe_apply_as(%{from: from, aliases: aliases} = query, as) do
if Map.has_key?(aliases, as) do
Builder.error!("alias `#{inspect(as)}` already exists")
else
%{query | aliases: Map.put(aliases, as, 0), from: %{from | as: as}}
end
end
defp maybe_apply_prefix(query, nil), do: query
defp maybe_apply_prefix(query, {:ok, prefix}) do
update_in query.from.prefix, fn
nil ->
prefix
from_prefix ->
Builder.error!(
"can't apply prefix `#{inspect(prefix)}`, `from` is already prefixed to `#{inspect(from_prefix)}`"
)
end
end
defp maybe_apply_hints(query, []), do: query
defp maybe_apply_hints(query, hints), do: update_in(query.from.hints, &(&1 ++ hints))
defp check_binds(query, count) do
if count > 1 and count > Builder.count_binds(query) do
Builder.error!(
"`from` in query expression specified #{count} " <>
"binds but query contains #{Builder.count_binds(query)} binds"
)
end
end
end

View File

@@ -0,0 +1,118 @@
import Kernel, except: [apply: 2]
defmodule Ecto.Query.Builder.GroupBy do
@moduledoc false
alias Ecto.Query.Builder
@doc """
Escapes a list of quoted expressions.
See `Ecto.Builder.escape/2`.
iex> escape(:group_by, quote do [x.x, 13] end, {[], %{}}, [x: 0], __ENV__)
{[{:{}, [], [{:{}, [], [:., [], [{:{}, [], [:&, [], [0]]}, :x]]}, [], []]},
13],
{[], %{}}}
"""
@spec escape(:group_by | :partition_by, Macro.t, {list, term}, Keyword.t, Macro.Env.t) ::
{Macro.t, {list, term}}
def escape(kind, expr, params_acc, vars, env) do
expr
|> List.wrap
|> Enum.map_reduce(params_acc, &do_escape(&1, &2, kind, vars, env))
end
defp do_escape({:^, _, [expr]}, params_acc, kind, _vars, _env) do
{quote(do: Ecto.Query.Builder.GroupBy.field!(unquote(kind), unquote(expr))), params_acc}
end
defp do_escape(field, params_acc, _kind, _vars, _env) when is_atom(field) do
{Macro.escape(to_field(field)), params_acc}
end
defp do_escape(expr, params_acc, _kind, vars, env) do
Builder.escape(expr, :any, params_acc, vars, env)
end
@doc """
Called at runtime to verify a field.
"""
def field!(_kind, field) when is_atom(field),
do: to_field(field)
def field!(kind, other) do
raise ArgumentError,
"expected a field as an atom in `#{kind}`, got: `#{inspect other}`"
end
@doc """
Shared between group_by and partition_by.
"""
def group_or_partition_by!(kind, query, exprs, params) do
{expr, {params, _, subqueries}} =
Enum.map_reduce(List.wrap(exprs), {params, length(params), []}, fn
field, params_count when is_atom(field) ->
{to_field(field), params_count}
%Ecto.Query.DynamicExpr{} = dynamic, {params, count, subqueries} ->
{expr, params, subqueries, _aliases, count} = Builder.Dynamic.partially_expand(query, dynamic, params, subqueries, %{}, count)
{expr, {params, count, subqueries}}
other, _params_count ->
raise ArgumentError,
"expected a list of fields and dynamics in `#{kind}`, got: `#{inspect other}`"
end)
{expr, params, subqueries}
end
defp to_field(field), do: {{:., [], [{:&, [], [0]}, field]}, [], []}
@doc """
Called at runtime to assemble group_by.
"""
def group_by!(query, group_by, file, line) do
{expr, params, subqueries} = group_or_partition_by!(:group_by, query, group_by, [])
expr = %Ecto.Query.ByExpr{expr: expr, params: Enum.reverse(params), line: line, file: file, subqueries: subqueries}
apply(query, expr)
end
@doc """
Builds a quoted expression.
The quoted expression should evaluate to a query at runtime.
If possible, it does all calculations at compile time to avoid
runtime work.
"""
@spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t
def build(query, _binding, {:^, _, [var]}, env) do
quote do
Ecto.Query.Builder.GroupBy.group_by!(unquote(query), unquote(var), unquote(env.file), unquote(env.line))
end
end
def build(query, binding, expr, env) do
{query, binding} = Builder.escape_binding(query, binding, env)
{expr, {params, acc}} = escape(:group_by, expr, {[], %{subqueries: []}}, binding, env)
params = Builder.escape_params(params)
group_by = quote do: %Ecto.Query.ByExpr{
expr: unquote(expr),
params: unquote(params),
subqueries: unquote(acc.subqueries),
file: unquote(env.file),
line: unquote(env.line)}
Builder.apply_query(query, __MODULE__, [group_by], env)
end
@doc """
The callback applied by `build/4` to build the query.
"""
@spec apply(Ecto.Queryable.t, term) :: Ecto.Query.t
def apply(%Ecto.Query{group_bys: group_bys} = query, expr) do
%{query | group_bys: group_bys ++ [expr]}
end
def apply(query, expr) do
apply(Ecto.Queryable.to_query(query), expr)
end
end

View File

@@ -0,0 +1,426 @@
import Kernel, except: [apply: 2]
defmodule Ecto.Query.Builder.Join do
@moduledoc false
alias Ecto.Query.Builder
alias Ecto.Query.{JoinExpr, QueryExpr}
@doc """
Escapes a join expression (not including the `on` expression).
It returns a tuple containing the binds, the on expression (if available)
and the association expression.
## Examples
iex> escape(quote(do: x in "foo"), [], __ENV__)
{:x, {"foo", nil}, nil, nil, []}
iex> escape(quote(do: "foo"), [], __ENV__)
{:_, {"foo", nil}, nil, nil, []}
iex> escape(quote(do: x in Sample), [], __ENV__)
{:x, {nil, Sample}, nil, nil, []}
iex> escape(quote(do: x in __MODULE__), [], __ENV__)
{:x, {nil, __MODULE__}, nil, nil, []}
iex> escape(quote(do: x in {"foo", :sample}), [], __ENV__)
{:x, {"foo", :sample}, nil, nil, []}
iex> escape(quote(do: x in {"foo", Sample}), [], __ENV__)
{:x, {"foo", Sample}, nil, nil, []}
iex> escape(quote(do: x in {"foo", __MODULE__}), [], __ENV__)
{:x, {"foo", __MODULE__}, nil, nil, []}
iex> escape(quote(do: c in assoc(p, :comments)), [p: 0], __ENV__)
{:c, nil, {0, :comments}, nil, []}
iex> escape(quote(do: x in fragment("foo")), [], __ENV__)
{:x, {:{}, [], [:fragment, [], [raw: "foo"]]}, nil, nil, []}
"""
@spec escape(Macro.t(), Keyword.t(), Macro.Env.t()) ::
{atom, Macro.t() | nil, Macro.t() | nil, list}
def escape({:in, _, [{var, _, context}, expr]}, vars, env)
when is_atom(var) and is_atom(context) do
{_, expr, assoc, prelude, params} = escape(expr, vars, env)
{var, expr, assoc, prelude, params}
end
def escape({:subquery, _, [expr]}, _vars, _env) do
{:_, quote(do: Ecto.Query.subquery(unquote(expr))), nil, nil, []}
end
def escape({:subquery, _, [expr, opts]}, _vars, _env) do
{:_, quote(do: Ecto.Query.subquery(unquote(expr), unquote(opts))), nil, nil, []}
end
def escape({:fragment, _, [_ | _]} = expr, vars, env) do
{expr, {params, _acc}} = Builder.escape(expr, :any, {[], %{}}, vars, env)
{:_, expr, nil, nil, params}
end
def escape({:values, _, [values_list, types]}, _vars, _env) do
prelude = quote do: values = Ecto.Query.Values.new(unquote(values_list), unquote(types))
types = quote do: values.types
num_rows = quote do: values.num_rows
params = quote do: values.params
{:_, {:{}, [], [:values, [], [types, num_rows]]}, nil, prelude, params}
end
def escape({string, schema} = join, _vars, env) when is_binary(string) do
case Macro.expand(schema, env) do
schema when is_atom(schema) ->
{:_, {string, schema}, nil, nil, []}
_ ->
Builder.error!("malformed join `#{Macro.to_string(join)}` in query expression")
end
end
def escape({:assoc, _, [{var, _, context}, field]}, vars, _env)
when is_atom(var) and is_atom(context) do
ensure_field!(field)
var = Builder.find_var!(var, vars)
field = Builder.quoted_atom!(field, "field/2")
{:_, nil, {var, field}, nil, []}
end
def escape({:^, _, [expr]}, _vars, _env) do
{:_, quote(do: Ecto.Query.Builder.Join.join!(unquote(expr))), nil, nil, []}
end
def escape(string, _vars, _env) when is_binary(string) do
{:_, {string, nil}, nil, nil, []}
end
def escape(schema, _vars, _env) when is_atom(schema) do
{:_, {nil, schema}, nil, nil, []}
end
def escape(join, vars, env) do
case Macro.expand(join, env) do
^join ->
Builder.error!("malformed join `#{Macro.to_string(join)}` in query expression")
join ->
escape(join, vars, env)
end
end
@doc """
Called at runtime to check dynamic joins.
"""
def join!(expr) when is_atom(expr),
do: {nil, expr}
def join!(expr) when is_binary(expr),
do: {expr, nil}
def join!({source, module}) when is_binary(source) and is_atom(module),
do: {source, module}
def join!(expr),
do: Ecto.Queryable.to_query(expr)
@doc """
Builds a quoted expression.
The quoted expression should evaluate to a query at runtime.
If possible, it does all calculations at compile time to avoid
runtime work.
"""
@spec build(
Macro.t(),
atom,
[Macro.t()],
Macro.t(),
Macro.t(),
Macro.t(),
atom,
nil | {:ok, Ecto.Schema.prefix()},
nil | String.t() | [String.t()],
Macro.Env.t()
) ::
{Macro.t(), Keyword.t(), non_neg_integer | nil}
def build(query, qual, binding, expr, count_bind, on, as, prefix, maybe_hints, env) do
{:ok, prefix} = prefix || {:ok, nil}
hints = List.wrap(maybe_hints)
unless Enum.all?(hints, &is_binary/1) do
Builder.error!(
"`hints` must be a compile time string or list of strings, " <>
"got: `#{Macro.to_string(maybe_hints)}`"
)
end
prefix =
case prefix do
nil ->
nil
prefix when is_binary(prefix) ->
prefix
{:^, _, [prefix]} ->
prefix
prefix ->
Builder.error!(
"`prefix` must be a compile time string or an interpolated value using ^, got: #{Macro.to_string(prefix)}"
)
end
as =
case as do
{:^, _, [as]} ->
as
as when is_atom(as) ->
as
as ->
Builder.error!(
"`as` must be a compile time atom or an interpolated value using ^, got: #{Macro.to_string(as)}"
)
end
{query, binding} = Builder.escape_binding(query, binding, env)
{join_bind, join_source, join_assoc, join_prelude, join_params} = escape(expr, binding, env)
join_params = escape_params(join_params)
join_qual = validate_qual(qual)
validate_bind(join_bind, binding)
{count_bind, query} =
if is_nil(count_bind) do
var = Macro.unique_var(:query, __MODULE__)
query =
quote do
unquote(var) = Ecto.Queryable.to_query(unquote(query))
end
{quote(do: Builder.count_binds(unquote(var))), query}
else
{count_bind, query}
end
binding = binding ++ [{join_bind, count_bind}]
next_bind =
if is_integer(count_bind) do
count_bind + 1
else
quote(do: unquote(count_bind) + 1)
end
join = [
as: as,
assoc: join_assoc,
file: env.file,
line: env.line,
params: join_params,
prefix: prefix,
qual: join_qual,
source: join_source,
hints: hints
]
on = ensure_on(on, join_assoc, join_qual, join_source, env)
query = build_on(on, join_prelude, join, as, query, binding, count_bind, env)
{query, binding, next_bind}
end
defp escape_params(params) when is_list(params) do
Builder.escape_params(params)
end
defp escape_params(params) do
quote do: Builder.escape_params(unquote(params))
end
def build_on({:^, _, [var]}, join_prelude, join, as, query, _binding, count_bind, env) do
quote do
query = unquote(query)
unquote(join_prelude)
Ecto.Query.Builder.Join.join!(
query,
%JoinExpr{unquote_splicing(join), on: %QueryExpr{}},
unquote(var),
unquote(as),
unquote(count_bind),
unquote(env.file),
unquote(env.line)
)
end
end
def build_on(on, join_prelude, join, as, query, binding, count_bind, env) do
case Ecto.Query.Builder.Filter.escape(:on, on, count_bind, binding, env) do
{_on_expr, {_on_params, %{subqueries: [_ | _]}}} ->
raise ArgumentError, "invalid expression for join `:on`, subqueries aren't supported"
{on_expr, {on_params, _acc}} ->
on_params = Builder.escape_params(on_params)
join =
quote do
unquote(join_prelude)
%JoinExpr{
unquote_splicing(join),
on: %QueryExpr{
expr: unquote(on_expr),
params: unquote(on_params),
line: unquote(env.line),
file: unquote(env.file)
}
}
end
Builder.apply_query(query, __MODULE__, [join, as, count_bind], env)
end
end
defp ensure_on(on, _assoc, _qual, _source, _env) when on != nil, do: on
defp ensure_on(nil, _assoc = nil, qual, source, env)
when qual not in [:cross, :cross_lateral] do
maybe_source =
with {source, alias} <- source,
source when source != nil <- source || alias do
" on #{inspect(source)}"
else
_ -> ""
end
stacktrace = Macro.Env.stacktrace(env)
IO.warn("missing `:on` in join#{maybe_source}, defaulting to `on: true`.", stacktrace)
true
end
defp ensure_on(nil, _assoc, _qual, _source, _env), do: true
@doc """
Applies the join expression to the query.
"""
def apply(%Ecto.Query{joins: joins} = query, expr, nil, _count_bind) do
%{query | joins: joins ++ [expr]}
end
def apply(%Ecto.Query{joins: joins, aliases: aliases} = query, expr, as, count_bind) do
aliases =
case aliases do
%{} -> runtime_aliases(aliases, as, count_bind)
_ -> compile_aliases(aliases, as, count_bind)
end
%{query | joins: joins ++ [expr], aliases: aliases}
end
def apply(query, expr, as, count_bind) do
apply(Ecto.Queryable.to_query(query), expr, as, count_bind)
end
@doc """
Called at runtime to build aliases.
"""
def runtime_aliases(aliases, nil, _), do: aliases
def runtime_aliases(aliases, name, join_count) when is_integer(join_count) do
if Map.has_key?(aliases, name) do
Builder.error!("alias `#{inspect(name)}` already exists")
else
Map.put(aliases, name, join_count)
end
end
defp compile_aliases({:%{}, meta, aliases}, name, join_count)
when is_atom(name) and is_integer(join_count) do
{:%{}, meta, aliases |> Map.new() |> runtime_aliases(name, join_count) |> Map.to_list()}
end
defp compile_aliases(aliases, name, join_count) do
quote do
Ecto.Query.Builder.Join.runtime_aliases(
unquote(aliases),
unquote(name),
unquote(join_count)
)
end
end
@doc """
Called at runtime to build a join.
"""
def join!(query, join, expr, as, count_bind, file, line) do
# join without expanded :on is built and applied to the query,
# so that expansion of dynamic :on accounts for the new binding
{on_expr, on_params, on_file, on_line} =
Ecto.Query.Builder.Filter.filter!(
:on,
apply(query, join, as, count_bind),
expr,
count_bind,
file,
line
)
join = %{
join
| on: %QueryExpr{expr: on_expr, params: on_params, line: on_line, file: on_file}
}
apply(query, join, as, count_bind)
end
defp validate_qual(qual) when is_atom(qual) do
qual!(qual)
end
defp validate_qual(qual) do
quote(do: Ecto.Query.Builder.Join.qual!(unquote(qual)))
end
defp validate_bind(bind, all) do
if bind != :_ and bind in all do
Builder.error!("variable `#{bind}` is already defined in query")
end
end
@qualifiers [
:inner,
:inner_lateral,
:left,
:left_lateral,
:right,
:full,
:cross,
:cross_lateral
]
@doc """
Called at runtime to check dynamic qualifier.
"""
def qual!(qual) when qual in @qualifiers, do: qual
def qual!(qual) do
raise ArgumentError,
"invalid join qualifier `#{inspect(qual)}`, accepted qualifiers are: " <>
Enum.map_join(@qualifiers, ", ", &"`#{inspect(&1)}`")
end
defp ensure_field!({var, _, _}) when var != :^ do
Builder.error!(
"you passed the variable `#{var}` to `assoc/2`. Did you mean to pass the atom `:#{var}`?"
)
end
defp ensure_field!(_), do: true
end

View File

@@ -0,0 +1,111 @@
import Kernel, except: [apply: 3]
defmodule Ecto.Query.Builder.LimitOffset do
@moduledoc false
alias Ecto.Query.Builder
@doc """
Validates `with_ties` at runtime.
"""
@spec with_ties!(any) :: boolean
def with_ties!(with_ties) when is_boolean(with_ties), do: with_ties
def with_ties!(with_ties),
do: raise("`with_ties` expression must evaluate to a boolean at runtime, got: `#{inspect(with_ties)}`")
@doc """
Builds a quoted expression.
The quoted expression should evaluate to a query at runtime.
If possible, it does all calculations at compile time to avoid
runtime work.
"""
@spec build(:limit | :with_ties | :offset, Macro.t(), [Macro.t()], Macro.t(), Macro.Env.t()) ::
Macro.t()
def build(type, query, binding, expr, env) do
{query, vars} = Builder.escape_binding(query, binding, env)
{expr, {params, _acc}} = escape(type, expr, {[], %{}}, vars, env)
params = Builder.escape_params(params)
quoted = build_quoted(type, expr, params, env)
Builder.apply_query(query, __MODULE__, [type, quoted], env)
end
defp escape(type, expr, params_acc, vars, env) when type in [:limit, :offset] do
Builder.escape(expr, :integer, params_acc, vars, env)
end
defp escape(:with_ties, expr, params_acc, _vars, _env) when is_boolean(expr) do
{expr, params_acc}
end
defp escape(:with_ties, {:^, _, [expr]}, params_acc, _vars, _env) do
{quote(do: Ecto.Query.Builder.LimitOffset.with_ties!(unquote(expr))), params_acc}
end
defp escape(:with_ties, expr, _params_acc, _vars, _env) do
Builder.error!(
"`with_ties` expression must be a compile time boolean or an interpolated value using ^, got: `#{Macro.to_string(expr)}`"
)
end
defp build_quoted(:limit, expr, params, env) do
quote do: %Ecto.Query.LimitExpr{
expr: unquote(expr),
params: unquote(params),
file: unquote(env.file),
line: unquote(env.line)
}
end
defp build_quoted(:offset, expr, params, env) do
quote do: %Ecto.Query.QueryExpr{
expr: unquote(expr),
params: unquote(params),
file: unquote(env.file),
line: unquote(env.line)
}
end
defp build_quoted(:with_ties, expr, _params, _env), do: expr
@doc """
The callback applied by `build/4` to build the query.
"""
@spec apply(Ecto.Queryable.t(), :limit | :with_ties | :offset, term) :: Ecto.Query.t()
def apply(%Ecto.Query{} = query, :limit, expr) do
%{query | limit: expr}
end
def apply(%Ecto.Query{limit: limit} = query, :with_ties, expr) do
%{query | limit: apply_limit(limit, expr)}
end
def apply(%Ecto.Query{} = query, :offset, expr) do
%{query | offset: expr}
end
def apply(query, kind, expr) do
apply(Ecto.Queryable.to_query(query), kind, expr)
end
@doc """
Applies the `with_ties` value to the `limit` struct.
"""
def apply_limit(nil, _with_ties) do
Builder.error!("`with_ties` can only be applied to queries containing a `limit`")
end
# Runtime
def apply_limit(%_{} = limit, with_ties) do
%{limit | with_ties: with_ties}
end
# Compile
def apply_limit(limit, with_ties) do
quote do
Ecto.Query.Builder.LimitOffset.apply_limit(unquote(limit), unquote(with_ties))
end
end
end

View File

@@ -0,0 +1,59 @@
import Kernel, except: [apply: 2]
defmodule Ecto.Query.Builder.Lock do
@moduledoc false
alias Ecto.Query.Builder
@doc """
Escapes the lock code.
iex> escape(quote(do: "FOO"), [], __ENV__)
"FOO"
"""
@spec escape(Macro.t(), Keyword.t, Macro.Env.t) :: Macro.t()
def escape(lock, _vars, _env) when is_binary(lock), do: lock
def escape({:fragment, _, [_ | _]} = expr, vars, env) do
{expr, {params, _acc}} = Builder.escape(expr, :any, {[], %{}}, vars, env)
if params != [] do
Builder.error!("value interpolation is not allowed in :lock")
end
expr
end
def escape(other, _, _) do
Builder.error!(
"`#{Macro.to_string(other)}` is not a valid lock. " <>
"For security reasons, a lock must always be a literal string or a fragment"
)
end
@doc """
Builds a quoted expression.
The quoted expression should evaluate to a query at runtime.
If possible, it does all calculations at compile time to avoid
runtime work.
"""
@spec build(Macro.t(), Macro.t(), Macro.t(), Macro.Env.t()) :: Macro.t()
def build(query, binding, expr, env) do
{query, binding} = Builder.escape_binding(query, binding, env)
Builder.apply_query(query, __MODULE__, [escape(expr, binding, env)], env)
end
@doc """
The callback applied by `build/4` to build the query.
"""
@spec apply(Ecto.Queryable.t(), term) :: Ecto.Query.t()
def apply(%Ecto.Query{} = query, value) do
%{query | lock: value}
end
def apply(query, value) do
apply(Ecto.Queryable.to_query(query), value)
end
end

View File

@@ -0,0 +1,282 @@
import Kernel, except: [apply: 3]
defmodule Ecto.Query.Builder.OrderBy do
@moduledoc false
alias Ecto.Query.Builder
@directions [
:asc,
:asc_nulls_last,
:asc_nulls_first,
:desc,
:desc_nulls_last,
:desc_nulls_first
]
@doc """
Returns `true` if term is a valid order_by direction; otherwise returns `false`.
## Examples
iex> valid_direction?(:asc)
true
iex> valid_direction?(:desc)
true
iex> valid_direction?(:invalid)
false
"""
def valid_direction?(term), do: term in @directions
@doc """
Escapes an order by query.
The query is escaped to a list of `{direction, expression}`
pairs at runtime. Escaping also validates direction is one of
`:asc`, `:asc_nulls_last`, `:asc_nulls_first`, `:desc`,
`:desc_nulls_last` or `:desc_nulls_first`.
## Examples
iex> escape(:order_by, quote do [x.x, desc: 13] end, {[], %{}}, [x: 0], __ENV__)
{[asc: {:{}, [], [{:{}, [], [:., [], [{:{}, [], [:&, [], [0]]}, :x]]}, [], []]},
desc: 13],
{[], %{}}}
"""
@spec escape(:order_by | :distinct, Macro.t(), {list, term}, Keyword.t(), Macro.Env.t()) ::
{Macro.t(), {list, term}}
def escape(kind, expr, params_acc, vars, env) do
expr
|> List.wrap()
|> Enum.flat_map_reduce(params_acc, &do_escape(&1, &2, kind, vars, env))
end
defp do_escape({dir, {:^, _, [expr]}}, params_acc, kind, _vars, _env) do
{[{quoted_dir!(kind, dir),
quote(do: Ecto.Query.Builder.OrderBy.field!(unquote(kind), unquote(expr)))}], params_acc}
end
defp do_escape({:^, _, [expr]}, params_acc, kind, _vars, _env) do
{[{:asc, quote(do: Ecto.Query.Builder.OrderBy.field!(unquote(kind), unquote(expr)))}],
params_acc}
end
defp do_escape({dir, field}, params_acc, kind, _vars, _env) when is_atom(field) do
{[{quoted_dir!(kind, dir), Macro.escape(to_field(field))}], params_acc}
end
defp do_escape(field, params_acc, _kind, _vars, _env) when is_atom(field) do
{[{:asc, Macro.escape(to_field(field))}], params_acc}
end
defp do_escape({dir, expr}, params_acc, kind, vars, env) do
fun = &escape_expansion(kind, &1, &2, &3, &4, &5)
{ast, params_acc} = Builder.escape(expr, :any, params_acc, vars, {get_env(env), fun})
{[{quoted_dir!(kind, dir), ast}], params_acc}
end
defp do_escape(expr, params_acc, kind, vars, env) do
fun = &escape_expansion(kind, &1, &2, &3, &4, &5)
{ast, params_acc} = Builder.escape(expr, :any, params_acc, vars, {get_env(env), fun})
if is_list(ast) do
{ast, params_acc}
else
{[{:asc, ast}], params_acc}
end
end
defp get_env({env, _}), do: env
defp get_env(env), do: env
defp escape_expansion(kind, expr, _type, params_acc, vars, env) when is_list(expr) do
escape(kind, expr, params_acc, vars, env)
end
defp escape_expansion(_kind, field, _type, params_acc, _vars, _env) when is_atom(field) do
{Macro.escape(to_field(field)), params_acc}
end
defp escape_expansion(kind, expr, type, params_acc, vars, env) do
fun = &escape_expansion(kind, &1, &2, &3, &4, &5)
Builder.escape(expr, type, params_acc, vars, {env, fun})
end
@doc """
Checks the variable is a quoted direction at compilation time or
delegate the check to runtime for interpolation.
"""
def quoted_dir!(kind, {:^, _, [expr]}),
do: quote(do: Ecto.Query.Builder.OrderBy.dir!(unquote(kind), unquote(expr)))
def quoted_dir!(_kind, dir) when dir in @directions,
do: dir
def quoted_dir!(kind, other) do
Builder.error!(
"expected #{Enum.map_join(@directions, ", ", &inspect/1)} or interpolated value " <>
"in `#{kind}`, got: `#{inspect(other)}`"
)
end
@doc """
Called at runtime to verify the direction.
"""
def dir!(_kind, dir) when dir in @directions,
do: dir
def dir!(kind, other) do
raise ArgumentError,
"expected one of #{Enum.map_join(@directions, ", ", &inspect/1)} " <>
"in `#{kind}`, got: `#{inspect(other)}`"
end
@doc """
Called at runtime to verify a field.
"""
def field!(_kind, field) when is_atom(field) do
to_field(field)
end
def field!(kind, %Ecto.Query.DynamicExpr{} = dynamic_expression) do
raise ArgumentError,
"expected a field as an atom in `#{kind}`, got: `#{inspect(dynamic_expression)}`. " <>
"To use dynamic expressions, you need to interpolate at root level, as in: " <>
"`^[asc: dynamic, desc: :id]`"
end
def field!(kind, other) do
raise ArgumentError, "expected a field as an atom in `#{kind}`, got: `#{inspect(other)}`"
end
defp to_field(field), do: {{:., [], [{:&, [], [0]}, field]}, [], []}
@doc """
Shared between order_by and distinct.
"""
def order_by_or_distinct!(kind, query, exprs, params) do
{expr, {params, _, subqueries}} =
Enum.map_reduce(List.wrap(exprs), {params, length(params), []}, fn
{dir, expr}, params_count when dir in @directions ->
{expr, params} = dynamic_or_field!(kind, expr, query, params_count)
{{dir, expr}, params}
expr, params_count ->
{expr, params} = dynamic_or_field!(kind, expr, query, params_count)
{{:asc, expr}, params}
end)
{expr, params, subqueries}
end
@doc """
Called at runtime to assemble order_by.
"""
def order_by!(query, exprs, op, file, line) do
{expr, params, subqueries} = order_by_or_distinct!(:order_by, query, exprs, [])
expr = %Ecto.Query.ByExpr{
expr: expr,
params: Enum.reverse(params),
line: line,
file: file,
subqueries: subqueries
}
apply(query, expr, op)
end
defp dynamic_or_field!(
_kind,
%Ecto.Query.DynamicExpr{} = dynamic,
query,
{params, count, subqueries}
) do
{expr, params, subqueries, _aliases, count} =
Ecto.Query.Builder.Dynamic.partially_expand(
query,
dynamic,
params,
subqueries,
%{},
count
)
{expr, {params, count, subqueries}}
end
defp dynamic_or_field!(_kind, field, _query, params_count) when is_atom(field) do
{to_field(field), params_count}
end
defp dynamic_or_field!(kind, other, _query, _params_count) do
raise ArgumentError,
"`#{kind}` interpolated on root expects a field or a keyword list " <>
"with the direction as keys and fields or dynamics as values, got: `#{inspect(other)}`"
end
@doc """
Builds a quoted expression.
The quoted expression should evaluate to a query at runtime.
If possible, it does all calculations at compile time to avoid
runtime work.
"""
@spec build(Macro.t(), [Macro.t()], Macro.t(), :append | :prepend, Macro.Env.t()) :: Macro.t()
def build(query, _binding, {:^, _, [var]}, op, env) do
quote do
Ecto.Query.Builder.OrderBy.order_by!(
unquote(query),
unquote(var),
unquote(op),
unquote(env.file),
unquote(env.line)
)
end
end
def build(query, binding, expr, op, env) do
{query, binding} = Builder.escape_binding(query, binding, env)
{expr, {params, acc}} = escape(:order_by, expr, {[], %{subqueries: []}}, binding, env)
params = Builder.escape_params(params)
order_by =
quote do: %Ecto.Query.ByExpr{
expr: unquote(expr),
params: unquote(params),
subqueries: unquote(acc.subqueries),
file: unquote(env.file),
line: unquote(env.line)
}
Builder.apply_query(query, __MODULE__, [order_by, op], env)
end
@doc """
The callback applied by `build/4` to build the query.
"""
@spec apply(Ecto.Queryable.t(), term, term) :: Ecto.Query.t()
def apply(%Ecto.Query{order_bys: orders} = query, expr, op) do
%{query | order_bys: update_order_bys(orders, expr, op)}
end
def apply(query, expr, op) do
apply(Ecto.Queryable.to_query(query), expr, op)
end
@doc """
Updates the `order_bys` value for a query.
"""
def update_order_bys(orders, expr, :append), do: orders ++ [expr]
def update_order_bys(orders, expr, :prepend), do: [expr | orders]
def update_order_bys(orders, expr, mode) do
quote do
Ecto.Query.Builder.OrderBy.update_order_bys(unquote(orders), unquote(expr), unquote(mode))
end
end
end

View File

@@ -0,0 +1,337 @@
import Kernel, except: [apply: 3]
defmodule Ecto.Query.Builder.Preload do
@moduledoc false
alias Ecto.Query.Builder
@doc """
Escapes a preload.
A preload may be an atom, a list of atoms or a keyword list
nested as a rose tree.
iex> escape(:foo, [])
{[:foo], []}
iex> escape([foo: :bar], [])
{[foo: [:bar]], []}
iex> escape([:foo, :bar], [])
{[:foo, :bar], []}
iex> escape([foo: [:bar, bar: :bat]], [])
{[foo: [:bar, bar: [:bat]]], []}
iex> escape([foo: {:^, [], ["external"]}], [])
{[foo: "external"], []}
iex> escape([foo: [:bar, {:^, [], ["external"]}], baz: :bat], [])
{[foo: [:bar, "external"], baz: [:bat]], []}
iex> escape([foo: {:c, [], nil}], [c: 1])
{[], [foo: {1, []}]}
iex> escape([foo: {{:c, [], nil}, bar: {:l, [], nil}}], [c: 1, l: 2])
{[], [foo: {1, [bar: {2, []}]}]}
iex> escape([foo: {:c, [], nil}, bar: {:l, [], nil}], [c: 1, l: 2])
{[], [foo: {1, []}, bar: {2, []}]}
iex> escape([foo: {{:c, [], nil}, :bar}], [c: 1])
{[foo: [:bar]], [foo: {1, []}]}
iex> escape([foo: [bar: {:c, [], nil}]], [c: 1])
** (Ecto.Query.CompileError) cannot preload join association `:bar` with binding `c` because parent preload is not a join association
"""
@spec escape(Macro.t(), Keyword.t()) :: {[Macro.t()], [Macro.t()]}
def escape(preloads, vars) do
{preloads, assocs} = escape(preloads, :both, [], [], vars)
{Enum.reverse(preloads), Enum.reverse(assocs)}
end
defp escape(atom, _mode, preloads, assocs, _vars) when is_atom(atom) do
{[atom | preloads], assocs}
end
defp escape(list, mode, preloads, assocs, vars) when is_list(list) do
Enum.reduce(list, {preloads, assocs}, fn item, acc ->
escape_each(item, mode, acc, vars)
end)
end
defp escape({:^, _, [inner]}, _mode, preloads, assocs, _vars) do
{[inner | preloads], assocs}
end
defp escape(other, _mode, _preloads, _assocs, _vars) do
Builder.error!(
"`#{Macro.to_string(other)}` is not a valid preload expression. " <>
"preload expects an atom, a list of atoms or a keyword list with " <>
"more preloads as values. Use ^ on the outermost preload to interpolate a value"
)
end
defp escape_each({key, {:^, _, [inner]}}, _mode, {preloads, assocs}, _vars) do
key = escape_key(key)
{[{key, inner} | preloads], assocs}
end
defp escape_each({key, {var, _, context}}, mode, {preloads, assocs}, vars)
when is_atom(context) do
assert_assoc!(mode, key, var)
key = escape_key(key)
idx = Builder.find_var!(var, vars)
{preloads, [{key, {idx, []}} | assocs]}
end
defp escape_each({key, {{var, _, context}, list}}, mode, {preloads, assocs}, vars)
when is_atom(context) do
assert_assoc!(mode, key, var)
key = escape_key(key)
idx = Builder.find_var!(var, vars)
{inner_preloads, inner_assocs} = escape(list, :assoc, [], [], vars)
assocs = [{key, {idx, Enum.reverse(inner_assocs)}} | assocs]
case inner_preloads do
[] -> {preloads, assocs}
_ -> {[{key, Enum.reverse(inner_preloads)} | preloads], assocs}
end
end
defp escape_each({key, list}, _mode, {preloads, assocs}, vars) do
key = escape_key(key)
{inner_preloads, []} = escape(list, :preload, [], [], vars)
{[{key, Enum.reverse(inner_preloads)} | preloads], assocs}
end
defp escape_each(other, mode, {preloads, assocs}, vars) do
escape(other, mode, preloads, assocs, vars)
end
defp escape_key(atom) when is_atom(atom) do
atom
end
defp escape_key({:^, _, [expr]}) do
quote(do: Ecto.Query.Builder.Preload.key!(unquote(expr)))
end
defp escape_key(other) do
Builder.error!("malformed key in preload `#{Macro.to_string(other)}` in query expression")
end
@doc """
Called at runtime to check dynamic preload keys.
"""
def key!(key) when is_atom(key),
do: key
def key!(key) do
raise ArgumentError,
"expected key in preload to be an atom, got: `#{inspect(key)}`"
end
@doc """
Applies the preloaded value into the query.
The quoted expression should evaluate to a query at runtime.
If possible, it does all calculations at compile time to avoid
runtime work.
"""
@spec build(Macro.t(), [Macro.t()], Macro.t(), Macro.Env.t()) :: Macro.t()
def build(query, _binding, {:^, _, [expr]}, _env) do
quote do
Ecto.Query.Builder.Preload.preload!(unquote(query), unquote(expr))
end
end
def build(query, binding, expr, env) do
{query, binding} = Builder.escape_binding(query, binding, env)
{preloads, assocs} = escape(expr, binding)
Builder.apply_query(query, __MODULE__, [Enum.reverse(preloads), Enum.reverse(assocs)], env)
end
@doc """
The callback applied by `build/4` to build the query.
"""
@spec apply(Ecto.Queryable.t(), term, term) :: Ecto.Query.t()
def apply(%Ecto.Query{preloads: p, assocs: a} = query, preloads, assocs) do
%{query | preloads: p ++ preloads, assocs: a ++ assocs}
end
def apply(query, preloads, assocs) do
apply(Ecto.Queryable.to_query(query), preloads, assocs)
end
@doc """
Called at runtime to assemble preload.
"""
def preload!(query, preload) do
{preloads, assocs} = expand(preload, query)
apply(query, preloads, assocs)
end
@doc """
Expands preloads at runtime.
## Examples
iex> expand(:foo, [])
{[:foo], []}
iex> expand([foo: :bar], [])
{[foo: :bar], []}
iex> expand([:foo, :bar], [])
{[:foo, :bar], []}
iex> expand([foo: [:bar, bar: :bat]], [])
{[foo: [:bar, bar: :bat]], []}
iex> expand([:a, :b, c: [:d]], [])
{[:a, :b, c: [:d]], []}
iex> expand([foo: ["external"]], [])
** (ArgumentError) `"external"` is not a valid preload expression, expected an atom or a list.
iex> require Ecto.Query
iex> expand([b: Ecto.Query.dynamic([_a, b], b)], Ecto.Query.from(a in "a", join: b in "b", on: true))
{[], [b: {1, []}]}
iex> require Ecto.Query
iex> expand(
...> [b: {Ecto.Query.dynamic([_a, b], b), c: Ecto.Query.dynamic([_a, _b, c], c)}],
...> Ecto.Query.from(a in "a", join: b in "b", on: true, join: c in "c", on: true)
...> )
{[], [b: {1, [c: {2, []}]}]}
"""
def expand(preloads, query) do
expand(preloads, query, :both, [], [])
end
defp expand(atom, _query, _mode, preloads, assocs) when is_atom(atom) do
{[atom | preloads], assocs}
end
defp expand(list, query, mode, preloads, assocs) when is_list(list) do
{preloads, assocs} =
Enum.reduce(list, {preloads, assocs}, fn item, acc ->
expand_each(item, query, mode, acc)
end)
{Enum.reverse(preloads), Enum.reverse(assocs)}
end
defp expand(other, _query, _mode, _preloads, _assocs) do
raise ArgumentError,
"`#{inspect(other)}` is not a valid preload expression, " <>
"expected an atom or a list."
end
defp expand_each(atom, _query, _mode, {preloads, assocs}) when is_atom(atom) do
{[atom | preloads], assocs}
end
defp expand_each({key, atom}, _query, _mode, {preloads, assocs}) when is_atom(atom) do
assert_key!(key)
{[{key, atom} | preloads], assocs}
end
defp expand_each({key, %Ecto.Query.DynamicExpr{} = dynamic}, query, mode, {preloads, assocs}) do
assert_key!(key)
assert_assoc!(mode, key)
idx = expand_dynamic(dynamic, query)
{preloads, [{key, {idx, []}} | assocs]}
end
defp expand_each(
{key, {%Ecto.Query.DynamicExpr{} = dynamic, inner}},
query,
mode,
{preloads, assocs}
) do
assert_key!(key)
assert_assoc!(mode, key)
idx = expand_dynamic(dynamic, query)
{inner_preloads, inner_assocs} = expand(inner, query, :assoc, [], [])
assocs = [{key, {idx, inner_assocs}} | assocs]
case inner_preloads do
[] -> {preloads, assocs}
_ -> {[{key, inner_preloads} | preloads], assocs}
end
end
defp expand_each({key, {query_or_fun, inner}}, query, _mode, {preloads, assocs}) do
assert_key!(key)
assert_query_or_fun!(query_or_fun, key)
{inner_preloads, []} = expand(inner, query, :preload, [], [])
{[{key, {query_or_fun, inner_preloads}} | preloads], assocs}
end
defp expand_each({key, list}, query, _mode, {preloads, assocs}) when is_list(list) do
assert_key!(key)
{inner_preloads, []} = expand(list, query, :preload, [], [])
{[{key, inner_preloads} | preloads], assocs}
end
defp expand_each({key, query_or_fun}, _query, _mode, {preloads, assocs}) do
assert_key!(key)
assert_query_or_fun!(query_or_fun, key)
{[{key, query_or_fun} | preloads], assocs}
end
defp expand_each(other, query, mode, {preloads, assocs}) do
expand(other, query, mode, preloads, assocs)
end
defp expand_dynamic(%Ecto.Query.DynamicExpr{} = dynamic, query) do
case Builder.Dynamic.fully_expand(query, dynamic) do
{{:&, [], [idx]}, _, _, _, _, _} when is_integer(idx) ->
idx
_ ->
raise ArgumentError,
"invalid dynamic in preload: `#{inspect(dynamic)}`. " <>
"Dynamic expressions in preload must evaluate to a single binding, as in: " <>
"`dynamic([comments: c], c)`"
end
end
defp assert_key!(key), do: key!(key) && :ok
defp assert_query_or_fun!(%Ecto.Query{}, _key), do: :ok
defp assert_query_or_fun!(fun, _key) when is_function(fun, 1), do: :ok
defp assert_query_or_fun!(fun, _key) when is_function(fun, 2), do: :ok
defp assert_query_or_fun!(other, key) do
raise ArgumentError,
"invalid preload for key `#{inspect(key)}`: #{inspect(other)}. " <>
"Preloads can be a query, a function expecting one or two arguments, " <>
"or a dynamic that evaluates to a single binding"
end
defp assert_assoc!(mode, _atom) when mode in [:both, :assoc], do: :ok
defp assert_assoc!(_mode, atom) do
raise ArgumentError,
"cannot preload join association `#{inspect(atom)}` " <>
"because parent preload is not a join association"
end
defp assert_assoc!(mode, _atom, _var) when mode in [:both, :assoc], do: :ok
defp assert_assoc!(_mode, atom, var) do
Builder.error!(
"cannot preload join association `#{Macro.to_string(atom)}` with binding `#{var}` " <>
"because parent preload is not a join association"
)
end
end

View File

@@ -0,0 +1,579 @@
import Kernel, except: [apply: 2]
defmodule Ecto.Query.Builder.Select do
@moduledoc false
alias Ecto.Query.Builder
@doc """
Escapes a select.
It allows tuples, lists and variables at the top level. Inside the
tuples and lists query expressions are allowed.
## Examples
iex> escape({1, 2}, [], __ENV__)
{{:{}, [], [:{}, [], [1, 2]]}, {[], %{take: %{}, subqueries: [], aliases: %{}}}}
iex> escape([1, 2], [], __ENV__)
{[1, 2], {[], %{take: %{}, subqueries: [], aliases: %{}}}}
iex> escape(quote(do: x), [x: 0], __ENV__)
{{:{}, [], [:&, [], [0]]}, {[], %{take: %{}, subqueries: [], aliases: %{}}}}
"""
@spec escape(Macro.t, Keyword.t, Macro.Env.t) :: {Macro.t, {list, %{take: map, subqueries: list}}}
def escape(atom, _vars, _env)
when is_atom(atom) and not is_boolean(atom) and atom != nil do
Builder.error! """
#{inspect(atom)} is not a valid query expression, :select expects a query expression or a list of fields
"""
end
def escape(other, vars, env) do
cond do
take?(other) ->
{
{:{}, [], [:&, [], [0]]},
{[], %{take: %{0 => {:any, Macro.expand(other, env)}}, subqueries: [], aliases: %{}}}
}
maybe_take?(other) ->
Builder.error! """
Cannot mix fields with interpolations, such as: `select: [:foo, ^:bar, :baz]`. \
Instead interpolate all fields at once, such as: `select: ^[:foo, :bar, :baz]`. \
Got: #{Macro.to_string(other)}.
"""
true ->
{expr, {params, acc}} = escape(other, {[], %{take: %{}, subqueries: [], aliases: %{}}}, vars, env)
acc = %{acc | subqueries: Enum.reverse(acc.subqueries)}
{expr, {params, acc}}
end
end
# Tuple
defp escape({left, right}, params_acc, vars, env) do
escape({:{}, [], [left, right]}, params_acc, vars, env)
end
# Tuple
defp escape({:{}, _, list}, params_acc, vars, env) do
{list, params_acc} = Enum.map_reduce(list, params_acc, &escape(&1, &2, vars, env))
expr = {:{}, [], [:{}, [], list]}
{expr, params_acc}
end
# Struct
defp escape({:%, _, [name, map]}, params_acc, vars, env) do
name = Macro.expand(name, env)
{escaped_map, params_acc} = escape(map, params_acc, vars, env)
{{:{}, [], [:%, [], [name, escaped_map]]}, params_acc}
end
# Map
defp escape({:%{}, _, [{:|, _, [data, pairs]}]}, params_acc, vars, env) do
{escaped_data, params_acc} = escape(data, params_acc, vars, env)
{pairs, params_acc} = escape_pairs(pairs, data, params_acc, vars, env)
{{:{}, [], [:%{}, [], [{:{}, [], [:|, [], [escaped_data, pairs]]}]]}, params_acc}
end
# Merge
defp escape({:merge, _, [left, {kind, _, _} = right]}, params_acc, vars, env)
when kind in [:%{}, :map] do
{left, params_acc} = escape(left, params_acc, vars, env)
{right, params_acc} = escape(right, params_acc, vars, env)
{{:{}, [], [:merge, [], [left, right]]}, params_acc}
end
defp escape({:merge, _, [_left, right]}, _params_acc, _vars, _env) do
Builder.error! "expected the second argument of merge/2 in select to be a map, got: `#{Macro.to_string(right)}`"
end
# Map
defp escape({:%{}, _, pairs}, params_acc, vars, env) do
{pairs, params_acc} = escape_pairs(pairs, nil, params_acc, vars, env)
{{:{}, [], [:%{}, [], pairs]}, params_acc}
end
# List
defp escape(list, params_acc, vars, env) when is_list(list) do
Enum.map_reduce(list, params_acc, &escape(&1, &2, vars, env))
end
# map/struct(var, [:foo, :bar])
defp escape({tag, _, [{var, _, context}, fields]}, {params, acc}, vars, env)
when tag in [:map, :struct] and is_atom(var) and is_atom(context) do
taken = escape_fields(fields, tag, env)
expr = Builder.escape_var!(var, vars)
acc = add_take(acc, Builder.find_var!(var, vars), {tag, taken})
{expr, {params, acc}}
end
# aliased values
defp escape({:selected_as, _, [expr, name]}, {params, acc}, vars, env) do
name = Builder.quoted_atom!(name, "selected_as/2")
{escaped, {params, acc}} = Builder.escape(expr, :any, {params, acc}, vars, env)
expr = {:{}, [], [:selected_as, [], [escaped, name]]}
aliases = Builder.add_select_alias(acc.aliases, name)
{expr, {params, %{acc | aliases: aliases}}}
end
defp escape(expr, params_acc, vars, env) do
Builder.escape(expr, :any, params_acc, vars, {env, &escape_expansion/5})
end
defp escape_expansion(expr, _type, params_acc, vars, env) do
escape(expr, params_acc, vars, env)
end
defp escape_pairs(pairs, update_data, params_acc, vars, env) do
Enum.map_reduce(pairs, params_acc, fn {k, v}, acc ->
v = tag_update_param(update_data, k, v)
{k, acc} = escape_key(k, acc, vars, env)
{v, acc} = escape(v, acc, vars, env)
{{k, v}, acc}
end)
end
defp tag_update_param({var, _, context}, field, {:^, _,[_]} = param) when is_atom(var) and is_atom(context) do
{:type, [], [param, {{:., [], [{var, [], context}, field]}, [], []}]}
end
defp tag_update_param(_, _, value), do: value
defp escape_key(k, params_acc, _vars, _env) when is_atom(k) do
{k, params_acc}
end
defp escape_key({:^, _, [k]}, params_acc, _vars, _env) do
checked = quote do: Ecto.Query.Builder.Select.map_key!(unquote(k))
{checked, params_acc}
end
defp escape_key(k, params_acc, vars, env) do
escape(k, params_acc, vars, env)
end
defp escape_fields({:^, _, [interpolated]}, tag, _env) do
quote do
Ecto.Query.Builder.Select.fields!(unquote(tag), unquote(interpolated))
end
end
defp escape_fields(expr, tag, env) do
case Macro.expand(expr, env) do
fields when is_list(fields) ->
fields
_ ->
Builder.error!(
"`#{tag}/2` in `select` expects either a literal or " <>
"an interpolated (1) list of atom fields, (2) dynamic, or " <>
"(3) map with dynamic values"
)
end
end
@doc """
Called at runtime to verify a field.
"""
def fields!(tag, fields) do
if take?(fields) do
fields
else
raise ArgumentError,
"expected a list of fields in `#{tag}/2` inside `select`, got: `#{inspect fields}`"
end
end
@doc """
Called at runtime to verify a map key
"""
def map_key!(key) when is_binary(key), do: key
def map_key!(key) when is_integer(key), do: key
def map_key!(key) when is_float(key), do: key
def map_key!(key) when is_atom(key), do: key
def map_key!(other) do
Builder.error!(
"interpolated map keys in `:select` can only be atoms, strings or numbers, got: #{inspect(other)}"
)
end
# atom list sigils
defp take?({name, _, [_, modifiers]}) when name in ~w(sigil_w sigil_W)a do
?a in modifiers
end
defp take?(fields) do
is_list(fields) and Enum.all?(fields, fn
{k, v} when is_atom(k) -> take?(List.wrap(v))
k when is_atom(k) -> true
_ -> false
end)
end
defp maybe_take?(fields) do
is_list(fields) and Enum.any?(fields, fn
{k, v} when is_atom(k) -> maybe_take?(List.wrap(v))
k when is_atom(k) -> true
_ -> false
end)
end
@doc """
Called at runtime for interpolated/dynamic selects.
"""
def select!(kind, query, fields, file, line) when is_map(fields) do
{expr, {params, subqueries, aliases, _count}} = expand_nested(fields, {[], [], %{}, 0}, query)
%Ecto.Query.SelectExpr{
expr: expr,
params: Enum.reverse(params),
subqueries: Enum.reverse(subqueries),
aliases: aliases,
file: file,
line: line
}
|> apply_or_merge(kind, query)
end
def select!(kind, query, fields, file, line) do
take = %{0 => {:any, fields!(:select, fields)}}
%Ecto.Query.SelectExpr{expr: {:&, [], [0]}, take: take, file: file, line: line}
|> apply_or_merge(kind, query)
end
defp apply_or_merge(select, kind, query) do
if kind == :select do
apply(query, select)
else
merge(query, select)
end
end
defp expand_nested(%Ecto.Query.DynamicExpr{} = dynamic, {params, subqueries, aliases, count}, query) do
{expr, params, subqueries, aliases, count} =
Ecto.Query.Builder.Dynamic.partially_expand(query, dynamic, params, subqueries, aliases, count)
{expr, {params, subqueries, aliases, count}}
end
defp expand_nested(%Ecto.SubQuery{} = subquery, {params, subqueries, aliases, count}, _query) do
index = length(subqueries)
# used both in ast and in parameters, as a placeholder.
expr = {:subquery, index}
params = [expr | params]
subqueries = [subquery | subqueries]
count = count + 1
{expr, {params, subqueries, aliases, count}}
end
defp expand_nested(%type{} = fields, acc, query) do
{fields, acc} = fields |> Map.from_struct() |> expand_nested(acc, query)
{{:%, [], [type, fields]}, acc}
end
defp expand_nested(fields, acc, query) when is_map(fields) do
{fields, acc} = fields |> Enum.map_reduce(acc, &expand_nested_pair(&1, &2, query))
{{:%{}, [], fields}, acc}
end
defp expand_nested(invalid, _acc, query) when is_list(invalid) or is_tuple(invalid) do
raise Ecto.QueryError,
query: query,
message:
"Interpolated map values in :select can only be " <>
"maps, structs, dynamics, subqueries and literals. Got #{inspect(invalid)}"
end
defp expand_nested(other, acc, _query) do
{other, acc}
end
defp expand_nested_pair({key, val}, acc, query) do
{val, acc} = expand_nested(val, acc, query)
{{key, val}, acc}
end
@doc """
Builds a quoted expression.
The quoted expression should evaluate to a query at runtime.
If possible, it does all calculations at compile time to avoid
runtime work.
"""
@spec build(:select | :merge, Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t
def build(kind, query, _binding, {:^, _, [var]}, env) do
quote do
Ecto.Query.Builder.Select.select!(unquote(kind), unquote(query), unquote(var),
unquote(env.file), unquote(env.line))
end
end
def build(kind, query, binding, expr, env) do
{query, binding} = Builder.escape_binding(query, binding, env)
{expr, {params, acc}} = escape(expr, binding, env)
params = Builder.escape_params(params)
take = {:%{}, [], Map.to_list(acc.take)}
aliases = escape_aliases(acc.aliases)
select = quote do: %Ecto.Query.SelectExpr{
expr: unquote(expr),
params: unquote(params),
file: unquote(env.file),
line: unquote(env.line),
take: unquote(take),
subqueries: unquote(acc.subqueries),
aliases: unquote(aliases)}
if kind == :select do
Builder.apply_query(query, __MODULE__, [select], env)
else
quote do
query = unquote(query)
Builder.Select.merge(query, unquote(select))
end
end
end
defp escape_aliases(%{} = aliases), do: {:%{}, [], Map.to_list(aliases)}
defp escape_aliases(aliases), do: aliases
@doc """
The callback applied by `build/5` to build the query.
"""
@spec apply(Ecto.Queryable.t, term) :: Ecto.Query.t
def apply(%Ecto.Query{select: nil} = query, expr) do
%{query | select: expr}
end
def apply(%Ecto.Query{}, _expr) do
Builder.error! "only one select expression is allowed in query"
end
def apply(query, expr) do
apply(Ecto.Queryable.to_query(query), expr)
end
@doc """
The callback applied by `build/5` when merging.
"""
def merge(%Ecto.Query{select: nil} = query, new_select) do
merge(query, new_select, {:&, [], [0]}, [], [], %{}, %{}, new_select)
end
def merge(%Ecto.Query{select: old_select} = query, new_select) do
%{expr: old_expr, params: old_params, subqueries: old_subqueries, take: old_take, aliases: old_aliases} = old_select
merge(query, old_select, old_expr, old_params, old_subqueries, old_take, old_aliases, new_select)
end
def merge(query, expr) do
merge(Ecto.Queryable.to_query(query), expr)
end
defp merge(query, select, old_expr, old_params, old_subqueries, old_take, old_aliases, new_select) do
%{expr: new_expr, params: new_params, subqueries: new_subqueries, take: new_take, aliases: new_aliases} = new_select
new_expr =
new_expr
|> Ecto.Query.Builder.bump_interpolations(old_params)
|> Ecto.Query.Builder.bump_subqueries(old_subqueries)
expr =
case {classify_merge(old_expr, old_take), classify_merge(new_expr, new_take)} do
{_, _} when old_expr == new_expr ->
new_expr
{{:source, meta, ix}, {:source, _, ix}} ->
{:&, meta, [ix]}
{{:struct, meta, name, old_fields}, {:map, _, new_fields}} when old_params == [] ->
cond do
new_fields == [] ->
old_expr
Keyword.keyword?(old_fields) and Keyword.keyword?(new_fields) ->
{:%, meta, [name, {:%{}, meta, Keyword.merge(old_fields, new_fields)}]}
true ->
{:merge, [], [old_expr, new_expr]}
end
{{:map, meta, old_fields}, {:map, _, new_fields}} ->
cond do
old_fields == [] ->
new_expr
new_fields == [] ->
old_expr
true ->
require_distinct_keys? = old_params != []
case merge_map_fields(old_fields, new_fields, require_distinct_keys?) do
fields when is_list(fields) ->
{:%{}, meta, fields}
:error ->
{:merge, [], [old_expr, new_expr]}
end
end
{_, {:map, _, _}} ->
{:merge, [], [old_expr, new_expr]}
{_, _} ->
message = """
cannot select_merge #{merge_argument_to_error(new_expr, query)} into \
#{merge_argument_to_error(old_expr, query)}, those select expressions \
are incompatible. You can only select_merge:
* a source (such as post) with another source (of the same type)
* a source (such as post) with a map
* a struct with a map
* a map with a map
Incompatible merge found
"""
raise Ecto.QueryError, query: query, message: message
end
select = %{
select | expr: expr,
params: old_params ++ bump_subquery_params(new_params, old_subqueries),
subqueries: old_subqueries ++ new_subqueries,
take: merge_take(query, old_expr, old_take, new_take),
aliases: merge_aliases(old_aliases, new_aliases)
}
%{query | select: select}
end
defp classify_merge({:&, meta, [ix]}, take) when is_integer(ix) do
case take do
%{^ix => {:map, _}} -> {:map, meta, :runtime}
_ -> {:source, meta, ix}
end
end
defp classify_merge({:%, meta, [name, {:%{}, _, fields}]}, _take)
when fields == [] or tuple_size(hd(fields)) == 2 do
{:struct, meta, name, fields}
end
defp classify_merge({:%{}, meta, fields}, _take)
when fields == [] or tuple_size(hd(fields)) == 2 do
{:map, meta, fields}
end
defp classify_merge({:%{}, meta, _}, _take) do
{:map, meta, :runtime}
end
defp classify_merge(_, _take) do
:error
end
defp merge_map_fields(old_fields, new_fields, false) do
if Keyword.keyword?(old_fields) and Keyword.keyword?(new_fields) do
Keyword.merge(old_fields, new_fields)
else
:error
end
end
defp merge_map_fields(old_fields, new_fields, true) when is_list(old_fields) do
if Keyword.keyword?(new_fields) do
valid? =
Enum.reduce_while(old_fields, true, fn
{k, _v}, _ when is_atom(k) ->
if Keyword.has_key?(new_fields, k),
do: {:halt, false},
else: {:cont, true}
_, _ ->
{:halt, false}
end)
if valid?, do: old_fields ++ new_fields, else: :error
else
:error
end
end
defp merge_map_fields(_, _, true), do: :error
defp merge_argument_to_error({:&, _, [0]}, %{from: %{source: {source, alias}}}) do
"source #{inspect(source || alias)}"
end
defp merge_argument_to_error({:&, _, [ix]}, _query) do
"join (at position #{ix})"
end
defp merge_argument_to_error(other, _query) do
Macro.to_string(other)
end
defp add_take(acc, key, value) do
take = Map.update(acc.take, key, value, &merge_take_kind_and_fields(key, &1, value))
%{acc | take: take}
end
defp bump_subquery_params(new_params, old_subqueries) do
len = length(old_subqueries)
Enum.map(new_params, fn
{:subquery, counter} -> {:subquery, len + counter}
other -> other
end)
end
defp merge_take(query, old_expr, %{} = old_take, %{} = new_take) do
Enum.reduce(new_take, old_take, fn {binding, {new_kind, new_fields} = new_value}, acc ->
case acc do
%{^binding => old_value} ->
Map.put(acc, binding, merge_take_kind_and_fields(binding, old_value, new_value))
%{} ->
# If merging with a schema, add the schema's query fields. This comes in handy if the user
# is merging fields with load_in_query = false.
# If merging with a schemaless source, do nothing so the planner can take all the fields.
case old_expr do
{:&, _, [^binding]} ->
source = Enum.at([query.from | query.joins], binding).source
case source do
{_, schema} when schema != nil ->
Map.put(acc, binding, {new_kind, Enum.uniq(new_fields ++ schema.__schema__(:query_fields))})
_ ->
acc
end
_ ->
Map.put(acc, binding, new_value)
end
end
end)
end
defp merge_take_kind_and_fields(binding, {old_kind, old_fields}, {new_kind, new_fields}) do
{merge_take_kind(binding, old_kind, new_kind), Enum.uniq(old_fields ++ new_fields)}
end
defp merge_take_kind(_, kind, kind), do: kind
defp merge_take_kind(_, :any, kind), do: kind
defp merge_take_kind(_, kind, :any), do: kind
defp merge_take_kind(binding, old, new) do
Builder.error! "cannot select_merge because the binding at position #{binding} " <>
"was previously specified as a `#{old}` and later as `#{new}`"
end
defp merge_aliases(old_aliases, new_aliases) do
Enum.reduce(new_aliases, old_aliases, fn {alias, _}, aliases ->
Builder.add_select_alias(aliases, alias)
end)
end
end

View File

@@ -0,0 +1,200 @@
import Kernel, except: [apply: 2]
defmodule Ecto.Query.Builder.Update do
@moduledoc false
@keys [:set, :inc, :push, :pull]
alias Ecto.Query.Builder
@doc """
Escapes a list of quoted expressions.
iex> escape([], [], __ENV__)
{[], [], []}
iex> escape([set: []], [], __ENV__)
{[], [], []}
iex> escape(quote(do: ^[set: []]), [], __ENV__)
{[], [set: []], []}
iex> escape(quote(do: [set: ^[foo: 1]]), [], __ENV__)
{[], [set: [foo: 1]], []}
iex> escape(quote(do: [set: [foo: ^1]]), [], __ENV__)
{[], [set: [foo: 1]], []}
"""
@spec escape(Macro.t, Keyword.t, Macro.Env.t) :: {Macro.t, Macro.t, list}
def escape(expr, vars, env) when is_list(expr) do
escape_op(expr, [], [], [], vars, env)
end
def escape({:^, _, [v]}, _vars, _env) do
{[], v, []}
end
def escape(expr, _vars, _env) do
compile_error!(expr)
end
defp escape_op([{k, v}|t], compile, runtime, params, vars, env) when is_atom(k) and is_list(v) do
validate_op!(k)
{compile_values, runtime_values, params} = escape_kw(k, v, params, vars, env)
compile =
if compile_values == [], do: compile, else: [{k, Enum.reverse(compile_values)} | compile]
runtime =
if runtime_values == [], do: runtime, else: [{k, Enum.reverse(runtime_values)} | runtime]
escape_op(t, compile, runtime, params, vars, env)
end
defp escape_op([{k, {:^, _, [v]}}|t], compile, runtime, params, vars, env) when is_atom(k) do
validate_op!(k)
escape_op(t, compile, [{k, v}|runtime], params, vars, env)
end
defp escape_op([], compile, runtime, params, _vars, _env) do
{Enum.reverse(compile), Enum.reverse(runtime), params}
end
defp escape_op(expr, _compile, _runtime, _params, _vars, _env) do
compile_error!(expr)
end
defp escape_kw(op, kw, params, vars, env) do
Enum.reduce kw, {[], [], params}, fn
{k, {:^, _, [v]}}, {compile, runtime, params} when is_atom(k) ->
{compile, [{k, v} | runtime], params}
{k, v}, {compile, runtime, params} ->
k = escape_field!(k)
{v, {params, _acc}} = Builder.escape(v, type_for_key(op, {0, k}), {params, %{}}, vars, env)
{[{k, v} | compile], runtime, params}
_, _acc ->
Builder.error! "malformed #{inspect op} in update `#{Macro.to_string(kw)}`, " <>
"expected a keyword list"
end
end
defp escape_field!({:^, _, [k]}), do: quote(do: Ecto.Query.Builder.Update.field!(unquote(k)))
defp escape_field!(k) when is_atom(k), do: k
defp escape_field!(k) do
Builder.error!(
"expected an atom field or an interpolated field in `update`, got `#{inspect(k)}`"
)
end
def field!(field) when is_atom(field), do: field
def field!(other) do
raise ArgumentError, "expected a field as an atom in `update`, got: `#{inspect other}`"
end
defp compile_error!(expr) do
Builder.error! "malformed update `#{Macro.to_string(expr)}` in query expression, " <>
"expected a keyword list with set/push/pull as keys with field-value " <>
"pairs as values"
end
@doc """
Builds a quoted expression.
The quoted expression should evaluate to a query at runtime.
If possible, it does all calculations at compile time to avoid
runtime work.
"""
@spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t
def build(query, binding, expr, env) do
{query, binding} = Builder.escape_binding(query, binding, env)
{compile, runtime, params} = escape(expr, binding, env)
query =
if compile == [] do
query
else
params = Builder.escape_params(params)
update = quote do
%Ecto.Query.QueryExpr{expr: unquote(compile), params: unquote(params),
file: unquote(env.file), line: unquote(env.line)}
end
Builder.apply_query(query, __MODULE__, [update], env)
end
if runtime == [] do
query
else
quote do
Ecto.Query.Builder.Update.update!(unquote(query), unquote(runtime),
unquote(env.file), unquote(env.line))
end
end
end
@doc """
The callback applied by `build/4` to build the query.
"""
@spec apply(Ecto.Queryable.t, term) :: Ecto.Query.t
def apply(%Ecto.Query{updates: updates} = query, expr) do
%{query | updates: updates ++ [expr]}
end
def apply(query, expr) do
apply(Ecto.Queryable.to_query(query), expr)
end
@doc """
If there are interpolated updates at compile time,
we need to handle them at runtime. We do such in
this callback.
"""
def update!(query, runtime, file, line) when is_list(runtime) do
{runtime, {params, _count}} =
Enum.map_reduce runtime, {[], 0}, fn
{k, v}, acc when is_atom(k) and is_list(v) ->
validate_op!(k)
{v, params} = runtime_field!(query, k, v, acc)
{{k, v}, params}
_, _ ->
runtime_error!(runtime)
end
expr = %Ecto.Query.QueryExpr{expr: runtime, params: Enum.reverse(params),
file: file, line: line}
apply(query, expr)
end
def update!(_query, runtime, _file, _line) do
runtime_error!(runtime)
end
defp runtime_field!(query, key, kw, acc) do
Enum.map_reduce kw, acc, fn
{k, %Ecto.Query.DynamicExpr{} = v}, {params, count} when is_atom(k) ->
{v, params, count} = Ecto.Query.Builder.Dynamic.partially_expand(:update, query, v, params, count)
{{k, v}, {params, count}}
{k, v}, {params, count} when is_atom(k) ->
params = [{v, type_for_key(key, {0, k})} | params]
{{k, {:^, [], [count]}}, {params, count + 1}}
_, _acc ->
raise ArgumentError, "malformed #{inspect key} in update `#{inspect(kw)}`, " <>
"expected a keyword list"
end
end
defp runtime_error!(value) do
raise ArgumentError,
"malformed update `#{inspect(value)}` in query expression, " <>
"expected a keyword list with set/push/pull as keys with field-value pairs as values"
end
defp validate_op!(key) when key in @keys, do: :ok
defp validate_op!(key), do: Builder.error! "unknown key `#{inspect(key)}` in update"
# Out means the given type must be taken out of an array
# It is the opposite of "left in right" in the query API.
defp type_for_key(:push, type), do: {:out, type}
defp type_for_key(:pull, type), do: {:out, type}
defp type_for_key(_, type), do: type
end

View File

@@ -0,0 +1,204 @@
import Kernel, except: [apply: 2]
defmodule Ecto.Query.Builder.Windows do
@moduledoc false
alias Ecto.Query.Builder
alias Ecto.Query.Builder.{GroupBy, OrderBy}
@sort_order [:partition_by, :order_by, :frame]
@doc """
Escapes a window params.
## Examples
iex> escape(quote do [order_by: [desc: 13]] end, {[], %{}}, [x: 0], __ENV__)
{[order_by: [desc: 13]], [], {[], %{}}}
"""
@spec escape([Macro.t], {list, term}, Keyword.t, Macro.Env.t | {Macro.Env.t, fun})
:: {Macro.t, [{atom, term}], {list, term}}
def escape(kw, params_acc, vars, env) when is_list(kw) do
{compile, runtime} = sort(@sort_order, kw, :compile, [], [])
{compile, params_acc} = Enum.map_reduce(compile, params_acc, &escape_compile(&1, &2, vars, env))
{compile, runtime, params_acc}
end
def escape(kw, _params_acc, _vars, _env) do
error!(kw)
end
defp sort([key | keys], kw, mode, compile, runtime) do
case Keyword.pop(kw, key) do
{nil, kw} ->
sort(keys, kw, mode, compile, runtime)
{{:^, _, [var]}, kw} ->
sort(keys, kw, :runtime, compile, [{key, var} | runtime])
{_, _} when mode == :runtime ->
[{runtime_key, _} | _] = runtime
raise ArgumentError, "window has an interpolated value under `#{runtime_key}` " <>
"and therefore `#{key}` must also be interpolated"
{expr, kw} ->
sort(keys, kw, mode, [{key, expr} | compile], runtime)
end
end
defp sort([], [], _mode, compile, runtime) do
{Enum.reverse(compile), Enum.reverse(runtime)}
end
defp sort([], kw, _mode, _compile, _runtime) do
error!(kw)
end
defp escape_compile({:partition_by, fields}, params_acc, vars, env) do
{fields, params_acc} = GroupBy.escape(:partition_by, fields, params_acc, vars, env)
{{:partition_by, fields}, params_acc}
end
defp escape_compile({:order_by, fields}, params_acc, vars, env) do
{fields, params_acc} = OrderBy.escape(:order_by, fields, params_acc, vars, env)
{{:order_by, fields}, params_acc}
end
defp escape_compile({:frame, frame_clause}, params_acc, vars, env) do
{frame_clause, params_acc} = escape_frame(frame_clause, params_acc, vars, env)
{{:frame, frame_clause}, params_acc}
end
defp escape_frame({:fragment, _, _} = fragment, params_acc, vars, env) do
Builder.escape(fragment, :any, params_acc, vars, env)
end
defp escape_frame(other, _, _, _) do
Builder.error!("expected a dynamic or fragment in `:frame`, got: `#{inspect other}`")
end
defp error!(other) do
Builder.error!(
"expected window definition to be a keyword list " <>
"with partition_by, order_by or frame as keys, got: `#{inspect other}`"
)
end
@doc """
Builds a quoted expression.
The quoted expression should evaluate to a query at runtime.
If possible, it does all calculations at compile time to avoid
runtime work.
"""
@spec build(Macro.t, [Macro.t], Keyword.t, Macro.Env.t) :: Macro.t
def build(query, binding, windows, env) when is_list(windows) do
{query, binding} = Builder.escape_binding(query, binding, env)
{compile, runtime} =
windows
|> Enum.map(&escape_window(binding, &1, env))
|> Enum.split_with(&elem(&1, 2) == [])
compile = Enum.map(compile, &build_compile_window(&1, env))
runtime = Enum.map(runtime, &build_runtime_window(&1, env))
query = Builder.apply_query(query, __MODULE__, [compile], env)
if runtime == [] do
query
else
quote do
Ecto.Query.Builder.Windows.runtime!(
unquote(query),
unquote(runtime),
unquote(env.file),
unquote(env.line)
)
end
end
end
def build(_, _, windows, _) do
Builder.error!(
"expected window definitions to be a keyword list with window names as keys and " <>
"a keyword list with the window definition as value, got: `#{inspect windows}`"
)
end
defp escape_window(vars, {name, expr}, env) do
{compile_acc, runtime_acc, {params, acc}} = escape(expr, {[], %{subqueries: []}}, vars, env)
{name, compile_acc, runtime_acc, Builder.escape_params(params), acc}
end
defp build_compile_window({name, compile_acc, _, params, acc}, env) do
{name,
quote do
%Ecto.Query.ByExpr{
expr: unquote(compile_acc),
params: unquote(params),
subqueries: unquote(acc.subqueries),
file: unquote(env.file),
line: unquote(env.line)
}
end}
end
defp build_runtime_window({name, compile_acc, runtime_acc, params, acc}, _env) do
{:{}, [], [name, Enum.reverse(compile_acc), runtime_acc, Enum.reverse(params), {:%{}, [], Map.to_list(acc)}]}
end
@doc """
Invoked for runtime windows.
"""
def runtime!(query, runtime, file, line) do
windows =
Enum.map(runtime, fn {name, compile_acc, runtime_acc, params, escape_acc} ->
{{acc, subqueries}, params} = do_runtime_window!(runtime_acc, query, {compile_acc, escape_acc.subqueries}, params)
expr = %Ecto.Query.ByExpr{expr: Enum.reverse(acc), params: Enum.reverse(params), file: file, line: line, subqueries: subqueries}
{name, expr}
end)
apply(query, windows)
end
defp do_runtime_window!([{:order_by, order_by} | kw], query, {acc, subqueries_acc}, params) do
{order_by, params, subqueries} = OrderBy.order_by_or_distinct!(:order_by, query, order_by, params)
do_runtime_window!(kw, query, {[{:order_by, order_by} | acc], subqueries_acc ++ subqueries}, params)
end
defp do_runtime_window!([{:partition_by, partition_by} | kw], query, {acc, subqueries_acc}, params) do
{partition_by, params, subqueries} = GroupBy.group_or_partition_by!(:partition_by, query, partition_by, params)
do_runtime_window!(kw, query, {[{:partition_by, partition_by} | acc], subqueries_acc ++ subqueries}, params)
end
defp do_runtime_window!([{:frame, frame} | kw], query, {acc, subqueries_acc}, params) do
case frame do
%Ecto.Query.DynamicExpr{} ->
{frame, params, _count} = Builder.Dynamic.partially_expand(:windows, query, frame, params, length(params))
do_runtime_window!(kw, query, {[{:frame, frame} | acc], subqueries_acc}, params)
_ ->
raise ArgumentError,
"expected a dynamic or fragment in `:frame`, got: `#{inspect frame}`"
end
end
defp do_runtime_window!([], _query, acc, params), do: {acc, params}
@doc """
The callback applied by `build/4` to build the query.
"""
@spec apply(Ecto.Queryable.t, Keyword.t) :: Ecto.Query.t
def apply(%Ecto.Query{windows: windows} = query, definitions) do
merged = Keyword.merge(windows, definitions, fn name, _, _ ->
Builder.error! "window with name #{name} is already defined"
end)
%{query | windows: merged}
end
def apply(query, definitions) do
apply(Ecto.Queryable.to_query(query), definitions)
end
end

View File

@@ -0,0 +1,442 @@
import Inspect.Algebra
import Kernel, except: [to_string: 1]
alias Ecto.Query.{DynamicExpr, JoinExpr, QueryExpr, WithExpr, LimitExpr}
defimpl Inspect, for: Ecto.Query.DynamicExpr do
def inspect(%DynamicExpr{binding: binding} = dynamic, opts) do
binding =
Enum.map(binding, fn
{{:^, _, [as]}, bind} when is_atom(as) -> {as, bind}
other -> other
end)
dynamic = %{dynamic | binding: binding}
joins =
binding
|> Enum.drop(1)
|> Enum.with_index()
|> Enum.map(&%JoinExpr{ix: &1})
aliases =
for({as, _} when is_atom(as) <- binding, do: as)
|> Enum.with_index()
|> Map.new()
query = %Ecto.Query{joins: joins, aliases: aliases}
{expr, binding, params, subqueries, _, _} =
Ecto.Query.Builder.Dynamic.fully_expand(query, dynamic)
names =
Enum.map(binding, fn
{_, {name, _, _}} -> name
{name, _, _} -> name
end)
query_expr = %{expr: expr, params: params, subqueries: subqueries}
inspected = Inspect.Ecto.Query.expr(expr, List.to_tuple(names), query_expr)
container_doc("dynamic(", [Macro.to_string(binding), inspected], ")", opts, fn str, _ ->
str
end)
end
end
defimpl Inspect, for: Ecto.Query do
@doc false
def inspect(query, opts) do
list =
Enum.map(to_list(query), fn
{key, string} ->
concat(Atom.to_string(key) <> ": ", string)
string ->
string
end)
result = container_doc("#Ecto.Query<", list, ">", opts, fn str, _ -> str end)
case query.with_ctes do
%WithExpr{recursive: recursive, queries: [_ | _] = queries} ->
with_ctes =
Enum.map(queries, fn {name, cte_opts, query} ->
cte =
case query do
%Ecto.Query{} -> __MODULE__.inspect(query, opts)
%Ecto.Query.QueryExpr{} -> expr(query, {})
end
concat([
"|> with_cte(\"" <> name <> "\", materialized: ",
inspect(cte_opts[:materialized]),
", as: ",
cte,
")"
])
end)
result = if recursive, do: glue(result, "\n", "|> recursive_ctes(true)"), else: result
[result | with_ctes] |> Enum.intersperse(break("\n")) |> concat()
_ ->
result
end
end
@doc false
def to_string(query) do
Enum.map_join(to_list(query), ",\n ", fn
{key, string} ->
Atom.to_string(key) <> ": " <> string
string ->
string
end)
end
defp to_list(query) do
names =
query
|> collect_sources()
|> generate_letters()
|> generate_names()
|> List.to_tuple()
from = bound_from(query.from, elem(names, 0), names)
joins = joins(query.joins, names)
preloads = preloads(query.preloads)
assocs = assocs(query.assocs, names)
windows = windows(query.windows, names)
combinations = combinations(query.combinations)
limit = limit(query.limit, names)
wheres = bool_exprs(%{and: :where, or: :or_where}, query.wheres, names)
group_bys = kw_exprs(:group_by, query.group_bys, names)
havings = bool_exprs(%{and: :having, or: :or_having}, query.havings, names)
order_bys = kw_exprs(:order_by, query.order_bys, names)
updates = kw_exprs(:update, query.updates, names)
lock = kw_inspect(:lock, query.lock)
offset = kw_expr(:offset, query.offset, names)
select = kw_expr(:select, query.select, names)
distinct = kw_expr(:distinct, query.distinct, names)
Enum.concat([
from,
joins,
wheres,
group_bys,
havings,
windows,
combinations,
order_bys,
limit,
offset,
lock,
distinct,
updates,
select,
preloads,
assocs
])
end
defp bound_from(nil, name, _names), do: ["from #{name} in query"]
defp bound_from(from, name, names) do
["from #{name} in #{inspect_source(from, names)}"] ++ kw_as_and_prefix(from)
end
defp inspect_source(%{source: %Ecto.Query{} = query}, _names), do: "^" <> inspect(query)
defp inspect_source(%{source: %Ecto.SubQuery{query: query}}, _names),
do: "subquery(#{to_string(query)})"
defp inspect_source(%{source: {source, nil}}, _names), do: inspect(source)
defp inspect_source(%{source: {nil, schema}}, _names), do: inspect(schema)
defp inspect_source(%{source: {:fragment, _, _} = source} = part, names),
do: "#{expr(source, names, part)}"
defp inspect_source(%{source: {:values, _, [types | _]}}, _names) do
fields = Keyword.keys(types)
"values (#{Enum.join(fields, ", ")})"
end
defp inspect_source(%{source: {source, schema}}, _names) do
inspect(if source == schema.__schema__(:source), do: schema, else: {source, schema})
end
defp joins(joins, names) do
joins
|> Enum.with_index()
|> Enum.flat_map(fn {expr, ix} -> join(expr, elem(names, expr.ix || ix + 1), names) end)
end
defp join(%JoinExpr{qual: qual, assoc: {ix, right}, on: on} = join, name, names) do
string = "#{name} in assoc(#{elem(names, ix)}, #{inspect(right)})"
[{join_qual(qual), string}] ++ kw_as_and_prefix(join) ++ maybe_on(on, names)
end
defp join(%JoinExpr{qual: qual, on: on} = join, name, names) do
string = "#{name} in #{inspect_source(join, names)}"
[{join_qual(qual), string}] ++ kw_as_and_prefix(join) ++ [on: expr(on, names)]
end
defp maybe_on(%QueryExpr{expr: true}, _names), do: []
defp maybe_on(%QueryExpr{} = on, names), do: [on: expr(on, names)]
defp preloads([]), do: []
defp preloads(preloads), do: [preload: inspect(preloads)]
defp assocs([], _names), do: []
defp assocs(assocs, names), do: [preload: expr(assocs(assocs), names, %{})]
defp assocs(assocs) do
Enum.map(assocs, fn
{field, {idx, []}} ->
{field, {:&, [], [idx]}}
{field, {idx, children}} ->
{field, {{:&, [], [idx]}, assocs(children)}}
end)
end
defp windows(windows, names) do
Enum.map(windows, &window(&1, names))
end
defp window({name, %{expr: definition} = part}, names) do
{:windows, "[#{name}: " <> expr(definition, names, part) <> "]"}
end
defp combinations(combinations) do
Enum.map(combinations, fn {key, val} -> {key, "(" <> to_string(val) <> ")"} end)
end
defp limit(nil, _names), do: []
defp limit(%LimitExpr{with_ties: false} = limit, names) do
[{:limit, expr(limit, names)}]
end
defp limit(%LimitExpr{with_ties: with_ties} = limit, names) do
[{:limit, expr(limit, names)}] ++ kw_inspect(:with_ties, with_ties)
end
defp bool_exprs(keys, exprs, names) do
Enum.map(exprs, fn %{expr: expr, op: op} = part ->
{Map.fetch!(keys, op), expr(expr, names, part)}
end)
end
defp kw_exprs(key, exprs, names) do
Enum.map(exprs, &{key, expr(&1, names)})
end
defp kw_expr(_key, nil, _names), do: []
defp kw_expr(key, expr, names), do: [{key, expr(expr, names)}]
defp kw_inspect(_key, nil), do: []
defp kw_inspect(key, val), do: [{key, inspect(val)}]
defp kw_as_and_prefix(%{as: as, prefix: prefix}) do
kw_inspect(:as, as) ++ kw_inspect(:prefix, prefix)
end
defp expr(%{expr: expr} = part, names) do
expr(expr, names, part)
end
@doc false
def expr(expr, names, part) do
expr
|> Macro.traverse(:ok, &{prewalk(&1, names), &2}, &{postwalk(&1, names, part), &2})
|> elem(0)
|> macro_to_string()
end
defp macro_to_string(expr), do: Macro.to_string(expr)
# Tagged values
defp prewalk(%Ecto.Query.Tagged{value: value, tag: nil}, _) do
value
end
defp prewalk(%Ecto.Query.Tagged{value: value, tag: tag}, _) do
{:type, [], [value, tag]}
end
defp prewalk({{:., dot_meta, [{:&, _, [ix]}, field]}, meta, []}, names) do
{{:., dot_meta, [binding(names, ix), field]}, meta, []}
end
defp prewalk(node, _) do
node
end
# Convert variables to proper names
defp postwalk({:&, _, [ix]}, names, part) do
binding_to_expr(ix, names, part)
end
# Format field/2 with string name
defp postwalk({{:., _, [{_, _, _} = binding, field]}, meta, []}, _names, _part)
when is_binary(field) do
{:field, meta, [binding, field]}
end
# Remove parens from field calls
defp postwalk({{:., _, [_, _]} = dot, meta, []}, _names, _part) do
{dot, [no_parens: true] ++ meta, []}
end
# Interpolated unknown value
defp postwalk({:^, _, [_ix, _len]}, _names, _part) do
{:^, [], [{:..., [], nil}]}
end
# Interpolated known value
defp postwalk({:^, _, [ix]}, _, %{params: params}) do
value =
case Enum.at(params || [], ix) do
# Wrap the head in a block so it is not treated as a charlist
{[head | tail], _type} -> [{:__block__, [], [head]} | tail]
{value, _type} -> value
_ -> {:..., [], nil}
end
{:^, [], [value]}
end
# Types need to be converted back to AST for fields
defp postwalk({:type, meta, [expr, type]}, names, part) do
{:type, meta, [expr, type_to_expr(type, names, part)]}
end
# For keyword and interpolated fragments use normal escaping
defp postwalk({:fragment, _, [{_, _} | _] = parts}, _names, _part) do
{:fragment, [], unmerge_fragments(parts, "", [])}
end
# Subqueries
defp postwalk({:subquery, i}, _names, %{subqueries: subqueries}) do
{:subquery, [], [Enum.fetch!(subqueries, i).query]}
end
# Jason
defp postwalk({:json_extract_path, _, [expr, path]}, _names, _part) do
Enum.reduce(path, expr, fn element, acc ->
{{:., [], [Access, :get]}, [], [acc, element]}
end)
end
defp postwalk(node, _names, _part) do
node
end
defp binding_to_expr(ix, names, part) do
case part do
%{take: %{^ix => {:any, fields}}} when ix == 0 ->
fields
%{take: %{^ix => {tag, fields}}} ->
{tag, [], [binding(names, ix), fields]}
_ ->
binding(names, ix)
end
end
defp type_to_expr({ix, type}, names, part) when is_integer(ix) do
{{:., [], [binding_to_expr(ix, names, part), type]}, [no_parens: true], []}
end
defp type_to_expr({composite, type}, names, part) when is_atom(composite) do
{composite, type_to_expr(type, names, part)}
end
defp type_to_expr(type, _names, _part) do
type
end
defp unmerge_fragments([{:raw, s}, {:expr, v} | t], frag, args) do
unmerge_fragments(t, frag <> s <> "?", [v | args])
end
defp unmerge_fragments([{:raw, s}], frag, args) do
[frag <> s | Enum.reverse(args)]
end
defp join_qual(:inner), do: :join
defp join_qual(:inner_lateral), do: :inner_lateral_join
defp join_qual(:left), do: :left_join
defp join_qual(:left_lateral), do: :left_lateral_join
defp join_qual(:right), do: :right_join
defp join_qual(:full), do: :full_join
defp join_qual(:cross), do: :cross_join
defp join_qual(:cross_lateral), do: :cross_lateral_join
defp collect_sources(%{from: nil, joins: joins}) do
["query" | join_sources(joins)]
end
defp collect_sources(%{from: %{source: source}, joins: joins}) do
[from_sources(source) | join_sources(joins)]
end
defp from_sources(%Ecto.SubQuery{query: query}), do: from_sources(query.from.source)
defp from_sources({source, schema}), do: schema || source
defp from_sources(nil), do: "query"
defp from_sources({:fragment, _, _}), do: "fragment"
defp from_sources({:values, _, _}), do: "values"
defp join_sources(joins) do
joins
|> Enum.sort_by(& &1.ix)
|> Enum.map(fn
%JoinExpr{assoc: {_var, assoc}} ->
assoc
%JoinExpr{source: {:fragment, _, _}} ->
"fragment"
%JoinExpr{source: %Ecto.Query{from: from}} ->
from_sources(from.source)
%JoinExpr{source: source} ->
from_sources(source)
end)
end
defp generate_letters(sources) do
Enum.map(sources, fn source ->
source
|> Kernel.to_string()
|> normalize_source()
|> String.first()
|> String.downcase()
end)
end
defp generate_names(letters) do
{names, _} = Enum.map_reduce(letters, 0, &{:"#{&1}#{&2}", &2 + 1})
names
end
defp binding(names, pos) do
try do
{elem(names, pos), [], nil}
rescue
ArgumentError -> {:"unknown_binding_#{pos}!", [], nil}
end
end
defp normalize_source("Elixir." <> _ = source),
do: source |> Module.split() |> List.last()
defp normalize_source(source),
do: source
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,232 @@
defmodule Ecto.Query.WindowAPI do
@moduledoc """
Lists all windows functions.
Windows functions must always be used as the first argument
of `over/2` where the second argument is the name of a window:
from e in Employee,
select: {e.depname, e.empno, e.salary, over(avg(e.salary), :department)},
windows: [department: [partition_by: e.depname]]
In the example above, we get the average salary per department.
`:department` is the window name, partitioned by `e.depname`
and `avg/1` is the window function.
However, note that defining a window is not necessary, as the
window definition can be given as the second argument to `over`:
from e in Employee,
select: {e.depname, e.empno, e.salary, over(avg(e.salary), partition_by: e.depname)}
Both queries are equivalent. However, if you are using the same
partitioning over and over again, defining a window will reduce
the query size. See `Ecto.Query.windows/3` for all possible window
expressions, such as `:partition_by` and `:order_by`.
"""
@dialyzer :no_return
@doc """
Counts the entries in the table.
from p in Post, select: count()
"""
def count, do: doc! []
@doc """
Counts the given entry.
from p in Post, select: count(p.id)
"""
def count(value), do: doc! [value]
@doc """
Calculates the average for the given entry.
from p in Payment, select: avg(p.value)
"""
def avg(value), do: doc! [value]
@doc """
Calculates the sum for the given entry.
from p in Payment, select: sum(p.value)
"""
def sum(value), do: doc! [value]
@doc """
Calculates the minimum for the given entry.
from p in Payment, select: min(p.value)
"""
def min(value), do: doc! [value]
@doc """
Calculates the maximum for the given entry.
from p in Payment, select: max(p.value)
"""
def max(value), do: doc! [value]
@doc """
Defines a value based on the function and the window. See moduledoc for more information.
from e in Employee, select: over(avg(e.salary), partition_by: e.depname)
"""
def over(window_function, window_name), do: doc! [window_function, window_name]
@doc """
Returns number of the current row within its partition, counting from 1.
from p in Post,
select: row_number() |> over(partition_by: p.category_id, order_by: p.date)
Note that this function must be invoked using window function syntax.
"""
def row_number(), do: doc! []
@doc """
Returns rank of the current row with gaps; same as `row_number/0` of its first peer.
from p in Post,
select: rank() |> over(partition_by: p.category_id, order_by: p.date)
Note that this function must be invoked using window function syntax.
"""
def rank(), do: doc! []
@doc """
Returns rank of the current row without gaps; this function counts peer groups.
from p in Post,
select: dense_rank() |> over(partition_by: p.category_id, order_by: p.date)
Note that this function must be invoked using window function syntax.
"""
def dense_rank(), do: doc! []
@doc """
Returns relative rank of the current row: (rank - 1) / (total rows - 1).
from p in Post,
select: percent_rank() |> over(partition_by: p.category_id, order_by: p.date)
Note that this function must be invoked using window function syntax.
"""
def percent_rank(), do: doc! []
@doc """
Returns relative rank of the current row:
(number of rows preceding or peer with current row) / (total rows).
from p in Post,
select: cume_dist() |> over(partition_by: p.category_id, order_by: p.date)
Note that this function must be invoked using window function syntax.
"""
def cume_dist(), do: doc! []
@doc """
Returns integer ranging from 1 to the argument value, dividing the partition as equally as possible.
from p in Post,
select: ntile(10) |> over(partition_by: p.category_id, order_by: p.date)
Note that this function must be invoked using window function syntax.
"""
def ntile(num_buckets), do: doc! [num_buckets]
@doc """
Returns value evaluated at the row that is the first row of the window frame.
from p in Post,
select: first_value(p.id) |> over(partition_by: p.category_id, order_by: p.date)
Note that this function must be invoked using window function syntax.
"""
def first_value(value), do: doc! [value]
@doc """
Returns value evaluated at the row that is the last row of the window frame.
from p in Post,
select: last_value(p.id) |> over(partition_by: p.category_id, order_by: p.date)
Note that this function must be invoked using window function syntax.
"""
def last_value(value), do: doc! [value]
@doc """
Applies the given expression as a FILTER clause against an
aggregate. This is currently only supported by Postgres.
from p in Post,
select: avg(p.value)
|> filter(p.value > 0 and p.value < 100)
|> over(partition_by: p.category_id, order_by: p.date)
"""
def filter(value, filter), do: doc! [value, filter]
@doc """
Returns value evaluated at the row that is the nth row of the window
frame (counting from 1); `nil` if no such row.
from p in Post,
select: nth_value(p.id, 4) |> over(partition_by: p.category_id, order_by: p.date)
Note that this function must be invoked using window function syntax.
"""
def nth_value(value, nth), do: doc! [value, nth]
@doc """
Returns value evaluated at the row that is offset rows before
the current row within the partition.
If there is no such row, instead return default (which must be of the
same type as value). Both offset and default are evaluated with respect
to the current row. If omitted, offset defaults to 1 and default to `nil`.
from e in Events,
windows: [w: [partition_by: e.name, order_by: e.tick]],
select: {
e.tick,
e.action,
e.name,
lag(e.action) |> over(:w), # previous_action
lead(e.action) |> over(:w) # next_action
}
Note that this function must be invoked using window function syntax.
"""
def lag(value, offset \\ 1, default \\ nil), do: doc! [value, offset, default]
@doc """
Returns value evaluated at the row that is offset rows after
the current row within the partition.
If there is no such row, instead return default (which must be of the
same type as value). Both offset and default are evaluated with respect
to the current row. If omitted, offset defaults to 1 and default to `nil`.
from e in Events,
windows: [w: [partition_by: e.name, order_by: e.tick]],
select: {
e.tick,
e.action,
e.name,
lag(e.action) |> over(:w), # previous_action
lead(e.action) |> over(:w) # next_action
}
Note that this function must be invoked using window function syntax.
"""
def lead(value, offset \\ 1, default \\ nil), do: doc! [value, offset, default]
defp doc!(_) do
raise "the functions in Ecto.Query.WindowAPI should not be invoked directly, " <>
"they serve for documentation purposes only"
end
end

View File

@@ -0,0 +1,151 @@
defprotocol Ecto.Queryable do
@moduledoc """
Converts a data structure into an `Ecto.Query`.
This is used by `Ecto.Repo` and also by the [`from`](`Ecto.Query.from/2`) macro.
For example, [`Repo.all`](`c:Ecto.Repo.all/2`)
expects any queryable as argument, which is why you can do `Repo.all(MySchema)`
or `Repo.all(query)`. Furthermore, when you write `from ALIAS in QUERYABLE`,
`QUERYABLE` accepts any data structure that implements `Ecto.Queryable`.
This module defines a few default implementations so let us go over each and
how to use them.
## Atom
The most common use case for this protocol is to convert atoms representing
an `Ecto.Schema` module into a query. This is what happens when you write:
query = from(p in Person)
Or when you directly pass a schema to a repository:
Repo.all(Person)
In case you did not know, Elixir modules are just atoms. This implementation
takes the provided module name and then tries to load the associated schema.
If no schema exists, it will raise `Protocol.UndefinedError`.
## BitString
This implementation allows you to directly specify a table that you would like
to query from:
from(
p in "people",
select: {p.first_name, p.last_name}
)
Or:
Repo.delete_all("people")
While this is quite simple to use, some repository operations, such as
`Repo.all`, require a `select` clause. When you query a schema, the
select is automatically defined for you based on the schema fields,
but when you pass a table directly, you need to explicitly list them.
This limitation now brings us to our next implementation!
## Tuple
Similar to the `BitString` implementation, this allows you to specify the
underlying table that you would like to query; however, this additionally
allows you to specify the schema you would like to use:
from(p in {"filtered_people", Person})
This can be particularly useful if you have database views that filter or
aggregate the underlying data of a table but share the same schema. This means
that you can reuse the same schema while specifying a separate "source" for
the data.
## Ecto.Query
This is a simple pass through. After all, all `Ecto.Query` instances
can be converted into `Ecto.Query`:
Repo.all(from u in User, where: u.active)
This also enables Ecto queries to compose, since we can pass one query
as the source of another:
active_users = from u in User, where: u.active
ordered_active_users = from u in active_users, order_by: u.created_at
## Ecto.SubQuery
Ecto also allows you to compose queries using subqueries. Imagine you
have a table of "people". Now imagine that you want to do something with
people with the most common last names. To get that list, you could write
something like:
sub = from(
p in Person,
group_by: p.last_name,
having: count(p.last_name) > 1,
select: %{last_name: p.last_name, count: count(p.last_name)}
)
Now if you want to do something else with this data, perhaps join on
additional tables and perform some calculations, you can do that as so:
from(
p in subquery(sub),
# other filtering etc here
)
Please note that the `Ecto.Query.subquery/2` is needed here to convert the
`Ecto.Query` into an instance of `Ecto.SubQuery`. This protocol then wraps
it into an `Ecto.Query`, but using the provided subquery in the FROM clause.
Please see `Ecto.Query.subquery/2` for more information.
"""
@doc """
Converts the given `data` into an `Ecto.Query`.
"""
def to_query(data)
end
defimpl Ecto.Queryable, for: Ecto.Query do
def to_query(query), do: query
end
defimpl Ecto.Queryable, for: Ecto.SubQuery do
def to_query(subquery) do
%Ecto.Query{from: %Ecto.Query.FromExpr{source: subquery}}
end
end
defimpl Ecto.Queryable, for: BitString do
def to_query(source) when is_binary(source) do
%Ecto.Query{from: %Ecto.Query.FromExpr{source: {source, nil}}}
end
end
defimpl Ecto.Queryable, for: Atom do
def to_query(module) do
try do
module.__schema__(:query)
rescue
UndefinedFunctionError ->
message =
if :code.is_loaded(module) do
"the given module does not provide a schema"
else
"the given module does not exist"
end
raise Protocol.UndefinedError, protocol: @protocol, value: module, description: message
FunctionClauseError ->
raise Protocol.UndefinedError, protocol: @protocol, value: module, description: "the given module is an embedded schema"
end
end
end
defimpl Ecto.Queryable, for: Tuple do
def to_query({source, schema} = from)
when is_binary(source) and is_atom(schema) and not is_nil(schema) do
%Ecto.Query{from: %Ecto.Query.FromExpr{source: from, prefix: schema.__schema__(:prefix)}}
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,141 @@
defmodule Ecto.Repo.Assoc do
# The module invoked by repo modules
# for association related functionality.
@moduledoc false
@doc """
Transforms a result set based on query assocs, loading
the associations onto their parent schema.
"""
@spec query([list], list, tuple, (list -> list)) :: [Ecto.Schema.t]
def query(rows, assocs, sources, fun)
def query([], _assocs, _sources, _fun), do: []
def query(rows, [], _sources, fun), do: Enum.map(rows, fun)
def query(rows, assocs, sources, fun) do
# Create rose tree of accumulator dicts in the same
# structure as the fields tree
accs = create_accs(0, assocs, sources, [])
# Populate tree of dicts of associated entities from the result set
{_keys, _cache, rows, sub_dicts} = Enum.reduce(rows, accs, fn row, acc ->
merge(fun.(row), acc, 0) |> elem(0)
end)
# Create the reflections that will be loaded into memory.
refls = create_refls(0, assocs, sub_dicts, sources)
# Retrieve and load the assocs from cached dictionaries recursively
for {item, sub_structs} <- Enum.reverse(rows) do
[load_assocs(item, refls)|sub_structs]
end
end
defp merge([struct|sub_structs], {primary_keys, cache, dict, sub_dicts}, parent_key) do
{struct, child_key} =
if struct do
{child_key, all_nil?} =
Enum.map_reduce(primary_keys, true, fn primary_key, all_nil? ->
case struct do
%_{^primary_key => nil} -> raise Ecto.NoPrimaryKeyValueError, struct: struct
# We allow maps to be returned with all `nil` values in queries without
# preloads. For preloads we have to treat maps with all `nil` values as
# `nil` instead of a map otherwise we can't associate the missing
# association to the parent struct
%{^primary_key => value} -> {value, all_nil? and value == nil}
%{} -> raise Ecto.NoPrimaryKeyValueError, struct: struct
end
end)
if all_nil?, do: {nil, nil}, else: {struct, child_key}
else
{nil, nil}
end
# Traverse sub_structs adding one by one to the tree.
# Note we need to traverse even if we don't have a child_key
# due to nested associations.
{sub_dicts, sub_structs} = Enum.map_reduce(sub_dicts, sub_structs, &merge(&2, &1, child_key))
cache_key = cache_key(parent_key, child_key, sub_structs, dict)
if struct && parent_key && not Map.get(cache, cache_key, false) do
cache = Map.put(cache, cache_key, true)
item = {child_key, struct}
# If we have a list, we are at the root, so we also store the sub structs
dict = update_dict(dict, parent_key, item, sub_structs)
{{primary_keys, cache, dict, sub_dicts}, sub_structs}
else
{{primary_keys, cache, dict, sub_dicts}, sub_structs}
end
end
defp cache_key(parent_key, child_key, sub_structs, dict) when is_list(dict) do
{parent_key, child_key, sub_structs}
end
defp cache_key(parent_key, child_key, _sub_structs, dict) when is_map(dict) do
{parent_key, child_key}
end
defp update_dict(dict, _parent_key, item, sub_structs) when is_list(dict) do
[{item, sub_structs} | dict]
end
defp update_dict(dict, parent_key, item, _sub_structs) when is_map(dict) do
Map.update(dict, parent_key, [item], &[item | &1])
end
defp load_assocs({child_key, struct}, refls) do
Enum.reduce refls, struct, fn {dict, refl, sub_refls}, acc ->
%{field: field, cardinality: cardinality} = refl
loaded =
dict
|> Map.get(child_key, [])
|> Enum.reverse()
|> Enum.map(&load_assocs(&1, sub_refls))
|> maybe_first(cardinality)
Map.put(acc, field, loaded)
end
end
defp maybe_first(list, :one), do: List.first(list)
defp maybe_first(list, _), do: list
defp create_refls(idx, fields, dicts, sources) do
schema = get_assoc_schema(sources, idx)
Enum.map(:lists.zip(dicts, fields), fn
{{_primary_keys, _cache, dict, sub_dicts}, {field, {child_idx, child_fields}}} ->
refl = schema.__schema__(:association, field)
sub_refls = create_refls(child_idx, child_fields, sub_dicts, sources)
{dict, refl, sub_refls}
end)
end
defp create_accs(idx, fields, sources, initial_dict) do
acc = Enum.map(fields, fn {_field, {child_idx, child_fields}} ->
create_accs(child_idx, child_fields, sources, %{})
end)
schema = get_assoc_schema(sources, idx)
case schema.__schema__(:primary_key) do
[] -> raise Ecto.NoPrimaryKeyFieldError, schema: schema
pk -> {pk, %{}, initial_dict, acc}
end
end
defp get_assoc_schema(sources, idx) do
case elem(sources, idx) do
{_, schema, _} ->
schema
%Ecto.SubQuery{select: {:source, {_, schema}, _, _}} ->
schema
end
end
end

View File

@@ -0,0 +1,712 @@
defmodule Ecto.Repo.Preloader do
# The module invoked by user defined repo_names
# for preload related functionality.
@moduledoc false
require Ecto.Query
require Logger
alias Ecto.Query.DynamicExpr
@doc """
Transforms a result set based on query preloads, loading
the associations onto their parent schema.
"""
@spec query([list], Ecto.Repo.t, list, Access.t, list, fun, {adapter_meta :: map, opts :: Keyword.t}) :: [list]
def query([], _repo_name, _preloads, _take, _assocs, _fun, _tuplet), do: []
def query(rows, _repo_name, [], _take, _assocs, fun, _tuplet), do: Enum.map(rows, fun)
def query(rows, repo_name, preloads, take, assocs, fun, tuplet) do
assocs = normalize_query_assocs(assocs)
rows
|> extract()
|> normalize_and_preload_each(repo_name, preloads, take, assocs, tuplet)
|> unextract(rows, fun)
end
defp extract([[nil|_]|t2]), do: extract(t2)
defp extract([[h|_]|t2]), do: [h|extract(t2)]
defp extract([]), do: []
defp unextract(structs, [[nil|_] = h2|t2], fun), do: [fun.(h2)|unextract(structs, t2, fun)]
defp unextract([h1|structs], [[_|t1]|t2], fun), do: [fun.([h1|t1])|unextract(structs, t2, fun)]
defp unextract([], [], _fun), do: []
@doc """
Implementation for `Ecto.Repo.preload/2`.
"""
@spec preload(structs, atom, atom | list, {adapter_meta :: map, opts :: Keyword.t}) ::
structs when structs: [Ecto.Schema.t] | Ecto.Schema.t | nil
def preload(nil, _repo_name, _preloads, _tuplet) do
nil
end
def preload(structs, repo_name, preloads, {_adapter_meta, opts} = tuplet) when is_list(structs) do
normalize_and_preload_each(structs, repo_name, preloads, opts[:take], %{}, tuplet)
end
def preload(struct, repo_name, preloads, {_adapter_meta, opts} = tuplet) when is_map(struct) do
normalize_and_preload_each([struct], repo_name, preloads, opts[:take], %{}, tuplet) |> hd()
end
defp normalize_and_preload_each(
structs,
repo_name,
preloads,
take,
query_assocs,
{adapter_meta, opts}
) do
tuplet = {adapter_meta, Keyword.put(opts, :ecto_query, :preload)}
preloads = normalize(preloads, take, preloads)
preload_each(structs, repo_name, preloads, query_assocs, tuplet)
rescue
e ->
# Reraise errors so we ignore the preload inner stacktrace
filter_and_reraise e, __STACKTRACE__
end
## Preloading
defp preload_each(structs, _repo_name, [], _query_assocs, _tuplet), do: structs
defp preload_each([], _repo_name, _preloads, _query_assocs, _tuplet), do: []
defp preload_each(structs, repo_name, preloads, query_assocs, tuplet) do
if sample = Enum.find(structs, & &1) do
module = sample.__struct__
prefix = preload_prefix(tuplet, sample)
{assocs, throughs, embeds} = expand(module, preloads, query_assocs, {%{}, [], []})
structs = preload_embeds(structs, embeds, repo_name, tuplet)
structs = preload_throughs(structs, throughs, repo_name, query_assocs, tuplet)
{fetched_assocs, to_fetch_queries} =
prepare_queries(structs, module, assocs, prefix, repo_name, tuplet)
fetched_queries = maybe_pmap(to_fetch_queries, repo_name, tuplet)
assocs = preload_assocs(fetched_assocs, fetched_queries, repo_name, query_assocs, tuplet)
for struct <- structs do
struct = Enum.reduce assocs, struct, &load_assoc/2
struct = Enum.reduce throughs, struct, &load_through/2
struct
end
else
structs
end
end
defp preload_prefix({_adapter_meta, opts}, sample) do
case Keyword.fetch(opts, :prefix) do
{:ok, prefix} ->
prefix
:error ->
case sample do
%{__meta__: %{prefix: prefix}} -> prefix
# Must be an embedded schema
_ -> nil
end
end
end
## Association preloading
# First we traverse all assocs and find which queries we need to run.
defp prepare_queries(structs, module, assocs, prefix, repo_name, tuplet) do
Enum.reduce(assocs, {[], []}, fn
{_key, {{:assoc, assoc, related_key}, take, query, preloads}}, {assocs, queries} ->
{fetch_ids, loaded_ids, loaded_structs} = fetch_ids(structs, module, assoc, tuplet)
queries =
if fetch_ids != [] do
[
fn tuplet ->
fetch_query(fetch_ids, assoc, repo_name, query, prefix, related_key, take, tuplet)
end
| queries
]
else
queries
end
{[{assoc, fetch_ids != [], loaded_ids, loaded_structs, preloads} | assocs], queries}
end)
end
# Then we execute queries in parallel
defp maybe_pmap(preloaders, _repo_name, {adapter_meta, opts}) do
if match?([_, _ | _] , preloaders) and not adapter_meta.adapter.checked_out?(adapter_meta) and
Keyword.get(opts, :in_parallel, true) do
# We pass caller: self() so the ownership pool knows where
# to fetch the connection from and set the proper timeouts.
# Note while the ownership pool uses '$callers' from pdict,
# it does not do so in automatic mode, hence this line is
# still necessary.
opts = Keyword.put_new(opts, :caller, self())
on_preloader_spawn = Keyword.get(opts, :on_preloader_spawn, fn -> :ok end)
preloaders
|> Task.async_stream(fn preloader ->
on_preloader_spawn.()
preloader.({adapter_meta, opts})
end, timeout: :infinity)
|> Enum.map(fn
{:ok, assoc} -> assoc
{:exit, reason} -> exit(reason)
end)
else
Enum.map(preloaders, &(&1.({adapter_meta, opts})))
end
end
# Then we unpack the query results, merge them, and preload recursively
defp preload_assocs(
[{assoc, query?, loaded_ids, loaded_structs, sub_preloads} | assocs],
queries,
repo_name,
query_assocs,
tuplet
) do
{fetch_ids, fetch_structs, queries} = maybe_unpack_query(query?, queries)
sub_query_assocs = Map.get(query_assocs, assoc.field, %{})
all = preload_each(Enum.reverse(loaded_structs, fetch_structs), repo_name, sub_preloads, sub_query_assocs, tuplet)
entry = {:assoc, assoc, assoc_map(assoc.cardinality, Enum.reverse(loaded_ids, fetch_ids), all)}
[entry | preload_assocs(assocs, queries, repo_name, query_assocs, tuplet)]
end
defp preload_assocs([] = _assocs, [] = _queries, _, _, _), do: []
defp preload_embeds(structs, [] = _embeds, _, _), do: structs
defp preload_embeds(structs, [embed | embeds], repo_name, tuplet) do
{%{field: field, cardinality: card}, sub_preloads} = embed
{embed_structs, counts} =
Enum.flat_map_reduce(structs, [], fn
%{^field => embeds}, counts when is_list(embeds) -> {embeds, [length(embeds) | counts]}
%{^field => nil}, counts -> {[], [0 | counts]}
%{^field => embed}, counts -> {[embed], [1 | counts]}
nil, counts -> {[], [0 | counts]}
struct, _counts -> raise ArgumentError, "expected #{inspect(struct)} to contain embed `#{field}`"
end)
# It is not possible for an embed to be preloaded through Ecto.Query.preload
# Therefore, we don't consider associations coming from queries
embed_structs = preload_each(embed_structs, repo_name, sub_preloads, %{}, tuplet)
structs = put_through_or_embed(card, field, structs, embed_structs, Enum.reverse(counts), [])
preload_embeds(structs, embeds, repo_name, tuplet)
end
defp preload_throughs(structs, [] = _throughs, _, _, _), do: structs
defp preload_throughs(
structs,
[{_, _, false = _from_query?} | throughs],
repo_name,
query_assocs,
tuplet
) do
# Through associations will not be preloaded directly unless they were
# loaded through a join using Ecto.Query.preload. When using Ecto.Repo.preload
# or Ecto.Query.preload where the through association is not part of a join,
# the chain of associations making up the through association are preloaded instead.
preload_throughs(structs, throughs, repo_name, query_assocs, tuplet)
end
defp preload_throughs(structs, [through | throughs], repo_name, query_assocs, tuplet) do
{{_, %{field: field, cardinality: card}, _}, sub_preloads, true} = through
sub_query_assocs = Map.get(query_assocs, field, %{})
{through_structs, counts} =
Enum.flat_map_reduce(structs, [], fn
%{^field => throughs}, counts when is_list(throughs) -> {throughs, [length(throughs) | counts]}
%{^field => nil}, counts -> {[], [0 | counts]}
%{^field => through}, counts -> {[through], [1 | counts]}
nil, counts -> {[], [0 | counts]}
struct, _counts -> raise ArgumentError, "expected #{inspect(struct)} to contain through association `#{field}`"
end)
through_structs = preload_each(through_structs, repo_name, sub_preloads, sub_query_assocs, tuplet)
structs = put_through_or_embed(card, field, structs, through_structs, Enum.reverse(counts), [])
preload_throughs(structs, throughs, repo_name, query_assocs, tuplet)
end
defp put_through_or_embed(_card, _field, [], [], [], acc), do: Enum.reverse(acc)
defp put_through_or_embed(card, field, [struct | structs], loaded_structs, [0 | counts], acc),
do: put_through_or_embed(card, field, structs, loaded_structs, counts, [struct | acc])
defp put_through_or_embed(:one, field, [struct | structs], [loaded | loaded_structs], [1 | counts], acc),
do: put_through_or_embed(:one, field, structs, loaded_structs, counts, [Map.put(struct, field, loaded) | acc])
defp put_through_or_embed(:many, field, [struct | structs], loaded_structs, [count | counts], acc) do
{current_loaded, rest_loaded} = split_n(loaded_structs, count, [])
acc = [Map.put(struct, field, Enum.reverse(current_loaded)) | acc]
put_through_or_embed(:many, field, structs, rest_loaded, counts, acc)
end
defp maybe_unpack_query(false, queries), do: {[], [], queries}
defp maybe_unpack_query(true, [{ids, structs} | queries]), do: {ids, structs, queries}
defp fetch_ids(structs, module, assoc, {_adapter_meta, opts}) do
%{field: field, owner_key: owner_key, cardinality: card} = assoc
force? = Keyword.get(opts, :force, false)
Enum.reduce structs, {[], [], []}, fn
nil, acc ->
acc
struct, {fetch_ids, loaded_ids, loaded_structs} ->
assert_struct!(module, struct)
%{^owner_key => id, ^field => value} = struct
loaded? = Ecto.assoc_loaded?(value) and not force?
if loaded? and is_nil(id) and not Ecto.Changeset.Relation.empty?(assoc, value) do
Logger.warning """
association `#{field}` for `#{inspect(module)}` has a loaded value but \
its association key `#{owner_key}` is nil. This usually means one of:
* `#{owner_key}` was not selected in a query
* the struct was set with default values for `#{field}` which now you want to override
If this is intentional, set force: true to disable this warning
"""
end
cond do
card == :one and loaded? ->
{fetch_ids, [id | loaded_ids], [value | loaded_structs]}
card == :many and loaded? ->
{fetch_ids, [{id, length(value)} | loaded_ids], value ++ loaded_structs}
is_nil(id) ->
{fetch_ids, loaded_ids, loaded_structs}
true ->
{[id | fetch_ids], loaded_ids, loaded_structs}
end
end
end
defp fetch_query(ids, assoc, _repo_name, query, _prefix, related_key, _take, _tuplet)
when is_function(query, 1) or is_function(query, 2) do
# Note we use an explicit sort because we don't want
# to reorder based on the struct. Only the ID.
ids
|> Enum.uniq()
|> preload_function(assoc, query)
|> fetched_records_to_tuple_ids(assoc, related_key)
|> Enum.sort(fn {id1, _}, {id2, _} -> id1 <= id2 end)
|> unzip_ids([], [])
end
defp fetch_query(ids, %{cardinality: card} = assoc, repo_name, query, prefix, related_key, take, tuplet) do
query = assoc.__struct__.assoc_query(assoc, query, Enum.uniq(ids))
related_field_ast = related_key_to_field(query, related_key)
# Normalize query
query = %{Ecto.Query.Planner.ensure_select(query, take || true) | prefix: prefix}
# Add the related key to the query results
query = update_in query.select.expr, &{:{}, [], [related_field_ast, &1]}
# If we are returning many results, we must sort by the key too
query =
case {card, query.combinations} do
{:many, [{kind, _} | []]} ->
raise ArgumentError,
"`#{kind}` queries must be wrapped inside of a subquery " <>
"when preloading a `has_many` or `many_to_many` association. " <>
"You must also ensure that all members of the `#{kind}` query " <>
"select the parent's foreign key"
{:many, _} ->
query = add_preload_order(assoc.preload_order, query)
update_in query.order_bys, fn order_bys ->
[%Ecto.Query.ByExpr{expr: [asc: related_field_ast], params: [],
file: __ENV__.file, line: __ENV__.line}|order_bys]
end
{:one, _} ->
query
end
unzip_ids Ecto.Repo.Queryable.all(repo_name, query, tuplet), [], []
end
defp preload_function(ids, _assoc, query) when is_function(query, 1), do: query.(ids)
defp preload_function(ids, assoc, query) when is_function(query, 2), do: query.(ids, assoc)
defp fetched_records_to_tuple_ids([], _assoc, _related_key),
do: []
defp fetched_records_to_tuple_ids([%{} | _] = entries, _assoc, {0, key}),
do: Enum.map(entries, &{Map.fetch!(&1, key), &1})
defp fetched_records_to_tuple_ids([{_, %{}} | _] = entries, _assoc, _related_key),
do: entries
defp fetched_records_to_tuple_ids([entry | _], assoc, _),
do: raise """
invalid custom preload for `#{assoc.field}` on `#{inspect assoc.owner}`.
For many_to_many associations, the custom function given to preload should \
return a tuple with the associated key as first element and the struct as \
second element.
For example, imagine posts has many to many tags through a posts_tags table. \
When preloading the tags, you may write:
custom_tags = fn post_ids ->
Repo.all(
from t in Tag,
join: pt in "posts_tags",
where: t.custom and pt.post_id in ^post_ids and pt.tag_id == t.id
)
end
from Post, preload: [tags: ^custom_tags]
Unfortunately the query above is not enough because Ecto won't know how to \
associate the posts with the tags. In those cases, you need to return a tuple \
with the `post_id` as first element and the tag struct as second. The new query \
will have a select field as follows:
from t in Tag,
join: pt in "posts_tags",
where: t.custom and pt.post_id in ^post_ids and pt.tag_id == t.id,
select: {pt.post_id, t}
Expected a tuple with ID and struct, got: #{inspect(entry)}
"""
defp related_key_to_field(query, {pos, key, field_type}) do
field_ast = related_key_to_field(query, {pos, key})
{:type, [], [field_ast, field_type]}
end
defp related_key_to_field(query, {pos, key}) do
{{:., [], [{:&, [], [related_key_pos(query, pos)]}, key]}, [], []}
end
defp related_key_pos(_query, pos) when pos >= 0, do: pos
defp related_key_pos(query, pos), do: Ecto.Query.Builder.count_binds(query) + pos
defp add_preload_order([], query), do: query
defp add_preload_order(order, query) when is_list(order) do
Ecto.Query.prepend_order_by(query, [q], ^order)
end
defp add_preload_order({m, f, a}, query) do
order =
case apply(m, f, a) do
order when is_list(order) ->
order
other ->
raise ArgumentError,
"`:preload_order` must resolve to a keyword list or a list of atoms/fields, " <>
"got: `#{inspect(other)}`"
end
Enum.each(order, fn
{direction, field} when is_atom(field) or is_struct(field, DynamicExpr) ->
unless Ecto.Query.Builder.OrderBy.valid_direction?(direction) do
raise ArgumentError,
"`:preload_order` must specify valid directions, " <>
"got: `#{inspect(order)}`, `#{inspect(direction)}` is not a valid direction"
end
:ok
field when is_atom(field) or is_struct(field, DynamicExpr) ->
:ok
other ->
raise ArgumentError,
"`:preload_order` must resolve to a keyword list or a list of atoms/fields, " <>
"got: `#{inspect(order)}`, `#{inspect(other)}` is not valid"
end)
add_preload_order(order, query)
end
defp unzip_ids([{k, v}|t], acc1, acc2), do: unzip_ids(t, [k|acc1], [v|acc2])
defp unzip_ids([], acc1, acc2), do: {acc1, acc2}
defp assert_struct!(mod, %{__struct__: mod}), do: true
defp assert_struct!(mod, %{__struct__: struct}) do
raise ArgumentError, "expected a homogeneous list containing the same struct, " <>
"got: #{inspect mod} and #{inspect struct}"
end
defp assoc_map(:one, ids, structs) do
one_assoc_map(ids, structs, %{})
end
defp assoc_map(:many, ids, structs) do
many_assoc_map(ids, structs, %{})
end
defp one_assoc_map([id|ids], [struct|structs], map) do
one_assoc_map(ids, structs, Map.put(map, id, struct))
end
defp one_assoc_map([], [], map) do
map
end
defp many_assoc_map([{id, n}|ids], structs, map) do
{acc, structs} = split_n(structs, n, [])
many_assoc_map(ids, structs, Map.put(map, id, acc))
end
defp many_assoc_map([id|ids], [struct|structs], map) do
{ids, structs, acc} = split_while(ids, structs, id, [struct])
many_assoc_map(ids, structs, Map.put(map, id, acc))
end
defp many_assoc_map([], [], map) do
map
end
defp split_n(structs, 0, acc), do: {acc, structs}
defp split_n([struct | structs], n, acc), do: split_n(structs, n - 1, [struct | acc])
defp split_while([id|ids], [struct|structs], id, acc),
do: split_while(ids, structs, id, [struct|acc])
defp split_while(ids, structs, _id, acc),
do: {ids, structs, acc}
## Load preloaded data
defp load_assoc({:assoc, _assoc, _ids}, nil) do
nil
end
defp load_assoc({:assoc, assoc, ids}, struct) do
%{field: field, owner_key: owner_key, cardinality: cardinality} = assoc
key = Map.fetch!(struct, owner_key)
loaded =
case ids do
%{^key => value} -> value
_ when cardinality == :many -> []
_ -> nil
end
Map.put(struct, field, loaded)
end
defp load_through({_, _, _}, nil), do: nil
defp load_through({_, _, true = _from_query?}, struct), do: struct
defp load_through({{:through, assoc, throughs}, _, false = _from_query?}, struct) do
%{cardinality: cardinality, field: field, owner: owner} = assoc
{loaded, _} = Enum.reduce(throughs, {[struct], owner}, &recur_through/2)
Map.put(struct, field, maybe_first(loaded, cardinality))
end
defp maybe_first(list, :one), do: List.first(list)
defp maybe_first(list, _), do: list
defp recur_through(field, {structs, owner}) do
assoc = owner.__schema__(:association, field)
case assoc.__struct__.preload_info(assoc) do
{:assoc, %{related: related}, _} ->
pk_fields =
related.__schema__(:primary_key)
|> validate_has_pk_field!(related, assoc)
{children, _} =
Enum.reduce(structs, {[], %{}}, fn struct, acc ->
struct
|> Map.fetch!(field)
|> List.wrap()
|> Enum.reduce(acc, fn child, {fresh, set} ->
pk_values =
child
|> through_pks(pk_fields, assoc)
|> validate_non_null_pk!(child, pk_fields, assoc)
case set do
%{^pk_values => true} ->
{fresh, set}
_ ->
{[child|fresh], Map.put(set, pk_values, true)}
end
end)
end)
{Enum.reverse(children), related}
{:through, _, through} ->
Enum.reduce(through, {structs, owner}, &recur_through/2)
end
end
defp validate_has_pk_field!([], related, assoc) do
raise ArgumentError,
"cannot preload through association `#{assoc.field}` on " <>
"`#{inspect assoc.owner}`. Ecto expected the #{inspect related} schema " <>
"to have at least one primary key field"
end
defp validate_has_pk_field!(pk_fields, _related, _assoc), do: pk_fields
defp through_pks(map, pks, assoc) do
Enum.map(pks, fn pk ->
case map do
%{^pk => value} ->
value
_ ->
raise ArgumentError,
"cannot preload through association `#{assoc.field}` on " <>
"`#{inspect assoc.owner}`. Ecto expected a map/struct with " <>
"the key `#{pk}` but got: #{inspect map}"
end
end)
end
defp validate_non_null_pk!(values, map, pks, assoc) do
case values do
[nil | _] ->
raise ArgumentError,
"cannot preload through association `#{assoc.field}` on " <>
"`#{inspect assoc.owner}` because the primary key `#{hd(pks)}` " <>
"is nil for map/struct: #{inspect map}"
_ ->
values
end
end
## Normalizer
def normalize(preload, take, original) do
normalize_each(wrap(preload, original), [], take, original)
end
defp normalize_each({atom, {query, list}}, acc, take, original)
when is_atom(atom) and (is_map(query) or is_function(query, 1) or is_function(query, 2)) do
fields = take(take, atom)
[{atom, {fields, query!(query), normalize_each(wrap(list, original), [], fields, original)}}|acc]
end
defp normalize_each({atom, query}, acc, take, _original)
when is_atom(atom) and (is_map(query) or is_function(query, 1) or is_function(query, 2)) do
[{atom, {take(take, atom), query!(query), []}}|acc]
end
defp normalize_each({atom, list}, acc, take, original) when is_atom(atom) do
fields = take(take, atom)
[{atom, {fields, nil, normalize_each(wrap(list, original), [], fields, original)}}|acc]
end
defp normalize_each(atom, acc, take, _original) when is_atom(atom) do
[{atom, {take(take, atom), nil, []}}|acc]
end
defp normalize_each(other, acc, take, original) do
Enum.reduce(wrap(other, original), acc, &normalize_each(&1, &2, take, original))
end
defp query!(query) when is_function(query, 1), do: query
defp query!(query) when is_function(query, 2), do: query
defp query!(%Ecto.Query{} = query), do: query
defp take(take, field) do
case Access.fetch(take, field) do
{:ok, fields} -> List.wrap(fields)
:error -> nil
end
end
defp wrap(list, _original) when is_list(list),
do: list
defp wrap(atom, _original) when is_atom(atom),
do: atom
defp wrap(other, original) do
raise ArgumentError, "invalid preload `#{inspect other}` in `#{inspect original}`. " <>
"preload expects an atom, a (nested) keyword or a (nested) list of atoms"
end
defp normalize_query_assocs([]), do: %{}
defp normalize_query_assocs(assocs) when is_list(assocs) do
Enum.reduce(assocs, %{}, &normalize_each_query_assoc(&1, &2))
end
defp normalize_each_query_assoc({field, {_idx, sub_assocs}}, acc) do
Map.put(acc, field, normalize_query_assocs(sub_assocs))
end
## Expand
def expand(schema, preloads, query_assocs, acc) do
Enum.reduce(preloads, acc, fn {preload, {fields, query, sub_preloads}},
{assocs, throughs, embeds} ->
assoc_or_embed = association_or_embed!(schema, preload)
info = assoc_or_embed.__struct__.preload_info(assoc_or_embed)
case info do
{:assoc, _, _} ->
value = {info, fields, query, sub_preloads}
assocs = Map.update(assocs, preload, value, &merge_preloads(preload, value, &1))
{assocs, throughs, embeds}
{:through, _, through} ->
case query_assocs do
%{^preload => _} ->
{assocs, [{info, sub_preloads, true} | throughs], embeds}
_ ->
{_, _, through} =
through
|> Enum.reverse()
|> Enum.reduce({fields, query, sub_preloads}, &{nil, nil, [{&1, &2}]})
expand(schema, through, query_assocs, {assocs, [{info, sub_preloads, false} | throughs], embeds})
end
:embed ->
if sub_preloads == [] do
raise ArgumentError,
"cannot preload embedded field #{inspect(assoc_or_embed.field)} " <>
"without also preloading one of its associations as it has no effect"
end
embeds = [{assoc_or_embed, sub_preloads} | embeds]
{assocs, throughs, embeds}
end
end)
end
defp merge_preloads(_preload, {info, _, nil, left}, {info, take, query, right}),
do: {info, take, query, left ++ right}
defp merge_preloads(_preload, {info, take, query, left}, {info, _, nil, right}),
do: {info, take, query, left ++ right}
defp merge_preloads(preload, {info, _, left, _}, {info, _, right, _}) do
raise ArgumentError, "cannot preload `#{preload}` as it has been supplied more than once " <>
"with different queries: #{inspect left} and #{inspect right}"
end
defp association_or_embed!(schema, preload) do
schema.__schema__(:association, preload) || schema.__schema__(:embed, preload) ||
raise ArgumentError, "schema #{inspect schema} does not have association or embed #{inspect preload}#{maybe_module(preload)}"
end
defp maybe_module(assoc) do
case Atom.to_string(assoc) do
"Elixir." <> _ ->
" (if you were trying to pass a schema as a query to preload, " <>
"you have to explicitly convert it to a query by doing `from x in #{inspect assoc}` " <>
"or by calling Ecto.Queryable.to_query/1)"
_ ->
""
end
end
defp filter_and_reraise(exception, stacktrace) do
reraise exception, Enum.reject(stacktrace, &match?({__MODULE__, _, _, _}, &1))
end
end

View File

@@ -0,0 +1,610 @@
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, tuplet) do
query =
queryable
|> Ecto.Queryable.to_query()
|> Ecto.Query.Planner.ensure_select(true)
execute(:all, name, query, tuplet) |> elem(1)
end
def all_by(name, queryable, clauses, tuplet) do
query =
queryable
|> Ecto.Query.where([], ^Enum.to_list(clauses))
|> Ecto.Query.Planner.ensure_select(true)
execute(:all, name, query, tuplet) |> elem(1)
end
def stream(_name, queryable, {adapter_meta, opts}) do
%{adapter: adapter, cache: cache, repo: repo} = adapter_meta
query =
queryable
|> Ecto.Queryable.to_query()
|> Ecto.Query.Planner.ensure_select(true)
{query, opts} = repo.prepare_query(:stream, query, opts)
query = attach_prefix(query, opts)
{query_meta, prepared, cast_params, dump_params} =
Planner.query(query, :all, cache, adapter, 0)
opts = [cast_params: cast_params] ++ opts
case query_meta do
%{select: nil} ->
adapter_meta
|> adapter.stream(query_meta, prepared, dump_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, dump_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, tuplet) do
case all(name, queryable, tuplet) do
[one] -> one
[] -> nil
other -> raise Ecto.MultipleResultsError, queryable: queryable, count: length(other)
end
end
def one!(name, queryable, tuplet) do
case all(name, queryable, tuplet) do
[one] -> one
[] -> raise Ecto.NoResultsError, queryable: queryable
other -> raise Ecto.MultipleResultsError, queryable: queryable, count: length(other)
end
end
def update_all(name, queryable, [], tuplet) do
update_all(name, queryable, tuplet)
end
def update_all(name, queryable, updates, tuplet) do
query = Query.from(queryable, update: ^updates)
update_all(name, query, tuplet)
end
defp update_all(name, queryable, tuplet) do
query = Ecto.Queryable.to_query(queryable)
execute(:update_all, name, query, tuplet)
end
def delete_all(name, queryable, tuplet) do
query = Ecto.Queryable.to_query(queryable)
execute(:delete_all, name, query, tuplet)
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) when struct != %{} do
{nil, values}
end
def struct_load!([], values, acc, _all_nil?, struct, _adapter) do
{Map.merge(struct, Map.new(acc)), values}
end
## Helpers
defp execute(operation, name, query, {adapter_meta, opts} = tuplet) do
%{adapter: adapter, cache: cache, repo: repo} = adapter_meta
{query, opts} = repo.prepare_query(operation, query, opts)
query = attach_prefix(query, opts)
{query_meta, prepared, cast_params, dump_params} =
Planner.query(query, operation, cache, adapter, 0)
opts = [cast_params: cast_params] ++ opts
case query_meta do
%{select: nil} ->
adapter.execute(adapter_meta, query_meta, prepared, dump_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, dump_params, opts)
postprocessor = postprocessor(from, postprocess, take, adapter)
{count,
rows
|> Ecto.Repo.Assoc.query(assocs, sources, preprocessor)
|> Ecto.Repo.Preloader.query(name, preloads, take, assocs, postprocessor, tuplet)}
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, {:source, :values, _prefix, types}, _from, adapter) do
values_list_load!(types, row, [], true, 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 ArgumentError,
"expected a struct named #{inspect(struct)}, got: #{inspect(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(nil, args, row, from, adapter) do
{_args, row} = process_kv(args, row, from, adapter)
{nil, 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 #{Ecto.Type.format(type)}#{field}#{struct}"
end
end
defp values_list_load!([{field, type} | types], [value | values], acc, all_nil?, adapter) do
all_nil? = all_nil? and value == nil
value = load!(type, value, field, nil, adapter)
values_list_load!(types, values, [{field, value} | acc], all_nil?, adapter)
end
defp values_list_load!([], values, _acc, true, _adapter) do
{nil, values}
end
defp values_list_load!([], values, acc, false, _adapter) do
{Map.new(acc), values}
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: prefix, source: source} = head.__meta__
case schema.__schema__(:primary_key) do
[pk] ->
keys = Enum.map(structs, &get_pk!(&1, pk))
query = Query.from(x in {source, 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: []}
%{prefix: prefix} = query ->
query =
query
|> Query.subquery()
|> Queryable.Ecto.SubQuery.to_query()
%{query | prefix: prefix}
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: []}
%{prefix: prefix} = query ->
select = %SelectExpr{expr: ast, file: __ENV__.file, line: __ENV__.line}
query =
%{query | select: select}
|> Query.subquery()
|> Queryable.Ecto.SubQuery.to_query()
%{query | prefix: prefix}
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 homogeneous 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

View File

@@ -0,0 +1,51 @@
defmodule Ecto.Repo.Registry do
@moduledoc false
use GenServer
def start_link(_opts) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
def associate(pid, name, value) when is_pid(pid) do
GenServer.call(__MODULE__, {:associate, pid, name, value})
end
def all_running() do
for [pid, name] <- :ets.match(__MODULE__, {:"$1", :_, :"$2", :_}) do
name || pid
end
end
def lookup(repo) when is_atom(repo) do
GenServer.whereis(repo)
|> Kernel.||(raise "could not lookup Ecto repo #{inspect repo} because it was not started or it does not exist")
|> lookup()
end
def lookup(pid) when is_pid(pid) do
:ets.lookup_element(__MODULE__, pid, 4)
end
## Callbacks
@impl true
def init(:ok) do
table = :ets.new(__MODULE__, [:named_table, read_concurrency: true])
{:ok, table}
end
@impl true
def handle_call({:associate, pid, name, value}, _from, table) do
ref = Process.monitor(pid)
true = :ets.insert(table, {pid, ref, name, value})
{:reply, :ok, table}
end
@impl true
def handle_info({:DOWN, ref, _type, pid, _reason}, table) do
[{^pid, ^ref, _, _}] = :ets.lookup(table, pid)
:ets.delete(table, pid)
{:noreply, table}
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,227 @@
defmodule Ecto.Repo.Supervisor do
@moduledoc false
use Supervisor
require Logger
@defaults [timeout: 15000, pool_size: 10]
@integer_url_query_params ["timeout", "pool_size", "idle_interval"]
@doc """
Starts the repo supervisor.
"""
def start_link(repo, otp_app, adapter, opts) do
name = Keyword.get(opts, :name, repo)
sup_opts = if name, do: [name: name], else: []
Supervisor.start_link(__MODULE__, {name, repo, otp_app, adapter, opts}, sup_opts)
end
@doc """
Retrieves the runtime configuration.
"""
def init_config(type, repo, otp_app, opts) do
config = Application.get_env(otp_app, repo, [])
config = [otp_app: otp_app] ++ (@defaults |> Keyword.merge(config) |> Keyword.merge(opts))
config = Keyword.put_new_lazy(config, :telemetry_prefix, fn -> telemetry_prefix(repo) end)
case repo_init(type, repo, config) do
{:ok, config} ->
{url, config} = Keyword.pop(config, :url)
url_config = parse_url(url || "")
url_config =
if is_list(config[:ssl]) and url_config[:ssl] == true do
Logger.warning(
"ignoring `ssl=true` parameter in URL because `ssl` is already set in the configuration: #{inspect(config[:ssl])}"
)
Keyword.delete(url_config, :ssl)
else
url_config
end
{:ok, Keyword.merge(config, url_config)}
:ignore ->
:ignore
end
end
defp telemetry_prefix(repo) do
repo
|> Module.split()
|> Enum.map(&(&1 |> Macro.underscore() |> String.to_atom()))
end
defp repo_init(type, repo, config) do
if Code.ensure_loaded?(repo) and function_exported?(repo, :init, 2) do
repo.init(type, config)
else
{:ok, config}
end
end
@doc """
Retrieves the compile time configuration.
"""
def compile_config(_repo, opts) do
otp_app = Keyword.fetch!(opts, :otp_app)
adapter = opts[:adapter]
unless adapter do
raise ArgumentError, "missing :adapter option on use Ecto.Repo"
end
if Code.ensure_compiled(adapter) != {:module, adapter} do
raise ArgumentError,
"adapter #{inspect(adapter)} was not compiled, " <>
"ensure it is correct and it is included as a project dependency"
end
behaviours =
for {:behaviour, behaviours} <- adapter.__info__(:attributes),
behaviour <- behaviours,
do: behaviour
unless Ecto.Adapter in behaviours do
raise ArgumentError,
"expected :adapter option given to Ecto.Repo to list Ecto.Adapter as a behaviour"
end
{otp_app, adapter, behaviours}
end
@doc """
Parses an Ecto URL allowed in configuration.
The format must be:
"ecto://username:password@hostname:port/database?ssl=true&timeout=1000"
"""
def parse_url(""), do: []
def parse_url(url) when is_binary(url) do
info = URI.parse(url)
if is_nil(info.host) do
raise Ecto.InvalidURLError, url: url, message: "host is not present"
end
if is_nil(info.path) or not (info.path =~ ~r"^/([^/])+$") do
raise Ecto.InvalidURLError, url: url, message: "path should be a database name"
end
destructure [username, password], info.userinfo && String.split(info.userinfo, ":")
"/" <> database = info.path
url_opts = [
scheme: info.scheme,
username: username,
password: password,
database: database,
port: info.port
]
url_opts = put_hostname_if_present(url_opts, info.host)
query_opts = parse_uri_query(info)
for {k, v} <- url_opts ++ query_opts,
not is_nil(v),
do: {k, if(is_binary(v), do: URI.decode(v), else: v)}
end
defp put_hostname_if_present(keyword, "") do
keyword
end
defp put_hostname_if_present(keyword, hostname) when is_binary(hostname) do
Keyword.put(keyword, :hostname, hostname)
end
defp parse_uri_query(%URI{query: nil}),
do: []
defp parse_uri_query(%URI{query: query} = url) do
query
|> URI.query_decoder()
|> Enum.reduce([], fn
{"ssl", "true"}, acc ->
[{:ssl, true}] ++ acc
{"ssl", "false"}, acc ->
[{:ssl, false}] ++ acc
{key, value}, acc when key in @integer_url_query_params ->
[{String.to_atom(key), parse_integer!(key, value, url)}] ++ acc
{key, value}, acc ->
[{String.to_atom(key), value}] ++ acc
end)
end
defp parse_integer!(key, value, url) do
case Integer.parse(value) do
{int, ""} ->
int
_ ->
raise Ecto.InvalidURLError,
url: url,
message: "cannot parse value `#{value}` for parameter `#{key}` as an integer"
end
end
@doc false
def tuplet(name, opts) do
adapter_meta = Ecto.Repo.Registry.lookup(name)
if opts[:stacktrace] || Map.get(adapter_meta, :stacktrace) do
{:current_stacktrace, stacktrace} = :erlang.process_info(self(), :current_stacktrace)
{adapter_meta, Keyword.put(opts, :stacktrace, stacktrace)}
else
{adapter_meta, opts}
end
end
## Callbacks
@doc false
def init({name, repo, otp_app, adapter, opts}) do
case init_config(:supervisor, repo, otp_app, opts) do
{:ok, opts} ->
:telemetry.execute(
[:ecto, :repo, :init],
%{system_time: System.system_time()},
%{repo: repo, opts: opts}
)
{:ok, child, meta} = adapter.init([repo: repo] ++ opts)
# Normalize name to atom, ignore via/global names
name = if is_atom(name), do: name, else: nil
cache = Ecto.Query.Planner.new_query_cache(name)
meta = Map.merge(meta, %{repo: repo, cache: cache})
child_spec = wrap_child_spec(child, [name, adapter, meta])
Supervisor.init([child_spec], strategy: :one_for_one, max_restarts: 0)
:ignore ->
:ignore
end
end
def start_child({mod, fun, args}, name, adapter, meta) do
case apply(mod, fun, args) do
{:ok, pid} ->
meta = Map.merge(meta, %{pid: pid, adapter: adapter})
Ecto.Repo.Registry.associate(self(), name, meta)
{:ok, pid}
other ->
other
end
end
defp wrap_child_spec(%{start: start} = spec, args) do
%{spec | start: {__MODULE__, :start_child, [start | args]}}
end
end

View File

@@ -0,0 +1,59 @@
defmodule Ecto.Repo.Transaction do
@moduledoc false
@dialyzer :no_opaque
def transact(repo, name, fun, adapter_opts) when is_function(fun, 0) do
transact(repo, name, fn _repo -> fun.() end, adapter_opts)
end
def transact(repo, _name, fun, {adapter_meta, opts}) when is_function(fun, 1) do
adapter_meta.adapter.transaction(adapter_meta, opts, fn ->
case fun.(repo) do
{:ok, result} ->
result
{:error, reason} ->
adapter_meta.adapter.rollback(adapter_meta, reason)
other ->
raise ArgumentError,
"expected to return {:ok, _} or {:error, _}, got: #{inspect(other)}"
end
end)
end
def transact(repo, _name, %Ecto.Multi{} = multi, {adapter_meta, opts}) do
%{adapter: adapter} = adapter_meta
wrap = &adapter.transaction(adapter_meta, opts, &1)
return = &adapter.rollback(adapter_meta, &1)
case Ecto.Multi.__apply__(multi, repo, wrap, return) do
{:ok, values} ->
{:ok, values}
{:error, {key, error_value, values}} ->
{:error, key, error_value, values}
{:error, operation} ->
raise """
operation #{inspect(operation)} is rolling back unexpectedly.
This can happen if `repo.rollback/1` is manually called, which is not \
supported by `Ecto.Multi`. It can also occur if a nested transaction \
has rolled back and its error is not bubbled up to the outer multi. \
Nested transactions are discouraged when using `Ecto.Multi`. Consider \
flattening out the transaction instead.
"""
end
end
def in_transaction?(name) do
%{adapter: adapter} = meta = Ecto.Repo.Registry.lookup(name)
adapter.in_transaction?(meta)
end
def rollback(name, value) do
%{adapter: adapter} = meta = Ecto.Repo.Registry.lookup(name)
adapter.rollback(meta, value)
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,106 @@
defmodule Ecto.Schema.Loader do
@moduledoc false
alias Ecto.Schema.Metadata
@doc """
Loads a struct to be used as a template in further operations.
"""
def load_struct(nil, _prefix, _source), do: %{}
def load_struct(schema, prefix, source) do
case schema.__schema__(:loaded) do
%{__meta__: %Metadata{prefix: ^prefix, source: ^source}} = struct ->
struct
%{__meta__: %Metadata{} = metadata} = struct ->
Map.put(struct, :__meta__, %{metadata | source: source, prefix: prefix})
%{} = struct ->
struct
end
end
@doc """
Loads data coming from the user/embeds into schema.
Assumes data does not all belong to schema/struct
and that it may also require source-based renaming.
"""
def unsafe_load(schema, data, loader) do
types = schema.__schema__(:load)
struct = schema.__schema__(:loaded)
unsafe_load(struct, types, data, loader)
end
@doc """
Loads data coming from the user/embeds into struct and types.
Assumes data does not all belong to schema/struct
and that it may also require source-based renaming.
"""
def unsafe_load(struct, types, map, loader) when is_map(map) do
Enum.reduce(types, struct, fn pair, acc ->
{field, source, type} = field_source_and_type(pair)
case fetch_string_or_atom_field(map, source) do
{:ok, value} -> Map.put(acc, field, load!(struct, field, type, value, loader))
:error -> acc
end
end)
end
@compile {:inline, field_source_and_type: 1, fetch_string_or_atom_field: 2}
defp field_source_and_type({field, {:source, source, type}}) do
{field, source, type}
end
defp field_source_and_type({field, type}) do
{field, field, type}
end
defp fetch_string_or_atom_field(map, field) when is_atom(field) do
case Map.fetch(map, Atom.to_string(field)) do
{:ok, value} -> {:ok, value}
:error -> Map.fetch(map, field)
end
end
@compile {:inline, load!: 5}
defp load!(struct, field, type, value, loader) do
case loader.(type, value) do
{:ok, value} ->
value
:error ->
raise ArgumentError,
"cannot load `#{inspect(value)}` as type #{Ecto.Type.format(type)} " <>
"for field `#{field}`#{error_data(struct)}"
end
end
defp error_data(%{__struct__: atom}) do
" in schema #{inspect(atom)}"
end
defp error_data(other) when is_map(other) do
""
end
@doc """
Dumps the given data.
"""
def safe_dump(struct, types, dumper) do
Enum.reduce(types, %{}, fn {field, {source, type, _writable}}, acc ->
value = Map.get(struct, field)
case dumper.(type, value) do
{:ok, value} ->
Map.put(acc, source, value)
:error ->
raise ArgumentError, "cannot dump `#{inspect value}` as type #{Ecto.Type.format(type)} " <>
"for field `#{field}` in schema #{inspect struct.__struct__}"
end
end)
end
end

View File

@@ -0,0 +1,66 @@
defmodule Ecto.Schema.Metadata do
@moduledoc """
Stores metadata of a struct.
## State
The state of the schema is stored in the `:state` field and allows
following values:
* `:built` - the struct was constructed in memory and is not persisted
to database yet;
* `:loaded` - the struct was loaded from database and represents
persisted data;
* `:deleted` - the struct was deleted and no longer represents persisted
data.
## Source
The `:source` tracks the (table or collection) where the struct is or should
be persisted to.
## Prefix
Tracks the source prefix in the data storage.
## Context
The `:context` field represents additional state some databases require
for proper updates of data. It is not used by the built-in adapters of
`Ecto.Adapters.Postgres` and `Ecto.Adapters.MySQL`.
## Schema
The `:schema` field refers the module name for the schema this metadata belongs to.
"""
defstruct [:state, :source, :context, :schema, :prefix]
@type state :: :built | :loaded | :deleted
@type context :: any
@type t(schema) :: %__MODULE__{
context: context,
prefix: Ecto.Schema.prefix(),
schema: schema,
source: Ecto.Schema.source(),
state: state
}
@type t :: t(module)
defimpl Inspect do
import Inspect.Algebra
def inspect(metadata, opts) do
%{source: source, prefix: prefix, state: state, context: context} = metadata
entries =
for entry <- [state, prefix, source, context],
entry != nil,
do: to_doc(entry, opts)
concat(["#Ecto.Schema.Metadata<"] ++ Enum.intersperse(entries, ", ") ++ [">"])
end
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,234 @@
defmodule Ecto.UUID do
@moduledoc """
An Ecto type for UUID strings.
"""
use Ecto.Type
@typedoc """
A hex-encoded UUID string.
"""
@type t :: <<_::288>>
@typedoc """
A raw binary representation of a UUID.
"""
@type raw :: <<_::128>>
@doc false
def type, do: :uuid
@doc """
Casts either a string in the canonical, human-readable UUID format or a
16-byte binary to a UUID in its canonical, human-readable UUID format.
If `uuid` is neither of these, `:error` will be returned.
Since both binaries and strings are represented as binaries, this means some
strings you may not expect are actually also valid UUIDs in their binary form
and so will be casted into their string form.
If you need further-restricted behavior or validation, you should define your
own custom `Ecto.Type`. There is also `Ecto.UUID.load/1` if you only want to
process `raw` UUIDs, which may be a more suitable reverse operation to
`Ecto.UUID.dump/1`.
## Examples
iex> Ecto.UUID.cast(<<0x60, 0x1D, 0x74, 0xE4, 0xA8, 0xD3, 0x4B, 0x6E,
...> 0x83, 0x65, 0xED, 0xDB, 0x4C, 0x89, 0x33, 0x27>>)
{:ok, "601d74e4-a8d3-4b6e-8365-eddb4c893327"}
iex> Ecto.UUID.cast("601d74e4-a8d3-4b6e-8365-eddb4c893327")
{:ok, "601d74e4-a8d3-4b6e-8365-eddb4c893327"}
iex> Ecto.UUID.cast("warehouse worker")
{:ok, "77617265-686f-7573-6520-776f726b6572"}
"""
@spec cast(t | raw | any) :: {:ok, t} | :error
def cast(uuid)
def cast(
<<a1, a2, a3, a4, a5, a6, a7, a8, ?-, b1, b2, b3, b4, ?-, c1, c2, c3, c4, ?-, d1, d2, d3,
d4, ?-, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12>>
) do
<<c(a1), c(a2), c(a3), c(a4), c(a5), c(a6), c(a7), c(a8), ?-, c(b1), c(b2), c(b3), c(b4), ?-,
c(c1), c(c2), c(c3), c(c4), ?-, c(d1), c(d2), c(d3), c(d4), ?-, c(e1), c(e2), c(e3), c(e4),
c(e5), c(e6), c(e7), c(e8), c(e9), c(e10), c(e11), c(e12)>>
catch
:error -> :error
else
hex_uuid -> {:ok, hex_uuid}
end
def cast(<<_::128>> = raw_uuid), do: {:ok, encode(raw_uuid)}
def cast(_), do: :error
@doc """
Same as `cast/1` but raises `Ecto.CastError` on invalid arguments.
"""
@spec cast!(t | raw | any) :: t
def cast!(uuid) do
case cast(uuid) do
{:ok, hex_uuid} -> hex_uuid
:error -> raise Ecto.CastError, type: __MODULE__, value: uuid
end
end
@compile {:inline, c: 1}
defp c(?0), do: ?0
defp c(?1), do: ?1
defp c(?2), do: ?2
defp c(?3), do: ?3
defp c(?4), do: ?4
defp c(?5), do: ?5
defp c(?6), do: ?6
defp c(?7), do: ?7
defp c(?8), do: ?8
defp c(?9), do: ?9
defp c(?A), do: ?a
defp c(?B), do: ?b
defp c(?C), do: ?c
defp c(?D), do: ?d
defp c(?E), do: ?e
defp c(?F), do: ?f
defp c(?a), do: ?a
defp c(?b), do: ?b
defp c(?c), do: ?c
defp c(?d), do: ?d
defp c(?e), do: ?e
defp c(?f), do: ?f
defp c(_), do: throw(:error)
@doc """
Converts a string representing a UUID into a raw binary.
"""
@spec dump(uuid_string :: t | any) :: {:ok, raw} | :error
def dump(uuid_string)
def dump(
<<a1, a2, a3, a4, a5, a6, a7, a8, ?-, b1, b2, b3, b4, ?-, c1, c2, c3, c4, ?-, d1, d2, d3,
d4, ?-, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12>>
) do
<<d(a1)::4, d(a2)::4, d(a3)::4, d(a4)::4, d(a5)::4, d(a6)::4, d(a7)::4, d(a8)::4, d(b1)::4,
d(b2)::4, d(b3)::4, d(b4)::4, d(c1)::4, d(c2)::4, d(c3)::4, d(c4)::4, d(d1)::4, d(d2)::4,
d(d3)::4, d(d4)::4, d(e1)::4, d(e2)::4, d(e3)::4, d(e4)::4, d(e5)::4, d(e6)::4, d(e7)::4,
d(e8)::4, d(e9)::4, d(e10)::4, d(e11)::4, d(e12)::4>>
catch
:error -> :error
else
raw_uuid -> {:ok, raw_uuid}
end
def dump(_), do: :error
@compile {:inline, d: 1}
defp d(?0), do: 0
defp d(?1), do: 1
defp d(?2), do: 2
defp d(?3), do: 3
defp d(?4), do: 4
defp d(?5), do: 5
defp d(?6), do: 6
defp d(?7), do: 7
defp d(?8), do: 8
defp d(?9), do: 9
defp d(?A), do: 10
defp d(?B), do: 11
defp d(?C), do: 12
defp d(?D), do: 13
defp d(?E), do: 14
defp d(?F), do: 15
defp d(?a), do: 10
defp d(?b), do: 11
defp d(?c), do: 12
defp d(?d), do: 13
defp d(?e), do: 14
defp d(?f), do: 15
defp d(_), do: throw(:error)
@doc """
Same as `dump/1` but raises `Ecto.ArgumentError` on invalid arguments.
"""
@spec dump!(t | any) :: raw
def dump!(uuid) do
case dump(uuid) do
{:ok, raw_uuid} -> raw_uuid
:error -> raise ArgumentError, "cannot dump given UUID to binary: #{inspect(uuid)}"
end
end
@doc """
Converts a binary UUID into a string.
"""
@spec load(raw | any) :: {:ok, t} | :error
def load(<<_::128>> = raw_uuid), do: {:ok, encode(raw_uuid)}
def load(<<_::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96>> = string) do
raise ArgumentError,
"trying to load string UUID as Ecto.UUID: #{inspect(string)}. " <>
"Maybe you wanted to declare :uuid as your database field?"
end
def load(_), do: :error
@doc """
Same as `load/1` but raises `Ecto.ArgumentError` on invalid arguments.
"""
@spec load!(raw | any) :: t
def load!(value) do
case load(value) do
{:ok, hex_uuid} -> hex_uuid
:error -> raise ArgumentError, "cannot load given binary as UUID: #{inspect(value)}"
end
end
@doc """
Generates a random, version 4 UUID.
"""
@spec generate() :: t
def generate(), do: encode(bingenerate())
@doc """
Generates a random, version 4 UUID in the binary format.
"""
@spec bingenerate() :: raw
def bingenerate() do
<<u0::48, _::4, u1::12, _::2, u2::62>> = :crypto.strong_rand_bytes(16)
<<u0::48, 4::4, u1::12, 2::2, u2::62>>
end
# Callback invoked by autogenerate fields.
@doc false
def autogenerate, do: generate()
@spec encode(raw) :: t
defp encode(
<<a1::4, a2::4, a3::4, a4::4, a5::4, a6::4, a7::4, a8::4, b1::4, b2::4, b3::4, b4::4,
c1::4, c2::4, c3::4, c4::4, d1::4, d2::4, d3::4, d4::4, e1::4, e2::4, e3::4, e4::4,
e5::4, e6::4, e7::4, e8::4, e9::4, e10::4, e11::4, e12::4>>
) do
<<e(a1), e(a2), e(a3), e(a4), e(a5), e(a6), e(a7), e(a8), ?-, e(b1), e(b2), e(b3), e(b4), ?-,
e(c1), e(c2), e(c3), e(c4), ?-, e(d1), e(d2), e(d3), e(d4), ?-, e(e1), e(e2), e(e3), e(e4),
e(e5), e(e6), e(e7), e(e8), e(e9), e(e10), e(e11), e(e12)>>
end
@compile {:inline, e: 1}
defp e(0), do: ?0
defp e(1), do: ?1
defp e(2), do: ?2
defp e(3), do: ?3
defp e(4), do: ?4
defp e(5), do: ?5
defp e(6), do: ?6
defp e(7), do: ?7
defp e(8), do: ?8
defp e(9), do: ?9
defp e(10), do: ?a
defp e(11), do: ?b
defp e(12), do: ?c
defp e(13), do: ?d
defp e(14), do: ?e
defp e(15), do: ?f
end

View File

@@ -0,0 +1,148 @@
defmodule Mix.Ecto do
@moduledoc """
Conveniences for writing Ecto related Mix tasks.
"""
@doc """
Parses the repository option from the given command line args list.
If no repo option is given, it is retrieved from the application environment.
"""
@spec parse_repo([term]) :: [Ecto.Repo.t]
def parse_repo(args) do
parse_repo(args, [])
end
defp parse_repo([key, value|t], acc) when key in ~w(--repo -r) do
parse_repo t, [Module.concat([value])|acc]
end
defp parse_repo([_|t], acc) do
parse_repo t, acc
end
defp parse_repo([], []) do
apps =
if apps_paths = Mix.Project.apps_paths() do
Enum.filter(Mix.Project.deps_apps(), &is_map_key(apps_paths, &1))
else
[Mix.Project.config()[:app]]
end
apps
|> Enum.flat_map(fn app ->
Application.load(app)
Application.get_env(app, :ecto_repos, [])
end)
|> Enum.uniq()
|> case do
[] ->
Mix.shell().error """
warning: could not find Ecto repos in any of the apps: #{inspect apps}.
You can avoid this warning by passing the -r flag or by setting the
repositories managed by those applications in your config/config.exs:
config #{inspect hd(apps)}, ecto_repos: [...]
"""
[]
repos ->
repos
end
end
defp parse_repo([], acc) do
Enum.reverse(acc)
end
@doc """
Ensures the given module is an Ecto.Repo.
"""
@spec ensure_repo(module, list) :: Ecto.Repo.t
def ensure_repo(repo, args) do
# Do not pass the --force switch used by some tasks downstream
args = List.delete(args, "--force")
Mix.Task.run("app.config", args)
case Code.ensure_compiled(repo) do
{:module, _} ->
if function_exported?(repo, :__adapter__, 0) do
repo
else
Mix.raise "Module #{inspect repo} is not an Ecto.Repo. " <>
"Please configure your app accordingly or pass a repo with the -r option."
end
{:error, error} ->
Mix.raise "Could not load #{inspect repo}, error: #{inspect error}. " <>
"Please configure your app accordingly or pass a repo with the -r option."
end
end
@doc """
Asks if the user wants to open a file based on ECTO_EDITOR.
By default, it attempts to open the file and line using the
`file:line` notation. For example, if your editor is called
`subl`, it will open the file as:
subl path/to/file:line
It is important that you choose an editor command that does
not block nor that attempts to run an editor directly in the
terminal. Command-line based editors likely need extra
configuration so they open up the given file and line in a
separate window.
Custom editors are supported by using the `__FILE__` and
`__LINE__` notations, for example:
ECTO_EDITOR="my_editor +__LINE__ __FILE__"
and Elixir will properly interpolate values.
"""
@spec open?(binary, non_neg_integer) :: boolean
def open?(file, line \\ 1) do
editor = System.get_env("ECTO_EDITOR") || ""
if editor != "" do
command =
if editor =~ "__FILE__" or editor =~ "__LINE__" do
editor
|> String.replace("__FILE__", inspect(file))
|> String.replace("__LINE__", Integer.to_string(line))
else
"#{editor} #{inspect(file)}:#{line}"
end
Mix.shell().cmd(command)
true
else
false
end
end
@doc """
Gets a path relative to the application path.
Raises on umbrella application.
"""
def no_umbrella!(task) do
if Mix.Project.umbrella?() do
Mix.raise "Cannot run task #{inspect task} from umbrella project root. " <>
"Change directory to one of the umbrella applications and try again"
end
end
@doc """
Returns `true` if module implements behaviour.
"""
def ensure_implements(module, behaviour, message) do
all = Keyword.take(module.__info__(:attributes), [:behaviour])
unless [behaviour] in Keyword.values(all) do
Mix.raise "Expected #{inspect module} to implement #{inspect behaviour} " <>
"in order to #{message}"
end
end
end

View File

@@ -0,0 +1,77 @@
defmodule Mix.Tasks.Ecto.Create do
use Mix.Task
import Mix.Ecto
@shortdoc "Creates the repository storage"
@switches [
quiet: :boolean,
repo: [:string, :keep],
no_compile: :boolean,
no_deps_check: :boolean
]
@aliases [
r: :repo,
q: :quiet
]
@moduledoc """
Create the storage for the given repository.
The repositories to create are the ones specified under the
`:ecto_repos` option in the current app configuration. However,
if the `-r` option is given, it replaces the `:ecto_repos` config.
Since Ecto tasks can only be executed once, if you need to create
multiple repositories, set `:ecto_repos` accordingly or pass the `-r`
flag multiple times.
## Examples
$ mix ecto.create
$ mix ecto.create -r Custom.Repo
## Command line options
* `-r`, `--repo` - the repo to create
* `--quiet` - do not log output
* `--no-compile` - do not compile before creating
* `--no-deps-check` - do not compile before creating
"""
@impl true
def run(args) do
repos = parse_repo(args)
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
Enum.each(repos, fn repo ->
ensure_repo(repo, args)
ensure_implements(
repo.__adapter__(),
Ecto.Adapter.Storage,
"create storage for #{inspect(repo)}"
)
case repo.__adapter__().storage_up(repo.config()) do
:ok ->
unless opts[:quiet] do
Mix.shell().info("The database for #{inspect(repo)} has been created")
end
{:error, :already_up} ->
unless opts[:quiet] do
Mix.shell().info("The database for #{inspect(repo)} has already been created")
end
{:error, term} when is_binary(term) ->
Mix.raise("The database for #{inspect(repo)} couldn't be created: #{term}")
{:error, term} ->
Mix.raise("The database for #{inspect(repo)} couldn't be created: #{inspect(term)}")
end
end)
end
end

View File

@@ -0,0 +1,106 @@
defmodule Mix.Tasks.Ecto.Drop do
use Mix.Task
import Mix.Ecto
@shortdoc "Drops the repository storage"
@default_opts [force: false, force_drop: false]
@aliases [
f: :force,
q: :quiet,
r: :repo
]
@switches [
force: :boolean,
force_drop: :boolean,
quiet: :boolean,
repo: [:keep, :string],
no_compile: :boolean,
no_deps_check: :boolean
]
@moduledoc """
Drop the storage for the given repository.
The repositories to drop are the ones specified under the
`:ecto_repos` option in the current app configuration. However,
if the `-r` option is given, it replaces the `:ecto_repos` config.
Since Ecto tasks can only be executed once, if you need to drop
multiple repositories, set `:ecto_repos` accordingly or pass the `-r`
flag multiple times.
## Examples
$ mix ecto.drop
$ mix ecto.drop -r Custom.Repo
## Command line options
* `-r`, `--repo` - the repo to drop
* `-q`, `--quiet` - run the command quietly
* `-f`, `--force` - do not ask for confirmation when dropping the database.
Configuration is asked only when `:start_permanent` is set to true
(typically in production)
* `--force-drop` - force the database to be dropped even
if it has connections to it (requires PostgreSQL 13+)
* `--no-compile` - do not compile before dropping
* `--no-deps-check` - do not compile before dropping
"""
@impl true
def run(args) do
repos = parse_repo(args)
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
opts = Keyword.merge(@default_opts, opts)
Enum.each(repos, fn repo ->
ensure_repo(repo, args)
ensure_implements(
repo.__adapter__(),
Ecto.Adapter.Storage,
"drop storage for #{inspect(repo)}"
)
if skip_safety_warnings?() or
opts[:force] or
Mix.shell().yes?(
"Are you sure you want to drop the database for repo #{inspect(repo)}?"
) do
drop_database(repo, opts)
end
end)
end
defp skip_safety_warnings? do
Mix.Project.config()[:start_permanent] != true
end
defp drop_database(repo, opts) do
config =
opts
|> Keyword.take([:force_drop])
|> Keyword.merge(repo.config())
case repo.__adapter__().storage_down(config) do
:ok ->
unless opts[:quiet] do
Mix.shell().info("The database for #{inspect(repo)} has been dropped")
end
{:error, :already_down} ->
unless opts[:quiet] do
Mix.shell().info("The database for #{inspect(repo)} has already been dropped")
end
{:error, term} when is_binary(term) ->
Mix.raise("The database for #{inspect(repo)} couldn't be dropped: #{term}")
{:error, term} ->
Mix.raise("The database for #{inspect(repo)} couldn't be dropped: #{inspect(term)}")
end
end
end

View File

@@ -0,0 +1,30 @@
defmodule Mix.Tasks.Ecto do
use Mix.Task
@shortdoc "Prints Ecto help information"
@moduledoc """
Prints Ecto tasks and their information.
$ mix ecto
"""
@impl true
def run(args) do
{_opts, args} = OptionParser.parse!(args, strict: [])
case args do
[] -> general()
_ -> Mix.raise "Invalid arguments, expected: mix ecto"
end
end
defp general() do
Application.ensure_all_started(:ecto)
Mix.shell().info "Ecto v#{Application.spec(:ecto, :vsn)}"
Mix.shell().info "A toolkit for data mapping and language integrated query for Elixir."
Mix.shell().info "\nAvailable tasks:\n"
Mix.Tasks.Help.run(["--search", "ecto."])
end
end

View File

@@ -0,0 +1,110 @@
defmodule Mix.Tasks.Ecto.Gen.Repo do
use Mix.Task
import Mix.Ecto
import Mix.Generator
@shortdoc "Generates a new repository"
@switches [
repo: [:string, :keep],
]
@aliases [
r: :repo,
]
@moduledoc """
Generates a new repository.
The repository will be placed in the `lib` directory.
## Examples
$ mix ecto.gen.repo -r Custom.Repo
This generator will automatically open the config/config.exs
after generation if you have `ECTO_EDITOR` set in your environment
variable.
## Command line options
* `-r`, `--repo` - the repo to generate
"""
@impl true
def run(args) do
no_umbrella!("ecto.gen.repo")
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
repo =
case Keyword.get_values(opts, :repo) do
[] -> Mix.raise "ecto.gen.repo expects the repository to be given as -r MyApp.Repo"
[repo] -> Module.concat([repo])
[_ | _] -> Mix.raise "ecto.gen.repo expects a single repository to be given"
end
config = Mix.Project.config()
underscored = Macro.underscore(inspect(repo))
base = Path.basename(underscored)
file = Path.join("lib", underscored) <> ".ex"
app = config[:app] || :YOUR_APP_NAME
opts = [mod: repo, app: app, base: base]
create_directory Path.dirname(file)
create_file file, repo_template(opts)
config_path = config[:config_path] || "config/config.exs"
case File.read(config_path) do
{:ok, contents} ->
check = String.contains?(contents, "import Config")
config_first_line = get_first_config_line(check) <> "\n"
new_contents = config_first_line <> "\n" <> config_template(opts)
Mix.shell().info [:green, "* updating ", :reset, config_path]
File.write! config_path, String.replace(contents, config_first_line, new_contents)
{:error, _} ->
create_file config_path, "import Config\n\n" <> config_template(opts)
end
open?(config_path, 3)
Mix.shell().info """
Don't forget to add your new repo to your supervision tree
(typically in lib/#{app}/application.ex):
def start(_type, _args) do
children = [
#{inspect repo},
]
And to add it to the list of Ecto repositories in your
configuration files (so Ecto tasks work as expected):
config #{inspect app},
ecto_repos: [#{inspect repo}]
"""
end
defp get_first_config_line(true), do: "import Config"
defp get_first_config_line(false), do: "use Mix.Config"
embed_template :repo, """
defmodule <%= inspect @mod %> do
use Ecto.Repo,
otp_app: <%= inspect @app %>,
adapter: Ecto.Adapters.Postgres
end
"""
embed_template :config, """
config <%= inspect @app %>, <%= inspect @mod %>,
database: "<%= @app %>_<%= @base %>",
username: "user",
password: "pass",
hostname: "localhost"
"""
end