Skip to content

Commit ea39ffd

Browse files
authored
Add UNION ALL support to V3 SQL engine (opensearch-project#5506)
Add UNION ALL set operations to the Calcite-based unified query path. The legacy V2 SQL engine rejects UNION with a SyntaxCheckException so queries fall back to the legacy engine appropriately. Signed-off-by: Chen Dai <daichen@amazon.com>
1 parent 132f3b1 commit ea39ffd

8 files changed

Lines changed: 82 additions & 4 deletions

File tree

api/src/main/java/org/opensearch/sql/api/parser/SqlV2QueryParser.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
import static org.opensearch.sql.ast.dsl.AstDSL.existsSubquery;
99
import static org.opensearch.sql.ast.dsl.AstDSL.inSubquery;
1010
import static org.opensearch.sql.ast.dsl.AstDSL.join;
11+
import static org.opensearch.sql.ast.dsl.AstDSL.union;
1112

13+
import java.util.List;
1214
import java.util.Optional;
15+
import java.util.stream.Collectors;
1316
import org.antlr.v4.runtime.tree.ParseTree;
1417
import org.opensearch.sql.ast.expression.Not;
1518
import org.opensearch.sql.ast.expression.UnresolvedExpression;
@@ -22,6 +25,7 @@
2225
import org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.ExistsSubqueryExpressionAtomContext;
2326
import org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.InSubqueryPredicateContext;
2427
import org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.JoinClauseContext;
28+
import org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.UnionSelectContext;
2529
import org.opensearch.sql.sql.parser.AstBuilder;
2630
import org.opensearch.sql.sql.parser.AstExpressionBuilder;
2731
import org.opensearch.sql.sql.parser.AstStatementBuilder;
@@ -81,6 +85,13 @@ private JoinType toJoinType(JoinClauseContext ctx) {
8185
};
8286
}
8387

88+
@Override
89+
public UnresolvedPlan visitUnionSelect(UnionSelectContext ctx) {
90+
List<UnresolvedPlan> datasets =
91+
ctx.querySpecification().stream().map(this::visit).collect(Collectors.toList());
92+
return union(datasets);
93+
}
94+
8495
/**
8596
* Expression builder with IN/EXISTS subquery support. Accesses the enclosing AstBuilder to
8697
* visit subquery plan nodes. Must be created via {@link #createExpressionBuilder()} because the

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,42 @@ WHERE age NOT IN (SELECT age FROM catalog.departments WHERE dept_name = 'Enginee
200200
""");
201201
}
202202

203+
@Test
204+
public void testUnionAll() {
205+
givenQuery(
206+
"""
207+
SELECT name FROM catalog.employees UNION ALL SELECT dept_name FROM catalog.departments
208+
""")
209+
.assertPlan(
210+
"""
211+
LogicalUnion(all=[true])
212+
LogicalProject(name=[$1])
213+
LogicalTableScan(table=[[catalog, employees]])
214+
LogicalProject(dept_name=[$1])
215+
LogicalTableScan(table=[[catalog, departments]])
216+
""");
217+
}
218+
219+
@Test
220+
public void testMultiWayUnion() {
221+
givenQuery(
222+
"""
223+
SELECT name FROM catalog.employees
224+
UNION ALL SELECT dept_name FROM catalog.departments
225+
UNION ALL SELECT name FROM catalog.employees
226+
""")
227+
.assertPlan(
228+
"""
229+
LogicalUnion(all=[true])
230+
LogicalProject(name=[$1])
231+
LogicalTableScan(table=[[catalog, employees]])
232+
LogicalProject(dept_name=[$1])
233+
LogicalTableScan(table=[[catalog, departments]])
234+
LogicalProject(name=[$1])
235+
LogicalTableScan(table=[[catalog, employees]])
236+
""");
237+
}
238+
203239
@Test
204240
public void testNotExistsSubquery() {
205241
givenQuery(

core/src/main/java/org/opensearch/sql/ast/dsl/AstDSL.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
import org.opensearch.sql.ast.tree.SubqueryAlias;
8585
import org.opensearch.sql.ast.tree.TableFunction;
8686
import org.opensearch.sql.ast.tree.Trendline;
87+
import org.opensearch.sql.ast.tree.Union;
8788
import org.opensearch.sql.ast.tree.UnresolvedPlan;
8889
import org.opensearch.sql.ast.tree.Values;
8990
import org.opensearch.sql.calcite.plan.OpenSearchConstants;
@@ -781,4 +782,8 @@ public static InSubquery inSubquery(UnresolvedPlan query, UnresolvedExpression..
781782
public static ExistsSubquery existsSubquery(UnresolvedPlan query) {
782783
return new ExistsSubquery(query);
783784
}
785+
786+
public static Union union(List<UnresolvedPlan> datasets) {
787+
return new Union(datasets);
788+
}
784789
}

core/src/main/java/org/opensearch/sql/ast/tree/Union.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,25 @@
2121
@RequiredArgsConstructor
2222
@AllArgsConstructor
2323
public class Union extends UnresolvedPlan {
24+
/** Input subplans (operands) combined by this UNION ALL. */
2425
private final List<UnresolvedPlan> datasets;
2526

