From ce6bfb232e111c92e42c4ad5c78b1b63b600c7c2 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Thu, 30 Jul 2026 10:50:29 -0500 Subject: [PATCH] fix(postgresql): stop declaring IPNetwork on both the cidr and inet mappings (weasel#405) GetTypeMapping resolves a CLR type with NpgsqlTypeMapper.Mappings.LastOrDefault(m => m.ClrTypes.Contains(type)) and LastOrDefault only means something over an ordered sequence. Mappings is a JasperFx.Core.Cache backed by an ImHashMap, so it enumerates in hash-tree order, not insertion order: enumeration : , , Bigint, Boolean, Box, Bytea, Circle, Char declared : Smallint, Integer, Bigint, Real, Double, Numeric, Money, Text IPNetwork was the one CLR type claimed by two mappings, cidr and inet. cidr is correct and `ipnetwork_resolves_to_cidr` passes -- but only incidentally: the source declares Cidr *before* Inet, so under real insertion order LastOrDefault would return "inet" and that test would fail. It passed because the hash layout happens to put Cidr last, which is a function of the current key set rather than of anything declared. Consuming code is explicitly invited to add mappings. Drop the stray typeof(IPNetwork) from the inet mapping. IPAddress, NpgsqlInet and the (IPAddress, int) tuple stay. Resolution is now unambiguous and order-independent. Two guards added: one pinning IPNetwork to exactly one mapping, and a general one asserting no CLR type is claimed twice, so the ambiguity cannot come back silently. Both fail if the duplicate is restored. Postgres suite green on all four CI matrix legs, 788 passed each. Co-Authored-By: Claude Opus 5 (1M context) --- .../PostgresqlProviderTests.cs | 28 +++++++++++++++++++ src/Weasel.Postgresql/NpgsqlTypeMapping.cs | 5 +++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Weasel.Postgresql.Tests/PostgresqlProviderTests.cs b/src/Weasel.Postgresql.Tests/PostgresqlProviderTests.cs index 84a3d3b8..51b1a712 100644 --- a/src/Weasel.Postgresql.Tests/PostgresqlProviderTests.cs +++ b/src/Weasel.Postgresql.Tests/PostgresqlProviderTests.cs @@ -85,6 +85,34 @@ public void ipnetwork_resolves_to_cidr() .ShouldBe("cidr"); } + [Fact] + public void ipnetwork_is_claimed_by_exactly_one_mapping() + { + // Guards the test above. IPNetwork used to be declared on both the cidr and the inet + // mapping, and GetTypeMapping breaks a tie with LastOrDefault over a Cache backed by + // an ImHashMap -- so "cidr" was winning on hash layout, not on anything declared. + // weasel#405. + NpgsqlTypeMapper.Mappings + .Count(mapping => mapping.ClrTypes.Contains(typeof(IPNetwork))) + .ShouldBe(1); + } + + [Fact] + public void no_clr_type_is_claimed_by_more_than_one_mapping() + { + // Any CLR type reachable from two mappings has an order-dependent, effectively + // arbitrary resolution. Keep that structurally impossible rather than relying on + // enumeration order. weasel#405. + var doubleClaimed = NpgsqlTypeMapper.Mappings + .SelectMany(mapping => mapping.ClrTypes.Select(clrType => (clrType, mapping))) + .GroupBy(x => x.clrType) + .Where(g => g.Count() > 1) + .Select(g => $"{g.Key.Name} <- {string.Join(", ", g.Select(x => x.mapping.NpgsqlDbType))}") + .ToArray(); + + doubleClaimed.ShouldBeEmpty(); + } + [Fact] public void canonicizesql_supports_tabs_as_whitespace() { diff --git a/src/Weasel.Postgresql/NpgsqlTypeMapping.cs b/src/Weasel.Postgresql/NpgsqlTypeMapping.cs index 3919c48b..ae394c71 100644 --- a/src/Weasel.Postgresql/NpgsqlTypeMapping.cs +++ b/src/Weasel.Postgresql/NpgsqlTypeMapping.cs @@ -82,9 +82,12 @@ public class NpgsqlTypeMapper {NpgsqlDbType.Multirange | NpgsqlDbType.TimestampTz, new NpgsqlTypeMapping(NpgsqlDbType.Multirange | NpgsqlDbType.TimestampTz, DbType.Object, "tstzmultirange")}, // Network types + // IPNetwork belongs to cidr only. It used to be listed on inet as well, and which of + // the two won was decided by LastOrDefault over a hash-ordered ImHashMap -- i.e. by + // accident rather than by declaration. See weasel#405. {NpgsqlDbType.Cidr, new NpgsqlTypeMapping(NpgsqlDbType.Cidr, DbType.Object, "cidr", typeof(IPNetwork))}, {NpgsqlDbType.Inet, new NpgsqlTypeMapping(NpgsqlDbType.Inet, DbType.Object, "inet", typeof(IPAddress), - typeof((IPAddress Address, int Subnet)), typeof(NpgsqlInet), typeof(IPNetwork), IPAddress.Loopback.GetType())}, + typeof((IPAddress Address, int Subnet)), typeof(NpgsqlInet), IPAddress.Loopback.GetType())}, {NpgsqlDbType.MacAddr, new NpgsqlTypeMapping(NpgsqlDbType.MacAddr, DbType.Object, "macaddr", typeof(PhysicalAddress))}, {NpgsqlDbType.MacAddr8, new NpgsqlTypeMapping(NpgsqlDbType.MacAddr8, DbType.Object, "macaddr8")},