Repository: elixir-ecto/ecto
Pull request #2175 (2017-08-22 10:05:26)

Parse URL query params
It is common to use params in PostgreSQL connection URIs
(https://www.postgresql.org/docs/9.6/static/libpq-connect.html#AEN45575)
 and we can leverage them to set our shared params.

Change is inspired by our production issue, we had to enable SSL after upgrading database on Heroku without rebuilding the release artifact.

josevalim: @michalmuskala thoughts?
AndrewDryga: Just in case adapter developers want to leverage this feature, we can extend adapter behavior with optional callbacks that resolve query params or return us list of supported options and their types.
michalmuskala: Many of the postgres libraries support URL params - JDBC seems to have couple pages of them, libpq has a couple. I guess we could do it too. I don't see a reason not to do it.

Patch:
diff --git a/README.md b/README.md
index bf128a72..0055ab18 100644
--- a/README.md
+++ b/README.md
@@ -105,9 +105,9 @@ We are currently looking for contributions to add support for other SQL database
 
 | Branch | Support                  |
 | ------ | ------------------------ |
-| v2.2   | In development (master)  |
-| v2.1   | Bug fixes                |
-| v2.0   | Security patches only    |
+| v2.2   | Bug fixes                |
+| v2.1   | Security patches only    |
+| v2.0   | Unsupported from 08/2017 |
 | v1.1   | Security patches only    |
 | v1.0   | Unsupported from 05/2017 |
 
diff --git a/guides/Associations.md b/guides/Associations.md
index cda097af..46941360 100644
--- a/guides/Associations.md
+++ b/guides/Associations.md
@@ -232,7 +232,7 @@ This time let's add another user with an avatar association. We can define it di
 
 ```elixir
 iex> avatar = %Avatar{nick_name: "Elixir", pic_url: "http://elixir-lang.org/images/logo.png"}
-iex> user = %User{name: "John Doe", email: "john.doe@example.com", avatar: avatar}
+iex> user = %User{name: "Jane Doe", email: "jane@example.com", avatar: avatar}
 iex> user = Repo.insert!(user)
 %EctoAssoc.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
  avatar: %{__meta__: #Ecto.Schema.Metadata<:loaded, "avatars">,
@@ -626,15 +626,61 @@ iex> tag = Repo.get(Tag, 1) |> Repo.preload(:posts)
    tags: #Ecto.Association.NotLoaded<association :tags is not loaded>}]}
 ```
 
-The advantage of using Ecto.Changeset is that it is responsible for tracking the changes between your data structures and the associated data. For example, if you want you remove the clickbait tag from from the post, one way to do so is by calling [`Ecto.Changeset.put_assoc/3`](Ecto.Changeset.html#put_assoc/4) once more but without the clickbait tag:
+The advantage of using Ecto.Changeset is that it is responsible for tracking the changes between your data structures and the associated data. For example, if you want you remove the clickbait tag from from the post, one way to do so is by calling [`Ecto.Changeset.put_assoc/3`](Ecto.Changeset.html#put_assoc/4) once more but without the clickbait tag.  This will not work right now, because the `:on_replace` option for the `many_to_many` relationship defaults to `:raise`.  Go ahead and try it.  When you try to call `put_assoc`, a runtime error will be raised:
 
 ```elixir
 iex> post_changeset = Ecto.Changeset.change(post)
 iex> post_with_tags = Ecto.Changeset.put_assoc(post_changeset, :tags, [misc_tag])
-iex> post = Repo.update!(post_with_tags)
+** (RuntimeError) you are attempting to change relation :tags of
+Website.CMS.Page 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 all existing
+data to be given on update. 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 "Related data" 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, at least
+    the same N children must be given on update.
+...
+```
+
+You should carefully read the documentation for [`Ecto.Schema.many_to_many/3`](Ecto.Schema.html#many_to_many/3). It makes sense in this case that we want to delete relationships in the join table `posts_tags` when updating a post with new tags.  Here we want to drop the tag "clickbait" and just keep the tag "misc", so we really do want the relationship in the joining table to be removed.  To do that, change the definition of the `many_to_many/3` in the Post schema:
+
+```elixir
+# lib/ecto_assoc/post.ex
+defmodule EctoAssoc.Post do
+  use Ecto.Schema
+
+  schema "posts" do
+    field :header, :string
+    field :body, :string
+    # the following line was edited to change the on_replace option from its default value of :raise
+    many_to_many :tags, EctoAssoc.Tag, join_through: "posts_tags", on_replace: :delete
+  end
+end
 ```
 
-Ecto will compare the data you gave with the tags currently in the post and conclude the association between the post and the clickbait tag must be removed.
+On the other hand, it probably *doesn't* make much sense to be able to remove relationships from the other end.  That is, with just a tag, it is hard to decide if a post should be related to the tag or not.  So it makes sense that we should still raise an error if we try to change posts that are related to tags from the tag side of things.
+
+With the `:on_replace` option changed, Ecto will compare the data you gave with the tags currently in the post and conclude the association between the post and the clickbait tag must be removed, as follows:
+
+```elixir
+iex> post_changeset = Ecto.Changeset.change(post)
+iex> post_with_tags = Ecto.Changeset.put_assoc(post_changeset, :tags, [misc_tag])
+iex> post = Repo.update!(post_with_tags)
+```
 
 ## References
 
diff --git a/guides/Getting Started.md b/guides/Getting Started.md
index 0aa5405c..b95eb1be 100644
--- a/guides/Getting Started.md
+++ b/guides/Getting Started.md
@@ -99,7 +99,6 @@ def start(_type, _args) do
 `Elixir >= 1.5.0`:
 ```elixir
 def start(_type, _args) do
-  import Supervisor.Spec
 
   children = [
     Friends.Repo,
diff --git a/lib/ecto/adapters/mysql/connection.ex b/lib/ecto/adapters/mysql/connection.ex
index 710d74a1..e85b55e8 100644
--- a/lib/ecto/adapters/mysql/connection.ex
+++ b/lib/ecto/adapters/mysql/connection.ex
@@ -142,9 +142,9 @@ if Code.ensure_loaded?(Mariaex) do
       quoted = quote_name(field)
       [" ON DUPLICATE KEY UPDATE ", quoted, " = " | quoted]
     end
-    defp on_conflict({:replace_all, _, []}, header) do
+    defp on_conflict({fields, _, []}, _header) when is_list(fields) do
       [" ON DUPLICATE KEY UPDATE " |
-       intersperse_map(header, ?,, fn field ->
+       intersperse_map(fields, ?,, fn field ->
          quoted = quote_name(field)
          [quoted, " = VALUES(", quoted, ?)]
        end)]
@@ -480,7 +480,7 @@ if Code.ensure_loaded?(Mariaex) do
       current =
         case elem(sources, pos) do
           {table, schema} ->
-            name = [String.first(table) | Integer.to_string(pos)]
+            name = [create_alias(table) | Integer.to_string(pos)]
             {quote_table(prefix, table), name, schema}
           {:fragment, _, _} ->
             {nil, [?f | Integer.to_string(pos)], nil}
@@ -494,6 +494,13 @@ if Code.ensure_loaded?(Mariaex) do
       []
     end
 
+    defp create_alias(<<first, _rest::binary>>) when first in ?a..?z when first in ?A..?Z do
+      <<first>>
+    end
+    defp create_alias(_) do
+      "t"
+    end
+
     ## DDL
 
     alias Ecto.Migration.{Table, Index, Reference, Constraint}
diff --git a/lib/ecto/adapters/postgres/connection.ex b/lib/ecto/adapters/postgres/connection.ex
index 7db48ebe..a4fcbc48 100644
--- a/lib/ecto/adapters/postgres/connection.ex
+++ b/lib/ecto/adapters/postgres/connection.ex
@@ -180,8 +180,8 @@ if Code.ensure_loaded?(Postgrex) do
       do: []
     defp on_conflict({:nothing, _, targets}, _header),
       do: [" ON CONFLICT ", conflict_target(targets) | "DO NOTHING"]
-    defp on_conflict({:replace_all, _, targets}, header),
-      do: [" ON CONFLICT ", conflict_target(targets), "DO " | replace_all(header)]
+    defp on_conflict({fields, _, targets}, _header) when is_list(fields),
+      do: [" ON CONFLICT ", conflict_target(targets), "DO " | replace(fields)]
     defp on_conflict({query, _, targets}, _header),
       do: [" ON CONFLICT ", conflict_target(targets), "DO " | update_all(query, "UPDATE SET ")]
 
@@ -192,9 +192,9 @@ if Code.ensure_loaded?(Postgrex) do
     defp conflict_target(targets),
       do: [?(, intersperse_map(targets, ?,, &quote_name/1), ?), ?\s]
 
-    defp replace_all(header) do
+    defp replace(fields) do
       ["UPDATE SET " |
-       intersperse_map(header, ?,, fn field ->
+       intersperse_map(fields, ?,, fn field ->
          quoted = quote_name(field)
          [quoted, " = ", "EXCLUDED." | quoted]
        end)]
@@ -564,7 +564,7 @@ if Code.ensure_loaded?(Postgrex) do
       current =
         case elem(sources, pos) do
           {table, schema} ->
-            name = [String.first(table) | Integer.to_string(pos)]
+            name = [create_alias(table) | Integer.to_string(pos)]
             {quote_table(prefix, table), name, schema}
           {:fragment, _, _} ->
             {nil, [?f | Integer.to_string(pos)], nil}
@@ -578,6 +578,13 @@ if Code.ensure_loaded?(Postgrex) do
       []
     end
 
+    defp create_alias(<<first, _rest::binary>>) when first in ?a..?z when first in ?A..?Z do
+      <<first>>
+    end
+    defp create_alias(_) do
+      "t"
+    end
+
     # DDL
 
     alias Ecto.Migration.{Table, Index, Reference, Constraint}
diff --git a/lib/ecto/changeset.ex b/lib/ecto/changeset.ex
index bdef4d62..66b344ac 100644
--- a/lib/ecto/changeset.ex
+++ b/lib/ecto/changeset.ex
@@ -482,7 +482,7 @@ defmodule Ecto.Changeset do
   defp type!(types, key) do
     case types do
       %{^key => {tag, _}} when tag in @relations ->
-        raise "casting #{tag}s with cast/4 is not supported, use cast_#{tag}/3 instead"
+        raise "casting #{tag}s with cast/4 for #{inspect key} field is not supported, use cast_#{tag}/3 instead"
       %{^key => type} ->
         type
       _ ->
@@ -717,7 +717,7 @@ defmodule Ecto.Changeset do
               missing_relation(%{changeset | changes: Map.put(changes, key, change),
                                  valid?: changeset.valid? and relation_valid?}, key, current, required?, relation, opts)
             :error ->
-              %{changeset | errors: [{key, {message(opts, :invalid_message, "is invalid"), [type: expected_relation_type(relation)]}} | changeset.errors], valid?: false}
+              %{changeset | errors: [{key, {message(opts, :invalid_message, "is invalid"), [validation: type, type: expected_relation_type(relation)]}} | changeset.errors], valid?: false}
             _ -> # ignore or ok with change == original
               missing_relation(changeset, key, current, required?, relation, opts)
           end
@@ -1303,7 +1303,7 @@ defmodule Ecto.Changeset do
       [title: {"cannot be foo", []}]
 
   """
-  @spec validate_change(t, atom, (atom, term -> [error])) :: t
+  @spec validate_change(t, atom, (atom, term -> [{atom, String.t} | {atom, {String.t, Keyword.t}}])) :: t
   def validate_change(%Changeset{} = changeset, field, validator) when is_atom(field) do
     %{changes: changes, errors: errors} = changeset
     ensure_field_exists!(changeset, field)
@@ -1342,7 +1342,7 @@ defmodule Ecto.Changeset do
       [title: :useless_validator]
 
   """
-  @spec validate_change(t, atom, term, (atom, term -> [error])) :: t
+  @spec validate_change(t, atom, term, (atom, term -> [{atom, String.t} | {atom, {String.t, Keyword.t}}])) :: t
   def validate_change(%Changeset{validations: validations} = changeset,
                       field, metadata, validator) do
     changeset = %{changeset | validations: [{field, metadata}|validations]}
@@ -1415,7 +1415,7 @@ defmodule Ecto.Changeset do
 
       unsafe_validate_unique(changeset, [:email], repo)
       unsafe_validate_unique(changeset, [:city_name, :state_name], repo)
-      unsafe_validate_unique(changeset, [:city_name, :state_name], repo, "city must be unique within state")
+      unsafe_validate_unique(changeset, [:city_name, :state_name], repo, message: "city must be unique within state")
 
   """
   def unsafe_validate_unique(changeset, fields, repo, opts \\ []) do
@@ -1584,11 +1584,18 @@ defmodule Ecto.Changeset do
   @doc """
   Validates a change is a string or list of the given length.
 
+  Note that the length of a string is counted in graphemes. If using
+  this validation to match a character limit of a database backend,
+  it's likely that the limit ignores graphemes and limits the number
+  of unicode characters. Then consider using the `:count` option to
+  limit the number of codepoints.
+
   ## Options
 
     * `:is` - the length must be exactly this value
     * `:min` - the length must be greater than or equal to this value
     * `:max` - the length must be less than or equal to this value
+    * `:count` - what length to count for string, `:graphemes` (default) or `:codepoints`
     * `:message` - the message on failure, depending on the validation, is one of:
       * for strings:
         * "should be %{count} character(s)"
@@ -1612,10 +1619,13 @@ defmodule Ecto.Changeset do
   def validate_length(changeset, field, opts) when is_list(opts) do
     validate_change changeset, field, {:length, opts}, fn
       _, value ->
-        {type, length} = case value do
-          value when is_binary(value) ->
+        count_type = opts[:count] || :graphemes
+        {type, length} = case {value, count_type} do
+          {value, :codepoints} when is_binary(value) ->
+            {:string, codepoints_length(value, 0)}
+          {value, :graphemes} when is_binary(value) ->
             {:string, String.length(value)}
-          value when is_list(value) ->
+          {value, _} when is_list(value) ->
             {:list, length(value)}
         end
 
@@ -1627,23 +1637,27 @@ defmodule Ecto.Changeset do
     end
   end
 
+  defp codepoints_length(<<_::utf8, rest::binary>>, acc), do: codepoints_length(rest, acc + 1)
+  defp codepoints_length(<<_, rest::binary>>, acc), do: codepoints_length(rest, acc + 1)
+  defp codepoints_length(<<>>, acc), do: acc
+
   defp wrong_length(_type, value, value, _opts), do: nil
   defp wrong_length(:string, _length, value, opts), do:
-    {message(opts, "should be %{count} character(s)"), count: value, validation: :length, is: value}
+    {message(opts, "should be %{count} character(s)"), count: value, validation: :length, kind: :is}
   defp wrong_length(:list, _length, value, opts), do:
-    {message(opts, "should have %{count} item(s)"), count: value, validation: :length, is: value}
+    {message(opts, "should have %{count} item(s)"), count: value, validation: :length, kind: :is}
 
   defp too_short(_type, length, value, _opts) when length >= value, do: nil
   defp too_short(:string, _length, value, opts), do:
-    {message(opts, "should be at least %{count} character(s)"), count: value, validation: :length, min: value}
+    {message(opts, "should be at least %{count} character(s)"), count: value, validation: :length, kind: :min}
   defp too_short(:list, _length, value, opts), do:
-    {message(opts, "should have at least %{count} item(s)"), count: value, validation: :length, min: value}
+    {message(opts, "should have at least %{count} item(s)"), count: value, validation: :length, kind: :min}
 
   defp too_long(_type, length, value, _opts) when length <= value, do: nil
   defp too_long(:string, _length, value, opts), do:
-    {message(opts, "should be at most %{count} character(s)"), count: value, validation: :length, max: value}
+    {message(opts, "should be at most %{count} character(s)"), count: value, validation: :length, kind: :max}
   defp too_long(:list, _length, value, opts), do:
-    {message(opts, "should have at most %{count} item(s)"), count: value, validation: :length, max: value}
+    {message(opts, "should have at most %{count} item(s)"), count: value, validation: :length, kind: :max}
 
   @doc """
   Validates the properties of a number.
@@ -1690,14 +1704,14 @@ defmodule Ecto.Changeset do
     result = Decimal.cmp(value, Decimal.new(target_value))
     case decimal_compare(result, spec_key) do
       true  -> nil
-      false -> [{field, {message, validation: :number, number: target_value}}]
+      false -> [{field, {message, validation: :number, kind: spec_key, number: target_value}}]
     end
   end
 
-  defp validate_number(field, value, message, _spec_key, spec_function, target_value) do
+  defp validate_number(field, value, message, spec_key, spec_function, target_value) do
     case apply(spec_function, [value, target_value]) do
       true  -> nil
-      false -> [{field, {message, validation: :number, number: target_value}}]
+      false -> [{field, {message, validation: :number, kind: spec_key, number: target_value}}]
     end
   end
 
@@ -2054,7 +2068,7 @@ defmodule Ecto.Changeset do
     constraint = opts[:name] || "#{get_source(changeset)}_#{get_field_source(changeset, field)}_index"
     message    = message(opts, "has already been taken")
     match_type = Keyword.get(opts, :match, :exact)
-    add_constraint(changeset, :unique, to_string(constraint), match_type, field, {message, []})
+    add_constraint(changeset, :unique, to_string(constraint), match_type, field, {message, [constraint: :unique]})
   end
 
   @doc """
@@ -2101,7 +2115,7 @@ defmodule Ecto.Changeset do
   def foreign_key_constraint(changeset, field, opts \\ []) do
     constraint = opts[:name] || "#{get_source(changeset)}_#{get_field_source(changeset, field)}_fkey"
     message    = message(opts, "does not exist")
-    add_constraint(changeset, :foreign_key, to_string(constraint), :exact, field, {message, []})
+    add_constraint(changeset, :foreign_key, to_string(constraint), :exact, field, {message, [constraint: :foreign]})
   end
 
   @doc """
@@ -2149,7 +2163,7 @@ defmodule Ecto.Changeset do
       end
 
     message = message(opts, "does not exist")
-    add_constraint(changeset, :foreign_key, to_string(constraint), :exact, assoc, {message, []})
+    add_constraint(changeset, :foreign_key, to_string(constraint), :exact, assoc, {message, [constraint: :assoc]})
   end
 
   @doc """
@@ -2199,7 +2213,7 @@ defmodule Ecto.Changeset do
             "no_assoc_constraint can only be added to has one/many associations, got: #{inspect other}"
       end
 
-    add_constraint(changeset, :foreign_key, to_string(constraint), :exact, assoc, {message, []})
+    add_constraint(changeset, :foreign_key, to_string(constraint), :exact, assoc, {message, [constraint: :no_assoc]})
   end
 
   @doc """
@@ -2290,9 +2304,10 @@ defmodule Ecto.Changeset do
       ...> end)
       %{title: ["should be at least 3 characters"]}
 
-  Optionally function can accept three arguments: `changeset`, `field` and error tuple `{msg, opts}`.
-  It is useful whenever you want to extract validations rules from `changeset.validations`
-  to build detailed error description.
+  Optionally function can accept three arguments: `changeset`, `field`
+  and error tuple `{msg, opts}`. It is useful whenever you want to extract
+  validations rules from `changeset.validations` to build detailed error
+  description.
   """
   @spec traverse_errors(t, (error -> String.t) | (Changeset.t, atom, error -> String.t)) :: %{atom => [String.t]}
   def traverse_errors(%Changeset{errors: errors, changes: changes, types: types} = changeset, msg_func)
diff --git a/lib/ecto/changeset/relation.ex b/lib/ecto/changeset/relation.ex
index 1f1e320b..902a2cfe 100644
--- a/lib/ecto/changeset/relation.ex
+++ b/lib/ecto/changeset/relation.ex
@@ -29,7 +29,12 @@ defmodule Ecto.Changeset.Relation do
   """
   def empty?(%{cardinality: _}, %NotLoaded{}), do: true
   def empty?(%{cardinality: :many}, []), do: true
-  def empty?(%{cardinality: :many}, changes), do: Enum.all?(changes, & &1.action in [:replace, :delete])
+  def empty?(%{cardinality: :many}, changes) do
+    Enum.all?(changes, fn
+      %Changeset{action: action} when action in [:replace, :delete] -> true
+      _ -> false
+    end)
+  end
   def empty?(%{cardinality: :one}, nil), do: true
   def empty?(%{}, _), do: false
 
diff --git a/lib/ecto/date_time.ex b/lib/ecto/date_time.ex
index 929fc0e0..22f6efa9 100644
--- a/lib/ecto/date_time.ex
+++ b/lib/ecto/date_time.ex
@@ -86,6 +86,15 @@ defmodule Ecto.DateTime.Utils do
 end
 
 defmodule Ecto.Date do
+  @moduledoc """
+  A deprecated Ecto type for dates.
+
+  This type is deprecated in favour of the `:date` type.
+  """
+
+  @behaviour Ecto.Type
+  defstruct [:year, :month, :day]
+
   import Ecto.DateTime.Utils
 
   @doc """
@@ -96,13 +105,6 @@ defmodule Ecto.Date do
   """
   defdelegate compare(t1, t2), to: Ecto.DateTime.Utils
 
-  @moduledoc """
-  An Ecto type for dates.
-  """
-
-  @behaviour Ecto.Type
-  defstruct [:year, :month, :day]
-
   @doc """
   The Ecto primitive type.
   """
@@ -223,6 +225,15 @@ defmodule Ecto.Date do
 end
 
 defmodule Ecto.Time do
+  @moduledoc """
+  A deprecated Ecto type for time.
+
+  This type is deprecated in favour of the `:time` type.
+  """
+
+  @behaviour Ecto.Type
+  defstruct [:hour, :min, :sec, usec: 0]
+
   import Ecto.DateTime.Utils
 
   @doc """
@@ -233,13 +244,6 @@ defmodule Ecto.Time do
   """
   defdelegate compare(t1, t2), to: Ecto.DateTime.Utils
 
-  @moduledoc """
-  An Ecto type for time.
-  """
-
-  @behaviour Ecto.Type
-  defstruct [:hour, :min, :sec, usec: 0]
-
   @doc """
   The Ecto primitive type.
   """
@@ -384,6 +388,15 @@ defmodule Ecto.Time do
 end
 
 defmodule Ecto.DateTime do
+  @moduledoc """
+  A deprecated Ecto type that includes a date and a time.
+
+  This type is deprecated in favour of the `:naive_datetime` type.
+  """
+
+  @behaviour Ecto.Type
+  defstruct [:year, :month, :day, :hour, :min, :sec, usec: 0]
+
   import Ecto.DateTime.Utils
 
   @unix_epoch :calendar.datetime_to_gregorian_seconds {{1970, 1, 1}, {0, 0, 0}}
@@ -396,13 +409,6 @@ defmodule Ecto.DateTime do
   """
   defdelegate compare(t1, t2), to: Ecto.DateTime.Utils
 
-  @moduledoc """
-  An Ecto type that includes a date and a time.
-  """
-
-  @behaviour Ecto.Type
-  defstruct [:year, :month, :day, :hour, :min, :sec, usec: 0]
-
   @doc """
   The Ecto primitive type.
   """
diff --git a/lib/ecto/exceptions.ex b/lib/ecto/exceptions.ex
index d7804e18..362c0330 100644
--- a/lib/ecto/exceptions.ex
+++ b/lib/ecto/exceptions.ex
@@ -277,17 +277,19 @@ defmodule Ecto.ConstraintError do
           "The changeset has not defined any constraint."
         constraints ->
           "The changeset defined the following constraints:\n\n" <>
-            Enum.map_join(constraints, "\n", &"    * #{&1.type}: #{&1.constraint}")
+            Enum.map_join(constraints, "\n", &"    * #{&1.constraint} (#{&1.type}_constraint)")
       end
 
     msg = """
     constraint error when attempting to #{action} struct:
 
-        * #{type}: #{constraint}
+        * #{constraint} (#{type}_constraint)
 
     If you would like to convert this constraint into an error, please
-    call #{type}_constraint/3 in your changeset and define the proper
-    constraint name. #{constraints}
+    call `#{type}_constraint/3` in your changeset with the constraint
+    `:name` as an option.
+
+    #{constraints}
     """
 
     %__MODULE__{message: msg, type: type, constraint: constraint}
diff --git a/lib/ecto/migration.ex b/lib/ecto/migration.ex
index 126dc6cf..57447789 100644
--- a/lib/ecto/migration.ex
+++ b/lib/ecto/migration.ex
@@ -673,15 +673,14 @@ defmodule Ecto.Migration do
   """
   def add(column, type, opts \\ [])
 
-  def add(column, :datetime, opts) when is_atom(column) do
-    IO.warn "the :datetime type in migrations is deprecated, " <>
-            "please use :utc_datetime or :naive_datetime instead"
-    add(column, :naive_datetime, opts)
+  def add(column, :datetime, _opts) when is_atom(column) do
+    raise ArgumentError, "the :datetime type in migrations is not supported, " <>
+                         "please use :utc_datetime or :naive_datetime instead"
   end
 
   def add(column, type, opts) when is_atom(column) and is_list(opts) do
     if opts[:scale] && !opts[:precision] do
-      raise ArgumentError, "Column #{Atom.to_string(column)} is missing precision option"
+      raise ArgumentError, "column #{Atom.to_string(column)} is missing precision option"
     end
 
     validate_type!(type)
@@ -775,15 +774,14 @@ defmodule Ecto.Migration do
   """
   def modify(column, type, opts \\ [])
 
-  def modify(column, :datetime, opts) when is_atom(column) do
-    IO.warn "the :datetime type in migrations is deprecated, " <>
-            "please use :utc_datetime or :naive_datetime instead"
-    modify(column, :naive_datetime, opts)
+  def modify(column, :datetime, _opts) when is_atom(column) do
+    raise ArgumentError, "the :datetime type in migrations is not supported, " <>
+                         "please use :utc_datetime or :naive_datetime instead"
   end
 
   def modify(column, type, opts) when is_atom(column) and is_list(opts) do
     if opts[:scale] && !opts[:precision] do
-      raise ArgumentError, "Column #{Atom.to_string(column)} is missing precision option"
+      raise ArgumentError, "column #{Atom.to_string(column)} is missing precision option"
     end
 
     Runner.subcommand {:modify, column, type, opts}
@@ -836,6 +834,8 @@ defmodule Ecto.Migration do
   end
 
   def references(table, opts) when is_binary(table) and is_list(opts) do
+    repo_opts = Keyword.take(Runner.repo_config(:migration_primary_key, []), [:type])
+    opts = Keyword.merge(repo_opts, opts)
     reference = struct(%Reference{table: table}, opts)
 
     unless reference.on_delete in [:nothing, :delete_all, :nilify_all] do
diff --git a/lib/ecto/query.ex b/lib/ecto/query.ex
index 4c81240b..df2adf57 100644
--- a/lib/ecto/query.ex
+++ b/lib/ecto/query.ex
@@ -409,6 +409,11 @@ defmodule Ecto.Query do
       query = from Employee, order_by: [desc: :salary], limit: 10
       from e in subquery(query), select: avg(e.salary)
 
+  A prefix can be specified for a subquery, similar to standard repo operations:
+
+      query = from Employee, order_by: [desc: :salary], limit: 10
+      from e in subquery(query, prefix: "my_prefix"), select: avg(e.salary)
+
   Although subqueries are not allowed in WHERE expressions,
   most subqueries in WHERE expression can be rewritten as JOINs.
   Imagine you want to write this query:
@@ -438,12 +443,17 @@ defmodule Ecto.Query do
       end)
 
   """
-  def subquery(%Ecto.SubQuery{} = subquery),
-    do: subquery
-  def subquery(%Ecto.Query{} = query),
-    do: %Ecto.SubQuery{query: query}
-  def subquery(queryable),
-    do: %Ecto.SubQuery{query: Ecto.Queryable.to_query(queryable)}
+  def subquery(query, opts \\ []) do
+    subquery = wrap_in_subquery(query)
+    case Keyword.fetch(opts, :prefix) do
+      {:ok, prefix} when is_binary(prefix) -> put_in(subquery.query.prefix, prefix)
+      :error -> subquery
+    end
+  end
+
+  defp wrap_in_subquery(%Ecto.SubQuery{} = subquery), do: subquery
+  defp wrap_in_subquery(%Ecto.Query{} = query), do: %Ecto.SubQuery{query: query}
+  defp wrap_in_subquery(queryable), do: %Ecto.SubQuery{query: Ecto.Queryable.to_query(queryable)}
 
   @doc """
   Resets a previously set field on a query.
diff --git a/lib/ecto/query/builder.ex b/lib/ecto/query/builder.ex
index 7445270c..066bcd0a 100644
--- a/lib/ecto/query/builder.ex
+++ b/lib/ecto/query/builder.ex
@@ -104,16 +104,14 @@ defmodule Ecto.Query.Builder do
 
   def escape({:datetime_add, _, [datetime, count, interval]} = expr, type, params_acc, vars, env) do
     assert_type!(expr, type, :naive_datetime)
-    # TODO: use escape once support for Ecto.DateTime is removed
-    {datetime, params_acc} = escape_datetime(datetime, params_acc, vars, env)
+    {datetime, params_acc} = escape(datetime, :naive_datetime, params_acc, vars, env)
     {count, interval, params_acc} = escape_interval(count, interval, params_acc, vars, env)
     {{:{}, [], [:datetime_add, [], [datetime, count, interval]]}, params_acc}
   end
 
   def escape({:date_add, _, [date, count, interval]} = expr, type, params_acc, vars, env) do
     assert_type!(expr, type, :date)
-    # TODO: use escape once support for Ecto.Date is removed
-    {date, params_acc} = escape_date(date, params_acc, vars, env)
+    {date, params_acc} = escape(date, :date, params_acc, vars, env)
     {count, interval, params_acc} = escape_interval(count, interval, params_acc, vars, env)
     {{:{}, [], [:date_add, [], [date, count, interval]]}, params_acc}
   end
@@ -322,22 +320,6 @@ defmodule Ecto.Query.Builder do
     error! "fragment(...) with keywords accepts only atoms as keys, got `#{Macro.to_string(key)}`"
   end
 
-  defp escape_datetime({:^, meta, [expr]}, params_acc, vars, env) do
-    expr = quote do: Ecto.Query.Builder.cast_datetime!(unquote(expr))
-    escape({:^, meta, [expr]}, :naive_datetime, params_acc, vars, env)
-  end
-  defp escape_datetime(expr, params_acc, vars, env) do
-    escape(expr, :naive_datetime, params_acc, vars, env)
-  end
-
-  defp escape_date({:^, meta, [expr]}, params_acc, vars, env) do
-    expr = quote do: Ecto.Query.Builder.cast_date!(unquote(expr))
-    escape({:^, meta, [expr]}, :date, params_acc, vars, env)
-  end
-  defp escape_date(expr, params_acc, vars, env) do
-    escape(expr, :date, params_acc, vars, env)
-  end
-
   defp merge_fragments([h1|t1], [h2|t2]),
     do: [{:raw, h1}, {:expr, h2}|merge_fragments(t1, t2)]
   defp merge_fragments([h1], []),
@@ -487,7 +469,7 @@ defmodule Ecto.Query.Builder do
   end
 
   defp try_expansion(expr, type, params, vars, {env, fun} = env_fun) do
-    case Macro.expand(expr, env) do
+    case Macro.expand_once(expr, env) do
       ^expr ->
         error! """
         `#{Macro.to_string(expr)}` is not a valid query expression.
@@ -570,28 +552,6 @@ defmodule Ecto.Query.Builder do
   def interval!(other),
     do: error!("invalid interval: `#{inspect other}` (expected one of #{Enum.join(@interval, ", ")})")
 
-  @doc """
-  Called by escaper at runtime to cast Ecto.DateTime to :naive_datetime.
-  """
-  def cast_datetime!(%Ecto.DateTime{year: year, month: month, day: day, hour: hour, min: min, sec: sec, usec: usec}) do
-    IO.warn "Ecto.DateTime is deprecated, please use :naive_datetime and NaiveDateTime instead"
-    {:ok, value} = NaiveDateTime.new(year, month, day, hour, min, sec, {usec, 6})
-    value
-  end
-  def cast_datetime!(value),
-    do: value
-
-  @doc """
-  Called by escaper at runtime to cast Ecto.Date to :date.
-  """
-  def cast_date!(%Ecto.Date{year: year, month: month, day: day}) do
-    IO.warn "Ecto.Date is deprecated, please use :date and Date instead"
-    {:ok, value} = Date.new(year, month, day)
-    value
-  end
-  def cast_date!(value),
-    do: value
-
   @doc """
   Negates the given number.
   """
diff --git a/lib/ecto/query/builder/join.ex b/lib/ecto/query/builder/join.ex
index aa651e19..c068a8bc 100644
--- a/lib/ecto/query/builder/join.ex
+++ b/lib/ecto/query/builder/join.ex
@@ -47,6 +47,10 @@ defmodule Ecto.Query.Builder.Join do
     {:_, quote(do: Ecto.Query.subquery(unquote(expr))), nil, %{}}
   end
 
+  def escape({:subquery, _, [expr, opts]}, _vars, _env) do
+    {:_, quote(do: Ecto.Query.subquery(unquote(expr), unquote(opts))), nil, %{}}
+  end
+
   def escape({:fragment, _, [_|_]} = expr, vars, env) do
     {expr, {params, :acc}} = Builder.escape(expr, :any, {%{}, :acc}, vars, env)
     {:_, expr, nil, params}
diff --git a/lib/ecto/repo.ex b/lib/ecto/repo.ex
index 838098d3..24443795 100644
--- a/lib/ecto/repo.ex
+++ b/lib/ecto/repo.ex
@@ -64,6 +64,12 @@ defmodule Ecto.Repo do
   The schema can be of any value. The path represents the database name
   while options are simply merged in.
 
+  URL can include query parameters to override shared and adapter-specific
+  options `ssl`, `timeout`, `pool_timeout`, `pool_size`:
+
+    config :my_app, Repo,
+        url: "ecto://postgres:postgres@localhost/ecto_simple?ssl=true&pool_size=10"
+
   In case the URL needs to be dynamically configured, for example by
   reading a system environment variable, such can be done via the
   `c:init/2` repository callback:
@@ -443,12 +449,23 @@ defmodule Ecto.Repo do
 
   ## Examples
 
+      # Use a single atom to preload an association
       posts = Repo.preload posts, :comments
-      posts = Repo.preload posts, comments: :permalinks
+
+      # Use a list of atoms to preload multiple associations
       posts = Repo.preload posts, [:comments, :authors]
-      posts = Repo.preload posts, [comments: [:replies, :likes], :authors]
-      posts = Repo.preload posts, comments: from(c in Comment, order_by: c.published_at)
 
+      # Use a keyword list to preload nested associations as well
+      posts = Repo.preload posts, [comments: [:replies, :likes], authors: []]
+
+      # Use a keyword list to customize how associations are queried
+      posts = Repo.preload posts, [comments: from(c in Comment, order_by: c.published_at)]
+
+      # Use a two-element tuple for a custom query and nested association definition
+      query = from c in Comment, order_by: c.published_at
+      posts = Repo.preload posts, [comments: {query, [:replies, :likes]}]
+
+  Note: The query given to preload may also preload its own associations.
   """
   @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
@@ -544,8 +561,9 @@ defmodule Ecto.Repo do
     * `:prefix` - The prefix to run the query on (such as the schema path
       in Postgres or the database in MySQL).
     * `:on_conflict` - It may be one of `:raise` (the default), `:nothing`,
-      `:replace_all`, a keyword list of update instructions or an `Ecto.Query`
-      query for updates. See the "Upserts" section for more information.
+      `:replace_all`, `:replace_all_except_primary_key`, a keyword list of
+      update instructions or an `Ecto.Query` query for updates. See the
+      "Upserts" section for more information.
     * `:conflict_target` - Which columns to verify for conflicts. If
       none is specified, the conflict target is left up to the database
       and is usually made of primary keys and/or unique/exclusion constraints.
@@ -570,6 +588,8 @@ defmodule Ecto.Repo do
     * `:replace_all` - replace all values on the existing row with the values
       in the excluded row (the corresponding record given in the function
       parameters)
+    * `:replace_all_except_primary_key` - same as above except primary keys are
+      not replaced. This option requires a schema
     * a keyword list of update instructions - such as the one given to
       `c:update_all/3`, for example: `[set: [title: "new title"]]`
     * an `Ecto.Query` that will act as an `UPDATE` statement, such as the
@@ -629,6 +649,8 @@ defmodule Ecto.Repo do
 
       MyRepo.update_all(Post, inc: [visits: 1])
 
+      MyRepo.update_all(Post, [inc: [visits: 1]], [returning: [:visits]])
+
       from(p in Post, where: p.id < 10)
       |> MyRepo.update_all(set: [title: "New title"])
 
@@ -701,8 +723,9 @@ defmodule Ecto.Repo do
       in Postgres or the database in MySQL). This overrides the prefix set
       in the struct.
     * `:on_conflict` - It may be one of `:raise` (the default), `:nothing`,
-      `:replace_all`, a keyword list of update instructions or an `Ecto.Query`
-      query for updates. See the "Upserts" section for more information.
+      `:replace_all`, `:replace_all_except_primary_key`, a keyword list of
+      update instructions or an `Ecto.Query` query for updates. See the
+      "Upserts" section for more information.
     * `:conflict_target` - Which columns to verify for conflicts. If
       none is specified, the conflict target is left up to the database
       and is usually made of primary keys and/or unique/exclusion constraints.
@@ -729,7 +752,9 @@ defmodule Ecto.Repo do
     * `:raise` - raises if there is a conflicting primary key or unique index
     * `:nothing` - ignores the error in case of conflicts
     * `:replace_all` - replace all values on the existing row with the values
-      in the excluded row (the record given in the function parameters)
+      in the excluded row except (the record given in the function parameters)
+    * `:replace_all_except_primary_key` - same as above except primary keys are
+      not replaced
     * a keyword list of update instructions - such as the one given to
       `c:update_all/3`, for example: `[set: [title: "new title"]]`
     * an `Ecto.Query` that will act as an `UPDATE` statement, such as the
@@ -781,7 +806,7 @@ defmodule Ecto.Repo do
   database, you have three options:
 
     * Use `on_conflict: :replace_all`, although that will replace all
-      fields in the database with current ones:
+      fields in the database with current ones (except for primary keys):
 
           MyRepo.insert(%Post{title: "this is unique"},
                         on_conflict: :replace_all, conflict_target: :title)
diff --git a/lib/ecto/repo/assoc.ex b/lib/ecto/repo/assoc.ex
index 348efe29..9dbddf25 100644
--- a/lib/ecto/repo/assoc.ex
+++ b/lib/ecto/repo/assoc.ex
@@ -14,38 +14,35 @@ defmodule Ecto.Repo.Assoc do
   def query(rows, [], _sources), do: rows
 
   def query(rows, assocs, sources) do
-    # Pre-create rose tree of reflections and accumulator
-    # dicts in the same structure as the fields tree
-    refls = create_refls(0, assocs, sources)
-    accs  = create_accs(assocs)
-
-    # Replace the dict in the accumulator by a list
-    # We use it as a flag to store the substructs
-    accs = put_elem(accs, 1, [])
+    # 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, rows, sub_dicts} = Enum.reduce(rows, accs, fn row, acc ->
+    {_keys, _cache, rows, sub_dicts} = Enum.reduce(rows, accs, fn row, acc ->
       merge(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, sub_dicts, refls)|sub_structs]
+      [load_assocs(item, refls)|sub_structs]
     end
   end
 
-  defp merge([struct|sub_structs], {keys, dict, sub_dicts}, parent_key) do
+  defp merge([struct|sub_structs], {primary_keys, cache, dict, sub_dicts}, parent_key) do
     child_key =
       if struct do
-        struct
-        |> Ecto.primary_key!()
-        |> Enum.map(&elem(&1, 1))
+        for primary_key <- primary_keys do
+          case Map.get(struct, primary_key) do
+            nil -> raise Ecto.NoPrimaryKeyValueError, struct: struct
+            value -> value
+          end
+        end
       end
 
-    if child_key && nil in child_key do
-      raise Ecto.NoPrimaryKeyValueError, struct: struct
-    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.
@@ -54,10 +51,10 @@ defmodule Ecto.Repo.Assoc do
 
     # Now if we have a struct and its parent key, we store the current
     # data unless we have already processed it.
-    cache = {parent_key, child_key}
+    cache_key = {parent_key, child_key}
 
-    if struct && parent_key && not Map.get(keys, cache, false) do
-      keys = Map.put(keys, cache, true)
+    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,
@@ -69,42 +66,44 @@ defmodule Ecto.Repo.Assoc do
           Map.update(dict, parent_key, [item], &[item|&1])
         end
 
-      {{keys, dict, sub_dicts}, sub_structs}
+      {{primary_keys, cache, dict, sub_dicts}, sub_structs}
     else
-      {{keys, dict, sub_dicts}, sub_structs}
+      {{primary_keys, cache, dict, sub_dicts}, sub_structs}
     end
   end
 
-  defp load_assocs({child_key, struct}, sub_dicts, refls) do
-    Enum.reduce :lists.zip(sub_dicts, refls), struct, fn
-      {{_keys, dict, sub_dicts}, {refl, refls}}, acc ->
-        %{field: field, cardinality: cardinality} = refl
-        loaded =
-          dict
-          |> Map.get(child_key, [])
-          |> Enum.reverse()
-          |> Enum.map(&load_assocs(&1, sub_dicts, refls))
-          |> maybe_first(cardinality)
-        Map.put(acc, field, loaded)
+  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, sources) do
-    Enum.map(fields, fn {field, {child_idx, child_fields}} ->
-      {_source, schema} = elem(sources, idx)
-      {schema.__schema__(:association, field),
-       create_refls(child_idx, child_fields, sources)}
+  defp create_refls(idx, fields, dicts, sources) do
+    {_source, schema} = elem(sources, idx)
+
+    Enum.map(:lists.zip(dicts, fields), fn
+      {{_primary_keys, _cache, dict, sub_dicts}, {field, {child_idx, child_fields}}} ->
+        sub_refls = create_refls(child_idx, child_fields, sub_dicts, sources)
+        {dict, schema.__schema__(:association, field), sub_refls}
     end)
   end
 
-  defp create_accs(fields) do
-    acc = Enum.map(fields, fn {_field, {_child_idx, child_fields}} ->
-      create_accs(child_fields)
+  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)
 
-    {%{}, %{}, acc}
+    {_source, schema} = elem(sources, idx)
+    {schema.__schema__(:primary_key), %{}, initial_dict, acc}
   end
 end
diff --git a/lib/ecto/repo/queryable.ex b/lib/ecto/repo/queryable.ex
index 2337d8f8..84815612 100644
--- a/lib/ecto/repo/queryable.ex
+++ b/lib/ecto/repo/queryable.ex
@@ -347,12 +347,7 @@ defmodule Ecto.Repo.Queryable do
   end
 
   defp query_for_get_by(_repo, queryable, clauses) do
-    Enum.reduce(clauses, queryable, fn
-      ({key, nil}, query) ->
-        Query.where(query, [x], is_nil(field(x, ^key)))
-      ({key, value}, query) ->
-        Query.where(query, [x], field(x, ^key) == ^value)
-    end)
+    Query.where(queryable, [], ^Enum.to_list(clauses))
   end
 
   defp query_for_aggregate(queryable, aggregate, field) do
diff --git a/lib/ecto/repo/schema.ex b/lib/ecto/repo/schema.ex
index e615470d..ab3a69c4 100644
--- a/lib/ecto/repo/schema.ex
+++ b/lib/ecto/repo/schema.ex
@@ -46,10 +46,13 @@ defmodule Ecto.Repo.Schema do
     {on_conflict, opts} = Keyword.pop(opts, :on_conflict, :raise)
     {conflict_target, opts} = Keyword.pop(opts, :conflict_target, [])
     conflict_target = conflict_target(conflict_target, dumper)
-    on_conflict = on_conflict(on_conflict, conflict_target, metadata, counter, adapter)
 
+    keys = Map.keys(header)
+    on_conflict =
+      on_conflict(on_conflict, conflict_target, metadata, keys, counter, adapter)
     {count, rows} =
-      adapter.insert_all(repo, metadata, Map.keys(header), rows, on_conflict, return_sources, opts)
+      adapter.insert_all(repo, metadata, keys, rows, on_conflict, return_sources, opts)
+
     {count, postprocess(rows, return_fields_or_types, adapter, schema, metadata)}
   end
 
@@ -68,18 +71,14 @@ defmodule Ecto.Repo.Schema do
   end
 
   defp extract_header_and_fields(rows, schema, dumper, autogen_id, adapter) do
-    header = init_header(autogen_id)
     mapper = init_mapper(schema, dumper, adapter)
 
-    Enum.map_reduce(rows, header, fn fields, header ->
+    Enum.map_reduce(rows, %{}, fn fields, header ->
       {fields, header} = Enum.map_reduce(fields, header, mapper)
-      {autogenerate_id(autogen_id, fields, adapter), header}
+      autogenerate_id(autogen_id, fields, header, adapter)
     end)
   end
 
-  defp init_header(nil), do: %{}
-  defp init_header({_, source, _}), do: %{source => true}
-
   defp init_mapper(nil, _dumper, _adapter) do
     fn {field, value}, acc ->
       case Ecto.DataType.dump(value) do
@@ -104,16 +103,16 @@ defmodule Ecto.Repo.Schema do
     end
   end
 
-  defp autogenerate_id(nil, fields, _adapter), do: fields
-  defp autogenerate_id({key, source, type}, fields, adapter) do
+  defp autogenerate_id(nil, fields, header, _adapter), do: {fields, header}
+  defp autogenerate_id({key, source, type}, fields, header, adapter) do
     case :lists.keyfind(key, 1, fields) do
       {^key, _} ->
-        fields
+        {fields, header}
       false ->
         if value = adapter.autogenerate(type) do
-          [{source, value} | fields]
+          {[{source, value} | fields], Map.put(header, source, true)}
         else
-          fields
+          {fields, header}
         end
     end
   end
@@ -203,9 +202,11 @@ defmodule Ecto.Repo.Schema do
 
         {changes, extra, return_types, return_sources} =
           autogenerate_id(autogen_id, changeset.changes, return_types, return_sources, adapter)
-        {changes, autogen} = dump_changes!(:insert, Map.take(changes, fields), schema, extra, dumper, adapter)
-        on_conflict = on_conflict(on_conflict, conflict_target, metadata,
-                                  fn -> length(changes) end, adapter)
+        {changes, autogen} =
+          dump_changes!(:insert, Map.take(changes, fields), schema, extra, dumper, adapter)
+        on_conflict =
+          on_conflict(on_conflict, conflict_target, metadata, Keyword.keys(changes),
+                      fn -> length(changes) end, adapter)
 
         args = [repo, metadata, changes, on_conflict, return_sources, opts]
         case apply(changeset, adapter, :insert, args) do
@@ -492,7 +493,7 @@ defmodule Ecto.Repo.Schema do
   end
 
   defp on_conflict(on_conflict, conflict_target,
-                   %{source: {prefix, source}, schema: schema}, changes, adapter) do
+                   %{source: {prefix, source}, schema: schema}, header, counter_fun, adapter) do
     case on_conflict do
       :raise when conflict_target == [] ->
         {:raise, [], []}
@@ -501,20 +502,24 @@ defmodule Ecto.Repo.Schema do
       :nothing ->
         {:nothing, [], conflict_target}
       :replace_all ->
-        {:replace_all, [], conflict_target}
+        {header, [], conflict_target}
+      :replace_all_except_primary_key when is_nil(schema) ->
+        raise ArgumentError, "cannot use :replace_all_except_primary_key on operations without a schema"
+      :replace_all_except_primary_key ->
+        {header -- schema.__schema__(:primary_key), [], conflict_target}
       [_ | _] = on_conflict ->
         from = if schema, do: {source, schema}, else: source
         query = Ecto.Query.from from, update: ^on_conflict
-        on_conflict_query(query, {source, schema}, prefix, changes, adapter, conflict_target)
+        on_conflict_query(query, {source, schema}, prefix, counter_fun, adapter, conflict_target)
       %Ecto.Query{} = query ->
-        on_conflict_query(query, {source, schema}, prefix, changes, adapter, conflict_target)
+        on_conflict_query(query, {source, schema}, prefix, counter_fun, adapter, conflict_target)
       other ->
         raise ArgumentError, "unknown value for :on_conflict, got: #{inspect other}"
     end
   end
 
-  defp on_conflict_query(query, from, prefix, changes, adapter, conflict_target) do
-    counter = changes.()
+  defp on_conflict_query(query, from, prefix, counter_fun, adapter, conflict_target) do
+    counter = counter_fun.()
 
     {query, params, _} =
       %{query | prefix: prefix}
@@ -735,7 +740,7 @@ defmodule Ecto.Repo.Schema do
 
   defp dump_changes!(action, changes, schema, extra, dumper, adapter) do
     autogen = autogenerate_changes(schema, action, changes)
-    dumped  =
+    dumped =
       dump_fields!(action, schema, changes, dumper, adapter) ++
       dump_fields!(action, schema, autogen, dumper, adapter) ++
       extra
diff --git a/lib/ecto/repo/supervisor.ex b/lib/ecto/repo/supervisor.ex
index b1018d3d..8dbb75e6 100644
--- a/lib/ecto/repo/supervisor.ex
+++ b/lib/ecto/repo/supervisor.ex
@@ -3,6 +3,7 @@ defmodule Ecto.Repo.Supervisor do
   use Supervisor
 
   @defaults [timeout: 15000, pool_timeout: 5000]
+  @integer_url_query_params ["timeout", "pool_size", "pool_timeout"]
 
   @doc """
   Starts the repo supervisor.
@@ -51,8 +52,8 @@ defmodule Ecto.Repo.Supervisor do
 
     case Keyword.get(config, :url) do
       {:system, env} = url ->
-        IO.warn """
-        Using #{inspect url} for your :url configuration is deprecated.
+        raise """
+        Using #{inspect url} for your :url configuration is no longer supported.
 
         Instead define an init/2 callback in your repository that sets
         the URL accordingly from your system environment:
@@ -83,15 +84,11 @@ defmodule Ecto.Repo.Supervisor do
 
   The format must be:
 
-      "ecto://username:password@hostname:port/database"
+      "ecto://username:password@hostname:port/database?ssl=true&timeout=1000"
 
   """
   def parse_url(""), do: []
 
-  def parse_url({:system, env}) when is_binary(env) do
-    parse_url(System.get_env(env) || "")
-  end
-
   def parse_url(url) when is_binary(url) do
     info = URI.parse(url)
 
@@ -106,13 +103,44 @@ defmodule Ecto.Repo.Supervisor do
     destructure [username, password], info.userinfo && String.split(info.userinfo, ":")
     "/" <> database = info.path
 
-    opts = [username: username,
-            password: password,
-            database: database,
-            hostname: info.host,
-            port:     info.port]
+    url_opts = [username: username,
+                password: password,
+                database: database,
+                hostname: info.host,
+                port:     info.port]
+
+    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 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
 
-    for {k, v} <- opts, v, do: {k, if(is_binary(v), do: URI.decode(v), else: v)}
+      {"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 ->
+        raise Ecto.InvalidURLError, url: url, message: "unsupported query parameter `#{key}`"
+    end)
+  end
+
+  defp parse_integer!(key, value, url) do
+    case Integer.parse(value) do
+      {int, ""} ->
+        int
+      _ ->
+        raise Ecto.InvalidURLError, url: url, message: "can not parse value `#{value}` for parameter `#{key}` as an integer"
+    end
   end
 
   ## Callbacks
diff --git a/lib/ecto/schema.ex b/lib/ecto/schema.ex
index 46c7cfb8..124f8be7 100644
--- a/lib/ecto/schema.ex
+++ b/lib/ecto/schema.ex
@@ -1865,25 +1865,16 @@ defmodule Ecto.Schema do
       type == :any and not virtual? ->
         raise ArgumentError, "only virtual fields can have type :any, " <>
                              "invalid type for field #{inspect name}"
-      type == Ecto.DateTime ->
-        IO.warn "Ecto.DateTime is deprecated, plese use :naive_datetime instead"
-        type
-      type == Ecto.Date ->
-        IO.warn "Ecto.Date is deprecated, plese use :date instead"
-        type
-      type == Ecto.Time ->
-        IO.warn "Ecto.Time is deprecated, plese use :time instead"
-        type
       Ecto.Type.primitive?(type) ->
         type
-      is_atom(type) ->
-        if Code.ensure_compiled?(type) and function_exported?(type, :type, 0) do
-          type
-        else
-          raise ArgumentError, "invalid or unknown type #{inspect type} for field #{inspect name}"
-        end
+      is_atom(type) and Code.ensure_compiled?(type) and function_exported?(type, :type, 0) ->
+        type
+      is_atom(type) and function_exported?(type, :__schema__, 1) ->
+        raise ArgumentError,
+          "schema #{inspect type} is not a valid type for field #{inspect name}." <>
+          " Did you mean to use belongs_to, has_one, has_many, embeds_one, or embeds_many instead?"
       true ->
-        raise ArgumentError, "invalid type #{inspect type} for field #{inspect name}"
+        raise ArgumentError, "invalid or unknown type #{inspect type} for field #{inspect name}"
     end
   end
 
diff --git a/lib/ecto/uuid.ex b/lib/ecto/uuid.ex
index 25ad28ca..3479c1a7 100644
--- a/lib/ecto/uuid.ex
+++ b/lib/ecto/uuid.ex
@@ -5,6 +5,16 @@ defmodule Ecto.UUID do
 
   @behaviour Ecto.Type
 
+  @typedoc """
+  A hex-encoded UUID string.
+  """
+  @type t :: <<_::288>>
+
+  @typedoc """
+  A raw binary representation of a UUID.
+  """
+  @type raw :: <<_::128>>
+
   @doc """
   The Ecto type.
   """
@@ -13,6 +23,7 @@ defmodule Ecto.UUID do
   @doc """
   Casts to UUID.
   """
+  @spec cast(t | raw | any) :: {:ok, t} | :error
   def cast(<< a1, a2, a3, a4, a5, a6, a7, a8, ?-,
               b1, b2, b3, b4, ?-,
               c1, c2, c3, c4, ?-,
@@ -37,6 +48,7 @@ defmodule Ecto.UUID do
   @doc """
   Same as `cast/1` but raises `Ecto.CastError` on invalid arguments.
   """
+  @spec cast!(t | raw | any) :: t | no_return
   def cast!(value) do
     case cast(value) do
       {:ok, uuid} -> uuid
@@ -73,6 +85,7 @@ defmodule Ecto.UUID do
   @doc """
   Converts a string representing a UUID into a binary.
   """
+  @spec dump(t | any) :: {:ok, raw} | :error
   def dump(<< a1, a2, a3, a4, a5, a6, a7, a8, ?-,
               b1, b2, b3, b4, ?-,
               c1, c2, c3, c4, ?-,
@@ -125,6 +138,7 @@ defmodule Ecto.UUID do
   @doc """
   Converts a binary UUID into a string.
   """
+  @spec load(raw | any) :: {:ok, t} | :error
   def load(<<_::128>> = uuid) do
     encode(uuid)
   end
@@ -140,7 +154,8 @@ defmodule Ecto.UUID do
   @doc """
   Generates a version 4 (random) UUID.
   """
-  def generate do
+  @spec generate() :: t
+  def generate() do
     {:ok, uuid} = encode(bingenerate())
     uuid
   end
@@ -148,7 +163,8 @@ defmodule Ecto.UUID do
   @doc """
   Generates a version 4 (random) UUID in the binary format.
   """
-  def bingenerate do
+  @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


Test patch:
diff --git a/integration_test/cases/repo.exs b/integration_test/cases/repo.exs
index d743a9d4..20ac9f12 100644
--- a/integration_test/cases/repo.exs
+++ b/integration_test/cases/repo.exs
@@ -271,7 +271,7 @@ defmodule Ecto.Integration.RepoTest do
         |> TestRepo.insert()
       end
 
-    assert exception.message =~ "unique: posts_uuid_index"
+    assert exception.message =~ "posts_uuid_index (unique_constraint)"
     assert exception.message =~ "The changeset has not defined any constraint."
 
     message = ~r/constraint error when attempting to insert struct/
@@ -282,13 +282,13 @@ defmodule Ecto.Integration.RepoTest do
         |> TestRepo.insert()
       end
 
-    assert exception.message =~ "unique: posts_email_changeset"
+    assert exception.message =~ "posts_email_changeset (unique_constraint)"
 
     {:error, changeset} =
       changeset
       |> Ecto.Changeset.unique_constraint(:uuid)
       |> TestRepo.insert()
-    assert changeset.errors == [uuid: {"has already been taken", []}]
+    assert changeset.errors == [uuid: {"has already been taken", [constraint: :unique]}]
     assert changeset.data.__meta__.state == :built
   end
 
@@ -305,7 +305,7 @@ defmodule Ecto.Integration.RepoTest do
       }
 
     [_, p2, _] = changeset.changes.posts
-    assert p2.errors == [uuid: {"has already been taken", []}]
+    assert p2.errors == [uuid: {"has already been taken", [constraint: :unique]}]
   end
 
   @tag :id_type
@@ -318,7 +318,7 @@ defmodule Ecto.Integration.RepoTest do
       changeset
       |> Ecto.Changeset.unique_constraint(:uuid)
       |> TestRepo.insert()
-    assert changeset.errors == [uuid: {"has already been taken", []}]
+    assert changeset.errors == [uuid: {"has already been taken", [constraint: :unique]}]
     assert changeset.data.__meta__.state == :built
   end
 
@@ -408,7 +408,7 @@ defmodule Ecto.Integration.RepoTest do
         |> TestRepo.insert()
       end
 
-    assert exception.message =~ "foreign_key: comments_post_id_fkey"
+    assert exception.message =~ "comments_post_id_fkey (foreign_key_constraint)"
     assert exception.message =~ "The changeset has not defined any constraint."
 
     message = ~r/constraint error when attempting to insert struct/
@@ -419,13 +419,13 @@ defmodule Ecto.Integration.RepoTest do
         |> TestRepo.insert()
       end
 
-    assert exception.message =~ "foreign_key: comments_post_id_other"
+    assert exception.message =~ "comments_post_id_other (foreign_key_constraint)"
 
     {:error, changeset} =
       changeset
       |> Ecto.Changeset.foreign_key_constraint(:post_id)
       |> TestRepo.insert()
-    assert changeset.errors == [post_id: {"does not exist", []}]
+    assert changeset.errors == [post_id: {"does not exist", [constraint: :foreign]}]
   end
 
   @tag :foreign_key_constraint
@@ -438,7 +438,7 @@ defmodule Ecto.Integration.RepoTest do
         |> TestRepo.insert()
       end
 
-    assert exception.message =~ "foreign_key: comments_post_id_fkey"
+    assert exception.message =~ "comments_post_id_fkey (foreign_key_constraint)"
     assert exception.message =~ "The changeset has not defined any constraint."
 
     message = ~r/constraint error when attempting to insert struct/
@@ -449,13 +449,13 @@ defmodule Ecto.Integration.RepoTest do
         |> TestRepo.insert()
       end
 
-    assert exception.message =~ "foreign_key: comments_post_id_other"
+    assert exception.message =~ "comments_post_id_other (foreign_key_constraint)"
 
     {:error, changeset} =
       changeset
       |> Ecto.Changeset.assoc_constraint(:post)
       |> TestRepo.insert()
-    assert changeset.errors == [post: {"does not exist", []}]
+    assert changeset.errors == [post: {"does not exist", [constraint: :assoc]}]
   end
 
   @tag :foreign_key_constraint
@@ -468,7 +468,7 @@ defmodule Ecto.Integration.RepoTest do
         TestRepo.delete!(user)
       end
 
-    assert exception.message =~ "foreign_key: permalinks_user_id_fkey"
+    assert exception.message =~ "permalinks_user_id_fkey (foreign_key_constraint)"
     assert exception.message =~ "The changeset has not defined any constraint."
   end
 
@@ -486,7 +486,7 @@ defmodule Ecto.Integration.RepoTest do
         |> TestRepo.delete()
       end
 
-    assert exception.message =~ "foreign_key: permalinks_user_id_pther"
+    assert exception.message =~ "permalinks_user_id_pther (foreign_key_constraint)"
   end
 
   @tag :foreign_key_constraint
@@ -499,7 +499,7 @@ defmodule Ecto.Integration.RepoTest do
       |> Ecto.Changeset.change
       |> Ecto.Changeset.no_assoc_constraint(:permalink)
       |> TestRepo.delete()
-    assert changeset.errors == [permalink: {"is still associated with this entry", []}]
+    assert changeset.errors == [permalink: {"is still associated with this entry", [constraint: :no_assoc]}]
   end
 
   test "insert and update with failing child foreign key" do
@@ -601,7 +601,6 @@ defmodule Ecto.Integration.RepoTest do
   test "get_by(!)" do
     post1 = TestRepo.insert!(%Post{title: "1", text: "hai"})
     post2 = TestRepo.insert!(%Post{title: "2", text: "hello"})
-    post3 = TestRepo.insert!(%Post{title: "3", text: nil})
 
     assert post1 == TestRepo.get_by(Post, id: post1.id)
     assert post1 == TestRepo.get_by(Post, text: post1.text)
@@ -609,13 +608,11 @@ defmodule Ecto.Integration.RepoTest do
     assert post2 == TestRepo.get_by(Post, id: to_string(post2.id)) # With casting
     assert nil   == TestRepo.get_by(Post, text: "hey")
     assert nil   == TestRepo.get_by(Post, id: post2.id, text: "hey")
-    assert post3 == TestRepo.get_by(Post, text: nil)
 
     assert post1 == TestRepo.get_by!(Post, id: post1.id)
     assert post1 == TestRepo.get_by!(Post, text: post1.text)
     assert post1 == TestRepo.get_by!(Post, id: post1.id, text: post1.text)
     assert post2 == TestRepo.get_by!(Post, id: to_string(post2.id)) # With casting
-    assert post3 == TestRepo.get_by!(Post, text: nil)
 
     assert post1 == TestRepo.get_by!(Post, %{id: post1.id})
 
@@ -1039,7 +1036,7 @@ defmodule Ecto.Integration.RepoTest do
       assert p3 == %{id: pid3}
     end
 
-    test "take with assocs" do
+    test "take with preload assocs" do
       %{id: pid} = TestRepo.insert!(%Post{title: "post"})
       TestRepo.insert!(%Comment{post_id: pid, text: "comment"})
       fields = [:id, :title, comments: [:text, :post_id]]
@@ -1056,13 +1053,7 @@ defmodule Ecto.Integration.RepoTest do
       assert p == %{id: pid, title: "post", comments: [%{text: "comment", post_id: pid}]}
     end
 
-    test "take with single nil column" do
-      %Post{} = TestRepo.insert!(%Post{title: "1", counter: nil})
-      assert %{counter: nil} =
-             TestRepo.one(from p in Post, where: p.title == "1", select: [:counter])
-    end
-
-    test "take with nil assoc" do
+    test "take with nil preload assoc" do
       %{id: cid} = TestRepo.insert!(%Comment{text: "comment"})
       fields = [:id, :text, post: [:title]]
 
@@ -1076,6 +1067,30 @@ defmodule Ecto.Integration.RepoTest do
       assert c == %{id: cid, text: "comment", post: nil}
     end
 
+    test "take with join assocs" do
+      %{id: pid} = TestRepo.insert!(%Post{title: "post"})
+      %{id: cid} = TestRepo.insert!(%Comment{post_id: pid, text: "comment"})
+      fields = [:id, :title, comments: [:text, :post_id, :id]]
+      query = from p in Post, where: p.id == ^pid, join: c in assoc(p, :comments), preload: [comments: c]
+
+      p = TestRepo.one(from q in query, select: ^fields)
+      assert %Post{title: "post"} = p
+      assert [%Comment{text: "comment"}] = p.comments
+
+      p = TestRepo.one(from q in query, select: struct(q, ^fields))
+      assert %Post{title: "post"} = p
+      assert [%Comment{text: "comment"}] = p.comments
+
+      p = TestRepo.one(from q in query, select: map(q, ^fields))
+      assert p == %{id: pid, title: "post", comments: [%{text: "comment", post_id: pid, id: cid}]}
+    end
+
+    test "take with single nil column" do
+      %Post{} = TestRepo.insert!(%Post{title: "1", counter: nil})
+      assert %{counter: nil} =
+             TestRepo.one(from p in Post, where: p.title == "1", select: [:counter])
+    end
+
     test "field source" do
       TestRepo.insert!(%Permalink{url: "url"})
       assert ["url"] = Permalink |> select([p], p.url) |> TestRepo.all()
@@ -1185,7 +1200,7 @@ defmodule Ecto.Integration.RepoTest do
     end
 
     test "on conflict ignore" do
-      post = %Post{title: "first", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"}
+      post = %Post{title: "first", uuid: Ecto.UUID.generate()}
       {:ok, inserted} = TestRepo.insert(post, on_conflict: :nothing)
       assert inserted.id
       assert inserted.__meta__.state == :loaded
@@ -1198,7 +1213,7 @@ defmodule Ecto.Integration.RepoTest do
     @tag :with_conflict_target
     test "on conflict and associations" do
       on_conflict = [set: [title: "second"]]
-      post = %Post{uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e",
+      post = %Post{uuid: Ecto.UUID.generate(),
                    title: "first", comments: [%Comment{}]}
       {:ok, inserted} = TestRepo.insert(post, on_conflict: on_conflict, conflict_target: [:uuid])
       assert inserted.id
@@ -1215,7 +1230,7 @@ defmodule Ecto.Integration.RepoTest do
 
     @tag :with_conflict_target
     test "on conflict ignore and conflict target" do
-      post = %Post{title: "first", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"}
+      post = %Post{title: "first", uuid: Ecto.UUID.generate()}
       {:ok, inserted} = TestRepo.insert(post, on_conflict: :nothing, conflict_target: [:uuid])
       assert inserted.id
 
@@ -1230,7 +1245,7 @@ defmodule Ecto.Integration.RepoTest do
     @tag :without_conflict_target
     test "on conflict keyword list" do
       on_conflict = [set: [title: "second"]]
-      post = %Post{title: "first", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"}
+      post = %Post{title: "first", uuid: Ecto.UUID.generate()}
       {:ok, inserted} = TestRepo.insert(post, on_conflict: on_conflict)
       assert inserted.id
 
@@ -1243,7 +1258,7 @@ defmodule Ecto.Integration.RepoTest do
     @tag :with_conflict_target
     test "on conflict keyword list and conflict target" do
       on_conflict = [set: [title: "second"]]
-      post = %Post{title: "first", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"}
+      post = %Post{title: "first", uuid: Ecto.UUID.generate()}
       {:ok, inserted} = TestRepo.insert(post, on_conflict: on_conflict, conflict_target: [:uuid])
       assert inserted.id
 
@@ -1305,7 +1320,7 @@ defmodule Ecto.Integration.RepoTest do
     @tag :returning
     @tag :with_conflict_target
     test "on conflict ignore and returning" do
-      post = %Post{title: "first", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"}
+      post = %Post{title: "first", uuid: Ecto.UUID.generate()}
       {:ok, inserted} = TestRepo.insert(post, on_conflict: :nothing, conflict_target: [:uuid])
       assert inserted.id
 
@@ -1316,7 +1331,7 @@ defmodule Ecto.Integration.RepoTest do
     @tag :without_conflict_target
     test "on conflict query" do
       on_conflict = from Post, update: [set: [title: "second"]]
-      post = %Post{title: "first", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"}
+      post = %Post{title: "first", uuid: Ecto.UUID.generate()}
       {:ok, inserted} = TestRepo.insert(post, on_conflict: on_conflict)
       assert inserted.id
 
@@ -1329,7 +1344,7 @@ defmodule Ecto.Integration.RepoTest do
     @tag :with_conflict_target
     test "on conflict query and conflict target" do
       on_conflict = from Post, update: [set: [title: "second"]]
-      post = %Post{title: "first", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"}
+      post = %Post{title: "first", uuid: Ecto.UUID.generate()}
       {:ok, inserted} = TestRepo.insert(post, on_conflict: on_conflict, conflict_target: [:uuid])
       assert inserted.id
 
@@ -1344,41 +1359,35 @@ defmodule Ecto.Integration.RepoTest do
 
     @tag :without_conflict_target
     test "on conflict replace_all" do
-      post = %Post{title: "first", text: "text", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"}
+      post = %Post{title: "first", text: "text", uuid: Ecto.UUID.generate()}
       {:ok, inserted} = TestRepo.insert(post, on_conflict: :replace_all)
       assert inserted.id
 
-      # Error on non-conflict target
-      post = %Post{id: inserted.id, title: "updated",
-                   text: "updated", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"}
-
-      # Error on conflict target
+      post = %Post{title: "updated", text: "updated", uuid: post.uuid}
       post = TestRepo.insert!(post, on_conflict: :replace_all)
+      assert post.id == inserted.id
       assert post.title == "updated"
       assert post.text == "updated"
 
-      assert TestRepo.all(from p in Post, select: p.title) == ["updated"]
-      assert TestRepo.all(from p in Post, select: p.text) == ["updated"]
+      assert TestRepo.all(from p in Post, select: {p.id, p.title, p.text}) ==
+             [{inserted.id, "updated", "updated"}]
       assert TestRepo.all(from p in Post, select: count(p.id)) == [1]
     end
 
     @tag :with_conflict_target
     test "on conflict replace_all and conflict target" do
-      post = %Post{title: "first", text: "text", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"}
-      {:ok, inserted} = TestRepo.insert(post, on_conflict: :replace_all, conflict_target: :id)
+      post = %Post{title: "first", text: "text", uuid: Ecto.UUID.generate()}
+      {:ok, inserted} = TestRepo.insert(post, on_conflict: :replace_all, conflict_target: :uuid)
       assert inserted.id
 
-      # Error on non-conflict target
-      post = %Post{id: inserted.id, title: "updated",
-                   text: "updated", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"}
-
-      # Error on conflict target
-      post = TestRepo.insert!(post, on_conflict: :replace_all, conflict_target: :id)
+      post = %Post{title: "updated", text: "updated", uuid: post.uuid}
+      post = TestRepo.insert!(post, on_conflict: :replace_all, conflict_target: :uuid)
+      assert post.id == inserted.id
       assert post.title == "updated"
       assert post.text == "updated"
 
-      assert TestRepo.all(from p in Post, select: p.title) == ["updated"]
-      assert TestRepo.all(from p in Post, select: p.text) == ["updated"]
+      assert TestRepo.all(from p in Post, select: {p.id, p.title, p.text}) ==
+             [{inserted.id, "updated", "updated"}]
       assert TestRepo.all(from p in Post, select: count(p.id)) == [1]
     end
   end
@@ -1387,13 +1396,13 @@ defmodule Ecto.Integration.RepoTest do
     @describetag :upsert_all
 
     test "on conflict raise" do
-      post = [title: "first", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"]
+      post = [title: "first", uuid: Ecto.UUID.generate()]
       {1, nil} = TestRepo.insert_all(Post, [post], on_conflict: :raise)
       assert catch_error(TestRepo.insert_all(Post, [post], on_conflict: :raise))
     end
 
     test "on conflict ignore" do
-      post = [title: "first", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"]
+      post = [title: "first", uuid: Ecto.UUID.generate()]
       assert TestRepo.insert_all(Post, [post], on_conflict: :nothing) == {1, nil}
 
       # PG returns 0, MySQL returns 1
@@ -1405,7 +1414,7 @@ defmodule Ecto.Integration.RepoTest do
 
     @tag :with_conflict_target
     test "on conflict ignore and conflict target" do
-      post = [title: "first", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"]
+      post = [title: "first", uuid: Ecto.UUID.generate()]
       assert TestRepo.insert_all(Post, [post], on_conflict: :nothing, conflict_target: [:uuid]) ==
              {1, nil}
 
@@ -1420,7 +1429,7 @@ defmodule Ecto.Integration.RepoTest do
     @tag :with_conflict_target
     test "on conflict keyword list and conflict target" do
       on_conflict = [set: [title: "second"]]
-      post = [title: "first", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"]
+      post = [title: "first", uuid: Ecto.UUID.generate()]
       {1, nil} = TestRepo.insert_all(Post, [post], on_conflict: on_conflict, conflict_target: [:uuid])
 
       # Error on non-conflict target
@@ -1450,7 +1459,7 @@ defmodule Ecto.Integration.RepoTest do
     @tag :with_conflict_target
     test "on conflict query and conflict target" do
       on_conflict = from Post, update: [set: [title: "second"]]
-      post = [title: "first", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"]
+      post = [title: "first", uuid: Ecto.UUID.generate()]
       assert TestRepo.insert_all(Post, [post], on_conflict: on_conflict, conflict_target: [:uuid]) ==
              {1, nil}
 
@@ -1467,7 +1476,7 @@ defmodule Ecto.Integration.RepoTest do
     @tag :with_conflict_target
     test "on conflict query and conflict target and returning" do
       on_conflict = from Post, update: [set: [title: "second"]]
-      post = [title: "first", uuid: "6fa459ea-ee8a-3ca4-894e-db77e160355e"]
+      post = [title: "first", uuid: Ecto.UUID.generate()]
       {1, [%{id: id}]} = TestRepo.insert_all(Post, [post], on_conflict: on_conflict,
                                             conflict_target: [:uuid], returning: [:id])
 
@@ -1484,7 +1493,7 @@ defmodule Ecto.Integration.RepoTest do
     @tag :with_conflict_target
     test "source (without an ecto schema) on conflict query and conflict target" do
       on_conflict = [set: [title: "second"]]
-      {:ok, uuid} = Ecto.UUID.dump("6fa459ea-ee8a-3ca4-894e-db77e160355e")
+      {:ok, uuid} = Ecto.UUID.dump(Ecto.UUID.generate())
       post = [title: "first", uuid: uuid]
       assert TestRepo.insert_all("posts", [post], on_conflict: on_conflict, conflict_target: [:uuid]) ==
              {1, nil}
@@ -1500,61 +1509,122 @@ defmodule Ecto.Integration.RepoTest do
 
     @tag :without_conflict_target
     test "on conflict replace_all" do
-      post_first = %Post{title: "first", public: true}
-      post_second = %Post{title: "second", public: false}
+      post_first = %Post{title: "first", public: true, uuid: Ecto.UUID.generate}
+      post_second = %Post{title: "second", public: false, uuid: Ecto.UUID.generate}
 
-      {:ok, inserted_first} = TestRepo.insert(post_first, on_conflict: :replace_all)
-      {:ok, inserted_second} = TestRepo.insert(post_second, on_conflict: :replace_all)
+      {:ok, post_first} = TestRepo.insert(post_first, on_conflict: :replace_all)
+      {:ok, post_second} = TestRepo.insert(post_second, on_conflict: :replace_all)
 
-      assert inserted_first.id
-      assert inserted_second.id
+      assert post_first.id
+      assert post_second.id
       assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
 
-      # multiple record change value
-      changes = [%{id: inserted_first.id, title: "first_updated", text: "first_updated"},
-                 %{id: inserted_second.id, title: "second_updated", text: "second_updated"}]
+      # Multiple record change value: note IDS are also replaced
+      changes = [%{id: post_first.id + 2, title: "first_updated",
+                   text: "first_updated", uuid: post_first.uuid},
+                 %{id: post_second.id + 2, title: "second_updated",
+                   text: "second_updated", uuid: post_second.uuid}]
 
       TestRepo.insert_all(Post, changes, on_conflict: :replace_all)
-
       assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
 
-      updated_first =  TestRepo.get(Post, inserted_first.id)
+      updated_first = TestRepo.get(Post, post_first.id + 2)
       assert updated_first.title == "first_updated"
       assert updated_first.text == "first_updated"
 
-      updated_first =  TestRepo.get(Post, inserted_second.id)
-      assert updated_first.title == "second_updated"
-      assert updated_first.text == "second_updated"
+      updated_second = TestRepo.get(Post, post_second.id + 2)
+      assert updated_second.title == "second_updated"
+      assert updated_second.text == "second_updated"
     end
 
     @tag :with_conflict_target
     test "on conflict replace_all and conflict_target" do
-      post_first = %Post{title: "first", public: true}
-      post_second = %Post{title: "second", public: false}
+      post_first = %Post{title: "first", public: true, uuid: Ecto.UUID.generate()}
+      post_second = %Post{title: "second", public: false, uuid: Ecto.UUID.generate()}
 
-      {:ok, inserted_first} = TestRepo.insert(post_first, on_conflict: :replace_all, conflict_target: :id)
-      {:ok, inserted_second} = TestRepo.insert(post_second, on_conflict: :replace_all, conflict_target: :id)
+      {:ok, post_first} = TestRepo.insert(post_first, on_conflict: :replace_all, conflict_target: :uuid)
+      {:ok, post_second} = TestRepo.insert(post_second, on_conflict: :replace_all, conflict_target: :uuid)
 
-      assert inserted_first.id
-      assert inserted_second.id
+      assert post_first.id
+      assert post_second.id
       assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
 
-      # multiple record change value
-      changes = [%{id: inserted_first.id, title: "first_updated", text: "first_updated"},
-                 %{id: inserted_second.id, title: "second_updated", text: "second_updated"}]
+      # Multiple record change value: note IDS are also replaced
+      changes = [%{id: post_first.id + 2, title: "first_updated",
+                   text: "first_updated", uuid: post_first.uuid},
+                 %{id: post_second.id + 2, title: "second_updated",
+                   text: "second_updated", uuid: post_second.uuid}]
+
+      TestRepo.insert_all(Post, changes, on_conflict: :replace_all, conflict_target: :uuid)
+      assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
+
+      updated_first = TestRepo.get(Post, post_first.id + 2)
+      assert updated_first.title == "first_updated"
+      assert updated_first.text == "first_updated"
+
+      updated_second = TestRepo.get(Post, post_second.id + 2)
+      assert updated_second.title == "second_updated"
+      assert updated_second.text == "second_updated"
+    end
+
+    @tag :without_conflict_target
+    test "on conflict replace_all_except_primary_key" do
+      post_first = %Post{title: "first", public: true, uuid: Ecto.UUID.generate}
+      post_second = %Post{title: "second", public: false, uuid: Ecto.UUID.generate}
+
+      {:ok, post_first} = TestRepo.insert(post_first, on_conflict: :replace_all_except_primary_key)
+      {:ok, post_second} = TestRepo.insert(post_second, on_conflict: :replace_all_except_primary_key)
+
+      assert post_first.id
+      assert post_second.id
+      assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
 
-      TestRepo.insert_all(Post, changes, on_conflict: :replace_all, conflict_target: :id)
+      # Multiple record change value: note IDS are not replaced
+      changes = [%{id: post_first.id + 2, title: "first_updated",
+                   text: "first_updated", uuid: post_first.uuid},
+                 %{id: post_second.id + 2, title: "second_updated",
+                   text: "second_updated", uuid: post_second.uuid}]
 
+      TestRepo.insert_all(Post, changes, on_conflict: :replace_all_except_primary_key)
       assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
 
-      updated_first =  TestRepo.get(Post, inserted_first.id)
+      updated_first = TestRepo.get(Post, post_first.id)
       assert updated_first.title == "first_updated"
       assert updated_first.text == "first_updated"
 
+      updated_second = TestRepo.get(Post, post_second.id)
+      assert updated_second.title == "second_updated"
+      assert updated_second.text == "second_updated"
+    end
+
+    @tag :with_conflict_target
+    test "on conflict replace_all_except_primary_key and conflict_target" do
+      post_first = %Post{title: "first", public: true, uuid: Ecto.UUID.generate()}
+      post_second = %Post{title: "second", public: false, uuid: Ecto.UUID.generate()}
+
+      {:ok, post_first} = TestRepo.insert(post_first, on_conflict: :replace_all_except_primary_key, conflict_target: :uuid)
+      {:ok, post_second} = TestRepo.insert(post_second, on_conflict: :replace_all_except_primary_key, conflict_target: :uuid)
+
+      assert post_first.id
+      assert post_second.id
+      assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
+
+      # Multiple record change value: note IDS are not replaced
+      changes = [%{id: post_first.id + 2, title: "first_updated",
+                   text: "first_updated", uuid: post_first.uuid},
+                 %{id: post_second.id + 2, title: "second_updated",
+                   text: "second_updated", uuid: post_second.uuid}]
+
+      TestRepo.insert_all(Post, changes, on_conflict: :replace_all_except_primary_key, conflict_target: :uuid)
+      assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
+
+      updated_first = TestRepo.get(Post, post_first.id)
+      assert updated_first.title == "first_updated"
+      assert updated_first.text == "first_updated"
 
-      updated_first =  TestRepo.get(Post, inserted_second.id)
-      assert updated_first.title == "second_updated"
-      assert updated_first.text == "second_updated"
+      updated_second = TestRepo.get(Post, post_second.id)
+      assert updated_second.title == "second_updated"
+      assert updated_second.text == "second_updated"
     end
   end
 end
diff --git a/integration_test/pg/constraints_test.exs b/integration_test/pg/constraints_test.exs
index 80b268c2..6d440e6f 100644
--- a/integration_test/pg/constraints_test.exs
+++ b/integration_test/pg/constraints_test.exs
@@ -49,7 +49,7 @@ defmodule Ecto.Integration.ConstraintsTest do
       assert_raise Ecto.ConstraintError, ~r/constraint error when attempting to insert struct/, fn ->
         PoolRepo.insert(overlapping_changeset)
       end
-    assert exception.message =~ "exclude: cannot_overlap"
+    assert exception.message =~ "cannot_overlap (exclude_constraint)"
     assert exception.message =~ "The changeset has not defined any constraint."
 
     message = ~r/constraint error when attempting to insert struct/
@@ -59,7 +59,7 @@ defmodule Ecto.Integration.ConstraintsTest do
         |> Ecto.Changeset.exclusion_constraint(:from)
         |> PoolRepo.insert()
       end
-    assert exception.message =~ "exclude: cannot_overlap"
+    assert exception.message =~ "cannot_overlap (exclude_constraint)"
 
     {:error, changeset} =
       overlapping_changeset
@@ -77,7 +77,7 @@ defmodule Ecto.Integration.ConstraintsTest do
         PoolRepo.insert(changeset)
       end
 
-    assert exception.message =~ "check: positive_price"
+    assert exception.message =~ "positive_price (check_constraint)"
     assert exception.message =~ "The changeset has not defined any constraint."
 
     # When the changeset does expect the db error, but doesn't give a custom message
diff --git a/test/ecto/adapters/mysql_test.exs b/test/ecto/adapters/mysql_test.exs
index 621d4f19..4d58a9d9 100644
--- a/test/ecto/adapters/mysql_test.exs
+++ b/test/ecto/adapters/mysql_test.exs
@@ -7,6 +7,7 @@ defmodule Ecto.Adapters.MySQLTest do
 
   alias Ecto.Queryable
   alias Ecto.Adapters.MySQL.Connection, as: SQL
+  alias Ecto.Migration.Reference
 
   defmodule Schema do
     use Ecto.Schema
@@ -75,8 +76,11 @@ defmodule Ecto.Adapters.MySQLTest do
     query = "posts" |> select([r], r.x) |> normalize
     assert all(query) == ~s{SELECT p0.`x` FROM `posts` AS p0}
 
-    query = "posts" |> select([:x]) |> normalize
-    assert all(query) == ~s{SELECT p0.`x` FROM `posts` AS p0}
+    query = "Posts" |> select([:x]) |> normalize
+    assert all(query) == ~s{SELECT P0.`x` FROM `Posts` AS P0}
+
+    query = "0posts" |> select([:x]) |> normalize
+    assert all(query) == ~s{SELECT t0.`x` FROM `0posts` AS t0}
 
     assert_raise Ecto.QueryError, ~r"MySQL does not support selecting all fields from `posts` without a schema", fn ->
       all from(p in "posts", select: p) |> normalize()
@@ -523,7 +527,7 @@ defmodule Ecto.Adapters.MySQLTest do
     query = insert(nil, "schema", [:x, :y], [[:x, :y]], {update, [], []}, [])
     assert query == ~s{INSERT INTO `schema` (`x`,`y`) VALUES (?,?) ON DUPLICATE KEY UPDATE `z` = ?}
 
-    query = insert(nil, "schema", [:x, :y], [[:x, :y]], {:replace_all, [], []}, [])
+    query = insert(nil, "schema", [:x, :y], [[:x, :y]], {[:x, :y], [], []}, [])
     assert query == ~s{INSERT INTO `schema` (`x`,`y`) VALUES (?,?) ON DUPLICATE KEY UPDATE `x` = VALUES(`x`),`y` = VALUES(`y`)}
   end
 
@@ -546,7 +550,7 @@ defmodule Ecto.Adapters.MySQLTest do
   # DDL
 
   import Ecto.Migration, only: [table: 1, table: 2, index: 2, index: 3,
-                                references: 1, references: 2, constraint: 3]
+                                constraint: 3]
 
   test "executing a string during migration" do
     assert execute_ddl("example") == ["example"]
@@ -581,7 +585,7 @@ defmodule Ecto.Adapters.MySQLTest do
 
   test "create table with prefix" do
     create = {:create, table(:posts, prefix: :foo),
-               [{:add, :category_0, references(:categories, prefix: :foo), []}]}
+               [{:add, :category_0, %Reference{table: :categories}, []}]}
 
     assert execute_ddl(create) == ["""
     CREATE TABLE `foo`.`posts` (`category_0` BIGINT UNSIGNED,
@@ -599,11 +603,11 @@ defmodule Ecto.Adapters.MySQLTest do
   test "create table with references" do
     create = {:create, table(:posts),
                [{:add, :id, :serial, [primary_key: true]},
-                {:add, :category_0, references(:categories), []},
-                {:add, :category_1, references(:categories, name: :foo_bar), []},
-                {:add, :category_2, references(:categories, on_delete: :nothing), []},
-                {:add, :category_3, references(:categories, on_delete: :delete_all), [null: false]},
-                {:add, :category_4, references(:categories, on_delete: :nilify_all), []}]}
+                {:add, :category_0, %Reference{table: :categories}, []},
+                {:add, :category_1, %Reference{table: :categories, name: :foo_bar}, []},
+                {:add, :category_2, %Reference{table: :categories, on_delete: :nothing}, []},
+                {:add, :category_3, %Reference{table: :categories, on_delete: :delete_all}, [null: false]},
+                {:add, :category_4, %Reference{table: :categories, on_delete: :nilify_all}, []}]}
 
     assert execute_ddl(create) == ["""
     CREATE TABLE `posts` (`id` bigint unsigned not null auto_increment,
@@ -688,10 +692,10 @@ defmodule Ecto.Adapters.MySQLTest do
   test "alter table" do
     alter = {:alter, table(:posts),
                [{:add, :title, :string, [default: "Untitled", size: 100, null: false]},
-                {:add, :author_id, references(:author), []},
+                {:add, :author_id, %Reference{table: :author}, []},
                 {:modify, :price, :numeric, [precision: 8, scale: 2, null: true]},
                 {:modify, :cost, :integer, [null: false, default: nil]},
-                {:modify, :permalink_id, references(:permalinks), null: false},
+                {:modify, :permalink_id, %Reference{table: :permalinks}, null: false},
                 {:remove, :summary}]}
 
     assert execute_ddl(alter) == ["""
@@ -707,8 +711,8 @@ defmodule Ecto.Adapters.MySQLTest do
 
   test "alter table with prefix" do
     alter = {:alter, table(:posts, prefix: :foo),
-               [{:add, :author_id, references(:author), []},
-                {:modify, :permalink_id, references(:permalinks), null: false}]}
+               [{:add, :author_id, %Reference{table: :author}, []},
+                {:modify, :permalink_id, %Reference{table: :permalinks}, null: false}]}
 
     assert execute_ddl(alter) == ["""
     ALTER TABLE `foo`.`posts` ADD `author_id` BIGINT UNSIGNED,
diff --git a/test/ecto/adapters/postgres_test.exs b/test/ecto/adapters/postgres_test.exs
index b438a990..826ae829 100644
--- a/test/ecto/adapters/postgres_test.exs
+++ b/test/ecto/adapters/postgres_test.exs
@@ -78,8 +78,11 @@ defmodule Ecto.Adapters.PostgresTest do
     query = "posts" |> select([r], r.x) |> normalize
     assert all(query) == ~s{SELECT p0."x" FROM "posts" AS p0}
 
-    query = "posts" |> select([:x]) |> normalize
-    assert all(query) == ~s{SELECT p0."x" FROM "posts" AS p0}
+    query = "Posts" |> select([:x]) |> normalize
+    assert all(query) == ~s{SELECT P0."x" FROM "Posts" AS P0}
+
+    query = "0posts" |> select([:x]) |> normalize
+    assert all(query) == ~s{SELECT t0."x" FROM "0posts" AS t0}
 
     assert_raise Ecto.QueryError, ~r"PostgreSQL does not support selecting all fields from \"posts\" without a schema", fn ->
       all from(p in "posts", select: p) |> normalize()
@@ -674,13 +677,13 @@ defmodule Ecto.Adapters.PostgresTest do
     assert query == ~s{INSERT INTO "schema" AS s0 ("x","y") VALUES ($1,$2) ON CONFLICT ("x","y") DO UPDATE SET "z" = $3 WHERE (s0."w" = TRUE) RETURNING "z"}
 
     # For :replace_all
-    query = insert(nil, "schema", [:x, :y], [[:x, :y]], {:replace_all, [], [:id]}, [])
+    query = insert(nil, "schema", [:x, :y], [[:x, :y]], {[:x, :y], [], [:id]}, [])
     assert query == ~s{INSERT INTO "schema" ("x","y") VALUES ($1,$2) ON CONFLICT ("id") DO UPDATE SET "x" = EXCLUDED."x","y" = EXCLUDED."y"}
 
-    query = insert(nil, "schema", [:x, :y], [[:x, :y]], {:replace_all, [], []}, [])
+    query = insert(nil, "schema", [:x, :y], [[:x, :y]], {[:x, :y], [], []}, [])
     assert query == ~s{INSERT INTO "schema" ("x","y") VALUES ($1,$2) ON CONFLICT DO UPDATE SET "x" = EXCLUDED."x","y" = EXCLUDED."y"}
 
-    query = insert(nil, "schema", [:x, :y], [[:x, :y]], {:replace_all, [], {:constraint, :foo}}, [])
+    query = insert(nil, "schema", [:x, :y], [[:x, :y]], {[:x, :y], [], {:constraint, :foo}}, [])
     assert query == ~s{INSERT INTO "schema" ("x","y") VALUES ($1,$2) ON CONFLICT ON CONSTRAINT \"foo\" DO UPDATE SET "x" = EXCLUDED."x","y" = EXCLUDED."y"}
   end
 
@@ -708,8 +711,9 @@ defmodule Ecto.Adapters.PostgresTest do
 
   # DDL
 
-  import Ecto.Migration, only: [table: 1, table: 2, index: 2, index: 3, references: 1,
-                                references: 2, constraint: 2, constraint: 3]
+  alias Ecto.Migration.Reference
+  import Ecto.Migration, only: [table: 1, table: 2, index: 2, index: 3,
+                                constraint: 2, constraint: 3]
 
   test "executing a string during migration" do
     assert execute_ddl("example") == ["example"]
@@ -736,7 +740,7 @@ defmodule Ecto.Adapters.PostgresTest do
 
   test "create table with prefix" do
     create = {:create, table(:posts, prefix: :foo),
-              [{:add, :category_0, references(:categories), []}]}
+              [{:add, :category_0, %Reference{table: :categories}, []}]}
 
     assert execute_ddl(create) == ["""
     CREATE TABLE "foo"."posts"
@@ -747,7 +751,7 @@ defmodule Ecto.Adapters.PostgresTest do
   test "create table with comment on columns and table" do
     create = {:create, table(:posts, comment: "comment"),
               [
-                {:add, :category_0, references(:categories), [comment: "column comment"]},
+                {:add, :category_0, %Reference{table: :categories}, [comment: "column comment"]},
                 {:add, :created_at, :timestamp, []},
                 {:add, :updated_at, :timestamp, [comment: "column comment 2"]}
               ]}
@@ -762,7 +766,7 @@ defmodule Ecto.Adapters.PostgresTest do
 
   test "create table with comment on table" do
     create = {:create, table(:posts, comment: "table comment", prefix: "foo"),
-              [{:add, :category_0, references(:categories), []}]}
+              [{:add, :category_0, %Reference{table: :categories}, []}]}
     assert execute_ddl(create) == [remove_newlines("""
     CREATE TABLE "foo"."posts"
     ("category_0" bigint CONSTRAINT "posts_category_0_fkey" REFERENCES "foo"."categories"("id"))
@@ -773,7 +777,7 @@ defmodule Ecto.Adapters.PostgresTest do
   test "create table with comment on columns" do
     create = {:create, table(:posts, prefix: "foo"),
               [
-                {:add, :category_0, references(:categories), [comment: "column comment"]},
+                {:add, :category_0, %Reference{table: :categories}, [comment: "column comment"]},
                 {:add, :created_at, :timestamp, []},
                 {:add, :updated_at, :timestamp, [comment: "column comment 2"]}
               ]}
@@ -788,15 +792,15 @@ defmodule Ecto.Adapters.PostgresTest do
   test "create table with references" do
     create = {:create, table(:posts),
               [{:add, :id, :serial, [primary_key: true]},
-               {:add, :category_0, references(:categories), []},
-               {:add, :category_1, references(:categories, name: :foo_bar), []},
-               {:add, :category_2, references(:categories, on_delete: :nothing), []},
-               {:add, :category_3, references(:categories, on_delete: :delete_all), [null: false]},
-               {:add, :category_4, references(:categories, on_delete: :nilify_all), []},
-               {:add, :category_5, references(:categories, on_update: :nothing), []},
-               {:add, :category_6, references(:categories, on_update: :update_all), [null: false]},
-               {:add, :category_7, references(:categories, on_update: :nilify_all), []},
-               {:add, :category_8, references(:categories, on_delete: :nilify_all, on_update: :update_all), [null: false]}]}
+               {:add, :category_0, %Reference{table: :categories}, []},
+               {:add, :category_1, %Reference{table: :categories, name: :foo_bar}, []},
+               {:add, :category_2, %Reference{table: :categories, on_delete: :nothing}, []},
+               {:add, :category_3, %Reference{table: :categories, on_delete: :delete_all}, [null: false]},
+               {:add, :category_4, %Reference{table: :categories, on_delete: :nilify_all}, []},
+               {:add, :category_5, %Reference{table: :categories, on_update: :nothing}, []},
+               {:add, :category_6, %Reference{table: :categories, on_update: :update_all}, [null: false]},
+               {:add, :category_7, %Reference{table: :categories, on_update: :nilify_all}, []},
+               {:add, :category_8, %Reference{table: :categories, on_delete: :nilify_all, on_update: :update_all}, [null: false]}]}
 
     assert execute_ddl(create) == ["""
     CREATE TABLE "posts" ("id" serial,
@@ -872,10 +876,10 @@ defmodule Ecto.Adapters.PostgresTest do
   test "alter table" do
     alter = {:alter, table(:posts),
              [{:add, :title, :string, [default: "Untitled", size: 100, null: false]},
-              {:add, :author_id, references(:author), []},
+              {:add, :author_id, %Reference{table: :author}, []},
               {:modify, :price, :numeric, [precision: 8, scale: 2, null: true]},
               {:modify, :cost, :integer, [null: false, default: nil]},
-              {:modify, :permalink_id, references(:permalinks), null: false},
+              {:modify, :permalink_id, %Reference{table: :permalinks}, null: false},
               {:remove, :summary}]}
 
     assert execute_ddl(alter) == ["""
@@ -898,7 +902,7 @@ defmodule Ecto.Adapters.PostgresTest do
     alter = {:alter, table(:posts, comment: "table comment"),
              [{:add, :title, :string, [default: "Untitled", size: 100, null: false, comment: "column comment"]},
               {:modify, :price, :numeric, [precision: 8, scale: 2, null: true]},
-              {:modify, :permalink_id, references(:permalinks), [null: false, comment: "column comment"]},
+              {:modify, :permalink_id, %Reference{table: :permalinks}, [null: false, comment: "column comment"]},
               {:remove, :summary}]}
 
     assert execute_ddl(alter) == [remove_newlines("""
@@ -919,8 +923,8 @@ defmodule Ecto.Adapters.PostgresTest do
 
   test "alter table with prefix" do
     alter = {:alter, table(:posts, prefix: :foo),
-             [{:add, :author_id, references(:author, prefix: :foo), []},
-              {:modify, :permalink_id, references(:permalinks, prefix: :foo), null: false}]}
+             [{:add, :author_id, %Reference{table: :author}, []},
+              {:modify, :permalink_id, %Reference{table: :permalinks}, null: false}]}
 
     assert execute_ddl(alter) == ["""
     ALTER TABLE "foo"."posts"
diff --git a/test/ecto/changeset/belongs_to_test.exs b/test/ecto/changeset/belongs_to_test.exs
index 3a2928b2..b16db7f2 100644
--- a/test/ecto/changeset/belongs_to_test.exs
+++ b/test/ecto/changeset/belongs_to_test.exs
@@ -71,7 +71,7 @@ defmodule Ecto.Changeset.BelongsToTest do
     refute changeset.valid?
 
     changeset = cast(%Author{}, %{"profile" => "value"}, :profile)
-    assert changeset.errors == [profile: {"is invalid", [type: :map]}]
+    assert changeset.errors == [profile: {"is invalid", [validation: :assoc, type: :map]}]
     refute changeset.valid?
   end
 
@@ -249,17 +249,17 @@ defmodule Ecto.Changeset.BelongsToTest do
 
     changeset = cast(schema, %{"invalid_profile" => nil}, :invalid_profile)
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_profile: {"is invalid", [type: :map]}]
+    assert changeset.errors == [invalid_profile: {"is invalid", [validation: :assoc, type: :map]}]
     refute changeset.valid?
 
     changeset = cast(schema, %{"invalid_profile" => %{"id" => 2}}, :invalid_profile)
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_profile: {"is invalid", [type: :map]}]
+    assert changeset.errors == [invalid_profile: {"is invalid", [validation: :assoc, type: :map]}]
     refute changeset.valid?
 
     changeset = cast(schema, %{"invalid_profile" => nil}, :invalid_profile, invalid_message: "a custom message")
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_profile: {"a custom message", [type: :map]}]
+    assert changeset.errors == [invalid_profile: {"a custom message", [validation: :assoc, type: :map]}]
     refute changeset.valid?
   end
 
diff --git a/test/ecto/changeset/embedded_no_pk_test.exs b/test/ecto/changeset/embedded_no_pk_test.exs
index 49c8d76f..fa714f1d 100644
--- a/test/ecto/changeset/embedded_no_pk_test.exs
+++ b/test/ecto/changeset/embedded_no_pk_test.exs
@@ -101,7 +101,7 @@ defmodule Ecto.Changeset.EmbeddedNoPkTest do
     refute changeset.valid?
 
     changeset = cast(%Author{}, %{"profile" => "value"}, :profile, required: true)
-    assert changeset.errors == [profile: {"is invalid", [type: :map]}]
+    assert changeset.errors == [profile: {"is invalid", [validation: :embed, type: :map]}]
     refute changeset.valid?
   end
 
@@ -204,17 +204,17 @@ defmodule Ecto.Changeset.EmbeddedNoPkTest do
 
     changeset = cast(schema, %{"invalid_profile" => nil}, :invalid_profile)
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_profile: {"is invalid", [type: :map]}]
+    assert changeset.errors == [invalid_profile: {"is invalid", [validation: :embed, type: :map]}]
     refute changeset.valid?
 
     changeset = cast(schema, %{"invalid_profile" => %{}}, :invalid_profile)
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_profile: {"is invalid", [type: :map]}]
+    assert changeset.errors == [invalid_profile: {"is invalid", [validation: :embed, type: :map]}]
     refute changeset.valid?
 
     changeset = cast(schema, %{"invalid_profile" => nil}, :invalid_profile, invalid_message: "a custom message")
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_profile: {"a custom message", [type: :map]}]
+    assert changeset.errors == [invalid_profile: {"a custom message", [validation: :embed, type: :map]}]
     refute changeset.valid?
   end
 
@@ -302,15 +302,15 @@ defmodule Ecto.Changeset.EmbeddedNoPkTest do
 
   test "cast embeds_many with invalid params" do
     changeset = cast(%Author{}, %{"posts" => "value"}, :posts)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :embed, type: {:array, :map}]}]
     refute changeset.valid?
 
     changeset = cast(%Author{}, %{"posts" => ["value"]}, :posts)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :embed, type: {:array, :map}]}]
     refute changeset.valid?
 
     changeset = cast(%Author{}, %{"posts" => nil}, :posts)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :embed, type: {:array, :map}]}]
     refute changeset.valid?
   end
 
@@ -332,7 +332,7 @@ defmodule Ecto.Changeset.EmbeddedNoPkTest do
 
     changeset = cast(%Author{posts: []}, %{"posts" => nil}, :posts, required: true)
     assert changeset.changes == %{}
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :embed, type: {:array, :map}]}]
   end
 
   test "cast embeds_many with empty parameters" do
@@ -359,12 +359,12 @@ defmodule Ecto.Changeset.EmbeddedNoPkTest do
 
     changeset = cast(schema, %{"invalid_posts" => []}, :invalid_posts)
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [invalid_posts: {"is invalid", [validation: :embed, type: {:array, :map}]}]
     refute changeset.valid?
 
     changeset = cast(schema, %{"invalid_posts" => [%{}]}, :invalid_posts)
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [invalid_posts: {"is invalid", [validation: :embed, type: {:array, :map}]}]
     refute changeset.valid?
   end
 
diff --git a/test/ecto/changeset/embedded_test.exs b/test/ecto/changeset/embedded_test.exs
index 1477862c..46b6de0b 100644
--- a/test/ecto/changeset/embedded_test.exs
+++ b/test/ecto/changeset/embedded_test.exs
@@ -106,7 +106,7 @@ defmodule Ecto.Changeset.EmbeddedTest do
     refute changeset.valid?
 
     changeset = cast(%Author{}, %{"profile" => "value"}, :profile, required: true)
-    assert changeset.errors == [profile: {"is invalid", [type: :map]}]
+    assert changeset.errors == [profile: {"is invalid", [validation: :embed, type: :map]}]
     refute changeset.valid?
   end
 
@@ -267,17 +267,17 @@ defmodule Ecto.Changeset.EmbeddedTest do
 
     changeset = cast(schema, %{"invalid_profile" => nil}, :invalid_profile)
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_profile: {"is invalid", [type: :map]}]
+    assert changeset.errors == [invalid_profile: {"is invalid", [validation: :embed, type: :map]}]
     refute changeset.valid?
 
     changeset = cast(schema, %{"invalid_profile" => %{"id" => 2}}, :invalid_profile)
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_profile: {"is invalid", [type: :map]}]
+    assert changeset.errors == [invalid_profile: {"is invalid", [validation: :embed, type: :map]}]
     refute changeset.valid?
 
     changeset = cast(schema, %{"invalid_profile" => nil}, :invalid_profile, invalid_message: "a custom message")
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_profile: {"a custom message", [type: :map]}]
+    assert changeset.errors == [invalid_profile: {"a custom message", [validation: :embed, type: :map]}]
     refute changeset.valid?
   end
 
@@ -391,19 +391,19 @@ defmodule Ecto.Changeset.EmbeddedTest do
 
   test "cast embeds_many with invalid params" do
     changeset = cast(%Author{}, %{"posts" => "value"}, :posts)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :embed, type: {:array, :map}]}]
     refute changeset.valid?
 
     changeset = cast(%Author{}, %{"posts" => ["value"]}, :posts)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :embed, type: {:array, :map}]}]
     refute changeset.valid?
 
     changeset = cast(%Author{}, %{"posts" => nil}, :posts)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :embed, type: {:array, :map}]}]
     refute changeset.valid?
 
     changeset = cast(%Author{}, %{"posts" => %{"id" => "invalid"}}, :posts)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :embed, type: {:array, :map}]}]
     refute changeset.valid?
   end
 
@@ -439,7 +439,7 @@ defmodule Ecto.Changeset.EmbeddedTest do
 
     changeset = cast(%Author{posts: []}, %{"posts" => nil}, :posts, required: true)
     assert changeset.changes == %{}
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :embed, type: {:array, :map}]}]
   end
 
   test "cast embeds_many with empty parameters" do
@@ -466,12 +466,12 @@ defmodule Ecto.Changeset.EmbeddedTest do
 
     changeset = cast(schema, %{"invalid_posts" => []}, :invalid_posts)
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [invalid_posts: {"is invalid", [validation: :embed, type: {:array, :map}]}]
     refute changeset.valid?
 
     changeset = cast(schema, %{"invalid_posts" => [%{"id" => 2}]}, :invalid_posts)
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [invalid_posts: {"is invalid", [validation: :embed, type: {:array, :map}]}]
     refute changeset.valid?
   end
 
@@ -844,8 +844,8 @@ defmodule Ecto.Changeset.EmbeddedTest do
     assert Changeset.traverse_errors(changeset, &(&1)) == %{posts: [{"can't be blank", [validation: :required]}]}
 
     changeset = cast(%Author{posts: []}, %{"posts" => nil}, :posts, required: true)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
-    assert Changeset.traverse_errors(changeset, &(&1)) == %{posts: [{"is invalid", [type: {:array, :map}]}]}
+    assert changeset.errors == [posts: {"is invalid", [validation: :embed, type: {:array, :map}]}]
+    assert Changeset.traverse_errors(changeset, &(&1)) == %{posts: [{"is invalid", [validation: :embed, type: {:array, :map}]}]}
 
     changeset = cast(%Author{posts: []}, %{"posts" => [%{title: nil}]}, :posts, required: true)
     assert changeset.errors == []
diff --git a/test/ecto/changeset/has_assoc_test.exs b/test/ecto/changeset/has_assoc_test.exs
index 5b8bb8df..02e8ea50 100644
--- a/test/ecto/changeset/has_assoc_test.exs
+++ b/test/ecto/changeset/has_assoc_test.exs
@@ -96,7 +96,7 @@ defmodule Ecto.Changeset.HasAssocTest do
     refute changeset.valid?
 
     changeset = cast(%Author{}, %{"profile" => "value"}, :profile)
-    assert changeset.errors == [profile: {"is invalid", [type: :map]}]
+    assert changeset.errors == [profile: {"is invalid", [validation: :assoc, type: :map]}]
     refute changeset.valid?
   end
 
@@ -287,17 +287,17 @@ defmodule Ecto.Changeset.HasAssocTest do
 
     changeset = cast(schema, %{"invalid_profile" => nil}, :invalid_profile)
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_profile: {"is invalid", [type: :map]}]
+    assert changeset.errors == [invalid_profile: {"is invalid", [validation: :assoc, type: :map]}]
     refute changeset.valid?
 
     changeset = cast(schema, %{"invalid_profile" => %{"id" => 2}}, :invalid_profile)
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_profile: {"is invalid", [type: :map]}]
+    assert changeset.errors == [invalid_profile: {"is invalid", [validation: :assoc, type: :map]}]
     refute changeset.valid?
 
     changeset = cast(schema, %{"invalid_profile" => nil}, :invalid_profile, invalid_message: "a custom message")
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_profile: {"a custom message", [type: :map]}]
+    assert changeset.errors == [invalid_profile: {"a custom message", [validation: :assoc, type: :map]}]
     refute changeset.valid?
   end
 
@@ -417,19 +417,19 @@ defmodule Ecto.Changeset.HasAssocTest do
 
   test "cast has_many with invalid params" do
     changeset = cast(%Author{}, %{"posts" => "value"}, :posts)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :assoc, type: {:array, :map}]}]
     refute changeset.valid?
 
     changeset = cast(%Author{}, %{"posts" => ["value"]}, :posts)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :assoc, type: {:array, :map}]}]
     refute changeset.valid?
 
     changeset = cast(%Author{}, %{"posts" => nil}, :posts)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :assoc, type: {:array, :map}]}]
     refute changeset.valid?
 
     changeset = cast(%Author{}, %{"posts" => %{"id" => "invalid"}}, :posts)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :assoc, type: {:array, :map}]}]
     refute changeset.valid?
   end
 
@@ -476,10 +476,15 @@ defmodule Ecto.Changeset.HasAssocTest do
     assert changeset.changes == %{}
     assert changeset.errors == [posts: {"can't be blank", [validation: :required]}]
 
+    changeset = cast(%Author{posts: [%Post{title: "hello", id: 1}]}, %{}, :posts, required: true)
+    assert changeset.required == [:posts]
+    assert changeset.changes == %{}
+    assert changeset.errors == []
+
     changeset = cast(%Author{posts: []}, %{"posts" => nil}, :posts, required: true)
     assert changeset.required == [:posts]
     assert changeset.changes == %{}
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :assoc, type: {:array, :map}]}]
   end
 
   test "cast has_many with empty parameters" do
@@ -509,12 +514,12 @@ defmodule Ecto.Changeset.HasAssocTest do
 
     changeset = cast(schema, %{"invalid_posts" => []}, :invalid_posts)
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [invalid_posts: {"is invalid", [validation: :assoc, type: {:array, :map}]}]
     refute changeset.valid?
 
     changeset = cast(schema, %{"invalid_posts" => [%{"id" => 2}]}, :invalid_posts)
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [invalid_posts: {"is invalid", [validation: :assoc, type: {:array, :map}]}]
     refute changeset.valid?
   end
 
@@ -1061,8 +1066,8 @@ defmodule Ecto.Changeset.HasAssocTest do
     assert Changeset.traverse_errors(changeset, &(&1)) == %{posts: [{"can't be blank", [validation: :required]}]}
 
     changeset = cast(%Author{posts: []}, %{"posts" => nil}, :posts, required: true)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
-    assert Changeset.traverse_errors(changeset, &(&1)) == %{posts: [{"is invalid", [type: {:array, :map}]}]}
+    assert changeset.errors == [posts: {"is invalid", [validation: :assoc, type: {:array, :map}]}]
+    assert Changeset.traverse_errors(changeset, &(&1)) == %{posts: [{"is invalid", [validation: :assoc, type: {:array, :map}]}]}
 
     changeset = cast(%Author{}, %{posts: []}, :posts, required: true)
     assert changeset.errors == [posts: {"can't be blank", [validation: :required]}]
diff --git a/test/ecto/changeset/many_to_many_test.exs b/test/ecto/changeset/many_to_many_test.exs
index 52de75c7..fdb11b1c 100644
--- a/test/ecto/changeset/many_to_many_test.exs
+++ b/test/ecto/changeset/many_to_many_test.exs
@@ -115,19 +115,19 @@ defmodule Ecto.Changeset.ManyToManyTest do
 
   test "cast many_to_many with invalid params" do
     changeset = cast(%Author{}, %{"posts" => "value"}, :posts)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :assoc, type: {:array, :map}]}]
     refute changeset.valid?
 
     changeset = cast(%Author{}, %{"posts" => ["value"]}, :posts)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :assoc, type: {:array, :map}]}]
     refute changeset.valid?
 
     changeset = cast(%Author{}, %{"posts" => nil}, :posts)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :assoc, type: {:array, :map}]}]
     refute changeset.valid?
 
     changeset = cast(%Author{}, %{"posts" => %{"id" => "invalid"}}, :posts)
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :assoc, type: {:array, :map}]}]
     refute changeset.valid?
   end
 
@@ -174,7 +174,7 @@ defmodule Ecto.Changeset.ManyToManyTest do
     changeset = cast(%Author{posts: []}, %{"posts" => nil}, :posts, required: true)
     assert changeset.required == [:posts]
     assert changeset.changes == %{}
-    assert changeset.errors == [posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [posts: {"is invalid", [validation: :assoc, type: {:array, :map}]}]
   end
 
   test "cast many_to_many with empty parameters" do
@@ -204,17 +204,17 @@ defmodule Ecto.Changeset.ManyToManyTest do
 
     changeset = cast(schema, %{"invalid_posts" => []}, :invalid_posts)
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [invalid_posts: {"is invalid", [validation: :assoc, type: {:array, :map}]}]
     refute changeset.valid?
 
     changeset = cast(schema, %{"invalid_posts" => [%{"id" => 2}]}, :invalid_posts)
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_posts: {"is invalid", [type: {:array, :map}]}]
+    assert changeset.errors == [invalid_posts: {"is invalid", [validation: :assoc, type: {:array, :map}]}]
     refute changeset.valid?
 
     changeset = cast(schema, %{"invalid_posts" => [%{"id" => 2}]}, :invalid_posts, invalid_message: "a custom message")
     assert changeset.changes == %{}
-    assert changeset.errors == [invalid_posts: {"a custom message", [type: {:array, :map}]}]
+    assert changeset.errors == [invalid_posts: {"a custom message", [validation: :assoc, type: {:array, :map}]}]
     refute changeset.valid?
   end
 
diff --git a/test/ecto/changeset_test.exs b/test/ecto/changeset_test.exs
index d3174666..19d2edaa 100644
--- a/test/ecto/changeset_test.exs
+++ b/test/ecto/changeset_test.exs
@@ -820,18 +820,25 @@ defmodule Ecto.ChangesetTest do
 
     changeset = changeset(%{"title" => "world"}) |> validate_length(:title, min: 6)
     refute changeset.valid?
-    assert changeset.errors == [title: {"should be at least %{count} character(s)", count: 6, validation: :length, min: 6}]
+    assert changeset.errors == [title: {"should be at least %{count} character(s)", count: 6, validation: :length, kind: :min}]
 
     changeset = changeset(%{"title" => "world"}) |> validate_length(:title, max: 4)
     refute changeset.valid?
-    assert changeset.errors == [title: {"should be at most %{count} character(s)", count: 4, validation: :length, max: 4}]
+    assert changeset.errors == [title: {"should be at most %{count} character(s)", count: 4, validation: :length, kind: :max}]
 
     changeset = changeset(%{"title" => "world"}) |> validate_length(:title, is: 10)
     refute changeset.valid?
-    assert changeset.errors == [title: {"should be %{count} character(s)", count: 10, validation: :length, is: 10}]
+    assert changeset.errors == [title: {"should be %{count} character(s)", count: 10, validation: :length, kind: :is}]
 
     changeset = changeset(%{"title" => "world"}) |> validate_length(:title, is: 10, message: "yada")
-    assert changeset.errors == [title: {"yada", count: 10, validation: :length, is: 10}]
+    assert changeset.errors == [title: {"yada", count: 10, validation: :length, kind: :is}]
+
+    changeset = changeset(%{"title" => "\u0065\u0301"}) |> validate_length(:title, max: 1)
+    assert changeset.valid?
+
+    changeset = changeset(%{"title" => "\u0065\u0301"}) |> validate_length(:title, max: 1, count: :codepoints)
+    refute changeset.valid?
+    assert changeset.errors == [title: {"should be at most %{count} character(s)", count: 1, validation: :length, kind: :max}]
   end
 
   test "validate_length/3 with list" do
@@ -848,18 +855,18 @@ defmodule Ecto.ChangesetTest do
 
     changeset = changeset(%{"topics" => ["Politics", "Security"]}) |> validate_length(:topics, min: 6, foo: true)
     refute changeset.valid?
-    assert changeset.errors == [topics: {"should have at least %{count} item(s)", count: 6, validation: :length, min: 6}]
+    assert changeset.errors == [topics: {"should have at least %{count} item(s)", count: 6, validation: :length, kind: :min}]
 
     changeset = changeset(%{"topics" => ["Politics", "Security", "Economy"]}) |> validate_length(:topics, max: 2)
     refute changeset.valid?
-    assert changeset.errors == [topics: {"should have at most %{count} item(s)", count: 2, validation: :length, max: 2}]
+    assert changeset.errors == [topics: {"should have at most %{count} item(s)", count: 2, validation: :length, kind: :max}]
 
     changeset = changeset(%{"topics" => ["Politics", "Security"]}) |> validate_length(:topics, is: 10)
     refute changeset.valid?
-    assert changeset.errors == [topics: {"should have %{count} item(s)", count: 10, validation: :length, is: 10}]
+    assert changeset.errors == [topics: {"should have %{count} item(s)", count: 10, validation: :length, kind: :is}]
 
     changeset = changeset(%{"topics" => ["Politics", "Security"]}) |> validate_length(:topics, is: 10, message: "yada")
-    assert changeset.errors == [topics: {"yada", count: 10, validation: :length, is: 10}]
+    assert changeset.errors == [topics: {"yada", count: 10, validation: :length, kind: :is}]
   end
 
   test "validate_number/3" do
@@ -873,7 +880,7 @@ defmodule Ecto.ChangesetTest do
     changeset = changeset(%{"upvotes" => -1})
                 |> validate_number(:upvotes, greater_than: 0)
     refute changeset.valid?
-    assert changeset.errors == [upvotes: {"must be greater than %{number}", validation: :number, number: 0}]
+    assert changeset.errors == [upvotes: {"must be greater than %{number}", validation: :number, kind: :greater_than, number: 0}]
     assert changeset.validations == [upvotes: {:number, [greater_than: 0]}]
 
     # Multiple validations
@@ -887,12 +894,12 @@ defmodule Ecto.ChangesetTest do
     changeset = changeset(%{"upvotes" => 3})
                 |> validate_number(:upvotes, greater_than: 100, less_than: 0)
     refute changeset.valid?
-    assert changeset.errors == [upvotes: {"must be greater than %{number}", validation: :number, number: 100}]
+    assert changeset.errors == [upvotes: {"must be greater than %{number}", validation: :number, kind: :greater_than, number: 100}]
 
     # Multiple validations with custom message errors
     changeset = changeset(%{"upvotes" => 3})
                 |> validate_number(:upvotes, greater_than: 100, less_than: 0, message: "yada")
-    assert changeset.errors == [upvotes: {"yada", validation: :number, number: 100}]
+    assert changeset.errors == [upvotes: {"yada", validation: :number, kind: :greater_than, number: 100}]
   end
 
   test "validate_number/3 with decimal" do
@@ -1133,19 +1140,19 @@ defmodule Ecto.ChangesetTest do
 
     assert changeset.constraints ==
            [%{type: :unique, field: :title, constraint: "posts_title_index", match: :exact,
-              error: {"has already been taken", []}}]
+              error: {"has already been taken", [constraint: :unique]}}]
 
     changeset = change(%Post{}) |> unique_constraint(:title, name: :whatever, message: "is taken")
     assert changeset.constraints ==
-           [%{type: :unique, field: :title, constraint: "whatever", match: :exact, error: {"is taken", []}}]
+           [%{type: :unique, field: :title, constraint: "whatever", match: :exact, error: {"is taken", [constraint: :unique]}}]
 
     changeset = change(%Post{}) |> unique_constraint(:title, name: :whatever, match: :suffix, message: "is taken")
     assert changeset.constraints ==
-           [%{type: :unique, field: :title, constraint: "whatever", match: :suffix, error: {"is taken", []}}]
+           [%{type: :unique, field: :title, constraint: "whatever", match: :suffix, error: {"is taken", [constraint: :unique]}}]
 
     changeset = change(%Post{}) |> unique_constraint(:title, name: :whatever, match: :prefix, message: "is taken")
     assert changeset.constraints ==
-           [%{type: :unique, field: :title, constraint: "whatever", match: :prefix, error: {"is taken", []}}]
+           [%{type: :unique, field: :title, constraint: "whatever", match: :prefix, error: {"is taken", [constraint: :unique]}}]
 
     assert_raise ArgumentError, ~r/invalid match type: :invalid/, fn ->
       change(%Post{}) |> unique_constraint(:title, name: :whatever, match: :invalid, message: "is taken")
@@ -1157,15 +1164,15 @@ defmodule Ecto.ChangesetTest do
 
     assert changeset.constraints ==
            [%{type: :unique, field: :permalink, constraint: "posts_url_index", match: :exact,
-              error: {"has already been taken", []}}]
+              error: {"has already been taken", [constraint: :unique]}}]
 
     changeset = change(%Post{}) |> unique_constraint(:permalink, name: :whatever, message: "is taken")
     assert changeset.constraints ==
-           [%{type: :unique, field: :permalink, constraint: "whatever", match: :exact, error: {"is taken", []}}]
+           [%{type: :unique, field: :permalink, constraint: "whatever", match: :exact, error: {"is taken", [constraint: :unique]}}]
 
     changeset = change(%Post{}) |> unique_constraint(:permalink, name: :whatever, match: :suffix, message: "is taken")
     assert changeset.constraints ==
-           [%{type: :unique, field: :permalink, constraint: "whatever", match: :suffix, error: {"is taken", []}}]
+           [%{type: :unique, field: :permalink, constraint: "whatever", match: :suffix, error: {"is taken", [constraint: :unique]}}]
 
     assert_raise ArgumentError, ~r/invalid match type: :invalid/, fn ->
       change(%Post{}) |> unique_constraint(:permalink, name: :whatever, match: :invalid, message: "is taken")
@@ -1176,44 +1183,44 @@ defmodule Ecto.ChangesetTest do
     changeset = change(%Comment{}) |> foreign_key_constraint(:post_id)
     assert changeset.constraints ==
            [%{type: :foreign_key, field: :post_id, constraint: "comments_post_id_fkey", match: :exact,
-              error: {"does not exist", []}}]
+              error: {"does not exist", [constraint: :foreign]}}]
 
     changeset = change(%Comment{}) |> foreign_key_constraint(:post_id, name: :whatever, message: "is not available")
     assert changeset.constraints ==
-           [%{type: :foreign_key, field: :post_id, constraint: "whatever", match: :exact, error: {"is not available", []}}]
+           [%{type: :foreign_key, field: :post_id, constraint: "whatever", match: :exact, error: {"is not available", [constraint: :foreign]}}]
   end
 
   test "foreign_key_constraint/3 on field with :source" do
     changeset = change(%Post{}) |> foreign_key_constraint(:permalink)
     assert changeset.constraints ==
            [%{type: :foreign_key, field: :permalink, constraint: "posts_url_fkey", match: :exact,
-              error: {"does not exist", []}}]
+              error: {"does not exist", [constraint: :foreign]}}]
 
     changeset = change(%Post{}) |> foreign_key_constraint(:permalink, name: :whatever, message: "is not available")
     assert changeset.constraints ==
-           [%{type: :foreign_key, field: :permalink, constraint: "whatever", match: :exact, error: {"is not available", []}}]
+           [%{type: :foreign_key, field: :permalink, constraint: "whatever", match: :exact, error: {"is not available", [constraint: :foreign]}}]
   end
 
   test "assoc_constraint/3" do
     changeset = change(%Comment{}) |> assoc_constraint(:post)
     assert changeset.constraints ==
            [%{type: :foreign_key, field: :post, constraint: "comments_post_id_fkey", match: :exact,
-              error: {"does not exist", []}}]
+              error: {"does not exist", [constraint: :assoc]}}]
 
     changeset = change(%Comment{}) |> assoc_constraint(:post, name: :whatever, message: "is not available")
     assert changeset.constraints ==
-           [%{type: :foreign_key, field: :post, constraint: "whatever", match: :exact, error: {"is not available", []}}]
+           [%{type: :foreign_key, field: :post, constraint: "whatever", match: :exact, error: {"is not available", [constraint: :assoc]}}]
   end
 
   test "assoc_constraint/3 on field with :source" do
     changeset = change(%Post{}) |> assoc_constraint(:category)
     assert changeset.constraints ==
            [%{type: :foreign_key, field: :category, constraint: "posts_category_id_fkey", match: :exact,
-              error: {"does not exist", []}}]
+              error: {"does not exist", [constraint: :assoc]}}]
 
     changeset = change(%Post{}) |> assoc_constraint(:category, name: :whatever, message: "is not available")
     assert changeset.constraints ==
-           [%{type: :foreign_key, field: :category, constraint: "whatever", match: :exact, error: {"is not available", []}}]
+           [%{type: :foreign_key, field: :category, constraint: "whatever", match: :exact, error: {"is not available", [constraint: :assoc]}}]
   end
 
   test "assoc_constraint/3 with errors" do
@@ -1232,24 +1239,24 @@ defmodule Ecto.ChangesetTest do
     changeset = change(%Post{}) |> no_assoc_constraint(:comments)
     assert changeset.constraints ==
            [%{type: :foreign_key, field: :comments, constraint: "comments_post_id_fkey", match: :exact,
-              error: {"are still associated with this entry", []}}]
+              error: {"are still associated with this entry", [constraint: :no_assoc]}}]
 
     changeset = change(%Post{}) |> no_assoc_constraint(:comments, name: :whatever, message: "exists")
     assert changeset.constraints ==
            [%{type: :foreign_key, field: :comments, constraint: "whatever", match: :exact,
-              error: {"exists", []}}]
+              error: {"exists", [constraint: :no_assoc]}}]
   end
 
   test "no_assoc_constraint/3 with has_one" do
     changeset = change(%Post{}) |> no_assoc_constraint(:comment)
     assert changeset.constraints ==
            [%{type: :foreign_key, field: :comment, constraint: "comments_post_id_fkey", match: :exact,
-              error: {"is still associated with this entry", []}}]
+              error: {"is still associated with this entry", [constraint: :no_assoc]}}]
 
     changeset = change(%Post{}) |> no_assoc_constraint(:comment, name: :whatever, message: "exists")
     assert changeset.constraints ==
            [%{type: :foreign_key, field: :comment, constraint: "whatever", match: :exact,
-              error: {"exists", []}}]
+              error: {"exists", [constraint: :no_assoc]}}]
   end
 
   test "no_assoc_constraint/3 with errors" do
@@ -1321,8 +1328,8 @@ defmodule Ecto.ChangesetTest do
       %Ecto.Changeset{}, field, {_, [name: "your title"]} ->
         "value in #{field} is taken"
         |> String.upcase()
-      %Ecto.Changeset{}, field, {_, [count: 3, validation: :length, min: 3] = keys} ->
-        "should be at least #{keys[:min]} character(s) in field #{field}"
+      %Ecto.Changeset{}, field, {_, [count: 3, validation: :length, kind: :min] = keys} ->
+        "should be at least #{keys[:count]} character(s) in field #{field}"
         |> String.upcase()
       %Ecto.Changeset{validations: validations}, field, {_, [validation: :format]} ->
         validation = Keyword.get_values(validations, field)
diff --git a/test/ecto/date_time_test.exs b/test/ecto/date_time_test.exs
deleted file mode 100644
index 7a810372..00000000
--- a/test/ecto/date_time_test.exs
+++ /dev/null
@@ -1,336 +0,0 @@
-defmodule Ecto.DateTest do
-  use ExUnit.Case, async: true
-
-  @date %Ecto.Date{year: 2015, month: 12, day: 31}
-  @leap_date %Ecto.Date{year: 2000, month: 2, day: 29}
-
-  test "cast itself" do
-    assert Ecto.Date.cast(@date) == {:ok, @date}
-  end
-
-  test "cast strings" do
-    assert Ecto.Date.cast("2015-12-31") == {:ok, @date}
-    assert Ecto.Date.cast("2000-02-29") == {:ok, @leap_date}
-    assert Ecto.Date.cast("2015-00-23") == :error
-    assert Ecto.Date.cast("2015-13-23") == :error
-    assert Ecto.Date.cast("2015-01-00") == :error
-    assert Ecto.Date.cast("2015-01-32") == :error
-    assert Ecto.Date.cast("2015-02-29") == :error
-    assert Ecto.Date.cast("1900-02-29") == :error
-
-    assert Ecto.Date.cast("2015-12-31 23:50:07") == {:ok, @date}
-    assert Ecto.Date.cast("2015-12-31T23:50:07") == {:ok, @date}
-    assert Ecto.Date.cast("2015-12-31T23:50:07Z") == {:ok, @date}
-    assert Ecto.Date.cast("2015-12-31T23:50:07.000Z") == {:ok, @date}
-    assert Ecto.Date.cast("2015-12-31P23:50:07") == :error
-
-    assert Ecto.Date.cast("2015-12-31T23:50:07.008") == {:ok, @date}
-    assert Ecto.Date.cast("2015-12-31T23:50:07.008Z") == {:ok, @date}
-  end
-
-  test "cast maps" do
-    assert Ecto.Date.cast(%{"year" => "2015", "month" => "12", "day" => "31"}) ==
-           {:ok, @date}
-    assert Ecto.Date.cast(%{year: 2015, month: 12, day: 31}) ==
-           {:ok, @date}
-    assert Ecto.Date.cast(%{"year" => "", "month" => "", "day" => ""}) ==
-           {:ok, nil}
-    assert Ecto.Date.cast(%{year: nil, month: nil, day: nil}) ==
-           {:ok, nil}
-    assert Ecto.Date.cast(%{"year" => "2015", "month" => "", "day" => "31"}) ==
-           :error
-    assert Ecto.Date.cast(%{"year" => "2015", "month" => nil, "day" => "31"}) ==
-           :error
-    assert Ecto.Date.cast(%{"year" => "2015", "month" => nil}) ==
-           :error
-    assert Ecto.Date.cast(%{"year" => "", "month" => "01", "day" => "30"}) ==
-           :error
-    assert Ecto.Date.cast(%{"year" => nil, "month" => "01", "day" => "30"}) ==
-           :error
-  end
-
-  test "cast erl date" do
-    assert Ecto.Date.cast({2015, 12, 31}) == {:ok, @date}
-    assert Ecto.Date.cast({2015, 13, 31}) == :error
-  end
-
-  test "cast!" do
-    assert Ecto.Date.cast!("2015-12-31") == @date
-
-    assert_raise Ecto.CastError, "cannot cast \"2015-00-23\" to Ecto.Date", fn ->
-      Ecto.Date.cast!("2015-00-23")
-    end
-  end
-
-  test "dump itself into a date triplet" do
-    assert Ecto.Date.dump(@date) == {:ok, {2015, 12, 31}}
-    assert Ecto.Date.dump({2015, 12, 31}) == :error
-  end
-
-  test "load a date triplet" do
-    assert Ecto.Date.load({2015, 12, 31}) == {:ok, @date}
-    assert Ecto.Date.load(@date) == :error
-  end
-
-  test "to_string" do
-    assert to_string(@date) == "2015-12-31"
-    assert Ecto.Date.to_string(@date) == "2015-12-31"
-  end
-
-  test "to_iso8601" do
-    assert Ecto.Date.to_iso8601(@date) == "2015-12-31"
-  end
-
-  test "to_erl and from_erl" do
-    assert @date |> Ecto.Date.to_erl |> Ecto.Date.from_erl == @date
-  end
-
-  test "inspect protocol" do
-    assert inspect(@date) == "#Ecto.Date<2015-12-31>"
-  end
-end
-
-defmodule Ecto.TimeTest do
-  use ExUnit.Case, async: true
-
-  @time %Ecto.Time{hour: 23, min: 50, sec: 07, usec: 0}
-  @time_zero %Ecto.Time{hour: 23, min: 50, sec: 0, usec: 0}
-  @time_usec %Ecto.Time{hour: 12, min: 40, sec: 33, usec: 30000}
-
-  test "cast itself" do
-    assert Ecto.Time.cast(@time) == {:ok, @time}
-    assert Ecto.Time.cast(@time_zero) ==  {:ok, @time_zero}
-  end
-
-  test "cast strings" do
-    assert Ecto.Time.cast("23:50:07") == {:ok, @time}
-    assert Ecto.Time.cast("23:50:07Z") == {:ok, @time}
-
-    assert Ecto.Time.cast("23:50:07.030")
-      == {:ok, %{@time | usec: 30000}}
-    assert Ecto.Time.cast("23:50:07.123456")
-      == {:ok, %{@time | usec: 123456}}
-    assert Ecto.Time.cast("23:50:07.123456Z")
-      == {:ok, %{@time | usec: 123456}}
-    assert Ecto.Time.cast("23:50:07.000123Z")
-      == {:ok, %{@time | usec: 123}}
-
-    assert Ecto.Time.cast("24:01:01") == :error
-    assert Ecto.Time.cast("00:61:00") == :error
-    assert Ecto.Time.cast("00:00:61") == :error
-    assert Ecto.Time.cast("00:00:009") == :error
-    assert Ecto.Time.cast("00:00:00.A00") == :error
-  end
-
-  test "cast maps" do
-    assert Ecto.Time.cast(%{"hour" => "23", "minute" => "50", "second" => "07"}) ==
-           {:ok, @time}
-    assert Ecto.Time.cast(%{hour: 23, minute: 50, second: 07}) ==
-           {:ok, @time}
-    assert Ecto.Time.cast(%{"hour" => "", "minute" => ""}) ==
-           {:ok, nil}
-    assert Ecto.Time.cast(%{hour: nil, minute: nil}) ==
-           {:ok, nil}
-    assert Ecto.Time.cast(%{"hour" => "23", "minute" => "50"}) ==
-           {:ok, @time_zero}
-    assert Ecto.Time.cast(%{hour: 23, minute: 50}) ==
-           {:ok, @time_zero}
-    assert Ecto.Time.cast(%{hour: 12, minute: 40, second: 33, microsecond: 30_000}) ==
-           {:ok, @time_usec}
-    assert Ecto.Time.cast(%{"hour" => 12, "minute" => 40, "second" => 33, "microsecond" => 30_000}) ==
-           {:ok, @time_usec}
-    assert Ecto.Time.cast(%{"hour" => "", "minute" => "50"}) ==
-           :error
-    assert Ecto.Time.cast(%{hour: 23, minute: nil}) ==
-           :error
-  end
-
-  test "cast tuple" do
-    assert Ecto.Time.cast({23, 50, 07}) == {:ok, @time}
-    assert Ecto.Time.cast({12, 40, 33, 30000}) == {:ok, @time_usec}
-    assert Ecto.Time.cast({00, 61, 33}) == :error
-  end
-
-  test "cast!" do
-    assert Ecto.Time.cast!("23:50:07") == @time
-
-    assert_raise Ecto.CastError, "cannot cast \"24:01:01\" to Ecto.Time", fn ->
-      Ecto.Time.cast!("24:01:01")
-    end
-  end
-
-  test "dump itself into a time tuple" do
-    assert Ecto.Time.dump(@time) == {:ok, {23, 50, 7, 0}}
-    assert Ecto.Time.dump(@time_usec) == {:ok, {12, 40, 33, 30000}}
-    assert Ecto.Time.dump({23, 50, 07}) == :error
-  end
-
-  test "load tuple" do
-    assert Ecto.Time.load({23, 50, 07}) == {:ok, @time}
-    assert Ecto.Time.load({12, 40, 33, 30000}) == {:ok, @time_usec}
-    assert Ecto.Time.load(@time) == :error
-  end
-
-  test "to_string" do
-    assert to_string(@time) == "23:50:07"
-    assert Ecto.Time.to_string(@time) == "23:50:07"
-
-    assert to_string(@time_usec) == "12:40:33.030000"
-    assert Ecto.Time.to_string(@time_usec) == "12:40:33.030000"
-
-    assert to_string(%Ecto.Time{hour: 1, min: 2, sec: 3, usec: 4})
-           == "01:02:03.000004"
-    assert Ecto.Time.to_string(%Ecto.Time{hour: 1, min: 2, sec: 3, usec: 4})
-           == "01:02:03.000004"
-  end
-
-  test "to_iso8601" do
-    assert Ecto.Time.to_iso8601(@time) == "23:50:07"
-    assert Ecto.Time.to_iso8601(@time_usec) == "12:40:33.030000"
-  end
-
-  test "to_erl and from_erl" do
-    assert @time |> Ecto.Time.to_erl |> Ecto.Time.from_erl == @time
-  end
-
-  test "inspect protocol" do
-    assert inspect(@time) == "#Ecto.Time<23:50:07>"
-    assert inspect(@time_usec) == "#Ecto.Time<12:40:33.030000>"
-  end
-
-  test "precision" do
-    assert %Ecto.Time{usec: 0} = Ecto.Time.utc
-    assert %Ecto.Time{usec: 0} = Ecto.Time.utc :sec
-  end
-end
-
-defmodule Ecto.DateTimeTest do
-  use ExUnit.Case, async: true
-
-  @datetime %Ecto.DateTime{year: 2015, month: 1, day: 23, hour: 23, min: 50, sec: 07, usec: 0}
-  @datetime_large %Ecto.DateTime{year: 10000, month: 1, day: 23, hour: 23, min: 50, sec: 07, usec: 0}
-  @datetime_zero %Ecto.DateTime{year: 2015, month: 1, day: 23, hour: 23, min: 50, sec: 0, usec: 0}
-  @datetime_usec %Ecto.DateTime{year: 2015, month: 1, day: 23, hour: 23, min: 50, sec: 07, usec: 8000}
-  @datetime_notime %Ecto.DateTime{year: 2015, month: 1, day: 23, hour: 0, min: 0, sec: 0, usec: 0}
-  @datetime_leapyear %Ecto.DateTime{year: 2000, month: 2, day: 29, hour: 23, min: 50, sec: 07, usec: 0}
-
-  test "cast itself" do
-    assert Ecto.DateTime.cast(@datetime) == {:ok, @datetime}
-    assert Ecto.DateTime.cast(@datetime_usec) == {:ok, @datetime_usec}
-    assert Ecto.DateTime.cast(@datetime_leapyear) == {:ok, @datetime_leapyear}
-  end
-
-  test "cast strings" do
-    assert Ecto.DateTime.cast("2015-01-23 23:50:07") == {:ok, @datetime}
-    assert Ecto.DateTime.cast("2015-01-23T23:50:07") == {:ok, @datetime}
-    assert Ecto.DateTime.cast("2015-01-23T23:50:07Z") == {:ok, @datetime}
-    assert Ecto.DateTime.cast("2015-01-23T23:50:07.000Z") == {:ok, @datetime}
-    assert Ecto.DateTime.cast("2000-02-29T23:50:07") == {:ok, @datetime_leapyear}
-    assert Ecto.DateTime.cast("2015-01-23P23:50:07") == :error
-
-    assert Ecto.DateTime.cast("2015-01-23T23:50:07.008") == {:ok, @datetime_usec}
-    assert Ecto.DateTime.cast("2015-01-23T23:50:07.008Z") == {:ok, @datetime_usec}
-    assert Ecto.DateTime.cast("2015-01-23T23:50:07.008000789") == {:ok, @datetime_usec}
-  end
-
-  test "cast maps" do
-    assert Ecto.DateTime.cast(%{"year" => "2015", "month" => "1", "day" => "23",
-                                "hour" => "23", "minute" => "50", "second" => "07"}) ==
-           {:ok, @datetime}
-
-    assert Ecto.DateTime.cast(%{year: 2015, month: 1, day: 23, hour: 23, minute: 50, second: 07}) ==
-           {:ok, @datetime}
-
-    assert Ecto.DateTime.cast(%{"year" => "", "month" => "", "day" => "", "hour" => "", "minute" => ""}) ==
-           {:ok, nil}
-
-    assert Ecto.DateTime.cast(%{year: nil, month: nil, day: nil, hour: nil, minute: nil}) ==
-           {:ok, nil}
-
-    assert Ecto.DateTime.cast(%{"year" => "2015", "month" => "1", "day" => "23",
-                                "hour" => "23", "minute" => "50"}) ==
-           {:ok, @datetime_zero}
-
-    assert Ecto.DateTime.cast(%{year: 2015, month: 1, day: 23, hour: 23, minute: 50}) ==
-           {:ok, @datetime_zero}
-
-    assert Ecto.DateTime.cast(%{year: 2015, month: 1, day: 23, hour: 23,
-                                minute: 50, second: 07, microsecond: 8_000}) ==
-           {:ok, @datetime_usec}
-
-    assert Ecto.DateTime.cast(%{"year" => 2015, "month" => 1, "day" => 23,
-                                "hour" => 23, "minute" => 50, "second" => 07,
-                                "microsecond" => 8_000}) ==
-           {:ok, @datetime_usec}
-
-    assert Ecto.DateTime.cast(%{"year" => "2015", "month" => "1", "day" => "23",
-                                "hour" => "", "minute" => "50"}) ==
-           :error
-
-    assert Ecto.DateTime.cast(%{year: 2015, month: 1, day: 23, hour: 23, minute: nil}) ==
-           :error
-  end
-
-  test "cast tuple" do
-    assert Ecto.DateTime.cast({{2015, 1, 23}, {23, 50, 07}}) == {:ok, @datetime}
-    assert Ecto.DateTime.cast({{2015, 1, 23}, {23, 50, 07, 8000}}) == {:ok, @datetime_usec}
-    assert Ecto.DateTime.cast({{2015, 1, 23}, {25, 50, 07, 8000}}) == :error
-  end
-
-  test "cast!" do
-    assert Ecto.DateTime.cast!("2015-01-23 23:50:07") == @datetime
-
-    assert_raise Ecto.CastError, "cannot cast \"2015-01-23P23:50:07\" to Ecto.DateTime", fn ->
-      Ecto.DateTime.cast!("2015-01-23P23:50:07")
-    end
-  end
-
-  test "dump itself to a tuple" do
-    assert Ecto.DateTime.dump(@datetime) == {:ok, {{2015, 1, 23}, {23, 50, 07, 0}}}
-    assert Ecto.DateTime.dump(@datetime_usec) == {:ok, {{2015, 1, 23}, {23, 50, 07, 8000}}}
-    assert Ecto.DateTime.dump({{2015, 1, 23}, {23, 50, 07}}) == :error
-  end
-
-  test "load tuple" do
-    assert Ecto.DateTime.load({{2015, 1, 23}, {23, 50, 07}}) == {:ok, @datetime}
-    assert Ecto.DateTime.load({{2015, 1, 23}, {23, 50, 07, 8000}}) == {:ok, @datetime_usec}
-    assert Ecto.DateTime.load(@datetime) == :error
-  end
-
-  test "from_date" do
-    assert Ecto.DateTime.from_date(%Ecto.Date{year: 2015, month: 1, day: 23}) == @datetime_notime
-  end
-
-  test "to_string" do
-    assert to_string(@datetime) == "2015-01-23 23:50:07"
-    assert Ecto.DateTime.to_string(@datetime) == "2015-01-23 23:50:07"
-
-    assert to_string(@datetime_usec) == "2015-01-23 23:50:07.008000"
-    assert Ecto.DateTime.to_string(@datetime_usec) == "2015-01-23 23:50:07.008000"
-  end
-
-  test "to_iso8601" do
-    assert Ecto.DateTime.to_iso8601(@datetime) == "2015-01-23T23:50:07"
-    assert Ecto.DateTime.to_iso8601(@datetime_usec) == "2015-01-23T23:50:07.008000"
-  end
-
-  test "to_erl and from_erl" do
-    assert @datetime |> Ecto.DateTime.to_erl |> Ecto.DateTime.from_erl == @datetime
-  end
-
-  test "inspect protocol" do
-    assert inspect(@datetime) == "#Ecto.DateTime<2015-01-23 23:50:07>"
-    assert inspect(@datetime_usec) == "#Ecto.DateTime<2015-01-23 23:50:07.008000>"
-    assert inspect(@datetime_large) == "#Ecto.DateTime<10000-01-23 23:50:07>"
-  end
-
-  test "from_unix!" do
-    assert Ecto.DateTime.from_unix!(1422057007, :seconds) == @datetime
-    assert Ecto.DateTime.from_unix!(1422057007008000, :microseconds) == @datetime_usec
-  end
-
-  test "precision" do
-    assert %Ecto.DateTime{usec: 0} = Ecto.DateTime.utc
-    assert %Ecto.DateTime{usec: 0} = Ecto.DateTime.utc :sec
-  end
-end
diff --git a/test/ecto/migration_test.exs b/test/ecto/migration_test.exs
index dabac85d..8296c55b 100644
--- a/test/ecto/migration_test.exs
+++ b/test/ecto/migration_test.exs
@@ -172,7 +172,7 @@ defmodule Ecto.MigrationTest do
   end
 
   test "forward: creates a table without precision option for numeric type" do
-    assert_raise ArgumentError, "Column cost is missing precision option", fn ->
+    assert_raise ArgumentError, "column cost is missing precision option", fn ->
       create(table(:posts)) do
         add :title, :string
         add :cost, :decimal, scale: 3
@@ -241,7 +241,7 @@ defmodule Ecto.MigrationTest do
   end
 
   test "forward: alter numeric column without specifying precision" do
-    assert_raise ArgumentError, "Column cost is missing precision option", fn ->
+    assert_raise ArgumentError, "column cost is missing precision option", fn ->
       alter table(:posts) do
         modify :cost, :decimal, scale: 5
       end
@@ -514,5 +514,16 @@ defmodule Ecto.MigrationTest do
     assert "SELECT 2" = last_command()
   end
 
+  test "references foreign keys types must be the same as primary defaults" do
+    %{runner: runner} = Process.get(:ecto_migration)
+    Agent.update(runner, fn state ->
+      config = Keyword.put(state.config, :migration_primary_key, [type: :binary_id])
+      Map.put(state, :config, config)
+    end)
+
+    assert references(:posts) ==
+           %Reference{table: "posts", column: :id, type: :binary_id}
+  end
+
   defp last_command(), do: Process.get(:last_command)
 end
diff --git a/test/ecto/multi_test.exs b/test/ecto/multi_test.exs
index a7d33d3e..83bcc9b7 100644
--- a/test/ecto/multi_test.exs
+++ b/test/ecto/multi_test.exs
@@ -405,7 +405,7 @@ defmodule Ecto.MultiTest do
       assert {:messages, [{:insert, {nil, "comments"}}]} == Process.info(self(), :messages)
       assert %Comment{} = changes.insert
       assert "ok" == changes.run
-      assert error.errors == [x: {"has already been taken", []}]
+      assert error.errors == [x: {"has already been taken", [constraint: :unique]}]
       refute Map.has_key?(changes, :update)
     end
 
diff --git a/test/ecto/query/builder/join_test.exs b/test/ecto/query/builder/join_test.exs
index e8665578..869036a5 100644
--- a/test/ecto/query/builder/join_test.exs
+++ b/test/ecto/query/builder/join_test.exs
@@ -66,6 +66,12 @@ defmodule Ecto.Query.Builder.JoinTest do
     join("posts", :left, [p], c in assoc(p, ^assoc), true)
   end
 
+  test "accepts subqueries" do
+    subquery = "comments"
+    join("posts", :left, [p], c in subquery(subquery), true)
+    join("posts", :left, [p], c in subquery(subquery, prefix: "sample"), true)
+  end
+
   test "raises on invalid qualifier" do
     assert_raise ArgumentError,
                  ~r/invalid join qualifier `:whatever`/, fn ->
diff --git a/test/ecto/query_test.exs b/test/ecto/query_test.exs
index 3f71812d..f4f2913d 100644
--- a/test/ecto/query_test.exs
+++ b/test/ecto/query_test.exs
@@ -32,6 +32,13 @@ defmodule Ecto.QueryTest do
       from(p in "posts", select: [macro_map(^key)])
     end
 
+    defmacrop macrotest(x), do: quote(do: is_nil(unquote(x)) or unquote(x) == "A")
+    defmacrop deeper_macrotest(x), do: quote(do: macrotest(unquote(x)) or unquote(x) == "B")
+    test "allows macro in where" do
+      _ = from(p in "posts", where: p.title == "C" or macrotest(p.title))
+      _ = from(p in "posts", where: p.title == "C" or deeper_macrotest(p.title))
+    end
+
     test "does not allow nils in comparison at compile time" do
       assert_raise Ecto.Query.CompileError,
                    ~r"comparison with nil is forbidden as it is unsafe", fn ->
@@ -83,6 +90,18 @@ defmodule Ecto.QueryTest do
       assert subquery(subquery("posts")).query.from == {"posts", nil}
       assert subquery(subquery("posts").query).query.from == {"posts", nil}
     end
+
+    test "prefix is not applied if left blank" do
+      assert subquery("posts").query.prefix == nil
+      assert subquery(subquery("posts")).query.prefix == nil
+      assert subquery(subquery("posts").query).query.prefix == nil
+    end
+
+    test "applies prefix to the subquery's query if provided" do
+      assert subquery("posts", prefix: "my_prefix").query.prefix == "my_prefix"
+      assert subquery(subquery("posts", prefix: "my_prefix")).query.prefix == "my_prefix"
+      assert subquery(subquery("posts", prefix: "my_prefix").query).query.prefix == "my_prefix"
+    end
   end
 
   describe "bindings" do
diff --git a/test/ecto/repo/supervisor_test.exs b/test/ecto/repo/supervisor_test.exs
index 87fd31f9..1fddf791 100644
--- a/test/ecto/repo/supervisor_test.exs
+++ b/test/ecto/repo/supervisor_test.exs
@@ -59,10 +59,28 @@ defmodule Ecto.Repo.SupervisorTest do
     assert {:port, 12345} in url
   end
 
+  test "parse_url query string" do
+    encoded_url = URI.encode("ecto://eric:it+й@host:12345/mydb?ssl=true&pool_timeout=1000&timeout=1000&pool_size=42")
+    url = parse_url(encoded_url)
+    assert {:password, "it+й"} in url
+    assert {:username, "eric"} in url
+    assert {:hostname, "host"} in url
+    assert {:database, "mydb"} in url
+    assert {:port, 12345} in url
+    assert {:ssl, true} in url
+    assert {:timeout, 1000} in url
+    assert {:pool_timeout, 1000} in url
+    assert {:pool_size, 42} in url
+  end
+
   test "parse_url returns no config when blank" do
     assert parse_url("") == []
   end
 
+  test "parse_url keeps false values" do
+    assert {:ssl, false} in parse_url("ecto://eric:it+й@host:12345/mydb?ssl=false")
+  end
+
   test "parse_urls empty username/password" do
     url = parse_url("ecto://host:12345/mydb")
     assert !Keyword.has_key?(url, :username)
@@ -85,5 +103,15 @@ defmodule Ecto.Repo.SupervisorTest do
     assert_raise Ecto.InvalidURLError, ~r"path should be a database name", fn ->
       parse_url("ecto://eric:hunter2@host:123")
     end
+
+    assert_raise Ecto.InvalidURLError, ~r"unsupported query parameter `uknown_param`", fn ->
+      parse_url("ecto://eric:it+й@host:12345/mydb?uknown_param=value")
+    end
+
+    for key <- ["timeout", "pool_size", "pool_timeout"] do
+      assert_raise Ecto.InvalidURLError, ~r"can not parse value `not_an_int` for parameter `#{key}` as an integer", fn ->
+        parse_url("ecto://eric:it+й@host:12345/mydb?#{key}=not_an_int")
+      end
+    end
   end
 end
diff --git a/test/ecto/repo_test.exs b/test/ecto/repo_test.exs
index 97e9ea46..837c4154 100644
--- a/test/ecto/repo_test.exs
+++ b/test/ecto/repo_test.exs
@@ -112,7 +112,6 @@ defmodule Ecto.RepoTest do
   test "validates get_by" do
     TestRepo.get_by(MySchema, id: 123)
     TestRepo.get_by(MySchema, %{id: 123})
-    TestRepo.get_by(MySchema, id: nil)
 
     message = ~r"value `:atom` in `where` cannot be cast to type :id in query"
     assert_raise Ecto.Query.CastError, message, fn ->
diff --git a/test/ecto/schema_test.exs b/test/ecto/schema_test.exs
index 5e2e54fc..992fb51d 100644
--- a/test/ecto/schema_test.exs
+++ b/test/ecto/schema_test.exs
@@ -305,7 +305,7 @@ defmodule Ecto.SchemaTest do
   end
 
   test "invalid field type" do
-    assert_raise ArgumentError, "invalid type {:apa} for field :name", fn ->
+    assert_raise ArgumentError, "invalid or unknown type {:apa} for field :name", fn ->
       defmodule SchemaInvalidFieldType do
         use Ecto.Schema
 
@@ -324,6 +324,16 @@ defmodule Ecto.SchemaTest do
         end
       end
     end
+
+    assert_raise ArgumentError, ~r/schema Ecto.SchemaTest.Schema is not a valid type for field :name/, fn ->
+      defmodule SchemaInvalidFieldType do
+        use Ecto.Schema
+
+        schema "invalidtype" do
+          field :name, Schema
+        end
+      end
+    end
   end
 
   test "fail invalid schema" do