Move conversion context to connector#6587
Move conversion context to connector#6587NinoFloris wants to merge 13 commits intonpgsql:mainnpgsql/npgsql:mainfrom NinoFloris:connector-conversion-contextNinoFloris/npgsql:connector-conversion-contextCopy head branch name to clipboard
Conversation
There was a problem hiding this comment.
Pull request overview
This PR moves conversion/session state (text encoding, time zone, and other ParameterStatus-driven values) from PgSerializerOptions to a per-connector PgConversionContext, and threads that context through binding/reading/writing so Npgsql can react to mid-session state changes (notably client_encoding and TimeZone).
Changes:
- Introduces per-connection
NpgsqlConnector.ConversionContext, rotates it on relevantParameterStatusupdates, and updates reader/writer/binding call sites to use it. - Adjusts type binding (
PgConcreteTypeInfo) to distinguish invariant vs context-dependent descriptors and re-resolve buffer requirements at runtime when needed. - Updates NodaTime legacy timestamp-tz converters to resolve time zone at read-time from the connection context instead of capturing it at type-mapping creation time.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/Npgsql.Tests/WriteStateTests.cs | Updates parameter binding calls to pass an explicit conversion context in tests. |
| test/Npgsql.Benchmarks/WriteParameter.cs | Uses connector conversion context for benchmark parameter binding. |
| src/Npgsql/Replication/PgOutput/ReplicationValue.cs | Uses connector conversion context when resolving field types and conversion contexts. |
| src/Npgsql/NpgsqlParameterCollection.cs | Threads conversion context into validation-time parameter binding. |
| src/Npgsql/NpgsqlParameter`.cs | Passes conversion context into typed parameter binding. |
| src/Npgsql/NpgsqlParameter.cs | Updates Bind/typed binding hooks to accept conversion context and flow it into concrete binding. |
| src/Npgsql/NpgsqlNestedDataReader.cs | Passes connector conversion context into field binding for nested reads. |
| src/Npgsql/NpgsqlDataSource.cs | Stops constructing serializer options with per-connector encoding/timezone; options become context-agnostic. |
| src/Npgsql/NpgsqlDataReader.cs | Threads connector conversion context into row description conversion-context resolution. |
| src/Npgsql/NpgsqlCommand.cs | Passes connector conversion context into parameter processing. |
| src/Npgsql/NpgsqlBinaryImporter.cs | Passes connector conversion context into binary import parameter binding. |
| src/Npgsql/NpgsqlBinaryExporter.cs | Passes connector conversion context into binary export field binding. |
| src/Npgsql/Internal/PgWriter.cs | Captures conversion context per Init and uses cached encoder when available to reduce allocations. |
| src/Npgsql/Internal/PgTypeInfo.cs | Probes descriptors with empty context, caches invariance flags, and re-resolves requirements when non-invariant. |
| src/Npgsql/Internal/PgSerializerOptions.cs | Removes TimeZone provider and shared ConversionContext from serializer options. |
| src/Npgsql/Internal/PgReader.cs | Sources conversion context from connector (or buffer fallback when connector-less). |
| src/Npgsql/Internal/PgConverter.cs | Extends PgConversionContext to carry encoder/timezone; documents invariance via ConverterDescriptor. |
| src/Npgsql/Internal/NpgsqlWriteBuffer.cs | Sources encoding from connector dynamically and uses connector encoder; adds fallback conversion context for connector-less buffers. |
| src/Npgsql/Internal/NpgsqlReadBuffer.cs | Sources encoding from connector dynamically; adds fallback conversion context for connector-less buffers. |
| src/Npgsql/Internal/NpgsqlConnector.FrontendMessages.cs | Binds parameters using connector conversion context when writing Bind messages. |
| src/Npgsql/Internal/NpgsqlConnector.cs | Adds per-connector conversion context and encoder cache; rotates them on TimeZone/client_encoding ParameterStatus changes. |
| src/Npgsql/Internal/Converters/RecordConverter.cs | Binds fields using the runtime reader conversion context. |
| src/Npgsql/Internal/Converters/CompositeConverter.cs | Resolves composite field read requirements using runtime conversion context. |
| src/Npgsql/Internal/Composites/Metadata/CompositeFieldInfo.cs | Re-resolves requirements for non-invariant composite fields using runtime conversion context; adjusts fast-path assumptions. |
| src/Npgsql/BackendMessages/RowDescriptionMessage.cs | Threads conversion context into field binding/conversion-context resolution and field-type lookup. |
| src/Npgsql.NodaTime/Internal/NodaTimeTypeInfoResolverFactory.cs | Stops capturing time zone at mapping creation; defers to runtime context. |
| src/Npgsql.NodaTime/Internal/LegacyConverters.cs | Resolves session time zone at read-time from PgConversionContext and preserves the localtime error behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 28 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
src/Npgsql/BackendMessages/RowDescriptionMessage.cs:360
- FieldDescription.GetInfoAndBind can return a cached _objectConversionContext that was built against a prior PgConversionContext when called with type==typeof(object) and DataFormat != Text. Since PgConversionContext instances are rotated on ParameterStatus updates (client_encoding/TimeZone), this can serve stale BufferRequirement data. Consider routing any access to the object-typed cache through GetObjectConversionContext(conversionContext) (at least when type is null or typeof(object)), rather than using _objectConversionContext directly.
if (result is { IsDefault: false, TypeInfo.Type: var typeToConvert } && typeToConvert == type)
return;
var objectInfo = DataFormat is DataFormat.Text && type is not null ? GetObjectConversionContext(conversionContext) : _objectConversionContext;
if (objectInfo.TypeInfo is not null && (typeof(object) == type || objectInfo.TypeInfo.Type == type))
{
result = objectInfo;
return;
55a8c7c to
a656d0c
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 28 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/Npgsql/BackendMessages/RowDescriptionMessage.cs:366
- FieldDescription.GetInfoAndBind can return a cached _objectConversionContext that was resolved against an older PgConversionContext: when type != null and DataFormat != Text, objectInfo is taken directly from _objectConversionContext (line 362) and returned for object/matching types without checking SourceContext. After a client_encoding/TimeZone rotation, this can reuse stale BufferRequirement/binding. Consider ensuring that any returned objectInfo was resolved against the supplied conversionContext (e.g., call GetObjectConversionContext(conversionContext) whenever type != null, or explicitly check ReferenceEquals(objectInfo.SourceContext, conversionContext) before using _objectConversionContext).
var objectInfo = DataFormat is DataFormat.Text && type is not null ? GetObjectConversionContext(conversionContext) : _objectConversionContext;
if (objectInfo.TypeInfo is not null && (typeof(object) == type || objectInfo.TypeInfo.Type == type))
{
result = objectInfo;
return;
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 28 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
src/Npgsql/NpgsqlParameter.cs:756
Bindis idempotent via_isBound, but it doesn’t validate that the cached_bindingwas produced against the samePgConversionContextinstance passed in. After a mid-sessionclient_encoding/TimeZonerotation the connector replacesConversionContext, and reusing a binding computed under the old context can yield an incorrect size (e.g. string byte count differs between UTF-8 and WIN1252), which can corrupt the bind message/protocol framing. Consider storing the sourcePgConversionContexton the parameter binding (similar toReadConversionContext.SourceContext) and forcingDisposeBindingState()/rebind when it differs (or when relevant fields changed).
/// Bind the current value to the type info, truncate (if applicable), take its size, and do any final validation before writing.
internal void Bind(PgConversionContext conversionContext, out DataFormat format, out Size size, DataFormat? requiredFormat = null)
{
if (TypeInfo is null || ConcreteTypeInfo is null)
ThrowHelper.ThrowInvalidOperationException($"Missing type info, {nameof(ResolveTypeInfo)} needs to be called before {nameof(Bind)}.");
// Invalidate a cached binding whose format no longer matches what the caller requires —
// the type info may still support the required format, just not the format the cached
// binding chose. Re-running Bind with the new preference can produce a satisfying binding.
if (_isBound && requiredFormat is not null && _binding.DataFormat != requiredFormat.GetValueOrDefault())
{
DisposeBindingState();
# Conflicts: # src/Npgsql/NpgsqlNestedDataReader.cs
45457c9 to
e3ff456
Compare
And update mid session changes via ParameterStatus. Closes #5146