From 3f39114faa1fd1952ab118a239533e1e828f680d Mon Sep 17 00:00:00 2001 From: Louis Chu Date: Fri, 24 Jul 2026 01:38:22 +0800 Subject: [PATCH] Fix ClassCastException in preserveCollation on composite-collation plans UnifiedQueryPlanner.preserveCollation reads the collation via RelTraitSet.getCollation(), which casts to a single RelCollation. When a plan derives several collations (Calcite stores them as a RelCompositeTrait) the cast throws ClassCastException and the query fails to plan. A multi-column literal-row plan (e.g. makeresults data=, which lowers to a multi-column LogicalValues) derives such composite collations and hits this; a single-column plan derives one collation and survives. Read the collation through the composite-safe getTraits(RelCollationTraitDef.INSTANCE) and materialize a LogicalSort only when there is exactly one genuine non-empty sort collation. A composite (several incidental derived collations) is not a user sort, so the plan is left unwrapped. Single-collation behavior is unchanged. Signed-off-by: Louis Chu --- .../sql/api/UnifiedQueryPlanner.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java index 9440833503f..b5622f98a1e 100644 --- a/api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java +++ b/api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java @@ -7,9 +7,10 @@ import static org.opensearch.sql.monitor.profile.MetricName.ANALYZE; +import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.calcite.rel.RelCollation; -import org.apache.calcite.rel.RelCollations; +import org.apache.calcite.rel.RelCollationTraitDef; import org.apache.calcite.rel.RelNode; import org.apache.calcite.rel.RelRoot; import org.apache.calcite.rel.core.Sort; @@ -154,9 +155,20 @@ public RelNode plan(String query) { } private RelNode preserveCollation(RelNode logical) { - RelCollation collation = logical.getTraitSet().getCollation(); - if (!(logical instanceof Sort) && collation != RelCollations.EMPTY) { - return LogicalSort.create(logical, collation, null, null); + if (logical instanceof Sort) { + return logical; + } + // Only a genuine single-sort collation is materialized. A literal-row plan (makeresults + // data=) carries several incidental derived collations kept as a RelCompositeTrait; those + // are not a user sort, so getTraits returns more than one and we leave the plan unwrapped. + // RelTraitSet.getCollation() would instead cast the composite to a single RelCollation and + // throw. + List collations = + logical.getTraitSet().getTraits(RelCollationTraitDef.INSTANCE); + if (collations != null + && collations.size() == 1 + && !collations.get(0).getFieldCollations().isEmpty()) { + return LogicalSort.create(logical, collations.get(0), null, null); } return logical; }