diff --git a/docs/changelog/130944.yaml b/docs/changelog/130944.yaml new file mode 100644 index 0000000000000..608a69addd149 --- /dev/null +++ b/docs/changelog/130944.yaml @@ -0,0 +1,6 @@ +pr: 130944 +summary: Remove unnecessary calls to Fold +area: ES|QL +type: enhancement +issues: + - 119756 diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/TypeResolutions.java b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/TypeResolutions.java index e9d4aacfe9fa7..f6e081771cbf5 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/TypeResolutions.java +++ b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/TypeResolutions.java @@ -133,36 +133,6 @@ public static TypeResolution isFoldable(Expression e, String operationName, Para return TypeResolution.TYPE_RESOLVED; } - /** - * Is this {@link Expression#foldable()} and not {@code null}. - * - * @deprecated instead of calling this, check for a {@link Literal} containing - * {@code null}. Foldable expressions will be folded by other rules, - * eventually, to a {@link Literal}. - */ - @Deprecated - public static TypeResolution isNotNullAndFoldable(Expression e, String operationName, ParamOrdinal paramOrd) { - TypeResolution resolution = isFoldable(e, operationName, paramOrd); - - if (resolution.unresolved()) { - return resolution; - } - - if (e.dataType() == DataType.NULL || e.fold(FoldContext.small()) == null) { - resolution = new TypeResolution( - format( - null, - "{}argument of [{}] cannot be null, received [{}]", - paramOrd == null || paramOrd == DEFAULT ? "" : paramOrd.name().toLowerCase(Locale.ROOT) + " ", - operationName, - Expressions.name(e) - ) - ); - } - - return resolution; - } - public static TypeResolution isNotNull(Expression e, String operationName, ParamOrdinal paramOrd) { if (e.dataType() == DataType.NULL) { return new TypeResolution( diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/FunctionUtils.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/FunctionUtils.java new file mode 100644 index 0000000000000..bbde965bc927f --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/FunctionUtils.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.esql.expression.function; + +import org.elasticsearch.common.lucene.BytesRefs; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException; +import org.elasticsearch.xpack.esql.common.Failure; +import org.elasticsearch.xpack.esql.common.Failures; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.expression.Literal; + +import static org.elasticsearch.common.logging.LoggerMessageFormat.format; +import static org.elasticsearch.xpack.esql.common.Failure.fail; + +public class FunctionUtils { + /** + * A utility class to validate the type resolution of expressions before and after logical planning. + * If null is passed for Failures to the constructor, it means we are only type resolution. + * This is usually called when doing pre-logical planning validation. + * If a {@link Failures} instance is passed, it means we are doing post-logical planning validation as well. + * This is usually called after folding is done, during + * {@link org.elasticsearch.xpack.esql.capabilities.PostOptimizationVerificationAware} verification + */ + public static class TypeResolutionValidator { + + Expression.TypeResolution typeResolution = Expression.TypeResolution.TYPE_RESOLVED; + @Nullable + private final Failures postValidationFailures; // null means we are doing pre-folding validation only + private final Expression field; + + public static TypeResolutionValidator forPreOptimizationValidation(Expression field) { + return new TypeResolutionValidator(field, null); + } + + public static TypeResolutionValidator forPostOptimizationValidation(Expression field, Failures failures) { + return new TypeResolutionValidator(field, failures); + } + + private TypeResolutionValidator(Expression field, Failures failures) { + this.field = field; + this.postValidationFailures = failures; + } + + public void invalidIfPostValidation(Failure failure) { + if (postValidationFailures != null) { + postValidationFailures.add(failure); + } + } + + public void invalid(Expression.TypeResolution message) { + typeResolution = message; + if (postValidationFailures != null) { + postValidationFailures.add(fail(field, message.message())); + } + } + + public Expression.TypeResolution getResolvedType() { + return typeResolution; + } + } + + public static Integer limitValue(Expression limitField, String sourceText) { + if (limitField instanceof Literal literal) { + Object value = literal.value(); + if (value instanceof Integer intValue) { + return intValue; + } + } + throw new EsqlIllegalArgumentException(format(null, "Limit value must be an integer in [{}], found [{}]", sourceText, limitField)); + } + + /** + * We check that the limit is not null and that if it is a literal, it is a positive integer + * During postOptimizationVerification folding is already done, so we also verify that it is definitively a literal + */ + public static Expression.TypeResolution resolveTypeLimit(Expression limitField, String sourceText, TypeResolutionValidator validator) { + if (limitField == null) { + validator.invalid( + new Expression.TypeResolution(format(null, "Limit must be a constant integer in [{}], found [{}]", sourceText, limitField)) + ); + } else if (limitField instanceof Literal literal) { + if (literal.value() == null) { + validator.invalid( + new Expression.TypeResolution( + format(null, "Limit must be a constant integer in [{}], found [{}]", sourceText, limitField) + ) + ); + } else { + int value = (Integer) literal.value(); + if (value <= 0) { + validator.invalid( + new Expression.TypeResolution(format(null, "Limit must be greater than 0 in [{}], found [{}]", sourceText, value)) + ); + } + } + } else { + // it is expected that the expression is a literal after folding + // we fail if it is not a literal + validator.invalidIfPostValidation( + fail(limitField, "Limit must be a constant integer in [{}], found [{}]", sourceText, limitField) + ); + } + return validator.getResolvedType(); + } + + /** + * We check that the query is not null and that if it is a literal, it is a string + * During postOptimizationVerification folding is already done, so we also verify that it is definitively a literal + */ + public static Expression.TypeResolution resolveTypeQuery(Expression queryField, String sourceText, TypeResolutionValidator validator) { + if (queryField == null) { + validator.invalid( + new Expression.TypeResolution(format(null, "Query must be a valid string in [{}], found [{}]", sourceText, queryField)) + ); + } else if (queryField instanceof Literal literal) { + if (literal.value() == null) { + validator.invalid( + new Expression.TypeResolution(format(null, "Query value cannot be null in [{}], but got [{}]", sourceText, queryField)) + ); + } + } else { + // it is expected that the expression is a literal after folding + // we fail if it is not a literal + validator.invalidIfPostValidation(fail(queryField, "Query must be a valid string in [{}], found [{}]", sourceText, queryField)); + } + return validator.getResolvedType(); + } + + public static Object queryAsObject(Expression queryField, String sourceText) { + if (queryField instanceof Literal literal) { + return literal.value(); + } + throw new EsqlIllegalArgumentException( + format(null, "Query value must be a constant string in [{}], found [{}]", sourceText, queryField) + ); + } + + public static String queryAsString(Expression queryField, String sourceText) { + if (queryField instanceof Literal literal) { + return BytesRefs.toString(literal.value()); + } + throw new EsqlIllegalArgumentException( + format(null, "Query value must be a constant string in [{}], found [{}]", sourceText, queryField) + ); + } +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Sample.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Sample.java index 781f9ad67c05c..ec9b0c60ff33d 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Sample.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Sample.java @@ -17,8 +17,9 @@ import org.elasticsearch.compute.aggregation.SampleIntAggregatorFunctionSupplier; import org.elasticsearch.compute.aggregation.SampleLongAggregatorFunctionSupplier; import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException; +import org.elasticsearch.xpack.esql.capabilities.PostOptimizationVerificationAware; +import org.elasticsearch.xpack.esql.common.Failures; import org.elasticsearch.xpack.esql.core.expression.Expression; -import org.elasticsearch.xpack.esql.core.expression.FoldContext; import org.elasticsearch.xpack.esql.core.expression.Literal; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; @@ -26,6 +27,7 @@ import org.elasticsearch.xpack.esql.expression.function.Example; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; import org.elasticsearch.xpack.esql.expression.function.FunctionType; +import org.elasticsearch.xpack.esql.expression.function.FunctionUtils; import org.elasticsearch.xpack.esql.expression.function.Param; import org.elasticsearch.xpack.esql.planner.PlannerUtils; import org.elasticsearch.xpack.esql.planner.ToAggregator; @@ -33,13 +35,15 @@ import java.io.IOException; import java.util.List; -import static org.elasticsearch.common.logging.LoggerMessageFormat.format; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNullAndFoldable; +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNull; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.TypeResolutionValidator.forPostOptimizationValidation; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.TypeResolutionValidator.forPreOptimizationValidation; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.resolveTypeLimit; -public class Sample extends AggregateFunction implements ToAggregator { +public class Sample extends AggregateFunction implements ToAggregator, PostOptimizationVerificationAware { public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Sample", Sample::new); @FunctionInfo( @@ -110,14 +114,14 @@ protected TypeResolution resolveType() { return new TypeResolution("Unresolved children"); } var typeResolution = isType(field(), dt -> dt != DataType.UNSIGNED_LONG, sourceText(), FIRST, "any type except unsigned_long").and( - isNotNullAndFoldable(limitField(), sourceText(), SECOND) + isNotNull(limitField(), sourceText(), SECOND) ).and(isType(limitField(), dt -> dt == DataType.INTEGER, sourceText(), SECOND, "integer")); if (typeResolution.unresolved()) { return typeResolution; } - int limit = limitValue(); - if (limit <= 0) { - return new TypeResolution(format(null, "Limit must be greater than 0 in [{}], found [{}]", sourceText(), limit)); + TypeResolution result = resolveTypeLimit(limitField(), sourceText(), forPreOptimizationValidation(limitField())); + if (result.equals(TypeResolution.TYPE_RESOLVED) == false) { + return result; } return TypeResolution.TYPE_RESOLVED; } @@ -164,11 +168,15 @@ Expression limitField() { } private int limitValue() { - return (int) limitField().fold(FoldContext.small() /* TODO remove me */); + return FunctionUtils.limitValue(limitField(), sourceText()); } Expression uuid() { return parameters().get(1); } + @Override + public void postOptimizationVerification(Failures failures) { + FunctionUtils.resolveTypeLimit(limitField(), sourceText(), forPostOptimizationValidation(limitField(), failures)); + } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Top.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Top.java index f31153d228e74..017fef8e917ea 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Top.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Top.java @@ -20,8 +20,9 @@ import org.elasticsearch.compute.aggregation.TopIpAggregatorFunctionSupplier; import org.elasticsearch.compute.aggregation.TopLongAggregatorFunctionSupplier; import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException; +import org.elasticsearch.xpack.esql.capabilities.PostOptimizationVerificationAware; +import org.elasticsearch.xpack.esql.common.Failures; import org.elasticsearch.xpack.esql.core.expression.Expression; -import org.elasticsearch.xpack.esql.core.expression.FoldContext; import org.elasticsearch.xpack.esql.core.expression.Literal; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; @@ -30,6 +31,8 @@ import org.elasticsearch.xpack.esql.expression.function.Example; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; import org.elasticsearch.xpack.esql.expression.function.FunctionType; +import org.elasticsearch.xpack.esql.expression.function.FunctionUtils; +import org.elasticsearch.xpack.esql.expression.function.FunctionUtils.TypeResolutionValidator; import org.elasticsearch.xpack.esql.expression.function.Param; import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; import org.elasticsearch.xpack.esql.planner.ToAggregator; @@ -39,14 +42,17 @@ import static java.util.Arrays.asList; import static org.elasticsearch.common.logging.LoggerMessageFormat.format; +import static org.elasticsearch.xpack.esql.common.Failure.fail; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.THIRD; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNullAndFoldable; +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNull; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.TypeResolutionValidator.forPostOptimizationValidation; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.TypeResolutionValidator.forPreOptimizationValidation; -public class Top extends AggregateFunction implements ToAggregator, SurrogateExpression { +public class Top extends AggregateFunction implements ToAggregator, SurrogateExpression, PostOptimizationVerificationAware { public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Top", Top::new); private static final String ORDER_ASC = "ASC"; @@ -116,16 +122,18 @@ Expression orderField() { return parameters().get(1); } - private int limitValue() { - return (int) limitField().fold(FoldContext.small() /* TODO remove me */); - } - - private String orderRawValue() { - return BytesRefs.toString(orderField().fold(FoldContext.small() /* TODO remove me */)); + private Integer limitValue() { + return FunctionUtils.limitValue(limitField(), sourceText()); } private boolean orderValue() { - return orderRawValue().equalsIgnoreCase(ORDER_ASC); + if (orderField() instanceof Literal literal) { + String order = BytesRefs.toString(literal.value()); + if (ORDER_ASC.equalsIgnoreCase(order) || ORDER_DESC.equalsIgnoreCase(order)) { + return order.equalsIgnoreCase(ORDER_ASC); + } + } + throw new EsqlIllegalArgumentException("Order value must be a literal, found: " + orderField()); } @Override @@ -148,29 +156,93 @@ protected TypeResolution resolveType() { "ip", "string", "numeric except unsigned_long or counter types" - ).and(isNotNullAndFoldable(limitField(), sourceText(), SECOND)) + ).and(isNotNull(limitField(), sourceText(), SECOND)) .and(isType(limitField(), dt -> dt == DataType.INTEGER, sourceText(), SECOND, "integer")) - .and(isNotNullAndFoldable(orderField(), sourceText(), THIRD)) + .and(isNotNull(orderField(), sourceText(), THIRD)) .and(isString(orderField(), sourceText(), THIRD)); if (typeResolution.unresolved()) { return typeResolution; } - var limit = limitValue(); - var order = orderRawValue(); - - if (limit <= 0) { - return new TypeResolution(format(null, "Limit must be greater than 0 in [{}], found [{}]", sourceText(), limit)); + TypeResolution result = resolveTypeLimit(); + if (result.equals(TypeResolution.TYPE_RESOLVED) == false) { + return result; + } + result = resolveTypeOrder(forPreOptimizationValidation(orderField())); + if (result.equals(TypeResolution.TYPE_RESOLVED) == false) { + return result; } + return TypeResolution.TYPE_RESOLVED; + } - if (order.equalsIgnoreCase(ORDER_ASC) == false && order.equalsIgnoreCase(ORDER_DESC) == false) { - return new TypeResolution( - format(null, "Invalid order value in [{}], expected [{}, {}] but got [{}]", sourceText(), ORDER_ASC, ORDER_DESC, order) - ); + /** + * We check that the limit is not null and that if it is a literal, it is a positive integer + * During postOptimizationVerification folding is already done, so we also verify that it is definitively a literal + */ + private TypeResolution resolveTypeLimit() { + return FunctionUtils.resolveTypeLimit(limitField(), sourceText(), forPreOptimizationValidation(limitField())); + } + + /** + * We check that the order is not null and that if it is a literal, it is one of the two valid values: "asc" or "desc". + * During postOptimizationVerification folding is already done, so we also verify that it is definitively a literal + */ + private Expression.TypeResolution resolveTypeOrder(TypeResolutionValidator validator) { + Expression order = orderField(); + if (order == null) { + validator.invalid(new TypeResolution(format(null, "Order must be a valid string in [{}], found [{}]", sourceText(), order))); + } else if (order instanceof Literal literal) { + if (literal.value() == null) { + validator.invalid( + new TypeResolution( + format( + null, + "Invalid order value in [{}], expected [{}, {}] but got [{}]", + sourceText(), + ORDER_ASC, + ORDER_DESC, + order + ) + ) + ); + } else { + String value = BytesRefs.toString(literal.value()); + if (value == null || value.equalsIgnoreCase(ORDER_ASC) == false && value.equalsIgnoreCase(ORDER_DESC) == false) { + validator.invalid( + new TypeResolution( + format( + null, + "Invalid order value in [{}], expected [{}, {}] but got [{}]", + sourceText(), + ORDER_ASC, + ORDER_DESC, + order + ) + ) + ); + } + } + } else { + // it is expected that the expression is a literal after folding + // we fail if it is not a literal + validator.invalidIfPostValidation(fail(order, "Order must be a valid string in [{}], found [{}]", sourceText(), order)); } + return validator.getResolvedType(); + } - return TypeResolution.TYPE_RESOLVED; + @Override + public void postOptimizationVerification(Failures failures) { + postOptimizationVerificationLimit(failures); + postOptimizationVerificationOrder(failures); + } + + private void postOptimizationVerificationLimit(Failures failures) { + FunctionUtils.resolveTypeLimit(limitField(), sourceText(), forPostOptimizationValidation(limitField(), failures)); + } + + private void postOptimizationVerificationOrder(Failures failures) { + resolveTypeOrder(forPostOptimizationValidation(orderField(), failures)); } @Override @@ -215,15 +287,13 @@ public AggregatorFunctionSupplier supplier() { @Override public Expression surrogate() { var s = source(); - - if (limitValue() == 1) { + if (orderField() instanceof Literal && limitField() instanceof Literal && limitValue() == 1) { if (orderValue()) { return new Min(s, field()); } else { return new Max(s, field()); } } - return null; } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java index b5378db783f46..fd11b3eff29ca 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.esql.expression.function.fulltext; -import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.compute.lucene.LuceneQueryEvaluator.ShardConfig; import org.elasticsearch.compute.lucene.LuceneQueryExpressionEvaluator; import org.elasticsearch.compute.lucene.LuceneQueryScoreEvaluator; @@ -17,11 +16,11 @@ import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.xpack.esql.action.EsqlCapabilities; import org.elasticsearch.xpack.esql.capabilities.PostAnalysisPlanVerificationAware; +import org.elasticsearch.xpack.esql.capabilities.PostOptimizationVerificationAware; import org.elasticsearch.xpack.esql.capabilities.TranslationAware; import org.elasticsearch.xpack.esql.common.Failures; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; -import org.elasticsearch.xpack.esql.core.expression.FoldContext; import org.elasticsearch.xpack.esql.core.expression.Nullability; import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; import org.elasticsearch.xpack.esql.core.expression.function.Function; @@ -55,8 +54,11 @@ import static org.elasticsearch.xpack.esql.common.Failure.fail; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.DEFAULT; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNullAndFoldable; +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNull; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.TypeResolutionValidator.forPostOptimizationValidation; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.TypeResolutionValidator.forPreOptimizationValidation; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.resolveTypeQuery; /** * Base class for full-text functions that use ES queries to match documents. @@ -68,7 +70,8 @@ public abstract class FullTextFunction extends Function TranslationAware, PostAnalysisPlanVerificationAware, EvaluatorMapper, - ExpressionScoreMapper { + ExpressionScoreMapper, + PostOptimizationVerificationAware { private final Expression query; private final QueryBuilder queryBuilder; @@ -108,23 +111,21 @@ protected TypeResolution resolveParams() { * @return type resolution for the query parameter */ protected TypeResolution resolveQuery(TypeResolutions.ParamOrdinal queryOrdinal) { - return isString(query(), sourceText(), queryOrdinal).and(isNotNullAndFoldable(query(), sourceText(), queryOrdinal)); + TypeResolution result = isString(query(), sourceText(), queryOrdinal).and(isNotNull(query(), sourceText(), queryOrdinal)); + if (result.unresolved()) { + return result; + } + result = resolveTypeQuery(query(), sourceText(), forPreOptimizationValidation(query())); + if (result.equals(TypeResolution.TYPE_RESOLVED) == false) { + return result; + } + return TypeResolution.TYPE_RESOLVED; } public Expression query() { return query; } - /** - * Returns the resulting query as an object - * - * @return query expression as an object - */ - public Object queryAsObject() { - Object queryAsObject = query().fold(FoldContext.small() /* TODO remove me */); - return BytesRefs.toString(queryAsObject); - } - @Override public Nullability nullable() { return Nullability.FALSE; @@ -417,4 +418,9 @@ public static FieldAttribute fieldAsFieldAttribute(Expression field) { } return fieldExpression instanceof FieldAttribute fieldAttribute ? fieldAttribute : null; } + + @Override + public void postOptimizationVerification(Failures failures) { + resolveTypeQuery(query(), sourceText(), forPostOptimizationValidation(query(), failures)); + } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java index df3cf5af84232..db9a7da19ebc6 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java @@ -20,6 +20,7 @@ import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo; import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; +import org.elasticsearch.xpack.esql.expression.function.FunctionUtils; import org.elasticsearch.xpack.esql.expression.function.Param; import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; import org.elasticsearch.xpack.esql.optimizer.rules.physical.local.LucenePushdownPredicates; @@ -28,7 +29,6 @@ import java.io.IOException; import java.util.List; -import java.util.Objects; /** * Full text function that performs a {@link KqlQuery} . @@ -95,7 +95,7 @@ protected NodeInfo info() { @Override protected Query translate(LucenePushdownPredicates pushdownPredicates, TranslatorHandler handler) { - return new KqlQuery(source(), Objects.toString(queryAsObject())); + return new KqlQuery(source(), FunctionUtils.queryAsString(query(), sourceText())); } @Override diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java index 5c5a46fd2f759..c9663939e19a7 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java @@ -19,7 +19,6 @@ import org.elasticsearch.xpack.esql.core.InvalidArgumentException; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; -import org.elasticsearch.xpack.esql.core.expression.FoldContext; import org.elasticsearch.xpack.esql.core.expression.MapExpression; import org.elasticsearch.xpack.esql.core.querydsl.query.Query; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; @@ -31,6 +30,7 @@ import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo; import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; +import org.elasticsearch.xpack.esql.expression.function.FunctionUtils; import org.elasticsearch.xpack.esql.expression.function.MapParam; import org.elasticsearch.xpack.esql.expression.function.OptionalArgument; import org.elasticsearch.xpack.esql.expression.function.Options; @@ -66,7 +66,6 @@ import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.THIRD; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNull; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNullAndFoldable; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; import static org.elasticsearch.xpack.esql.core.type.DataType.BOOLEAN; import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME; @@ -80,6 +79,8 @@ import static org.elasticsearch.xpack.esql.core.type.DataType.TEXT; import static org.elasticsearch.xpack.esql.core.type.DataType.UNSIGNED_LONG; import static org.elasticsearch.xpack.esql.core.type.DataType.VERSION; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.TypeResolutionValidator.forPreOptimizationValidation; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.resolveTypeQuery; import static org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison.formatIncompatibleTypesMessage; /** @@ -317,13 +318,21 @@ private TypeResolution resolveField() { } private TypeResolution resolveQuery() { - return isType( + TypeResolution result = isType( query(), QUERY_DATA_TYPES::contains, sourceText(), SECOND, "keyword, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version" - ).and(isNotNullAndFoldable(query(), sourceText(), SECOND)); + ).and(isNotNull(query(), sourceText(), SECOND)); + if (result.unresolved()) { + return result; + } + result = resolveTypeQuery(query(), sourceText(), forPreOptimizationValidation(query())); + if (result.equals(TypeResolution.TYPE_RESOLVED) == false) { + return result; + } + return TypeResolution.TYPE_RESOLVED; } private TypeResolution checkParamCompatibility() { @@ -395,9 +404,8 @@ public BiConsumer postAnalysisPlanVerification() { }; } - @Override public Object queryAsObject() { - Object queryAsObject = query().fold(FoldContext.small() /* TODO remove me */); + Object queryAsObject = FunctionUtils.queryAsObject(query(), sourceText()); // Convert BytesRef to string for string-based values if (queryAsObject instanceof BytesRef bytesRef) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchPhrase.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchPhrase.java index 4ed0e16ab5b4a..0ecba5a3994d5 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchPhrase.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchPhrase.java @@ -17,7 +17,6 @@ import org.elasticsearch.xpack.esql.core.InvalidArgumentException; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; -import org.elasticsearch.xpack.esql.core.expression.FoldContext; import org.elasticsearch.xpack.esql.core.expression.MapExpression; import org.elasticsearch.xpack.esql.core.querydsl.query.Query; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; @@ -28,6 +27,7 @@ import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo; import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; +import org.elasticsearch.xpack.esql.expression.function.FunctionUtils; import org.elasticsearch.xpack.esql.expression.function.MapParam; import org.elasticsearch.xpack.esql.expression.function.OptionalArgument; import org.elasticsearch.xpack.esql.expression.function.Options; @@ -56,13 +56,14 @@ import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.THIRD; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNull; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNullAndFoldable; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_NANOS; import static org.elasticsearch.xpack.esql.core.type.DataType.FLOAT; import static org.elasticsearch.xpack.esql.core.type.DataType.INTEGER; import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD; import static org.elasticsearch.xpack.esql.core.type.DataType.TEXT; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.TypeResolutionValidator.forPreOptimizationValidation; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.resolveTypeQuery; /** * Full text function that performs a {@link org.elasticsearch.xpack.esql.querydsl.query.MatchPhraseQuery} . @@ -196,9 +197,17 @@ private TypeResolution resolveField() { } private TypeResolution resolveQuery() { - return isType(query(), QUERY_DATA_TYPES::contains, sourceText(), SECOND, "keyword").and( - isNotNullAndFoldable(query(), sourceText(), SECOND) + TypeResolution result = isType(query(), QUERY_DATA_TYPES::contains, sourceText(), SECOND, "keyword").and( + isNotNull(query(), sourceText(), SECOND) ); + if (result.unresolved()) { + return result; + } + result = resolveTypeQuery(query(), sourceText(), forPreOptimizationValidation(query())); + if (result.equals(TypeResolution.TYPE_RESOLVED) == false) { + return result; + } + return TypeResolution.TYPE_RESOLVED; } private Map matchPhraseQueryOptions() throws InvalidArgumentException { @@ -248,9 +257,8 @@ public BiConsumer postAnalysisPlanVerification() { }; } - @Override public Object queryAsObject() { - Object queryAsObject = query().fold(FoldContext.small() /* TODO remove me */); + Object queryAsObject = FunctionUtils.queryAsObject(query(), sourceText()); // Convert BytesRef to string for string-based values if (queryAsObject instanceof BytesRef bytesRef) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatch.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatch.java index 3e9fed6be850d..74138a88367f8 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatch.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatch.java @@ -27,6 +27,7 @@ import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo; import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; +import org.elasticsearch.xpack.esql.expression.function.FunctionUtils; import org.elasticsearch.xpack.esql.expression.function.MapParam; import org.elasticsearch.xpack.esql.expression.function.OptionalArgument; import org.elasticsearch.xpack.esql.expression.function.Options; @@ -66,7 +67,6 @@ import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.THIRD; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isMapExpression; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNull; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNullAndFoldable; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; import static org.elasticsearch.xpack.esql.core.type.DataType.BOOLEAN; import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME; @@ -80,6 +80,8 @@ import static org.elasticsearch.xpack.esql.core.type.DataType.TEXT; import static org.elasticsearch.xpack.esql.core.type.DataType.UNSIGNED_LONG; import static org.elasticsearch.xpack.esql.core.type.DataType.VERSION; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.TypeResolutionValidator.forPreOptimizationValidation; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.resolveTypeQuery; /** * Full text function that performs a {@link org.elasticsearch.xpack.esql.querydsl.query.MultiMatchQuery} . @@ -345,7 +347,7 @@ protected Query translate(LucenePushdownPredicates pushdownPredicates, Translato String fieldName = Match.getNameFromFieldAttribute(fieldAttribute); fieldsWithBoost.put(fieldName, 1.0f); } - return new MultiMatchQuery(source(), Objects.toString(queryAsObject()), fieldsWithBoost, getOptions()); + return new MultiMatchQuery(source(), FunctionUtils.queryAsString(query(), sourceText()), fieldsWithBoost, getOptions()); } @Override @@ -417,13 +419,21 @@ private TypeResolution resolveOptions() { } private TypeResolution resolveQuery() { - return isType( + TypeResolution result = isType( query(), QUERY_DATA_TYPES::contains, sourceText(), FIRST, "keyword, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version" - ).and(isNotNullAndFoldable(query(), sourceText(), FIRST)); + ).and(isNotNull(query(), sourceText(), FIRST)); + if (result.unresolved()) { + return result; + } + result = resolveTypeQuery(query(), sourceText(), forPreOptimizationValidation(query())); + if (result.equals(TypeResolution.TYPE_RESOLVED) == false) { + return result; + } + return TypeResolution.TYPE_RESOLVED; } @Override diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java index 7285f19fc5aa7..1678ba696a1fb 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java @@ -24,6 +24,7 @@ import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo; import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; +import org.elasticsearch.xpack.esql.expression.function.FunctionUtils; import org.elasticsearch.xpack.esql.expression.function.MapParam; import org.elasticsearch.xpack.esql.expression.function.OptionalArgument; import org.elasticsearch.xpack.esql.expression.function.Options; @@ -62,13 +63,15 @@ import static org.elasticsearch.index.query.QueryStringQueryBuilder.TIME_ZONE_FIELD; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNullAndFoldable; +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNull; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; import static org.elasticsearch.xpack.esql.core.type.DataType.BOOLEAN; import static org.elasticsearch.xpack.esql.core.type.DataType.FLOAT; import static org.elasticsearch.xpack.esql.core.type.DataType.INTEGER; import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD; import static org.elasticsearch.xpack.esql.core.type.DataType.TEXT; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.TypeResolutionValidator.forPreOptimizationValidation; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.resolveTypeQuery; /** * Full text function that performs a {@link QueryStringQuery} . @@ -311,9 +314,17 @@ public Expression options() { public static final Set QUERY_DATA_TYPES = Set.of(KEYWORD, TEXT); private TypeResolution resolveQuery() { - return isType(query(), QUERY_DATA_TYPES::contains, sourceText(), FIRST, "keyword, text").and( - isNotNullAndFoldable(query(), sourceText(), FIRST) + TypeResolution result = isType(query(), QUERY_DATA_TYPES::contains, sourceText(), FIRST, "keyword, text").and( + isNotNull(query(), sourceText(), FIRST) ); + if (result.unresolved()) { + return result; + } + result = resolveTypeQuery(query(), sourceText(), forPreOptimizationValidation(query())); + if (result.equals(TypeResolution.TYPE_RESOLVED) == false) { + return result; + } + return TypeResolution.TYPE_RESOLVED; } private Map queryStringOptions() throws InvalidArgumentException { @@ -343,7 +354,7 @@ protected NodeInfo info() { @Override protected Query translate(LucenePushdownPredicates pushdownPredicates, TranslatorHandler handler) { - return new QueryStringQuery(source(), Objects.toString(queryAsObject()), Map.of(), queryStringOptions()); + return new QueryStringQuery(source(), FunctionUtils.queryAsString(query(), sourceText()), Map.of(), queryStringOptions()); } @Override diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Term.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Term.java index cecef10a136f7..fc62c451ba589 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Term.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Term.java @@ -25,6 +25,7 @@ import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo; import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; +import org.elasticsearch.xpack.esql.expression.function.FunctionUtils; import org.elasticsearch.xpack.esql.expression.function.Param; import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; import org.elasticsearch.xpack.esql.optimizer.rules.physical.local.LucenePushdownPredicates; @@ -133,7 +134,7 @@ protected TypeResolutions.ParamOrdinal queryParamOrdinal() { @Override protected Query translate(LucenePushdownPredicates pushdownPredicates, TranslatorHandler handler) { // Uses a term query that contributes to scoring - return new TermQuery(source(), ((FieldAttribute) field()).name(), queryAsObject(), false, true); + return new TermQuery(source(), ((FieldAttribute) field()).name(), FunctionUtils.queryAsObject(query(), sourceText()), false, true); } @Override diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/vector/Knn.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/vector/Knn.java index cab5ec862d7f5..bb80f828b19ae 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/vector/Knn.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/vector/Knn.java @@ -7,16 +7,19 @@ package org.elasticsearch.xpack.esql.expression.function.vector; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException; import org.elasticsearch.xpack.esql.capabilities.PostAnalysisPlanVerificationAware; import org.elasticsearch.xpack.esql.capabilities.TranslationAware; import org.elasticsearch.xpack.esql.common.Failures; import org.elasticsearch.xpack.esql.core.InvalidArgumentException; import org.elasticsearch.xpack.esql.core.expression.Expression; -import org.elasticsearch.xpack.esql.core.expression.FoldContext; +import org.elasticsearch.xpack.esql.core.expression.Literal; import org.elasticsearch.xpack.esql.core.expression.MapExpression; import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; import org.elasticsearch.xpack.esql.core.querydsl.query.Query; @@ -49,6 +52,7 @@ import java.util.function.BiConsumer; import static java.util.Map.entry; +import static org.elasticsearch.common.logging.LoggerMessageFormat.format; import static org.elasticsearch.index.query.AbstractQueryBuilder.BOOST_FIELD; import static org.elasticsearch.search.vectors.KnnVectorQueryBuilder.K_FIELD; import static org.elasticsearch.search.vectors.KnnVectorQueryBuilder.NUM_CANDS_FIELD; @@ -59,13 +63,15 @@ import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.THIRD; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isFoldable; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNull; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNullAndFoldable; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; import static org.elasticsearch.xpack.esql.core.type.DataType.DENSE_VECTOR; import static org.elasticsearch.xpack.esql.core.type.DataType.FLOAT; import static org.elasticsearch.xpack.esql.core.type.DataType.INTEGER; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.TypeResolutionValidator.forPreOptimizationValidation; +import static org.elasticsearch.xpack.esql.expression.function.FunctionUtils.resolveTypeQuery; public class Knn extends FullTextFunction implements OptionalArgument, VectorFunction, PostAnalysisPlanVerificationAware { + private final Logger log = LogManager.getLogger(getClass()); public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Knn", Knn::readFrom); @@ -207,9 +213,16 @@ private TypeResolution resolveField() { } private TypeResolution resolveQuery() { - return isType(query(), dt -> dt == DENSE_VECTOR, sourceText(), TypeResolutions.ParamOrdinal.SECOND, "dense_vector").and( - isNotNullAndFoldable(query(), sourceText(), SECOND) - ); + TypeResolution result = isType(query(), dt -> dt == DENSE_VECTOR, sourceText(), TypeResolutions.ParamOrdinal.SECOND, "dense_vector") + .and(isNotNull(query(), sourceText(), SECOND)); + if (result.unresolved()) { + return result; + } + result = resolveTypeQuery(query(), sourceText(), forPreOptimizationValidation(query())); + if (result.equals(TypeResolution.TYPE_RESOLVED) == false) { + return result; + } + return TypeResolution.TYPE_RESOLVED; } private TypeResolution resolveK() { @@ -222,6 +235,24 @@ private TypeResolution resolveK() { .and(isNotNull(k(), sourceText(), THIRD)); } + public List queryAsObject() { + // we need to check that we got a list and every element in the list is a number + Expression query = query(); + if (query instanceof Literal literal) { + @SuppressWarnings("unchecked") + List result = ((List) literal.value()); + return result; + } + throw new EsqlIllegalArgumentException(format(null, "Query value must be a list of numbers in [{}], found [{}]", source(), query)); + } + + int getKIntValue() { + if (k() instanceof Literal literal) { + return (int) (Number) literal.value(); + } + throw new EsqlIllegalArgumentException(format(null, "K value must be a constant integer in [{}], found [{}]", source(), k())); + } + @Override public Expression replaceQueryBuilder(QueryBuilder queryBuilder) { return new Knn(source(), field(), query(), k(), options(), queryBuilder, filterExpressions()); @@ -244,13 +275,12 @@ protected Query translate(LucenePushdownPredicates pushdownPredicates, Translato Check.notNull(fieldAttribute, "Knn must have a field attribute as the first argument"); String fieldName = getNameFromFieldAttribute(fieldAttribute); - @SuppressWarnings("unchecked") - List queryFolded = (List) query().fold(FoldContext.small() /* TODO remove me */); + List queryFolded = queryAsObject(); float[] queryAsFloats = new float[queryFolded.size()]; for (int i = 0; i < queryFolded.size(); i++) { queryAsFloats[i] = queryFolded.get(i).floatValue(); } - int kValue = ((Number) k().fold(FoldContext.small())).intValue(); + int kValue = getKIntValue(); Map opts = queryOptions(); opts.put(K_FIELD.getPreferredName(), kValue); @@ -340,12 +370,13 @@ public boolean equals(Object o) { return Objects.equals(field(), knn.field()) && Objects.equals(query(), knn.query()) && Objects.equals(queryBuilder(), knn.queryBuilder()) + && Objects.equals(k(), knn.k()) && Objects.equals(filterExpressions(), knn.filterExpressions()); } @Override public int hashCode() { - return Objects.hash(field(), query(), queryBuilder(), filterExpressions()); + return Objects.hash(field(), query(), queryBuilder(), k(), filterExpressions()); } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PostOptimizationPhasePlanVerifier.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PostOptimizationPhasePlanVerifier.java index 647dafe649984..e187264b2432f 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PostOptimizationPhasePlanVerifier.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PostOptimizationPhasePlanVerifier.java @@ -52,6 +52,13 @@ public Failures verify(P optimizedPlan, boolean skipRemoteEnrichVerification, Li abstract void checkPlanConsistency(P optimizedPlan, Failures failures, Failures depFailures); private static void verifyOutputNotChanged(QueryPlan optimizedPlan, List expectedOutputAttributes, Failures failures) { + // disable this check if there are other failures already + // it is possible that some of the attributes are not resolved yet and that is reflected in the failures + // we cannot get the datatype on an unresolved attribute + // if we try it, it causes an exception and the exception hides the more detailed error message + if (failures.hasFailures()) { + return; + } if (dataTypeEquals(expectedOutputAttributes, optimizedPlan.output()) == false) { // If the output level is empty we add a column called ProjectAwayColumns.ALL_FIELDS_PROJECTED // We will ignore such cases for output verification diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index 52271c7ee263a..5f3123bd9574a 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -2249,31 +2249,6 @@ private void checkFullTextFunctionNullArgs(String functionInvocation, String arg ); } - public void testFullTextFunctionsConstantArg() throws Exception { - checkFullTextFunctionsConstantArg("match(title, category)", "second"); - checkFullTextFunctionsConstantArg("qstr(title)", ""); - checkFullTextFunctionsConstantArg("kql(title)", ""); - checkFullTextFunctionsConstantArg("match_phrase(title, tags)", "second"); - if (EsqlCapabilities.Cap.MULTI_MATCH_FUNCTION.isEnabled()) { - checkFullTextFunctionsConstantArg("multi_match(category, body)", "first"); - checkFullTextFunctionsConstantArg("multi_match(concat(title, \"world\"), title)", "first"); - } - if (EsqlCapabilities.Cap.TERM_FUNCTION.isEnabled()) { - checkFullTextFunctionsConstantArg("term(title, tags)", "second"); - } - if (EsqlCapabilities.Cap.KNN_FUNCTION_V3.isEnabled()) { - checkFullTextFunctionsConstantArg("knn(vector, vector, 10)", "second"); - checkFullTextFunctionsConstantArg("knn(vector, [0, 1, 2], category)", "third"); - } - } - - private void checkFullTextFunctionsConstantArg(String functionInvocation, String argOrdinal) throws Exception { - assertThat( - error("from test | where " + functionInvocation, fullTextAnalyzer), - containsString(argOrdinal + " argument of [" + functionInvocation + "] must be a constant") - ); - } - public void testInsistNotOnTopOfFrom() { assumeTrue("requires snapshot builds", Build.current().isSnapshot()); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/TopTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/TopTests.java index 342b0b0aba697..7e10b3ac32c85 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/TopTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/TopTests.java @@ -262,7 +262,7 @@ public static Iterable parameters() { new TestCaseSupplier.TypedData(null, DataType.INTEGER, "limit").forceLiteral(), new TestCaseSupplier.TypedData(new BytesRef("desc"), DataType.KEYWORD, "order").forceLiteral() ), - "second argument of [source] cannot be null, received [limit]" + "Limit must be a constant integer in [source], found [null]" ) ), new TestCaseSupplier( @@ -273,7 +273,7 @@ public static Iterable parameters() { new TestCaseSupplier.TypedData(1, DataType.INTEGER, "limit").forceLiteral(), new TestCaseSupplier.TypedData(null, DataType.KEYWORD, "order").forceLiteral() ), - "third argument of [source] cannot be null, received [order]" + "Invalid order value in [source], expected [ASC, DESC] but got [null]" ) ) ) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/KnnTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/KnnTests.java index 595eb58118a09..9bd4896350ca7 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/KnnTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/KnnTests.java @@ -31,6 +31,7 @@ import java.util.Map; import java.util.function.Supplier; +import static org.elasticsearch.xpack.esql.SerializationTestUtils.assertSerialization; import static org.elasticsearch.xpack.esql.SerializationTestUtils.serializeDeserialize; import static org.elasticsearch.xpack.esql.core.type.DataType.BOOLEAN; import static org.elasticsearch.xpack.esql.core.type.DataType.DENSE_VECTOR; @@ -144,4 +145,21 @@ protected Expression serializeDeserializeExpression(Expression expression) { // Fields use synthetic sources, which can't be serialized. So we use the originals instead. return newExpression.replaceChildren(expression.children()); } + + public void testSerializationOfSimple() { + // do nothing + assumeTrue("can't serialize function", canSerialize()); + Expression expression = buildFieldExpression(testCase); + if (expression instanceof Knn knn) { + // The K parameter is not serialized, so we need to remove it from the children + // before we compare the serialization results + List newChildren = knn.children(); + newChildren.set(2, null); // remove the k parameter + Expression knnWithoutK = knn.replaceChildren(newChildren); + assertSerialization(knnWithoutK, testCase.getConfiguration()); + } else { + // If not a Knn instance we fail the test as it is supposed to be a Knn function + fail("Expression is not Knn"); + } + } } diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/230_folding.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/230_folding.yml new file mode 100644 index 0000000000000..af47b69566ce8 --- /dev/null +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/230_folding.yml @@ -0,0 +1,561 @@ +--- +setup: + - requires: + test_runner_features: [ capabilities, contains ] + reason: "make sure new functions run where supported only" + - do: + indices.create: + index: employees + body: + mappings: + properties: + hire_date: + type: date + salary_change: + type: double + salary: + type: integer + salary_change_long: + type: long + name: + type: keyword + image_vector: + type: dense_vector + dims: 3 + index: true + similarity: l2_norm + + - do: + bulk: + index: employees + refresh: true + body: + - { "index": { } } + - { "hire_date": "2020-01-01", "salary_change": 100.5, "salary": 50000, "salary_change_long": 100, "name": "Alice Smith", "image_vector": [ 0.1, 0.2, 0.3 ] } + - { "index": { } } + - { "hire_date": "2021-01-01", "salary_change": 200.5, "salary": 60000, "salary_change_long": 200, "name": "Bob Johnson", "image_vector": [ 0.4, 0.5, 0.6 ] } + - { "index": { } } + - { "hire_date": "2019-01-01", "salary_change": 50.5, "salary": 40000, "salary_change_long": 50, "name": "Charlie Smith", "image_vector": [ 0.7, 0.8, 0.9 ] } + +--- +TOP function with constant folding: + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ agg_top ] + reason: "Uses TOP function" + + - do: + esql.query: + body: + query: | + FROM employees + | STATS + date = TOP(hire_date, 1+1, "dEsc"), + double = TOP(salary_change, 100-98, REVERSE("csed")), + integer = TOP(salary, 4-(1+1), Substring("Ascending",0,3)), + long = TOP(salary_change_long, 10 - 4*2, Concat("as","c")) + | LIMIT 5 + - match: { columns.0.name: "date" } + - match: { columns.1.name: "double" } + - match: { columns.2.name: "integer" } + - match: { columns.3.name: "long" } + - length: { values: 1 } + - length: { values.0: 4 } + # Check that the values are as expected for the folded constants + - match: { values.0.0: [ "2021-01-01T00:00:00.000Z", "2020-01-01T00:00:00.000Z" ] } + - match: { values.0.1: [ 200.5, 100.5 ] } + - match: { values.0.2: [ 40000, 50000 ] } + - match: { values.0.3: [ 50, 100 ] } + + +--- + +TOP function with negative limit value after folding: + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ agg_top ] + reason: "Uses TOP function" + - do: + catch: bad_request + esql.query: + body: + query: | + FROM employees + | STATS + date = TOP(hire_date, 10 - 20, "dEsc"), + double = TOP(salary_change, 100-98, REVERSE("csed")), + integer = TOP(salary, 4-(1+1), Substring("Ascending",0,3)), + long = TOP(salary_change_long, 10 - 4*2, Concat("as","c")) + | LIMIT 5 + - match: { error.type: "verification_exception" } + - contains: { error.reason: "Limit must be greater than 0 in [TOP(hire_date, 10 - 20, \"dEsc\")], found [-10]" } + +--- + +Top function with invalid sort order: + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ agg_top ] + reason: "Uses TOP function" + - do: + catch: bad_request + esql.query: + body: + query: | + FROM employees + | STATS + date = TOP(hire_date, 2, REVERSE("csed123")), + double = TOP(salary_change, 100-98, REVERSE("csed")), + integer = TOP(salary, 4-(1+1), Substring("Ascending",0,3)), + long = TOP(salary_change_long, 10 - 4*2, Concat("as","c")) + | LIMIT 5 + - match: { error.type: "verification_exception" } + - contains: { error.reason: "Invalid order value in [TOP(hire_date, 2, REVERSE(\"csed123\"))], expected [ASC, DESC] but got [321desc]" } + +--- + +SAMPLE function with constant folding: + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ sample_v3 ] + reason: "Uses SAMPLE function" + - do: + esql.query: + body: + query: | + FROM employees + | STATS + sample_salary = SAMPLE(salary, 1+2) + | LIMIT 5 + - match: { columns.0.name: "sample_salary" } + - length: { values: 1 } + - length: { values.0: 1 } + +--- + +SAMPLE function with negative limit value after folding: + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ sample_v3 ] + reason: "Uses SAMPLE function" + - do: + catch: bad_request + esql.query: + body: + query: | + FROM employees + | STATS + sample_salary = SAMPLE(salary, 2-5) + | LIMIT 5 + - match: { error.type: "verification_exception" } + - contains: { error.reason: "Limit must be greater than 0 in [SAMPLE(salary, 2-5)], found [-3]" } + +--- + +MATCH function with foldable query: + - do: + esql.query: + body: + query: | + FROM employees + | WHERE MATCH(salary, 50000+10000) + | KEEP hire_date, salary, salary_change, salary_change_long, name + | LIMIT 5 + - match: { columns.0.name: "hire_date" } + - match: { columns.1.name: "salary" } + - match: { columns.2.name: "salary_change" } + - match: { columns.3.name: "salary_change_long" } + - match: { columns.4.name: "name" } + - length: { values: 1 } + - match: { values.0.0: "2021-01-01T00:00:00.000Z" } + - match: { values.0.1: 60000 } + - match: { values.0.2: 200.5 } + - match: { values.0.3: 200 } + - match: { values.0.4: "Bob Johnson" } + +--- + +MATCH function with non-foldable query: + - do: + catch: bad_request + esql.query: + body: + query: | + FROM employees + | WHERE MATCH(salary, salary + 10000 ) + | LIMIT 5 + - match: { error.type: "verification_exception" } + #We only check that the problematic string is there, because the error message is slightly different in old versions + #"Query must be a valid string in [MATCH(salary, salary + 10000 )], found [salary + 10000]" + #second argument of [MATCH(salary, salary + 10000 )] must be a constant, received [salary + 10000] + - contains: { error.reason: "[MATCH(salary, salary + 10000 )]" } + +--- + +Foldable query using MATCH_PHRASE on name: + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ match_phrase_function ] + reason: "Uses MATCH_PHRASE function" + - do: + esql.query: + body: + query: | + FROM employees + | WHERE MATCH_PHRASE(name, CONCAT("Bob ", "Johnson")) + | KEEP hire_date, salary, salary_change, salary_change_long, name + | LIMIT 5 + - match: { columns.0.name: "hire_date" } + - match: { columns.1.name: "salary" } + - match: { columns.2.name: "salary_change" } + - match: { columns.3.name: "salary_change_long" } + - match: { columns.4.name: "name" } + - length: { values: 1 } + - match: { values.0.0: "2021-01-01T00:00:00.000Z" } + - match: { values.0.1: 60000 } + - match: { values.0.2: 200.5 } + - match: { values.0.3: 200 } + - match: { values.0.4: "Bob Johnson" } + +--- + +Foldable query using MATCH_PHRASE on name but with non-foldable expression: + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ match_phrase_function ] + reason: "Uses MATCH_PHRASE function" + - do: + catch: bad_request + esql.query: + body: + query: | + FROM employees + | WHERE MATCH_PHRASE(name, CONCAT("Bob ", name)) + | KEEP hire_date, salary, salary_change, salary_change_long, name + | LIMIT 5 + - match: { error.type: "verification_exception" } + # We only check that the problematic string is there, because the error message is slightly different in old versions + #second argument of [MATCH_PHRASE(name, CONCAT("Bob ", name))] must be a constant, received [CONCAT("Bob ", name)]" + #Query must be a valid string in [MATCH_PHRASE(name, CONCAT(\"Bob \", name))], found [CONCAT(\"Bob \", name)] + - contains: { error.reason: "[MATCH_PHRASE(name, CONCAT(\"Bob \", name))]" } + +--- + +Foldable query using MATCH_PHRASE on name but with non constant query: + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ match_phrase_function ] + reason: "Uses MATCH_PHRASE function" + - do: + catch: bad_request + esql.query: + body: + query: | + FROM employees + | WHERE MATCH_PHRASE(name, name) + | KEEP hire_date, salary, salary_change, salary_change_long, name + | LIMIT 5 + - match: { error.type: "verification_exception" } + # We only check that the problematic string is there, because the error message is slightly different in old versions + # second argument of [MATCH_PHRASE(name, name)] must be a constant, received [name] + # Query must be a valid string in [MATCH_PHRASE(name, name)], found [name + - contains: { error.reason: "[MATCH_PHRASE(name, name)]" } + +--- + + +Foldable query using MULTI_MATCH on name: + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ multi_match_function ] + reason: "Uses MULTI_MATCH function" + - do: + esql.query: + body: + query: | + FROM employees + | WHERE MULTI_MATCH(CONCAT("Bob ", "Johnson"), name) + | KEEP hire_date, salary, salary_change, salary_change_long, name + | LIMIT 5 + - match: { columns.0.name: "hire_date" } + - match: { columns.1.name: "salary" } + - match: { columns.2.name: "salary_change" } + - match: { columns.3.name: "salary_change_long" } + - match: { columns.4.name: "name" } + - length: { values: 1 } + - match: { values.0.0: "2021-01-01T00:00:00.000Z" } + - match: { values.0.1: 60000 } + - match: { values.0.2: 200.5 } + - match: { values.0.3: 200 } + - match: { values.0.4: "Bob Johnson" } + +--- + +Foldable query using MULTI_MATCH on name but with non-foldable expression: + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ multi_match_function ] + reason: "Uses MULTI_MATCH function" + - do: + catch: bad_request + esql.query: + body: + query: | + FROM employees + | WHERE MULTI_MATCH(CONCAT("Bob ", name), name) + | KEEP hire_date, salary, salary_change, salary_change_long, name + | LIMIT 5 + - match: { error.type: "verification_exception" } + # We only check that the problematic string is there, because the error message is slightly different in old versions + # first argument of [MULTI_MATCH(CONCAT("Bob ", name), name)] must be a constant, received [CONCAT("Bob ", name)] + # Query must be a valid string in [MULTI_MATCH(CONCAT(\"Bob \", name), name)], found [CONCAT(\"Bob \", name)] + - contains: { error.reason: "[MULTI_MATCH(CONCAT(\"Bob \", name), name)]" } + +--- + +Query using MULTI_MATCH on name but with non constant query: + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ multi_match_function ] + reason: "Uses MULTI_MATCH function" + - do: + catch: bad_request + esql.query: + body: + query: | + FROM employees + | WHERE MULTI_MATCH(name, name) + | KEEP hire_date, salary, salary_change, salary_change_long, name + | LIMIT 5 + - match: { error.type: "verification_exception" } + # We only check that the problematic string is there, because the error message is slightly different in old versions + #first argument of [MULTI_MATCH(CONCAT("Bob ", name), name)] must be a constant, received [CONCAT("Bob ", name)] + #Query must be a valid string in [MULTI_MATCH(name, name)], found [name + - contains: { error.reason: "[MULTI_MATCH(name, name)]" } + +--- + +Foldable query using QSTR on name: + - do: + esql.query: + body: + query: | + FROM employees + | WHERE QSTR(CONCAT("name:", "Bob*")) + | KEEP hire_date, salary, salary_change, salary_change_long, name + | LIMIT 5 + - match: { columns.0.name: "hire_date" } + - match: { columns.1.name: "salary" } + - match: { columns.2.name: "salary_change" } + - match: { columns.3.name: "salary_change_long" } + - match: { columns.4.name: "name" } + - length: { values: 1 } + - match: { values.0.0: "2021-01-01T00:00:00.000Z" } + - match: { values.0.1: 60000 } + - match: { values.0.2: 200.5 } + - match: { values.0.3: 200 } + - match: { values.0.4: "Bob Johnson" } + +--- + +Foldable query using QSTR on name but with non-foldable expression: + - do: + catch: bad_request + esql.query: + body: + query: | + FROM employees + | WHERE QSTR(CONCAT(name, "Bob")) + | KEEP hire_date, salary, salary_change, salary_change_long, name + | LIMIT 5 + - match: { error.type: "verification_exception" } + # We only check that the problematic string is there, because the error message is slightly different in old versions + #first argument of [QSTR(CONCAT(name, "Bob"))] must be a constant, received [CONCAT(name, "Bob")]" + #Query must be a valid string in [QSTR(CONCAT(name, \"Bob\"))], found [CONCAT(name, \"Bob\")] + - contains: { error.reason: "[QSTR(CONCAT(name, \"Bob\"))]" } + +--- + +Foldable query using KQL on name but with non-foldable expression: + - do: + catch: bad_request + esql.query: + body: + query: | + FROM employees + | WHERE KQL(name) + | KEEP hire_date, salary, salary_change, salary_change_long, name + | LIMIT 5 + - match: { error.type: "verification_exception" } + # We only check that the problematic string is there, because the error message is slightly different in old versions + #Query must be a valid string in [KQL(name)], found [name + #argument of [KQL(name)] must be a constant, received [name] + - contains: { error.reason: "[KQL(name)]" } + +--- + +Foldable query using KNN on image_vector: + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ knn_function_v3 ] + reason: "Uses KNN function" + - do: + esql.query: + body: + query: | + FROM employees + | WHERE KNN(image_vector, [0.4, 0.5, 0.9], 1 + 1) + | KEEP hire_date, salary, salary_change, salary_change_long, name, image_vector + | SORT name + | LIMIT 2 + - match: { columns.0.name: "hire_date" } + - match: { columns.1.name: "salary" } + - match: { columns.2.name: "salary_change" } + - match: { columns.3.name: "salary_change_long" } + - match: { columns.4.name: "name" } + - match: { columns.5.name: "image_vector" } + - length: { values: 2 } + +--- + +Foldable query using KNN on image_vector but with non-foldable expression: + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ knn_function_v3 ] + reason: "Uses KNN function" + - do: + catch: bad_request + esql.query: + body: + query: | + FROM employees + | WHERE KNN(image_vector, [0.4, 0.5, 0.9], 1+salary) + | KEEP hire_date, salary, salary_change, salary_change_long, name, image_vector + | LIMIT 5 + - match: { error.type: "verification_exception" } + - contains: { error.reason: "third argument of [KNN(image_vector, [0.4, 0.5, 0.9], 1+salary)] must be a constant, received [1+salary]" } + +--- + +KNN on non constant k(): + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ knn_function_v3 ] + reason: "Uses KNN function" + - do: + catch: bad_request + esql.query: + body: + query: | + FROM employees + | WHERE KNN(image_vector, [0.4, 0.5, 0.9], salary) + | KEEP hire_date, salary, salary_change, salary_change_long, name, image_vector + | LIMIT 5 + - match: { error.type: "verification_exception" } + - contains: { error.reason: "third argument of [KNN(image_vector, [0.4, 0.5, 0.9], salary)] must be a constant, received [salary" } + +--- + +KNN on non constant query: + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ knn_function_v3 ] + reason: "Uses KNN function" + - do: + catch: bad_request + esql.query: + body: + query: | + FROM employees + | WHERE KNN(image_vector, image_vector, 1) + | KEEP hire_date, salary, salary_change, salary_change_long, name, image_vector + | LIMIT 5 + - match: { error.type: "verification_exception" } + #We only check that the problematic string is there, because the error message is slightly different in old versions + #Query must be a valid string in [KNN(image_vector, image_vector, 1)], found [image_vector + #second argument of [KNN(image_vector, image_vector, 1)] must be a constant, received [image_vector] + - contains: { error.reason: "[KNN(image_vector, image_vector, 1)]" } + +--- + +Query using TERM function on name but with non constant query: + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ term_function ] + reason: "Uses TERM function" + - do: + catch: bad_request + esql.query: + body: + query: | + FROM employees + | WHERE TERM(name, salary) + | KEEP hire_date, salary, salary_change, salary_change_long, name + | LIMIT 5 + - match: { error.type: "verification_exception" } + - contains: { error.reason: "second argument of [TERM(name, salary)] must be [string], found value [salary] type [integer]" } +