Skip to content

Commit cb5a0de

Browse files
committed
fix(calcite): preserve SQL aggregate aliases in plan
SELECT col, COUNT(*) AS cnt produces an identity-shaped projection that only renames a field. RelBuilder.project (force=false) treats such a rename-only projection over unchanged columns as a no-op and skips emitting the LogicalProject, so the alias was dropped from the output schema (the column came back as the raw expression name, e.g. COUNT(*)). Force the projection only when an AS genuinely renames a field, mirroring how Calcite's own SqlToRelConverter materializes SELECT-list aliases. The PPL path is unaffected: it never produces an AS RexCall through visitProject (rename/eval/fields/stats build output names differently). Signed-off-by: Chen Dai <daichen@amazon.com>
1 parent ea39ffd commit cb5a0de

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerSqlV2Test.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,42 @@ SELECT department, COUNT(*) AS cnt FROM catalog.employees
355355
""");
356356
}
357357

358+
@Test
359+
public void testOrderByAggregateAlias() {
360+
givenQuery(
361+
"""
362+
SELECT department, COUNT(*) AS cnt FROM catalog.employees
363+
GROUP BY department ORDER BY cnt DESC LIMIT 3
364+
""")
365+
.assertPlan(
366+
"""
367+
LogicalSort(sort0=[$1], dir0=[DESC-nulls-last])
368+
LogicalProject(department=[$1], cnt=[$0])
369+
LogicalSort(sort0=[$0], dir0=[DESC-nulls-last], fetch=[3])
370+
LogicalProject(COUNT(*)=[$1], department=[$0])
371+
LogicalAggregate(group=[{0}], COUNT(*)=[COUNT()])
372+
LogicalProject(department=[$3])
373+
LogicalTableScan(table=[[catalog, employees]])
374+
""");
375+
}
376+
377+
@Test
378+
public void testAliasPreservedInOutputSchema() {
379+
givenQuery("SELECT COUNT(*) AS cnt FROM catalog.employees").assertFields("cnt");
380+
381+
givenQuery("SELECT department, COUNT(*) AS cnt FROM catalog.employees GROUP BY department")
382+
.assertFields("department", "cnt");
383+
384+
givenQuery("SELECT department, COUNT(*) FROM catalog.employees GROUP BY department")
385+
.assertFields("department", "COUNT(*)");
386+
387+
givenQuery("SELECT MAX(age) + MIN(age) AS range_sum FROM catalog.employees")
388+
.assertFields("range_sum");
389+
390+
givenQuery("SELECT id, name, age AS years, department FROM catalog.employees")
391+
.assertFields("id", "name", "years", "department");
392+
}
393+
358394
@Test
359395
public void testHavingCompoundAnd() {
360396
givenQuery(

core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,11 +481,30 @@ public RelNode visitProject(Project node, CalcitePlanContext context) {
481481
if (!context.isResolvingSubquery()) {
482482
context.setProjectVisited(true);
483483
}
484-
context.relBuilder.project(expandedFields);
484+
485+
// Force the projection on a rename: without it, Calcite omits the project node when the
486+
// columns are unchanged (same fields and order), so an alias like COUNT(*) AS cnt is lost.
487+
boolean force = isRenameFieldsProject(expandedFields, currentFields);
488+
context.relBuilder.project(expandedFields, ImmutableList.of(), force);
485489
}
486490
return context.relBuilder.peek();
487491
}
488492

493+
private static boolean isRenameFieldsProject(List<RexNode> fields, List<String> currentFields) {
494+
for (RexNode r : fields) {
495+
if (r.getKind() == AS) {
496+
RexCall as = (RexCall) r;
497+
if (as.getOperands().get(0) instanceof RexInputRef ref) {
498+
String name = ((RexLiteral) as.getOperands().get(1)).getValueAs(String.class);
499+
if (!name.equals(currentFields.get(ref.getIndex()))) {
500+
return true;
501+
}
502+
}
503+
}
504+
}
505+
return false;
506+
}
507+
489508
private boolean isSingleAllFieldsProject(Project node) {
490509
return node.getProjectList().size() == 1
491510
&& node.getProjectList().getFirst() instanceof AllFields;

0 commit comments

Comments
 (0)