Skip to content

Commit 43c0a08

Browse files
committed
feat(sql): add profile support to analytics path
Add a `profile` boolean field to SQLQueryRequest and thread it through the SQL REST handler to the analytics router. Mirrors PPL's existing profile implementation: parsed from the JSON body's `profile` key and gated by the same `isProfileSupported` rules (only honored for non- explain JDBC requests on the analytics engine path). Replaces the hardcoded `false` in SQLPlugin#createSqlAnalyticsRouter so analytics-engine queries now honor the request's profile flag. The V2 SQL engine path is unchanged; profile is silently ignored when the request does not route to the analytics engine. Includes: - New 6-arg SQLQueryRequest constructor (5-arg delegates with false) - isProfileSupported helper in RestSqlAction (mirrors PPL) - YAML REST integration tests with analytics-backed index setup - New rest-api-spec/api/sql.json and sql.explain.json - Documentation in docs/user/interfaces/endpoint.rst Refs: opensearch-project#5317 Signed-off-by: Chen Dai <daichen@amazon.com>
1 parent 0d80347 commit 43c0a08

8 files changed

Lines changed: 289 additions & 3 deletions

File tree

docs/user/interfaces/endpoint.rst

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,46 @@ Result set::
266266
"status": 200
267267
}
268268

269+
Profile (Experimental)
270+
======================
271+
272+
Description
273+
-----------
274+
275+
Profiling captures per-stage timings (in milliseconds) for SQL query
276+
execution. To enable profiling, set ``"profile": true`` in the request
277+
body alongside ``"query"``.
278+
279+
.. note::
280+
The ``profile`` parameter only takes effect when the query runs on
281+
the Analytics Engine. In all other cases the flag is silently ignored.
282+
283+
Profile output is returned only for regular query execution (not
284+
``_explain``) and only with the default ``format=jdbc``.
285+
286+
Example
287+
-------
288+
289+
Request::
290+
291+
POST /_plugins/_sql
292+
{
293+
"query": "SELECT customer_id, SUM(amount) FROM orders GROUP BY customer_id",
294+
"profile": true
295+
}
296+
297+
Response::
298+
299+
{
300+
"schema": [...],
301+
"datarows": [...],
302+
"profile": {
303+
"summary": { "total_time_ms": 42 },
304+
"phases": { "analyze": 3, "optimize": 5, "execute": 30, "format": 4 },
305+
"plan": { ... }
306+
}
307+
}
308+
269309
Fetch Size (PPL) [Experimental]
270310
================================
271311

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"sql.explain": {
3+
"documentation": {
4+
"url": "https://github.com/opensearch-project/sql/blob/main/docs/user/index.rst",
5+
"description": "OpenSearch SQL Reference Manual"
6+
},
7+
"stability" : "stable",
8+
"url": {
9+
"paths": [
10+
{
11+
"path" : "/_plugins/_sql/_explain",
12+
"methods" : ["POST"]
13+
}
14+
]
15+
},
16+
"params": {},
17+
"body": {
18+
"description": "SQL Explain Query",
19+
"required":true
20+
}
21+
}
22+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"sql": {
3+
"documentation": {
4+
"url": "https://github.com/opensearch-project/sql/blob/main/docs/user/index.rst",
5+
"description": "OpenSearch SQL Reference Manual"
6+
},
7+
"stability" : "stable",
8+
"url": {
9+
"paths": [
10+
{
11+
"path" : "/_plugins/_sql",
12+
"methods" : ["POST"]
13+
}
14+
]
15+
},
16+
"params": {
17+
"format":{
18+
"type":"string",
19+
"description":"response format: jdbc, csv, raw"
20+
}
21+
},
22+
"body": {
23+
"description": "SQL Query",
24+
"required":true
25+
}
26+
}
27+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
setup:
2+
- do:
3+
query.settings:
4+
body:
5+
transient:
6+
plugins.calcite.enabled : true
7+
- do:
8+
indices.create:
9+
index: sql_profile
10+
body:
11+
settings:
12+
number_of_shards: 1
13+
number_of_replicas: 0
14+
index.pluggable.dataformat.enabled: true
15+
index.pluggable.dataformat: composite
16+
mappings:
17+
properties:
18+
message:
19+
type: keyword
20+
- do:
21+
bulk:
22+
refresh: true
23+
body:
24+
- '{"index": {"_index": "sql_profile", "_id": 1}}'
25+
- '{"message": "hello"}'
26+
- '{"index": {"_index": "sql_profile", "_id": 2}}'
27+
- '{"message": "world"}'
28+
29+
---
30+
teardown:
31+
- do:
32+
indices.delete:
33+
index: sql_profile
34+
ignore_unavailable: true
35+
- do:
36+
query.settings:
37+
body:
38+
transient:
39+
plugins.calcite.enabled : false
40+
41+
---
42+
"Profile metrics returned for sql query":
43+
- skip:
44+
features:
45+
- headers
46+
- allowed_warnings
47+
- do:
48+
headers:
49+
Content-Type: 'application/json'
50+
sql:
51+
body:
52+
query: 'SELECT message FROM sql_profile'
53+
profile: true
54+
- gt: {profile.summary.total_time_ms: 0.0}
55+
- gt: {profile.phases.analyze.time_ms: 0.0}
56+
- gt: {profile.phases.optimize.time_ms: 0.0}
57+
- gt: {profile.phases.execute.time_ms: 0.0}
58+
- gt: {profile.phases.format.time_ms: 0.0}
59+
- gt: {profile.plan.time_ms: 0.0}
60+
- match: {profile.plan.rows: 2}
61+
62+
---
63+
"Profile ignored for explain api":
64+
- skip:
65+
features:
66+
- headers
67+
- allowed_warnings
68+
- do:
69+
headers:
70+
Content-Type: 'application/json'
71+
sql.explain:
72+
body:
73+
query: 'SELECT message FROM sql_profile'
74+
profile: true
75+
- match: {profile: null}
76+
77+
---
78+
"Profile ignored for csv format":
79+
- skip:
80+
features:
81+
- headers
82+
- allowed_warnings
83+
- do:
84+
headers:
85+
Content-Type: 'application/json'
86+
sql:
87+
body:
88+
query: 'SELECT message FROM sql_profile'
89+
profile: true
90+
format: 'csv'
91+
- match: {profile: null}
92+
93+
---
94+
"Profile absent when not requested":
95+
- skip:
96+
features:
97+
- headers
98+
- allowed_warnings
99+
- do:
100+
headers:
101+
Content-Type: 'application/json'
102+
sql:
103+
body:
104+
query: 'SELECT message FROM sql_profile'
105+
- match: {profile: null}
106+
107+
---
108+
"Profile ignored for explain query":
109+
- skip:
110+
features:
111+
- headers
112+
- allowed_warnings
113+
- do:
114+
headers:
115+
Content-Type: 'application/json'
116+
sql:
117+
body:
118+
query: 'EXPLAIN SELECT message FROM sql_profile'
119+
profile: true
120+
- match: {profile: null}

legacy/src/main/java/org/opensearch/sql/legacy/plugin/RestSqlAction.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,21 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli
145145

146146
Format format = SqlRequestParam.getFormat(request.params());
147147

148+
// Compute profile flag mirroring PPL's gating logic
149+
boolean profileRequested =
150+
sqlRequest.getJsonContent() != null
151+
&& sqlRequest.getJsonContent().optBoolean("profile", false);
152+
boolean enableProfile =
153+
profileRequested && isProfileSupported(request.path(), format, sqlRequest.getSql());
154+
148155
SQLQueryRequest newSqlRequest =
149156
new SQLQueryRequest(
150157
sqlRequest.getJsonContent(),
151158
sqlRequest.getSql(),
152159
request.path(),
153160
request.params(),
154-
sqlRequest.cursor());
161+
sqlRequest.cursor(),
162+
enableProfile);
155163

156164
// Route to analytics engine for non-Lucene (e.g., Parquet-backed) indices.
157165
// The router returns true and sends the response directly if it handled the request.
@@ -366,4 +374,18 @@ private static ColumnTypeProvider performAnalysis(String sql) {
366374
return new ColumnTypeProvider();
367375
}
368376
}
377+
378+
private static final String DEFAULT_RESPONSE_FORMAT = "jdbc";
379+
380+
private static boolean isProfileSupported(String path, Format format, String query) {
381+
boolean explainPath = isExplainRequest(path);
382+
boolean explainQuery = query != null && query.trim().toLowerCase().startsWith("explain");
383+
boolean isJdbcFormat =
384+
format != null && DEFAULT_RESPONSE_FORMAT.equalsIgnoreCase(format.getFormatName());
385+
return !explainPath && !explainQuery && isJdbcFormat;
386+
}
387+
388+
private static boolean isExplainRequest(String path) {
389+
return path != null && path.endsWith("/_explain");
390+
}
369391
}

