Skip to content

Commit eb41b88

Browse files
committed
Add UnifiedQueryParser with language-specific implementations
Extract parsing logic from UnifiedQueryPlanner into a UnifiedQueryParser interface with language-specific implementations: PPLQueryParser (returns UnresolvedPlan) and CalciteSqlQueryParser (returns SqlNode). UnifiedQueryContext owns the parser instance, created eagerly by the builder which has direct access to query type and future SQL config. Each implementation receives only its required dependencies: PPLQueryParser takes Settings, CalciteSqlQueryParser takes CalcitePlanContext. UnifiedQueryPlanner.CustomVisitorStrategy now obtains the parser from the context via the interface type. Signed-off-by: Chen Dai <daichen@amazon.com>
1 parent 71541b1 commit eb41b88

7 files changed

Lines changed: 189 additions & 31 deletions

File tree

api/README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ 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) or SQL queries and returns Calcite `RelNode` logical plans as intermediate representation.
11+
- **`UnifiedQueryParser`**: Parses PPL (Piped Processing Language) or SQL queries and returns the native parse result (`UnresolvedPlan` for PPL, `SqlNode` for Calcite SQL).
12+
- **`UnifiedQueryPlanner`**: Accepts PPL or SQL queries and returns Calcite `RelNode` logical plans as intermediate representation.
1213
- **`UnifiedQueryTranspiler`**: Converts Calcite logical plans (`RelNode`) into SQL strings for various target databases using different SQL dialects.
1314

