diff --git a/docs/user/interfaces/endpoint.rst b/docs/user/interfaces/endpoint.rst index 1deab285103..bd5a9637c5b 100644 --- a/docs/user/interfaces/endpoint.rst +++ b/docs/user/interfaces/endpoint.rst @@ -266,6 +266,58 @@ Result set:: "status": 200 } +Profile [Experimental] +====================== + +Description +----------- + +Profiling captures per-stage timings (in milliseconds) for SQL query +execution. To enable profiling, set ``"profile": true`` in the request +body alongside ``"query"``. + +.. note:: + The ``profile`` parameter only takes effect when the query runs on + the Analytics Engine. In all other cases the flag is silently ignored. + + Profile output is returned only for regular query execution (not + ``_explain``) and only with the default ``format=jdbc``. + +Example +------- + +Request:: + + POST /_plugins/_sql + { + "query": "SELECT customer_id, SUM(amount) FROM orders GROUP BY customer_id", + "profile": true + } + +Expected output (trimmed):: + + { + "profile": { + "summary": { + "total_time_ms": 33.34 + }, + "phases": { + "analyze": { "time_ms": 8.68 }, + "optimize": { "time_ms": 18.2 }, + "execute": { "time_ms": 4.87 }, + "format": { "time_ms": 0.05 } + }, + "plan": { + "node": "EnumerableCalc", + "time_ms": 4.82, + "rows": 2, + "children": [ + { "node": "CalciteEnumerableIndexScan", "time_ms": 4.12, "rows": 2 } + ] + } + } + } + Fetch Size (PPL) [Experimental] ================================ diff --git a/plugin/src/main/java/org/opensearch/sql/plugin/SQLPlugin.java b/plugin/src/main/java/org/opensearch/sql/plugin/SQLPlugin.java index 3692f49688d..a4968766a50 100644 --- a/plugin/src/main/java/org/opensearch/sql/plugin/SQLPlugin.java +++ b/plugin/src/main/java/org/opensearch/sql/plugin/SQLPlugin.java @@ -271,7 +271,7 @@ public void onFailure(Exception e) { unifiedQueryHandler.execute( sqlRequest.getQuery(), QueryType.SQL, - false, + sqlRequest.isProfileEnabled(), new ActionListener<>() { @Override public void onResponse(TransportPPLQueryResponse response) { diff --git a/sql/src/main/java/org/opensearch/sql/sql/domain/SQLQueryRequest.java b/sql/src/main/java/org/opensearch/sql/sql/domain/SQLQueryRequest.java index df456d4d780..456ea212717 100644 --- a/sql/src/main/java/org/opensearch/sql/sql/domain/SQLQueryRequest.java +++ b/sql/src/main/java/org/opensearch/sql/sql/domain/SQLQueryRequest.java @@ -26,8 +26,9 @@ @RequiredArgsConstructor public class SQLQueryRequest { private static final String QUERY_FIELD_CURSOR = "cursor"; + private static final String QUERY_FIELD_PROFILE = "profile"; private static final Set SUPPORTED_FIELDS = - Set.of("query", "fetch_size", "parameters", QUERY_FIELD_CURSOR); + Set.of("query", "fetch_size", "parameters", QUERY_FIELD_CURSOR, QUERY_FIELD_PROFILE); private static final String QUERY_PARAMS_FORMAT = "format"; private static final String QUERY_PARAMS_SANITIZE = "sanitize"; private static final String QUERY_PARAMS_PRETTY = "pretty"; @@ -118,6 +119,14 @@ public boolean isExplainRequest() { return path.endsWith("/_explain"); } + /** Check if profiling should run for this request. */ + public boolean isProfileEnabled() { + return jsonContent != null + && jsonContent.optBoolean(QUERY_FIELD_PROFILE, false) + && !isExplainRequest() + && Format.JDBC.getFormatName().equalsIgnoreCase(format); + } + public boolean isCursorCloseRequest() { return path.endsWith("/close"); } diff --git a/sql/src/test/java/org/opensearch/sql/sql/domain/SQLQueryRequestTest.java b/sql/src/test/java/org/opensearch/sql/sql/domain/SQLQueryRequestTest.java index a8ac6c71c4e..e5f2400e6cb 100644 --- a/sql/src/test/java/org/opensearch/sql/sql/domain/SQLQueryRequestTest.java +++ b/sql/src/test/java/org/opensearch/sql/sql/domain/SQLQueryRequestTest.java @@ -284,6 +284,70 @@ public void should_support_raw_format() { assertTrue(csvRequest.isSupported()); } + @Test + public void should_support_query_with_profile_field() { + SQLQueryRequest request = + SQLQueryRequestBuilder.request("SELECT 1") + .jsonContent("{\"query\": \"SELECT 1\", \"profile\": true}") + .build(); + assertTrue(request.isSupported()); + } + + @Test + public void should_disable_profile_when_no_json_content() { + SQLQueryRequest request = + new SQLQueryRequest(null, "SELECT 1", "_plugins/_sql", Map.of(), null); + assertFalse(request.isProfileEnabled()); + } + + @Test + public void should_disable_profile_when_profile_field_absent() { + SQLQueryRequest request = + new SQLQueryRequest( + new JSONObject("{\"query\": \"SELECT 1\"}"), + "SELECT 1", + "_plugins/_sql", + Map.of(), + null); + assertFalse(request.isProfileEnabled()); + } + + @Test + public void should_enable_profile_for_jdbc_query() { + SQLQueryRequest request = + new SQLQueryRequest( + new JSONObject("{\"query\": \"SELECT 1\", \"profile\": true}"), + "SELECT 1", + "_plugins/_sql", + Map.of(), + null); + assertTrue(request.isProfileEnabled()); + } + + @Test + public void should_disable_profile_on_explain_path() { + SQLQueryRequest request = + new SQLQueryRequest( + new JSONObject("{\"query\": \"SELECT 1\", \"profile\": true}"), + "SELECT 1", + "_plugins/_sql/_explain", + Map.of(), + null); + assertFalse(request.isProfileEnabled()); + } + + @Test + public void should_disable_profile_for_non_jdbc_format() { + SQLQueryRequest request = + new SQLQueryRequest( + new JSONObject("{\"query\": \"SELECT 1\", \"profile\": true}"), + "SELECT 1", + "_plugins/_sql", + Map.of("format", "csv"), + null); + assertFalse(request.isProfileEnabled()); + } + /** SQL query request build helper to improve test data setup readability. */ private static class SQLQueryRequestBuilder { private String jsonContent;