Skip to content

FR: @agg: collect with @of optional — whole-object rollup (the designated origin.collection re-entry shape) #335

Description

@dmealing

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.

{ "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 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions