From 0cc5c19843aab1193644f213008763cffb6655db Mon Sep 17 00:00:00 2001 From: StressTestor <212606152+StressTestor@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:00:44 -0600 Subject: [PATCH] fix: apply per-candidate index when traversing arrays/maps by a streamed index `$o[.]` over a streamed context (e.g. `keys[] | $o[.]`) only returned the first match. The index expression yields one index set per incoming candidate, but traverseArrayOperator used only the first set (rhs.MatchingNodes.Front()), dropping the rest. Pair each index set with its candidate: when the LHS has one node per candidate (e.g. `.[] | .[idx]`) each node is traversed with its own index set; when the LHS collapses to a single node (a variable) it is traversed against every index set. Covers both arrays and maps. Fixes #2593. --- pkg/yqlib/operator_traverse_path.go | 43 ++++++++++++++++++------ pkg/yqlib/operator_traverse_path_test.go | 21 ++++++++++++ 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/pkg/yqlib/operator_traverse_path.go b/pkg/yqlib/operator_traverse_path.go index 1ed07572..0c2fbb11 100644 --- a/pkg/yqlib/operator_traverse_path.go +++ b/pkg/yqlib/operator_traverse_path.go @@ -111,10 +111,10 @@ func traverseArrayOperator(d *dataTreeNavigator, context Context, expressionNode return Context{}, err } - // rhs is a collect expression that will yield indices to retrieve of the arrays - + // rhs is a collect expression that yields the indices to retrieve. It is + // evaluated over the whole context, producing one index set per incoming + // candidate. rhs, err := d.GetMatchingNodes(context.ReadOnlyClone(), expressionNode.RHS) - if err != nil { return Context{}, err } @@ -123,16 +123,37 @@ func traverseArrayOperator(d *dataTreeNavigator, context Context, expressionNode if expressionNode.Operation.Preferences != nil { prefs = expressionNode.Operation.Preferences.(traversePreferences) } - var indicesToTraverse = rhs.MatchingNodes.Front().Value.(*CandidateNode).Content - log.Debugf("indicesToTraverse %v", len(indicesToTraverse)) - - //now we traverse the result of the lhs against the indices we found - result, err := traverseNodesWithArrayIndices(lhs, indicesToTraverse, prefs) - if err != nil { - return Context{}, err + results := list.New() + if lhs.MatchingNodes.Len() == rhs.MatchingNodes.Len() { + // One index set per LHS node (both derive from the same context): + // traverse each LHS node with its own index set. Previously only the + // first index set was used, so a context-dependent index like `$o[.]` + // over a `keys[]` stream dropped every match but the first (#2593). + rhsEl := rhs.MatchingNodes.Front() + for lhsEl := lhs.MatchingNodes.Front(); lhsEl != nil; lhsEl = lhsEl.Next() { + indicesToTraverse := rhsEl.Value.(*CandidateNode).Content + result, err := traverseNodesWithArrayIndices(context.SingleChildContext(lhsEl.Value.(*CandidateNode)), indicesToTraverse, prefs) + if err != nil { + return Context{}, err + } + results.PushBackList(result.MatchingNodes) + rhsEl = rhsEl.Next() + } + } else { + // LHS collapsed to a single node (e.g. a variable) while the index + // varies per candidate: traverse the LHS against every index set. + for rhsEl := rhs.MatchingNodes.Front(); rhsEl != nil; rhsEl = rhsEl.Next() { + indicesToTraverse := rhsEl.Value.(*CandidateNode).Content + result, err := traverseNodesWithArrayIndices(lhs, indicesToTraverse, prefs) + if err != nil { + return Context{}, err + } + results.PushBackList(result.MatchingNodes) + } } - return context.ChildContext(result.MatchingNodes), nil + + return context.ChildContext(results), nil } func traverseNodesWithArrayIndices(context Context, indicesToTraverse []*CandidateNode, prefs traversePreferences) (Context, error) { diff --git a/pkg/yqlib/operator_traverse_path_test.go b/pkg/yqlib/operator_traverse_path_test.go index e09d4991..d35dc0f8 100644 --- a/pkg/yqlib/operator_traverse_path_test.go +++ b/pkg/yqlib/operator_traverse_path_test.go @@ -675,6 +675,27 @@ var traversePathOperatorScenarios = []expressionScenario{ expression: ". = (.x = 1)", expectedError: "alias cycle detected", }, + { + // Regression test for https://github.com/mikefarah/yq/issues/2593 + // A context-dependent index (here the streamed key) must be applied + // per candidate; previously only the first index was used. + skipDoc: true, + document: `["a","b"]`, + expression: `. as $o | keys[] | $o[.]`, + expected: []string{ + "D0, P[0], (!!str)::a\n", + "D0, P[1], (!!str)::b\n", + }, + }, + { + skipDoc: true, + document: `{"x": 1, "y": 2}`, + expression: `. as $o | keys[] | $o[.]`, + expected: []string{ + "D0, P[x], (!!int)::1\n", + "D0, P[y], (!!int)::2\n", + }, + }, } func TestTraversePathOperatorScenarios(t *testing.T) {