fix(postgresql): seed Npgsql's global type mapper before the test suite runs (weasel#398) - #402
Merged
jeremydmiller merged 1 commit intoJul 30, 2026
Conversation
…uns (weasel#398)
`CommandExtensionsTests.add_first_parameter` failed in 3 of 4 Postgres CI
jobs and went green on an unchanged re-run.
The cause is not a bad parameter type. `command.AddParameter("a")` passes
`dbType: null`, so `DatabaseProvider.AddParameter` never calls
`SetParameterType` and Weasel's `determineParameterType` /
`ParameterTypeMemo` are not on this path at all. The assertion is reading
Npgsql's *inference* from the value, and that answers out of Npgsql's
process-global type mapper, which stays empty — reporting
`NpgsqlDbType.Unknown` for everything — until the process constructs its
first `NpgsqlDataSource` or `NpgsqlConnection` with a connection string.
So the test only passed when some other test in the same process happened
to build a data source first. Under xUnit v3's parallel collections that
is a race. In a cold process it fails 100% of the time, which is how it
reproduces locally.
Seed the mapper once from the existing module initializer, before any test
runs. `new NpgsqlDataSourceBuilder()` is enough; it opens no sockets and
needs no connection string.
Also pin the surrounding contract so this cannot regress:
- `add_parameter_honors_an_explicit_type` covers the path Weasel actually
controls, and holds regardless of global mapper state.
- `add_parameter_without_a_type_defers_to_npgsql_rather_than_weasels_mapping`
guards against "fixing" this by having `AddParameter` stamp Weasel's own
mapping onto untyped parameters. That would be a regression: Weasel maps
`DateTime` to `timestamp without time zone` for every value, while Npgsql
resolves per value, and writing a `Kind=Utc` DateTime as
`timestamp without time zone` throws at execution time. Verified against a
live Postgres. `DateTime` is the only divergence across the common CLR
types.
No product code changes. Full suite green on all four CI matrix legs
(net9.0/net10.0 x case-sensitive true/false), 786 passed each.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Jul 30, 2026
AddNamedParameter sends a UTC DateTime as 'timestamp without time zone' and throws at execution
#403
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #398.
TL;DR
The parameter type is not silently wrong.
add_first_parameterasserts on Npgsql's inference, and that inference depends on process-global state that another test happens to initialize first. In a cold process it fails 100% of the time.What actually happens
command.AddParameter("a")passesdbType: null, soDatabaseProvider.AddParameternever callsSetParameterType:PostgresqlProvider.determineParameterType,ResolveNpgsqlDbTypeandParameterTypeMemoare not on this path at all. So the fix suggested in the issue — seedingParameterTypeMemoinstoreMappings()— would not have changed this test's outcome. Neither would anything aboutNpgsqlTypeMapper.Mappings; that staticCacheis only read by Weasel's own resolution, which never runs here._npgsqlDbTypeis left null, so theparam.NpgsqlDbTypegetter infers fromValuevia Npgsql's process-global type mapper. That mapper stays empty — reportingUnknownfor every value — until the process constructs its firstNpgsqlDataSourceorNpgsqlConnectionwith a connection string. Measured, one fresh process per row:new NpgsqlCommand().AddParameter("a").NpgsqlDbTypeUnknownnew NpgsqlConnection()(parameterless)Unknownnew NpgsqlConnectionStringBuilder(cs)Unknownnew NpgsqlConnection(cs)Textnew NpgsqlDataSourceBuilder(cs)/.Build()Textnew NpgsqlDataSourceBuilder()TextSo the test passes only when some other test in the same process built a data source first. Under xUnit v3's parallel collections that is a race — hence 3 of 4 jobs failing and a clean re-run going green. It did not reproduce locally because a local full-suite run almost always wins that race.
This is a test-visibility artifact, not a wire-level defect: with
_npgsqlDbTypeunset, Npgsql resolves the type at execution time against the real connection, and a command cannot execute without a connection existing.The fix
Seed the mapper once from the existing module initializer, before any test runs.
new NpgsqlDataSourceBuilder()is enough — no sockets, no connection string.Why not make
AddParameterset the typeThat was the tempting fix (
AddNamedParameteralready does it) and it is a regression. Weasel's mapping is per-type; Npgsql's inference is per-value. Across the common CLR typesDateTimeis the only divergence, and it is a breaking one:DateTime.UtcNowTimestampTzTimestampDateTime.NowTimestampTimestampVerified against a live Postgres:
All other checked types agree exactly:
string,char,int,long,short,double,float,decimal,bool,Guid,DateTimeOffset,TimeSpan,DateOnly,TimeOnly,byte[],string[],int[],List<int>,IPAddress.Two new tests pin this so the trap is not walked into later:
add_parameter_honors_an_explicit_type— the path Weasel controls, independent of global mapper state.add_parameter_without_a_type_defers_to_npgsql_rather_than_weasels_mapping— fails ifAddParameterever starts stamping Weasel's mapping onto untyped parameters.Verification
Counterfactual, with the warm-up commented out, cold process:
— the exact CI failure. With the warm-out restored, green, including 5 consecutive cold-process runs of that class alone.
Full suite, all four CI matrix legs (net9.0/net10.0 x
USE_CASE_SENSITIVE_QUALIFIED_NAMEStrue/false): 786 passed, 0 failed, 3 skipped each.No product code changed.
Follow-ups, deliberately not in this PR
AddParametervsAddNamedParameterdisagree on untyped values — the former defers to the driver, the latter callsToParameterType. Worth reconciling, but it needs theDateTime-Kindquestion answered first.AddNamedParameter(cmd, name, DateTime.UtcNow)already setsTimestampand so already throws on write. Pre-existing, separate from this flake.NpgsqlTypeMapper.Mappingsreally is a mutable staticCacheenumerated without synchronization, andPostgresqlProviderTestswrites to it while other collections read. Not the cause here, but still a genuine hazard for anything that does go through Weasel's resolution.🤖 Generated with Claude Code