Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
0f080a5
Fix for top
julian-elastic Jul 9, 2025
8a24206
Fix for sample function
julian-elastic Jul 10, 2025
9dd5502
[CI] Auto commit changes from spotless
Jul 10, 2025
67dc79f
Handle Match function
julian-elastic Jul 10, 2025
923dc02
Handle MatchPhrase function
julian-elastic Jul 11, 2025
29cce29
Handle MultiMatch function
julian-elastic Jul 11, 2025
fdf54dc
Handle QueryString function
julian-elastic Jul 11, 2025
2641cb3
Handle KNN function
julian-elastic Jul 11, 2025
8e2c6b4
Merge remote-tracking branch 'origin/foldable' into foldable
julian-elastic Jul 12, 2025
15dfed3
Migrate tests from VerifierTests to 230_folding.yml
julian-elastic Jul 14, 2025
408c8fd
Remove partiallyFoldable code as it is not needed
julian-elastic Jul 14, 2025
f187139
Fix some of the failing UTs
julian-elastic Jul 14, 2025
d66fdfc
Merge branch 'main' into foldable
julian-elastic Jul 15, 2025
d80dd57
[CI] Auto commit changes from spotless
Jul 15, 2025
6f34981
Remove some debugging code
julian-elastic Jul 15, 2025
4247581
Fix failing UTs in old version
julian-elastic Jul 16, 2025
b2213e1
Merge branch 'main' into foldable
julian-elastic Jul 16, 2025
029f96a
Integrate with knn_function_v3
julian-elastic Jul 16, 2025
18e18a2
Fix UT fails
julian-elastic Jul 17, 2025
23359b5
Fix UT fail
julian-elastic Jul 17, 2025
4007272
Merge branch 'main' into foldable
julian-elastic Jul 17, 2025
9537344
Fix merge error
julian-elastic Jul 17, 2025
8e9d34b
Merge branch 'main' into foldable
julian-elastic Jul 18, 2025
20245b6
Fix merge error
julian-elastic Jul 18, 2025
b2e796c
[CI] Auto commit changes from spotless
Jul 18, 2025
cb676a7
Fix UT error
julian-elastic Jul 18, 2025
910c1d0
Merge branch 'main' into foldable
julian-elastic Jul 18, 2025
91f76d6
Remove isNotNullAndFoldable function completely
julian-elastic Jul 18, 2025
b3ee3a0
Update docs/changelog/130944.yaml
julian-elastic Jul 18, 2025
8dfdf1a
Fix UT failures related to trying to get the datatype on unresolved a…
julian-elastic Jul 18, 2025
4087f30
Merge branch 'main' into foldable
julian-elastic Jul 19, 2025
8e11323
Merge branch 'main' into foldable
julian-elastic Jul 21, 2025
89dfb4e
Update docs/changelog/130944.yaml
julian-elastic Jul 21, 2025
55b9246
Fix failing UTs
julian-elastic Jul 21, 2025
94909ec
Fix failing UT
julian-elastic Jul 21, 2025
2444673
Merge branch 'main' into foldable
julian-elastic Jul 21, 2025
1d730a5
Fix UT error
julian-elastic Jul 21, 2025
6714b8b
Update docs/changelog/130944.yaml
julian-elastic Jul 21, 2025
094aa80
Address code review feedback
julian-elastic Jul 23, 2025
50918a4
Fix checkstyle
julian-elastic Jul 23, 2025
5012a2e
Merge branch 'main' into foldable
julian-elastic Jul 23, 2025
09bbf06
Address code review comments
julian-elastic Jul 24, 2025
9fce42b
Merge branch 'main' into foldable
julian-elastic Jul 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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.xpack.esql.EsqlIllegalArgumentException;
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 {
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
* We will do a more thorough check in the postOptimizationVerification once folding is done.
*/
public static Expression.TypeResolution resolveTypeLimit(Expression limitField, String sourceText) {
if (limitField == null) {
return new Expression.TypeResolution(
format(null, "Limit must be a constant integer in [{}], found [{}]", sourceText, limitField)
);
}
if (limitField instanceof Literal literal) {
if (literal.value() == null) {
return new Expression.TypeResolution(
format(null, "Limit must be a constant integer in [{}], found [{}]", sourceText, limitField)
);
}
int value = (Integer) literal.value();
if (value <= 0) {
return new Expression.TypeResolution(format(null, "Limit must be greater than 0 in [{}], found [{}]", sourceText, value));
}
}
return Expression.TypeResolution.TYPE_RESOLVED;
}

public static void postOptimizationVerificationLimit(Failures failures, Expression limitField, String sourceText) {
if (limitField == null) {
failures.add(fail(limitField, "Limit must be a constant integer in [{}], found [{}]", sourceText, limitField));
}
if (limitField instanceof Literal literal) {
int value = (Integer) literal.value();
if (value <= 0) {
failures.add(fail(limitField, "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
failures.add(fail(limitField, "Limit must be a constant integer in [{}], found [{}]", sourceText, limitField));
}
}

public static Expression.TypeResolution resolveTypeQuery(Expression queryField, String sourceText) {
if (queryField == null) {
return new Expression.TypeResolution(format(null, "Query must be a valid string in [{}], found [{}]", sourceText, queryField));
}
if (queryField instanceof Literal literal) {
if (literal.value() == null) {
return new Expression.TypeResolution(
format(null, "Query value cannot be null in [{}], but got [{}]", sourceText, queryField)
);
}
}
return Expression.TypeResolution.TYPE_RESOLVED;
}

public static void postOptimizationVerificationQuery(Failures failures, Expression queryField, String sourceText) {
if (queryField == null) {
failures.add(fail(queryField, "Query must be a valid string in [{}], found [{}]", sourceText, queryField));
}
if (queryField instanceof Literal literal) {
if (literal.value() == null) {
failures.add(fail(queryField, "Invalid query value in [{}], found [{}]", sourceText, literal.value()));
}
} else {
// it is expected that the expression is a literal after folding
// we fail if it is not a literal
failures.add(fail(queryField, "Query must be a valid string in [{}], found [{}]", sourceText, queryField));
}
}

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)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,32 @@
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;
import org.elasticsearch.xpack.esql.core.type.DataType;
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;

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.postOptimizationVerificationLimit;
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(
Expand Down Expand Up @@ -110,14 +113,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());
if (result.equals(TypeResolution.TYPE_RESOLVED) == false) {
return result;
}
return TypeResolution.TYPE_RESOLVED;
}
Expand Down Expand Up @@ -164,11 +167,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) {
postOptimizationVerificationLimit(failures, limitField(), sourceText());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,6 +31,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.io.stream.PlanStreamInput;
import org.elasticsearch.xpack.esql.planner.ToAggregator;
Expand All @@ -39,14 +41,15 @@

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;

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";
Expand Down Expand Up @@ -116,16 +119,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
Expand All @@ -148,31 +153,88 @@ 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;
}

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)
);
result = resolveTypeOrder();
if (result.equals(TypeResolution.TYPE_RESOLVED) == false) {
return result;
}
return TypeResolution.TYPE_RESOLVED;
}

/**
* We check that the limit is not null and that if it is a literal, it is a positive integer
* We will do a more thorough check in the postOptimizationVerification once folding is done.
*/
private TypeResolution resolveTypeLimit() {
return FunctionUtils.resolveTypeLimit(limitField(), sourceText());
}

/**
* 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".
* We will do a more thorough check in the postOptimizationVerification once folding is done.
*/
private TypeResolution resolveTypeOrder() {
Expression order = orderField();
if (order == null) {
return new TypeResolution(format(null, "Order must be a valid string in [{}], found [{}]", sourceText(), order));
}
if (order instanceof Literal literal) {
if (literal.value() == null) {
return new TypeResolution(
format(null, "Invalid order value in [{}], expected [{}, {}] but got [{}]", sourceText(), ORDER_ASC, ORDER_DESC, order)
);
}
String value = BytesRefs.toString(literal.value());
if (value == null || value.equalsIgnoreCase(ORDER_ASC) == false && value.equalsIgnoreCase(ORDER_DESC) == false) {
return new TypeResolution(
format(null, "Invalid order value in [{}], expected [{}, {}] but got [{}]", sourceText(), ORDER_ASC, ORDER_DESC, order)
);
}
}
return TypeResolution.TYPE_RESOLVED;
}

@Override
public void postOptimizationVerification(Failures failures) {
postOptimizationVerificationLimit(failures);
postOptimizationVerificationOrder(failures);
}

private void postOptimizationVerificationLimit(Failures failures) {
FunctionUtils.postOptimizationVerificationLimit(failures, limitField(), sourceText());
}

private void postOptimizationVerificationOrder(Failures failures) {
Expression order = orderField();
if (order == null) {
failures.add(fail(order, "Order must be a valid string in [{}], found [{}]", sourceText(), order));
}
if (order instanceof Literal literal) {
String value = BytesRefs.toString(literal.value());
if (value == null || value.equalsIgnoreCase(ORDER_ASC) == false && value.equalsIgnoreCase(ORDER_DESC) == false) {
failures.add(
fail(order, "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
failures.add(fail(order, "Order must be a valid string in [{}], found [{}]", sourceText(), order));
}
}

@Override
public DataType dataType() {
return field().dataType().noText();
Expand Down Expand Up @@ -215,15 +277,20 @@ public AggregatorFunctionSupplier supplier() {
@Override
public Expression surrogate() {
var s = source();

if (limitValue() == 1) {
if (orderValue()) {
return new Min(s, field());
} else {
return new Max(s, field());
try {
if (limitValue() == 1) {
if (orderValue()) {
return new Min(s, field());
} else {
return new Max(s, field());
}
}
} catch (EsqlIllegalArgumentException e) {
// If the limit is not a literal or is not a positive integer, we cannot create a surrogate
// so we return null to indicate that no surrogate can be created.
// This is possible if the limit is an expression, and folding has not been done yet.
return null;
}

return null;
}
}
Loading