1
0
Эх сурвалжийг харах

EQL: Add number function (#55084)

* EQL: Add number function
* EQL: Fix the locale used for number for deterministic functionality
* EQL: Add more ToNumber tests
* EQL: Add more number ToNumberProcessor unit tests
* EQL: Remove unnecessary overrides, fix processor methods
* EQL: Remove additional unnecessary overrides
* EQL: Lint fixes for ToNumber
* EQL: ToNumber renames from PR feedback
* EQL: Remove NumberFormat locale handling
* EQL: Removed NumberFormat from ToNumber
* EQL: Add number function tests
* EQL: ToNumberProcessorTests formatting
* EQL: Remove newline in ToNumberProcessorTests
* EQL: Add number(..., null) test
* EQL: Create expression.function.scalar.math package
* EQL: Remove painless whitespace for ToNumber.asScript
* EQL: Add Long support
Ross Wolf 5 жил өмнө
parent
commit
b0863cc194

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

@@ -51,6 +51,32 @@ expected_event_ids  = [1, 2, 3, 4]
 query = 'process where string(serial_event_id) = "1"'
 expected_event_ids  = [1]
 
+
+[[queries]]
+query = 'any where number(string(serial_event_id)) == 17'
+expected_event_ids = [17]
+
+
+[[queries]]
+query = 'any where number(string(serial_event_id), null) == 17'
+expected_event_ids = [17]
+
+
+[[queries]]
+query = 'any where number(string(serial_event_id), 10) == 17'
+expected_event_ids = [17]
+
+
+[[queries]]
+query = 'any where number(string(serial_event_id), 13) == number("31", 13)'
+expected_event_ids = [31]
+
+
+[[queries]]
+query = 'any where number(string(serial_event_id), 16) == 17'
+expected_event_ids = [11]
+
+
 [[queries]]
 expected_event_ids  = [98]
 notes = "regexp doesn't support character classes"

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

@@ -822,24 +822,8 @@ process where modulo(11, add(serial_event_id, 1)) == serial_event_id'''
 expected_event_ids  = [1, 2, 3, 5, 11]
 description = "test built-in math functions"
 
-[[queries]]
-query = '''
-process where serial_event_id == number('5')'''
-expected_event_ids  = [5]
-description = "test string/number conversions"
-
-[[queries]]
-expected_event_ids  = [50]
-description = "test string/number conversions"
-query = '''
-process where serial_event_id == number('0x32', 16)'''
-
-[[queries]]
-expected_event_ids  = [50]
-description = "test string/number conversions"
-query = '''
-process where serial_event_id == number('32', 16)'''
 
+# this should be removed and the number function should be updated to take only strings
 [[queries]]
 query = '''
 process where number(serial_event_id) == number(5)'''

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

@@ -16,6 +16,7 @@ import org.elasticsearch.xpack.eql.expression.function.scalar.string.Match;
 import org.elasticsearch.xpack.eql.expression.function.scalar.string.StartsWith;
 import org.elasticsearch.xpack.eql.expression.function.scalar.string.StringContains;
 import org.elasticsearch.xpack.eql.expression.function.scalar.string.Substring;
+import org.elasticsearch.xpack.eql.expression.function.scalar.math.ToNumber;
 import org.elasticsearch.xpack.eql.expression.function.scalar.string.ToString;
 import org.elasticsearch.xpack.eql.expression.function.scalar.string.Wildcard;
 import org.elasticsearch.xpack.ql.expression.function.FunctionDefinition;
@@ -50,7 +51,7 @@ public class EqlFunctionRegistry extends FunctionRegistry {
                 def(ToString.class, ToString::new, "string"),
                 def(StringContains.class, StringContains::new, "stringcontains"),
                 def(Substring.class, Substring::new, "substring"),
-                def(Wildcard.class, Wildcard::new, "wildcard")
+                def(Wildcard.class, Wildcard::new, "wildcard"),
             },
         // Arithmetic
             new FunctionDefinition[] {
@@ -58,7 +59,8 @@ public class EqlFunctionRegistry extends FunctionRegistry {
                 def(Div.class, Div::new, "divide"),
                 def(Mod.class, Mod::new, "modulo"),
                 def(Mul.class, Mul::new, "multiply"),
-                def(Sub.class, Sub::new, "subtract")
+                def(ToNumber.class, ToNumber::new, "number"),
+                def(Sub.class, Sub::new, "subtract"),
             }
         };
     }

+ 116 - 0
x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/math/ToNumber.java

@@ -0,0 +1,116 @@
+/*
+ * 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.math;
+
+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.Literal;
+import org.elasticsearch.xpack.ql.expression.function.OptionalArgument;
+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.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+import static java.lang.String.format;
+import static org.elasticsearch.xpack.eql.expression.function.scalar.math.ToNumberFunctionProcessor.doProcess;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isInteger;
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isStringAndExact;
+import static org.elasticsearch.xpack.ql.expression.gen.script.ParamsBuilder.paramsBuilder;
+
+/**
+ * EQL specific function for parsing strings into numbers.
+ */
+public class ToNumber extends ScalarFunction implements OptionalArgument {
+
+    private final Expression value, base;
+
+    public ToNumber(Source source, Expression value, Expression base) {
+        super(source, Arrays.asList(value, base != null ? base : new Literal(source, null, DataTypes.NULL)));
+        this.value = value;
+        this.base = arguments().get(1);
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        if (!childrenResolved()) {
+            return new TypeResolution("Unresolved children");
+        }
+
+        TypeResolution valueResolution = isStringAndExact(value, sourceText(), ParamOrdinal.FIRST);
+        if (valueResolution.unresolved()) {
+            return valueResolution;
+        }
+
+        return isInteger(base, sourceText(), ParamOrdinal.SECOND);
+    }
+
+    @Override
+    protected Pipe makePipe() {
+        return new ToNumberFunctionPipe(source(), this, Expressions.pipe(value), Expressions.pipe(base));
+    }
+
+    @Override
+    public boolean foldable() {
+        return value.foldable() && base.foldable();
+    }
+
+    @Override
+    public Object fold() {
+        return doProcess(value.fold(), base.fold());
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, ToNumber::new, value, base);
+    }
+
+    @Override
+    public ScriptTemplate asScript() {
+        ScriptTemplate valueScript = asScript(value);
+        ScriptTemplate baseScript = asScript(base);
+
+        return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{eql}.%s(%s,%s)"),
+                "number",
+                valueScript.template(),
+                baseScript.template()),
+                paramsBuilder()
+                    .script(valueScript.params())
+                    .script(baseScript.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.DOUBLE;
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        if (newChildren.size() != 2) {
+            throw new IllegalArgumentException("expected [2] children but received [" + newChildren.size() + "]");
+        }
+
+        return new ToNumber(source(), newChildren.get(0), newChildren.get(1));
+    }
+}

+ 44 - 0
x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/math/ToNumberFunctionPipe.java

@@ -0,0 +1,44 @@
+/*
+ * 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.math;
+
+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.Arrays;
+import java.util.List;
+
+public class ToNumberFunctionPipe extends Pipe {
+
+    private final Pipe value, base;
+
+    public ToNumberFunctionPipe(Source source, Expression expression, Pipe value, Pipe base) {
+        super(source, expression, Arrays.asList(value, base));
+        this.value = value;
+        this.base = base;
+
+    }
+
+    @Override
+    public final Pipe replaceChildren(List<Pipe> newChildren) {
+        if (newChildren.size() != 2) {
+            throw new IllegalArgumentException("expected [2] children but received [" + newChildren.size() + "]");
+        }
+        return new ToNumberFunctionPipe(source(), expression(), newChildren.get(0), newChildren.get(1));
+    }
+
+    @Override
+    protected NodeInfo<ToNumberFunctionPipe> info() {
+        return NodeInfo.create(this, ToNumberFunctionPipe::new, expression(), value, base);
+    }
+
+    @Override
+    public ToNumberFunctionProcessor asProcessor() {
+        return new ToNumberFunctionProcessor(value.asProcessor(), base.asProcessor());
+    }
+}

+ 118 - 0
x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/math/ToNumberFunctionProcessor.java

@@ -0,0 +1,118 @@
+/*
+ * 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.math;
+
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.xpack.eql.EqlIllegalArgumentException;
+import org.elasticsearch.xpack.ql.expression.gen.processor.Processor;
+
+import java.io.IOException;
+import java.util.Objects;
+
+public class ToNumberFunctionProcessor implements Processor {
+
+    public static final String NAME = "num";
+
+    private final Processor value, base;
+
+    public ToNumberFunctionProcessor(Processor value, Processor base) {
+        this.value = value;
+        this.base = base;
+    }
+
+    public ToNumberFunctionProcessor(StreamInput in) throws IOException {
+        value = in.readNamedWriteable(Processor.class);
+        base = in.readNamedWriteable(Processor.class);
+    }
+
+    @Override
+    public final void writeTo(StreamOutput out) throws IOException {
+        out.writeNamedWriteable(value);
+        out.writeNamedWriteable(base);
+    }
+
+    @Override
+    public Object process(Object input) {
+        return doProcess(value.process(input), base.process(input));
+    }
+
+    private static Number parseDecimal(String source) {
+        try {
+            return Long.valueOf(source);
+        } catch (NumberFormatException e) {
+            return Double.valueOf(source);
+        }
+    }
+
+    public static Object doProcess(Object value, Object base) {
+        if (value == null) {
+            return null;
+        }
+
+        if (!(value instanceof String || value instanceof Character)) {
+            throw new EqlIllegalArgumentException("A string/char is required; received [{}]", value);
+        }
+
+        boolean detectedHexPrefix = value.toString().startsWith("0x");
+
+        if (base == null) {
+            base = detectedHexPrefix ? 16 : 10;
+        } else if (base instanceof Integer == false) {
+            throw new EqlIllegalArgumentException("An integer base is required; received [{}]", base);
+        }
+
+        int radix = (Integer) base;
+
+        if (detectedHexPrefix && radix == 16) {
+            value = value.toString().substring(2);
+        }
+
+        try {
+            if (radix == 10) {
+                return parseDecimal(value.toString());
+            } else {
+                return Long.parseLong(value.toString(), radix);
+            }
+        } catch (NumberFormatException e) {
+            throw new EqlIllegalArgumentException("Unable to convert [{}] to number of base [{}]", value, radix);
+        }
+
+    }
+
+    protected Processor value() {
+        return value;
+    }
+
+    protected Processor base() {
+        return base;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+
+        ToNumberFunctionProcessor other = (ToNumberFunctionProcessor) obj;
+        return Objects.equals(value(), other.value()) && Objects.equals(base(), other.base());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(value, base);
+    }
+
+
+    @Override
+    public String getWriteableName() {
+        return NAME;
+    }
+}

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

@@ -13,6 +13,7 @@ import org.elasticsearch.xpack.eql.expression.function.scalar.string.IndexOfFunc
 import org.elasticsearch.xpack.eql.expression.function.scalar.string.LengthFunctionProcessor;
 import org.elasticsearch.xpack.eql.expression.function.scalar.string.StringContainsFunctionProcessor;
 import org.elasticsearch.xpack.eql.expression.function.scalar.string.SubstringFunctionProcessor;
+import org.elasticsearch.xpack.eql.expression.function.scalar.math.ToNumberFunctionProcessor;
 import org.elasticsearch.xpack.eql.expression.function.scalar.string.ToStringFunctionProcessor;
 import org.elasticsearch.xpack.ql.expression.function.scalar.whitelist.InternalQlScriptUtils;
 
@@ -55,6 +56,10 @@ public class InternalEqlScriptUtils extends InternalQlScriptUtils {
         return (Boolean) StringContainsFunctionProcessor.doProcess(string, substring);
     }
 
+    public static Number number(String source, Number base) {
+        return (Number) ToNumberFunctionProcessor.doProcess(source, base);
+    }
+
     public static String substring(String s, Number start, Number end) {
         return (String) SubstringFunctionProcessor.doProcess(s, start, end);
     }

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

@@ -70,6 +70,7 @@ class org.elasticsearch.xpack.eql.expression.function.scalar.whitelist.InternalE
   Boolean endsWith(String, String)
   Integer indexOf(String, String, Number)
   Integer length(String)
+  Number  number(String, Number)
   String  string(Object)
   Boolean stringContains(String, String)
   String  substring(String, Number, Number)

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

@@ -116,12 +116,6 @@ public class VerifierTests extends ESTestCase {
                 error("network where safe(process_name)"));
     }
 
-    // Test the known EQL functions that are not supported
-    public void testFunctionVerificationUnknown() {
-        assertEquals("1:34: Unknown function [number]",
-                error("process where serial_event_id == number('5')"));
-    }
-
     // Test unsupported array indexes
     public void testArrayIndexesUnsupported() {
         assertEquals("1:84: Array indexes are not supported",
@@ -323,13 +317,13 @@ public class VerifierTests extends ESTestCase {
         assertEquals("1:11: Cannot use field [multi_field_nested.dep_name] type [text] with unsupported nested type in hierarchy " +
                         "(field [multi_field_nested])",
                 error(idxr, "foo where multi_field_nested.dep_name == 'bar'"));
-        assertEquals("1:11: Cannot use field [multi_field_nested.dep_id.keyword] type [keyword] with unsupported nested type in " + 
+        assertEquals("1:11: Cannot use field [multi_field_nested.dep_id.keyword] type [keyword] with unsupported nested type in " +
                         "hierarchy (field [multi_field_nested])",
                 error(idxr, "foo where multi_field_nested.dep_id.keyword == 'bar'"));
-        assertEquals("1:11: Cannot use field [multi_field_nested.end_date] type [datetime] with unsupported nested type in " + 
+        assertEquals("1:11: Cannot use field [multi_field_nested.end_date] type [datetime] with unsupported nested type in " +
                         "hierarchy (field [multi_field_nested])",
                 error(idxr, "foo where multi_field_nested.end_date == ''"));
-        assertEquals("1:11: Cannot use field [multi_field_nested.start_date] type [datetime] with unsupported nested type in " + 
+        assertEquals("1:11: Cannot use field [multi_field_nested.start_date] type [datetime] with unsupported nested type in " +
                         "hierarchy (field [multi_field_nested])",
                 error(idxr, "foo where multi_field_nested.start_date == 'bar'"));
     }

+ 167 - 0
x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/expression/function/scalar/math/ToNumberProcessorTests.java

@@ -0,0 +1,167 @@
+/*
+ * 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.math;
+
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xpack.ql.QlIllegalArgumentException;
+
+import static org.elasticsearch.xpack.ql.expression.function.scalar.FunctionTestUtils.l;
+import static org.elasticsearch.xpack.ql.tree.Source.EMPTY;
+
+
+public class ToNumberProcessorTests extends ESTestCase {
+
+    private static Object process(Object value, Object base) {
+        return new ToNumber(EMPTY, l(value), l(base)).makePipe().asProcessor().process(null);
+    }
+
+    private static String error(Object value, Object base) {
+        QlIllegalArgumentException saie = expectThrows(QlIllegalArgumentException.class,
+            () -> new ToNumber(EMPTY, l(value), l(base)).makePipe().asProcessor().process(null));
+        return saie.getMessage();
+    }
+
+    public void toNumberWithLongRange() {
+        long number = randomLongBetween(Integer.MAX_VALUE, Long.MAX_VALUE);
+
+        assertEquals(number, process(Long.toString(number), null));
+        assertEquals(number, process("0x" + Long.toHexString(number), null));
+
+        assertEquals(number, process(Long.toString(number), 10));
+        assertEquals(number, process(Long.toOctalString(number), 8));
+        assertEquals(number, process(Long.toHexString(number), 16));
+        assertEquals(number, process("0x" + Long.toHexString(number), 16));
+    }
+
+    public void toNumberWithPositiveInteger() {
+        int number = randomIntBetween(0, 1000);
+
+        assertEquals(number, process(Integer.toString(number), null));
+        assertEquals(number, process("0x" + Integer.toHexString(number), null));
+
+        assertEquals(number, process(Integer.toString(number), 10));
+        assertEquals(number, process(Integer.toOctalString(number), 8));
+        assertEquals(number, process(Integer.toHexString(number), 16));
+        assertEquals(number, process("0x" + Integer.toHexString(number), 16));
+    }
+
+    public void toNumberWithNegativeInteger() {
+        int posInt = randomIntBetween(1, 1000);
+        int negInt = -posInt;
+
+        assertEquals(negInt, process(Integer.toString(negInt), null));
+
+        assertEquals(negInt, process(Integer.toString(negInt), 10));
+        assertEquals(negInt, process("-" + Integer.toOctalString(posInt), 8));
+        assertEquals(negInt, process("-" + Integer.toHexString(posInt), 16));
+
+        assertEquals(negInt, process("-0x" + Integer.toHexString(posInt), 16));
+    }
+
+    public void toNumberWithPositiveFloat() {
+        double number = randomDoubleBetween(0.0, 1000.0, true);
+
+        assertEquals(number, process(Double.toString(number), null));
+        assertEquals(number, process(Double.toString(number), 10));
+    }
+
+    public void toNumberWithNegativeFloat() {
+        double number = randomDoubleBetween(-1000.0, -0.1, true);
+
+        assertEquals(number, process(Double.toString(number), null));
+        assertEquals(number, process(Double.toString(number), 10));
+    }
+
+    public void toNumberWithMissingInput() {
+        assertNull(process(null, null));
+        assertNull(process(null, 8));
+        assertNull(process(null, 10));
+        assertNull(process(null, 16));
+    }
+
+    public void toNumberWithPositiveExponent() {
+        int number = randomIntBetween(-100, 100);
+        int exponent = randomIntBetween(0, 20);
+
+        double expected = Math.pow((double) number, (double) exponent);
+
+        assertEquals(expected, process(number  + "e" + exponent, null));
+        assertEquals(expected, process(number  + "e" + exponent, 10));
+    }
+
+    public void toNumberWithNegativeExponent() {
+        int number = randomIntBetween(-100, 100);
+        int exponent = randomIntBetween(-10, -1);
+
+        double expected = Math.pow(number, exponent);
+
+        assertEquals(expected, process(number  + "e-" + exponent, null));
+        assertEquals(expected, process(number  + "e-" + exponent, 10));
+    }
+
+    public void toNumberWithLocales() {
+        assertEquals("Unable to convert [1,000] to number of base [10]",
+            error("1,000", 7));
+        assertEquals("Unable to convert [1,000] to number of base [10]",
+            error("1,000,000", 7));
+        assertEquals("Unable to convert [1,000] to number of base [10]",
+            error("1.000.000", 7));
+        assertEquals("Unable to convert [1,000] to number of base [10]",
+            error("1,000.000.000", 7));
+    }
+
+    public void toNumberWithUnsupportedDoubleBase() {
+        // test that only base 10 fractions are supported
+        double decimal = randomDouble();
+        assertEquals("Unable to convert [1.0] to number of base [7]",
+            error(Double.toString(decimal), 7));
+        assertEquals("Unable to convert [1.0] to number of base [8]",
+            error(Double.toString(decimal), 8));
+        assertEquals("Unable to convert [1.0] to number of base [16]",
+            error(Double.toString(decimal), 16));
+    }
+
+    public void testNegativeBase16() {
+        assertEquals("Unable to convert [-0x1] to number of base [16]",
+            error("-0x1", 16));
+    }
+
+    public void testNumberInvalidDataType() {
+        assertEquals("A string/char is required; received [false]",
+            error(false, null));
+        assertEquals("A string/char is required; received [1.0]",
+            error(1.0, null));
+        assertEquals("A string/char is required; received [1]",
+            error(1, null));
+    }
+
+    public void testInvalidBase() {
+        int number = randomIntBetween(-100, 100);
+
+        assertEquals("An integer base is required; received [foo]",
+            error(Integer.toString(number), "foo"));
+        assertEquals("An integer base is required; received [1.0]",
+            error(Integer.toString(number), 1.0));
+        assertEquals("An integer base is required; received [false]",
+            error(Integer.toString(number), false));
+    }
+
+    public void testInvalidSourceString() {
+        assertEquals("Unable to convert [] to number of base [10]",
+            error("", null));
+        assertEquals("Unable to convert [] to number of base [16]",
+            error("", 16));
+        assertEquals("Unable to convert [foo] to number of base [10]",
+            error("foo", null));
+        assertEquals("Unable to convert [foo] to number of base [16]",
+            error("foo", 16));
+        assertEquals("Unable to convert [1.2.3.4] to number of base [10]",
+            error("1.2.3.4", 10));
+        assertEquals("Unable to convert [1.2.3.4] to number of base [16]",
+            error("1.2.3.4", 16));
+    }
+}

+ 26 - 1
x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/planner/QueryFolderFailTests.java

@@ -158,6 +158,32 @@ public class QueryFolderFailTests extends AbstractQueryFolderTestCase {
             "must be [string], found value [1] type [integer]", msg);
     }
 
+    public void testNumberFunctionAlreadyNumber() {
+        VerificationException e = expectThrows(VerificationException.class,
+            () -> plan("process where number(pid) == 1"));
+        String msg = e.getMessage();
+        assertEquals("Found 1 problem\nline 1:15: first argument of [number(pid)] must be [string], "
+            + "found value [pid] type [long]", msg);
+    }
+
+    public void testNumberFunctionFloatBase() {
+        VerificationException e = expectThrows(VerificationException.class,
+            () -> plan("process where number(process_name, 1.0) == 1"));
+        String msg = e.getMessage();
+        assertEquals("Found 1 problem\nline 1:15: second argument of [number(process_name, 1.0)] must be [integer], "
+            + "found value [1.0] type [double]", msg);
+
+    }
+
+    public void testNumberFunctionNonString() {
+        VerificationException e = expectThrows(VerificationException.class,
+            () -> plan("process where number(plain_text) == 1"));
+        String msg = e.getMessage();
+        assertEquals("Found 1 problem\nline 1:15: [number(plain_text)] cannot operate on first argument field of data type "
+            + "[text]: No keyword/multi-field defined exact matches for [plain_text]; define one or use MATCH/QUERY instead", msg);
+
+    }
+
     public void testPropertyEquationFilterUnsupported() {
         QlIllegalArgumentException e = expectThrows(QlIllegalArgumentException.class,
                 () -> plan("process where (serial_event_id<9 and serial_event_id >= 7) or (opcode == pid)"));
@@ -191,7 +217,6 @@ public class QueryFolderFailTests extends AbstractQueryFolderTestCase {
                 + "[text]: No keyword/multi-field defined exact matches for [plain_text]; define one or use MATCH/QUERY instead", msg);
     }
 
-
     public void testStringContainsWrongParams() {
         assertEquals("1:16: error building [stringcontains]: expects exactly two arguments",
                 errorParsing("process where stringContains()"));

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

@@ -257,6 +257,12 @@ process where match(command_line, "^.*?net.exe", "net\\.exe", "C:\\\\Windows\\\\
 "regexp":{"command_line":{"value":"^.*?net.exe|net\\.exe|C:\\\\Windows\\\\system32\\\\net1\\s+"
 ;
 
+numberFunctionSingleArgument
+process where number(process_name) == 1;
+InternalEqlScriptUtils.number(InternalQlScriptUtils.docValue(doc,params.v0),params.v1)
+"params":{"v0":"process_name","v1":null,"v2":1}
+;
+
 matchFunctionScalar
 process where match(substring(command_line, 5), "^.*?net.exe", "net\\.exe", "C:\\\\Windows\\\\system32\\\\net1\\s+")
 ;
@@ -265,6 +271,30 @@ InternalQlScriptUtils.docValue(doc,params.v0),params.v1,params.v2),params.v3))",
 "params":{"v0":"command_line","v1":5,"v2":null,"v3":"^.*?net.exe|net\\.exe|C:\\\\Windows\\\\system32\\\\net1\\s+"}}
 ;
 
+
+numberFunctionTwoFieldArguments
+process where number(process_name, pid) != null;
+InternalEqlScriptUtils.number(InternalQlScriptUtils.docValue(doc,params.v0),InternalQlScriptUtils.docValue(doc,params.v1))))",
+"params":{"v0":"process_name","v1":"pid"}
+;
+
+numberFunctionTwoArguments
+process where number(process_name, 16) != null;
+InternalEqlScriptUtils.number(InternalQlScriptUtils.docValue(doc,params.v0),params.v1)
+"params":{"v0":"process_name","v1":16}
+;
+
+numberFunctionFoldedComparison
+process where serial_event_id == number("-32.5");
+{"term":{"serial_event_id":{"value":-32.5,
+;
+
+numberFunctionFoldedHexComparison
+process where serial_event_id == number("0x32", 16);
+{"term":{"serial_event_id":{"value":50,
+;
+
+
 wildcardFunctionSingleArgument
 process where wildcard(process_path, "*\\red_ttp\\wininit.*")
 ;