Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 @@ -2,6 +2,15 @@
query = 'process where serial_event_id = 1'
expected_event_ids = [1]

[[queries]]
query = 'process where string(serial_event_id) = "1"'
expected_event_ids = [1]

[[queries]]
note = "check that string(null) returns 'null'"
expected_event_ids = [1, 2]
query = 'process where opcode == 3 and string(ppid) == "null"'

Copy link
Contributor

Choose a reason for hiding this comment

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

The original idea was to keep this file unchanged from the original eql implementation
https://github.com/endgameinc/eql/blob/master/eql/etc/test_queries.toml
so we can sync up easier with the reference repo/impl

and add more queries into separate file(s), could be just one, could be individual files for specific extensions
started doing something like this in my other PR
https://github.com/elastic/elasticsearch/pull/54277/files/be0b67cdf5022f38ec3610a68bbb91af8b10b8b3#diff-00cfec987b028a9cc67295e57c02cf68R1

[[queries]]
query = 'process where serial_event_id < 4'
expected_event_ids = [1, 2, 3]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.elasticsearch.xpack.eql.expression.function.scalar.string.Length;
import org.elasticsearch.xpack.eql.expression.function.scalar.string.StartsWith;
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 @@ -20,7 +21,7 @@ public class EqlFunctionRegistry extends FunctionRegistry {
public EqlFunctionRegistry() {
super(functions());
}

private static FunctionDefinition[][] functions() {
return new FunctionDefinition[][] {
// Scalar functions
Expand All @@ -29,7 +30,8 @@ private static FunctionDefinition[][] functions() {
def(EndsWith.class, EndsWith::new, "endswith"),
def(Length.class, Length::new, "length"),
def(StartsWith.class, StartsWith::new, "startswith"),
def(Substring.class, Substring::new, "substring")
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,104 @@
/*
* 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.DEFAULT);
}

@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 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.EndsWithFunctionProcessor;
import org.elasticsearch.xpack.eql.expression.function.scalar.string.LengthFunctionProcessor;
import org.elasticsearch.xpack.eql.expression.function.scalar.string.StartsWithFunctionProcessor;
Expand All @@ -21,6 +22,7 @@ public class InternalEqlScriptUtils extends InternalQlScriptUtils {

InternalEqlScriptUtils() {}


public static Boolean endsWith(String s, String pattern) {
return (Boolean) EndsWithFunctionProcessor.doProcess(s, pattern);
}
Expand All @@ -33,6 +35,10 @@ public static Boolean startsWith(String s, String pattern) {
return (Boolean) StartsWithFunctionProcessor.doProcess(s, pattern);
}

public static String string(Object s) {
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 @@ -12,7 +12,7 @@ class org.elasticsearch.xpack.ql.expression.function.scalar.whitelist.InternalQl

#
# Utilities
#
#
def docValue(java.util.Map, String)
boolean nullSafeFilter(Boolean)
double nullSafeSortNumeric(Number)
Expand Down Expand Up @@ -54,9 +54,10 @@ class org.elasticsearch.xpack.eql.expression.function.scalar.whitelist.InternalE

#
# ASCII Functions
#
#
Boolean endsWith(String, String)
Integer length(String)
Boolean startsWith(String, String)
String substring(String, Number, Number)
String string(Object)
}
Original file line number Diff line number Diff line change
Expand Up @@ -359,4 +359,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'"));
}
}
Loading