From f4f77089ffbdff8db661665939914e2a49739437 Mon Sep 17 00:00:00 2001 From: lawrence3699 Date: Thu, 9 Apr 2026 19:23:57 +1000 Subject: [PATCH] fix: traverse array with streaming indices from variable binding When streaming values (e.g. from keys[]) pipe into an array traversal like $o[.], only the first streaming index was used. The RHS collect expression produces a separate SequenceNode per streaming value when EvaluateTogether is false. The existing code at line 126 only took the first SequenceNode, discarding the rest. When the LHS is a single bound variable and the RHS has multiple index sets, traverse each set separately. Fixes #2593 --- pkg/yqlib/operator_traverse_path.go | 21 ++++++++++++++++ pkg/yqlib/operator_traverse_path_test.go | 32 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/pkg/yqlib/operator_traverse_path.go b/pkg/yqlib/operator_traverse_path.go index 1ed07572..6ed991cc 100644 --- a/pkg/yqlib/operator_traverse_path.go +++ b/pkg/yqlib/operator_traverse_path.go @@ -123,6 +123,27 @@ func traverseArrayOperator(d *dataTreeNavigator, context Context, expressionNode if expressionNode.Operation.Preferences != nil { prefs = expressionNode.Operation.Preferences.(traversePreferences) } + + // When streaming values produce per-value index sets in the RHS and the LHS + // is a single node (e.g. a bound variable), traverse the LHS with each + // index set separately. Without this, only the first set of indices is used + // and the rest are silently dropped. + if rhs.MatchingNodes.Len() > 1 && lhs.MatchingNodes.Len() < rhs.MatchingNodes.Len() { + var allResults = list.New() + for el := rhs.MatchingNodes.Front(); el != nil; el = el.Next() { + seqNode := el.Value.(*CandidateNode) + if seqNode.Kind != SequenceNode { + continue + } + result, err := traverseNodesWithArrayIndices(lhs, seqNode.Content, prefs) + if err != nil { + return Context{}, err + } + allResults.PushBackList(result.MatchingNodes) + } + return context.ChildContext(allResults), nil + } + var indicesToTraverse = rhs.MatchingNodes.Front().Value.(*CandidateNode).Content log.Debugf("indicesToTraverse %v", len(indicesToTraverse)) diff --git a/pkg/yqlib/operator_traverse_path_test.go b/pkg/yqlib/operator_traverse_path_test.go index 6b2d63e3..9e0d4082 100644 --- a/pkg/yqlib/operator_traverse_path_test.go +++ b/pkg/yqlib/operator_traverse_path_test.go @@ -132,6 +132,38 @@ var badTraversePathOperatorScenarios = []expressionScenario{ } var traversePathOperatorScenarios = []expressionScenario{ + { + skipDoc: true, + description: "traverse array with streaming indices from keys", + document: `["a","b"]`, + expression: `. as $o | keys[] | $o[.]`, + expected: []string{ + "D0, P[0], (!!str)::a\n", + "D0, P[1], (!!str)::b\n", + }, + }, + { + skipDoc: true, + description: "traverse map with streaming indices from keys", + document: `{x: "a", y: "b"}`, + expression: `. as $o | keys[] | $o[.]`, + expected: []string{ + "D0, P[x], (!!str)::a\n", + "D0, P[y], (!!str)::b\n", + }, + }, + { + skipDoc: true, + description: "traverse longer array with streaming indices from keys", + document: `["a","b","c","d"]`, + expression: `. as $o | keys[] | $o[.]`, + expected: []string{ + "D0, P[0], (!!str)::a\n", + "D0, P[1], (!!str)::b\n", + "D0, P[2], (!!str)::c\n", + "D0, P[3], (!!str)::d\n", + }, + }, { skipDoc: true, description: "strange map with key but no value",