Skip to content

Commit 94cb8a8

Browse files
committed
⭐ add: having(), to chain non-empty sets
tldr: Introduce `having` on lists/maps, which acts like a `where` filter that also checks that the resulting list is not empty. ```coffee my.collection.having( some condition ).all( ... ) ``` **Problem** As explained by @LittleSalkin1806 in #6633 (comment) > If the bucket has no tags at all, the where clause will return an empty list, the values will take values from no existing key (empty list) and the all function will evaluate to true because there are no elements that violate the condition. This can lead to false positives in the compliance checks. I ran into this same problem way too often myself! I've been working on a function to address this problem. The `where` clause behaves like a traiditional `filter` in JS/TS and is close to SQL, so we wanted to keep their behavior/meaning. However, there is clearly a need for the combination, because this pattern happens way too often: ```coffee collection.where( condition ) != empty collection.where( condition ).otherCalls... ``` Using @LittleSalkin1806 's example: ```coffee aws.s3.bucket.tags.where(key.downcase == 'dtit:sec:infosecclass') != empty aws.s3.bucket.tags.where(key.downcase == 'dtit:sec:infosecclass').values.map(downcase).in([...]) ``` **Solution** This PR introduces the `having` keyword. It acts like `where` that also makes sure that the list is not empty: ```coffee collection.having( condition ).otherCalls... ``` or with the above example: ```coffee aws.s3.bucket.tags.having(key.downcase == 'dtit:sec:infosecclass').values.map(downcase).in([ ... ]) ``` If the filtered list is empty, the query fails. If it is non-empty, it uses the filtered values and looks at the next condition. Signed-off-by: Dominik Richter <dominik.richter@gmail.com>
1 parent d64900c commit 94cb8a8

6 files changed

Lines changed: 163 additions & 8 deletions

File tree

