Skip to content

Commit b1c5970

Browse files
committed
fix(calcite): handle Limit, Alias projection, RelationSubquery, and Values dual-table in visitor
Add visitLimit to support LIMIT/OFFSET clauses. Handle Alias nodes in project list by referencing already-computed aggregate fields instead of re-analyzing. Add visitRelationSubquery for derived tables in FROM clause. Fix visitValues to treat a single empty row as a dual-table (SELECT without FROM). Make bucketNullable lookup null-safe with getOrDefault in visitAggregation. Add integration tests covering LIMIT OFFSET, aggregate with alias, GROUP BY without bucket nullable, SELECT with alias, derived table subquery, and SELECT without FROM clause. Signed-off-by: Chen Dai <daichen@amazon.com>
1 parent 6421658 commit b1c5970

2 files changed

Lines changed: 136 additions & 4 deletions

File tree

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,4 +259,96 @@ SELECT department, count(*)
259259
""")
260260
.assertErrorMessage("Encountered");
261261
}
262+
263+
@Test
264+
public void testSqlLimitOffset() {
265+
givenQuery(
266+
"""
267+
SELECT name
268+
FROM catalog.employees
269+
LIMIT 10 OFFSET 5\
270+
""")
271+
.assertPlan(
272+
"""
273+
LogicalProject(name=[$1])
274+
LogicalSort(offset=[5], fetch=[10])
275+
LogicalTableScan(table=[[catalog, employees]])
276+
""");
277+
}
278+
279+
@Test
280+
public void testSqlAggregateWithAlias() {
281+
givenQuery(
282+
"""
283+
SELECT department, COUNT(*) AS cnt
284+
FROM catalog.employees
285+
GROUP BY department\
286+
""")
287+
.assertPlan(
288+
"""
289+
LogicalAggregate(group=[{0}], COUNT(*)=[COUNT()])
290+
LogicalProject(department=[$3])
291+
LogicalTableScan(table=[[catalog, employees]])
292+
""");
293+
}
294+
295+
@Test
296+
public void testSqlGroupByWithoutBucketNullable() {
297+
givenQuery(
298+
"""
299+
SELECT age, COUNT(*) AS cnt
300+
FROM catalog.employees
301+
GROUP BY age\
302+
""")
303+
.assertPlan(
304+
"""
305+
LogicalAggregate(group=[{0}], COUNT(*)=[COUNT()])
306+
LogicalProject(age=[$2])
307+
LogicalTableScan(table=[[catalog, employees]])
308+
""");
309+
}
310+
311+
@Test
312+
public void testSqlSelectWithAlias() {
313+
givenQuery(
314+
"""
315+
SELECT age AS employee_age, name AS employee_name
316+
FROM catalog.employees\
317+
""")
318+
.assertPlan(
319+
"""
320+
LogicalProject(employee_age=[$2], employee_name=[$1])
321+
LogicalTableScan(table=[[catalog, employees]])
322+
""");
323+
}
324+
325+
@Test
326+
public void testSqlDerivedTableInFromClause() {
327+
// SELECT ... FROM (SELECT ...) AS t — exercises visitRelationSubquery override.
328+
givenQuery(
329+
"""
330+
SELECT t.id
331+
FROM (SELECT id, name FROM catalog.employees WHERE age > 30) AS t\
332+
""")
333+
.assertPlan(
334+
"""
335+
LogicalProject(t.id=[$0])
336+
LogicalFilter(condition=[>($2, 30)])
337+
LogicalTableScan(table=[[catalog, employees]])
338+
""");
339+
}
340+
341+
@Test
342+
public void testSqlSelectWithoutFromClause() {
343+
// SELECT 1 — exercises visitValues dual-table case (single empty row).
344+
givenQuery(
345+
"""
346+
SELECT 1\
347+
""")
348+
.assertPlan(
349+
"""
350+
LogicalSort(sort0=[$0], dir0=[ASC])
351+
LogicalValues(tuples=[[]])
352+
""");
353+
}
262354
}

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

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
import org.opensearch.sql.ast.tree.Head;
133133
import org.opensearch.sql.ast.tree.Join;
134134
import org.opensearch.sql.ast.tree.Kmeans;
135+
import org.opensearch.sql.ast.tree.Limit;
135136
import org.opensearch.sql.ast.tree.Lookup;
136137
import org.opensearch.sql.ast.tree.Lookup.OutputStrategy;
137138
import org.opensearch.sql.ast.tree.ML;
@@ -146,6 +147,7 @@
146147
import org.opensearch.sql.ast.tree.RareTopN;
147148
import org.opensearch.sql.ast.tree.Regex;
148149
import org.opensearch.sql.ast.tree.Relation;
150+
import org.opensearch.sql.ast.tree.RelationSubquery;
149151
import org.opensearch.sql.ast.tree.Rename;
150152
import org.opensearch.sql.ast.tree.Replace;
151153
import org.opensearch.sql.ast.tree.ReplacePair;
@@ -541,6 +543,25 @@ private List<RexNode> expandProjectFields(
541543
.filter(addedFields::add)
542544
.forEach(field -> expandedFields.add(context.relBuilder.field(field)));
543545
}
546+
case Alias alias -> {
547+
String aliasName =
548+
Strings.isNullOrEmpty(alias.getAlias()) ? alias.getName() : alias.getAlias();
549+
// When an aggregate was already computed (its output name matches a current field),
550+
// reference the existing field instead of re-analyzing (which would return null).
551+
if (alias.getDelegated() instanceof AggregateFunction) {
552+
String aggFieldName = alias.getName();
553+
if (currentFields.contains(aliasName)) {
554+
expandedFields.add(context.relBuilder.field(aliasName));
555+
} else if (aggFieldName != null && currentFields.contains(aggFieldName)) {
556+
expandedFields.add(
557+
context.relBuilder.alias(context.relBuilder.field(aggFieldName), aliasName));
558+
} else {
559+
expandedFields.add(rexVisitor.analyze(alias, context));
560+
}
561+
} else {
562+
expandedFields.add(rexVisitor.analyze(alias, context));
563+
}
564+
}
544565
default ->
545566
throw new IllegalStateException(
546567
"Unexpected expression type in project list: " + expr.getClass().getSimpleName());
@@ -763,6 +784,13 @@ public RelNode visitHead(Head node, CalcitePlanContext context) {
763784
return context.relBuilder.peek();
764785
}
765786

787+
@Override
788+
public RelNode visitLimit(Limit node, CalcitePlanContext context) {
789+
visitChildren(node, context);
790+
context.relBuilder.limit(node.getOffset(), node.getLimit());
791+
return context.relBuilder.peek();
792+
}
793+
766794
/**
767795
* Insert a reversed sort node after finding the original sort in the tree. This rebuilds the tree
768796
* with the reversed sort inserted right after the original sort.
@@ -1621,7 +1649,8 @@ private Pair<List<RexNode>, List<AggCall>> resolveAttributesForAggregation(
16211649
@Override
16221650
public RelNode visitAggregation(Aggregation node, CalcitePlanContext context) {
16231651
Argument.ArgumentMap statsArgs = Argument.ArgumentMap.of(node.getArgExprList());
1624-
Boolean bucketNullable = (Boolean) statsArgs.get(Argument.BUCKET_NULLABLE).getValue();
1652+
boolean bucketNullable =
1653+
(Boolean) statsArgs.getOrDefault(Argument.BUCKET_NULLABLE, Literal.TRUE).getValue();
16251654
int nGroup = node.getGroupExprList().size() + (Objects.nonNull(node.getSpan()) ? 1 : 0);
16261655
BitSet nonNullGroupMask = new BitSet(nGroup);
16271656
if (!bucketNullable) {
@@ -1928,6 +1957,15 @@ public RelNode visitSubqueryAlias(SubqueryAlias node, CalcitePlanContext context
19281957
return context.relBuilder.peek();
19291958
}
19301959

1960+
@Override
1961+
public RelNode visitRelationSubquery(RelationSubquery node, CalcitePlanContext context) {
1962+
// Derived table in FROM clause: SELECT ... FROM (SELECT ...) AS t
1963+
// Visit the inner query, then apply the alias as the relation name.
1964+
visitChildren(node, context);
1965+
context.relBuilder.as(node.getAliasAsTableName());
1966+
return context.relBuilder.peek();
1967+
}
1968+
19311969
@Override
19321970
public RelNode visitLookup(Lookup node, CalcitePlanContext context) {
19331971
// 1. resolve source side
@@ -4122,12 +4160,14 @@ public RelNode visitMvExpand(MvExpand mvExpand, CalcitePlanContext context) {
41224160

41234161
@Override
41244162
public RelNode visitValues(Values values, CalcitePlanContext context) {
4125-
if (values.getValues() == null || values.getValues().isEmpty()) {
4163+
List<List<Literal>> rows = values.getValues();
4164+
if (rows == null || rows.isEmpty() || (rows.size() == 1 && rows.get(0).isEmpty())) {
4165+
// Single empty row (dual table) = SELECT without FROM. Provide a 0-column relation
4166+
// so subsequent Project can evaluate constant expressions.
41264167
context.relBuilder.values(context.relBuilder.getTypeFactory().builder().build());
41274168
return context.relBuilder.peek();
4128-
} else {
4129-
throw new CalciteUnsupportedException("Explicit values node is unsupported in Calcite");
41304169
}
4170+
throw new CalciteUnsupportedException("Inline VALUES with literal rows is unsupported");
41314171
}
41324172

41334173
@Override

0 commit comments

Comments
 (0)