From 44c7a51764c921911ac64bc67006592ad52aa8c1 Mon Sep 17 00:00:00 2001 From: Dai Date: Tue, 15 Dec 2020 15:32:38 -0800 Subject: [PATCH 01/16] Change grammar and AST builder --- .../sql/analysis/ExpressionAnalyzer.java | 7 ++- .../sql/ast/dsl/AstDSL.java | 2 +- .../sql/ast/expression/WindowFunction.java | 2 +- sql/src/main/antlr/OpenDistroSQLParser.g4 | 12 ++-- .../sql/sql/parser/AstExpressionBuilder.java | 57 +++++++++++-------- .../sql/parser/AstExpressionBuilderTest.java | 11 ++++ 6 files changed, 59 insertions(+), 32 deletions(-) diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java index 06d90608f6..eb6143aa7e 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java @@ -165,7 +165,12 @@ public Expression visitFunction(Function node, AnalysisContext context) { @Override public Expression visitWindowFunction(WindowFunction node, AnalysisContext context) { - return visitFunction(node.getFunction(), context); + Expression expr = node.getFunction().accept(this, context); + // Wrap regular aggregator by aggregate window function to adapt window operator use + if (expr instanceof Aggregator) { + //return new AggregateWindowFunction((Aggregator) expr); + } + return expr; } @Override diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/dsl/AstDSL.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/dsl/AstDSL.java index fc0e79622f..7074a8d89d 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/dsl/AstDSL.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/dsl/AstDSL.java @@ -230,7 +230,7 @@ public When when(UnresolvedExpression condition, UnresolvedExpression result) { return new When(condition, result); } - public UnresolvedExpression window(Function function, + public UnresolvedExpression window(UnresolvedExpression function, List partitionByList, List> sortList) { return new WindowFunction(function, partitionByList, sortList); diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/WindowFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/WindowFunction.java index 2e323f937f..5124616aa8 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/WindowFunction.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/WindowFunction.java @@ -32,7 +32,7 @@ @RequiredArgsConstructor public class WindowFunction extends UnresolvedExpression { - private final Function function; + private final UnresolvedExpression function; private List partitionByList; private List> sortList; diff --git a/sql/src/main/antlr/OpenDistroSQLParser.g4 b/sql/src/main/antlr/OpenDistroSQLParser.g4 index 5dbc92c86d..d5782b7131 100644 --- a/sql/src/main/antlr/OpenDistroSQLParser.g4 +++ b/sql/src/main/antlr/OpenDistroSQLParser.g4 @@ -155,12 +155,14 @@ limitClause ; // Window Function's Details -windowFunction - : function=rankingWindowFunction overClause +windowFunctionClause + : function=windowFunction overClause ; -rankingWindowFunction - : functionName=(ROW_NUMBER | RANK | DENSE_RANK) LR_BRACKET RR_BRACKET +windowFunction + : functionName=(ROW_NUMBER | RANK | DENSE_RANK) + LR_BRACKET functionArgs? RR_BRACKET #scalarWindowFunction + | aggregateFunction #aggregateWindowFunction ; overClause @@ -283,7 +285,7 @@ nullNotnull functionCall : scalarFunctionName LR_BRACKET functionArgs? RR_BRACKET #scalarFunctionCall | specificFunction #specificFunctionCall - | windowFunction #windowFunctionCall + | windowFunctionClause #windowFunctionCall | aggregateFunction #aggregateFunctionCall ; diff --git a/sql/src/main/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/AstExpressionBuilder.java b/sql/src/main/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/AstExpressionBuilder.java index 4c5947a223..c06c20cd99 100644 --- a/sql/src/main/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/AstExpressionBuilder.java +++ b/sql/src/main/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/AstExpressionBuilder.java @@ -27,7 +27,10 @@ import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.BooleanContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.CaseFuncAlternativeContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.CaseFunctionCallContext; +import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.ColumnFilterContext; +import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.ConvertedDataTypeContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.CountStarFunctionCallContext; +import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.DataTypeFunctionCallContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.DateLiteralContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.IsNullPredicateContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.LikePredicateContext; @@ -37,16 +40,19 @@ import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.OrderByElementContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.OverClauseContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.QualifiedNameContext; -import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.RankingWindowFunctionContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.RegexpPredicateContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.RegularAggregateFunctionCallContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.ScalarFunctionCallContext; +import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.ScalarWindowFunctionContext; +import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.ShowDescribePatternContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.SignedDecimalContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.SignedRealContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.StringContext; +import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.StringLiteralContext; +import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.TableFilterContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.TimeLiteralContext; import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.TimestampLiteralContext; -import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.WindowFunctionContext; +import static com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.WindowFunctionClauseContext; import com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL; import com.amazon.opendistroforelasticsearch.sql.ast.expression.AggregateFunction; @@ -64,9 +70,9 @@ import com.amazon.opendistroforelasticsearch.sql.ast.expression.When; import com.amazon.opendistroforelasticsearch.sql.ast.expression.WindowFunction; import com.amazon.opendistroforelasticsearch.sql.common.utils.StringUtils; -import com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser; import com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.AndExpressionContext; import com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.ColumnNameContext; +import com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.FunctionArgsContext; import com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.IdentContext; import com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.IntervalLiteralContext; import com.amazon.opendistroforelasticsearch.sql.sql.antlr.parser.OpenDistroSQLParser.NestedExpressionAtomContext; @@ -121,28 +127,18 @@ public UnresolvedExpression visitNestedExpressionAtom(NestedExpressionAtomContex @Override public UnresolvedExpression visitScalarFunctionCall(ScalarFunctionCallContext ctx) { - if (ctx.functionArgs() == null) { - return new Function(ctx.scalarFunctionName().getText(), Collections.emptyList()); - } - return new Function( - ctx.scalarFunctionName().getText(), - ctx.functionArgs() - .functionArg() - .stream() - .map(this::visitFunctionArg) - .collect(Collectors.toList()) - ); + return visitFunction(ctx.scalarFunctionName().getText(), ctx.functionArgs()); } @Override - public UnresolvedExpression visitTableFilter(OpenDistroSQLParser.TableFilterContext ctx) { + public UnresolvedExpression visitTableFilter(TableFilterContext ctx) { return new Function( LIKE.getName().getFunctionName(), Arrays.asList(qualifiedName("TABLE_NAME"), visit(ctx.showDescribePattern()))); } @Override - public UnresolvedExpression visitColumnFilter(OpenDistroSQLParser.ColumnFilterContext ctx) { + public UnresolvedExpression visitColumnFilter(ColumnFilterContext ctx) { return new Function( LIKE.getName().getFunctionName(), Arrays.asList(qualifiedName("COLUMN_NAME"), visit(ctx.showDescribePattern()))); @@ -150,7 +146,7 @@ public UnresolvedExpression visitColumnFilter(OpenDistroSQLParser.ColumnFilterCo @Override public UnresolvedExpression visitShowDescribePattern( - OpenDistroSQLParser.ShowDescribePatternContext ctx) { + ShowDescribePatternContext ctx) { if (ctx.compatibleID() != null) { return stringLiteral(ctx.compatibleID().getText()); } else { @@ -159,7 +155,7 @@ public UnresolvedExpression visitShowDescribePattern( } @Override - public UnresolvedExpression visitWindowFunction(WindowFunctionContext ctx) { + public UnresolvedExpression visitWindowFunctionClause(WindowFunctionClauseContext ctx) { OverClauseContext overClause = ctx.overClause(); List partitionByList = Collections.emptyList(); @@ -179,12 +175,12 @@ public UnresolvedExpression visitWindowFunction(WindowFunctionContext ctx) { .map(item -> ImmutablePair.of(getOrder(item), visit(item.expression()))) .collect(Collectors.toList()); } - return new WindowFunction((Function) visit(ctx.function), partitionByList, sortList); + return new WindowFunction(visit(ctx.function), partitionByList, sortList); } @Override - public UnresolvedExpression visitRankingWindowFunction(RankingWindowFunctionContext ctx) { - return new Function(ctx.functionName.getText(), Collections.emptyList()); + public UnresolvedExpression visitScalarWindowFunction(ScalarWindowFunctionContext ctx) { + return visitFunction(ctx.functionName.getText(), ctx.functionArgs()); } @Override @@ -258,7 +254,7 @@ public UnresolvedExpression visitBoolean(BooleanContext ctx) { } @Override - public UnresolvedExpression visitStringLiteral(OpenDistroSQLParser.StringLiteralContext ctx) { + public UnresolvedExpression visitStringLiteral(StringLiteralContext ctx) { return AstDSL.stringLiteral(StringUtils.unquoteText(ctx.getText())); } @@ -318,16 +314,29 @@ public UnresolvedExpression visitCaseFuncAlternative(CaseFuncAlternativeContext @Override public UnresolvedExpression visitDataTypeFunctionCall( - OpenDistroSQLParser.DataTypeFunctionCallContext ctx) { + DataTypeFunctionCallContext ctx) { return new Cast(visit(ctx.expression()), visit(ctx.convertedDataType())); } @Override public UnresolvedExpression visitConvertedDataType( - OpenDistroSQLParser.ConvertedDataTypeContext ctx) { + ConvertedDataTypeContext ctx) { return AstDSL.stringLiteral(ctx.getText()); } + private Function visitFunction(String functionName, FunctionArgsContext args) { + if (args == null) { + return new Function(functionName, Collections.emptyList()); + } + return new Function( + functionName, + args.functionArg() + .stream() + .map(this::visitFunctionArg) + .collect(Collectors.toList()) + ); + } + private QualifiedName visitIdentifiers(List identifiers) { return new QualifiedName( identifiers.stream() diff --git a/sql/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/AstExpressionBuilderTest.java b/sql/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/AstExpressionBuilderTest.java index 622a5be712..4dc833af4f 100644 --- a/sql/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/AstExpressionBuilderTest.java +++ b/sql/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/AstExpressionBuilderTest.java @@ -16,6 +16,7 @@ package com.amazon.opendistroforelasticsearch.sql.sql.parser; +import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.aggregate; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.and; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.booleanLiteral; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.caseWhen; @@ -279,6 +280,16 @@ public void canBuildWindowFunctionWithoutOrderBy() { buildExprAst("RANK() OVER (PARTITION BY state)")); } + @Test + public void canBuildAggregateWindowFunction() { + assertEquals( + window( + aggregate("AVG", qualifiedName("age")), + ImmutableList.of(qualifiedName("state")), + ImmutableList.of(ImmutablePair.of("ASC", qualifiedName("age")))), + buildExprAst("AVG(age) OVER (PARTITION BY state ORDER BY age)")); + } + @Test public void canBuildCaseConditionStatement() { assertEquals( From 4cd76bce98522b45c1dc696479c9ea24d4430213 Mon Sep 17 00:00:00 2001 From: Dai Date: Tue, 15 Dec 2020 16:51:34 -0800 Subject: [PATCH 02/16] Add tostring and getchild for window function AST node --- .../sql/ast/expression/WindowFunction.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/WindowFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/WindowFunction.java index 5124616aa8..5e3a5a2831 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/WindowFunction.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/WindowFunction.java @@ -18,18 +18,20 @@ import com.amazon.opendistroforelasticsearch.sql.ast.AbstractNodeVisitor; import com.amazon.opendistroforelasticsearch.sql.ast.Node; -import java.util.Collections; +import com.google.common.collect.ImmutableList; import java.util.List; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.ToString; import org.apache.commons.lang3.tuple.Pair; @AllArgsConstructor @EqualsAndHashCode(callSuper = false) @Getter @RequiredArgsConstructor +@ToString public class WindowFunction extends UnresolvedExpression { private final UnresolvedExpression function; @@ -38,7 +40,11 @@ public class WindowFunction extends UnresolvedExpression { @Override public List getChild() { - return Collections.singletonList(function); + ImmutableList.Builder children = ImmutableList.builder(); + children.add(function); + children.addAll(partitionByList); + sortList.forEach(pair -> children.add(pair.getRight())); + return children.build(); } @Override From eefd50d6b27ac3cd6a5c1e6a9f79b835cb7f09ce Mon Sep 17 00:00:00 2001 From: Dai Date: Tue, 15 Dec 2020 17:05:08 -0800 Subject: [PATCH 03/16] Add aggregate window function class --- .../sql/analysis/ExpressionAnalyzer.java | 5 +- .../aggregation/AggregateWindowFunction.java | 68 +++++++ .../sql/analysis/ExpressionAnalyzerTest.java | 45 +++-- .../AggregateWindowFunctionTest.java | 177 ++++++++++++++++++ 4 files changed, 280 insertions(+), 15 deletions(-) create mode 100644 core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunction.java create mode 100644 core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunctionTest.java diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java index eb6143aa7e..79be58a025 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java @@ -44,12 +44,14 @@ import com.amazon.opendistroforelasticsearch.sql.expression.DSL; import com.amazon.opendistroforelasticsearch.sql.expression.Expression; import com.amazon.opendistroforelasticsearch.sql.expression.ReferenceExpression; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.AggregationState; import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.Aggregator; import com.amazon.opendistroforelasticsearch.sql.expression.conditional.cases.CaseClause; import com.amazon.opendistroforelasticsearch.sql.expression.conditional.cases.WhenClause; import com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionName; import com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionRepository; import com.amazon.opendistroforelasticsearch.sql.expression.function.FunctionName; +import com.amazon.opendistroforelasticsearch.sql.expression.window.aggregation.AggregateWindowFunction; import com.google.common.collect.ImmutableSet; import java.util.ArrayList; import java.util.Arrays; @@ -163,12 +165,13 @@ public Expression visitFunction(Function node, AnalysisContext context) { return (Expression) repository.compile(functionName, arguments); } + @SuppressWarnings("unchecked") @Override public Expression visitWindowFunction(WindowFunction node, AnalysisContext context) { Expression expr = node.getFunction().accept(this, context); // Wrap regular aggregator by aggregate window function to adapt window operator use if (expr instanceof Aggregator) { - //return new AggregateWindowFunction((Aggregator) expr); + return new AggregateWindowFunction((Aggregator) expr); } return expr; } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunction.java new file mode 100644 index 0000000000..49f6273613 --- /dev/null +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunction.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package com.amazon.opendistroforelasticsearch.sql.expression.window.aggregation; + +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; +import com.amazon.opendistroforelasticsearch.sql.data.type.ExprType; +import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionNodeVisitor; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.AggregationState; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.Aggregator; +import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.WindowFrame; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; + +/** + * Aggregate function adapter that adapts Aggregator for window operator use. + */ +@EqualsAndHashCode +@RequiredArgsConstructor +public class AggregateWindowFunction implements Expression { + + private final Aggregator aggregator; + private AggregationState state; + + @Override + public ExprValue valueOf(Environment valueEnv) { + WindowFrame frame = (WindowFrame) valueEnv; + if (frame.isNewPartition()) { + state = aggregator.create(); + } + + ExprTupleValue row = frame.get(frame.currentIndex()); + state = aggregator.iterate(row.bindingTuples(), state); + return state.result(); + } + + @Override + public ExprType type() { + return aggregator.type(); + } + + @Override + public T accept(ExpressionNodeVisitor visitor, C context) { + return aggregator.accept(visitor, context); + } + + @Override + public String toString() { + return aggregator.toString(); + } + +} diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzerTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzerTest.java index ce40c26721..fd3d1e1368 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzerTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzerTest.java @@ -16,11 +16,13 @@ package com.amazon.opendistroforelasticsearch.sql.analysis; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.field; +import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.qualifiedName; import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.LITERAL_TRUE; import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.integerValue; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.BOOLEAN; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRUCT; +import static java.util.Collections.emptyList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -35,11 +37,8 @@ import com.amazon.opendistroforelasticsearch.sql.exception.SemanticCheckException; import com.amazon.opendistroforelasticsearch.sql.expression.DSL; import com.amazon.opendistroforelasticsearch.sql.expression.Expression; -import com.amazon.opendistroforelasticsearch.sql.expression.LiteralExpression; import com.amazon.opendistroforelasticsearch.sql.expression.config.ExpressionConfig; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; +import com.amazon.opendistroforelasticsearch.sql.expression.window.aggregation.AggregateWindowFunction; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.context.annotation.Configuration; @@ -95,7 +94,7 @@ public void not() { public void qualified_name() { assertAnalyzeEqual( DSL.ref("integer_value", INTEGER), - AstDSL.qualifiedName("integer_value") + qualifiedName("integer_value") ); } @@ -111,7 +110,7 @@ public void case_value() { dsl.equal(DSL.ref("integer_value", INTEGER), DSL.literal(50)), DSL.literal("Fifty"))), AstDSL.caseWhen( - AstDSL.qualifiedName("integer_value"), + qualifiedName("integer_value"), AstDSL.stringLiteral("Default value"), AstDSL.when(AstDSL.intLiteral(30), AstDSL.stringLiteral("Thirty")), AstDSL.when(AstDSL.intLiteral(50), AstDSL.stringLiteral("Fifty")))); @@ -132,11 +131,11 @@ public void case_conditions() { null, AstDSL.when( AstDSL.function(">", - AstDSL.qualifiedName("integer_value"), + qualifiedName("integer_value"), AstDSL.intLiteral(50)), AstDSL.stringLiteral("Fifty")), AstDSL.when( AstDSL.function(">", - AstDSL.qualifiedName("integer_value"), + qualifiedName("integer_value"), AstDSL.intLiteral(30)), AstDSL.stringLiteral("Thirty")))); } @@ -154,7 +153,7 @@ public void castAnalyzer() { @Test public void case_with_default_result_type_different() { UnresolvedExpression caseWhen = AstDSL.caseWhen( - AstDSL.qualifiedName("integer_value"), + qualifiedName("integer_value"), AstDSL.intLiteral(60), AstDSL.when(AstDSL.intLiteral(30), AstDSL.stringLiteral("Thirty")), AstDSL.when(AstDSL.intLiteral(50), AstDSL.stringLiteral("Fifty"))); @@ -166,19 +165,37 @@ public void case_with_default_result_type_different() { exception.getMessage()); } + @Test + public void scalar_window_function() { + assertAnalyzeEqual( + dsl.rank(), + AstDSL.window(AstDSL.function("rank"), emptyList(), emptyList())); + } + + @SuppressWarnings("unchecked") + @Test + public void aggregate_window_function() { + assertAnalyzeEqual( + new AggregateWindowFunction(dsl.avg(DSL.ref("integer_value", INTEGER))), + AstDSL.window( + AstDSL.aggregate("avg", qualifiedName("integer_value")), + emptyList(), + emptyList())); + } + @Test public void qualified_name_with_qualifier() { analysisContext.push(); analysisContext.peek().define(new Symbol(Namespace.INDEX_NAME, "index_alias"), STRUCT); assertAnalyzeEqual( DSL.ref("integer_value", INTEGER), - AstDSL.qualifiedName("index_alias", "integer_value") + qualifiedName("index_alias", "integer_value") ); analysisContext.peek().define(new Symbol(Namespace.FIELD_NAME, "nested_field"), STRUCT); SyntaxCheckException exception = assertThrows(SyntaxCheckException.class, - () -> analyze(AstDSL.qualifiedName("nested_field", "integer_value"))); + () -> analyze(qualifiedName("nested_field", "integer_value"))); assertEquals( "The qualifier [nested_field] of qualified name [nested_field.integer_value] " + "must be an index name or its alias", @@ -213,7 +230,7 @@ public void case_clause() { AstDSL.nullLiteral(), AstDSL.when( AstDSL.function("=", - AstDSL.qualifiedName("integer_value"), + qualifiedName("integer_value"), AstDSL.intLiteral(30)), AstDSL.stringLiteral("test")))); } @@ -222,7 +239,7 @@ public void case_clause() { public void skip_struct_data_type() { SyntaxCheckException exception = assertThrows(SyntaxCheckException.class, - () -> analyze(AstDSL.qualifiedName("struct_value"))); + () -> analyze(qualifiedName("struct_value"))); assertEquals( "Identifier [struct_value] of type [STRUCT] is not supported yet", exception.getMessage() @@ -233,7 +250,7 @@ public void skip_struct_data_type() { public void skip_array_data_type() { SyntaxCheckException exception = assertThrows(SyntaxCheckException.class, - () -> analyze(AstDSL.qualifiedName("array_value"))); + () -> analyze(qualifiedName("array_value"))); assertEquals( "Identifier [array_value] of type [ARRAY] is not supported yet", exception.getMessage() diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunctionTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunctionTest.java new file mode 100644 index 0000000000..ae89017ce3 --- /dev/null +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunctionTest.java @@ -0,0 +1,177 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package com.amazon.opendistroforelasticsearch.sql.expression.window.aggregation; + +import static com.amazon.opendistroforelasticsearch.sql.ast.tree.Sort.SortOption.DEFAULT_ASC; +import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue.fromExprValueMap; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.LONG; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRING; +import static com.amazon.opendistroforelasticsearch.sql.expression.DSL.ref; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprIntegerValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprStringValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils; +import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionTestBase; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.AggregationState; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.Aggregator; +import com.amazon.opendistroforelasticsearch.sql.expression.window.CumulativeWindowFrame; +import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Aggregate window function test collection. + */ +@SuppressWarnings("unchecked") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@ExtendWith(MockitoExtension.class) +class AggregateWindowFunctionTest extends ExpressionTestBase { + + @SuppressWarnings("rawtypes") + @Test + void test_delegated_methods() { + Aggregator aggregator = mock(Aggregator.class); + when(aggregator.type()).thenReturn(LONG); + when(aggregator.accept(any(), any())).thenReturn(123); + when(aggregator.toString()).thenReturn("avg(age)"); + + AggregateWindowFunction windowFunction = new AggregateWindowFunction(aggregator); + assertEquals(LONG, windowFunction.type()); + assertEquals(123, (Integer) windowFunction.accept(null, null)); + assertEquals("avg(age)", windowFunction.toString()); + } + + @Test + void test_count() { + windowFunction(dsl.count(ref("age", INTEGER))) + .add("WA", 20) + .assertValue(1) + .add("WA", 25) + .assertValue(2) + .add("WA", 30) + .assertValue(3) + .add("CA", 15) + .assertValue(1) + .add("CA", 20) + .assertValue(2); + } + + @Test + void test_sum() { + windowFunction(dsl.sum(ref("age", INTEGER))) + .add("WA", 20) + .assertValue(20) + .add("WA", 25) + .assertValue(45) + .add("WA", 30) + .assertValue(75) + .add("CA", 15) + .assertValue(15) + .add("CA", 20) + .assertValue(35); + } + + @Test + void test_avg() { + windowFunction(dsl.avg(ref("age", INTEGER))) + .add("WA", 20) + .assertValue(20.0) + .add("WA", 30) + .assertValue(25.0) + .add("WA", 40) + .assertValue(30.0) + .add("CA", 15) + .assertValue(15.0) + .add("CA", 25) + .assertValue(20.0); + } + + @Test + void test_min() { + windowFunction(dsl.min(ref("age", INTEGER))) + .add("WA", 20) + .assertValue(20) + .add("WA", 25) + .assertValue(20) + .add("WA", 30) + .assertValue(20) + .add("CA", 15) + .assertValue(15) + .add("CA", 20) + .assertValue(15); + } + + @Test + void test_max() { + windowFunction(dsl.max(ref("age", INTEGER))) + .add("WA", 20) + .assertValue(20) + .add("WA", 25) + .assertValue(25) + .add("WA", 30) + .assertValue(30) + .add("CA", 15) + .assertValue(15) + .add("CA", 20) + .assertValue(20); + } + + private static AggregateWindowFunctionAssertion windowFunction( + Aggregator func) { + return new AggregateWindowFunctionAssertion(func); + } + + private static class AggregateWindowFunctionAssertion { + private final CumulativeWindowFrame windowFrame = new CumulativeWindowFrame( + new WindowDefinition( + ImmutableList.of(ref("state", STRING)), + ImmutableList.of(Pair.of(DEFAULT_ASC, ref("age", INTEGER))))); + + private final Expression windowFunction; + + private AggregateWindowFunctionAssertion(Aggregator windowFunction) { + this.windowFunction = new AggregateWindowFunction(windowFunction); + } + + AggregateWindowFunctionAssertion add(String state, int age) { + windowFrame.add(fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue(state), + "age", new ExprIntegerValue(age)))); + return this; + } + + AggregateWindowFunctionAssertion assertValue(Object expected) { + assertEquals( + ExprValueUtils.fromObjectValue(expected), + windowFunction.valueOf(windowFrame)); + return this; + } + } + +} From 95ed0bef8d6ffb1b2d424370f82e0c87bc9a54df Mon Sep 17 00:00:00 2001 From: Dai Date: Wed, 16 Dec 2020 08:49:06 -0800 Subject: [PATCH 04/16] Add peer window frame and refactor --- .../window/WindowFunctionExpression.java | 34 ++++ .../aggregation/AggregateWindowFunction.java | 18 +- .../{ => frame}/CumulativeWindowFrame.java | 42 +++-- .../window/frame/PeerWindowFrame.java | 121 +++++++++++++ .../expression/window/frame/WindowFrame.java | 25 ++- .../window/ranking/DenseRankFunction.java | 2 +- .../window/ranking/RankFunction.java | 2 +- .../window/ranking/RankingWindowFunction.java | 20 ++- .../window/ranking/RowNumberFunction.java | 2 +- .../sql/planner/physical/WindowOperator.java | 8 +- .../window/CumulativeWindowFrameTest.java | 65 ++++--- .../AggregateWindowFunctionTest.java | 134 +++----------- .../window/frame/PeerWindowFrameTest.java | 166 ++++++++++++++++++ .../ranking/RankingWindowFunctionTest.java | 137 ++++++++------- 14 files changed, 530 insertions(+), 246 deletions(-) create mode 100644 core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/WindowFunctionExpression.java rename core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/{ => frame}/CumulativeWindowFrame.java (80%) create mode 100644 core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrame.java create mode 100644 core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrameTest.java diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/WindowFunctionExpression.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/WindowFunctionExpression.java new file mode 100644 index 0000000000..163a82d934 --- /dev/null +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/WindowFunctionExpression.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package com.amazon.opendistroforelasticsearch.sql.expression.window; + +import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.WindowFrame; + +/** + * Window function abstraction. + */ +public interface WindowFunctionExpression extends Expression { + + /** + * Create specific window frame based on window definition and what's current window function. + * @param definition window definition + * @return window frame + */ + WindowFrame createWindowFrame(WindowDefinition definition); + +} diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunction.java index 49f6273613..592138e65f 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunction.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunction.java @@ -16,7 +16,6 @@ package com.amazon.opendistroforelasticsearch.sql.expression.window.aggregation; -import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; import com.amazon.opendistroforelasticsearch.sql.data.type.ExprType; import com.amazon.opendistroforelasticsearch.sql.expression.Expression; @@ -24,7 +23,11 @@ import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.AggregationState; import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.Aggregator; import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; +import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; +import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowFunctionExpression; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.PeerWindowFrame; import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.WindowFrame; +import java.util.List; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; @@ -33,11 +36,16 @@ */ @EqualsAndHashCode @RequiredArgsConstructor -public class AggregateWindowFunction implements Expression { +public class AggregateWindowFunction implements WindowFunctionExpression { private final Aggregator aggregator; private AggregationState state; + @Override + public WindowFrame createWindowFrame(WindowDefinition definition) { + return new PeerWindowFrame(definition); + } + @Override public ExprValue valueOf(Environment valueEnv) { WindowFrame frame = (WindowFrame) valueEnv; @@ -45,8 +53,10 @@ public ExprValue valueOf(Environment valueEnv) { state = aggregator.create(); } - ExprTupleValue row = frame.get(frame.currentIndex()); - state = aggregator.iterate(row.bindingTuples(), state); + List peers = frame.next(); + for (ExprValue peer : peers) { + state = aggregator.iterate(peer.bindingTuples(), state); + } return state.result(); } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CumulativeWindowFrame.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CumulativeWindowFrame.java similarity index 80% rename from core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CumulativeWindowFrame.java rename to core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CumulativeWindowFrame.java index 75a5b3605f..5f4ae51b1d 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CumulativeWindowFrame.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CumulativeWindowFrame.java @@ -14,13 +14,14 @@ * */ -package com.amazon.opendistroforelasticsearch.sql.expression.window; +package com.amazon.opendistroforelasticsearch.sql.expression.window.frame; -import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; import com.amazon.opendistroforelasticsearch.sql.expression.Expression; import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; -import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.WindowFrame; +import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; +import com.google.common.collect.ImmutableList; +import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -37,15 +38,15 @@ * to add "real" cumulative frame implementation in future as needed. */ @EqualsAndHashCode -@Getter @RequiredArgsConstructor @ToString public class CumulativeWindowFrame implements WindowFrame { + @Getter private final WindowDefinition windowDefinition; - private ExprTupleValue previous; - private ExprTupleValue current; + private ExprValue previous; + private ExprValue current; @Override public boolean isNewPartition() { @@ -61,26 +62,31 @@ public boolean isNewPartition() { } @Override - public int currentIndex() { - // Current row index is always 1 since only 2 rows maintained - return 1; + public void load(Iterator it) { + previous = current; + current = it.next(); } @Override - public void add(ExprTupleValue row) { - previous = current; - current = row; + public boolean hasNext() { + return false; } @Override - public ExprTupleValue get(int index) { - if (index != 0 && index != 1) { - throw new IndexOutOfBoundsException("Index is out of boundary of window frame: " + index); - } - return (index == 0) ? previous : current; + public List next() { + return ImmutableList.of(current); + } + + @Override + public ExprValue current() { + return current; + } + + public ExprValue previous() { + return previous; } - private List resolve(List expressions, ExprTupleValue row) { + private List resolve(List expressions, ExprValue row) { Environment valueEnv = row.bindingTuples(); return expressions.stream() .map(expr -> expr.valueOf(valueEnv)) diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrame.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrame.java new file mode 100644 index 0000000000..4791f02534 --- /dev/null +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrame.java @@ -0,0 +1,121 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package com.amazon.opendistroforelasticsearch.sql.expression.window.frame; + +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; +import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; +import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.tuple.Pair; + +/** + * Window frame that only keep peers (tuples with same value of fields specified in sort list + * in window definition). + */ +@RequiredArgsConstructor +public class PeerWindowFrame implements WindowFrame { + + private final WindowDefinition windowDefinition; + + private List peers = new ArrayList<>(); + private ExprValue next; + private int current; + + private boolean isNewPartition = true; + + @Override + public boolean hasNext() { + return current < peers.size(); + } + + @Override + public void load(Iterator it) { + if (hasNext()) { + return; + } + + if (next != null) { + isNewPartition = !isSamePartition(peers.get(peers.size() - 1), next); + peers.clear(); + peers.add(next); + } + + // load until next peer or partition + ExprValue cur = null; + while (it.hasNext() && isSamePartitionAndSortValues(cur = it.next())) { + peers.add(cur); + } + + current = 0; + next = cur; + } + + @Override + public boolean isNewPartition() { + return isNewPartition; + } + + @Override + public List next() { + isNewPartition = false; + if (current++ == 0) { + return peers; + } + return Collections.emptyList(); + } + + @Override + public ExprValue current() { + return peers.get(current); + } + + private List resolve(List expressions, ExprValue row) { + Environment valueEnv = row.bindingTuples(); + return expressions.stream() + .map(expr -> expr.valueOf(valueEnv)) + .collect(Collectors.toList()); + } + + private List getSortFields() { + return windowDefinition.getSortList() + .stream() + .map(Pair::getRight) + .collect(Collectors.toList()); + } + + private boolean isSamePartitionAndSortValues(ExprValue cur) { + if (peers.isEmpty()) { + return true; + } + + ExprValue prev = peers.get(peers.size() - 1); + return isSamePartition(cur, prev) + && resolve(getSortFields(), prev).equals(resolve(getSortFields(), cur)); + } + + private boolean isSamePartition(ExprValue cur, ExprValue prev) { + return resolve(windowDefinition.getPartitionByList(), prev) + .equals(resolve(windowDefinition.getPartitionByList(), cur)); + } + +} diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java index 4920598f69..81808a43b6 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java @@ -16,10 +16,11 @@ package com.amazon.opendistroforelasticsearch.sql.expression.window.frame; -import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; import com.amazon.opendistroforelasticsearch.sql.expression.Expression; import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; +import java.util.Iterator; +import java.util.List; /** * Window frame that represents a subset of a window which is all data accessible to @@ -30,11 +31,12 @@ * Note that which type of window frame is used is determined by both window function itself * and frame definition in a window definition. */ -public interface WindowFrame extends Environment { +public interface WindowFrame extends Environment, + Iterator> { @Override default ExprValue resolve(Expression var) { - return var.valueOf(get(currentIndex()).bindingTuples()); + return var.valueOf(current().bindingTuples()); } /** @@ -44,22 +46,15 @@ default ExprValue resolve(Expression var) { boolean isNewPartition(); /** - * Get current row index in the frame. - * @return index + * Load any number of rows as needed. + * @param iterator row iterator */ - int currentIndex(); + void load(Iterator iterator); /** - * Add a row to the window frame. - * @param row data row - */ - void add(ExprTupleValue row); - - /** - * Get a data rows within the frame by offset. - * @param index index starting from 0 to upper boundary + * Get current data row. * @return data row */ - ExprTupleValue get(int index); + ExprValue current(); } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/DenseRankFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/DenseRankFunction.java index 0eb0941fa7..8ea513de53 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/DenseRankFunction.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/DenseRankFunction.java @@ -17,7 +17,7 @@ package com.amazon.opendistroforelasticsearch.sql.expression.window.ranking; import com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionName; -import com.amazon.opendistroforelasticsearch.sql.expression.window.CumulativeWindowFrame; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CumulativeWindowFrame; /** * Dense rank window function that assigns a rank number to each row similarly as diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankFunction.java index 2569c2ca16..b0022b79e1 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankFunction.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankFunction.java @@ -17,7 +17,7 @@ package com.amazon.opendistroforelasticsearch.sql.expression.window.ranking; import com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionName; -import com.amazon.opendistroforelasticsearch.sql.expression.window.CumulativeWindowFrame; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CumulativeWindowFrame; /** * Rank window function that assigns a rank number to each row based on sort items diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunction.java index bb5419c105..9b500f0441 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunction.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunction.java @@ -26,7 +26,9 @@ import com.amazon.opendistroforelasticsearch.sql.expression.FunctionExpression; import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; import com.amazon.opendistroforelasticsearch.sql.expression.function.FunctionName; -import com.amazon.opendistroforelasticsearch.sql.expression.window.CumulativeWindowFrame; +import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; +import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowFunctionExpression; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CumulativeWindowFrame; import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.WindowFrame; import com.amazon.opendistroforelasticsearch.sql.storage.bindingtuple.BindingTuple; import java.util.List; @@ -37,7 +39,8 @@ * Ranking window function base class that captures same info across different ranking functions, * such as same return type (integer), same argument list (no arg). */ -public abstract class RankingWindowFunction extends FunctionExpression { +public abstract class RankingWindowFunction extends FunctionExpression + implements WindowFunctionExpression { /** * Current rank number assigned. @@ -53,6 +56,11 @@ public ExprType type() { return ExprCoreType.INTEGER; } + @Override + public WindowFrame createWindowFrame(WindowDefinition definition) { + return new CumulativeWindowFrame(definition); + } + @Override public ExprValue valueOf(Environment valueEnv) { return new ExprIntegerValue(rank((CumulativeWindowFrame) valueEnv)); @@ -81,8 +89,8 @@ protected boolean isSortFieldValueDifferent(CumulativeWindowFrame frame) { .map(Pair::getRight) .collect(Collectors.toList()); - List previous = resolve(frame, sortItems, frame.currentIndex() - 1); - List current = resolve(frame, sortItems, frame.currentIndex()); + List previous = resolve(frame, sortItems, frame.previous()); + List current = resolve(frame, sortItems, frame.current()); return !current.equals(previous); } @@ -90,8 +98,8 @@ private boolean isSortItemsNotDefined(CumulativeWindowFrame frame) { return frame.getWindowDefinition().getSortList().isEmpty(); } - private List resolve(WindowFrame frame, List expressions, int index) { - BindingTuple valueEnv = frame.get(index).bindingTuples(); + private List resolve(WindowFrame frame, List expressions, ExprValue row) { + BindingTuple valueEnv = row.bindingTuples(); return expressions.stream() .map(expr -> expr.valueOf(valueEnv)) .collect(Collectors.toList()); diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RowNumberFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RowNumberFunction.java index e11d071ffc..e4ea747606 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RowNumberFunction.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RowNumberFunction.java @@ -17,7 +17,7 @@ package com.amazon.opendistroforelasticsearch.sql.expression.window.ranking; import com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionName; -import com.amazon.opendistroforelasticsearch.sql.expression.window.CumulativeWindowFrame; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CumulativeWindowFrame; /** * Row number window function that assigns row number starting from 1 to each row in a partition. diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java index 92730d95b3..5dd76e3da1 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java @@ -19,8 +19,8 @@ import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; import com.amazon.opendistroforelasticsearch.sql.expression.Expression; -import com.amazon.opendistroforelasticsearch.sql.expression.window.CumulativeWindowFrame; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; +import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowFunctionExpression; import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.WindowFrame; import com.google.common.collect.ImmutableMap; import java.util.Collections; @@ -90,7 +90,7 @@ public ExprValue next() { * 2. Aggregate window functions: operates on cumulative or sliding window based on definition. */ private WindowFrame createWindowFrame() { - return new CumulativeWindowFrame(windowDefinition); + return ((WindowFunctionExpression) windowFunction).createWindowFrame(windowDefinition); } /** @@ -98,7 +98,7 @@ private WindowFrame createWindowFrame() { * should be based on window frame type. */ private void loadRowsIntoWindowFrame() { - windowFrame.add((ExprTupleValue) input.next()); + windowFrame.load(input); } private ExprValue enrichCurrentRowByWindowFunctionResult() { @@ -109,7 +109,7 @@ private ExprValue enrichCurrentRowByWindowFunctionResult() { } private void preserveAllOriginalColumns(ImmutableMap.Builder mapBuilder) { - ExprTupleValue inputValue = windowFrame.get(windowFrame.currentIndex()); + ExprValue inputValue = windowFrame.current(); inputValue.tupleValue().forEach(mapBuilder::put); } diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CumulativeWindowFrameTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CumulativeWindowFrameTest.java index 62659710ae..2dcbd195bf 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CumulativeWindowFrameTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CumulativeWindowFrameTest.java @@ -21,61 +21,80 @@ import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRING; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprIntegerValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprStringValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; import com.amazon.opendistroforelasticsearch.sql.expression.DSL; -import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.WindowFrame; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CumulativeWindowFrame; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterators; +import java.util.Iterator; import org.apache.commons.lang3.tuple.ImmutablePair; import org.junit.jupiter.api.Test; class CumulativeWindowFrameTest { - private final WindowDefinition windowDefinition = new WindowDefinition( - ImmutableList.of(DSL.ref("state", STRING)), - ImmutableList.of(ImmutablePair.of(DEFAULT_ASC, DSL.ref("age", INTEGER)))); - - private final WindowFrame windowFrame = new CumulativeWindowFrame(windowDefinition); + private final CumulativeWindowFrame windowFrame = new CumulativeWindowFrame( + new WindowDefinition( + ImmutableList.of(DSL.ref("state", STRING)), + ImmutableList.of(ImmutablePair.of(DEFAULT_ASC, DSL.ref("age", INTEGER))))); @Test void should_return_new_partition_if_partition_by_field_value_changed() { - ExprTupleValue tuple1 = ExprTupleValue.fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), - "age", new ExprIntegerValue(20))); - windowFrame.add(tuple1); + Iterator iterator = Iterators.forArray( + ExprTupleValue.fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), + "age", new ExprIntegerValue(20))), + ExprTupleValue.fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), + "age", new ExprIntegerValue(30))), + ExprTupleValue.fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("CA"), + "age", new ExprIntegerValue(18)))); + + windowFrame.load(iterator); assertTrue(windowFrame.isNewPartition()); - ExprTupleValue tuple2 = ExprTupleValue.fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), - "age", new ExprIntegerValue(30))); - windowFrame.add(tuple2); + windowFrame.load(iterator); assertFalse(windowFrame.isNewPartition()); - ExprTupleValue tuple3 = ExprTupleValue.fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("CA"), - "age", new ExprIntegerValue(18))); - windowFrame.add(tuple3); + windowFrame.load(iterator); assertTrue(windowFrame.isNewPartition()); } @Test void can_resolve_single_expression_value() { - windowFrame.add(ExprTupleValue.fromExprValueMap(ImmutableMap.of( + windowFrame.load(Iterators.singletonIterator( + ExprTupleValue.fromExprValueMap(ImmutableMap.of( "state", new ExprStringValue("WA"), - "age", new ExprIntegerValue(20)))); + "age", new ExprIntegerValue(20))))); assertEquals( new ExprIntegerValue(20), windowFrame.resolve(DSL.ref("age", INTEGER))); } @Test - void should_throw_exception_if_access_row_out_of_boundary() { - assertThrows(IndexOutOfBoundsException.class, () -> windowFrame.get(2)); + void can_return_previous_and_current_row() { + ExprValue row1 = ExprTupleValue.fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), + "age", new ExprIntegerValue(20))); + ExprValue row2 = ExprTupleValue.fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), + "age", new ExprIntegerValue(30))); + Iterator iterator = Iterators.forArray(row1, row2); + + windowFrame.load(iterator); + assertNull(windowFrame.previous()); + assertEquals(row1, windowFrame.current()); + + windowFrame.load(iterator); + assertEquals(row1, windowFrame.previous()); + assertEquals(row2, windowFrame.current()); } } \ No newline at end of file diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunctionTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunctionTest.java index ae89017ce3..1bd3099a54 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunctionTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunctionTest.java @@ -16,29 +16,23 @@ package com.amazon.opendistroforelasticsearch.sql.expression.window.aggregation; -import static com.amazon.opendistroforelasticsearch.sql.ast.tree.Sort.SortOption.DEFAULT_ASC; import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue.fromExprValueMap; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.LONG; -import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRING; -import static com.amazon.opendistroforelasticsearch.sql.expression.DSL.ref; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprIntegerValue; -import com.amazon.opendistroforelasticsearch.sql.data.model.ExprStringValue; -import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils; -import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import com.amazon.opendistroforelasticsearch.sql.expression.DSL; import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionTestBase; -import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.AggregationState; import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.Aggregator; -import com.amazon.opendistroforelasticsearch.sql.expression.window.CumulativeWindowFrame; -import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; +import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.WindowFrame; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -68,110 +62,22 @@ void test_delegated_methods() { } @Test - void test_count() { - windowFunction(dsl.count(ref("age", INTEGER))) - .add("WA", 20) - .assertValue(1) - .add("WA", 25) - .assertValue(2) - .add("WA", 30) - .assertValue(3) - .add("CA", 15) - .assertValue(1) - .add("CA", 20) - .assertValue(2); - } - - @Test - void test_sum() { - windowFunction(dsl.sum(ref("age", INTEGER))) - .add("WA", 20) - .assertValue(20) - .add("WA", 25) - .assertValue(45) - .add("WA", 30) - .assertValue(75) - .add("CA", 15) - .assertValue(15) - .add("CA", 20) - .assertValue(35); - } - - @Test - void test_avg() { - windowFunction(dsl.avg(ref("age", INTEGER))) - .add("WA", 20) - .assertValue(20.0) - .add("WA", 30) - .assertValue(25.0) - .add("WA", 40) - .assertValue(30.0) - .add("CA", 15) - .assertValue(15.0) - .add("CA", 25) - .assertValue(20.0); - } - - @Test - void test_min() { - windowFunction(dsl.min(ref("age", INTEGER))) - .add("WA", 20) - .assertValue(20) - .add("WA", 25) - .assertValue(20) - .add("WA", 30) - .assertValue(20) - .add("CA", 15) - .assertValue(15) - .add("CA", 20) - .assertValue(15); - } - - @Test - void test_max() { - windowFunction(dsl.max(ref("age", INTEGER))) - .add("WA", 20) - .assertValue(20) - .add("WA", 25) - .assertValue(25) - .add("WA", 30) - .assertValue(30) - .add("CA", 15) - .assertValue(15) - .add("CA", 20) - .assertValue(20); - } - - private static AggregateWindowFunctionAssertion windowFunction( - Aggregator func) { - return new AggregateWindowFunctionAssertion(func); - } - - private static class AggregateWindowFunctionAssertion { - private final CumulativeWindowFrame windowFrame = new CumulativeWindowFrame( - new WindowDefinition( - ImmutableList.of(ref("state", STRING)), - ImmutableList.of(Pair.of(DEFAULT_ASC, ref("age", INTEGER))))); - - private final Expression windowFunction; - - private AggregateWindowFunctionAssertion(Aggregator windowFunction) { - this.windowFunction = new AggregateWindowFunction(windowFunction); - } - - AggregateWindowFunctionAssertion add(String state, int age) { - windowFrame.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue(state), - "age", new ExprIntegerValue(age)))); - return this; - } - - AggregateWindowFunctionAssertion assertValue(Object expected) { - assertEquals( - ExprValueUtils.fromObjectValue(expected), - windowFunction.valueOf(windowFrame)); - return this; - } + void should_accumulate_all_peer_values_and_not_reset_state_if_same_partition() { + WindowFrame windowFrame = mock(WindowFrame.class, + withSettings().extraInterfaces(Environment.class)); + AggregateWindowFunction windowFunction = + new AggregateWindowFunction(dsl.sum(DSL.ref("age", INTEGER))); + + when(windowFrame.isNewPartition()).thenReturn(true); + when(windowFrame.next()).thenReturn(ImmutableList.of( + fromExprValueMap(ImmutableMap.of("age", new ExprIntegerValue(10))), + fromExprValueMap(ImmutableMap.of("age", new ExprIntegerValue(20))))); + assertEquals(new ExprIntegerValue(30), windowFunction.valueOf(windowFrame)); + + when(windowFrame.isNewPartition()).thenReturn(false); + when(windowFrame.next()).thenReturn(ImmutableList.of( + fromExprValueMap(ImmutableMap.of("age", new ExprIntegerValue(30))))); + assertEquals(new ExprIntegerValue(60), windowFunction.valueOf(windowFrame)); } } diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrameTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrameTest.java new file mode 100644 index 0000000000..92dca98987 --- /dev/null +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrameTest.java @@ -0,0 +1,166 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package com.amazon.opendistroforelasticsearch.sql.expression.window.frame; + +import static com.amazon.opendistroforelasticsearch.sql.ast.tree.Sort.SortOption.DEFAULT_ASC; +import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue.fromExprValueMap; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRING; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprIntegerValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprStringValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; +import com.amazon.opendistroforelasticsearch.sql.expression.DSL; +import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.Iterator; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@ExtendWith(MockitoExtension.class) +class PeerWindowFrameTest { + + private final PeerWindowFrame windowFrame = new PeerWindowFrame( + new WindowDefinition( + ImmutableList.of(DSL.ref("state", STRING)), + ImmutableList.of(Pair.of(DEFAULT_ASC, DSL.ref("age", INTEGER))))); + + @Test + void single_row_test() { + Iterator tuples = ImmutableList.of(tuple("WA", 10, 100)).iterator(); + windowFrame.load(tuples); + assertTrue(windowFrame.isNewPartition()); + assertEquals(ImmutableList.of(tuple("WA", 10, 100)), windowFrame.next()); + assertFalse(windowFrame.hasNext()); + } + + @Test + void single_partition_test() { + Iterator tuples = ImmutableList.of( + tuple("WA", 10, 100), + tuple("WA", 20, 200), + tuple("WA", 20, 50) + ).iterator(); + + windowFrame.load(tuples); + assertTrue(windowFrame.isNewPartition()); + assertEquals(ImmutableList.of(tuple("WA", 10, 100)), windowFrame.next()); + + assertFalse(windowFrame.hasNext()); + windowFrame.load(tuples); + assertFalse(windowFrame.isNewPartition()); + assertEquals( + ImmutableList.of(tuple("WA", 20, 200), tuple("WA", 20, 50)), + windowFrame.next()); + + assertTrue(windowFrame.hasNext()); + windowFrame.load(tuples); + assertFalse(windowFrame.isNewPartition()); + assertEquals(ImmutableList.of(), windowFrame.next()); + + assertFalse(windowFrame.hasNext()); + } + + @Test + void two_partitions_test() { + Iterator tuples = ImmutableList.of( + tuple("WA", 10, 100), + tuple("WA", 20, 200), + tuple("WA", 20, 50), + tuple("WA", 35, 150), + tuple("CA", 18, 150), + tuple("CA", 18, 100), + tuple("CA", 30, 200) + ).iterator(); + + // Here we simulate how WindowFrame interacts with WindowOperator which calls load() + // and WindowFunction which calls isNewPartition() and move() + windowFrame.load(tuples); + assertTrue(windowFrame.isNewPartition()); + assertEquals( + ImmutableList.of( + tuple("WA", 10, 100)), + windowFrame.next()); + + assertFalse(windowFrame.hasNext()); + windowFrame.load(tuples); + assertFalse(windowFrame.isNewPartition()); + assertEquals( + ImmutableList.of( + tuple("WA", 20, 200), + tuple("WA", 20, 50)), + windowFrame.next()); + + assertTrue(windowFrame.hasNext()); + windowFrame.load(tuples); + assertFalse(windowFrame.isNewPartition()); + assertEquals( + ImmutableList.of(), + windowFrame.next()); + + assertFalse(windowFrame.hasNext()); + windowFrame.load(tuples); + assertFalse(windowFrame.isNewPartition()); + assertEquals( + ImmutableList.of( + tuple("WA", 35, 150)), + windowFrame.next()); + + assertFalse(windowFrame.hasNext()); + windowFrame.load(tuples); + assertTrue(windowFrame.isNewPartition()); + assertEquals( + ImmutableList.of( + tuple("CA", 18, 150), + tuple("CA", 18, 100)), + windowFrame.next()); + + assertTrue(windowFrame.hasNext()); + windowFrame.load(tuples); + assertFalse(windowFrame.isNewPartition()); + assertEquals( + ImmutableList.of(), + windowFrame.next()); + + assertFalse(windowFrame.hasNext()); + windowFrame.load(tuples); + assertFalse(windowFrame.isNewPartition()); + assertEquals( + ImmutableList.of( + tuple("CA", 30, 200)), + windowFrame.next()); + + assertFalse(windowFrame.hasNext()); + } + + private ExprValue tuple(String state, int age, int balance) { + return fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue(state), + "age", new ExprIntegerValue(age), + "balance", new ExprIntegerValue(balance))); + } + +} diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunctionTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunctionTest.java index ada077ca09..b5b279b21c 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunctionTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunctionTest.java @@ -24,13 +24,17 @@ import com.amazon.opendistroforelasticsearch.sql.data.model.ExprIntegerValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprStringValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; import com.amazon.opendistroforelasticsearch.sql.expression.DSL; import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionTestBase; -import com.amazon.opendistroforelasticsearch.sql.expression.window.CumulativeWindowFrame; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CumulativeWindowFrame; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterators; +import java.util.Iterator; import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -54,12 +58,43 @@ class RankingWindowFunctionTest extends ExpressionTestBase { ImmutableList.of(DSL.ref("state", STRING)), ImmutableList.of())); // No sort items defined + private Iterator iterator1; + private Iterator iterator2; + + @BeforeEach + void set_up() { + iterator1 = Iterators.forArray( + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30))), + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30))), + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(40))), + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("CA"), "age", new ExprIntegerValue(20)))); + + iterator2 = Iterators.forArray( + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30))), + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30))), + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(50))), + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(55))), + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("CA"), "age", new ExprIntegerValue(15)))); + } + @Test void test_value_of() { - windowFrame1.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30)))); + Iterator iterator = Iterators.singletonIterator( + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30)))); RankingWindowFunction rowNumber = dsl.rowNumber(); + + windowFrame1.load(iterator); assertEquals(new ExprIntegerValue(1), rowNumber.valueOf(windowFrame1)); } @@ -67,20 +102,16 @@ void test_value_of() { void test_row_number() { RankingWindowFunction rowNumber = dsl.rowNumber(); - windowFrame1.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30)))); + windowFrame1.load(iterator1); assertEquals(1, rowNumber.rank(windowFrame1)); - windowFrame1.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30)))); + windowFrame1.load(iterator1); assertEquals(2, rowNumber.rank(windowFrame1)); - windowFrame1.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(40)))); + windowFrame1.load(iterator1); assertEquals(3, rowNumber.rank(windowFrame1)); - windowFrame1.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("CA"), "age", new ExprIntegerValue(20)))); + windowFrame1.load(iterator1); assertEquals(1, rowNumber.rank(windowFrame1)); } @@ -88,24 +119,19 @@ void test_row_number() { void test_rank() { RankingWindowFunction rank = dsl.rank(); - windowFrame1.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30)))); + windowFrame1.load(iterator2); assertEquals(1, rank.rank(windowFrame1)); - windowFrame1.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30)))); + windowFrame1.load(iterator2); assertEquals(1, rank.rank(windowFrame1)); - windowFrame1.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(50)))); + windowFrame1.load(iterator2); assertEquals(3, rank.rank(windowFrame1)); - windowFrame1.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(55)))); + windowFrame1.load(iterator2); assertEquals(4, rank.rank(windowFrame1)); - windowFrame1.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("CA"), "age", new ExprIntegerValue(15)))); + windowFrame1.load(iterator2); assertEquals(1, rank.rank(windowFrame1)); } @@ -113,24 +139,19 @@ void test_rank() { void test_dense_rank() { RankingWindowFunction denseRank = dsl.denseRank(); - windowFrame1.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30)))); + windowFrame1.load(iterator2); assertEquals(1, denseRank.rank(windowFrame1)); - windowFrame1.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30)))); + windowFrame1.load(iterator2); assertEquals(1, denseRank.rank(windowFrame1)); - windowFrame1.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(50)))); + windowFrame1.load(iterator2); assertEquals(2, denseRank.rank(windowFrame1)); - windowFrame1.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(55)))); + windowFrame1.load(iterator2); assertEquals(3, denseRank.rank(windowFrame1)); - windowFrame1.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("CA"), "age", new ExprIntegerValue(15)))); + windowFrame1.load(iterator2); assertEquals(1, denseRank.rank(windowFrame1)); } @@ -138,45 +159,48 @@ void test_dense_rank() { void row_number_should_work_if_no_sort_items_defined() { RankingWindowFunction rowNumber = dsl.rowNumber(); - windowFrame2.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30)))); + windowFrame2.load(iterator1); assertEquals(1, rowNumber.rank(windowFrame2)); - windowFrame2.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30)))); + windowFrame2.load(iterator1); assertEquals(2, rowNumber.rank(windowFrame2)); - windowFrame2.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(40)))); + windowFrame2.load(iterator1); assertEquals(3, rowNumber.rank(windowFrame2)); - windowFrame2.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("CA"), "age", new ExprIntegerValue(20)))); + windowFrame2.load(iterator1); assertEquals(1, rowNumber.rank(windowFrame2)); } @Test void rank_should_always_return_1_if_no_sort_items_defined() { + Iterator iterator = Iterators.forArray( + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30))), + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30))), + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(50))), + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(55))), + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("CA"), "age", new ExprIntegerValue(15)))); + RankingWindowFunction rank = dsl.rank(); - windowFrame2.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30)))); + windowFrame2.load(iterator); assertEquals(1, rank.rank(windowFrame2)); - windowFrame2.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30)))); + windowFrame2.load(iterator); assertEquals(1, rank.rank(windowFrame2)); - windowFrame2.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(50)))); + windowFrame2.load(iterator); assertEquals(1, rank.rank(windowFrame2)); - windowFrame2.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(55)))); + windowFrame2.load(iterator); assertEquals(1, rank.rank(windowFrame2)); - windowFrame2.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("CA"), "age", new ExprIntegerValue(15)))); + windowFrame2.load(iterator); assertEquals(1, rank.rank(windowFrame2)); } @@ -184,24 +208,19 @@ void rank_should_always_return_1_if_no_sort_items_defined() { void dense_rank_should_always_return_1_if_no_sort_items_defined() { RankingWindowFunction denseRank = dsl.denseRank(); - windowFrame2.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30)))); + windowFrame2.load(iterator2); assertEquals(1, denseRank.rank(windowFrame2)); - windowFrame2.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30)))); + windowFrame2.load(iterator2); assertEquals(1, denseRank.rank(windowFrame2)); - windowFrame2.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(50)))); + windowFrame2.load(iterator2); assertEquals(1, denseRank.rank(windowFrame2)); - windowFrame2.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(55)))); + windowFrame2.load(iterator2); assertEquals(1, denseRank.rank(windowFrame2)); - windowFrame2.add(fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("CA"), "age", new ExprIntegerValue(15)))); + windowFrame2.load(iterator2); assertEquals(1, denseRank.rank(windowFrame2)); } From b67036fcf6996628ae734d6eb6aacfa3deb620fc Mon Sep 17 00:00:00 2001 From: Dai Date: Wed, 16 Dec 2020 15:15:15 -0800 Subject: [PATCH 05/16] Name window function and optimize it when analysis --- .../sql/analysis/ExpressionAnalyzer.java | 1 + .../ExpressionReferenceOptimizer.java | 11 +++++++- .../analysis/SelectExpressionAnalyzer.java | 10 ++++++- .../analysis/WindowExpressionAnalyzer.java | 17 ++++++------ .../window/WindowFunctionExpression.java | 5 ++++ .../sql/planner/logical/LogicalPlanDSL.java | 2 +- .../sql/planner/logical/LogicalWindow.java | 6 ++--- .../sql/planner/physical/PhysicalPlanDSL.java | 2 +- .../sql/planner/physical/WindowOperator.java | 26 +++++-------------- .../sql/analysis/AnalyzerTest.java | 6 +++-- .../ExpressionReferenceOptimizerTest.java | 6 ++--- .../SelectExpressionAnalyzerTest.java | 3 ++- .../WindowExpressionAnalyzerTest.java | 2 +- .../sql/executor/ExplainTest.java | 2 +- .../sql/planner/DefaultImplementorTest.java | 2 +- .../logical/LogicalPlanNodeVisitorTest.java | 2 +- .../physical/PhysicalPlanNodeVisitorTest.java | 2 +- .../planner/physical/WindowOperatorTest.java | 13 +++++++--- .../ElasticsearchExecutionProtectorTest.java | 2 +- 19 files changed, 70 insertions(+), 50 deletions(-) diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java index 79be58a025..fe8f904f70 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java @@ -52,6 +52,7 @@ import com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionRepository; import com.amazon.opendistroforelasticsearch.sql.expression.function.FunctionName; import com.amazon.opendistroforelasticsearch.sql.expression.window.aggregation.AggregateWindowFunction; +import com.amazon.opendistroforelasticsearch.sql.expression.window.ranking.RankingWindowFunction; import com.google.common.collect.ImmutableSet; import java.util.ArrayList; import java.util.Arrays; diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionReferenceOptimizer.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionReferenceOptimizer.java index b999f50f15..7cf1b483bf 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionReferenceOptimizer.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionReferenceOptimizer.java @@ -20,6 +20,7 @@ import com.amazon.opendistroforelasticsearch.sql.expression.Expression; import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionNodeVisitor; import com.amazon.opendistroforelasticsearch.sql.expression.FunctionExpression; +import com.amazon.opendistroforelasticsearch.sql.expression.NamedExpression; import com.amazon.opendistroforelasticsearch.sql.expression.ReferenceExpression; import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.Aggregator; import com.amazon.opendistroforelasticsearch.sql.expression.conditional.cases.CaseClause; @@ -89,6 +90,14 @@ public Expression visitAggregator(Aggregator node, AnalysisContext context) { return expressionMap.getOrDefault(node, node); } + @Override + public Expression visitNamed(NamedExpression node, AnalysisContext context) { + if (expressionMap.containsKey(node)) { + return expressionMap.get(node); + } + return node.getDelegated().accept(this, context); + } + /** * Implement this because Case/When is not registered in function repository. */ @@ -144,7 +153,7 @@ public Void visitAggregation(LogicalAggregation plan, Void context) { public Void visitWindow(LogicalWindow plan, Void context) { Expression windowFunc = plan.getWindowFunction(); expressionMap.put(windowFunc, - new ReferenceExpression(windowFunc.toString(), windowFunc.type())); + new ReferenceExpression(((NamedExpression) windowFunc).getName(), windowFunc.type())); return visitNode(plan, context); } } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/SelectExpressionAnalyzer.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/SelectExpressionAnalyzer.java index 8a07d847b8..a949e6fcf3 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/SelectExpressionAnalyzer.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/SelectExpressionAnalyzer.java @@ -91,7 +91,15 @@ public List visitAlias(Alias node, AnalysisContext context) { private Expression referenceIfSymbolDefined(Alias expr, AnalysisContext context) { UnresolvedExpression delegatedExpr = expr.getDelegated(); - return optimizer.optimize(delegatedExpr.accept(expressionAnalyzer, context), context); + + // Pass named expression because expression like window function loses full name + // (OVER clause) and thus depends on name in alias to be replaced correctly + return optimizer.optimize( + DSL.named( + expr.getName(), + delegatedExpr.accept(expressionAnalyzer, context), + expr.getAlias()), + context); } @Override diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/WindowExpressionAnalyzer.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/WindowExpressionAnalyzer.java index 67440045ca..26e8bd2fd2 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/WindowExpressionAnalyzer.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/WindowExpressionAnalyzer.java @@ -25,6 +25,7 @@ import com.amazon.opendistroforelasticsearch.sql.ast.expression.WindowFunction; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Sort.SortOption; import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import com.amazon.opendistroforelasticsearch.sql.expression.NamedExpression; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlan; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalSort; @@ -66,19 +67,19 @@ public LogicalPlan analyze(UnresolvedExpression projectItem, AnalysisContext con @Override public LogicalPlan visitAlias(Alias node, AnalysisContext context) { - return node.getDelegated().accept(this, context); - } + if (!(node.getDelegated() instanceof WindowFunction)) { + return null; + } - @Override - public LogicalPlan visitWindowFunction(WindowFunction node, AnalysisContext context) { - Expression windowFunction = expressionAnalyzer.analyze(node, context); - List partitionByList = analyzePartitionList(node, context); - List> sortList = analyzeSortList(node, context); + WindowFunction unresolved = (WindowFunction) node.getDelegated(); + Expression windowFunction = expressionAnalyzer.analyze(unresolved, context); + List partitionByList = analyzePartitionList(unresolved, context); + List> sortList = analyzeSortList(unresolved, context); WindowDefinition windowDefinition = new WindowDefinition(partitionByList, sortList); return new LogicalWindow( new LogicalSort(child,windowDefinition.getAllSortItems()), - windowFunction, + new NamedExpression(node.getName(), windowFunction, node.getAlias()), windowDefinition); } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/WindowFunctionExpression.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/WindowFunctionExpression.java index 163a82d934..f22dcd9ba5 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/WindowFunctionExpression.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/WindowFunctionExpression.java @@ -26,6 +26,11 @@ public interface WindowFunctionExpression extends Expression { /** * Create specific window frame based on window definition and what's current window function. + * For now two types of cumulative window frame is returned: + * 1. Ranking window functions: ignore frame definition and always operates on + * previous and current row. + * 2. Aggregate window functions: frame partition into peers and sliding window is not supported. + * * @param definition window definition * @return window frame */ diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalPlanDSL.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalPlanDSL.java index 9f2cd274f2..f3be1955b8 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalPlanDSL.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalPlanDSL.java @@ -59,7 +59,7 @@ public static LogicalPlan project(LogicalPlan input, NamedExpression... fields) } public LogicalPlan window(LogicalPlan input, - Expression windowFunction, + NamedExpression windowFunction, WindowDefinition windowDefinition) { return new LogicalWindow(input, windowFunction, windowDefinition); } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalWindow.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalWindow.java index 664f12686d..aa7a04c7c4 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalWindow.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalWindow.java @@ -16,7 +16,7 @@ package com.amazon.opendistroforelasticsearch.sql.planner.logical; -import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import com.amazon.opendistroforelasticsearch.sql.expression.NamedExpression; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; import java.util.Collections; import lombok.EqualsAndHashCode; @@ -32,7 +32,7 @@ @Getter @ToString public class LogicalWindow extends LogicalPlan { - private final Expression windowFunction; + private final NamedExpression windowFunction; private final WindowDefinition windowDefinition; /** @@ -40,7 +40,7 @@ public class LogicalWindow extends LogicalPlan { */ public LogicalWindow( LogicalPlan child, - Expression windowFunction, + NamedExpression windowFunction, WindowDefinition windowDefinition) { super(Collections.singletonList(child)); this.windowFunction = windowFunction; diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/PhysicalPlanDSL.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/PhysicalPlanDSL.java index eb5442b5f9..f0a0a5be8b 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/PhysicalPlanDSL.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/PhysicalPlanDSL.java @@ -83,7 +83,7 @@ public static DedupeOperator dedupe( } public WindowOperator window(PhysicalPlan input, - Expression windowFunction, + NamedExpression windowFunction, WindowDefinition windowDefinition) { return new WindowOperator(input, windowFunction, windowDefinition); } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java index 5dd76e3da1..32c4c1e3af 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java @@ -18,7 +18,7 @@ import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; -import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import com.amazon.opendistroforelasticsearch.sql.expression.NamedExpression; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowFunctionExpression; import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.WindowFrame; @@ -39,7 +39,7 @@ public class WindowOperator extends PhysicalPlan { private final PhysicalPlan input; @Getter - private final Expression windowFunction; + private final NamedExpression windowFunction; @Getter private final WindowDefinition windowDefinition; @@ -55,7 +55,7 @@ public class WindowOperator extends PhysicalPlan { * @param windowDefinition window definition */ public WindowOperator(PhysicalPlan input, - Expression windowFunction, + NamedExpression windowFunction, WindowDefinition windowDefinition) { this.input = input; this.windowFunction = windowFunction; @@ -80,25 +80,13 @@ public boolean hasNext() { @Override public ExprValue next() { - loadRowsIntoWindowFrame(); + windowFrame.load(input); return enrichCurrentRowByWindowFunctionResult(); } - /** - * For now cumulative window frame is returned always. When frame definition is supported: - * 1. Ranking window functions: ignore frame definition and always operates on entire window. - * 2. Aggregate window functions: operates on cumulative or sliding window based on definition. - */ private WindowFrame createWindowFrame() { - return ((WindowFunctionExpression) windowFunction).createWindowFrame(windowDefinition); - } - - /** - * For now always load next row into window frame. In future, how/how many rows loaded - * should be based on window frame type. - */ - private void loadRowsIntoWindowFrame() { - windowFrame.load(input); + return ((WindowFunctionExpression) windowFunction.getDelegated()) + .createWindowFrame(windowDefinition); } private ExprValue enrichCurrentRowByWindowFunctionResult() { @@ -115,7 +103,7 @@ private void preserveAllOriginalColumns(ImmutableMap.Builder private void addWindowFunctionResultColumn(ImmutableMap.Builder mapBuilder) { ExprValue exprValue = windowFunction.valueOf(windowFrame); - mapBuilder.put(windowFunction.toString(), exprValue); + mapBuilder.put(windowFunction.getName(), exprValue); } } diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/AnalyzerTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/AnalyzerTest.java index 8b0f3cebf5..f3888036d1 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/AnalyzerTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/AnalyzerTest.java @@ -370,13 +370,15 @@ public void window_function() { LogicalPlanDSL.relation("test"), ImmutablePair.of(DEFAULT_ASC, DSL.ref("string_value", STRING)), ImmutablePair.of(DEFAULT_ASC, DSL.ref("integer_value", INTEGER))), - dsl.rowNumber(), + DSL.named("window_function", dsl.rowNumber()), new WindowDefinition( ImmutableList.of(DSL.ref("string_value", STRING)), ImmutableList.of( ImmutablePair.of(DEFAULT_ASC, DSL.ref("integer_value", INTEGER))))), DSL.named("string_value", DSL.ref("string_value", STRING)), - DSL.named("window_function", DSL.ref("row_number()", INTEGER))), + // Alias name "window_function" is used as internal symbol name to connect + // project item and window operator output + DSL.named("window_function", DSL.ref("window_function", INTEGER))), AstDSL.project( AstDSL.relation("test"), AstDSL.alias("string_value", AstDSL.qualifiedName("string_value")), diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionReferenceOptimizerTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionReferenceOptimizerTest.java index 7b39cf82f6..902408043d 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionReferenceOptimizerTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionReferenceOptimizerTest.java @@ -149,9 +149,9 @@ void window_expression_should_be_replaced() { LogicalPlanDSL.window( LogicalPlanDSL.window( LogicalPlanDSL.relation("test"), - dsl.rank(), + DSL.named(dsl.rank()), new WindowDefinition(emptyList(), emptyList())), - dsl.denseRank(), + DSL.named(dsl.denseRank()), new WindowDefinition(emptyList(), emptyList())); assertEquals( @@ -169,7 +169,7 @@ Expression optimize(Expression expression) { Expression optimize(Expression expression, LogicalPlan logicalPlan) { final ExpressionReferenceOptimizer optimizer = new ExpressionReferenceOptimizer(functionRepository, logicalPlan); - return optimizer.optimize(expression, new AnalysisContext()); + return optimizer.optimize(DSL.named(expression), new AnalysisContext()); } LogicalPlan logicalPlan() { diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/SelectExpressionAnalyzerTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/SelectExpressionAnalyzerTest.java index f0fe2db2a5..08558520f0 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/SelectExpressionAnalyzerTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/SelectExpressionAnalyzerTest.java @@ -112,7 +112,8 @@ public void field_name_in_expression_with_qualifier() { } protected List analyze(UnresolvedExpression unresolvedExpression) { - doAnswer(returnsFirstArg()).when(optimizer).optimize(any(), any()); + doAnswer(invocation -> ((NamedExpression) invocation.getArgument(0)) + .getDelegated()).when(optimizer).optimize(any(), any()); return new SelectExpressionAnalyzer(expressionAnalyzer) .analyze(Arrays.asList(unresolvedExpression), analysisContext, optimizer); diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/WindowExpressionAnalyzerTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/WindowExpressionAnalyzerTest.java index a13be922ce..f2d665804a 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/WindowExpressionAnalyzerTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/WindowExpressionAnalyzerTest.java @@ -64,7 +64,7 @@ void should_wrap_child_with_window_and_sort_operator_if_project_item_windowed() LogicalPlanDSL.relation("test"), ImmutablePair.of(DEFAULT_ASC, DSL.ref("string_value", STRING)), ImmutablePair.of(DEFAULT_DESC, DSL.ref("integer_value", INTEGER))), - dsl.rowNumber(), + DSL.named("row_number", dsl.rowNumber()), new WindowDefinition( ImmutableList.of(DSL.ref("string_value", STRING)), ImmutableList.of( diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/executor/ExplainTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/executor/ExplainTest.java index dd63d53e69..3314feae18 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/executor/ExplainTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/executor/ExplainTest.java @@ -168,7 +168,7 @@ void can_explain_window() { List> sortList = ImmutableList.of( ImmutablePair.of(DEFAULT_ASC, ref("age", INTEGER))); - PhysicalPlan plan = window(tableScan, dsl.rank(), + PhysicalPlan plan = window(tableScan, named(dsl.rank()), new WindowDefinition(partitionByList, sortList)); assertEquals( diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/DefaultImplementorTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/DefaultImplementorTest.java index 7fdcc6f3e4..04ee791325 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/DefaultImplementorTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/DefaultImplementorTest.java @@ -178,7 +178,7 @@ public void visitRelationShouldThrowException() { @SuppressWarnings({"rawtypes", "unchecked"}) @Test public void visitWindowOperatorShouldReturnPhysicalWindowOperator() { - Expression windowFunction = new RowNumberFunction(); + NamedExpression windowFunction = named(new RowNumberFunction()); WindowDefinition windowDefinition = new WindowDefinition( Collections.singletonList(ref("state", STRING)), Collections.singletonList( diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalPlanNodeVisitorTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalPlanNodeVisitorTest.java index e7a7ed590e..9b9863a5cc 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalPlanNodeVisitorTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalPlanNodeVisitorTest.java @@ -114,7 +114,7 @@ public void testAbstractPlanNodeVisitorShouldReturnNull() { assertNull(dedup.accept(new LogicalPlanNodeVisitor() { }, null)); - LogicalPlan window = LogicalPlanDSL.window(relation, expression, new WindowDefinition( + LogicalPlan window = LogicalPlanDSL.window(relation, named(expression), new WindowDefinition( ImmutableList.of(ref), ImmutableList.of(Pair.of(SortOption.DEFAULT_ASC, expression)))); assertNull(window.accept(new LogicalPlanNodeVisitor() { }, null)); diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/PhysicalPlanNodeVisitorTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/PhysicalPlanNodeVisitorTest.java index 34399abaf2..f97c551afe 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/PhysicalPlanNodeVisitorTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/PhysicalPlanNodeVisitorTest.java @@ -118,7 +118,7 @@ public void test_PhysicalPlanVisitor_should_return_null() { assertNull(project.accept(new PhysicalPlanNodeVisitor() { }, null)); - PhysicalPlan window = PhysicalPlanDSL.window(plan, dsl.rowNumber(), + PhysicalPlan window = PhysicalPlanDSL.window(plan, named(dsl.rowNumber()), new WindowDefinition(emptyList(), emptyList())); assertNull(window.accept(new PhysicalPlanNodeVisitor() { }, null)); diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperatorTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperatorTest.java index 9063b0d1e2..9385495662 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperatorTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperatorTest.java @@ -26,8 +26,9 @@ import com.amazon.opendistroforelasticsearch.sql.ast.tree.Sort.SortOption; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils; +import com.amazon.opendistroforelasticsearch.sql.expression.DSL; import com.amazon.opendistroforelasticsearch.sql.expression.Expression; -import com.amazon.opendistroforelasticsearch.sql.expression.FunctionExpression; +import com.amazon.opendistroforelasticsearch.sql.expression.NamedExpression; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; import com.google.common.collect.ImmutableMap; import java.util.ArrayList; @@ -47,7 +48,7 @@ class WindowOperatorTest extends PhysicalPlanTestBase { @Test - void test() { + void test_ranking_window_function() { window(dsl.rank()) .partitionBy(ref("action", STRING)) .sortBy(DEFAULT_ASC, ref("response", INTEGER)) @@ -69,18 +70,22 @@ void test() { .done(); } - private WindowOperatorAssertion window(FunctionExpression windowFunction) { + private WindowOperatorAssertion window(Expression windowFunction) { return new WindowOperatorAssertion(windowFunction); } @RequiredArgsConstructor private static class WindowOperatorAssertion { - private final Expression windowFunction; + private final NamedExpression windowFunction; private final List partitionByList = new ArrayList<>(); private final List> sortList = new ArrayList<>(); private WindowOperator windowOperator; + private WindowOperatorAssertion(Expression windowFunction) { + this.windowFunction = DSL.named(windowFunction); + } + WindowOperatorAssertion partitionBy(Expression expr) { partitionByList.add(expr); return this; diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/executor/ElasticsearchExecutionProtectorTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/executor/ElasticsearchExecutionProtectorTest.java index 62658841f0..03af6f5641 100644 --- a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/executor/ElasticsearchExecutionProtectorTest.java +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/executor/ElasticsearchExecutionProtectorTest.java @@ -189,7 +189,7 @@ public void testProtectIndexScan() { @SuppressWarnings("unchecked") @Test public void testProtectSortForWindowOperator() { - Expression rank = mock(RankFunction.class); + NamedExpression rank = named(mock(RankFunction.class)); Pair sortItem = ImmutablePair.of(DEFAULT_ASC, DSL.ref("age", INTEGER)); WindowDefinition windowDefinition = From 4e7c0ceeeb930f1c0e4b5f54a5cdf6395e0d8c95 Mon Sep 17 00:00:00 2001 From: Dai Date: Wed, 16 Dec 2020 17:56:02 -0800 Subject: [PATCH 06/16] Refactor peer frame by peeking iterator --- .../window/frame/CumulativeWindowFrame.java | 4 +- .../window/frame/PeerWindowFrame.java | 76 ++++++++++--------- .../expression/window/frame/WindowFrame.java | 3 +- .../sql/planner/physical/WindowOperator.java | 13 +++- .../window/CumulativeWindowFrameTest.java | 34 +++++---- .../window/frame/PeerWindowFrameTest.java | 76 ++++++++++++++----- .../ranking/RankingWindowFunctionTest.java | 44 ++++++----- .../planner/physical/WindowOperatorTest.java | 25 ++++++ 8 files changed, 176 insertions(+), 99 deletions(-) diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CumulativeWindowFrame.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CumulativeWindowFrame.java index 5f4ae51b1d..0904238fd3 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CumulativeWindowFrame.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CumulativeWindowFrame.java @@ -21,7 +21,7 @@ import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; import com.google.common.collect.ImmutableList; -import java.util.Iterator; +import com.google.common.collect.PeekingIterator; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -62,7 +62,7 @@ public boolean isNewPartition() { } @Override - public void load(Iterator it) { + public void load(PeekingIterator it) { previous = current; current = it.next(); } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrame.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrame.java index 4791f02534..a25df917ae 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrame.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrame.java @@ -20,9 +20,9 @@ import com.amazon.opendistroforelasticsearch.sql.expression.Expression; import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; +import com.google.common.collect.PeekingIterator; import java.util.ArrayList; import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -37,8 +37,8 @@ public class PeerWindowFrame implements WindowFrame { private final WindowDefinition windowDefinition; - private List peers = new ArrayList<>(); - private ExprValue next; + private final List peers = new ArrayList<>(); + private int current; private boolean isNewPartition = true; @@ -49,25 +49,30 @@ public boolean hasNext() { } @Override - public void load(Iterator it) { + public void load(PeekingIterator it) { if (hasNext()) { return; } - if (next != null) { - isNewPartition = !isSamePartition(peers.get(peers.size() - 1), next); - peers.clear(); - peers.add(next); - } - - // load until next peer or partition - ExprValue cur = null; - while (it.hasNext() && isSamePartitionAndSortValues(cur = it.next())) { - peers.add(cur); - } - + // Reset state current = 0; - next = cur; + isNewPartition = it.hasNext() && !isSamePartition(it.peek()); + peers.clear(); + + // Load until: + // 1) sort key different (different peer); + // 2) or new partition + // 3) or no more data + while (it.hasNext()) { + ExprValue next = it.peek(); + if (peers.isEmpty()) { + peers.add(it.next()); + } else if (isSamePartition(next) && isPeer(next)) { + peers.add(it.next()); + } else { + break; + } + } } @Override @@ -89,33 +94,32 @@ public ExprValue current() { return peers.get(current); } - private List resolve(List expressions, ExprValue row) { - Environment valueEnv = row.bindingTuples(); - return expressions.stream() - .map(expr -> expr.valueOf(valueEnv)) - .collect(Collectors.toList()); - } + private boolean isPeer(ExprValue next) { + List sortFields = + windowDefinition.getSortList() + .stream() + .map(Pair::getRight) + .collect(Collectors.toList()); - private List getSortFields() { - return windowDefinition.getSortList() - .stream() - .map(Pair::getRight) - .collect(Collectors.toList()); + ExprValue cur = peers.get(peers.size() - 1); + return resolve(sortFields, cur).equals(resolve(sortFields, next)); } - private boolean isSamePartitionAndSortValues(ExprValue cur) { + private boolean isSamePartition(ExprValue next) { if (peers.isEmpty()) { - return true; + return false; } - ExprValue prev = peers.get(peers.size() - 1); - return isSamePartition(cur, prev) - && resolve(getSortFields(), prev).equals(resolve(getSortFields(), cur)); + List partitionByList = windowDefinition.getPartitionByList(); + ExprValue cur = peers.get(peers.size() - 1); + return resolve(partitionByList, cur).equals(resolve(partitionByList, next)); } - private boolean isSamePartition(ExprValue cur, ExprValue prev) { - return resolve(windowDefinition.getPartitionByList(), prev) - .equals(resolve(windowDefinition.getPartitionByList(), cur)); + private List resolve(List expressions, ExprValue row) { + Environment valueEnv = row.bindingTuples(); + return expressions.stream() + .map(expr -> expr.valueOf(valueEnv)) + .collect(Collectors.toList()); } } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java index 81808a43b6..d36699e193 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java @@ -19,6 +19,7 @@ import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; import com.amazon.opendistroforelasticsearch.sql.expression.Expression; import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; +import com.google.common.collect.PeekingIterator; import java.util.Iterator; import java.util.List; @@ -49,7 +50,7 @@ default ExprValue resolve(Expression var) { * Load any number of rows as needed. * @param iterator row iterator */ - void load(Iterator iterator); + void load(PeekingIterator iterator); /** * Get current data row. diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java index 32c4c1e3af..3a1666c0e6 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java @@ -23,6 +23,8 @@ import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowFunctionExpression; import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.WindowFrame; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterators; +import com.google.common.collect.PeekingIterator; import java.util.Collections; import java.util.List; import lombok.EqualsAndHashCode; @@ -48,6 +50,12 @@ public class WindowOperator extends PhysicalPlan { @ToString.Exclude private final WindowFrame windowFrame; + /** + * Peeking iterator that can peek next element which is required + * by window frame such as peer frame. + */ + private final PeekingIterator peekingIterator; + /** * Initialize window operator. * @param input child operator @@ -61,6 +69,7 @@ public WindowOperator(PhysicalPlan input, this.windowFunction = windowFunction; this.windowDefinition = windowDefinition; this.windowFrame = createWindowFrame(); + this.peekingIterator = Iterators.peekingIterator(input); } @Override @@ -75,12 +84,12 @@ public List getChild() { @Override public boolean hasNext() { - return input.hasNext(); + return peekingIterator.hasNext(); } @Override public ExprValue next() { - windowFrame.load(input); + windowFrame.load(peekingIterator); return enrichCurrentRowByWindowFunctionResult(); } diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CumulativeWindowFrameTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CumulativeWindowFrameTest.java index 2dcbd195bf..cfec0812bb 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CumulativeWindowFrameTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CumulativeWindowFrameTest.java @@ -33,7 +33,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterators; -import java.util.Iterator; +import com.google.common.collect.PeekingIterator; import org.apache.commons.lang3.tuple.ImmutablePair; import org.junit.jupiter.api.Test; @@ -46,16 +46,17 @@ class CumulativeWindowFrameTest { @Test void should_return_new_partition_if_partition_by_field_value_changed() { - Iterator iterator = Iterators.forArray( - ExprTupleValue.fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), - "age", new ExprIntegerValue(20))), - ExprTupleValue.fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), - "age", new ExprIntegerValue(30))), - ExprTupleValue.fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("CA"), - "age", new ExprIntegerValue(18)))); + PeekingIterator iterator = Iterators.peekingIterator( + Iterators.forArray( + ExprTupleValue.fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), + "age", new ExprIntegerValue(20))), + ExprTupleValue.fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), + "age", new ExprIntegerValue(30))), + ExprTupleValue.fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("CA"), + "age", new ExprIntegerValue(18))))); windowFrame.load(iterator); assertTrue(windowFrame.isNewPartition()); @@ -69,10 +70,11 @@ void should_return_new_partition_if_partition_by_field_value_changed() { @Test void can_resolve_single_expression_value() { - windowFrame.load(Iterators.singletonIterator( - ExprTupleValue.fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), - "age", new ExprIntegerValue(20))))); + windowFrame.load(Iterators.peekingIterator( + Iterators.singletonIterator( + ExprTupleValue.fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), + "age", new ExprIntegerValue(20)))))); assertEquals( new ExprIntegerValue(20), windowFrame.resolve(DSL.ref("age", INTEGER))); @@ -86,7 +88,7 @@ void can_return_previous_and_current_row() { ExprValue row2 = ExprTupleValue.fromExprValueMap(ImmutableMap.of( "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30))); - Iterator iterator = Iterators.forArray(row1, row2); + PeekingIterator iterator = Iterators.peekingIterator(Iterators.forArray(row1, row2)); windowFrame.load(iterator); assertNull(windowFrame.previous()); diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrameTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrameTest.java index 92dca98987..4834802f0e 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrameTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrameTest.java @@ -31,7 +31,8 @@ import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import java.util.Iterator; +import com.google.common.collect.Iterators; +import com.google.common.collect.PeekingIterator; import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -50,7 +51,8 @@ class PeerWindowFrameTest { @Test void single_row_test() { - Iterator tuples = ImmutableList.of(tuple("WA", 10, 100)).iterator(); + PeekingIterator tuples = Iterators.peekingIterator( + Iterators.singletonIterator(tuple("WA", 10, 100))); windowFrame.load(tuples); assertTrue(windowFrame.isNewPartition()); assertEquals(ImmutableList.of(tuple("WA", 10, 100)), windowFrame.next()); @@ -58,13 +60,15 @@ void single_row_test() { } @Test - void single_partition_test() { - Iterator tuples = ImmutableList.of( - tuple("WA", 10, 100), - tuple("WA", 20, 200), - tuple("WA", 20, 50) - ).iterator(); + void single_partition_test1() { + PeekingIterator tuples = Iterators.peekingIterator( + Iterators.forArray( + tuple("WA", 10, 100), + tuple("WA", 20, 200), + tuple("WA", 20, 50))); + // Here we simulate how WindowFrame interacts with WindowOperator which calls load() + // and WindowFunction which calls isNewPartition() and move() windowFrame.load(tuples); assertTrue(windowFrame.isNewPartition()); assertEquals(ImmutableList.of(tuple("WA", 10, 100)), windowFrame.next()); @@ -85,19 +89,14 @@ void single_partition_test() { } @Test - void two_partitions_test() { - Iterator tuples = ImmutableList.of( - tuple("WA", 10, 100), - tuple("WA", 20, 200), - tuple("WA", 20, 50), - tuple("WA", 35, 150), - tuple("CA", 18, 150), - tuple("CA", 18, 100), - tuple("CA", 30, 200) - ).iterator(); + void single_partition_test2() { + PeekingIterator tuples = Iterators.peekingIterator( + Iterators.forArray( + tuple("WA", 10, 100), + tuple("WA", 20, 200), + tuple("WA", 20, 50), + tuple("WA", 35, 150))); - // Here we simulate how WindowFrame interacts with WindowOperator which calls load() - // and WindowFunction which calls isNewPartition() and move() windowFrame.load(tuples); assertTrue(windowFrame.isNewPartition()); assertEquals( @@ -130,6 +129,24 @@ void two_partitions_test() { windowFrame.next()); assertFalse(windowFrame.hasNext()); + } + + @Test + void two_partitions_test1() { + PeekingIterator tuples = Iterators.peekingIterator( + Iterators.forArray( + tuple("WA", 10, 100), + tuple("CA", 18, 150), + tuple("CA", 18, 100))); + + windowFrame.load(tuples); + assertTrue(windowFrame.isNewPartition()); + assertEquals( + ImmutableList.of( + tuple("WA", 10, 100)), + windowFrame.next()); + + assertFalse(windowFrame.hasNext()); windowFrame.load(tuples); assertTrue(windowFrame.isNewPartition()); assertEquals( @@ -146,8 +163,25 @@ void two_partitions_test() { windowFrame.next()); assertFalse(windowFrame.hasNext()); + } + + @Test + void two_partitions_test2() { + PeekingIterator tuples = Iterators.peekingIterator( + Iterators.forArray( + tuple("WA", 10, 100), + tuple("CA", 30, 200))); + windowFrame.load(tuples); - assertFalse(windowFrame.isNewPartition()); + assertTrue(windowFrame.isNewPartition()); + assertEquals( + ImmutableList.of( + tuple("WA", 10, 100)), + windowFrame.next()); + + assertFalse(windowFrame.hasNext()); + windowFrame.load(tuples); + assertTrue(windowFrame.isNewPartition()); assertEquals( ImmutableList.of( tuple("CA", 30, 200)), diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunctionTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunctionTest.java index b5b279b21c..71afdf94c6 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunctionTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunctionTest.java @@ -32,7 +32,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterators; -import java.util.Iterator; +import com.google.common.collect.PeekingIterator; import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; @@ -58,12 +58,12 @@ class RankingWindowFunctionTest extends ExpressionTestBase { ImmutableList.of(DSL.ref("state", STRING)), ImmutableList.of())); // No sort items defined - private Iterator iterator1; - private Iterator iterator2; + private PeekingIterator iterator1; + private PeekingIterator iterator2; @BeforeEach void set_up() { - iterator1 = Iterators.forArray( + iterator1 = Iterators.peekingIterator(Iterators.forArray( fromExprValueMap(ImmutableMap.of( "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30))), fromExprValueMap(ImmutableMap.of( @@ -71,9 +71,9 @@ void set_up() { fromExprValueMap(ImmutableMap.of( "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(40))), fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("CA"), "age", new ExprIntegerValue(20)))); + "state", new ExprStringValue("CA"), "age", new ExprIntegerValue(20))))); - iterator2 = Iterators.forArray( + iterator2 = Iterators.peekingIterator(Iterators.forArray( fromExprValueMap(ImmutableMap.of( "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30))), fromExprValueMap(ImmutableMap.of( @@ -83,14 +83,15 @@ void set_up() { fromExprValueMap(ImmutableMap.of( "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(55))), fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("CA"), "age", new ExprIntegerValue(15)))); + "state", new ExprStringValue("CA"), "age", new ExprIntegerValue(15))))); } @Test void test_value_of() { - Iterator iterator = Iterators.singletonIterator( - fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30)))); + PeekingIterator iterator = Iterators.peekingIterator( + Iterators.singletonIterator( + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30))))); RankingWindowFunction rowNumber = dsl.rowNumber(); @@ -174,17 +175,18 @@ void row_number_should_work_if_no_sort_items_defined() { @Test void rank_should_always_return_1_if_no_sort_items_defined() { - Iterator iterator = Iterators.forArray( - fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30))), - fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30))), - fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(50))), - fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(55))), - fromExprValueMap(ImmutableMap.of( - "state", new ExprStringValue("CA"), "age", new ExprIntegerValue(15)))); + PeekingIterator iterator = Iterators.peekingIterator( + Iterators.forArray( + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30))), + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(30))), + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(50))), + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("WA"), "age", new ExprIntegerValue(55))), + fromExprValueMap(ImmutableMap.of( + "state", new ExprStringValue("CA"), "age", new ExprIntegerValue(15))))); RankingWindowFunction rank = dsl.rank(); diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperatorTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperatorTest.java index 9385495662..3180d4a396 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperatorTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperatorTest.java @@ -30,6 +30,7 @@ import com.amazon.opendistroforelasticsearch.sql.expression.Expression; import com.amazon.opendistroforelasticsearch.sql.expression.NamedExpression; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; +import com.amazon.opendistroforelasticsearch.sql.expression.window.aggregation.AggregateWindowFunction; import com.google.common.collect.ImmutableMap; import java.util.ArrayList; import java.util.List; @@ -70,6 +71,30 @@ void test_ranking_window_function() { .done(); } + @SuppressWarnings("unchecked") + @Test + void test_aggregate_window_function() { + window(new AggregateWindowFunction(dsl.sum(ref("response", INTEGER)))) + .partitionBy(ref("action", STRING)) + .sortBy(DEFAULT_ASC, ref("response", INTEGER)) + .expectNext(ImmutableMap.of( + "ip", "209.160.24.63", "action", "GET", "response", 200, "referer", "www.amazon.com", + "sum(response)", 400)) + .expectNext(ImmutableMap.of( + "ip", "112.111.162.4", "action", "GET", "response", 200, "referer", "www.amazon.com", + "sum(response)", 400)) + .expectNext(ImmutableMap.of( + "ip", "209.160.24.63", "action", "GET", "response", 404, "referer", "www.amazon.com", + "sum(response)", 804)) + .expectNext(ImmutableMap.of( + "ip", "74.125.19.106", "action", "POST", "response", 200, "referer", "www.google.com", + "sum(response)", 200)) + .expectNext(ImmutableMap.of( + "ip", "74.125.19.106", "action", "POST", "response", 500, + "sum(response)", 700)) + .done(); + } + private WindowOperatorAssertion window(Expression windowFunction) { return new WindowOperatorAssertion(windowFunction); } From b7d6580281e7aabb660a23058f48d5553aae05cd Mon Sep 17 00:00:00 2001 From: Dai Date: Wed, 16 Dec 2020 18:01:42 -0800 Subject: [PATCH 07/16] Comparison test --- integ-test/src/test/resources/correctness/queries/window.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/integ-test/src/test/resources/correctness/queries/window.txt b/integ-test/src/test/resources/correctness/queries/window.txt index 53e682b0f0..3ade9f4fdc 100644 --- a/integ-test/src/test/resources/correctness/queries/window.txt +++ b/integ-test/src/test/resources/correctness/queries/window.txt @@ -6,6 +6,11 @@ SELECT DistanceMiles, RANK() OVER (ORDER BY DistanceMiles DESC) AS rnk FROM kiba SELECT DistanceMiles, DENSE_RANK() OVER (ORDER BY DistanceMiles DESC) AS rnk FROM kibana_sample_data_flights SELECT user, RANK() OVER (ORDER BY user) AS rnk FROM kibana_sample_data_ecommerce SELECT user, DENSE_RANK() OVER (ORDER BY user) AS rnk FROM kibana_sample_data_ecommerce +SELECT user, COUNT(day_of_week_i) OVER (ORDER BY user) AS cnt FROM kibana_sample_data_ecommerce +SELECT user, SUM(day_of_week_i) OVER (ORDER BY user) AS num FROM kibana_sample_data_ecommerce +SELECT user, AVG(day_of_week_i) OVER (ORDER BY user) AS num FROM kibana_sample_data_ecommerce +SELECT user, MAX(day_of_week_i) OVER (ORDER BY user) AS num FROM kibana_sample_data_ecommerce +SELECT user, MIN(day_of_week_i) OVER (ORDER BY user) AS num FROM kibana_sample_data_ecommerce SELECT user, RANK() OVER (ORDER BY user DESC) AS rnk FROM kibana_sample_data_ecommerce SELECT user, DENSE_RANK() OVER (ORDER BY user DESC) AS rnk FROM kibana_sample_data_ecommerce SELECT customer_gender, user, ROW_NUMBER() OVER (PARTITION BY customer_gender ORDER BY user) AS num FROM kibana_sample_data_ecommerce From 656779771fa8d0c4356a833bbfabf4fb0f066cd5 Mon Sep 17 00:00:00 2001 From: Dai Date: Thu, 17 Dec 2020 14:49:55 -0800 Subject: [PATCH 08/16] Add doctest --- docs/user/dql/window.rst | 111 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/docs/user/dql/window.rst b/docs/user/dql/window.rst index 59b2f024cf..3e5165c1a5 100644 --- a/docs/user/dql/window.rst +++ b/docs/user/dql/window.rst @@ -36,6 +36,117 @@ The syntax of a window function is as follows in which both ``PARTITION BY`` and ) +Aggregate Functions +=================== + +Aggregate functions are window functions that operates on a cumulative window frame to calculate an aggregated result. How cumulative data in the window frame being aggregated is exactly same as how regular aggregate functions work. So aggregate window functions can be used to perform running calculation easily, for example running average or running sum. Note that if ``PARTITION BY`` clause present and specified column value(s) changed, the state of aggregate function will be reset. + +COUNT +----- + +Here is an example for ``COUNT`` function:: + + od> SELECT + ... gender, balance, + ... COUNT(balance) OVER( + ... PARTITION BY gender ORDER BY balance + ... ) AS cnt + ... FROM accounts; + fetched rows / total rows = 4/4 + +----------+-----------+-------+ + | gender | balance | cnt | + |----------+-----------+-------| + | F | 32838 | 1 | + | M | 4180 | 1 | + | M | 5686 | 2 | + | M | 39225 | 3 | + +----------+-----------+-------+ + +MIN +--- + +Here is an example for ``MIN`` function:: + + od> SELECT + ... gender, balance, + ... MIN(balance) OVER( + ... PARTITION BY gender ORDER BY balance + ... ) AS cnt + ... FROM accounts; + fetched rows / total rows = 4/4 + +----------+-----------+-------+ + | gender | balance | cnt | + |----------+-----------+-------| + | F | 32838 | 32838 | + | M | 4180 | 4180 | + | M | 5686 | 4180 | + | M | 39225 | 4180 | + +----------+-----------+-------+ + +MAX +--- + +Here is an example for ``MAX`` function:: + + od> SELECT + ... gender, balance, + ... MAX(balance) OVER( + ... PARTITION BY gender ORDER BY balance + ... ) AS cnt + ... FROM accounts; + fetched rows / total rows = 4/4 + +----------+-----------+-------+ + | gender | balance | cnt | + |----------+-----------+-------| + | F | 32838 | 32838 | + | M | 4180 | 4180 | + | M | 5686 | 5686 | + | M | 39225 | 39225 | + +----------+-----------+-------+ + +AVG +--- + +Here is an example for ``AVG`` function:: + + od> SELECT + ... gender, balance, + ... AVG(balance) OVER( + ... PARTITION BY gender ORDER BY balance + ... ) AS cnt + ... FROM accounts; + fetched rows / total rows = 4/4 + +----------+-----------+--------------------+ + | gender | balance | cnt | + |----------+-----------+--------------------| + | F | 32838 | 32838.0 | + | M | 4180 | 4180.0 | + | M | 5686 | 4933.0 | + | M | 39225 | 16363.666666666666 | + +----------+-----------+--------------------+ + +SUM +--- + +Here is an example for ``SUM`` function:: + + od> SELECT + ... gender, balance, + ... SUM(balance) OVER( + ... PARTITION BY gender ORDER BY balance + ... ) AS cnt + ... FROM accounts; + fetched rows / total rows = 4/4 + +----------+-----------+-------+ + | gender | balance | cnt | + |----------+-----------+-------| + | F | 32838 | 32838 | + | M | 4180 | 4180 | + | M | 5686 | 9866 | + | M | 39225 | 49091 | + +----------+-----------+-------+ + + Ranking Functions ================= From f937efa11b71a69cb83c86c7e6325416afe7a910 Mon Sep 17 00:00:00 2001 From: Dai Date: Thu, 17 Dec 2020 14:50:13 -0800 Subject: [PATCH 09/16] Add more comparison tests --- integ-test/src/test/resources/correctness/queries/window.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/integ-test/src/test/resources/correctness/queries/window.txt b/integ-test/src/test/resources/correctness/queries/window.txt index 3ade9f4fdc..95a6ee05d8 100644 --- a/integ-test/src/test/resources/correctness/queries/window.txt +++ b/integ-test/src/test/resources/correctness/queries/window.txt @@ -13,6 +13,11 @@ SELECT user, MAX(day_of_week_i) OVER (ORDER BY user) AS num FROM kibana_sample_d SELECT user, MIN(day_of_week_i) OVER (ORDER BY user) AS num FROM kibana_sample_data_ecommerce SELECT user, RANK() OVER (ORDER BY user DESC) AS rnk FROM kibana_sample_data_ecommerce SELECT user, DENSE_RANK() OVER (ORDER BY user DESC) AS rnk FROM kibana_sample_data_ecommerce +SELECT user, COUNT(day_of_week_i) OVER (PARTITION BY user ORDER BY order_id) AS cnt FROM kibana_sample_data_ecommerce +SELECT user, SUM(day_of_week_i) OVER (PARTITION BY user ORDER BY order_id) AS num FROM kibana_sample_data_ecommerce +SELECT user, AVG(day_of_week_i) OVER (PARTITION BY user ORDER BY order_id) AS num FROM kibana_sample_data_ecommerce +SELECT user, MAX(day_of_week_i) OVER (PARTITION BY user ORDER BY order_id) AS num FROM kibana_sample_data_ecommerce +SELECT user, MIN(day_of_week_i) OVER (PARTITION BY user ORDER BY order_id) AS num FROM kibana_sample_data_ecommerce SELECT customer_gender, user, ROW_NUMBER() OVER (PARTITION BY customer_gender ORDER BY user) AS num FROM kibana_sample_data_ecommerce SELECT customer_gender, user, RANK() OVER (PARTITION BY customer_gender ORDER BY user) AS num FROM kibana_sample_data_ecommerce SELECT customer_gender, user, DENSE_RANK() OVER (PARTITION BY customer_gender ORDER BY user) AS num FROM kibana_sample_data_ecommerce From 4357238e53e003e6867d4dd3ae370c47b634fc93 Mon Sep 17 00:00:00 2001 From: Dai Date: Thu, 17 Dec 2020 15:41:54 -0800 Subject: [PATCH 10/16] Add java doc --- .../window/frame/PeerWindowFrame.java | 71 +++++++++++++------ .../sql/planner/physical/WindowOperator.java | 5 +- 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrame.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrame.java index a25df917ae..24ef5a48bf 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrame.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrame.java @@ -37,15 +37,56 @@ public class PeerWindowFrame implements WindowFrame { private final WindowDefinition windowDefinition; + /** + * All peer rows (peer means rows in a partition that share same sorting key + * based on sort list in window definition. + */ private final List peers = new ArrayList<>(); - private int current; + /** + * Which row in the peer is currently being enriched by window function. + */ + private int position; + /** + * Does row at current position represents a new partition. + */ private boolean isNewPartition = true; + /** + * Is any pre-fetched row not consumed by window function yet. + * @return true if still have rows waiting to be consumed + */ @Override public boolean hasNext() { - return current < peers.size(); + return position < peers.size(); + } + + /** + * Move position and clear new partition flag. + * Note that because all peer rows have same result from window function, + * this is only returned at first time to change window function state. + * Afterwards, empty list is returned to avoid changes until next peer loaded. + * + * @return all rows for the peer + */ + @Override + public List next() { + isNewPartition = false; + if (position++ == 0) { + return peers; + } + return Collections.emptyList(); + } + + /** + * Current row at the position. Because rows are pre-fetched here, + * window operator needs to get them from here too. + * @return row at current position that being enriched by window function + */ + @Override + public ExprValue current() { + return peers.get(position); } @Override @@ -54,9 +95,9 @@ public void load(PeekingIterator it) { return; } - // Reset state - current = 0; + // Reset state: check if new partition before clear isNewPartition = it.hasNext() && !isSamePartition(it.peek()); + position = 0; peers.clear(); // Load until: @@ -80,20 +121,6 @@ public boolean isNewPartition() { return isNewPartition; } - @Override - public List next() { - isNewPartition = false; - if (current++ == 0) { - return peers; - } - return Collections.emptyList(); - } - - @Override - public ExprValue current() { - return peers.get(current); - } - private boolean isPeer(ExprValue next) { List sortFields = windowDefinition.getSortList() @@ -101,8 +128,8 @@ private boolean isPeer(ExprValue next) { .map(Pair::getRight) .collect(Collectors.toList()); - ExprValue cur = peers.get(peers.size() - 1); - return resolve(sortFields, cur).equals(resolve(sortFields, next)); + ExprValue last = peers.get(peers.size() - 1); + return resolve(sortFields, last).equals(resolve(sortFields, next)); } private boolean isSamePartition(ExprValue next) { @@ -111,8 +138,8 @@ private boolean isSamePartition(ExprValue next) { } List partitionByList = windowDefinition.getPartitionByList(); - ExprValue cur = peers.get(peers.size() - 1); - return resolve(partitionByList, cur).equals(resolve(partitionByList, next)); + ExprValue last = peers.get(peers.size() - 1); + return resolve(partitionByList, last).equals(resolve(partitionByList, next)); } private List resolve(List expressions, ExprValue row) { diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java index 3a1666c0e6..38d2faaaf1 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java @@ -52,8 +52,11 @@ public class WindowOperator extends PhysicalPlan { /** * Peeking iterator that can peek next element which is required - * by window frame such as peer frame. + * by window frame such as peer frame to prefetch all rows related + * to same peer (of same sorting key). */ + @EqualsAndHashCode.Exclude + @ToString.Exclude private final PeekingIterator peekingIterator; /** From 5cae0143da52b0e683819eab7232390d2413bc2f Mon Sep 17 00:00:00 2001 From: Dai Date: Thu, 17 Dec 2020 16:01:36 -0800 Subject: [PATCH 11/16] Rename and fix broken UT --- .../aggregation/AggregateWindowFunction.java | 6 ++-- ...wFrame.java => CurrentRowWindowFrame.java} | 16 ++------- ...dowFrame.java => PeerRowsWindowFrame.java} | 36 +++++++++---------- .../expression/window/frame/WindowFrame.java | 5 +-- .../window/ranking/DenseRankFunction.java | 4 +-- .../window/ranking/RankFunction.java | 4 +-- .../window/ranking/RankingWindowFunction.java | 12 +++---- .../window/ranking/RowNumberFunction.java | 4 +-- ...st.java => CurrentRowWindowFrameTest.java} | 6 ++-- .../AggregateWindowFunctionTest.java | 7 ++-- ...Test.java => PeerRowsWindowFrameTest.java} | 21 ++--------- .../ranking/RankingWindowFunctionTest.java | 6 ++-- 12 files changed, 44 insertions(+), 83 deletions(-) rename core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/{CumulativeWindowFrame.java => CurrentRowWindowFrame.java} (85%) rename core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/{PeerWindowFrame.java => PeerRowsWindowFrame.java} (85%) rename core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/{CumulativeWindowFrameTest.java => CurrentRowWindowFrameTest.java} (96%) rename core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/{PeerWindowFrameTest.java => PeerRowsWindowFrameTest.java} (91%) diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunction.java index 592138e65f..8d04bf6039 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunction.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunction.java @@ -25,7 +25,7 @@ import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowFunctionExpression; -import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.PeerWindowFrame; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.PeerRowsWindowFrame; import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.WindowFrame; import java.util.List; import lombok.EqualsAndHashCode; @@ -43,12 +43,12 @@ public class AggregateWindowFunction implements WindowFunctionExpression { @Override public WindowFrame createWindowFrame(WindowDefinition definition) { - return new PeerWindowFrame(definition); + return new PeerRowsWindowFrame(definition); } @Override public ExprValue valueOf(Environment valueEnv) { - WindowFrame frame = (WindowFrame) valueEnv; + PeerRowsWindowFrame frame = (PeerRowsWindowFrame) valueEnv; if (frame.isNewPartition()) { state = aggregator.create(); } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CumulativeWindowFrame.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CurrentRowWindowFrame.java similarity index 85% rename from core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CumulativeWindowFrame.java rename to core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CurrentRowWindowFrame.java index 0904238fd3..efe826e770 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CumulativeWindowFrame.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CurrentRowWindowFrame.java @@ -20,7 +20,6 @@ import com.amazon.opendistroforelasticsearch.sql.expression.Expression; import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; -import com.google.common.collect.ImmutableList; import com.google.common.collect.PeekingIterator; import java.util.List; import java.util.Objects; @@ -31,8 +30,7 @@ import lombok.ToString; /** - * Cumulative window frame that accumulates data row incrementally as window operator iterates - * input rows. Conceptually, cumulative window frame should hold all seen rows till next partition. + * Conceptually, cumulative window frame should hold all seen rows till next partition. * This class is actually an optimized version that only hold previous and current row. This is * efficient and sufficient for ranking and aggregate window function support for now, though need * to add "real" cumulative frame implementation in future as needed. @@ -40,7 +38,7 @@ @EqualsAndHashCode @RequiredArgsConstructor @ToString -public class CumulativeWindowFrame implements WindowFrame { +public class CurrentRowWindowFrame implements WindowFrame { @Getter private final WindowDefinition windowDefinition; @@ -67,16 +65,6 @@ public void load(PeekingIterator it) { current = it.next(); } - @Override - public boolean hasNext() { - return false; - } - - @Override - public List next() { - return ImmutableList.of(current); - } - @Override public ExprValue current() { return current; diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrame.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerRowsWindowFrame.java similarity index 85% rename from core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrame.java rename to core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerRowsWindowFrame.java index 24ef5a48bf..390e955b49 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrame.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerRowsWindowFrame.java @@ -30,15 +30,16 @@ /** * Window frame that only keep peers (tuples with same value of fields specified in sort list - * in window definition). + * in window definition). See PeerWindowFrameTest for details about how this window frame + * interacts with window operator and window function. */ @RequiredArgsConstructor -public class PeerWindowFrame implements WindowFrame { +public class PeerRowsWindowFrame implements WindowFrame { private final WindowDefinition windowDefinition; /** - * All peer rows (peer means rows in a partition that share same sorting key + * All peer rows (peer means rows in a partition that share same sort key * based on sort list in window definition. */ private final List peers = new ArrayList<>(); @@ -53,15 +54,6 @@ public class PeerWindowFrame implements WindowFrame { */ private boolean isNewPartition = true; - /** - * Is any pre-fetched row not consumed by window function yet. - * @return true if still have rows waiting to be consumed - */ - @Override - public boolean hasNext() { - return position < peers.size(); - } - /** * Move position and clear new partition flag. * Note that because all peer rows have same result from window function, @@ -70,7 +62,6 @@ public boolean hasNext() { * * @return all rows for the peer */ - @Override public List next() { isNewPartition = false; if (position++ == 0) { @@ -89,21 +80,26 @@ public ExprValue current() { return peers.get(position); } + /** + * Preload all peer rows if last peer rows done. This is called only + * when there are more rows in the given iterator. + * Load until: + * 1. Different peer found (row with different sort key) + * 2. Or new partition (row with different partition key) + * 3. Or no more rows + * @param it rows iterator + */ @Override public void load(PeekingIterator it) { - if (hasNext()) { + if (position < peers.size()) { return; } - // Reset state: check if new partition before clear - isNewPartition = it.hasNext() && !isSamePartition(it.peek()); + // Reset state: reset new partition before clearing peers + isNewPartition = !isSamePartition(it.peek()); position = 0; peers.clear(); - // Load until: - // 1) sort key different (different peer); - // 2) or new partition - // 3) or no more data while (it.hasNext()) { ExprValue next = it.peek(); if (peers.isEmpty()) { diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java index d36699e193..820e2f2c76 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java @@ -20,8 +20,6 @@ import com.amazon.opendistroforelasticsearch.sql.expression.Expression; import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; import com.google.common.collect.PeekingIterator; -import java.util.Iterator; -import java.util.List; /** * Window frame that represents a subset of a window which is all data accessible to @@ -32,8 +30,7 @@ * Note that which type of window frame is used is determined by both window function itself * and frame definition in a window definition. */ -public interface WindowFrame extends Environment, - Iterator> { +public interface WindowFrame extends Environment { @Override default ExprValue resolve(Expression var) { diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/DenseRankFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/DenseRankFunction.java index 8ea513de53..bea3fa3a4e 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/DenseRankFunction.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/DenseRankFunction.java @@ -17,7 +17,7 @@ package com.amazon.opendistroforelasticsearch.sql.expression.window.ranking; import com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionName; -import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CumulativeWindowFrame; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CurrentRowWindowFrame; /** * Dense rank window function that assigns a rank number to each row similarly as @@ -30,7 +30,7 @@ public DenseRankFunction() { } @Override - protected int rank(CumulativeWindowFrame frame) { + protected int rank(CurrentRowWindowFrame frame) { if (frame.isNewPartition()) { rank = 1; } else { diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankFunction.java index b0022b79e1..eb2c45299f 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankFunction.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankFunction.java @@ -17,7 +17,7 @@ package com.amazon.opendistroforelasticsearch.sql.expression.window.ranking; import com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionName; -import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CumulativeWindowFrame; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CurrentRowWindowFrame; /** * Rank window function that assigns a rank number to each row based on sort items @@ -36,7 +36,7 @@ public RankFunction() { } @Override - protected int rank(CumulativeWindowFrame frame) { + protected int rank(CurrentRowWindowFrame frame) { if (frame.isNewPartition()) { total = 1; rank = 1; diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunction.java index 9b500f0441..0be473b7e3 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunction.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunction.java @@ -28,7 +28,7 @@ import com.amazon.opendistroforelasticsearch.sql.expression.function.FunctionName; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowFunctionExpression; -import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CumulativeWindowFrame; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CurrentRowWindowFrame; import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.WindowFrame; import com.amazon.opendistroforelasticsearch.sql.storage.bindingtuple.BindingTuple; import java.util.List; @@ -58,12 +58,12 @@ public ExprType type() { @Override public WindowFrame createWindowFrame(WindowDefinition definition) { - return new CumulativeWindowFrame(definition); + return new CurrentRowWindowFrame(definition); } @Override public ExprValue valueOf(Environment valueEnv) { - return new ExprIntegerValue(rank((CumulativeWindowFrame) valueEnv)); + return new ExprIntegerValue(rank((CurrentRowWindowFrame) valueEnv)); } /** @@ -71,14 +71,14 @@ public ExprValue valueOf(Environment valueEnv) { * @param frame window frame * @return rank number */ - protected abstract int rank(CumulativeWindowFrame frame); + protected abstract int rank(CurrentRowWindowFrame frame); /** * Check sort field to see if current value is different from previous. * @param frame window frame * @return true if different, false if same or no sort list defined */ - protected boolean isSortFieldValueDifferent(CumulativeWindowFrame frame) { + protected boolean isSortFieldValueDifferent(CurrentRowWindowFrame frame) { if (isSortItemsNotDefined(frame)) { return false; } @@ -94,7 +94,7 @@ protected boolean isSortFieldValueDifferent(CumulativeWindowFrame frame) { return !current.equals(previous); } - private boolean isSortItemsNotDefined(CumulativeWindowFrame frame) { + private boolean isSortItemsNotDefined(CurrentRowWindowFrame frame) { return frame.getWindowDefinition().getSortList().isEmpty(); } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RowNumberFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RowNumberFunction.java index e4ea747606..bb5abaa525 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RowNumberFunction.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RowNumberFunction.java @@ -17,7 +17,7 @@ package com.amazon.opendistroforelasticsearch.sql.expression.window.ranking; import com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionName; -import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CumulativeWindowFrame; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CurrentRowWindowFrame; /** * Row number window function that assigns row number starting from 1 to each row in a partition. @@ -29,7 +29,7 @@ public RowNumberFunction() { } @Override - protected int rank(CumulativeWindowFrame frame) { + protected int rank(CurrentRowWindowFrame frame) { if (frame.isNewPartition()) { rank = 1; } diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CumulativeWindowFrameTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CurrentRowWindowFrameTest.java similarity index 96% rename from core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CumulativeWindowFrameTest.java rename to core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CurrentRowWindowFrameTest.java index cfec0812bb..60ee25c19e 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CumulativeWindowFrameTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CurrentRowWindowFrameTest.java @@ -29,7 +29,7 @@ import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; import com.amazon.opendistroforelasticsearch.sql.expression.DSL; -import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CumulativeWindowFrame; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CurrentRowWindowFrame; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterators; @@ -37,9 +37,9 @@ import org.apache.commons.lang3.tuple.ImmutablePair; import org.junit.jupiter.api.Test; -class CumulativeWindowFrameTest { +class CurrentRowWindowFrameTest { - private final CumulativeWindowFrame windowFrame = new CumulativeWindowFrame( + private final CurrentRowWindowFrame windowFrame = new CurrentRowWindowFrame( new WindowDefinition( ImmutableList.of(DSL.ref("state", STRING)), ImmutableList.of(ImmutablePair.of(DEFAULT_ASC, DSL.ref("age", INTEGER))))); diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunctionTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunctionTest.java index 1bd3099a54..df1eb7c25e 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunctionTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/aggregation/AggregateWindowFunctionTest.java @@ -23,14 +23,12 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.mockito.Mockito.withSettings; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprIntegerValue; import com.amazon.opendistroforelasticsearch.sql.expression.DSL; import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionTestBase; import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.Aggregator; -import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; -import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.WindowFrame; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.PeerRowsWindowFrame; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import org.junit.jupiter.api.DisplayNameGeneration; @@ -63,8 +61,7 @@ void test_delegated_methods() { @Test void should_accumulate_all_peer_values_and_not_reset_state_if_same_partition() { - WindowFrame windowFrame = mock(WindowFrame.class, - withSettings().extraInterfaces(Environment.class)); + PeerRowsWindowFrame windowFrame = mock(PeerRowsWindowFrame.class); AggregateWindowFunction windowFunction = new AggregateWindowFunction(dsl.sum(DSL.ref("age", INTEGER))); diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrameTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerRowsWindowFrameTest.java similarity index 91% rename from core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrameTest.java rename to core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerRowsWindowFrameTest.java index 4834802f0e..0f77524298 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerWindowFrameTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerRowsWindowFrameTest.java @@ -42,9 +42,9 @@ @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @ExtendWith(MockitoExtension.class) -class PeerWindowFrameTest { +class PeerRowsWindowFrameTest { - private final PeerWindowFrame windowFrame = new PeerWindowFrame( + private final PeerRowsWindowFrame windowFrame = new PeerRowsWindowFrame( new WindowDefinition( ImmutableList.of(DSL.ref("state", STRING)), ImmutableList.of(Pair.of(DEFAULT_ASC, DSL.ref("age", INTEGER))))); @@ -56,7 +56,6 @@ void single_row_test() { windowFrame.load(tuples); assertTrue(windowFrame.isNewPartition()); assertEquals(ImmutableList.of(tuple("WA", 10, 100)), windowFrame.next()); - assertFalse(windowFrame.hasNext()); } @Test @@ -73,19 +72,15 @@ void single_partition_test1() { assertTrue(windowFrame.isNewPartition()); assertEquals(ImmutableList.of(tuple("WA", 10, 100)), windowFrame.next()); - assertFalse(windowFrame.hasNext()); windowFrame.load(tuples); assertFalse(windowFrame.isNewPartition()); assertEquals( ImmutableList.of(tuple("WA", 20, 200), tuple("WA", 20, 50)), windowFrame.next()); - assertTrue(windowFrame.hasNext()); windowFrame.load(tuples); assertFalse(windowFrame.isNewPartition()); assertEquals(ImmutableList.of(), windowFrame.next()); - - assertFalse(windowFrame.hasNext()); } @Test @@ -104,7 +99,6 @@ void single_partition_test2() { tuple("WA", 10, 100)), windowFrame.next()); - assertFalse(windowFrame.hasNext()); windowFrame.load(tuples); assertFalse(windowFrame.isNewPartition()); assertEquals( @@ -113,22 +107,18 @@ void single_partition_test2() { tuple("WA", 20, 50)), windowFrame.next()); - assertTrue(windowFrame.hasNext()); windowFrame.load(tuples); assertFalse(windowFrame.isNewPartition()); assertEquals( ImmutableList.of(), windowFrame.next()); - assertFalse(windowFrame.hasNext()); windowFrame.load(tuples); assertFalse(windowFrame.isNewPartition()); assertEquals( ImmutableList.of( tuple("WA", 35, 150)), windowFrame.next()); - - assertFalse(windowFrame.hasNext()); } @Test @@ -146,7 +136,6 @@ void two_partitions_test1() { tuple("WA", 10, 100)), windowFrame.next()); - assertFalse(windowFrame.hasNext()); windowFrame.load(tuples); assertTrue(windowFrame.isNewPartition()); assertEquals( @@ -155,14 +144,11 @@ void two_partitions_test1() { tuple("CA", 18, 100)), windowFrame.next()); - assertTrue(windowFrame.hasNext()); windowFrame.load(tuples); assertFalse(windowFrame.isNewPartition()); assertEquals( ImmutableList.of(), windowFrame.next()); - - assertFalse(windowFrame.hasNext()); } @Test @@ -179,15 +165,12 @@ void two_partitions_test2() { tuple("WA", 10, 100)), windowFrame.next()); - assertFalse(windowFrame.hasNext()); windowFrame.load(tuples); assertTrue(windowFrame.isNewPartition()); assertEquals( ImmutableList.of( tuple("CA", 30, 200)), windowFrame.next()); - - assertFalse(windowFrame.hasNext()); } private ExprValue tuple(String state, int age, int balance) { diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunctionTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunctionTest.java index 71afdf94c6..83c79c3dc5 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunctionTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/ranking/RankingWindowFunctionTest.java @@ -28,7 +28,7 @@ import com.amazon.opendistroforelasticsearch.sql.expression.DSL; import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionTestBase; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; -import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CumulativeWindowFrame; +import com.amazon.opendistroforelasticsearch.sql.expression.window.frame.CurrentRowWindowFrame; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterators; @@ -48,12 +48,12 @@ @ExtendWith(MockitoExtension.class) class RankingWindowFunctionTest extends ExpressionTestBase { - private final CumulativeWindowFrame windowFrame1 = new CumulativeWindowFrame( + private final CurrentRowWindowFrame windowFrame1 = new CurrentRowWindowFrame( new WindowDefinition( ImmutableList.of(DSL.ref("state", STRING)), ImmutableList.of(Pair.of(DEFAULT_ASC, DSL.ref("age", INTEGER))))); - private final CumulativeWindowFrame windowFrame2 = new CumulativeWindowFrame( + private final CurrentRowWindowFrame windowFrame2 = new CurrentRowWindowFrame( new WindowDefinition( ImmutableList.of(DSL.ref("state", STRING)), ImmutableList.of())); // No sort items defined From 99587c629a4edd3935d7322f9908dbb942e378b2 Mon Sep 17 00:00:00 2001 From: Dai Date: Thu, 17 Dec 2020 16:34:53 -0800 Subject: [PATCH 12/16] Add more IT --- integ-test/src/test/resources/correctness/queries/window.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/integ-test/src/test/resources/correctness/queries/window.txt b/integ-test/src/test/resources/correctness/queries/window.txt index 95a6ee05d8..68cb72bad2 100644 --- a/integ-test/src/test/resources/correctness/queries/window.txt +++ b/integ-test/src/test/resources/correctness/queries/window.txt @@ -4,6 +4,10 @@ SELECT DistanceMiles, DENSE_RANK() OVER (ORDER BY DistanceMiles) AS rnk FROM kib SELECT DistanceMiles, ROW_NUMBER() OVER (ORDER BY DistanceMiles DESC) AS num FROM kibana_sample_data_flights SELECT DistanceMiles, RANK() OVER (ORDER BY DistanceMiles DESC) AS rnk FROM kibana_sample_data_flights SELECT DistanceMiles, DENSE_RANK() OVER (ORDER BY DistanceMiles DESC) AS rnk FROM kibana_sample_data_flights +SELECT FlightDelayMin, DistanceMiles, SUM(DistanceMiles) OVER (ORDER BY FlightDelayMin) AS num FROM kibana_sample_data_flights +SELECT FlightDelayMin, DistanceMiles, AVG(DistanceMiles) OVER (ORDER BY FlightDelayMin) AS num FROM kibana_sample_data_flights +SELECT FlightDelayMin, DistanceMiles, MAX(DistanceMiles) OVER (ORDER BY FlightDelayMin) AS num FROM kibana_sample_data_flights +SELECT FlightDelayMin, DistanceMiles, MIN(DistanceMiles) OVER (ORDER BY FlightDelayMin) AS num FROM kibana_sample_data_flights SELECT user, RANK() OVER (ORDER BY user) AS rnk FROM kibana_sample_data_ecommerce SELECT user, DENSE_RANK() OVER (ORDER BY user) AS rnk FROM kibana_sample_data_ecommerce SELECT user, COUNT(day_of_week_i) OVER (ORDER BY user) AS cnt FROM kibana_sample_data_ecommerce From 89ffe710ad7ab3c40e99319745865134258bce76 Mon Sep 17 00:00:00 2001 From: Dai Date: Thu, 17 Dec 2020 17:27:29 -0800 Subject: [PATCH 13/16] Add design doc --- docs/dev/AggregateWindowFunction.md | 94 ++++++++++++++++++++ docs/dev/img/aggregate-window-functions.png | Bin 0 -> 89495 bytes 2 files changed, 94 insertions(+) create mode 100644 docs/dev/AggregateWindowFunction.md create mode 100644 docs/dev/img/aggregate-window-functions.png diff --git a/docs/dev/AggregateWindowFunction.md b/docs/dev/AggregateWindowFunction.md new file mode 100644 index 0000000000..53c01c27cb --- /dev/null +++ b/docs/dev/AggregateWindowFunction.md @@ -0,0 +1,94 @@ +# SQL Aggregate Window Functions + +## 1.Overview + +To support aggregate window functions, the following two problems need to be addressed: + +1. How to make existing aggregate functions work as window function +2. How to handle duplicate sort key (field values in ORDER BY, will elaborate shortly) + +For the first problem, a wrapper class AggregateWindowFunction is created. In particular, it extends Expression interface and reuse existing aggregate functions to calculate result based on window frame. **Now let’s examine in details how to address the second problem**. + + +## 2.Problem Statement + +First let’s check why it’s a problem to the window frame and ranking window function framework introduced earlier. In the following example, `age` as sort key in `ORDER BY age` is unique, so the running total is accumulated on each row incrementally. + +``` +mysql> SELECT + -> ROW_NUMBER() OVER () AS "no.", + -> state, age, balance, + -> SUM(balance) OVER (PARTITION BY state ORDER BY age) AS "running total" + -> FROM accounts + -> ORDER BY state DESC, age; ++-----+-------+------+---------+---------------+ +| no. | state | age | balance | running total | ++-----+-------+------+---------+---------------+ +| 1 | WA | 10 | 100 | 100 | +| 2 | WA | 20 | 200 | 300 | +| 3 | WA | 25 | 50 | 350 | +| 4 | WA | 35 | 150 | 500 | +| 5 | CA | 18 | 150 | 150 | +| 6 | CA | 25 | 100 | 250 | +| 7 | CA | 30 | 200 | 450 | ++-----+-------+------+---------+---------------+ +``` + +However, problem arises when the sort key has duplicate values. For example, the 2nd and 3rd row (called peers) in ‘WA’ partition has same value. Same for the 5th and 6th row in the ‘CA’ partition. In this case, the running total would be the same for peer rows. This looks strange at first sight, though the reason is **the fact that which row is current is defined by sort key. That’s why the existing window frame and function implementation only based on and access to current row won’t work.** + +``` +mysql> SELECT + -> ROW_NUMBER() OVER () AS "no.", + -> state, age, balance, + -> SUM(balance) OVER (PARTITION BY state ORDER BY age) AS "running total" + -> FROM accounts + -> ORDER BY state DESC, age; ++-----+-------+------+---------+---------------+ +| no. | state | age | balance | running total | ++-----+-------+------+---------+---------------+ +| 1 | WA | 10 | 100 | 100 | +| 2 | WA | 20 | 200 | 350 | +| 3 | WA | 20 | 50 | 350 | +| 4 | WA | 35 | 150 | 500 | +| 5 | CA | 18 | 150 | 250 | +| 6 | CA | 18 | 100 | 250 | +| 7 | CA | 30 | 200 | 450 | ++-----+-------+------+---------+---------------+ +``` + +## 3.Solution + +### 3.1 How It Works + +By the examples above, we should be able to understand what an aggregate window function does conceptually. To implement, first we need to figure out how aggregate window functions work from iterative thinking. Let’s review the previous example and imagine a query engine behind it doing the calculation. + +``` ++-----+-------+------+---------+---------------+ +| no. | state | age | balance | running total | ++-----+-------+------+---------+---------------+ <- initial state +| 1 | WA | 10 | 100 | 100 | <- load 100, return 100 as sum +| 2 | WA | 20 | 200 | 350 | <- load 200 and 50, return 350 +| 3 | WA | 20 | 50 | 350 | <- load nothing, return 350 again +| 4 | WA | 35 | 150 | 500 | <- load 150, return 500 +| 5 | CA | 18 | 150 | 250 | <- new partition, reset and load 100 and 150 +| 6 | CA | 18 | 100 | 250 | <- load nothing, return 250 again +| 7 | CA | 30 | 200 | 450 | <- load 200, return 450 ++-----+-------+------+---------+---------------+ <- no more data, done +``` + +### 3.2 Design + +To explain the design in more intuitive way, formal sequence diagram in UML is not present here. Instead the following informal diagram illustrates how `WindowOperator`, `PeerRowsWindowFrame` and `AggregateWindowFunction` component work together as a whole to implement the same logic in last section. + +![High Level Design](img/aggregate-window-functions.png) + +### 3.3 Performance + +For time complexity, aggregate window functions are same as ranking functions which only scan input linearly. However, as for space complexity, there seems no way to avoid this memory consumption. Because more rows needs to be pre-fetched for calculation and meanwhile window operator need access to each previous row, one by one, as output. + +In the worst case, all input data will be pulled out into window frame if: + +1. Single partition due to no PARTITION BY clause +2. (and) All values of ORDER BY fields are exactly the same + +In this case, circuit breaker needs to be enabled to protect window operator from consuming large memory. diff --git a/docs/dev/img/aggregate-window-functions.png b/docs/dev/img/aggregate-window-functions.png new file mode 100644 index 0000000000000000000000000000000000000000..9132280e60133afa5369b320d30176054468e5dc GIT binary patch literal 89495 zcmdSAc|4Tu`#;>>s@2vW$<{`gv5c{17-q&S7`xDnWsGgi!YsDCQfNW;%2J53l_=Ru zM2HZgkc4c>p0($kyZiHezrW9Y|MfioJkRTe8P|1Q=XEZ}c`WbuafF-Tjdt%iykoh1$wCcP6_h|%CvhYvsyB0da{v-Ns44N0XVyF&NP*PBVsw*j|tAp!Ak}HWp{a?NCC+ETu12qN;3!{N1&JY)z4co-e6YdQ~TdJ^3Ao^g@5qbo7UuTM* zw;o#$XQkrGbc5hsEZB5?XAioKo2m-K*w+ea2HL@0(KM6vHCr_{!vtaB1jq0=1f~U?4k4?mkr^B`-Q0!_F;Ug? zGx5}SatH0nSR|8yrkLsZF_k=^2n1douY!W0NwyppEZJ3=LvS_rQ1-z(f(ei#gey;p13pYt)KD~4Q)?Js{!n^)JOi$kHQXJd z?B{06bn!EFBe=W5ZJm_74RI_#q$z)DD(Xf!J(wjE=gs6%{A?|qdFr-)N;oMlt9bjFU~oi^8Jpm1?8Y$FBfFU}%-qaSO0GUAl8HN==#HQ`u~-&#uDYwI3QC>q zgH-W!HKV%Y8J=j0B|@EsQc*(_Xl$Y$7#7w8N5gxX;vvptXCEach$_z52x`c*W?Fl& z+(?FADi+orP?Rx&>*?fXL387H@Fjr@{ut_8TBsT`=vFE`D9Q@ri!m`W)W@qUaeZkV zcN7Ne=WeQJtLE(Ii*q&O;F#bk42i7djP>wQHRAXgFsv=eIFd1j;7K&VxX|%RZccQH zo{AI6)D{hMve9!U;3zIo6JJX-o#JO{?1fNb6ICf_TPzIkf^fD#BV1IB;B0qyrWr;V zXQOPxHc&F7Q&HCL9tPkPAjoVI$JLaJQZhq2`O;7}YD_%@%n#fk!3auTMt%run1M0F z3rRv5xqDztY|+Lpe$gv|zRs2F}1G!Dhd$Q*@sg&<&bPZLum8w?4lr$TpEc4ep{ zkUWeJ&WgR3x( zKNTVsVuSa@+EVo?2Fl(jgrTK56XEVhf?1h);Z*(ba5n=g#@Y+UqM@NsFC~T(Qk6`z zWV$%xJ=94!XO0t+VeU*Y_OUSWF|&m^tGK#Sd9EaL8(VX@n~J-sksgBXJBu%z*B!Vn!zT(y&k_%^PmaG{O71 zSujlDWF>Q?H`2?}#EEWUVoXDOxRI??@t!EU8Uv~dH>AU@jEvO?Y+oW@_4`5eA%-w_ zmLb9$Yv!YGY3N2%p}Jz!eO+mAy1J_$hX)J{(0hhdV{e`{un~q9BqLWeOP^zkbGA0b zag`aKUY^dXN*GHQ2$^evBE#L(R9#@UFbfxVDo2&zZOel)tld5Jy}XgC=2Vu6i4u!K zaI!V$k(7KmzI3iT-iE?6)(3)5v^6#1vQPwdGc$9l9-ZUj>Fs6Zg(Bc+wq_&?74U8n z2LoCWOxP@&zgqdYL)F|_zAjcKel8>&hGl|9x~qAkQAj0cZ=xH|6#mA;-woRtf2=>Y{21Mc6p`L6A(MG1Sw?%lY?^h!g^z>j^^r;u<4 z1brVXGd&jrHk4+7fYE3K0t1Wl_BG%y9}RD1?TuwP;qXu!H8jGL$$`0`tW6mfmJE&_ zL|>J|Qlhib%K8**3!JZ|Dc8!9X6Q;WLaN{iBwte=lA>;HZH7j|==?*cs?TNGV=*UOwnGjg(~A)HtSW<0zboxehE7A{^$o|mmU&(8+wM>htS z4818>3|>i}YDLmh#u*tfDMoG-GLinb!1{Vvaal+M8l6V8_VKlLQ*weh!`T*A5R?f} z5r+C)WgN@M%99N>!I7M;)GXBW-Tin5=4fvYmTBZ=rcN?-GvhJ5@f;tnlChep9+w6~ zdhv}5+Q~)F%Z+W#wRLlc@DQrLbT@Nrj+LsJEyTc$KW_rb#>S85VTHn~(G9&2Ocest zie=-4F*UdFFeSK|GRUqt3KHXG%|ZLX+3GYmD})Efi$mgA&{Q~jhBT5j9EGJ>o7y;0 z+0HDS8pX=i6=zFe87P~vxCAN+rR1r{b8^$?8C#okRn!esYzbao2v1)xh#GKMJgf*D zx;oyNt41(ndg*hW)Y%L?4S^ysnQSbIV1VJMdHXP#o@6M3Vgu#aKyd0ta4#GWivd0o z!p9R&;n3)&P(QSXrM0iBwW)y-&&$})-PYR4RoR7Y<%uO?xS*k*9-fYJB{@OO{gjwU z7!S`t5e+@OC{8x2Xf}iDWXNS%6Fk6bfFBPv3kumn#fa^VQ!xUD!w;wL!&4)vdzqOd zl`&rWriK)Z9$A@UfHCJ8@W>Vn3YTK8%C|dkEJ9D6YwNDh!ji4^a3r{kDG5q)HBp)X*0EJvO(2S}GY>DZ49Uu-0%Rvbr&u-wr}j zbE9xjcr_^B@iOuwF==EQRgyCULqYRRx;ul)01Dj2hC(n#pz-FwPvBX=!40CCuO5zN zsf?wX8^c-d9vmo)hXo!h!V|;v1@E(FlYvAVnmX|vPi5dl8~(>*{mXp?|Nk8$C?n~G zb)=L8&0^`GLVuJx@v&6>CM zn0!j%EstiU;w3MfJticudEKTx9U7^xDrlUHo`nv4f+F@9fYWS|z+e#!B-Ix7<;G{isVdY&OIR8!Oz9H}*fy3K5c&pPU? zbhl&@f|uL7cQ!Dszk=5}*^i%E$20GEmr|Dp1cN_x zJvbZirkj$QtoF(0Rb@l=%8>t51*~}diUW7@3@!WG#>voP|4d0?<`gYmFlG%OS(Re#=v|`|>ab1eW+FW}KWslC! z&vUJjK`LjSzBhwUbTGk?Q$=Vaat_m1D@)77A%SBLv)Y=JTRvGWy{#;-JL;by9jOHk zE?VsKA9=R6SoI4hvHUKyO)%)krB(LVm5QmrspnN?A2*H*5#bd(B6{N`TDJXTcs2at z{V^p<(1L^(M%PIMYbDcPA3~q>5Oy%{dyhCTT+AQiXox7jKBcY0%2C_jRnj^A(Vc5ec0vA!Gj zDN6skpzi9AqBe}2i{d!DYNhwnv*OD6yisE6vbpy*;fRUF)YY#i(D0nRXSO|*)If%K zeT3%sME!K#9~T6joi;?~2BUO#J*rw?o+z-)n(uPd^(q;PFUbWP&6fYf0gp9InnpUV zjaiMxq^>%bez_{g2<)Qb+G6GzTNAIXi`$v==`qiyo2Lc1us-W-7Bq8}`fVQN_Y-$=C$SzU^MeFRx14 zKjL-dwB3O9eV8jjU~igG9F7bPDF$;(zn6|fApu=weQ~;$$1j%G?zj8eU$r`F*wzmQ8*c+cXQ!4eSoLhe9@@L7E<_wXexD+i? z5q~FU`vVkS3O+I7*cW%Z-kGr9=Z<}E+O5cWxpQV}Y$#WrNM`<$n#xupAcBKaYTIp}8iTLpv%;~aI&758TvSq$R^|j5SHM0$JkD5Xb zIspAeYX?F-G2yb%%h*#aL~IeG*9ez5gsHY^_6N+mT-5A3#~jKquBXBmKg!20K7ADE zwGcZKJrmLlsS_LRoeE+v)JPQvuYBxp0~?`ch+X_ht1244?U89=Y2z|+j-I>iA8Vr| zwz=A=XY=BhHG?QTd?cM_}QoH&6mQlB2!i0(yq02RjtpT zn~EN_4pWF!p>Hh`Y8N?b-?wz9fAzS(Uhq<*qpS4U>BJp74No2kf2c&-oFW#yI_*Si z`ti{%{l-kcY@W!>PKmD;-T$;~N{0nDL%iAqn<4uZ??=M=ID-M5@403J<@NiehDSse zdafo~D%VNr%YNDmL8@~PCF}VU7hR^jEQE}$>i3f6a>2HeUPSM*cq1uz z=HfM>S7UE!L9@yxS16L1V~@l`x?&b%1T6eqBA;*`+X)k`_ec6{|2V(qKI|-6HYBg) zdDV)il1X|m^d$$r6o&HStevs1u(Kp2O<0R zl=u359AfVktN1mR^QhTijoaeyptkwgi~wgk@$|}tZAa_-nF@(8OaJ7Op?jA7A`YuS z5f954Yh~6pYE@(f73qK}8Xo%N(1$~-2iGwM^(VVvWBd93!;8DZcN@_TOF%=*NzB@M z7Sy)M7dAn^bv2t*o|8)xh6nFI=ks!V;G#k0_4P@NA=mNhEJdnMqNZp|1GEXlFo=?O z&|QqH{8_8m96Y!k`Me=crb39=RZ!n!C$eY1FT$ktnnE7v zjKEva)t7}U&Gv7spS?N}PjP9^>w9DCRcb<}7&k6f5Y;}P^6{%Z zC3|`%u8+Qj^(O@@Qi#Sg>Bllf3yNwqfq{K-*!|Jte9K6A_2|zBw>>C`cU-9 zzVy54f@@(OO=98a-#djQ3EYfop+{IE3gk+JkEiKOM*szw_({O$`8)AzDYmR;rwF)f z)94qsj|ycI(MPVHZPpmiT^ezBwr_>QQD1LLJ_?%8nSHwl`BjFFBy&ThU6JSVzFkGu z^}MgO?c$^(i%|*n2J^JIb44GaD}gDmn)3FEF&JIgSa7H=%oWrf)f|2sJH2nz>E-?z z_hZ0v_o^znD5T^m$*Re%`=w3{=4n_UH_DP4bBP&IM)xYlE@;KS zk(2g$!?^ZN?opIo%MUGLgV5J6L#qqLRmsnQ7#7s(OJy1>lsJ19oEI+Sa1xl=0@^K# ziV(eFdwb@4Q=_5Uw9^v5e%2{N<}07Pvo1PZIE#NDa&3_>Y;%aa2I=Pe60!#hPog_j z{WsFZF21Cfhf&MQ3bu6|iC&5m+x@n0ppWL^|Jx8J3q2TiqXxuF>o@P~+A0;p(l(RL zOcT_$>a==YHKs8!HS_$(#ptxui;wRd zsrW^b zGw^`>W9oO@Dx)JW$2gr9C0-%6hSiR$3BQsH8#TDARomB!!e&{|rosQR zRo9Pf%x`4WTnL*vZ!Ox47DpqpV+Pjo~rgt z4P3Y-Ipg+8LubbhAX;?&4Wyz>8~9pSlA0Of4&QE(!MWLJ{vCK<`oE`BJHLM+e?kcB%R^EE$ESx}SeOy4? z6XFr4j72nyQG}Mqo?i;_@2Fw!Rl*?4!Vd2UF;U$S@i@oTEbP?AG3~b@GkWp%e%-58 z*Qd+34QV})d(PK8xi8@TkIvM)r0*qTTJQZdzYIl|oE}-vNew?5P1w*alja&lG@IFF zk~E?3oAK!m&W81ib1G-qo=wZ4@Q>T|;~OKWkPKnwZPSJ%)vTO1kvz%FUF^=7C-HGl z^FFzmhMgi`KW0bCE51&@oT#Rc%h(A_<&tt zrNQZxpykn9A!;#W1;SU14a*HlpNK&@(g%#h=mu(=qZwjbUY@Zs+QR_qPF$vxSmar- z3{BgvDp#;TEPB|Q``1}oHhi&lYM0ivQq2dKFBbS1vtG`Ip)f2Xn|rIMi|->68`QrC zQ5(WHA2sma(689tdhwKo_$$wz@~)?U67u}SZR*WTe^Ks4AyiiHZj_V8Xr zV7bsd#RgZIqIK(pNZs|H<%avnw)rl($1&L=L>Que=bvXAX^4XrVXBS7ds>ugd zxja^3z+KqjB@c6dH;a4_W}xR>p%4Epaq10%zrl#aPJzAAZ&pIznVxqZynpbAgV| z)%j;fhzx8$BN}mhKuq}JNBOF=l9{<{xeFHeSW9-2nX*YGT7%aU`A5cx2D%XP+$8go13JL@jwP4kbZmj_p8TO_12 z8F}h{rZ}OYL%vAQ9JKYMVzd_5;Sl29_BM1>>r6fa=@S0<)OOUi-VQzNwi`aN62Ea5 zr95FDwL(a^SmfO(YfGAQ_!n~vPLFW=NAI#Tlp7gQv4zQd4`y6z_&CF&w)99-r#8|N^>1XBFwf-o-CR>9eOE(xRKV-fx5+Yu;n;z4UHij4&R`_4q zt%eerfiHHTusK%2ahnOU7tqp>`_H919(%`QuXahw@I5=xKklswq~1B%05=MUUH%!L zBM|7ld+&C=>|0fb?uD$0gj^@(Iwv8qE^|-!NT%(&(mYx<;i7owLSp=Uwiw04Qk>lI zb0kS?x{#fdrJBDFa%Mbk%9<%Yc+yBWyQT=Eanr%U<;x+XlLt@R2{(*QN-v}s4g|Wy z?zPwy8U5@0J87A)(YwMCGn;+~M48$fWx3mq3F%AP0}N$kSD)e4Z9}1R2<+ve9UW58 zoWd`}o|Ev+5G2%?d|kw>QG;-gbV_j~H`_9}|NdjRlX-=I?EO@72ewo))8e8{zAf=M z;Qp3HX#AUuPN>bvqdzveGskPGi`L*{qJRSof2U z-r3cZ$Pmm9wkViQa{jn<&+Wi#MBV3u(wF<)^*fwSZ&IkX>GT|--ZU^hQ$yES*_Q6A zNgi7^oIioMS3#`SIBGIbU!N&0v-w$0?i0-M*#-rV%5TYx*fdQ2Hn_pX#62Rt+fRO6 zOL5Ux{VP@NzM@xiuH$QB>8Ryt0gu>r&(9Zt@f%z`Mn$W%5(cgL`ElHGi_xr>u+zaNkNbm@yl#_bF3{?B^c zb7YKV=$Ub$df#aC{<5*f=LSvY%j0L$b(r|v+h=Q!zMi#fPOJT}8)(;7gBM97LvkK= z5AUM{f)(oyU(FJ|nR_+LIy^Yn*C0)5>sdoYM$W1O!m)ALs&m-gua_KK=xR$02tClb zY#dT?`Xf89SgRshSorHc(pT%#Q>Tv(h^RWex^mt4@OL(B0W)~9L*>~47qcZo#!sBc!t_gny??xrxWn4kTu}e+YX72qk-2t$|@6#vM z8u%-|{p^dL*Y(F$Ed1!qwhV7>VEy?ppfiAgw~PT zTR|Q+@UH%cM-ef3gKM4QuDf^wn$@2?-(JevY$D|?mU_~xyru(NlRVZtHKK=ILHgA9 z(_}KA$oiGCx-t}~SvY9C`@q)bsmBI|KZN$v8ZNxsG?E@RYn=O5zW4YogCjwT=+hx~ zA-}EKZ;{~JU*h&ZYGQ;V-`zLP?0@+FwYweXJeSP%zXtPr=e3)x;TE#(T#z&*FEh$J zz_(;mKJQA_^dZST@^bQ~LlZGk8wbv3)Z8|{EvfvK6e_M}S!3WbaabwCG~mB5!5_Oe zW7O;-4s{h^ErhpDVDsf%8sNXe-8_Wa!_RX6oN_b5+S!-NWtL~WjW1|gI=n5Vch=BZ zaZq;B09#vCcc}djKkvQb|1)Z!HI>W+1PXl(3qPf5nZ+H+YIrVD`W14pWpHnD_JNur zQH1n5SCiZ`)@CpH{BmMx{@ruppHpN0yn^XVZEA1x3Bst9LAHRA#E)vZkoF z*U*A&Tk+?I`&9~5ni00PQy=rIVsEou})3v`<&VN46Zx#|DYivw@N7%P4n;9)$ z^4ZETpFuDEidGO-SUfG6>iDzz0OLt|G&T2TVyWP$X8#gi7Z*PD4@TpK3i7WZ{0s_z zIIQ{U+;@i=|LL@bPvd`aMZ&) zhbpFDA8GILc%<#erIpkNB;7ew`%nCTuOS%f-tV6*aXAhLn7`H*M{>!iZ_q~h_PLAm zWmA=!jyonkxhvDTQx%JScN)3P%oOF_!Z(ip1FAo(3!tOPZ58O{UPH3Ruhm&XTfD*3 zzwP4x;QHrJ9}}u!ot`5ncnOeuWY14?{!7G!1a|EOu=A$NY?Yk{cyg=+`O2SP;8+;n z+W${CzaJt=g1)Tl>-_)m8vcFgMm?<4v41?o-|k)ez$^4G>pJ|CN#cuBjmqo)2*v*~ zx)&zGKtjxa4i5d7(EV-=UbpN2Bd^O38%?{Wa9g2jb*AXOnepzYZM~HNq2hk1@%%FH z9>0%XWou&@f$M-!!T_X=TRIePciij9ZHs{HXC6_>dlPqy^me+W0zPgO2oMgCITJpo zg=l008LF~jz*-(VNaQiLS+oyVY<9XCuF7dZMSk5m!!>M)4 zEENw%XfD{aeO+1x+)@JIvMN}7rX>Z_^*)SXX1?I|4uW5!xTZ+Ekwl=1_WXO|p*tnRx8(|0@)dJ! zON;Q4B;{Uqf8Z!!Wp4jbc6q<;NdQ7SEYbEB67g`CD=k)bUQdfgGw%#nCG*A}jsjFM zWnk9pS@-e0=U=V{#;yGvjLbTb!jVcS18b6bL#^keS8=c3(jp++wBKsYHjVzdX%9*e zkYT%~`T#G(rw~6*_fC%Gy$EXfzOQhfrM09sIMF5AuunevJ)FTC?c(fDH-l4M;%(Xr zQ|5Ckm(%7?S-2SQd? zJPY5(yC(Z~UdZDnceaym)M|`VA9*-btr(nNs5+$Lu;O>U@E6I}s_Rx$;%@ye$do3j z(;N3NT_U2V586FGJJ(JSG>;soXzk)jd@HKLy@tQvUWorx(98Lb!rpub!2Js9p~e2- z;IuGtzYiq9w$w{q%K)pW{RkQRhGjbyH1m4KZ+A`U2q&)59z=9iYUipdzP4d%XZNpvQ-Ouv8&6zJ?&-+y^PPKT z`pNM)GntPdUKpdVT@Tfp+C-%9@vY7kWhVu%FWK!GiKvN%J>+0*%pU2QULCET1Z@|6{WOtv32VVvpeDuf^ea=qtUOL|4{> zo;w+lZM`tX^lH(a`s2m8I_1qo)hogL-*d zY%5%0Q=wkkks6lzx7T)mDbQ~c{DCb*+6K#p;M*jEVkSR^QDL77m|5IaKGNn=rum`)Lggr z`X{zOwd<9jZc={P_tK(<(>WPHlT_rW@m1I8Rk?f=L3fR{-e>H%+%}gqqLq~cy|U(` zn7F&FnK-Zp6s6E#r_M*OD>tK#(8}pBn^6aWVJ0Eqizr{qCc3|kT-dzv&;`BG z&_8G%n~&$C5Cu-hz^P3LO8#rT?y^OkJnaRAOwdEQnS z-E+v2wXsTRBP{pfe(UoWR9duVGeEY+Ded*2gVg(RmKcy>Fncfp(U%V>dzGp3*-T(5 z?v492;(L3>t6#;P**piR10x=v!;W|0jed%d^3||QtOr^+IZv)7XrZwE;!cIcDhJA4 z5Z4Dc9vuVbDSzDOMbHTyf}Egay`CXQ`hg`IZ8eg>Df)Lewn<%+hi>u&SqoEb{9T@?uqgJU$gTL^D+&G7h-1W^u41uek4aI`W`dqiK?#ztB*l28y2*k?DcY2`u}5 z8z%gL&r#t8=CURHPQ!F)H_krN2v-e_nEN!M16%4oJWLU$&ncEtEUfP9+lh|$O+K#& zDGay111nr0z0=Bp>!Lt;Bb(54<8Er};2Xf_Men!sEI4$yE*%|QWNE8;XwK5!w`28t zw$^lw=;-hrTCls_n=F_OS}UO<^r`Q?h_NA}jc(Z>PNV8Wf+Zr!U;MA6t%l6d)TMRK5K~20wUcabx!K*u4A% zzkOsL-*|&018AhOuP#XWVM@eEtbX3cPm5_W43kQ63|^bB3!jhxKQ@XTwoCCVR%;m-je$qrca5+@;ksl{N z`gWGD+lu$^+5B4Yg2UXaY5$cu`G;1?A?sLLdL7@%JEWx{dSZL>C`f+rTrUym4S3pN zX`H(9Y2-;|$JJ}cl37zzL5pS77VSl`_D$liy4`c~peG>st9vX_?p(p%UjI>-Z4v=K z>$5;DC-Plq$jEEGZQ?3^GHJOT*RL4M7Z`lhdq>U7?EkX0lTE(C*957)T|p(7haX~P zGPP)GYL~7)`IXPEkSoaU3;wm%ebr-jt{Y^AwP4M-2ZUctM?36K0cmX`x5CCg@R^l@ z2am5F+Zt8_vc3lA0E4gmtFn(bK9nu>m~Y3QeHCbTDLJd~v~KgjWLq;x?SToV(E`Wq zM)f6@O?2w?KOgfP0~r)(iKTj@C!x0%n+)_Wavg1K$54+%gRh-=h5|n5t3>)|aIORP zI9eRdY4vbi>6t*QkYxWrR0?}syC|cl^Q)(;GT!eKy-o`r;2=x#9qapk%b~|2jB5IX zpIlbdYIwxOBT-|&M{H%mhg%`J*1q9X&8Dl-t_AAAgtd=5ES8Gxd%U*rafE&>_D)z3ebI|E))p`SvQ*Zcg^of|6AfN_@Lole7BYi)vtiq)t5@%wc^x3I%*uriG z`}Z8*wX=(ZQk*>EZs0k+LD)@bUPlIc(R zKlf|M#I>e}a1SJLbU0tnMDvpi*Od{c#jGEo>_@j0mb%n2^MD(6INd2qK;4$Cct0en zQK3veui*7zr<(VNoqC^ks>a|4NR{o^6K&~|ZS9Y~XI`c#_ z%j|fP?91cO(49Wd=%~-SwFATO2>GwAt=I5v?s>zXp9peb6e0Sm&rqCIb4`V_;P=p+ zlT3_AJ1}h0f`DB=Vj?frztqf*{$bfy&looUmirFrpqb&LmC<#%?ry@3cduJSrtd2T zo~i(L>}ZZzPV+iYo4560IbZJ`A-fB?`428(WeRG_+12+G!WFe{jpQHpa73Jl9a+#n>Ld2M}-!_8sC~TrT$}_99c%?kSPt`c6s6}Vx>t!rzYs-QMNJe9=PS=fC z?YwW8J1*3YPvp~JcTOcrCRKNn8h#8+=%vHT<&}W+_~}TS*2KXEAd{56xF$9LFn2Y# zPoL>LFD!>>$$e?S65JA|cTG4>il1}p^2$zmG%j%m+M}2@3q6*Z*I7RL!LfC?6?xZc z&4*>DvaC9ejBL-&*6I1s;93(~Y*yp6eaNd8nHv}O@EmMT50Uk>(_;RoU(MdEdsF*_ zPd&W;R_&ww$lC#y$5*{Ik#_QR)&wHgr~iU3((zd+)T`}MO^z=z{*tgf%gvrXjX z1?No7x8;Lr#J$<*EIHu&uDLBz;Br)-1^@|NlfxdOE&`LxmUk}i@jgoFbGsru!t;-F zP#T}NpRV`?a3U4MS9gU@*2FHFIc$2Zaz(G?53!p!qwrsEXum()j_sM$8(MNbO01LU znf#~=QgQ)bkv*9HiEeb)an0wGoXYMU6BZ|^M%Y8+;-PX@GQ7~)`}_pWPz*M&5#g0% zh^RYsd9@QPZ`}d0S;p&U7{t#0&nk4!8$d~9ibIfNo&nYRr|2T%0(Ld4UO~tt%IZu) zRZ`xoUYWpC{ZlRjdM9Nf-xX|)W5(C(#TiIb&wVQo(W-2o;jDvHQ>4z$Z%xMuZ3{S))g&dm=I`(?k#;^*y^!rQ2HUdu{tA~EQ zamLA{|2#cc)brSJJc?HE3SA71g?e~IT0R3h!>EhDLWFv3 zlXdT(ZH?8^d#!T0Z?235T0~~`-%qntGegUyoE1B{Q$Wo_ZN^JTBZ2zZJVwLxVAKn# zdI~zDW+m)c+^#1+7xa3K1O1bKR>z5_97kYlv&SVwUtREyF%!u4lY9^>8AG%StJO=p z9^Gqs@U)_O=GkoCx87uJxPrnk+RB9Oa9!DklWE9?_jp|20sU6DR(1D=JZ^Nve z)8@|J)!A6_Ep*;E^28mN&ab7ZDqgY6C-kH`5;DpXcA3d=Yo$ehG-Fkzob>tD*Qx$|^9b#uYyV7*qgk^hj z$3Cqw;w(#Py{+0NH*V(Ov*)tBmCj98AnM<0*mTu*Nn$F0qxpsvt#ss4)dY$JsU>u9%jV@)L8E)?F$H@ry+`Ci3_EFb)cwlEpgytw2C8I~iYnp1`-Bur-6WMo~a6y^0~yYjcRy&i2wD)jJCh*qz)+ zd-!Jm-_+v&LfC%^{$YA1`reMGagm02;nKpvpk6}+{s0zOq=b|Rzkfq)()e&rYecyF zzhOQ0d}_s>r_pohUl1F94FbzR^LX~Yf4Rxx(>bisQ8&uJAU4uhL9i)2nfSo+Uv9SZ z1N-*-S?S6D2IrjQ<5opZX}14z6MKM9x?As0SXL9L6j|V@()J)n@^BbNM*xVzf zBtHL&BpCi5*wDMg$*P8xdCL`3+{g2pOriX_hOvUhaU0)VwLL>tt>ET2A6DfW98ln(MCM)iX- z;|Yf~(1E#`)iD5IiKv<5qW}c$dGpt<`6a^`d2wUK3f6ich5U!Yi+(OOBAmz`AY|&r}se#k?(mAhpc&)_5TSvZ@tk~DLyz6 zoY>jndtBM|)jQv&6@21TJL1^4(DY};*o9>+5Jhk57+gLic&Trj#w0D~Z_wEjmZB*_ zl$l%jNTb=lUR&%>JxK;4@?lr1;*)x*zXI;yc1a3A-gl+i^=)nX6^pNV;peJmOX z%(+rfQ{%7T4|oY`VW2IaF;@5_FRm^wda$LRO z0Px%PZ1~2Qb#7tGLh0~)8DX>QuOr!mPicZ4Z?Dli&qauS7xOZuiU9GK0v| z=CgM(;mx@$fO~q}<(<`*qo(@ZvRC``u9xOF22!&p4}U$ky=iN>_xO{+>!UtCk;Q*0J(B}`jJ&hf z)uiPz9(vN*sPGdwgk3dr&pvt|(R9+dEu>Xwxj%*PNWZC6vv}$g5wE6{R{1YlY4gVaSLpTs&tQ71 z8h+`W6Yu3thbTasQEmh^o>pBQcl>pG7-VOt@CoK@-XgKv=qOi)x@cy6rh@?4wDg_N zQV)adqSZ%shlc!oKgN&E611ib0RFokmja+5wKK<+#<%qj1T2EIU*p%~d~AKd{O)VJ z;Kj23`!=q(WY2*5G1Zg^qs+q_`A9k-3A5S&QlvJ38z1T&e$Ph)ih%MWa02TFFN4h3 z;Pyk5q?>$&qXOz`ZW!H^wtvkR(xU-CUY;}Z|JY`%sLhy91{~&NO;Yw4{nhKIsf+yQ z2mqw@>D+dBXdqMqAE~qK##<;%?QNGU{lD4NwP9El8ld%!{@Ldg9XHmG=q#478ct

5?{mcdC+l*rH0D8do-RK;HJ;+sX}TZq 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�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

4HD%{6uo7#ovhImkv|KYw-u zbm~iWuRvG9yjSnFY1sj*t~R-w;eek!f8(0${Z=q~j9@e>xm%5}`Y9wiX3utGP>?fg zu{4+S-grmHSlQ?mKzXi9jcHa6b`Z^~pfIw7BrwwmbJ6*X#wt&;Nj=tZbuXN94V3bS z@-v~#%xyVDpFkAx?xP_3#^?Ub>m+G(n?#K?kd-DI!m-Zxor=U^Csu4b6%i@YUc#k~ zdljS{1=)CJav`$)Xg-C?eqrYcJ%wC|pB+Lvz$Tn0*L+sjwE>sRmz-6)rxjF7+D;Qp zZRW)k7X^_Y|DBhc5f@rVPI6cs7S@y;iGkQi za91`oW(~`29SQC_necpx=+yuaU*OG`4Y7wvd|ODUrw5PTBwvfZNm`yi;9yN8U5g;uK z4$FmwFCnIFTXuJ?#)!E^Wg5R4O&YcMH`!)&p~H8%{)p$Ubk7?HbryjlNKU|~o0CN| z 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 literal 0 HcmV?d00001 From 8d29ffce074f4fe80a242fd967144f9c432067cc Mon Sep 17 00:00:00 2001 From: Dai Date: Fri, 18 Dec 2020 11:43:48 -0800 Subject: [PATCH 14/16] Prepare PR --- .../expression/window/frame/WindowFrame.java | 6 +- .../window/frame/PeerRowsWindowFrameTest.java | 91 ++++++++++++++++++- docs/user/limitations/limitations.rst | 3 +- 3 files changed, 91 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java index 820e2f2c76..33ec240412 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java @@ -44,13 +44,13 @@ default ExprValue resolve(Expression var) { boolean isNewPartition(); /** - * Load any number of rows as needed. - * @param iterator row iterator + * Load one or more rows as window function calculation needed. + * @param iterator peeking iterator that can peek next element without moving iterator */ void load(PeekingIterator iterator); /** - * Get current data row. + * Get current data row for giving window operator chance to get rows preloaded into frame. * @return data row */ ExprValue current(); diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerRowsWindowFrameTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerRowsWindowFrameTest.java index 0f77524298..a95ba5f029 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerRowsWindowFrameTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerRowsWindowFrameTest.java @@ -50,7 +50,7 @@ class PeerRowsWindowFrameTest { ImmutableList.of(Pair.of(DEFAULT_ASC, DSL.ref("age", INTEGER))))); @Test - void single_row_test() { + void test_single_row() { PeekingIterator tuples = Iterators.peekingIterator( Iterators.singletonIterator(tuple("WA", 10, 100))); windowFrame.load(tuples); @@ -59,7 +59,7 @@ void single_row_test() { } @Test - void single_partition_test1() { + void test_single_partition_with_no_more_rows_after_peers() { PeekingIterator tuples = Iterators.peekingIterator( Iterators.forArray( tuple("WA", 10, 100), @@ -84,7 +84,7 @@ void single_partition_test1() { } @Test - void single_partition_test2() { + void test_single_partition_with_more_rows_after_peers() { PeekingIterator tuples = Iterators.peekingIterator( Iterators.forArray( tuple("WA", 10, 100), @@ -122,7 +122,7 @@ void single_partition_test2() { } @Test - void two_partitions_test1() { + void test_two_partitions_with_all_same_peers_in_second_partition() { PeekingIterator tuples = Iterators.peekingIterator( Iterators.forArray( tuple("WA", 10, 100), @@ -152,7 +152,7 @@ void two_partitions_test1() { } @Test - void two_partitions_test2() { + void test_two_partitions_with_single_row_in_each_partition() { PeekingIterator tuples = Iterators.peekingIterator( Iterators.forArray( tuple("WA", 10, 100), @@ -173,6 +173,87 @@ void two_partitions_test2() { windowFrame.next()); } + @Test + void test_window_definition_with_no_partition_by() { + PeerRowsWindowFrame windowFrame = new PeerRowsWindowFrame( + new WindowDefinition( + ImmutableList.of(), + ImmutableList.of(Pair.of(DEFAULT_ASC, DSL.ref("age", INTEGER))))); + + PeekingIterator tuples = Iterators.peekingIterator( + Iterators.forArray( + tuple("WA", 10, 100), + tuple("CA", 30, 200))); + + windowFrame.load(tuples); + assertTrue(windowFrame.isNewPartition()); + assertEquals( + ImmutableList.of( + tuple("WA", 10, 100)), + windowFrame.next()); + + windowFrame.load(tuples); + assertFalse(windowFrame.isNewPartition()); + assertEquals( + ImmutableList.of( + tuple("CA", 30, 200)), + windowFrame.next()); + } + + @Test + void test_window_definition_with_no_order_by() { + PeerRowsWindowFrame windowFrame = new PeerRowsWindowFrame( + new WindowDefinition( + ImmutableList.of(DSL.ref("state", STRING)), + ImmutableList.of())); + + PeekingIterator tuples = Iterators.peekingIterator( + Iterators.forArray( + tuple("WA", 10, 100), + tuple("CA", 30, 200))); + + windowFrame.load(tuples); + assertTrue(windowFrame.isNewPartition()); + assertEquals( + ImmutableList.of( + tuple("WA", 10, 100)), + windowFrame.next()); + + windowFrame.load(tuples); + assertTrue(windowFrame.isNewPartition()); + assertEquals( + ImmutableList.of( + tuple("CA", 30, 200)), + windowFrame.next()); + } + + @Test + void test_window_definition_with_no_partition_by_and_order_by() { + PeerRowsWindowFrame windowFrame = new PeerRowsWindowFrame( + new WindowDefinition( + ImmutableList.of(), + ImmutableList.of())); + + PeekingIterator tuples = Iterators.peekingIterator( + Iterators.forArray( + tuple("WA", 10, 100), + tuple("CA", 30, 200))); + + windowFrame.load(tuples); + assertTrue(windowFrame.isNewPartition()); + assertEquals( + ImmutableList.of( + tuple("WA", 10, 100), + tuple("CA", 30, 200)), + windowFrame.next()); + + windowFrame.load(tuples); + assertFalse(windowFrame.isNewPartition()); + assertEquals( + ImmutableList.of(), + windowFrame.next()); + } + private ExprValue tuple(String state, int age, int balance) { return fromExprValueMap(ImmutableMap.of( "state", new ExprStringValue(state), diff --git a/docs/user/limitations/limitations.rst b/docs/user/limitations/limitations.rst index cab4eac590..923a2e6528 100644 --- a/docs/user/limitations/limitations.rst +++ b/docs/user/limitations/limitations.rst @@ -57,13 +57,14 @@ Here's a link to the Github issue - `Issue 110 Date: Fri, 18 Dec 2020 17:30:08 -0800 Subject: [PATCH 15/16] Fix no sort key bug --- .../analysis/WindowExpressionAnalyzer.java | 11 ++++++++-- .../window/frame/CurrentRowWindowFrame.java | 15 +++++++++++++ .../window/frame/PeerRowsWindowFrame.java | 15 ++++++++++--- .../expression/window/frame/WindowFrame.java | 4 +++- .../sql/planner/physical/WindowOperator.java | 2 +- .../WindowExpressionAnalyzerTest.java | 19 ++++++++++++++++ .../window/CurrentRowWindowFrameTest.java | 6 +++++ .../planner/physical/WindowOperatorTest.java | 22 +++++++++++++++++++ docs/user/limitations/limitations.rst | 10 +++++++++ .../resources/correctness/queries/window.txt | 5 +++++ 10 files changed, 102 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/WindowExpressionAnalyzer.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/WindowExpressionAnalyzer.java index 26e8bd2fd2..27609e46c4 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/WindowExpressionAnalyzer.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/WindowExpressionAnalyzer.java @@ -75,11 +75,18 @@ public LogicalPlan visitAlias(Alias node, AnalysisContext context) { Expression windowFunction = expressionAnalyzer.analyze(unresolved, context); List partitionByList = analyzePartitionList(unresolved, context); List> sortList = analyzeSortList(unresolved, context); + WindowDefinition windowDefinition = new WindowDefinition(partitionByList, sortList); + NamedExpression namedWindowFunction = + new NamedExpression(node.getName(), windowFunction, node.getAlias()); + List> allSortItems = windowDefinition.getAllSortItems(); + if (allSortItems.isEmpty()) { + return new LogicalWindow(child, namedWindowFunction, windowDefinition); + } return new LogicalWindow( - new LogicalSort(child,windowDefinition.getAllSortItems()), - new NamedExpression(node.getName(), windowFunction, node.getAlias()), + new LogicalSort(child, allSortItems), + namedWindowFunction, windowDefinition); } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CurrentRowWindowFrame.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CurrentRowWindowFrame.java index efe826e770..4a4d15e826 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CurrentRowWindowFrame.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/CurrentRowWindowFrame.java @@ -21,6 +21,7 @@ import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; import com.amazon.opendistroforelasticsearch.sql.expression.window.WindowDefinition; import com.google.common.collect.PeekingIterator; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -81,4 +82,18 @@ private List resolve(List expressions, ExprValue row) { .collect(Collectors.toList()); } + /** + * Current row window frame won't pre-fetch any row ahead. + * So always return false as nothing "cached" in frame. + */ + @Override + public boolean hasNext() { + return false; + } + + @Override + public List next() { + return Collections.emptyList(); + } + } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerRowsWindowFrame.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerRowsWindowFrame.java index 390e955b49..7ba29ca014 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerRowsWindowFrame.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/PeerRowsWindowFrame.java @@ -54,6 +54,14 @@ public class PeerRowsWindowFrame implements WindowFrame { */ private boolean isNewPartition = true; + /** + * If any more pre-fetched rows not returned to window operator yet. + */ + @Override + public boolean hasNext() { + return position < peers.size(); + } + /** * Move position and clear new partition flag. * Note that because all peer rows have same result from window function, @@ -62,6 +70,7 @@ public class PeerRowsWindowFrame implements WindowFrame { * * @return all rows for the peer */ + @Override public List next() { isNewPartition = false; if (position++ == 0) { @@ -81,8 +90,8 @@ public ExprValue current() { } /** - * Preload all peer rows if last peer rows done. This is called only - * when there are more rows in the given iterator. + * Preload all peer rows if last peer rows done. Note that when no more data in peeking iterator, + * there must be rows in frame (hasNext()=true), so no need to check it.hasNext() in this method. * Load until: * 1. Different peer found (row with different sort key) * 2. Or new partition (row with different partition key) @@ -91,7 +100,7 @@ public ExprValue current() { */ @Override public void load(PeekingIterator it) { - if (position < peers.size()) { + if (hasNext()) { return; } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java index 33ec240412..fcc36e15fc 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/window/frame/WindowFrame.java @@ -20,6 +20,8 @@ import com.amazon.opendistroforelasticsearch.sql.expression.Expression; import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; import com.google.common.collect.PeekingIterator; +import java.util.Iterator; +import java.util.List; /** * Window frame that represents a subset of a window which is all data accessible to @@ -30,7 +32,7 @@ * Note that which type of window frame is used is determined by both window function itself * and frame definition in a window definition. */ -public interface WindowFrame extends Environment { +public interface WindowFrame extends Environment, Iterator> { @Override default ExprValue resolve(Expression var) { diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java index 38d2faaaf1..1286307564 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperator.java @@ -87,7 +87,7 @@ public List getChild() { @Override public boolean hasNext() { - return peekingIterator.hasNext(); + return peekingIterator.hasNext() || windowFrame.hasNext(); } @Override diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/WindowExpressionAnalyzerTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/WindowExpressionAnalyzerTest.java index f2d665804a..0a80324d21 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/WindowExpressionAnalyzerTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/WindowExpressionAnalyzerTest.java @@ -80,6 +80,25 @@ void should_wrap_child_with_window_and_sort_operator_if_project_item_windowed() analysisContext)); } + @Test + void should_not_generate_sort_operator_if_no_partition_by_and_order_by_list() { + assertEquals( + LogicalPlanDSL.window( + LogicalPlanDSL.relation("test"), + DSL.named("row_number", dsl.rowNumber()), + new WindowDefinition( + ImmutableList.of(), + ImmutableList.of())), + analyzer.analyze( + AstDSL.alias( + "row_number", + AstDSL.window( + AstDSL.function("row_number"), + ImmutableList.of(), + ImmutableList.of())), + analysisContext)); + } + @Test void should_return_original_child_if_project_item_not_windowed() { assertEquals( diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CurrentRowWindowFrameTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CurrentRowWindowFrameTest.java index 60ee25c19e..64d271dec8 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CurrentRowWindowFrameTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/window/CurrentRowWindowFrameTest.java @@ -44,6 +44,12 @@ class CurrentRowWindowFrameTest { ImmutableList.of(DSL.ref("state", STRING)), ImmutableList.of(ImmutablePair.of(DEFAULT_ASC, DSL.ref("age", INTEGER))))); + @Test + void test_iterator_methods() { + assertFalse(windowFrame.hasNext()); + assertTrue(windowFrame.next().isEmpty()); + } + @Test void should_return_new_partition_if_partition_by_field_value_changed() { PeekingIterator iterator = Iterators.peekingIterator( diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperatorTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperatorTest.java index 3180d4a396..61f0f0a9ae 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperatorTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/WindowOperatorTest.java @@ -95,6 +95,28 @@ void test_aggregate_window_function() { .done(); } + @SuppressWarnings("unchecked") + @Test + void test_aggregate_window_function_without_sort_key() { + window(new AggregateWindowFunction(dsl.sum(ref("response", INTEGER)))) + .expectNext(ImmutableMap.of( + "ip", "209.160.24.63", "action", "GET", "response", 200, "referer", "www.amazon.com", + "sum(response)", 1504)) + .expectNext(ImmutableMap.of( + "ip", "74.125.19.106", "action", "POST", "response", 500, + "sum(response)", 1504)) + .expectNext(ImmutableMap.of( + "ip", "74.125.19.106", "action", "POST", "response", 200, "referer", "www.google.com", + "sum(response)", 1504)) + .expectNext(ImmutableMap.of( + "ip", "112.111.162.4", "action", "GET", "response", 200, "referer", "www.amazon.com", + "sum(response)", 1504)) + .expectNext(ImmutableMap.of( + "ip", "209.160.24.63", "action", "GET", "response", 404, "referer", "www.amazon.com", + "sum(response)", 1504)) + .done(); + } + private WindowOperatorAssertion window(Expression windowFunction) { return new WindowOperatorAssertion(windowFunction); } diff --git a/docs/user/limitations/limitations.rst b/docs/user/limitations/limitations.rst index 923a2e6528..54b507ca79 100644 --- a/docs/user/limitations/limitations.rst +++ b/docs/user/limitations/limitations.rst @@ -66,6 +66,16 @@ For now, only the field defined in index is allowed, all the other calculated fi Another limitation is that currently window function cannot be nested in another expression, for example, ``CASE WHEN RANK() OVER(...) THEN ...``. +Workaround for both limitations mentioned above is using a sub-query in FROM clause:: + + SELECT + SUM(t.avg_flight_time) OVER(...) + FROM ( + SELECT OriginCountry, AVG(FlightTimeMin) AS avg_flight_time, + FROM kibana_sample_data_flights + GROUP BY OriginCountry + ) AS t + Limitations on Pagination ========================= diff --git a/integ-test/src/test/resources/correctness/queries/window.txt b/integ-test/src/test/resources/correctness/queries/window.txt index 68cb72bad2..8a1191d938 100644 --- a/integ-test/src/test/resources/correctness/queries/window.txt +++ b/integ-test/src/test/resources/correctness/queries/window.txt @@ -4,6 +4,11 @@ SELECT DistanceMiles, DENSE_RANK() OVER (ORDER BY DistanceMiles) AS rnk FROM kib SELECT DistanceMiles, ROW_NUMBER() OVER (ORDER BY DistanceMiles DESC) AS num FROM kibana_sample_data_flights SELECT DistanceMiles, RANK() OVER (ORDER BY DistanceMiles DESC) AS rnk FROM kibana_sample_data_flights SELECT DistanceMiles, DENSE_RANK() OVER (ORDER BY DistanceMiles DESC) AS rnk FROM kibana_sample_data_flights +SELECT DistanceMiles, COUNT(DistanceMiles) OVER () AS num FROM kibana_sample_data_flights +SELECT DistanceMiles, SUM(DistanceMiles) OVER () AS num FROM kibana_sample_data_flights +SELECT DistanceMiles, AVG(DistanceMiles) OVER () AS num FROM kibana_sample_data_flights +SELECT DistanceMiles, MAX(DistanceMiles) OVER () AS num FROM kibana_sample_data_flights +SELECT DistanceMiles, MIN(DistanceMiles) OVER () AS num FROM kibana_sample_data_flights SELECT FlightDelayMin, DistanceMiles, SUM(DistanceMiles) OVER (ORDER BY FlightDelayMin) AS num FROM kibana_sample_data_flights SELECT FlightDelayMin, DistanceMiles, AVG(DistanceMiles) OVER (ORDER BY FlightDelayMin) AS num FROM kibana_sample_data_flights SELECT FlightDelayMin, DistanceMiles, MAX(DistanceMiles) OVER (ORDER BY FlightDelayMin) AS num FROM kibana_sample_data_flights From 59c758291adc56e9c62be3648e07b918b8741747 Mon Sep 17 00:00:00 2001 From: Dai Date: Thu, 7 Jan 2021 10:10:49 -0800 Subject: [PATCH 16/16] Change README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 622b943281..a1e0ba02ae 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Here is a documentation list with features only available in this improved SQL q * [Aggregations](./docs/user/dql/aggregations.rst): aggregation over expression and more other features * [Complex queries](./docs/user/dql/complex.rst) * Improvement on Subqueries in FROM clause -* [Window functions](./docs/user/dql/window.rst): ranking window function support +* [Window functions](./docs/user/dql/window.rst): ranking and aggregate window function support To avoid impact on your side, normally you won't see any difference in query response. If you want to check if and why your query falls back to be handled by old SQL engine, please explain your query and check Elasticsearch log for "Request is falling back to old SQL engine due to ...".