Skip to content

fix(parser)!: read date values without an offset as utc, with DateTimeKindForValuesWithoutOffset - #116

Merged
pdevito3 merged 6 commits into
v2from
fm/qk-breaking-prs-114
Oct 3, 2026
Merged

pdevito3 merged 6 commits into
v2from
fm/qk-breaking-prs-114

Conversation

@pdevito3

@pdevito3 pdevito3 commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

A DateTime or DateTimeOffset filter value without an offset is now UTC by default. The new setting DateTimeKindForValuesWithoutOffset (default DateTimeKind.Utc) selects a different kind.

 ParseFilter(input, config)
+  kind = config.DateTimeKindForValuesWithoutOffset   // Utc by default
   DateTime value without an offset
-    scalar: Local (AssumeLocal)
-    list and custom operation: a different rule (AdjustToUniversal only)
+    every path (scalar, quoted, ^^ [...] list, custom operation): kind
   DateTime value with an offset
+    converted to the UTC time, then given the kind
   DateTimeOffset value without an offset
-    the offset of the server
+    UTC, or the offset of the server when kind is Local
DateTimeKindForValuesWithoutOffset 2024-01-15T08:00:00 becomes Use it for
Utc (default) 08:00, Kind=Utc timestamp with time zone (timestamptz) on Npgsql, and most APIs
Unspecified 08:00, Kind=Unspecified timestamp without time zone on Npgsql
Local 08:00 in the server time zone, Kind=Local the v1.14.2 scalar behavior

The setting is on QueryKitSettings, QueryKitConfiguration, and IQueryKitFilterBehavior (a default interface member that returns Utc, so existing implementers still compile). The parser sets it for one parse on the current thread and restores it after the parse.

Breaking change

Npgsql needs a Utc DateTime for a timestamptz column. It rejects a Utc DateTime for a timestamp without time zone column.

  • timestamptz columns: a filter value without an offset threw on Postgres in v1.14.2. Now it works. No action is necessary.
  • timestamp without time zone columns: a filter value without an offset now throws "'timestamp without time zone' literal cannot be generated for a UTC DateTime". To correct this, set the kind to Unspecified:
var config = new QueryKitConfiguration(settings =>
{
    settings.DateTimeKindForValuesWithoutOffset = DateTimeKind.Unspecified;
});
  • To keep the v1.14.2 time zone rule (the server time zone), set the kind to Local.
  • The setting is global for one filter. One filter cannot compare a timestamptz column and a timestamp without time zone column on Npgsql.

The README has a new section, "Date and Time Values Without an Offset".

Evidence

verify-querykit harness, Postgres, machine time zone EET (UTC+2). CreatedAt is timestamptz. LocalCreatedAt is timestamp without time zone. Pancakes has 2024-01-15 08:00.

Filter Config v2 before This PR
CreatedAt == "2024-01-15T08:00:00" default, literal and parameterized ArgumentException: "a UTC DateTime is required" Pancakes
CreatedAt ^^ [2024-01-15T08:00:00] default, literal and parameterized ArgumentException: "a UTC DateTime is required" Pancakes
LocalCreatedAt == "2024-01-15T08:00:00" default Pancakes throws (see Breaking change)
LocalCreatedAt == "2024-01-15T08:00:00" unspecified-datetime, literal and parameterized not available Pancakes, TIMESTAMP '2024-01-15T08:00:00'
LocalCreatedAt ^^ [2024-01-15T10:00:00+02:00] unspecified-datetime not available Pancakes

Tests:

  • Unit, DateTimeKindForValuesWithoutOffsetTests: the Utc, Unspecified, and Local kinds for the scalar, quoted, list, quoted-list, and custom operation forms, with and without parameters. Also values with an offset, DateTimeOffset, the restore after the parse, and an interface implementer without the setting.
  • Integration, Postgres (Testcontainers):
    • timestamptz_value_without_offset_is_utc_by_default: 24 cases on SpecificDateTime and SpecificDate.
    • timestamp_without_time_zone_value_matches_with_unspecified_kind: 12 cases on the new LocalDateTime column (migration AddPersonLocalDateTime).
  • dotnet test after the rebase on v2: 564 unit tests and 357 integration tests pass. The build with -warnaserror has 0 warnings. A nullable warning from fix(filter)!: resolve query names in the grammar again #154 in AliasCultureTests is corrected here.

Merge Danger

Door: two-way

The change is code only. The new test column is in the test web project, not in the library.

Blast Radius: consumers

Each consumer that filters a timestamp without time zone column on Npgsql with a value without an offset must set Unspecified. On a server that is not on UTC, a consumer that depended on the server time zone gets different rows. That consumer must set Local.

@pdevito3
pdevito3 force-pushed the fm/qk-breaking-prs-114 branch 3 times, most recently from 20a5851 to 85c0b76 Compare October 1, 2026 21:24
@pdevito3
pdevito3 changed the base branch from main to v2 October 3, 2026 11:38
@pdevito3
pdevito3 force-pushed the fm/qk-breaking-prs-114 branch from 85c0b76 to 8b550ea Compare October 3, 2026 11:55
@pdevito3 pdevito3 changed the title fix(parser)!: read date values without an offset as utc fix(parser)!: read date values without an offset as utc, with DateTimeKindForValuesWithoutOffset Oct 3, 2026
@pdevito3
pdevito3 force-pushed the fm/qk-breaking-prs-114 branch from 8b550ea to 31588be Compare October 3, 2026 11:58
A DateTime or DateTimeOffset value without an offset was read in the time zone of the server. The same filter matched different rows on servers in different zones. List values and custom operation values also used a different rule from scalar values.

A value without an offset is now read as UTC. A DateTime value with an offset is converted to UTC. The scalar, list, and custom operation paths use the same two helpers.

BREAKING CHANGE: a DateTime or DateTimeOffset filter value without an offset is read as UTC, not in the time zone of the server. A scalar DateTime value now has DateTimeKind.Utc, not DateTimeKind.Local. To keep a local time, send the offset in the value, for example 2022-07-01T00:00:03+03:00.
@pdevito3
pdevito3 force-pushed the fm/qk-breaking-prs-114 branch from 31588be to be6786c Compare October 3, 2026 12:00
@pdevito3
pdevito3 merged commit ffd3f21 into v2 Oct 3, 2026
2 checks passed
@pdevito3
pdevito3 deleted the fm/qk-breaking-prs-114 branch October 3, 2026 12:02
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.

1 participant