diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java index 212149683ffd9..c9d652861f800 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java @@ -68,6 +68,15 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.string.RTrim; import org.elasticsearch.xpack.sql.expression.function.scalar.string.Space; import org.elasticsearch.xpack.sql.expression.function.scalar.string.UCase; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.Concat; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.Insert; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.Left; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.Locate; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.Position; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.Repeat; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.Replace; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.Right; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.Substring; import org.elasticsearch.xpack.sql.parser.ParsingException; import org.elasticsearch.xpack.sql.tree.Location; import org.elasticsearch.xpack.sql.util.StringUtils; @@ -154,6 +163,15 @@ public class FunctionRegistry { def(LTrim.class, LTrim::new), def(RTrim.class, RTrim::new), def(Space.class, Space::new), + def(Concat.class, Concat::new), + def(Insert.class, Insert::new), + def(Left.class, Left::new), + def(Locate.class, Locate::new), + def(Position.class, Position::new), + def(Repeat.class, Repeat::new), + def(Replace.class, Replace::new), + def(Right.class, Right::new), + def(Substring.class, Substring::new), def(UCase.class, UCase::new), // Special def(Score.class, Score::new))); @@ -337,6 +355,47 @@ private static FunctionDefinition def(Class function, Functi private interface FunctionBuilder { Function build(Location location, List children, boolean distinct, TimeZone tz); } + + @SuppressWarnings("overloads") // These are ambiguous if you aren't using ctor references but we always do + static FunctionDefinition def(Class function, + ThreeParametersFunctionBuilder ctorRef, String... aliases) { + FunctionBuilder builder = (location, children, distinct, tz) -> { + boolean isLocateFunction = function.isAssignableFrom(Locate.class); + if (isLocateFunction && (children.size() > 3 || children.size() < 2)) { + throw new IllegalArgumentException("expects two or three arguments"); + } else if (!isLocateFunction && children.size() != 3) { + throw new IllegalArgumentException("expects exactly three arguments"); + } + if (distinct) { + throw new IllegalArgumentException("does not support DISTINCT yet it was specified"); + } + return ctorRef.build(location, children.get(0), children.get(1), children.size() == 3 ? children.get(2) : null); + }; + return def(function, builder, false, aliases); + } + + interface ThreeParametersFunctionBuilder { + T build(Location location, Expression source, Expression exp1, Expression exp2); + } + + @SuppressWarnings("overloads") // These are ambiguous if you aren't using ctor references but we always do + static FunctionDefinition def(Class function, + FourParametersFunctionBuilder ctorRef, String... aliases) { + FunctionBuilder builder = (location, children, distinct, tz) -> { + if (children.size() != 4) { + throw new IllegalArgumentException("expects exactly four arguments"); + } + if (distinct) { + throw new IllegalArgumentException("does not support DISTINCT yet it was specified"); + } + return ctorRef.build(location, children.get(0), children.get(1), children.get(2), children.get(3)); + }; + return def(function, builder, false, aliases); + } + + interface FourParametersFunctionBuilder { + T build(Location location, Expression source, Expression exp1, Expression exp2, Expression exp3); + } private static String normalize(String name) { // translate CamelCase to camel_case diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java index 3c3f629cc1caf..0f36654fa4aff 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java @@ -1,5 +1,5 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +* 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. */ @@ -18,6 +18,13 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.HitExtractorProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor; import org.elasticsearch.xpack.sql.expression.function.scalar.string.StringProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringNumericProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringStringProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.ConcatFunctionProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.InsertFunctionProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.LocateFunctionProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.ReplaceFunctionProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.SubstringFunctionProcessor; import java.util.ArrayList; import java.util.List; @@ -49,6 +56,13 @@ public static List getNamedWriteables() { entries.add(new Entry(Processor.class, MathProcessor.NAME, MathProcessor::new)); // string entries.add(new Entry(Processor.class, StringProcessor.NAME, StringProcessor::new)); + entries.add(new Entry(Processor.class, BinaryStringNumericProcessor.NAME, BinaryStringNumericProcessor::new)); + entries.add(new Entry(Processor.class, BinaryStringStringProcessor.NAME, BinaryStringStringProcessor::new)); + entries.add(new Entry(Processor.class, ConcatFunctionProcessor.NAME, ConcatFunctionProcessor::new)); + entries.add(new Entry(Processor.class, InsertFunctionProcessor.NAME, InsertFunctionProcessor::new)); + entries.add(new Entry(Processor.class, LocateFunctionProcessor.NAME, LocateFunctionProcessor::new)); + entries.add(new Entry(Processor.class, ReplaceFunctionProcessor.NAME, ReplaceFunctionProcessor::new)); + entries.add(new Entry(Processor.class, SubstringFunctionProcessor.NAME, SubstringFunctionProcessor::new)); return entries; } } \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringFunction.java new file mode 100644 index 0000000000000..2ee241c8a0fe7 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringFunction.java @@ -0,0 +1,96 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.FieldAttribute; +import org.elasticsearch.xpack.sql.expression.function.scalar.BinaryScalarFunction; +import org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.type.DataType; +import org.elasticsearch.xpack.sql.util.StringUtils; + +import java.util.Locale; +import java.util.Objects; +import java.util.function.BiFunction; + +import static java.lang.String.format; +import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ParamsBuilder.paramsBuilder; +import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate.formatTemplate; + +/** + * Base class for binary functions that have the first parameter a string, the second parameter a number + * or a string and the result can be a string or a number. + */ +public abstract class BinaryStringFunction extends BinaryScalarFunction { + + protected BinaryStringFunction(Location location, Expression left, Expression right) { + super(location, left, right); + } + + /* + * the operation the binary function handles can receive one String argument, a number or String as second argument + * and it can return a number or a String. The BiFunction below is the base operation for the subsequent implementations. + * T is the second argument, R is the result of applying the operation. + */ + protected abstract BiFunction operation(); + + @Override + protected TypeResolution resolveType() { + if (!childrenResolved()) { + return new TypeResolution("Unresolved children"); + } + + if (!left().dataType().isString()) { + return new TypeResolution("'%s' requires first parameter to be a string type, received %s", functionName(), left().dataType()); + } + + return resolveSecondParameterInputType(right().dataType()); + } + + protected abstract TypeResolution resolveSecondParameterInputType(DataType inputType); + + @Override + public Object fold() { + @SuppressWarnings("unchecked") + T fold = (T) right().fold(); + return operation().apply((String) left().fold(), fold); + } + + @Override + protected ScriptTemplate asScriptFrom(ScriptTemplate leftScript, ScriptTemplate rightScript) { + // basically, transform the script to InternalSqlScriptUtils.[function_name](function_or_field1, function_or_field2) + return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{sql}.%s(%s,%s)"), + StringUtils.underscoreToLowerCamelCase(operation().toString()), + leftScript.template(), + rightScript.template()), + paramsBuilder() + .script(leftScript.params()).script(rightScript.params()) + .build(), dataType()); + } + + @Override + protected ScriptTemplate asScriptFrom(FieldAttribute field) { + return new ScriptTemplate(formatScript("doc[{}].value"), + paramsBuilder().variable(field.isInexact() ? field.exactAttribute().name() : field.name()).build(), + dataType()); + } + + @Override + public int hashCode() { + return Objects.hash(left(), right()); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != getClass()) { + return false; + } + BinaryStringFunction other = (BinaryStringFunction) obj; + return Objects.equals(other.left(), left()) + && Objects.equals(other.right(), right()); + } +} \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericFunction.java new file mode 100644 index 0000000000000..3e3637d57df2d --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericFunction.java @@ -0,0 +1,32 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.type.DataType; + +/** + * A binary string function with a numeric second parameter and a string result + */ +public abstract class BinaryStringNumericFunction extends BinaryStringFunction { + + public BinaryStringNumericFunction(Location location, Expression left, Expression right) { + super(location, left, right); + } + + @Override + protected TypeResolution resolveSecondParameterInputType(DataType inputType) { + return inputType.isNumeric() ? + TypeResolution.TYPE_RESOLVED : + new TypeResolution("'%s' requires second parameter to be a numeric type, received %s", functionName(), inputType); + } + + @Override + public DataType dataType() { + return DataType.KEYWORD; + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericProcessor.java new file mode 100644 index 0000000000000..683bcba5c3f45 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericProcessor.java @@ -0,0 +1,92 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringNumericProcessor.BinaryStringNumericOperation; + +import java.io.IOException; +import java.util.function.BiFunction; + +/** + * Processor class covering string manipulating functions that have the first parameter as string, + * second parameter as numeric and a string result. + */ +public class BinaryStringNumericProcessor extends BinaryStringProcessor { + + public static final String NAME = "sn"; + + public BinaryStringNumericProcessor(StreamInput in) throws IOException { + super(in, i -> i.readEnum(BinaryStringNumericOperation.class)); + } + + public BinaryStringNumericProcessor(Processor left, Processor right, BinaryStringNumericOperation operation) { + super(left, right, operation); + } + + public enum BinaryStringNumericOperation implements BiFunction { + LEFT((s,c) -> { + int i = c.intValue(); + if (i < 0) return ""; + return i > s.length() ? s : s.substring(0, i); + }), + RIGHT((s,c) -> { + int i = c.intValue(); + if (i < 0) return ""; + return i > s.length() ? s : s.substring(s.length() - i); + }), + REPEAT((s,c) -> { + int i = c.intValue(); + if (i <= 0) return null; + + StringBuilder sb = new StringBuilder(s.length() * i); + for (int j = 0; j < i; j++) { + sb.append(s); + } + return sb.toString(); + }); + + BinaryStringNumericOperation(BiFunction op) { + this.op = op; + } + + private final BiFunction op; + + @Override + public String apply(String stringExp, Number count) { + return op.apply(stringExp, count); + } + } + + @Override + protected void doWrite(StreamOutput out) throws IOException { + out.writeEnum(operation()); + } + + @Override + protected Object doProcess(Object left, Object right) { + if (left == null || right == null) { + return null; + } + if (!(left instanceof String || left instanceof Character)) { + throw new SqlIllegalArgumentException("A string/char is required; received [{}]", left); + } + if (!(right instanceof Number)) { + throw new SqlIllegalArgumentException("A number is required; received [{}]", right); + } + + return operation().apply(left instanceof Character ? left.toString() : (String) left, (Number) right); + } + + @Override + public String getWriteableName() { + return NAME; + } + +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericProcessorDefinition.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericProcessorDefinition.java new file mode 100644 index 0000000000000..b0adcdf1ff2c6 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericProcessorDefinition.java @@ -0,0 +1,69 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.BinaryProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringNumericProcessor.BinaryStringNumericOperation; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; + +import java.util.Objects; + +/** + * Processor definition for String operations requiring one string and one numeric argument. + */ +public class BinaryStringNumericProcessorDefinition extends BinaryProcessorDefinition { + + private final BinaryStringNumericOperation operation; + + public BinaryStringNumericProcessorDefinition(Location location, Expression expression, ProcessorDefinition left, + ProcessorDefinition right, BinaryStringNumericOperation operation) { + super(location, expression, left, right); + this.operation = operation; + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, BinaryStringNumericProcessorDefinition::new, expression(), left(), right(), operation()); + } + + public BinaryStringNumericOperation operation() { + return operation; + } + + @Override + protected BinaryProcessorDefinition replaceChildren(ProcessorDefinition newLeft, ProcessorDefinition newRight) { + return new BinaryStringNumericProcessorDefinition(location(), expression(), newLeft, newRight, operation()); + } + + @Override + public BinaryStringNumericProcessor asProcessor() { + return new BinaryStringNumericProcessor(left().asProcessor(), right().asProcessor(), operation()); + } + + @Override + public int hashCode() { + return Objects.hash(left(), right(), operation); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + BinaryStringNumericProcessorDefinition other = (BinaryStringNumericProcessorDefinition) obj; + return Objects.equals(operation, other.operation) + && Objects.equals(left(), other.left()) + && Objects.equals(right(), other.right()); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringProcessor.java new file mode 100644 index 0000000000000..0303d15adad7b --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringProcessor.java @@ -0,0 +1,59 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.BinaryProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor; + +import java.io.IOException; +import java.util.Objects; +import java.util.function.BiFunction; + +public abstract class BinaryStringProcessor & BiFunction, T, R> extends BinaryProcessor { + + private final O operation; + + public BinaryStringProcessor(Processor left, Processor right, O operation) { + super(left, right); + this.operation = operation; + } + + public BinaryStringProcessor(StreamInput in, Reader reader) throws IOException { + super(in); + operation = reader.read(in); + } + + protected O operation() { + return operation; + } + + @Override + public int hashCode() { + return Objects.hash(operation); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + BinaryStringProcessor other = (BinaryStringProcessor) obj; + return Objects.equals(operation, other.operation) + && Objects.equals(left(), other.left()) + && Objects.equals(right(), other.right()); + } + + @Override + public String toString() { + return operation.toString(); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringStringFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringStringFunction.java new file mode 100644 index 0000000000000..0c3c8d9453ccd --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringStringFunction.java @@ -0,0 +1,32 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.type.DataType; + +/** + * A binary string function with two string parameters and a numeric result + */ +public abstract class BinaryStringStringFunction extends BinaryStringFunction { + + public BinaryStringStringFunction(Location location, Expression left, Expression right) { + super(location, left, right); + } + + @Override + protected TypeResolution resolveSecondParameterInputType(DataType inputType) { + return inputType.isString() ? + TypeResolution.TYPE_RESOLVED : + new TypeResolution("'%s' requires second parameter to be a string type, received %s", functionName(), inputType); + } + + @Override + public DataType dataType() { + return DataType.INTEGER; + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringStringProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringStringProcessor.java new file mode 100644 index 0000000000000..8f3425bdb4e88 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringStringProcessor.java @@ -0,0 +1,77 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringStringProcessor.BinaryStringStringOperation; + +import java.io.IOException; +import java.util.function.BiFunction; + +/** + * Processor class covering string manipulating functions that have two string parameters and a numeric result. + */ +public class BinaryStringStringProcessor extends BinaryStringProcessor { + + public static final String NAME = "ss"; + + public BinaryStringStringProcessor(StreamInput in) throws IOException { + super(in, i -> i.readEnum(BinaryStringStringOperation.class)); + } + + public BinaryStringStringProcessor(Processor left, Processor right, BinaryStringStringOperation operation) { + super(left, right, operation); + } + + public enum BinaryStringStringOperation implements BiFunction { + POSITION((sub,str) -> { + if (sub == null || str == null) return null; + int pos = str.indexOf(sub); + return pos < 0 ? 0 : pos+1; + }); + + BinaryStringStringOperation(BiFunction op) { + this.op = op; + } + + private final BiFunction op; + + @Override + public Number apply(String stringExpLeft, String stringExpRight) { + return op.apply(stringExpLeft, stringExpRight); + } + } + + @Override + protected void doWrite(StreamOutput out) throws IOException { + out.writeEnum(operation()); + } + + @Override + protected Object doProcess(Object left, Object right) { + if (left == null || right == null) { + return null; + } + if (!(left instanceof String || left instanceof Character)) { + throw new SqlIllegalArgumentException("A string/char is required; received [{}]", left); + } + if (!(right instanceof String || right instanceof Character)) { + throw new SqlIllegalArgumentException("A string/char is required; received [{}]", right); + } + + return operation().apply(left instanceof Character ? left.toString() : (String) left, + right instanceof Character ? right.toString() : (String) right); + } + + @Override + public String getWriteableName() { + return NAME; + } + +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringStringProcessorDefinition.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringStringProcessorDefinition.java new file mode 100644 index 0000000000000..33692845bf96e --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringStringProcessorDefinition.java @@ -0,0 +1,69 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.BinaryProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringStringProcessor.BinaryStringStringOperation; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; + +import java.util.Objects; + +/** + * Processor definition for String operations requiring two string arguments. + */ +public class BinaryStringStringProcessorDefinition extends BinaryProcessorDefinition { + + private final BinaryStringStringOperation operation; + + public BinaryStringStringProcessorDefinition(Location location, Expression expression, ProcessorDefinition left, + ProcessorDefinition right, BinaryStringStringOperation operation) { + super(location, expression, left, right); + this.operation = operation; + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, BinaryStringStringProcessorDefinition::new, expression(), left(), right(), operation); + } + + public BinaryStringStringOperation operation() { + return operation; + } + + @Override + protected BinaryProcessorDefinition replaceChildren(ProcessorDefinition left, ProcessorDefinition right) { + return new BinaryStringStringProcessorDefinition(location(), expression(), left, right, operation); + } + + @Override + public BinaryStringStringProcessor asProcessor() { + return new BinaryStringStringProcessor(left().asProcessor(), right().asProcessor(), operation); + } + + @Override + public int hashCode() { + return Objects.hash(left(), right(), operation); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + BinaryStringStringProcessorDefinition other = (BinaryStringStringProcessorDefinition) obj; + return Objects.equals(operation, other.operation) + && Objects.equals(left(), other.left()) + && Objects.equals(right(), other.right()); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Concat.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Concat.java new file mode 100644 index 0000000000000..a5dc8fc9ac3d7 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Concat.java @@ -0,0 +1,109 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.FieldAttribute; +import org.elasticsearch.xpack.sql.expression.function.scalar.BinaryScalarFunction; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions; +import org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; +import org.elasticsearch.xpack.sql.type.DataType; + +import java.util.Locale; + +import static java.lang.String.format; +import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ParamsBuilder.paramsBuilder; +import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate.formatTemplate; +import static org.elasticsearch.xpack.sql.expression.function.scalar.string.ConcatFunctionProcessor.doProcessInScripts; + +/** + * Returns a string that is the result of concatenating the two strings received as parameters. + * The result of the function is null only if both parameters are null, otherwise the result is the non-null + * parameter or the concatenation of the two strings if none of them is null. + */ +public class Concat extends BinaryScalarFunction { + + public Concat(Location location, Expression source1, Expression source2) { + super(location, source1, source2); + } + + protected TypeResolution resolveType() { + if (!childrenResolved()) { + return new TypeResolution("Unresolved children"); + } + + TypeResolution sourceResolution = StringFunctionUtils.resolveStringInputType(left().dataType(), functionName()); + if (sourceResolution != TypeResolution.TYPE_RESOLVED) { + return sourceResolution; + } + + return StringFunctionUtils.resolveStringInputType(right().dataType(), functionName()); + } + + @Override + protected ProcessorDefinition makeProcessorDefinition() { + return new ConcatFunctionProcessorDefinition(location(), this, + ProcessorDefinitions.toProcessorDefinition(left()), + ProcessorDefinitions.toProcessorDefinition(right())); + } + + @Override + public boolean foldable() { + return left().foldable() + && right().foldable(); + } + + @Override + public Object fold() { + return doProcessInScripts(left().fold(), right().fold()); + } + + @Override + protected Concat replaceChildren(Expression newLeft, Expression newRight) { + return new Concat(location(), newLeft, newRight); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, Concat::new, left(), right()); + } + + @Override + public ScriptTemplate asScript() { + ScriptTemplate sourceScript1 = asScript(left()); + ScriptTemplate sourceScript2 = asScript(right()); + + return asScriptFrom(sourceScript1, sourceScript2); + } + + @Override + protected ScriptTemplate asScriptFrom(ScriptTemplate leftScript, ScriptTemplate rightScript) { + // basically, transform the script to InternalSqlScriptUtils.[function_name](function_or_field1, function_or_field2) + return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{sql}.%s(%s,%s)"), + "concat", + leftScript.template(), + rightScript.template()), + paramsBuilder() + .script(leftScript.params()).script(rightScript.params()) + .build(), dataType()); + } + + @Override + protected ScriptTemplate asScriptFrom(FieldAttribute field) { + return new ScriptTemplate(formatScript("doc[{}].value"), + paramsBuilder().variable(field.isInexact() ? field.exactAttribute().name() : field.name()).build(), + dataType()); + } + + @Override + public DataType dataType() { + return DataType.KEYWORD; + } + +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ConcatFunctionProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ConcatFunctionProcessor.java new file mode 100644 index 0000000000000..3a5e9bbee24d4 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ConcatFunctionProcessor.java @@ -0,0 +1,82 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.BinaryProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor; + +import java.io.IOException; +import java.util.Objects; + +public class ConcatFunctionProcessor extends BinaryProcessor { + + public static final String NAME = "cb"; + + public ConcatFunctionProcessor(Processor source1, Processor source2) { + super(source1, source2); + } + + public ConcatFunctionProcessor(StreamInput in) throws IOException { + super(in); + } + + @Override + protected Object doProcess(Object source1, Object source2) { + return doProcessInScripts(source1, source2); + } + + /** + * Used in Painless scripting + */ + public static Object doProcessInScripts(Object source1, Object source2) { + if (source1 == null) { + return source2; + } + if (source2 == null) { + return source1; + } + if (!(source1 instanceof String || source1 instanceof Character)) { + throw new SqlIllegalArgumentException("A string/char is required; received [{}]", source1); + } + if (!(source2 instanceof String || source2 instanceof Character)) { + throw new SqlIllegalArgumentException("A string/char is required; received [{}]", source2); + } + + return source1.toString().concat(source2.toString()); + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + ConcatFunctionProcessor other = (ConcatFunctionProcessor) obj; + return Objects.equals(left(), other.left()) + && Objects.equals(right(), other.right()); + } + + @Override + public int hashCode() { + return Objects.hash(left(), right()); + } + + @Override + protected void doWrite(StreamOutput out) throws IOException { + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ConcatFunctionProcessorDefinition.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ConcatFunctionProcessorDefinition.java new file mode 100644 index 0000000000000..f7f86419be16b --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ConcatFunctionProcessorDefinition.java @@ -0,0 +1,57 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.BinaryProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; + +import java.util.Objects; + +public class ConcatFunctionProcessorDefinition extends BinaryProcessorDefinition { + + public ConcatFunctionProcessorDefinition(Location location, Expression expression, ProcessorDefinition left, + ProcessorDefinition right) { + super(location, expression, left, right); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, ConcatFunctionProcessorDefinition::new, expression(), left(), right()); + } + + @Override + protected BinaryProcessorDefinition replaceChildren(ProcessorDefinition left, ProcessorDefinition right) { + return new ConcatFunctionProcessorDefinition(location(), expression(), left, right); + } + + @Override + public ConcatFunctionProcessor asProcessor() { + return new ConcatFunctionProcessor(left().asProcessor(), right().asProcessor()); + } + + @Override + public int hashCode() { + return Objects.hash(left(), right()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + ConcatFunctionProcessorDefinition other = (ConcatFunctionProcessorDefinition) obj; + return Objects.equals(left(), other.left()) + && Objects.equals(right(), other.right()); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Insert.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Insert.java new file mode 100644 index 0000000000000..9d635b7cdcb89 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Insert.java @@ -0,0 +1,139 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.FieldAttribute; +import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions; +import org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; +import org.elasticsearch.xpack.sql.type.DataType; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import static java.lang.String.format; +import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ParamsBuilder.paramsBuilder; +import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate.formatTemplate; +import static org.elasticsearch.xpack.sql.expression.function.scalar.string.InsertFunctionProcessor.doProcess; + +/** + * Returns a character string where length characters have been deleted from the source string, beginning at start, + * and where the replacement string has been inserted into the source string, beginning at start. + */ +public class Insert extends ScalarFunction { + + private final Expression source, start, length, replacement; + + public Insert(Location location, Expression source, Expression start, Expression length, Expression replacement) { + super(location, Arrays.asList(source, start, length, replacement)); + this.source = source; + this.start = start; + this.length = length; + this.replacement = replacement; + } + + protected TypeResolution resolveType() { + if (!childrenResolved()) { + return new TypeResolution("Unresolved children"); + } + + TypeResolution sourceResolution = StringFunctionUtils.resolveStringInputType(source.dataType(), functionName()); + if (sourceResolution != TypeResolution.TYPE_RESOLVED) { + return sourceResolution; + } + + TypeResolution startResolution = StringFunctionUtils.resolveNumericInputType(start.dataType(), functionName()); + if (startResolution != TypeResolution.TYPE_RESOLVED) { + return startResolution; + } + + TypeResolution lengthResolution = StringFunctionUtils.resolveNumericInputType(length.dataType(), functionName()); + if (lengthResolution != TypeResolution.TYPE_RESOLVED) { + return lengthResolution; + } + + return StringFunctionUtils.resolveStringInputType(replacement.dataType(), functionName()); + } + + @Override + public boolean foldable() { + return source.foldable() + && start.foldable() + && length.foldable() + && replacement.foldable(); + } + + @Override + public Object fold() { + return doProcess(source.fold(), start.fold(), length.fold(), replacement.fold()); + } + + @Override + protected ProcessorDefinition makeProcessorDefinition() { + return new InsertFunctionProcessorDefinition(location(), this, + ProcessorDefinitions.toProcessorDefinition(source), + ProcessorDefinitions.toProcessorDefinition(start), + ProcessorDefinitions.toProcessorDefinition(length), + ProcessorDefinitions.toProcessorDefinition(replacement)); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, Insert::new, source, start, length, replacement); + } + + @Override + public ScriptTemplate asScript() { + ScriptTemplate sourceScript = asScript(source); + ScriptTemplate startScript = asScript(start); + ScriptTemplate lengthScript = asScript(length); + ScriptTemplate replacementScript = asScript(replacement); + + return asScriptFrom(sourceScript, startScript, lengthScript, replacementScript); + } + + protected ScriptTemplate asScriptFrom(ScriptTemplate sourceScript, ScriptTemplate startScript, + ScriptTemplate lengthScript, ScriptTemplate replacementScript) + { + // basically, transform the script to InternalSqlScriptUtils.[function_name](function_or_field1, function_or_field2,...) + return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{sql}.%s(%s,%s,%s,%s)"), + "insert", + sourceScript.template(), + startScript.template(), + lengthScript.template(), + replacementScript.template()), + paramsBuilder() + .script(sourceScript.params()).script(startScript.params()) + .script(lengthScript.params()).script(replacementScript.params()) + .build(), dataType()); + } + + @Override + protected ScriptTemplate asScriptFrom(FieldAttribute field) { + return new ScriptTemplate(formatScript("doc[{}].value"), + paramsBuilder().variable(field.isInexact() ? field.exactAttribute().name() : field.name()).build(), + dataType()); + } + + @Override + public DataType dataType() { + return DataType.KEYWORD; + } + + @Override + public Expression replaceChildren(List newChildren) { + if (newChildren.size() != 4) { + throw new IllegalArgumentException("expected [4] children but received [" + newChildren.size() + "]"); + } + + return new Insert(location(), newChildren.get(0), newChildren.get(1), newChildren.get(2), newChildren.get(3)); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/InsertFunctionProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/InsertFunctionProcessor.java new file mode 100644 index 0000000000000..4649925614fa8 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/InsertFunctionProcessor.java @@ -0,0 +1,131 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor; + +import java.io.IOException; +import java.util.Objects; + +public class InsertFunctionProcessor implements Processor { + + private final Processor source, start, length, replacement; + public static final String NAME = "ins"; + + public InsertFunctionProcessor(Processor source, Processor start, Processor length, Processor replacement) { + this.source = source; + this.start = start; + this.length = length; + this.replacement = replacement; + } + + public InsertFunctionProcessor(StreamInput in) throws IOException { + source = in.readNamedWriteable(Processor.class); + start = in.readNamedWriteable(Processor.class); + length = in.readNamedWriteable(Processor.class); + replacement = in.readNamedWriteable(Processor.class); + } + + @Override + public final void writeTo(StreamOutput out) throws IOException { + out.writeNamedWriteable(source()); + out.writeNamedWriteable(start()); + out.writeNamedWriteable(length()); + out.writeNamedWriteable(replacement()); + } + + @Override + public Object process(Object input) { + return doProcess(source().process(input), start().process(input), length().process(input), replacement().process(input)); + } + + public static Object doProcess(Object source, Object start, Object length, Object replacement) { + if (source == null) { + return null; + } + if (!(source instanceof String || source instanceof Character)) { + throw new SqlIllegalArgumentException("A string/char is required; received [{}]", source); + } + if (replacement == null) { + return source; + } + if (!(replacement instanceof String || replacement instanceof Character)) { + throw new SqlIllegalArgumentException("A string/char is required; received [{}]", replacement); + } + if (start == null || length == null) { + return source; + } + if (!(start instanceof Number)) { + throw new SqlIllegalArgumentException("A number is required; received [{}]", start); + } + if (!(length instanceof Number)) { + throw new SqlIllegalArgumentException("A number is required; received [{}]", length); + } + if (((Number) length).intValue() < 0) { + throw new SqlIllegalArgumentException("A positive number is required for [length]; received [{}]", length); + } + + int startInt = ((Number) start).intValue() - 1; + int realStart = startInt < 0 ? 0 : startInt; + + if (startInt > source.toString().length()) { + return source; + } + + StringBuilder sb = new StringBuilder(source.toString()); + String replString = (replacement.toString()); + + return sb.replace(realStart, + realStart + ((Number) length).intValue(), + replString).toString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + InsertFunctionProcessor other = (InsertFunctionProcessor) obj; + return Objects.equals(source(), other.source()) + && Objects.equals(start(), other.start()) + && Objects.equals(length(), other.length()) + && Objects.equals(replacement(), other.replacement()); + } + + @Override + public int hashCode() { + return Objects.hash(source(), start(), length(), replacement()); + } + + public Processor source() { + return source; + } + + public Processor start() { + return start; + } + + public Processor length() { + return length; + } + + public Processor replacement() { + return replacement; + } + + @Override + public String getWriteableName() { + return NAME; + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/InsertFunctionProcessorDefinition.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/InsertFunctionProcessorDefinition.java new file mode 100644 index 0000000000000..ab82622788d6f --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/InsertFunctionProcessorDefinition.java @@ -0,0 +1,130 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.execution.search.SqlSourceBuilder; +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class InsertFunctionProcessorDefinition extends ProcessorDefinition { + + private final ProcessorDefinition source, start, length, replacement; + + public InsertFunctionProcessorDefinition(Location location, Expression expression, + ProcessorDefinition source, ProcessorDefinition start, + ProcessorDefinition length, ProcessorDefinition replacement) { + super(location, expression, Arrays.asList(source, start, length, replacement)); + this.source = source; + this.start = start; + this.length = length; + this.replacement = replacement; + } + + @Override + public final ProcessorDefinition replaceChildren(List newChildren) { + if (newChildren.size() != 4) { + throw new IllegalArgumentException("expected [4] children but received [" + newChildren.size() + "]"); + } + return replaceChildren(newChildren.get(0), newChildren.get(1), newChildren.get(2), newChildren.get(3)); + } + + @Override + public final ProcessorDefinition resolveAttributes(AttributeResolver resolver) { + ProcessorDefinition newSource = source.resolveAttributes(resolver); + ProcessorDefinition newStart = start.resolveAttributes(resolver); + ProcessorDefinition newLength = length.resolveAttributes(resolver); + ProcessorDefinition newReplacement = replacement.resolveAttributes(resolver); + if (newSource == source + && newStart == start + && newLength == length + && newReplacement == replacement) { + return this; + } + return replaceChildren(newSource, newStart, newLength, newReplacement); + } + + @Override + public boolean supportedByAggsOnlyQuery() { + return source.supportedByAggsOnlyQuery() + && start.supportedByAggsOnlyQuery() + && length.supportedByAggsOnlyQuery() + && replacement.supportedByAggsOnlyQuery(); + } + + @Override + public boolean resolved() { + return source.resolved() && start.resolved() && length.resolved() && replacement.resolved(); + } + + protected ProcessorDefinition replaceChildren(ProcessorDefinition newSource, + ProcessorDefinition newStart, + ProcessorDefinition newLength, + ProcessorDefinition newReplacement) { + return new InsertFunctionProcessorDefinition(location(), expression(), newSource, newStart, newLength, newReplacement); + } + + @Override + public final void collectFields(SqlSourceBuilder sourceBuilder) { + source.collectFields(sourceBuilder); + start.collectFields(sourceBuilder); + length.collectFields(sourceBuilder); + replacement.collectFields(sourceBuilder); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, InsertFunctionProcessorDefinition::new, expression(), source, start, length, replacement); + } + + @Override + public InsertFunctionProcessor asProcessor() { + return new InsertFunctionProcessor(source.asProcessor(), start.asProcessor(), length.asProcessor(), replacement.asProcessor()); + } + + public ProcessorDefinition source() { + return source; + } + + public ProcessorDefinition start() { + return start; + } + + public ProcessorDefinition length() { + return length; + } + + public ProcessorDefinition replacement() { + return replacement; + } + + @Override + public int hashCode() { + return Objects.hash(source, start, length, replacement); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + InsertFunctionProcessorDefinition other = (InsertFunctionProcessorDefinition) obj; + return Objects.equals(source, other.source) + && Objects.equals(start, other.start) + && Objects.equals(length, other.length) + && Objects.equals(replacement, other.replacement); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Left.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Left.java new file mode 100644 index 0000000000000..2e31ecf3e7b0b --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Left.java @@ -0,0 +1,49 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringNumericProcessor.BinaryStringNumericOperation; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; + +import java.util.function.BiFunction; + +/** + * Returns the leftmost count characters of a string. + */ +public class Left extends BinaryStringNumericFunction { + + public Left(Location location, Expression left, Expression right) { + super(location, left, right); + } + + @Override + protected BiFunction operation() { + return BinaryStringNumericOperation.LEFT; + } + + @Override + protected Left replaceChildren(Expression newLeft, Expression newRight) { + return new Left(location(), newLeft, newRight); + } + + @Override + protected ProcessorDefinition makeProcessorDefinition() { + return new BinaryStringNumericProcessorDefinition(location(), this, + ProcessorDefinitions.toProcessorDefinition(left()), + ProcessorDefinitions.toProcessorDefinition(right()), + BinaryStringNumericOperation.LEFT); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, Left::new, left(), right()); + } + +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Locate.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Locate.java new file mode 100644 index 0000000000000..d6e2f80b025ca --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Locate.java @@ -0,0 +1,151 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.FieldAttribute; +import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions; +import org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; +import org.elasticsearch.xpack.sql.type.DataType; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import static java.lang.String.format; +import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ParamsBuilder.paramsBuilder; +import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate.formatTemplate; +import static org.elasticsearch.xpack.sql.expression.function.scalar.string.LocateFunctionProcessor.doProcess; + +/** + * Returns the starting position of the first occurrence of the pattern within the source string. + * The search for the first occurrence of the pattern begins with the first character position in the source string + * unless the optional argument, start, is specified. If start is specified, the search begins with the character + * position indicated by the value of start. The first character position in the source string is indicated by the value 1. + * If the pattern is not found within the source string, the value 0 is returned. + */ +public class Locate extends ScalarFunction { + + private final Expression pattern, source, start; + + public Locate(Location location, Expression pattern, Expression source, Expression start) { + super(location, start != null ? Arrays.asList(pattern, source, start) : Arrays.asList(pattern, source)); + this.pattern = pattern; + this.source = source; + this.start = start; + } + + protected TypeResolution resolveType() { + if (!childrenResolved()) { + return new TypeResolution("Unresolved children"); + } + + TypeResolution patternResolution = StringFunctionUtils.resolveStringInputType(pattern.dataType(), functionName()); + if (patternResolution != TypeResolution.TYPE_RESOLVED) { + return patternResolution; + } + + TypeResolution sourceResolution = StringFunctionUtils.resolveStringInputType(source.dataType(), functionName()); + if (sourceResolution != TypeResolution.TYPE_RESOLVED) { + return sourceResolution; + } + + return start == null ? TypeResolution.TYPE_RESOLVED : StringFunctionUtils.resolveNumericInputType(start.dataType(), functionName()); + } + + @Override + protected ProcessorDefinition makeProcessorDefinition() { + LocateFunctionProcessorDefinition processorDefinition; + if (start == null) { + processorDefinition = new LocateFunctionProcessorDefinition(location(), this, + ProcessorDefinitions.toProcessorDefinition(pattern), + ProcessorDefinitions.toProcessorDefinition(source)); + } + else { + processorDefinition = new LocateFunctionProcessorDefinition(location(), this, + ProcessorDefinitions.toProcessorDefinition(pattern), + ProcessorDefinitions.toProcessorDefinition(source), + ProcessorDefinitions.toProcessorDefinition(start)); + } + + return processorDefinition; + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, Locate::new, pattern, source, start); + } + + @Override + public boolean foldable() { + return pattern.foldable() + && source.foldable() + && (start == null? true : start.foldable()); + } + + @Override + public Object fold() { + return doProcess(pattern.fold(), source.fold(), (start == null ? null : start.fold())); + } + + @Override + public ScriptTemplate asScript() { + ScriptTemplate patternScript = asScript(pattern); + ScriptTemplate sourceScript = asScript(source); + ScriptTemplate startScript = start == null ? null : asScript(start); + + return asScriptFrom(patternScript, sourceScript, startScript); + } + + protected ScriptTemplate asScriptFrom(ScriptTemplate patternScript, ScriptTemplate sourceScript, + ScriptTemplate startScript) + { + if (start == null) { + return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{sql}.%s(%s,%s)"), + "locate", + patternScript.template(), + sourceScript.template()), + paramsBuilder() + .script(patternScript.params()).script(sourceScript.params()) + .build(), dataType()); + } + // basically, transform the script to InternalSqlScriptUtils.[function_name](function_or_field1, function_or_field2,...) + return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{sql}.%s(%s,%s,%s)"), + "locate", + patternScript.template(), + sourceScript.template(), + startScript.template()), + paramsBuilder() + .script(patternScript.params()).script(sourceScript.params()) + .script(startScript.params()) + .build(), dataType()); + } + + @Override + protected ScriptTemplate asScriptFrom(FieldAttribute field) { + return new ScriptTemplate(formatScript("doc[{}].value"), + paramsBuilder().variable(field.isInexact() ? field.exactAttribute().name() : field.name()).build(), + dataType()); + } + + @Override + public DataType dataType() { + return DataType.INTEGER; + } + + @Override + public Expression replaceChildren(List newChildren) { + if (newChildren.size() != 3) { + throw new IllegalArgumentException("expected [3] children but received [" + newChildren.size() + "]"); + } + + return new Locate(location(), newChildren.get(0), newChildren.get(1), newChildren.get(2)); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/LocateFunctionProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/LocateFunctionProcessor.java new file mode 100644 index 0000000000000..7831fc44df29d --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/LocateFunctionProcessor.java @@ -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.sql.expression.function.scalar.string; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor; + +import java.io.IOException; +import java.util.Objects; + +public class LocateFunctionProcessor implements Processor { + + private final Processor pattern, source, start; + public static final String NAME = "lc"; + + public LocateFunctionProcessor(Processor pattern, Processor source, Processor start) { + this.pattern = pattern; + this.source = source; + this.start = start; + } + + public LocateFunctionProcessor(StreamInput in) throws IOException { + pattern = in.readNamedWriteable(Processor.class); + source = in.readNamedWriteable(Processor.class); + start = in.readOptionalNamedWriteable(Processor.class); + } + + @Override + public final void writeTo(StreamOutput out) throws IOException { + out.writeNamedWriteable(pattern); + out.writeNamedWriteable(source); + out.writeOptionalNamedWriteable(start); + } + + @Override + public Object process(Object input) { + return doProcess(pattern().process(input), source().process(input), start() == null ? null : start().process(input)); + } + + public static Object doProcess(Object pattern, Object source, Object start) { + if (source == null) { + return null; + } + if (!(source instanceof String || source instanceof Character)) { + throw new SqlIllegalArgumentException("A string/char is required; received [{}]", source); + } + if (pattern == null) { + return 0; + } + + if (!(pattern instanceof String || pattern instanceof Character)) { + throw new SqlIllegalArgumentException("A string/char is required; received [{}]", pattern); + } + if (start != null && !(start instanceof Number)) { + throw new SqlIllegalArgumentException("A number is required; received [{}]", start); + } + + String stringSource = source instanceof Character ? source.toString() : (String) source; + String stringPattern = pattern instanceof Character ? pattern.toString() : (String) pattern; + + return (Integer) (1 + (start != null ? + stringSource.indexOf(stringPattern, ((Number) start).intValue() - 1) + : stringSource.indexOf(stringPattern))); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + LocateFunctionProcessor other = (LocateFunctionProcessor) obj; + return Objects.equals(pattern(), other.pattern()) + && Objects.equals(source(), other.source()) + && Objects.equals(start(), other.start()); + } + + @Override + public int hashCode() { + return Objects.hash(pattern(), source(), start()); + } + + public Processor pattern() { + return pattern; + } + + public Processor source() { + return source; + } + + public Processor start() { + return start; + } + + @Override + public String getWriteableName() { + return NAME; + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/LocateFunctionProcessorDefinition.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/LocateFunctionProcessorDefinition.java new file mode 100644 index 0000000000000..84b0c4457b170 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/LocateFunctionProcessorDefinition.java @@ -0,0 +1,126 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.execution.search.SqlSourceBuilder; +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class LocateFunctionProcessorDefinition extends ProcessorDefinition { + + private final ProcessorDefinition pattern, source, start; + + public LocateFunctionProcessorDefinition(Location location, Expression expression, ProcessorDefinition pattern, + ProcessorDefinition source, ProcessorDefinition start) { + super(location, expression, Arrays.asList(pattern, source, start)); + this.pattern = pattern; + this.source = source; + this.start = start; + } + + public LocateFunctionProcessorDefinition(Location location, Expression expression, ProcessorDefinition pattern, + ProcessorDefinition source) { + super(location, expression, Arrays.asList(pattern, source)); + this.pattern = pattern; + this.source = source; + this.start = null; + } + + @Override + public final ProcessorDefinition replaceChildren(List newChildren) { + int childrenSize = newChildren.size(); + if (childrenSize > 3 || childrenSize < 2) { + throw new IllegalArgumentException("expected [2 or 3] children but received [" + newChildren.size() + "]"); + } + return replaceChildren(newChildren.get(0), newChildren.get(1), childrenSize == 2 ? null : newChildren.get(2)); + } + + @Override + public final ProcessorDefinition resolveAttributes(AttributeResolver resolver) { + ProcessorDefinition newPattern = pattern.resolveAttributes(resolver); + ProcessorDefinition newSource = source.resolveAttributes(resolver); + ProcessorDefinition newStart = start == null ? start : start.resolveAttributes(resolver); + if (newPattern == pattern && newSource == source && newStart == start) { + return this; + } + return replaceChildren(newPattern, newSource, newStart); + } + + @Override + public boolean supportedByAggsOnlyQuery() { + return pattern.supportedByAggsOnlyQuery() && source.supportedByAggsOnlyQuery() + && (start == null || start.supportedByAggsOnlyQuery()); + } + + @Override + public boolean resolved() { + return pattern.resolved() && source.resolved() && (start == null || start.resolved()); + } + + protected ProcessorDefinition replaceChildren(ProcessorDefinition newPattern, ProcessorDefinition newSource, + ProcessorDefinition newStart) { + if (newStart == null) { + return new LocateFunctionProcessorDefinition(location(), expression(), newPattern, newSource); + } + return new LocateFunctionProcessorDefinition(location(), expression(), newPattern, newSource, newStart); + } + + @Override + public final void collectFields(SqlSourceBuilder sourceBuilder) { + pattern.collectFields(sourceBuilder); + source.collectFields(sourceBuilder); + if (start != null) { + start.collectFields(sourceBuilder); + } + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, LocateFunctionProcessorDefinition::new, expression(), pattern, source, start); + } + + @Override + public LocateFunctionProcessor asProcessor() { + return new LocateFunctionProcessor(pattern.asProcessor(), source.asProcessor(), start == null ? null : start.asProcessor()); + } + + public ProcessorDefinition source() { + return source; + } + + public ProcessorDefinition start() { + return start; + } + + public ProcessorDefinition pattern() { + return pattern; + } + + @Override + public int hashCode() { + return Objects.hash(pattern, source, start); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + LocateFunctionProcessorDefinition other = (LocateFunctionProcessorDefinition) obj; + return Objects.equals(pattern, other.pattern) && Objects.equals(source, other.source) && Objects.equals(start, other.start); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Position.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Position.java new file mode 100644 index 0000000000000..9dfd3e094bb2c --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Position.java @@ -0,0 +1,49 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringStringProcessor.BinaryStringStringOperation; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; + +import java.util.function.BiFunction; + +/** + * Returns the position of the first character expression in the second character expression, if not found it returns 0. + */ +public class Position extends BinaryStringStringFunction { + + public Position(Location location, Expression left, Expression right) { + super(location, left, right); + } + + @Override + protected BiFunction operation() { + return BinaryStringStringOperation.POSITION; + } + + @Override + protected Position replaceChildren(Expression newLeft, Expression newRight) { + return new Position(location(), newLeft, newRight); + } + + @Override + protected ProcessorDefinition makeProcessorDefinition() { + return new BinaryStringStringProcessorDefinition(location(), this, + ProcessorDefinitions.toProcessorDefinition(left()), + ProcessorDefinitions.toProcessorDefinition(right()), + BinaryStringStringOperation.POSITION); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, Position::new, left(), right()); + } + +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Repeat.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Repeat.java new file mode 100644 index 0000000000000..89ea7664e40e6 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Repeat.java @@ -0,0 +1,49 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringNumericProcessor.BinaryStringNumericOperation; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; + +import java.util.function.BiFunction; + +/** + * Creates a string composed of a string repeated count times. + */ +public class Repeat extends BinaryStringNumericFunction { + + public Repeat(Location location, Expression left, Expression right) { + super(location, left, right); + } + + @Override + protected BiFunction operation() { + return BinaryStringNumericOperation.REPEAT; + } + + @Override + protected Repeat replaceChildren(Expression newLeft, Expression newRight) { + return new Repeat(location(), newLeft, newRight); + } + + @Override + protected ProcessorDefinition makeProcessorDefinition() { + return new BinaryStringNumericProcessorDefinition(location(), this, + ProcessorDefinitions.toProcessorDefinition(left()), + ProcessorDefinitions.toProcessorDefinition(right()), + BinaryStringNumericOperation.REPEAT); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, Repeat::new, left(), right()); + } + +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Replace.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Replace.java new file mode 100644 index 0000000000000..9325986ac1f1c --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Replace.java @@ -0,0 +1,128 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.FieldAttribute; +import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions; +import org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; +import org.elasticsearch.xpack.sql.type.DataType; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import static java.lang.String.format; +import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ParamsBuilder.paramsBuilder; +import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate.formatTemplate; +import static org.elasticsearch.xpack.sql.expression.function.scalar.string.SubstringFunctionProcessor.doProcess; + +/** + * Search the source string for occurrences of the pattern, and replace with the replacement string. + */ +public class Replace extends ScalarFunction { + + private final Expression source, pattern, replacement; + + public Replace(Location location, Expression source, Expression pattern, Expression replacement) { + super(location, Arrays.asList(source, pattern, replacement)); + this.source = source; + this.pattern = pattern; + this.replacement = replacement; + } + + protected TypeResolution resolveType() { + if (!childrenResolved()) { + return new TypeResolution("Unresolved children"); + } + + TypeResolution sourceResolution = StringFunctionUtils.resolveStringInputType(source.dataType(), functionName()); + if (sourceResolution != TypeResolution.TYPE_RESOLVED) { + return sourceResolution; + } + + TypeResolution patternResolution = StringFunctionUtils.resolveStringInputType(pattern.dataType(), functionName()); + if (patternResolution != TypeResolution.TYPE_RESOLVED) { + return patternResolution; + } + + return StringFunctionUtils.resolveStringInputType(replacement.dataType(), functionName()); + } + + @Override + protected ProcessorDefinition makeProcessorDefinition() { + return new ReplaceFunctionProcessorDefinition(location(), this, + ProcessorDefinitions.toProcessorDefinition(source), + ProcessorDefinitions.toProcessorDefinition(pattern), + ProcessorDefinitions.toProcessorDefinition(replacement)); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, Replace::new, source, pattern, replacement); + } + + @Override + public boolean foldable() { + return source.foldable() + && pattern.foldable() + && replacement.foldable(); + } + + @Override + public Object fold() { + return doProcess(source.fold(), pattern.fold(), replacement.fold()); + } + + @Override + public ScriptTemplate asScript() { + ScriptTemplate sourceScript = asScript(source); + ScriptTemplate patternScript = asScript(pattern); + ScriptTemplate replacementScript = asScript(replacement); + + return asScriptFrom(sourceScript, patternScript, replacementScript); + } + + protected ScriptTemplate asScriptFrom(ScriptTemplate sourceScript, ScriptTemplate patternScript, + ScriptTemplate replacementScript) + { + // basically, transform the script to InternalSqlScriptUtils.[function_name](function_or_field1, function_or_field2,...) + return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{sql}.%s(%s,%s,%s)"), + "replace", + sourceScript.template(), + patternScript.template(), + replacementScript.template()), + paramsBuilder() + .script(sourceScript.params()).script(patternScript.params()) + .script(replacementScript.params()) + .build(), dataType()); + } + + @Override + protected ScriptTemplate asScriptFrom(FieldAttribute field) { + return new ScriptTemplate(formatScript("doc[{}].value"), + paramsBuilder().variable(field.isInexact() ? field.exactAttribute().name() : field.name()).build(), + dataType()); + } + + @Override + public DataType dataType() { + return DataType.KEYWORD; + } + + @Override + public Expression replaceChildren(List newChildren) { + if (newChildren.size() != 3) { + throw new IllegalArgumentException("expected [3] children but received [" + newChildren.size() + "]"); + } + + return new Replace(location(), newChildren.get(0), newChildren.get(1), newChildren.get(2)); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ReplaceFunctionProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ReplaceFunctionProcessor.java new file mode 100644 index 0000000000000..529ceb408aaa6 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ReplaceFunctionProcessor.java @@ -0,0 +1,105 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor; + +import java.io.IOException; +import java.util.Objects; + +public class ReplaceFunctionProcessor implements Processor { + + private final Processor source, pattern, replacement; + public static final String NAME = "r"; + + public ReplaceFunctionProcessor(Processor source, Processor pattern, Processor replacement) { + this.source = source; + this.pattern = pattern; + this.replacement = replacement; + } + + public ReplaceFunctionProcessor(StreamInput in) throws IOException { + source = in.readNamedWriteable(Processor.class); + pattern = in.readNamedWriteable(Processor.class); + replacement = in.readNamedWriteable(Processor.class); + } + + @Override + public final void writeTo(StreamOutput out) throws IOException { + out.writeNamedWriteable(source); + out.writeNamedWriteable(pattern); + out.writeNamedWriteable(replacement); + } + + @Override + public Object process(Object input) { + return doProcess(source().process(input), pattern().process(input), replacement().process(input)); + } + + public static Object doProcess(Object source, Object pattern, Object replacement) { + if (source == null) { + return null; + } + if (!(source instanceof String || source instanceof Character)) { + throw new SqlIllegalArgumentException("A string/char is required; received [{}]", source); + } + if (pattern == null || replacement == null) { + return source; + } + if (!(pattern instanceof String || pattern instanceof Character)) { + throw new SqlIllegalArgumentException("A string/char is required; received [{}]", pattern); + } + if (!(replacement instanceof String || replacement instanceof Character)) { + throw new SqlIllegalArgumentException("A string/char is required; received [{}]", replacement); + } + + return Strings.replace(source instanceof Character ? source.toString() : (String)source, + pattern instanceof Character ? pattern.toString() : (String) pattern, + replacement instanceof Character ? replacement.toString() : (String) replacement); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + ReplaceFunctionProcessor other = (ReplaceFunctionProcessor) obj; + return Objects.equals(source(), other.source()) + && Objects.equals(pattern(), other.pattern()) + && Objects.equals(replacement(), other.replacement()); + } + + @Override + public int hashCode() { + return Objects.hash(source(), pattern(), replacement()); + } + + public Processor source() { + return source; + } + + public Processor pattern() { + return pattern; + } + + public Processor replacement() { + return replacement; + } + + @Override + public String getWriteableName() { + return NAME; + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ReplaceFunctionProcessorDefinition.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ReplaceFunctionProcessorDefinition.java new file mode 100644 index 0000000000000..14e7e929e3555 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ReplaceFunctionProcessorDefinition.java @@ -0,0 +1,113 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.execution.search.SqlSourceBuilder; +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class ReplaceFunctionProcessorDefinition extends ProcessorDefinition { + + private final ProcessorDefinition source, pattern, replacement; + + public ReplaceFunctionProcessorDefinition(Location location, Expression expression, ProcessorDefinition source, + ProcessorDefinition pattern, ProcessorDefinition replacement) { + super(location, expression, Arrays.asList(source, pattern, replacement)); + this.source = source; + this.pattern = pattern; + this.replacement = replacement; + } + + @Override + public final ProcessorDefinition replaceChildren(List newChildren) { + if (newChildren.size() != 3) { + throw new IllegalArgumentException("expected [3] children but received [" + newChildren.size() + "]"); + } + return replaceChildren(newChildren.get(0), newChildren.get(1), newChildren.get(2)); + } + + @Override + public final ProcessorDefinition resolveAttributes(AttributeResolver resolver) { + ProcessorDefinition newSource = source.resolveAttributes(resolver); + ProcessorDefinition newPattern = pattern.resolveAttributes(resolver); + ProcessorDefinition newReplacement = replacement.resolveAttributes(resolver); + if (newSource == source && newPattern == pattern && newReplacement == replacement) { + return this; + } + return replaceChildren(newSource, newPattern, newReplacement); + } + + @Override + public boolean supportedByAggsOnlyQuery() { + return source.supportedByAggsOnlyQuery() && pattern.supportedByAggsOnlyQuery() && replacement.supportedByAggsOnlyQuery(); + } + + @Override + public boolean resolved() { + return source.resolved() && pattern.resolved() && replacement.resolved(); + } + + protected ProcessorDefinition replaceChildren(ProcessorDefinition newSource, ProcessorDefinition newPattern, + ProcessorDefinition newReplacement) { + return new ReplaceFunctionProcessorDefinition(location(), expression(), newSource, newPattern, newReplacement); + } + + @Override + public final void collectFields(SqlSourceBuilder sourceBuilder) { + source.collectFields(sourceBuilder); + pattern.collectFields(sourceBuilder); + replacement.collectFields(sourceBuilder); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, ReplaceFunctionProcessorDefinition::new, expression(), source, pattern, replacement); + } + + @Override + public ReplaceFunctionProcessor asProcessor() { + return new ReplaceFunctionProcessor(source.asProcessor(), pattern.asProcessor(), replacement.asProcessor()); + } + + public ProcessorDefinition source() { + return source; + } + + public ProcessorDefinition pattern() { + return pattern; + } + + public ProcessorDefinition replacement() { + return replacement; + } + + @Override + public int hashCode() { + return Objects.hash(source, pattern, replacement); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + ReplaceFunctionProcessorDefinition other = (ReplaceFunctionProcessorDefinition) obj; + return Objects.equals(source, other.source) + && Objects.equals(pattern, other.pattern) + && Objects.equals(replacement, other.replacement); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Right.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Right.java new file mode 100644 index 0000000000000..3250dcc0a6e79 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Right.java @@ -0,0 +1,49 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringNumericProcessor.BinaryStringNumericOperation; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; + +import java.util.function.BiFunction; + +/** + * Returns the rightmost count characters of a string. + */ +public class Right extends BinaryStringNumericFunction { + + public Right(Location location, Expression left, Expression right) { + super(location, left, right); + } + + @Override + protected BiFunction operation() { + return BinaryStringNumericOperation.RIGHT; + } + + @Override + protected Right replaceChildren(Expression newLeft, Expression newRight) { + return new Right(location(), newLeft, newRight); + } + + @Override + protected ProcessorDefinition makeProcessorDefinition() { + return new BinaryStringNumericProcessorDefinition(location(), this, + ProcessorDefinitions.toProcessorDefinition(left()), + ProcessorDefinitions.toProcessorDefinition(right()), + BinaryStringNumericOperation.RIGHT); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, Right::new, left(), right()); + } + +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/StringFunctionUtils.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/StringFunctionUtils.java index cef826d37ed5c..c9ec628b6ce48 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/StringFunctionUtils.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/StringFunctionUtils.java @@ -5,10 +5,35 @@ */ package org.elasticsearch.xpack.sql.expression.function.scalar.string; +import org.elasticsearch.xpack.sql.expression.Expression.TypeResolution; +import org.elasticsearch.xpack.sql.type.DataType; + abstract class StringFunctionUtils { /** - * Trims the trailing whitespace characters from the given String. Uses {@link Character#isWhitespace(char)} + * Extract a substring from the given string, using start index and length of the extracted substring. + * + * @param s the original String + * @param start starting position for the substring within the original string. 0-based index position + * @param length length in characters of the substracted substring + * @return the resulting String + */ + static String substring(String s, int start, int length) { + if (!hasLength(s)) { + return s; + } + + if (start < 0) + start = 0; + + if (start + 1 > s.length() || length < 0) + return ""; + + return (start + length > s.length()) ? s.substring(start) : s.substring(start, start + length); + } + + /** + * Trims the trailing whitespace characters from the given String. Uses {@link java.lang.Character.isWhitespace(char)} * to determine if a character is whitespace or not. * * @param s the original String @@ -48,4 +73,16 @@ static String trimLeadingWhitespaces(String s) { private static boolean hasLength(String s) { return (s != null && s.length() > 0); } + + static TypeResolution resolveStringInputType(DataType inputType, String functionName) { + return inputType.isString() ? + TypeResolution.TYPE_RESOLVED : + new TypeResolution("'%s' requires a string type, received %s", functionName, inputType.esType); + } + + static TypeResolution resolveNumericInputType(DataType inputType, String functionName) { + return inputType.isNumeric() ? + TypeResolution.TYPE_RESOLVED : + new TypeResolution("'%s' requires a numeric type, received %s", functionName, inputType.esType); + } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Substring.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Substring.java new file mode 100644 index 0000000000000..199cda01b9b5c --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Substring.java @@ -0,0 +1,129 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.FieldAttribute; +import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions; +import org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; +import org.elasticsearch.xpack.sql.type.DataType; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import static java.lang.String.format; +import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ParamsBuilder.paramsBuilder; +import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate.formatTemplate; +import static org.elasticsearch.xpack.sql.expression.function.scalar.string.SubstringFunctionProcessor.doProcess; + +/** + * Returns a character string that is derived from the source string, beginning at the character position specified by start + * for length characters. + */ +public class Substring extends ScalarFunction { + + private final Expression source, start, length; + + public Substring(Location location, Expression source, Expression start, Expression length) { + super(location, Arrays.asList(source, start, length)); + this.source = source; + this.start = start; + this.length = length; + } + + protected TypeResolution resolveType() { + if (!childrenResolved()) { + return new TypeResolution("Unresolved children"); + } + + TypeResolution sourceResolution = StringFunctionUtils.resolveStringInputType(source.dataType(), functionName()); + if (sourceResolution != TypeResolution.TYPE_RESOLVED) { + return sourceResolution; + } + + TypeResolution startResolution = StringFunctionUtils.resolveNumericInputType(start.dataType(), functionName()); + if (startResolution != TypeResolution.TYPE_RESOLVED) { + return startResolution; + } + + return StringFunctionUtils.resolveNumericInputType(length.dataType(), functionName()); + } + + @Override + protected ProcessorDefinition makeProcessorDefinition() { + return new SubstringFunctionProcessorDefinition(location(), this, + ProcessorDefinitions.toProcessorDefinition(source), + ProcessorDefinitions.toProcessorDefinition(start), + ProcessorDefinitions.toProcessorDefinition(length)); + } + + @Override + public boolean foldable() { + return source.foldable() + && start.foldable() + && length.foldable(); + } + + @Override + public Object fold() { + return doProcess(source.fold(), start.fold(), length.fold()); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, Substring::new, source, start, length); + } + + @Override + public ScriptTemplate asScript() { + ScriptTemplate sourceScript = asScript(source); + ScriptTemplate startScript = asScript(start); + ScriptTemplate lengthScript = asScript(length); + + return asScriptFrom(sourceScript, startScript, lengthScript); + } + + protected ScriptTemplate asScriptFrom(ScriptTemplate sourceScript, ScriptTemplate startScript, + ScriptTemplate lengthScript) + { + // basically, transform the script to InternalSqlScriptUtils.[function_name](function_or_field1, function_or_field2,...) + return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{sql}.%s(%s,%s,%s)"), + "substring", + sourceScript.template(), + startScript.template(), + lengthScript.template()), + paramsBuilder() + .script(sourceScript.params()).script(startScript.params()) + .script(lengthScript.params()) + .build(), dataType()); + } + + @Override + protected ScriptTemplate asScriptFrom(FieldAttribute field) { + return new ScriptTemplate(formatScript("doc[{}].value"), + paramsBuilder().variable(field.isInexact() ? field.exactAttribute().name() : field.name()).build(), + dataType()); + } + + @Override + public DataType dataType() { + return DataType.KEYWORD; + } + + @Override + public Expression replaceChildren(List newChildren) { + if (newChildren.size() != 3) { + throw new IllegalArgumentException("expected [3] children but received [" + newChildren.size() + "]"); + } + + return new Substring(location(), newChildren.get(0), newChildren.get(1), newChildren.get(2)); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/SubstringFunctionProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/SubstringFunctionProcessor.java new file mode 100644 index 0000000000000..28f251bf0dbac --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/SubstringFunctionProcessor.java @@ -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.sql.expression.function.scalar.string; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor; + +import java.io.IOException; +import java.util.Objects; + +public class SubstringFunctionProcessor implements Processor { + + private final Processor source, start, length; + public static final String NAME = "sb"; + + public SubstringFunctionProcessor(Processor source, Processor start, Processor length) { + this.source = source; + this.start = start; + this.length = length; + } + + public SubstringFunctionProcessor(StreamInput in) throws IOException { + source = in.readNamedWriteable(Processor.class); + start = in.readNamedWriteable(Processor.class); + length = in.readNamedWriteable(Processor.class); + } + + @Override + public final void writeTo(StreamOutput out) throws IOException { + out.writeNamedWriteable(source); + out.writeNamedWriteable(start); + out.writeNamedWriteable(length); + } + + @Override + public Object process(Object input) { + return doProcess(source.process(input), start.process(input), length.process(input)); + } + + public static Object doProcess(Object source, Object start, Object length) { + if (source == null) { + return null; + } + if (!(source instanceof String || source instanceof Character)) { + throw new SqlIllegalArgumentException("A string/char is required; received [{}]", source); + } + if (start == null || length == null) { + return source; + } + if (!(start instanceof Number)) { + throw new SqlIllegalArgumentException("A number is required; received [{}]", start); + } + if (!(length instanceof Number)) { + throw new SqlIllegalArgumentException("A number is required; received [{}]", length); + } + if (((Number) length).intValue() < 0) { + throw new SqlIllegalArgumentException("A positive number is required for [length]; received [{}]", length); + } + + return StringFunctionUtils.substring(source instanceof Character ? source.toString() : (String) source, + ((Number) start).intValue() - 1, // SQL is 1-based when it comes to string manipulation + ((Number) length).intValue()); + } + + protected Processor source() { + return source; + } + + protected Processor start() { + return start; + } + + protected Processor length() { + return length; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + SubstringFunctionProcessor other = (SubstringFunctionProcessor) obj; + return Objects.equals(source(), other.source()) + && Objects.equals(start(), other.start()) + && Objects.equals(length(), other.length()); + } + + @Override + public int hashCode() { + return Objects.hash(source(), start(), length()); + } + + + @Override + public String getWriteableName() { + return NAME; + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/SubstringFunctionProcessorDefinition.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/SubstringFunctionProcessorDefinition.java new file mode 100644 index 0000000000000..653aac24f9855 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/SubstringFunctionProcessorDefinition.java @@ -0,0 +1,111 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.execution.search.SqlSourceBuilder; +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class SubstringFunctionProcessorDefinition extends ProcessorDefinition { + + private final ProcessorDefinition source, start, length; + + public SubstringFunctionProcessorDefinition(Location location, Expression expression, ProcessorDefinition source, + ProcessorDefinition start, ProcessorDefinition length) { + super(location, expression, Arrays.asList(source, start, length)); + this.source = source; + this.start = start; + this.length = length; + } + + @Override + public final ProcessorDefinition replaceChildren(List newChildren) { + if (newChildren.size() != 3) { + throw new IllegalArgumentException("expected [3] children but received [" + newChildren.size() + "]"); + } + return replaceChildren(newChildren.get(0), newChildren.get(1), newChildren.get(2)); + } + + @Override + public final ProcessorDefinition resolveAttributes(AttributeResolver resolver) { + ProcessorDefinition newSource = source.resolveAttributes(resolver); + ProcessorDefinition newStart = start.resolveAttributes(resolver); + ProcessorDefinition newLength = length.resolveAttributes(resolver); + if (newSource == source && newStart == start && newLength == length) { + return this; + } + return replaceChildren(newSource, newStart, newLength); + } + + @Override + public boolean supportedByAggsOnlyQuery() { + return source.supportedByAggsOnlyQuery() && start.supportedByAggsOnlyQuery() && length.supportedByAggsOnlyQuery(); + } + + @Override + public boolean resolved() { + return source.resolved() && start.resolved() && length.resolved(); + } + + protected ProcessorDefinition replaceChildren(ProcessorDefinition newSource, ProcessorDefinition newStart, + ProcessorDefinition newLength) { + return new SubstringFunctionProcessorDefinition(location(), expression(), newSource, newStart, newLength); + } + + @Override + public final void collectFields(SqlSourceBuilder sourceBuilder) { + source.collectFields(sourceBuilder); + start.collectFields(sourceBuilder); + length.collectFields(sourceBuilder); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, SubstringFunctionProcessorDefinition::new, expression(), source, start, length); + } + + @Override + public SubstringFunctionProcessor asProcessor() { + return new SubstringFunctionProcessor(source.asProcessor(), start.asProcessor(), length.asProcessor()); + } + + public ProcessorDefinition source() { + return source; + } + + public ProcessorDefinition start() { + return start; + } + + public ProcessorDefinition length() { + return length; + } + + @Override + public int hashCode() { + return Objects.hash(source, start, length); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + SubstringFunctionProcessorDefinition other = (SubstringFunctionProcessorDefinition) obj; + return Objects.equals(source, other.source) && Objects.equals(start, other.start) && Objects.equals(length, other.length); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java index ccd5c24c6412d..12faeb78b662d 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java @@ -6,7 +6,14 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.whitelist; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeFunction; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringNumericProcessor.BinaryStringNumericOperation; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringStringProcessor.BinaryStringStringOperation; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.ConcatFunctionProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.InsertFunctionProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.LocateFunctionProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.ReplaceFunctionProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.string.StringProcessor.StringOperation; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.SubstringFunctionProcessor; /** * Whitelisted class for SQL scripts. @@ -60,4 +67,44 @@ public static String ltrim(String s) { public static String space(Number n) { return (String) StringOperation.SPACE.apply(n); } + + public static String left(String s, int count) { + return BinaryStringNumericOperation.LEFT.apply(s, count); + } + + public static String right(String s, int count) { + return BinaryStringNumericOperation.RIGHT.apply(s, count); + } + + public static String concat(String s1, String s2) { + return ConcatFunctionProcessor.doProcessInScripts(s1, s2).toString(); + } + + public static String repeat(String s, int count) { + return BinaryStringNumericOperation.REPEAT.apply(s, count); + } + + public static Integer position(String s1, String s2) { + return (Integer) BinaryStringStringOperation.POSITION.apply(s1, s2); + } + + public static String insert(String s, int start, int length, String r) { + return InsertFunctionProcessor.doProcess(s, start, length, r).toString(); + } + + public static String substring(String s, int start, int length) { + return SubstringFunctionProcessor.doProcess(s, start, length).toString(); + } + + public static String replace(String s1, String s2, String s3) { + return ReplaceFunctionProcessor.doProcess(s1, s2, s3).toString(); + } + + public static Integer locate(String s1, String s2, Integer pos) { + return (Integer) LocateFunctionProcessor.doProcess(s1, s2, pos); + } + + public static Integer locate(String s1, String s2) { + return locate(s1, s2, null); + } } diff --git a/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt b/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt index 73a002c249f3c..8f86685889c55 100644 --- a/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt +++ b/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt @@ -19,4 +19,14 @@ class org.elasticsearch.xpack.sql.expression.function.scalar.whitelist.InternalS String rtrim(String) String ltrim(String) String space(Number) + String left(String, int) + String right(String, int) + String concat(String, String) + String repeat(String, int) + Integer position(String, String) + String insert(String, int, int, String) + String substring(String, int, int) + String replace(String, String, String) + Integer locate(String, String) + Integer locate(String, String, Integer) } \ No newline at end of file diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java index dce665a97e95d..95949070f2e51 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java @@ -55,7 +55,7 @@ public void testMissingFunction() { } public void testMisspelledFunction() { - assertEquals("1:8: Unknown function [COONT], did you mean any of [COUNT, COT]?", verify("SELECT COONT(bool) FROM test")); + assertEquals("1:8: Unknown function [COONT], did you mean any of [COUNT, COT, CONCAT]?", verify("SELECT COONT(bool) FROM test")); } public void testMissingColumnInGroupBy() { diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/FunctionTestUtils.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/FunctionTestUtils.java new file mode 100644 index 0000000000000..c57dff15ca99a --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/FunctionTestUtils.java @@ -0,0 +1,70 @@ +/* + * 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.sql.expression.function.scalar; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.sql.expression.Literal; + +import java.util.BitSet; +import java.util.Iterator; + +import static org.elasticsearch.xpack.sql.tree.Location.EMPTY; + +public final class FunctionTestUtils { + + public static Literal l(Object value) { + return Literal.of(EMPTY, value); + } + + public static Literal randomStringLiteral() { + return l(ESTestCase.randomRealisticUnicodeOfLength(1024)); + } + + public static Literal randomIntLiteral() { + return l(ESTestCase.randomInt()); + } + + public static class Combinations implements Iterable { + private int n; + private int k; + + public Combinations(int n, int k) { + this.n = n; + this.k = k; + } + + @Override + public Iterator iterator() { + return new Iterator() { + BitSet bs = new BitSet(n); + { + bs.set(0, k); + } + + @Override + public boolean hasNext() { + return bs != null; + } + + @Override + public BitSet next() { + BitSet old = (BitSet) bs.clone(); + int b = bs.previousClearBit(n - 1); + int b1 = bs.previousSetBit(b); + if (b1 == -1) + bs = null; + else { + bs.clear(b1); + bs.set(b1 + 1, b1 + (n - b) + 1); + bs.clear(b1 + (n - b) + 1, n); + } + return old; + } + }; + } + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericProcessorDefinitionTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericProcessorDefinitionTests.java new file mode 100644 index 0000000000000..0b644995fc6b2 --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericProcessorDefinitionTests.java @@ -0,0 +1,146 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.BinaryProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringNumericProcessor.BinaryStringNumericOperation; +import org.elasticsearch.xpack.sql.tree.AbstractNodeTestCase; +import org.elasticsearch.xpack.sql.tree.Location; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomIntLiteral; +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomStringLiteral; +import static org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions.toProcessorDefinition; +import static org.elasticsearch.xpack.sql.tree.LocationTests.randomLocation; + +public class BinaryStringNumericProcessorDefinitionTests + extends AbstractNodeTestCase { + + @Override + protected BinaryStringNumericProcessorDefinition randomInstance() { + return randomBinaryStringNumericProcessorDefinition(); + } + + private Expression randomBinaryStringNumericExpression() { + return randomBinaryStringNumericProcessorDefinition().expression(); + } + + private BinaryStringNumericOperation randomBinaryStringNumericOperation() { + return randomBinaryStringNumericProcessorDefinition().operation(); + } + + public static BinaryStringNumericProcessorDefinition randomBinaryStringNumericProcessorDefinition() { + List functions = new ArrayList<>(); + functions.add(new Left(randomLocation(), randomStringLiteral(), randomIntLiteral()).makeProcessorDefinition()); + functions.add(new Right(randomLocation(), randomStringLiteral(), randomIntLiteral()).makeProcessorDefinition()); + functions.add(new Repeat(randomLocation(), randomStringLiteral(), randomIntLiteral()).makeProcessorDefinition()); + + return (BinaryStringNumericProcessorDefinition) randomFrom(functions); + } + + @Override + public void testTransform() { + // test transforming only the properties (location, expression, operation), + // skipping the children (the two parameters of the binary function) which are tested separately + BinaryStringNumericProcessorDefinition b1 = randomInstance(); + + Expression newExpression = randomValueOtherThan(b1.expression(), () -> randomBinaryStringNumericExpression()); + BinaryStringNumericProcessorDefinition newB = new BinaryStringNumericProcessorDefinition( + b1.location(), + newExpression, + b1.left(), + b1.right(), + b1.operation()); + assertEquals(newB, b1.transformPropertiesOnly(v -> Objects.equals(v, b1.expression()) ? newExpression : v, Expression.class)); + + BinaryStringNumericProcessorDefinition b2 = randomInstance(); + BinaryStringNumericOperation newOp = randomValueOtherThan(b2.operation(), () -> randomBinaryStringNumericOperation()); + newB = new BinaryStringNumericProcessorDefinition( + b2.location(), + b2.expression(), + b2.left(), + b2.right(), + newOp); + assertEquals(newB, + b2.transformPropertiesOnly(v -> Objects.equals(v, b2.operation()) ? newOp : v, BinaryStringNumericOperation.class)); + + BinaryStringNumericProcessorDefinition b3 = randomInstance(); + Location newLoc = randomValueOtherThan(b3.location(), () -> randomLocation()); + newB = new BinaryStringNumericProcessorDefinition( + newLoc, + b3.expression(), + b3.left(), + b3.right(), + b3.operation()); + assertEquals(newB, + b3.transformPropertiesOnly(v -> Objects.equals(v, b3.location()) ? newLoc : v, Location.class)); + } + + @Override + public void testReplaceChildren() { + BinaryStringNumericProcessorDefinition b = randomInstance(); + ProcessorDefinition newLeft = toProcessorDefinition((Expression) randomValueOtherThan(b.left(), () -> randomStringLiteral())); + ProcessorDefinition newRight = toProcessorDefinition((Expression) randomValueOtherThan(b.right(), () -> randomIntLiteral())); + BinaryStringNumericProcessorDefinition newB = + new BinaryStringNumericProcessorDefinition(b.location(), b.expression(), b.left(), b.right(), b.operation()); + BinaryProcessorDefinition transformed = newB.replaceChildren(newLeft, b.right()); + + assertEquals(transformed.left(), newLeft); + assertEquals(transformed.location(), b.location()); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.right(), b.right()); + + transformed = newB.replaceChildren(b.left(), newRight); + assertEquals(transformed.left(), b.left()); + assertEquals(transformed.location(), b.location()); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.right(), newRight); + + transformed = newB.replaceChildren(newLeft, newRight); + assertEquals(transformed.left(), newLeft); + assertEquals(transformed.location(), b.location()); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.right(), newRight); + } + + @Override + protected BinaryStringNumericProcessorDefinition mutate(BinaryStringNumericProcessorDefinition instance) { + List> randoms = new ArrayList<>(); + randoms.add(f -> new BinaryStringNumericProcessorDefinition(f.location(), + f.expression(), + toProcessorDefinition((Expression) randomValueOtherThan(f.left(), () -> randomStringLiteral())), + f.right(), + f.operation())); + randoms.add(f -> new BinaryStringNumericProcessorDefinition(f.location(), + f.expression(), + f.left(), + toProcessorDefinition((Expression) randomValueOtherThan(f.right(), () -> randomIntLiteral())), + f.operation())); + randoms.add(f -> new BinaryStringNumericProcessorDefinition(f.location(), + f.expression(), + toProcessorDefinition((Expression) randomValueOtherThan(f.left(), () -> randomStringLiteral())), + toProcessorDefinition((Expression) randomValueOtherThan(f.right(), () -> randomIntLiteral())), + f.operation())); + + return randomFrom(randoms).apply(instance); + } + + @Override + protected BinaryStringNumericProcessorDefinition copy(BinaryStringNumericProcessorDefinition instance) { + return new BinaryStringNumericProcessorDefinition(instance.location(), + instance.expression(), + instance.left(), + instance.right(), + instance.operation()); + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericProcessorTests.java new file mode 100644 index 0000000000000..4cfc43d50b166 --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericProcessorTests.java @@ -0,0 +1,112 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.Writeable.Reader; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.scalar.Processors; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.ConstantProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringNumericProcessor.BinaryStringNumericOperation; + +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.l; +import static org.elasticsearch.xpack.sql.tree.Location.EMPTY; + +public class BinaryStringNumericProcessorTests extends AbstractWireSerializingTestCase { + + @Override + protected BinaryStringNumericProcessor createTestInstance() { + return new BinaryStringNumericProcessor( + new ConstantProcessor(randomRealisticUnicodeOfLengthBetween(1, 128)), + new ConstantProcessor(randomInt(256)), + randomFrom(BinaryStringNumericOperation.values())); + } + + @Override + protected Reader instanceReader() { + return BinaryStringNumericProcessor::new; + } + + @Override + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(Processors.getNamedWriteables()); + } + + public void testLeftFunctionWithValidInput() { + assertEquals("foo", new Left(EMPTY, l("foo bar"), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foo bar", new Left(EMPTY, l("foo bar"), l(7)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foo bar", new Left(EMPTY, l("foo bar"), l(123)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("f", new Left(EMPTY, l('f'), l(1)).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testLeftFunctionWithEdgeCases() { + assertNull(new Left(EMPTY, l("foo bar"), l(null)).makeProcessorDefinition().asProcessor().process(null)); + assertNull(new Left(EMPTY, l(null), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertNull(new Left(EMPTY, l(null), l(null)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("", new Left(EMPTY, l("foo bar"), l(-1)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("", new Left(EMPTY, l("foo bar"), l(0)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("", new Left(EMPTY, l('f'), l(0)).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testLeftFunctionInputValidation() { + SqlIllegalArgumentException siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Left(EMPTY, l(5), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A string/char is required; received [5]", siae.getMessage()); + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Left(EMPTY, l("foo bar"), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A number is required; received [baz]", siae.getMessage()); + } + + public void testRightFunctionWithValidInput() { + assertEquals("bar", new Right(EMPTY, l("foo bar"), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foo bar", new Right(EMPTY, l("foo bar"), l(7)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foo bar", new Right(EMPTY, l("foo bar"), l(123)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("f", new Right(EMPTY, l('f'), l(1)).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testRightFunctionWithEdgeCases() { + assertNull(new Right(EMPTY, l("foo bar"), l(null)).makeProcessorDefinition().asProcessor().process(null)); + assertNull(new Right(EMPTY, l(null), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertNull(new Right(EMPTY, l(null), l(null)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("", new Right(EMPTY, l("foo bar"), l(-1)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("", new Right(EMPTY, l("foo bar"), l(0)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("", new Right(EMPTY, l('f'), l(0)).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testRightFunctionInputValidation() { + SqlIllegalArgumentException siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Right(EMPTY, l(5), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A string/char is required; received [5]", siae.getMessage()); + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Right(EMPTY, l("foo bar"), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A number is required; received [baz]", siae.getMessage()); + } + + public void testRepeatFunctionWithValidInput() { + assertEquals("foofoofoo", new Repeat(EMPTY, l("foo"), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foo", new Repeat(EMPTY, l("foo"), l(1)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("fff", new Repeat(EMPTY, l('f'), l(3)).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testRepeatFunctionWithEdgeCases() { + assertNull(new Repeat(EMPTY, l("foo"), l(null)).makeProcessorDefinition().asProcessor().process(null)); + assertNull(new Repeat(EMPTY, l(null), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertNull(new Repeat(EMPTY, l(null), l(null)).makeProcessorDefinition().asProcessor().process(null)); + assertNull(new Repeat(EMPTY, l("foo"), l(-1)).makeProcessorDefinition().asProcessor().process(null)); + assertNull(new Repeat(EMPTY, l("foo"), l(0)).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testRepeatFunctionInputsValidation() { + SqlIllegalArgumentException siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Repeat(EMPTY, l(5), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A string/char is required; received [5]", siae.getMessage()); + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Repeat(EMPTY, l("foo bar"), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A number is required; received [baz]", siae.getMessage()); + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringStringProcessorDefinitionTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringStringProcessorDefinitionTests.java new file mode 100644 index 0000000000000..77b1329bdeaec --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringStringProcessorDefinitionTests.java @@ -0,0 +1,130 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.BinaryProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.tree.AbstractNodeTestCase; +import org.elasticsearch.xpack.sql.tree.Location; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomStringLiteral; +import static org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions.toProcessorDefinition; +import static org.elasticsearch.xpack.sql.tree.LocationTests.randomLocation; + +public class BinaryStringStringProcessorDefinitionTests + extends AbstractNodeTestCase { + + @Override + protected BinaryStringStringProcessorDefinition randomInstance() { + return randomBinaryStringStringProcessorDefinition(); + } + + private Expression randomBinaryStringStringExpression() { + return randomBinaryStringStringProcessorDefinition().expression(); + } + + public static BinaryStringStringProcessorDefinition randomBinaryStringStringProcessorDefinition() { + List functions = new ArrayList<>(); + functions.add(new Position( + randomLocation(), + randomStringLiteral(), + randomStringLiteral() + ).makeProcessorDefinition()); + // if we decide to add DIFFERENCE(string,string) in the future, here we'd add it as well + return (BinaryStringStringProcessorDefinition) randomFrom(functions); + } + + @Override + public void testTransform() { + // test transforming only the properties (location, expression), + // skipping the children (the two parameters of the binary function) which are tested separately + BinaryStringStringProcessorDefinition b1 = randomInstance(); + Expression newExpression = randomValueOtherThan(b1.expression(), () -> randomBinaryStringStringExpression()); + BinaryStringStringProcessorDefinition newB = new BinaryStringStringProcessorDefinition( + b1.location(), + newExpression, + b1.left(), + b1.right(), + b1.operation()); + assertEquals(newB, b1.transformPropertiesOnly(v -> Objects.equals(v, b1.expression()) ? newExpression : v, Expression.class)); + + BinaryStringStringProcessorDefinition b2 = randomInstance(); + Location newLoc = randomValueOtherThan(b2.location(), () -> randomLocation()); + newB = new BinaryStringStringProcessorDefinition( + newLoc, + b2.expression(), + b2.left(), + b2.right(), + b2.operation()); + assertEquals(newB, + b2.transformPropertiesOnly(v -> Objects.equals(v, b2.location()) ? newLoc : v, Location.class)); + } + + @Override + public void testReplaceChildren() { + BinaryStringStringProcessorDefinition b = randomInstance(); + ProcessorDefinition newLeft = toProcessorDefinition((Expression) randomValueOtherThan(b.left(), () -> randomStringLiteral())); + ProcessorDefinition newRight = toProcessorDefinition((Expression) randomValueOtherThan(b.right(), () -> randomStringLiteral())); + BinaryStringStringProcessorDefinition newB = + new BinaryStringStringProcessorDefinition(b.location(), b.expression(), b.left(), b.right(), b.operation()); + + BinaryProcessorDefinition transformed = newB.replaceChildren(newLeft, b.right()); + assertEquals(transformed.left(), newLeft); + assertEquals(transformed.location(), b.location()); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.right(), b.right()); + + transformed = newB.replaceChildren(b.left(), newRight); + assertEquals(transformed.left(), b.left()); + assertEquals(transformed.location(), b.location()); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.right(), newRight); + + transformed = newB.replaceChildren(newLeft, newRight); + assertEquals(transformed.left(), newLeft); + assertEquals(transformed.location(), b.location()); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.right(), newRight); + } + + @Override + protected BinaryStringStringProcessorDefinition mutate(BinaryStringStringProcessorDefinition instance) { + List> randoms = new ArrayList<>(); + randoms.add(f -> new BinaryStringStringProcessorDefinition(f.location(), + f.expression(), + toProcessorDefinition((Expression) randomValueOtherThan(f.left(), () -> randomStringLiteral())), + f.right(), + f.operation())); + randoms.add(f -> new BinaryStringStringProcessorDefinition(f.location(), + f.expression(), + f.left(), + toProcessorDefinition((Expression) randomValueOtherThan(f.right(), () -> randomStringLiteral())), + f.operation())); + randoms.add(f -> new BinaryStringStringProcessorDefinition(f.location(), + f.expression(), + toProcessorDefinition((Expression) randomValueOtherThan(f.left(), () -> randomStringLiteral())), + toProcessorDefinition((Expression) randomValueOtherThan(f.right(), () -> randomStringLiteral())), + f.operation())); + + return randomFrom(randoms).apply(instance); + } + + @Override + protected BinaryStringStringProcessorDefinition copy(BinaryStringStringProcessorDefinition instance) { + return new BinaryStringStringProcessorDefinition(instance.location(), + instance.expression(), + instance.left(), + instance.right(), + instance.operation()); + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringStringProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringStringProcessorTests.java new file mode 100644 index 0000000000000..3d0a1ebb1abbb --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringStringProcessorTests.java @@ -0,0 +1,63 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.Writeable.Reader; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.scalar.Processors; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.ConstantProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringStringProcessor.BinaryStringStringOperation; + +import static org.elasticsearch.xpack.sql.tree.Location.EMPTY; +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.l; + +public class BinaryStringStringProcessorTests extends AbstractWireSerializingTestCase { + + @Override + protected BinaryStringStringProcessor createTestInstance() { + return new BinaryStringStringProcessor( + new ConstantProcessor(randomRealisticUnicodeOfLengthBetween(1, 128)), + new ConstantProcessor(randomRealisticUnicodeOfLengthBetween(1, 128)), + randomFrom(BinaryStringStringOperation.values())); + } + + @Override + protected Reader instanceReader() { + return BinaryStringStringProcessor::new; + } + + @Override + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(Processors.getNamedWriteables()); + } + + public void testPositionFunctionWithValidInput() { + assertEquals(4, new Position(EMPTY, l("bar"), l("foobar")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals(1, new Position(EMPTY, l("foo"), l("foobar")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals(0, new Position(EMPTY, l("foo"), l("bar")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals(3, new Position(EMPTY, l('r'), l("bar")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals(0, new Position(EMPTY, l('z'), l("bar")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals(1, new Position(EMPTY, l('b'), l('b')).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testPositionFunctionWithEdgeCases() { + assertNull(new Position(EMPTY, l("foo"), l(null)).makeProcessorDefinition().asProcessor().process(null)); + assertNull(new Position(EMPTY, l(null), l("foo")).makeProcessorDefinition().asProcessor().process(null)); + assertNull(new Position(EMPTY, l(null), l(null)).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testPositionFunctionInputsValidation() { + SqlIllegalArgumentException siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Position(EMPTY, l(5), l("foo")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A string/char is required; received [5]", siae.getMessage()); + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Position(EMPTY, l("foo bar"), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A string/char is required; received [3]", siae.getMessage()); + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ConcatFunctionProcessorDefinitionTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ConcatFunctionProcessorDefinitionTests.java new file mode 100644 index 0000000000000..4e14e15c1953d --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ConcatFunctionProcessorDefinitionTests.java @@ -0,0 +1,121 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.BinaryProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.tree.AbstractNodeTestCase; +import org.elasticsearch.xpack.sql.tree.Location; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomStringLiteral; +import static org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions.toProcessorDefinition; +import static org.elasticsearch.xpack.sql.tree.LocationTests.randomLocation; + +public class ConcatFunctionProcessorDefinitionTests extends AbstractNodeTestCase { + + @Override + protected ConcatFunctionProcessorDefinition randomInstance() { + return randomConcatFunctionProcessorDefinition(); + } + + private Expression randomConcatFunctionExpression() { + return randomConcatFunctionProcessorDefinition().expression(); + } + + public static ConcatFunctionProcessorDefinition randomConcatFunctionProcessorDefinition() { + return (ConcatFunctionProcessorDefinition) new Concat( + randomLocation(), + randomStringLiteral(), + randomStringLiteral()) + .makeProcessorDefinition(); + } + + @Override + public void testTransform() { + // test transforming only the properties (location, expression), + // skipping the children (the two parameters of the binary function) which are tested separately + ConcatFunctionProcessorDefinition b1 = randomInstance(); + + Expression newExpression = randomValueOtherThan(b1.expression(), () -> randomConcatFunctionExpression()); + ConcatFunctionProcessorDefinition newB = new ConcatFunctionProcessorDefinition( + b1.location(), + newExpression, + b1.left(), + b1.right()); + assertEquals(newB, b1.transformPropertiesOnly(v -> Objects.equals(v, b1.expression()) ? newExpression : v, Expression.class)); + + ConcatFunctionProcessorDefinition b2 = randomInstance(); + Location newLoc = randomValueOtherThan(b2.location(), () -> randomLocation()); + newB = new ConcatFunctionProcessorDefinition( + newLoc, + b2.expression(), + b2.left(), + b2.right()); + assertEquals(newB, + b2.transformPropertiesOnly(v -> Objects.equals(v, b2.location()) ? newLoc : v, Location.class)); + } + + @Override + public void testReplaceChildren() { + ConcatFunctionProcessorDefinition b = randomInstance(); + ProcessorDefinition newLeft = toProcessorDefinition((Expression) randomValueOtherThan(b.left(), () -> randomStringLiteral())); + ProcessorDefinition newRight = toProcessorDefinition((Expression) randomValueOtherThan(b.right(), () -> randomStringLiteral())); + ConcatFunctionProcessorDefinition newB = + new ConcatFunctionProcessorDefinition(b.location(), b.expression(), b.left(), b.right()); + BinaryProcessorDefinition transformed = newB.replaceChildren(newLeft, b.right()); + + assertEquals(transformed.left(), newLeft); + assertEquals(transformed.location(), b.location()); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.right(), b.right()); + + transformed = newB.replaceChildren(b.left(), newRight); + assertEquals(transformed.left(), b.left()); + assertEquals(transformed.location(), b.location()); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.right(), newRight); + + transformed = newB.replaceChildren(newLeft, newRight); + assertEquals(transformed.left(), newLeft); + assertEquals(transformed.location(), b.location()); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.right(), newRight); + } + + @Override + protected ConcatFunctionProcessorDefinition mutate(ConcatFunctionProcessorDefinition instance) { + List> randoms = new ArrayList<>(); + randoms.add(f -> new ConcatFunctionProcessorDefinition(f.location(), + f.expression(), + toProcessorDefinition((Expression) randomValueOtherThan(f.left(), () -> randomStringLiteral())), + f.right())); + randoms.add(f -> new ConcatFunctionProcessorDefinition(f.location(), + f.expression(), + f.left(), + toProcessorDefinition((Expression) randomValueOtherThan(f.right(), () -> randomStringLiteral())))); + randoms.add(f -> new ConcatFunctionProcessorDefinition(f.location(), + f.expression(), + toProcessorDefinition((Expression) randomValueOtherThan(f.left(), () -> randomStringLiteral())), + toProcessorDefinition((Expression) randomValueOtherThan(f.right(), () -> randomStringLiteral())))); + + return randomFrom(randoms).apply(instance); + } + + @Override + protected ConcatFunctionProcessorDefinition copy(ConcatFunctionProcessorDefinition instance) { + return new ConcatFunctionProcessorDefinition(instance.location(), + instance.expression(), + instance.left(), + instance.right()); + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ConcatProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ConcatProcessorTests.java new file mode 100644 index 0000000000000..804f9a9d150b2 --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ConcatProcessorTests.java @@ -0,0 +1,57 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.Writeable.Reader; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.scalar.Processors; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.ConstantProcessor; + +import static org.elasticsearch.xpack.sql.tree.Location.EMPTY; +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.l; + +public class ConcatProcessorTests extends AbstractWireSerializingTestCase { + + @Override + protected ConcatFunctionProcessor createTestInstance() { + return new ConcatFunctionProcessor( + new ConstantProcessor(randomRealisticUnicodeOfLengthBetween(0, 128)), + new ConstantProcessor(randomRealisticUnicodeOfLengthBetween(0, 128))); + } + + @Override + protected Reader instanceReader() { + return ConcatFunctionProcessor::new; + } + + @Override + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(Processors.getNamedWriteables()); + } + + public void testConcatFunctionWithValidInput() { + assertEquals("foobar", new Concat(EMPTY, l("foo"), l("bar")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("fb", new Concat(EMPTY, l('f'), l('b')).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testConcatFunctionWithEdgeCases() { + assertEquals("foo", new Concat(EMPTY, l("foo"), l(null)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("bar", new Concat(EMPTY, l(null), l("bar")).makeProcessorDefinition().asProcessor().process(null)); + assertNull(new Concat(EMPTY, l(null), l(null)).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testConcatFunctionInputsValidation() { + SqlIllegalArgumentException siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Concat(EMPTY, l(5), l("foo")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A string/char is required; received [5]", siae.getMessage()); + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Concat(EMPTY, l("foo bar"), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A string/char is required; received [3]", siae.getMessage()); + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/InsertFunctionProcessorDefinitionTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/InsertFunctionProcessorDefinitionTests.java new file mode 100644 index 0000000000000..bf17962b0837c --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/InsertFunctionProcessorDefinitionTests.java @@ -0,0 +1,135 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.Combinations; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.tree.AbstractNodeTestCase; +import org.elasticsearch.xpack.sql.tree.Location; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomIntLiteral; +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomStringLiteral; +import static org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions.toProcessorDefinition; +import static org.elasticsearch.xpack.sql.tree.LocationTests.randomLocation; + +public class InsertFunctionProcessorDefinitionTests extends AbstractNodeTestCase { + + @Override + protected InsertFunctionProcessorDefinition randomInstance() { + return randomInsertFunctionProcessorDefinition(); + } + + private Expression randomInsertFunctionExpression() { + return randomInsertFunctionProcessorDefinition().expression(); + } + + public static InsertFunctionProcessorDefinition randomInsertFunctionProcessorDefinition() { + return (InsertFunctionProcessorDefinition) (new Insert(randomLocation(), + randomStringLiteral(), + randomIntLiteral(), + randomIntLiteral(), + randomStringLiteral()) + .makeProcessorDefinition()); + } + + @Override + public void testTransform() { + // test transforming only the properties (location, expression), + // skipping the children (the two parameters of the binary function) which are tested separately + InsertFunctionProcessorDefinition b1 = randomInstance(); + Expression newExpression = randomValueOtherThan(b1.expression(), () -> randomInsertFunctionExpression()); + InsertFunctionProcessorDefinition newB = new InsertFunctionProcessorDefinition( + b1.location(), + newExpression, + b1.source(), + b1.start(), + b1.length(), + b1.replacement()); + assertEquals(newB, b1.transformPropertiesOnly(v -> Objects.equals(v, b1.expression()) ? newExpression : v, Expression.class)); + + InsertFunctionProcessorDefinition b2 = randomInstance(); + Location newLoc = randomValueOtherThan(b2.location(), () -> randomLocation()); + newB = new InsertFunctionProcessorDefinition( + newLoc, + b2.expression(), + b2.source(), + b2.start(), + b2.length(), + b2.replacement()); + assertEquals(newB, + b2.transformPropertiesOnly(v -> Objects.equals(v, b2.location()) ? newLoc : v, Location.class)); + } + + @Override + public void testReplaceChildren() { + InsertFunctionProcessorDefinition b = randomInstance(); + ProcessorDefinition newSource = toProcessorDefinition((Expression) randomValueOtherThan(b.source(), () -> randomStringLiteral())); + ProcessorDefinition newStart = toProcessorDefinition((Expression) randomValueOtherThan(b.start(), () -> randomIntLiteral())); + ProcessorDefinition newLength = toProcessorDefinition((Expression) randomValueOtherThan(b.length(), () -> randomIntLiteral())); + ProcessorDefinition newR = toProcessorDefinition((Expression) randomValueOtherThan(b.replacement(), () -> randomStringLiteral())); + InsertFunctionProcessorDefinition newB = + new InsertFunctionProcessorDefinition(b.location(), b.expression(), b.source(), b.start(), b.length(), b.replacement()); + InsertFunctionProcessorDefinition transformed = null; + + // generate all the combinations of possible children modifications and test all of them + for(int i = 1; i < 5; i++) { + for(BitSet comb : new Combinations(4, i)) { + transformed = (InsertFunctionProcessorDefinition) newB.replaceChildren( + comb.get(0) ? newSource : b.source(), + comb.get(1) ? newStart : b.start(), + comb.get(2) ? newLength : b.length(), + comb.get(3) ? newR : b.replacement()); + assertEquals(transformed.source(), comb.get(0) ? newSource : b.source()); + assertEquals(transformed.start(), comb.get(1) ? newStart : b.start()); + assertEquals(transformed.length(), comb.get(2) ? newLength : b.length()); + assertEquals(transformed.replacement(), comb.get(3) ? newR : b.replacement()); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.location(), b.location()); + } + } + } + + @Override + protected InsertFunctionProcessorDefinition mutate(InsertFunctionProcessorDefinition instance) { + List> randoms = new ArrayList<>(); + + for(int i = 1; i < 5; i++) { + for(BitSet comb : new Combinations(4, i)) { + randoms.add(f -> new InsertFunctionProcessorDefinition( + f.location(), + f.expression(), + comb.get(0) ? toProcessorDefinition((Expression) randomValueOtherThan(f.source(), + () -> randomStringLiteral())) : f.source(), + comb.get(1) ? toProcessorDefinition((Expression) randomValueOtherThan(f.start(), + () -> randomIntLiteral())) : f.start(), + comb.get(2) ? toProcessorDefinition((Expression) randomValueOtherThan(f.length(), + () -> randomIntLiteral())): f.length(), + comb.get(3) ? toProcessorDefinition((Expression) randomValueOtherThan(f.replacement(), + () -> randomStringLiteral())) : f.replacement())); + } + } + + return randomFrom(randoms).apply(instance); + } + + @Override + protected InsertFunctionProcessorDefinition copy(InsertFunctionProcessorDefinition instance) { + return new InsertFunctionProcessorDefinition(instance.location(), + instance.expression(), + instance.source(), + instance.start(), + instance.length(), + instance.replacement()); + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/InsertProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/InsertProcessorTests.java new file mode 100644 index 0000000000000..851dc6f1aa8ef --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/InsertProcessorTests.java @@ -0,0 +1,84 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.Writeable.Reader; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.scalar.Processors; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.ConstantProcessor; + +import static org.elasticsearch.xpack.sql.tree.Location.EMPTY; +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.l; + +public class InsertProcessorTests extends AbstractWireSerializingTestCase { + + @Override + protected InsertFunctionProcessor createTestInstance() { + return new InsertFunctionProcessor( + new ConstantProcessor(randomRealisticUnicodeOfLengthBetween(0, 128)), + new ConstantProcessor(randomInt(256)), + new ConstantProcessor(randomInt(128)), + new ConstantProcessor(randomRealisticUnicodeOfLengthBetween(0, 256))); + } + + @Override + protected Reader instanceReader() { + return InsertFunctionProcessor::new; + } + + @Override + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(Processors.getNamedWriteables()); + } + + public void testInsertWithValidInputs() { + assertEquals("bazbar", new Insert(EMPTY, l("foobar"), l(1), l(3), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foobaz", new Insert(EMPTY, l("foobar"), l(4), l(3), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testInsertWithEdgeCases() { + assertNull(new Insert(EMPTY, l(null), l(4), l(3), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foobar", new Insert(EMPTY, l("foobar"), l(4), l(3), l(null)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foobar", + new Insert(EMPTY, l("foobar"), l(null), l(3), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foobar", + new Insert(EMPTY, l("foobar"), l(4), l(null), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("bazbar", new Insert(EMPTY, l("foobar"), l(-1), l(3), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foobaz", new Insert(EMPTY, l("foobar"), l(4), l(30), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foobaz", new Insert(EMPTY, l("foobar"), l(6), l(1), l('z')).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foobarbaz", + new Insert(EMPTY, l("foobar"), l(7), l(1000), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foobar", + new Insert(EMPTY, l("foobar"), l(8), l(1000), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("fzr", new Insert(EMPTY, l("foobar"), l(2), l(4), l('z')).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("CAR", new Insert(EMPTY, l("FOOBAR"), l(1), l(5), l("CA")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("z", new Insert(EMPTY, l('f'), l(1), l(10), l('z')).makeProcessorDefinition().asProcessor().process(null)); + + assertEquals("bla", new Insert(EMPTY, l(""), l(1), l(10), l("bla")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("", new Insert(EMPTY, l(""), l(2), l(10), l("bla")).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testInsertInputsValidation() { + SqlIllegalArgumentException siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Insert(EMPTY, l(5), l(1), l(3), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A string/char is required; received [5]", siae.getMessage()); + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Insert(EMPTY, l("foobar"), l(1), l(3), l(66)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A string/char is required; received [66]", siae.getMessage()); + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Insert(EMPTY, l("foobar"), l("c"), l(3), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A number is required; received [c]", siae.getMessage()); + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Insert(EMPTY, l("foobar"), l(1), l('z'), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A number is required; received [z]", siae.getMessage()); + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Insert(EMPTY, l("foobar"), l(1), l(-1), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A positive number is required for [length]; received [-1]", siae.getMessage()); + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/LocateFunctionProcessorDefinitionTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/LocateFunctionProcessorDefinitionTests.java new file mode 100644 index 0000000000000..464660d7907f5 --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/LocateFunctionProcessorDefinitionTests.java @@ -0,0 +1,169 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.Combinations; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.tree.AbstractNodeTestCase; +import org.elasticsearch.xpack.sql.tree.Location; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomIntLiteral; +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomStringLiteral; +import static org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions.toProcessorDefinition; +import static org.elasticsearch.xpack.sql.tree.LocationTests.randomLocation; + +public class LocateFunctionProcessorDefinitionTests extends AbstractNodeTestCase { + + @Override + protected LocateFunctionProcessorDefinition randomInstance() { + return randomLocateFunctionProcessorDefinition(); + } + + private Expression randomLocateFunctionExpression() { + return randomLocateFunctionProcessorDefinition().expression(); + } + + public static LocateFunctionProcessorDefinition randomLocateFunctionProcessorDefinition() { + return (LocateFunctionProcessorDefinition) (new Locate(randomLocation(), + randomStringLiteral(), + randomStringLiteral(), + frequently() ? randomIntLiteral() : null) + .makeProcessorDefinition()); + } + + @Override + public void testTransform() { + // test transforming only the properties (location, expression), + // skipping the children (the two parameters of the binary function) which are tested separately + LocateFunctionProcessorDefinition b1 = randomInstance(); + Expression newExpression = randomValueOtherThan(b1.expression(), () -> randomLocateFunctionExpression()); + LocateFunctionProcessorDefinition newB; + if (b1.start() == null) { + newB = new LocateFunctionProcessorDefinition( + b1.location(), + newExpression, + b1.pattern(), + b1.source()); + } else { + newB = new LocateFunctionProcessorDefinition( + b1.location(), + newExpression, + b1.pattern(), + b1.source(), + b1.start()); + } + assertEquals(newB, b1.transformPropertiesOnly(v -> Objects.equals(v, b1.expression()) ? newExpression : v, Expression.class)); + + LocateFunctionProcessorDefinition b2 = randomInstance(); + Location newLoc = randomValueOtherThan(b2.location(), () -> randomLocation()); + if (b2.start() == null) { + newB = new LocateFunctionProcessorDefinition( + newLoc, + b2.expression(), + b2.pattern(), + b2.source()); + } else { + newB = new LocateFunctionProcessorDefinition( + newLoc, + b2.expression(), + b2.pattern(), + b2.source(), + b2.start()); + } + assertEquals(newB, + b2.transformPropertiesOnly(v -> Objects.equals(v, b2.location()) ? newLoc : v, Location.class)); + } + + @Override + public void testReplaceChildren() { + LocateFunctionProcessorDefinition b = randomInstance(); + ProcessorDefinition newPattern = toProcessorDefinition((Expression) randomValueOtherThan(b.pattern(), () -> randomStringLiteral())); + ProcessorDefinition newSource = toProcessorDefinition((Expression) randomValueOtherThan(b.source(), () -> randomStringLiteral())); + ProcessorDefinition newStart; + + LocateFunctionProcessorDefinition newB; + if (b.start() == null) { + newB = new LocateFunctionProcessorDefinition(b.location(), b.expression(), b.pattern(), b.source()); + newStart = null; + } + else { + newB = new LocateFunctionProcessorDefinition(b.location(), b.expression(), b.pattern(), b.source(), b.start()); + newStart = toProcessorDefinition((Expression) randomValueOtherThan(b.start(), () -> randomIntLiteral())); + } + LocateFunctionProcessorDefinition transformed = null; + + // generate all the combinations of possible children modifications and test all of them + for(int i = 1; i < 4; i++) { + for(BitSet comb : new Combinations(3, i)) { + transformed = (LocateFunctionProcessorDefinition) newB.replaceChildren( + comb.get(0) ? newPattern : b.pattern(), + comb.get(1) ? newSource : b.source(), + comb.get(2) ? newStart : b.start()); + + assertEquals(transformed.pattern(), comb.get(0) ? newPattern : b.pattern()); + assertEquals(transformed.source(), comb.get(1) ? newSource : b.source()); + assertEquals(transformed.start(), comb.get(2) ? newStart : b.start()); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.location(), b.location()); + } + } + } + + @Override + protected LocateFunctionProcessorDefinition mutate(LocateFunctionProcessorDefinition instance) { + List> randoms = new ArrayList<>(); + if (instance.start() == null) { + for(int i = 1; i < 3; i++) { + for(BitSet comb : new Combinations(2, i)) { + randoms.add(f -> new LocateFunctionProcessorDefinition(f.location(), + f.expression(), + comb.get(0) ? toProcessorDefinition((Expression) randomValueOtherThan(f.pattern(), + () -> randomStringLiteral())) : f.pattern(), + comb.get(1) ? toProcessorDefinition((Expression) randomValueOtherThan(f.source(), + () -> randomStringLiteral())) : f.source())); + } + } + } else { + for(int i = 1; i < 4; i++) { + for(BitSet comb : new Combinations(3, i)) { + randoms.add(f -> new LocateFunctionProcessorDefinition(f.location(), + f.expression(), + comb.get(0) ? toProcessorDefinition((Expression) randomValueOtherThan(f.pattern(), + () -> randomStringLiteral())) : f.pattern(), + comb.get(1) ? toProcessorDefinition((Expression) randomValueOtherThan(f.source(), + () -> randomStringLiteral())) : f.source(), + comb.get(2) ? toProcessorDefinition((Expression) randomValueOtherThan(f.start(), + () -> randomIntLiteral())) : f.start())); + } + } + } + + return randomFrom(randoms).apply(instance); + } + + @Override + protected LocateFunctionProcessorDefinition copy(LocateFunctionProcessorDefinition instance) { + return instance.start() == null ? + new LocateFunctionProcessorDefinition(instance.location(), + instance.expression(), + instance.pattern(), + instance.source()) + : + new LocateFunctionProcessorDefinition(instance.location(), + instance.expression(), + instance.pattern(), + instance.source(), + instance.start()); + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/LocateProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/LocateProcessorTests.java new file mode 100644 index 0000000000000..e03ef72b8b8ea --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/LocateProcessorTests.java @@ -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.sql.expression.function.scalar.string; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.Writeable.Reader; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.scalar.Processors; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.ConstantProcessor; + +import static org.elasticsearch.xpack.sql.tree.Location.EMPTY; +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.l; + +public class LocateProcessorTests extends AbstractWireSerializingTestCase { + + @Override + protected LocateFunctionProcessor createTestInstance() { + // the "start" parameter is optional and is treated as null in the constructor + // when it is not used. Need to take this into account when generating random + // values for it. + Integer start = frequently() ? randomInt() : null; + return new LocateFunctionProcessor( + new ConstantProcessor(randomRealisticUnicodeOfLengthBetween(0, 128)), + new ConstantProcessor(randomRealisticUnicodeOfLengthBetween(0, 128)), + new ConstantProcessor(start)); + } + + @Override + protected Reader instanceReader() { + return LocateFunctionProcessor::new; + } + + @Override + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(Processors.getNamedWriteables()); + } + + public void testLocateFunctionWithValidInput() { + assertEquals(4, new Locate(EMPTY, l("bar"), l("foobarbar"), l(null)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals(7, new Locate(EMPTY, l("bar"), l("foobarbar"), l(5)).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testLocateFunctionWithEdgeCasesInputs() { + assertEquals(4, new Locate(EMPTY, l("bar"), l("foobarbar"), l(null)).makeProcessorDefinition().asProcessor().process(null)); + assertNull(new Locate(EMPTY, l("bar"), l(null), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals(0, new Locate(EMPTY, l(null), l("foobarbar"), l(null)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals(0, new Locate(EMPTY, l(null), l("foobarbar"), l(null)).makeProcessorDefinition().asProcessor().process(null)); + + assertEquals(1, new Locate(EMPTY, l("foo"), l("foobarbar"), l(null)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals(1, new Locate(EMPTY, l('o'), l('o'), l(null)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals(9, new Locate(EMPTY, l('r'), l("foobarbar"), l(9)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals(4, new Locate(EMPTY, l("bar"), l("foobarbar"), l(-3)).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testLocateFunctionValidatingInputs() { + SqlIllegalArgumentException siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Locate(EMPTY, l(5), l("foobarbar"), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A string/char is required; received [5]", siae.getMessage()); + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Locate(EMPTY, l("foo"), l(1), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A string/char is required; received [1]", siae.getMessage()); + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Locate(EMPTY, l("foobarbar"), l("bar"), l('c')).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A number is required; received [c]", siae.getMessage()); + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ReplaceFunctionProcessorDefinitionTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ReplaceFunctionProcessorDefinitionTests.java new file mode 100644 index 0000000000000..14e9e51f47107 --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ReplaceFunctionProcessorDefinitionTests.java @@ -0,0 +1,126 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.Combinations; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.tree.AbstractNodeTestCase; +import org.elasticsearch.xpack.sql.tree.Location; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomStringLiteral; +import static org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions.toProcessorDefinition; +import static org.elasticsearch.xpack.sql.tree.LocationTests.randomLocation; + +public class ReplaceFunctionProcessorDefinitionTests extends AbstractNodeTestCase { + + @Override + protected ReplaceFunctionProcessorDefinition randomInstance() { + return randomReplaceFunctionProcessorDefinition(); + } + + private Expression randomReplaceFunctionExpression() { + return randomReplaceFunctionProcessorDefinition().expression(); + } + + public static ReplaceFunctionProcessorDefinition randomReplaceFunctionProcessorDefinition() { + return (ReplaceFunctionProcessorDefinition) (new Replace(randomLocation(), + randomStringLiteral(), + randomStringLiteral(), + randomStringLiteral()) + .makeProcessorDefinition()); + } + + @Override + public void testTransform() { + // test transforming only the properties (location, expression), + // skipping the children (the two parameters of the binary function) which are tested separately + ReplaceFunctionProcessorDefinition b1 = randomInstance(); + + Expression newExpression = randomValueOtherThan(b1.expression(), () -> randomReplaceFunctionExpression()); + ReplaceFunctionProcessorDefinition newB = new ReplaceFunctionProcessorDefinition( + b1.location(), + newExpression, + b1.source(), + b1.pattern(), + b1.replacement()); + assertEquals(newB, b1.transformPropertiesOnly(v -> Objects.equals(v, b1.expression()) ? newExpression : v, Expression.class)); + + ReplaceFunctionProcessorDefinition b2 = randomInstance(); + Location newLoc = randomValueOtherThan(b2.location(), () -> randomLocation()); + newB = new ReplaceFunctionProcessorDefinition( + newLoc, + b2.expression(), + b2.source(), + b2.pattern(), + b2.replacement()); + assertEquals(newB, + b2.transformPropertiesOnly(v -> Objects.equals(v, b2.location()) ? newLoc : v, Location.class)); + } + + @Override + public void testReplaceChildren() { + ReplaceFunctionProcessorDefinition b = randomInstance(); + ProcessorDefinition newSource = toProcessorDefinition((Expression) randomValueOtherThan(b.source(), () -> randomStringLiteral())); + ProcessorDefinition newPattern = toProcessorDefinition((Expression) randomValueOtherThan(b.pattern(), () -> randomStringLiteral())); + ProcessorDefinition newR = toProcessorDefinition((Expression) randomValueOtherThan(b.replacement(), () -> randomStringLiteral())); + ReplaceFunctionProcessorDefinition newB = + new ReplaceFunctionProcessorDefinition(b.location(), b.expression(), b.source(), b.pattern(), b.replacement()); + ReplaceFunctionProcessorDefinition transformed = null; + + // generate all the combinations of possible children modifications and test all of them + for(int i = 1; i < 4; i++) { + for(BitSet comb : new Combinations(3, i)) { + transformed = (ReplaceFunctionProcessorDefinition) newB.replaceChildren( + comb.get(0) ? newSource : b.source(), + comb.get(1) ? newPattern : b.pattern(), + comb.get(2) ? newR : b.replacement()); + + assertEquals(transformed.source(), comb.get(0) ? newSource : b.source()); + assertEquals(transformed.pattern(), comb.get(1) ? newPattern : b.pattern()); + assertEquals(transformed.replacement(), comb.get(2) ? newR : b.replacement()); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.location(), b.location()); + } + } + } + + @Override + protected ReplaceFunctionProcessorDefinition mutate(ReplaceFunctionProcessorDefinition instance) { + List> randoms = new ArrayList<>(); + + for(int i = 1; i < 4; i++) { + for(BitSet comb : new Combinations(3, i)) { + randoms.add(f -> new ReplaceFunctionProcessorDefinition(f.location(), + f.expression(), + comb.get(0) ? toProcessorDefinition((Expression) randomValueOtherThan(f.source(), + () -> randomStringLiteral())) : f.source(), + comb.get(1) ? toProcessorDefinition((Expression) randomValueOtherThan(f.pattern(), + () -> randomStringLiteral())) : f.pattern(), + comb.get(2) ? toProcessorDefinition((Expression) randomValueOtherThan(f.replacement(), + () -> randomStringLiteral())) : f.replacement())); + } + } + + return randomFrom(randoms).apply(instance); + } + + @Override + protected ReplaceFunctionProcessorDefinition copy(ReplaceFunctionProcessorDefinition instance) { + return new ReplaceFunctionProcessorDefinition(instance.location(), + instance.expression(), + instance.source(), + instance.pattern(), + instance.replacement()); + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ReplaceProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ReplaceProcessorTests.java new file mode 100644 index 0000000000000..77c84d5e7f4a3 --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ReplaceProcessorTests.java @@ -0,0 +1,66 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.Writeable.Reader; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.scalar.Processors; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.ConstantProcessor; + +import static org.elasticsearch.xpack.sql.tree.Location.EMPTY; +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.l; + +public class ReplaceProcessorTests extends AbstractWireSerializingTestCase { + + @Override + protected ReplaceFunctionProcessor createTestInstance() { + return new ReplaceFunctionProcessor( + new ConstantProcessor(randomRealisticUnicodeOfLengthBetween(0, 128)), + new ConstantProcessor(randomRealisticUnicodeOfLengthBetween(0, 128)), + new ConstantProcessor(randomRealisticUnicodeOfLengthBetween(0, 128))); + } + + @Override + protected Reader instanceReader() { + return ReplaceFunctionProcessor::new; + } + + @Override + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(Processors.getNamedWriteables()); + } + + public void testReplaceFunctionWithValidInput() { + assertEquals("foobazbaz", + new Replace(EMPTY, l("foobarbar"), l("bar"), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foobXrbXr", new Replace(EMPTY, l("foobarbar"), l('a'), l('X')).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("z", new Replace(EMPTY, l('f'), l('f'), l('z')).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testReplaceFunctionWithEdgeCases() { + assertEquals("foobarbar", + new Replace(EMPTY, l("foobarbar"), l("bar"), l(null)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foobarbar", + new Replace(EMPTY, l("foobarbar"), l(null), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertNull(new Replace(EMPTY, l(null), l("bar"), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertNull(new Replace(EMPTY, l(null), l(null), l(null)).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testReplaceFunctionInputsValidation() { + SqlIllegalArgumentException siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Replace(EMPTY, l(5), l("bar"), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A string/char is required; received [5]", siae.getMessage()); + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Replace(EMPTY, l("foobarbar"), l(4), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A string/char is required; received [4]", siae.getMessage()); + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Replace(EMPTY, l("foobarbar"), l("bar"), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A string/char is required; received [3]", siae.getMessage()); + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/SubstringFunctionProcessorDefinitionTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/SubstringFunctionProcessorDefinitionTests.java new file mode 100644 index 0000000000000..250949123a9b2 --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/SubstringFunctionProcessorDefinitionTests.java @@ -0,0 +1,127 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.Combinations; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.tree.AbstractNodeTestCase; +import org.elasticsearch.xpack.sql.tree.Location; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomIntLiteral; +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomStringLiteral; +import static org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions.toProcessorDefinition; +import static org.elasticsearch.xpack.sql.tree.LocationTests.randomLocation; + +public class SubstringFunctionProcessorDefinitionTests + extends AbstractNodeTestCase { + + @Override + protected SubstringFunctionProcessorDefinition randomInstance() { + return randomSubstringFunctionProcessorDefinition(); + } + + private Expression randomSubstringFunctionExpression() { + return randomSubstringFunctionProcessorDefinition().expression(); + } + + public static SubstringFunctionProcessorDefinition randomSubstringFunctionProcessorDefinition() { + return (SubstringFunctionProcessorDefinition) (new Substring(randomLocation(), + randomStringLiteral(), + randomIntLiteral(), + randomIntLiteral()) + .makeProcessorDefinition()); + } + + @Override + public void testTransform() { + // test transforming only the properties (location, expression), + // skipping the children (the two parameters of the binary function) which are tested separately + SubstringFunctionProcessorDefinition b1 = randomInstance(); + Expression newExpression = randomValueOtherThan(b1.expression(), () -> randomSubstringFunctionExpression()); + SubstringFunctionProcessorDefinition newB = new SubstringFunctionProcessorDefinition( + b1.location(), + newExpression, + b1.source(), + b1.start(), + b1.length()); + assertEquals(newB, b1.transformPropertiesOnly(v -> Objects.equals(v, b1.expression()) ? newExpression : v, Expression.class)); + + SubstringFunctionProcessorDefinition b2 = randomInstance(); + Location newLoc = randomValueOtherThan(b2.location(), () -> randomLocation()); + newB = new SubstringFunctionProcessorDefinition( + newLoc, + b2.expression(), + b2.source(), + b2.start(), + b2.length()); + assertEquals(newB, + b2.transformPropertiesOnly(v -> Objects.equals(v, b2.location()) ? newLoc : v, Location.class)); + } + + @Override + public void testReplaceChildren() { + SubstringFunctionProcessorDefinition b = randomInstance(); + ProcessorDefinition newSource = toProcessorDefinition((Expression) randomValueOtherThan(b.source(), () -> randomStringLiteral())); + ProcessorDefinition newStart = toProcessorDefinition((Expression) randomValueOtherThan(b.start(), () -> randomIntLiteral())); + ProcessorDefinition newLength = toProcessorDefinition((Expression) randomValueOtherThan(b.length(), () -> randomIntLiteral())); + SubstringFunctionProcessorDefinition newB = + new SubstringFunctionProcessorDefinition(b.location(), b.expression(), b.source(), b.start(), b.length()); + SubstringFunctionProcessorDefinition transformed = null; + + // generate all the combinations of possible children modifications and test all of them + for(int i = 1; i < 4; i++) { + for(BitSet comb : new Combinations(3, i)) { + transformed = (SubstringFunctionProcessorDefinition) newB.replaceChildren( + comb.get(0) ? newSource : b.source(), + comb.get(1) ? newStart : b.start(), + comb.get(2) ? newLength : b.length()); + assertEquals(transformed.source(), comb.get(0) ? newSource : b.source()); + assertEquals(transformed.start(), comb.get(1) ? newStart : b.start()); + assertEquals(transformed.length(), comb.get(2) ? newLength : b.length()); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.location(), b.location()); + } + } + } + + @Override + protected SubstringFunctionProcessorDefinition mutate(SubstringFunctionProcessorDefinition instance) { + List> randoms = new ArrayList<>(); + + for(int i = 1; i < 4; i++) { + for(BitSet comb : new Combinations(3, i)) { + randoms.add(f -> new SubstringFunctionProcessorDefinition( + f.location(), + f.expression(), + comb.get(0) ? toProcessorDefinition((Expression) randomValueOtherThan(f.source(), + () -> randomStringLiteral())) : f.source(), + comb.get(1) ? toProcessorDefinition((Expression) randomValueOtherThan(f.start(), + () -> randomIntLiteral())) : f.start(), + comb.get(2) ? toProcessorDefinition((Expression) randomValueOtherThan(f.length(), + () -> randomIntLiteral())): f.length())); + } + } + + return randomFrom(randoms).apply(instance); + } + + @Override + protected SubstringFunctionProcessorDefinition copy(SubstringFunctionProcessorDefinition instance) { + return new SubstringFunctionProcessorDefinition(instance.location(), + instance.expression(), + instance.source(), + instance.start(), + instance.length()); + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/SubstringProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/SubstringProcessorTests.java new file mode 100644 index 0000000000000..8afc63a05dac4 --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/SubstringProcessorTests.java @@ -0,0 +1,75 @@ +/* + * 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.sql.expression.function.scalar.string; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.Writeable.Reader; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.scalar.Processors; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.ConstantProcessor; + +import static org.elasticsearch.xpack.sql.tree.Location.EMPTY; +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.l; + +public class SubstringProcessorTests extends AbstractWireSerializingTestCase { + + @Override + protected SubstringFunctionProcessor createTestInstance() { + return new SubstringFunctionProcessor( + new ConstantProcessor(randomRealisticUnicodeOfLengthBetween(0, 256)), + new ConstantProcessor(randomInt(256)), + new ConstantProcessor(randomInt(256))); + } + + @Override + protected Reader instanceReader() { + return SubstringFunctionProcessor::new; + } + + @Override + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(Processors.getNamedWriteables()); + } + + public void testSubstringFunctionWithValidInput() { + assertEquals("bar", new Substring(EMPTY, l("foobarbar"), l(4), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foo", new Substring(EMPTY, l("foobarbar"), l(1), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("baz", new Substring(EMPTY, l("foobarbaz"), l(7), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("f", new Substring(EMPTY, l('f'), l(1), l(1)).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testSubstringFunctionWithEdgeCases() { + assertEquals("foobarbar", + new Substring(EMPTY, l("foobarbar"), l(1), l(null)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("foobarbar", + new Substring(EMPTY, l("foobarbar"), l(null), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertNull(new Substring(EMPTY, l(null), l(1), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertNull(new Substring(EMPTY, l(null), l(null), l(null)).makeProcessorDefinition().asProcessor().process(null)); + + assertEquals("foo", new Substring(EMPTY, l("foobarbar"), l(-5), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("barbar", new Substring(EMPTY, l("foobarbar"), l(4), l(30)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("r", new Substring(EMPTY, l("foobarbar"), l(9), l(1)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("", new Substring(EMPTY, l("foobarbar"), l(10), l(1)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("", new Substring(EMPTY, l("foobarbar"), l(123), l(3)).makeProcessorDefinition().asProcessor().process(null)); + } + + public void testSubstringFunctionInputsValidation() { + SqlIllegalArgumentException siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Substring(EMPTY, l(5), l(1), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A string/char is required; received [5]", siae.getMessage()); + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Substring(EMPTY, l("foobarbar"), l(1), l("baz")).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A number is required; received [baz]", siae.getMessage()); + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Substring(EMPTY, l("foobarbar"), l("bar"), l(3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A number is required; received [bar]", siae.getMessage()); + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new Substring(EMPTY, l("foobarbar"), l(1), l(-3)).makeProcessorDefinition().asProcessor().process(null)); + assertEquals("A positive number is required for [length]; received [-3]", siae.getMessage()); + } +} diff --git a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java index 32d1a67e5620f..f5b9381c54b31 100644 --- a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java +++ b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java @@ -1,5 +1,5 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +* 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. */ @@ -51,6 +51,8 @@ public void testShowFunctionsLikePrefix() throws IOException { assertThat(readLine(), RegexMatcher.matches("\\s*LCASE\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*LENGTH\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*LTRIM\\s*\\|\\s*SCALAR\\s*")); + assertThat(readLine(), RegexMatcher.matches("\\s*LEFT\\s*\\|\\s*SCALAR\\s*")); + assertThat(readLine(), RegexMatcher.matches("\\s*LOCATE\\s*\\|\\s*SCALAR\\s*")); assertEquals("", readLine()); } diff --git a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/CsvTestUtils.java b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/CsvTestUtils.java index ad26db3104758..abf84dc7311d8 100644 --- a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/CsvTestUtils.java +++ b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/CsvTestUtils.java @@ -145,6 +145,8 @@ private static String resolveColumnType(String type) { return "double"; case "ts": return "timestamp"; + case "bt": + return "byte"; default: return type; } diff --git a/x-pack/qa/sql/src/main/resources/command.csv-spec b/x-pack/qa/sql/src/main/resources/command.csv-spec index 47615a4c3ae84..89e86e887e140 100644 --- a/x-pack/qa/sql/src/main/resources/command.csv-spec +++ b/x-pack/qa/sql/src/main/resources/command.csv-spec @@ -78,6 +78,15 @@ LENGTH |SCALAR LTRIM |SCALAR RTRIM |SCALAR SPACE |SCALAR +CONCAT |SCALAR +INSERT |SCALAR +LEFT |SCALAR +LOCATE |SCALAR +POSITION |SCALAR +REPEAT |SCALAR +REPLACE |SCALAR +RIGHT |SCALAR +SUBSTRING |SCALAR UCASE |SCALAR SCORE |SCORE ; diff --git a/x-pack/qa/sql/src/main/resources/docs.csv-spec b/x-pack/qa/sql/src/main/resources/docs.csv-spec index 8d7debee331fc..7385bf14df33b 100644 --- a/x-pack/qa/sql/src/main/resources/docs.csv-spec +++ b/x-pack/qa/sql/src/main/resources/docs.csv-spec @@ -232,6 +232,15 @@ LTRIM |SCALAR RTRIM |SCALAR SPACE |SCALAR UCASE |SCALAR +CONCAT |SCALAR +INSERT |SCALAR +LEFT |SCALAR +LOCATE |SCALAR +POSITION |SCALAR +REPEAT |SCALAR +REPLACE |SCALAR +RIGHT |SCALAR +SUBSTRING |SCALAR SCORE |SCORE // end::showFunctions diff --git a/x-pack/qa/sql/src/main/resources/example.csv-spec b/x-pack/qa/sql/src/main/resources/example.csv-spec index a7964cec8d238..77e9947af9369 100644 --- a/x-pack/qa/sql/src/main/resources/example.csv-spec +++ b/x-pack/qa/sql/src/main/resources/example.csv-spec @@ -12,7 +12,7 @@ SELECT COUNT(*) FROM "emp"; // list of // type might be missing in which case it will be autodetected or can be one of the following -// d - double, f - float, i - int, b - byte, l - long, t - timestamp, date +// d - double, f - float, i - int, bt - byte, b - boolean, l - long, t - timestamp, date A,B:d,C:i // actual values foo,2.5,3 diff --git a/x-pack/qa/sql/src/main/resources/functions.csv-spec b/x-pack/qa/sql/src/main/resources/functions.csv-spec index 09320c3d384c0..1a610aec04861 100644 --- a/x-pack/qa/sql/src/main/resources/functions.csv-spec +++ b/x-pack/qa/sql/src/main/resources/functions.csv-spec @@ -28,3 +28,382 @@ len:i | first_name:s 72 |Prasadram 88 |Sreekrishna ; + +selectConcatWithOrderBy +SELECT first_name f, last_name l, CONCAT(first_name,last_name) cct FROM test_emp ORDER BY CONCAT(first_name,last_name) LIMIT 10; + +f:s | l:s | cct:s +Adamantios | Portugali | AdamantiosPortugali +Alain | Chappelet | AlainChappelet +Alejandro | Brender | AlejandroBrender +Alejandro | McAlpine | AlejandroMcAlpine +Amabile | Gomatam | AmabileGomatam +Anneke | Preusig | AnnekePreusig +Anoosh | Peyn | AnooshPeyn +Arif | Merlo | ArifMerlo +Arumugam | Ossenbruggen | ArumugamOssenbruggen +Bader | Swan | BaderSwan +; + +selectNestedConcatWithOrderBy +SELECT first_name f, last_name l, CONCAT(first_name,CONCAT(' ',last_name)) cct FROM test_emp ORDER BY CONCAT(first_name,CONCAT(' ',last_name)) LIMIT 10; + +f:s | l:s | cct:s +Adamantios | Portugali | Adamantios Portugali +Alain | Chappelet | Alain Chappelet +Alejandro | Brender | Alejandro Brender +Alejandro | McAlpine | Alejandro McAlpine +Amabile | Gomatam | Amabile Gomatam +Anneke | Preusig | Anneke Preusig +Anoosh | Peyn | Anoosh Peyn +Arif | Merlo | Arif Merlo +Arumugam | Ossenbruggen | Arumugam Ossenbruggen +Bader | Swan | Bader Swan +; + +selectConcatWithGroupBy +SELECT CONCAT(first_name,last_name) cct FROM test_emp GROUP BY CONCAT(first_name,last_name) ORDER BY CONCAT(first_name,last_name) LIMIT 1; + +cct:s +AdamantiosPortugali +; + +selectAsciiOfConcatWithGroupByOrderByCount +SELECT ASCII(CONCAT("first_name","last_name")) ascii, COUNT(*) count FROM "test_emp" GROUP BY ASCII(CONCAT("first_name","last_name")) ORDER BY ASCII(CONCAT("first_name","last_name")) DESC LIMIT 10; + +ascii:i | count:l +90 | 2 +89 | 3 +88 | 1 +87 | 1 +86 | 3 +85 | 2 +84 | 3 +83 | 11 +82 | 3 +80 | 6 +; + +selectRepeatTwice +SELECT "first_name" orig, REPEAT("first_name",2) reps FROM "test_emp" WHERE ASCII("first_name")=65 ORDER BY REPEAT("first_name",2) ASC LIMIT 10; + +orig:s | reps:s +Adamantios | AdamantiosAdamantios +Alain | AlainAlain +Alejandro | AlejandroAlejandro +Alejandro | AlejandroAlejandro +Amabile | AmabileAmabile +Anneke | AnnekeAnneke +Anoosh | AnooshAnoosh +Arif | ArifArif +Arumugam | ArumugamArumugam +; + +selectInsertWithLcase +SELECT "first_name" orig, INSERT("first_name",2,1000,LCASE("first_name")) modified FROM "test_emp" WHERE ASCII("first_name")=65 ORDER BY "first_name" ASC LIMIT 10; + +orig:s | modified:s +Adamantios | Aadamantios +Alain | Aalain +Alejandro | Aalejandro +Alejandro | Aalejandro +Amabile | Aamabile +Anneke | Aanneke +Anoosh | Aanoosh +Arif | Aarif +Arumugam | Aarumugam +; + +selectInsertWithLcaseAndLengthWithOrderBy +SELECT "first_name" origFN, "last_name" origLN, INSERT(UCASE("first_name"),LENGTH("first_name")+1,123,LCASE("last_name")) modified FROM "test_emp" WHERE ASCII("first_name")=65 ORDER BY "first_name" ASC LIMIT 10; + +origFN:s | origLN:s | modified:s +Adamantios | Portugali | ADAMANTIOSportugali +Alain | Chappelet | ALAINchappelet +Alejandro | Brender | ALEJANDRObrender +Alejandro | McAlpine | ALEJANDROmcalpine +Amabile | Gomatam | AMABILEgomatam +Anneke | Preusig | ANNEKEpreusig +Anoosh | Peyn | ANOOSHpeyn +Arif | Merlo | ARIFmerlo +Arumugam | Ossenbruggen | ARUMUGAMossenbruggen +; + +selectInsertWithUcaseWithGroupByAndOrderBy +SELECT INSERT(UCASE("first_name"),2,123000,INSERT(UCASE("last_name"),2,500,'')) modified, COUNT(*) count FROM "test_emp" WHERE ASCII("first_name")=65 GROUP BY INSERT(UCASE("first_name"),2,123000,INSERT(UCASE("last_name"),2,500,'')) ORDER BY INSERT(UCASE("first_name"),2,123000,INSERT(UCASE("last_name"),2,500,'')) ASC LIMIT 10; + +modified:s | count:l +AB | 1 +AC | 1 +AG | 1 +AM | 2 +AO | 1 +AP | 3 +; + +selectSubstringWithGroupBy +SELECT SUBSTRING("first_name",1,2) modified, COUNT(*) count FROM "test_emp" WHERE ASCII("first_name")=65 GROUP BY SUBSTRING("first_name",1,2) ORDER BY SUBSTRING("first_name",1,2) ASC LIMIT 10; + +modified:s | count:l +Ad | 1 +Al | 3 +Am | 1 +An | 2 +Ar | 2 +; + +selectSubstringWithWhereCountAndGroupBy +SELECT SUBSTRING("first_name",1,2) modified, COUNT(*) count FROM "test_emp" WHERE SUBSTRING("first_name",1,2)='Al' GROUP BY SUBSTRING("first_name",1,2) LIMIT 10; + +modified:s | count:l +Al | 3 +; + +//this one doesn't return anything. The problem is "IS NOT NULL". GH issue created to generally investigate the NULLs usage: https://github.com/elastic/elasticsearch/issues/32079 +//selectSubstringWithWhereNotNullAndCountGroupBy +//SELECT SUBSTRING("first_name",5,20) modified, COUNT(*) count FROM "test_emp" WHERE SUBSTRING("first_name",5,20) IS NOT NULL GROUP BY SUBSTRING("first_name",5,20) ORDER BY SUBSTRING("first_name",5,20) LIMIT 10; + +// modified:s | count:l +//---------------+--------------- +// |15 +//adram |1 +//af |1 +//aja |1 +//al |1 +//andro |2 +//antios |1 +//ard |1 +//areta |1 +//arsan |1 +//; + +selectSubstringWithWhereAndGroupBy +SELECT SUBSTRING("first_name",5,20) modified, COUNT(*) count FROM "test_emp" GROUP BY SUBSTRING("first_name",5,20) ORDER BY SUBSTRING("first_name",5,20) LIMIT 10; + + modified:s | count:l +---------------+--------------- + |15 +adram |1 +af |1 +aja |1 +al |1 +andro |2 +antios |1 +ard |1 +areta |1 +arsan |1 +; + +selectReplace +SELECT REPLACE("first_name",'A','X') modified, "first_name" origFN FROM "test_emp" ORDER BY "first_name" LIMIT 10; + + modified:s | origFN:s +---------------+--------------- +Xdamantios |Adamantios +Xlain |Alain +Xlejandro |Alejandro +Xlejandro |Alejandro +Xmabile |Amabile +Xnneke |Anneke +Xnoosh |Anoosh +Xrif |Arif +Xrumugam |Arumugam +Bader |Bader +; + +selectReplaceWithGroupBy +SELECT REPLACE("first_name",'jan','_JAN_') modified, COUNT(*) count FROM "test_emp" GROUP BY REPLACE("first_name",'jan','_JAN_') ORDER BY REPLACE("first_name",'jan','_JAN_') LIMIT 10; + + modified:s | count:l +---------------+--------------- +Adamantios |1 +Alain |1 +Ale_JAN_dro |2 +Amabile |1 +Anneke |1 +Anoosh |1 +Arif |1 +Arumugam |1 +Bader |1 +Basil |1 +; + +selectReplaceWithCastAndCondition +SELECT REPLACE(CAST("languages" AS VARCHAR),'1','100') foo, "languages" FROM "test_emp" WHERE "languages"=1 OR "languages"=2 LIMIT 5; + + foo:s | languages:bt +---------------+--------------- +2 |2 +100 |1 +2 |2 +100 |1 +100 |1 +; + +selectPositionWithConditionAndLcase +SELECT POSITION('x',LCASE("first_name")) pos, "first_name" FROM "test_emp" WHERE POSITION('x',LCASE("first_name")) != 0; + + pos:i | first_name:s +---------------+--------------- +4 |Guoxiang +1 |Xinglin +; + +selectPositionWithLcaseAndConditionWithGroupByAndOrderBy +SELECT POSITION('m',LCASE("first_name")), COUNT(*) pos FROM "test_emp" WHERE POSITION('m',LCASE("first_name")) != 0 GROUP BY POSITION('m',LCASE("first_name")) ORDER BY POSITION('m',LCASE("first_name")) DESC; + +POSITION(m,LCASE(first_name)):i| pos:l +-------------------------------+--------------- +9 |1 +7 |1 +4 |3 +3 |6 +2 |1 +1 |9 +; + +selectInsertWithPositionAndCondition +SELECT INSERT("first_name",POSITION('m',"first_name"),1,'M') modified, POSITION('m',"first_name") pos FROM "test_emp" WHERE POSITION('m',"first_name") > 1; + + modified:s | pos:i +---------------+--------------- +SuMant |3 +RaMzi |3 +PrasadraM |9 +DoMenick |3 +OtMar |3 +AdaMantios |4 +HidefuMi |7 +MayuMi |5 +PreMal |4 +SoMnath |3 +AMabile |2 +AruMugam |4 +ReMzi |3 +; + +selectLocateAndInsertWithLocateWithConditionAndThreeParameters +SELECT LOCATE('a',"first_name",7) pos, INSERT("first_name",LOCATE('a',"first_name",7),1,'AAA') FROM "test_emp" WHERE LOCATE('a',"first_name",7) > 0; + + pos:i |INSERT(first_name,LOCATE(a,first_name,7),1,AAA):s +---------------+----------------------------------------------- +8 |ChirstiAAAn +7 |DuangkAAAew +8 |PrasadrAAAm +7 |YongqiAAAo +7 |YinghuAAA +8 |BreanndAAA +9 |MargaretAAA +8 |SudharsAAAn +7 |SailajAAA +7 |ArumugAAAm +11 |SreekrishnAAA +; + +selectLocateAndInsertWithLocateWithConditionAndTwoParameters +SELECT LOCATE('a',"first_name") pos, INSERT("first_name",LOCATE('a',"first_name"),1,'AAA') FROM "test_emp" WHERE LOCATE('a',"first_name") > 0 ORDER BY "first_name" LIMIT 10; + + pos:i |INSERT(first_name,LOCATE(a,first_name),1,AAA):s +---------------+--------------------------------------------- +3 |AdAAAmantios +3 |AlAAAin +5 |AlejAAAndro +5 |AlejAAAndro +3 |AmAAAbile +7 |ArumugAAAm +2 |BAAAder +2 |BAAAsil +5 |BerhAAArd +4 |BezAAAlel +; + +selectLeft +SELECT LEFT("first_name",2) f FROM "test_emp" ORDER BY "first_name" LIMIT 10; + +f:s +Ad +Al +Al +Al +Am +An +An +Ar +Ar +Ba +; + +selectRight +SELECT RIGHT("first_name",2) f FROM "test_emp" ORDER BY "first_name" LIMIT 10; + +f:s +os +in +ro +ro +le +ke +sh +if +am +er +; + +selectRightWithGroupByAndOrderBy +SELECT RIGHT("first_name",2) f, COUNT(*) count FROM "test_emp" GROUP BY RIGHT("first_name",2) ORDER BY RIGHT("first_name",2) LIMIT 10; + + f:s |count:l +---------------+--------------------------------------------- +af |1 +al |2 +am |2 +an |7 +ao |3 +ar |2 +ay |1 +be |1 +bu |1 +by |1 +; + +upperCasingTheSecondLetterFromTheRightFromFirstName +SELECT CONCAT(CONCAT(SUBSTRING("first_name",1,LENGTH("first_name")-2),UCASE(LEFT(RIGHT("first_name",2),1))),RIGHT("first_name",1)) f FROM "test_emp" ORDER BY "first_name" LIMIT 10; + + +f:s +AdamantiOs +AlaIn +AlejandRo +AlejandRo +AmabiLe +AnneKe +AnooSh +ArIf +ArumugAm +BadEr +; + +upperCasingTheSecondLetterFromTheRightFromFirstNameWithOrderByAndGroupBy +SELECT CONCAT(CONCAT(SUBSTRING("first_name",1,LENGTH("first_name")-2),UCASE(LEFT(RIGHT("first_name",2),1))),RIGHT("first_name",1)) f, COUNT(*) c FROM "test_emp" GROUP BY CONCAT(CONCAT(SUBSTRING("first_name",1,LENGTH("first_name")-2),UCASE(LEFT(RIGHT("first_name",2),1))),RIGHT("first_name",1)) ORDER BY CONCAT(CONCAT(SUBSTRING("first_name",1,LENGTH("first_name")-2),UCASE(LEFT(RIGHT("first_name",2),1))),RIGHT("first_name",1)) LIMIT 10; + + f:s |c:l +---------------+--------------------------------------------- +AdamantiOs |1 +AlaIn |1 +AlejandRo |2 +AmabiLe |1 +AnneKe |1 +AnooSh |1 +ArIf |1 +ArumugAm |1 +BadEr |1 +BasIl |1 +; + +upperCasingTheSecondLetterFromTheRightFromFirstNameWithWhere +SELECT CONCAT(CONCAT(SUBSTRING("first_name",1,LENGTH("first_name")-2),UCASE(LEFT(RIGHT("first_name",2),1))),RIGHT("first_name",1)) f, COUNT(*) c FROM "test_emp" WHERE CONCAT(CONCAT(SUBSTRING("first_name",1,LENGTH("first_name")-2),UCASE(LEFT(RIGHT("first_name",2),1))),RIGHT("first_name",1))='AlejandRo' GROUP BY CONCAT(CONCAT(SUBSTRING("first_name",1,LENGTH("first_name")-2),UCASE(LEFT(RIGHT("first_name",2),1))),RIGHT("first_name",1)) ORDER BY CONCAT(CONCAT(SUBSTRING("first_name",1,LENGTH("first_name")-2),UCASE(LEFT(RIGHT("first_name",2),1))),RIGHT("first_name",1)) LIMIT 10; + + f:s |c:l +---------------+--------------------------------------------- +AlejandRo |2 +; diff --git a/x-pack/qa/sql/src/main/resources/string-functions.sql-spec b/x-pack/qa/sql/src/main/resources/string-functions.sql-spec index d9a35edf1b030..5820d1f171472 100644 --- a/x-pack/qa/sql/src/main/resources/string-functions.sql-spec +++ b/x-pack/qa/sql/src/main/resources/string-functions.sql-spec @@ -74,3 +74,78 @@ SELECT SPACE("languages") s, COUNT(*) count FROM "test_emp" GROUP BY SPACE("lang spaceGroupByAndOrderByWithCharLength SELECT SPACE("languages") s, COUNT(*) count, CAST(CHAR_LENGTH(SPACE("languages")) AS INT) cls FROM "test_emp" WHERE "languages" IS NOT NULL GROUP BY SPACE("languages") ORDER BY SPACE("languages"); + +selectConcatWithOrderBy +SELECT first_name f, last_name l, CONCAT(first_name,last_name) cct FROM test_emp ORDER BY CONCAT(first_name,last_name) LIMIT 10; + +selectNestedConcatWithOrderBy +SELECT first_name f, last_name l, CONCAT(first_name,CONCAT(' ',last_name)) cct FROM test_emp ORDER BY CONCAT(first_name,CONCAT(' ',last_name)) LIMIT 10; + +selectConcatWithGroupBy +SELECT CONCAT(first_name,last_name) cct FROM test_emp GROUP BY CONCAT(first_name,last_name) ORDER BY CONCAT(first_name,last_name) LIMIT 1; + +selectAsciiOfConcatWithGroupByOrderByCount +SELECT ASCII(CONCAT("first_name","last_name")) ascii, COUNT(*) count FROM "test_emp" GROUP BY ASCII(CONCAT("first_name","last_name")) ORDER BY ASCII(CONCAT("first_name","last_name")) DESC LIMIT 10; + +selectRepeatTwice +SELECT "first_name" orig, REPEAT("first_name",2) reps FROM "test_emp" WHERE ASCII("first_name")=65 ORDER BY REPEAT("first_name",2) ASC LIMIT 10; + +selectInsertWithLcase +SELECT "first_name" orig, INSERT("first_name",2,1000,LCASE("first_name")) modified FROM "test_emp" WHERE ASCII("first_name")=65 ORDER BY "first_name" ASC LIMIT 10; + +selectInsertWithLcaseAndLengthWithOrderBy +SELECT "first_name" origFN, "last_name" origLN, INSERT(UCASE("first_name"),LENGTH("first_name")+1,123,LCASE("last_name")) modified FROM "test_emp" WHERE ASCII("first_name")=65 ORDER BY "first_name" ASC, "last_name" ASC LIMIT 10; + +selectInsertWithUcaseWithGroupByAndOrderBy +SELECT INSERT(UCASE("first_name"),2,123000,INSERT(UCASE("last_name"),2,500,' ')) modified, COUNT(*) count FROM "test_emp" WHERE ASCII("first_name")=65 GROUP BY INSERT(UCASE("first_name"),2,123000,INSERT(UCASE("last_name"),2,500,' ')) ORDER BY INSERT(UCASE("first_name"),2,123000,INSERT(UCASE("last_name"),2,500,' ')) ASC LIMIT 10; + +selectSubstringWithGroupBy +SELECT SUBSTRING("first_name",1,2) modified, COUNT(*) count FROM "test_emp" WHERE ASCII("first_name")=65 GROUP BY SUBSTRING("first_name",1,2) ORDER BY SUBSTRING("first_name",1,2) ASC LIMIT 10; + +selectSubstringWithWhereCountAndGroupBy +SELECT SUBSTRING("first_name",1,2) modified, COUNT(*) count FROM "test_emp" WHERE SUBSTRING("first_name",1,2)='Al' GROUP BY SUBSTRING("first_name",1,2) LIMIT 10; + +selectSubstringWithWhereAndGroupBy +SELECT SUBSTRING("first_name",5,20) modified, COUNT(*) count FROM "test_emp" GROUP BY SUBSTRING("first_name",5,20) ORDER BY SUBSTRING("first_name",5,20) LIMIT 10; + +selectReplace +SELECT REPLACE("first_name",'A','X') modified, "first_name" origFN FROM "test_emp" ORDER BY "first_name" LIMIT 10; + +selectReplaceWithGroupBy +SELECT REPLACE("first_name",'jan','_JAN_') modified, COUNT(*) count FROM "test_emp" GROUP BY REPLACE("first_name",'jan','_JAN_') ORDER BY REPLACE("first_name",'jan','_JAN_') LIMIT 10; + +selectReplaceWithCastAndCondition +SELECT REPLACE(CAST("languages" AS VARCHAR),'1','100') foo, "languages" FROM "test_emp" WHERE "languages"=1 OR "languages"=2 LIMIT 5; + +selectPositionWithConditionAndLcase +SELECT POSITION('x',LCASE("first_name")) pos, "first_name" FROM "test_emp" WHERE POSITION('x',LCASE("first_name")) != 0; + +selectPositionWithLcaseAndConditionWithGroupByAndOrderBy +SELECT POSITION('m',LCASE("first_name")) posOfM, COUNT(*) pos FROM "test_emp" WHERE POSITION('m',LCASE("first_name")) != 0 GROUP BY POSITION('m',LCASE("first_name")) ORDER BY POSITION('m',LCASE("first_name")) DESC; + +selectInsertWithPositionAndCondition +SELECT INSERT("first_name",POSITION('m',"first_name"),1,'M') modified, POSITION('m',"first_name") pos FROM "test_emp" WHERE POSITION('m',"first_name") > 1; + +selectLocateAndInsertWithLocateWithConditionAndThreeParameters +SELECT LOCATE('a',"first_name",7) pos, INSERT("first_name",LOCATE('a',"first_name",7),1,'AAA') inserted FROM "test_emp" WHERE LOCATE('a',"first_name",7) > 0; + +selectLocateAndInsertWithLocateWithConditionAndTwoParameters +SELECT LOCATE('a',"first_name") pos, INSERT("first_name",LOCATE('a',"first_name"),1,'AAA') inserted FROM "test_emp" WHERE LOCATE('a',"first_name") > 0 ORDER BY "first_name" LIMIT 10; + +selectLeft +SELECT LEFT("first_name",2) f FROM "test_emp" ORDER BY "first_name" LIMIT 10; + +selectRight +SELECT RIGHT("first_name",2) f FROM "test_emp" ORDER BY "first_name" LIMIT 10; + +selectRightWithGroupByAndOrderBy +SELECT RIGHT("first_name",2) f, COUNT(*) count FROM "test_emp" GROUP BY RIGHT("first_name",2) ORDER BY RIGHT("first_name",2) LIMIT 10; + +upperCasingTheSecondLetterFromTheRightFromFirstName +SELECT CONCAT(CONCAT(SUBSTRING("first_name",1,LENGTH("first_name")-2),UCASE(LEFT(RIGHT("first_name",2),1))),RIGHT("first_name",1)) f FROM "test_emp" ORDER BY "first_name" LIMIT 10; + +upperCasingTheSecondLetterFromTheRightFromFirstNameWithOrderByAndGroupBy +SELECT CONCAT(CONCAT(SUBSTRING("first_name",1,LENGTH("first_name")-2),UCASE(LEFT(RIGHT("first_name",2),1))),RIGHT("first_name",1)) f, COUNT(*) c FROM "test_emp" GROUP BY CONCAT(CONCAT(SUBSTRING("first_name",1,LENGTH("first_name")-2),UCASE(LEFT(RIGHT("first_name",2),1))),RIGHT("first_name",1)) ORDER BY CONCAT(CONCAT(SUBSTRING("first_name",1,LENGTH("first_name")-2),UCASE(LEFT(RIGHT("first_name",2),1))),RIGHT("first_name",1)) LIMIT 10; + +upperCasingTheSecondLetterFromTheRightFromFirstNameWithWhere +SELECT CONCAT(CONCAT(SUBSTRING("first_name",1,LENGTH("first_name")-2),UCASE(LEFT(RIGHT("first_name",2),1))),RIGHT("first_name",1)) f, COUNT(*) c FROM "test_emp" WHERE CONCAT(CONCAT(SUBSTRING("first_name",1,LENGTH("first_name")-2),UCASE(LEFT(RIGHT("first_name",2),1))),RIGHT("first_name",1))='AlejandRo' GROUP BY CONCAT(CONCAT(SUBSTRING("first_name",1,LENGTH("first_name")-2),UCASE(LEFT(RIGHT("first_name",2),1))),RIGHT("first_name",1)) ORDER BY CONCAT(CONCAT(SUBSTRING("first_name",1,LENGTH("first_name")-2),UCASE(LEFT(RIGHT("first_name",2),1))),RIGHT("first_name",1)) LIMIT 10;