plugin/src/main/java/org/opensearch/sql/plugin/SQLPlugin.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ public void onFailure(Exception e) {
271271
unifiedQueryHandler.execute(
272272
sqlRequest.getQuery(),
273273
QueryType.SQL,
274-
false,
274+
sqlRequest.profile(),
275275
new ActionListener<>() {
276276
@Override
277277
public void onResponse(TransportPPLQueryResponse response) {

sql/src/main/java/org/opensearch/sql/sql/domain/SQLQueryRequest.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import lombok.EqualsAndHashCode;
1616
import lombok.Getter;
1717
import lombok.RequiredArgsConstructor;
18+
import lombok.Setter;
1819
import lombok.ToString;
1920
import lombok.experimental.Accessors;
2021
import org.json.JSONObject;
@@ -27,7 +28,7 @@
2728
public class SQLQueryRequest {
2829
private static final String QUERY_FIELD_CURSOR = "cursor";
2930
private static final Set<String> SUPPORTED_FIELDS =
30-
Set.of("query", "fetch_size", "parameters", QUERY_FIELD_CURSOR);
31+
Set.of("query", "fetch_size", "parameters", QUERY_FIELD_CURSOR, "profile");
3132
private static final String QUERY_PARAMS_FORMAT = "format";
3233
private static final String QUERY_PARAMS_SANITIZE = "sanitize";
3334
private static final String QUERY_PARAMS_PRETTY = "pretty";
@@ -55,6 +56,11 @@ public class SQLQueryRequest {
5556
@Accessors(fluent = true)
5657
private boolean pretty = false;
5758

59+
@Setter
60+
@Getter
61+
@Accessors(fluent = true)
62+
private boolean profile = false;
63+
5864
private String cursor;
5965

6066
/** Constructor of SQLQueryRequest that passes request params. */
@@ -64,6 +70,17 @@ public SQLQueryRequest(
6470
String path,
6571
Map<String, String> params,
6672
String cursor) {
73+
this(jsonContent, query, path, params, cursor, false);
74+
}
75+
76+
/** Constructor of SQLQueryRequest that passes request params and profile flag. */
77+
public SQLQueryRequest(
78+
JSONObject jsonContent,
79+
String query,
80+
String path,
81+
Map<String, String> params,
82+
String cursor,
83+
boolean profile) {
6784
this.jsonContent = jsonContent;
6885
this.query = query;
6986
this.path = path;
@@ -72,6 +89,7 @@ public SQLQueryRequest(
7289
this.sanitize = shouldSanitize(params);
7390
this.pretty = shouldPretty(params);
7491
this.cursor = cursor;
92+
this.profile = profile;
7593
}
7694

7795
/**

sql/src/test/java/org/opensearch/sql/sql/domain/SQLQueryRequestTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,43 @@ public void should_support_raw_format() {
284284
assertTrue(csvRequest.isSupported());
285285
}
286286

287+
@Test
288+
public void should_support_query_with_profile_field() {
289+
SQLQueryRequest request =
290+
SQLQueryRequestBuilder.request("SELECT 1")
291+
.jsonContent("{\"query\": \"SELECT 1\", \"profile\": true}")
292+
.build();
293+
assertTrue(request.isSupported());
294+
}
295+
296+
@Test
297+
public void should_have_profile_false_by_default() {
298+
SQLQueryRequest request = SQLQueryRequestBuilder.request("SELECT 1").build();
299+
assertFalse(request.profile());
300+
}
301+
302+
@Test
303+
public void should_set_profile_via_constructor() {
304+
SQLQueryRequest request =
305+
new SQLQueryRequest(
306+
new JSONObject("{\"query\": \"SELECT 1\", \"profile\": true}"),
307+
"SELECT 1",
308+
"_plugins/_sql",
309+
Map.of(),
310+
null,
311+
true);
312+
assertTrue(request.profile());
313+
}
314+
315+
@Test
316+
public void should_support_query_and_profile_fields_only() {
317+
SQLQueryRequest request =
318+
SQLQueryRequestBuilder.request("SELECT 1")
319+
.jsonContent("{\"query\": \"SELECT 1\", \"profile\": true}")
320+
.build();
321+
assertTrue(request.isSupported());
322+
}
323+
287324
/** SQL query request build helper to improve test data setup readability. */
288325
private static class SQLQueryRequestBuilder {
289326
private String jsonContent;

0 commit comments

Comments
 (0)