1415
### Unified Execution Runtime
@@ -42,6 +43,20 @@ UnifiedQueryContext context = UnifiedQueryContext.builder()
4243
.build();
4344
```
4445

46+
### UnifiedQueryParser
47+
48+
Use `UnifiedQueryParser` to parse queries into their native parse tree. The parser is owned by `UnifiedQueryContext` and returns the native parse result for each language.
49+
50+
```java
51+
// PPL parsing
52+
UnresolvedPlan ast = (UnresolvedPlan) context.getParser().parse("source = logs | where status = 200");
53+
54+
// SQL parsing (with QueryType.SQL context)
55+
SqlNode sqlNode = (SqlNode) sqlContext.getParser().parse("SELECT * FROM logs WHERE status = 200");
56+
```
57+
58+
Callers can then use each language's native visitor infrastructure (`AbstractNodeVisitor` for PPL, `SqlBasicVisitor` for Calcite SQL) on the typed result for further analysis.
59+
4560
### UnifiedQueryPlanner
4661

4762
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.

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
import java.util.List;
1414
import java.util.Map;
1515
import java.util.Objects;
16-
import lombok.Value;
16+
import lombok.AllArgsConstructor;
17+
import lombok.Getter;
1718
import org.apache.calcite.avatica.util.Casing;
1819
import org.apache.calcite.jdbc.CalciteSchema;
1920
import org.apache.calcite.plan.RelTraitDef;
@@ -24,6 +25,9 @@
2425
import org.apache.calcite.tools.FrameworkConfig;
2526
import org.apache.calcite.tools.Frameworks;
2627
import org.apache.calcite.tools.Programs;
28+
import org.opensearch.sql.api.parser.CalciteSqlQueryParser;
29+
import org.opensearch.sql.api.parser.PPLQueryParser;
30+
import org.opensearch.sql.api.parser.UnifiedQueryParser;
2731
import org.opensearch.sql.calcite.CalcitePlanContext;
2832
import org.opensearch.sql.calcite.SysLimit;
2933
import org.opensearch.sql.common.setting.Settings;
@@ -34,14 +38,18 @@
3438
* centralizes configuration for catalog schemas, query type, execution limits, and other settings,
3539
* enabling consistent behavior across all unified query operations.
3640
*/
37-
@Value
41+
@AllArgsConstructor
42+
@Getter
3843
public class UnifiedQueryContext implements AutoCloseable {
3944

4045
/** CalcitePlanContext containing Calcite framework configuration and query type. */
41-
CalcitePlanContext planContext;
46+
private final CalcitePlanContext planContext;
4247

4348
/** Settings containing execution limits and feature flags used by parsers and planners. */
44-
Settings settings;
49+
private final Settings settings;
50+
51+
/** Query parser created eagerly from this context's configuration. */
52+
private final UnifiedQueryParser<?> parser;
4553

4654
/**
4755
* Closes the underlying resource managed by this context.
@@ -152,7 +160,14 @@ public UnifiedQueryContext build() {
152160
CalcitePlanContext planContext =
153161
CalcitePlanContext.create(
154162
buildFrameworkConfig(), SysLimit.fromSettings(settings), queryType);
155-
return new UnifiedQueryContext(planContext, settings);
163+
return new UnifiedQueryContext(planContext, settings, createParser(planContext, settings));
164+
}
165+
166+
private UnifiedQueryParser<?> createParser(CalcitePlanContext planContext, Settings settings) {
167+
return switch (queryType) {
168+
case PPL -> new PPLQueryParser(settings);
169+
case SQL -> new CalciteSqlQueryParser(planContext);
170+
};
156171
}
157172

158173
private Settings buildSettings() {

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

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
package org.opensearch.sql.api;
77

88
import lombok.RequiredArgsConstructor;
9-
import org.antlr.v4.runtime.tree.ParseTree;
109
import org.apache.calcite.rel.RelCollation;
1110
import org.apache.calcite.rel.RelCollations;
1211
import org.apache.calcite.rel.RelNode;
@@ -16,15 +15,11 @@
1615
import org.apache.calcite.sql.SqlNode;
1716
import org.apache.calcite.tools.Frameworks;
1817
import org.apache.calcite.tools.Planner;
19-
import org.opensearch.sql.ast.statement.Query;
20-
import org.opensearch.sql.ast.statement.Statement;
18+
import org.opensearch.sql.api.parser.UnifiedQueryParser;
2119
import org.opensearch.sql.ast.tree.UnresolvedPlan;
2220
import org.opensearch.sql.calcite.CalciteRelNodeVisitor;
2321
import org.opensearch.sql.common.antlr.SyntaxCheckException;
2422
import org.opensearch.sql.executor.QueryType;
25-
import org.opensearch.sql.ppl.antlr.PPLSyntaxParser;
26-
import org.opensearch.sql.ppl.parser.AstBuilder;
27-
import org.opensearch.sql.ppl.parser.AstStatementBuilder;
2823

2924
/**
3025
* {@code UnifiedQueryPlanner} provides a high-level API for parsing and analyzing queries using the
@@ -87,36 +82,26 @@ public RelNode plan(String query) throws Exception {
8782
}
8883
}
8984

90-
/** AST-based planning via ANTLR parser → UnresolvedPlan → CalciteRelNodeVisitor. */
91-
@RequiredArgsConstructor
85+
/** AST-based planning via context-owned parser → UnresolvedPlan → CalciteRelNodeVisitor. */
9286
private static class CustomVisitorStrategy implements PlanningStrategy {
9387
private final UnifiedQueryContext context;
94-
private final PPLSyntaxParser parser = new PPLSyntaxParser();
88+
private final UnifiedQueryParser<UnresolvedPlan> parser;
9589
private final CalciteRelNodeVisitor relNodeVisitor =
9690
new CalciteRelNodeVisitor(new EmptyDataSourceService());
9791

92+
@SuppressWarnings("unchecked")
93+
CustomVisitorStrategy(UnifiedQueryContext context) {
94+
this.context = context;
95+
this.parser = (UnifiedQueryParser<UnresolvedPlan>) context.getParser();
96+
}
97+
9898
@Override
9999
public RelNode plan(String query) {
100-
UnresolvedPlan ast = parse(query);
100+
UnresolvedPlan ast = parser.parse(query);
101101
RelNode logical = relNodeVisitor.analyze(ast, context.getPlanContext());
102102
return preserveCollation(logical);
103103
}
104104

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-
}
119-
120105
private RelNode preserveCollation(RelNode logical) {
121106
RelCollation collation = logical.getTraitSet().getCollation();
122107
if (!(logical instanceof Sort) && collation != RelCollations.EMPTY) {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.api.parser;
7+
8+
import lombok.RequiredArgsConstructor;
9+
import org.apache.calcite.sql.SqlNode;
10+
import org.apache.calcite.sql.parser.SqlParseException;
11+
import org.apache.calcite.sql.parser.SqlParser;
12+
import org.opensearch.sql.calcite.CalcitePlanContext;
13+
import org.opensearch.sql.common.antlr.SyntaxCheckException;
14+
15+
/** Calcite SQL query parser that produces {@link SqlNode} as the native parse result. */
16+
@RequiredArgsConstructor
17+
public class CalciteSqlQueryParser implements UnifiedQueryParser<SqlNode> {
18+
19+
/** Calcite plan context providing parser configuration (e.g., case sensitivity, conformance). */
20+
private final CalcitePlanContext planContext;
21+
22+
@Override
23+
public SqlNode parse(String query) {
24+
try {
25+
SqlParser parser = SqlParser.create(query, planContext.config.getParserConfig());
26+
return parser.parseQuery();
27+
} catch (SqlParseException e) {
28+
throw new SyntaxCheckException("Failed to parse SQL query: " + e.getMessage());
29+
}
30+
}
31+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.api.parser;
7+
8+
import lombok.RequiredArgsConstructor;
9+
import org.antlr.v4.runtime.tree.ParseTree;
10+
import org.opensearch.sql.ast.statement.Query;
11+
import org.opensearch.sql.ast.statement.Statement;
12+
import org.opensearch.sql.ast.tree.UnresolvedPlan;
13+
import org.opensearch.sql.common.setting.Settings;
14+
import org.opensearch.sql.ppl.antlr.PPLSyntaxParser;
15+
import org.opensearch.sql.ppl.parser.AstBuilder;
16+
import org.opensearch.sql.ppl.parser.AstStatementBuilder;
17+
18+
/** PPL query parser that produces {@link UnresolvedPlan} as the native parse result. */
19+
@RequiredArgsConstructor
20+
public class PPLQueryParser implements UnifiedQueryParser<UnresolvedPlan> {
21+
22+
/** Settings containing execution limits and feature flags used by AST builders. */
23+
private final Settings settings;
24+
25+
/** Reusable ANTLR-based PPL syntax parser. Stateless and thread-safe. */
26+
private final PPLSyntaxParser syntaxParser = new PPLSyntaxParser();
27+
28+
@Override
29+
public UnresolvedPlan parse(String query) {
30+
ParseTree cst = syntaxParser.parse(query);
31+
AstStatementBuilder astStmtBuilder =
32+
new AstStatementBuilder(
33+
new AstBuilder(query, settings),
34+
AstStatementBuilder.StatementBuilderContext.builder().build());
35+
Statement statement = cst.accept(astStmtBuilder);
36+
37+
if (statement instanceof Query) {
38+
return ((Query) statement).getPlan();
39+
}
40+
throw new UnsupportedOperationException(
41+
"Only query statements are supported but got " + statement.getClass().getSimpleName());
42+
}
43+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.api.parser;
7+
8+
/**
9+
* Language-neutral query parser interface. Returns the native parse result for the language (e.g.,
10+
* {@code UnresolvedPlan} for PPL, {@code SqlNode} for Calcite SQL).
11+
*
12+
* @param <R> the native parse result type for this language
13+
*/
14+
public interface UnifiedQueryParser<R> {
15+
16+
/**
17+
* Parses the query and returns the native parse result.
18+
*
19+
* @param query the raw query string
20+
* @return the native parse result
21+
*/
22+
R parse(String query);
23+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.api.parser;
7+
8+
import static org.junit.Assert.assertNotNull;
9+
import static org.junit.Assert.assertThrows;
10+
11+
import org.apache.calcite.sql.SqlNode;
12+
import org.junit.Test;
13+
import org.opensearch.sql.api.UnifiedQueryTestBase;
14+
import org.opensearch.sql.ast.tree.UnresolvedPlan;
15+
import org.opensearch.sql.common.antlr.SyntaxCheckException;
16+
import org.opensearch.sql.executor.QueryType;
17+
18+
public class UnifiedQueryParserTest extends UnifiedQueryTestBase {
19+
20+
@Test
21+
public void testPplParseReturnsUnresolvedPlan() {
22+
UnresolvedPlan ast = (UnresolvedPlan) context.getParser().parse("source = catalog.employees");
23+
assertNotNull(ast);
24+
}
25+
26+
@Test
27+
public void testPplParseSyntaxErrorThrows() {
28+
assertThrows(SyntaxCheckException.class, () -> context.getParser().parse("not a valid query"));
29+
}
30+
31+
@Test
32+
public void testSqlParseReturnsSqlNode() throws Exception {
33+
try (var sqlContext = buildContext(QueryType.SQL)) {
34+
SqlNode node = (SqlNode) sqlContext.getParser().parse("SELECT * FROM my_index");
35+
assertNotNull(node);
36+
}
37+
}
38+
39+
@Test
40+
public void testSqlParseSyntaxErrorThrows() throws Exception {
41+
try (var sqlContext = buildContext(QueryType.SQL)) {
42+
assertThrows(
43+
SyntaxCheckException.class, () -> sqlContext.getParser().parse("NOT VALID SQL !!!"));
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)