Skip to content

Commit fe03c89

Browse files
committed
fix: propagate null through dict and map bracket access
Change dict [] and map [] operators to propagate null when the parent element is nil, instead of returning a runtime error. This aligns the behavior of [] with the existing []? (conditional access) operator. Before this change, expressions like: dict["domain"]["key"] would fail with "cannot access field key, parent element is null" when the parent key did not exist in the dict/map. After this change, the expression evaluates to null, which then correctly evaluates to false in boolean comparisons. This is the expected behavior for dict/map chains where intermediate keys may not be present. The []? operator already had this behavior; now [] matches it. The isConditional parameter is removed since both paths are identical.
1 parent da25948 commit fe03c89

File tree

2 files changed

+86
-18
lines changed

2 files changed

+86
-18
lines changed

llx/builtin_map.go

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ import (
1818
var mapFunctions map[string]chunkHandlerV2 //nolint:unused
1919

2020
func mapGetIndex(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
21-
return _mapGetIndex(e, bind, chunk, ref, false)
21+
return _mapGetIndex(e, bind, chunk, ref)
2222
}
2323

2424
func mapGetConditionalIndex(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
25-
return _mapGetIndex(e, bind, chunk, ref, true)
25+
return _mapGetIndex(e, bind, chunk, ref)
2626
}
2727

28-
func _mapGetIndex(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64, isConditional bool) (*RawData, uint64, error) {
28+
func _mapGetIndex(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
2929
args := chunk.Function.Args
3030
// TODO: all this needs to go into the compile phase
3131
if len(args) < 1 {
@@ -37,12 +37,8 @@ func _mapGetIndex(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64, isC
3737
// ^^ TODO
3838

3939
if bind.Value == nil {
40-
if isConditional {
41-
return &RawData{Type: bind.Type.Child()}, 0, nil
42-
} else {
43-
field := args[0].LabelV2(e.ctx.code)
44-
return nil, 0, errors.New("cannot access map field " + field + ", map is null")
45-
}
40+
// Propagate null through map access chains instead of erroring.
41+
return &RawData{Type: bind.Type.Child()}, 0, nil
4642
}
4743

4844
var key string
@@ -372,14 +368,14 @@ func mapValuesV2(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*Ra
372368
}
373369

374370
func dictGetIndex(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
375-
return _dictGetIndex(e, bind, chunk, ref, false)
371+
return _dictGetIndex(e, bind, chunk, ref)
376372
}
377373

378374
func dictGetConditionalIndex(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
379-
return _dictGetIndex(e, bind, chunk, ref, true)
375+
return _dictGetIndex(e, bind, chunk, ref)
380376
}
381377

382-
func _dictGetIndex(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64, isConditional bool) (*RawData, uint64, error) {
378+
func _dictGetIndex(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
383379
args := chunk.Function.Args
384380
// TODO: all this needs to go into the compile phase
385381
if len(args) < 1 {
@@ -390,12 +386,10 @@ func _dictGetIndex(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64, is
390386
}
391387

392388
if bind.Value == nil {
393-
if isConditional {
394-
return &RawData{Type: bind.Type}, 0, nil
395-
} else {
396-
field := args[0].LabelV2(e.ctx.code)
397-
return nil, 0, errors.New("cannot access field " + field + ", parent element is null")
398-
}
389+
// Propagate null through dict access chains instead of erroring.
390+
// This enables expressions like dict['a']['b'] to evaluate to null
391+
// when dict['a'] is nil, rather than failing with a runtime error.
392+
return &RawData{Type: bind.Type}, 0, nil
399393
}
400394

401395
switch x := bind.Value.(type) {

llx/builtin_map_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package llx
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
"go.mondoo.com/mql/v13/types"
12+
)
13+
14+
func newTestBlockExecutor() *blockExecutor {
15+
return &blockExecutor{
16+
ctx: &MQLExecutorV2{code: &CodeV2{}},
17+
}
18+
}
19+
20+
func newStringKeyChunk() *Chunk {
21+
return &Chunk{
22+
Function: &Function{
23+
Args: []*Primitive{{Value: []byte("key"), Type: string(types.String)}},
24+
},
25+
}
26+
}
27+
28+
func TestDictGetIndex_NilValue(t *testing.T) {
29+
t.Run("returns typed null when parent dict is nil", func(t *testing.T) {
30+
e := newTestBlockExecutor()
31+
bind := &RawData{Type: types.Dict, Value: nil}
32+
33+
res, ref, err := dictGetIndex(e, bind, newStringKeyChunk(), 0)
34+
require.NoError(t, err)
35+
assert.Equal(t, uint64(0), ref)
36+
assert.Equal(t, types.Dict, res.Type)
37+
assert.Nil(t, res.Value, "null dict access should propagate null, not error")
38+
})
39+
40+
t.Run("conditional index also returns typed null", func(t *testing.T) {
41+
e := newTestBlockExecutor()
42+
bind := &RawData{Type: types.Dict, Value: nil}
43+
44+
res, ref, err := dictGetConditionalIndex(e, bind, newStringKeyChunk(), 0)
45+
require.NoError(t, err)
46+
assert.Equal(t, uint64(0), ref)
47+
assert.Equal(t, types.Dict, res.Type)
48+
assert.Nil(t, res.Value, "conditional null dict access should propagate null")
49+
})
50+
}
51+
52+
func TestMapGetIndex_NilValue(t *testing.T) {
53+
t.Run("returns typed null when parent map is nil", func(t *testing.T) {
54+
e := newTestBlockExecutor()
55+
mapType := types.Map(types.String, types.String)
56+
bind := &RawData{Type: mapType, Value: nil}
57+
58+
res, ref, err := mapGetIndex(e, bind, newStringKeyChunk(), 0)
59+
require.NoError(t, err)
60+
assert.Equal(t, uint64(0), ref)
61+
assert.Nil(t, res.Value, "null map access should propagate null, not error")
62+
})
63+
64+
t.Run("conditional index also returns typed null", func(t *testing.T) {
65+
e := newTestBlockExecutor()
66+
mapType := types.Map(types.String, types.String)
67+
bind := &RawData{Type: mapType, Value: nil}
68+
69+
res, ref, err := mapGetConditionalIndex(e, bind, newStringKeyChunk(), 0)
70+
require.NoError(t, err)
71+
assert.Equal(t, uint64(0), ref)
72+
assert.Nil(t, res.Value, "conditional null map access should propagate null")
73+
})
74+
}

0 commit comments

Comments
 (0)