Skip to content

Commit 96a903b

Browse files
committed
EQL: Add string function (#54470)
* EQL: Add string() function * EQL: Reorder queryfolder_tests * EQL: Add test queries * EQL: Fix InternalEqlScriptUtils.string and test case * EQL: Fix testStringFunctionWithText error message * EQL: Flatten ToStringFunctionPipe.equals * EQL: Reorder painless whitelist * EQL: Address feedback and remove string(null) handling * EQL: Move string(pid) test over * EQL: Rename source -> value
1 parent d14ed34 commit 96a903b

File tree

9 files changed

+296
-4
lines changed

9 files changed

+296
-4
lines changed

x-pack/plugin/eql/qa/common/src/main/resources/test_queries_supported.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ expected_event_ids = [95]
1313
query = '''
1414
file where between(file_path, "dev", ".json", true) == "\\TestLogs\\something"
1515
'''
16+
17+
[[queries]]
18+
query = 'process where string(serial_event_id) = "1"'
19+
expected_event_ids = [1]

x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/EqlFunctionRegistry.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
import org.elasticsearch.xpack.eql.expression.function.scalar.string.IndexOf;
1313
import org.elasticsearch.xpack.eql.expression.function.scalar.string.Length;
1414
import org.elasticsearch.xpack.eql.expression.function.scalar.string.StartsWith;
15-
import org.elasticsearch.xpack.eql.expression.function.scalar.string.Substring;
1615
import org.elasticsearch.xpack.eql.expression.function.scalar.string.StringContains;
16+
import org.elasticsearch.xpack.eql.expression.function.scalar.string.Substring;
17+
import org.elasticsearch.xpack.eql.expression.function.scalar.string.ToString;
1718
import org.elasticsearch.xpack.eql.expression.function.scalar.string.Wildcard;
1819
import org.elasticsearch.xpack.ql.expression.function.FunctionDefinition;
1920
import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry;
@@ -37,6 +38,7 @@ private static FunctionDefinition[][] functions() {
3738
def(IndexOf.class, IndexOf::new, "indexof"),
3839
def(Length.class, Length::new, "length"),
3940
def(StartsWith.class, StartsWith::new, "startswith"),
41+
def(ToString.class, ToString::new, "string"),
4042
def(StringContains.class, StringContains::new, "stringcontains"),
4143
def(Substring.class, Substring::new, "substring"),
4244
def(Wildcard.class, Wildcard::new, "wildcard"),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.eql.expression.function.scalar.string;
8+
9+
import org.elasticsearch.xpack.ql.expression.Expression;
10+
import org.elasticsearch.xpack.ql.expression.Expressions;
11+
import org.elasticsearch.xpack.ql.expression.Expressions.ParamOrdinal;
12+
import org.elasticsearch.xpack.ql.expression.FieldAttribute;
13+
import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction;
14+
import org.elasticsearch.xpack.ql.expression.gen.pipeline.Pipe;
15+
import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate;
16+
import org.elasticsearch.xpack.ql.expression.gen.script.Scripts;
17+
import org.elasticsearch.xpack.ql.tree.NodeInfo;
18+
import org.elasticsearch.xpack.ql.tree.Source;
19+
import org.elasticsearch.xpack.ql.type.DataType;
20+
import org.elasticsearch.xpack.ql.type.DataTypes;
21+
22+
import java.util.Collections;
23+
import java.util.List;
24+
import java.util.Locale;
25+
26+
import static java.lang.String.format;
27+
import static org.elasticsearch.xpack.eql.expression.function.scalar.string.ToStringFunctionProcessor.doProcess;
28+
import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isExact;
29+
import static org.elasticsearch.xpack.ql.expression.gen.script.ParamsBuilder.paramsBuilder;
30+
31+
/**
32+
* EQL specific string function that wraps object.toString.
33+
*/
34+
public class ToString extends ScalarFunction {
35+
36+
private final Expression value;
37+
38+
public ToString(Source source, Expression src) {
39+
super(source, Collections.singletonList(src));
40+
this.value = src;
41+
}
42+
43+
@Override
44+
protected TypeResolution resolveType() {
45+
if (!childrenResolved()) {
46+
return new TypeResolution("Unresolved children");
47+
}
48+
49+
return isExact(value, sourceText(), ParamOrdinal.DEFAULT);
50+
}
51+
52+
@Override
53+
protected Pipe makePipe() {
54+
return new ToStringFunctionPipe(source(), this, Expressions.pipe(value));
55+
}
56+
57+
@Override
58+
public boolean foldable() {
59+
return value.foldable();
60+
}
61+
62+
@Override
63+
public Object fold() {
64+
return doProcess(value.fold());
65+
}
66+
67+
@Override
68+
protected NodeInfo<? extends Expression> info() {
69+
return NodeInfo.create(this, ToString::new, value);
70+
}
71+
72+
@Override
73+
public ScriptTemplate asScript() {
74+
ScriptTemplate sourceScript = asScript(value);
75+
76+
return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{eql}.%s(%s)"),
77+
"string",
78+
sourceScript.template()),
79+
paramsBuilder()
80+
.script(sourceScript.params())
81+
.build(), dataType());
82+
}
83+
84+
@Override
85+
public ScriptTemplate scriptWithField(FieldAttribute field) {
86+
return new ScriptTemplate(processScript(Scripts.DOC_VALUE),
87+
paramsBuilder().variable(field.exactAttribute().name()).build(),
88+
dataType());
89+
}
90+
91+
@Override
92+
public DataType dataType() {
93+
return DataTypes.KEYWORD;
94+
}
95+
96+
@Override
97+
public Expression replaceChildren(List<Expression> newChildren) {
98+
if (newChildren.size() != 1) {
99+
throw new IllegalArgumentException("expected [1] children but received [" + newChildren.size() + "]");
100+
}
101+
102+
return new ToString(source(), newChildren.get(0));
103+
}
104+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.eql.expression.function.scalar.string;
7+
8+
import org.elasticsearch.xpack.ql.execution.search.QlSourceBuilder;
9+
import org.elasticsearch.xpack.ql.expression.Expression;
10+
import org.elasticsearch.xpack.ql.expression.gen.pipeline.Pipe;
11+
import org.elasticsearch.xpack.ql.tree.NodeInfo;
12+
import org.elasticsearch.xpack.ql.tree.Source;
13+
14+
import java.util.Collections;
15+
import java.util.List;
16+
import java.util.Objects;
17+
18+
public class ToStringFunctionPipe extends Pipe {
19+
20+
private final Pipe source;
21+
22+
public ToStringFunctionPipe(Source source, Expression expression, Pipe src) {
23+
super(source, expression, Collections.singletonList(src));
24+
this.source = src;
25+
}
26+
27+
@Override
28+
public final Pipe replaceChildren(List<Pipe> newChildren) {
29+
if (newChildren.size() != 1) {
30+
throw new IllegalArgumentException("expected [1] children but received [" + newChildren.size() + "]");
31+
}
32+
return new ToStringFunctionPipe(source(), expression(), newChildren.get(0));
33+
}
34+
35+
@Override
36+
public final Pipe resolveAttributes(AttributeResolver resolver) {
37+
Pipe newSource = source.resolveAttributes(resolver);
38+
if (newSource == source) {
39+
return this;
40+
}
41+
return replaceChildren(Collections.singletonList(newSource));
42+
}
43+
44+
@Override
45+
public boolean supportedByAggsOnlyQuery() {
46+
return source.supportedByAggsOnlyQuery();
47+
}
48+
49+
@Override
50+
public boolean resolved() {
51+
return source.resolved();
52+
}
53+
54+
@Override
55+
public final void collectFields(QlSourceBuilder sourceBuilder) {
56+
source.collectFields(sourceBuilder);
57+
}
58+
59+
@Override
60+
protected NodeInfo<ToStringFunctionPipe> info() {
61+
return NodeInfo.create(this, ToStringFunctionPipe::new, expression(), source);
62+
}
63+
64+
@Override
65+
public ToStringFunctionProcessor asProcessor() {
66+
return new ToStringFunctionProcessor(source.asProcessor());
67+
}
68+
69+
public Pipe src() {
70+
return source;
71+
}
72+
73+
@Override
74+
public int hashCode() {
75+
return Objects.hash(source);
76+
}
77+
78+
@Override
79+
public boolean equals(Object obj) {
80+
if (this == obj) {
81+
return true;
82+
}
83+
84+
if (obj == null || getClass() != obj.getClass()) {
85+
return false;
86+
}
87+
88+
return Objects.equals(source, ((ToStringFunctionPipe) obj).source);
89+
}
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.eql.expression.function.scalar.string;
7+
8+
import org.elasticsearch.common.io.stream.StreamInput;
9+
import org.elasticsearch.common.io.stream.StreamOutput;
10+
import org.elasticsearch.xpack.ql.expression.gen.processor.Processor;
11+
12+
import java.io.IOException;
13+
import java.util.Objects;
14+
15+
public class ToStringFunctionProcessor implements Processor {
16+
17+
public static final String NAME = "sstr";
18+
19+
private final Processor source;
20+
21+
public ToStringFunctionProcessor(Processor source) {
22+
this.source = source;
23+
}
24+
25+
public ToStringFunctionProcessor(StreamInput in) throws IOException {
26+
source = in.readNamedWriteable(Processor.class);
27+
}
28+
29+
@Override
30+
public final void writeTo(StreamOutput out) throws IOException {
31+
out.writeNamedWriteable(source);
32+
}
33+
34+
@Override
35+
public Object process(Object input) {
36+
return doProcess(source.process(input));
37+
}
38+
39+
public static Object doProcess(Object source) {
40+
return source == null ? null : source.toString();
41+
}
42+
43+
protected Processor source() {
44+
return source;
45+
}
46+
47+
@Override
48+
public boolean equals(Object obj) {
49+
if (this == obj) {
50+
return true;
51+
}
52+
53+
if (obj == null || getClass() != obj.getClass()) {
54+
return false;
55+
}
56+
57+
ToStringFunctionProcessor other = (ToStringFunctionProcessor) obj;
58+
return Objects.equals(source(), other.source());
59+
}
60+
61+
@Override
62+
public int hashCode() {
63+
return Objects.hash(source());
64+
}
65+
66+
67+
@Override
68+
public String getWriteableName() {
69+
return NAME;
70+
}
71+
}

x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/whitelist/InternalEqlScriptUtils.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
import org.elasticsearch.xpack.eql.expression.function.scalar.string.IndexOfFunctionProcessor;
1212
import org.elasticsearch.xpack.eql.expression.function.scalar.string.LengthFunctionProcessor;
1313
import org.elasticsearch.xpack.eql.expression.function.scalar.string.StartsWithFunctionProcessor;
14-
import org.elasticsearch.xpack.eql.expression.function.scalar.string.SubstringFunctionProcessor;
1514
import org.elasticsearch.xpack.eql.expression.function.scalar.string.StringContainsFunctionProcessor;
15+
import org.elasticsearch.xpack.eql.expression.function.scalar.string.SubstringFunctionProcessor;
16+
import org.elasticsearch.xpack.eql.expression.function.scalar.string.ToStringFunctionProcessor;
1617
import org.elasticsearch.xpack.ql.expression.function.scalar.whitelist.InternalQlScriptUtils;
1718

1819
/*
@@ -44,6 +45,10 @@ public static Boolean startsWith(String s, String pattern) {
4445
return (Boolean) StartsWithFunctionProcessor.doProcess(s, pattern);
4546
}
4647

48+
public static String string(Object s) {
49+
return (String) ToStringFunctionProcessor.doProcess(s);
50+
}
51+
4752
public static Boolean stringContains(String string, String substring) {
4853
return (Boolean) StringContainsFunctionProcessor.doProcess(string, substring);
4954
}

x-pack/plugin/eql/src/main/resources/org/elasticsearch/xpack/eql/plugin/eql_whitelist.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class org.elasticsearch.xpack.ql.expression.function.scalar.whitelist.InternalQl
1212

1313
#
1414
# Utilities
15-
#
15+
#
1616
def docValue(java.util.Map, String)
1717
boolean nullSafeFilter(Boolean)
1818
double nullSafeSortNumeric(Number)
@@ -54,12 +54,13 @@ class org.elasticsearch.xpack.eql.expression.function.scalar.whitelist.InternalE
5454

5555
#
5656
# ASCII Functions
57-
#
57+
#
5858
String between(String, String, String, Boolean, Boolean)
5959
Boolean endsWith(String, String)
6060
Integer indexOf(String, String, Number)
6161
Integer length(String)
6262
Boolean startsWith(String, String)
63+
String string(Object)
6364
Boolean stringContains(String, String)
6465
String substring(String, Number, Number)
6566
}

x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/VerifierTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,4 +337,12 @@ public void testMultiField() {
337337
accept(idxr, "foo where multi_field_nested.end_date == ''");
338338
accept(idxr, "foo where multi_field_nested.start_date == 'bar'");
339339
}
340+
341+
public void testStringFunctionWithText() {
342+
final IndexResolution idxr = loadIndexResolution("mapping-multi-field.json");
343+
assertEquals("1:15: [string(multi_field.english)] cannot operate on field " +
344+
"of data type [text]: No keyword/multi-field defined exact matches for [english]; " +
345+
"define one or use MATCH/QUERY instead",
346+
error(idxr, "process where string(multi_field.english) == 'foo'"));
347+
}
340348
}

x-pack/plugin/eql/src/test/resources/queryfolder_tests.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ InternalQlScriptUtils.docValue(doc,params.v0),params.v1))"
113113
"params":{"v0":"process_name","v1":"foo"}
114114
;
115115

116+
stringFunction
117+
process where string(pid) == "123";
118+
"script":{"source":"InternalQlScriptUtils.nullSafeFilter(InternalQlScriptUtils.eq(
119+
InternalEqlScriptUtils.string(InternalQlScriptUtils.docValue(doc,params.v0)),params.v1))",
120+
"params":{"v0":"pid","v1":"123"}
121+
;
122+
116123
indexOfFunction
117124
process where indexOf(user_name, 'A', 2) > 0
118125
;

0 commit comments

Comments
 (0)