Skip to content

Commit 9826cb0

Browse files
committed
Merge branch 'main' into feat/report-builder
2 parents 6f8d67a + 8a7524c commit 9826cb0

27 files changed

Lines changed: 4325 additions & 2494 deletions

api/README.md

Lines changed: 69 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.
@@ -179,6 +194,59 @@ try (UnifiedQueryContext context = UnifiedQueryContext.builder()
179194
}
180195
```
181196

197+
## Profiling
198+
199+
The unified query API supports the same [profiling capability](../docs/user/ppl/interfaces/endpoint.md#profile-experimental) as the PPL REST endpoint. When enabled, each unified query component automatically collects per-phase timing metrics. For code outside unified query components (e.g., `PreparedStatement.executeQuery()` or response formatting), `context.measure()` records custom phases into the same profile.
200+
201+
```java
202+
try (UnifiedQueryContext context = UnifiedQueryContext.builder()
203+
.language(QueryType.PPL)
204+
.catalog("catalog", schema)
205+
.defaultNamespace("catalog")
206+
.profiling(true)
207+
.build()) {
208+
209+
// Auto-profiled: ANALYZE
210+
RelNode plan = new UnifiedQueryPlanner(context).plan(query);
211+
212+
// Auto-profiled: OPTIMIZE
213+
PreparedStatement stmt = new UnifiedQueryCompiler(context).compile(plan);
214+
215+
// User-profiled via measure()
216+
ResultSet rs = context.measure(MetricName.EXECUTE, stmt::executeQuery);
217+
String json = context.measure(MetricName.FORMAT, () -> formatter.format(result));
218+
219+
// Retrieve profile snapshot
220+
QueryProfile profile = context.getProfile();
221+
}
222+
```
223+
224+
The returned `QueryProfile` follows the same JSON structure as the REST API:
225+
226+
```json
227+
{
228+
"summary": {
229+
"total_time_ms": 33.34
230+
},
231+
"phases": {
232+
"analyze": { "time_ms": 8.68 },
233+
"optimize": { "time_ms": 18.2 },
234+
"execute": { "time_ms": 4.87 },
235+
"format": { "time_ms": 0.05 }
236+
},
237+
"plan": {
238+
"node": "EnumerableCalc",
239+
"time_ms": 4.82,
240+
"rows": 2,
241+
"children": [
242+
{ "node": "CalciteEnumerableIndexScan", "time_ms": 4.12, "rows": 2 }
243+
]
244+
}
245+
}
246+
```
247+
248+
When profiling is disabled (the default), all components execute with zero overhead.
249+
182250
## Development & Testing
183251

184252
A set of unit tests is provided to validate planner behavior.

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

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
import java.util.List;
1414
import java.util.Map;
1515
import java.util.Objects;
16-
import lombok.Value;
16+
import java.util.Optional;
17+
import java.util.concurrent.Callable;
18+
import lombok.AllArgsConstructor;
19+
import lombok.Getter;
1720
import org.apache.calcite.avatica.util.Casing;
1821
import org.apache.calcite.jdbc.CalciteSchema;
1922
import org.apache.calcite.plan.RelTraitDef;
@@ -24,24 +27,64 @@
2427
import org.apache.calcite.tools.FrameworkConfig;
2528
import org.apache.calcite.tools.Frameworks;
2629
import org.apache.calcite.tools.Programs;
30+
import org.opensearch.sql.api.parser.CalciteSqlQueryParser;
31+
import org.opensearch.sql.api.parser.PPLQueryParser;
32+
import org.opensearch.sql.api.parser.UnifiedQueryParser;
2733
import org.opensearch.sql.calcite.CalcitePlanContext;
2834
import org.opensearch.sql.calcite.SysLimit;
2935
import org.opensearch.sql.common.setting.Settings;
3036
import org.opensearch.sql.executor.QueryType;
37+
import org.opensearch.sql.monitor.profile.MetricName;
38+
import org.opensearch.sql.monitor.profile.ProfileMetric;
39+
import org.opensearch.sql.monitor.profile.QueryProfile;
40+
import org.opensearch.sql.monitor.profile.QueryProfiling;
3141

3242
/**
3343
* A reusable abstraction shared across unified query components (planner, compiler, etc.). This
3444
* centralizes configuration for catalog schemas, query type, execution limits, and other settings,
3545
* enabling consistent behavior across all unified query operations.
3646
*/
37-
@Value
47+
@AllArgsConstructor
48+
@Getter
3849
public class UnifiedQueryContext implements AutoCloseable {
3950

4051
/** CalcitePlanContext containing Calcite framework configuration and query type. */
41-
CalcitePlanContext planContext;
52+
private final CalcitePlanContext planContext;
4253

4354
/** Settings containing execution limits and feature flags used by parsers and planners. */
44-
Settings settings;
55+
private final Settings settings;
56+
57+
/** Query parser created eagerly from this context's configuration. */
58+
private final UnifiedQueryParser<?> parser;
59+
60+
/**
61+
* Returns the profiling result. Call after query execution to retrieve collected metrics. Returns
62+
* empty if profiling was not enabled.
63+
*/
64+
public Optional<QueryProfile> getProfile() {
65+
return Optional.ofNullable(QueryProfiling.current().finish());
66+
}
67+
68+
/**
69+
* Measures the execution time of the given action and records it as a profiling metric. When
70+
* profiling is disabled, the action executes with no overhead. Use this for phases outside
71+
* unified query components (e.g., execution, formatting).
72+
*
73+
* @param <T> the return type of the action
74+
* @param metricName the metric to record
75+
* @param action the action to measure
76+
* @return the result of the action
77+
* @throws Exception if the action throws
78+
*/
79+
public <T> T measure(MetricName metricName, Callable<T> action) throws Exception {
80+
ProfileMetric metric = QueryProfiling.current().getOrCreateMetric(metricName);
81+
long start = System.nanoTime();
82+
try {
83+
return action.call();
84+
} finally {
85+
metric.set(System.nanoTime() - start);
86+
}
87+
}
4588

4689
/**
4790
* Closes the underlying resource managed by this context.
@@ -50,6 +93,7 @@ public class UnifiedQueryContext implements AutoCloseable {
5093
*/
5194
@Override
5295
public void close() throws Exception {
96+
QueryProfiling.clear();
5397
if (planContext != null && planContext.connection != null) {
5498
planContext.connection.close();
5599
}
@@ -66,6 +110,7 @@ public static class Builder {
66110
private final Map<String, Schema> catalogs = new HashMap<>();
67111
private String defaultNamespace;
68112
private boolean cacheMetadata = false;
113+
private boolean profiling = false;
69114

70115
/**
71116
* Setting values with defaults from SysLimit.DEFAULT. Only includes planning-required settings
@@ -125,6 +170,18 @@ public Builder cacheMetadata(boolean cache) {
125170
return this;
126171
}
127172

173+
/**
174+
* Enables or disables query profiling. When enabled, profiling metrics are collected during
175+
* query planning and execution, retrievable via {@link UnifiedQueryContext#getProfile()}.
176+
*
177+
* @param enabled whether to enable profiling
178+
* @return this builder instance
179+
*/
180+
public Builder profiling(boolean enabled) {
181+
this.profiling = enabled;
182+
return this;
183+
}
184+
128185
/**
129186
* Sets a specific setting value by name.
130187
*
@@ -152,7 +209,15 @@ public UnifiedQueryContext build() {
152209
CalcitePlanContext planContext =
153210
CalcitePlanContext.create(
154211
buildFrameworkConfig(), SysLimit.fromSettings(settings), queryType);
155-
return new UnifiedQueryContext(planContext, settings);
212+
QueryProfiling.activate(profiling);
213+
return new UnifiedQueryContext(planContext, settings, createParser(planContext, settings));
214+
}
215+
216+
private UnifiedQueryParser<?> createParser(CalcitePlanContext planContext, Settings settings) {
217+
return switch (queryType) {
218+
case PPL -> new PPLQueryParser(settings);
219+
case SQL -> new CalciteSqlQueryParser(planContext);
220+
};
156221
}
157222

158223
private Settings buildSettings() {

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

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55

66
package org.opensearch.sql.api;
77

8+
import static org.opensearch.sql.monitor.profile.MetricName.ANALYZE;
9+
810
import lombok.RequiredArgsConstructor;
9-
import org.antlr.v4.runtime.tree.ParseTree;
1011
import org.apache.calcite.rel.RelCollation;
1112
import org.apache.calcite.rel.RelCollations;
1213
import org.apache.calcite.rel.RelNode;
@@ -16,15 +17,11 @@
1617
import org.apache.calcite.sql.SqlNode;
1718
import org.apache.calcite.tools.Frameworks;
1819
import org.apache.calcite.tools.Planner;
19-
import org.opensearch.sql.ast.statement.Query;
20-
import org.opensearch.sql.ast.statement.Statement;
20+
import org.opensearch.sql.api.parser.UnifiedQueryParser;
2121
import org.opensearch.sql.ast.tree.UnresolvedPlan;
2222
import org.opensearch.sql.calcite.CalciteRelNodeVisitor;
2323
import org.opensearch.sql.common.antlr.SyntaxCheckException;
2424
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;
2825

2926
/**
3027
* {@code UnifiedQueryPlanner} provides a high-level API for parsing and analyzing queries using the
@@ -36,12 +33,16 @@ public class UnifiedQueryPlanner {
3633
/** Planning strategy selected at construction time based on query type. */
3734
private final PlanningStrategy strategy;
3835

36+
/** Unified query context for profiling support. */
37+
private final UnifiedQueryContext context;
38+
3939
/**
4040
* Constructs a UnifiedQueryPlanner with a unified query context.
4141
*
4242
* @param context the unified query context containing CalcitePlanContext
4343
*/
4444
public UnifiedQueryPlanner(UnifiedQueryContext context) {
45+
this.context = context;
4546
this.strategy =
4647
context.getPlanContext().queryType == QueryType.SQL
4748
? new CalciteNativeStrategy(context)
@@ -57,7 +58,7 @@ public UnifiedQueryPlanner(UnifiedQueryContext context) {
5758
*/
5859
public RelNode plan(String query) {
5960
try {
60-
return strategy.plan(query);
61+
return context.measure(ANALYZE, () -> strategy.plan(query));
6162
} catch (SyntaxCheckException e) {
6263
// Re-throw syntax error without wrapping
6364
throw e;
@@ -87,36 +88,26 @@ public RelNode plan(String query) throws Exception {
8788
}
8889
}
8990

90-
/** AST-based planning via ANTLR parser → UnresolvedPlan → CalciteRelNodeVisitor. */
91-
@RequiredArgsConstructor
91+
/** AST-based planning via context-owned parser → UnresolvedPlan → CalciteRelNodeVisitor. */
9292
private static class CustomVisitorStrategy implements PlanningStrategy {
9393
private final UnifiedQueryContext context;
94-
private final PPLSyntaxParser parser = new PPLSyntaxParser();
94+
private final UnifiedQueryParser<UnresolvedPlan> parser;
9595
private final CalciteRelNodeVisitor relNodeVisitor =
9696
new CalciteRelNodeVisitor(new EmptyDataSourceService());
9797

98+
@SuppressWarnings("unchecked")
99+
CustomVisitorStrategy(UnifiedQueryContext context) {
100+
this.context = context;
101+
this.parser = (UnifiedQueryParser<UnresolvedPlan>) context.getParser();
102+
}
103+
98104
@Override
99105
public RelNode plan(String query) {
100-
UnresolvedPlan ast = parse(query);
106+
UnresolvedPlan ast = parser.parse(query);
101107
RelNode logical = relNodeVisitor.analyze(ast, context.getPlanContext());
102108
return preserveCollation(logical);
103109
}
104110

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-
120111
private RelNode preserveCollation(RelNode logical) {
121112
RelCollation collation = logical.getTraitSet().getCollation();
122113
if (!(logical instanceof Sort) && collation != RelCollations.EMPTY) {

api/src/main/java/org/opensearch/sql/api/compiler/UnifiedQueryCompiler.java

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
package org.opensearch.sql.api.compiler;
77

8+
import static org.opensearch.sql.monitor.profile.MetricName.OPTIMIZE;
9+
810
import java.sql.Connection;
911
import java.sql.PreparedStatement;
1012
import lombok.NonNull;
@@ -46,26 +48,29 @@ public UnifiedQueryCompiler(UnifiedQueryContext context) {
4648
*/
4749
public PreparedStatement compile(@NonNull RelNode plan) {
4850
try {
49-
// Apply shuttle to convert LogicalTableScan to BindableTableScan
50-
final RelHomogeneousShuttle shuttle =
51-
new RelHomogeneousShuttle() {
52-
@Override
53-
public RelNode visit(TableScan scan) {
54-
final RelOptTable table = scan.getTable();
55-
if (scan instanceof LogicalTableScan
56-
&& Bindables.BindableTableScan.canHandle(table)) {
57-
return Bindables.BindableTableScan.create(scan.getCluster(), table);
58-
}
59-
return super.visit(scan);
60-
}
61-
};
62-
RelNode transformedPlan = plan.accept(shuttle);
63-
64-
Connection connection = context.getPlanContext().connection;
65-
final RelRunner runner = connection.unwrap(RelRunner.class);
66-
return runner.prepareStatement(transformedPlan);
51+
return context.measure(OPTIMIZE, () -> doCompile(plan));
6752
} catch (Exception e) {
6853
throw new IllegalStateException("Failed to compile logical plan", e);
6954
}
7055
}
56+
57+
private PreparedStatement doCompile(RelNode plan) throws Exception {
58+
// Apply shuttle to convert LogicalTableScan to BindableTableScan
59+
final RelHomogeneousShuttle shuttle =
60+
new RelHomogeneousShuttle() {
61+
@Override
62+
public RelNode visit(TableScan scan) {
63+
final RelOptTable table = scan.getTable();
64+
if (scan instanceof LogicalTableScan && Bindables.BindableTableScan.canHandle(table)) {
65+
return Bindables.BindableTableScan.create(scan.getCluster(), table);
66+
}
67+
return super.visit(scan);
68+
}
69+
};
70+
RelNode transformedPlan = plan.accept(shuttle);
71+
72+
Connection connection = context.getPlanContext().connection;
73+
final RelRunner runner = connection.unwrap(RelRunner.class);
74+
return runner.prepareStatement(transformedPlan);
75+
}
7176
}

0 commit comments

Comments
 (0)