update
This commit is contained in:
754
phoenix/deps/ecto/lib/ecto.ex
Normal file
754
phoenix/deps/ecto/lib/ecto.ex
Normal 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
|
||||
142
phoenix/deps/ecto/lib/ecto/adapter.ex
Normal file
142
phoenix/deps/ecto/lib/ecto/adapter.ex
Normal 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
|
||||
127
phoenix/deps/ecto/lib/ecto/adapter/queryable.ex
Normal file
127
phoenix/deps/ecto/lib/ecto/adapter/queryable.ex
Normal 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
|
||||
92
phoenix/deps/ecto/lib/ecto/adapter/schema.ex
Normal file
92
phoenix/deps/ecto/lib/ecto/adapter/schema.ex
Normal 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
|
||||
53
phoenix/deps/ecto/lib/ecto/adapter/storage.ex
Normal file
53
phoenix/deps/ecto/lib/ecto/adapter/storage.ex
Normal 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
|
||||
31
phoenix/deps/ecto/lib/ecto/adapter/transaction.ex
Normal file
31
phoenix/deps/ecto/lib/ecto/adapter/transaction.ex
Normal 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
|
||||
13
phoenix/deps/ecto/lib/ecto/application.ex
Normal file
13
phoenix/deps/ecto/lib/ecto/application.ex
Normal 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
|
||||
1689
phoenix/deps/ecto/lib/ecto/association.ex
Normal file
1689
phoenix/deps/ecto/lib/ecto/association.ex
Normal file
File diff suppressed because it is too large
Load Diff
4395
phoenix/deps/ecto/lib/ecto/changeset.ex
Normal file
4395
phoenix/deps/ecto/lib/ecto/changeset.ex
Normal file
File diff suppressed because it is too large
Load Diff
586
phoenix/deps/ecto/lib/ecto/changeset/relation.ex
Normal file
586
phoenix/deps/ecto/lib/ecto/changeset/relation.ex
Normal 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
|
||||
301
phoenix/deps/ecto/lib/ecto/embedded.ex
Normal file
301
phoenix/deps/ecto/lib/ecto/embedded.ex
Normal 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
|
||||
393
phoenix/deps/ecto/lib/ecto/enum.ex
Normal file
393
phoenix/deps/ecto/lib/ecto/enum.ex
Normal 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
|
||||
321
phoenix/deps/ecto/lib/ecto/exceptions.ex
Normal file
321
phoenix/deps/ecto/lib/ecto/exceptions.ex
Normal 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
|
||||
52
phoenix/deps/ecto/lib/ecto/json.ex
Normal file
52
phoenix/deps/ecto/lib/ecto/json.ex
Normal 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
|
||||
998
phoenix/deps/ecto/lib/ecto/multi.ex
Normal file
998
phoenix/deps/ecto/lib/ecto/multi.ex
Normal 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
|
||||
220
phoenix/deps/ecto/lib/ecto/parameterized_type.ex
Normal file
220
phoenix/deps/ecto/lib/ecto/parameterized_type.ex
Normal 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
|
||||
3103
phoenix/deps/ecto/lib/ecto/query.ex
Normal file
3103
phoenix/deps/ecto/lib/ecto/query.ex
Normal file
File diff suppressed because it is too large
Load Diff
974
phoenix/deps/ecto/lib/ecto/query/api.ex
Normal file
974
phoenix/deps/ecto/lib/ecto/query/api.ex
Normal 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
|
||||
1631
phoenix/deps/ecto/lib/ecto/query/builder.ex
Normal file
1631
phoenix/deps/ecto/lib/ecto/query/builder.ex
Normal file
File diff suppressed because it is too large
Load Diff
36
phoenix/deps/ecto/lib/ecto/query/builder/combination.ex
Normal file
36
phoenix/deps/ecto/lib/ecto/query/builder/combination.ex
Normal 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
|
||||
121
phoenix/deps/ecto/lib/ecto/query/builder/cte.ex
Normal file
121
phoenix/deps/ecto/lib/ecto/query/builder/cte.ex
Normal 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
|
||||
91
phoenix/deps/ecto/lib/ecto/query/builder/distinct.ex
Normal file
91
phoenix/deps/ecto/lib/ecto/query/builder/distinct.ex
Normal 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
|
||||
112
phoenix/deps/ecto/lib/ecto/query/builder/dynamic.ex
Normal file
112
phoenix/deps/ecto/lib/ecto/query/builder/dynamic.ex
Normal 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
|
||||
196
phoenix/deps/ecto/lib/ecto/query/builder/filter.ex
Normal file
196
phoenix/deps/ecto/lib/ecto/query/builder/filter.ex
Normal 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
|
||||
237
phoenix/deps/ecto/lib/ecto/query/builder/from.ex
Normal file
237
phoenix/deps/ecto/lib/ecto/query/builder/from.ex
Normal 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
|
||||
118
phoenix/deps/ecto/lib/ecto/query/builder/group_by.ex
Normal file
118
phoenix/deps/ecto/lib/ecto/query/builder/group_by.ex
Normal 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
|
||||
426
phoenix/deps/ecto/lib/ecto/query/builder/join.ex
Normal file
426
phoenix/deps/ecto/lib/ecto/query/builder/join.ex
Normal 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
|
||||
111
phoenix/deps/ecto/lib/ecto/query/builder/limit_offset.ex
Normal file
111
phoenix/deps/ecto/lib/ecto/query/builder/limit_offset.ex
Normal 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
|
||||
59
phoenix/deps/ecto/lib/ecto/query/builder/lock.ex
Normal file
59
phoenix/deps/ecto/lib/ecto/query/builder/lock.ex
Normal 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
|
||||
282
phoenix/deps/ecto/lib/ecto/query/builder/order_by.ex
Normal file
282
phoenix/deps/ecto/lib/ecto/query/builder/order_by.ex
Normal 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
|
||||
337
phoenix/deps/ecto/lib/ecto/query/builder/preload.ex
Normal file
337
phoenix/deps/ecto/lib/ecto/query/builder/preload.ex
Normal 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
|
||||
579
phoenix/deps/ecto/lib/ecto/query/builder/select.ex
Normal file
579
phoenix/deps/ecto/lib/ecto/query/builder/select.ex
Normal 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
|
||||
200
phoenix/deps/ecto/lib/ecto/query/builder/update.ex
Normal file
200
phoenix/deps/ecto/lib/ecto/query/builder/update.ex
Normal 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
|
||||
204
phoenix/deps/ecto/lib/ecto/query/builder/windows.ex
Normal file
204
phoenix/deps/ecto/lib/ecto/query/builder/windows.ex
Normal 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
|
||||
442
phoenix/deps/ecto/lib/ecto/query/inspect.ex
Normal file
442
phoenix/deps/ecto/lib/ecto/query/inspect.ex
Normal 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
|
||||
2751
phoenix/deps/ecto/lib/ecto/query/planner.ex
Normal file
2751
phoenix/deps/ecto/lib/ecto/query/planner.ex
Normal file
File diff suppressed because it is too large
Load Diff
232
phoenix/deps/ecto/lib/ecto/query/window_api.ex
Normal file
232
phoenix/deps/ecto/lib/ecto/query/window_api.ex
Normal 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
|
||||
151
phoenix/deps/ecto/lib/ecto/queryable.ex
Normal file
151
phoenix/deps/ecto/lib/ecto/queryable.ex
Normal 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
|
||||
2537
phoenix/deps/ecto/lib/ecto/repo.ex
Normal file
2537
phoenix/deps/ecto/lib/ecto/repo.ex
Normal file
File diff suppressed because it is too large
Load Diff
141
phoenix/deps/ecto/lib/ecto/repo/assoc.ex
Normal file
141
phoenix/deps/ecto/lib/ecto/repo/assoc.ex
Normal 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
|
||||
712
phoenix/deps/ecto/lib/ecto/repo/preloader.ex
Normal file
712
phoenix/deps/ecto/lib/ecto/repo/preloader.ex
Normal 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
|
||||
610
phoenix/deps/ecto/lib/ecto/repo/queryable.ex
Normal file
610
phoenix/deps/ecto/lib/ecto/repo/queryable.ex
Normal 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
|
||||
51
phoenix/deps/ecto/lib/ecto/repo/registry.ex
Normal file
51
phoenix/deps/ecto/lib/ecto/repo/registry.ex
Normal 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
|
||||
1313
phoenix/deps/ecto/lib/ecto/repo/schema.ex
Normal file
1313
phoenix/deps/ecto/lib/ecto/repo/schema.ex
Normal file
File diff suppressed because it is too large
Load Diff
227
phoenix/deps/ecto/lib/ecto/repo/supervisor.ex
Normal file
227
phoenix/deps/ecto/lib/ecto/repo/supervisor.ex
Normal 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
|
||||
59
phoenix/deps/ecto/lib/ecto/repo/transaction.ex
Normal file
59
phoenix/deps/ecto/lib/ecto/repo/transaction.ex
Normal 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
|
||||
2674
phoenix/deps/ecto/lib/ecto/schema.ex
Normal file
2674
phoenix/deps/ecto/lib/ecto/schema.ex
Normal file
File diff suppressed because it is too large
Load Diff
106
phoenix/deps/ecto/lib/ecto/schema/loader.ex
Normal file
106
phoenix/deps/ecto/lib/ecto/schema/loader.ex
Normal 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
|
||||
66
phoenix/deps/ecto/lib/ecto/schema/metadata.ex
Normal file
66
phoenix/deps/ecto/lib/ecto/schema/metadata.ex
Normal 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
|
||||
1591
phoenix/deps/ecto/lib/ecto/type.ex
Normal file
1591
phoenix/deps/ecto/lib/ecto/type.ex
Normal file
File diff suppressed because it is too large
Load Diff
234
phoenix/deps/ecto/lib/ecto/uuid.ex
Normal file
234
phoenix/deps/ecto/lib/ecto/uuid.ex
Normal 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
|
||||
148
phoenix/deps/ecto/lib/mix/ecto.ex
Normal file
148
phoenix/deps/ecto/lib/mix/ecto.ex
Normal 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
|
||||
77
phoenix/deps/ecto/lib/mix/tasks/ecto.create.ex
Normal file
77
phoenix/deps/ecto/lib/mix/tasks/ecto.create.ex
Normal 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
|
||||
106
phoenix/deps/ecto/lib/mix/tasks/ecto.drop.ex
Normal file
106
phoenix/deps/ecto/lib/mix/tasks/ecto.drop.ex
Normal 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
|
||||
30
phoenix/deps/ecto/lib/mix/tasks/ecto.ex
Normal file
30
phoenix/deps/ecto/lib/mix/tasks/ecto.ex
Normal 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
|
||||
110
phoenix/deps/ecto/lib/mix/tasks/ecto.gen.repo.ex
Normal file
110
phoenix/deps/ecto/lib/mix/tasks/ecto.gen.repo.ex
Normal 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
|
||||
Reference in New Issue
Block a user