Skip to content

bug: Demonstrate AshPostgres.Timestamptz operator overloading causing precision loss in keyset pagination#760

Merged
zachdaniel merged 1 commit into
mainfrom
bug/timestamptz-second-truncation
May 25, 2026
Merged

bug: Demonstrate AshPostgres.Timestamptz operator overloading causing precision loss in keyset pagination#760
zachdaniel merged 1 commit into
mainfrom
bug/timestamptz-second-truncation

Conversation

@sevenseacat
Copy link
Copy Markdown
Contributor

Discovered while investigating sevenseacat/cinder#176 and tracing it back to a problem upstream.

The failing test demonstrates the problem - sorting by the timestamp causes issues when navigating between pages, as precision is lost in the timestamp comparison.

The root cause appears to be ash_postgres/lib/types/timestamptz.ex:44-79 - operator_overloads/0 overloads only <, <=, >, >=, and the match check via Ash.Expr.determine_types uses matches_type?, which is permissive enough that any %DateTime{} value matches the Timestamptz slot. So users with plain UtcDatetimeUsec columns get their ordering comparisons silently force-cast to seconds-precision Timestamptz, while their equality comparisons keep microsecond precision.

Demo:

iex(1)> resource = CinderSort.Locations.Location
CinderSort.Locations.Location

iex(2)> attr = Ash.Resource.Info.field(resource, :inserted_at)
%Ash.Resource.Attribute{
  name: :inserted_at,
  type: Ash.Type.UtcDatetimeUsec,
  ...
  }
}

iex(3)> Ash.Type.storage_type(attr.type, attr.constraints)
:utc_datetime_usec

iex(4)> ref = %Ash.Query.Ref{attribute: attr, relationship_path: [], resource: resource}
inserted_at

iex(5)> value = ~U[2026-05-25 05:34:24.130812Z]
~U[2026-05-25 05:34:24.130812Z]

iex(6)> Ash.Expr.determine_types(Ash.Query.Operator.Eq, [ref, value])
{[
   {Ash.Type.UtcDatetimeUsec,
    [precision: :microsecond, cast_dates_as: :start_of_day, timezone: :utc]},
   {Ash.Type.UtcDatetimeUsec,
    [precision: :microsecond, cast_dates_as: :start_of_day, timezone: :utc]}
 ], {Ash.Type.Boolean, []}}

iex(7)> Ash.Expr.determine_types(Ash.Query.Operator.LessThan, [ref, value])
{[{AshPostgres.Timestamptz, []}, {AshPostgres.Timestamptz, []}],
 {Ash.Type.Boolean, []}}

iex(8)> Ash.Expr.determine_types(Ash.Query.Operator.GreaterThan, [ref, value])
{[{AshPostgres.Timestamptz, []}, {AshPostgres.Timestamptz, []}],
 {Ash.Type.Boolean, []}}

Contributor checklist

Leave anything that you believe does not apply unchecked.

  • I accept the AI Policy, or AI was not used in the creation of this PR.
  • Bug fixes include regression tests
  • Chores
  • Documentation changes
  • Features include unit/acceptance tests
  • Refactoring
  • Update dependencies

…or overloading causing precision loss in keyset pagination
@zachdaniel zachdaniel merged commit 0089dc8 into main May 25, 2026
105 of 120 checks passed
@sevenseacat sevenseacat deleted the bug/timestamptz-second-truncation branch May 25, 2026 14:04
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