99
1010import org .elasticsearch .common .logging .HeaderWarning ;
1111import org .elasticsearch .common .logging .LoggerMessageFormat ;
12- import org .elasticsearch .common .util .Maps ;
1312import org .elasticsearch .compute .data .Block ;
1413import org .elasticsearch .core .Strings ;
1514import org .elasticsearch .index .IndexMode ;
1817import org .elasticsearch .xpack .esql .Column ;
1918import org .elasticsearch .xpack .esql .EsqlIllegalArgumentException ;
2019import org .elasticsearch .xpack .esql .VerificationException ;
21- import org .elasticsearch .xpack .esql .action .EsqlCapabilities ;
2220import org .elasticsearch .xpack .esql .analysis .AnalyzerRules .ParameterizedAnalyzerRule ;
2321import org .elasticsearch .xpack .esql .common .Failure ;
2422import org .elasticsearch .xpack .esql .core .capabilities .Resolvables ;
6361import org .elasticsearch .xpack .esql .expression .function .scalar .convert .ConvertFunction ;
6462import org .elasticsearch .xpack .esql .expression .function .scalar .convert .FoldablesConvertFunction ;
6563import org .elasticsearch .xpack .esql .expression .function .scalar .convert .ToDateNanos ;
66- import org .elasticsearch .xpack .esql .expression .function .scalar .convert .ToDatetime ;
6764import org .elasticsearch .xpack .esql .expression .function .scalar .convert .ToDouble ;
6865import org .elasticsearch .xpack .esql .expression .function .scalar .convert .ToInteger ;
6966import org .elasticsearch .xpack .esql .expression .function .scalar .convert .ToLong ;
9491import org .elasticsearch .xpack .esql .plan .logical .Project ;
9592import org .elasticsearch .xpack .esql .plan .logical .Rename ;
9693import org .elasticsearch .xpack .esql .plan .logical .RrfScoreEval ;
97- import org .elasticsearch .xpack .esql .plan .logical .UnaryPlan ;
9894import org .elasticsearch .xpack .esql .plan .logical .UnresolvedRelation ;
9995import org .elasticsearch .xpack .esql .plan .logical .inference .Completion ;
10096import org .elasticsearch .xpack .esql .plan .logical .inference .InferencePlan ;
153149import static org .elasticsearch .xpack .esql .core .type .DataType .TIME_DURATION ;
154150import static org .elasticsearch .xpack .esql .core .type .DataType .UNSUPPORTED ;
155151import static org .elasticsearch .xpack .esql .core .type .DataType .VERSION ;
156- import static org .elasticsearch .xpack .esql .core .type .DataType .isMillisOrNanos ;
157152import static org .elasticsearch .xpack .esql .core .type .DataType .isTemporalAmount ;
158153import static org .elasticsearch .xpack .esql .telemetry .FeatureMetric .LIMIT ;
159154import static org .elasticsearch .xpack .esql .type .EsqlDataTypeConverter .maybeParseTemporalAmount ;
@@ -180,10 +175,11 @@ public class Analyzer extends ParameterizedRuleExecutor<LogicalPlan, AnalyzerCon
180175 new ResolveFunctions (),
181176 new ResolveUnionTypesInEsRelation ()
182177 ),
183- new Batch <>("Resolution" , new ResolveRefs (), new ImplicitCasting (), new ResolveUnionTypes () // Must be after ResolveRefs, so union
184- // types can be found
185- // Must be after ResolveUnionTypes, if there is explicit casting on the union typed fields, implicit casting won't be added
186- // new ImplicitCastingForUnionTypedFields()
178+ new Batch <>(
179+ "Resolution" ,
180+ new ResolveRefs (),
181+ new ImplicitCasting (),
182+ new ResolveUnionTypes () // Must be after ResolveRefs, so union types can be found
187183 ),
188184 new Batch <>("Finish Analysis" , Limiter .ONCE , new AddImplicitLimit (), new AddImplicitForkLimit (), new UnionTypesCleanup ())
189185 );
@@ -1680,15 +1676,16 @@ private Expression resolveConvertFunction(ConvertFunction convert, List<FieldAtt
16801676 return createIfDoesNotAlreadyExist (fa , resolvedField , unionFieldAttributes );
16811677 }
16821678 } else if (convert .field () instanceof FieldAttribute fa
1683- && fa .synthetic () == false
1679+ && fa .synthetic () == false // MultiTypeEsField in EsRelation created by ResolveUnionTypesInEsRelation has synthetic = false
16841680 && fa .field () instanceof MultiTypeEsField mtf ) {
16851681 // This is an explicit casting of a union typed field that has been converted to MultiTypeEsField in EsRelation by
1686- // ResolveUnionTypesInEsRelation, do not do double casting.
1682+ // ResolveUnionTypesInEsRelation, it is not necessary to cast it into date_nanos and then do explicit casting.
16871683 if (((Expression ) convert ).dataType () == mtf .getDataType ()) {
1688- // The same data type between implicit and explicit casting, explicit conversion is not needed
1684+ // The same data type between implicit(date_nanos) and explicit casting, explicit conversion is not needed
16891685 return convert .field ();
16901686 } else {
1691- // TODO Is there an easy way to convert from one MultiTypeEsField to another MultiTypeEsField?
1687+ // Data type is different between implicit(date_nanos) and explicit casting, create a new MultiTypeEsField with
1688+ // explicit casting type. TODO Is there an easy way to convert one MultiTypeEsField to another MultiTypeEsField?
16921689 HashMap <TypeResolutionKey , Expression > typeResolutions = new HashMap <>();
16931690 Set <DataType > supportedTypes = convert .supportedTypes ();
16941691 Holder <Boolean > conversionSupported = new Holder <>(true );
@@ -1705,10 +1702,11 @@ private Expression resolveConvertFunction(ConvertFunction convert, List<FieldAtt
17051702 conversionSupported .set (false );
17061703 }
17071704 });
1708- // If the conversions are supported, create a new FieldAttribute with a new MultiTypeEsField, and add it to
1709- // unionFieldAttributes.
1705+ // If the conversions are supported, all the data types in a MultiTypeEsField can be cast to the explicit casting
1706+ // data type, create a new FieldAttribute with a new MultiTypeEsField, and add it to unionFieldAttributes.
17101707 if (conversionSupported .get ()) {
1711- // build the map between index name and conversion expressions
1708+ // Build the mapping between index name and conversion expressions, as a MultiTypeEsField does not store the
1709+ // mapping between data types and index names,
17121710 Map <String , Expression > indexToConversionExpressions = new HashMap <>();
17131711 for (Map .Entry <String , Expression > entry : mtf .getIndexToConversionExpressions ().entrySet ()) {
17141712 String indexName = entry .getKey ();
@@ -1851,186 +1849,6 @@ private static LogicalPlan planWithoutSyntheticAttributes(LogicalPlan plan) {
18511849 }
18521850 }
18531851
1854- /**
1855- * Cast union typed fields that are mixed of date and date_nanos types into date_nanos.
1856- */
1857- private static class ImplicitCastingForUnionTypedFields extends ParameterizedRule <LogicalPlan , LogicalPlan , AnalyzerContext > {
1858- @ Override
1859- public LogicalPlan apply (LogicalPlan plan , AnalyzerContext context ) {
1860- if (EsqlCapabilities .Cap .IMPLICIT_CASTING_DATE_AND_DATE_NANOS .isEnabled () == false ) {
1861- return plan ;
1862- }
1863- // This rule should be applied after ResolveUnionTypes, so that the InvalidMappedFields with explicit casting are converted into
1864- // MultiTypeEsField, and don't get double cast here.
1865- Map <FieldAttribute , Alias > invalidMappedFieldCasted = new HashMap <>();
1866- LogicalPlan transformedPlan = plan .transformUp (LogicalPlan .class , p -> {
1867- // exclude LookupJoin for now, as it doesn't support date_nanos as join key yet
1868- if (p instanceof UnaryPlan == false ) {
1869- return p ;
1870- }
1871- Set <FieldAttribute > invalidMappedFields = invalidMappedFieldsInLogicalPlan (p );
1872- if (invalidMappedFields .isEmpty () == false ) {
1873- // If we are at a plan node that has invalid mapped fields, we need to either add an EVAL, or if that has been done
1874- // we should instead replace with the already cast field
1875- Map <FieldAttribute , Alias > newAliases = Maps .newHashMapWithExpectedSize (invalidMappedFields .size ());
1876- Map <FieldAttribute , Alias > existingAliases = Maps .newHashMapWithExpectedSize (invalidMappedFields .size ());
1877- for (FieldAttribute fa : invalidMappedFields ) {
1878- if (invalidMappedFieldCasted .containsKey (fa )) {
1879- // There is already an eval plan created for the implicit cast field, just reference to it
1880- Alias alias = invalidMappedFieldCasted .get (fa );
1881- existingAliases .put (fa , alias );
1882- } else {
1883- // Create a new alias and later on add a new EVAL with this new aliases for implicit casting
1884- DataType targetType = commonDataType (fa );
1885- if (targetType != null ) {
1886- Expression conversionFunction = castInvalidMappedField (targetType , fa );
1887- Alias alias = new Alias (fa .source (), fa .name (), conversionFunction );
1888- newAliases .put (fa , alias );
1889- invalidMappedFieldCasted .put (fa , alias );
1890- }
1891- }
1892- }
1893- // If there are new aliases created, create a new eval child with new aliases for the current plan。
1894- // How many children does a LogicalPlan have? Only deal with UnaryPlan and LookupJoin for now.
1895- if (newAliases .isEmpty () == false ) { // create a new eval child plan
1896- UnaryPlan u = (UnaryPlan ) p ; // this must be a unary plan, as it is checked at the beginning of plan loop
1897- Eval eval = new Eval (u .source (), u .child (), newAliases .values ().stream ().toList ());
1898- p = u .replaceChild (eval );
1899- // TODO Lookup join does not work on date_nanos field yet, joining on a date_nanos field does not find a match.
1900- // And lookup up join is a special case as a lookup join has two children, after date_nanos is supported as a join
1901- // key, the transformation needs to take it into account.
1902- }
1903- // If there are new or existing aliases identified, combine them into one map
1904- Map <FieldAttribute , Alias > allAliases = Maps .newHashMapWithExpectedSize (invalidMappedFields .size ());
1905- allAliases .putAll (newAliases );
1906- allAliases .putAll (existingAliases );
1907- if (allAliases .isEmpty () == false ) { // there is already eval plan for that union typed field, reference to the aliases
1908- p = p .transformExpressionsOnly (FieldAttribute .class , fa -> {
1909- Alias alias = allAliases .get (fa );
1910- return alias != null ? alias .toAttribute () : fa ;
1911- });
1912- // MvExpand and Stats have ReferenceAttribute referencing the FieldAttribute in the same plan.
1913- // The ReferenceAttribute need to be updated to point to the casting expression.
1914- if (p instanceof MvExpand mvExpand ) {
1915- p = transformMvExpand (mvExpand );
1916- } else if (p instanceof Aggregate aggregate ) {
1917- p = transformAggregate (aggregate );
1918- }
1919- }
1920- }
1921- return p ;
1922- });
1923- transformedPlan = castInvalidMappedFieldInFinalOutput (transformedPlan );
1924- return transformedPlan ;
1925- }
1926-
1927- /**
1928- * Find a common data type that the union typed field can cast to, only date and date_nanos types are supported.
1929- * This method can be extended to support implicit casting for the other data types.
1930- */
1931- private static DataType commonDataType (FieldAttribute unionTypedField ) {
1932- DataType targetType = null ;
1933- if (unionTypedField .field () instanceof InvalidMappedField imf ) {
1934- for (DataType type : imf .types ()) {
1935- if (isMillisOrNanos (type ) == false ) { // if there is field that is no date or date_nanos, don't do implicit casting
1936- return null ;
1937- }
1938- if (targetType == null ) { // initialize the target type to the first type
1939- targetType = type ;
1940- } else if (targetType == DATE_NANOS || type == DATE_NANOS ) {
1941- targetType = DATE_NANOS ;
1942- }
1943- }
1944- }
1945- return targetType ;
1946- }
1947-
1948- /**
1949- * Do implicit casting for date and date_nanos only.
1950- */
1951- private static Expression castInvalidMappedField (DataType targetType , FieldAttribute fa ) {
1952- Source source = fa .source ();
1953- return switch (targetType ) {
1954- case DATETIME -> new ToDatetime (source , fa ); // in case we decided to use DATE as a common type instead of DATE_NANOS
1955- case DATE_NANOS -> new ToDateNanos (source , fa );
1956- default -> throw new EsqlIllegalArgumentException ("unexpected data type: " + targetType );
1957- };
1958- }
1959-
1960- /**
1961- * Return all the FieldAttribute that contain InvalidMappedField in the current plan.
1962- */
1963- private static Set <FieldAttribute > invalidMappedFieldsInLogicalPlan (LogicalPlan plan ) {
1964- Set <FieldAttribute > fas = new HashSet <>();
1965- // Invalid mapped fields are legal at EsRelation level, as long as they are not used elsewhere. In the final output, if they
1966- // have not been dropped, implicit cast will be added for them, so that we can return not null values, the implicit casting is
1967- // deferred to when the fields are used or returned.
1968- if (plan instanceof EsRelation == false ) {
1969- plan .forEachExpression (FieldAttribute .class , fa -> {
1970- if (fa .field () instanceof InvalidMappedField ) {
1971- fas .add (fa );
1972- }
1973- });
1974- }
1975- return fas ;
1976- }
1977-
1978- /**
1979- * Cast the InvalidMappedFields in the final output of the query, this is needed when these fields are not referenced in the query
1980- * explicitly, so there is no chance to cast them to a common type earlier, an example of such query is from index*.
1981- */
1982- private static LogicalPlan castInvalidMappedFieldInFinalOutput (LogicalPlan logicalPlan ) {
1983- // Check the output of the query, if the top level plan is resolved, check if there is InvalidMappedField in its output,
1984- // if so add a project with eval, so that a not null value can be returned for a union typed field
1985- if (logicalPlan .resolved ()) {
1986- List <Attribute > output = logicalPlan .output ();
1987- Map <FieldAttribute , Alias > newAliases = Maps .newHashMapWithExpectedSize (output .size ());
1988- output .forEach (a -> {
1989- if (a instanceof FieldAttribute fa && fa .field () instanceof InvalidMappedField ) {
1990- DataType targetType = commonDataType (fa );
1991- if (targetType != null ) {
1992- Expression conversionFunction = castInvalidMappedField (targetType , fa );
1993- Alias alias = new Alias (fa .source (), fa .name (), conversionFunction );
1994- newAliases .put (fa , alias );
1995- }
1996- }
1997- });
1998- if (newAliases .isEmpty () == false ) { // add an Eval for the union typed fields left that are not cast implicitly yet
1999- if (logicalPlan instanceof EsRelation esr ) {
2000- // EsRelation does not have a child, we should not see row here, add a eval on top of it
2001- logicalPlan = new Eval (esr .source (), esr , newAliases .values ().stream ().toList ());
2002- } else if (logicalPlan instanceof UnaryPlan unary ) {
2003- // Add an Eval as the child of this plan
2004- Eval eval = new Eval (unary .source (), unary .child (), newAliases .values ().stream ().toList ());
2005- logicalPlan = unary .replaceChild (eval );
2006- }
2007- // TODO LookupJoin is a binary plan, it does not create a new field, ideally adding an Eval on top of it should be fine,
2008- // however because the output of a LookupJoin does not include InvalidMappedFields even the LHS output has
2009- // InvalidMappedFields, it is a bug need to be addressed
2010- }
2011- }
2012- return logicalPlan ;
2013- }
2014-
2015- private static MvExpand transformMvExpand (MvExpand mvExpand ) {
2016- NamedExpression target = mvExpand .target ();
2017- return new MvExpand (mvExpand .source (), mvExpand .child (), target , target .toAttribute ());
2018- }
2019-
2020- private static Aggregate transformAggregate (Aggregate aggregate ) {
2021- List <? extends NamedExpression > aggregates = aggregate .aggregates ();
2022- List <Expression > groupings = aggregate .groupings ();
2023- List <NamedExpression > aggregatesWithNewRefs = new ArrayList <>(aggregates .size ());
2024- for (int i = 0 ; i < aggregates .size () - groupings .size (); i ++) {
2025- aggregatesWithNewRefs .add (aggregates .get (i ));
2026- }
2027- for (Expression e : groupings ) { // Add groupings
2028- aggregatesWithNewRefs .add (Expressions .attribute (e ));
2029- }
2030- return aggregate .with (groupings , aggregatesWithNewRefs );
2031- }
2032- }
2033-
20341852 /**
20351853 * Cast the union typed fields in EsRelation to date_nanos if they are mixed date and date_nanos types.
20361854 */
0 commit comments