diff --git a/docs/reference/esql/functions/kibana/definition/like.json b/docs/reference/esql/functions/kibana/definition/like.json index 2fcb29622efbd..4a26dca276696 100644 --- a/docs/reference/esql/functions/kibana/definition/like.json +++ b/docs/reference/esql/functions/kibana/definition/like.json @@ -1,6 +1,7 @@ { "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.", - "type" : "eval", + "type" : "operator", + "operator" : "LIKE", "name" : "like", "description" : "Use `LIKE` to filter data based on string patterns using wildcards. `LIKE`\nusually acts on a field placed on the left-hand side of the operator, but it can\nalso act on a constant (literal) expression. The right-hand side of the operator\nrepresents the pattern.\n\nThe following wildcard characters are supported:\n\n* `*` matches zero or more characters.\n* `?` matches one character.", "signatures" : [ diff --git a/docs/reference/esql/functions/kibana/definition/match.json b/docs/reference/esql/functions/kibana/definition/match.json index 23e5fd6f51574..7f2a8239cc0d0 100644 --- a/docs/reference/esql/functions/kibana/definition/match.json +++ b/docs/reference/esql/functions/kibana/definition/match.json @@ -1,7 +1,6 @@ { "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.", - "type" : "operator", - "operator" : ":", + "type" : "eval", "name" : "match", "description" : "Performs a <> on the specified field. Returns true if the provided query matches the row.", "signatures" : [ diff --git a/docs/reference/esql/functions/kibana/definition/match_operator.json b/docs/reference/esql/functions/kibana/definition/match_operator.json index 665071aed55c4..a67c6b0e45c4a 100644 --- a/docs/reference/esql/functions/kibana/definition/match_operator.json +++ b/docs/reference/esql/functions/kibana/definition/match_operator.json @@ -3,7 +3,7 @@ "type" : "operator", "operator" : ":", "name" : "match_operator", - "description" : "Use `MATCH` to perform a <> on the specified field.\nUsing `MATCH` is equivalent to using the `match` query in the Elasticsearch Query DSL.\n\nMatch can be used on fields from the text family like <> and <>,\nas well as other field types like keyword, boolean, dates, and numeric types.\n\nFor a simplified syntax, you can use the <> `:` operator instead of `MATCH`.\n\n`MATCH` returns true if the provided query matches the row.", + "description" : "Use the match operator (`:`) to perform a <> on the specified field.\nUsing `:` is equivalent to using the `match` query in the Elasticsearch Query DSL.\n\nThe match operator is equivalent to the <>.\n\nFor using the function syntax, or adding <>, you can use the\n<>.\n\n`:` returns true if the provided query matches the row.", "signatures" : [ { "params" : [ diff --git a/docs/reference/esql/functions/kibana/definition/rlike.json b/docs/reference/esql/functions/kibana/definition/rlike.json index 47cbd7800c821..88d6c7d6bb9b2 100644 --- a/docs/reference/esql/functions/kibana/definition/rlike.json +++ b/docs/reference/esql/functions/kibana/definition/rlike.json @@ -1,6 +1,7 @@ { "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.", - "type" : "eval", + "type" : "operator", + "operator" : "RLIKE", "name" : "rlike", "description" : "Use `RLIKE` to filter data based on string patterns using using\n<>. `RLIKE` usually acts on a field placed on\nthe left-hand side of the operator, but it can also act on a constant (literal)\nexpression. The right-hand side of the operator represents the pattern.", "signatures" : [ diff --git a/docs/reference/esql/functions/kibana/docs/match_operator.md b/docs/reference/esql/functions/kibana/docs/match_operator.md index 98f55aacde0b8..0624329182f3a 100644 --- a/docs/reference/esql/functions/kibana/docs/match_operator.md +++ b/docs/reference/esql/functions/kibana/docs/match_operator.md @@ -3,15 +3,15 @@ This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../READ --> ### MATCH_OPERATOR -Use `MATCH` to perform a <> on the specified field. -Using `MATCH` is equivalent to using the `match` query in the Elasticsearch Query DSL. +Use the match operator (`:`) to perform a <> on the specified field. +Using `:` is equivalent to using the `match` query in the Elasticsearch Query DSL. -Match can be used on fields from the text family like <> and <>, -as well as other field types like keyword, boolean, dates, and numeric types. +The match operator is equivalent to the <>. -For a simplified syntax, you can use the <> `:` operator instead of `MATCH`. +For using the function syntax, or adding <>, you can use the +<>. -`MATCH` returns true if the provided query matches the row. +`:` returns true if the provided query matches the row. ``` FROM books diff --git a/docs/reference/esql/functions/search.asciidoc b/docs/reference/esql/functions/search.asciidoc index ba399ead8adfc..1be4dee58e234 100644 --- a/docs/reference/esql/functions/search.asciidoc +++ b/docs/reference/esql/functions/search.asciidoc @@ -11,10 +11,13 @@ Returns true if the provided query matches the row. The match operator is equivalent to the <>. +For using the function syntax, or adding <>, you can use the +<>. + [.text-center] image::esql/functions/signature/match_operator.svg[Embedded,opts=inline] -include::types/match.asciidoc[] +include::types/match_operator.asciidoc[] [source.merge.styled,esql] ---- diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/AbstractMatchFullTextFunction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/AbstractMatchFullTextFunction.java new file mode 100644 index 0000000000000..86f1f6e30108c --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/AbstractMatchFullTextFunction.java @@ -0,0 +1,219 @@ +/* + * 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.fulltext; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.xpack.esql.capabilities.PostOptimizationVerificationAware; +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.FieldAttribute; +import org.elasticsearch.xpack.esql.core.expression.FoldContext; +import org.elasticsearch.xpack.esql.core.querydsl.query.Query; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.core.type.MultiTypeEsField; +import org.elasticsearch.xpack.esql.core.util.NumericUtils; +import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction; +import org.elasticsearch.xpack.esql.planner.TranslatorHandler; +import org.elasticsearch.xpack.esql.querydsl.query.MatchQuery; +import org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal; +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.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; +import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_NANOS; +import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE; +import static org.elasticsearch.xpack.esql.core.type.DataType.INTEGER; +import static org.elasticsearch.xpack.esql.core.type.DataType.IP; +import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD; +import static org.elasticsearch.xpack.esql.core.type.DataType.LONG; +import static org.elasticsearch.xpack.esql.core.type.DataType.SEMANTIC_TEXT; +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.predicate.operator.comparison.EsqlBinaryComparison.formatIncompatibleTypesMessage; + +/** + * This class contains the common functionalities between the match function ({@link Match}) and match operator ({@link MatchOperator}), + * so the two subclasses just contains the different code + */ +public abstract class AbstractMatchFullTextFunction extends FullTextFunction implements PostOptimizationVerificationAware { + public static final Set FIELD_DATA_TYPES = Set.of( + KEYWORD, + TEXT, + SEMANTIC_TEXT, + BOOLEAN, + DATETIME, + DATE_NANOS, + DOUBLE, + INTEGER, + IP, + LONG, + UNSIGNED_LONG, + VERSION + ); + public static final Set QUERY_DATA_TYPES = Set.of( + KEYWORD, + BOOLEAN, + DATETIME, + DATE_NANOS, + DOUBLE, + INTEGER, + IP, + LONG, + UNSIGNED_LONG, + VERSION + ); + protected final Expression field; + + protected AbstractMatchFullTextFunction( + Source source, + Expression query, + List children, + QueryBuilder queryBuilder, + Expression field + ) { + super(source, query, children, queryBuilder); + this.field = field; + } + + public Expression field() { + return field; + } + + @Override + protected TypeResolution resolveNonQueryParamTypes() { + return isNotNull(field, sourceText(), FIRST).and( + isType( + field, + FIELD_DATA_TYPES::contains, + sourceText(), + FIRST, + "keyword, text, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version" + ) + ); + } + + @Override + protected TypeResolution resolveQueryParamType() { + return isType( + query(), + QUERY_DATA_TYPES::contains, + sourceText(), + queryParamOrdinal(), + "keyword, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version" + ).and(isNotNullAndFoldable(query(), sourceText(), queryParamOrdinal())); + } + + @Override + protected TypeResolution checkParamCompatibility() { + DataType fieldType = field().dataType(); + DataType queryType = query().dataType(); + + // Field and query types should match. If the query is a string, then it can match any field type. + if ((fieldType == queryType) || (queryType == KEYWORD)) { + return TypeResolution.TYPE_RESOLVED; + } + + if (fieldType.isNumeric() && queryType.isNumeric()) { + // When doing an unsigned long query, field must be an unsigned long + if ((queryType == UNSIGNED_LONG && fieldType != UNSIGNED_LONG) == false) { + return TypeResolution.TYPE_RESOLVED; + } + } + + return new TypeResolution(formatIncompatibleTypesMessage(fieldType, queryType, sourceText())); + } + + @Override + public void postOptimizationVerification(Failures failures) { + Expression fieldExpression = field(); + // Field may be converted to other data type (field_name :: data_type), so we need to check the original field + if (fieldExpression instanceof AbstractConvertFunction convertFunction) { + fieldExpression = convertFunction.field(); + } + if (fieldExpression instanceof FieldAttribute == false) { + failures.add( + Failure.fail( + field, + "[{}] {} cannot operate on [{}], which is not a field from an index mapping", + functionName(), + functionType(), + field.sourceText() + ) + ); + } + } + + @Override + public Object queryAsObject() { + Object queryAsObject = query().fold(FoldContext.small() /* TODO remove me */); + + // Convert BytesRef to string for string-based values + if (queryAsObject instanceof BytesRef bytesRef) { + return switch (query().dataType()) { + case IP -> EsqlDataTypeConverter.ipToString(bytesRef); + case VERSION -> EsqlDataTypeConverter.versionToString(bytesRef); + default -> bytesRef.utf8ToString(); + }; + } + + // Converts specific types to the correct type for the query + if (query().dataType() == DataType.UNSIGNED_LONG) { + return NumericUtils.unsignedLongAsBigInteger((Long) queryAsObject); + } else if (query().dataType() == DataType.DATETIME && queryAsObject instanceof Long) { + // When casting to date and datetime, we get a long back. But Match query needs a date string + return EsqlDataTypeConverter.dateTimeToString((Long) queryAsObject); + } else if (query().dataType() == DATE_NANOS && queryAsObject instanceof Long) { + return EsqlDataTypeConverter.nanoTimeToString((Long) queryAsObject); + } + + return queryAsObject; + } + + @Override + protected Query translate(TranslatorHandler handler) { + Expression fieldExpression = field; + // Field may be converted to other data type (field_name :: data_type), so we need to check the original field + if (fieldExpression instanceof AbstractConvertFunction convertFunction) { + fieldExpression = convertFunction.field(); + } + if (fieldExpression instanceof FieldAttribute fieldAttribute) { + String fieldName = fieldAttribute.name(); + if (fieldAttribute.field() instanceof MultiTypeEsField multiTypeEsField) { + // If we have multiple field types, we allow the query to be done, but getting the underlying field name + fieldName = multiTypeEsField.getName(); + } + // Make query lenient so mixed field types can be queried when a field type is incompatible with the value provided + return new MatchQuery(source(), fieldName, queryAsObject(), Map.of("lenient", "true")); + } + + throw new IllegalArgumentException("Match must have a field attribute as the first argument"); + } + + @Override + public Expression replaceQueryBuilder(QueryBuilder queryBuilder) { + return new Match(source(), field, query(), queryBuilder); + } + + protected ParamOrdinal queryParamOrdinal() { + return SECOND; + } + +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextWritables.java index a3a14004b1c89..f42433c22e775 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextWritables.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextWritables.java @@ -25,6 +25,7 @@ public static List getNamedWriteables() { entries.add(MultiMatchQueryPredicate.ENTRY); entries.add(QueryString.ENTRY); entries.add(Match.ENTRY); + entries.add(MatchOperator.ENTRY); entries.add(Kql.ENTRY); if (EsqlCapabilities.Cap.TERM_FUNCTION.isEnabled()) { 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 20b22daed3857..5406997097927 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 @@ -7,101 +7,33 @@ package org.elasticsearch.xpack.esql.expression.function.fulltext; -import org.apache.lucene.util.BytesRef; import org.elasticsearch.TransportVersions; 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.capabilities.PostOptimizationVerificationAware; -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.FieldAttribute; -import org.elasticsearch.xpack.esql.core.expression.FoldContext; -import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; -import org.elasticsearch.xpack.esql.core.querydsl.query.Query; -import org.elasticsearch.xpack.esql.core.querydsl.query.QueryStringQuery; 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.core.type.MultiTypeEsField; -import org.elasticsearch.xpack.esql.core.util.NumericUtils; import org.elasticsearch.xpack.esql.expression.function.Example; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; import org.elasticsearch.xpack.esql.expression.function.Param; -import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction; import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; -import org.elasticsearch.xpack.esql.planner.TranslatorHandler; -import org.elasticsearch.xpack.esql.querydsl.query.MatchQuery; -import org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter; import java.io.IOException; import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -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.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; -import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_NANOS; -import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE; -import static org.elasticsearch.xpack.esql.core.type.DataType.INTEGER; -import static org.elasticsearch.xpack.esql.core.type.DataType.IP; -import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD; -import static org.elasticsearch.xpack.esql.core.type.DataType.LONG; -import static org.elasticsearch.xpack.esql.core.type.DataType.SEMANTIC_TEXT; -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.predicate.operator.comparison.EsqlBinaryComparison.formatIncompatibleTypesMessage; /** - * Full text function that performs a {@link QueryStringQuery} . + * Full text function that performs a {@link org.elasticsearch.xpack.esql.querydsl.query.MatchQuery} . */ -public class Match extends FullTextFunction implements PostOptimizationVerificationAware { +public class Match extends AbstractMatchFullTextFunction { public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Match", Match::readFrom); - private final Expression field; - private transient Boolean isOperator; - public static final Set FIELD_DATA_TYPES = Set.of( - KEYWORD, - TEXT, - SEMANTIC_TEXT, - BOOLEAN, - DATETIME, - DATE_NANOS, - DOUBLE, - INTEGER, - IP, - LONG, - UNSIGNED_LONG, - VERSION - ); - public static final Set QUERY_DATA_TYPES = Set.of( - KEYWORD, - BOOLEAN, - DATETIME, - DATE_NANOS, - DOUBLE, - INTEGER, - IP, - LONG, - UNSIGNED_LONG, - VERSION - ); - @FunctionInfo( returnType = "boolean", - operator = ":", preview = true, description = "Performs a <> on the specified field. " + "Returns true if the provided query matches the row.", @@ -124,8 +56,7 @@ public Match( } public Match(Source source, Expression field, Expression matchQuery, QueryBuilder queryBuilder) { - super(source, matchQuery, List.of(field, matchQuery), queryBuilder); - this.field = field; + super(source, matchQuery, List.of(field, matchQuery), queryBuilder, field); } private static Match readFrom(StreamInput in) throws IOException { @@ -154,96 +85,6 @@ public String getWriteableName() { return ENTRY.name; } - @Override - protected TypeResolution resolveNonQueryParamTypes() { - return isNotNull(field, sourceText(), FIRST).and( - isType( - field, - FIELD_DATA_TYPES::contains, - sourceText(), - FIRST, - "keyword, text, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version" - ) - ); - } - - @Override - protected TypeResolution resolveQueryParamType() { - return isType( - query(), - QUERY_DATA_TYPES::contains, - sourceText(), - queryParamOrdinal(), - "keyword, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version" - ).and(isNotNullAndFoldable(query(), sourceText(), queryParamOrdinal())); - } - - @Override - protected TypeResolution checkParamCompatibility() { - DataType fieldType = field().dataType(); - DataType queryType = query().dataType(); - - // Field and query types should match. If the query is a string, then it can match any field type. - if ((fieldType == queryType) || (queryType == KEYWORD)) { - return TypeResolution.TYPE_RESOLVED; - } - - if (fieldType.isNumeric() && queryType.isNumeric()) { - // When doing an unsigned long query, field must be an unsigned long - if ((queryType == UNSIGNED_LONG && fieldType != UNSIGNED_LONG) == false) { - return TypeResolution.TYPE_RESOLVED; - } - } - - return new TypeResolution(formatIncompatibleTypesMessage(fieldType, queryType, sourceText())); - } - - @Override - public void postOptimizationVerification(Failures failures) { - Expression fieldExpression = field(); - // Field may be converted to other data type (field_name :: data_type), so we need to check the original field - if (fieldExpression instanceof AbstractConvertFunction convertFunction) { - fieldExpression = convertFunction.field(); - } - if (fieldExpression instanceof FieldAttribute == false) { - failures.add( - Failure.fail( - field, - "[{}] {} cannot operate on [{}], which is not a field from an index mapping", - functionName(), - functionType(), - field.sourceText() - ) - ); - } - } - - @Override - public Object queryAsObject() { - Object queryAsObject = query().fold(FoldContext.small() /* TODO remove me */); - - // Convert BytesRef to string for string-based values - if (queryAsObject instanceof BytesRef bytesRef) { - return switch (query().dataType()) { - case IP -> EsqlDataTypeConverter.ipToString(bytesRef); - case VERSION -> EsqlDataTypeConverter.versionToString(bytesRef); - default -> bytesRef.utf8ToString(); - }; - } - - // Converts specific types to the correct type for the query - if (query().dataType() == DataType.UNSIGNED_LONG) { - return NumericUtils.unsignedLongAsBigInteger((Long) queryAsObject); - } else if (query().dataType() == DataType.DATETIME && queryAsObject instanceof Long) { - // When casting to date and datetime, we get a long back. But Match query needs a date string - return EsqlDataTypeConverter.dateTimeToString((Long) queryAsObject); - } else if (query().dataType() == DATE_NANOS && queryAsObject instanceof Long) { - return EsqlDataTypeConverter.nanoTimeToString((Long) queryAsObject); - } - - return queryAsObject; - } - @Override public Expression replaceChildren(List newChildren) { return new Match(source(), newChildren.get(0), newChildren.get(1), queryBuilder()); @@ -251,56 +92,6 @@ public Expression replaceChildren(List newChildren) { @Override protected NodeInfo info() { - return NodeInfo.create(this, Match::new, field, query(), queryBuilder()); - } - - protected TypeResolutions.ParamOrdinal queryParamOrdinal() { - return SECOND; - } - - public Expression field() { - return field; - } - - @Override - public String functionType() { - return isOperator() ? "operator" : super.functionType(); - } - - @Override - protected Query translate(TranslatorHandler handler) { - Expression fieldExpression = field; - // Field may be converted to other data type (field_name :: data_type), so we need to check the original field - if (fieldExpression instanceof AbstractConvertFunction convertFunction) { - fieldExpression = convertFunction.field(); - } - if (fieldExpression instanceof FieldAttribute fieldAttribute) { - String fieldName = fieldAttribute.name(); - if (fieldAttribute.field() instanceof MultiTypeEsField multiTypeEsField) { - // If we have multiple field types, we allow the query to be done, but getting the underlying field name - fieldName = multiTypeEsField.getName(); - } - // Make query lenient so mixed field types can be queried when a field type is incompatible with the value provided - return new MatchQuery(source(), fieldName, queryAsObject(), Map.of("lenient", "true")); - } - - throw new IllegalArgumentException("Match must have a field attribute as the first argument"); - } - - @Override - public Expression replaceQueryBuilder(QueryBuilder queryBuilder) { - return new Match(source(), field, query(), queryBuilder); - } - - @Override - public String functionName() { - return isOperator() ? ":" : super.functionName(); - } - - private boolean isOperator() { - if (isOperator == null) { - isOperator = source().text().toUpperCase(Locale.ROOT).matches("^" + super.functionName() + "\\s*\\(.*\\)") == false; - } - return isOperator; + return NodeInfo.create(this, Match::new, field(), query(), queryBuilder()); } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperator.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperator.java new file mode 100644 index 0000000000000..e3e4bc4678089 --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperator.java @@ -0,0 +1,98 @@ +/* + * 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.fulltext; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.tree.NodeInfo; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.expression.function.Example; +import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; +import org.elasticsearch.xpack.esql.expression.function.Param; +import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; + +import java.io.IOException; +import java.util.List; + +/** + * This class performs a {@link org.elasticsearch.xpack.esql.querydsl.query.MatchQuery} using an operator. + */ +public class MatchOperator extends Match { + + public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry( + Expression.class, + "MatchOperator", + MatchOperator::readFrom + ); + + @FunctionInfo( + returnType = "boolean", + operator = ":", + preview = true, + description = """ + Use the match operator (`:`) to perform a <> on the specified field. + Using `:` is equivalent to using the `match` query in the Elasticsearch Query DSL. + + The match operator is equivalent to the <>. + + For using the function syntax, or adding <>, you can use the + <>. + + `:` returns true if the provided query matches the row.""", + examples = { @Example(file = "match-function", tag = "match-with-field") } + ) + public MatchOperator( + Source source, + @Param( + name = "field", + type = { "keyword", "text", "boolean", "date", "date_nanos", "double", "integer", "ip", "long", "unsigned_long", "version" }, + description = "Field that the query will target." + ) Expression field, + @Param( + name = "query", + type = { "keyword", "boolean", "date", "date_nanos", "double", "integer", "ip", "long", "unsigned_long", "version" }, + description = "Value to find in the provided field." + ) Expression matchQuery + ) { + super(source, field, matchQuery); + } + + private static Match readFrom(StreamInput in) throws IOException { + Source source = Source.readFrom((PlanStreamInput) in); + Expression field = in.readNamedWriteable(Expression.class); + Expression query = in.readNamedWriteable(Expression.class); + + return new MatchOperator(source, field, query); + } + + @Override + public String functionType() { + return "operator"; + } + + @Override + public String functionName() { + return ":"; + } + + @Override + public String getWriteableName() { + return ENTRY.name; + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, MatchOperator::new, field(), query()); + } + + @Override + public Expression replaceChildren(List newChildren) { + return new MatchOperator(source(), newChildren.get(0), newChildren.get(1)); + } +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLike.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLike.java index 688cbbb992443..76facd2631001 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLike.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLike.java @@ -58,7 +58,7 @@ Matching special characters (eg. `.`, `*`, `(`...) will require escaping. ---- include::{esql-specs}/string.csv-spec[tag=rlikeEscapingTripleQuotes] ---- - """, examples = @Example(file = "docs", tag = "rlike")) + """, operator = "RLIKE", examples = @Example(file = "docs", tag = "rlike")) public RLike( Source source, @Param(name = "str", type = { "keyword", "text" }, description = "A literal value.") Expression value, diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/WildcardLike.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/WildcardLike.java index 8c596ee032bee..da0c0e47fbc52 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/WildcardLike.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/WildcardLike.java @@ -69,7 +69,7 @@ also act on a constant (literal) expression. The right-hand side of the operator ---- include::{esql-specs}/string.csv-spec[tag=likeEscapingTripleQuotes] ---- - """, examples = @Example(file = "docs", tag = "like")) + """, operator = "LIKE", examples = @Example(file = "docs", tag = "like")) public WildcardLike( Source source, @Param(name = "str", type = { "keyword", "text" }, description = "A literal expression.") Expression left, diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java index e21bd3a17af17..b34c11949fe2c 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java @@ -41,7 +41,7 @@ import org.elasticsearch.xpack.esql.expression.function.FunctionResolutionStrategy; import org.elasticsearch.xpack.esql.expression.function.UnresolvedFunction; import org.elasticsearch.xpack.esql.expression.function.aggregate.FilteredExpression; -import org.elasticsearch.xpack.esql.expression.function.fulltext.Match; +import org.elasticsearch.xpack.esql.expression.function.fulltext.MatchOperator; import org.elasticsearch.xpack.esql.expression.function.scalar.string.RLike; import org.elasticsearch.xpack.esql.expression.function.scalar.string.WildcardLike; import org.elasticsearch.xpack.esql.expression.predicate.logical.And; @@ -984,6 +984,6 @@ public Expression visitMatchBooleanExpression(EsqlBaseParser.MatchBooleanExpress matchFieldExpression = expression(ctx.fieldExp); } - return new Match(source(ctx), matchFieldExpression, expression(ctx.matchQuery)); + return new MatchOperator(source(ctx), matchFieldExpression, expression(ctx.matchQuery)); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java index 71d68c8422f45..1b8122bb2f29d 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java @@ -44,7 +44,7 @@ import org.elasticsearch.xpack.esql.core.util.StringUtils; import org.elasticsearch.xpack.esql.evaluator.EvalMapper; import org.elasticsearch.xpack.esql.evaluator.mapper.EvaluatorMapper; -import org.elasticsearch.xpack.esql.expression.function.fulltext.Match; +import org.elasticsearch.xpack.esql.expression.function.fulltext.MatchOperator; import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Greatest; import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce; import org.elasticsearch.xpack.esql.expression.function.scalar.string.RLike; @@ -139,7 +139,7 @@ public abstract class AbstractFunctionTestCase extends ESTestCase { entry("is_null", IsNull.class), entry("is_not_null", IsNotNull.class), // Match operator is both a function and an operator - entry("match_operator", Match.class) + entry("match_operator", MatchOperator.class) ); private static EsqlFunctionRegistry functionRegistry = new EsqlFunctionRegistry().snapshotRegistry(); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/AbstractMatchFullTextFunctionTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/AbstractMatchFullTextFunctionTests.java new file mode 100644 index 0000000000000..0a80da9c60625 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/AbstractMatchFullTextFunctionTests.java @@ -0,0 +1,365 @@ +/* + * 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.fulltext; + +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.core.util.NumericUtils; +import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase; +import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.stringCases; +import static org.hamcrest.Matchers.equalTo; + +public abstract class AbstractMatchFullTextFunctionTests extends AbstractFunctionTestCase { + @ParametersFactory + public static Iterable parameters() { + List suppliers = new ArrayList<>(); + + AbstractMatchFullTextFunctionTests.addUnsignedLongCases(suppliers); + AbstractMatchFullTextFunctionTests.addNumericCases(suppliers); + AbstractMatchFullTextFunctionTests.addNonNumericCases(suppliers); + AbstractMatchFullTextFunctionTests.addQueryAsStringTestCases(suppliers); + AbstractMatchFullTextFunctionTests.addStringTestCases(suppliers); + + return parameterSuppliersFromTypedData(suppliers); + } + + private static void addNonNumericCases(List suppliers) { + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.booleanCases(), + TestCaseSupplier.booleanCases(), + List.of(), + false + ) + ); + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.ipCases(), + TestCaseSupplier.ipCases(), + List.of(), + false + ) + ); + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.versionCases(""), + TestCaseSupplier.versionCases(""), + List.of(), + false + ) + ); + // Datetime + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.dateCases(), + TestCaseSupplier.dateCases(), + List.of(), + false + ) + ); + + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.dateNanosCases(), + TestCaseSupplier.dateNanosCases(), + List.of(), + false + ) + ); + } + + private static void addNumericCases(List suppliers) { + suppliers.addAll( + TestCaseSupplier.forBinaryComparisonWithWidening( + new TestCaseSupplier.NumericTypeTestConfigs<>( + new TestCaseSupplier.NumericTypeTestConfig<>( + (Integer.MIN_VALUE >> 1) - 1, + (Integer.MAX_VALUE >> 1) - 1, + (l, r) -> true, + "EqualsIntsEvaluator" + ), + new TestCaseSupplier.NumericTypeTestConfig<>( + (Long.MIN_VALUE >> 1) - 1, + (Long.MAX_VALUE >> 1) - 1, + (l, r) -> true, + "EqualsLongsEvaluator" + ), + new TestCaseSupplier.NumericTypeTestConfig<>( + Double.NEGATIVE_INFINITY, + Double.POSITIVE_INFINITY, + // NB: this has different behavior than Double::equals + (l, r) -> true, + "EqualsDoublesEvaluator" + ) + ), + "field", + "query", + (lhs, rhs) -> List.of(), + false + ) + ); + } + + private static void addUnsignedLongCases(List suppliers) { + // TODO: These should be integrated into the type cross product above, but are currently broken + // see https://github.com/elastic/elasticsearch/issues/102935 + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), + TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), + List.of(), + false + ) + ); + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), + TestCaseSupplier.intCases(Integer.MIN_VALUE, Integer.MAX_VALUE, true), + List.of(), + false + ) + ); + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), + TestCaseSupplier.longCases(Long.MIN_VALUE, Long.MAX_VALUE, true), + List.of(), + false + ) + ); + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), + TestCaseSupplier.doubleCases(Double.MIN_VALUE, Double.MAX_VALUE, true), + List.of(), + false + ) + ); + } + + private static void addQueryAsStringTestCases(List suppliers) { + + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.intCases(Integer.MIN_VALUE, Integer.MAX_VALUE, true), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.intCases(Integer.MIN_VALUE, Integer.MAX_VALUE, true), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.longCases(Integer.MIN_VALUE, Integer.MAX_VALUE, true), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.doubleCases(Double.MIN_VALUE, Double.MAX_VALUE, true), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + + // Unsigned Long cases + // TODO: These should be integrated into the type cross product above, but are currently broken + // see https://github.com/elastic/elasticsearch/issues/102935 + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.booleanCases(), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.ipCases(), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.versionCases(""), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + // Datetime + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.dateCases(), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + + suppliers.addAll( + TestCaseSupplier.forBinaryNotCasting( + null, + "field", + "query", + Object::equals, + DataType.BOOLEAN, + TestCaseSupplier.dateNanosCases(), + TestCaseSupplier.stringCases(DataType.KEYWORD), + List.of(), + false + ) + ); + } + + private static void addStringTestCases(List suppliers) { + for (DataType fieldType : DataType.stringTypes()) { + if (DataType.UNDER_CONSTRUCTION.containsKey(fieldType)) { + continue; + } + for (TestCaseSupplier.TypedDataSupplier queryDataSupplier : stringCases(fieldType)) { + suppliers.add( + TestCaseSupplier.testCaseSupplier( + queryDataSupplier, + new TestCaseSupplier.TypedDataSupplier(fieldType.typeName(), () -> randomAlphaOfLength(10), DataType.KEYWORD), + (d1, d2) -> equalTo("string"), + DataType.BOOLEAN, + (o1, o2) -> true + ) + ); + } + } + } + + public final void testLiteralExpressions() { + Expression expression = buildLiteralExpression(testCase); + assertFalse("expected resolved", expression.typeResolved().unresolved()); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchErrorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchErrorTests.java index 1f4e8e40a8259..a83cb24a44a45 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchErrorTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchErrorTests.java @@ -26,7 +26,7 @@ public class MatchErrorTests extends ErrorsForCasesWithoutExamplesTestCase { @Override protected List cases() { - return paramsToSuppliers(MatchTests.parameters()); + return paramsToSuppliers(AbstractMatchFullTextFunctionTests.parameters()); } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperatorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperatorTests.java index 951aff80541bd..78ea3f5451880 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperatorTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperatorTests.java @@ -10,9 +10,12 @@ import com.carrotsearch.randomizedtesting.annotations.Name; import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.expression.function.FunctionName; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; +import java.util.List; import java.util.function.Supplier; /** @@ -27,6 +30,11 @@ public MatchOperatorTests(@Name("TestCase") Supplier @ParametersFactory public static Iterable parameters() { - return MatchTests.parameters(); + return AbstractMatchFullTextFunctionTests.parameters(); + } + + @Override + protected Expression build(Source source, List args) { + return new MatchOperator(source, args.get(0), args.get(1)); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchTests.java index cb0c9b263b547..4280ab487f213 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchTests.java @@ -12,22 +12,14 @@ 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.core.util.NumericUtils; -import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase; import org.elasticsearch.xpack.esql.expression.function.FunctionName; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; -import java.math.BigInteger; -import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; -import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.stringCases; -import static org.hamcrest.Matchers.equalTo; - @FunctionName("match") -public class MatchTests extends AbstractFunctionTestCase { +public class MatchTests extends AbstractMatchFullTextFunctionTests { public MatchTests(@Name("TestCase") Supplier testCaseSupplier) { this.testCase = testCaseSupplier.get(); @@ -35,342 +27,7 @@ public MatchTests(@Name("TestCase") Supplier testCase @ParametersFactory public static Iterable parameters() { - List suppliers = new ArrayList<>(); - - addUnsignedLongCases(suppliers); - addNumericCases(suppliers); - addNonNumericCases(suppliers); - addQueryAsStringTestCases(suppliers); - addStringTestCases(suppliers); - - return parameterSuppliersFromTypedData(suppliers); - } - - private static void addNonNumericCases(List suppliers) { - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.booleanCases(), - TestCaseSupplier.booleanCases(), - List.of(), - false - ) - ); - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.ipCases(), - TestCaseSupplier.ipCases(), - List.of(), - false - ) - ); - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.versionCases(""), - TestCaseSupplier.versionCases(""), - List.of(), - false - ) - ); - // Datetime - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.dateCases(), - TestCaseSupplier.dateCases(), - List.of(), - false - ) - ); - - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.dateNanosCases(), - TestCaseSupplier.dateNanosCases(), - List.of(), - false - ) - ); - } - - private static void addNumericCases(List suppliers) { - suppliers.addAll( - TestCaseSupplier.forBinaryComparisonWithWidening( - new TestCaseSupplier.NumericTypeTestConfigs<>( - new TestCaseSupplier.NumericTypeTestConfig<>( - (Integer.MIN_VALUE >> 1) - 1, - (Integer.MAX_VALUE >> 1) - 1, - (l, r) -> true, - "EqualsIntsEvaluator" - ), - new TestCaseSupplier.NumericTypeTestConfig<>( - (Long.MIN_VALUE >> 1) - 1, - (Long.MAX_VALUE >> 1) - 1, - (l, r) -> true, - "EqualsLongsEvaluator" - ), - new TestCaseSupplier.NumericTypeTestConfig<>( - Double.NEGATIVE_INFINITY, - Double.POSITIVE_INFINITY, - // NB: this has different behavior than Double::equals - (l, r) -> true, - "EqualsDoublesEvaluator" - ) - ), - "field", - "query", - (lhs, rhs) -> List.of(), - false - ) - ); - } - - private static void addUnsignedLongCases(List suppliers) { - // TODO: These should be integrated into the type cross product above, but are currently broken - // see https://github.com/elastic/elasticsearch/issues/102935 - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), - TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), - List.of(), - false - ) - ); - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), - TestCaseSupplier.intCases(Integer.MIN_VALUE, Integer.MAX_VALUE, true), - List.of(), - false - ) - ); - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), - TestCaseSupplier.longCases(Long.MIN_VALUE, Long.MAX_VALUE, true), - List.of(), - false - ) - ); - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), - TestCaseSupplier.doubleCases(Double.MIN_VALUE, Double.MAX_VALUE, true), - List.of(), - false - ) - ); - } - - private static void addQueryAsStringTestCases(List suppliers) { - - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.intCases(Integer.MIN_VALUE, Integer.MAX_VALUE, true), - TestCaseSupplier.stringCases(DataType.KEYWORD), - List.of(), - false - ) - ); - - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.intCases(Integer.MIN_VALUE, Integer.MAX_VALUE, true), - TestCaseSupplier.stringCases(DataType.KEYWORD), - List.of(), - false - ) - ); - - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.longCases(Integer.MIN_VALUE, Integer.MAX_VALUE, true), - TestCaseSupplier.stringCases(DataType.KEYWORD), - List.of(), - false - ) - ); - - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.doubleCases(Double.MIN_VALUE, Double.MAX_VALUE, true), - TestCaseSupplier.stringCases(DataType.KEYWORD), - List.of(), - false - ) - ); - - // Unsigned Long cases - // TODO: These should be integrated into the type cross product above, but are currently broken - // see https://github.com/elastic/elasticsearch/issues/102935 - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.ulongCases(BigInteger.ZERO, NumericUtils.UNSIGNED_LONG_MAX, true), - TestCaseSupplier.stringCases(DataType.KEYWORD), - List.of(), - false - ) - ); - - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.booleanCases(), - TestCaseSupplier.stringCases(DataType.KEYWORD), - List.of(), - false - ) - ); - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.ipCases(), - TestCaseSupplier.stringCases(DataType.KEYWORD), - List.of(), - false - ) - ); - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.versionCases(""), - TestCaseSupplier.stringCases(DataType.KEYWORD), - List.of(), - false - ) - ); - // Datetime - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.dateCases(), - TestCaseSupplier.stringCases(DataType.KEYWORD), - List.of(), - false - ) - ); - - suppliers.addAll( - TestCaseSupplier.forBinaryNotCasting( - null, - "field", - "query", - Object::equals, - DataType.BOOLEAN, - TestCaseSupplier.dateNanosCases(), - TestCaseSupplier.stringCases(DataType.KEYWORD), - List.of(), - false - ) - ); - } - - private static void addStringTestCases(List suppliers) { - for (DataType fieldType : DataType.stringTypes()) { - if (DataType.UNDER_CONSTRUCTION.containsKey(fieldType)) { - continue; - } - for (TestCaseSupplier.TypedDataSupplier queryDataSupplier : stringCases(fieldType)) { - suppliers.add( - TestCaseSupplier.testCaseSupplier( - queryDataSupplier, - new TestCaseSupplier.TypedDataSupplier(fieldType.typeName(), () -> randomAlphaOfLength(10), DataType.KEYWORD), - (d1, d2) -> equalTo("string"), - DataType.BOOLEAN, - (o1, o2) -> true - ) - ); - } - } - } - - public final void testLiteralExpressions() { - Expression expression = buildLiteralExpression(testCase); - assertFalse("expected resolved", expression.typeResolved().unresolved()); + return AbstractMatchFullTextFunctionTests.parameters(); } @Override