Skip to content

Add IDbTypeResolver to allow plugins to control how DbTypes are mapped#6267

Merged
NinoFloris merged 16 commits intonpgsql:mainfrom
PauloHMattos:db-type-resolver
Nov 18, 2025
Merged

Add IDbTypeResolver to allow plugins to control how DbTypes are mapped#6267
NinoFloris merged 16 commits intonpgsql:mainfrom
PauloHMattos:db-type-resolver

Conversation

@PauloHMattos
Copy link
Contributor

@PauloHMattos PauloHMattos commented Oct 25, 2025

closes #6233

@roji
Copy link
Member

roji commented Oct 25, 2025

/cc @NinoFloris

Copy link
Member

@NinoFloris NinoFloris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this so quickly, this is a good start.

Take a look at my feedback on the files themselves. Additionally I'd like to see a bit of test coverage of this feature for the DbType and DataTypeName properties.

The tests should assert that when these properties are expected to return resolved results they do so correctly. After ResolveTypeInfo has run a user should be able to observe that their DataTypeName for DbType.String changed from e.g. "text" to "citext".

Comment on lines +148 to +149
Assert.That(parameter.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Uuid));
Assert.That(parameter.DataTypeName, Is.EqualTo("uuid"));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NinoFloris here I think we want this:

Suggested change
Assert.That(parameter.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Uuid));
Assert.That(parameter.DataTypeName, Is.EqualTo("uuid"));
Assert.That(parameter.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Unknow));
Assert.That(parameter.DataTypeName, Is.EqualTo(type));

But since when NpgsqlParameter.DbType is set, _npgsqlDbType is also set I couldn't get this result.

To achieve this DbType would have to take priority in the get of NpgsqlDbType and DataTypeName, or remove this behavior of when one is set the other is also set.

I haven't test this properly but I suspect both approaches would cause regressions.
I will take a closer look at this tomorrow, but if you have any ideias, or see something wrong, let me know.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes they have their own storage now so they can be decoupled.

Copy link
Contributor Author

@PauloHMattos PauloHMattos Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When both values are set, like in this test:

await AssertTypeWrite(5.5m, "5.5", "numeric", NpgsqlDbType.Numeric, DbType.VarNumeric, inferredDbType: DbType.Decimal);

We should prioritize the value of NpgsqlDbType? Or let DbType return the value that was explicitly set into it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like most of the failing tests are related with the inference of the DbType when NpgsqlDbType and DbType are provided

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those should be fixed now. If you have anything else to add please let me know. Otherwise it all looks good to me now and I can merge it for 10.0.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NinoFloris thank you so much! All looks good to me too.

Copy link
Member

@NinoFloris NinoFloris Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your work on this!

Fyi, I did just find an issue with the api shape and how it interacts with type reloading, which I didn't quite foresee.

Compared to the lifetime of the rest of the resolver chain (which is generally built once), in an IDbTypeResolver you'd ideally want to be able to use facts about the current database. For example, to only map to citext if it's actually installed. Doing that works in the current design if you do this on every call into the resolver, but it would be pretty suboptimal to do so. Instead you would want to cache that fact inside the IDbTypeResolver instance. However at that point when a user calls ReloadTypes we have no way of getting that cached state reset, as we cannot recreate the resolver chain today (and neither would we want to for the type info resolvers).

So, at a minimum I'll have to change how we track these db type resolvers such that we can recreate when required. But it's likely I'll also tweak the api signatures a bit as we may not need to flow options through the method arguments any longer.

EDIT:
Additionally, lumping this resolver in with the others on PgTypeInfoResolverFactory means we should technically also support this feature through the global type mapper. This is not desired as we don't want to add new functionality to it any longer. All of this points to making DbTypeResolverFactory its own thing.

@NinoFloris NinoFloris force-pushed the db-type-resolver branch 2 times, most recently from fdbbe67 to 563831e Compare November 10, 2025 17:37
@NinoFloris

This comment was marked as outdated.

@PauloHMattos
Copy link
Contributor Author

@NinoFloris Just wanted to let you know that I’ve been testing my applications with Npgsql from this branch with Babelfish, and everything is working flawlessly.

I honestly didn’t expect this change to require so much refactoring on your side — thank you so much for taking the time to review and improve it. I really appreciate all your work on this project!

@NinoFloris
Copy link
Member

Good to hear it all works as expected.

Features in this area always end up being more involved than anticipated. The postgres plugin system really does make npgsql quite a bit more complex when it comes to type mapping than other drivers. So thank you to for taking a shot at a complex issue, hopefully we'll see you back again!

@NinoFloris NinoFloris force-pushed the db-type-resolver branch 5 times, most recently from c4ae65c to f49844f Compare November 16, 2025 19:14
var param = (NpgsqlParameter)p;
param.NpgsqlDbType = (NpgsqlDbType)row[SchemaTableColumn.ProviderType];
// DbCommandBuilder is going to set DbType.Int32 onto an existing parameter, reset other db type fields.
if (param.SourceColumnNullMapping)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See https://github.com/dotnet/runtime/blob/8f1dc6d3d2ed2918f1c68e285a5f3970e0d4cc91/src/libraries/System.Data.Common/src/System/Data/Common/DBCommandBuilder.cs#L1121. This is the only place that sets this property in the BCL. Why did this have to require heap storage on all parameter instances and couldn't be passed through this ApplyParameterInfo method? Who knows, it's probably very necessary™️

@NinoFloris NinoFloris changed the title Add IDbTypeResolver to allow PgTypeInfoResolverFactory to control how DbTypes are mapped Add IDbTypeResolver to allow plugins to control how DbTypes are mapped Nov 16, 2025
@NinoFloris NinoFloris force-pushed the db-type-resolver branch 2 times, most recently from a1419af to 4d64c2a Compare November 18, 2025 18:53
… implementation compatible with unqualified names
@NinoFloris NinoFloris merged commit b16f722 into npgsql:main Nov 18, 2025
13 checks passed
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.

Allow IPgTypeInfoResolver to override built-in primitive PgTypeId for compatibility layers (e.g., Babelfish)

3 participants