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) {