Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:Ledest:erlang:18
ecto
ecto-3.7.1-git.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File ecto-3.7.1-git.patch of Package ecto
diff --git a/CHANGELOG.md b/CHANGELOG.md index 6041d7f3..147421c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -166,7 +166,7 @@ v3.5 requires Elixir v1.8+. ### Bug fixes * [Ecto.Changeset] Ensure `:empty_values` in `cast/4` does not automatically propagate to following cast calls. If you want a given set of `:empty_values` to apply to all `cast/4` calls, change the value stored in `changeset.empty_values` instead - * [Ecto.Changeset] Do not force repository updates to happen when using `optimistic_lock` + * [Ecto.Changeset] **Potentially breaking change**: Do not force repository updates to happen when using `optimistic_lock`. The lock field will only be incremented if the record has other changes. If no changes, nothing happens. * [Ecto.Changeset] Do not automatically share empty values across `cast/3` calls * [Ecto.Query] Consider query prefix in cte/combination query cache * [Ecto.Query] Allow the entry to be marked as nil when using left join with subqueries diff --git a/README.md b/README.md index 159ddc2e..c8c42a3f 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,9 @@ defmodule Sample.App do end ``` -Ecto is commonly used to interact with databases, such as Postgres and MySQL via [Ecto.Adapters.SQL](http://hexdocs.pm/ecto_sql) ([source code](https://github.com/elixir-ecto/ecto_sql)). Ecto is also commonly used to map data from any source into Elixir structs, whether they are backed by a database or not. +Ecto is commonly used to interact with databases, such as Postgres and MySQL via [Ecto.Adapters.SQL](https://hexdocs.pm/ecto_sql) ([source code](https://github.com/elixir-ecto/ecto_sql)). Ecto is also commonly used to map data from any source into Elixir structs, whether they are backed by a database or not. -See the [getting started guide](http://hexdocs.pm/ecto/getting-started.html) and the [online documentation](http://hexdocs.pm/ecto) for more information. Other resources available are: +See the [getting started guide](https://hexdocs.pm/ecto/getting-started.html) and the [online documentation](https://hexdocs.pm/ecto) for more information. Other resources available are: * [Programming Ecto](https://pragprog.com/book/wmecto/programming-ecto), by Darin Wilson and Eric Meadows-Jönsson, which guides you from fundamentals up to advanced concepts @@ -78,10 +78,10 @@ MSSQL | Ecto.Adapters.Tds | [ecto_sql][ecto_sql] (requires Ecto v3.4+) SQLite3 | Ecto.Adapters.SQLite3 | [ecto_sql][ecto_sql] (requires Ecto v3.5+) + [ecto_sqlite3][ecto_sqlite3] ETS | Etso | [ecto][ecto] + [etso][etso] -[ecto]: http://github.com/elixir-ecto/ecto -[ecto_sql]: http://github.com/elixir-ecto/ecto_sql -[postgrex]: http://github.com/elixir-ecto/postgrex -[myxql]: http://github.com/elixir-ecto/myxql +[ecto]: https://github.com/elixir-ecto/ecto +[ecto_sql]: https://github.com/elixir-ecto/ecto_sql +[postgrex]: https://github.com/elixir-ecto/postgrex +[myxql]: https://github.com/elixir-ecto/myxql [tds]: https://github.com/livehelpnow/tds [ecto_sqlite3]: https://github.com/elixir-sqlite/ecto_sqlite3 [etso]: https://github.com/evadne/etso @@ -127,7 +127,7 @@ With the version 3.0, Ecto has become API stable. This means our main focus is o ## Important links - * [Documentation](http://hexdocs.pm/ecto) + * [Documentation](https://hexdocs.pm/ecto) * [Mailing list](https://groups.google.com/forum/#!forum/elixir-ecto) * [Examples](https://github.com/elixir-ecto/ecto/tree/master/examples) @@ -168,7 +168,7 @@ Then once you enter the containerized shell, you can inspect the underlying data "Ecto" and the Ecto logo are Copyright (c) 2020 Dashbit. -The Ecto logo was designed by [Dane Wesolko](http://www.danewesolko.com). +The Ecto logo was designed by [Dane Wesolko](https://www.danewesolko.com). ## License @@ -177,7 +177,7 @@ Copyright (c) 2020 Dashbit Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) +You may obtain a copy of the License at [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/guides/introduction/Getting Started.md b/guides/introduction/Getting Started.md index d85718f9..0f9f535b 100644 --- a/guides/introduction/Getting Started.md +++ b/guides/introduction/Getting Started.md @@ -20,7 +20,7 @@ To start off with, we'll generate a new Elixir application by running this comma mix new friends --sup ``` -The `--sup` option ensures that this application has [a supervision tree](http://elixir-lang.org/getting-started/mix-otp/supervisor-and-application.html), which we'll need for Ecto a little later on. +The `--sup` option ensures that this application has [a supervision tree](https://elixir-lang.org/getting-started/mix-otp/supervisor-and-application.html), which we'll need for Ecto a little later on. To add Ecto to this application, there are a few steps that we need to take. The first step will be adding Ecto and a driver called Postgrex to our `mix.exs` file, which we'll do by changing the `deps` definition in that file to this: @@ -237,7 +237,7 @@ A successful insertion will return a tuple, like so: ```elixir {:ok, - %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded>, age: nil, + %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded, "people">, age: nil, first_name: nil, id: 1, last_name: nil}} ``` @@ -332,7 +332,7 @@ The changeset does not have errors, and is valid. Therefore if we try to insert ```elixir Friends.Repo.insert(changeset) #=> {:ok, - %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded>, age: nil, + %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded, "people">, age: nil, first_name: "Ryan", id: 3, last_name: "Bigg"}} ``` @@ -481,7 +481,7 @@ Friends.Person |> Ecto.Query.first |> Friends.Repo.one The `one` function retrieves just one record from our database and returns a new struct from the `Friends.Person` module: ```elixir -%Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded>, age: 28, +%Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded, "people">, age: 28, first_name: "Ryan", id: 1, last_name: "Bigg"} ``` @@ -489,7 +489,7 @@ Similar to `first`, there is also `last`: ```elixir Friends.Person |> Ecto.Query.last |> Friends.Repo.one -#=> %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded>, age: 26, +#=> %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded, "people">, age: 26, first_name: "Jane", id: 3, last_name: "Smith"} ``` @@ -535,11 +535,11 @@ Friends.Person |> Friends.Repo.all This will return a `Friends.Person` struct representation of all the records that currently exist within our `people` table: ```elixir -[%Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded>, age: 28, +[%Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded, "people">, age: 28, first_name: "Ryan", id: 1, last_name: "Bigg"}, - %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded>, age: 27, + %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded, "people">, age: 27, first_name: "John", id: 2, last_name: "Smith"}, - %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded>, age: 26, + %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded, "people">, age: 26, first_name: "Jane", id: 3, last_name: "Smith"}] ``` @@ -549,7 +549,7 @@ To fetch a record based on its ID, you use the `get` function: ```elixir Friends.Person |> Friends.Repo.get(1) -#=> %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded>, age: 28, +#=> %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded, "people">, age: 28, first_name: "Ryan", id: 1, last_name: "Bigg"} ``` @@ -559,7 +559,7 @@ If we want to get a record based on something other than the `id` attribute, we ```elixir Friends.Person |> Friends.Repo.get_by(first_name: "Ryan") - #=> %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded>, age: 28, + #=> %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded, "people">, age: 28, first_name: "Ryan", id: 1, last_name: "Bigg"} ``` @@ -572,9 +572,9 @@ Friends.Person |> Ecto.Query.where(last_name: "Smith") |> Friends.Repo.all ``` ```elixir -[%Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded>, age: 27, +[%Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded, "people">, age: 27, first_name: "John", id: 2, last_name: "Smith"}, - %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded>, age: 26, + %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded, "people">, age: 26, first_name: "Jane", id: 3, last_name: "Smith"}] ``` @@ -686,7 +686,7 @@ Just like `Friends.Repo.insert`, `Friends.Repo.update` will return a tuple: ```elixir {:ok, - %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded>, age: 29, + %Friends.Person{__meta__: #Ecto.Schema.Metadata<:loaded, "people">, age: 29, first_name: "Ryan", id: 1, last_name: "Bigg"}} ``` @@ -759,7 +759,7 @@ Similar to updating, we must first fetch a record from the database and then cal person = Friends.Repo.get(Friends.Person, 1) Friends.Repo.delete(person) #=> {:ok, - %Friends.Person{__meta__: #Ecto.Schema.Metadata<:deleted>, age: 29, + %Friends.Person{__meta__: #Ecto.Schema.Metadata<:deleted, "people">, age: 29, first_name: "Ryan", id: 2, last_name: "Bigg"}} ``` diff --git a/lib/ecto.ex b/lib/ecto.ex index b16e2dec..143ec052 100644 --- a/lib/ecto.ex +++ b/lib/ecto.ex @@ -21,13 +21,13 @@ defmodule Ecto do Besides the four components above, most developers use Ecto to interact with SQL databases, such as Postgres and MySQL via the - [`ecto_sql`](http://hexdocs.pm/ecto_sql) project. `ecto_sql` provides many + [`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](http://hexdocs.pm/ecto/getting-started.html) and + 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. diff --git a/lib/ecto/changeset.ex b/lib/ecto/changeset.ex index 9b9bb49d..614f06f4 100644 --- a/lib/ecto/changeset.ex +++ b/lib/ecto/changeset.ex @@ -34,7 +34,10 @@ defmodule Ecto.Changeset do The difference between them is that most validations can be executed without a need to interact with the database and, therefore, are always executed before attempting to insert or update the entry - in the database. Some validations may happen against the database but + in the database. Validations run immediately when a validation function + is called on the data that is contained in the changeset at that time. + + Some validations may happen against the database but they are inherently unsafe. Those validations start with a `unsafe_` prefix, such as `unsafe_validate_unique/3`. @@ -410,10 +413,10 @@ defmodule Ecto.Changeset do end @doc """ - Applies the given `params` as changes for the given `data` according to - the given set of `permitted` keys. Returns a changeset. + Applies the given `params` as changes on the `data` according to + the set of `permitted` keys. Returns a changeset. - The given `data` may be either a changeset, a schema struct or a `{data, types}` + `data` may be either a changeset, a schema struct or a `{data, types}` tuple. The second argument is a map of `params` that are cast according to the type information from `data`. `params` is a map with string keys or a map with atom keys, containing potentially invalid data. Mixed keys @@ -1153,7 +1156,7 @@ defmodule Ecto.Changeset do Updates a change. The given `function` is invoked with the change value only if there - is a change for the given `key`. Note that the value of the change + is a change for `key`. Note that the value of the change can still be `nil` (unless the field was marked as required on `validate_required/3`). ## Examples @@ -1192,14 +1195,15 @@ defmodule Ecto.Changeset do ## Examples - iex> changeset = change(%Post{author: "bar"}, %{title: "foo"}) + iex> changeset = change(%Post{}, %{title: "foo"}) iex> changeset = put_change(changeset, :title, "bar") iex> changeset.changes %{title: "bar"} - iex> changeset = put_change(changeset, :author, "bar") + iex> changeset = change(%Post{title: "foo"}) + iex> changeset = put_change(changeset, :title, "foo") iex> changeset.changes - %{title: "bar"} + %{} """ @spec put_change(t, atom, term) :: t @@ -1867,7 +1871,7 @@ defmodule Ecto.Changeset do %Ecto.Changeset{} -> raise ArgumentError, "unsafe_validate_unique/4 does not work with schemaless changesets" end - changeset = %{changeset | validations: [{:unsafe_unique, fields} | validations]} + changeset = %{changeset | validations: [{hd(fields), {:unsafe_unique, fields: fields}} | validations]} where_clause = for field <- fields do {field, get_field(changeset, field)} @@ -2019,6 +2023,8 @@ defmodule Ecto.Changeset do If you need to validate if a single value is inside the given enumerable, you should use `validate_inclusion/4` instead. + Type of the field must be array. + ## Options * `:message` - the message on failure, defaults to "has an invalid entry" @@ -2032,7 +2038,15 @@ defmodule Ecto.Changeset do @spec validate_subset(t, atom, Enum.t, Keyword.t) :: t def validate_subset(changeset, field, data, opts \\ []) do validate_change changeset, field, {:subset, data}, fn _, value -> - {:array, element_type} = Map.fetch!(changeset.types, field) + element_type = + case Map.fetch!(changeset.types, field) do + {:array, element_type} -> + element_type + + type -> + raise ArgumentError, + "validate_subset/4 expects field type to be array, field `#{inspect(field)}` has type `#{inspect(type)}`" + end case Enum.any?(value, fn element -> not Ecto.Type.include?(element_type, element, data) end) do true -> [{field, {message(opts, "has an invalid entry"), [validation: :subset, enum: data]}}] @@ -2361,7 +2375,7 @@ defmodule Ecto.Changeset do Applies optimistic locking to the changeset. [Optimistic - locking](http://en.wikipedia.org/wiki/Optimistic_concurrency_control) (or + locking](https://en.wikipedia.org/wiki/Optimistic_concurrency_control) (or *optimistic concurrency control*) is a technique that allows concurrent edits on a single record. While pessimistic locking works by locking a resource for an entire transaction, optimistic locking only checks if the resource changed @@ -2916,8 +2930,11 @@ defmodule Ecto.Changeset do defp raise_invalid_assoc(types, assoc) do associations = for {_key, {:assoc, %{field: field}}} <- types, do: field - raise ArgumentError, "cannot add constraint to changeset because association `#{assoc}` does not exist. " <> - "Did you mean one of `#{Enum.join(associations, "`, `")}`?" + one_of = if match?([_], associations), do: "", else: "one of " + + raise ArgumentError, + "cannot add constraint to changeset because association `#{assoc}` does not exist. " <> + "Did you mean #{one_of}`#{Enum.join(associations, "`, `")}`?" end defp get_field_source(%{data: %{__struct__: schema}}, field) when is_atom(schema), @@ -3060,7 +3077,7 @@ defimpl Inspect, for: Ecto.Changeset do end redacted_fields = case data do - %type{} -> + %type{} -> if function_exported?(type, :__schema__, 1) do type.__schema__(:redact_fields) else diff --git a/lib/ecto/exceptions.ex b/lib/ecto/exceptions.ex index 28aeeac0..c2cf3011 100644 --- a/lib/ecto/exceptions.ex +++ b/lib/ecto/exceptions.ex @@ -28,7 +28,7 @@ defmodule Ecto.QueryError do def exception(opts) do message = Keyword.fetch!(opts, :message) query = Keyword.fetch!(opts, :query) - hint = Keyword.fetch(opts, :hint) + hint = Keyword.get(opts, :hint) message = """ #{message} in query: @@ -48,9 +48,10 @@ defmodule Ecto.QueryError do end message = - case hint do - {:ok, text} -> message <> "\n" <> text <> "\n" - _ -> message + if hint do + message <> "\n" <> hint <> "\n" + else + message end %__MODULE__{message: message} diff --git a/lib/ecto/query.ex b/lib/ecto/query.ex index ec2c634b..aac92edc 100644 --- a/lib/ecto/query.ex +++ b/lib/ecto/query.ex @@ -223,7 +223,7 @@ defmodule Ecto.Query do Only atoms are accepted for binding names. Named binding references must always be placed at the end of the bindings list: - + [positional_binding_1, positional_binding_2, named_1: binding, named_2: binding] Named bindings can also be used for late binding with the `as/1` @@ -632,22 +632,22 @@ defmodule Ecto.Query do We can write it as a join expression: - set = from(p in Post, + subset = from(p in Post, where: p.synced == false and (is_nil(p.sync_started_at) or p.sync_started_at < ^min_sync_started_at), limit: ^batch_size ) Repo.update_all( - from(p in Post, join: s in subquery(set), on: s.id == p.id), + from(p in Post, join: s in subquery(subset), on: s.id == p.id), set: [sync_started_at: NaiveDateTime.utc_now()] ) Or as a `where` condition: - subset = from(p in subset, select: p.id) + subset_ids = from(p in subset, select: p.id) Repo.update_all( - from(p in Post, where: p.id in subquery(subset)), + from(p in Post, where: p.id in subquery(subset_ids)), set: [sync_started_at: NaiveDateTime.utc_now()] ) @@ -1705,7 +1705,7 @@ defmodule Ecto.Query do If `lock` is used more than once, the last one used takes precedence. Ecto also supports [optimistic - locking](http://en.wikipedia.org/wiki/Optimistic_concurrency_control) but not + locking](https://en.wikipedia.org/wiki/Optimistic_concurrency_control) but not through queries. For more information on optimistic locking, have a look at the `Ecto.Changeset.optimistic_lock/3` function. @@ -1715,7 +1715,7 @@ defmodule Ecto.Query do ## Expressions example - User |> where(u.id == ^current_user) |> lock("FOR SHARE NOWAIT") + User |> where([u], u.id == ^current_user) |> lock("FOR SHARE NOWAIT") """ defmacro lock(query, binding \\ [], expr) do diff --git a/lib/ecto/query/api.ex b/lib/ecto/query/api.ex index 64cb2469..c451ef3f 100644 --- a/lib/ecto/query/api.ex +++ b/lib/ecto/query/api.ex @@ -107,6 +107,26 @@ defmodule Ecto.Query.API do @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] diff --git a/lib/ecto/query/planner.ex b/lib/ecto/query/planner.ex index 0648b555..a54d801c 100644 --- a/lib/ecto/query/planner.ex +++ b/lib/ecto/query/planner.ex @@ -926,14 +926,15 @@ defmodule Ecto.Query.Planner do query |> normalize_query(operation, adapter, counter) |> elem(0) - |> normalize_select(keep_literals?(query)) + |> normalize_select(keep_literals?(operation, query)) rescue e -> # Reraise errors so we ignore the planner inner stacktrace filter_and_reraise e, __STACKTRACE__ end - defp keep_literals?(%{combinations: combinations}), do: combinations != [] + defp keep_literals?(:insert_all, _), do: true + defp keep_literals?(_, %{combinations: combinations}), do: combinations != [] defp normalize_query(query, operation, adapter, counter) do case operation do @@ -1736,7 +1737,31 @@ defmodule Ecto.Query.Planner do error! query, expr, "field `#{field}` in `#{kind}` is a virtual field in schema #{inspect schema}" true -> - error! query, expr, "field `#{field}` in `#{kind}` does not exist in schema #{inspect schema}" + hint = closest_fields_hint(field, schema) + error! query, expr, "field `#{field}` in `#{kind}` does not exist in schema #{inspect schema}", hint + end + end + + defp closest_fields_hint(input, schema) do + input_string = Atom.to_string(input) + + schema.__schema__(:fields) + |> Enum.map(fn field -> {field, String.jaro_distance(input_string, Atom.to_string(field))} end) + |> Enum.filter(fn {_field, score} -> score >= 0.77 end) + |> Enum.sort(& elem(&1, 0) >= elem(&2, 0)) + |> Enum.take(5) + |> Enum.map(&elem(&1, 0)) + |> case do + [] -> + nil + + [suggestion] -> + "Did you mean `#{suggestion}`?" + + suggestions -> + Enum.reduce(suggestions, "Did you mean one of: \n", fn suggestion, acc -> + acc <> "\n * `#{suggestion}`" + end) end end diff --git a/lib/ecto/repo.ex b/lib/ecto/repo.ex index 9e5188aa..18d2f39a 100644 --- a/lib/ecto/repo.ex +++ b/lib/ecto/repo.ex @@ -416,6 +416,7 @@ defmodule Ecto.Repo do application environment. It must return `{:ok, keyword}` with the updated list of configuration or `:ignore` (only in the `:supervisor` case). """ + @doc group: "User callbacks" @callback init(context :: :supervisor | :runtime, config :: Keyword.t()) :: {:ok, Keyword.t()} | :ignore @@ -424,6 +425,7 @@ defmodule Ecto.Repo do @doc """ Returns the adapter tied to the repository. """ + @doc group: "Runtime API" @callback __adapter__ :: Ecto.Adapter.t() @doc """ @@ -432,6 +434,7 @@ defmodule Ecto.Repo do If the `c:init/2` callback is implemented in the repository, it will be invoked with the first argument set to `:runtime`. """ + @doc group: "Runtime API" @callback config() :: Keyword.t() @doc """ @@ -446,6 +449,7 @@ defmodule Ecto.Repo do See the configuration in the moduledoc for options shared between adapters, for adapter-specific configuration see the adapter's documentation. """ + @doc group: "Runtime API" @callback start_link(opts :: Keyword.t()) :: {:ok, pid} | {:error, {:already_started, pid}} @@ -454,6 +458,7 @@ defmodule Ecto.Repo do @doc """ Shuts down the repository. """ + @doc group: "Runtime API" @callback stop(timeout) :: :ok @doc """ @@ -474,6 +479,7 @@ defmodule Ecto.Repo do See the ["Shared options"](#module-shared-options) section at the module documentation for more options. """ + @doc group: "Transaction API" @callback checkout((() -> result), opts :: Keyword.t()) :: result when result: var @doc """ @@ -496,14 +502,15 @@ defmodule Ecto.Repo do end) """ + @doc group: "Transaction API" @callback checked_out?() :: boolean @doc """ - Loads `data` into a struct or a map. + Loads `data` into a schema or a map. - The first argument can be a a schema module, or a - map (of types) and determines the return value: - a struct or a map, respectively. + The first argument can be a a schema module or a map (of types). + The first argument 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. @@ -540,8 +547,9 @@ defmodule Ecto.Repo do [%User{...}, ...] """ + @doc group: "Schema API" @callback load( - module_or_map :: module | map(), + schema_or_map :: module | map(), data :: map() | Keyword.t() | {list, list} ) :: Ecto.Schema.t() | map() @@ -550,6 +558,7 @@ defmodule Ecto.Repo do See `c:put_dynamic_repo/1` for more information. """ + @doc group: "Runtime API" @callback get_dynamic_repo() :: atom() | pid() @doc """ @@ -581,10 +590,8 @@ defmodule Ecto.Repo do From this moment on, all future queries done by the current process will run on `:tenant_foo`. - - **Note this feature is experimental and may be changed or removed in future - releases.** """ + @doc group: "Runtime API" @callback put_dynamic_repo(name_or_pid :: atom() | pid()) :: atom() | pid() ## Ecto.Adapter.Queryable @@ -619,6 +626,7 @@ defmodule Ecto.Repo do MyRepo.get(Post, 42, prefix: "public") """ + @doc group: "Query API" @callback get(queryable :: Ecto.Queryable.t(), id :: term, opts :: Keyword.t()) :: Ecto.Schema.t() | nil @@ -644,6 +652,7 @@ defmodule Ecto.Repo do MyRepo.get!(Post, 42, prefix: "public") """ + @doc group: "Query API" @callback get!(queryable :: Ecto.Queryable.t(), id :: term, opts :: Keyword.t()) :: Ecto.Schema.t() @@ -671,6 +680,7 @@ defmodule Ecto.Repo do MyRepo.get_by(Post, [title: "My post"], prefix: "public") """ + @doc group: "Query API" @callback get_by( queryable :: Ecto.Queryable.t(), clauses :: Keyword.t() | map, @@ -702,6 +712,7 @@ defmodule Ecto.Repo do MyRepo.get_by!(Post, [title: "My post"], prefix: "public") """ + @doc group: "Query API" @callback get_by!( queryable :: Ecto.Queryable.t(), clauses :: Keyword.t() | map, @@ -726,6 +737,7 @@ defmodule Ecto.Repo do MyRepo.reload([deleted_post, post1]) [nil, %Post{}] """ + @doc group: "Schema API" @callback reload( struct_or_structs :: Ecto.Schema.t() | [Ecto.Schema.t()], opts :: Keyword.t() @@ -744,6 +756,7 @@ defmodule Ecto.Repo do MyRepo.reload!([post1, post2]) [%Post{}, %Post{}] """ + @doc group: "Schema API" @callback reload!(struct_or_structs, opts :: Keyword.t()) :: struct_or_structs when struct_or_structs: Ecto.Schema.t() | [Ecto.Schema.t()] @@ -781,6 +794,7 @@ defmodule Ecto.Repo do Repo.aggregate(Post, :count, prefix: "private") """ + @doc group: "Query API" @callback aggregate( queryable :: Ecto.Queryable.t(), aggregate :: :count, @@ -805,6 +819,7 @@ defmodule Ecto.Repo do query = from Post, limit: 10 Repo.aggregate(query, :avg, :visits) """ + @doc group: "Query API" @callback aggregate( queryable :: Ecto.Queryable.t(), aggregate :: :avg | :count | :max | :min | :sum, @@ -842,6 +857,7 @@ defmodule Ecto.Repo do query = from p in Post, where: p.like_count > 10 Repo.exists?(query) """ + @doc group: "Query API" @callback exists?(queryable :: Ecto.Queryable.t(), opts :: Keyword.t()) :: boolean() @doc """ @@ -868,6 +884,7 @@ defmodule Ecto.Repo do query = from p in Post, join: c in assoc(p, :comments), where: p.id == ^post_id Repo.one(query, prefix: "private") """ + @doc group: "Query API" @callback one(queryable :: Ecto.Queryable.t(), opts :: Keyword.t()) :: Ecto.Schema.t() | nil @@ -888,6 +905,7 @@ defmodule Ecto.Repo do See the ["Shared options"](#module-shared-options) section at the module documentation for more options. """ + @doc group: "Query API" @callback one!(queryable :: Ecto.Queryable.t(), opts :: Keyword.t()) :: Ecto.Schema.t() @@ -936,6 +954,7 @@ defmodule Ecto.Repo do The query given to preload may also preload its own associations. """ + @doc group: "Schema API" @callback preload(structs_or_struct_or_nil, preloads :: term, opts :: Keyword.t()) :: structs_or_struct_or_nil when structs_or_struct_or_nil: [Ecto.Schema.t()] | Ecto.Schema.t() | nil @@ -975,6 +994,7 @@ defmodule Ecto.Repo do made from associations and preloads. It is not invoked for each individual join inside a query. """ + @doc group: "User callbacks" @callback prepare_query(operation, query :: Ecto.Query.t(), opts :: Keyword.t()) :: {Ecto.Query.t(), Keyword.t()} when operation: :all | :update_all | :delete_all | :stream | :insert_all @@ -993,6 +1013,7 @@ defmodule Ecto.Repo do this callback will be invoked once at the beginning, but the options returned here will be passed to all following operations. """ + @doc group: "User callbacks" @callback default_options(operation) :: Keyword.t() when operation: :all | :insert_all | :update_all | :delete_all | :stream | :transaction | :insert | :update | :delete | :insert_or_update @@ -1021,6 +1042,7 @@ defmodule Ecto.Repo do select: p.title MyRepo.all(query) """ + @doc group: "Query API" @callback all(queryable :: Ecto.Queryable.t(), opts :: Keyword.t()) :: [Ecto.Schema.t()] @doc """ @@ -1053,10 +1075,11 @@ defmodule Ecto.Repo do query = from p in Post, select: p.title stream = MyRepo.stream(query) - MyRepo.transaction(fn() -> + MyRepo.transaction(fn -> Enum.to_list(stream) end) """ + @doc group: "Query API" @callback stream(queryable :: Ecto.Queryable.t(), opts :: Keyword.t()) :: Enum.t() @doc """ @@ -1101,6 +1124,7 @@ defmodule Ecto.Repo do |> MyRepo.update_all([]) """ + @doc group: "Query API" @callback update_all( queryable :: Ecto.Queryable.t(), updates :: Keyword.t(), @@ -1130,6 +1154,7 @@ defmodule Ecto.Repo do from(p in Post, where: p.id < 10) |> MyRepo.delete_all """ + @doc group: "Query API" @callback delete_all(queryable :: Ecto.Queryable.t(), opts :: Keyword.t()) :: {integer, nil | [term]} @@ -1303,6 +1328,7 @@ defmodule Ecto.Repo do so they are not currently compatible with MySQL """ + @doc group: "Schema API" @callback insert_all( schema_or_source :: binary | {binary, module} | module, entries_or_query :: [map | [{atom, term | Ecto.Query.t}]] | Ecto.Query.t, @@ -1317,7 +1343,8 @@ defmodule Ecto.Repo do In case a changeset is given, the changes in the changeset are merged with the struct fields, and all of them are sent to the - database. + database. If more than one database operation is required, they're + automatically wrapped in a transaction. It returns `{:ok, struct}` if the struct has been successfully inserted or `{:error, changeset}` if there was a validation @@ -1473,6 +1500,7 @@ defmodule Ecto.Repo do at the same time is not recommended, as Ecto will be unable to actually track the proper status of the association. """ + @doc group: "Schema API" @callback insert( struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t(), opts :: Keyword.t() @@ -1484,7 +1512,8 @@ defmodule Ecto.Repo do A changeset is required as it is the only mechanism for tracking dirty changes. Only the fields present in the `changes` part of the changeset are sent to the database. Any other, in-memory - changes done to the schema are ignored. + changes done to the schema are ignored. If more than one database + operation is required, they're automatically wrapped in a transaction. If the struct has no primary key, `Ecto.NoPrimaryKeyFieldError` will be raised. @@ -1533,6 +1562,7 @@ defmodule Ecto.Repo do {:error, changeset} -> # Something went wrong end """ + @doc group: "Schema API" @callback update(changeset :: Ecto.Changeset.t(), opts :: Keyword.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} @@ -1583,6 +1613,7 @@ defmodule Ecto.Repo do {:error, changeset} -> # Something went wrong end """ + @doc group: "Schema API" @callback insert_or_update(changeset :: Ecto.Changeset.t(), opts :: Keyword.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} @@ -1590,12 +1621,16 @@ defmodule Ecto.Repo do Deletes a struct using its primary key. If the struct has no primary key, `Ecto.NoPrimaryKeyFieldError` - will be raised. If the struct has been removed from db prior to - call, `Ecto.StaleEntryError` will be raised. + will be raised. If the struct has been removed prior to the call, + `Ecto.StaleEntryError` will be raised. If more than one database + operation is required, they're automatically wrapped in a transaction. It returns `{:ok, struct}` if the struct has been successfully deleted or `{:error, changeset}` if there was a validation - or a known constraint error. + or a known constraint error. By default, constraint errors will + raise the `Ecto.ConstraintError` exception, unless a changeset is + given as the first argument with the relevant constraints declared + in it (see `Ecto.Changeset`). ## Options @@ -1622,6 +1657,7 @@ defmodule Ecto.Repo do end """ + @doc group: "Schema API" @callback delete( struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t(), opts :: Keyword.t() @@ -1630,6 +1666,7 @@ defmodule Ecto.Repo do @doc """ Same as `c:insert/2` but returns the struct or raises if the changeset is invalid. """ + @doc group: "Schema API" @callback insert!( struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t(), opts :: Keyword.t() @@ -1638,6 +1675,7 @@ defmodule Ecto.Repo do @doc """ Same as `c:update/2` but returns the struct or raises if the changeset is invalid. """ + @doc group: "Schema API" @callback update!(changeset :: Ecto.Changeset.t(), opts :: Keyword.t()) :: Ecto.Schema.t() @@ -1645,12 +1683,14 @@ defmodule Ecto.Repo do Same as `c:insert_or_update/2` but returns the struct or raises if the changeset is invalid. """ + @doc group: "Schema API" @callback insert_or_update!(changeset :: Ecto.Changeset.t(), opts :: Keyword.t()) :: Ecto.Schema.t() @doc """ Same as `c:delete/2` but returns the struct or raises if the changeset is invalid. """ + @doc group: "Schema API" @callback delete!( struct_or_changeset :: Ecto.Schema.t() | Ecto.Changeset.t(), opts :: Keyword.t() @@ -1731,6 +1771,7 @@ defmodule Ecto.Repo do |> MyRepo.transaction """ + @doc group: "Transaction API" @callback transaction(fun_or_multi :: fun | Ecto.Multi.t(), opts :: Keyword.t()) :: {:ok, any} | {:error, any} @@ -1758,6 +1799,7 @@ defmodule Ecto.Repo do end) """ + @doc group: "Transaction API" @callback in_transaction?() :: boolean @doc """ @@ -1767,5 +1809,6 @@ defmodule Ecto.Repo do Note that calling `rollback` causes the code in the transaction to stop executing. """ + @doc group: "Transaction API" @callback rollback(value :: any) :: no_return end diff --git a/lib/ecto/schema.ex b/lib/ecto/schema.ex index 023cfb01..9554f76f 100644 --- a/lib/ecto/schema.ex +++ b/lib/ecto/schema.ex @@ -349,7 +349,7 @@ defmodule Ecto.Schema do stored in a text field. For maps to work in such databases, Ecto will need a JSON library. - By default Ecto will use [Jason](http://github.com/michalmuskala/jason) + By default Ecto will use [Jason](https://github.com/michalmuskala/jason) which needs to be added to your deps in `mix.exs`: {:jason, "~> 1.0"} @@ -486,6 +486,24 @@ defmodule Ecto.Schema do end end + @field_opts [ + :default, + :source, + :autogenerate, + :read_after_writes, + :virtual, + :primary_key, + :load_in_query, + :redact, + :foreign_key, + :on_replace, + :defaults, + :type, + :where, + :references, + :skip_default_validation + ] + @doc """ Defines an embedded schema with the given field definitions. @@ -645,10 +663,16 @@ defmodule Ecto.Schema do ## Options * `:default` - Sets the default value on the schema and the struct. + The default value is calculated at compilation time, so don't use expressions like `DateTime.utc_now` or `Ecto.UUID.generate` as they would then be the same for all records: in this scenario you can use - the `:autogenerate` option to generate at insertion time. + the `:autogenerate` option to generate at insertion time. + + The default value is validated against the field's type at compilation time + and it will raise an ArgumentError if there is a type mismatch. If you cannot + infer the field's type at compilation time, you can use the + `:skip_default_validation` option on the field to skip validations. Once a default value is set, if you send changes to the changeset that contains the same value defined as default, validations will not be performed @@ -685,6 +709,9 @@ defmodule Ecto.Schema do when inspected in changes inside a `Ecto.Changeset` and be excluded from inspect on the schema. Defaults to `false`. + * `:skip_default_validation` - When true, it will skip the type validation + step at compile time. + """ defmacro field(name, type \\ :string, opts \\ []) do quote do @@ -770,8 +797,9 @@ defmodule Ecto.Schema do * `:on_replace` - The action taken on associations when the record is replaced when casting or manipulating parent changeset. May be - `:raise` (default), `:mark_as_invalid`, `:nilify`, or `:delete`. - See `Ecto.Changeset`'s section about ":on_replace" for more info. + `:raise` (default), `:mark_as_invalid`, `:nilify`, `:delete` or + `:delete_if_exists`. See `Ecto.Changeset`'s section about `:on_replace` for + more info. * `:defaults` - Default values to use when building the association. It may be a keyword list of options that override the association schema @@ -1873,14 +1901,12 @@ defmodule Ecto.Schema do @doc false def __field__(mod, name, type, opts) do - if type == :any and !opts[:virtual] do - raise ArgumentError, "only virtual fields can have type :any, " <> - "invalid type for field #{inspect name}" - end - type = check_field_type!(mod, name, type, opts) + + opts = Keyword.put(opts, :type, type) + check_options!(opts, @field_opts, "field/3") Module.put_attribute(mod, :changeset_fields, {name, type}) - validate_default!(type, opts[:default]) + validate_default!(type, opts[:default], opts[:skip_default_validation]) define_field(mod, name, type, opts) end @@ -2134,13 +2160,14 @@ defmodule Ecto.Schema do fields = Module.get_attribute(mod, :struct_fields) if List.keyfind(fields, name, 0) do - raise ArgumentError, "field/association #{inspect name} is already set on schema" + raise ArgumentError, "field/association #{inspect name} already exists on schema, you must either remove the duplication or choose a different name" end Module.put_attribute(mod, :struct_fields, {name, assoc}) end - defp validate_default!(type, value) do + defp validate_default!(_type, _value, true), do: :ok + defp validate_default!(type, value, _skip) do case Ecto.Type.dump(type, value) do {:ok, _} -> :ok @@ -2150,15 +2177,27 @@ defmodule Ecto.Schema do end defp check_options!(opts, valid, fun_arity) do - type = Keyword.get(opts, :type) + case opts[:type] do + {:parameterized, _, _} -> + :ok - if is_atom(type) and Code.ensure_compiled(type) == {:module, type} and function_exported?(type, :type, 1) do - :ok - else - case Enum.find(opts, fn {k, _} -> not(k in valid) end) do - {k, _} -> raise ArgumentError, "invalid option #{inspect k} for #{fun_arity}" - nil -> :ok - end + {_, {:parameterized, _, _}} -> + :ok + + :any -> + if !opts[:virtual], do: + raise ArgumentError, "only virtual fields can have type :any, " <> + "invalid type for field #{inspect opts[:name]}" + + type -> + if is_atom(type) and Code.ensure_compiled(type) == {:module, type} and function_exported?(type, :type, 1) do + :ok + else + case Enum.find(opts, fn {k, _} -> not(k in valid) end) do + {k, _} -> raise ArgumentError, "invalid option #{inspect k} for #{fun_arity}" + nil -> :ok + end + end end end diff --git a/mix.exs b/mix.exs index 6594232c..75a1082e 100644 --- a/mix.exs +++ b/mix.exs @@ -54,13 +54,19 @@ defmodule Ecto.MixProject do [ main: "Ecto", source_ref: "v#{@version}", - canonical: "http://hexdocs.pm/ecto", logo: "guides/images/e.png", extra_section: "GUIDES", source_url: @source_url, skip_undefined_reference_warnings_on: ["CHANGELOG.md"], extras: extras(), groups_for_extras: groups_for_extras(), + groups_for_functions: [ + group_for_function("Query API"), + group_for_function("Schema API"), + group_for_function("Transaction API"), + group_for_function("Runtime API"), + group_for_function("User callbacks") + ], groups_for_modules: [ # Ecto, # Ecto.Changeset, @@ -122,6 +128,8 @@ defmodule Ecto.MixProject do ] end + defp group_for_function(group), do: {String.to_atom(group), &(&1[:group] == group)} + defp groups_for_extras do [ "Introduction": ~r/guides\/introduction\/.?/, diff --git a/mix.lock b/mix.lock index 9f089df5..fefc1d0f 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "decimal": {:hex, :decimal, "1.6.0", "bfd84d90ff966e1f5d4370bdd3943432d8f65f07d3bab48001aebd7030590dcc", [:mix], [], "hexpm", "bbd124e240e3ff40f407d50fced3736049e72a73d547f69201484d3a624ab569"}, "earmark_parser": {:hex, :earmark_parser, "1.4.15", "b29e8e729f4aa4a00436580dcc2c9c5c51890613457c193cc8525c388ccb2f06", [:mix], [], "hexpm", "044523d6438ea19c1b8ec877ec221b008661d3c27e3b848f4c879f500421ca5c"}, - "ex_doc": {:hex, :ex_doc, "0.25.1", "4b736fa38dc76488a937e5ef2944f5474f3eff921de771b25371345a8dc810bc", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3200b0a69ddb2028365281fbef3753ea9e728683863d8cdaa96580925c891f67"}, + "ex_doc": {:hex, :ex_doc, "0.25.2", "4f1cae793c4d132e06674b282f1d9ea3bf409bcca027ddb2fe177c4eed6a253f", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5b0c172e87ac27f14dfd152d52a145238ec71a95efbf29849550278c58a393d6"}, "jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b96c400e04b7b765c0854c05a4966323e90c0d11fee0483b1567cda079abb205"}, "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, "makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"}, diff --git a/test/ecto/changeset_test.exs b/test/ecto/changeset_test.exs index 2856971e..a3fcf931 100644 --- a/test/ecto/changeset_test.exs +++ b/test/ecto/changeset_test.exs @@ -36,12 +36,37 @@ defmodule Ecto.ChangesetTest do end end + defmodule Email do + use Ecto.Type + + def type, do: :string + def cast(val) when is_binary(val), do: {:ok, val} + def cast(_), do: :error + def load(val) when is_binary(val), do: {:ok, val} + def load(_), do: :error + def dump(val) when is_binary(val), do: {:ok, val} + def dump(_), do: :error + + def equal?(email_a, email_b) when is_binary(email_a) and is_binary(email_b) do + [username_a, domain_a] = String.split(email_a, "@") + [username_b, domain_b] = String.split(email_b, "@") + + [significant_a | _] = String.split(username_a, "+") + [significant_b | _] = String.split(username_b, "+") + + significant_a == significant_b && domain_a == domain_b + end + + def equal?(a, b), do: a == b + end + defmodule Post do use Ecto.Schema schema "posts" do field :token, :integer, primary_key: true field :title, :string, default: "" + field :author_email, Email field :body field :uuid, :binary_id field :color, :binary @@ -72,7 +97,7 @@ defmodule Ecto.ChangesetTest do end defp changeset(schema \\ %Post{}, params) do - cast(schema, params, ~w(id token title body upvotes decimal color topics virtual)a) + cast(schema, params, ~w(id token title author_email body upvotes decimal color topics virtual)a) end defmodule CustomError do @@ -1014,7 +1039,9 @@ defmodule Ecto.ChangesetTest do changeset(%{"title" => "hello"}) |> validate_inclusion(:title, ~w(world), message: "yada") assert changeset.errors == [title: {"yada", [validation: :inclusion, enum: ~w(world)]}] + end + test "validate_inclusion/3 with decimal" do changeset = {%{}, %{value: :decimal}} |> Ecto.Changeset.cast(%{value: 0}, [:value]) @@ -1023,6 +1050,16 @@ defmodule Ecto.ChangesetTest do assert changeset.valid? end + test "validate_inclusion/3 with custom type and custom equal function" do + changeset = + changeset(%{"author_email" => "carl+1@example.com"}) + |> validate_inclusion(:author_email, ["carl@example.com"]) + + assert changeset.valid? + assert changeset.errors == [] + assert validations(changeset) == [author_email: {:inclusion, ["carl@example.com"]}] + end + test "validate_subset/3" do changeset = changeset(%{"topics" => ["cat", "dog"]}) @@ -1042,14 +1079,24 @@ defmodule Ecto.ChangesetTest do changeset(%{"topics" => ["laptop"]}) |> validate_subset(:topics, ~w(cat dog), message: "yada") assert changeset.errors == [topics: {"yada", [validation: :subset, enum: ~w(cat dog)]}] + end + test "validate_subset/3 with decimal" do changeset = {%{}, %{value: {:array, :decimal}}} |> Ecto.Changeset.cast(%{value: [0, 0.2]}, [:value]) |> validate_subset(:value, Enum.map([0.0, 0.2], &Decimal.from_float/1)) + assert changeset.valid? end + test "validate_subset/3 raises if field type is not array" do + assert_raise ArgumentError, "validate_subset/4 expects field type to be array, field `:title` has type `:string`", fn -> + changeset(%{"title" => "hello"}) + |> validate_subset(:title, ["hello"]) + end + end + test "validate_exclusion/3" do changeset = changeset(%{"title" => "world"}) @@ -1069,7 +1116,9 @@ defmodule Ecto.ChangesetTest do changeset(%{"title" => "world"}) |> validate_exclusion(:title, ~w(world), message: "yada") assert changeset.errors == [title: {"yada", [validation: :exclusion, enum: ~w(world)]}] + end + test "validate_exclusion/3 with decimal" do decimals = Enum.map([0.0, 0.2], &Decimal.from_float/1) changeset = {%{}, %{value: :decimal}} @@ -1291,8 +1340,8 @@ defmodule Ecto.ChangesetTest do end end - test "validate_number/3 with bad value" do - assert_raise ArgumentError, "expected value to be of type Decimal, Integer or Float, got: \"Oops\"", fn -> + test "validate_number/3 with bad value" do + assert_raise ArgumentError, "expected value to be of type Decimal, Integer or Float, got: \"Oops\"", fn -> validate_number(changeset(%{"virtual" => "Oops"}), :virtual, greater_than: 0) end end @@ -1444,6 +1493,8 @@ defmodule Ecto.ChangesetTest do assert changeset.errors == [title: {"has already been taken", validation: :unsafe_unique, fields: [:title]}] + assert changeset.validations == [title: {:unsafe_unique, fields: [:title]}] + Process.put(:test_repo_all_results, context.no_dup_result) changeset = unsafe_validate_unique(context.base_changeset, :title, TestRepo) assert changeset.valid? @@ -1459,6 +1510,8 @@ defmodule Ecto.ChangesetTest do {"has already been taken", validation: :unsafe_unique, fields: [:title, :body]} ] + assert changeset.validations == [title: {:unsafe_unique, fields: [:title, :body]}] + Process.put(:test_repo_all_results, context.no_dup_result) changeset = unsafe_validate_unique(context.base_changeset, [:title, :body], TestRepo) assert changeset.valid? @@ -1528,46 +1581,91 @@ defmodule Ecto.ChangesetTest do assert Macro.to_string(check_expr) == "&0.body() == ^0" end - test "generates correct where clause for single primary key without query option" do - body_change = cast(%SinglePkSchema{id: 0, body: "hi"}, %{body: "ho"}, [:body]) - unsafe_validate_unique(body_change, :body, MockRepo) - assert_receive [MockRepo, function: :one, query: %Ecto.Query{wheres: wheres}, opts: []] - assert [%{expr: pk_expr}, %{expr: check_expr}] = wheres + # TODO: AST is represented as string differently on versions pre 1.13 + if Version.match?(System.version(), ">= 1.13.0-dev") do + test "generates correct where clause for single primary key without query option" do + body_change = cast(%SinglePkSchema{id: 0, body: "hi"}, %{body: "ho"}, [:body]) + unsafe_validate_unique(body_change, :body, MockRepo) + assert_receive [MockRepo, function: :one, query: %Ecto.Query{wheres: wheres}, opts: []] + assert [%{expr: pk_expr}, %{expr: check_expr}] = wheres - assert Macro.to_string(pk_expr) == "not(&0.id() == ^0)" - assert Macro.to_string(check_expr) == "&0.body() == ^0" - end + assert Macro.to_string(pk_expr) == "not (&0.id() == ^0)" + assert Macro.to_string(check_expr) == "&0.body() == ^0" + end - test "generates correct where clause for composite primary keys without query option" do - body_change = changeset(%Post{id: 0, token: 1, body: "hi"}, %{body: "ho"}) - unsafe_validate_unique(body_change, :body, MockRepo) - assert_receive [MockRepo, function: :one, query: %Ecto.Query{wheres: wheres}, opts: []] - assert [%{expr: pk_expr}, %{expr: check_expr}] = wheres + test "generates correct where clause for composite primary keys without query option" do + body_change = changeset(%Post{id: 0, token: 1, body: "hi"}, %{body: "ho"}) + unsafe_validate_unique(body_change, :body, MockRepo) + assert_receive [MockRepo, function: :one, query: %Ecto.Query{wheres: wheres}, opts: []] + assert [%{expr: pk_expr}, %{expr: check_expr}] = wheres - assert Macro.to_string(pk_expr) == "not(&0.id() == ^0 and &0.token() == ^1)" - assert Macro.to_string(check_expr) == "&0.body() == ^0" - end + assert Macro.to_string(pk_expr) == "not (&0.id() == ^0 and &0.token() == ^1)" + assert Macro.to_string(check_expr) == "&0.body() == ^0" + end - test "generates correct where clause for single primary key with query option" do - body_change = cast(%SinglePkSchema{id: 0, body: "hi"}, %{body: "ho"}, [:body]) - unsafe_validate_unique(body_change, :body, MockRepo, query: Ecto.Query.from(p in SinglePkSchema, where: is_nil(p.published_at))) - assert_receive [MockRepo, function: :one, query: %Ecto.Query{wheres: wheres}, opts: []] - assert [%{expr: query_expr}, %{expr: pk_expr}, %{expr: check_expr}] = wheres + test "generates correct where clause for single primary key with query option" do + body_change = cast(%SinglePkSchema{id: 0, body: "hi"}, %{body: "ho"}, [:body]) + unsafe_validate_unique(body_change, :body, MockRepo, query: Ecto.Query.from(p in SinglePkSchema, where: is_nil(p.published_at))) + assert_receive [MockRepo, function: :one, query: %Ecto.Query{wheres: wheres}, opts: []] + assert [%{expr: query_expr}, %{expr: pk_expr}, %{expr: check_expr}] = wheres - assert Macro.to_string(query_expr) == "is_nil(&0.published_at())" - assert Macro.to_string(pk_expr) == "not(&0.id() == ^0)" - assert Macro.to_string(check_expr) == "&0.body() == ^0" - end + assert Macro.to_string(query_expr) == "is_nil(&0.published_at())" + assert Macro.to_string(pk_expr) == "not (&0.id() == ^0)" + assert Macro.to_string(check_expr) == "&0.body() == ^0" + end - test "generates correct where clause for composite primary keys with query option" do - body_change = changeset(%Post{id: 0, token: 1, body: "hi"}, %{body: "ho"}) - unsafe_validate_unique(body_change, :body, MockRepo, query: Ecto.Query.from(p in Post, where: is_nil(p.published_at))) - assert_receive [MockRepo, function: :one, query: %Ecto.Query{wheres: wheres}, opts: []] - assert [%{expr: query_expr}, %{expr: pk_expr}, %{expr: check_expr}] = wheres + test "generates correct where clause for composite primary keys with query option" do + body_change = changeset(%Post{id: 0, token: 1, body: "hi"}, %{body: "ho"}) + unsafe_validate_unique(body_change, :body, MockRepo, query: Ecto.Query.from(p in Post, where: is_nil(p.published_at))) + assert_receive [MockRepo, function: :one, query: %Ecto.Query{wheres: wheres}, opts: []] + assert [%{expr: query_expr}, %{expr: pk_expr}, %{expr: check_expr}] = wheres - assert Macro.to_string(query_expr) == "is_nil(&0.published_at())" - assert Macro.to_string(pk_expr) == "not(&0.id() == ^0 and &0.token() == ^1)" - assert Macro.to_string(check_expr) == "&0.body() == ^0" + assert Macro.to_string(query_expr) == "is_nil(&0.published_at())" + assert Macro.to_string(pk_expr) == "not (&0.id() == ^0 and &0.token() == ^1)" + assert Macro.to_string(check_expr) == "&0.body() == ^0" + end + else + test "generates correct where clause for single primary key without query option" do + body_change = cast(%SinglePkSchema{id: 0, body: "hi"}, %{body: "ho"}, [:body]) + unsafe_validate_unique(body_change, :body, MockRepo) + assert_receive [MockRepo, function: :one, query: %Ecto.Query{wheres: wheres}, opts: []] + assert [%{expr: pk_expr}, %{expr: check_expr}] = wheres + + assert Macro.to_string(pk_expr) == "not(&0.id() == ^0)" + assert Macro.to_string(check_expr) == "&0.body() == ^0" + end + + test "generates correct where clause for composite primary keys without query option" do + body_change = changeset(%Post{id: 0, token: 1, body: "hi"}, %{body: "ho"}) + unsafe_validate_unique(body_change, :body, MockRepo) + assert_receive [MockRepo, function: :one, query: %Ecto.Query{wheres: wheres}, opts: []] + assert [%{expr: pk_expr}, %{expr: check_expr}] = wheres + + assert Macro.to_string(pk_expr) == "not(&0.id() == ^0 and &0.token() == ^1)" + assert Macro.to_string(check_expr) == "&0.body() == ^0" + end + + test "generates correct where clause for single primary key with query option" do + body_change = cast(%SinglePkSchema{id: 0, body: "hi"}, %{body: "ho"}, [:body]) + unsafe_validate_unique(body_change, :body, MockRepo, query: Ecto.Query.from(p in SinglePkSchema, where: is_nil(p.published_at))) + assert_receive [MockRepo, function: :one, query: %Ecto.Query{wheres: wheres}, opts: []] + assert [%{expr: query_expr}, %{expr: pk_expr}, %{expr: check_expr}] = wheres + + assert Macro.to_string(query_expr) == "is_nil(&0.published_at())" + assert Macro.to_string(pk_expr) == "not(&0.id() == ^0)" + assert Macro.to_string(check_expr) == "&0.body() == ^0" + end + + test "generates correct where clause for composite primary keys with query option" do + body_change = changeset(%Post{id: 0, token: 1, body: "hi"}, %{body: "ho"}) + unsafe_validate_unique(body_change, :body, MockRepo, query: Ecto.Query.from(p in Post, where: is_nil(p.published_at))) + assert_receive [MockRepo, function: :one, query: %Ecto.Query{wheres: wheres}, opts: []] + assert [%{expr: query_expr}, %{expr: pk_expr}, %{expr: check_expr}] = wheres + + assert Macro.to_string(query_expr) == "is_nil(&0.published_at())" + assert Macro.to_string(pk_expr) == "not(&0.id() == ^0 and &0.token() == ^1)" + assert Macro.to_string(check_expr) == "&0.body() == ^0" + end end test "only queries the db when necessary" do diff --git a/test/ecto/query/builder/dynamic_test.exs b/test/ecto/query/builder/dynamic_test.exs index b03d8151..5bc3e0b3 100644 --- a/test/ecto/query/builder/dynamic_test.exs +++ b/test/ecto/query/builder/dynamic_test.exs @@ -27,13 +27,25 @@ defmodule Ecto.Query.Builder.DynamicTest do assert params == [{1, {0, :foo}}] end - test "with dynamic interpolation" do - dynamic = dynamic([p], p.bar == ^2) - dynamic = dynamic([p], p.foo == ^1 and ^dynamic or p.baz == ^3) - assert {expr, _, params, [], _, _} = fully_expand(query(), dynamic) - assert Macro.to_string(expr) == - "&0.foo() == ^0 and &0.bar() == ^1 or &0.baz() == ^2" - assert params == [{1, {0, :foo}}, {2, {0, :bar}}, {3, {0, :baz}}] + # TODO: AST is represented as string differently on versions pre 1.13 + if Version.match?(System.version(), ">= 1.13.0-dev") do + test "with dynamic interpolation" do + dynamic = dynamic([p], p.bar == ^2) + dynamic = dynamic([p], p.foo == ^1 and ^dynamic or p.baz == ^3) + assert {expr, _, params, [], _, _} = fully_expand(query(), dynamic) + assert Macro.to_string(expr) == + "(&0.foo() == ^0 and &0.bar() == ^1) or &0.baz() == ^2" + assert params == [{1, {0, :foo}}, {2, {0, :bar}}, {3, {0, :baz}}] + end + else + test "with dynamic interpolation" do + dynamic = dynamic([p], p.bar == ^2) + dynamic = dynamic([p], p.foo == ^1 and ^dynamic or p.baz == ^3) + assert {expr, _, params, [], _, _} = fully_expand(query(), dynamic) + assert Macro.to_string(expr) == + "&0.foo() == ^0 and &0.bar() == ^1 or &0.baz() == ^2" + assert params == [{1, {0, :foo}}, {2, {0, :bar}}, {3, {0, :baz}}] + end end test "with subquery and dynamic interpolation" do diff --git a/test/ecto/query/inspect_test.exs b/test/ecto/query/inspect_test.exs index de071ee2..db05356e 100644 --- a/test/ecto/query/inspect_test.exs +++ b/test/ecto/query/inspect_test.exs @@ -348,13 +348,25 @@ defmodule Ecto.Query.InspectTest do ) == string end - test "container values" do - assert i(from(Post, select: <<1, 2, 3>>)) == - "from p0 in Inspect.Post, select: <<1, 2, 3>>" - - foo = <<1, 2, 3>> - assert i(from(p in Post, select: {p, ^foo})) == - "from p0 in Inspect.Post, select: {p0, ^<<1, 2, 3>>}" + # TODO: AST is represented as string differently on versions pre 1.13 + if Version.match?(System.version(), ">= 1.13.0-dev") do + test "container values" do + assert i(from(Post, select: <<1, 2, 3>>)) == + "from p0 in Inspect.Post, select: \"\\x01\\x02\\x03\"" + + foo = <<1, 2, 3>> + assert i(from(p in Post, select: {p, ^foo})) == + "from p0 in Inspect.Post, select: {p0, ^\"\\x01\\x02\\x03\"}" + end + else + test "container values" do + assert i(from(Post, select: <<1, 2, 3>>)) == + "from p0 in Inspect.Post, select: <<1, 2, 3>>" + + foo = <<1, 2, 3>> + assert i(from(p in Post, select: {p, ^foo})) == + "from p0 in Inspect.Post, select: {p0, ^<<1, 2, 3>>}" + end end test "select" do diff --git a/test/ecto/query/planner_test.exs b/test/ecto/query/planner_test.exs index 4d520f63..89e05d1f 100644 --- a/test/ecto/query/planner_test.exs +++ b/test/ecto/query/planner_test.exs @@ -981,6 +981,16 @@ defmodule Ecto.Query.PlannerTest do normalize(query) end + exception = assert_raise Ecto.QueryError, fn -> + query = from(Comment, []) |> select([c], c.postd) + normalize(query) + end + + assert exception.message =~ "field `postd` in `select` does not exist in schema" + assert exception.message =~ "Did you mean one of:" + assert exception.message =~ "* `posted`" + assert exception.message =~ "* `post_id`" + message = ~r"field `temp` in `select` is a virtual field in schema Ecto.Query.PlannerTest.Comment" assert_raise Ecto.QueryError, message, fn -> query = from(Comment, []) |> select([c], c.temp) diff --git a/test/ecto/repo_test.exs b/test/ecto/repo_test.exs index 14ed156e..8bbd69ad 100644 --- a/test/ecto/repo_test.exs +++ b/test/ecto/repo_test.exs @@ -532,6 +532,26 @@ defmodule Ecto.RepoTest do assert ["one", "two", "ten"] = params end + test "takes query as datasource with literals" do + import Ecto.Query + + threshold = "ten" + + query = from s in MySchema, + where: s.x > ^threshold, + select: %{ + foo: s.x, + bar: "bar", + baz: nil + } + + TestRepo.insert_all(MySchema, query) + + assert_received {:insert_all, %{source: "my_schema"}, {%Ecto.Query{} = query, params}} + assert [{{:., _, [{:&, [], [0]}, :x]}, _, []}, "bar", nil] = query.select.fields + assert ["ten"] = params + end + test "raises when a bad query is given as source" do assert_raise ArgumentError, fn -> TestRepo.insert_all(MySchema, from(s in MySchema)) diff --git a/test/ecto/schema_test.exs b/test/ecto/schema_test.exs index 464004db..ddcdeb84 100644 --- a/test/ecto/schema_test.exs +++ b/test/ecto/schema_test.exs @@ -8,7 +8,7 @@ defmodule Ecto.SchemaTest do schema "my schema" do field :name, :string, default: "eric", autogenerate: {String, :upcase, ["eric"]} - field :email, :string, uniq: true, read_after_writes: true + field :email, :string, read_after_writes: true field :password, :string, redact: true field :temp, :any, default: "temp", virtual: true, redact: true field :count, :decimal, read_after_writes: true, source: :cnt @@ -384,7 +384,7 @@ defmodule Ecto.SchemaTest do ## Errors test "field name clash" do - assert_raise ArgumentError, "field/association :name is already set on schema", fn -> + assert_raise ArgumentError, ~r"field/association :name already exists on schema", fn -> defmodule SchemaFieldNameClash do use Ecto.Schema @@ -418,6 +418,39 @@ defmodule Ecto.SchemaTest do end end + test "skipping validations on invalid types" do + defmodule SchemaSkipValidationsDefault do + use Ecto.Schema + + schema "invalid_default" do + # Without skip_default_validation this would fail to compile + field :count, :integer, default: "1", skip_default_validation: true + end + end + end + + test "invalid option for field" do + assert_raise ArgumentError, ~s/invalid option :starts_on for field\/3/, fn -> + defmodule SchemaInvalidFieldOption do + use Ecto.Schema + + schema "invalid_option" do + field :count, :integer, starts_on: 3 + end + end + end + + # doesn't validate for parameterized types + defmodule SchemaInvalidOptionParameterized do + use Ecto.Schema + + schema "invalid_option_parameterized" do + field :my_enum, Ecto.Enum, values: [:a, :b], random_option: 3 + field :my_enums, Ecto.Enum, values: [:a, :b], random_option: 3 + end + end + end + test "invalid field type" do assert_raise ArgumentError, "invalid or unknown type {:apa} for field :name", fn -> defmodule SchemaInvalidFieldType do
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor