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