Skip to content

Commit cb0dab4

Browse files
dmealingclaude
andcommitted
test(codegen-compile): fold the adopter-build shapes into the shared fitness model
The codegen-compile gate compiles every port's output from fixtures/persistence-conformance/canonical/meta.fitness.json. Shapes an adopter app's build broke on were never in it, so the fixes for them were held by nothing. Folded INTO the one model, not a second one (AGENTS.md: "a second kitchen sink would drift"): - ReferralAuth, a fourth TPH subtype, is the target of an identity.reference and of an M:N (Post.referrals through the new PostReferral junction), declares an M:N of its own (ReferralAuth.posts), and has a @required + @default field (urgency). Subtype-as-FK/M:N-target once emitted an import of a table a subtype does not have (TS) and a migration that dropped the FK; @default on a required subtype field desynced the TS interface from Zod. - ProgramView.status restates Program.status through `extends` instead of repeating @values. That once emitted a C# enum type colliding with its own property. Kept on a subtype no persistence scenario reads, so no existing scenario row changes. The schema artifact is regenerated (the FK onto the subtype lands on "auths"; urgency carries no DB default, per the subtype-only-column rule), the bootstrap migration expects post_referrals and that FK, and the C# integration fixtures are regenerated (ProgramView.Status is now non-nullable: it inherits Program.status's @required, per ADR-0039). All five compile gates are green on the widened model - the earlier fixes hold, and now something holds them. Verified: TS compile gate 2/0 + schema artifact/migration/query/oracle 46/0; C# compile gate + drift 3/0, C# integration 36/0; Java compile gate + names 18/0, integration-tests 138/0; Kotlin compile gate 1/0, integration-tests-kotlin 160/0 (incl. the generated-vs-reference table check); Python compile gate + CLI 113/0, query scenarios 28/0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EaGv4zNk4oT7FooD4eK5MD
1 parent 16b29c0 commit cb0dab4

16 files changed

Lines changed: 208 additions & 15 deletions

File tree

‎docs/CONFORMANCE.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,13 @@ manifest contains without moving the boundary between the two halves.
8484

