Skip to content

Commit d6335d2

Browse files
committed
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>
1 parent ada2e34 commit d6335d2

2 files changed

Lines changed: 70 additions & 39 deletions

File tree

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
}

0 commit comments

Comments
 (0)