Match, Like and RLike operators improved docs (#120504)
This commit is contained in:
parent
8fc5a507c5
commit
f61f139653
|
@ -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" : [
|
||||
|
|
|
@ -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" : "Use `MATCH` to perform a <<query-dsl-match-query,match query>> 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 <<text, text>> and <<semantic-text, semantic_text>>,\nas well as other field types like keyword, boolean, dates, and numeric types.\n\nFor a simplified syntax, you can use the <<esql-search-operators,match operator>> `:` operator instead of `MATCH`.\n\n`MATCH` returns true if the provided query matches the row.",
|
||||
"signatures" : [
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"type" : "operator",
|
||||
"operator" : ":",
|
||||
"name" : "match_operator",
|
||||
"description" : "Use `MATCH` to perform a <<query-dsl-match-query,match query>> 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 <<text, text>> and <<semantic-text, semantic_text>>,\nas well as other field types like keyword, boolean, dates, and numeric types.\n\nFor a simplified syntax, you can use the <<esql-search-operators,match operator>> `:` operator instead of `MATCH`.\n\n`MATCH` returns true if the provided query matches the row.",
|
||||
"description" : "Use the match operator (`:`) to perform a <<query-dsl-match-query,match query>> 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 <<esql-match,match function>>.\n\nFor using the function syntax, or adding <<match-field-params,match query parameters>>, you can use the\n<<esql-match,match function>>.\n\n`:` returns true if the provided query matches the row.",
|
||||
"signatures" : [
|
||||
{
|
||||
"params" : [
|
||||
|
|
|
@ -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<<regexp-syntax,regular expressions>>. `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" : [
|
||||
|
|
|
@ -3,15 +3,15 @@ This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../READ
|
|||
-->
|
||||
|
||||
### MATCH_OPERATOR
|
||||
Use `MATCH` to perform a <<query-dsl-match-query,match query>> 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 <<query-dsl-match-query,match query>> 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 <<text, text>> and <<semantic-text, semantic_text>>,
|
||||
as well as other field types like keyword, boolean, dates, and numeric types.
|
||||
The match operator is equivalent to the <<esql-match,match function>>.
|
||||
|
||||
For a simplified syntax, you can use the <<esql-search-operators,match operator>> `:` operator instead of `MATCH`.
|
||||
For using the function syntax, or adding <<match-field-params,match query parameters>>, you can use the
|
||||
<<esql-match,match function>>.
|
||||
|
||||
`MATCH` returns true if the provided query matches the row.
|
||||
`:` returns true if the provided query matches the row.
|
||||
|
||||
```
|
||||
FROM books
|
||||
|
|
|
@ -11,10 +11,13 @@ Returns true if the provided query matches the row.
|
|||
|
||||
The match operator is equivalent to the <<esql-match,match function>>.
|
||||
|
||||
For using the function syntax, or adding <<match-field-params,match query parameters>>, you can use the
|
||||
<<esql-match,match function>>.
|
||||
|
||||
[.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]
|
||||
----
|
||||
|
|
|
@ -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<DataType> FIELD_DATA_TYPES = Set.of(
|
||||
KEYWORD,
|
||||
TEXT,
|
||||
SEMANTIC_TEXT,
|
||||
BOOLEAN,
|
||||
DATETIME,
|
||||
DATE_NANOS,
|
||||
DOUBLE,
|
||||
INTEGER,
|
||||
IP,
|
||||
LONG,
|
||||
UNSIGNED_LONG,
|
||||
VERSION
|
||||
);
|
||||
public static final Set<DataType> 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<Expression> 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -25,6 +25,7 @@ public class FullTextWritables {
|
|||
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()) {
|
||||
|
|
|
@ -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<DataType> FIELD_DATA_TYPES = Set.of(
|
||||
KEYWORD,
|
||||
TEXT,
|
||||
SEMANTIC_TEXT,
|
||||
BOOLEAN,
|
||||
DATETIME,
|
||||
DATE_NANOS,
|
||||
DOUBLE,
|
||||
INTEGER,
|
||||
IP,
|
||||
LONG,
|
||||
UNSIGNED_LONG,
|
||||
VERSION
|
||||
);
|
||||
public static final Set<DataType> QUERY_DATA_TYPES = Set.of(
|
||||
KEYWORD,
|
||||
BOOLEAN,
|
||||
DATETIME,
|
||||
DATE_NANOS,
|
||||
DOUBLE,
|
||||
INTEGER,
|
||||
IP,
|
||||
LONG,
|
||||
UNSIGNED_LONG,
|
||||
VERSION
|
||||
);
|
||||
|
||||
@FunctionInfo(
|
||||
returnType = "boolean",
|
||||
operator = ":",
|
||||
preview = true,
|
||||
description = """
|
||||
Use `MATCH` to perform a <<query-dsl-match-query,match query>> on the specified field.
|
||||
|
@ -132,8 +64,7 @@ public class Match extends FullTextFunction implements PostOptimizationVerificat
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -162,96 +93,6 @@ public class Match extends FullTextFunction implements PostOptimizationVerificat
|
|||
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<Expression> newChildren) {
|
||||
return new Match(source(), newChildren.get(0), newChildren.get(1), queryBuilder());
|
||||
|
@ -259,56 +100,6 @@ public class Match extends FullTextFunction implements PostOptimizationVerificat
|
|||
|
||||
@Override
|
||||
protected NodeInfo<? extends Expression> 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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <<query-dsl-match-query,match query>> on the specified field.
|
||||
Using `:` is equivalent to using the `match` query in the Elasticsearch Query DSL.
|
||||
|
||||
The match operator is equivalent to the <<esql-match,match function>>.
|
||||
|
||||
For using the function syntax, or adding <<match-field-params,match query parameters>>, you can use the
|
||||
<<esql-match,match function>>.
|
||||
|
||||
`:` 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<? extends Expression> info() {
|
||||
return NodeInfo.create(this, MatchOperator::new, field(), query());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression replaceChildren(List<Expression> newChildren) {
|
||||
return new MatchOperator(source(), newChildren.get(0), newChildren.get(1));
|
||||
}
|
||||
}
|
|
@ -58,7 +58,7 @@ public class RLike extends org.elasticsearch.xpack.esql.core.expression.predicat
|
|||
----
|
||||
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,
|
||||
|
|
|
@ -69,7 +69,7 @@ public class WildcardLike extends org.elasticsearch.xpack.esql.core.expression.p
|
|||
----
|
||||
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,
|
||||
|
|
|
@ -41,7 +41,7 @@ import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry;
|
|||
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;
|
||||
|
@ -988,6 +988,6 @@ public abstract class ExpressionBuilder extends IdentifierBuilder {
|
|||
matchFieldExpression = expression(ctx.fieldExp);
|
||||
}
|
||||
|
||||
return new Match(source(ctx), matchFieldExpression, expression(ctx.matchQuery));
|
||||
return new MatchOperator(source(ctx), matchFieldExpression, expression(ctx.matchQuery));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ import org.elasticsearch.xpack.esql.core.util.NumericUtils;
|
|||
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();
|
||||
|
|
|
@ -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<Object[]> parameters() {
|
||||
List<TestCaseSupplier> 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<TestCaseSupplier> 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<TestCaseSupplier> 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<TestCaseSupplier> 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<TestCaseSupplier> 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<TestCaseSupplier> 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());
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ import static org.hamcrest.Matchers.equalTo;
|
|||
public class MatchErrorTests extends ErrorsForCasesWithoutExamplesTestCase {
|
||||
@Override
|
||||
protected List<TestCaseSupplier> cases() {
|
||||
return paramsToSuppliers(MatchTests.parameters());
|
||||
return paramsToSuppliers(AbstractMatchFullTextFunctionTests.parameters());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -10,9 +10,12 @@ package org.elasticsearch.xpack.esql.expression.function.fulltext;
|
|||
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 class MatchOperatorTests extends MatchTests {
|
|||
|
||||
@ParametersFactory
|
||||
public static Iterable<Object[]> parameters() {
|
||||
return MatchTests.parameters();
|
||||
return AbstractMatchFullTextFunctionTests.parameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Expression build(Source source, List<Expression> args) {
|
||||
return new MatchOperator(source, args.get(0), args.get(1));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,22 +12,14 @@ 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.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.TestCase> testCaseSupplier) {
|
||||
this.testCase = testCaseSupplier.get();
|
||||
|
@ -35,342 +27,7 @@ public class MatchTests extends AbstractFunctionTestCase {
|
|||
|
||||
@ParametersFactory
|
||||
public static Iterable<Object[]> parameters() {
|
||||
List<TestCaseSupplier> suppliers = new ArrayList<>();
|
||||
|
||||
addUnsignedLongCases(suppliers);
|
||||
addNumericCases(suppliers);
|
||||
addNonNumericCases(suppliers);
|
||||
addQueryAsStringTestCases(suppliers);
|
||||
addStringTestCases(suppliers);
|
||||
|
||||
return parameterSuppliersFromTypedData(suppliers);
|
||||
}
|
||||
|
||||
private static void addNonNumericCases(List<TestCaseSupplier> 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<TestCaseSupplier> 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<TestCaseSupplier> 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<TestCaseSupplier> 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<TestCaseSupplier> 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
|
||||
|
|
Loading…
Reference in New Issue