Skip to content

Commit eef9870

Browse files
dmealingclaude
andcommitted
fix(codegen-ts): stripPackage relationship @objectref in projection join resolver
A projection aggregate that traverses a relationship whose @objectref is package-qualified (pkg::Entity) silently dropped the join — and with it every aggregate column traversing that join — leaving the generated view with only PK + passthrough columns. Root cause: buildJoinTree resolved the relationship target via root.findObject(targetName) on the RAW objectRef, but findObject keys on the bare name (as the @via / @Of / @from paths already account for via stripPackage). The directory loader qualifies a same-package objectRef even when authored bare, so real directory-loaded models hit this while the string-loaded test fixture (bare objectRef) did not. Fix: stripPackage(targetName) before findObject. Adds a regression test with a package-qualified @objectref asserting the inverse-FK join + count aggregate survive. Full codegen-ts suite green (865 tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DV7ad8tggzcpFJzXYvHwRU
1 parent 2835500 commit eef9870

3 files changed

Lines changed: 71 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
77

88
## [Unreleased]
99

10+
### Fixed
11+
- **codegen-ts — package-qualified relationship `@objectRef` dropped projection
12+
joins.** When a projection's aggregate traversed a relationship whose `@objectRef`
13+
was package-qualified (`pkg::Entity`) — the shape the directory loader produces for
14+
a same-package objectRef even when authored bare — the join resolver looked the
15+
target up by the raw qualified name while `findObject` keys on the bare name, so the
16+
join silently failed to resolve and **every aggregate traversing it was dropped**:
17+
the view degraded to PK + passthrough columns only. `extract-view-spec` now
18+
`stripPackage`s the `@objectRef` before `findObject`, matching the `@via` / `@of` /
19+
`@from` paths. Surfaced by a directory-loaded consumer whose `count`/filtered-`max`
20+
aggregate columns vanished from a generated view while the string-loaded test fixture
21+
(bare objectRef) passed.
22+
1023
## [0.14.0] — 2026-06-29
1124

1225
_npm / PyPI / NuGet `0.14.0` · Maven Central `7.6.0`. A coordinated **minor** with

server/typescript/packages/codegen-ts/src/projection/extract-view-spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,11 @@ function buildJoinTree(
277277
const rel = findRelationship(currentObj, relName);
278278
if (!rel) break;
279279
const targetName = rel.ownAttr(RELATIONSHIP_ATTR_OBJECT_REF) as string | undefined;
280-
const target = targetName ? root.findObject(targetName) : undefined;
280+
// @objectRef may be package-qualified ("pkg::Entity") — the directory loader
281+
// qualifies a same-package objectRef even when authored bare — but findObject
282+
// keys on the BARE name (like the @via/@of/@from paths above). Strip first, or
283+
// the join (and every aggregate traversing it) is silently dropped.
284+
const target = targetName ? root.findObject(stripPackage(targetName)) : undefined;
281285
if (!target || !targetName) break;
282286

283287
const cardAttr = rel.ownAttr(RELATIONSHIP_ATTR_CARDINALITY) as string | undefined;

server/typescript/packages/codegen-ts/test/projection/build-projection-views.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,56 @@ describe("buildProjectionViews — scoped aggregate (origin.aggregate @filter)",
147147
expect(v!.sql).not.toContain("FILTER (WHERE");
148148
});
149149
});
150+
151+
describe("buildProjectionViews — package-qualified relationship @objectRef", () => {
152+
// A relationship's @objectRef is package-qualified ("acme::Week") — the shape the
153+
// directory loader produces for a same-package objectRef authored bare. The join
154+
// resolver keyed findObject on the RAW objectRef while findObject keys on the BARE
155+
// name, so the join (and every aggregate that traverses it) was silently dropped —
156+
// the view degraded to PK + passthroughs only. Must stripPackage before findObject,
157+
// exactly as the @via/@of/@from paths already do.
158+
const model = [
159+
{
160+
"object.entity": {
161+
name: "Program",
162+
children: [
163+
{ "source.rdb": { "@table": "programs" } },
164+
{ "field.int": { name: "id" } },
165+
{ "identity.primary": { name: "id", "@fields": "id" } },
166+
{ "relationship.composition": { name: "weeks", "@objectRef": "acme::Week", "@cardinality": "many" } },
167+
],
168+
},
169+
},
170+
{
171+
"object.entity": {
172+
name: "Week",
173+
children: [
174+
{ "source.rdb": { "@table": "weeks" } },
175+
{ "field.int": { name: "id" } },
176+
{ "field.int": { name: "programId" } },
177+
{ "identity.primary": { name: "id", "@fields": "id" } },
178+
{ "identity.reference": { name: "ref_program", "@fields": "programId", "@references": "Program" } },
179+
],
180+
},
181+
},
182+
{
183+
"object.projection": {
184+
name: "ProgramSummary",
185+
children: [
186+
{ "source.rdb": { "@kind": "view", "@table": "v_program_summary" } },
187+
{ "field.int": { name: "id", extends: "acme::Program.id" } },
188+
{ "field.int": { name: "weekCount", children: [{ "origin.aggregate": { "@agg": "count", "@of": "acme::Week.id", "@via": "Program.weeks" } }] } },
189+
{ "identity.primary": { name: "id", extends: "acme::Program.id" } },
190+
],
191+
},
192+
},
193+
];
194+
195+
test("qualified @objectRef still resolves the inverse-FK join + aggregate", async () => {
196+
const root = await load(model);
197+
const [v] = buildProjectionViews(root, { dialect: "postgres", columnNamingStrategy: "snake_case" });
198+
expect(v!.sql).toContain("LEFT OUTER JOIN weeks"); // join present, not dropped
199+
expect(v!.sql).toContain("COUNT(DISTINCT"); // aggregate emitted, not degraded away
200+
expect(v!.sql).toMatch(/COUNT\(DISTINCT [a-z0-9]+\.id\) AS week_count/);
201+
});
202+
});

0 commit comments

Comments
 (0)