Skip to content

Commit fb3d37f

Browse files
Support fetch_size API for PPL (#5109) (#5120)
* Support fetch_size API for PPL # Conflicts: # integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteExplainIT.java * update * remove upper limit * Support fetch_size as a URL parameter * add Test coverage * update docs * formatting --------- (cherry picked from commit 18b1610) Signed-off-by: Kai Huang <ahkcs@amazon.com> Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent bcec083 commit fb3d37f

18 files changed

Lines changed: 628 additions & 9 deletions

File tree

docs/user/interfaces/endpoint.rst

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,13 @@ Explain::
208208
}
209209
}
210210

211-
Cursor
212-
======
211+
Cursor (SQL)
212+
============
213213

214214
Description
215215
-----------
216216

217-
To get paginated response for a query, user needs to provide `fetch_size` parameter as part of normal query. The value of `fetch_size` should be greater than `0`. In absence of `fetch_size` or a value of `0`, it will fallback to non-paginated response. This feature is only available over `jdbc` format for now.
217+
To get paginated response for a SQL query, user needs to provide `fetch_size` parameter as part of normal query. The value of `fetch_size` should be greater than `0`. In absence of `fetch_size` or a value of `0`, it will fallback to non-paginated response. This feature is only available over `jdbc` format for now.
218218

219219
Example
220220
-------
@@ -266,3 +266,74 @@ Result set::
266266
"size": 5,
267267
"status": 200
268268
}
269+
270+
Fetch Size (PPL) [Experimental]
271+
================================
272+
273+
Description
274+
-----------
275+
276+
The ``fetch_size`` parameter limits the number of rows returned in a PPL query response. The value of ``fetch_size`` should be greater than ``0``. In absence of ``fetch_size`` or a value of ``0``, the result size is governed by the ``plugins.query.size_limit`` cluster setting.
277+
278+
``fetch_size`` can be specified either as a URL parameter or in the JSON request body. If both are provided, the JSON body value takes precedence.
279+
280+
If ``fetch_size`` is larger than ``plugins.query.size_limit``, the result is capped at ``plugins.query.size_limit``. The effective number of rows returned is always ``min(fetch_size, plugins.query.size_limit)``.
281+
282+
Note
283+
----
284+
285+
Unlike SQL's ``fetch_size`` which enables cursor-based pagination, PPL's ``fetch_size`` does not return a cursor and does not support fetching additional pages. The response is always complete and final.
286+
287+
+--------------------+-------------------------------------+------------------------------------+
288+
| Aspect | SQL ``fetch_size`` | PPL ``fetch_size`` |
289+
+====================+=====================================+====================================+
290+
| Purpose | Cursor-based pagination | Response size limiting |
291+
+--------------------+-------------------------------------+------------------------------------+
292+
| Returns cursor? | Yes | No |
293+
+--------------------+-------------------------------------+------------------------------------+
294+
| Can fetch more? | Yes (with cursor) | No (single response) |
295+
+--------------------+-------------------------------------+------------------------------------+
296+
297+
Example 1: JSON body
298+
-------
299+
300+
PPL query::
301+
302+
>> curl -H 'Content-Type: application/json' -X POST localhost:9200/_plugins/_ppl -d '{
303+
"fetch_size" : 5,
304+
"query" : "source = accounts | fields firstname, lastname | where age > 20"
305+
}'
306+
307+
Example 2: URL parameter
308+
-------
309+
310+
PPL query::
311+
312+
>> curl -H 'Content-Type: application/json' -X POST localhost:9200/_plugins/_ppl?fetch_size=5 -d '{
313+
"query" : "source = accounts | fields firstname, lastname | where age > 20"
314+
}'
315+
316+
Result set::
317+
318+
{
319+
"schema": [
320+
{
321+
"name": "firstname",
322+
"type": "text"
323+
},
324+
{
325+
"name": "lastname",
326+
"type": "text"
327+
}
328+
],
329+
"total": 5,
330+
"datarows": [
331+
["Cherry", "Carey"],
332+
["Lindsey", "Hawkins"],
333+
["Sargent", "Powers"],
334+
["Campos", "Olsen"],
335+
["Savannah", "Kirby"]
336+
],
337+
"size": 5,
338+
"status": 200
339+
}

docs/user/ppl/limitations/limitations.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ For the following functionalities, the query will be forwarded to the V2 query e
6262
* ML
6363
* Kmeans
6464
* `show datasources` and command
65-
* Commands with `fetch_size` parameter
65+
* SQL queries with `fetch_size` parameter (cursor-based pagination). Note: PPL's `fetch_size` (response size limiting, no cursor) is supported in Calcite Engine.
6666

