From 14d1c5aa1c2da3510e404210033f8955637cc966 Mon Sep 17 00:00:00 2001 From: youdie006 Date: Tue, 15 Sep 2026 14:06:49 +0900 Subject: [PATCH] Normalize DateTime casts to UTC for :time and :time_usec #4775 moved :date, :naive_datetime and :naive_datetime_usec onto the UTC instant when given a non-UTC %DateTime{}, but cast_time/1 has no %DateTime{} clause, so it falls through to the map clauses and rebuilds a Time from the wall-clock fields. That leaves a schema casting one %DateTime{} into a :date field and a :time field with 2020-05-31 and 00:30:07, which is neither the UTC instant nor the submitted local time. Route %DateTime{} through cast_utc_datetime/1 first, the same way cast_date/1 and cast_naive_datetime/1 do. --- CHANGELOG.md | 1 + lib/ecto/type.ex | 7 +++++++ test/ecto/type_test.exs | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8abd9341bd..c3b0e76bff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * [Ecto.Query] Fix subqueries in interpolated join filters (this changes the representation of `JoinExpr.on` fields to a `BooleanExpr`) * [Ecto.Query] Fix selecting lists of fields and using `map/2` from subqueries + * [Ecto.Type] Normalize `DateTime` casts to UTC for `:time` and `:time_usec` ## v3.14.2 (2026-08-14) diff --git a/lib/ecto/type.ex b/lib/ecto/type.ex index f697dc562f..107cafce71 100644 --- a/lib/ecto/type.ex +++ b/lib/ecto/type.ex @@ -1114,6 +1114,13 @@ defmodule Ecto.Type do end end + defp cast_time(%DateTime{} = datetime) do + case cast_utc_datetime(datetime) do + {:ok, datetime} -> {:ok, DateTime.to_time(datetime)} + :error -> :error + end + end + defp cast_time(%{"hour" => empty, "minute" => empty}) when empty in ["", nil], do: {:ok, nil} diff --git a/test/ecto/type_test.exs b/test/ecto/type_test.exs index f2bc5aafe3..b84aa03961 100644 --- a/test/ecto/type_test.exs +++ b/test/ecto/type_test.exs @@ -532,6 +532,9 @@ defmodule Ecto.TypeTest do assert Ecto.Type.cast(:time, ~D[2016-11-11]) == :error + + assert Ecto.Type.cast(:time, @non_utc_datetime) == + {:ok, ~T[22:30:07]} end test "dump" do @@ -607,6 +610,8 @@ defmodule Ecto.TypeTest do test "cast from DateTime" do utc_datetime = DateTime.from_naive!(~N[2016-11-11 23:30:10], "Etc/UTC") assert Ecto.Type.cast(:time_usec, utc_datetime) == {:ok, ~T[23:30:10.000000]} + + assert Ecto.Type.cast(:time_usec, @non_utc_datetime) == {:ok, ~T[22:30:07.008000]} end test "cast from Date" do