Skip to content

Commit 49324c4

Browse files
committed
test(persistence-cs): wire field.uri + field.inet into the AllTypes roundtrip harness
The shared AllTypes entity gained uriVal (field.uri), inetVal + inet6Val (field.inet, IPv4 + IPv6). The C# integration harness predated the fixture change, so the op:roundtrip read-back threw "Entity 'AllTypes' has no property for metadata field 'uriVal'". The production codecs (CSharpNaming / EntityGenerator / DbContextGenerator) already handle uri/inet — only the committed test harness was stale. - Regenerated the committed Generated/AllTypes.g.cs + AppDbContext.g.cs via the IntegrationFixtureDriftTests mutation harness: +Uri UriVal (text column, HasConversion Uri<->string), +IPAddress InetVal / Inet6Val (inet columns, Npgsql native IPAddress<->inet binding). Drift gate green. - WriteCoercion: coerce the YAML authoring string to Uri (new Uri(raw)) and IPAddress (IPAddress.Parse) on the INSERT/UPDATE write path. - EntityRow: pass Uri / IPAddress straight to Normalization (not the owned-POCO reflection path). - Normalization: render Uri -> verbatim absolute string; IPAddress -> bare address. The read goes through EF/Npgsql's native inet binding (NO ::text cast, so no /32 or /128 mask), and IPAddress.ToString() yields the COMPRESSED IPv6 form (e.g. 2001:0db8:..:8a2e:0370:7334 -> 2001:db8::8a2e:370:7334), matching the fixture's expected wire values. Shared fixtures (fixtures/persistence-conformance/...) unchanged — matched, not modified. QueryScenarioTests green (24/24) against Testcontainers Postgres, incl. the AllTypes uriVal/inetVal/inet6Val roundtrip and update-delete-all-types. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n
1 parent 29b3d7f commit 49324c4

5 files changed

Lines changed: 33 additions & 0 deletions

File tree

server/csharp/MetaObjects.IntegrationTests/Generated/AllTypes.g.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.ComponentModel.DataAnnotations;
77
using System.ComponentModel.DataAnnotations.Schema;
8+
using System.Net;
89

910
namespace MetaObjects.IntegrationTests.Generated;
1011

@@ -45,5 +46,14 @@ public enum AllTypesEnumVal { LOW, MEDIUM, HIGH }
4546
public AllTypesEnumVal EnumVal { get; set; }
4647
[Column("uuidVal")]
4748
public Guid UuidVal { get; set; }
49+
[Column("uriVal")]
50+
[Required]
51+
public Uri UriVal { get; set; } = default!;
52+
[Column("inetVal")]
53+
[Required]
54+
public IPAddress InetVal { get; set; } = default!;
55+
[Column("inet6Val")]
56+
[Required]
57+
public IPAddress Inet6Val { get; set; } = default!;
4858
public Settings? Settings { get; set; }
4959
}

server/csharp/MetaObjects.IntegrationTests/Generated/AppDbContext.g.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
3535
modelBuilder.Entity<AllTypes>().Property(x => x.DecVal).HasPrecision(18, 6);
3636
modelBuilder.Entity<AllTypes>().Property(x => x.TsVal).HasColumnType("timestamp without time zone");
3737
modelBuilder.Entity<AllTypes>().Property(x => x.TsTzVal).HasColumnType("timestamp with time zone");
38+
modelBuilder.Entity<AllTypes>().Property(x => x.UriVal).HasColumnType("text").HasConversion(v => v!.ToString(), v => new Uri(v));
39+
modelBuilder.Entity<AllTypes>().Property(x => x.InetVal).HasColumnType("inet");
40+
modelBuilder.Entity<AllTypes>().Property(x => x.Inet6Val).HasColumnType("inet");
3841
modelBuilder.Entity<Asset>().Property(x => x.RecordedAt).HasColumnType("timestamp with time zone");
3942
modelBuilder.Entity<Asset>().Property(x => x.ObservedAt).HasColumnType("timestamp without time zone");
4043
modelBuilder.Entity<Asset>().Property(x => x.ExternalId).HasColumnType("uuid").HasConversion(v => Guid.Parse(v!), g => g.ToString("D"));

