Skip to content

Commit 666719f

Browse files
authored
EQL: startsWith function implementation (#54400)
1 parent 1e7ef16 commit 666719f

12 files changed

Lines changed: 399 additions & 37 deletions

File tree

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

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -786,44 +786,12 @@ process where original_file_name == process_name
786786
expected_event_ids = [97, 98, 75273, 75303]
787787
description = "check that case insensitive comparisons are performed for fields."
788788

789-
[[queries]]
790-
query = '''
791-
file where opcode=0 and startsWith(file_name, 'exploRER.')
792-
'''
793-
expected_event_ids = [88, 92]
794-
description = "check built-in string functions"
795-
796-
[[queries]]
797-
query = '''
798-
file where opcode=0 and startsWith(file_name, 'expLORER.exe')
799-
'''
800-
expected_event_ids = [88, 92]
801-
description = "check built-in string functions"
802-
803789
[[queries]]
804790
query = '''
805791
file where opcode=0 and endsWith(file_name, 'loREr.exe')'''
806792
expected_event_ids = [88]
807793
description = "check built-in string functions"
808794

809-
[[queries]]
810-
query = '''
811-
file where opcode=0 and startsWith(file_name, 'explORER.EXE')'''
812-
expected_event_ids = [88, 92]
813-
description = "check built-in string functions"
814-
815-
[[queries]]
816-
query = '''
817-
file where opcode=0 and startsWith('explorer.exeaaaaaaaa', file_name)'''
818-
expected_event_ids = [88]
819-
description = "check built-in string functions"
820-
821-
[[queries]]
822-
query = '''
823-
file where opcode=0 and serial_event_id = 88 and startsWith('explorer.exeaAAAA', 'EXPLORER.exe')'''
824-
expected_event_ids = [88]
825-
description = "check built-in string functions"
826-
827795
[[queries]]
828796
query = '''
829797
file where opcode=0 and stringContains('ABCDEFGHIexplorer.exeJKLMNOP', file_name)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package org.elasticsearch.xpack.eql.expression.function;
88

99
import org.elasticsearch.xpack.eql.expression.function.scalar.string.Length;
10+
import org.elasticsearch.xpack.eql.expression.function.scalar.string.StartsWith;
1011
import org.elasticsearch.xpack.eql.expression.function.scalar.string.Substring;
1112
import org.elasticsearch.xpack.ql.expression.function.FunctionDefinition;
1213
import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry;
@@ -25,6 +26,7 @@ private static FunctionDefinition[][] functions() {
2526
// String
2627
new FunctionDefinition[] {
2728
def(Length.class, Length::new, "length"),
29+
def(StartsWith.class, StartsWith::new, "startswith"),
2830
def(Substring.class, Substring::new, "substring")
2931
}
3032
};
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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.Arrays;
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.StartsWithFunctionProcessor.doProcess;
28+
import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isStringAndExact;
29+
import static org.elasticsearch.xpack.ql.expression.gen.script.ParamsBuilder.paramsBuilder;
30+
31+
/**
32+
* Function that checks if first parameter starts with the second parameter. Both parameters should be strings
33+
* and the function returns a boolean value. The function is case insensitive.
34+
*/
35+
public class StartsWith extends ScalarFunction {
36+
37+
private final Expression source;
38+
private final Expression pattern;
39+
40+
public StartsWith(Source source, Expression src, Expression pattern) {
41+
super(source, Arrays.asList(src, pattern));
42+
this.source = src;
43+
this.pattern = pattern;
44+
}
45+
46+
@Override
47+
protected TypeResolution resolveType() {
48+
if (!childrenResolved()) {
49+
return new TypeResolution("Unresolved children");
50+
}
51+
52+
TypeResolution sourceResolution = isStringAndExact(source, sourceText(), ParamOrdinal.FIRST);
53+
if (sourceResolution.unresolved()) {
54+
return sourceResolution;
55+
}
56+
57+
return isStringAndExact(pattern, sourceText(), ParamOrdinal.SECOND);
58+
}
59+
60+
@Override
61+
protected Pipe makePipe() {
62+
return new StartsWithFunctionPipe(source(), this, Expressions.pipe(source), Expressions.pipe(pattern));
63+
}
64+
65+
@Override
66+
public boolean foldable() {
67+
return source.foldable() && pattern.foldable();
68+
}
69+
70+
@Override
71+
public Object fold() {
72+
return doProcess(source.fold(), pattern.fold());
73+
}
74+
75+
@Override
76+
protected NodeInfo<? extends Expression> info() {
77+
return NodeInfo.create(this, StartsWith::new, source, pattern);
78+
}
79+
80+
@Override
81+
public ScriptTemplate asScript() {
82+
ScriptTemplate sourceScript = asScript(source);
83+
ScriptTemplate patternScript = asScript(pattern);
84+
85+
return asScriptFrom(sourceScript, patternScript);
86+
}
87+
88+
protected ScriptTemplate asScriptFrom(ScriptTemplate sourceScript, ScriptTemplate patternScript) {
89+
return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{eql}.%s(%s,%s)"),
90+
"startsWith",
91+
sourceScript.template(),
92+
patternScript.template()),
93+
paramsBuilder()
94+
.script(sourceScript.params())
95+
.script(patternScript.params())
96+
.build(), dataType());
97+
}
98+
99+
@Override
100+
public ScriptTemplate scriptWithField(FieldAttribute field) {
101+
return new ScriptTemplate(processScript(Scripts.DOC_VALUE),
102+
paramsBuilder().variable(field.exactAttribute().name()).build(),
103+
dataType());
104+
}
105+
106+
@Override
107+
public DataType dataType() {
108+
return DataTypes.BOOLEAN;
109+
}
110+
111+
@Override
112+
public Expression replaceChildren(List<Expression> newChildren) {
113+
if (newChildren.size() != 2) {
114+
throw new IllegalArgumentException("expected [2] children but received [" + newChildren.size() + "]");
115+
}
116+
117+
return new StartsWith(source(), newChildren.get(0), newChildren.get(1));
118+
}
119+
120+
}
Lines changed: 104 additions & 0 deletions
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+
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.Arrays;
15+
import java.util.List;
16+
import java.util.Objects;
17+
18+
public class StartsWithFunctionPipe extends Pipe {
19+
20+
private final Pipe source;
21+
private final Pipe pattern;
22+
23+
public StartsWithFunctionPipe(Source source, Expression expression, Pipe src, Pipe pattern) {
24+
super(source, expression, Arrays.asList(src, pattern));
25+
this.source = src;
26+
this.pattern = pattern;
27+
}
28+
29+
@Override
30+
public final Pipe replaceChildren(List<Pipe> newChildren) {
31+
if (newChildren.size() != 2) {
32+
throw new IllegalArgumentException("expected [2] children but received [" + newChildren.size() + "]");
33+
}
34+
return replaceChildren(newChildren.get(0), newChildren.get(1));
35+
}
36+
37+
@Override
38+
public final Pipe resolveAttributes(AttributeResolver resolver) {
39+
Pipe newSource = source.resolveAttributes(resolver);
40+
Pipe newPattern = pattern.resolveAttributes(resolver);
41+
if (newSource == source && newPattern == pattern) {
42+
return this;
43+
}
44+
return replaceChildren(newSource, newPattern);
45+
}
46+
47+
@Override
48+
public boolean supportedByAggsOnlyQuery() {
49+
return source.supportedByAggsOnlyQuery() && pattern.supportedByAggsOnlyQuery();
50+
}
51+
52+
@Override
53+
public boolean resolved() {
54+
return source.resolved() && pattern.resolved();
55+
}
56+
57+
protected Pipe replaceChildren(Pipe newSource, Pipe newPattern) {
58+
return new StartsWithFunctionPipe(source(), expression(), newSource, newPattern);
59+
}
60+
61+
@Override
62+
public final void collectFields(QlSourceBuilder sourceBuilder) {
63+
source.collectFields(sourceBuilder);
64+
pattern.collectFields(sourceBuilder);
65+
}
66+
67+
@Override
68+
protected NodeInfo<StartsWithFunctionPipe> info() {
69+
return NodeInfo.create(this, StartsWithFunctionPipe::new, expression(), source, pattern);
70+
}
71+
72+
@Override
73+
public StartsWithFunctionProcessor asProcessor() {
74+
return new StartsWithFunctionProcessor(source.asProcessor(), pattern.asProcessor());
75+
}
76+
77+
public Pipe src() {
78+
return source;
79+
}
80+
81+
public Pipe pattern() {
82+
return pattern;
83+
}
84+
85+
@Override
86+
public int hashCode() {
87+
return Objects.hash(source, pattern);
88+
}
89+
90+
@Override
91+
public boolean equals(Object obj) {
92+
if (this == obj) {
93+
return true;
94+
}
95+
96+
if (obj == null || getClass() != obj.getClass()) {
97+
return false;
98+
}
99+
100+
StartsWithFunctionPipe other = (StartsWithFunctionPipe) obj;
101+
return Objects.equals(source, other.source)
102+
&& Objects.equals(pattern, other.pattern);
103+
}
104+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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.eql.EqlIllegalArgumentException;
11+
import org.elasticsearch.xpack.ql.expression.gen.processor.Processor;
12+
13+
import java.io.IOException;
14+
import java.util.Locale;
15+
import java.util.Objects;
16+
17+
public class StartsWithFunctionProcessor implements Processor {
18+
19+
public static final String NAME = "sstw";
20+
21+
private final Processor source;
22+
private final Processor pattern;
23+
24+
public StartsWithFunctionProcessor(Processor source, Processor pattern) {
25+
this.source = source;
26+
this.pattern = pattern;
27+
}
28+
29+
public StartsWithFunctionProcessor(StreamInput in) throws IOException {
30+
source = in.readNamedWriteable(Processor.class);
31+
pattern = in.readNamedWriteable(Processor.class);
32+
}
33+
34+
@Override
35+
public final void writeTo(StreamOutput out) throws IOException {
36+
out.writeNamedWriteable(source);
37+
out.writeNamedWriteable(pattern);
38+
}
39+
40+
@Override
41+
public Object process(Object input) {
42+
return doProcess(source.process(input), pattern.process(input));
43+
}
44+
45+
public static Object doProcess(Object source, Object pattern) {
46+
if (source == null) {
47+
return null;
48+
}
49+
if (source instanceof String == false && source instanceof Character == false) {
50+
throw new EqlIllegalArgumentException("A string/char is required; received [{}]", source);
51+
}
52+
if (pattern == null) {
53+
return null;
54+
}
55+
if (pattern instanceof String == false && pattern instanceof Character == false) {
56+
throw new EqlIllegalArgumentException("A string/char is required; received [{}]", pattern);
57+
}
58+
59+
return source.toString().toLowerCase(Locale.ROOT).startsWith(pattern.toString().toLowerCase(Locale.ROOT));
60+
}
61+
62+
protected Processor source() {
63+
return source;
64+
}
65+
66+
protected Processor pattern() {
67+
return pattern;
68+
}
69+
70+
@Override
71+
public boolean equals(Object obj) {
72+
if (this == obj) {
73+
return true;
74+
}
75+
76+
if (obj == null || getClass() != obj.getClass()) {
77+
return false;
78+
}
79+
80+
StartsWithFunctionProcessor other = (StartsWithFunctionProcessor) obj;
81+
return Objects.equals(source(), other.source())
82+
&& Objects.equals(pattern(), other.pattern());
83+
}
84+
85+
@Override
86+
public int hashCode() {
87+
return Objects.hash(source(), pattern());
88+
}
89+
90+
91+
@Override
92+
public String getWriteableName() {
93+
return NAME;
94+
}
95+
}

0 commit comments

Comments
 (0)