27+
/** Whether inputs are unified to a common schema by name (PPL) vs combined positionally (SQL). */
28+
private boolean unifySchema;
29+
30+
/** Optional cap on output rows (PPL {@code maxout}); {@code null} if unbounded. */
2631
private Integer maxout;
2732

33+
/** PPL constructor: UNION ALL with schema unification. */
34+
public Union(List<UnresolvedPlan> datasets, Integer maxout) {
35+
this(datasets, true, maxout);
36+
}
37+
2838
@Override
2939
public UnresolvedPlan attach(UnresolvedPlan child) {
3040
List<UnresolvedPlan> newDatasets =
3141
ImmutableList.<UnresolvedPlan>builder().add(child).addAll(datasets).build();
32-
return new Union(newDatasets, maxout);
42+
return new Union(newDatasets, unifySchema, maxout);
3343
}
3444

3545
@Override

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3031,8 +3031,10 @@ public RelNode visitUnion(Union node, CalcitePlanContext context) {
30313031
"Union command requires at least two datasets. Provided: " + inputNodes.size());
30323032
}
30333033

3034-
List<RelNode> unifiedInputs =
3035-
SchemaUnifier.buildUnifiedSchemaWithTypeCoercion(inputNodes, context);
3034+
List<RelNode> unifiedInputs = inputNodes;
3035+
if (node.isUnifySchema()) {
3036+
unifiedInputs = SchemaUnifier.buildUnifiedSchemaWithTypeCoercion(inputNodes, context);
3037+
}
30363038

30373039
for (RelNode input : unifiedInputs) {
30383040
context.relBuilder.push(input);

sql/src/main/antlr/OpenSearchSQLParser.g4

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ dmlStatement
6868

6969
// Primary DML Statements
7070
selectStatement
71-
: querySpecification # simpleSelect
71+
: querySpecification # simpleSelect
72+
| querySpecification (UNION ALL querySpecification)+ # unionSelect
7273
;
7374

7475
adminStatement

sql/src/main/java/org/opensearch/sql/sql/parser/AstBuilder.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,12 @@ public UnresolvedPlan visitJoinClause(OpenSearchSQLParser.JoinClauseContext ctx)
263263
"JOIN is not supported in the V2 SQL engine. Falling back to legacy engine.");
264264
}
265265

266+
@Override
267+
public UnresolvedPlan visitUnionSelect(OpenSearchSQLParser.UnionSelectContext ctx) {
268+
throw new SyntaxCheckException(
269+
"UNION is not supported in the V2 SQL engine. Falling back to legacy engine.");
270+
}
271+
266272
@Override
267273
public UnresolvedPlan visitHavingClause(HavingClauseContext ctx) {
268274
AstHavingFilterBuilder builder = new AstHavingFilterBuilder(context.peek());

sql/src/test/java/org/opensearch/sql/sql/parser/AstBuilderTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,13 @@ public UnresolvedPlan visitJoinClause(OpenSearchSQLParser.JoinClauseContext ctx)
757757
assertNotNull(new SQLSyntaxParser().parse(query).accept(builder));
758758
}
759759

760+
@Test
761+
public void union_throws_syntax_check_exception() {
762+
assertThrows(
763+
SyntaxCheckException.class,
764+
() -> buildAST("SELECT name FROM t1 UNION ALL SELECT name FROM t2"));
765+
}
766+
760767
@Test
761768
public void in_subquery_throws_syntax_check_exception() {
762769
assertThrows(

0 commit comments

Comments
 (0)