Additive. Make @of optional on origin.aggregate @agg: collect, where absent means a
whole-object rollup: collect the related rows as an array of the field's declared
@objectRef value-object, rather than an array of one scalar column.
Today that requires @of, so only a single scalar column can be collected — an array of
values, never an array of objects.
Why now / why this shape
This is the designated re-entry shape recorded in FR-037 R2 when origin.collection was
retired to reserved-not-registered. origin.collection expressed exactly this idea and
never emitted anything: there is no collection column kind, and codegen-ts/src,
migrate-ts/src and runtime-ts/src contain zero references to it. Its last real consumer,
the payload-VO typing edge, was deleted in 0.20.16 (#270) for being actively wrong. So the
capability has been declarable for a long time and never worked.
R2 deliberately did not activate the fold-in at retirement time, on the ADR-0007 Amendment 2
bar — a member enters the registry only when a shipping consumer dispatches on it. This
issue is that bar being met: build the consumer, then the vocabulary earns its place.
Parent-with-children-inline is one of the most common read-model shapes, and hand-writing
that view is precisely what a declared projection exists to avoid.
Why it is cheap on the vocabulary axis
@of is already required: false in expected-registry.json. The "collect requires
@of" constraint lives in validation, not the registry:
server/typescript/packages/metadata/src/loader/validation-passes.ts →
// --- count/sum/avg/min/max/collect: @of REQUIRED ---, where collect is grouped with the
scalar aggregates.
So this needs no registry change and is additive — it relaxes a rule, making
previously-invalid metadata valid. No metamodelVersion move, no breaking slot.
Design
1. Loader — all four ports. Split collect out of the @of-required gate.
2. Codegen — TypeScript only. Projection view DDL is TS-owned (ADR-0015).
projection/extract-view-spec.ts — the AGG_COLLECT branch currently sits inside @of
resolution. Add a whole-object arm that resolves the target entity from @via and emits a
new column kind (e.g. collectObjectAgg) carrying the declared value-object's field list
with their resolved physical column names.
projection/view-ddl-emit.ts — mirror the existing scalar lowering, including its
FILTER (WHERE <joined pk> IS NOT NULL) empty-set guard and @orderBy / @distinct
handling:
- postgres:
COALESCE(json_agg(json_build_object('f', a.c, …) ORDER BY …) FILTER (WHERE … IS NOT NULL), '[]')
- sqlite:
COALESCE(json_group_array(json_object('f', a.c, …)) FILTER (…), json_array())
- The projection column is json/jsonb typed as an array of the declared value-object.
Acceptance
- Loader:
@of-absent collect on a field.object @objectRef array loads in all five ports;
the same without @objectRef, or targeting a non-value, errors. Cross-port conformance
fixtures both ways.
- No-churn: a
collect with @of emits byte-identical SQL to today.
- A
view-lifecycle round-trip against a real engine, both dialects — emit → apply →
introspect → re-diff must be empty, and read the rows back to confirm the array-of-objects
shape. Golden-comparing new SQL is how the migrate defects got through before; new DDL does
not ship on a snapshot alone.
flattened-kitchen-sink's supplierBriefs field, dropped when origin.collection
retired, comes back expressed this way.
Not in scope
Non-RDB lowerings — that is #211's capability matrix.
Additive. Make
@ofoptional onorigin.aggregate @agg: collect, where absent means awhole-object rollup: collect the related rows as an array of the field's declared
@objectRefvalue-object, rather than an array of one scalar column.{ "field.object": { "name": "supplierBriefs", "isArray": true, "@objectRef": "acme::common::SupplierBrief", "children": [ { "origin.aggregate": { "@agg": "collect", "@via": "acme::catalog::Product.suppliers" } } ] }}Today that requires
@of, so only a single scalar column can be collected — an array ofvalues, never an array of objects.
Why now / why this shape
This is the designated re-entry shape recorded in FR-037 R2 when
origin.collectionwasretired to reserved-not-registered.
origin.collectionexpressed exactly this idea andnever emitted anything: there is no
collectioncolumn kind, andcodegen-ts/src,migrate-ts/srcandruntime-ts/srccontain zero references to it. Its last real consumer,the payload-VO typing edge, was deleted in 0.20.16 (#270) for being actively wrong. So the
capability has been declarable for a long time and never worked.
R2 deliberately did not activate the fold-in at retirement time, on the ADR-0007 Amendment 2
bar — a member enters the registry only when a shipping consumer dispatches on it. This
issue is that bar being met: build the consumer, then the vocabulary earns its place.
Parent-with-children-inline is one of the most common read-model shapes, and hand-writing
that view is precisely what a declared projection exists to avoid.
Why it is cheap on the vocabulary axis
@ofis alreadyrequired: falseinexpected-registry.json. The "collect requires@of" constraint lives in validation, not the registry:server/typescript/packages/metadata/src/loader/validation-passes.ts→// --- count/sum/avg/min/max/collect: @of REQUIRED ---, wherecollectis grouped with thescalar aggregates.
So this needs no registry change and is additive — it relaxes a rule, making
previously-invalid metadata valid. No
metamodelVersionmove, no breaking slot.Design
1. Loader — all four ports. Split
collectout of the@of-required gate.@ofABSENT → require the carrying field to befield.objectwith@objectRef(
isArray: trueis already enforced for collect). The element type isdeclared-authoritative (Prelude to #210: retire redundant payload origin-typing (Kotlin, Python) and close Java's KNOWN_GAPS entry as moot #270): type from the declared
@objectRef, never derived fromthe
@viarelationship's target entity.@objectRefmust target anobject.value, whichis already a loader rule from Retire assembly origins on object.value (one spelling of derived read shape) — RULED: batch into the pre-1.0 consolidation #210.
@ofPRESENT → unchanged, including today's element-type-preserving check.2. Codegen — TypeScript only. Projection view DDL is TS-owned (ADR-0015).
projection/extract-view-spec.ts— theAGG_COLLECTbranch currently sits inside@ofresolution. Add a whole-object arm that resolves the target entity from
@viaand emits anew column kind (e.g.
collectObjectAgg) carrying the declared value-object's field listwith their resolved physical column names.
projection/view-ddl-emit.ts— mirror the existing scalar lowering, including itsFILTER (WHERE <joined pk> IS NOT NULL)empty-set guard and@orderBy/@distincthandling:
COALESCE(json_agg(json_build_object('f', a.c, …) ORDER BY …) FILTER (WHERE … IS NOT NULL), '[]')COALESCE(json_group_array(json_object('f', a.c, …)) FILTER (…), json_array())Acceptance
@of-absent collect on afield.object @objectRefarray loads in all five ports;the same without
@objectRef, or targeting a non-value, errors. Cross-port conformancefixtures both ways.
collectwith@ofemits byte-identical SQL to today.view-lifecycleround-trip against a real engine, both dialects — emit → apply →introspect → re-diff must be empty, and read the rows back to confirm the array-of-objects
shape. Golden-comparing new SQL is how the migrate defects got through before; new DDL does
not ship on a snapshot alone.
flattened-kitchen-sink'ssupplierBriefsfield, dropped whenorigin.collectionretired, comes back expressed this way.
Not in scope
Non-RDB lowerings — that is #211's capability matrix.