8585
- *Not a corpus, so it has no row.* It reuses
8686
`fixtures/persistence-conformance/canonical/meta.fitness.json` rather than adding a
87-
kitchen sink beside the one that already exists — 16 entities, two view-backed
88-
projections, two M:N junctions including a self-join, a 4-entity TPH hierarchy, jsonb,
89-
isArray, currency, decimal, and `AllTypes` carrying every persistable field subtype.
90-
A second such model would drift from the first, and this one is maintained by the
91-
other corpora already.
87+
kitchen sink beside the one that already exists — 18 entities, two view-backed
88+
projections (one restating an entity's enum through `extends`), M:N junctions including
89+
a self-join, a 5-entity TPH hierarchy whose subtypes are an FK and M:N target and declare
90+
an M:N of their own, a `@required` + `@default` subtype field, jsonb, isArray, currency,
91+
decimal, and `AllTypes` carrying every persistable field subtype. A second such model
92+
would drift from the first, and this one is maintained by the other corpora already.
93+
Shapes an adopter's build tripped over are folded INTO it, not into a second model.
9294
- *Why it exists.* Every corpus above gates BEHAVIOUR. None asks whether the emitted
9395
code builds — so four "generated code does not compile" defects shipped in 1.0.4 with
9496
the whole matrix green, because `gen` exits 0 in all four cases and the adopter's

‎fixtures/persistence-conformance/README.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,15 @@ TPH behavior is gated separately by the **api-contract** generated-controller la
203203
(e.g. Kotlin's `TphGeneratedApiContractConformanceTest` boots the unmodified
204204
generated controller over HTTP).
205205

206+
A fourth subtype, `ReferralAuth`, is in the model for the **codegen-compile gate**, not
207+
for these scenarios, and no `tph-*` scenario touches it. It carries the shapes an
208+
adopter's build broke on: it is the target of an `identity.reference` and of an M:N
209+
(`Post.referrals` through `PostReferral`), it declares an M:N of its own
210+
(`ReferralAuth.posts`), and it has a `@required` + `@default` field (`urgency`).
211+
`ProgramView.status` restates `Program.status` through `extends` for the same reason.
212+
Keeping these shapes on a subtype no scenario reads means existing scenario rows do not
213+
change.
214+
206215
### `op: roundtrip` — runtime WRITE round-trip
207216

208217
`roundtrip` is the **all-types write** gate: the runner INSERTS the `insert:` row

‎fixtures/persistence-conformance/canonical/meta.fitness.json‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
{ "field.long": { "name": "id", "extends": "Program.id",
7474
"children": [ { "origin.passthrough": { "@from": "Program.id" } } ] } },
7575
{ "field.string": { "name": "title", "children": [ { "origin.passthrough": { "@from": "Program.title" } } ] } },
76-
{ "field.enum": { "name": "status", "@values": ["DRAFT", "PUBLISHED", "ARCHIVED"],
76+
{ "field.enum": { "name": "status", "extends": "Program.status",
7777
"children": [ { "origin.passthrough": { "@from": "Program.status" } } ] } },
7878
{ "identity.primary": { "name": "id", "extends": "Program.id", "@fields": "id" } }
7979
]
@@ -115,6 +115,7 @@
115115
{ "field.long": { "name": "id" } },
116116
{ "field.string": { "name": "title", "@required": true, "@maxLength": 200 } },
117117
{ "relationship.association": { "name": "tags", "@cardinality": "many", "@objectRef": "Tag", "@through": "PostTag" } },
118+
{ "relationship.association": { "name": "referrals", "@cardinality": "many", "@objectRef": "ReferralAuth", "@through": "PostReferral" } },
118119
{ "identity.primary": { "name": "id", "@fields": "id", "@generation": "increment" } }
119120
]
120121
}},
@@ -179,7 +180,7 @@
179180
"children": [
180181
{ "source.rdb": { "@table": "auths" } },
181182
{ "field.long": { "name": "id" } },
182-
{ "field.enum": { "name": "type", "@values": ["Bridge", "Copay", "PriorAuth"] } },
183+
{ "field.enum": { "name": "type", "@values": ["Bridge", "Copay", "PriorAuth", "Referral"] } },
183184
{ "field.string": { "name": "reference", "@required": true, "@maxLength": 80 } },
184185
{ "identity.primary": { "name": "id", "@fields": "id", "@generation": "increment" } }
185186
]
@@ -208,6 +209,26 @@
208209
{ "field.string": { "name": "approver", "@maxLength": 80 } }
209210
]
210211
}},
212+
{ "object.entity": {
213+
"name": "ReferralAuth",
214+
"extends": "Auth",
215+
"@discriminatorValue": "Referral",
216+
"children": [
217+
{ "field.string": { "name": "urgency", "@required": true, "@default": "routine", "@maxLength": 16 } },
218+
{ "relationship.association": { "name": "posts", "@cardinality": "many", "@objectRef": "Post", "@through": "PostReferral" } }
219+
]
220+
}},
221+
{ "object.entity": {
222+
"name": "PostReferral",
223+
"children": [
224+
{ "source.rdb": { "@table": "post_referrals" } },
225+
{ "field.long": { "name": "postId", "@required": true } },
226+
{ "field.long": { "name": "referralId", "@required": true } },
227+
{ "identity.primary": { "name": "id", "@fields": ["postId", "referralId"] } },
228+
{ "identity.reference": { "name": "fkPost", "@fields": "postId", "@references": "Post" } },
229+
{ "identity.reference": { "name": "fkReferral", "@fields": "referralId", "@references": "ReferralAuth" } }
230+
]
231+
}},
211232
{ "object.value": {
212233
"name": "Settings",
213234
"children": [

‎fixtures/persistence-conformance/canonical/schema.postgres.sql‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,15 @@ CREATE TABLE "auths" (
9393
"quantity" INTEGER,
9494
"copayAmount" NUMERIC(10,2),
9595
"approver" VARCHAR(80),
96+
"urgency" VARCHAR(16),
9697
CONSTRAINT "auths_pkey" PRIMARY KEY ("id"),
97-
CONSTRAINT "auths_type_chk" CHECK ("type" IN ('Bridge', 'Copay', 'PriorAuth'))
98+
CONSTRAINT "auths_type_chk" CHECK ("type" IN ('Bridge', 'Copay', 'PriorAuth', 'Referral'))
99+
);
100+
101+
CREATE TABLE "post_referrals" (
102+
"postId" BIGINT NOT NULL,
103+
"referralId" BIGINT NOT NULL,
104+
CONSTRAINT "post_referrals_pkey" PRIMARY KEY ("postId", "referralId")
98105
);
99106

100107
CREATE TABLE "all_types" (
@@ -144,6 +151,10 @@ ALTER TABLE "friendships" ADD CONSTRAINT "friendships_personAId_fk" FOREIGN KEY
144151

145152
ALTER TABLE "friendships" ADD CONSTRAINT "friendships_personBId_fk" FOREIGN KEY ("personBId") REFERENCES "people" ("id");
146153

154+
ALTER TABLE "post_referrals" ADD CONSTRAINT "post_referrals_postId_fk" FOREIGN KEY ("postId") REFERENCES "posts" ("id");
155+
156+
ALTER TABLE "post_referrals" ADD CONSTRAINT "post_referrals_referralId_fk" FOREIGN KEY ("referralId") REFERENCES "auths" ("id");
157+
147158
CREATE VIEW "v_program" AS
148159
SELECT
149160
p.id AS id,

‎fixtures/persistence-conformance/migrations/bootstrap-canonical-from-empty.yaml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ expect:
4747
- 'CHECK ("intEnumVal" IN (0, 5, 9))'
4848
- 'CREATE UNIQUE INDEX "byTitle" ON "programs" ("title");'
4949
- 'ALTER TABLE "weeks" ADD CONSTRAINT "weeks_programId_fk"'
50+
# An identity.reference onto a TPH subtype binds the base's table (a subtype has none).
51+
- 'ALTER TABLE "post_referrals" ADD CONSTRAINT "post_referrals_referralId_fk" FOREIGN KEY ("referralId") REFERENCES "auths" ("id")'
5052
# Match both TS (`CREATE VIEW`) and C# (`CREATE OR REPLACE VIEW`, idempotent)
5153
# via the common suffix substring.
5254
- 'VIEW "v_program" AS'
@@ -71,6 +73,9 @@ expect:
7173
- { table_name: "measurements" }
7274
- { table_name: "nodes" }
7375
- { table_name: "people" }
76+
# post_referrals: an M:N junction whose second reference targets a TPH SUBTYPE
77+
# (ReferralAuth) — its FK must land on the base's single table, "auths".
78+
- { table_name: "post_referrals" }
7479
- { table_name: "post_tags" }
7580
- { table_name: "posts" }
7681
- { table_name: "programs" }

‎server/csharp/MetaObjects.Codegen.Tests/CodegenCompileConformanceTests.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
// WHY THIS IS NOT DbContextCompileTests. That test compiles a HAND-BUILT model chosen to
2222
// corner specific EF API surfaces, and it is the right tool for that. This one compiles
2323
// the shared corpus every other port also generates from, so a defect that needs the real
24-
// model's shape — 16 entities resolving against each other, two view-backed projections,
25-
// a self-joining M:N, a 4-entity TPH hierarchy — is caught by the same fixture in five
24+
// model's shape — 18 entities resolving against each other, two view-backed projections,
25+
// a self-joining M:N, a 5-entity TPH hierarchy with a subtype as an FK / M:N target — is caught by the same fixture in five
2626
// languages rather than by whichever port happened to hand-write a case for it.
2727
//
2828
// WHY THIS IS NOT IntegrationFixtureDriftTests. That one already loads this corpus and

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
1818
public DbSet<Node> Nodes { get; set; } = default!;
1919
public DbSet<Person> Persons { get; set; } = default!;
2020
public DbSet<Post> Posts { get; set; } = default!;
21+
public DbSet<PostReferral> PostReferrals { get; set; } = default!;
2122
public DbSet<PostTag> PostTags { get; set; } = default!;
2223
public DbSet<Program> Programs { get; set; } = default!;
2324
public DbSet<ProgramStat> ProgramStats { get; set; } = default!;
@@ -45,14 +46,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
4546
modelBuilder.Entity<Asset>().Property(x => x.ExternalId).HasColumnType("uuid").HasConversion(v => System.Guid.Parse(v!), g => g.ToString("D"));
4647
modelBuilder.Entity<Asset>().Property(x => x.Payload).HasColumnType("jsonb");
4748
modelBuilder.Entity<Auth>().Property(x => x.Type).HasConversion<string>();
48-
modelBuilder.Entity<Auth>().HasDiscriminator(e => e.Type).HasValue<BridgeAuth>(Auth.AuthType.Bridge).HasValue<CopayAuth>(Auth.AuthType.Copay).HasValue<PriorAuthAuth>(Auth.AuthType.PriorAuth);
49+
modelBuilder.Entity<Auth>().HasDiscriminator(e => e.Type).HasValue<BridgeAuth>(Auth.AuthType.Bridge).HasValue<CopayAuth>(Auth.AuthType.Copay).HasValue<PriorAuthAuth>(Auth.AuthType.PriorAuth).HasValue<ReferralAuth>(Auth.AuthType.Referral);
4950
modelBuilder.Entity<Follow>().HasOne<Person>().WithMany().HasForeignKey(e => e.FollowerId).OnDelete(DeleteBehavior.NoAction);
5051
modelBuilder.Entity<Follow>().HasOne<Person>().WithMany().HasForeignKey(e => e.FolloweeId).OnDelete(DeleteBehavior.NoAction);
5152
modelBuilder.Entity<Friendship>().HasOne<Person>().WithMany().HasForeignKey(e => e.PersonAId).OnDelete(DeleteBehavior.NoAction);
5253
modelBuilder.Entity<Friendship>().HasOne<Person>().WithMany().HasForeignKey(e => e.PersonBId).OnDelete(DeleteBehavior.NoAction);
5354
modelBuilder.Entity<Measurement>().Property(x => x.PreciseKg).HasPrecision(9, 4);
5455
modelBuilder.Entity<Node>().HasOne<Node>().WithMany().HasForeignKey(e => e.ParentId).OnDelete(DeleteBehavior.NoAction);
5556
modelBuilder.Entity<Post>().HasMany(x => x.Tags).WithMany().UsingEntity<PostTag>(l => l.HasOne<Tag>().WithMany().HasForeignKey(j => j.TagId), r => r.HasOne<Post>().WithMany().HasForeignKey(j => j.PostId));
57+
modelBuilder.Entity<Post>().HasMany(x => x.Referrals).WithMany().UsingEntity<PostReferral>(l => l.HasOne<ReferralAuth>().WithMany().HasForeignKey(j => j.ReferralId), r => r.HasOne<Post>().WithMany().HasForeignKey(j => j.PostId));
5658
modelBuilder.Entity<Program>().Property(x => x.Status).HasConversion<string>();
5759
modelBuilder.Entity<Program>().Property(x => x.CreatedAt).HasColumnType("timestamp without time zone");
5860
modelBuilder.Entity<Week>().HasOne<Program>().WithMany().HasForeignKey(e => e.ProgramId).OnDelete(DeleteBehavior.Cascade);

‎server/csharp/MetaObjects.IntegrationTests/Generated/Auth.g.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ namespace MetaObjects.IntegrationTests.Generated;
1212
[System.Text.Json.Serialization.JsonDerivedType(typeof(BridgeAuth))]
1313
[System.Text.Json.Serialization.JsonDerivedType(typeof(CopayAuth))]
1414
[System.Text.Json.Serialization.JsonDerivedType(typeof(PriorAuthAuth))]
15+
[System.Text.Json.Serialization.JsonDerivedType(typeof(ReferralAuth))]
1516
public abstract class Auth
1617
{
17-
public enum AuthType { Bridge, Copay, PriorAuth }
18+
public enum AuthType { Bridge, Copay, PriorAuth, Referral }
1819
[Key]
1920
[Column(AuthNames.IdColumn)]
2021
public long Id { get; set; }

‎server/csharp/MetaObjects.IntegrationTests/Generated/Post.g.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ public class Post
2020
[MinLength(1)]
2121
public string Title { get; set; } = default!;
2222
public ICollection<Tag> Tags { get; set; } = new List<Tag>();
23+
public ICollection<ReferralAuth> Referrals { get; set; } = new List<ReferralAuth>();
2324
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// <auto-generated/>
2+
// Generated by MetaObjects entity-generator. Do not edit by hand.
3+
#nullable enable
4+
using System;
5+
using System.Collections.Generic;
6+
using System.ComponentModel.DataAnnotations;
7+
using System.ComponentModel.DataAnnotations.Schema;
8+
using Microsoft.EntityFrameworkCore;
9+
10+
namespace MetaObjects.IntegrationTests.Generated;
11+
12+
[PrimaryKey(nameof(PostId), nameof(ReferralId))]
13+
[Table(PostReferralNames.SourcePrimaryTable)]
14+
public class PostReferral
15+
{
16+
[Column(PostReferralNames.PostIdColumn)]
17+
public long PostId { get; set; }
18+
[Column(PostReferralNames.ReferralIdColumn)]
19+
public long ReferralId { get; set; }
20+
}

0 commit comments

Comments
 (0)