From 247492ffba4089e7d33f6cef17789bd35e80b0b6 Mon Sep 17 00:00:00 2001 From: 00200200 <83299832+00200200@users.noreply.github.com> Date: Thu, 1 Oct 2026 03:15:22 +0200 Subject: [PATCH] fix: apply ireduce per pipeline input like jq reduce (#2869) match(...) | (.captures[] as $c ireduce (...)) folded every match's captures into one accumulator, so only the last object remained. jq reduce runs once per input. Use the same EvaluateTogether split as `as` and collect; eval-all file merges are unchanged. Fixes #2859 --- pkg/yqlib/operator_reduce.go | 26 ++++++++++++++++++++++++++ pkg/yqlib/operator_reduce_test.go | 8 ++++++++ 2 files changed, 34 insertions(+) diff --git a/pkg/yqlib/operator_reduce.go b/pkg/yqlib/operator_reduce.go index cde88360..5465ffd5 100644 --- a/pkg/yqlib/operator_reduce.go +++ b/pkg/yqlib/operator_reduce.go @@ -21,6 +21,32 @@ func reduceOperator(d *dataTreeNavigator, context Context, expressionNode *Expre return Context{}, fmt.Errorf("reduce must be given a block, got %v instead", expressionNode.RHS.Operation.OperationType.Type) } + // Same EvaluateTogether split as `as` / collect / crossFunction: + // file documents are reduced together (eval-all merge), while + // pipeline results such as match(...) are reduced per input, like jq. + var evaluateAllTogether = true + for matchEl := context.MatchingNodes.Front(); matchEl != nil; matchEl = matchEl.Next() { + evaluateAllTogether = evaluateAllTogether && matchEl.Value.(*CandidateNode).EvaluateTogether + if !evaluateAllTogether { + break + } + } + if evaluateAllTogether { + return reduceOperatorSingle(d, context, expressionNode) + } + + results := list.New() + for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { + result, err := reduceOperatorSingle(d, context.SingleChildContext(el.Value.(*CandidateNode)), expressionNode) + if err != nil { + return Context{}, err + } + results.PushBackList(result.MatchingNodes) + } + return context.ChildContext(results), nil +} + +func reduceOperatorSingle(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { arrayExpNode := expressionNode.LHS.LHS array, err := d.GetMatchingNodes(context, arrayExpNode) diff --git a/pkg/yqlib/operator_reduce_test.go b/pkg/yqlib/operator_reduce_test.go index 80b45a56..20d545be 100644 --- a/pkg/yqlib/operator_reduce_test.go +++ b/pkg/yqlib/operator_reduce_test.go @@ -30,6 +30,14 @@ var reduceOperatorScenarios = []expressionScenario{ "D0, P[], (!!map)::Cathy: apples\nBob: bananas\n", }, }, + { + skipDoc: true, + description: "Issue 2859: match then ireduce .captures[] matches jq reduce / capture", + expression: `"\"a\"=\"b\" \"x\"=\"y\"" | [match("\"(?.*?)\"=\"(?.*?)\""; "g") | (.captures[] as $capture ireduce ({}; .[$capture.name] = $capture.string))]`, + expected: []string{ + "D0, P[], (!!seq)::- key: a\n value: b\n- key: x\n value: y\n", + }, + }, } func TestReduceOperatorScenarios(t *testing.T) {