All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.xpack.esql.analysis.Analyzer Maven / Gradle / Ivy

There is a newer version: 8.16.1
Show newest version
/*
 * 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.analysis;

import org.elasticsearch.common.logging.HeaderWarning;
import org.elasticsearch.common.logging.LoggerMessageFormat;
import org.elasticsearch.compute.data.Block;
import org.elasticsearch.logging.Logger;
import org.elasticsearch.xpack.core.enrich.EnrichPolicy;
import org.elasticsearch.xpack.esql.Column;
import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException;
import org.elasticsearch.xpack.esql.VerificationException;
import org.elasticsearch.xpack.esql.core.analyzer.AnalyzerRules;
import org.elasticsearch.xpack.esql.core.analyzer.AnalyzerRules.BaseAnalyzerRule;
import org.elasticsearch.xpack.esql.core.analyzer.AnalyzerRules.ParameterizedAnalyzerRule;
import org.elasticsearch.xpack.esql.core.capabilities.Resolvables;
import org.elasticsearch.xpack.esql.core.common.Failure;
import org.elasticsearch.xpack.esql.core.expression.Alias;
import org.elasticsearch.xpack.esql.core.expression.Attribute;
import org.elasticsearch.xpack.esql.core.expression.AttributeMap;
import org.elasticsearch.xpack.esql.core.expression.EmptyAttribute;
import org.elasticsearch.xpack.esql.core.expression.Expression;
import org.elasticsearch.xpack.esql.core.expression.Expressions;
import org.elasticsearch.xpack.esql.core.expression.FieldAttribute;
import org.elasticsearch.xpack.esql.core.expression.Literal;
import org.elasticsearch.xpack.esql.core.expression.NameId;
import org.elasticsearch.xpack.esql.core.expression.NamedExpression;
import org.elasticsearch.xpack.esql.core.expression.Nullability;
import org.elasticsearch.xpack.esql.core.expression.ReferenceAttribute;
import org.elasticsearch.xpack.esql.core.expression.UnresolvedAttribute;
import org.elasticsearch.xpack.esql.core.expression.UnresolvedStar;
import org.elasticsearch.xpack.esql.core.expression.function.scalar.ScalarFunction;
import org.elasticsearch.xpack.esql.core.expression.predicate.BinaryOperator;
import org.elasticsearch.xpack.esql.core.expression.predicate.operator.comparison.BinaryComparison;
import org.elasticsearch.xpack.esql.core.index.EsIndex;
import org.elasticsearch.xpack.esql.core.plan.TableIdentifier;
import org.elasticsearch.xpack.esql.core.plan.logical.Limit;
import org.elasticsearch.xpack.esql.core.plan.logical.LogicalPlan;
import org.elasticsearch.xpack.esql.core.rule.ParameterizedRule;
import org.elasticsearch.xpack.esql.core.rule.ParameterizedRuleExecutor;
import org.elasticsearch.xpack.esql.core.rule.Rule;
import org.elasticsearch.xpack.esql.core.rule.RuleExecutor;
import org.elasticsearch.xpack.esql.core.session.Configuration;
import org.elasticsearch.xpack.esql.core.tree.Source;
import org.elasticsearch.xpack.esql.core.type.DataType;
import org.elasticsearch.xpack.esql.core.type.EsField;
import org.elasticsearch.xpack.esql.core.type.InvalidMappedField;
import org.elasticsearch.xpack.esql.core.type.UnsupportedEsField;
import org.elasticsearch.xpack.esql.core.util.CollectionUtils;
import org.elasticsearch.xpack.esql.core.util.Holder;
import org.elasticsearch.xpack.esql.core.util.StringUtils;
import org.elasticsearch.xpack.esql.expression.NamedExpressions;
import org.elasticsearch.xpack.esql.expression.UnresolvedNamePattern;
import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry;
import org.elasticsearch.xpack.esql.expression.function.FunctionDefinition;
import org.elasticsearch.xpack.esql.expression.function.UnresolvedFunction;
import org.elasticsearch.xpack.esql.expression.function.UnsupportedAttribute;
import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction;
import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction;
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.DateTimeArithmeticOperation;
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.EsqlArithmeticOperation;
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.In;
import org.elasticsearch.xpack.esql.optimizer.LogicalPlanOptimizer;
import org.elasticsearch.xpack.esql.plan.logical.Aggregate;
import org.elasticsearch.xpack.esql.plan.logical.Drop;
import org.elasticsearch.xpack.esql.plan.logical.Enrich;
import org.elasticsearch.xpack.esql.plan.logical.EsRelation;
import org.elasticsearch.xpack.esql.plan.logical.EsqlAggregate;
import org.elasticsearch.xpack.esql.plan.logical.EsqlUnresolvedRelation;
import org.elasticsearch.xpack.esql.plan.logical.Eval;
import org.elasticsearch.xpack.esql.plan.logical.Keep;
import org.elasticsearch.xpack.esql.plan.logical.Lookup;
import org.elasticsearch.xpack.esql.plan.logical.MvExpand;
import org.elasticsearch.xpack.esql.plan.logical.Project;
import org.elasticsearch.xpack.esql.plan.logical.Rename;
import org.elasticsearch.xpack.esql.plan.logical.local.EsqlProject;
import org.elasticsearch.xpack.esql.plan.logical.local.LocalRelation;
import org.elasticsearch.xpack.esql.plan.logical.local.LocalSupplier;
import org.elasticsearch.xpack.esql.stats.FeatureMetric;
import org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter;
import org.elasticsearch.xpack.esql.type.EsqlDataTypes;
import org.elasticsearch.xpack.esql.type.MultiTypeEsField;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static java.util.Collections.singletonList;
import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
import static org.elasticsearch.xpack.core.enrich.EnrichPolicy.GEO_MATCH_TYPE;
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.DOUBLE;
import static org.elasticsearch.xpack.esql.core.type.DataType.FLOAT;
import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_POINT;
import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_SHAPE;
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.NESTED;
import static org.elasticsearch.xpack.esql.core.type.DataType.TEXT;
import static org.elasticsearch.xpack.esql.core.type.DataType.VERSION;
import static org.elasticsearch.xpack.esql.stats.FeatureMetric.LIMIT;
import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.isTemporalAmount;

public class Analyzer extends ParameterizedRuleExecutor {
    // marker list of attributes for plans that do not have any concrete fields to return, but have other computed columns to return
    // ie from test | stats c = count(*)
    public static final List NO_FIELDS = List.of(
        new ReferenceAttribute(Source.EMPTY, "", DataType.NULL, null, Nullability.TRUE, null, true)
    );
    private static final Iterable> rules;

    static {
        var init = new Batch<>(
            "Initialize",
            Limiter.ONCE,
            new ResolveTable(),
            new ResolveEnrich(),
            new ResolveLookupTables(),
            new ResolveFunctions()
        );
        var resolution = new Batch<>(
            "Resolution",
            new ResolveRefs(),
            new ResolveUnionTypes(),  // Must be after ResolveRefs, so union types can be found
            new ImplicitCasting()
        );
        var finish = new Batch<>("Finish Analysis", Limiter.ONCE, new AddImplicitLimit(), new UnionTypesCleanup());
        rules = List.of(init, resolution, finish);
    }

    private final Verifier verifier;

    public Analyzer(AnalyzerContext context, Verifier verifier) {
        super(context);
        this.verifier = verifier;
    }

    public LogicalPlan analyze(LogicalPlan plan) {
        BitSet partialMetrics = new BitSet(FeatureMetric.values().length);
        return verify(execute(plan), gatherPreAnalysisMetrics(plan, partialMetrics));
    }

    public LogicalPlan verify(LogicalPlan plan, BitSet partialMetrics) {
        Collection failures = verifier.verify(plan, partialMetrics);
        if (failures.isEmpty() == false) {
            throw new VerificationException(failures);
        }
        return plan;
    }

    @Override
    protected Iterable> batches() {
        return rules;
    }

    private static class ResolveTable extends ParameterizedAnalyzerRule {

        @Override
        protected LogicalPlan rule(EsqlUnresolvedRelation plan, AnalyzerContext context) {
            if (context.indexResolution().isValid() == false) {
                return plan.unresolvedMessage().equals(context.indexResolution().toString())
                    ? plan
                    : new EsqlUnresolvedRelation(
                        plan.source(),
                        plan.table(),
                        plan.metadataFields(),
                        plan.indexMode(),
                        context.indexResolution().toString()
                    );
            }
            TableIdentifier table = plan.table();
            if (context.indexResolution().matches(table.index()) == false) {
                // TODO: fix this (and tests), or drop check (seems SQL-inherited, where's also defective)
                new EsqlUnresolvedRelation(
                    plan.source(),
                    plan.table(),
                    plan.metadataFields(),
                    plan.indexMode(),
                    "invalid [" + table + "] resolution to [" + context.indexResolution() + "]"
                );
            }

            EsIndex esIndex = context.indexResolution().get();
            var attributes = mappingAsAttributes(plan.source(), esIndex.mapping());
            attributes.addAll(plan.metadataFields());
            return new EsRelation(plan.source(), esIndex, attributes.isEmpty() ? NO_FIELDS : attributes, plan.indexMode());
        }

    }

    /**
     * Specific flattening method, different from the default EsRelation that:
     * 1. takes care of data type widening (for certain types)
     * 2. drops the object and keyword hierarchy
     */
    private static List mappingAsAttributes(Source source, Map mapping) {
        var list = new ArrayList();
        mappingAsAttributes(list, source, null, mapping);
        list.sort(Comparator.comparing(Attribute::qualifiedName));
        return list;
    }

    private static void mappingAsAttributes(List list, Source source, FieldAttribute parent, Map mapping) {
        for (Map.Entry entry : mapping.entrySet()) {
            String name = entry.getKey();
            EsField t = entry.getValue();

            if (t != null) {
                name = parent == null ? name : parent.fieldName() + "." + name;
                var fieldProperties = t.getProperties();
                var type = t.getDataType().widenSmallNumeric();
                // due to a bug also copy the field since the Attribute hierarchy extracts the data type
                // directly even if the data type is passed explicitly
                if (type != t.getDataType()) {
                    t = new EsField(t.getName(), type, t.getProperties(), t.isAggregatable(), t.isAlias());
                }

                FieldAttribute attribute = t instanceof UnsupportedEsField uef
                    ? new UnsupportedAttribute(source, name, uef)
                    : new FieldAttribute(source, parent, name, t);
                // primitive branch
                if (EsqlDataTypes.isPrimitive(type)) {
                    list.add(attribute);
                }
                // allow compound object even if they are unknown (but not NESTED)
                if (type != NESTED && fieldProperties.isEmpty() == false) {
                    mappingAsAttributes(list, source, attribute, fieldProperties);
                }
            }
        }
    }

    private static class ResolveEnrich extends ParameterizedAnalyzerRule {

        @Override
        protected LogicalPlan rule(Enrich plan, AnalyzerContext context) {
            if (plan.policyName().resolved() == false) {
                // the policy does not exist
                return plan;
            }
            final String policyName = (String) plan.policyName().fold();
            final var resolved = context.enrichResolution().getResolvedPolicy(policyName, plan.mode());
            if (resolved != null) {
                var policy = new EnrichPolicy(resolved.matchType(), null, List.of(), resolved.matchField(), resolved.enrichFields());
                var matchField = plan.matchField() == null || plan.matchField() instanceof EmptyAttribute
                    ? new UnresolvedAttribute(plan.source(), policy.getMatchField())
                    : plan.matchField();
                List enrichFields = calculateEnrichFields(
                    plan.source(),
                    policyName,
                    mappingAsAttributes(plan.source(), resolved.mapping()),
                    plan.enrichFields(),
                    policy
                );
                return new Enrich(
                    plan.source(),
                    plan.child(),
                    plan.mode(),
                    plan.policyName(),
                    matchField,
                    policy,
                    resolved.concreteIndices(),
                    enrichFields
                );
            } else {
                String error = context.enrichResolution().getError(policyName, plan.mode());
                var policyNameExp = new UnresolvedAttribute(plan.policyName().source(), policyName, null, error);
                return new Enrich(plan.source(), plan.child(), plan.mode(), policyNameExp, plan.matchField(), null, Map.of(), List.of());
            }
        }

        public static List calculateEnrichFields(
            Source source,
            String policyName,
            List mapping,
            List enrichFields,
            EnrichPolicy policy
        ) {
            Set policyEnrichFieldSet = new HashSet<>(policy.getEnrichFields());
            Map fieldMap = mapping.stream()
                .filter(e -> policyEnrichFieldSet.contains(e.name()))
                .collect(Collectors.toMap(NamedExpression::name, Function.identity()));
            List result = new ArrayList<>();
            if (enrichFields == null || enrichFields.isEmpty()) {
                // use the policy to infer the enrich fields
                for (String enrichFieldName : policy.getEnrichFields()) {
                    result.add(createEnrichFieldExpression(source, policyName, fieldMap, enrichFieldName));
                }
            } else {
                for (NamedExpression enrichField : enrichFields) {
                    String enrichFieldName = Expressions.name(enrichField instanceof Alias a ? a.child() : enrichField);
                    NamedExpression field = createEnrichFieldExpression(source, policyName, fieldMap, enrichFieldName);
                    result.add(enrichField instanceof Alias a ? new Alias(a.source(), a.name(), field) : field);
                }
            }
            return result;
        }

        private static NamedExpression createEnrichFieldExpression(
            Source source,
            String policyName,
            Map fieldMap,
            String enrichFieldName
        ) {
            Attribute mappedField = fieldMap.get(enrichFieldName);
            if (mappedField == null) {
                String msg = "Enrich field [" + enrichFieldName + "] not found in enrich policy [" + policyName + "]";
                List similar = StringUtils.findSimilar(enrichFieldName, fieldMap.keySet());
                if (CollectionUtils.isEmpty(similar) == false) {
                    msg += ", did you mean " + (similar.size() == 1 ? "[" + similar.get(0) + "]" : "any of " + similar) + "?";
                }
                return new UnresolvedAttribute(source, enrichFieldName, null, msg);
            } else {
                return new ReferenceAttribute(source, enrichFieldName, mappedField.dataType(), null, Nullability.TRUE, null, false);
            }
        }
    }

    private static class ResolveLookupTables extends ParameterizedAnalyzerRule {

        @Override
        protected LogicalPlan rule(Lookup lookup, AnalyzerContext context) {
            // the parser passes the string wrapped in a literal
            Source source = lookup.source();
            Expression tableNameExpression = lookup.tableName();
            String tableName = lookup.tableName().toString();
            Map> tables = context.configuration().tables();
            LocalRelation localRelation = null;

            if (tables.containsKey(tableName) == false) {
                String message = "Unknown table [" + tableName + "]";
                // typos check
                List potentialMatches = StringUtils.findSimilar(tableName, tables.keySet());
                if (CollectionUtils.isEmpty(potentialMatches) == false) {
                    message = UnresolvedAttribute.errorMessage(tableName, potentialMatches).replace("column", "table");
                }
                tableNameExpression = new UnresolvedAttribute(tableNameExpression.source(), tableName, null, message);
            }
            // wrap the table in a local relationship for idiomatic field resolution
            else {
                localRelation = tableMapAsRelation(source, tables.get(tableName));
                // postpone the resolution for ResolveRefs
            }

            return new Lookup(source, lookup.child(), tableNameExpression, lookup.matchFields(), localRelation);
        }

        private LocalRelation tableMapAsRelation(Source source, Map mapTable) {
            Block[] blocks = new Block[mapTable.size()];

            List attributes = new ArrayList<>(blocks.length);
            int i = 0;
            for (Map.Entry entry : mapTable.entrySet()) {
                String name = entry.getKey();
                Column column = entry.getValue();
                // create a fake ES field - alternative is to use a ReferenceAttribute
                EsField field = new EsField(name, column.type(), Map.of(), false, false);
                attributes.add(new FieldAttribute(source, null, name, field));
                // prepare the block for the supplier
                blocks[i++] = column.values();
            }
            LocalSupplier supplier = LocalSupplier.of(blocks);
            return new LocalRelation(source, attributes, supplier);
        }
    }

    public static class ResolveRefs extends BaseAnalyzerRule {
        @Override
        protected LogicalPlan doRule(LogicalPlan plan) {
            if (plan.childrenResolved() == false) {
                return plan;
            }
            final List childrenOutput = new ArrayList<>();

            for (LogicalPlan child : plan.children()) {
                var output = child.output();
                childrenOutput.addAll(output);
            }

            if (plan instanceof Aggregate agg) {
                return resolveAggregate(agg, childrenOutput);
            }

            if (plan instanceof Drop d) {
                return resolveDrop(d, childrenOutput);
            }

            if (plan instanceof Rename r) {
                return resolveRename(r, childrenOutput);
            }

            if (plan instanceof Keep p) {
                return resolveKeep(p, childrenOutput);
            }

            if (plan instanceof Eval p) {
                return resolveEval(p, childrenOutput);
            }

            if (plan instanceof Enrich p) {
                return resolveEnrich(p, childrenOutput);
            }

            if (plan instanceof MvExpand p) {
                return resolveMvExpand(p, childrenOutput);
            }

            if (plan instanceof Lookup l) {
                return resolveLookup(l, childrenOutput);
            }

            return plan.transformExpressionsOnly(UnresolvedAttribute.class, ua -> maybeResolveAttribute(ua, childrenOutput));
        }

        private LogicalPlan resolveAggregate(Aggregate a, List childrenOutput) {
            // if the grouping is resolved but the aggs are not, use the former to resolve the latter
            // e.g. STATS a ... GROUP BY a = x + 1
            Holder changed = new Holder<>(false);
            List groupings = a.groupings();
            // first resolve groupings since the aggs might refer to them
            // trying to globally resolve unresolved attributes will lead to some being marked as unresolvable
            if (Resolvables.resolved(groupings) == false) {
                List newGroupings = new ArrayList<>(groupings.size());
                for (Expression g : groupings) {
                    Expression resolved = g.transformUp(UnresolvedAttribute.class, ua -> maybeResolveAttribute(ua, childrenOutput));
                    if (resolved != g) {
                        changed.set(true);
                    }
                    newGroupings.add(resolved);
                }
                groupings = newGroupings;
                if (changed.get()) {
                    a = new EsqlAggregate(a.source(), a.child(), a.aggregateType(), newGroupings, a.aggregates());
                    changed.set(false);
                }
            }

            if (a.expressionsResolved() == false) {
                AttributeMap resolved = new AttributeMap<>();
                for (Expression e : groupings) {
                    Attribute attr = Expressions.attribute(e);
                    if (attr != null && attr.resolved()) {
                        resolved.put(attr, attr);
                    }
                }
                List resolvedList = NamedExpressions.mergeOutputAttributes(new ArrayList<>(resolved.keySet()), childrenOutput);
                List newAggregates = new ArrayList<>();

                for (NamedExpression aggregate : a.aggregates()) {
                    var agg = (NamedExpression) aggregate.transformUp(UnresolvedAttribute.class, ua -> {
                        Expression ne = ua;
                        Attribute maybeResolved = maybeResolveAttribute(ua, resolvedList);
                        if (maybeResolved != null) {
                            changed.set(true);
                            ne = maybeResolved;
                        }
                        return ne;
                    });
                    newAggregates.add(agg);
                }

                a = changed.get() ? new EsqlAggregate(a.source(), a.child(), a.aggregateType(), groupings, newAggregates) : a;
            }

            return a;
        }

        private LogicalPlan resolveMvExpand(MvExpand p, List childrenOutput) {
            if (p.target() instanceof UnresolvedAttribute ua) {
                Attribute resolved = maybeResolveAttribute(ua, childrenOutput);
                if (resolved == ua) {
                    return p;
                }
                return new MvExpand(
                    p.source(),
                    p.child(),
                    resolved,
                    resolved.resolved()
                        ? new ReferenceAttribute(
                            resolved.source(),
                            resolved.name(),
                            resolved.dataType(),
                            null,
                            resolved.nullable(),
                            null,
                            false
                        )
                        : resolved
                );
            }
            return p;
        }

        private LogicalPlan resolveLookup(Lookup l, List childrenOutput) {
            // check if the table exists before performing any resolution
            if (l.localRelation() == null) {
                return l;
            }

            // check the on field against both the child output and the inner relation
            List matchFields = new ArrayList<>(l.matchFields().size());
            List localOutput = l.localRelation().output();
            boolean modified = false;

            for (Attribute matchField : l.matchFields()) {
                Attribute matchFieldChildReference = matchField;
                if (matchField instanceof UnresolvedAttribute ua && ua.customMessage() == false) {
                    modified = true;
                    Attribute joinedAttribute = maybeResolveAttribute(ua, localOutput);
                    // can't find the field inside the local relation
                    if (joinedAttribute instanceof UnresolvedAttribute lua) {
                        // adjust message
                        matchFieldChildReference = lua.withUnresolvedMessage(
                            lua.unresolvedMessage().replace("Unknown column", "Unknown column in lookup target")
                        );
                    } else {
                        // check also the child output by resolving to it
                        Attribute attr = maybeResolveAttribute(ua, childrenOutput);
                        matchFieldChildReference = attr;
                        if (attr instanceof UnresolvedAttribute == false) {
                            /*
                             * If they do, make sure the data types line up. If either is
                             * null it's fine to match it against anything.
                             */
                            boolean dataTypesOk = joinedAttribute.dataType().equals(attr.dataType());
                            if (false == dataTypesOk) {
                                dataTypesOk = joinedAttribute.dataType() == DataType.NULL || attr.dataType() == DataType.NULL;
                            }
                            if (false == dataTypesOk) {
                                dataTypesOk = joinedAttribute.dataType().equals(KEYWORD) && attr.dataType().equals(TEXT);
                            }
                            if (false == dataTypesOk) {
                                matchFieldChildReference = new UnresolvedAttribute(
                                    attr.source(),
                                    attr.name(),
                                    attr.qualifier(),
                                    attr.id(),
                                    "column type mismatch, table column was ["
                                        + joinedAttribute.dataType().typeName()
                                        + "] and original column was ["
                                        + attr.dataType().typeName()
                                        + "]",
                                    null
                                );
                            }
                        }
                    }
                }

                matchFields.add(matchFieldChildReference);
            }
            if (modified) {
                return new Lookup(l.source(), l.child(), l.tableName(), matchFields, l.localRelation());
            }
            return l;
        }

        private Attribute maybeResolveAttribute(UnresolvedAttribute ua, List childrenOutput) {
            return maybeResolveAttribute(ua, childrenOutput, log);
        }

        private static Attribute maybeResolveAttribute(UnresolvedAttribute ua, List childrenOutput, Logger logger) {
            if (ua.customMessage()) {
                return ua;
            }
            return resolveAttribute(ua, childrenOutput, logger);
        }

        private Attribute resolveAttribute(UnresolvedAttribute ua, List childrenOutput) {
            return resolveAttribute(ua, childrenOutput, log);
        }

        private static Attribute resolveAttribute(UnresolvedAttribute ua, List childrenOutput, Logger logger) {
            Attribute resolved = ua;
            var named = resolveAgainstList(ua, childrenOutput);
            // if resolved, return it; otherwise keep it in place to be resolved later
            if (named.size() == 1) {
                resolved = named.get(0);
                if (logger != null && logger.isTraceEnabled() && resolved.resolved()) {
                    logger.trace("Resolved {} to {}", ua, resolved);
                }
            } else {
                if (named.size() > 0) {
                    resolved = ua.withUnresolvedMessage("Resolved [" + ua + "] unexpectedly to multiple attributes " + named);
                }
            }
            return resolved;
        }

        private LogicalPlan resolveEval(Eval eval, List childOutput) {
            List allResolvedInputs = new ArrayList<>(childOutput);
            List newFields = new ArrayList<>();
            boolean changed = false;
            for (Alias field : eval.fields()) {
                Alias result = (Alias) field.transformUp(UnresolvedAttribute.class, ua -> resolveAttribute(ua, allResolvedInputs));

                changed |= result != field;
                newFields.add(result);

                if (result.resolved()) {
                    // for proper resolution, duplicate attribute names are problematic, only last occurrence matters
                    Attribute existing = allResolvedInputs.stream()
                        .filter(attr -> attr.name().equals(result.name()))
                        .findFirst()
                        .orElse(null);
                    if (existing != null) {
                        allResolvedInputs.remove(existing);
                    }
                    allResolvedInputs.add(result.toAttribute());
                }
            }
            return changed ? new Eval(eval.source(), eval.child(), newFields) : eval;
        }

        /**
         * resolve each item manually.
         *
         * Fields are added in the order they appear.
         *
         * If one field matches multiple expressions, the following precedence rules apply (higher to lower):
         * 1. complete field name (ie. no wildcards)
         * 2. partial wildcard expressions (eg. fieldNam*)
         * 3. wildcard only (ie. *)
         *
         * If a field name matches multiple expressions with the same precedence, last one is used.
         *
         * A few examples below:
         *
         * // full name
         * row foo = 1, bar = 2 | keep foo, bar, foo   ->  bar, foo
         *
         * // the full name has precedence on wildcard expression
         * row foo = 1, bar = 2 | keep foo, bar, foo*   ->  foo, bar
         *
         * // the two wildcard expressions have the same priority, even though the first one is more specific
         * // so last one wins
         * row foo = 1, bar = 2 | keep foo*, bar, fo*   ->  bar, foo
         *
         * // * has the lowest priority
         * row foo = 1, bar = 2 | keep *, foo   ->  bar, foo
         * row foo = 1, bar = 2 | keep foo, *   ->  foo, bar
         * row foo = 1, bar = 2 | keep bar*, foo, *   ->  bar, foo
         */
        private LogicalPlan resolveKeep(Project p, List childOutput) {
            List resolvedProjections = new ArrayList<>();
            var projections = p.projections();
            // start with projections

            // no projection specified or just *
            if (projections.isEmpty() || (projections.size() == 1 && projections.get(0) instanceof UnresolvedStar)) {
                resolvedProjections.addAll(childOutput);
            }
            // otherwise resolve them
            else {
                Map priorities = new LinkedHashMap<>();
                for (var proj : projections) {
                    final List resolved;
                    final int priority;
                    if (proj instanceof UnresolvedStar) {
                        resolved = childOutput;
                        priority = 2;
                    } else if (proj instanceof UnresolvedNamePattern up) {
                        resolved = resolveAgainstList(up, childOutput);
                        priority = 1;
                    } else if (proj instanceof UnresolvedAttribute ua) {
                        resolved = resolveAgainstList(ua, childOutput);
                        priority = 0;
                    } else {
                        throw new EsqlIllegalArgumentException("unexpected projection: " + proj);
                    }
                    for (Attribute attr : resolved) {
                        Integer previousPrio = priorities.get(attr);
                        if (previousPrio == null || previousPrio >= priority) {
                            priorities.remove(attr);
                            priorities.put(attr, priority);
                        }
                    }
                }
                resolvedProjections = new ArrayList<>(priorities.keySet());
            }

            return new EsqlProject(p.source(), p.child(), resolvedProjections);
        }

        private LogicalPlan resolveDrop(Drop drop, List childOutput) {
            List resolvedProjections = new ArrayList<>(childOutput);

            for (var ne : drop.removals()) {
                List resolved;

                if (ne instanceof UnresolvedNamePattern np) {
                    resolved = resolveAgainstList(np, childOutput);
                } else if (ne instanceof UnresolvedAttribute ua) {
                    resolved = resolveAgainstList(ua, childOutput);
                } else {
                    resolved = singletonList(ne);
                }

                // the return list might contain either resolved elements or unresolved ones.
                // if things are resolved, remove them - if not add them to the list to trip the Verifier;
                // thus make sure to remove the intersection but add the unresolved difference (if any).
                // so, remove things that are in common
                resolvedProjections.removeIf(resolved::contains);
                // but add non-projected, unresolved extras to later trip the Verifier.
                resolved.forEach(r -> {
                    if (r.resolved() == false && r instanceof UnsupportedAttribute == false) {
                        resolvedProjections.add(r);
                    }
                });
            }

            return new EsqlProject(drop.source(), drop.child(), resolvedProjections);
        }

        private LogicalPlan resolveRename(Rename rename, List childrenOutput) {
            List projections = projectionsForRename(rename, childrenOutput, log);

            return new EsqlProject(rename.source(), rename.child(), projections);
        }

        public static List projectionsForRename(Rename rename, List childrenOutput, Logger logger) {
            List projections = new ArrayList<>(childrenOutput);

            int renamingsCount = rename.renamings().size();
            List unresolved = new ArrayList<>(renamingsCount);
            Map reverseAliasing = new HashMap<>(renamingsCount); // `| rename a as x` => map(a: x)

            rename.renamings().forEach(alias -> {
                // skip NOPs: `| rename a as a`
                if (alias.child() instanceof UnresolvedAttribute ua && alias.name().equals(ua.name()) == false) {
                    // remove attributes overwritten by a renaming: `| keep a, b, c | rename a as b`
                    projections.removeIf(x -> x.name().equals(alias.name()));

                    var resolved = maybeResolveAttribute(ua, childrenOutput, logger);
                    if (resolved instanceof UnsupportedAttribute || resolved.resolved()) {
                        var realiased = (NamedExpression) alias.replaceChildren(List.of(resolved));
                        projections.replaceAll(x -> x.equals(resolved) ? realiased : x);
                        childrenOutput.removeIf(x -> x.equals(resolved));
                        reverseAliasing.put(resolved.name(), alias.name());
                    } else { // remained UnresolvedAttribute
                        // is the current alias referencing a previously declared alias?
                        boolean updated = false;
                        if (reverseAliasing.containsValue(resolved.name())) {
                            for (var li = projections.listIterator(); li.hasNext();) {
                                // does alias still exist? i.e. it hasn't been renamed again (`| rename a as b, b as c, b as d`)
                                if (li.next() instanceof Alias a && a.name().equals(resolved.name())) {
                                    reverseAliasing.put(resolved.name(), alias.name());
                                    // update aliased projection in place
                                    li.set(alias.replaceChildren(a.children()));
                                    updated = true;
                                    break;
                                }
                            }
                        }
                        if (updated == false) {
                            var u = resolved;
                            var previousAliasName = reverseAliasing.get(resolved.name());
                            if (previousAliasName != null) {
                                String message = format(
                                    null,
                                    "Column [{}] renamed to [{}] and is no longer available [{}]",
                                    resolved.name(),
                                    previousAliasName,
                                    alias.sourceText()
                                );
                                u = ua.withUnresolvedMessage(message);
                            }
                            unresolved.add(u);
                        }
                    }
                }
            });

            // add unresolved renamings to later trip the Verifier.
            projections.addAll(unresolved);

            return projections;
        }

        private LogicalPlan resolveEnrich(Enrich enrich, List childrenOutput) {

            if (enrich.matchField().toAttribute() instanceof UnresolvedAttribute ua) {
                Attribute resolved = maybeResolveAttribute(ua, childrenOutput);
                if (resolved.equals(ua)) {
                    return enrich;
                }
                if (resolved.resolved() && enrich.policy() != null) {
                    final DataType dataType = resolved.dataType();
                    String matchType = enrich.policy().getType();
                    DataType[] allowed = allowedEnrichTypes(matchType);
                    if (Arrays.asList(allowed).contains(dataType) == false) {
                        String suffix = "only " + Arrays.toString(allowed) + " allowed for type [" + matchType + "]";
                        resolved = ua.withUnresolvedMessage(
                            "Unsupported type [" + resolved.dataType() + "] for enrich matching field [" + ua.name() + "]; " + suffix
                        );
                    }
                }
                return new Enrich(
                    enrich.source(),
                    enrich.child(),
                    enrich.mode(),
                    enrich.policyName(),
                    resolved,
                    enrich.policy(),
                    enrich.concreteIndices(),
                    enrich.enrichFields()
                );
            }
            return enrich;
        }

        private static final DataType[] GEO_TYPES = new DataType[] { GEO_POINT, GEO_SHAPE };
        private static final DataType[] NON_GEO_TYPES = new DataType[] { KEYWORD, TEXT, IP, LONG, INTEGER, FLOAT, DOUBLE, DATETIME };

        private DataType[] allowedEnrichTypes(String matchType) {
            return matchType.equals(GEO_MATCH_TYPE) ? GEO_TYPES : NON_GEO_TYPES;
        }
    }

    private static List resolveAgainstList(UnresolvedNamePattern up, Collection attrList) {
        UnresolvedAttribute ua = new UnresolvedAttribute(up.source(), up.pattern(), null);
        Predicate matcher = a -> up.match(a.name()) || up.match(a.qualifiedName());
        var matches = AnalyzerRules.maybeResolveAgainstList(matcher, () -> ua, attrList, true, a -> Analyzer.handleSpecialFields(ua, a));
        return potentialCandidatesIfNoMatchesFound(ua, matches, attrList, list -> UnresolvedNamePattern.errorMessage(up.pattern(), list));
    }

    private static List resolveAgainstList(UnresolvedAttribute ua, Collection attrList) {
        var matches = AnalyzerRules.maybeResolveAgainstList(ua, attrList, a -> Analyzer.handleSpecialFields(ua, a));
        return potentialCandidatesIfNoMatchesFound(ua, matches, attrList, list -> UnresolvedAttribute.errorMessage(ua.name(), list));
    }

    private static List potentialCandidatesIfNoMatchesFound(
        UnresolvedAttribute ua,
        List matches,
        Collection attrList,
        java.util.function.Function, String> messageProducer
    ) {
        // none found - add error message
        if (matches.isEmpty()) {
            Set names = new HashSet<>(attrList.size());
            for (var a : attrList) {
                String nameCandidate = a.name();
                if (EsqlDataTypes.isPrimitive(a.dataType())) {
                    names.add(nameCandidate);
                }
            }
            var name = ua.name();
            UnresolvedAttribute unresolved = ua.withUnresolvedMessage(messageProducer.apply(StringUtils.findSimilar(name, names)));
            matches = singletonList(unresolved);
        }
        return matches;
    }

    private static Attribute handleSpecialFields(UnresolvedAttribute u, Attribute named) {
        return named.withLocation(u.source());
    }

    private static class ResolveFunctions extends ParameterizedAnalyzerRule {

        @Override
        protected LogicalPlan rule(LogicalPlan plan, AnalyzerContext context) {
            // Allow resolving snapshot-only functions, but do not include them in the documentation
            final EsqlFunctionRegistry snapshotRegistry = context.functionRegistry().snapshotRegistry();
            return plan.transformExpressionsOnly(
                UnresolvedFunction.class,
                uf -> resolveFunction(uf, context.configuration(), snapshotRegistry)
            );
        }

        public static org.elasticsearch.xpack.esql.core.expression.function.Function resolveFunction(
            UnresolvedFunction uf,
            Configuration configuration,
            EsqlFunctionRegistry functionRegistry
        ) {
            org.elasticsearch.xpack.esql.core.expression.function.Function f = null;
            if (uf.analyzed()) {
                f = uf;
            } else {
                String functionName = functionRegistry.resolveAlias(uf.name());
                if (functionRegistry.functionExists(functionName) == false) {
                    f = uf.missing(functionName, functionRegistry.listFunctions());
                } else {
                    FunctionDefinition def = functionRegistry.resolveFunction(functionName);
                    f = uf.buildResolved(configuration, def);
                }
            }
            return f;
        }
    }

    private static class AddImplicitLimit extends ParameterizedRule {
        @Override
        public LogicalPlan apply(LogicalPlan logicalPlan, AnalyzerContext context) {
            List limits = logicalPlan.collectFirstChildren(Limit.class::isInstance);
            int limit;
            if (limits.isEmpty()) {
                HeaderWarning.addWarning(
                    "No limit defined, adding default limit of [{}]",
                    context.configuration().resultTruncationDefaultSize()
                );
                limit = context.configuration().resultTruncationDefaultSize(); // user provided no limit: cap to a default
            } else {
                limit = context.configuration().resultTruncationMaxSize(); // user provided a limit: cap result entries to the max
            }
            var source = logicalPlan.source();
            return new Limit(source, new Literal(source, limit, DataType.INTEGER), logicalPlan);
        }
    }

    private BitSet gatherPreAnalysisMetrics(LogicalPlan plan, BitSet b) {
        // count only the explicit "limit" the user added, otherwise all queries will have a "limit" and telemetry won't reflect reality
        if (plan.collectFirstChildren(Limit.class::isInstance).isEmpty() == false) {
            b.set(LIMIT.ordinal());
        }
        plan.forEachDown(p -> FeatureMetric.set(p, b));
        return b;
    }

    private static class ImplicitCasting extends ParameterizedRule {
        @Override
        public LogicalPlan apply(LogicalPlan plan, AnalyzerContext context) {
            return plan.transformExpressionsUp(ScalarFunction.class, e -> ImplicitCasting.cast(e, context.functionRegistry()));
        }

        private static Expression cast(ScalarFunction f, EsqlFunctionRegistry registry) {
            if (f instanceof EsqlScalarFunction esf) {
                return processScalarFunction(esf, registry);
            }
            if (f instanceof EsqlArithmeticOperation || f instanceof BinaryComparison) {
                return processBinaryOperator((BinaryOperator) f);
            }
            if (f instanceof In in) {
                return processIn(in);
            }
            return f;
        }

        private static Expression processScalarFunction(EsqlScalarFunction f, EsqlFunctionRegistry registry) {
            List args = f.arguments();
            List targetDataTypes = registry.getDataTypeForStringLiteralConversion(f.getClass());
            if (targetDataTypes == null || targetDataTypes.isEmpty()) {
                return f;
            }
            List newChildren = new ArrayList<>(args.size());
            boolean childrenChanged = false;
            DataType targetDataType = DataType.NULL;
            Expression arg;
            for (int i = 0; i < args.size(); i++) {
                arg = args.get(i);
                if (arg.resolved() && arg.dataType() == KEYWORD && arg.foldable() && ((arg instanceof EsqlScalarFunction) == false)) {
                    if (i < targetDataTypes.size()) {
                        targetDataType = targetDataTypes.get(i);
                    }
                    if (targetDataType != DataType.NULL && targetDataType != DataType.UNSUPPORTED) {
                        Expression e = castStringLiteral(arg, targetDataType);
                        childrenChanged = true;
                        newChildren.add(e);
                        continue;
                    }
                }
                newChildren.add(args.get(i));
            }
            return childrenChanged ? f.replaceChildren(newChildren) : f;
        }

        private static Expression processBinaryOperator(BinaryOperator o) {
            Expression left = o.left();
            Expression right = o.right();
            if (left.resolved() == false || right.resolved() == false) {
                return o;
            }
            List newChildren = new ArrayList<>(2);
            boolean childrenChanged = false;
            DataType targetDataType = DataType.NULL;
            Expression from = Literal.NULL;

            if (left.dataType() == KEYWORD && left.foldable() && (left instanceof EsqlScalarFunction == false)) {
                if (supportsImplicitCasting(right.dataType())) {
                    targetDataType = right.dataType();
                    from = left;
                } else if (supportsImplicitTemporalCasting(right, o)) {
                    targetDataType = DATETIME;
                    from = left;
                }
            }
            if (right.dataType() == KEYWORD && right.foldable() && (right instanceof EsqlScalarFunction == false)) {
                if (supportsImplicitCasting(left.dataType())) {
                    targetDataType = left.dataType();
                    from = right;
                } else if (supportsImplicitTemporalCasting(left, o)) {
                    targetDataType = DATETIME;
                    from = right;
                }
            }
            if (from != Literal.NULL) {
                Expression e = castStringLiteral(from, targetDataType);
                newChildren.add(from == left ? e : left);
                newChildren.add(from == right ? e : right);
                childrenChanged = true;
            }
            return childrenChanged ? o.replaceChildren(newChildren) : o;
        }

        private static Expression processIn(In in) {
            Expression left = in.value();
            List right = in.list();

            if (left.resolved() == false || supportsImplicitCasting(left.dataType()) == false) {
                return in;
            }
            List newChildren = new ArrayList<>(right.size() + 1);
            boolean childrenChanged = false;

            for (Expression value : right) {
                if (value.dataType() == KEYWORD && value.foldable()) {
                    Expression e = castStringLiteral(value, left.dataType());
                    newChildren.add(e);
                    childrenChanged = true;
                } else {
                    newChildren.add(value);
                }
            }
            newChildren.add(left);
            return childrenChanged ? in.replaceChildren(newChildren) : in;
        }

        private static boolean supportsImplicitTemporalCasting(Expression e, BinaryOperator o) {
            return isTemporalAmount(e.dataType()) && (o instanceof DateTimeArithmeticOperation);
        }

        private static boolean supportsImplicitCasting(DataType type) {
            return type == DATETIME || type == IP || type == VERSION || type == BOOLEAN;
        }

        public static Expression castStringLiteral(Expression from, DataType target) {
            assert from.foldable();
            try {
                Object to = EsqlDataTypeConverter.convert(from.fold(), target);
                return new Literal(from.source(), to, target);
            } catch (Exception e) {
                String message = LoggerMessageFormat.format(
                    "Cannot convert string [{}] to [{}], error [{}]",
                    from.fold(),
                    target,
                    e.getMessage()
                );
                return new UnsupportedAttribute(
                    from.source(),
                    String.valueOf(from.fold()),
                    new UnsupportedEsField(String.valueOf(from.fold()), from.dataType().typeName()),
                    message
                );
            }
        }
    }

    /**
     * The EsqlIndexResolver will create InvalidMappedField instances for fields that are ambiguous (i.e. have multiple mappings).
     * During ResolveRefs we do not convert these to UnresolvedAttribute instances, as we want to first determine if they can
     * instead be handled by conversion functions within the query. This rule looks for matching conversion functions and converts
     * those fields into MultiTypeEsField, which encapsulates the knowledge of how to convert these into a single type.
     * This knowledge will be used later in generating the FieldExtractExec with built-in type conversion.
     * Any fields which could not be resolved by conversion functions will be converted to UnresolvedAttribute instances in a later rule
     * (See UnresolveUnionTypes below).
     */
    private static class ResolveUnionTypes extends Rule {

        record TypeResolutionKey(String fieldName, DataType fieldType) {}

        private List unionFieldAttributes;

        @Override
        public LogicalPlan apply(LogicalPlan plan) {
            unionFieldAttributes = new ArrayList<>();
            // Collect field attributes from previous runs
            plan.forEachUp(EsRelation.class, rel -> {
                for (Attribute attr : rel.output()) {
                    if (attr instanceof FieldAttribute fa && fa.field() instanceof MultiTypeEsField) {
                        unionFieldAttributes.add(fa);
                    }
                }
            });

            return plan.transformUp(LogicalPlan.class, p -> p.resolved() || p.childrenResolved() == false ? p : doRule(p));
        }

        private LogicalPlan doRule(LogicalPlan plan) {
            int alreadyAddedUnionFieldAttributes = unionFieldAttributes.size();
            // See if the eval function has an unresolved MultiTypeEsField field
            // Replace the entire convert function with a new FieldAttribute (containing type conversion knowledge)
            plan = plan.transformExpressionsOnly(
                AbstractConvertFunction.class,
                convert -> resolveConvertFunction(convert, unionFieldAttributes)
            );
            // If no union fields were generated, return the plan as is
            if (unionFieldAttributes.size() == alreadyAddedUnionFieldAttributes) {
                return plan;
            }

            // In ResolveRefs the aggregates are resolved from the groupings, which might have an unresolved MultiTypeEsField.
            // Now that we have resolved those, we need to re-resolve the aggregates.
            if (plan instanceof EsqlAggregate agg) {
                // If the union-types resolution occurred in a child of the aggregate, we need to check the groupings
                plan = agg.transformExpressionsOnly(FieldAttribute.class, UnionTypesCleanup::checkUnresolved);

                // Aggregates where the grouping key comes from a union-type field need to be resolved against the grouping key
                Map resolved = new HashMap<>();
                for (Expression e : agg.groupings()) {
                    Attribute attr = Expressions.attribute(e);
                    if (attr != null && attr.resolved()) {
                        resolved.put(attr, e);
                    }
                }
                plan = plan.transformExpressionsOnly(UnresolvedAttribute.class, ua -> resolveAttribute(ua, resolved));
            }

            // And add generated fields to EsRelation, so these new attributes will appear in the OutputExec of the Fragment
            // and thereby get used in FieldExtractExec
            plan = plan.transformDown(EsRelation.class, esr -> {
                List missing = new ArrayList<>();
                for (FieldAttribute fa : unionFieldAttributes) {
                    // Using outputSet().contains looks by NameId, resp. uses semanticEquals.
                    if (esr.outputSet().contains(fa) == false) {
                        missing.add(fa);
                    }
                }

                if (missing.isEmpty() == false) {
                    List newOutput = new ArrayList<>(esr.output());
                    newOutput.addAll(missing);
                    return new EsRelation(esr.source(), esr.index(), newOutput, esr.indexMode(), esr.frozen());
                }
                return esr;
            });
            return plan;
        }

        private Expression resolveAttribute(UnresolvedAttribute ua, Map resolved) {
            var named = resolveAgainstList(ua, resolved.keySet());
            return switch (named.size()) {
                case 0 -> ua;
                case 1 -> named.get(0).equals(ua) ? ua : resolved.get(named.get(0));
                default -> ua.withUnresolvedMessage("Resolved [" + ua + "] unexpectedly to multiple attributes " + named);
            };
        }

        private Expression resolveConvertFunction(AbstractConvertFunction convert, List unionFieldAttributes) {
            if (convert.field() instanceof FieldAttribute fa && fa.field() instanceof InvalidMappedField imf) {
                HashMap typeResolutions = new HashMap<>();
                Set supportedTypes = convert.supportedTypes();
                imf.getTypesToIndices().keySet().forEach(typeName -> {
                    DataType type = DataType.fromTypeName(typeName);
                    if (supportedTypes.contains(type)) {
                        TypeResolutionKey key = new TypeResolutionKey(fa.name(), type);
                        var concreteConvert = typeSpecificConvert(convert, fa.source(), type, imf);
                        typeResolutions.put(key, concreteConvert);
                    }
                });
                // If all mapped types were resolved, create a new FieldAttribute with the resolved MultiTypeEsField
                if (typeResolutions.size() == imf.getTypesToIndices().size()) {
                    var resolvedField = resolvedMultiTypeEsField(fa, typeResolutions);
                    return createIfDoesNotAlreadyExist(fa, resolvedField, unionFieldAttributes);
                }
            } else if (convert.field() instanceof AbstractConvertFunction subConvert) {
                return convert.replaceChildren(Collections.singletonList(resolveConvertFunction(subConvert, unionFieldAttributes)));
            }
            return convert;
        }

        private Expression createIfDoesNotAlreadyExist(
            FieldAttribute fa,
            MultiTypeEsField resolvedField,
            List unionFieldAttributes
        ) {
            // Generate new ID for the field and suffix it with the data type to maintain unique attribute names.
            String unionTypedFieldName = LogicalPlanOptimizer.rawTemporaryName(
                fa.name(),
                "converted_to",
                resolvedField.getDataType().typeName()
            );
            FieldAttribute unionFieldAttribute = new FieldAttribute(fa.source(), fa.parent(), unionTypedFieldName, resolvedField);
            int existingIndex = unionFieldAttributes.indexOf(unionFieldAttribute);
            if (existingIndex >= 0) {
                // Do not generate multiple name/type combinations with different IDs
                return unionFieldAttributes.get(existingIndex);
            } else {
                unionFieldAttributes.add(unionFieldAttribute);
                return unionFieldAttribute;
            }
        }

        private MultiTypeEsField resolvedMultiTypeEsField(FieldAttribute fa, HashMap typeResolutions) {
            Map typesToConversionExpressions = new HashMap<>();
            InvalidMappedField imf = (InvalidMappedField) fa.field();
            imf.getTypesToIndices().forEach((typeName, indexNames) -> {
                DataType type = DataType.fromTypeName(typeName);
                TypeResolutionKey key = new TypeResolutionKey(fa.name(), type);
                if (typeResolutions.containsKey(key)) {
                    typesToConversionExpressions.put(typeName, typeResolutions.get(key));
                }
            });
            return MultiTypeEsField.resolveFrom(imf, typesToConversionExpressions);
        }

        private Expression typeSpecificConvert(AbstractConvertFunction convert, Source source, DataType type, InvalidMappedField mtf) {
            EsField field = new EsField(mtf.getName(), type, mtf.getProperties(), mtf.isAggregatable());
            NameId id = ((FieldAttribute) convert.field()).id();
            FieldAttribute resolvedAttr = new FieldAttribute(source, null, field.getName(), field, null, Nullability.TRUE, id, false);
            return convert.replaceChildren(Collections.singletonList(resolvedAttr));
        }
    }

    /**
     * {@link ResolveUnionTypes} creates new, synthetic attributes for union types:
     * If there was no {@code AbstractConvertFunction} that resolved multi-type fields in the {@link ResolveUnionTypes} rule,
     * then there could still be some {@code FieldAttribute}s that contain unresolved {@link MultiTypeEsField}s.
     * These need to be converted back to actual {@code UnresolvedAttribute} in order for validation to generate appropriate failures.
     * 

* Finally, if {@code client_ip} is present in 2 indices, once with type {@code ip} and once with type {@code keyword}, * using {@code EVAL x = to_ip(client_ip)} will create a single attribute @{code $$client_ip$converted_to$ip}. * This should not spill into the query output, so we drop such attributes at the end. */ private static class UnionTypesCleanup extends Rule { public LogicalPlan apply(LogicalPlan plan) { LogicalPlan planWithCheckedUnionTypes = plan.transformUp(LogicalPlan.class, p -> { if (p instanceof EsRelation esRelation) { // Leave esRelation as InvalidMappedField so that UNSUPPORTED fields can still pass through return esRelation; } return p.transformExpressionsOnly(FieldAttribute.class, UnionTypesCleanup::checkUnresolved); }); // To drop synthetic attributes at the end, we need to compute the plan's output. // This is only legal to do if the plan is resolved. return planWithCheckedUnionTypes.resolved() ? planWithoutSyntheticAttributes(planWithCheckedUnionTypes) : planWithCheckedUnionTypes; } static Attribute checkUnresolved(FieldAttribute fa) { if (fa.field() instanceof InvalidMappedField imf) { String unresolvedMessage = "Cannot use field [" + fa.name() + "] due to ambiguities being " + imf.errorMessage(); return new UnresolvedAttribute(fa.source(), fa.name(), fa.qualifier(), fa.id(), unresolvedMessage, null); } return fa; } private static LogicalPlan planWithoutSyntheticAttributes(LogicalPlan plan) { List output = plan.output(); List newOutput = new ArrayList<>(output.size()); for (Attribute attr : output) { // TODO: this should really use .synthetic() // https://github.com/elastic/elasticsearch/issues/105821 if (attr.name().startsWith(FieldAttribute.SYNTHETIC_ATTRIBUTE_NAME_PREFIX) == false) { newOutput.add(attr); } } return newOutput.size() == output.size() ? plan : new Project(Source.EMPTY, plan, newOutput); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy