ID |
|
|---|---|
Status |
Backlog |
Bucket |
architecture |
Priority |
7 |
Theme |
service |
Updated |
2026-07-14 |
@batchKeyLifter Record return-type symmetry
(Mechanism section re-anchored 2026-07-13: the original write-up named BatchKeyLifterDirectiveResolver and a BatchKey.LifterRowKeyed permit, both long deleted. The substance below is unchanged and re-verified; the live surface is LifterRef / SourceKey.Reader.SourceRowsCall / Wrap.)
R61 (shipped, see the changelog) added RecordN<...> source-shape support alongside the pre-existing RowN<...> shape on the @service classifier path: developers freely choose either at the source declaration, and variant identity tracks shape. The only consumer-supplied surface left without that symmetry is the batch-key lifter, where a Record1..Record22 return is still rejected today. This item brings the lifter API to the same Row-or-Record symmetry the source-shape path already has.
The live Row-only asymmetry
Two coupled sites pin the lifter to RowN:
-
SourceRowDirectiveResolver(SourceRowDirectiveResolver.java:239-255) validates the lifter method’s return type againstorg.jooq.Row1..Row22and rejects anything else, includingRecordN. -
SourceKey’s compact constructor (model/SourceKey.java:124-128) pins `Reader.SourceRowsCall -> Wrap.Rowand throws otherwise, with the message "lifter contract pins output to RowN<…>". The sibling pinReader.AccessorCall -> Wrap.Recordshows the shape channel already exists; the lifter arm just never got the Record side.
The classified carrier is SourceKey.Reader.SourceRowsCall(LifterRef) (model/SourceKey.java:288), produced from @sourceRow’s lifter resolution in `SourceRowDirectiveResolver.
Shape of the fix
The design question the original write-up framed as "split LifterRowKeyed into per-shape permits" maps onto today’s model as: let SourceRowsCall construct with the Wrap matching the lifter’s declared return shape (Wrap.Row for RowN, Wrap.Record for RecordN), selected once at classify time, and relax the SourceRowDirectiveResolver gate to accept both families. That preserves the principle the R61/R74/R70 trio converged on (variant identity carries the shape contract; downstream switches fork on typed identity, not a discriminator field): Wrap is already the sealed shape carrier, so no new permits are needed on the reader side unless emit sites turn out to re-derive shape.
No migration is forced: existing Row1..Row22 lifters keep working unchanged.
Sequencing: this item rides on SourceKey, which R431 (decompose-sourcekey) plans to decompose; the SourceRowsCall -> Wrap.Row pin is one of the coupled facts R431 will relocate. Do the symmetry work after (or as part of) R431 rather than against the current conflated record; hence the depends-on edge.
Acceptance criteria
-
The lifter resolver accepts both
Row1..Row22andRecord1..Record22return types; the rejection message is updated accordingly. -
The
SourceRowsCall/Wrappairing is selected from the declared return shape at classify time; the compact-constructor pin becomes a both-shapes invariant rather than Row-only. -
Emit sites that read the wrap (rows-method signatures, key extraction) handle both shapes; pipeline assertions track the chosen shape.
-
Existing
@sourceRowlifter fixtures updated; new fixtures cover theRecordNshape.