Skip to content

EQL: Add string function #54470

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Apr 10, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package org.elasticsearch.xpack.eql.expression.function;

import org.elasticsearch.xpack.eql.expression.function.scalar.string.Substring;
import org.elasticsearch.xpack.eql.expression.function.scalar.string.ToString;
import org.elasticsearch.xpack.ql.expression.function.FunctionDefinition;
import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry;

Expand All @@ -17,13 +18,14 @@ public class EqlFunctionRegistry extends FunctionRegistry {
public EqlFunctionRegistry() {
super(functions());
}

private static FunctionDefinition[][] functions() {
return new FunctionDefinition[][] {
// Scalar functions
// String
new FunctionDefinition[] {
def(Substring.class, Substring::new, "substring"),
def(ToString.class, ToString::new, "string"),
},
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

package org.elasticsearch.xpack.eql.expression.function.scalar.string;

import org.elasticsearch.xpack.ql.expression.Expression;
import org.elasticsearch.xpack.ql.expression.Expressions;
import org.elasticsearch.xpack.ql.expression.Expressions.ParamOrdinal;
import org.elasticsearch.xpack.ql.expression.FieldAttribute;
import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction;
import org.elasticsearch.xpack.ql.expression.gen.pipeline.Pipe;
import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate;
import org.elasticsearch.xpack.ql.expression.gen.script.Scripts;
import org.elasticsearch.xpack.ql.tree.NodeInfo;
import org.elasticsearch.xpack.ql.tree.Source;
import org.elasticsearch.xpack.ql.type.DataType;
import org.elasticsearch.xpack.ql.type.DataTypes;

import java.util.Collections;
import java.util.List;
import java.util.Locale;

import static java.lang.String.format;
import static org.elasticsearch.xpack.eql.expression.function.scalar.string.ToStringFunctionProcessor.doProcess;
import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isExact;
import static org.elasticsearch.xpack.ql.expression.gen.script.ParamsBuilder.paramsBuilder;

/**
* EQL specific string function that wraps object.toString.
*/
public class ToString extends ScalarFunction {

private final Expression source;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename source to something else (delegate, target, value?) since it conflicts with Source source convention. While source was used in a couple of Processors, it was mainly to follow the function delegation and their official param names.
This applies to the whole PR.


public ToString(Source source, Expression src) {
super(source, Collections.singletonList(src));
this.source = src;
}

@Override
protected TypeResolution resolveType() {
if (!childrenResolved()) {
return new TypeResolution("Unresolved children");
}

return isExact(source, sourceText(), ParamOrdinal.FIRST);
}

@Override
protected Pipe makePipe() {
return new ToStringFunctionPipe(source(), this, Expressions.pipe(source));
}

@Override
public boolean foldable() {
return source.foldable();
}

@Override
public Object fold() {
return doProcess(source.fold());
}

@Override
protected NodeInfo<? extends Expression> info() {
return NodeInfo.create(this, ToString::new, source);
}

@Override
public ScriptTemplate asScript() {
ScriptTemplate sourceScript = asScript(source);

return asScriptFrom(sourceScript);
}

protected ScriptTemplate asScriptFrom(ScriptTemplate sourceScript) {
return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{eql}.%s(%s)"),
"string",
sourceScript.template()),
paramsBuilder()
.script(sourceScript.params())
.build(), dataType());
}

@Override
public ScriptTemplate scriptWithField(FieldAttribute field) {
return new ScriptTemplate(processScript(Scripts.DOC_VALUE),
paramsBuilder().variable(field.exactAttribute().name()).build(),
dataType());
}

@Override
public DataType dataType() {
return DataTypes.KEYWORD;
}

@Override
public Expression replaceChildren(List<Expression> newChildren) {
if (newChildren.size() != 1) {
throw new IllegalArgumentException("expected [1] children but received [" + newChildren.size() + "]");
}

return new ToString(source(), newChildren.get(0));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.eql.expression.function.scalar.string;

import org.elasticsearch.xpack.ql.execution.search.QlSourceBuilder;
import org.elasticsearch.xpack.ql.expression.Expression;
import org.elasticsearch.xpack.ql.expression.gen.pipeline.Pipe;
import org.elasticsearch.xpack.ql.tree.NodeInfo;
import org.elasticsearch.xpack.ql.tree.Source;

import java.util.Collections;
import java.util.List;
import java.util.Objects;

public class ToStringFunctionPipe extends Pipe {

private final Pipe source;

public ToStringFunctionPipe(Source source, Expression expression, Pipe src) {
super(source, expression, Collections.singletonList(src));
this.source = src;
}

@Override
public final Pipe replaceChildren(List<Pipe> newChildren) {
if (newChildren.size() != 1) {
throw new IllegalArgumentException("expected [1] children but received [" + newChildren.size() + "]");
}
return new ToStringFunctionPipe(source(), expression(), newChildren.get(0));
}

@Override
public final Pipe resolveAttributes(AttributeResolver resolver) {
Pipe newSource = source.resolveAttributes(resolver);
if (newSource == source) {
return this;
}
return replaceChildren(Collections.singletonList(newSource));
}

@Override
public boolean supportedByAggsOnlyQuery() {
return source.supportedByAggsOnlyQuery();
}

@Override
public boolean resolved() {
return source.resolved();
}

@Override
public final void collectFields(QlSourceBuilder sourceBuilder) {
source.collectFields(sourceBuilder);
}

@Override
protected NodeInfo<ToStringFunctionPipe> info() {
return NodeInfo.create(this, ToStringFunctionPipe::new, expression(), source);
}

@Override
public ToStringFunctionProcessor asProcessor() {
return new ToStringFunctionProcessor(source.asProcessor());
}

public Pipe src() {
return source;
}

@Override
public int hashCode() {
return Objects.hash(source);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}

if (obj == null || getClass() != obj.getClass()) {
return false;
}

ToStringFunctionPipe other = (ToStringFunctionPipe) obj;
return Objects.equals(source, other.source);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.eql.expression.function.scalar.string;

import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.xpack.ql.expression.gen.processor.Processor;

import java.io.IOException;
import java.util.Objects;

public class ToStringFunctionProcessor implements Processor {

public static final String NAME = "sstr";

private final Processor source;

public ToStringFunctionProcessor(Processor source) {
this.source = source;
}

public ToStringFunctionProcessor(StreamInput in) throws IOException {
source = in.readNamedWriteable(Processor.class);
}

@Override
public final void writeTo(StreamOutput out) throws IOException {
out.writeNamedWriteable(source);
}

@Override
public Object process(Object input) {
return doProcess(source.process(input));
}

public static Object doProcess(Object source) {
return source == null ? "null" : source.toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not null the instance instead of "null" the string?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

per #54465 and https://github.com/endgameinc/eql/blob/master/eql/functions.py#L626 I was following a contract (we can change it, of course) that this function always returns a string. So I catch null directly, since I can't call a method on it. Here's the current behavior for how it folds string(null):

>>> import eql
>>> eql.parse_expression("string(null)")
String(value='None')

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're not using some concept of Optional, returning null instead of a string "null" is better since otherwise there's no way to differentiate between a string with "null" chars vs actual null since they would both be equivalent which is not what we want.

}

protected Processor source() {
return source;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}

if (obj == null || getClass() != obj.getClass()) {
return false;
}

ToStringFunctionProcessor other = (ToStringFunctionProcessor) obj;
return Objects.equals(source(), other.source());
}

@Override
public int hashCode() {
return Objects.hash(source());
}


@Override
public String getWriteableName() {
return NAME;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

package org.elasticsearch.xpack.eql.expression.function.scalar.whitelist;

import org.elasticsearch.xpack.eql.expression.function.scalar.string.ToStringFunctionProcessor;
import org.elasticsearch.xpack.eql.expression.function.scalar.string.SubstringFunctionProcessor;
import org.elasticsearch.xpack.ql.expression.function.scalar.whitelist.InternalQlScriptUtils;

Expand All @@ -18,6 +19,10 @@ public class InternalEqlScriptUtils extends InternalQlScriptUtils {

InternalEqlScriptUtils() {}

public static String string(String s, Number start, Number end) {
return (String) ToStringFunctionProcessor.doProcess(s);
}

public static String substring(String s, Number start, Number end) {
return (String) SubstringFunctionProcessor.doProcess(s, start, end);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,4 +357,12 @@ public void testMultiField() {
accept(idxr, "foo where multi_field_nested.end_date == ''");
accept(idxr, "foo where multi_field_nested.start_date == 'bar'");
}

public void testStringFunctionWithText() {
final IndexResolution idxr = loadIndexResolution("mapping-multi-field.json");
assertEquals("1:15: [string(multi_field.english)] cannot operate on first argument field " +
"of data type [text]: No keyword/multi-field defined exact matches for [english]; " +
"define one or use MATCH/QUERY instead",
error(idxr, "process where string(multi_field.english) == 'foo'"));
}
}
20 changes: 19 additions & 1 deletion x-pack/plugin/eql/src/test/resources/queryfolder_tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,27 @@ process where process_path == "*\\red_ttp\\wininit.*" and opcode in (0,1,2,3)
"term":{"opcode":{"value":3


stringFunction
process where string(pid) == "123"
"script":{"source":"InternalQlScriptUtils.nullSafeFilter(InternalQlScriptUtils.eq(
InternalEqlScriptUtils.string(InternalQlScriptUtils.docValue(doc,params.v0)),params.v1))",
"params":{"v0":"pid","v1":"123"}


stringFunction
process where string(pid) == string(123)
"script":{"source":"InternalQlScriptUtils.nullSafeFilter(InternalQlScriptUtils.eq(
InternalEqlScriptUtils.string(InternalQlScriptUtils.docValue(doc,params.v0)),params.v1))",
"params":{"v0":"pid","v1":"123"}


stringFunction
process where process_name == string(null)
"term":{"process_name":{"value":"null"


substringFunction
process where substring(file_name, -4) == '.exe'
"script":{"source":"InternalQlScriptUtils.nullSafeFilter(InternalQlScriptUtils.eq(
InternalEqlScriptUtils.substring(InternalQlScriptUtils.docValue(doc,params.v0),params.v1,params.v2),params.v3))",
"params":{"v0":"file_name.keyword","v1":-4,"v2":null,"v3":".exe"}