Skip to content

Commit aecca57

Browse files
PPL percentile function shortcut perc() and p() support (opensearch-project#4085)
* First revision with shortcuts working Signed-off-by: Aaron Alvarez <aaarone@amazon.com> * Updating documentation Signed-off-by: Aaron Alvarez <aaarone@amazon.com> * Updading documentation Signed-off-by: Aaron Alvarez <aaarone@amazon.com> * Updating the grammar files, still in progress Signed-off-by: Aaron Alvarez <aaarone@amazon.com> * Adding cross-cluster test and improving code Signed-off-by: Aaron Alvarez <aaarone@amazon.com> * Updating grammar rules and expression builder Signed-off-by: Aaron Alvarez <aaarone@amazon.com> * Restoring visit visitStatsFunctionCall Signed-off-by: Aaron Alvarez <aaarone@amazon.com> * Fixing formatting Signed-off-by: Aaron Alvarez <aaarone@amazon.com> * Fixing formatting Signed-off-by: Aaron Alvarez <aaarone@amazon.com> * Addressing comments left by Chen Signed-off-by: Aaron Alvarez <aaarone@amazon.com> * Addressing comments Signed-off-by: Aaron Alvarez <aaarone@amazon.com> --------- Signed-off-by: Aaron Alvarez <aaarone@amazon.com> Signed-off-by: Aaron Alvarez <900908alvarezaaron@gmail.com> Co-authored-by: Aaron Alvarez <aaarone@amazon.com>
1 parent e6c36ab commit aecca57

8 files changed

Lines changed: 365 additions & 32 deletions

File tree

docs/user/dql/aggregations.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,34 @@ Example::
391391
| M | 36 |
392392
+--------+-----+
393393

394+
Percentile Shortcut Functions
395+
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
396+
397+
For convenience, OpenSearch PPL provides shortcut functions for common percentiles:
398+
399+
- ``PERC<percent>(expr)`` - Equivalent to ``PERCENTILE(expr, <percent>)``
400+
- ``P<percent>(expr)`` - Equivalent to ``PERCENTILE(expr, <percent>)``
401+
402+
Both integer and decimal percentiles from 0 to 100 are supported (e.g., ``PERC95``, ``P99.5``).
403+
404+
Example::
405+
406+
ppl> source=accounts | stats perc99.5(age);
407+
fetched rows / total rows = 1/1
408+
+---------------+
409+
| perc99.5(age) |
410+
|---------------|
411+
| 36 |
412+
+---------------+
413+
414+
ppl> source=accounts | stats p50(age);
415+
fetched rows / total rows = 1/1
416+
+---------+
417+
| p50(age) |
418+
|---------|
419+
| 32 |
420+
+---------+
421+
394422
HAVING Clause
395423
=============
396424

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,4 +811,70 @@ public void testAggByByteNumberWithScript() throws IOException {
811811
TEST_INDEX_DATATYPE_NUMERIC));
812812
verifyDataRows(response, rows(1, 4));
813813
}
814+
815+
@Test
816+
public void testPercentileShortcuts() throws IOException {
817+
JSONObject actual =
818+
executeQuery(
819+
String.format("source=%s | stats perc50(balance), p95(balance)", TEST_INDEX_BANK));
820+
verifySchema(actual, schema("perc50(balance)", "bigint"), schema("p95(balance)", "bigint"));
821+
verifyDataRows(actual, rows(32838, 48086));
822+
}
823+
824+
@Test
825+
public void testPercentileShortcutsWithDecimals() throws IOException {
826+
JSONObject actual =
827+
executeQuery(String.format("source=%s | stats perc99.5(balance)", TEST_INDEX_BANK));
828+
verifySchema(actual, schema("perc99.5(balance)", "bigint"));
829+
verifyDataRows(actual, rows(48086));
830+
}
831+
832+
@Test
833+
public void testPercentileShortcutsFloatingPoint() throws IOException {
834+
JSONObject actual =
835+
executeQuery(
836+
String.format(
837+
"source=%s | stats perc25.5(balance), p75.25(balance), perc0.1(balance)",
838+
TEST_INDEX_BANK));
839+
verifySchema(
840+
actual,
841+
schema("perc25.5(balance)", "bigint"),
842+
schema("p75.25(balance)", "bigint"),
843+
schema("perc0.1(balance)", "bigint"));
844+
verifyDataRows(actual, rows(5686, 40540, 4180));
845+
}
846+
847+
@Test
848+
public void testPercentileShortcutsFloatingEquivalence() throws IOException {
849+
JSONObject shortcut =
850+
executeQuery(String.format("source=%s | stats perc25.5(balance)", TEST_INDEX_BANK));
851+
JSONObject standard =
852+
executeQuery(String.format("source=%s | stats percentile(balance, 25.5)", TEST_INDEX_BANK));
853+
854+
verifySchema(shortcut, schema("perc25.5(balance)", "bigint"));
855+
verifySchema(standard, schema("percentile(balance, 25.5)", "bigint"));
856+
857+
Object shortcutValue = shortcut.getJSONArray("datarows").getJSONArray(0).get(0);
858+
Object standardValue = standard.getJSONArray("datarows").getJSONArray(0).get(0);
859+
860+
verifyDataRows(shortcut, rows(shortcutValue));
861+
verifyDataRows(standard, rows(standardValue));
862+
}
863+
864+
@Test
865+
public void testPercentileShortcutsEquivalentToStandard() throws IOException {
866+
JSONObject shortcut =
867+
executeQuery(String.format("source=%s | stats perc50(balance)", TEST_INDEX_BANK));
868+
JSONObject standard =
869+
executeQuery(String.format("source=%s | stats percentile(balance, 50)", TEST_INDEX_BANK));
870+
871+
verifySchema(shortcut, schema("perc50(balance)", "bigint"));
872+
verifySchema(standard, schema("percentile(balance, 50)", "bigint"));
873+
874+
Object shortcutValue = shortcut.getJSONArray("datarows").getJSONArray(0).get(0);
875+
Object standardValue = standard.getJSONArray("datarows").getJSONArray(0).get(0);
876+
877+
verifyDataRows(shortcut, rows(shortcutValue));
878+
verifyDataRows(standard, rows(standardValue));
879+
}
814880
}

