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(); }