Skip to content

Normalize offset strings to UTC when casting to naive types - #4796

Closed
youdie006 wants to merge 1 commit into
elixir-ecto:masterfrom
youdie006:naive-cast-string-offset
Closed

youdie006 wants to merge 1 commit into
elixir-ecto:masterfrom
youdie006:naive-cast-string-offset

Conversation

@youdie006

Copy link
Copy Markdown
Contributor

#4775 normalized a non-UTC %DateTime{} to UTC for :date, :naive_datetime and :naive_datetime_usec, and #4792 did the same for :time. An ISO 8601 string carrying the same offset still goes through NaiveDateTime.from_iso8601, which drops the offset, so one instant casts two ways:

input :naive_datetime :date :utc_datetime
"2020-06-01T00:30:07+02:00" ~N[2020-06-01 00:30:07] ~D[2020-06-01] ~U[2020-05-31 22:30:07Z]
the same instant as %DateTime{} ~N[2020-05-31 22:30:07] ~D[2020-05-31] ~U[2020-05-31 22:30:07Z]

This parses with DateTime.from_iso8601 first and falls back to NaiveDateTime.from_iso8601, so strings without an offset, including Phoenix datetime-local values, are unchanged. It follows the conclusion in #2054 that a dropped offset should either be converted or rejected. If you would rather reject non-zero offsets for the naive types, that is a small change and I can switch to it.

Verification

Base f4d84a1a. New rows go into the existing :date, :naive_datetime and :naive_datetime_usec cast tests, mirroring the :utc_datetime offset rows at test/ecto/type_test.exs:1006 and :1011.

row lib/ecto/type.ex md5 test/ecto/type_test.exs
master e91eacf61b 3 failures: the new date, naive and naive_usec rows
this PR 27ba9a1702 0 failures
date site left as before 43d4775fc6 1 failure: the date row
require an offset 9957058100 5 failures, all existing rows such as cast(:date, "2015-12-31T00:00:00")

The CI unit-test steps on Elixir 1.18 / OTP 27: mix deps.unlock --check-unused, mix compile --warnings-as-errors, mix test (97 doctests, 1512 tests, 0 failures); mix format --check-formatted on both files. The same on Elixir 1.14.5 / OTP 26 passes. Not run: the exact OTP pins of the matrix and the Earthly integration job.

Written with AI assistance (Claude); the measurements above were run locally and I have reviewed the change.

elixir-ecto#4775 normalized a non-UTC %DateTime{} to UTC for :date, :naive_datetime and
:naive_datetime_usec, but an ISO 8601 string with the same offset still went
through NaiveDateTime.from_iso8601, which drops the offset. The same instant
cast two ways gave two values:

    cast(:naive_datetime, "2020-06-01T00:30:07+02:00")  ~N[2020-06-01 00:30:07]
    cast(:naive_datetime, <the same instant as %DateTime{}>)  ~N[2020-05-31 22:30:07]

Parse with DateTime.from_iso8601 first and fall back to
NaiveDateTime.from_iso8601, so strings without an offset are unchanged.
@josevalim

Copy link
Copy Markdown
Member

Thank you. Upon further inspection, I am reverting the other two PRs, because the dropping of offsets is exactly how Elixir behaves for NaiveDateTime, Time, Date when they pass a DateTime. In other words, we respect the wall time of said datetimes. Which makes sense, if you ask Date.days_in_month, you want the current datetime, not the UTC variant.

@josevalim josevalim closed this Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants