Skip to content

Commit 0a4d40e

Browse files
Fix ClassCastException in PPL multisearch on indexes with @timestamp alias field (#5577)
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 <gingeekrishna@gmail.com>
1 parent cc65d75 commit 0a4d40e

3 files changed

Lines changed: 43 additions & 2 deletions

File tree

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteMultisearchCommandIT.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55

66
package org.opensearch.sql.calcite.remote;
77

8+
import static org.junit.Assume.assumeFalse;
89
import static org.opensearch.sql.legacy.TestsConstants.*;
910
import static org.opensearch.sql.util.Capability.MULTISEARCH_COLUMN_ORDER;
1011
import static org.opensearch.sql.util.Capability.MULTISEARCH_SAME_INDEX_CONFLATION;
1112
import static org.opensearch.sql.util.MatcherUtils.rows;
1213
import static org.opensearch.sql.util.MatcherUtils.schema;
1314
import static org.opensearch.sql.util.MatcherUtils.verifyDataRows;
1415
import static org.opensearch.sql.util.MatcherUtils.verifySchema;
16+
import static org.opensearch.sql.util.MatcherUtils.verifySchemaInOrder;
1517

1618
import java.io.IOException;
1719
import org.json.JSONObject;
@@ -31,6 +33,7 @@ public void init() throws Exception {
3133
loadIndex(Index.TIME_TEST_DATA);
3234
loadIndex(Index.TIME_TEST_DATA2);
3335
loadIndex(Index.LOCATIONS_TYPE_CONFLICT);
36+
loadIndex(Index.DATA_TYPE_ALIAS);
3437
}
3538

3639
@Test
@@ -462,4 +465,42 @@ public void testMultisearchTypeConflictWithStats() {
462465
.getMessage()
463466
.contains("Unable to process column 'age' due to incompatible types:"));
464467
}
468+
469+
/**
470+
* Regression test for GitHub issue #5533. When {@code @timestamp} is defined as a field-type
471+
* alias in the index mapping, multisearch used to throw:
472+
*
473+
* <pre>ClassCastException: RelCompositeTrait cannot be cast to RelCollation</pre>
474+
*
475+
* <p>Root cause: {@code reIndexCollations()} and {@code pushDownSort()} both used {@code
476+
* RelTraitSet.plus()} which composes collation traits into a {@link
477+
* org.apache.calcite.rel.RelCompositeTrait} when a collation is already present. Calcite's {@code
478+
* RelTraitSet.getCollation()} then fails with a ClassCastException. Fixed by using {@code
479+
* RelTraitSet.replace()} instead to always replace the collation trait.
480+
*/
481+
@Test
482+
public void testMultisearchWithTimestampAliasFieldDoesNotThrow() throws IOException {
483+
// alias-typed fields are stripped when loading indices in analytics-engine parquet mode,
484+
// so @timestamp does not exist in TEST_INDEX_ALIAS on that route.
485+
assumeFalse(
486+
"alias-typed fields are stripped in analytics-engine parquet mode;"
487+
+ " @timestamp won't exist in TEST_INDEX_ALIAS on that route.",
488+
isAnalyticsParquetIndicesEnabled());
489+
// TEST_INDEX_ALIAS has @timestamp defined as an alias field pointing to original_date.
490+
// Running multisearch on such an index used to crash with ClassCastException.
491+
JSONObject result =
492+
executeQuery(
493+
String.format(
494+
"| multisearch "
495+
+ "[search source=%s | where original_col > 1 | fields original_col,"
496+
+ " @timestamp] "
497+
+ "[search source=%s | where original_col = 1 | fields original_col,"
498+
+ " @timestamp]",
499+
TEST_INDEX_ALIAS, TEST_INDEX_ALIAS));
500+
501+
verifySchemaInOrder(
502+
result, schema("original_col", null, "int"), schema("@timestamp", null, "timestamp"));
503+
// 2 rows from original_col > 1, 1 row from original_col = 1
504+
assertEquals(3, result.getInt("total"));
505+
}
465506
}

opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/AbstractCalciteIndexScan.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ && isAnyCollationNameInAggregators(collationNames)) {
323323
// aggregators.
324324
return null;
325325
}
326-
RelTraitSet traitsWithCollations = getTraitSet().plus(RelCollations.of(collations));
326+
RelTraitSet traitsWithCollations = getTraitSet().replace(RelCollations.of(collations));
327327
PushDownContext pushDownContextWithoutSort = this.pushDownContext.cloneWithoutSort();
328328
AbstractAction<?> action;
329329
Object digest;

opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/CalciteLogicalIndexScan.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ private RelTraitSet reIndexCollations(List<Integer> selectedColumns) {
321321
collation ->
322322
collation.withFieldIndex(selectedColumns.indexOf(collation.getFieldIndex())))
323323
.collect(Collectors.toList());
324-
newTraitSet = getTraitSet().plus(RelCollations.of(newCollations));
324+
newTraitSet = getTraitSet().replace(RelCollations.of(newCollations));
325325
} else {
326326
newTraitSet = getTraitSet();
327327
}

0 commit comments

Comments
 (0)