| 
 | 1 | +/*  | 
 | 2 | + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one  | 
 | 3 | + * or more contributor license agreements. Licensed under the Elastic License  | 
 | 4 | + * 2.0; you may not use this file except in compliance with the Elastic License  | 
 | 5 | + * 2.0.  | 
 | 6 | + */  | 
 | 7 | +package org.elasticsearch.xpack.esql.expression.function;  | 
 | 8 | + | 
 | 9 | +import org.elasticsearch.common.lucene.BytesRefs;  | 
 | 10 | +import org.elasticsearch.core.Nullable;  | 
 | 11 | +import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException;  | 
 | 12 | +import org.elasticsearch.xpack.esql.common.Failure;  | 
 | 13 | +import org.elasticsearch.xpack.esql.common.Failures;  | 
 | 14 | +import org.elasticsearch.xpack.esql.core.expression.Expression;  | 
 | 15 | +import org.elasticsearch.xpack.esql.core.expression.Literal;  | 
 | 16 | + | 
 | 17 | +import static org.elasticsearch.common.logging.LoggerMessageFormat.format;  | 
 | 18 | +import static org.elasticsearch.xpack.esql.common.Failure.fail;  | 
 | 19 | + | 
 | 20 | +public class FunctionUtils {  | 
 | 21 | +    /**  | 
 | 22 | +     * A utility class to validate the type resolution of expressions before and after logical planning.  | 
 | 23 | +     * If null is passed for Failures to the constructor, it means we are only type resolution.  | 
 | 24 | +     * This is usually called when doing pre-logical planning validation.  | 
 | 25 | +     * If a {@link Failures} instance is passed, it means we are doing post-logical planning validation as well.  | 
 | 26 | +     * This is usually called after folding is done, during  | 
 | 27 | +     * {@link org.elasticsearch.xpack.esql.capabilities.PostOptimizationVerificationAware} verification  | 
 | 28 | +     */  | 
 | 29 | +    public static class TypeResolutionValidator {  | 
 | 30 | + | 
 | 31 | +        Expression.TypeResolution typeResolution = Expression.TypeResolution.TYPE_RESOLVED;  | 
 | 32 | +        @Nullable  | 
 | 33 | +        private final Failures postValidationFailures; // null means we are doing pre-folding validation only  | 
 | 34 | +        private final Expression field;  | 
 | 35 | + | 
 | 36 | +        public static TypeResolutionValidator forPreOptimizationValidation(Expression field) {  | 
 | 37 | +            return new TypeResolutionValidator(field, null);  | 
 | 38 | +        }  | 
 | 39 | + | 
 | 40 | +        public static TypeResolutionValidator forPostOptimizationValidation(Expression field, Failures failures) {  | 
 | 41 | +            return new TypeResolutionValidator(field, failures);  | 
 | 42 | +        }  | 
 | 43 | + | 
 | 44 | +        private TypeResolutionValidator(Expression field, Failures failures) {  | 
 | 45 | +            this.field = field;  | 
 | 46 | +            this.postValidationFailures = failures;  | 
 | 47 | +        }  | 
 | 48 | + | 
 | 49 | +        public void invalidIfPostValidation(Failure failure) {  | 
 | 50 | +            if (postValidationFailures != null) {  | 
 | 51 | +                postValidationFailures.add(failure);  | 
 | 52 | +            }  | 
 | 53 | +        }  | 
 | 54 | + | 
 | 55 | +        public void invalid(Expression.TypeResolution message) {  | 
 | 56 | +            typeResolution = message;  | 
 | 57 | +            if (postValidationFailures != null) {  | 
 | 58 | +                postValidationFailures.add(fail(field, message.message()));  | 
 | 59 | +            }  | 
 | 60 | +        }  | 
 | 61 | + | 
 | 62 | +        public Expression.TypeResolution getResolvedType() {  | 
 | 63 | +            return typeResolution;  | 
 | 64 | +        }  | 
 | 65 | +    }  | 
 | 66 | + | 
 | 67 | +    public static Integer limitValue(Expression limitField, String sourceText) {  | 
 | 68 | +        if (limitField instanceof Literal literal) {  | 
 | 69 | +            Object value = literal.value();  | 
 | 70 | +            if (value instanceof Integer intValue) {  | 
 | 71 | +                return intValue;  | 
 | 72 | +            }  | 
 | 73 | +        }  | 
 | 74 | +        throw new EsqlIllegalArgumentException(format(null, "Limit value must be an integer in [{}], found [{}]", sourceText, limitField));  | 
 | 75 | +    }  | 
 | 76 | + | 
 | 77 | +    /**  | 
 | 78 | +     * We check that the limit is not null and that if it is a literal, it is a positive integer  | 
 | 79 | +     * During postOptimizationVerification folding is already done, so we also verify that it is definitively a literal  | 
 | 80 | +     */  | 
 | 81 | +    public static Expression.TypeResolution resolveTypeLimit(Expression limitField, String sourceText, TypeResolutionValidator validator) {  | 
 | 82 | +        if (limitField == null) {  | 
 | 83 | +            validator.invalid(  | 
 | 84 | +                new Expression.TypeResolution(format(null, "Limit must be a constant integer in [{}], found [{}]", sourceText, limitField))  | 
 | 85 | +            );  | 
 | 86 | +        } else if (limitField instanceof Literal literal) {  | 
 | 87 | +            if (literal.value() == null) {  | 
 | 88 | +                validator.invalid(  | 
 | 89 | +                    new Expression.TypeResolution(  | 
 | 90 | +                        format(null, "Limit must be a constant integer in [{}], found [{}]", sourceText, limitField)  | 
 | 91 | +                    )  | 
 | 92 | +                );  | 
 | 93 | +            } else {  | 
 | 94 | +                int value = (Integer) literal.value();  | 
 | 95 | +                if (value <= 0) {  | 
 | 96 | +                    validator.invalid(  | 
 | 97 | +                        new Expression.TypeResolution(format(null, "Limit must be greater than 0 in [{}], found [{}]", sourceText, value))  | 
 | 98 | +                    );  | 
 | 99 | +                }  | 
 | 100 | +            }  | 
 | 101 | +        } else {  | 
 | 102 | +            // it is expected that the expression is a literal after folding  | 
 | 103 | +            // we fail if it is not a literal  | 
 | 104 | +            validator.invalidIfPostValidation(  | 
 | 105 | +                fail(limitField, "Limit must be a constant integer in [{}], found [{}]", sourceText, limitField)  | 
 | 106 | +            );  | 
 | 107 | +        }  | 
 | 108 | +        return validator.getResolvedType();  | 
 | 109 | +    }  | 
 | 110 | + | 
 | 111 | +    /**  | 
 | 112 | +     * We check that the query is not null and that if it is a literal, it is a string  | 
 | 113 | +     * During postOptimizationVerification folding is already done, so we also verify that it is definitively a literal  | 
 | 114 | +     */  | 
 | 115 | +    public static Expression.TypeResolution resolveTypeQuery(Expression queryField, String sourceText, TypeResolutionValidator validator) {  | 
 | 116 | +        if (queryField == null) {  | 
 | 117 | +            validator.invalid(  | 
 | 118 | +                new Expression.TypeResolution(format(null, "Query must be a valid string in [{}], found [{}]", sourceText, queryField))  | 
 | 119 | +            );  | 
 | 120 | +        } else if (queryField instanceof Literal literal) {  | 
 | 121 | +            if (literal.value() == null) {  | 
 | 122 | +                validator.invalid(  | 
 | 123 | +                    new Expression.TypeResolution(format(null, "Query value cannot be null in [{}], but got [{}]", sourceText, queryField))  | 
 | 124 | +                );  | 
 | 125 | +            }  | 
 | 126 | +        } else {  | 
 | 127 | +            // it is expected that the expression is a literal after folding  | 
 | 128 | +            // we fail if it is not a literal  | 
 | 129 | +            validator.invalidIfPostValidation(fail(queryField, "Query must be a valid string in [{}], found [{}]", sourceText, queryField));  | 
 | 130 | +        }  | 
 | 131 | +        return validator.getResolvedType();  | 
 | 132 | +    }  | 
 | 133 | + | 
 | 134 | +    public static Object queryAsObject(Expression queryField, String sourceText) {  | 
 | 135 | +        if (queryField instanceof Literal literal) {  | 
 | 136 | +            return literal.value();  | 
 | 137 | +        }  | 
 | 138 | +        throw new EsqlIllegalArgumentException(  | 
 | 139 | +            format(null, "Query value must be a constant string in [{}], found [{}]", sourceText, queryField)  | 
 | 140 | +        );  | 
 | 141 | +    }  | 
 | 142 | + | 
 | 143 | +    public static String queryAsString(Expression queryField, String sourceText) {  | 
 | 144 | +        if (queryField instanceof Literal literal) {  | 
 | 145 | +            return BytesRefs.toString(literal.value());  | 
 | 146 | +        }  | 
 | 147 | +        throw new EsqlIllegalArgumentException(  | 
 | 148 | +            format(null, "Query value must be a constant string in [{}], found [{}]", sourceText, queryField)  | 
 | 149 | +        );  | 
 | 150 | +    }  | 
 | 151 | +}  | 
0 commit comments