Skip to content

fix(postgresql): type a DateTime parameter from its Kind, not its CLR type (weasel#403) - #409

Merged
jeremydmiller merged 1 commit into
masterfrom
fix/403-utc-datetime-parameter-type
Jul 30, 2026
Merged

fix(postgresql): type a DateTime parameter from its Kind, not its CLR type (weasel#403)#409
jeremydmiller merged 1 commit into
masterfrom
fix/403-utc-datetime-parameter-type

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #403. Narrows #404.

The bug

cmd.AddNamedParameter("n", DateTime.UtcNow);
await cmd.ExecuteScalarAsync();
ArgumentException: Cannot write DateTime with Kind=UTC to PostgreSQL type
'timestamp without time zone', consider using 'timestamp with time zone'.

Verified against a live Postgres. It only ever surfaced at execution time, so a unit assertion on the stamped type would not have caught the original report.

Cause

With no explicit type, Weasel typed the parameter from the value's CLR type, and PostgresqlProvider maps typeof(DateTime) to Timestamp unconditionally. But Postgres' choice depends on the value — Npgsql resolves Kind=Utc to timestamptz, Kind=Local/Unspecified to timestamp. A per-type mapping cannot express that.

DateTime[] and List<DateTime> failed identically through the array branch.

Change

New IDatabaseProvider<,,>.ToParameterTypeForValue(object), defaulting to today's ToParameterType(value.GetType()) — so no other provider changes behaviour. It is a default interface method plus a public virtual on DatabaseProvider<,,>, so external implementers are unaffected. PostgresqlProvider overrides it to read DateTime.Kind, for scalars and for IReadOnlyList<DateTime> (covers arrays and List<T>; Npgsql forbids mixing Kinds in one array, so the first element decides).

Three call sites were typing per-CLR-type and now route through it:

site note
DatabaseProvider.AddNamedParameter the reported one
CommandBuilderBase.AppendParameter same bug, reached via the interface
CommandExtensions.With(cmd, name, DateTime) hard-coded NpgsqlDbType.Timestamp, so .With(name, DateTime.UtcNow) always threw

The second and third were found while fixing the first; each would have left the bug reachable.

Column types are deliberately untouched

A column has one type, so DDL stays per-type — GetDatabaseType(typeof(DateTime)) still returns timestamp without time zone, now pinned by a test. Only parameters follow their value.

Verification

Live Postgres, every case previously throwing:

value before after
DateTime.UtcNow ✗ throws TimestampTz
DateTime[] all Utc ✗ throws Array | TimestampTz
List<DateTime> all Utc ✗ throws Array | TimestampTz
DateTime.Now Timestamp Timestamp
DateTime[] empty Array | Timestamp unchanged ✓
string / Guid / int / DateTimeOffset / string[] unchanged ✓

Includes an integration test that actually writes and reads back a UTC DateTime, since the unit-level assertion alone would not have caught this.

Suites: Postgres 798 passed on all four matrix legs (net9.0/net10.0 × case-sensitive true/false), Core 21, SQLite 361. MSSQL / Oracle / MySQL covered by CI — unchanged by the default implementation.

On #404

This removes the observable AddParameter / AddNamedParameter divergence for DateTime: both now yield TimestampTz for a UTC value, one by deferring to Npgsql and one by resolving per value. What remains of #404 is narrower — AddNamedParameter still throws for types with no mapping where AddParameter succeeds. Left open deliberately; it is an API-direction call, not a bug fix.

🤖 Generated with Claude Code

… type (weasel#403)

AddNamedParameter with a UTC DateTime threw at execution time:

    ArgumentException: Cannot write DateTime with Kind=UTC to PostgreSQL type
    'timestamp without time zone', consider using 'timestamp with time zone'

When no explicit type is supplied, Weasel typed the parameter from the
value's CLR type, and PostgresqlProvider maps typeof(DateTime) to Timestamp
unconditionally. But Postgres' choice depends on the *value*: Npgsql
resolves Kind=Utc to timestamptz and Kind=Local/Unspecified to timestamp. A
per-type mapping cannot express that, so every UTC DateTime got the wrong
type and was rejected on write. DateTime[] and List<DateTime> failed the
same way via the array branch.

Add IDatabaseProvider<,,>.ToParameterTypeForValue(object), defaulting to
today's ToParameterType(value.GetType()) so no other provider changes
behaviour. It is a default interface method plus a public virtual on
DatabaseProvider<,,>, so external implementers are unaffected.
PostgresqlProvider overrides it to read DateTime.Kind, for scalars and for
IReadOnlyList<DateTime> (covering arrays and List<T>; Npgsql forbids mixing
Kinds in one array, so the first element decides).

Three call sites were typing per-CLR-type and now route through it:

  - DatabaseProvider.AddNamedParameter
  - CommandBuilderBase.AppendParameter -- same bug, reached via the interface
  - CommandExtensions.With(command, name, DateTime) -- hard-coded
    NpgsqlDbType.Timestamp, so .With(name, DateTime.UtcNow) always threw

Column types are unaffected and stay per-type: a column has one type, only
parameters follow their value. GetDatabaseType(typeof(DateTime)) still
returns "timestamp without time zone", pinned by a test.

Verified against a live Postgres: UTC scalar, UTC array, UTC List and the
empty array now all round-trip, and string/Guid/int/DateTimeOffset/string[]
are unchanged. Postgres suite 796 passed, Core 21, SQLite 361.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit f06c609 into master Jul 30, 2026
16 checks passed
@jeremydmiller
jeremydmiller deleted the fix/403-utc-datetime-parameter-type branch July 30, 2026 16:00
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.

AddNamedParameter sends a UTC DateTime as 'timestamp without time zone' and throws at execution

1 participant