feat(intent): aggregates: keyed cross-entity aggregate (model + glue + keyed-upsert handler)#6407
Merged
Conversation
… (stage 1)
First slice of aggregates: (the two-key balance rollup / materialised on-hand; CLAUDE.md §13 and the
prerequisite for the guard checks - kf-catalog PROPOSAL_AGGREGATE_CHECKS.md). Generalises rollups:
(single-key child->parent) to a running sum/count grouped by MULTIPLE to-one relations, materialised
into a SEPARATE target entity keyed by that group.
- AggregateIntent model (name/of/op/sum/by[]/into/field) + IntentModel.aggregates.
- GlueIntentGenerator.buildAggregates: resolves source + target coordinates, validates each `by` key
is a to-one relation of BOTH source and target (the target is keyed by the same FKs), emits the
descriptor (op, source/sum, keys, target/field). glue.put("aggregates").
- GlueAggregatesTest: onHand = sum(StockMovement.quantity) by [Product, Store] -> ProductAvailability
.onHand; asserts the resolved descriptor + both keys. engine-intent 218/218.
Stage 2: the keyed-upsert handler template (maintain the target row per key-tuple on source
create/post/delete) + generateUtils wiring; then the guard checks on top, then inventory adoption.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the aggregates: feature: each logical aggregate expands into three MessageHandlers (source create / -updated / -deleted) that upsert the target row for the incoming row's key-tuple and recompute the sum/count from the store (idempotent, self-healing). Same-model, BigDecimal into a decimal target. - events/Aggregate.java.template: the keyed-upsert handler (shape-only; all key expressions pre-rendered by generateUtils). - template.js: register the aggregates collection -> Aggregate.java.template. - generateUtils.js case "aggregates": 3-handler expansion + expression rendering. - GlueIntentGenerator early-return guard: add posts + aggregates (latent bug - an aggregates-only OR posts-only intent skipped glue emission entirely; masked for posts because those modules always carried other glue). Boot-verified end-to-end on a real instance: handlers compile + connect as TOPIC listeners; REST-posting StockMovements maintains ProductAvailability.onHand keyed by (Product,Store) - 10+5-3=12 for one tuple, 7 for another, two isolated rows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
aggregates: keyed cross-entity aggregate (COMPLETE - model + glue + handler, boot-verified)
A new intent primitive: a running sum/count of a source entity's field, grouped by one or more of
its to-one relations, materialised into a separate target entity keyed by that group (one target
row per key-tuple). This is the two-key balance-rollup the ledger on-hand needs - a generalisation of
rollups:(which is single-key, child -> composition parent).What it emits
Each logical aggregate expands into three MessageHandlers (source
create/-updated/-deleted). Every one upserts the target row for the incoming row's key-tuple and recomputes theaggregate from the store over all source rows sharing that tuple - idempotent and self-healing
(re-delivery / replay converge). A source row with any grouping key null is ignored.
model/AggregateIntent.java+IntentModel.aggregates(stage 1)GlueIntentGenerator.buildAggregates- validates eachbykey is a to-one relation of BOTH sourceand target; emits the descriptor (source/target coordinates, keys, summed field, target field)
events/Aggregate.java.template- the keyed-upsert handler (shape-only; key expressionspre-rendered)
template.js+generateUtils.js case "aggregates"- the 3-handler expansion + expression renderingposts+aggregatesto the emptiness guard inGlueIntentGenerator.generate. Latent bug - an aggregates-only (or posts-only) intent skipped glueemission entirely; masked for
postsonly because those modules always carried other glue.Verification
GlueAggregatesTest(glue descriptor + both keys resolved); full engine-intent suite 218/218.(
agg-demo-StockMovement-StockMovement[-updated|-deleted]);StockMovementrows maintainsProductAvailability.onHandkeyed by (Product,Store):10 + 5 - 3 = 12 for one tuple, 7 for another - two isolated materialised rows, signed sum.
Scope / follow-ups (v1)
edit keys; the old-tuple recompute is a documented follow-up, same limitation as
rollups:).countinto an integer target and cross-model targets are follow-ups.Downstream (KeyFolders): this is the linchpin under the aggregate guard checks (negative-stock,
credit-limit, vacation-capacity) and retires the hand-written costing listener + the on-hand
live-report workaround - materialising on-hand as a real FK target.