Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions pkg/yqlib/operator_traverse_path.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Comment on lines +127 to +133
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new branch is intended to handle the case where the LHS is a single node (e.g. a bound variable), but the condition lhs.MatchingNodes.Len() < rhs.MatchingNodes.Len() can also be true when the LHS has multiple matches (e.g. 2 LHS nodes, 3 RHS sequences). In that case this code traverses each RHS index set against all LHS nodes, which can introduce an unintended cross-product of results. Consider tightening the condition to explicitly guard on lhs.MatchingNodes.Len() == 1 (or <= 1) so the multi-RHS traversal only applies to the intended bound-variable scenario.

Copilot uses AI. Check for mistakes.
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))
Expand Down
32 changes: 32 additions & 0 deletions pkg/yqlib/operator_traverse_path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading