Changeset truncates Time, DateTime and NaiveDateTime to whole seconds

Ash 3.5.8 is removing microseconds from Time, DateTime and NaiveDateTime, which I think is a bug. I've noticed this in a create changeset. For example ~U[2025-05-09 08:02:21.294214Z] is truncated to ~U[2025-05-09 08:02:21Z].
14 Replies
Rebecca Le
Rebecca Le2w ago
do you have an example of code that does this?
Matt Beanland
Matt BeanlandOP2w ago
I realise there is such a thing as :utc_datetime_usec which has microsec, but I'm trying to build a datalayer which reads existing UTC dateTime and Time to correct elixir types attribute :datetime, :datetime, public?: true attribute :time, :time, public?: true
Matt Beanland
Matt BeanlandOP2w ago
Yes I've got a failed test on ash_neo4j on the 9-property-types branch https://github.com/diffo-dev/ash_neo4j/tree/9-property-types
GitHub
GitHub - diffo-dev/ash_neo4j at 9-property-types
Ash Neo4j datalayer. Contribute to diffo-dev/ash_neo4j development by creating an account on GitHub.
sevenseacat
sevenseacat2w ago
can you narrow it down a little?
Matt Beanland
Matt BeanlandOP2w ago
There is a single test case that is failing, it is showing that ash create (where I inject a time property defined as above) is truncating the time / date_time and naive_datetime values in the Test.Type resource to whole seconds, actually in the changeset rather than the create. The failing test is in ash_neo4j_test.exs and is “type node can be created using ash with properties” It is comparing the injected and returned property values
Matt Beanland
Matt BeanlandOP2w ago
Microseconds are important …
No description
Matt Beanland
Matt BeanlandOP2w ago
It is actually the Ecto.Type.cast which is truncating the microseconds, for example iex(13)> time = Time.utc_now() ~T[09:53:47.984771] iex(14)> Ecto.Type.cast(:time, time) {:ok, ~T[09:53:47]} iex(15)> {:ok, ecto_time} = Ecto.Type.cast(:time, time) {:ok, ~T[09:53:47]} iex(16)> ecto_time ~T[09:53:47] iex(17)> time == ecto_time false iex(18)> time |> Map.from_struct() %{ microsecond: {984771, 6}, second: 47, calendar: Calendar.ISO, minute: 53, hour: 9 } iex(19)> ecto_time |> Map.from_struct() %{microsecond: {0, 0}, second: 47, calendar: Calendar.ISO, minute: 53, hour: 9}
ZachDaniel
ZachDaniel2w ago
could you link me to the specific test? 🤔 actually I'm not sure I understand If you're using just :datetime, it will truncate the usec You shouldn't first Ecto.Type.cast the value you should use Ash.Type.cast_stored(type, initialized_constraints)
Matt Beanland
Matt BeanlandOP2w ago
Yes I named it above. The issue is with microsecond. In Ecto.Type.Time microsecond is an integer, in Elixir’s Time it is a Calender.microsecond struct, so ecto doesn’t cast it. Ash time.ex can fix by converting time struct to map or tuple ahead of cast, decapsulating microsecond. I can PR ash if you like? I’m not casting using Ecto, ash is in time.ex I was just showing the issue with the cast One easy way to test it in ash is by modifying time_test.exs to inject/expect times with and without microseconds. I’m building a datalayer to support Time, DateTime, etc so I need a solution for time with usec. I’m aware of utc_datetime_usec in Ash. I’m not sure why Ash.Type.Time, Ash.Type.DateTime and Ash.Type.NaiveDateTime are lossy when they are used to store corresponding Elixir types but I think this is wrong.
ZachDaniel
ZachDaniel2w ago
So, the :time type explicitly does not support usecs we just need to add :time_usec that does Just like :utc_datetime does not support usec either and you're meant to use :utc_datetime_usec if you want usec precision
Matt Beanland
Matt BeanlandOP2w ago
OK, we should have naive_datetime_usec too. If the intent of existing types is to never have microseconds we should nerf the Calendar.microsecond struct in ash before the ecto cast or one day if someone fixes ecto to accept the elixir types completely rather than accidentally then ash users of these native types will still have zeroed microseconds in the database. Do you want me to raise a PR along these lines? I’m not sure it is explicit, looks accidental. It isn’t in the ash docs… I understand fixing it might be disruptive to those with ash in prod.
ZachDaniel
ZachDaniel2w ago
It may not be documented, but it is an intentional split, modelled after ecto, and I'm confident they wouldn't consider it a bug either (hence why they have :time and :time_usec) We only really did it because they did, though FWIW, and at the time we were making these initial types we were trying to stay close to Ecto's patterns
def cast_input(
%DateTime{microsecond: {_, _} = microseconds} = datetime,
[{:precision, :second} | _] = constraints
)
when microseconds != {0, 0} do
cast_input(%{datetime | microsecond: {0, 0}}, constraints)
end
def cast_input(
%DateTime{microsecond: {_, _} = microseconds} = datetime,
[{:precision, :second} | _] = constraints
)
when microseconds != {0, 0} do
cast_input(%{datetime | microsecond: {0, 0}}, constraints)
end
we have pretty explicit code for this, so I'm not really sure what you mean 🤔 But more concretely, we should add a :precision option for the :time type, and default it to :second in the same way and then we can define:
defmodule Ash.Type.TimeUsec do
use Ash.Type.NewType, subtype_of: :time, constraints: [precision: :microsecond]
end
defmodule Ash.Type.TimeUsec do
use Ash.Type.NewType, subtype_of: :time, constraints: [precision: :microsecond]
end
and add a short code for it. PR/issues welcome for that 🙂
Matt Beanland
Matt BeanlandOP2w ago
Thanks Zach, will do
Matt Beanland
Matt BeanlandOP2w ago
GitHub
Support time with microsecond precision, :time_usec · Issue #2022 ...
Ash supports types with short name :datetime and :utc_datetime_usec, but lacks support for time_usec. I'd prefer date, dateTime and time to behave like their Elixir counterparts rather than Ect...
GitHub
feat - support :time_usec by matt-beanland · Pull Request #2023 ·...
Contributor checklist [ n/a] Bug fixes include regression tests [✅] Chores [✅] Documentation changes [✅] Features include unit/acceptance tests [n/a] Refactoring [n/a ] Update dependencies

Did you find this page helpful?