diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLDedupIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLDedupIT.java
index 9c93b12e6ac..4177d108440 100644
--- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLDedupIT.java
+++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLDedupIT.java
@@ -348,6 +348,45 @@ public void testMultiColumnSortThenDedup() throws IOException {
verifyDataRows(actual, rows("M", "AK", 20, 23), rows("F", "AK", 21, 334));
}
+ /**
+ * Regression test for https://github.com/opensearch-project/sql/issues/5150
+ *
+ *
A renamed field that is not the dedup key must retain its value after dedup aggregation
+ * pushdown. Previously the top_hits response returned the original index field name ({@code
+ * name}), which the enumerator could not resolve to the renamed output name ({@code nm}),
+ * yielding null.
+ */
+ @Test
+ public void testDedupWithRenamedField() throws IOException {
+ JSONObject actual =
+ executeQuery(
+ String.format(
+ "source=%s | rename name as nm | dedup 1 category | fields category, nm",
+ TEST_INDEX_DUPLICATION_NULLABLE));
+ // One representative row per category; nm must not be null
+ verifyDataRows(actual, rows("X", "A"), rows("Z", "B"), rows("Y", "A"));
+ }
+
+ /**
+ * Regression test for https://github.com/opensearch-project/sql/issues/5197
+ *
+ *
When both a {@code rename} and an {@code eval} column-reference resolve to the same original
+ * index field, the old {@code Map<String,String>} mapping silently dropped one alias on
+ * collision. With {@code Map<String,List<String>>} both aliases must appear in the
+ * result with correct values.
+ */
+ @Test
+ public void testDedupWithRenamedFieldMappingCollision() throws IOException {
+ JSONObject actual =
+ executeQuery(
+ String.format(
+ "source=%s | eval nm2 = name | rename name as nm | dedup 1 category"
+ + " | fields category, nm, nm2",
+ TEST_INDEX_DUPLICATION_NULLABLE));
+ // Both nm (from rename) and nm2 (from eval col-ref) must carry the same non-null name value
+ verifyDataRows(actual, rows("X", "A", "A"), rows("Z", "B", "B"), rows("Y", "A", "A"));
+ }
+
/** Regression test for https://github.com/opensearch-project/sql/issues/3922 */
@Test
public void testSortThenDedupKeepEmpty() throws IOException {
diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/request/AggregateAnalyzer.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/request/AggregateAnalyzer.java
index f919fdc0e30..775b0278683 100644
--- a/opensearch/src/main/java/org/opensearch/sql/opensearch/request/AggregateAnalyzer.java
+++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/request/AggregateAnalyzer.java
@@ -38,6 +38,7 @@
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -618,7 +619,31 @@ yield switch (functionName) {
topHitsAggregationBuilder.sort(
SortBuilders.fieldSort(key.field()).order(order).missing(missing));
}
- yield Pair.of(topHitsAggregationBuilder, new TopHitsParser(aggName, false, false));
+ // Build a mapping from original index field name to output (renamed) field names.
+ // top_hits returns _source / fields keyed by the original index name, but the Calcite
+ // row-type uses the renamed output name. A single original field may map to multiple
+ // output names when both a rename and an eval column-ref resolve to the same source
+ // field (issue #5197), so the value is a List rather than a single String.
+ Map> fieldNameMapping = new HashMap<>();
+ for (Pair arg : args) {
+ if (arg.getKey() instanceof RexInputRef) {
+ NamedFieldExpression namedField = helper.inferNamedField(arg.getKey());
+ if (namedField == null) {
+ continue;
+ }
+ String originalName = namedField.getRootName();
+ String outputName = arg.getValue();
+ if (!originalName.equals(outputName)) {
+ fieldNameMapping
+ .computeIfAbsent(originalName, k -> new ArrayList<>())
+ .add(outputName);
+ }
+ }
+ }
+ yield Pair.of(
+ topHitsAggregationBuilder,
+ new TopHitsParser(
+ aggName, false, false, fieldNameMapping.isEmpty() ? null : fieldNameMapping));
}
default ->
throw new AggregateAnalyzer.AggregateAnalyzerException(
diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/response/agg/TopHitsParser.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/response/agg/TopHitsParser.java
index f9c3d5bb5d2..9fa3589f1fb 100644
--- a/opensearch/src/main/java/org/opensearch/sql/opensearch/response/agg/TopHitsParser.java
+++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/response/agg/TopHitsParser.java
@@ -12,6 +12,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import javax.annotation.Nullable;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.opensearch.common.document.DocumentField;
@@ -27,10 +28,29 @@ public class TopHitsParser implements MetricParser {
private final boolean returnSingleValue;
private final boolean returnMergeValue;
+ /**
+ * Maps each original OpenSearch field name to one or more output (renamed) field names. Used by
+ * the dedup aggregation pushdown path when {@code rename} creates aliases that differ from the
+ * index field name: top_hits returns {@code _source} / {@code fields} entries keyed by the
+ * original name, while the Calcite row-type expects the renamed name. A single original field may
+ * map to multiple output names when both {@code rename} and an {@code eval} column reference
+ * resolve to the same source field (issue #5197).
+ */
+ @Nullable private final Map> fieldNameMapping;
+
public TopHitsParser(String name, boolean returnSingleValue, boolean returnMergeValue) {
+ this(name, returnSingleValue, returnMergeValue, null);
+ }
+
+ public TopHitsParser(
+ String name,
+ boolean returnSingleValue,
+ boolean returnMergeValue,
+ @Nullable Map> fieldNameMapping) {
this.name = name;
this.returnSingleValue = returnSingleValue;
this.returnMergeValue = returnMergeValue;
+ this.fieldNameMapping = fieldNameMapping;
}
@Override
@@ -129,12 +149,43 @@ public List