mqlc/builtin.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,9 @@ func init() {
166166
compile: compileDictNone, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}},
167167
desc: "Check if no entry in this array or map satisfies a given condition",
168168
},
169-
"map": {compile: compileArrayMap, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
170-
"flat": {compile: compileDictFlat, signature: FunctionSignature{}},
169+
"having": {compile: compileDictHaving, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
170+
"map": {compile: compileArrayMap, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
171+
"flat": {compile: compileDictFlat, signature: FunctionSignature{}},
171172
// map-ish
172173
"keys": {typ: stringArrayType, signature: FunctionSignature{}},
173174
"values": {typ: dictArrayType, signature: FunctionSignature{}},
@@ -208,6 +209,7 @@ func init() {
208209
"any": {compile: compileArrayAny, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
209210
"one": {compile: compileArrayOne, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
210211
"none": {compile: compileArrayNone, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
212+
"having": {compile: compileArrayHaving, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
211213
"map": {compile: compileArrayMap, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
212214
"flat": {compile: compileArrayFlat, signature: FunctionSignature{}},
213215
"reverse": {typHandler: &sameType, signature: FunctionSignature{}},
@@ -225,6 +227,7 @@ func init() {
225227
"all": {compile: compileMapAll, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
226228
"one": {compile: compileMapOne, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
227229
"none": {compile: compileMapNone, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
230+
"having": {compile: compileMapHaving, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
228231
},
229232
types.ResourceLike: {
230233
"first": {compile: compileResourceChildAccess, signature: FunctionSignature{}},
@@ -237,6 +240,7 @@ func init() {
237240
"any": {compile: compileResourceAny, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
238241
"one": {compile: compileResourceOne, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
239242
"none": {compile: compileResourceNone, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
243+
"having": {compile: compileResourceHaving, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
240244
"map": {compile: compileResourceMap, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
241245
"==" + string(types.Empty): {compile: compileResourceCmpEmpty},
242246
"!=" + string(types.Empty): {compile: compileResourceCmpEmpty},

mqlc/builtin_array.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,38 @@ func compileArrayNone(c *compiler, typ types.Type, ref uint64, id string, call *
447447
return types.Bool, nil
448448
}
449449

450+
func compileArrayHaving(c *compiler, typ types.Type, ref uint64, id string, call *parser.Call) (types.Type, error) {
451+
_, err := compileArrayWhere(c, typ, ref, "where", call)
452+
if err != nil {
453+
return types.Nil, err
454+
}
455+
whereRef := c.tailRef()
456+
457+
if err := compileListAssertionMsg(c, typ, ref, ref, whereRef); err != nil {
458+
return types.Nil, err
459+
}
460+
461+
c.addChunk(&llx.Chunk{
462+
Call: llx.Chunk_FUNCTION,
463+
Id: "$any",
464+
Function: &llx.Function{
465+
Type: string(types.Bool),
466+
Binding: whereRef,
467+
},
468+
})
469+
anyRef := c.tailRef()
470+
471+
checksum := c.Result.CodeV2.Checksums[anyRef]
472+
c.Result.Labels.Labels[checksum] = "[].having()"
473+
474+
c.block.Entrypoints = append(c.block.Entrypoints, anyRef)
475+
c.block.Datapoints = append(c.block.Datapoints, whereRef)
476+
477+
c.overrideTailDataRef = whereRef
478+
479+
return typ, nil
480+
}
481+
450482
func compileArrayMap(c *compiler, typ types.Type, ref uint64, id string, call *parser.Call) (types.Type, error) {
451483
if call == nil {
452484
return types.Nil, errors.New("missing filter argument for calling '" + id + "'")

mqlc/builtin_map.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,38 @@ func compileDictNone(c *compiler, typ types.Type, ref uint64, id string, call *p
437437
return types.Bool, nil
438438
}
439439

440+
func compileDictHaving(c *compiler, typ types.Type, ref uint64, id string, call *parser.Call) (types.Type, error) {
441+
_, err := compileDictQuery(c, typ, ref, "where", call)
442+
if err != nil {
443+
return types.Nil, err
444+
}
445+
whereRef := c.tailRef()
446+
447+
if err := compileListAssertionMsg(c, typ, ref, ref, whereRef); err != nil {
448+
return types.Nil, err
449+
}
450+
451+
c.addChunk(&llx.Chunk{
452+
Call: llx.Chunk_FUNCTION,
453+
Id: "$any",
454+
Function: &llx.Function{
455+
Type: string(types.Bool),
456+
Binding: whereRef,
457+
},
458+
})
459+
anyRef := c.tailRef()
460+
461+
checksum := c.Result.CodeV2.Checksums[anyRef]
462+
c.Result.Labels.Labels[checksum] = "[].having()"
463+
464+
c.block.Entrypoints = append(c.block.Entrypoints, anyRef)
465+
c.block.Datapoints = append(c.block.Datapoints, whereRef)
466+
467+
c.overrideTailDataRef = whereRef
468+
469+
return typ, nil
470+
}
471+
440472
func compileDictFlat(c *compiler, _ types.Type, ref uint64, id string, call *parser.Call) (types.Type, error) {
441473
if call != nil && len(call.Function) > 0 {
442474
return types.Nil, errors.New("no arguments supported for '" + id + "'")
@@ -658,3 +690,35 @@ func compileMapNone(c *compiler, typ types.Type, ref uint64, id string, call *pa
658690

659691
return types.Bool, nil
660692
}
693+
694+
func compileMapHaving(c *compiler, typ types.Type, ref uint64, id string, call *parser.Call) (types.Type, error) {
695+
_, err := compileMapWhere(c, typ, ref, "where", call)
696+
if err != nil {
697+
return types.Nil, err
698+
}
699+
whereRef := c.tailRef()
700+
701+
if err := compileListAssertionMsg(c, typ, ref, ref, whereRef); err != nil {
702+
return types.Nil, err
703+
}
704+
705+
c.addChunk(&llx.Chunk{
706+
Call: llx.Chunk_FUNCTION,
707+
Id: "$any",
708+
Function: &llx.Function{
709+
Type: string(types.Bool),
710+
Binding: whereRef,
711+
},
712+
})
713+
anyRef := c.tailRef()
714+
715+
checksum := c.Result.CodeV2.Checksums[anyRef]
716+
c.Result.Labels.Labels[checksum] = "{}.having()"
717+
718+
c.block.Entrypoints = append(c.block.Entrypoints, anyRef)
719+
c.block.Datapoints = append(c.block.Datapoints, whereRef)
720+
721+
c.overrideTailDataRef = whereRef
722+
723+
return typ, nil
724+
}

mqlc/builtin_resource.go

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ func compileResourceSample(c *compiler, typ types.Type, ref uint64, id string, c
206206
return types.Nil, errors.New("too many arguments when calling '" + id + "', only 1 is supported")
207207
}
208208

209-
resourceRef := c.tailRef()
209+
resourceRef := ref
210210

211211
listType, err := compileResourceDefault(c, typ, ref, "list", nil)
212212
if err != nil {
@@ -265,6 +265,8 @@ func compileResourceMap(c *compiler, typ types.Type, ref uint64, id string, call
265265
bindingName = arg.Name
266266
}
267267

268+
resourceRef := ref
269+
268270
refs, err := c.blockExpressions([]*parser.Expression{arg.Value}, types.Array(types.Type(resource.ListType)), ref, bindingName)
269271
if err != nil {
270272
return types.Nil, err
@@ -279,8 +281,6 @@ func compileResourceMap(c *compiler, typ types.Type, ref uint64, id string, call
279281
return types.Nil, multierr.Wrap(err, "called '"+id+"' with a bad function block, types don't match")
280282
}
281283

282-
resourceRef := c.tailRef()
283-
284284
listType, err := compileResourceDefault(c, typ, ref, "list", nil)
285285
if err != nil {
286286
return listType, err
@@ -487,6 +487,45 @@ func compileResourceNone(c *compiler, typ types.Type, ref uint64, id string, cal
487487
return types.Bool, nil
488488
}
489489

490+
func compileResourceHaving(c *compiler, typ types.Type, ref uint64, id string, call *parser.Call) (types.Type, error) {
491+
// resource.where
492+
_, err := compileResourceWhere(c, typ, ref, "where", call)
493+
if err != nil {
494+
return types.Nil, err
495+
}
496+
whereRef := c.tailRef()
497+
498+
listType, err := compileResourceDefault(c, typ, whereRef, "list", nil)
499+
if err != nil {
500+
return listType, err
501+
}
502+
listRef := c.tailRef()
503+
504+
if err := compileListAssertionMsg(c, listType, whereRef-1, whereRef-1, listRef); err != nil {
505+
return types.Nil, err
506+
}
507+
508+
c.addChunk(&llx.Chunk{
509+
Call: llx.Chunk_FUNCTION,
510+
Id: "$any",
511+
Function: &llx.Function{
512+
Type: string(types.Bool),
513+
Binding: listRef,
514+
},
515+
})
516+
anyRef := c.tailRef()
517+
518+
checksum := c.Result.CodeV2.Checksums[anyRef]
519+
c.Result.Labels.Labels[checksum] = typ.ResourceName() + ".having()"
520+
521+
c.block.Entrypoints = append(c.block.Entrypoints, anyRef)
522+
c.block.Datapoints = append(c.block.Datapoints, whereRef)
523+
524+
c.overrideTailDataRef = whereRef
525+
526+
return typ, nil
527+
}
528+
490529
func compileResourceChildAccess(c *compiler, typ types.Type, ref uint64, id string, call *parser.Call) (types.Type, error) {
491530
if call != nil && len(call.Function) > 0 {
492531
return types.Nil, errors.New("function " + id + " does not take arguments")
@@ -527,7 +566,7 @@ func compileResourceLength(c *compiler, typ types.Type, ref uint64, id string, c
527566
return types.Nil, multierr.Wrap(err, "failed to compile "+id)
528567
}
529568

530-
resourceRef := c.tailRef()
569+
resourceRef := ref
531570

532571
t, err := compileResourceDefault(c, typ, ref, "list", nil)
533572
if err != nil {

mqlc/mqlc.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ type compiler struct {
158158

159159
// helps chaining of builtin calls like `if (..) else if (..) else ..`
160160
prevID string
161+
162+
// overrideTailDataRef is set by `having` so that subsequent chained calls
163+
// bind to the where-filtered list instead of the $any boolean chunk.
164+
overrideTailDataRef uint64
161165
}
162166

163167
func (c *compiler) isInMyBlock(ref uint64) bool {
@@ -180,6 +184,18 @@ func (c *compiler) tailRef() uint64 {
180184
return c.block.TailRef(c.blockRef)
181185
}
182186

187+
// tailDataRef returns overrideTailDataRef if set (consuming it),
188+
// otherwise falls back to tailRef. Used after builtin calls like
189+
// `having` where the data continuation point differs from the tail.
190+
func (c *compiler) tailDataRef() uint64 {
191+
if c.overrideTailDataRef != 0 {
192+
ref := c.overrideTailDataRef
193+
c.overrideTailDataRef = 0
194+
return ref
195+
}
196+
return c.tailRef()
197+
}
198+
183199
// Creates a new block and its accompanying compiler.
184200
// It carries a set of variables that apply within the scope of this block.
185201
func (c *compiler) newBlockCompiler(binding *variable) compiler {
@@ -1503,7 +1519,7 @@ func (c *compiler) compileOperand(operand *parser.Operand) (*llx.Primitive, erro
15031519
if call != nil && len(calls) > 0 {
15041520
calls = calls[1:]
15051521
}
1506-
ref = c.tailRef()
1522+
ref = c.tailDataRef()
15071523
res = llx.RefPrimitiveV2(ref)
15081524

15091525
continue

mqlc/mqlc_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2147,7 +2147,7 @@ func TestSuggestions(t *testing.T) {
21472147
{
21482148
// list resource with empty field call
21492149
"users.",
2150-
[]string{"all", "any", "contains", "first", "last", "length", "list", "map", "none", "one", "sample", "where"},
2150+
[]string{"all", "any", "contains", "first", "having", "last", "length", "list", "map", "none", "one", "sample", "where"},
21512151
errors.New("incomplete query, missing identifier after '.' at <source>:1:7"),
21522152
nil,
21532153
},

0 commit comments

Comments
 (0)