server/csharp/MetaObjects.IntegrationTests/Runner/EntityRow.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
// column — keeping the wire form identical across read/write and across ports.
1111
// Scalars / temporal / Guid / enum pass straight through to Normalization.
1212

13+
using System.Net;
1314
using System.Reflection;
1415
using System.Text.Json;
1516
using System.Text.Json.Nodes;
@@ -45,6 +46,12 @@ public static class EntityRow
4546
// UTC wire string + "Z"), NOT into PocoToJsonNode's reflection path.
4647
if (t.IsPrimitive || value is string or decimal or DateTime or DateTimeOffset or DateOnly or TimeOnly or Guid or Enum)
4748
return value;
49+
// ADR-0036 Wave 3 — a field.uri materializes as System.Uri (EF HasConversion) and a
50+
// field.inet as System.Net.IPAddress (Npgsql inet). Both are reference types, so pass
51+
// them straight to Normalization (which renders the wire string) rather than into
52+
// PocoToJsonNode's reflection path. IPAddress.ToString() yields the bare, compressed
53+
// address — matching Postgres's inet read form (no ::text cast → no /32, /128 mask).
54+
if (value is Uri or IPAddress) return value;
4855
// A @dbColumnType:jsonb open-bag surfaces as JsonDocument/JsonElement (issue #98) —
4956
// pass it through so Normalization re-serializes the parsed value with sorted keys,
5057
// exactly as a string-typed jsonb column does. NOT a POCO to reflect over.

server/csharp/MetaObjects.IntegrationTests/Runner/Normalization.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// scenario's `expect:` block can be authored once and validated everywhere.
66

77
using System.Globalization;
8+
using System.Net;
89
using System.Text.Json;
910
using System.Text.Json.Nodes;
1011

@@ -69,6 +70,12 @@ string js when TryParseJsonContainer(js, out var node) => NormalizeJsonContainer
6970
// back byte-identical to the other ports' canonicalTimestamptz wire form.
7071
DateTimeOffset dto => FormatDateTime(dto.UtcDateTime),
7172
Guid uuid => uuid.ToString("D").ToLowerInvariant(),
73+
// ADR-0036 Wave 3 — field.uri (System.Uri) round-trips verbatim via its absolute
74+
// string; field.inet (System.Net.IPAddress) renders the bare address — IPAddress.ToString()
75+
// emits no CIDR mask and the COMPRESSED IPv6 form (Postgres compresses inet on read,
76+
// e.g. 2001:0db8:..:8a2e:0370:7334 → 2001:db8::8a2e:370:7334), matching the wire contract.
77+
Uri uri => uri.ToString(),
78+
IPAddress ip => ip.ToString(),
7279
byte[] bytes => Convert.ToBase64String(bytes),
7380
// A pre-parsed JsonNode (assembled by a runner path) — sort keys.
7481
// (JsonDocument/JsonElement from a @dbColumnType:jsonb column are handled above.)

server/csharp/MetaObjects.IntegrationTests/Runner/WriteCoercion.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
// caught by that scenario's per-subtype re-encode assertions.
2121

2222
using System.Globalization;
23+
using System.Net;
2324
using System.Reflection;
2425
using YamlDotNet.Core;
2526
using YamlDotNet.RepresentationModel;
@@ -58,6 +59,11 @@ public static PropertyInfo Property(Type entity, string fieldName) =>
5859

5960
if (underlying.IsEnum) return Enum.Parse(underlying, raw, ignoreCase: false);
6061
if (underlying == typeof(Guid)) return Guid.Parse(raw);
62+
// ADR-0036 Wave 3 — field.uri → System.Uri (EF's HasConversion stores v.ToString()
63+
// to the text column); field.inet → System.Net.IPAddress (Npgsql binds IPAddress↔inet
64+
// natively, no CIDR mask on a HOST address).
65+
if (underlying == typeof(Uri)) return new Uri(raw);
66+
if (underlying == typeof(IPAddress)) return IPAddress.Parse(raw);
6167
if (underlying == typeof(bool)) return ParseBool(raw);
6268
if (underlying == typeof(string)) return raw;
6369
if (underlying == typeof(int)) return int.Parse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture);

0 commit comments

Comments
 (0)