integ-test/src/test/java/org/opensearch/sql/security/CrossClusterSearchIT.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,15 @@ public void testCrossClusterSortWithTypeCasting() throws IOException {
205205
verifyDataRows(result, rows(1), rows(6), rows(13), rows(18), rows(20), rows(25), rows(32));
206206
}
207207

208+
@Test
209+
public void testCrossClusterPercentileShortcuts() throws IOException {
210+
JSONObject result =
211+
executeQuery(
212+
String.format(
213+
"search source=%s | stats perc50(balance), p95(balance)", TEST_INDEX_BANK_REMOTE));
214+
verifyColumn(result, columnName("perc50(balance)"), columnName("p95(balance)"));
215+
}
216+
208217
@Test
209218
public void testCrossClusterMultiMatchWithoutFields() throws IOException {
210219
// Test multi_match without fields parameter on remote cluster

ppl/src/main/antlr/OpenSearchPPLLexer.g4

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ VAR_SAMP: 'VAR_SAMP';
237237
VAR_POP: 'VAR_POP';
238238
STDDEV_SAMP: 'STDDEV_SAMP';
239239
STDDEV_POP: 'STDDEV_POP';
240+
PERC: 'PERC';
240241
PERCENTILE: 'PERCENTILE';
241242
PERCENTILE_APPROX: 'PERCENTILE_APPROX';
242243
EARLIEST: 'EARLIEST';
@@ -500,6 +501,10 @@ CS: 'CS';
500501
DS: 'DS';
501502

502503

504+
// PERCENTILE SHORTCUT FUNCTIONS
505+
// Must precede ID to avoid conflicts with identifier matching
506+
PERCENTILE_SHORTCUT: PERC(INTEGER_LITERAL | DECIMAL_LITERAL) | 'P'(INTEGER_LITERAL | DECIMAL_LITERAL);
507+
503508
// LITERALS AND VALUES
504509
//STRING_LITERAL: DQUOTA_STRING | SQUOTA_STRING | BQUOTA_STRING;
505510
ID: ID_LITERAL;

ppl/src/main/antlr/OpenSearchPPLParser.g4

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ statsAggTerm
460460
statsFunction
461461
: statsFunctionName LT_PRTHS valueExpression RT_PRTHS # statsFunctionCall
462462
| COUNT LT_PRTHS RT_PRTHS # countAllFunctionCall
463+
| PERCENTILE_SHORTCUT LT_PRTHS valueExpression RT_PRTHS # percentileShortcutFunctionCall
463464
| (DISTINCT_COUNT | DC | DISTINCT_COUNT_APPROX) LT_PRTHS valueExpression RT_PRTHS # distinctCountFunctionCall
464465
| takeAggFunction # takeAggFunctionCall
465466
| percentileApproxFunction # percentileApproxFunctionCall
@@ -481,6 +482,8 @@ statsFunctionName
481482
| LATEST
482483
;
483484

485+
486+
484487
takeAggFunction
485488
: TAKE LT_PRTHS fieldExpression (COMMA size = integerLiteral)? RT_PRTHS
486489
;
@@ -1285,4 +1288,5 @@ keywordsCanBeId
12851288
| ANTI
12861289
| LEFT_HINT
12871290
| RIGHT_HINT
1291+
| PERCENTILE_SHORTCUT
12881292
;

ppl/src/main/java/org/opensearch/sql/ppl/parser/AstExpressionBuilder.java

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,6 @@
66
package org.opensearch.sql.ppl.parser;
77

88
import static org.opensearch.sql.expression.function.BuiltinFunctionName.*;
9-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.BinaryArithmeticContext;
10-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.BooleanLiteralContext;
11-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.BySpanClauseContext;
12-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.CompareExprContext;
13-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.ConvertedDataTypeContext;
14-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.CountAllFunctionCallContext;
15-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.DataTypeFunctionCallContext;
16-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.DecimalLiteralContext;
17-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.DistinctCountFunctionCallContext;
18-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.DoubleLiteralContext;
19-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.EvalClauseContext;
20-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.EvalFunctionCallContext;
21-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.FieldExpressionContext;
22-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.FloatLiteralContext;
23-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.IdentsAsQualifiedNameContext;
24-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.IdentsAsTableQualifiedNameContext;
25-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.IdentsAsWildcardQualifiedNameContext;
26-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.InExprContext;
27-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.IntegerLiteralContext;
28-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.IntervalLiteralContext;
29-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.LogicalAndContext;
30-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.LogicalNotContext;
31-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.LogicalOrContext;
32-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.LogicalXorContext;
33-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.MultiFieldRelevanceFunctionContext;
34-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.SingleFieldRelevanceFunctionContext;
35-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.SortFieldContext;
36-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.SpanClauseContext;
37-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.StatsFunctionCallContext;
38-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.StringLiteralContext;
39-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.TableSourceContext;
40-
import static org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.WcFieldExpressionContext;
419

4210
import com.google.common.collect.ImmutableList;
4311
import com.google.common.collect.ImmutableMap;
@@ -62,6 +30,38 @@
6230
import org.opensearch.sql.common.antlr.SyntaxCheckException;
6331
import org.opensearch.sql.common.utils.StringUtils;
6432
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser;
33+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.BinaryArithmeticContext;
34+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.BooleanLiteralContext;
35+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.BySpanClauseContext;
36+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.CompareExprContext;
37+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.ConvertedDataTypeContext;
38+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.CountAllFunctionCallContext;
39+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.DataTypeFunctionCallContext;
40+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.DecimalLiteralContext;
41+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.DistinctCountFunctionCallContext;
42+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.DoubleLiteralContext;
43+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.EvalClauseContext;
44+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.EvalFunctionCallContext;
45+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.FieldExpressionContext;
46+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.FloatLiteralContext;
47+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.IdentsAsQualifiedNameContext;
48+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.IdentsAsTableQualifiedNameContext;
49+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.IdentsAsWildcardQualifiedNameContext;
50+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.InExprContext;
51+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.IntegerLiteralContext;
52+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.IntervalLiteralContext;
53+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.LogicalAndContext;
54+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.LogicalNotContext;
55+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.LogicalOrContext;
56+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.LogicalXorContext;
57+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.MultiFieldRelevanceFunctionContext;
58+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.SingleFieldRelevanceFunctionContext;
59+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.SortFieldContext;
60+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.SpanClauseContext;
61+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.StatsFunctionCallContext;
62+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.StringLiteralContext;
63+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.TableSourceContext;
64+
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParser.WcFieldExpressionContext;
6565
import org.opensearch.sql.ppl.antlr.parser.OpenSearchPPLParserBaseVisitor;
6666
import org.opensearch.sql.ppl.utils.ArgumentFactory;
6767

@@ -268,6 +268,27 @@ public UnresolvedExpression visitTakeAggFunctionCall(
268268
"take", visit(ctx.takeAggFunction().fieldExpression()), builder.build());
269269
}
270270

