From 7fb0d696f8573da79d3dd32c98e6aa1840e50b71 Mon Sep 17 00:00:00 2001 From: Radhakrishnan Pachyappan Date: Sun, 21 Jun 2026 15:33:03 +0530 Subject: [PATCH] Fix ClassCastException in PPL multisearch on indexes with @timestamp alias field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #5533. When `@timestamp` is defined as a field-type alias in the index mapping, multisearch queries threw: ClassCastException: RelCompositeTrait cannot be cast to RelCollation Root cause: `reIndexCollations()` in `CalciteLogicalIndexScan` and `pushDownSort()` in `AbstractCalciteIndexScan` both called `RelTraitSet.plus()` to update the collation trait on a scan node. `plus()` *composes* traits — if the trait set already contains a `RelCollation`, it merges the old and new collations into a `RelCompositeTrait`. Calcite's `RelTraitSet.getCollation()` then does an unchecked cast `(RelCollation) getTrait(...)` which fails at runtime for `RelCompositeTrait`. The `@timestamp` alias path specifically triggers this because `wrapProjectForAliasFields()` adds a project on top of each sub-scan which is later pushed back down via `pushDownProject()`. `pushDownProject()` calls `reIndexCollations()` to remap field indices inside an existing collation — but re-using `plus()` here composes the existing sort collation with the re-indexed one, producing the bad composite. Fix: use `RelTraitSet.replace()` in both locations. `replace()` substitutes the collation trait in-place regardless of what was there before, which is the correct semantics for "this scan is now sorted by these columns". Added a regression IT (`testMultisearchWithTimestampAliasFieldDoesNotThrow`) that runs a multisearch against `TEST_INDEX_ALIAS`, whose mapping defines `@timestamp` as an alias for `original_date`. Signed-off-by: Radhakrishnan Pachyappan --- .../remote/CalciteMultisearchCommandIT.java | 41 +++++++++++++++++++ .../scan/AbstractCalciteIndexScan.java | 2 +- .../storage/scan/CalciteLogicalIndexScan.java | 2 +- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteMultisearchCommandIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteMultisearchCommandIT.java index 10cc7ffd459..383ae5e400f 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteMultisearchCommandIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteMultisearchCommandIT.java @@ -5,6 +5,7 @@ package org.opensearch.sql.calcite.remote; +import static org.junit.Assume.assumeFalse; import static org.opensearch.sql.legacy.TestsConstants.*; import static org.opensearch.sql.util.Capability.MULTISEARCH_COLUMN_ORDER; import static org.opensearch.sql.util.Capability.MULTISEARCH_SAME_INDEX_CONFLATION; @@ -12,6 +13,7 @@ import static org.opensearch.sql.util.MatcherUtils.schema; import static org.opensearch.sql.util.MatcherUtils.verifyDataRows; import static org.opensearch.sql.util.MatcherUtils.verifySchema; +import static org.opensearch.sql.util.MatcherUtils.verifySchemaInOrder; import java.io.IOException; import org.json.JSONObject; @@ -31,6 +33,7 @@ public void init() throws Exception { loadIndex(Index.TIME_TEST_DATA); loadIndex(Index.TIME_TEST_DATA2); loadIndex(Index.LOCATIONS_TYPE_CONFLICT); + loadIndex(Index.DATA_TYPE_ALIAS); } @Test @@ -462,4 +465,42 @@ public void testMultisearchTypeConflictWithStats() { .getMessage() .contains("Unable to process column 'age' due to incompatible types:")); } + + /** + * Regression test for GitHub issue #5533. When {@code @timestamp} is defined as a field-type + * alias in the index mapping, multisearch used to throw: + * + *
ClassCastException: RelCompositeTrait cannot be cast to RelCollation
+ * + *

Root cause: {@code reIndexCollations()} and {@code pushDownSort()} both used {@code + * RelTraitSet.plus()} which composes collation traits into a {@link + * org.apache.calcite.rel.RelCompositeTrait} when a collation is already present. Calcite's {@code + * RelTraitSet.getCollation()} then fails with a ClassCastException. Fixed by using {@code + * RelTraitSet.replace()} instead to always replace the collation trait. + */ + @Test + public void testMultisearchWithTimestampAliasFieldDoesNotThrow() throws IOException { + // alias-typed fields are stripped when loading indices in analytics-engine parquet mode, + // so @timestamp does not exist in TEST_INDEX_ALIAS on that route. + assumeFalse( + "alias-typed fields are stripped in analytics-engine parquet mode;" + + " @timestamp won't exist in TEST_INDEX_ALIAS on that route.", + isAnalyticsParquetIndicesEnabled()); + // TEST_INDEX_ALIAS has @timestamp defined as an alias field pointing to original_date. + // Running multisearch on such an index used to crash with ClassCastException. + JSONObject result = + executeQuery( + String.format( + "| multisearch " + + "[search source=%s | where original_col > 1 | fields original_col," + + " @timestamp] " + + "[search source=%s | where original_col = 1 | fields original_col," + + " @timestamp]", + TEST_INDEX_ALIAS, TEST_INDEX_ALIAS)); + + verifySchemaInOrder( + result, schema("original_col", null, "int"), schema("@timestamp", null, "timestamp")); + // 2 rows from original_col > 1, 1 row from original_col = 1 + assertEquals(3, result.getInt("total")); + } } diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/AbstractCalciteIndexScan.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/AbstractCalciteIndexScan.java index 609a5aaa92f..52d64fb5a73 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/AbstractCalciteIndexScan.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/AbstractCalciteIndexScan.java @@ -323,7 +323,7 @@ && isAnyCollationNameInAggregators(collationNames)) { // aggregators. return null; } - RelTraitSet traitsWithCollations = getTraitSet().plus(RelCollations.of(collations)); + RelTraitSet traitsWithCollations = getTraitSet().replace(RelCollations.of(collations)); PushDownContext pushDownContextWithoutSort = this.pushDownContext.cloneWithoutSort(); AbstractAction action; Object digest; diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/CalciteLogicalIndexScan.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/CalciteLogicalIndexScan.java index 740801ff418..2017437e7bd 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/CalciteLogicalIndexScan.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/CalciteLogicalIndexScan.java @@ -321,7 +321,7 @@ private RelTraitSet reIndexCollations(List selectedColumns) { collation -> collation.withFieldIndex(selectedColumns.indexOf(collation.getFieldIndex()))) .collect(Collectors.toList()); - newTraitSet = getTraitSet().plus(RelCollations.of(newCollations)); + newTraitSet = getTraitSet().replace(RelCollations.of(newCollations)); } else { newTraitSet = getTraitSet(); }