6767

6868
## Malformed Field Names in Object Fields

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteExplainIT.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package org.opensearch.sql.calcite.remote;
77

8+
import static org.opensearch.sql.legacy.TestUtils.getResponseBody;
89
import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_ACCOUNT;
910
import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_ALIAS;
1011
import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_BANK;
@@ -25,8 +26,12 @@
2526

2627
import java.io.IOException;
2728
import java.util.Locale;
29+
import org.junit.Assert;
2830
import org.junit.Ignore;
2931
import org.junit.Test;
32+
import org.opensearch.client.Request;
33+
import org.opensearch.client.RequestOptions;
34+
import org.opensearch.client.Response;
3035
import org.opensearch.sql.ast.statement.ExplainMode;
3136
import org.opensearch.sql.common.setting.Settings;
3237
import org.opensearch.sql.common.setting.Settings.Key;
@@ -2497,4 +2502,68 @@ public void testExplainMvCombine() throws IOException {
24972502
String expected = loadExpectedPlan("explain_mvcombine.yaml");
24982503
assertYamlEqualsIgnoreId(expected, actual);
24992504
}
2505+
2506+
// ==================== fetch_size explain tests ====================
2507+
2508+
@Test
2509+
public void testExplainFetchSizePushDown() throws IOException {
2510+
// fetch_size=5 injects Head(5, 0) on top of the plan
2511+
// Logical plan: LogicalSort(fetch=[5]) wraps the Project
2512+
String expected = loadExpectedPlan("explain_fetch_size_push.yaml");
2513+
assertYamlEqualsIgnoreId(
2514+
expected,
2515+
explainQueryWithFetchSizeYaml(
2516+
String.format("source=%s | fields age", TEST_INDEX_ACCOUNT), 5));
2517+
}
2518+
2519+
@Test
2520+
public void testExplainFetchSizeWithSmallerHead() throws IOException {
2521+
// fetch_size=10 with user's | head 3
2522+
// Two LogicalSort nodes: inner fetch=[3] from user head, outer fetch=[10] from fetch_size
2523+
// Effective limit = min(3, 10) = 3
2524+
String expected = loadExpectedPlan("explain_fetch_size_with_head_push.yaml");
2525+
assertYamlEqualsIgnoreId(
2526+
expected,
2527+
explainQueryWithFetchSizeYaml(
2528+
String.format("source=%s | head 3 | fields age", TEST_INDEX_ACCOUNT), 10));
2529+
}
2530+
2531+
@Test
2532+
public void testExplainFetchSizeSmallerThanHead() throws IOException {
2533+
// fetch_size=5 with user's | head 100
2534+
// Two LogicalSort nodes: inner fetch=[100] from user head, outer fetch=[5] from fetch_size
2535+
// Effective limit = min(100, 5) = 5
2536+
String expected = loadExpectedPlan("explain_fetch_size_smaller_than_head_push.yaml");
2537+
assertYamlEqualsIgnoreId(
2538+
expected,
2539+
explainQueryWithFetchSizeYaml(
2540+
String.format("source=%s | head 100 | fields age", TEST_INDEX_ACCOUNT), 5));
2541+
}
2542+
2543+
/**
2544+
* Send an explain request with fetch_size in the JSON body and return YAML output.
2545+
*
2546+
* @param query the PPL query string
2547+
* @param fetchSize the fetch_size parameter value
2548+
* @return the explain output as YAML string
2549+
*/
2550+
private String explainQueryWithFetchSizeYaml(String query, int fetchSize) throws IOException {
2551+
Request request =
2552+
new Request(
2553+
"POST",
2554+
String.format(
2555+
"/_plugins/_ppl/_explain?format=%s&mode=%s", Format.YAML, ExplainMode.STANDARD));
2556+
String jsonBody =
2557+
String.format(
2558+
Locale.ROOT, "{\n \"query\": \"%s\",\n \"fetch_size\": %d\n}", query, fetchSize);
2559+
request.setJsonEntity(jsonBody);
2560+
2561+
RequestOptions.Builder restOptionsBuilder = RequestOptions.DEFAULT.toBuilder();
2562+
restOptionsBuilder.addHeader("Content-Type", "application/json");
2563+
request.setOptions(restOptionsBuilder);
2564+
2565+
Response response = client().performRequest(request);
2566+
Assert.assertEquals(200, response.getStatusLine().getStatusCode());
2567+
return getResponseBody(response, true);
2568+
}
25002569
}

0 commit comments

Comments
 (0)