271+
@Override
272+
public UnresolvedExpression visitPercentileShortcutFunctionCall(
273+
OpenSearchPPLParser.PercentileShortcutFunctionCallContext ctx) {
274+
String functionName = ctx.getStart().getText();
275+
276+
int prefixLength = functionName.toLowerCase().startsWith("perc") ? 4 : 1;
277+
String percentileValue = functionName.substring(prefixLength);
278+
279+
double percent = Double.parseDouble(percentileValue);
280+
if (percent < 0.0 || percent > 100.0) {
281+
throw new SyntaxCheckException(
282+
String.format("Percentile value must be between 0 and 100, got: %s", percent));
283+
}
284+
285+
return new AggregateFunction(
286+
"percentile",
287+
visit(ctx.valueExpression()),
288+
Collections.singletonList(
289+
new UnresolvedArgument("percent", AstDSL.doubleLiteral(percent))));
290+
}
291+
271292
/** Case function. */
272293
@Override
273294
public UnresolvedExpression visitCaseFunctionCall(

ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLAggregationTest.java

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,4 +543,92 @@ public void testTwoLevelStats() {
543543
+ "GROUP BY `MGR`";
544544
verifyPPLToSparkSQL(root, expectedSparkSql);
545545
}
546+
547+
@Test
548+
public void testPercentileShortcuts() {
549+
String ppl = "source=EMP | stats perc50(SAL), p95(SAL)";
550+
RelNode root = getRelNode(ppl);
551+
String expectedLogical =
552+
"LogicalAggregate(group=[{}], perc50(SAL)=[percentile_approx($0, $1, $2)],"
553+
+ " p95(SAL)=[percentile_approx($0, $3, $2)])\n"
554+
+ " LogicalProject(SAL=[$5], $f2=[50.0E0:DOUBLE], $f3=[FLAG(DECIMAL)],"
555+
+ " $f4=[95.0E0:DOUBLE])\n"
556+
+ " LogicalTableScan(table=[[scott, EMP]])\n";
557+
verifyLogical(root, expectedLogical);
558+
559+
String expectedSparkSql =
560+
"SELECT `percentile_approx`(`SAL`, 5.00E1, DECIMAL) `perc50(SAL)`,"
561+
+ " `percentile_approx`(`SAL`, 9.50E1, DECIMAL) `p95(SAL)`\n"
562+
+ "FROM `scott`.`EMP`";
563+
verifyPPLToSparkSQL(root, expectedSparkSql);
564+
}
565+
566+
@Test
567+
public void testPercentileShortcutsWithDecimals() {
568+
String ppl = "source=EMP | stats perc25.5(SAL), p99.9(SAL)";
569+
RelNode root = getRelNode(ppl);
570+
String expectedLogical =
571+
"LogicalAggregate(group=[{}], perc25.5(SAL)=[percentile_approx($0, $1, $2)],"
572+
+ " p99.9(SAL)=[percentile_approx($0, $3, $2)])\n"
573+
+ " LogicalProject(SAL=[$5], $f2=[25.5E0:DOUBLE], $f3=[FLAG(DECIMAL)],"
574+
+ " $f4=[99.9E0:DOUBLE])\n"
575+
+ " LogicalTableScan(table=[[scott, EMP]])\n";
576+
verifyLogical(root, expectedLogical);
577+
578+
String expectedSparkSql =
579+
"SELECT `percentile_approx`(`SAL`, 2.55E1, DECIMAL) `perc25.5(SAL)`,"
580+
+ " `percentile_approx`(`SAL`, 9.99E1, DECIMAL) `p99.9(SAL)`\n"
581+
+ "FROM `scott`.`EMP`";
582+
verifyPPLToSparkSQL(root, expectedSparkSql);
583+
}
584+
585+
@Test
586+
public void testPercentileShortcutsByField() {
587+
String ppl = "source=EMP | stats perc75(SAL) by DEPTNO";
588+
RelNode root = getRelNode(ppl);
589+
String expectedLogical =
590+
"LogicalProject(perc75(SAL)=[$1], DEPTNO=[$0])\n"
591+
+ " LogicalAggregate(group=[{0}], perc75(SAL)=[percentile_approx($1, $2, $3)])\n"
592+
+ " LogicalProject(DEPTNO=[$7], SAL=[$5], $f2=[75.0E0:DOUBLE],"
593+
+ " $f3=[FLAG(DECIMAL)])\n"
594+
+ " LogicalTableScan(table=[[scott, EMP]])\n";
595+
verifyLogical(root, expectedLogical);
596+
597+
String expectedSparkSql =
598+
"SELECT `percentile_approx`(`SAL`, 7.50E1, DECIMAL) `perc75(SAL)`, `DEPTNO`\n"
599+
+ "FROM `scott`.`EMP`\n"
600+
+ "GROUP BY `DEPTNO`";
601+
verifyPPLToSparkSQL(root, expectedSparkSql);
602+
}
603+
604+
@Test
605+
public void testPercentileShortcutsBoundaryValues() {
606+
String ppl = "source=EMP | stats perc0(SAL), p100(SAL)";
607+
RelNode root = getRelNode(ppl);
608+
String expectedLogical =
609+
"LogicalAggregate(group=[{}], perc0(SAL)=[percentile_approx($0, $1, $2)],"
610+
+ " p100(SAL)=[percentile_approx($0, $3, $2)])\n"
611+
+ " LogicalProject(SAL=[$5], $f2=[0.0E0:DOUBLE], $f3=[FLAG(DECIMAL)],"
612+
+ " $f4=[100.0E0:DOUBLE])\n"
613+
+ " LogicalTableScan(table=[[scott, EMP]])\n";
614+
verifyLogical(root, expectedLogical);
615+
616+
String expectedSparkSql =
617+
"SELECT `percentile_approx`(`SAL`, 0E0, DECIMAL) `perc0(SAL)`,"
618+
+ " `percentile_approx`(`SAL`, 1.000E2, DECIMAL) `p100(SAL)`\n"
619+
+ "FROM `scott`.`EMP`";
620+
verifyPPLToSparkSQL(root, expectedSparkSql);
621+
}
622+
623+
@Test(expected = Exception.class)
624+
public void testPercentileShortcutInvalidValueAbove100() {
625+
String ppl = "source=EMP | stats p101(SAL)";
626+
getRelNode(ppl);
627+
}
628+
629+
@Test(expected = Exception.class)
630+
public void testPercentileShortcutInvalidDecimalValueAbove100() {
631+
String ppl = "source=EMP | stats perc100.1(SAL)";
632+
getRelNode(ppl);
633+
}
546634
}

0 commit comments

Comments
 (0)