Skip to content

Commit 71541b1

Browse files
authored
Add Calcite native SQL planning in UnifiedQueryPlanner (#5257)
* feat(api): Add Calcite native SQL planning path in UnifiedQueryPlanner Add SQL support to the unified query API using Calcite's native parser pipeline (SqlParser → SqlValidator → SqlToRelConverter → RelNode), bypassing the ANTLR parser used by PPL. Changes: - UnifiedQueryPlanner: use PlanningStrategy to dispatch CalciteNativeStrategy vs CustomVisitorStrategy - CalciteNativeStrategy: Calcite Planner with try-with-resources for ANSI SQL - CustomVisitorStrategy: ANTLR-based path for PPL (and future SQL V2) - UnifiedQueryContext: SqlParser.Config with Casing.UNCHANGED to preserve lowercase OpenSearch index names Signed-off-by: Chen Dai <daichen@amazon.com> * test(api): Add SQL planner tests and refactor test base for multi-language support - Refactor UnifiedQueryTestBase with queryType() hook for subclass override - Add UnifiedSqlQueryPlannerTest covering SELECT, WHERE, GROUP BY, JOIN, ORDER BY, subquery, case sensitivity, namespaces, and error handling - Update UnifiedQueryContextTest to verify SQL context creation Signed-off-by: Chen Dai <daichen@amazon.com> * perf(benchmarks): Add SQL queries to UnifiedQueryBenchmark Add language (PPL/SQL) and queryPattern param dimensions for side-by-side comparison of equivalent queries across both languages. Remove separate UnifiedSqlQueryBenchmark in favor of unified class. Signed-off-by: Chen Dai <daichen@amazon.com> * docs(api): Update README to reflect SQL support in UnifiedQueryPlanner Signed-off-by: Chen Dai <daichen@amazon.com> * fix(api): Normalize trailing whitespace in assertPlan comparison RelOptUtil.toString() appends a trailing newline after the last plan node, which doesn't match Java text block expectations. Also add \r\n normalization for Windows CI compatibility, consistent with the existing pattern in core module tests. Signed-off-by: Chen Dai <daichen@amazon.com> --------- Signed-off-by: Chen Dai <daichen@amazon.com>
1 parent d903f24 commit 71541b1

File tree

7 files changed

+409
-67
lines changed

7 files changed

+409
-67
lines changed

api/README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This module provides components organized into two main areas aligned with the [
88

99
### Unified Language Specification
1010

11-
- **`UnifiedQueryPlanner`**: Accepts PPL (Piped Processing Language) queries and returns Calcite `RelNode` logical plans as intermediate representation.
11+
- **`UnifiedQueryPlanner`**: Accepts PPL (Piped Processing Language) or SQL queries and returns Calcite `RelNode` logical plans as intermediate representation.
1212
- **`UnifiedQueryTranspiler`**: Converts Calcite logical plans (`RelNode`) into SQL strings for various target databases using different SQL dialects.
1313

1414
### Unified Execution Runtime
@@ -17,7 +17,7 @@ This module provides components organized into two main areas aligned with the [
1717
- **`UnifiedFunction`**: Engine-agnostic function interface that enables functions to be evaluated across different execution engines without engine-specific code duplication.
1818
- **`UnifiedFunctionRepository`**: Repository for discovering and loading functions as `UnifiedFunction` instances, providing a bridge between function definitions and external execution engines.
1919

20-
Together, these components enable complete workflows: parse PPL queries into logical plans, transpile those plans into target database SQL, compile and execute queries directly, or export PPL functions for use in external execution engines.
20+
Together, these components enable complete workflows: parse PPL or SQL queries into logical plans, transpile those plans into target database SQL, compile and execute queries directly, or export PPL functions for use in external execution engines.
2121

2222
### Experimental API Design
2323

@@ -33,7 +33,7 @@ Create a context with catalog configuration, query type, and optional settings:
3333

3434
```java
3535
UnifiedQueryContext context = UnifiedQueryContext.builder()
36-
.language(QueryType.PPL)
36+
.language(QueryType.PPL) // or QueryType.SQL for SQL
3737
.catalog("opensearch", opensearchSchema)
3838
.catalog("spark_catalog", sparkSchema)
3939
.defaultNamespace("opensearch")
@@ -44,7 +44,7 @@ UnifiedQueryContext context = UnifiedQueryContext.builder()
4444

4545
### UnifiedQueryPlanner
4646

47-
Use `UnifiedQueryPlanner` to parse and analyze PPL queries into Calcite logical plans. The planner accepts a `UnifiedQueryContext` and can be reused for multiple queries.
47+
Use `UnifiedQueryPlanner` to parse and analyze PPL or SQL queries into Calcite logical plans. The planner accepts a `UnifiedQueryContext` and can be reused for multiple queries.
4848

4949
```java
5050
// Create planner with context
@@ -53,6 +53,9 @@ UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
5353
// Plan multiple queries (context is reused)
5454
RelNode plan1 = planner.plan("source = logs | where status = 200");
5555
RelNode plan2 = planner.plan("source = metrics | stats avg(cpu)");
56+
57+
// SQL queries are also supported (with QueryType.SQL context)
58+
RelNode plan3 = planner.plan("SELECT * FROM logs WHERE status = 200");
5659
```
5760

5861
### UnifiedQueryTranspiler
@@ -226,5 +229,4 @@ public class MySchema extends AbstractSchema {
226229

227230
## Future Work
228231

229-
- Expand support to SQL language.
230232
- Extend planner to generate optimized physical plans using Calcite's optimization frameworks.

api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.Map;
1515
import java.util.Objects;
1616
import lombok.Value;
17+
import org.apache.calcite.avatica.util.Casing;
1718
import org.apache.calcite.jdbc.CalciteSchema;
1819
import org.apache.calcite.plan.RelTraitDef;
1920
import org.apache.calcite.rel.metadata.DefaultRelMetadataProvider;
@@ -176,13 +177,18 @@ private FrameworkConfig buildFrameworkConfig() {
176177

177178
SchemaPlus defaultSchema = findSchemaByPath(rootSchema, defaultNamespace);
178179
return Frameworks.newConfigBuilder()
179-
.parserConfig(SqlParser.Config.DEFAULT)
180+
.parserConfig(buildParserConfig())
180181
.defaultSchema(defaultSchema)
181182
.traitDefs((List<RelTraitDef>) null)
182183
.programs(Programs.calc(DefaultRelMetadataProvider.INSTANCE))
183184
.build();
184185
}
185186

187+
private SqlParser.Config buildParserConfig() {
188+
// Preserve identifier case for lowercase OpenSearch index names
189+
return SqlParser.Config.DEFAULT.withUnquotedCasing(Casing.UNCHANGED);
190+
}
191+
186192
private SchemaPlus findSchemaByPath(SchemaPlus rootSchema, String defaultPath) {
187193
if (defaultPath == null) {
188194
return rootSchema;

api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java

Lines changed: 63 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,21 @@
55

66
package org.opensearch.sql.api;
77

8+
import lombok.RequiredArgsConstructor;
89
import org.antlr.v4.runtime.tree.ParseTree;
910
import org.apache.calcite.rel.RelCollation;
1011
import org.apache.calcite.rel.RelCollations;
1112
import org.apache.calcite.rel.RelNode;
13+
import org.apache.calcite.rel.RelRoot;
1214
import org.apache.calcite.rel.core.Sort;
1315
import org.apache.calcite.rel.logical.LogicalSort;
16+
import org.apache.calcite.sql.SqlNode;
17+
import org.apache.calcite.tools.Frameworks;
18+
import org.apache.calcite.tools.Planner;
1419
import org.opensearch.sql.ast.statement.Query;
1520
import org.opensearch.sql.ast.statement.Statement;
1621
import org.opensearch.sql.ast.tree.UnresolvedPlan;
1722
import org.opensearch.sql.calcite.CalciteRelNodeVisitor;
18-
import org.opensearch.sql.common.antlr.Parser;
1923
import org.opensearch.sql.common.antlr.SyntaxCheckException;
2024
import org.opensearch.sql.executor.QueryType;
2125
import org.opensearch.sql.ppl.antlr.PPLSyntaxParser;
@@ -28,36 +32,32 @@
2832
* such as Spark or command-line tools, abstracting away Calcite internals.
2933
*/
3034
public class UnifiedQueryPlanner {
31-
/** The parser instance responsible for converting query text into a parse tree. */
32-
private final Parser parser;
3335

34-
/** Unified query context containing CalcitePlanContext with all configuration. */
35-
private final UnifiedQueryContext context;
36-
37-
/** AST-to-RelNode visitor that builds logical plans from the parsed AST. */
38-
private final CalciteRelNodeVisitor relNodeVisitor =
39-
new CalciteRelNodeVisitor(new EmptyDataSourceService());
36+
/** Planning strategy selected at construction time based on query type. */
37+
private final PlanningStrategy strategy;
4038

4139
/**
4240
* Constructs a UnifiedQueryPlanner with a unified query context.
4341
*
4442
* @param context the unified query context containing CalcitePlanContext
4543
*/
4644
public UnifiedQueryPlanner(UnifiedQueryContext context) {
47-
this.parser = buildQueryParser(context.getPlanContext().queryType);
48-
this.context = context;
45+
this.strategy =
46+
context.getPlanContext().queryType == QueryType.SQL
47+
? new CalciteNativeStrategy(context)
48+
: new CustomVisitorStrategy(context);
4949
}
5050

5151
/**
5252
* Parses and analyzes a query string into a Calcite logical plan (RelNode). TODO: Generate
5353
* optimal physical plan to fully unify query execution and leverage Calcite's optimizer.
5454
*
55-
* @param query the raw query string in PPL or other supported syntax
55+
* @param query the raw query string in PPL or SQL syntax
5656
* @return a logical plan representing the query
5757
*/
5858
public RelNode plan(String query) {
5959
try {
60-
return preserveCollation(analyze(parse(query)));
60+
return strategy.plan(query);
6161
} catch (SyntaxCheckException e) {
6262
// Re-throw syntax error without wrapping
6363
throw e;
@@ -66,38 +66,63 @@ public RelNode plan(String query) {
6666
}
6767
}
6868

69-
private Parser buildQueryParser(QueryType queryType) {
70-
if (queryType == QueryType.PPL) {
71-
return new PPLSyntaxParser();
72-
}
73-
throw new IllegalArgumentException("Unsupported query type: " + queryType);
69+
/** Strategy interface for language-specific planning logic. */
70+
private interface PlanningStrategy {
71+
RelNode plan(String query) throws Exception;
7472
}
7573

76-
private UnresolvedPlan parse(String query) {
77-
ParseTree cst = parser.parse(query);
78-
AstStatementBuilder astStmtBuilder =
79-
new AstStatementBuilder(
80-
new AstBuilder(query, context.getSettings()),
81-
AstStatementBuilder.StatementBuilderContext.builder().build());
82-
Statement statement = cst.accept(astStmtBuilder);
74+
/** ANSI SQL planning using Calcite's native SqlParser → SqlValidator → SqlToRelConverter. */
75+
@RequiredArgsConstructor
76+
private static class CalciteNativeStrategy implements PlanningStrategy {
77+
private final UnifiedQueryContext context;
8378

84-
if (statement instanceof Query) {
85-
return ((Query) statement).getPlan();
79+
@Override
80+
public RelNode plan(String query) throws Exception {
81+
try (Planner planner = Frameworks.getPlanner(context.getPlanContext().config)) {
82+
SqlNode parsed = planner.parse(query);
83+
SqlNode validated = planner.validate(parsed);
84+
RelRoot relRoot = planner.rel(validated);
85+
return relRoot.project();
86+
}
8687
}
87-
throw new UnsupportedOperationException(
88-
"Only query statements are supported but got " + statement.getClass().getSimpleName());
8988
}
9089

91-
private RelNode analyze(UnresolvedPlan ast) {
92-
return relNodeVisitor.analyze(ast, context.getPlanContext());
93-
}
90+
/** AST-based planning via ANTLR parser → UnresolvedPlan → CalciteRelNodeVisitor. */
91+
@RequiredArgsConstructor
92+
private static class CustomVisitorStrategy implements PlanningStrategy {
93+
private final UnifiedQueryContext context;
94+
private final PPLSyntaxParser parser = new PPLSyntaxParser();
95+
private final CalciteRelNodeVisitor relNodeVisitor =
96+
new CalciteRelNodeVisitor(new EmptyDataSourceService());
97+
98+
@Override
99+
public RelNode plan(String query) {
100+
UnresolvedPlan ast = parse(query);
101+
RelNode logical = relNodeVisitor.analyze(ast, context.getPlanContext());
102+
return preserveCollation(logical);
103+
}
104+
105+
private UnresolvedPlan parse(String query) {
106+
ParseTree cst = parser.parse(query);
107+
AstStatementBuilder astStmtBuilder =
108+
new AstStatementBuilder(
109+
new AstBuilder(query, context.getSettings()),
110+
AstStatementBuilder.StatementBuilderContext.builder().build());
111+
Statement statement = cst.accept(astStmtBuilder);
112+
113+
if (statement instanceof Query) {
114+
return ((Query) statement).getPlan();
115+
}
116+
throw new UnsupportedOperationException(
117+
"Only query statements are supported but got " + statement.getClass().getSimpleName());
118+
}
94119

95-
private RelNode preserveCollation(RelNode logical) {
96-
RelNode calcitePlan = logical;
97-
RelCollation collation = logical.getTraitSet().getCollation();
98-
if (!(logical instanceof Sort) && collation != RelCollations.EMPTY) {
99-
calcitePlan = LogicalSort.create(logical, collation, null, null);
120+
private RelNode preserveCollation(RelNode logical) {
121+
RelCollation collation = logical.getTraitSet().getCollation();
122+
if (!(logical instanceof Sort) && collation != RelCollations.EMPTY) {
123+
return LogicalSort.create(logical, collation, null, null);
124+
}
125+
return logical;
100126
}
101-
return calcitePlan;
102127
}
103128
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,15 @@ public void testMissingQueryType() {
6363
UnifiedQueryContext.builder().catalog("opensearch", testSchema).build();
6464
}
6565

66-
@Test(expected = IllegalArgumentException.class)
67-
public void testUnsupportedQueryType() {
66+
@Test
67+
public void testSqlQueryType() {
6868
UnifiedQueryContext context =
6969
UnifiedQueryContext.builder()
70-
.language(QueryType.SQL) // only PPL is supported for now
70+
.language(QueryType.SQL)
7171
.catalog("opensearch", testSchema)
7272
.build();
73-
new UnifiedQueryPlanner(context);
73+
UnifiedQueryPlanner planner = new UnifiedQueryPlanner(context);
74+
assertNotNull("SQL planner should be created", planner);
7475
}
7576

7677
@Test(expected = IllegalArgumentException.class)

0 commit comments

Comments
 (0)