diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/string.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/string.csv-spec index 063b74584a28b..53d7d1fd0d352 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/string.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/string.csv-spec @@ -839,6 +839,41 @@ emp_no:integer | full_name:keyword | full_name_2:keyword | job_positions:keyword 10005 | Kyoichi Maliniak | Maliniak,Kyoichi | null | [-2.14,13.07] | [-2.14,13.07] ; +mvZipLiteralNullDelim +required_capability: mv_sort + +FROM employees +| EVAL full_name = mv_zip(first_name, last_name, null) +| KEEP emp_no, full_name +| SORT emp_no +| LIMIT 5; + +emp_no:integer | full_name:keyword +10001 | null +10002 | null +10003 | null +10004 | null +10005 | null +; + +mvZipLiteralLongDelim +required_capability: mv_sort + +FROM employees +| EVAL full_name = mv_zip(first_name, last_name, " words words words ") +| KEEP emp_no, full_name +| SORT emp_no +| LIMIT 5; + +emp_no:integer | full_name:keyword +10001 | Georgi words words words Facello +10002 | Bezalel words words words Simmel +10003 | Parto words words words Bamford +10004 | Chirstian words words words Koblick +10005 | Kyoichi words words words Maliniak +; + + showTextFields from hosts | sort description, card, ip0, ip1 | where host == "beta" | keep host, host_group, description; ignoreOrder:true diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvZip.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvZip.java index 6298057b16013..4f42858cbedba 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvZip.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvZip.java @@ -14,6 +14,7 @@ import org.elasticsearch.compute.operator.EvalOperator; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.Literal; +import org.elasticsearch.xpack.esql.core.expression.Nullability; import org.elasticsearch.xpack.esql.core.expression.function.OptionalArgument; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; @@ -93,6 +94,12 @@ public boolean foldable() { return mvLeft.foldable() && mvRight.foldable() && (delim == null || delim.foldable()); } + @Override + public Nullability nullable() { + // Nullability.TRUE means if *any* parameter is null we return null. We're only null if the first two are null. + return Nullability.FALSE; + } + @Override public EvalOperator.ExpressionEvaluator.Factory toEvaluator( Function toEvaluator diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractScalarFunctionTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractScalarFunctionTestCase.java deleted file mode 100644 index c8fe9e536beea..0000000000000 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractScalarFunctionTestCase.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.esql.expression.function.scalar; - -import org.elasticsearch.xpack.esql.core.expression.Expression; -import org.elasticsearch.xpack.esql.core.expression.Literal; -import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; -import org.elasticsearch.xpack.esql.core.tree.Location; -import org.elasticsearch.xpack.esql.core.tree.Source; -import org.elasticsearch.xpack.esql.core.type.DataType; -import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase; -import org.elasticsearch.xpack.esql.type.EsqlDataTypes; -import org.hamcrest.Matcher; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.stream.Stream; - -import static org.hamcrest.Matchers.equalTo; - -/** - * Base class for function tests. - * @deprecated extends from {@link AbstractFunctionTestCase} instead - * and {@link AbstractFunctionTestCase#errorsForCasesWithoutExamples}. - */ -@Deprecated -public abstract class AbstractScalarFunctionTestCase extends AbstractFunctionTestCase { - /** - * Describe supported arguments. Build each argument with - * {@link #required} or {@link #optional}. - */ - protected abstract List argSpec(); - - /** - * The data type that applying this function to arguments of this type should produce. - */ - protected abstract DataType expectedType(List argTypes); - - /** - * Define a required argument. - */ - protected final ArgumentSpec required(DataType... validTypes) { - return new ArgumentSpec(false, withNullAndSorted(validTypes)); - } - - /** - * Define an optional argument. - */ - protected final ArgumentSpec optional(DataType... validTypes) { - return new ArgumentSpec(true, withNullAndSorted(validTypes)); - } - - private Set withNullAndSorted(DataType[] validTypes) { - Set realValidTypes = new LinkedHashSet<>(); - Arrays.stream(validTypes).sorted(Comparator.comparing(DataType::nameUpper)).forEach(realValidTypes::add); - realValidTypes.add(DataType.NULL); - return realValidTypes; - } - - public Set sortedTypesSet(DataType[] validTypes, DataType... additionalTypes) { - Set mergedSet = new LinkedHashSet<>(); - Stream.concat(Stream.of(validTypes), Stream.of(additionalTypes)) - .sorted(Comparator.comparing(DataType::nameUpper)) - .forEach(mergedSet::add); - return mergedSet; - } - - /** - * All integer types (long, int, short, byte). For passing to {@link #required} or {@link #optional}. - */ - protected static DataType[] integers() { - return DataType.types().stream().filter(DataType::isInteger).toArray(DataType[]::new); - } - - /** - * All rational types (double, float, whatever). For passing to {@link #required} or {@link #optional}. - */ - protected static DataType[] rationals() { - return DataType.types().stream().filter(DataType::isRational).toArray(DataType[]::new); - } - - /** - * All numeric types (integers and rationals.) For passing to {@link #required} or {@link #optional}. - */ - protected static DataType[] numerics() { - return DataType.types().stream().filter(DataType::isNumeric).toArray(DataType[]::new); - } - - protected final DataType[] representableNumerics() { - // TODO numeric should only include representable numbers but that is a change for a followup - return DataType.types().stream().filter(DataType::isNumeric).filter(EsqlDataTypes::isRepresentable).toArray(DataType[]::new); - } - - protected record ArgumentSpec(boolean optional, Set validTypes) {} - - public final void testResolveType() { - List specs = argSpec(); - for (int mutArg = 0; mutArg < specs.size(); mutArg++) { - for (DataType mutArgType : DataType.types()) { - List args = new ArrayList<>(specs.size()); - for (int arg = 0; arg < specs.size(); arg++) { - if (mutArg == arg) { - args.add(new Literal(new Source(Location.EMPTY, "arg" + arg), "", mutArgType)); - } else { - args.add(new Literal(new Source(Location.EMPTY, "arg" + arg), "", specs.get(arg).validTypes.iterator().next())); - } - } - assertResolution(specs, args, mutArg, mutArgType, specs.get(mutArg).validTypes.contains(mutArgType)); - int optionalIdx = specs.size() - 1; - while (optionalIdx > 0 && specs.get(optionalIdx).optional()) { - args.remove(optionalIdx--); - assertResolution( - specs, - args, - mutArg, - mutArgType, - args.size() <= mutArg || specs.get(mutArg).validTypes.contains(mutArgType) - ); - } - } - } - } - - private void assertResolution(List specs, List args, int mutArg, DataType mutArgType, boolean shouldBeValid) { - Expression exp = build(new Source(Location.EMPTY, "exp"), args); - logger.info("checking {} is {}", exp.nodeString(), shouldBeValid ? "valid" : "invalid"); - if (shouldBeValid) { - assertResolveTypeValid(exp, expectedType(args.stream().map(Expression::dataType).toList())); - return; - } - Expression.TypeResolution resolution = exp.typeResolved(); - assertFalse(exp.nodeString(), resolution.resolved()); - assertThat(exp.nodeString(), resolution.message(), badTypeError(specs, mutArg, mutArgType)); - } - - protected Matcher badTypeError(List spec, int badArgPosition, DataType badArgType) { - String ordinal = spec.size() == 1 - ? "" - : TypeResolutions.ParamOrdinal.fromIndex(badArgPosition).name().toLowerCase(Locale.ROOT) + " "; - return equalTo( - ordinal - + "argument of [exp] must be [" - + expectedTypeName(spec.get(badArgPosition).validTypes()) - + "], found value [arg" - + badArgPosition - + "] type [" - + badArgType.typeName() - + "]" - ); - } - - private String expectedTypeName(Set validTypes) { - List withoutNull = validTypes.stream().filter(t -> t != DataType.NULL).toList(); - if (withoutNull.equals(Arrays.asList(strings()))) { - return "string"; - } - if (withoutNull.equals(Arrays.asList(integers())) || withoutNull.equals(List.of(DataType.INTEGER))) { - return "integer"; - } - if (withoutNull.equals(Arrays.asList(rationals()))) { - return "double"; - } - if (withoutNull.equals(Arrays.asList(numerics())) || withoutNull.equals(Arrays.asList(representableNumerics()))) { - return "numeric"; - } - if (withoutNull.equals(List.of(DataType.DATETIME))) { - return "datetime"; - } - if (withoutNull.equals(List.of(DataType.IP))) { - return "ip"; - } - List negations = Stream.concat(Stream.of(numerics()), Stream.of(DataType.DATE_PERIOD, DataType.TIME_DURATION)) - .sorted(Comparator.comparing(DataType::nameUpper)) - .toList(); - if (withoutNull.equals(negations)) { - return "numeric, date_period or time_duration"; - } - if (validTypes.equals(Set.copyOf(Arrays.asList(representableTypes())))) { - return "representable"; - } - if (validTypes.equals(Set.copyOf(Arrays.asList(representableNonSpatialTypes())))) { - return "representableNonSpatial"; - } - throw new IllegalArgumentException("can't guess expected type for " + validTypes); - } -} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/AbsTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/AbsTests.java index 5158fb9aad372..63642a01fa117 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/AbsTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/AbsTests.java @@ -13,8 +13,8 @@ import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; -import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractScalarFunctionTestCase; import java.math.BigInteger; import java.util.ArrayList; @@ -23,7 +23,7 @@ import static org.hamcrest.Matchers.equalTo; -public class AbsTests extends AbstractScalarFunctionTestCase { +public class AbsTests extends AbstractFunctionTestCase { @ParametersFactory public static Iterable parameters() { List suppliers = new ArrayList<>(); @@ -74,14 +74,4 @@ public AbsTests(@Name("TestCase") Supplier testCaseSu protected Expression build(Source source, List args) { return new Abs(source, args.get(0)); } - - @Override - protected List argSpec() { - return List.of(required(numerics())); - } - - @Override - protected DataType expectedType(List argTypes) { - return argTypes.get(0); - } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CeilTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CeilTests.java index f562ccbf0071b..735113c34ca1b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CeilTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/CeilTests.java @@ -13,8 +13,8 @@ import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; -import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractScalarFunctionTestCase; import java.math.BigInteger; import java.util.ArrayList; @@ -23,7 +23,7 @@ import static org.hamcrest.Matchers.equalTo; -public class CeilTests extends AbstractScalarFunctionTestCase { +public class CeilTests extends AbstractFunctionTestCase { public CeilTests(@Name("TestCase") Supplier testCaseSupplier) { this.testCase = testCaseSupplier.get(); } @@ -31,7 +31,7 @@ public CeilTests(@Name("TestCase") Supplier testCaseS @ParametersFactory public static Iterable parameters() { List suppliers = new ArrayList<>(); - suppliers.addAll(List.of(new TestCaseSupplier("large double value", () -> { + suppliers.addAll(List.of(new TestCaseSupplier("large double value", List.of(DataType.DOUBLE), () -> { double arg = 1 / randomDouble(); return new TestCaseSupplier.TestCase( List.of(new TestCaseSupplier.TypedData(arg, DataType.DOUBLE, "arg")), @@ -39,7 +39,7 @@ public static Iterable parameters() { DataType.DOUBLE, equalTo(Math.ceil(arg)) ); - }), new TestCaseSupplier("integer value", () -> { + }), new TestCaseSupplier("integer value", List.of(DataType.INTEGER), () -> { int arg = randomInt(); return new TestCaseSupplier.TestCase( List.of(new TestCaseSupplier.TypedData(arg, DataType.INTEGER, "arg")), @@ -47,7 +47,7 @@ public static Iterable parameters() { DataType.INTEGER, equalTo(arg) ); - }), new TestCaseSupplier("long value", () -> { + }), new TestCaseSupplier("long value", List.of(DataType.LONG), () -> { long arg = randomLong(); return new TestCaseSupplier.TestCase( List.of(new TestCaseSupplier.TypedData(arg, DataType.LONG, "arg")), @@ -66,17 +66,7 @@ public static Iterable parameters() { UNSIGNED_LONG_MAX, List.of() ); - return parameterSuppliersFromTypedData(suppliers); - } - - @Override - protected DataType expectedType(List argTypes) { - return argTypes.get(0); - } - - @Override - protected List argSpec() { - return List.of(required(numerics())); + return parameterSuppliersFromTypedData(errorsForCasesWithoutExamples(anyNullIsNull(false, suppliers))); } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/LogTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/LogTests.java index a25fc66ab2d73..ce53fdbfc1851 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/LogTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/LogTests.java @@ -13,13 +13,13 @@ import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; -import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractScalarFunctionTestCase; import java.util.List; import java.util.function.Supplier; -public class LogTests extends AbstractScalarFunctionTestCase { +public class LogTests extends AbstractFunctionTestCase { public LogTests(@Name("TestCase") Supplier testCaseSupplier) { this.testCase = testCaseSupplier.get(); } @@ -194,16 +194,6 @@ public static Iterable parameters() { return parameterSuppliersFromTypedData(errorsForCasesWithoutExamples(suppliers)); } - @Override - protected DataType expectedType(List argTypes) { - return DataType.DOUBLE; - } - - @Override - protected List argSpec() { - return List.of(optional(numerics()), required(numerics())); - } - @Override protected Expression build(Source source, List args) { return new Log(source, args.get(0), args.size() > 1 ? args.get(1) : null); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/PowTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/PowTests.java index 855e3070d442f..545e7c14ff2b2 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/PowTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/PowTests.java @@ -13,13 +13,13 @@ import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; -import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractScalarFunctionTestCase; import java.util.List; import java.util.function.Supplier; -public class PowTests extends AbstractScalarFunctionTestCase { +public class PowTests extends AbstractFunctionTestCase { public PowTests(@Name("TestCase") Supplier testCaseSupplier) { this.testCase = testCaseSupplier.get(); } @@ -80,16 +80,6 @@ public static Iterable parameters() { return parameterSuppliersFromTypedData(errorsForCasesWithoutExamples(suppliers)); } - @Override - protected DataType expectedType(List argTypes) { - return DataType.DOUBLE; - } - - @Override - protected List argSpec() { - return List.of(required(numerics()), required(numerics())); - } - @Override protected Expression build(Source source, List args) { return new Pow(source, args.get(0), args.get(1)); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvZipTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvZipTests.java index e465d72555e4e..30fe420f29960 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvZipTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/multivalue/MvZipTests.java @@ -15,8 +15,8 @@ import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; -import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractScalarFunctionTestCase; import java.util.ArrayList; import java.util.List; @@ -25,59 +25,79 @@ import static java.lang.Math.max; import static org.hamcrest.Matchers.equalTo; -public class MvZipTests extends AbstractScalarFunctionTestCase { +public class MvZipTests extends AbstractFunctionTestCase { public MvZipTests(@Name("TestCase") Supplier testCaseSupplier) { this.testCase = testCaseSupplier.get(); } @ParametersFactory public static Iterable parameters() { + // Note that any null is *not* null, so we explicitly test with nulls List suppliers = new ArrayList<>(); - suppliers.add(new TestCaseSupplier(List.of(DataType.KEYWORD, DataType.KEYWORD, DataType.KEYWORD), () -> { - List left = randomList(1, 3, () -> randomLiteral(DataType.KEYWORD).value()); - List right = randomList(1, 3, () -> randomLiteral(DataType.KEYWORD).value()); - String delim = randomAlphaOfLengthBetween(1, 1); + for (DataType leftType : DataType.types()) { + if (leftType != DataType.NULL && DataType.isString(leftType) == false) { + continue; + } + for (DataType rightType : DataType.types()) { + if (rightType != DataType.NULL && DataType.isString(rightType) == false) { + continue; + } + for (DataType delimType : DataType.types()) { + if (delimType != DataType.NULL && DataType.isString(delimType) == false) { + continue; + } + suppliers.add(supplier(leftType, rightType, delimType)); + } + suppliers.add(supplier(leftType, rightType)); + } + } + + return parameterSuppliersFromTypedData(errorsForCasesWithoutExamples(suppliers)); + } + + private static TestCaseSupplier supplier(DataType leftType, DataType rightType, DataType delimType) { + return new TestCaseSupplier(List.of(leftType, rightType, delimType), () -> { + List left = randomList(leftType); + List right = randomList(rightType); + BytesRef delim = delimType == DataType.NULL ? null : new BytesRef(randomAlphaOfLength(1)); + List expected = calculateExpected(left, right, delim); return new TestCaseSupplier.TestCase( List.of( - new TestCaseSupplier.TypedData(left, DataType.KEYWORD, "mvLeft"), - new TestCaseSupplier.TypedData(right, DataType.KEYWORD, "mvRight"), - new TestCaseSupplier.TypedData(delim, DataType.KEYWORD, "delim") + new TestCaseSupplier.TypedData(left, leftType, "mvLeft"), + new TestCaseSupplier.TypedData(right, rightType, "mvRight"), + new TestCaseSupplier.TypedData(delim, delimType, "delim") ), "MvZipEvaluator[leftField=Attribute[channel=0], rightField=Attribute[channel=1], delim=Attribute[channel=2]]", DataType.KEYWORD, - equalTo(expected.size() == 1 ? expected.iterator().next() : expected) + equalTo(expected == null ? null : expected.size() == 1 ? expected.iterator().next() : expected) ); - })); + }); + } - suppliers.add(new TestCaseSupplier(List.of(DataType.TEXT, DataType.TEXT, DataType.TEXT), () -> { - List left = randomList(1, 10, () -> randomLiteral(DataType.TEXT).value()); - List right = randomList(1, 10, () -> randomLiteral(DataType.TEXT).value()); - String delim = randomAlphaOfLengthBetween(1, 1); - List expected = calculateExpected(left, right, delim); + private static TestCaseSupplier supplier(DataType leftType, DataType rightType) { + return new TestCaseSupplier(List.of(leftType, rightType), () -> { + List left = randomList(leftType); + List right = randomList(rightType); + + List expected = calculateExpected(left, right, new BytesRef(",")); return new TestCaseSupplier.TestCase( List.of( - new TestCaseSupplier.TypedData(left, DataType.TEXT, "mvLeft"), - new TestCaseSupplier.TypedData(right, DataType.TEXT, "mvRight"), - new TestCaseSupplier.TypedData(delim, DataType.TEXT, "delim") + new TestCaseSupplier.TypedData(left, leftType, "mvLeft"), + new TestCaseSupplier.TypedData(right, rightType, "mvRight") ), - "MvZipEvaluator[leftField=Attribute[channel=0], rightField=Attribute[channel=1], delim=Attribute[channel=2]]", + "MvZipEvaluator[leftField=Attribute[channel=0], rightField=Attribute[channel=1], delim=LiteralsEvaluator[lit=,]]", DataType.KEYWORD, - equalTo(expected.size() == 1 ? expected.iterator().next() : expected) + equalTo(expected == null ? null : expected.size() == 1 ? expected.iterator().next() : expected) ); - })); - - return parameterSuppliersFromTypedData(suppliers); + }); } - @Override - protected DataType expectedType(List argTypes) { - return DataType.KEYWORD; - } - - @Override - protected List argSpec() { - return List.of(required(strings()), required(strings()), optional(strings())); + private static List randomList(DataType type) { + if (type == DataType.NULL) { + return null; + } + return randomList(1, 3, () -> new BytesRef(randomAlphaOfLength(5))); } @Override @@ -85,27 +105,36 @@ protected Expression build(Source source, List args) { return new MvZip(source, args.get(0), args.get(1), args.size() > 2 ? args.get(2) : null); } - private static List calculateExpected(List left, List right, String delim) { + private static List calculateExpected(List left, List right, BytesRef delim) { + if (delim == null) { + return null; + } + if (left == null) { + return right; + } + if (right == null) { + return left; + } List expected = new ArrayList<>(max(left.size(), right.size())); int i = 0, j = 0; while (i < left.size() && j < right.size()) { BytesRefBuilder work = new BytesRefBuilder(); - work.append((BytesRef) left.get(i)); - work.append(new BytesRef(delim)); - work.append((BytesRef) right.get(j)); + work.append(left.get(i)); + work.append(delim); + work.append(right.get(j)); expected.add(work.get()); i++; j++; } while (i < left.size()) { BytesRefBuilder work = new BytesRefBuilder(); - work.append((BytesRef) left.get(i)); + work.append(left.get(i)); expected.add(work.get()); i++; } while (j < right.size()) { BytesRefBuilder work = new BytesRefBuilder(); - work.append((BytesRef) right.get(j)); + work.append(right.get(j)); expected.add(work.get()); j++; }