Skip to content

Commit a4b1d6e

Browse files
authored
EQL: indexOf function implementation (elastic#54543)
1 parent 6fd6895 commit a4b1d6e

11 files changed

Lines changed: 450 additions & 20 deletions

File tree

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

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -807,12 +807,6 @@ file where opcode=0 and indexOf(file_name, 'explorer.') and indexOf(file_name, '
807807
expected_event_ids = []
808808
description = "check built-in string functions"
809809

810-
[[queries]]
811-
query = '''
812-
file where opcode=0 and indexOf(file_name, 'plorer.', 0) == 2'''
813-
expected_event_ids = [88, 92]
814-
description = "check built-in string functions"
815-
816810
[[queries]]
817811
query = '''
818812
file where opcode=0 and indexOf(file_name, 'plorer.', 2)'''
@@ -831,18 +825,6 @@ file where opcode=0 and indexOf(file_name, 'thing that never happened')'''
831825
expected_event_ids = []
832826
description = "check built-in string functions"
833827

834-
[[queries]]
835-
query = '''
836-
file where opcode=0 and indexOf(file_name, 'plorer.', 2) == 2'''
837-
expected_event_ids = [88, 92]
838-
description = "check substring ranges"
839-
840-
[[queries]]
841-
query = '''
842-
file where opcode=0 and indexOf(file_name, 'explorer.', 0) == 0'''
843-
expected_event_ids = [88, 92]
844-
description = "check substring ranges"
845-
846828
[[queries]]
847829
query = '''
848830
process where add(serial_event_id, 0) == 1 and add(0, 1) == serial_event_id'''

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
@@ -9,6 +9,7 @@
99
import org.elasticsearch.xpack.eql.expression.function.scalar.string.CIDRMatch;
1010
import org.elasticsearch.xpack.eql.expression.function.scalar.string.Between;
1111
import org.elasticsearch.xpack.eql.expression.function.scalar.string.EndsWith;
12+
import org.elasticsearch.xpack.eql.expression.function.scalar.string.IndexOf;
1213
import org.elasticsearch.xpack.eql.expression.function.scalar.string.Length;
1314
import org.elasticsearch.xpack.eql.expression.function.scalar.string.StartsWith;
1415
import org.elasticsearch.xpack.eql.expression.function.scalar.string.Substring;
@@ -33,6 +34,7 @@ private static FunctionDefinition[][] functions() {
3334
def(Between.class, Between::new, 2, "between"),
3435
def(CIDRMatch.class, CIDRMatch::new, "cidrmatch"),
3536
def(EndsWith.class, EndsWith::new, "endswith"),
37+
def(IndexOf.class, IndexOf::new, "indexof"),
3638
def(Length.class, Length::new, "length"),
3739
def(StartsWith.class, StartsWith::new, "startswith"),
3840
def(StringContains.class, StringContains::new, "stringcontains"),
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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.Literal;
14+
import org.elasticsearch.xpack.ql.expression.function.OptionalArgument;
15+
import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction;
16+
import org.elasticsearch.xpack.ql.expression.gen.pipeline.Pipe;
17+
import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate;
18+
import org.elasticsearch.xpack.ql.expression.gen.script.Scripts;
19+
import org.elasticsearch.xpack.ql.tree.NodeInfo;
20+
import org.elasticsearch.xpack.ql.tree.Source;
21+
import org.elasticsearch.xpack.ql.type.DataType;
22+
import org.elasticsearch.xpack.ql.type.DataTypes;
23+
24+
import java.util.Arrays;
25+
import java.util.List;
26+
import java.util.Locale;
27+
28+
import static java.lang.String.format;
29+
import static org.elasticsearch.xpack.eql.expression.function.scalar.string.IndexOfFunctionProcessor.doProcess;
30+
import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isInteger;
31+
import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isStringAndExact;
32+
import static org.elasticsearch.xpack.ql.expression.gen.script.ParamsBuilder.paramsBuilder;
33+
34+
/**
35+
* Find the first position (zero-indexed) of a string where a substring is found.
36+
* If the optional parameter start is provided, then this will find the first occurrence at or after the start position.
37+
*/
38+
public class IndexOf extends ScalarFunction implements OptionalArgument {
39+
40+
private final Expression source, substring, start;
41+
42+
public IndexOf(Source source, Expression src, Expression substring, Expression start) {
43+
super(source, Arrays.asList(src, substring, start != null ? start : new Literal(source, null, DataTypes.NULL)));
44+
this.source = src;
45+
this.substring = substring;
46+
this.start = arguments().get(2);
47+
}
48+
49+
@Override
50+
protected TypeResolution resolveType() {
51+
if (!childrenResolved()) {
52+
return new TypeResolution("Unresolved children");
53+
}
54+
55+
TypeResolution resolution = isStringAndExact(source, sourceText(), ParamOrdinal.FIRST);
56+
if (resolution.unresolved()) {
57+
return resolution;
58+
}
59+
60+
resolution = isStringAndExact(substring, sourceText(), ParamOrdinal.SECOND);
61+
if (resolution.unresolved()) {
62+
return resolution;
63+
}
64+
65+
return isInteger(start, sourceText(), ParamOrdinal.THIRD);
66+
}
67+
68+
@Override
69+
protected Pipe makePipe() {
70+
return new IndexOfFunctionPipe(source(), this, Expressions.pipe(source), Expressions.pipe(substring), Expressions.pipe(start));
71+
}
72+
73+
@Override
74+
public boolean foldable() {
75+
return source.foldable() && substring.foldable() && start.foldable();
76+
}
77+
78+
@Override
79+
public Object fold() {
80+
return doProcess(source.fold(), substring.fold(), start.fold());
81+
}
82+
83+
@Override
84+
protected NodeInfo<? extends Expression> info() {
85+
return NodeInfo.create(this, IndexOf::new, source, substring, start);
86+
}
87+
88+
@Override
89+
public ScriptTemplate asScript() {
90+
ScriptTemplate sourceScript = asScript(source);
91+
ScriptTemplate substringScript = asScript(substring);
92+
ScriptTemplate startScript = asScript(start);
93+
94+
return asScriptFrom(sourceScript, substringScript, startScript);
95+
}
96+
97+
protected ScriptTemplate asScriptFrom(ScriptTemplate sourceScript, ScriptTemplate substringScript, ScriptTemplate startScript) {
98+
return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{eql}.%s(%s,%s,%s)"),
99+
"indexOf",
100+
sourceScript.template(),
101+
substringScript.template(),
102+
startScript.template()),
103+
paramsBuilder()
104+
.script(sourceScript.params())
105+
.script(substringScript.params())
106+
.script(startScript.params())
107+
.build(), dataType());
108+
}
109+
110+
@Override
111+
public ScriptTemplate scriptWithField(FieldAttribute field) {
112+
return new ScriptTemplate(processScript(Scripts.DOC_VALUE),
113+
paramsBuilder().variable(field.exactAttribute().name()).build(),
114+
dataType());
115+
}
116+
117+
@Override
118+
public DataType dataType() {
119+
return DataTypes.INTEGER;
120+
}
121+
122+
@Override
123+
public Expression replaceChildren(List<Expression> newChildren) {
124+
if (newChildren.size() != 3) {
125+
throw new IllegalArgumentException("expected [3] children but received [" + newChildren.size() + "]");
126+
}
127+
128+
return new IndexOf(source(), newChildren.get(0), newChildren.get(1), newChildren.get(2));
129+
}
130+
131+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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 IndexOfFunctionPipe extends Pipe {
19+
20+
private final Pipe source, substring, start;
21+
22+
public IndexOfFunctionPipe(Source source, Expression expression, Pipe src, Pipe substring, Pipe start) {
23+
super(source, expression, Arrays.asList(src, substring, start));
24+
this.source = src;
25+
this.substring = substring;
26+
this.start = start;
27+
}
28+
29+
@Override
30+
public final Pipe replaceChildren(List<Pipe> newChildren) {
31+
if (newChildren.size() != 3) {
32+
throw new IllegalArgumentException("expected [3] children but received [" + newChildren.size() + "]");
33+
}
34+
return replaceChildren(newChildren.get(0), newChildren.get(1), newChildren.get(2));
35+
}
36+
37+
@Override
38+
public final Pipe resolveAttributes(AttributeResolver resolver) {
39+
Pipe newSource = source.resolveAttributes(resolver);
40+
Pipe newSubstring = substring.resolveAttributes(resolver);
41+
Pipe newStart = start.resolveAttributes(resolver);
42+
if (newSource == source && newSubstring == substring && newStart == start) {
43+
return this;
44+
}
45+
return replaceChildren(newSource, newSubstring, newStart);
46+
}
47+
48+
@Override
49+
public boolean supportedByAggsOnlyQuery() {
50+
return source.supportedByAggsOnlyQuery() && substring.supportedByAggsOnlyQuery() && start.supportedByAggsOnlyQuery();
51+
}
52+
53+
@Override
54+
public boolean resolved() {
55+
return source.resolved() && substring.resolved() && start.resolved();
56+
}
57+
58+
protected Pipe replaceChildren(Pipe newSource, Pipe newSubstring, Pipe newStart) {
59+
return new IndexOfFunctionPipe(source(), expression(), newSource, newSubstring, newStart);
60+
}
61+
62+
@Override
63+
public final void collectFields(QlSourceBuilder sourceBuilder) {
64+
source.collectFields(sourceBuilder);
65+
substring.collectFields(sourceBuilder);
66+
start.collectFields(sourceBuilder);
67+
}
68+
69+
@Override
70+
protected NodeInfo<IndexOfFunctionPipe> info() {
71+
return NodeInfo.create(this, IndexOfFunctionPipe::new, expression(), source, substring, start);
72+
}
73+
74+
@Override
75+
public IndexOfFunctionProcessor asProcessor() {
76+
return new IndexOfFunctionProcessor(source.asProcessor(), substring.asProcessor(), start.asProcessor());
77+
}
78+
79+
public Pipe src() {
80+
return source;
81+
}
82+
83+
public Pipe substring() {
84+
return substring;
85+
}
86+
87+
public Pipe start() {
88+
return start;
89+
}
90+
91+
@Override
92+
public int hashCode() {
93+
return Objects.hash(source, substring, start);
94+
}
95+
96+
@Override
97+
public boolean equals(Object obj) {
98+
if (this == obj) {
99+
return true;
100+
}
101+
102+
if (obj == null || getClass() != obj.getClass()) {
103+
return false;
104+
}
105+
106+
IndexOfFunctionPipe other = (IndexOfFunctionPipe) obj;
107+
return Objects.equals(source, other.source)
108+
&& Objects.equals(substring, other.substring)
109+
&& Objects.equals(start, other.start);
110+
}
111+
}

0 commit comments

Comments
 (0)