Skip to content

Commit 3459dbf

Browse files
authored
✨ add string.notIn([..]) (#5538)
Add the counterpart to the `in` call: ```coffee > term.notIn([a,b,c]) true > term.notIn([term]) false ``` Signed-off-by: Dominik Richter <dominik.richter@gmail.com>
1 parent 569b8a3 commit 3459dbf

File tree

9 files changed

+112
-6
lines changed

9 files changed

+112
-6
lines changed

llx/builtin.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ func init() {
330330
string("contains" + types.Regex): {f: stringContainsRegex, Label: "contains"},
331331
string("contains" + types.Array(types.Regex)): {f: stringContainsArrayRegex, Label: "contains"},
332332
string("in"): {f: stringInArray, Label: "in"},
333+
string("notIn"): {f: stringNotInArray, Label: "in"},
333334
string("find"): {f: stringFindV2, Label: "find"},
334335
string("camelcase"): {f: stringCamelcaseV2, Label: "camelcase"},
335336
string("downcase"): {f: stringDowncaseV2, Label: "downcase"},
@@ -564,6 +565,7 @@ func init() {
564565
string("contains" + types.Regex): {f: dictContainsRegex, Label: "contains"},
565566
string("contains" + types.Array(types.Regex)): {f: dictContainsArrayRegex, Label: "contains"},
566567
"in": {f: dictIn, Label: "in"},
568+
"notIn": {f: dictNotIn, Label: "notIn"},
567569
string("find"): {f: dictFindV2, Label: "find"},
568570
// NOTE: the following functions are internal ONLY!
569571
// We have not yet decided if and how these may be exposed to users
@@ -627,6 +629,7 @@ func init() {
627629
"unique": {f: arrayUniqueV2},
628630
"difference": {f: arrayDifferenceV2},
629631
"in": {f: anyArrayInStringArray},
632+
"notIn": {f: anyArrayNotInStringArray},
630633
"containsAll": {f: arrayContainsAll},
631634
"containsNone": {f: arrayContainsNone},
632635
"==": {Compiler: compileArrayOpArray("=="), f: tarrayCmpTarrayV2, Label: "=="},

llx/builtin_array.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,18 @@ func anyArrayInStringArray(e *blockExecutor, bind *RawData, chunk *Chunk, ref ui
820820
return BoolTrue, 0, nil
821821
}
822822

823+
func anyArrayNotInStringArray(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
824+
// mildly inefficient, but reduces complexity
825+
res, ref, err := anyArrayInStringArray(e, bind, chunk, ref)
826+
if res == BoolTrue {
827+
return BoolFalse, ref, err
828+
}
829+
if res == BoolFalse {
830+
return BoolTrue, ref, err
831+
}
832+
return res, ref, err
833+
}
834+
823835
func arrayContainsAll(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
824836
if bind.Value == nil {
825837
return &RawData{Type: bind.Type, Error: bind.Error}, 0, nil

llx/builtin_map.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,6 +1535,17 @@ func dictIn(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData
15351535
}
15361536
}
15371537

1538+
func dictNotIn(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
1539+
switch bind.Value.(type) {
1540+
case string:
1541+
return stringNotInArray(e, bind, chunk, ref)
1542+
case []any:
1543+
return anyArrayNotInStringArray(e, bind, chunk, ref)
1544+
default:
1545+
return nil, 0, errors.New("dict value does not support field `in`")
1546+
}
1547+
}
1548+
15381549
func dictFindV2(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
15391550
switch bind.Value.(type) {
15401551
case string:

llx/builtin_simple.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2262,6 +2262,35 @@ func stringInArray(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*
22622262
return BoolFalse, 0, nil
22632263
}
22642264

2265+
func stringNotInArray(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
2266+
if bind.Value == nil {
2267+
return BoolTrue, 0, nil
2268+
}
2269+
2270+
argRef := chunk.Function.Args[0]
2271+
arg, rref, err := e.resolveValue(argRef, ref)
2272+
if err != nil || rref > 0 {
2273+
return nil, rref, err
2274+
}
2275+
2276+
if arg.Value == nil {
2277+
return BoolTrue, 0, nil
2278+
}
2279+
2280+
arr := arg.Value.([]interface{})
2281+
for i := range arr {
2282+
v, ok := arr[i].(string)
2283+
if !ok {
2284+
return nil, 0, errors.New("invalid type in array")
2285+
}
2286+
2287+
if v == bind.Value.(string) {
2288+
return BoolFalse, 0, nil
2289+
}
2290+
}
2291+
return BoolTrue, 0, nil
2292+
}
2293+
22652294
func stringFindV2(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
22662295
if bind.Value == nil {
22672296
return ArrayData([]interface{}{}, types.String), 0, nil

mql/mql_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ func TestNullString(t *testing.T) {
327327
})
328328
}
329329

330-
func TestDictMethods(t *testing.T) {
330+
func TestDictContains(t *testing.T) {
331331
x := testutils.InitTester(testutils.LinuxMock())
332332
x.TestSimple(t, []testutils.SimpleTest{
333333
{

mqlc/builtin.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,13 @@ func init() {
6868
desc: "Checks if this string contains another string",
6969
},
7070
"in": {
71-
typ: boolType, compile: compileStringIn,
71+
typ: boolType, compile: compileStringInOrNotIn,
7272
desc: "Checks if this string is contained in an array of strings",
7373
},
74+
"notIn": {
75+
typ: boolType, compile: compileStringInOrNotIn,
76+
desc: "Checks if this string is not contained in an array of strings",
77+
},
7478
"find": {
7579
typ: stringArrayType, signature: FunctionSignature{Required: 1, Args: []types.Type{types.Regex}},
7680
desc: "Find a regular expression in a string and return all matches as an array",
@@ -126,6 +130,9 @@ func init() {
126130
"lines": {typ: stringArrayType, signature: FunctionSignature{}},
127131
"split": {typ: stringArrayType, signature: FunctionSignature{Required: 1, Args: []types.Type{types.String}}},
128132
"trim": {typ: stringType, signature: FunctionSignature{Required: 0, Args: []types.Type{types.String}}},
133+
// string / array
134+
"in": {typ: boolType, signature: FunctionSignature{Required: 1, Args: []types.Type{types.Array(types.String), types.Array(types.Dict)}}},
135+
"notIn": {typ: boolType, signature: FunctionSignature{Required: 1, Args: []types.Type{types.Array(types.String), types.Array(types.Dict)}}},
129136
// array- or map-ish
130137
"first": {typ: dictType, signature: FunctionSignature{}},
131138
"last": {typ: dictType, signature: FunctionSignature{}},
@@ -136,7 +143,6 @@ func init() {
136143
compile: compileDictContains, typ: boolType, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}},
137144
desc: "When dealing with strings, check if it contains another string. When dealing with maps or arrays, check if any entry matches the given condition.",
138145
},
139-
"in": {typ: boolType, signature: FunctionSignature{Required: 1, Args: []types.Type{types.Array(types.String)}}},
140146
"containsOnly": {compile: compileDictContainsOnly, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
141147
"containsAll": {compile: compileDictContainsAll, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
142148
"containsNone": {compile: compileDictContainsNone, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
@@ -187,7 +193,8 @@ func init() {
187193
"sample": {typHandler: &sameType, signature: FunctionSignature{Required: 1, Args: []types.Type{types.Int}}},
188194
"duplicates": {compile: compileArrayDuplicates, signature: FunctionSignature{Required: 0, Args: []types.Type{types.String}}},
189195
"unique": {compile: compileArrayUnique, signature: FunctionSignature{Required: 0}},
190-
"in": {typ: boolType, compile: compileStringIn, signature: FunctionSignature{Required: 1, Args: []types.Type{types.Array(types.String)}}},
196+
"in": {typ: boolType, compile: compileStringInOrNotIn, signature: FunctionSignature{Required: 1, Args: []types.Type{types.Array(types.String)}}},
197+
"notIn": {typ: boolType, compile: compileStringInOrNotIn, signature: FunctionSignature{Required: 1, Args: []types.Type{types.Array(types.String)}}},
191198
"contains": {compile: compileArrayContains, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
192199
"containsOnly": {compile: compileArrayContainsOnly, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},
193200
"containsAll": {compile: compileArrayContainsAll, signature: FunctionSignature{Required: 1, Args: []types.Type{types.FunctionLike}}},

mqlc/builtin_simple.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ func compileStringContains(c *compiler, typ types.Type, ref uint64, id string, c
166166
}
167167
}
168168

169-
func compileStringIn(c *compiler, typ types.Type, ref uint64, id string, call *parser.Call) (types.Type, error) {
169+
func compileStringInOrNotIn(c *compiler, typ types.Type, ref uint64, id string, call *parser.Call) (types.Type, error) {
170170
if call == nil || len(call.Function) != 1 {
171171
return types.Nil, errors.New("function " + id + " needs one argument")
172172
}
@@ -178,7 +178,7 @@ func compileStringIn(c *compiler, typ types.Type, ref uint64, id string, call *p
178178

179179
c.addChunk(&llx.Chunk{
180180
Call: llx.Chunk_FUNCTION,
181-
Id: "in",
181+
Id: id,
182182
Function: &llx.Function{
183183
Type: string(types.Bool),
184184
Binding: ref,

providers/core/resources/mql_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,14 @@ func TestString_Methods(t *testing.T) {
436436
Code: "'hiya'.in([])",
437437
Expectation: false,
438438
},
439+
{
440+
Code: "'hiya'.notIn(['one', 'hiya'])",
441+
Expectation: false,
442+
},
443+
{
444+
Code: "'hiya'.notIn(['one', 'two'])",
445+
Expectation: true,
446+
},
439447
{
440448
Code: "'oh-hello-world!'.camelcase",
441449
Expectation: "ohHelloWorld!",

providers/os/resources/mql_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,42 @@ func TestResource_duplicateFields(t *testing.T) {
262262
})
263263
}
264264

265+
func TestDict_Methods_In(t *testing.T) {
266+
p := "parse.json('/dummy.json')."
267+
268+
x := testutils.InitTester(testutils.LinuxMock())
269+
x.TestSimple(t, []testutils.SimpleTest{
270+
{
271+
Code: p + "params['hello'].in(['1','2','hello'])",
272+
ResultIndex: 1,
273+
Expectation: true,
274+
},
275+
{
276+
Code: p + "params['hello'].in(['1','2'])",
277+
ResultIndex: 1,
278+
Expectation: false,
279+
},
280+
})
281+
}
282+
283+
func TestDict_Methods_NotIn(t *testing.T) {
284+
p := "parse.json('/dummy.json')."
285+
286+
x := testutils.InitTester(testutils.LinuxMock())
287+
x.TestSimple(t, []testutils.SimpleTest{
288+
{
289+
Code: p + "params['hello'].notIn(['1','2','hello'])",
290+
ResultIndex: 1,
291+
Expectation: false,
292+
},
293+
{
294+
Code: p + "params['hello'].notIn(['1','2'])",
295+
ResultIndex: 1,
296+
Expectation: true,
297+
},
298+
})
299+
}
300+
265301
func TestDict_Methods_InRange(t *testing.T) {
266302
p := "parse.json('/dummy.json')."
267303

0 commit comments

Comments
 (0)