From 3fa954c4c9584c7554f78f0c21171ccf5733610b Mon Sep 17 00:00:00 2001 From: lyndonb-bq Date: Tue, 22 Sep 2020 17:57:16 -0700 Subject: [PATCH] [1] Trying to fix commit --- README.md | 2 +- .../sql/analysis/Analyzer.java | 42 +- .../sql/analysis/ExpressionAnalyzer.java | 1 - .../sql/analysis/NamedExpressionAnalyzer.java | 61 + .../analysis/SelectExpressionAnalyzer.java | 37 +- .../sql/ast/AbstractNodeVisitor.java | 10 + .../sql/ast/dsl/AstDSL.java | 24 + .../sql/ast/expression/Function.java | 12 +- .../sql/ast/expression/Literal.java | 6 +- .../ast/expression/UnresolvedArgument.java | 49 + .../sql/ast/tree/Head.java | 58 + .../sql/ast/tree/RareTopN.java | 2 +- .../sql/executor/ExecutionEngine.java | 31 + .../sql/executor/Explain.java | 181 +++ .../sql/expression/DSL.java | 9 + .../sql/expression/ExpressionNodeVisitor.java | 4 + .../sql/expression/NamedExpression.java | 7 +- .../aggregation/NamedAggregator.java | 87 ++ .../expression/datetime/DateTimeFunction.java | 60 +- .../function/BuiltinFunctionName.java | 1 + .../sql/planner/DefaultImplementor.java | 11 + .../planner/logical/LogicalAggregation.java | 8 +- .../sql/planner/logical/LogicalHead.java | 47 + .../sql/planner/logical/LogicalPlanDSL.java | 9 +- .../logical/LogicalPlanNodeVisitor.java | 4 + .../sql/planner/logical/LogicalRareTopN.java | 2 +- .../planner/physical/AggregationOperator.java | 27 +- .../sql/planner/physical/HeadOperator.java | 114 ++ .../sql/planner/physical/PhysicalPlanDSL.java | 8 +- .../physical/PhysicalPlanNodeVisitor.java | 5 + .../planner/physical/RareTopNOperator.java | 2 +- .../sql/planner/physical/ValuesOperator.java | 2 + .../sql/analysis/AnalyzerTest.java | 119 +- .../analysis/NamedExpressionAnalyzerTest.java | 46 + .../sql/analysis/SelectAnalyzeTest.java | 10 +- .../SelectExpressionAnalyzerTest.java | 4 +- .../sql/executor/ExplainTest.java | 247 +++ .../expression/ExpressionNodeVisitorTest.java | 4 + .../datetime/DateTimeFunctionTest.java | 62 +- .../sql/planner/DefaultImplementorTest.java | 96 +- .../sql/planner/PlannerTest.java | 4 +- .../logical/LogicalPlanNodeVisitorTest.java | 35 +- .../physical/AggregationOperatorTest.java | 16 +- .../planner/physical/HeadOperatorTest.java | 152 ++ .../physical/PhysicalPlanNodeVisitorTest.java | 40 +- .../physical/PhysicalPlanTestBase.java | 55 +- .../physical/RareTopNOperatorTest.java | 15 + .../planner/physical/RenameOperatorTest.java | 5 +- .../ppl/RFC_ Pipe Processing Language.pdf | Bin 0 -> 316152 bytes docs/experiment/ppl/cmd/head.rst | 102 ++ docs/experiment/ppl/cmd/stats.rst | 12 + docs/experiment/ppl/index.rst | 2 + docs/user/dql/functions.rst | 1007 +++++++------ elasticsearch/build.gradle | 4 +- .../value/ElasticsearchExprValueFactory.java | 29 +- .../ElasticsearchExecutionEngine.java | 27 + .../ElasticsearchExecutionProtector.java | 11 + .../request/ElasticsearchQueryRequest.java | 19 +- .../request/ElasticsearchRequest.java | 7 + .../request/ElasticsearchScrollRequest.java | 8 +- ...lasticsearchAggregationResponseParser.java | 92 ++ .../response/ElasticsearchResponse.java | 68 +- .../storage/ElasticsearchIndex.java | 32 +- .../storage/ElasticsearchIndexScan.java | 33 +- .../script/ExpressionScriptEngine.java | 10 +- .../storage/script/ScriptUtils.java | 41 + .../aggregation/AggregationQueryBuilder.java | 92 ++ .../ExpressionAggregationScript.java | 69 + .../ExpressionAggregationScriptFactory.java | 48 + ...xpressionAggregationScriptLeafFactory.java | 66 + .../dsl/AggregationBuilderHelper.java | 63 + .../dsl/BucketAggregationBuilder.java | 54 + .../dsl/MetricAggregationBuilder.java | 80 + .../storage/script/core/ExpressionScript.java | 177 +++ .../script/filter/ExpressionFilterScript.java | 122 +- .../client/ElasticsearchNodeClientTest.java | 28 +- .../client/ElasticsearchRestClientTest.java | 34 +- .../ElasticsearchExprValueFactoryTest.java | 5 + .../ElasticsearchExecutionEngineTest.java | 52 + .../ElasticsearchExecutionProtectorTest.java | 39 +- .../ElasticsearchQueryRequestTest.java | 11 +- .../ElasticsearchScrollRequestTest.java | 11 +- .../response/AggregationResponseUtils.java | 102 ++ ...icsearchAggregationResponseParserTest.java | 160 ++ .../response/ElasticsearchResponseTest.java | 106 +- .../storage/ElasticsearchIndexScanTest.java | 22 +- .../storage/ElasticsearchIndexTest.java | 98 +- .../script/ExpressionScriptEngineTest.java | 3 +- .../AggregationQueryBuilderTest.java | 263 ++++ ...xpressionAggregationScriptFactoryTest.java | 80 + .../ExpressionAggregationScriptTest.java | 182 +++ .../dsl/BucketAggregationBuilderTest.java | 99 ++ .../dsl/MetricAggregationBuilderTest.java | 139 ++ integ-test/build.gradle | 6 + .../runner/connection/JDBCConnection.java | 4 +- .../sql/correctness/testset/TestDataSet.java | 2 + .../sql/ppl/ExplainIT.java | 51 + .../sql/ppl/HeadCommandIT.java | 87 ++ .../sql/ppl/PPLIntegTestCase.java | 13 +- .../sql/ppl/StandaloneIT.java | 2 +- .../sql/ppl/StatsCommandIT.java | 27 +- .../sql/sql/DateTimeFunctionIT.java | 67 + .../expressions/date_and_time_functions.txt | 0 ...nctions.txt => mathematical_functions.txt} | 1 + .../expectedOutput/ppl/explain_output.json | 60 + .../sql/legacy/plugin/RestSQLQueryAction.java | 48 +- .../legacy/plugin/RestSQLQueryActionTest.java | 8 +- .../unittest/SqlRequestFactoryTest.java | 16 + .../sql/legacy/util/AggregationUtils.java | 7 +- .../request/PPLQueryRequestFactory.java | 5 +- .../sql/plugin/rest/RestPPLQueryAction.java | 59 +- ppl/src/main/antlr/OpenDistroPPLLexer.g4 | 4 + ppl/src/main/antlr/OpenDistroPPLParser.g4 | 11 +- .../sql/ppl/PPLService.java | 43 +- .../sql/ppl/domain/PPLQueryRequest.java | 12 +- .../sql/ppl/parser/AstBuilder.java | 53 +- .../sql/ppl/utils/ArgumentFactory.java | 25 + .../sql/ppl/PPLServiceTest.java | 79 +- .../sql/ppl/domain/PPLQueryRequestTest.java | 12 +- .../sql/ppl/parser/AstBuilderTest.java | 114 +- .../ppl/parser/AstExpressionBuilderTest.java | 23 +- .../sql/ppl/utils/ArgumentFactoryTest.java | 7 +- ...lasticsearch-sql.release-notes-1.10.1.0.md | 3 +- sql-jdbc/src/main/java/ESConnect.java | 36 + .../jdbc/types/BaseTypeConverter.java | 2 + .../jdbc/types/ElasticsearchType.java | 7 +- .../jdbc/types/TimeType.java | 73 + .../jdbc/types/TypeConverters.java | 28 +- .../jdbc/types/TimeTypeTest.java | 41 + sql/src/main/antlr/OpenDistroSQLLexer.g4 | 3 + sql/src/main/antlr/OpenDistroSQLParser.g4 | 2 +- .../sql/sql/SQLService.java | 14 + .../sql/sql/domain/SQLQueryRequest.java | 17 +- .../sql/sql/parser/AstAggregationBuilder.java | 12 +- .../sql/sql/parser/AstBuilder.java | 24 +- .../sql/sql/parser/ParserUtils.java | 38 + .../parser/context/QuerySpecification.java | 19 +- .../sql/sql/SQLServiceTest.java | 42 + .../sql/sql/domain/SQLQueryRequestTest.java | 5 +- .../sql/parser/AstAggregationBuilderTest.java | 39 +- .../sql/sql/parser/AstBuilderTest.java | 66 +- .../context/QuerySpecificationTest.java | 13 +- .../.cypress/integration/ui.spec.js | 8 +- .../.cypress/plugins/index.js | 0 .../.cypress/support/commands.js | 0 .../.cypress/support/index.js | 0 .../.cypress/utils/constants.js | 0 {sql-workbench => workbench}/.gitignore | 0 .../.kibana-plugin-helpers.json | 0 .../CODE_OF_CONDUCT.md | 0 {sql-workbench => workbench}/CONTRIBUTING.md | 0 {sql-workbench => workbench}/CONTRIBUTORS.md | 0 {sql-workbench => workbench}/LICENSE.TXT | 0 {sql-workbench => workbench}/NOTICE | 0 {sql-workbench => workbench}/README.md | 12 +- {sql-workbench => workbench}/THIRD-PARTY | 0 {sql-workbench => workbench}/babel.config.js | 0 {sql-workbench => workbench}/cypress.json | 0 {sql-workbench => workbench}/index.js | 12 +- {sql-workbench => workbench}/package.json | 10 +- .../public/ace-themes/sql_console.css | 0 .../public/ace-themes/sql_console.js | 0 {sql-workbench => workbench}/public/app.js | 0 {sql-workbench => workbench}/public/app.scss | 16 - .../public/components/Header/Header.test.tsx | 0 .../public/components/Header/Header.tsx | 0 .../Header/__snapshots__/Header.test.tsx.snap | 0 .../Main/__snapshots__/main.test.tsx.snap | 1335 ++++++++--------- .../public/components/Main/index.ts | 0 .../public/components/Main/main.test.tsx | 14 +- .../public/components/Main/main.tsx | 138 +- .../QueryEditor/QueryEditor.test.tsx | 26 +- .../components/QueryEditor/QueryEditor.tsx | 118 +- .../__snapshots__/QueryEditor.test.tsx.snap | 276 +--- .../components/QueryLanguageSwitch/Switch.tsx | 64 + .../QueryResults/QueryResults.test.tsx | 0 .../components/QueryResults/QueryResults.tsx | 0 .../QueryResults/QueryResultsBody.test.tsx | 0 .../QueryResults/QueryResultsBody.tsx | 0 .../__snapshots__/QueryResults.test.tsx.snap | 498 +++--- .../QueryResultsBody.test.tsx.snap | 73 +- .../public/icons/minus.svg | 0 .../public/icons/plus.svg | 0 .../public/icons/sql.svg | 0 .../public/less/main.less | 0 .../public/utils/constants.ts | 0 .../public/utils/utils.ts | 0 .../sql-workbench.release-notes-1.7.0.0.md | 0 .../sql-workbench.release-notes-1.8.0.0.md | 0 .../sql-workbench.release-notes-1.9.0.0.md | 0 .../sql-workbench.release-notes-1.9.0.1.md | 0 .../sql-workbench.release-notes-1.9.0.2.md | 0 .../server/clusters/index.js | 0 .../server/clusters/sql/createSqlCluster.js | 0 .../server/clusters/sql/sqlPlugin.js | 42 +- .../server/routes/query.ts | 13 +- .../server/routes/translate.ts | 10 +- .../server/services/QueryService.ts | 11 +- .../server/services/TranslateService.ts | 18 +- .../server/services/utils/constants.ts | 6 +- .../server/utils/constants.ts | 3 +- .../test/jest.config.js | 0 .../test/mocks/browserServicesMock.ts | 0 .../test/mocks/httpClientMock.ts | 0 .../test/mocks/index.ts | 0 .../test/mocks/mockData.ts | 0 .../test/mocks/styleMock.ts | 0 .../test/polyfills.ts | 0 .../test/polyfills/mutationObserver.js | 0 .../test/setup.jest.ts | 0 .../test/setupTests.ts | 0 {sql-workbench => workbench}/tsconfig.json | 0 {sql-workbench => workbench}/tslint.yaml | 0 {sql-workbench => workbench}/yarn.lock | 1319 ++++++++++++---- 214 files changed, 8167 insertions(+), 2727 deletions(-) create mode 100644 core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/NamedExpressionAnalyzer.java create mode 100644 core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/UnresolvedArgument.java create mode 100644 core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/tree/Head.java create mode 100644 core/src/main/java/com/amazon/opendistroforelasticsearch/sql/executor/Explain.java create mode 100644 core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/aggregation/NamedAggregator.java create mode 100644 core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalHead.java create mode 100644 core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/HeadOperator.java create mode 100644 core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/NamedExpressionAnalyzerTest.java create mode 100644 core/src/test/java/com/amazon/opendistroforelasticsearch/sql/executor/ExplainTest.java create mode 100644 core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/HeadOperatorTest.java create mode 100644 docs/experiment/ppl/RFC_ Pipe Processing Language.pdf create mode 100644 docs/experiment/ppl/cmd/head.rst create mode 100644 elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/ElasticsearchAggregationResponseParser.java create mode 100644 elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/ScriptUtils.java create mode 100644 elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/AggregationQueryBuilder.java create mode 100644 elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScript.java create mode 100644 elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScriptFactory.java create mode 100644 elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScriptLeafFactory.java create mode 100644 elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/AggregationBuilderHelper.java create mode 100644 elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/BucketAggregationBuilder.java create mode 100644 elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/MetricAggregationBuilder.java create mode 100644 elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/core/ExpressionScript.java create mode 100644 elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/AggregationResponseUtils.java create mode 100644 elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/ElasticsearchAggregationResponseParserTest.java create mode 100644 elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/AggregationQueryBuilderTest.java create mode 100644 elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScriptFactoryTest.java create mode 100644 elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScriptTest.java create mode 100644 elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/BucketAggregationBuilderTest.java create mode 100644 elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/MetricAggregationBuilderTest.java create mode 100644 integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/ExplainIT.java create mode 100644 integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/HeadCommandIT.java create mode 100644 integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/DateTimeFunctionIT.java create mode 100644 integ-test/src/test/resources/correctness/expressions/date_and_time_functions.txt rename integ-test/src/test/resources/correctness/expressions/{functions.txt => mathematical_functions.txt} (96%) create mode 100644 integ-test/src/test/resources/expectedOutput/ppl/explain_output.json create mode 100644 sql-jdbc/src/main/java/ESConnect.java create mode 100644 sql-jdbc/src/main/java/com/amazon/opendistroforelasticsearch/jdbc/types/TimeType.java create mode 100644 sql-jdbc/src/test/java/com/amazon/opendistroforelasticsearch/jdbc/types/TimeTypeTest.java create mode 100644 sql/src/main/java/com/amazon/opendistroforelasticsearch/sql/sql/parser/ParserUtils.java rename {sql-workbench => workbench}/.cypress/integration/ui.spec.js (96%) rename {sql-workbench => workbench}/.cypress/plugins/index.js (100%) rename {sql-workbench => workbench}/.cypress/support/commands.js (100%) rename {sql-workbench => workbench}/.cypress/support/index.js (100%) rename {sql-workbench => workbench}/.cypress/utils/constants.js (100%) rename {sql-workbench => workbench}/.gitignore (100%) rename {sql-workbench => workbench}/.kibana-plugin-helpers.json (100%) rename {sql-workbench => workbench}/CODE_OF_CONDUCT.md (100%) rename {sql-workbench => workbench}/CONTRIBUTING.md (100%) rename {sql-workbench => workbench}/CONTRIBUTORS.md (100%) rename {sql-workbench => workbench}/LICENSE.TXT (100%) rename {sql-workbench => workbench}/NOTICE (100%) rename {sql-workbench => workbench}/README.md (83%) rename {sql-workbench => workbench}/THIRD-PARTY (100%) rename {sql-workbench => workbench}/babel.config.js (100%) rename {sql-workbench => workbench}/cypress.json (100%) rename {sql-workbench => workbench}/index.js (87%) rename {sql-workbench => workbench}/package.json (95%) rename {sql-workbench => workbench}/public/ace-themes/sql_console.css (100%) rename {sql-workbench => workbench}/public/ace-themes/sql_console.js (100%) rename {sql-workbench => workbench}/public/app.js (100%) rename {sql-workbench => workbench}/public/app.scss (89%) rename {sql-workbench => workbench}/public/components/Header/Header.test.tsx (100%) rename {sql-workbench => workbench}/public/components/Header/Header.tsx (100%) rename {sql-workbench => workbench}/public/components/Header/__snapshots__/Header.test.tsx.snap (100%) rename {sql-workbench => workbench}/public/components/Main/__snapshots__/main.test.tsx.snap (73%) rename {sql-workbench => workbench}/public/components/Main/index.ts (100%) rename {sql-workbench => workbench}/public/components/Main/main.test.tsx (90%) rename {sql-workbench => workbench}/public/components/Main/main.tsx (84%) rename {sql-workbench => workbench}/public/components/QueryEditor/QueryEditor.test.tsx (77%) rename {sql-workbench => workbench}/public/components/QueryEditor/QueryEditor.tsx (63%) rename {sql-workbench => workbench}/public/components/QueryEditor/__snapshots__/QueryEditor.test.tsx.snap (57%) create mode 100644 workbench/public/components/QueryLanguageSwitch/Switch.tsx rename {sql-workbench => workbench}/public/components/QueryResults/QueryResults.test.tsx (100%) rename {sql-workbench => workbench}/public/components/QueryResults/QueryResults.tsx (100%) rename {sql-workbench => workbench}/public/components/QueryResults/QueryResultsBody.test.tsx (100%) rename {sql-workbench => workbench}/public/components/QueryResults/QueryResultsBody.tsx (100%) rename {sql-workbench => workbench}/public/components/QueryResults/__snapshots__/QueryResults.test.tsx.snap (97%) rename {sql-workbench => workbench}/public/components/QueryResults/__snapshots__/QueryResultsBody.test.tsx.snap (99%) rename {sql-workbench => workbench}/public/icons/minus.svg (100%) rename {sql-workbench => workbench}/public/icons/plus.svg (100%) rename {sql-workbench => workbench}/public/icons/sql.svg (100%) rename {sql-workbench => workbench}/public/less/main.less (100%) rename {sql-workbench => workbench}/public/utils/constants.ts (100%) rename {sql-workbench => workbench}/public/utils/utils.ts (100%) rename {sql-workbench => workbench}/release-notes/sql-workbench.release-notes-1.7.0.0.md (100%) rename {sql-workbench => workbench}/release-notes/sql-workbench.release-notes-1.8.0.0.md (100%) rename {sql-workbench => workbench}/release-notes/sql-workbench.release-notes-1.9.0.0.md (100%) rename {sql-workbench => workbench}/release-notes/sql-workbench.release-notes-1.9.0.1.md (100%) rename {sql-workbench => workbench}/release-notes/sql-workbench.release-notes-1.9.0.2.md (100%) rename {sql-workbench => workbench}/server/clusters/index.js (100%) rename {sql-workbench => workbench}/server/clusters/sql/createSqlCluster.js (100%) rename {sql-workbench => workbench}/server/clusters/sql/sqlPlugin.js (62%) rename {sql-workbench => workbench}/server/routes/query.ts (78%) rename {sql-workbench => workbench}/server/routes/translate.ts (80%) rename {sql-workbench => workbench}/server/services/QueryService.ts (85%) rename {sql-workbench => workbench}/server/services/TranslateService.ts (67%) rename {sql-workbench => workbench}/server/services/utils/constants.ts (80%) rename {sql-workbench => workbench}/server/utils/constants.ts (87%) rename {sql-workbench => workbench}/test/jest.config.js (100%) rename {sql-workbench => workbench}/test/mocks/browserServicesMock.ts (100%) rename {sql-workbench => workbench}/test/mocks/httpClientMock.ts (100%) rename {sql-workbench => workbench}/test/mocks/index.ts (100%) rename {sql-workbench => workbench}/test/mocks/mockData.ts (100%) rename {sql-workbench => workbench}/test/mocks/styleMock.ts (100%) rename {sql-workbench => workbench}/test/polyfills.ts (100%) rename {sql-workbench => workbench}/test/polyfills/mutationObserver.js (100%) rename {sql-workbench => workbench}/test/setup.jest.ts (100%) rename {sql-workbench => workbench}/test/setupTests.ts (100%) rename {sql-workbench => workbench}/tsconfig.json (100%) rename {sql-workbench => workbench}/tslint.yaml (100%) rename {sql-workbench => workbench}/yarn.lock (89%) diff --git a/README.md b/README.md index 12228c473f..ea22e4669e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The following projects have been merged into this repository as separate folders * [SQL CLI](https://github.com/opendistro-for-elasticsearch/sql/tree/master/sql-cli) * [SQL JDBC](https://github.com/opendistro-for-elasticsearch/sql/tree/master/sql-jdbc) * [SQL ODBC](https://github.com/opendistro-for-elasticsearch/sql/tree/master/sql-odbc) -* [SQL Workbench](https://github.com/opendistro-for-elasticsearch/sql/tree/master/sql-workbench) +* [SQL Workbench](https://github.com/opendistro-for-elasticsearch/sql/tree/master/workbench) ## Documentation diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/Analyzer.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/Analyzer.java index a3c3f37365..054ae0e004 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/Analyzer.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/Analyzer.java @@ -25,11 +25,13 @@ import com.amazon.opendistroforelasticsearch.sql.ast.expression.Let; import com.amazon.opendistroforelasticsearch.sql.ast.expression.Literal; import com.amazon.opendistroforelasticsearch.sql.ast.expression.Map; +import com.amazon.opendistroforelasticsearch.sql.ast.expression.UnresolvedArgument; import com.amazon.opendistroforelasticsearch.sql.ast.expression.UnresolvedExpression; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Aggregation; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Dedupe; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Eval; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Filter; +import com.amazon.opendistroforelasticsearch.sql.ast.tree.Head; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Project; import com.amazon.opendistroforelasticsearch.sql.ast.tree.RareTopN; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Relation; @@ -46,10 +48,12 @@ 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.aggregation.NamedAggregator; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalAggregation; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalDedupe; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalEval; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalFilter; +import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalHead; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlan; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalProject; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalRareTopN; @@ -80,6 +84,8 @@ public class Analyzer extends AbstractNodeVisitor private final SelectExpressionAnalyzer selectExpressionAnalyzer; + private final NamedExpressionAnalyzer namedExpressionAnalyzer; + private final StorageEngine storageEngine; /** @@ -91,6 +97,7 @@ public Analyzer( this.expressionAnalyzer = expressionAnalyzer; this.storageEngine = storageEngine; this.selectExpressionAnalyzer = new SelectExpressionAnalyzer(expressionAnalyzer); + this.namedExpressionAnalyzer = new NamedExpressionAnalyzer(expressionAnalyzer); } public LogicalPlan analyze(UnresolvedPlan unresolved, AnalysisContext context) { @@ -153,25 +160,27 @@ public LogicalPlan visitRename(Rename node, AnalysisContext context) { @Override public LogicalPlan visitAggregation(Aggregation node, AnalysisContext context) { final LogicalPlan child = node.getChild().get(0).accept(this, context); - ImmutableList.Builder aggregatorBuilder = new ImmutableList.Builder<>(); + ImmutableList.Builder aggregatorBuilder = new ImmutableList.Builder<>(); for (UnresolvedExpression expr : node.getAggExprList()) { - aggregatorBuilder.add((Aggregator) expressionAnalyzer.analyze(expr, context)); + NamedExpression aggExpr = namedExpressionAnalyzer.analyze(expr, context); + aggregatorBuilder + .add(new NamedAggregator(aggExpr.getName(), (Aggregator) aggExpr.getDelegated())); } - ImmutableList aggregators = aggregatorBuilder.build(); + ImmutableList aggregators = aggregatorBuilder.build(); - ImmutableList.Builder groupbyBuilder = new ImmutableList.Builder<>(); + ImmutableList.Builder groupbyBuilder = new ImmutableList.Builder<>(); for (UnresolvedExpression expr : node.getGroupExprList()) { - groupbyBuilder.add(expressionAnalyzer.analyze(expr, context)); + groupbyBuilder.add(namedExpressionAnalyzer.analyze(expr, context)); } - ImmutableList groupBys = groupbyBuilder.build(); + ImmutableList groupBys = groupbyBuilder.build(); // new context context.push(); TypeEnvironment newEnv = context.peek(); aggregators.forEach(aggregator -> newEnv.define(new Symbol(Namespace.FIELD_NAME, - aggregator.toString()), aggregator.type())); + aggregator.getName()), aggregator.type())); groupBys.forEach(group -> newEnv.define(new Symbol(Namespace.FIELD_NAME, - group.toString()), group.type())); + group.getName()), group.type())); return new LogicalAggregation(child, aggregators, groupBys); } @@ -311,6 +320,23 @@ public LogicalPlan visitDedupe(Dedupe node, AnalysisContext context) { consecutive); } + /** + * Build {@link LogicalHead}. + */ + public LogicalPlan visitHead(Head node, AnalysisContext context) { + LogicalPlan child = node.getChild().get(0).accept(this, context); + List options = node.getOptions(); + Boolean keeplast = (Boolean) getOptionAsLiteral(options, 0).getValue(); + Expression whileExpr = expressionAnalyzer.analyze(options.get(1).getValue(), context); + Integer number = (Integer) getOptionAsLiteral(options, 2).getValue(); + + return new LogicalHead(child, keeplast, whileExpr, number); + } + + private static Literal getOptionAsLiteral(List options, int optionIdx) { + return (Literal) options.get(optionIdx).getValue(); + } + @Override public LogicalPlan visitValues(Values node, AnalysisContext context) { List> values = node.getValues(); 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 7b14f3a21b..3aec63a6be 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 @@ -25,7 +25,6 @@ import com.amazon.opendistroforelasticsearch.sql.ast.expression.Field; import com.amazon.opendistroforelasticsearch.sql.ast.expression.Function; import com.amazon.opendistroforelasticsearch.sql.ast.expression.Interval; -import com.amazon.opendistroforelasticsearch.sql.ast.expression.IntervalUnit; import com.amazon.opendistroforelasticsearch.sql.ast.expression.Literal; import com.amazon.opendistroforelasticsearch.sql.ast.expression.Not; import com.amazon.opendistroforelasticsearch.sql.ast.expression.Or; diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/NamedExpressionAnalyzer.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/NamedExpressionAnalyzer.java new file mode 100644 index 0000000000..b7ac75519f --- /dev/null +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/NamedExpressionAnalyzer.java @@ -0,0 +1,61 @@ +/* + * + * 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.analysis; + +import com.amazon.opendistroforelasticsearch.sql.ast.AbstractNodeVisitor; +import com.amazon.opendistroforelasticsearch.sql.ast.expression.Alias; +import com.amazon.opendistroforelasticsearch.sql.ast.expression.QualifiedName; +import com.amazon.opendistroforelasticsearch.sql.ast.expression.UnresolvedExpression; +import com.amazon.opendistroforelasticsearch.sql.expression.DSL; +import com.amazon.opendistroforelasticsearch.sql.expression.NamedExpression; +import lombok.RequiredArgsConstructor; + +/** + * Analyze the Alias node in the {@link AnalysisContext} to construct the list of + * {@link NamedExpression}. + */ +@RequiredArgsConstructor +public class NamedExpressionAnalyzer extends + AbstractNodeVisitor { + private final ExpressionAnalyzer expressionAnalyzer; + + /** + * Analyze Select fields. + */ + public NamedExpression analyze(UnresolvedExpression expression, + AnalysisContext analysisContext) { + return expression.accept(this, analysisContext); + } + + @Override + public NamedExpression visitAlias(Alias node, AnalysisContext context) { + return DSL.named( + unqualifiedNameIfFieldOnly(node, context), + node.getDelegated().accept(expressionAnalyzer, context), + node.getAlias()); + } + + private String unqualifiedNameIfFieldOnly(Alias node, AnalysisContext context) { + UnresolvedExpression selectItem = node.getDelegated(); + if (selectItem instanceof QualifiedName) { + QualifierAnalyzer qualifierAnalyzer = new QualifierAnalyzer(context); + return qualifierAnalyzer.unqualified((QualifiedName) selectItem); + } + return node.getName(); + } +} 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 c49cbca552..ed40b830d3 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 @@ -20,9 +20,11 @@ import com.amazon.opendistroforelasticsearch.sql.analysis.symbol.Namespace; import com.amazon.opendistroforelasticsearch.sql.analysis.symbol.Symbol; import com.amazon.opendistroforelasticsearch.sql.ast.AbstractNodeVisitor; +import com.amazon.opendistroforelasticsearch.sql.ast.expression.AggregateFunction; import com.amazon.opendistroforelasticsearch.sql.ast.expression.Alias; import com.amazon.opendistroforelasticsearch.sql.ast.expression.AllFields; import com.amazon.opendistroforelasticsearch.sql.ast.expression.Field; +import com.amazon.opendistroforelasticsearch.sql.ast.expression.Function; import com.amazon.opendistroforelasticsearch.sql.ast.expression.QualifiedName; import com.amazon.opendistroforelasticsearch.sql.ast.expression.UnresolvedExpression; import com.amazon.opendistroforelasticsearch.sql.data.type.ExprType; @@ -67,23 +69,42 @@ public List visitField(Field node, AnalysisContext context) { @Override public List visitAlias(Alias node, AnalysisContext context) { - Expression expr = referenceIfSymbolDefined(node.getDelegated(), context); + Expression expr = referenceIfSymbolDefined(node, context); return Collections.singletonList(DSL.named( unqualifiedNameIfFieldOnly(node, context), expr, node.getAlias())); } - private Expression referenceIfSymbolDefined(UnresolvedExpression expr, + /** + * The Alias could be + * 1. SELECT name, AVG(age) FROM s BY name -> + * Project(Alias("name", expr), Alias("AVG(age)", aggExpr)) + * Agg(Alias("AVG(age)", aggExpr)) + * 2. SELECT length(name), AVG(age) FROM s BY length(name) + * Project(Alias("name", expr), Alias("AVG(age)", aggExpr)) + * Agg(Alias("AVG(age)", aggExpr)) + * 3. SELECT length(name) as l, AVG(age) FROM s BY l + * Project(Alias("name", expr, l), Alias("AVG(age)", aggExpr)) + * Agg(Alias("AVG(age)", aggExpr), Alias("length(name)", groupExpr)) + */ + private Expression referenceIfSymbolDefined(Alias expr, AnalysisContext context) { + UnresolvedExpression delegatedExpr = expr.getDelegated(); try { - // Since resolved aggregator.toString() is used as symbol name, unresolved expression - // needs to be analyzed too to get toString() name for consistency - String symbolName = expressionAnalyzer.analyze(expr, context).toString(); - ExprType type = context.peek().resolve(new Symbol(Namespace.FIELD_NAME, symbolName)); - return DSL.ref(symbolName, type); + if ((delegatedExpr instanceof AggregateFunction) + || (delegatedExpr instanceof Function)) { + ExprType type = context.peek().resolve(new Symbol(Namespace.FIELD_NAME, expr.getName())); + return DSL.ref(expr.getName(), type); + } else { + // Since resolved aggregator.toString() is used as symbol name, unresolved expression + // needs to be analyzed too to get toString() name for consistency + String symbolName = expressionAnalyzer.analyze(delegatedExpr, context).toString(); + ExprType type = context.peek().resolve(new Symbol(Namespace.FIELD_NAME, symbolName)); + return DSL.ref(symbolName, type); + } } catch (SemanticCheckException e) { - return expr.accept(expressionAnalyzer, context); + return delegatedExpr.accept(expressionAnalyzer, context); } } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/AbstractNodeVisitor.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/AbstractNodeVisitor.java index ac099b5f94..a6b640bdb8 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/AbstractNodeVisitor.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/AbstractNodeVisitor.java @@ -33,12 +33,14 @@ import com.amazon.opendistroforelasticsearch.sql.ast.expression.Not; import com.amazon.opendistroforelasticsearch.sql.ast.expression.Or; import com.amazon.opendistroforelasticsearch.sql.ast.expression.QualifiedName; +import com.amazon.opendistroforelasticsearch.sql.ast.expression.UnresolvedArgument; import com.amazon.opendistroforelasticsearch.sql.ast.expression.UnresolvedAttribute; import com.amazon.opendistroforelasticsearch.sql.ast.expression.Xor; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Aggregation; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Dedupe; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Eval; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Filter; +import com.amazon.opendistroforelasticsearch.sql.ast.tree.Head; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Project; import com.amazon.opendistroforelasticsearch.sql.ast.tree.RareTopN; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Relation; @@ -179,6 +181,10 @@ public T visitDedupe(Dedupe node, C context) { return visitChildren(node, context); } + public T visitHead(Head node, C context) { + return visitChildren(node, context); + } + public T visitRareTopN(RareTopN node, C context) { return visitChildren(node, context); } @@ -198,4 +204,8 @@ public T visitAllFields(AllFields node, C context) { public T visitInterval(Interval node, C context) { return visitChildren(node, context); } + + public T visitUnresolvedArgument(UnresolvedArgument node, C context) { + return visitChildren(node, context); + } } 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 2d6b70920c..8d04fcb3ac 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 @@ -33,6 +33,7 @@ import com.amazon.opendistroforelasticsearch.sql.ast.expression.Not; import com.amazon.opendistroforelasticsearch.sql.ast.expression.Or; import com.amazon.opendistroforelasticsearch.sql.ast.expression.QualifiedName; +import com.amazon.opendistroforelasticsearch.sql.ast.expression.UnresolvedArgument; import com.amazon.opendistroforelasticsearch.sql.ast.expression.UnresolvedAttribute; import com.amazon.opendistroforelasticsearch.sql.ast.expression.UnresolvedExpression; import com.amazon.opendistroforelasticsearch.sql.ast.expression.Xor; @@ -40,6 +41,7 @@ import com.amazon.opendistroforelasticsearch.sql.ast.tree.Dedupe; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Eval; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Filter; +import com.amazon.opendistroforelasticsearch.sql.ast.tree.Head; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Project; import com.amazon.opendistroforelasticsearch.sql.ast.tree.RareTopN; import com.amazon.opendistroforelasticsearch.sql.ast.tree.RareTopN.CommandType; @@ -214,6 +216,10 @@ public static Argument argument(String argName, Literal argValue) { return new Argument(argName, argValue); } + public static UnresolvedArgument unresolvedArg(String argName, UnresolvedExpression argValue) { + return new UnresolvedArgument(argName, argValue); + } + public static UnresolvedExpression field(UnresolvedExpression field) { return new Field((QualifiedName) field); } @@ -254,6 +260,10 @@ public static List exprList(Argument... exprList) { return Arrays.asList(exprList); } + public static List unresolvedArgList(UnresolvedArgument... exprList) { + return Arrays.asList(exprList); + } + public static List defaultFieldsArgs() { return exprList(argument("exclude", booleanLiteral(false))); } @@ -299,6 +309,20 @@ public static Dedupe dedupe(UnresolvedPlan input, List options, Field. return new Dedupe(input, options, Arrays.asList(fields)); } + public static Head head(UnresolvedPlan input, List options) { + return new Head(input, options); + } + + /** + * Default Head Command Args. + */ + public static List defaultHeadArgs() { + return unresolvedArgList( + unresolvedArg("keeplast", booleanLiteral(true)), + unresolvedArg("whileExpr", booleanLiteral(true)), + unresolvedArg("number", intLiteral(10))); + } + public static List defaultTopArgs() { return exprList(argument("noOfResults", intLiteral(10))); } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/Function.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/Function.java index 1732734512..d0edb4a21a 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/Function.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/Function.java @@ -18,17 +18,17 @@ import com.amazon.opendistroforelasticsearch.sql.ast.AbstractNodeVisitor; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; -import lombok.ToString; + /** * Expression node of scalar function. * Params include function name (@funcName) and function arguments (@funcArgs) */ @Getter -@ToString @EqualsAndHashCode(callSuper = false) @RequiredArgsConstructor public class Function extends UnresolvedExpression { @@ -44,4 +44,12 @@ public List getChild() { public R accept(AbstractNodeVisitor nodeVisitor, C context) { return nodeVisitor.visitFunction(this, context); } + + @Override + public String toString() { + return String.format("%s(%s)", funcName, + funcArgs.stream() + .map(Object::toString) + .collect(Collectors.joining(", "))); + } } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/Literal.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/Literal.java index 5a0f60175c..d76c8a8a34 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/Literal.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/Literal.java @@ -29,7 +29,6 @@ * literal data type (@type) which can be selected from {@link DataType}. */ @Getter -@ToString @EqualsAndHashCode(callSuper = false) @RequiredArgsConstructor public class Literal extends UnresolvedExpression { @@ -46,4 +45,9 @@ public List getChild() { public R accept(AbstractNodeVisitor nodeVisitor, C context) { return nodeVisitor.visitLiteral(this, context); } + + @Override + public String toString() { + return value.toString(); + } } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/UnresolvedArgument.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/UnresolvedArgument.java new file mode 100644 index 0000000000..471686c429 --- /dev/null +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/UnresolvedArgument.java @@ -0,0 +1,49 @@ +/* + * 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.ast.expression; + +import com.amazon.opendistroforelasticsearch.sql.ast.AbstractNodeVisitor; +import java.util.Arrays; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * Argument. + */ +@Getter +@ToString +@EqualsAndHashCode(callSuper = false) +public class UnresolvedArgument extends UnresolvedExpression { + private final String argName; + private final UnresolvedExpression value; + + public UnresolvedArgument(String argName, UnresolvedExpression value) { + this.argName = argName; + this.value = value; + } + + @Override + public List getChild() { + return Arrays.asList(value); + } + + @Override + public R accept(AbstractNodeVisitor nodeVisitor, C context) { + return nodeVisitor.visitUnresolvedArgument(this, context); + } +} diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/tree/Head.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/tree/Head.java new file mode 100644 index 0000000000..89a5b36e67 --- /dev/null +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/tree/Head.java @@ -0,0 +1,58 @@ +/* + * 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.ast.tree; + +import com.amazon.opendistroforelasticsearch.sql.ast.AbstractNodeVisitor; +import com.amazon.opendistroforelasticsearch.sql.ast.expression.UnresolvedArgument; +import com.google.common.collect.ImmutableList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +/** + * AST node represent Head operation. + */ +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = false) +@RequiredArgsConstructor +@AllArgsConstructor +public class Head extends UnresolvedPlan { + + private UnresolvedPlan child; + private final List options; + + @Override + public Head attach(UnresolvedPlan child) { + this.child = child; + return this; + } + + @Override + public List getChild() { + return ImmutableList.of(this.child); + } + + @Override + public T accept(AbstractNodeVisitor nodeVisitor, C context) { + return nodeVisitor.visitHead(this, context); + } +} diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/tree/RareTopN.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/tree/RareTopN.java index 8f04d6bea1..33c6989966 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/tree/RareTopN.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/tree/RareTopN.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * 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. diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/executor/ExecutionEngine.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/executor/ExecutionEngine.java index d75379ee1e..87e9c1499b 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/executor/ExecutionEngine.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/executor/ExecutionEngine.java @@ -21,7 +21,10 @@ import com.amazon.opendistroforelasticsearch.sql.data.type.ExprType; import com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlan; import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.RequiredArgsConstructor; /** * Execution engine that encapsulates execution details. @@ -36,6 +39,16 @@ public interface ExecutionEngine { */ void execute(PhysicalPlan plan, ResponseListener listener); + /** + * Explain physical plan and call back response listener. The reason why this has to + * be part of execution engine interface is that the physical plan probably needs to + * be executed to get more info for profiling, such as actual execution time, rows fetched etc. + * + * @param plan physical plan to explain + * @param listener response listener + */ + void explain(PhysicalPlan plan, ResponseListener listener); + /** * Data class that encapsulates ExprValue. */ @@ -57,4 +70,22 @@ public static class Column { } } + /** + * Data class that encapsulates explain result. This can help decouple core engine + * from concrete explain response format. + */ + @Data + class ExplainResponse { + private final ExplainResponseNode root; + } + + @AllArgsConstructor + @Data + @RequiredArgsConstructor + class ExplainResponseNode { + private final String name; + private Map description; + private List children; + } + } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/executor/Explain.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/executor/Explain.java new file mode 100644 index 0000000000..76ef42c776 --- /dev/null +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/executor/Explain.java @@ -0,0 +1,181 @@ +/* + * 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.executor; + +import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.ExplainResponse; +import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.ExplainResponseNode; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.AggregationOperator; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.DedupeOperator; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.EvalOperator; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.FilterOperator; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.HeadOperator; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlan; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlanNodeVisitor; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.ProjectOperator; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.RareTopNOperator; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.RemoveOperator; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.RenameOperator; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.SortOperator; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.ValuesOperator; +import com.amazon.opendistroforelasticsearch.sql.storage.TableScanOperator; +import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.apache.commons.lang3.tuple.Pair; + +/** + * Visitor that explains a physical plan to JSON format. + */ +public class Explain extends PhysicalPlanNodeVisitor + implements Function { + + @Override + public ExplainResponse apply(PhysicalPlan plan) { + return new ExplainResponse(plan.accept(this, null)); + } + + @Override + public ExplainResponseNode visitProject(ProjectOperator node, Object context) { + return explain(node, context, explainNode -> explainNode.setDescription(ImmutableMap.of( + "fields", node.getProjectList().toString()))); + } + + @Override + public ExplainResponseNode visitFilter(FilterOperator node, Object context) { + return explain(node, context, explainNode -> explainNode.setDescription(ImmutableMap.of( + "conditions", node.getConditions().toString()))); + } + + @Override + public ExplainResponseNode visitSort(SortOperator node, Object context) { + Map> sortListDescription = + node.getSortList() + .stream() + .collect(Collectors.toMap( + p -> p.getRight().toString(), + p -> ImmutableMap.of( + "sortOrder", p.getLeft().getSortOrder().toString(), + "nullOrder", p.getLeft().getNullOrder().toString()))); + + return explain(node, context, explainNode -> explainNode.setDescription(ImmutableMap.of( + "count", node.getCount(), + "sortList", sortListDescription))); + } + + @Override + public ExplainResponseNode visitTableScan(TableScanOperator node, Object context) { + return explain(node, context, explainNode -> explainNode.setDescription(ImmutableMap.of( + "request", node.toString()))); + } + + @Override + public ExplainResponseNode visitAggregation(AggregationOperator node, Object context) { + return explain(node, context, explainNode -> explainNode.setDescription(ImmutableMap.of( + "aggregators", node.getAggregatorList().toString(), + "groupBy", node.getGroupByExprList().toString()))); + } + + @Override + public ExplainResponseNode visitRename(RenameOperator node, Object context) { + Map renameMappingDescription = + node.getMapping() + .entrySet() + .stream() + .collect(Collectors.toMap( + e -> e.getKey().toString(), + e -> e.getValue().toString())); + + return explain(node, context, explainNode -> explainNode.setDescription(ImmutableMap.of( + "mapping", renameMappingDescription))); + } + + @Override + public ExplainResponseNode visitRemove(RemoveOperator node, Object context) { + return explain(node, context, explainNode -> explainNode.setDescription(ImmutableMap.of( + "removeList", node.getRemoveList().toString()))); + } + + @Override + public ExplainResponseNode visitEval(EvalOperator node, Object context) { + return explain(node, context, explainNode -> explainNode.setDescription(ImmutableMap.of( + "expressions", convertPairListToMap(node.getExpressionList())))); + } + + @Override + public ExplainResponseNode visitDedupe(DedupeOperator node, Object context) { + return explain(node, context, explainNode -> explainNode.setDescription(ImmutableMap.of( + "dedupeList", node.getDedupeList().toString(), + "allowedDuplication", node.getAllowedDuplication(), + "keepEmpty", node.getKeepEmpty(), + "consecutive", node.getConsecutive()))); + } + + @Override + public ExplainResponseNode visitRareTopN(RareTopNOperator node, Object context) { + return explain(node, context, explainNode -> explainNode.setDescription(ImmutableMap.of( + "commandType", node.getCommandType(), + "noOfResults", node.getNoOfResults(), + "fields", node.getFieldExprList().toString(), + "groupBy", node.getGroupByExprList().toString() + ))); + } + + @Override + public ExplainResponseNode visitHead(HeadOperator node, Object context) { + return explain(node, context, explainNode -> explainNode.setDescription(ImmutableMap.of( + "keepLast", node.getKeepLast(), + "whileExpr", node.getWhileExpr().toString(), + "number", node.getNumber() + ))); + } + + @Override + public ExplainResponseNode visitValues(ValuesOperator node, Object context) { + return explain(node, context, explainNode -> explainNode.setDescription(ImmutableMap.of( + "values", node.getValues()))); + } + + protected ExplainResponseNode explain(PhysicalPlan node, Object context, + Consumer doExplain) { + ExplainResponseNode explainNode = new ExplainResponseNode(getOperatorName(node)); + + List children = new ArrayList<>(); + for (PhysicalPlan child : node.getChild()) { + children.add(child.accept(this, context)); + } + explainNode.setChildren(children); + + doExplain.accept(explainNode); + return explainNode; + } + + private String getOperatorName(PhysicalPlan node) { + return node.getClass().getSimpleName(); + } + + private Map convertPairListToMap(List> pairs) { + return pairs.stream() + .collect(Collectors.toMap( + p -> p.getLeft().toString(), + p -> p.getRight().toString())); + } + +} diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/DSL.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/DSL.java index edb1748f6e..cc2c5e21cf 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/DSL.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/DSL.java @@ -20,6 +20,7 @@ import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils; import com.amazon.opendistroforelasticsearch.sql.data.type.ExprType; import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.Aggregator; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.NamedAggregator; import com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionName; import com.amazon.opendistroforelasticsearch.sql.expression.function.BuiltinFunctionRepository; import java.util.Arrays; @@ -104,6 +105,10 @@ public static NamedExpression named(String name, Expression expression, String a return new NamedExpression(name, expression, alias); } + public static NamedAggregator named(String name, Aggregator aggregator) { + return new NamedAggregator(name, aggregator); + } + public FunctionExpression abs(Expression... expressions) { return function(BuiltinFunctionName.ABS, expressions); } @@ -256,6 +261,10 @@ public FunctionExpression timestamp(Expression... expressions) { return function(BuiltinFunctionName.TIMESTAMP, expressions); } + public FunctionExpression adddate(Expression... expressions) { + return function(BuiltinFunctionName.ADDDATE, expressions); + } + public FunctionExpression divide(Expression... expressions) { return function(BuiltinFunctionName.DIVIDE, expressions); } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/ExpressionNodeVisitor.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/ExpressionNodeVisitor.java index f2b1618357..4c3799c25f 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/ExpressionNodeVisitor.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/ExpressionNodeVisitor.java @@ -17,6 +17,7 @@ package com.amazon.opendistroforelasticsearch.sql.expression; import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.Aggregator; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.NamedAggregator; import com.amazon.opendistroforelasticsearch.sql.expression.function.FunctionImplementation; /** @@ -74,4 +75,7 @@ public T visitAggregator(Aggregator node, C context) { return visitChildren(node, context); } + public T visitNamedAggregator(NamedAggregator node, C context) { + return visitChildren(node, context); + } } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/NamedExpression.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/NamedExpression.java index da38d4b9cc..17fd1225eb 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/NamedExpression.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/NamedExpression.java @@ -34,7 +34,6 @@ @AllArgsConstructor @EqualsAndHashCode @RequiredArgsConstructor -@ToString public class NamedExpression implements Expression { /** @@ -45,6 +44,7 @@ public class NamedExpression implements Expression { /** * Expression that being named. */ + @Getter private final Expression delegated; /** @@ -76,4 +76,9 @@ public T accept(ExpressionNodeVisitor visitor, C context) { return visitor.visitNamed(this, context); } + @Override + public String toString() { + return getName(); + } + } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/aggregation/NamedAggregator.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/aggregation/NamedAggregator.java new file mode 100644 index 0000000000..912015594f --- /dev/null +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/aggregation/NamedAggregator.java @@ -0,0 +1,87 @@ +/* + * + * 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.aggregation; + +import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionNodeVisitor; +import com.amazon.opendistroforelasticsearch.sql.storage.bindingtuple.BindingTuple; +import com.google.common.base.Strings; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * NamedAggregator expression that represents expression with name. + * Please see more details in associated unresolved expression operator + * {@link com.amazon.opendistroforelasticsearch.sql.ast.expression.Alias}. + */ +@EqualsAndHashCode +public class NamedAggregator extends Aggregator { + + /** + * Aggregator name. + */ + private final String name; + + /** + * Aggregator that being named. + */ + private final Aggregator delegated; + + /** + * NamedAggregator. + * + * @param name name + * @param delegated delegated + */ + public NamedAggregator( + String name, + Aggregator delegated) { + super(delegated.getFunctionName(), delegated.getArguments(), delegated.returnType); + this.name = name; + this.delegated = delegated; + } + + @Override + public AggregationState create() { + return delegated.create(); + } + + @Override + public AggregationState iterate(BindingTuple tuple, AggregationState state) { + return delegated.iterate(tuple, state); + } + + /** + * Get expression name using name or its alias (if it's present). + * @return expression name + */ + public String getName() { + return name; + } + + @Override + public T accept(ExpressionNodeVisitor visitor, C context) { + return visitor.visitNamedAggregator(this, context); + } + + @Override + public String toString() { + return getName(); + } + +} diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunction.java index ea4716e9a2..6fdac81ddd 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunction.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunction.java @@ -17,10 +17,11 @@ package com.amazon.opendistroforelasticsearch.sql.expression.datetime; -import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.getStringValue; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.DATE; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.DATETIME; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTERVAL; +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.data.type.ExprCoreType.TIME; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.TIMESTAMP; @@ -30,6 +31,7 @@ import static com.amazon.opendistroforelasticsearch.sql.expression.function.FunctionDSL.nullMissingHandling; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprDateValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprDatetimeValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprIntegerValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprStringValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTimeValue; @@ -57,6 +59,7 @@ public void register(BuiltinFunctionRepository repository) { repository.register(dayOfMonth()); repository.register(time()); repository.register(timestamp()); + repository.register(adddate()); } /** @@ -78,7 +81,8 @@ private FunctionResolver date() { private FunctionResolver dayOfMonth() { return define(DAYOFMONTH.getName(), impl(nullMissingHandling(DateTimeFunction::exprDayOfMonth), - INTEGER, DATE) + INTEGER, DATE), + impl(nullMissingHandling(DateTimeFunction::exprDayOfMonth), INTEGER, STRING) ); } @@ -109,6 +113,27 @@ private FunctionResolver timestamp() { impl(nullMissingHandling(DateTimeFunction::exprTimestamp), TIMESTAMP, TIMESTAMP)); } + /** + * Specify a start date and add a temporal amount to the date. + * The return type depends on the date type and the interval unit. Detailed supported signatures: + * (DATE, DATETIME/TIMESTAMP, INTERVAL) -> DATETIME + * (DATE, LONG) -> DATE + * (DATETIME/TIMESTAMP, LONG) -> DATETIME + */ + private FunctionResolver adddate() { + return define(BuiltinFunctionName.ADDDATE.getName(), + impl(nullMissingHandling(DateTimeFunction::exprAddDateInterval), DATE, DATE, INTERVAL), + impl(nullMissingHandling(DateTimeFunction::exprAddDateInterval), DATETIME, DATE, INTERVAL), + impl(nullMissingHandling(DateTimeFunction::exprAddDateInterval), + DATETIME, DATETIME, INTERVAL), + impl(nullMissingHandling(DateTimeFunction::exprAddDateInterval), + DATETIME, TIMESTAMP, INTERVAL), + impl(nullMissingHandling(DateTimeFunction::exprAddDateDays), DATE, DATE, LONG), + impl(nullMissingHandling(DateTimeFunction::exprAddDateDays), DATETIME, DATETIME, LONG), + impl(nullMissingHandling(DateTimeFunction::exprAddDateDays), DATETIME, TIMESTAMP, LONG) + ); + } + /** * Date implementation for ExprValue. * @param exprValue ExprValue of Date type or String type. @@ -128,7 +153,11 @@ private ExprValue exprDate(ExprValue exprValue) { * @return ExprValue. */ private ExprValue exprDayOfMonth(ExprValue date) { - return new ExprIntegerValue(date.dateValue().getMonthValue()); + if (date instanceof ExprStringValue) { + return new ExprIntegerValue( + new ExprDateValue(date.stringValue()).dateValue().getDayOfMonth()); + } + return new ExprIntegerValue(date.dateValue().getDayOfMonth()); } /** @@ -156,4 +185,29 @@ private ExprValue exprTimestamp(ExprValue exprValue) { return new ExprTimestampValue(exprValue.timestampValue()); } } + + /** + * ADDDATE function implementation for ExprValue. + * + * @param date ExprValue of Date/Datetime/Timestamp type. + * @param expr ExprValue of Interval type, the temporal amount to add. + * @return Date/Datetime resulted from expr added to date. + */ + private ExprValue exprAddDateInterval(ExprValue date, ExprValue expr) { + return new ExprDatetimeValue(date.datetimeValue().plus(expr.intervalValue())); + } + + /** + * ADDDATE function implementation for ExprValue. + * + * @param date ExprValue of Date/Datetime/Timestamp type. + * @param days ExprValue of Long type, representing the number of days to add. + * @return Date/Datetime resulted from days added to date. + */ + private ExprValue exprAddDateDays(ExprValue date, ExprValue days) { + if (date instanceof ExprDateValue) { + return new ExprDateValue(date.dateValue().plusDays(days.longValue())); + } + return new ExprDatetimeValue(date.datetimeValue().plusDays(days.longValue())); + } } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/function/BuiltinFunctionName.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/function/BuiltinFunctionName.java index eefc13c0d8..b853b9c855 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/function/BuiltinFunctionName.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/function/BuiltinFunctionName.java @@ -55,6 +55,7 @@ public enum BuiltinFunctionName { DAYOFMONTH(FunctionName.of("dayofmonth")), TIME(FunctionName.of("time")), TIMESTAMP(FunctionName.of("timestamp")), + ADDDATE(FunctionName.of("adddate")), /** * Text Functions. diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/DefaultImplementor.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/DefaultImplementor.java index 85f358e57b..e7c68df204 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/DefaultImplementor.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/DefaultImplementor.java @@ -20,6 +20,7 @@ import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalDedupe; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalEval; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalFilter; +import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalHead; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlan; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlanNodeVisitor; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalProject; @@ -33,6 +34,7 @@ import com.amazon.opendistroforelasticsearch.sql.planner.physical.DedupeOperator; import com.amazon.opendistroforelasticsearch.sql.planner.physical.EvalOperator; import com.amazon.opendistroforelasticsearch.sql.planner.physical.FilterOperator; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.HeadOperator; import com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlan; import com.amazon.opendistroforelasticsearch.sql.planner.physical.ProjectOperator; import com.amazon.opendistroforelasticsearch.sql.planner.physical.RareTopNOperator; @@ -74,6 +76,15 @@ public PhysicalPlan visitDedupe(LogicalDedupe node, C context) { node.getConsecutive()); } + @Override + public PhysicalPlan visitHead(LogicalHead node, C context) { + return new HeadOperator( + visitChild(node, context), + node.getKeeplast(), + node.getWhileExpr(), + node.getNumber()); + } + @Override public PhysicalPlan visitProject(LogicalProject node, C context) { return new ProjectOperator(visitChild(node, context), node.getProjectList()); diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalAggregation.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalAggregation.java index ef89d91cef..e4d9b690ac 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalAggregation.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalAggregation.java @@ -15,8 +15,8 @@ package com.amazon.opendistroforelasticsearch.sql.planner.logical; -import com.amazon.opendistroforelasticsearch.sql.expression.Expression; -import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.Aggregator; +import com.amazon.opendistroforelasticsearch.sql.expression.NamedExpression; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.NamedAggregator; import java.util.Collections; import java.util.List; import lombok.EqualsAndHashCode; @@ -33,9 +33,9 @@ public class LogicalAggregation extends LogicalPlan { private final LogicalPlan child; @Getter - private final List aggregatorList; + private final List aggregatorList; @Getter - private final List groupByList; + private final List groupByList; @Override public List getChild() { diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalHead.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalHead.java new file mode 100644 index 0000000000..bb66f688f6 --- /dev/null +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalHead.java @@ -0,0 +1,47 @@ +/* + * 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.planner.logical; + +import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import java.util.Arrays; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + + +@Getter +@ToString +@EqualsAndHashCode(callSuper = false) +@RequiredArgsConstructor +public class LogicalHead extends LogicalPlan { + + private final LogicalPlan child; + private final Boolean keeplast; + private final Expression whileExpr; + private final Integer number; + + @Override + public List getChild() { + return Arrays.asList(child); + } + + @Override + public R accept(LogicalPlanNodeVisitor visitor, C context) { + return visitor.visitHead(this, context); + } +} 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 99cd707577..a19ce69112 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 @@ -21,7 +21,7 @@ import com.amazon.opendistroforelasticsearch.sql.expression.LiteralExpression; 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.aggregation.NamedAggregator; import com.google.common.collect.ImmutableSet; import java.util.Arrays; import java.util.List; @@ -36,7 +36,7 @@ public class LogicalPlanDSL { public static LogicalPlan aggregation( - LogicalPlan input, List aggregatorList, List groupByList) { + LogicalPlan input, List aggregatorList, List groupByList) { return new LogicalAggregation(input, aggregatorList, groupByList); } @@ -85,6 +85,11 @@ public static LogicalPlan dedupe( input, Arrays.asList(fields), allowedDuplication, keepEmpty, consecutive); } + public static LogicalPlan head( + LogicalPlan input, boolean keeplast, Expression whileExpr, int number) { + return new LogicalHead(input, keeplast, whileExpr, number); + } + public static LogicalPlan rareTopN(LogicalPlan input, CommandType commandType, List groupByList, Expression... fields) { return rareTopN(input, commandType, 10, groupByList, fields); diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalPlanNodeVisitor.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalPlanNodeVisitor.java index 1b9ce5208f..601b466909 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalPlanNodeVisitor.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalPlanNodeVisitor.java @@ -43,6 +43,10 @@ public R visitDedupe(LogicalDedupe plan, C context) { return visitNode(plan, context); } + public R visitHead(LogicalHead plan, C context) { + return visitNode(plan, context); + } + public R visitRename(LogicalRename plan, C context) { return visitNode(plan, context); } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalRareTopN.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalRareTopN.java index 22de70894d..89c75d774a 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalRareTopN.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/logical/LogicalRareTopN.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * 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. diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/AggregationOperator.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/AggregationOperator.java index 9fd395e8e3..3bcf8301f5 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/AggregationOperator.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/AggregationOperator.java @@ -18,8 +18,10 @@ 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.aggregation.AggregationState; import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.Aggregator; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.NamedAggregator; import com.amazon.opendistroforelasticsearch.sql.storage.bindingtuple.BindingTuple; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; @@ -47,9 +49,9 @@ public class AggregationOperator extends PhysicalPlan { @Getter private final PhysicalPlan input; @Getter - private final List aggregatorList; + private final List aggregatorList; @Getter - private final List groupByExprList; + private final List groupByExprList; @EqualsAndHashCode.Exclude private final Group group; @EqualsAndHashCode.Exclude @@ -57,12 +59,13 @@ public class AggregationOperator extends PhysicalPlan { /** * AggregationOperator Constructor. - * @param input Input {@link PhysicalPlan} - * @param aggregatorList List of {@link Aggregator} + * + * @param input Input {@link PhysicalPlan} + * @param aggregatorList List of {@link Aggregator} * @param groupByExprList List of group by {@link Expression} */ - public AggregationOperator(PhysicalPlan input, List aggregatorList, - List groupByExprList) { + public AggregationOperator(PhysicalPlan input, List aggregatorList, + List groupByExprList) { this.input = input; this.aggregatorList = aggregatorList; this.groupByExprList = groupByExprList; @@ -103,7 +106,7 @@ public void open() { @RequiredArgsConstructor public class Group { - private final Map>> groupListMap = + private final Map>> groupListMap = new HashMap<>(); /** @@ -131,12 +134,12 @@ public void push(ExprValue inputValue) { */ public List result() { ImmutableList.Builder resultBuilder = new ImmutableList.Builder<>(); - for (Map.Entry>> entry : groupListMap - .entrySet()) { + for (Map.Entry>> + entry : groupListMap.entrySet()) { LinkedHashMap map = new LinkedHashMap<>(); map.putAll(entry.getKey().groupKeyMap()); - for (Map.Entry stateEntry : entry.getValue()) { - map.put(stateEntry.getKey().toString(), stateEntry.getValue().result()); + for (Map.Entry stateEntry : entry.getValue()) { + map.put(stateEntry.getKey().getName(), stateEntry.getValue().result()); } resultBuilder.add(ExprTupleValue.fromExprValueMap(map)); } @@ -169,7 +172,7 @@ public GroupKey(ExprValue value) { public LinkedHashMap groupKeyMap() { LinkedHashMap map = new LinkedHashMap<>(); for (int i = 0; i < groupByExprList.size(); i++) { - map.put(groupByExprList.get(i).toString(), groupByValueList.get(i)); + map.put(groupByExprList.get(i).getName(), groupByValueList.get(i)); } return map; } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/HeadOperator.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/HeadOperator.java new file mode 100644 index 0000000000..48e35eacab --- /dev/null +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/HeadOperator.java @@ -0,0 +1,114 @@ +/* + * 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.planner.physical; + +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprBooleanValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; +import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import com.amazon.opendistroforelasticsearch.sql.expression.LiteralExpression; +import com.amazon.opendistroforelasticsearch.sql.expression.operator.predicate.BinaryPredicateOperator; +import java.util.Collections; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; + +/** + * The Head operator returns the first {@link HeadOperator#number} number of results until the + * {@link HeadOperator#whileExpr} evaluates to true. If {@link HeadOperator#keepLast} is true then + * first result which evalutes {@link HeadOperator#whileExpr} to false is also returned. + * The NULL and MISSING are handled by the logic defined in {@link BinaryPredicateOperator}. + */ +@Getter +@EqualsAndHashCode +public class HeadOperator extends PhysicalPlan { + + @Getter + private final PhysicalPlan input; + @Getter + private final Boolean keepLast; + @Getter + private final Expression whileExpr; + @Getter + private final Integer number; + + private static final Integer DEFAULT_LIMIT = 10; + private static final Boolean IGNORE_LAST = false; + + @EqualsAndHashCode.Exclude + private int recordCount = 0; + @EqualsAndHashCode.Exclude + private boolean foundFirstFalse = false; + @EqualsAndHashCode.Exclude + private ExprValue next; + + @NonNull + public HeadOperator(PhysicalPlan input) { + this(input, IGNORE_LAST, new LiteralExpression(ExprBooleanValue.of(true)), DEFAULT_LIMIT); + } + + /** + * HeadOperator Constructor. + * + * @param input Input {@link PhysicalPlan} + * @param keepLast Controls whether the last result in the result set is retained. The last + * result returned is the result that caused the whileExpr to evaluate to false + * or NULL. + * @param whileExpr The search returns results until this expression evaluates to false + * @param number Number of specified results + */ + @NonNull + public HeadOperator(PhysicalPlan input, Boolean keepLast, Expression whileExpr, Integer number) { + this.input = input; + this.keepLast = keepLast; + this.whileExpr = whileExpr; + this.number = number; + } + + @Override + public R accept(PhysicalPlanNodeVisitor visitor, C context) { + return visitor.visitHead(this, context); + } + + @Override + public List getChild() { + return Collections.singletonList(input); + } + + @Override + public boolean hasNext() { + if (!input.hasNext() || foundFirstFalse || (recordCount >= number)) { + return false; + } + ExprValue inputVal = input.next(); + ExprValue exprValue = whileExpr.valueOf(inputVal.bindingTuples()); + if (exprValue.isNull() || exprValue.isMissing() || !(exprValue.booleanValue())) { + // First false is when we decide whether to keep the last value + foundFirstFalse = true; + if (!keepLast) { + return false; + } + } + this.next = inputVal; + recordCount++; + return true; + } + + @Override + public ExprValue next() { + return this.next; + } +} 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 515711a84f..03b22d6d7a 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 @@ -22,6 +22,7 @@ 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.aggregation.NamedAggregator; import com.google.common.collect.ImmutableSet; import java.util.Arrays; import java.util.List; @@ -36,7 +37,7 @@ public class PhysicalPlanDSL { public static AggregationOperator agg( - PhysicalPlan input, List aggregators, List groups) { + PhysicalPlan input, List aggregators, List groups) { return new AggregationOperator(input, aggregators, groups); } @@ -81,6 +82,11 @@ public static DedupeOperator dedupe( input, Arrays.asList(expressions), allowedDuplication, keepEmpty, consecutive); } + public static HeadOperator head(PhysicalPlan input, boolean keepLast, Expression whileExpr, + int number) { + return new HeadOperator(input, keepLast, whileExpr, number); + } + public static RareTopNOperator rareTopN(PhysicalPlan input, CommandType commandType, List groups, Expression... expressions) { return new RareTopNOperator(input, commandType, Arrays.asList(expressions), groups); diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/PhysicalPlanNodeVisitor.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/PhysicalPlanNodeVisitor.java index 62b35af003..13bab7eb34 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/PhysicalPlanNodeVisitor.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/PhysicalPlanNodeVisitor.java @@ -69,7 +69,12 @@ public R visitSort(SortOperator node, C context) { return visitNode(node, context); } + public R visitHead(HeadOperator node, C context) { + return visitNode(node, context); + } + public R visitRareTopN(RareTopNOperator node, C context) { return visitNode(node, context); } + } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/RareTopNOperator.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/RareTopNOperator.java index 4e98787c76..99f5e79855 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/RareTopNOperator.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/RareTopNOperator.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * 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. diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/ValuesOperator.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/ValuesOperator.java index 215589e5ef..5ba041e574 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/ValuesOperator.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/ValuesOperator.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.stream.Collectors; import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.ToString; /** @@ -36,6 +37,7 @@ public class ValuesOperator extends PhysicalPlan { /** * Original values list for print and equality check. */ + @Getter private final List> values; /** 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 b41818ac5e..28b4725861 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 @@ -15,25 +15,36 @@ package com.amazon.opendistroforelasticsearch.sql.analysis; +import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.aggregate; +import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.alias; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.argument; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.booleanLiteral; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.compare; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.field; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.filter; +import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.function; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.intLiteral; +import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.qualifiedName; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.relation; +import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.unresolvedArg; +import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.unresolvedArgList; +import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.booleanValue; import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.integerValue; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.DOUBLE; 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 java.util.Collections.emptyList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL; import com.amazon.opendistroforelasticsearch.sql.ast.tree.RareTopN.CommandType; +import com.amazon.opendistroforelasticsearch.sql.ast.tree.UnresolvedPlan; import com.amazon.opendistroforelasticsearch.sql.exception.SemanticCheckException; import com.amazon.opendistroforelasticsearch.sql.expression.DSL; import com.amazon.opendistroforelasticsearch.sql.expression.config.ExpressionConfig; +import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlan; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlanDSL; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -61,6 +72,20 @@ public void filter_relation() { AstDSL.equalTo(AstDSL.field("integer_value"), AstDSL.intLiteral(1)))); } + @Test + public void head_relation() { + assertAnalyzeEqual( + LogicalPlanDSL.head( + LogicalPlanDSL.relation("schema"), + false, dsl.equal(DSL.ref("integer_value", INTEGER), DSL.literal(integerValue(1))), 10), + AstDSL.head( + AstDSL.relation("schema"), + unresolvedArgList( + unresolvedArg("keeplast", booleanLiteral(false)), + unresolvedArg("whileExpr", compare("=", field("integer_value"), intLiteral(1))), + unresolvedArg("number", intLiteral(10))))); + } + @Test public void analyze_filter_relation() { assertAnalyzeEqual( @@ -81,37 +106,24 @@ public void rename_relation() { AstDSL.map(AstDSL.field("integer_value"), AstDSL.field("ivalue")))); } - @Test - public void rename_stats_source() { - assertAnalyzeEqual( - LogicalPlanDSL.rename( - LogicalPlanDSL.aggregation( - LogicalPlanDSL.relation("schema"), - ImmutableList.of(dsl.avg(DSL.ref("integer_value", INTEGER))), - ImmutableList.of()), - ImmutableMap.of(DSL.ref("avg(integer_value)", DOUBLE), DSL.ref("ivalue", DOUBLE))), - AstDSL.rename( - AstDSL.agg( - AstDSL.relation("schema"), - AstDSL.exprList(AstDSL.aggregate("avg", field("integer_value"))), - null, - ImmutableList.of(), - AstDSL.defaultStatsArgs()), - AstDSL.map(AstDSL.aggregate("avg", field("integer_value")), field("ivalue")))); - } - @Test public void stats_source() { assertAnalyzeEqual( LogicalPlanDSL.aggregation( LogicalPlanDSL.relation("schema"), - ImmutableList.of(dsl.avg(DSL.ref("integer_value", INTEGER))), - ImmutableList.of(DSL.ref("string_value", STRING))), + ImmutableList + .of(DSL.named("avg(integer_value)", dsl.avg(DSL.ref("integer_value", INTEGER)))), + ImmutableList.of(DSL.named("string_value", DSL.ref("string_value", STRING)))), AstDSL.agg( AstDSL.relation("schema"), - AstDSL.exprList(AstDSL.aggregate("avg", field("integer_value"))), + AstDSL.exprList( + AstDSL.alias( + "avg(integer_value)", + AstDSL.aggregate("avg", field("integer_value"))) + ), null, - ImmutableList.of(field("string_value")), + ImmutableList.of( + AstDSL.alias("string_value", field("string_value"))), AstDSL.defaultStatsArgs())); } @@ -165,7 +177,9 @@ public void rename_to_invalid_expression() { AstDSL.rename( AstDSL.agg( AstDSL.relation("schema"), - AstDSL.exprList(AstDSL.aggregate("avg", field("integer_value"))), + AstDSL.exprList( + AstDSL.alias("avg(integer_value)", AstDSL.aggregate("avg", field( + "integer_value")))), Collections.emptyList(), ImmutableList.of(), AstDSL.defaultStatsArgs()), @@ -241,4 +255,61 @@ public void project_values() { ); } + /** + * SELECT name, AVG(age) FROM test GROUP BY name. + */ + @Test + public void sql_group_by_field() { + assertAnalyzeEqual( + LogicalPlanDSL.project( + LogicalPlanDSL.aggregation( + LogicalPlanDSL.relation("schema"), + ImmutableList + .of(DSL + .named("AVG(integer_value)", dsl.avg(DSL.ref("integer_value", INTEGER)))), + ImmutableList.of(DSL.named("string_value", DSL.ref("string_value", STRING)))), + DSL.named("string_value", DSL.ref("string_value", STRING)), + DSL.named("AVG(integer_value)", DSL.ref("AVG(integer_value)", DOUBLE))), + AstDSL.project( + AstDSL.agg( + AstDSL.relation("schema"), + ImmutableList.of(alias("AVG(integer_value)", + aggregate("AVG", qualifiedName("integer_value")))), + emptyList(), + ImmutableList.of(alias("string_value", qualifiedName("string_value"))), + emptyList()), + AstDSL.alias("string_value", qualifiedName("string_value")), + AstDSL.alias("AVG(integer_value)", aggregate("AVG", qualifiedName("integer_value")))) + ); + } + + /** + * SELECT abs(name), AVG(age) FROM test GROUP BY abs(name). + */ + @Test + public void sql_group_by_function() { + assertAnalyzeEqual( + LogicalPlanDSL.project( + LogicalPlanDSL.aggregation( + LogicalPlanDSL.relation("schema"), + ImmutableList + .of(DSL + .named("AVG(integer_value)", dsl.avg(DSL.ref("integer_value", INTEGER)))), + ImmutableList.of(DSL.named("abs(long_value)", + dsl.abs(DSL.ref("long_value", LONG))))), + DSL.named("abs(long_value)", DSL.ref("abs(long_value)", LONG)), + DSL.named("AVG(integer_value)", DSL.ref("AVG(integer_value)", DOUBLE))), + AstDSL.project( + AstDSL.agg( + AstDSL.relation("schema"), + ImmutableList.of(alias("AVG(integer_value)", + aggregate("AVG", qualifiedName("integer_value")))), + emptyList(), + ImmutableList + .of(alias("abs(long_value)", function("abs", qualifiedName("long_value")))), + emptyList()), + AstDSL.alias("abs(long_value)", function("abs", qualifiedName("long_value"))), + AstDSL.alias("AVG(integer_value)", aggregate("AVG", qualifiedName("integer_value")))) + ); + } } diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/NamedExpressionAnalyzerTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/NamedExpressionAnalyzerTest.java new file mode 100644 index 0000000000..4386ec7501 --- /dev/null +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/NamedExpressionAnalyzerTest.java @@ -0,0 +1,46 @@ +/* + * + * 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.analysis; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL; +import com.amazon.opendistroforelasticsearch.sql.ast.expression.Alias; +import com.amazon.opendistroforelasticsearch.sql.expression.NamedExpression; +import com.amazon.opendistroforelasticsearch.sql.expression.config.ExpressionConfig; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@Configuration +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = {ExpressionConfig.class, AnalyzerTestBase.class}) +class NamedExpressionAnalyzerTest extends AnalyzerTestBase { + @Test + void visit_named_seleteitem() { + Alias alias = AstDSL.alias("integer_value", AstDSL.qualifiedName("integer_value")); + + NamedExpressionAnalyzer analyzer = + new NamedExpressionAnalyzer(expressionAnalyzer); + + NamedExpression analyze = analyzer.analyze(alias, analysisContext); + assertEquals("integer_value", analyze.getName()); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/SelectAnalyzeTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/SelectAnalyzeTest.java index 376e7538c9..814fb63a83 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/SelectAnalyzeTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/SelectAnalyzeTest.java @@ -124,17 +124,19 @@ public void stats_and_project_all() { LogicalPlanDSL.project( LogicalPlanDSL.aggregation( LogicalPlanDSL.relation("schema"), - ImmutableList.of(dsl.avg(DSL.ref("integer_value", INTEGER))), - ImmutableList.of(DSL.ref("string_value", STRING))), + ImmutableList.of(DSL + .named("avg(integer_value)", dsl.avg(DSL.ref("integer_value", INTEGER)))), + ImmutableList.of(DSL.named("string_value", DSL.ref("string_value", STRING)))), DSL.named("string_value", DSL.ref("string_value", STRING)), DSL.named("avg(integer_value)", DSL.ref("avg(integer_value)", DOUBLE)) ), AstDSL.projectWithArg( AstDSL.agg( AstDSL.relation("schema"), - AstDSL.exprList(AstDSL.aggregate("avg", field("integer_value"))), + AstDSL.exprList(AstDSL.alias("avg(integer_value)", AstDSL.aggregate("avg", + field("integer_value")))), null, - ImmutableList.of(field("string_value")), + ImmutableList.of(AstDSL.alias("string_value", field("string_value"))), AstDSL.defaultStatsArgs()), AstDSL.defaultFieldsArgs(), AllFields.of())); } 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 d4e59c8219..a5f9fe5a65 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 @@ -61,10 +61,10 @@ public void named_expression_with_alias() { @Test public void named_expression_with_delegated_expression_defined_in_symbol_table() { analysisContext.push(); - analysisContext.peek().define(new Symbol(Namespace.FIELD_NAME, "avg(integer_value)"), FLOAT); + analysisContext.peek().define(new Symbol(Namespace.FIELD_NAME, "AVG(integer_value)"), FLOAT); assertAnalyzeEqual( - DSL.named("AVG(integer_value)", DSL.ref("avg(integer_value)", FLOAT)), + DSL.named("AVG(integer_value)", DSL.ref("AVG(integer_value)", FLOAT)), AstDSL.alias("AVG(integer_value)", AstDSL.aggregate("AVG", AstDSL.qualifiedName("integer_value"))) ); 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 new file mode 100644 index 0000000000..9eee96b082 --- /dev/null +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/executor/ExplainTest.java @@ -0,0 +1,247 @@ +/* + * 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.executor; + +import static com.amazon.opendistroforelasticsearch.sql.ast.tree.RareTopN.CommandType.TOP; +import static com.amazon.opendistroforelasticsearch.sql.ast.tree.Sort.SortOption.PPL_ASC; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.DOUBLE; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRING; +import static com.amazon.opendistroforelasticsearch.sql.expression.DSL.literal; +import static com.amazon.opendistroforelasticsearch.sql.expression.DSL.named; +import static com.amazon.opendistroforelasticsearch.sql.expression.DSL.ref; +import static com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlanDSL.agg; +import static com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlanDSL.dedupe; +import static com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlanDSL.eval; +import static com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlanDSL.filter; +import static com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlanDSL.head; +import static com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlanDSL.project; +import static com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlanDSL.rareTopN; +import static com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlanDSL.remove; +import static com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlanDSL.rename; +import static com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlanDSL.sort; +import static com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlanDSL.values; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.amazon.opendistroforelasticsearch.sql.ast.tree.Sort; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; +import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.ExplainResponse; +import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.ExplainResponseNode; +import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionTestBase; +import com.amazon.opendistroforelasticsearch.sql.expression.LiteralExpression; +import com.amazon.opendistroforelasticsearch.sql.expression.NamedExpression; +import com.amazon.opendistroforelasticsearch.sql.expression.ReferenceExpression; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.NamedAggregator; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlan; +import com.amazon.opendistroforelasticsearch.sql.storage.TableScanOperator; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.tuple.ImmutablePair; +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; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ExplainTest extends ExpressionTestBase { + + private final Explain explain = new Explain(); + + private final FakeTableScan tableScan = new FakeTableScan(); + + @Test + void can_explain_project_filter_table_scan() { + Expression filterExpr = + dsl.and( + dsl.equal(ref("balance", INTEGER), literal(10000)), + dsl.greater(ref("age", INTEGER), literal(30))); + NamedExpression[] projectList = { + named("full_name", ref("full_name", STRING), "name"), + named("age", ref("age", INTEGER)) + }; + + PhysicalPlan plan = + project( + filter( + tableScan, + filterExpr), + projectList); + + assertEquals( + new ExplainResponse( + new ExplainResponseNode( + "ProjectOperator", + ImmutableMap.of("fields", "[name, age]"), + singletonList(new ExplainResponseNode( + "FilterOperator", + ImmutableMap.of("conditions", "and(=(balance, 10000), >(age, 30))"), + singletonList(tableScan.explain()))))), + explain.apply(plan)); + } + + @Test + void can_explain_aggregations() { + List aggExprs = ImmutableList.of(ref("balance", DOUBLE)); + List aggList = ImmutableList.of( + named("avg(balance)", dsl.avg(aggExprs.toArray(new Expression[0])))); + List groupByList = ImmutableList.of( + named("state", ref("state", STRING))); + + PhysicalPlan plan = agg(new FakeTableScan(), aggList, groupByList); + assertEquals( + new ExplainResponse( + new ExplainResponseNode( + "AggregationOperator", + ImmutableMap.of( + "aggregators", "[avg(balance)]", + "groupBy", "[state]"), + singletonList(tableScan.explain()))), + explain.apply(plan)); + } + + @Test + void can_explain_rare_top_n() { + Expression field = ref("state", STRING); + + PhysicalPlan plan = rareTopN(tableScan, TOP, emptyList(), field); + assertEquals( + new ExplainResponse( + new ExplainResponseNode( + "RareTopNOperator", + ImmutableMap.of( + "commandType", TOP, + "noOfResults", 10, + "fields", "[state]", + "groupBy", "[]"), + singletonList(tableScan.explain()))), + explain.apply(plan)); + } + + @Test + void can_explain_head() { + Boolean keepLast = false; + Expression whileExpr = dsl.and( + dsl.equal(ref("balance", INTEGER), literal(10000)), + dsl.greater(ref("age", INTEGER), literal(30))); + Integer number = 5; + + PhysicalPlan plan = head(tableScan, keepLast, whileExpr, number); + + assertEquals( + new ExplainResponse( + new ExplainResponseNode( + "HeadOperator", + ImmutableMap.of( + "keepLast", false, + "whileExpr", "and(=(balance, 10000), >(age, 30))", + "number", 5), + singletonList(tableScan.explain()))), + explain.apply(plan)); + } + + @Test + void can_explain_other_operators() { + ReferenceExpression[] removeList = {ref("state", STRING)}; + Map renameMapping = ImmutableMap.of( + ref("state", STRING), ref("s", STRING)); + Pair evalExprs = ImmutablePair.of( + ref("age", INTEGER), dsl.add(ref("age", INTEGER), literal(2))); + Expression[] dedupeList = {ref("age", INTEGER)}; + Pair sortList = ImmutablePair.of( + PPL_ASC, ref("age", INTEGER)); + List values = ImmutableList.of(literal("WA"), literal(30)); + + PhysicalPlan plan = + remove( + rename( + eval( + dedupe( + sort( + values(values), + 1000, + sortList), + dedupeList), + evalExprs), + renameMapping), + removeList); + + assertEquals( + new ExplainResponse( + new ExplainResponseNode( + "RemoveOperator", + ImmutableMap.of("removeList", "[state]"), + singletonList(new ExplainResponseNode( + "RenameOperator", + ImmutableMap.of("mapping", ImmutableMap.of("state", "s")), + singletonList(new ExplainResponseNode( + "EvalOperator", + ImmutableMap.of("expressions", ImmutableMap.of("age", "+(age, 2)")), + singletonList(new ExplainResponseNode( + "DedupeOperator", + ImmutableMap.of( + "dedupeList", "[age]", + "allowedDuplication", 1, + "keepEmpty", false, + "consecutive", false), + singletonList(new ExplainResponseNode( + "SortOperator", + ImmutableMap.of( + "count", 1000, + "sortList", ImmutableMap.of( + "age", ImmutableMap.of( + "sortOrder", "ASC", + "nullOrder", "NULL_FIRST"))), + singletonList(new ExplainResponseNode( + "ValuesOperator", + ImmutableMap.of("values", ImmutableList.of(values)), + emptyList()))))))))))) + ), + explain.apply(plan) + ); + } + + private static class FakeTableScan extends TableScanOperator { + @Override + public boolean hasNext() { + return false; + } + + @Override + public ExprValue next() { + return null; + } + + @Override + public String toString() { + return "Fake DSL request"; + } + + /** Used to ignore table scan which is duplicate but required for each operator test. */ + public ExplainResponseNode explain() { + return new ExplainResponseNode( + "FakeTableScan", + ImmutableMap.of("request", "Fake DSL request"), + emptyList()); + } + } + +} \ No newline at end of file diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/ExpressionNodeVisitorTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/ExpressionNodeVisitorTest.java index 58a4eff006..3ee0e80b19 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/ExpressionNodeVisitorTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/ExpressionNodeVisitorTest.java @@ -25,7 +25,9 @@ import static org.junit.jupiter.api.Assertions.assertNull; import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.Aggregator; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.AvgAggregator; import com.amazon.opendistroforelasticsearch.sql.expression.config.ExpressionConfig; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import org.junit.jupiter.api.DisplayNameGeneration; @@ -45,6 +47,8 @@ void should_return_null_by_default() { assertNull(named("bool", literal(true)).accept(visitor, null)); assertNull(dsl.abs(literal(-10)).accept(visitor, null)); assertNull(dsl.sum(literal(10)).accept(visitor, null)); + assertNull(named("avg", new AvgAggregator(Collections.singletonList(ref("age", INTEGER)), + INTEGER)).accept(visitor, null)); } @Test diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunctionTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunctionTest.java index 1f6e35ba16..b0ce5088d9 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunctionTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunctionTest.java @@ -23,6 +23,8 @@ import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.DATE; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.DATETIME; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTERVAL; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.LONG; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.TIME; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.TIMESTAMP; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -65,13 +67,18 @@ public void setup() { public void dayOfMonth() { when(nullRef.type()).thenReturn(DATE); when(missingRef.type()).thenReturn(DATE); - - FunctionExpression expression = dsl.dayofmonth(DSL.literal(new ExprDateValue("2020-07-07"))); - assertEquals(INTEGER, expression.type()); - assertEquals("dayofmonth(DATE '2020-07-07')", expression.toString()); - assertEquals(integerValue(7), eval(expression)); assertEquals(nullValue(), eval(dsl.dayofmonth(nullRef))); assertEquals(missingValue(), eval(dsl.dayofmonth(missingRef))); + + FunctionExpression expression = dsl.dayofmonth(DSL.literal(new ExprDateValue("2020-07-08"))); + assertEquals(INTEGER, expression.type()); + assertEquals("dayofmonth(DATE '2020-07-08')", expression.toString()); + assertEquals(integerValue(8), eval(expression)); + + expression = dsl.dayofmonth(DSL.literal("2020-07-08")); + assertEquals(INTEGER, expression.type()); + assertEquals("dayofmonth(\"2020-07-08\")", expression.toString()); + assertEquals(integerValue(8), eval(expression)); } @Test @@ -128,6 +135,51 @@ public void timestamp() { assertEquals("timestamp(TIMESTAMP '2020-08-17 01:01:01')", expr.toString()); } + @Test + public void adddate() { + FunctionExpression expr = dsl.adddate(dsl.date(DSL.literal("2020-08-26")), DSL.literal(7)); + assertEquals(DATE, expr.type()); + assertEquals(new ExprDateValue("2020-09-02"), expr.valueOf(env)); + assertEquals("adddate(date(\"2020-08-26\"), 7)", expr.toString()); + + expr = dsl.adddate(dsl.timestamp(DSL.literal("2020-08-26 12:05:00")), DSL.literal(7)); + assertEquals(DATETIME, expr.type()); + assertEquals(new ExprDatetimeValue("2020-09-02 12:05:00"), expr.valueOf(env)); + assertEquals("adddate(timestamp(\"2020-08-26 12:05:00\"), 7)", expr.toString()); + + expr = dsl.adddate( + dsl.date(DSL.literal("2020-08-26")), dsl.interval(DSL.literal(1), DSL.literal("hour"))); + assertEquals(DATETIME, expr.type()); + assertEquals(new ExprDatetimeValue("2020-08-26 01:00:00"), expr.valueOf(env)); + assertEquals("adddate(date(\"2020-08-26\"), interval(1, \"hour\"))", expr.toString()); + + when(nullRef.type()).thenReturn(DATE); + assertEquals(nullValue(), eval(dsl.adddate(nullRef, DSL.literal(1L)))); + assertEquals(nullValue(), + eval(dsl.adddate(nullRef, dsl.interval(DSL.literal(1), DSL.literal("month"))))); + + when(missingRef.type()).thenReturn(DATE); + assertEquals(missingValue(), eval(dsl.adddate(missingRef, DSL.literal(1L)))); + assertEquals(missingValue(), + eval(dsl.adddate(missingRef, dsl.interval(DSL.literal(1), DSL.literal("month"))))); + + when(nullRef.type()).thenReturn(LONG); + when(missingRef.type()).thenReturn(LONG); + assertEquals(nullValue(), eval(dsl.adddate(dsl.date(DSL.literal("2020-08-26")), nullRef))); + assertEquals(missingValue(), + eval(dsl.adddate(dsl.date(DSL.literal("2020-08-26")), missingRef))); + + when(nullRef.type()).thenReturn(INTERVAL); + when(missingRef.type()).thenReturn(INTERVAL); + assertEquals(nullValue(), eval(dsl.adddate(dsl.date(DSL.literal("2020-08-26")), nullRef))); + assertEquals(missingValue(), + eval(dsl.adddate(dsl.date(DSL.literal("2020-08-26")), missingRef))); + + when(nullRef.type()).thenReturn(DATE); + when(missingRef.type()).thenReturn(INTERVAL); + assertEquals(missingValue(), eval(dsl.adddate(nullRef, missingRef))); + } + private ExprValue eval(Expression expression) { return expression.valueOf(env); } 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 06a8033764..2db202c8df 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 @@ -24,6 +24,7 @@ import static com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlanDSL.aggregation; import static com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlanDSL.eval; import static com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlanDSL.filter; +import static com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlanDSL.head; import static com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlanDSL.project; import static com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlanDSL.rareTopN; import static com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlanDSL.remove; @@ -38,11 +39,12 @@ import com.amazon.opendistroforelasticsearch.sql.ast.tree.Sort; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprBooleanValue; import com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType; +import com.amazon.opendistroforelasticsearch.sql.expression.DSL; import com.amazon.opendistroforelasticsearch.sql.expression.Expression; 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.aggregation.AvgAggregator; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.NamedAggregator; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlan; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlanDSL; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalRelation; @@ -67,10 +69,12 @@ public void visitShouldReturnDefaultPhysicalOperator() { ReferenceExpression exclude = ref("name", STRING); ReferenceExpression dedupeField = ref("name", STRING); Expression filterExpr = literal(ExprBooleanValue.of(true)); - List groupByExprs = Arrays.asList(ref("age", INTEGER)); + List groupByExprs = Arrays.asList(DSL.named("age", ref("age", INTEGER))); + List aggExprs = Arrays.asList(ref("age", INTEGER)); ReferenceExpression rareTopNField = ref("age", INTEGER); - List aggregators = - Arrays.asList(new AvgAggregator(groupByExprs, ExprCoreType.DOUBLE)); + List topByExprs = Arrays.asList(ref("age", INTEGER)); + List aggregators = + Arrays.asList(DSL.named("avg(age)", new AvgAggregator(aggExprs, ExprCoreType.DOUBLE))); Map mappings = ImmutableMap.of(ref("name", STRING), ref("lastname", STRING)); Pair newEvalField = @@ -78,27 +82,34 @@ public void visitShouldReturnDefaultPhysicalOperator() { Integer sortCount = 100; Pair sortField = ImmutablePair.of(Sort.SortOption.PPL_ASC, ref("name1", STRING)); + Boolean keeplast = true; + Expression whileExpr = literal(ExprBooleanValue.of(true)); + Integer number = 5; LogicalPlan plan = project( LogicalPlanDSL.dedupe( - rareTopN( - sort( - eval( - remove( - rename( - aggregation( - filter(values(emptyList()), filterExpr), - aggregators, - groupByExprs), - mappings), - exclude), - newEvalField), - sortCount, - sortField), - CommandType.TOP, - groupByExprs, - rareTopNField), + head( + rareTopN( + sort( + eval( + remove( + rename( + aggregation( + filter(values(emptyList()), filterExpr), + aggregators, + groupByExprs), + mappings), + exclude), + newEvalField), + sortCount, + sortField), + CommandType.TOP, + topByExprs, + rareTopNField), + keeplast, + whileExpr, + number), dedupeField), include); @@ -107,29 +118,32 @@ public void visitShouldReturnDefaultPhysicalOperator() { assertEquals( PhysicalPlanDSL.project( PhysicalPlanDSL.dedupe( - PhysicalPlanDSL.rareTopN( - PhysicalPlanDSL.sort( - PhysicalPlanDSL.eval( - PhysicalPlanDSL.remove( - PhysicalPlanDSL.rename( - PhysicalPlanDSL.agg( - PhysicalPlanDSL.filter( - PhysicalPlanDSL.values(emptyList()), - filterExpr), - aggregators, - groupByExprs), - mappings), - exclude), - newEvalField), - sortCount, - sortField), - CommandType.TOP, - groupByExprs, - rareTopNField), + PhysicalPlanDSL.head( + PhysicalPlanDSL.rareTopN( + PhysicalPlanDSL.sort( + PhysicalPlanDSL.eval( + PhysicalPlanDSL.remove( + PhysicalPlanDSL.rename( + PhysicalPlanDSL.agg( + PhysicalPlanDSL.filter( + PhysicalPlanDSL.values(emptyList()), + filterExpr), + aggregators, + groupByExprs), + mappings), + exclude), + newEvalField), + sortCount, + sortField), + CommandType.TOP, + topByExprs, + rareTopNField), + keeplast, + whileExpr, + number), dedupeField), include), actual); - } @Test diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/PlannerTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/PlannerTest.java index 33e14cf902..be783c7d42 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/PlannerTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/PlannerTest.java @@ -71,7 +71,7 @@ public void planner_test() { scan, dsl.equal(DSL.ref("response", INTEGER), DSL.literal(10)) ), - ImmutableList.of(dsl.avg(DSL.ref("response", INTEGER))), + ImmutableList.of(DSL.named("avg(response)", dsl.avg(DSL.ref("response", INTEGER)))), ImmutableList.of() ), ImmutableMap.of(DSL.ref("ivalue", INTEGER), DSL.ref("avg(response)", DOUBLE)) @@ -82,7 +82,7 @@ public void planner_test() { LogicalPlanDSL.relation("schema"), dsl.equal(DSL.ref("response", INTEGER), DSL.literal(10)) ), - ImmutableList.of(dsl.avg(DSL.ref("response", INTEGER))), + ImmutableList.of(DSL.named("avg(response)", dsl.avg(DSL.ref("response", INTEGER)))), ImmutableList.of() ), ImmutableMap.of(DSL.ref("ivalue", INTEGER), DSL.ref("avg(response)", DOUBLE)) 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 397a79c8b3..bbc82c9f7c 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 @@ -21,6 +21,7 @@ import com.amazon.opendistroforelasticsearch.sql.ast.tree.RareTopN.CommandType; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Sort.SortOption; +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.Aggregator; @@ -51,17 +52,19 @@ public void logicalPlanShouldTraversable() { LogicalPlan logicalPlan = LogicalPlanDSL.rename( LogicalPlanDSL.aggregation( - LogicalPlanDSL.rareTopN( - LogicalPlanDSL.filter(LogicalPlanDSL.relation("schema"), expression), - CommandType.TOP, - ImmutableList.of(expression), - expression), - ImmutableList.of(aggregator), - ImmutableList.of(expression)), + LogicalPlanDSL.head( + LogicalPlanDSL.rareTopN( + LogicalPlanDSL.filter(LogicalPlanDSL.relation("schema"), expression), + CommandType.TOP, + ImmutableList.of(expression), + expression), + false, expression, 10), + ImmutableList.of(DSL.named("avg", aggregator)), + ImmutableList.of(DSL.named("group", expression))), ImmutableMap.of(ref, ref)); Integer result = logicalPlan.accept(new NodesCount(), null); - assertEquals(5, result); + assertEquals(6, result); } @Test @@ -74,9 +77,14 @@ public void testAbstractPlanNodeVisitorShouldReturnNull() { assertNull(filter.accept(new LogicalPlanNodeVisitor() { }, null)); + LogicalPlan head = LogicalPlanDSL.head(relation, false, expression, 10); + assertNull(head.accept(new LogicalPlanNodeVisitor() { + }, null)); + LogicalPlan aggregation = LogicalPlanDSL.aggregation( - filter, ImmutableList.of(aggregator), ImmutableList.of(expression)); + filter, ImmutableList.of(DSL.named("avg", aggregator)), ImmutableList.of(DSL.named( + "group", expression))); assertNull(aggregation.accept(new LogicalPlanNodeVisitor() { }, null)); @@ -111,7 +119,6 @@ public void testAbstractPlanNodeVisitorShouldReturnNull() { } private static class NodesCount extends LogicalPlanNodeVisitor { - @Override public Integer visitRelation(LogicalRelation plan, Object context) { return 1; @@ -125,6 +132,14 @@ public Integer visitFilter(LogicalFilter plan, Object context) { .collect(Collectors.summingInt(Integer::intValue)); } + @Override + public Integer visitHead(LogicalHead plan, Object context) { + return 1 + + plan.getChild().stream() + .map(child -> child.accept(this, context)) + .collect(Collectors.summingInt(Integer::intValue)); + } + @Override public Integer visitAggregation(LogicalAggregation plan, Object context) { return 1 diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/AggregationOperatorTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/AggregationOperatorTest.java index fbfca109c9..a2dfd53390 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/AggregationOperatorTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/AggregationOperatorTest.java @@ -34,8 +34,9 @@ class AggregationOperatorTest extends PhysicalPlanTestBase { @Test public void avg_with_one_groups() { PhysicalPlan plan = new AggregationOperator(new TestScan(), - Collections.singletonList(dsl.avg(DSL.ref("response", INTEGER))), - Collections.singletonList(DSL.ref("action", STRING))); + Collections + .singletonList(DSL.named("avg(response)", dsl.avg(DSL.ref("response", INTEGER)))), + Collections.singletonList(DSL.named("action", DSL.ref("action", STRING)))); List result = execute(plan); assertEquals(2, result.size()); assertThat(result, containsInAnyOrder( @@ -47,8 +48,10 @@ public void avg_with_one_groups() { @Test public void avg_with_two_groups() { PhysicalPlan plan = new AggregationOperator(new TestScan(), - Collections.singletonList(dsl.avg(DSL.ref("response", INTEGER))), - Arrays.asList(DSL.ref("action", STRING), DSL.ref("ip", STRING))); + Collections + .singletonList(DSL.named("avg(response)", dsl.avg(DSL.ref("response", INTEGER)))), + Arrays.asList(DSL.named("action", DSL.ref("action", STRING)), + DSL.named("ip", DSL.ref("ip", STRING)))); List result = execute(plan); assertEquals(3, result.size()); assertThat(result, containsInAnyOrder( @@ -64,8 +67,9 @@ public void avg_with_two_groups() { @Test public void sum_with_one_groups() { PhysicalPlan plan = new AggregationOperator(new TestScan(), - Collections.singletonList(dsl.sum(DSL.ref("response", INTEGER))), - Collections.singletonList(DSL.ref("action", STRING))); + Collections + .singletonList(DSL.named("sum(response)", dsl.sum(DSL.ref("response", INTEGER)))), + Collections.singletonList(DSL.named("action", DSL.ref("action", STRING)))); List result = execute(plan); assertEquals(2, result.size()); assertThat(result, containsInAnyOrder( diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/HeadOperatorTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/HeadOperatorTest.java new file mode 100644 index 0000000000..a510529cea --- /dev/null +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/HeadOperatorTest.java @@ -0,0 +1,152 @@ +/* + * 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.planner.physical; + +import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.LITERAL_MISSING; +import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.LITERAL_NULL; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils; +import com.amazon.opendistroforelasticsearch.sql.expression.DSL; +import com.google.common.collect.ImmutableMap; +import java.util.LinkedHashMap; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class HeadOperatorTest extends PhysicalPlanTestBase { + + @Mock + private PhysicalPlan inputPlan; + + private final int defaultResultCount = 10; + + @Test + public void headTest() { + HeadOperator plan = new HeadOperator(new CountTestScan()); + List result = execute(plan); + assertEquals(defaultResultCount, result.size()); + assertThat(result, containsInAnyOrder( + ExprValueUtils.tupleValue(ImmutableMap.of("id", 1, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 2, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 3, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 4, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 5, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 6, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 7, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 8, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 9, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 10, "testString", "asdf")) + )); + } + + @Test + public void headTest_Number() { + HeadOperator plan = new HeadOperator(new CountTestScan(), + false, DSL.literal(true), 2); + List result = execute(plan); + assertEquals(2, result.size()); + assertThat(result, containsInAnyOrder( + ExprValueUtils.tupleValue(ImmutableMap.of("id", 1, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 2, "testString", "asdf")) + )); + } + + @Test + public void headTest_InputEnd() { + HeadOperator plan = new HeadOperator(new CountTestScan(), + false, DSL.literal(true), 12); + List result = execute(plan); + assertEquals(11, result.size()); + assertThat(result, containsInAnyOrder( + ExprValueUtils.tupleValue(ImmutableMap.of("id", 1, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 2, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 3, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 4, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 5, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 6, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 7, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 8, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 9, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 10, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 11, "testString", "asdf")) + )); + } + + @Test + public void headTest_keepLastTrue() { + HeadOperator plan = new HeadOperator(new CountTestScan(), + true, dsl.less(DSL.ref("id", INTEGER), DSL.literal(5)), defaultResultCount); + List result = execute(plan); + assertEquals(5, result.size()); + assertThat(result, containsInAnyOrder( + ExprValueUtils.tupleValue(ImmutableMap.of("id", 1, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 2, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 3, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 4, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 5, "testString", "asdf")) + )); + } + + @Test + public void headTest_keepLastFalse() { + HeadOperator plan = new HeadOperator(new CountTestScan(), + false, dsl.less(DSL.ref("id", INTEGER), DSL.literal(5)), defaultResultCount); + List result = execute(plan); + assertEquals(4, result.size()); + assertThat(result, containsInAnyOrder( + ExprValueUtils.tupleValue(ImmutableMap.of("id", 1, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 2, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 3, "testString", "asdf")), + ExprValueUtils.tupleValue(ImmutableMap.of("id", 4, "testString", "asdf")) + )); + } + + @Test + public void nullValueShouldBeenIgnored() { + LinkedHashMap value = new LinkedHashMap<>(); + value.put("id", LITERAL_NULL); + when(inputPlan.hasNext()).thenReturn(true, false); + when(inputPlan.next()).thenReturn(new ExprTupleValue(value)); + + HeadOperator plan = new HeadOperator(inputPlan, + false, dsl.less(DSL.ref("id", INTEGER), DSL.literal(5)), defaultResultCount); + List result = execute(plan); + assertEquals(0, result.size()); + } + + @Test + public void headTest_missingValueShouldBeenIgnored() { + LinkedHashMap value = new LinkedHashMap<>(); + value.put("id", LITERAL_MISSING); + when(inputPlan.hasNext()).thenReturn(true, false); + when(inputPlan.next()).thenReturn(new ExprTupleValue(value)); + + HeadOperator plan = new HeadOperator(inputPlan, + false, dsl.less(DSL.ref("id", INTEGER), DSL.literal(5)), defaultResultCount); + List result = execute(plan); + assertEquals(0, result.size()); + } +} \ No newline at end of file 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 151afdf779..f4f10aec27 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 @@ -42,7 +42,6 @@ */ @ExtendWith(MockitoExtension.class) class PhysicalPlanNodeVisitorTest extends PhysicalPlanTestBase { - @Mock PhysicalPlan plan; @Mock @@ -55,14 +54,19 @@ public void print_physical_plan() { PhysicalPlanDSL.project( PhysicalPlanDSL.rename( PhysicalPlanDSL.agg( - PhysicalPlanDSL.rareTopN( - PhysicalPlanDSL.filter( - new TestScan(), - dsl.equal(DSL.ref("response", INTEGER), DSL.literal(10))), - CommandType.TOP, - ImmutableList.of(), - DSL.ref("response", INTEGER)), - ImmutableList.of(dsl.avg(DSL.ref("response", INTEGER))), + PhysicalPlanDSL.head( + PhysicalPlanDSL.rareTopN( + PhysicalPlanDSL.filter( + new TestScan(), + dsl.equal(DSL.ref("response", INTEGER), DSL.literal(10))), + CommandType.TOP, + ImmutableList.of(), + DSL.ref("response", INTEGER)), + false, + DSL.literal(false), + 10), + ImmutableList + .of(DSL.named("avg(response)", dsl.avg(DSL.ref("response", INTEGER)))), ImmutableList.of()), ImmutableMap.of(DSL.ref("ivalue", INTEGER), DSL.ref("avg(response)", DOUBLE))), named("ref", ref)), @@ -74,8 +78,9 @@ public void print_physical_plan() { + "\tProject->\n" + "\t\tRename->\n" + "\t\t\tAggregation->\n" - + "\t\t\t\tRareTopN->\n" - + "\t\t\t\t\tFilter->", + + "\t\t\t\tHead->\n" + + "\t\t\t\t\tRareTopN->\n" + + "\t\t\t\t\t\tFilter->", printer.print(plan)); } @@ -87,9 +92,15 @@ public void test_PhysicalPlanVisitor_should_return_null() { assertNull(filter.accept(new PhysicalPlanNodeVisitor() { }, null)); + PhysicalPlan head = PhysicalPlanDSL.head( + new TestScan(), false, dsl.equal(DSL.ref("response", INTEGER), DSL.literal(10)), 10); + assertNull(head.accept(new PhysicalPlanNodeVisitor() { + }, null)); + PhysicalPlan aggregation = PhysicalPlanDSL.agg( - filter, ImmutableList.of(dsl.avg(DSL.ref("response", INTEGER))), ImmutableList.of()); + filter, ImmutableList.of(DSL.named("avg(response)", + dsl.avg(DSL.ref("response", INTEGER)))), ImmutableList.of()); assertNull(aggregation.accept(new PhysicalPlanNodeVisitor() { }, null)); @@ -141,6 +152,11 @@ public String visitFilter(FilterOperator node, Integer tabs) { return name(node, "Filter->", tabs); } + @Override + public String visitHead(HeadOperator node, Integer tabs) { + return name(node, "Head->", tabs); + } + @Override public String visitAggregation(AggregationOperator node, Integer tabs) { return name(node, "Aggregation->", tabs); diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/PhysicalPlanTestBase.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/PhysicalPlanTestBase.java index 9bc4fdc38b..5589b37664 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/PhysicalPlanTestBase.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/PhysicalPlanTestBase.java @@ -43,7 +43,32 @@ public class PhysicalPlanTestBase { @Autowired protected DSL dsl; - private static final List inputs = new ImmutableList.Builder() + protected static final List countTestInputs = new ImmutableList.Builder() + .add(ExprValueUtils.tupleValue(ImmutableMap + .of("id", 1, "testString", "asdf"))) + .add(ExprValueUtils.tupleValue(ImmutableMap + .of("id", 2, "testString", "asdf"))) + .add(ExprValueUtils.tupleValue(ImmutableMap + .of("id", 3, "testString", "asdf"))) + .add(ExprValueUtils.tupleValue(ImmutableMap + .of("id", 4, "testString", "asdf"))) + .add(ExprValueUtils.tupleValue(ImmutableMap + .of("id", 5, "testString", "asdf"))) + .add(ExprValueUtils.tupleValue(ImmutableMap + .of("id", 6, "testString", "asdf"))) + .add(ExprValueUtils.tupleValue(ImmutableMap + .of("id", 7, "testString", "asdf"))) + .add(ExprValueUtils.tupleValue(ImmutableMap + .of("id", 8, "testString", "asdf"))) + .add(ExprValueUtils.tupleValue(ImmutableMap + .of("id", 9, "testString", "asdf"))) + .add(ExprValueUtils.tupleValue(ImmutableMap + .of("id", 10, "testString", "asdf"))) + .add(ExprValueUtils.tupleValue(ImmutableMap + .of("id", 11, "testString", "asdf"))) + .build(); + + protected static final List inputs = new ImmutableList.Builder() .add(ExprValueUtils.tupleValue(ImmutableMap .of("ip", "209.160.24.63", "action", "GET", "response", 200, "referer", "www.amazon.com"))) @@ -118,4 +143,32 @@ public ExprValue next() { return iterator.next(); } } + + protected static class CountTestScan extends PhysicalPlan { + private final Iterator iterator; + + public CountTestScan() { + iterator = countTestInputs.iterator(); + } + + @Override + public R accept(PhysicalPlanNodeVisitor visitor, C context) { + return null; + } + + @Override + public List getChild() { + return ImmutableList.of(); + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public ExprValue next() { + return iterator.next(); + } + } } diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/RareTopNOperatorTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/RareTopNOperatorTest.java index a8bf626a37..a2b7be1c62 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/RareTopNOperatorTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/RareTopNOperatorTest.java @@ -1,3 +1,18 @@ +/* + * 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.planner.physical; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/RenameOperatorTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/RenameOperatorTest.java index ddccf4bbbf..d43bc3a83d 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/RenameOperatorTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/planner/physical/RenameOperatorTest.java @@ -43,8 +43,9 @@ public class RenameOperatorTest extends PhysicalPlanTestBase { public void avg_aggregation_rename() { PhysicalPlan plan = new RenameOperator( new AggregationOperator(new TestScan(), - Collections.singletonList(dsl.avg(DSL.ref("response", INTEGER))), - Collections.singletonList(DSL.ref("action", STRING))), + Collections + .singletonList(DSL.named("avg(response)", dsl.avg(DSL.ref("response", INTEGER)))), + Collections.singletonList(DSL.named("action", DSL.ref("action", STRING)))), ImmutableMap.of(DSL.ref("avg(response)", DOUBLE), DSL.ref("avg", DOUBLE)) ); List result = execute(plan); diff --git a/docs/experiment/ppl/RFC_ Pipe Processing Language.pdf b/docs/experiment/ppl/RFC_ Pipe Processing Language.pdf new file mode 100644 index 0000000000000000000000000000000000000000..33f6d0b07a83ac50876e0df90aba31e73e7b5068 GIT binary patch literal 316152 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

`TqkaX#>TH?<8DkdwL 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

?pEm6VR zy&aSzZUje4%L-0Rm&9|^0%jjrRG!vnc>|w6E7t>NE*G^F#$lJ_AP*capL~?ID97br zzVod?OIHYJHrA>kIPpjx^U;xeHtJ2`4XSCh9e^;$2keeGFx3*JHONi%!mg#PJDnb&7?E}R_9xhw%88_lN!gA z?@^B%C3wOMj3Jy)Zck~9{`jtdwpj}s?Jym>q@-ZRP4_q!N1`_WF>0y&I*?Dm5VMo7 zyu9stGr}kQ<&C*~e})a*<@q}}dCF^wvJ0&0>7x)HJneL<0C6ZBMh0P(oO)-EWHq@& zCtW=~7Hf@OMiv&uY701|J{v#?m)-&y$xkNgVq@x$Cw~6?X@lEZFs*ze$fnX;y5q9m z=%B+jnztyx{dMMX<28hrwRP{LjNx)E%M-)TCz0s6EYs=c=x zLD0?nVgc13&{1^4_6haQ{A(2xd-0=qjKre~BeyAFZk=V#V43se`+Nn?+uo21M7(>G zX>#&QVK(QnM=cnZ{s26x=?GF~?{HirdT-O1#uOMIpW*J2O-f5DXrx7oMwe@ey|WF~ zcPyTcAh@z3tv~$<2cOUAJ+@_iBcmJDv}RWI0GOqewdXG$HMUO!n?vvZ4^Y1|Ye`q2 zogz+ZE6(|X*?c#45dv?)(%G?ccugu6>+wi(-1!~TfPzI3eihayX`e)^DbM@%fR@(l z9X?8kxv?6&0w+HV3D#e}7Z(GH7e^z+#c^KFycFb(yPYiNQL?a~{U{+a`L6g-QE{q7 zLgw^WK;iZcGrvlgHs=Bo&d8$IVcw#ca9k}uGY7B|e2E-uaKcg>&FrTuDx4*$rtLc0 z<7Memw>aoNk5@f*`?ezk{0R{Z?4oeh*N)oCE|1>KBG;byaxm;@nrShZ8TW+o1@b7& z;ZRaszW3(obDvJ;3cKerNfCZ3@Y|Pa{@f3ICMzwjJE`aNFM~G3$L)of8e8U2R7 z9O*%4Q!=SCgct-3KQ>J6)>4v_x_DH$;tyO0j+qDL0(DJ8C+$QzI zxw!z21zjVBKy7VuAC{7yL5yjW}!&j(uik@Xqy_rxO`c_XbfSwz!j3z7QC2 z4jj6RJbajb`LQA+J^f@r=(EL9!+Q3o!9m3VFqB~NWFG#sDWEi|0G=)EU$I&N05HPT zBW3RZpkUe5!C29Wzo6X>KGOd;=RKT10YH}nQTSFEgvWeWLn8#V=pao?)B={kgMpWF z-b}<;b( zT5lsp9^Yr$>$;+WWVy$+e$^5OWQ}yiwE1|=Te-8y@3m{FENlu+!LGrk=RdZZ`<)9G z1yz)B=@G;V{rm6GXv%lsC>f7;Ndo32W0vul5}hT5tOA)pptsb=uUuVM*)GW_%$6 z)(s9mhnPd*Im{g)Bls;x-F?OTif>6#QGX>OK|;#A^C2X;f8X8u>(HOf!FDfGwmBEY z&j&%Mw7xo;qb_?3?Mo8qiU!DSRNtTNvde44p~f}A5m(MN zaT_oWej5AKhr0y`H%L4df|V0b01W_DZl9r6SlTid>nl*q`)a9rYz;n$sP zuL2oopFrS(6`j)I;#*$1a@yjw6wUXq!)tJ8#{0Ob>_;Vpz^V)wpXCdr{{ zElO;0Q+W`-t4cPd#gnYs0zS|F4JHti8rC`+!96hlgyMIloHK_9CuWQbZYR;;K zSsJug&*R(gl{|-~UL={X35YMhA;KgpqGhBzDRotjsMZrJDkkQ+dmWIDmg$Ees0}&= z%B5%rlnUemt*op}OfpGQKRy=!hB0ovMX0M|;%rbV=Pg8lYmT#eit@mExp;~q))%|^ zrKimYg}B0Y?^#^ji;9wRd{~RMfj_S0q$H+}jzv!z*!OOcioly%C^n*7YrvqH5$c-N5&!D|j_i2sfO~YWlo;m& zKdL>nRVJy7HP$aKF0#6_H~Wrlo0k1d{XPs8H7XYxcy%iTrh~=V1+1mnDaF(C+=%p| zEI&VU$AV-feAJ*EZ@uy(y^!;weycm5-?R;x*q0;D@4rPSo)2?&)#t(wRNOG#xF4AJ(6M&go9cUOAHq z&!DQe5OHfGA*|3d12IMO?2z+^Ti}qIdk$?EK@M9sRMK}R5Gr5mpZ4{TAfoAUz#1COz?<0`pTn*wq=wD@CBlLynfVvL(#FI64H%;1Yp$%L+ z)Z3V~ad>W1V{Ema{rp5V{gj-v97gpaXKI`bjCxmrkCQEE8$&lzY``bwodLxB4mjOq ze;OJKmkS|m85^7zh<9hY*}ftc`fJ_q&g>{N3kyRl@DuQ1V)1Ld6!U&g%QQ8#Hv|1_Y?x8}MmI;tj&ky|m>3zoob5D1 zPtPFVzxE=FNmnf7$;JcnKyS)t%@`C(Tu`8f!9nI7Wol((Yx?Zj$vV@Ue@dRitLv+u z29XOgKZt|Aj*Qs6Scc(w5Fhb&y6jDn+JDJa9DEbao*>O`+Bv$NVqo^?D-)zXUe%2v z@vV5VXkspGa?s%;j;)$#efeV}U^u((05lrLP_B>(!TBvDi@#v3kb;>MO$Q4-2_f{{ zwTb<DqWqj%4G z!QTMf?b@w7R%y+S{Qt3*n5}l&0u6|4EtpTZqNXKQ?3Co>y?>Hz72v%q5Z-hBX5d8G zdPRNx#88iX;p(PcuA}YXuQ%_Lk3& z5u7G8m8ID4-3G3&VHWjgoIn{Bv)HOs5ug%14v}_qYEJ=`H2w|{Xo&i_dYgKz5;?#?b$%Ij1#!C*Xt{d;3Tx6|-&{#JH_M3PfwDUO_X zm6PLB*bUG^qZPD?>V9xW_CjSfVEDJAo!A&LyJ>j>Wqn=QQdSXdXaPr(E=J}F5R2aN zIYr%Tf#ETr)Aq!9@l2dFdUdc1dOPC}b+fj&zI;^-jQOcRI#0eQljNAhHBp#h<&j0d zIXdTGXg9Jl>foKSD;ExhQpk{JcC0tcTJg54?MD9kP*H|=RikEUPB8H{pp`_kUp89W zQ~Q6h_SRupZeO?X0|Fu;AtfLo-JsIl-3TfTDqYfzf^-Us2qF@Kgn-gr0-~gZlypjW zbLOLa?|1*s_g?S$&UbzL+J7L=!yW5h_gZs|Ip&ym)}StmkdROYzy4%P`S-b5zN`K8 zT)4IZ73qU!$tBt|)5Lhy8RTPKEwjjttnkAVQ~rRFSuD5Z`HtCp*ZRSGk9{JSLgXpH zj7BGJ_BNq{4_l&(U|Mr)2nv+3sUJ5N!1K1_&&#Y^wdJqVwW!f{4crqwoQ(%Jj<|6k z{%Jdn&yVzepWP1H2@*O$^ktU((5hI~LN-!Z{;y}86cnJx|9O@bZka7U&y6{y&M7~m zsxdq~QPa?{AZ`V~6aAqS)UCXXO&m-sN?;a_ey ztO0fT+%TJQX!&({m;y6jHdzU8LP!If9Jmfy_-hKu(|LNhC_H4GKbZsm&`*nEK7`yNV#Sr=r;TU?g< zWZlL^a!prNR4xy2wDYWBSibzS+oxopM$q{zv42vQbyK4X>KF+~kQLcbgBUo&xK2(t z2k8LQa6pB%55AIQwiqvwG^@kj7tE%4`!>2X?qNh!)YJ7ISl$46dfriU&$su5r_=Pj zB0M5u%3@en$oFoB8o3)0>?p|hO715D)@9rs(^T5ugbZgZteS zw3VN=`tz)}px#YbSbr%=M~k)4uUR`MCnuI$jhQN_CTQ-`+MV$uKfG6Y%)&C^UyWd9@2?&CQymTTNajZ-8$WV>n^`ZaRJYqspfn8)--eB(Sjl`7*0sSPfgJ0 z&12=YNPjaQdqVlf;3?SrRHBS6z1-rfyUo8j)Gv{|abA}1Mf*!wvLC_}Ur6ae?hJ4S z+QY7tuyz>Yuw=&NJpkXzoI3pCQpW|J?hWF~gKr9pSlbIeLib@CL$-Xrms{cCC3l^H zU)k3h6<3f&HJ5hPV9~)P&i_OJn?oa$X#7FFrab~JvU~h8U45rr$j;R#a2r%bw9b1_ zDftLcc7)yL{q_``6eR8SV+=}cy`YLwcKm)XQzkIx36fO=baEQwC#2m2s`mGGvGPp7 zKn60JIM~=%Tt8WVC>r6`UHE{@t?A6_ob%32SY!MNi6{0F4+Y66n`|9toN`TpJ%pxh zhfsU^{3S45*XNl~#+p8jhhQqqNmg;740+w8*7Wr*E<_e3rD-F)1FK>TDfRy!D<#K8V|MQ-D1b= zAG{$f0am*vbY}{Yg6GY6ik+LxvpCT8LY^(36KG$0dbBA>Lh!@R%)+t+?kBpP#JU3& z+qUmIU(p#Q7gBU%wggH)=e?J@{179u2J6PyV7e^!oTsEjo$_=uia-=ePo47In*qeH z$4)?zoqzONxNh0#W>40El?U{y%@O8UH=wk5M%9n-G;&*C^RIdDErpn^kJPpety-Yv zFtSFlR$JrdCM$BUurfE>xt+%T*j+^!1P(;0m+%z5rU;(F%1Z;ShEjKC9fQ=)Nh~|-xSI4+1_3o6O8H^hg6fK8(W!Yqtw?U+@ zQ$E?~RMAOr6Q@Bi-8b(~2 zx5Ker2<{Ca)-{~k!TuE9s`&@`0Jt^8Ef<{5L#e>nSHbcL96%nd0S52dgFGXNnaq>A zLCTrfqbh4RxV4St+dWkF&MizA9LYH*=M6gsLLwVuMh@LOUzy*u5QgkY)6|ohK_FH3 z_-5-nFamEX`~yFmkf5eEZ?elVnXVPD!SjQCOUc19R?}p229KEW`qXcJCg2ab=uv3V z_j-y?K9V|M8_1lOSKk;&lKFhmJHEzq-ej*EHcsKF!_Qkn@i^+Z*)IB)2SUx=J;wcg z{Y*Stj?aLo$ryj>%=}I#uu$Y9QHXbS@TGot#N`+Y_DiXa!4DR&erD~WpPN|v;0|8S zJb^^!+Q1f>KRR(6N9-8C)7ULt_P0p>llxNDz6zf$j5YtoNtq#$FVUj$UQV$1K-2?u z)gAUgF@rZ1ak2No#uO~<6CN{Bl7SdNIuP*yHvp|{Ah_7p@`SbO>DT?_iIZ041^4jj z3iQb1`7R}c=w)49-93nh50!jt0FZI@oU-16v&9KroFp>5LcUUARf1UW^NLGJIa#Y- zzh|d*7ceK3r{7zL4qIC`U{$XGO&G1d$KoIxH_JdK2ww4K`Vz+4hh6)GQ^c)J#(|C2UuzN^*G%r|d-r8eIPr4Ai1~UY5 zi(la4I3fS-wXj~|&Cz1;$-0lUwy9QBn9@ zt3ZX`?#JE4BqbthSML#X`Z7l`eV{iSpYJeHXJ}*;9S_UVOmJfow9#fCvIpWRHKo!> zz;z6@**EWs>^SHOB9A~z5so!kf9-1q*Fq`)yV4<}z@MUvb&47_Jh&Q!m+;o#zPp3PbBPD3PhCC}a6ZvbQCK9}*9|I^z9cOO$t&CMk{ykb>k^ z;O-mWy{l)_vpxB$$$WQhPt*J4!!KwCfSrm`W&#=^G@B2S0BKcb3dv|}2W&I=f!u1z zfUVlDm@0sVKO|w9Bn9bTv~P7NK&uvU?#G*daZ~VMzZ%q9%h=hvvv5a4T zjwYe~NXZfm>YC!<`3Bzk2I!?io<427)%dE>7mTG+>iA7xGe=XL;cCx!)1}oglKfap zPPg9}(HMrQGFoZq*uu_t4H`i&LzE)>xkoJB9xVA-Mc%y&ogcrhOa0~>4oTO5g0WL~ushSG1P~3r zdm$0UTz1tVx}0C~sI!3;r8lGRW%jV(b+_X^yP%;0eJ1h7B47g*$>hH{u-b_GA$!L&tU>*KK|$&9dB1tP202QIRe}iV=BI(gh&3e3Dk|c((s}>|8Q{{dyziRFI97d=2pfHR{*PfScZpuu^i!TuhLW%7zBLnK`4N zlVs?G-ahxYf|p>iIZvYCwayf+THIB1LXra7tYlZ5)*|_FOe#@MN$s2K4z8>OIu%V2cn!#KDy!kkYBz2 z{IU(b;}y#>piBoY*tL_YQFb8CmBVCEPrlE22y~=T>g(zTYfS}xRCcAqXuh4`+Xbj} zgZg7~*e3OjKJQYII@B)IwG`0#u{1}EfwvW#m>AV#WSIK-vnzDfWMR1!JZ}Ac0_^%~ zZ%hltTSKz)N&UWx7YO>o)>4FAm8!~&yq2<%F0z?8+)Y4eItt<2_hl*q!Qt}-7T0pC z@AE+R9}Hc;b!!KDoK?-r{+*A!buYl3#fm$;;W1?4zyq9DT)aLlEe%}RA@PVoggDEn zxxs}UtPoTDjW9u0jE;^D%tc+kMEnUr$ef8cC_90VdSRYBlw4-)e_ARM_A_uBm7E>4iJz^x*JEG0v}vGGBKZ#3 z^Jm1y>l5{$&p9fuKgI1b2j>`$vaIfWz6DNp_HRnAdEz?dY3O>?cm!0Yp|9AbX_te~ zsUTm(_C0fROtNyracV;Ou!;0+AYtENgB~=QGjR!}*yOy{CD7XKjZTm|%ce1V*=KQv z8vAh}&*37_uX=C;(M2HxydJr!;Z}OpaH+_uTpA&?PClnV8b_V$ZK_CS%Zz7QY(b#? zanS&J72L<3=Jh-Apd5IoN+AO>tFOr~58TU(its~bh9Kv7ka@jEIqV#D4%yLrfG?2@ zFJoZuNdqj~95Jnv;H!gAMfE}1=yy=ZD7l}iC?Y^I+Qf_5%mOo5=kDF4*Hx}-V^>pO zWwy}`W-4CEQyrI?&o5IWzPkPpGPAgJAoyt=H;LjXE9Mgk0Ov?(j{6KPEG+ED0K;F| zn6%^Du@B0hJG{npg9N0u`-t*BX20qow*f38Ak8cY&PkTThBW=zS0G>jMlnFXG6Wf!&taQ~-knHS z7*}jOybyKw?-;42@KW8eTYdq3&X^$mG))U@S?oW?`& znCIvqu+0J;jV|f(AXp^=N4sn!#k-P3|5yp8*P}-tzrWFfnBv*9oepNZK}xa{%}dlr z_dFvJ12PXZA3(aInxKJFz)@meM0!Lw|Bk7v$3y`rj0f4#k<$<_P2e`aNGi!h!B87y z%6L7d4Mc^3R4N#%792^?(@|Ks|68>n2C9Vwd&NK61T?;lp|rc?#(a(HAUkJfXV2gM z#RPOnEwp$3iSB~^Gb^a#%ko(1jUm(zogc=%QEhQ94@hRU*fa-sSl>cN>Ys$-;SOX- zK{^qLJ&8-)|ANmZ?MGDUFaAg+1)o}*eNyA-4ZGwF1TzL!;Prt#EEGp#@S9M=?2Y&b zwOjZl9UFd{|1n0&tAlm>`(mulhI=&%!Q)rp?I1X=!M)GhRap3hQ)3)+leW&QW2D@H znt)&`>gI9j_`%MnrF2k&;vBI%?~XKQ9ZpOLdn~=h<0nFVmN3yL~Z)T?KUJItPKG^}aNDTKGi#8=cSEYlWHUItm&Jv^KjEfGjM!?tUlr zYHLoAjK9-Z{~ASjfhJ3RHCC0hD;*r?Bw<+4{-@)_Nw2*^`7GR}SRw~MKT&B{mm*UA z^9-NbY*u@|-|73f@3wq3ZK3V4&sP00MflN^Rapn)@|61N804KjjZ;t3Zc}yN?=)Ts zp0TDE8_M^x(|TxJo?buAh5SNj3 zf@jjOeZ{?~pk&r*^{Ul`jhtd_Q*(_ z=C4^y^Cc7`d2wi&RY@DHWH_K~F(dmc4;wVq0z2eeC~u&Xi-h+kp&i`0Shp5%amaRW zW{>w6{e7oAki@eJgYSnKGjA2Os2wQ?5U$h32LpK@kHpr^uK zI$F-{U=+d?5QqIa0zuHS+%4_}LnV;YI^8X?BtV50VaDKOFY~kBpL%neAB;}HPhd>B zk}bn;S`h5b=zMk-HG{JBwGuVOokwtUDKyQyKx}l5P8AMva-<{$XxJ(iRQ;mmEKoy8>HGd^VMMWj0 zEu+DX9An4~Q>}uNv9b>sc(wfc-!7IO*#B=|zBnCy2V=koJDtkR8GbOT33p#PQt{i5 zg*rZXkaas7IYd!3I{fL61=qEtP%qKFxv`r=eIj3B+fMEe?%<4ZRgp(Y-|ayVLGBFm z7mZL(?15W`9|Rf9%*+}`OuOXBse&oxYW`c4wn>X2{s80mpP;?{XU0uSJizlL{O8pu2RzaO^(jil>#3RtTOeMg=Vh+my|8G?TQzQOmCNYi^DqClWc0Z&0+pN+an z*U_gVw&C?-db~^AuAjsnO&C}iz{lQQ*%oGyvhN)f!y2pr5H9z-*s{S9yuPe&%VwrWNn?+X0A}}kZ|+A z5f;&4dNW$p^4<$rmJ3aET&}x1UvalH7oh6w#{N(5!n(GRPS#@Y3(&-LU?QS)s=7U*_E%==A=N=RPvZEoOW( zXB6-XV9VEMF3?%b5c>g^tG<7U?e{RO++9K$yn~p2kzzc-aqc`IbFzDX|=Q zIFeq9k@MH1<;+KFQqt#j<~3AT-5kH$_xJY)1qFeGhc*RLt|TO7Py~kNxB@~lSd4&m zf?}=@-K9J-<9t+H@>A!ViL{0L+@OGw)7^Lg}rj51fAL@~5MK z3X=bEC#lWN*}zR>d(pDOS@~L_v~NXk|6#THS20omWL42R0w~QC_u9#60ZT^X8V`cU ze^zZ#&MtMbclPJKH^w;q43@9A?0Z`zthD+IEIse)jTOmF#zRJTm*&M99j+0lL@)xw7=mbTdJ<@bttkO zHxH)qn+lfhW9%*uqvTur#Naux{D|Yn&AT8)q;HY#d)P@(*txK5-oQvjiIjPcl~6#C z21*+lq!?IX0@iyF&@~tXl7KicdSHGG_{b&KC>(8m;n{2$7|Gp)_KncX$I9zo`s0?h zf_XvscuT|C)GOZ-^1oUvQuX6h9S@9!)~cA^k_lVS{I8l>trO`cL;?!XET~O0O!5<{}6Px4-$9 z`EXg~mKUP-2E$7Usha)@D}E&E+{bkMu@Yg(+&9?JWWn@{pB z28JvYV^5n6-mSKCb%DH0HNd~fvsYMAv~_;-svQa#N>nqNA=Bu13aeF zy@_NxhRegAd+VN0O9gJ%yUQtd=n2U?2)NySbO_JE;ngkKI@LFJyxyLvhIOYu4<&RZ zTi!Ev8BM=6~R?y_QIJS-YwX zDbe{FpCTR2NFE+6SSA z8eh-8`{`(?L6MtylfO?vh+&-$e_^hE*BWZcr88Qdzhh337H3xq5$p7oj>R8vgAt-S z{RJ>P-ImK>adg(TrB1A3&Q(??5IC@zuKq$E99sONuH?G8wehNu^G2`N?#98&R#sJ| zGrD2aV=5QEk72k&n@Q)kmy9XSWto@DJbo3Ej>t~kn9_MuIKsig)MZ3S)r%5$t-8L{ zwvENGzcWa$(d5>>24uYaP#Q%43@vNK?dC{3Woq zv);pBK`Q*tIBiC}6MIv8Kj%cr1N|awcXgTA<#$f2?3c~c)a3cfQ$Og6fcMCF8F7DT z8}8%y_UFf4AIAK<%=DXn?HXOcx7==1)8RKvLn7?dJPH?iH}O|zPet5|{1gIJZ++Dq zuN!lt9AOH3YbFqr8N@-{98qta|5d)jP?lc?>uJS)o8Wwed6xW6A+<_c)qv;fN~^@{ zXy_H8Xk)I|Z|8H554E>=y(wExTi=#Tq(ioi_)VFmT}Msfq;d5&I?A5P;3ZK1Z{qvP z+qtDy*PZ=f;J@M^?0oyM<5SKkwNNdJKgYgC*z2b<9a*F5QcJhGFd}NR4@d@7^~WjO zkZ0=ndi>+|^|$tpFF3>(xEydtT+*d{+~T_2uP{|RgF#Tsm2aMjgl2pyH{KX%@;g`% z%`VR`$_rl`_ObZJgsGCU{r#d{Hv7T7eh$+ugTvjt*V$9P7PD)H&;9-8>ONsM3Y`Wwh zReMqKWS%o;rHf~wly6=X)a;-zah@N03wImFEdEe6qw%TwW>ES__Yz>AX`XPqZ^MRZ zqD6CAs);`Pi9bIpn%KU##~p=BvKP`U-t?$6pDx@dog*I1HQytexL3Kp^7!LL`-k$b zc~>APqUvKknKKq2Q1E)gUbg>!JV|f;jFD5?L*`=Jf!}&$AKQdvhFGg2ysRKA0_=!e zgXxMN#OCxTRP|`DKbyB&Dmt2szk6+B(WY3ncc?dd7o@n-XUM zyt=hjD13OG@NP?8;*XhCj|vZkuyL66(+v<#(TX_jJCYwC`&NHF<{gK|Pu=+U zF0mJqWH%DaF;*)b_22nO2h$_MijP$qnBFYhv0WEsVH0LizAs^)Nlhpex{XJ%pOk#K zCUWe~fZhJ0Mqb3a&zAnhA&&8?NWqTE2SEc?mIr64pZT74ucf>_Ul?+WLKp_tzne8< zZR;dY_-D=(+$;Ay2|G8}zppLdjNlx~mveAFUrkRLVnA{D=D zJL8g`jGrHv>r@W!kUmv`;9+@;MLHts7hZ1nVTADs>J%Vz#obkx&J;H~%z^)0xK_UH zHQC%Gaai1Rs}T|SRaa^0WHRu0TM5u2|AR`uyDv0I>S9%IVWuXmgcGsWSH*J~+>Ef} zP?}=~gT*=9W5sr{SLwRtYdq5U8ZS`AER)riJi7Y&w6d!ns#Qz6VoO0ykw^QvpU95? zS+~E&JX>9#;E0*GvfjzN{HPFG&W+ELuNb-w9~N`y);yZHOF$qyo`4>em8T(=C+fA8 z0_)W(dzJm+p^a|3{k5vyVC6Zrx{vAW+jiH9N14CUb|^i+5uTkh+CE$Irpw%$jq-ss zipITN!Q|70Ppan`vXID2S$-va%YICtNe;A-NCelg(gB$B5AB0zwZjFBs$}s?_bW$u z*XZ!NmM?t%sBunliV;I5K5q6)O(k_ZzRGo#g5TBj!=miSM-I2;z1|XZ68-YG>~lLX z9ix@+hgRWUeZBIqZex9=A2*zFi8xV1lmD=vG5X23FIeFMV{0krzx+=#n3m@;@7n|V z`dKaaH_Tb(Y=Gz7QPa%4D2$drPXCxzsa3k&aHg?|~l40Tdeak7r-8 z;3^hoqP$(4>HKtxwl_Ol{L#feE1NOs#M-9Jc3x&Mpbfv!+%F}%wV9!iY00n@R9GP#0wC zH?Y%tZJmP06`~Db^$#~B(n4*QvF0|QHZ$f=Hwob?^j=W&uYwLd@}zRIu(%YI%V8s9 z(U8QEGZeRn#$hjnNw_I;!#dJC)RNOt9GoqF129N&;~fYBWwAO@?+>j4Ifp|3AkEEK zP_#F8`Bd({?B+;C`SX__?n$p;oCvs&l|$bt(3F4t_z~Rmug1I-Im{+q`}pY-5I%n( z*4iO2*55FAGZsWpCKkay+&>pcx&v_kqK)7Fz;&NL%+Ahs|25djNho*8C^_g)HbtaD z;QYT9$lo0CER%EM(b%G=t2BC+J|Plb?0%&9i}Mnihe`thgR4#y0ERM7coBcCRH~qS zBDT3OH2<}r?LP@(+1eFk>z9?EsEg1D*32yZBQ`QCl8>U-68+CCHVo(_#p4X%*U``) z$=@xZP^g4{0}u9hmfq;%0Fi%uUQp%b3VhxL&X8*`@k)NC+?EP8f~(E!eD(d4dOadu zQ0L-vsexNU^u)^#_|ps^v#;d_5ceuEah6(%A&CqbdE~bqEIXBh zO5|O3cHR|wsA>EB+0!WZpKD5|gWLX#vT}6YRaX|J7IIN|o%RP-q$aXr4n$c>xY7mn zCwdJ>{TBY?FTVP2JZD#dq@|2l{|pU$d^8dTFnMh|ALi-rzq>hgo0|;EBZC%01y?Z!I56QtrCeHmFZO>3=ii?I zA4t*rJ6!#J7|UOJ9?6&|ug*_J-`KoJrU(y5tv`4aR5&_d%makypA(NP559GFKdIVt zeBb@xYbUO(>|^8g5yOm*Kln{X{+ktl$pDA_LCC@Gr*8v)NOfX#s^_senh%h-izMhn zqv5J8^YoXecMo4sO)7iz2Q|vEFf_#p(OnixME_ zp&}ID5XL7*d%VTBxyIjEx9>v!iRv@{soz@v5EN-xJN zhjdLqPLz^D{<;LkfNBPMs@F2h$vsdD-1o$!J;VNn9*Ln6QiyGacWe47jeb-R(x;&3 zy*ATW&hYT?|9Q%~AST{>FahtTwFrOK1luyNe`Ncn@p?vKt7bQc!1Te4inGDr?M`uYdpX#pAgI5(x=iS`!>|vSzYYGzpx4 zd5-*TQ}SPiCI9ggnTn+8)G;-ENtDOjQ_@lLB3iopAu6i3S&i-N%P?qzV!cqRm#?5p zz9VNG*UQ>^+p0MUw=O>^&``w~ea(GJN->I-2Kgs(rEc$oqZPlf}V094Ov9ydcExsvOU7nT+(Jg zRqxd7-igq)lT&>ab-){Aa;3>HUPl2L zthi5)%BDmV_O$M|Io=mr_?Znb3O79jptJQ!Wqy(8MXq;zfNV-?Sb_f zsI*|e?DN*JA1EW_Q1@QjA3(PL#Dr!m_vn5qFJmO)b}&%fk}9y5Zu4XH+Vn|QKr@P1 zt6ylc!@O9=e-Y=iF*7+Oe+o*ynS1-<{VavI{^OV^i!`tROS!aN zb3M+&<@pM&UC1|$wD@^9zVDVpr+GtCikPY6mVqPR&kHh{(FgyXp(lBgfHfuiMxLn?>1JaZz~U;ACmg2eYPS8r^sw2mhbNXJP* zd=s=7eJmQk)t%2hNla2s- zZdX(Lp*knb7-d5=Um}*IPiEEW_5$7F;#A=q?X$Ns!d6BGi{3AgOfG`1J!T(8y~$E0l;MQgan&_kCKV>lbV?sw8(Sgm)drml5%J>RR1u>Vn085*vVq-6IJWD`QY6fk;2gwOiq+c zDDq~Qa@Tuv8pH7AnjxA*sJ>-BGszf_T`bXY0$>|?qr7A`8*%v~2m9ANq+ zY3-+mIL+Mu?f)tp&!AT-0m0@pXdgG4urb~|!AQGmiSue!N*+2Tf+!UF z$BIB5*pd+HxMX$@uS<_MY3=*>RzGVzL5w$X{Bz&OyH^yuDp2jM&0-pMwk&^LT5Y{R zxP!Mf-mP!%>p+sd_-)fp@?x**exWziqeB}+5*PgOem}LHS#0vKh=`0MC@h=d!1ah+7w;B;#}JEHhtRn9l~$@RiY5M+<8yaDuNC5+H(Zvq*RNmKkcpvLlK!+xoOWzp=cekU;cqCMM5I{d#e#~rd$!z7S|VYp zuBsZ9eQ(aLM`w33gjhm2AMt2s0sCH>-Zh`IB57gE+{Tk9LA9v)h!Iot*bH&w9x0k= zlwH10s20!tE&ZA$vYysI_nP(>G8*;eQ@YQClv}?KuhkhC=U4-E8zy2RT zaMDH@86Ab5#CF@zloztKM$j6#={RU_nMk661^nB1UvV6yp|J(Toi;#AP zhJ4?V5FQoeW z2!8P+;q_}(&KfA`1D(5;5C}!TrXwIFrn@8dTr^CX7fb%s>KoklY!QS3p1-xrQ{r^6vhMwNV{h;Q)HfXL1PJz%NlGJKz z3}#8+J1DvJ@mVb$s-k2GPES>7|@f8Ff=D@K<4>`*aT`%b_nbvVjvf|w{?wG!q%1@OF~a|-UC4mJWQ`&W+MCD z8$VQhd`{p{v0+}|JwbZ|lURe0+9|h~u7uo5#`5z1qq6$T8vmI|fNn5n+^+mcC~Ts1+)vn`?gkJ{P1QP|u}^jmwPip$r3= zg()df@DN>tTScNeQCfC(h#&t2lxMX*Ay`P=xR3$7wIF5ez1v}2YE67&svSz*q)~nS zp~q%PN5iGN;Qc}fq^zuI1z%sRQi!+UsBwt?=@ngszRubO;N4B3k@YiaT3piRl87Bh zbk8zO_#N9ll5j;Y(p!M*i-XGk2in?Kf;szM)_qCTOv}tf6Q&YyL{Rfof;{PJ7_ZHs z<*;%QY5)lGl~Ql(=-lU;c#A6HdqmV2DT#5Kk1~EW2MjO^tyFt|{!~Nf9+JLa^u?sk zAs~$EQbo;++LYD8=7O_JYyljX-=OR0>iX>sTa@^POq7Vtx)VhP<1z5}c&xA>PRNzLgA#TKrufHe<;f2f zpd;b+(=Orl*!|vRBSVyxfiNq~FmNF?xdXKuCP%@6v0PvM>msSHPfFw-l|APu%>M`@#~;j#V*Y5*d)#4o3$MN`uJKw@6nut0R*Eb5gKvoz=z1TVeNo*ynw^~u3PMu?(@T9Vgbbz=iEg3MYLVJA zR5U`6+osEkGK%RNcXf7VncIz3XcFRwcprX3ZQyD9viMANziqkI#7G!SCd64-!Y+^Z z^l)4C(bGf8W+k-a3_L_ZnMPwX4j$4z?CH_y=>3?eed<~R9k;ENeJdd<4L zbesD6Qo!=c;EdLa>i%48h4(%uO8{j>c=`-yonjsB#HgTZ+1R zQ_e`-hdK$CrnpycA8@VyeJ!7-d;_|=x}YT03y0}qb7=<-)-66hDq%O3EW#+)!GQr< zwH~7wY;W{2;m4hwodSlsNdm5*{#smJb?Uo2oh8JMlbIRzwoo*Dre7~J&}9HGT`$5;OPF=y?JyzP1YlwY#z zm`zLT9U8C)yknsiu$HF1`08t8Jx6F%vBc#|mwJ@t6ucORhc?MR>8|*Qy3Ze5TRB~0OAz+z$bJL;=`C0FO(3X= zO#;CW>gUg&kro=f)!aAFq(`^wGPlA<5CIT`?(q^zgx38?OmS!V>IG1A7MN?2u=IcZr zsMpurzcap0cw%>V$C(>nrUL@Qb z4XwAHqBdB+**O#;^Vk9nf=!-!a+(CR9@o=L?6ur+l@@_L=qs;)PC!+a&^>Z`dU}o5 z%wrQ1;@}^e2^9=1v=S(F4sx{^iYV|q+J@=$_ArWR!u;Y0JGI&;ebWSE6s>TT*GbMwB($UtD?LoGchJ*Vjn)sU|c}D%5xd{?%qCv!arP}AVwNraJ1RjaE$DRaKLOW zSeND|2M2hDvO~H8s*0ej7ylcNd21RX9dq=YS_w-+SP4STH0~2GAf039!0{JC#aG`@ z&Yv+T$KLcg*Z_-&Y36M8H*%*6lUu{1*w|VO7Sqj}_?$+alZ-%zS2t&DJi_TZF?zs%;0XE;SMn^_wqIDHb zH`{!E28kmbiF?sg_@K8hskPq0jBGKsMr6^VGG=YF*I!R)Ag%=51wI|P^Fw=%k_AF* zZ0Ou~&mKFNnN34oGq`5Y#tGHtB8o0&V4^py#2A?z6?J4=ysvn5C&kU({rm0KNm_I= zaX*oMKZq|BpdgB(7xf)pVCU9jB8kiMA5k>9*rgZT6^9reeW zY(@#xx4nNKMjkfkbzV~WG#!wM7+{3p@rF*%MP@8jN+8hNus9Yd_D#Xul%9 ztpwLcqW~QbxF;2sHXk&<1VzrfnwpYW5)LZ8QAC2e+T1J>=H@e^*kn4hUqd@`A{#$0 zrd^F%3of|(Nlp_be^1H|gQDbws?b4G2hp-REAR+CfUK!{t~Q>iVHtU4;oY#GS>9+n z%tVCiMG*y1WqdbudBwbx&YZe%bmX-Zu5sB=o*3QnTrJJ`ZQsMhb+jgX|DhqKjj#r& zl*65`q|&=e{k5Y5XMPm*7Ja6%U@92m_tZaWK0@DC@J{0@0P>S9M zgl^0SBIS4Q-jTDHla@J5s9~jf4D=H>WV)n6{Kala5QSxeS9N^HuUeoqW{VoPO7Om<`vR}DbubC~%Uf22eJ@wQ^}~dl%j5XySRTx?428yBD6$mWZ%jo z>1X-3PcQa~dNtlLMCmUle1wjF^-$kwclq{Vpf}4_TUNq5dI3h=i;NMdpj+0u%Fenp zh_A|~D_&-DEQY|Mh?0{v4zB8ZhSru|t%!_ z1}OMqirI;&_E3Lt@ArHpYA~*H7Xl~EK+i)5tt#tG1XS0x4918Zwd|Np0S9T7Q3%&`gezdt2yE%0 zC6e*b(yOJ0Wuc^Z%+EjEbq~V-O#9IB6AD=nU^qKl+m*GocT@MkhsVh{=iu}|T1(_NBI9&!ONhc);E}R}N4r0AP^y!j*cvZV?JNmFcl19?J^ckvcNdQamLw!`S zh^&iYIO91%A!cn*Ev@LoOe=a%b|v?qKhQ7lYVnmP%=9$Z7`LyJGSFgQe&Y`g3<*R5 zYada)i^JW}@u?4wnM(X_-5G4sIF{J7v~bczF~Cr^5=y$28dv6J@QY>4_ERQ;(9up2 zWzyzlSeiK|E50`kt>_Ldx&+E1)o$_Iw|k?2~e!JoK&dj?&IwSaieo5F&#n9(M(E3Rdx5LHlaF;L@_$r z%~_irK2*RZ9|l+zwGdeAR!fi3Ja4^_3Rf4^xXCd2x#Z_thl>)NB_ij~E=`^HU=4o<%AS<#SS{}f<~j5bAgEWl71#>K_u^l=@3pG`&n#y@EXd02>7 zLp2fp(tpLKmjO|qypg^tdmtX-^&Vq0r#$K0TOb4TeBbd;kPBXU-89~1?c8X!&y~a- z(nJ4&8oxnB%F!$O42Q*E=VWB8g$ql6LyE5U_Ou(ZzAb(}>NJ*(S#5~4VQLeT9HPN2 zF3jI?d8CbM9HN=*+o$@fV;f4nv*2U9L8n53b<m}uzuJO-(n*8z2a2tl zw33{;73~96=9PR%Dn%xHKq^(EgMIqw!o9YSYvj7JDd|1oZ7M#g}9Z<$zXN=l>|5eB7@OX!W`qoWsG#$w>fjC5cU z)d&DGF{h)*v>9IyN(sQM;O#7Un>05y(cCz|e)?7eih?iwcmRbQ$U646m6a7>oWL>~ z{}59xHXh2>wsIh9ycJLv{Qgs%xf!~nUmi7VO>HSwD1PB5yT69CjVB90HDhaKRXk3b zs`fPqSiu=&jTwtL7gx;QQ%nuipZCC(60Jj8F*ZOxtuBEUk|!O{}nw{A?rg08IvYw|=_JctP4=ic1}z$~*s0k(7zPR{6~fsql` z2~uy`?EyXGr`vt>$kqX~+Ev-v7ni`eBOZ0E)|-@J?%8#g2Jk)5_*hbsup{#U0mmpK z%;^?CPaz7>Wkw15XFi119wRW%2#S^e7jf?a4d)lGj|xEu5;BO0HtOg>bRrlA(MA^` z5{ce>CkRH38YTLuL3E-;l<1-*x@_)9bg?*{C99OXe=|>1D z%Q^fj?8xs}^($z4u-N&;;v@4869Rz*3Bgk&|9ul>2Z9tp$)@Gr#B=@7K9KpZ>)e%T zrF;6))obzz9F84pYHh8otSk$Z24cJ(zJvH+27{;taP*yPD=Sd!Fa5;uU)ngk>(U@q z7GSn{toN^^fgrjP+S=F7u3rtY6FSr;*4+F&MOYianSlm`dg8=8*t9L!bhZPHyV@8( z3~C{p;qY*x>c(<<7>14r0~k2VUDEKdu=VxzhdS9_AoK(kPp2Y`%Ryyf zfXtpZeX&XZVF34n_yh!;ot%VgwYxA zLaqNf-?+Z-A7o(Y+gsqMZvx){Wd#Vpwt7JwVIm4AbMyfJ6DIe83JoOyoj5=hw7cD# z!LEP-ilYek>$h;n*1mSvJvV~z{P+#o60VWH*YPXmiq7%0Je=X_+FzIVJ6UG79tDD^ z2cKkX@cKgmQz?gnNIm3)Sl!_Kq)}G`MnQ@ z6ebo{vlKeC*W=ro8-Z+mm!HNo@wovx2mZukX3 zek-Z|LI;GGJTA1a6H4%YzpJ}xvS|mvso20Z`h!__%*Qu5#c(isE-oL}M0=~A23+PmH1{VW^ zKl<-f^@ZE!?%C=%`Q5jM|NU1~(%JgbbJl=>#&Z(-)^La=&+J*!Gyc`VvcaFJwu3Wj zT;1}QgGos@8JlH|J{#~-Qc_arkrLgk+7CLV5j9wBsw~0$RD#DVSbSRcNW(ELEpO&v z5R|hNC@iYaDQ|VjSWYlH3VL#A{u)3Xeg$6*JM1SzuisXPJax~v{D@&;@gh_jgL5q$ z^ZVE|ec2-lHVn$7yME(J0@HVFWUsMAZapv1zBV%APjSozUV3VC@9Hl%<$wE|l*%IE zd20WSxz@J$r{=Xt9cDJQzBgam7Vvg)v2!g~^90=MjqDENr6o#BYgO>3Q-%5YsopV{D4b;*tEf!T4F_K)YUq`L@#(}}47-Z-wKrSs#0#TC3l@Kx4LIBY(TYCCV%s2rVTE%y~wiPny7EsYggrB?YXemaiD zR4*vPPT{&7J%8%%#Wy$o@q(&*T+LWx@*#A4lj}VG!0p$jT-OilUQz$ODSG3Q)4kE% z7H8a^FX`SFT?WTzEjT&HNth!~Z|Yh724!%pFVDChz3r+aKJMMjm|}cd<{Ez!-)6o| zYK>%9eGU)L7kh`7Rkh^vqO_pK%VUL?!h6gnln25ZNCo_@ z=*y!%JnPoy-%`(KVh7__JiMa(zMZLD%fEEvc&>kOXm`d%0mI1lauJ|7Llj{Ihx~|C z4!AgrNBB1ip)-#0l5g@K*9)En{G`W_ARIwO;K_BNH6-Rg%IJB zr}R&BzrD3+2@eBB=pUGNI^4&8NH2C!bqD7A{l&M7mjhBj7((x-=kY&@pO@Xb6Q2w#{Kg*oTCHjbQ z`@7zK-h(U#*a&=|{H$)V8e{IwVnl}ogU^%h-W$~XMr+L*u~m)q)?%72>lUMgJBNeu zX0{U?wRSsw@lu|V(XLwzFbtx4u1(ApX$zl-(QlXjI4=ie&(Rf!x#U4+oWtex*KD_r z<%pKfLrh z#}OyfoRDNONUU4yQp+vG0ybdFeWV!svn*M+M9P%4VP854$G3@vw^VoJ89r%$awGeKPdlQ0 z4YSu`8dsv5OAAA{aQ8C|a(|x*gyrIo&FtQ3ZNvZ4M%Mkm_jjQI(_?E{XGq2NG_NYD zv#o7G$8Y?rtFfos4=QZ!_-AkBi6DH(bs|9WN{M2EthbiGwwnqY%qwUK%nDlu@Wr=r zv=|Qqn*t_PF>JOxs5F2^JDE1lqT}E>8iCPu?KATiaMH!M6_4I{9~yiAuV$^k^BCs4 zODwFK*slKt#YgDBC_evpgQi4&Ta^4+kfI{DQ<+=I#ZS#E5B4>=5<;=EJ`@ycF*vX(Q$HkeRXJKee z#;In1+vjtG+n35A?!IxxiGZ{ufuQnUA3Z_E3RFB0nPSP073Gx0uGOLLf3 zIG?xCe3!$Dn*6Q`&nHhAh(Vw^>BR{1Ygk&F?(4l;YM0215?COR+*5~!%Nv!yzK!?3 zeTf!&=a8x_Y=a?MMy(8;kCWgx#h>{7`sZo!Xhv=f1su*uM+HXVQt}l;307_X#ARAL zDMH#QZ7IBK2~7kxw6u=Oh4iD8GCY3IGUpN4PQUg1apNdjLXKZou39e4B>2c1etX=F zwVWz&2+vx}_4O%uGt#uKJ)j{afGxZi{szBGypMzCIb|R5Tcx&}qy%?v{Qf|x!r%Pj zi-Zr+Crb>1J>eC#I#Kz%tnFAgunt^*vA%2j66HJ4;j>mx9XMy$oU}b2Xvni*#g~RP zNV_U4BlnzKIUOhYSO0Sy56os+-4O^>)Oye3XkpeBZvtZ;idMM$`mJIiW0AN@6hW}2 zr0>zC?_SKECo?IrUpwN4sgE|Iay*zhO8o7E(t;2^v3S8EK{tCx+sx(F2W=wNGe)ok zYa%9^JT>VLuP==cGJ1TP^ZbM%pC{qCD5`@wNngc?<)?aVs^R;X;kq*aLmxzY7ax5u zN0c=!l?OjIR0?lak{`xc1T9(q+&JGbbG!RgD1h^lcghQRYZ4LC(aIiEZbhk`%aK5{ZiHs`)Re5#51dQ7+&y^N3iNO zM!uILVkEckpWX4H&m7Y+Z%=)KZ~UrUeN@5?vuY3W3!8^<%HRlS)tAX572XbDxy8ab zaL8iqR$ltNDt9t_&&b+#U%<%yiN{#`O9DQ9Tt{VG^tJe)YqS0~x3&D%UN9#MCe15gB^(sp|eHgTn1IiVuhF?VQy!^s8iy3^cc1|g6 zk9+6K%i~o{@fF|2-C?8*WAgTGc^RosL48US^HbV8iM7x0di*N{>9lY6iE~L#;gwWL z3wSO$Z9(I6D0rT#u|GUGIUW#Fj0jy2GDu;@;O2IV|JAt|OMyAl^V!FTdfZ&09Pek; zvn|P23UB-yc*L(OFye67ViZ=CHa_WTcmDEoC$Y(`xE7VARS=fF+%e$b%>UN*i{f%G z>*Cu{ij=Pc4|qP`EdKob^C0|NM7>z`@Q^>t7u_epaI-@TF)G7dFJ$B_fchRwCR6$!@Ljv%MJg(D)22J@bEvGmV*Bu z7Wn2D6yg^8x1(SCfU5^Fp88g^!{iLpdQ#JsAl=81vbank6)M9Rm-eD*>bBo=c!@9R zO;!{w3k&PWZICz_1APvCaxr0; z;CUb0zefxfKghMKH|F~MJ;ppnUHyxpF7fx+Neln;KkGloSCP;^|C55FL{Bh**o%xf z{=)Ghy%~L7kNV!pHvd){{9q-q#NqF=%0a?SFAok!i;kl4Fup$oXYfh9QzIbr3?-=6 zrd(iOwaMoi?{-(DGdum8H#<*)?CN5@;x_Xgj5PDBZ+|F@=*I~0ZP51;-=O>!dY7#( zmtoao@^b*VQip2@VvMwf8!%nbpKu7#%A=T@nFp=Z5lYL`-=fm+a+Y_+PqXHOM+nT+ zbUi-29Ex`cKVEn$dy7grTrNH~^@&Jfx>&L8X`96Usxu#dzI;UA?_H&Vvzk|{Wi|1C+BWPz zZdEd;70oEXNg5ZhAxg`@z)Po$c&9>N3Mr1A5qzaa`&yd;OvIU4g~k4)gDH z9oC%XF8I%f zd}RLgL*{RpRnTwxLK>V|L9bgmWIRZ_rR@e)t2ifX7&ydhpRA`qd)5y(h1&=1Uw1E# zi7$A{6|~3n&gspuuJ{(Tr&arFs^S1!GPw>cEmufGuHQJdT$K8QB%wcq9)J^5=4{v~-@L_;U97>kAE{}s|v6!rSPs<#&r3g%WC{#xe z+nDE#v~#AKS~d1}rRG#w6U^yGkeXW-GC%piEhe&@Wy4?MWQ#RWF!5J72|miX)A1WrZrTW+V)odrr(H-Dr+kKq?=Q?YlnCxl zfF2&$Rl4c}oIib_CIIUcPO(PnXmaZG5RR2wbunfMFzPH8SaMn2iDEMP=9Ry5II6!~ zdS>ed%v}QO&zK`_^sF?RDwU@Us&zXeGJ9;{^5)3lOrhDf(dmTj=ZV|#_9*q8;X{bV zc*Z4Pal;QuDOm_nmwvOHVTF9a337Cu8@-ZK)S)d0Qw$3IpRi0q8F9lSc5@ z+oWOsJ*r$82KZ6A_V@TdaWddEK0FBG?*ea}-(osKra4C%(ll8vWw5q|5 z_JRjlQUsO74_YGiOTK*ZfeeR5ecXMQE(>g|cRrp~{BGu(TZvEK#XtBYMPNNG74*C9 z%Rg_7i>dBTne0|%pa5P-a93|O!YDJov7^9sx2H1~%}g zp7^j%*_~Y-)Y%W~4n#X6WO`<7tifUFqXfYNcKF*p6gCb{R<34pN6nE^w6o&SKvB=4 zmQt+1&hsDc7DyUA=0|Al?M0N}2T$K`(f6Z^3dQN*1D_x@#d{NttC5EHU(mweQpG`~ z+*$$bhg4@|@kD@kpq2Bk=CRL|M5D;L?9doqh50LkDa&GEw;`-#^ z8{?PFce-m2vky2DtM2l?umE<=Nh5B;@Q}Wwq)W-llcNT`k*TKMd zWz7;J8RV7fVo~F>9*(BtW-y^WwaHAOy(9OXGyLkf0H^_>(d>@ zkA#}i!h^iPjOK73q1q!Sme?BTQVKB@5+aC?#&ap$%f=hPDjHj^dg{{EA0#~r=PbS1 zKoLBquzAxiQ9ObbcG zomsusX=fXKR#1%JFCte`<6TY~A)8QRzsikzyL^v>a-ljDaO~@CN^{Ftwe$+5z0Rux z^-Rcei*zALBL8k6t?HI-trmiYh$cUV?1+ zUYFvhrh^Z~azi`6`!)ktCP&g*R`=P-Qk>k+db&4;>Fuq19p`I=HwuCwVRuDQKU=8= zu!uD#=yKEodnxPBj_;Fa<)&l7B=^`Np=q-cD6?=u#+inR5QT+4CnkYk%1dR4_N=dc z?n&IZ2XSC3-rF#xdoO0GG^zmGv7Ki5F~9sd{8Wd>n!-K&#KkDSua{i!_o`J47_%A* zVlKL$g=eHKaKd~$^B!=y*eE+_AQ`AY`R+J&cqvgH>#EBInBvi?U9;DuQ1^uYH7Yc> zVwp(%r0Dv=z>K(W#>nTh8A7yob72&WF}_pa-6dI%<|14VrzVEM@DkPhI6oe(jXIK{ zeM?4e?7L*Wb>O+;r`nhkG~uW4rgDRH(VHlc3Z38WFRDvryFK@?A@XO#2Uu!f0>ACs zNGRbfIr*<6g+S?_;>`IDT3;i6h~fiXvkU+Ib9$G8RH_T@iYko$v~KOqrX@J zss+A9eKSKq9bxR0o zNqxIYd`7fsPPNhMTc4ZQT8~b%7*h`@UI>K-_Q*3?g=cIl;L0Jh49^%uMZuVUDsC>V z^Z9u|wPjLgj?(*!SX0?VCagLzhnSLRB{*(Wa&hai{a>$%V-9Dg*GMUc=!0tp%Pjjt z?WJ;U7wqW)_PSQbk|b@EnOt;r@@C)Yr`Kf;@W)Rjs~Hny(DdmCJTMZ2d)&E~-+6|= z&HH<*x;gPOo6p3tkrk56&-m>!z!SeU3eY~8ke3E+hQn3r`7-Tjf}%FAdcD(d;K{** zi(gOAeIauHGNl!qW97N2_sxc~$2`$FtDir3)xy{3GcJ?uYSgu}8-4}6DPSN74ph@& z=8JR`O{JozTaosBo0z0`xBiim2XoXfi>dj{!0=SAY7)v6+E{4MY@=PiC?jVXNTO8; zmwN;uQDkBgDTbFF!&=Fe^#?n7Z^5E)kp4?rVAlEO3t49BRZ=*b(K>a?CamXlgblpf zQ$>M#Za|^}WJgy_@%~p&4k#WKS;)KCs7n=hsh~|jNH#l`k%y;LbLIgc(&%=!wxWv` zq8jZNmGm7eI1V?bX&{i8$VipulrR-kkzNgSJPib*K}8KfX9{z*`RN8q^&pqK_5lYi zOf(7*7=emGY)p*0@YBA(rZb=z^>QFWh9TcuKvl{n?-M&CXSB+h>QDbf1yzuP#46Sa zVpu~eLV_TDC<_fl?bHj3jz|88js9z7WsC@B}C@HwK8Y z#)QJ5_y}TdtE*`~w-8cOQj0*F4&i4H}D=YX$N4vnhJGgt z*|&Ru!b@U+Z3{xoJNu>J>vv1b&77T`5vpinr1UKV2*@7UvF|Mdf*R>0fN}{khCl)i zvvP`!5Cz2Y25U|nH8nLTk*cR8$>X>(G%}*CrKP2ypy2L)y0_=BY@2+NegXLNfQ$$P z!i6|*5fVDu*xbO!*KPC?b2KqFCck_4=vNa~&nhbymqE!3?7~)1akHaCP8dfQd*5I= zGaFYSGh-^3P^=vAia2w$c)}(I2YI2;H=WB|VZJhOoCZ+9xXBN95D9p?fNTaPQ*`_` zNQZINS@DlU8*1z5^jk?XDF-w)H4TN*tN|9NQQ6Qhkgf(&0@88ee(Ak{2iaZUU+}># z^MeWC4wp}X+#gAlk2oeQpU&8xtJS~t)a^t_xEmXv$LKq^TS!q2q(8m$DT%PrT!GT0 zx?A2GW$2X_eLV`D3yQq1XaKfxChJLo{`}@<>gCEj*p5^c2+)gkdb=hDNt#Im2n_}; zJgJl|es%L1U}Iet`Rz&f8*Hx(XGRGl`T;v*Dbh0k^Jd6vpxVji?l#JYAaIF$)~fEv zI{?a7=Ay{s8QIuE@)1761xG>3`I-6^Kr)4J^)r|RP z+Dr$UoKa*^UW3tTQ`+~$Q#G}<+sj_(Z}A;R?L8)>YItK`xH z3n!0+_CT^zD=9^`qaS&hy;*wl#GD>p8cHdk#0tMj&Y9&M+3ITz;`n>k50(3pg`zKF z54N{|>*PWD`+f6)r1{bFPS5bW55B59mO|<1e4WsM1v1E9k#oToo8jYwA-p0aBC^X) zMX|f;a2z@|XwUOvi^VpGt~QP2giXKAO&w_u%aE$6VHGuh^~!O+GZq-#7M&O9A;1bm z6E;-h3wzaXLot$Z%oR|OuQWn>( zD5H=mD@fBBNU?*87rU=C-QC^u^Ua<;e*Czu9kAsQk+~(GV&perV`av-G~U4y@|AA) zBaJlYT(a}k1okijApNX}Wgy6^gEATB({+QP#z5T~AW4n-spN_5+v9O^bVM76>e}&& zKg#~Slb`|{Q#4rzq~;kS8ZkG9toFXX2zI692LOIJ$3$jbGw36qetbS=Ui|R1+7-+% zfrR(dkMc<9b1IEdOFpH#Bs(;D5GqlZ$k+CEb}syag4)T(K@ky)tPNR=u~6|#soz+) zNu#E=kf5X_E1l$!G8)-(NEJK`B&UBUqCyXz5YvRW^2rfX9RfCLdzq0SQT~lugq$34 zScj#km>4Ly*X>yf3GOT2R+Lv~QR)XS;cdZ>qGw$pH(^lM1=gr*UiJMB8mluA@B2Tw zT6nxdK|%Aau;bM;M_bz@feau0cPdnFtp1STrx8aFJK{gLKdp}AsseozJgkbyK)AHe zw+tehf3U;L8*LP*9~o$50k--l$h#Viir>J+arrb5hvFVjLJ1JD@gfhNE!t!z8Y1fWDqT%~B7c2#TuTvj<0%srNzc&~B_UFM_%lcYqt#@vg1+z`q=oNU*eI0Jdu3&F zZ7vm+fvwLIjjAuIkIvW=8tOV_7_(V{a=d+X51^I#;7XV~I5{mWqPS>*(>KI`A&t9( zN{pn>PqkUaNz%fd&`W>|&jZUC1@x*AsawavA3l6oEe;%Kg@`&^X#4bZY9n!wTeMe7$i1Lj z>AMt^>DN34MW)p=4WG?1&uBu`hHDbRlEpj@dM~(otQD|@f1^QSpdwo|wA3~a0@e@x zeD!0laoIpRtc822qDQ}lOdW}Y?kRg700J7Qn@2*QZro5;m)vP88(CEK8fkQ+So`^N z3DnT>T6A1-(qC@zc&s8asAR_Ln9bikCD4VsP2y%Wp(uSW_wS8 zTEasGX4H0-AP*z4obM|y{9*Uuw72pPhxty@(JHC|;^a;_Jdg%RkAIv76yy;~j-V3s z=pmzKE<<1(C=8u^)R~!>HQ=a`yCDdzNVD4~HEjpvGeCCI7)SFhf<@AvbYA>mb5_$8 zFKa+Y9%%iG@m)yz>0%rI*+fLya&DFe2Y1jURj7C{q#Wx?-F;`P}@+iYIx;x)7P|^^E#;w#q~cdQIJ;skD^& z#5^aMCE@s%>RBv~4`?F-01jecJWu{})+^j?_WD%|Uj+c}GXFU#c=Sck7Xq&53hZyg z%R=D%LGHMf{@faP?kXS<^%t%Sz4JdiWquSi6Mgx!8G%|V2F7xAa;OCA?%#`?`>#iz zAYxkc{>0yY{D{j6x?mN%ef=@`jRBhqq(AMAH?-||2;!_8SI^znNTKuu>{lhEw0A5u zcnSOlMFAw{&qL9`e;$C?hUC?|{q6Xjvfj-;KPQiZo)jD*06#!?yd5;*})czlH2H3SM0Ck6G{;%B6Ian5x_ zW~pR%yD&a~$uvE9;!Z6*_HD`edEk@2Sl&jHpDXKCQ)!DHVXNH=aN-R97n{2GFf>&C z0+cFEw?(RxHGBN#qTH;=pCj(pK;Cg|HPpQ-Ze%G>c+nit0hfz@tpuf-8GSJOO`3sf zhToju-C^TkKgqgYZeytDEa{7%NX^+$Pn39^XVCg-WsO^Hq7%>l$lT-kIffz2)8etq z4Zmp}f^zMd2Ep|wiQ4oD+VRxSa#3WWiB#Xj)`E!^#`elgH7hga9CIHxHhJbP=2H_z zl;yzt^6y3d+NT+t7O-jNQH~FPCQ^A^*Kr;^8BV;->ooz zYC#REu8>7vY}xf}oklUQAyVQG_g;xLeLiOH$+Kq_bu5tHTPSNzq9zxE?|oTL=w|ED z@CXsO4IE(`JAAg0l0`&W0r!QaOnuz`_*i>1QaV<61qq?}9DXxiNIBbZi!U$0#eoke>dt-+ zn&oBjM_kiV608EMq{0fcTh-4#>U35l&%XG}TPNaLwH}&%>zD+1^zl$TIxovAUQ5|; zCo`1rQk*QY0Z(6tH~8yRIk}tW1)f@|s?Q>3b)v4PXqPw%k>Zz;&~5)3=Gb?6)yEj{ z3y+5}Uy=0*2_sYwP4v@gbFzNxL=HCd!^Dc#U4^gmO~J9T`T`Rrb8%u{R0n4&hxP*l z{^#2+uFnP+h1%aLn#Fk1DQt&L-M$(Jg%Gj&51kbsgv%%EdmP(hR7DkJhZm##xTvas zR`;%r7B+`?{^*X0N852GSO~=6w>GHVfys-P#u|SMU-_2v(4~^W+@L~C`4dG;8DEK8 zf}h}$mPcyQ#oZdz3G*c#idU)?Yjk-LY?q``$f(ySK8?<|)+!DjifMM=##$QAoV6bw zrDk1>!VaH(OSsZ8cz)_IELusq_`=m~;{7!WJdqEJBikP%0`G9NdwTU3X{_|GX9=7U zXdj#}xDF%LtNylU1~4ujDcVf!xdnEmS&NDJE&(c4UL-Lx=KWrC##kGxM8Rk%|| z-a~@(^&Wk57K(uSq4tloh*va$HG?hreo$xC!a)+*6wA@i=>6;Fss%cz+=M}74zS@J zb}@h6{My0R(%rb8hnt5SeZrh6g-0^hH-L)^)kJZU!n@096!q9e_cp(|21@Gdg&XiW zpV@ab#f$omS6K~zl}6+DF&ZmO=mA~%ayk0yEG8s`rY88u>Js+?@HUs#@ejQZLU^at zi)t1x-D6H?EPsy|*YKOHhNvy!9=hZ|YRjK8W_e}&(jW2;5y`a;6}9UG-boLdTv*N^ zzm&ZC!o7&<#i$=yP7Zu`#D6?8mqGeitS0u}Qe)_K_H*qojnoa=kqjv6*>#2OJy8`6 z?Z|Jt`RH7$aqPP!GMw=16d@;h?=S26s<{?$O5IURJ;^PEEu;cSOitVBB@k6sf2?Od z-e5gG68wW-%>rc&A)RDN{>7a#t|BByuH0vr(i&E1A#0B(*Pr*4QB;w-wE~#EeaBm$ z{gnuGzB8-ajRBn0^0T?2o=d&Cu}}6`@mmkYSr^|Kr}PPC3d^-tB%*^pMu;r7dLK4z znz&u;xav{bx%5#CYrt6rR6j_lfl+@QjCvV1cuUVsTaVpP!TRSJOz@w*_ZQmYJR2XV zQdOIdeVc__J|3ek61%@ZFLq=Wm$8`t@_Y|Qf9%64$&*%NeD9OVM24%HWO5ksJzDgD z0EOEpT&H5!qR*(q8Ot1R0V`#@cE%TTExJ{sFIS62eH%7~IB(A$gP($pX`Zo9ir>A@Ngh zkPyC&pz1B#!>IAsTish%TcNiG_9^FxjEVFP1%(kOC+Fq~SN5Kxo8$CjU()biEpLHE zZp1SNyPLM(IT|qojWpwr2S)Nyk*;u{T)TKX>1?-o#DkH zzZPfcNUi1#k{Xo7v*Bb>AS7NLXB=8Oi5H_se~Jd4^CUNDWyNVsXd!&ewj zxfzZ>LlOmDMhoj?_t~p>3+kbN%a~hnHyMid;rG$OSQ}JZ2IHD>gAUG~KU8>kq(@o! za}Esw(^6ypJ90fX6^~kTDzLQ6jD;E$lP@AXMUxS?MSnzO zQ`+J!=CSQsBv;90B})4!{lS)l(UNe{0^XpJcUNL z^cFw8(${6*LJq|Qi~&Ac1~As9x%mo)!>|oe1>{`Ga%<@jo&eb)b2Nlh&vW}iZP}I1 zvD$VL&_s^$$;FTj4@53IL!ze~Bcblh?nMuyig5!W9o#rDv&npHSY(V8o&N6Y`s(PM zGY|HMOwZokd$R!Ml7?1acY|Lm6;@@f*n;9msHZxZ{IwIGU1C0l8tb4g#uj*^<2$8a zLwg~uR6B*0#&Kl#+U954y(hYKD;(}Uq>YNO(D24_Y0{PJ9|~@6Rq?KN6J>Ar!z7mH=D+2Pg=`%K;=9_v3U^c0A0$CFat ztF3YzCqKL$#jnl^9|%zyC=Qvp_nDE2{%ua4_loOOY0b>R>xTYBF41?U_j|(GKC24a z|9)a7u_g@3y|8gD<+=PghDQQU4c?sOp~nTBo` zl0oV1R{TNGSWvxr!Mc!JW0f1J4%xIXy`MR-2`~?^J`>R)6j++hDR8rtj7eQ6k>e`r zw`bYl!XNwmMjGMq_?j5yj5wPcb;{439i56kYHo&5JrsgB|3t@myzk5&EC`5NuLfu&MSVAz&%ny(x2ehcS!ILomCUO(XcJ z6U`K2Vd|~+0r$K69^Q4SiXmG~A3ipF9y?g|r6xe9?+Pq5HpR6qXZ)8!e z7KxF5IoDQWL%N|wV6#Vy&;ueog3Gb?gZHbkwO#F`(G_h%pTI;hW zXmcBZB21`>5E?}keCn_B(uCNSEP!{WmiUaql@1u@6V}jYUIhKDutlQjuYC z$M(ZMj0?6d#e%o>N{_?#a3q+tT4*kLj6BDUce*niMwB+q7PirHL9?8B3~;9(+xa6KCxMOjVDBshv*NJ zRDJI%PCvJmaxQx~uiu;3!5W1houQVQWpMQ5dubraEt0$(WVQUnt8!jZ3BJtzfl~Jj zKlHR;q2mkQi0_Cciraoh8s=bIDSRl0Dc79cRN(y}o9-Gc%5V;;F3{4N7MJQ(_v$=@ zDk%;Xkvh$#Fwm0{c1_e>20II((?VJR2Bg8l^32LQtGGj-0ol;|poq}04<_pstqLAo z=shHDF63%EuR8hOLO}hTuUONoqyD!WKhD+~#jQwaE5-KYjr~c*cLo^|i10|yMgjE8 zpWzgX1Ek(@f3YOh$)m&Bx?OBstn+xqPZWGFJ9B$h$$~c9`bYmPT?si9g@*SJ!-}YRIuw=CdruRiWS~jz<0R|#~jxaDzZe-o? zIRW6<6=t(U513&qxBj9!X%T|_RGN3$Jt*LW5BaEgBm*es zin$3elf5mOonr5O&E1Ggpcozt_Cu^Wx@MV2g4g~M-N5YxX=jtr;lsZFx;AP0!sc>= zuP30(21baHw)^)EGZwj&$r9Abl9@mgVE=7`AY$eaqb5_pvC{|CzpFccFRx7Yn-9?E z3;_Cdh4^6ZN5cgb0boP^4`3tVRC*&P7z4x2ABimI<-@>0`#>JD4E*yhdPq!0$BhhW zS~y{tppO5bGzP{wKs1H@X~QTHsy<-|(q~%A#V#pDe4~yHu=rYzP}e)pZ+5#X!-{ z_o(mzSay$7h7o>@@Dgv#>rHv>xW8v8xhBEKP~1%tu;nM zTU;<yU=0C=T=e?J=IoRC!y-n_ z{m$iqJgNf&4jg1w4lxpv9(NT|(63BKFM&jEy!{=n=9_IqB% z>&@AlYJTnC9m5)5Q6S#&?Z3-NBA;QMJPDMn%RTVYA<>C+JFpG(XnrFaBtGq>XnBRW^^sh9L^fMqY^oNmEeAlB z)mP}@gE^r_(dof&%Ei(}w92q6Or~PY4zY{K$o4?j*hl%fR9YAJf^8|IKI>j-x}+Q4 zwuNL2&40Ec!+08~sEyV=>8b!>K0tG+DlgA6J$b6G&QSkd#!E5`3lr0b{1+mUIv=?_ zUjw5514t0A0o0e$d{B!+BlnrOf}EV{rcqZMhdSDo^er*488nl1epyafI)CBQirb9d zwMjK58%MzFcL%ncM#}`gI*YrPXyRr0KzzjfY^vl8I#=$faA$S!rCBfxXNv zFTHt44K~7VWvSfkBBtbrPOR>aIFv$Es`%OytI4tuiNXr~vq>;^p?Wt5NI=v0vzkwW zyhC6GqWQ|OU7T8}+|$%u?)3L=7(o6$UGf7@;g*=c0n@Frq7t}q^X4oG;EJTQcXYV! z%mr_uzkK-ugt+43<9F9S>XhN*;{&EjTN%(%{i==dLaM0&C5d)2=}zhM=qj6apwZRY z*~z4oY?3d0s=l2M5%WB8*!tDPXK7`n)F*0b^&kbrT}<@@46h@NEb@{d+cos2xd$|d zr6~$oETSXP1xrhE^B3a1rSPMrW6$Lh`QkFs>J*1fU1(}zTCZFjumUwg5Oo0&2yxW`goqyI9RinkzmQi4xjo$9-92Qxvch|KjSGy-4D8_ru3i1 zvFg|7usnG1&e-qG8&CwMHmaR@Q{i6&$gX7OTa-ig zV6)jXPL5a^|mCMJxw`x?0~2ypGJkAwD zo7C|~$H)KK7k-dJUEv<-eN}o}seXfT4;jWg7C2(wF!+75-8H3qqyxTD3c?;>w-C3z zw%~ZvA2OeVvzxA&OzdVOCO0+=wlYTY!&U~sk2uCdCkpnN?& zJwO^4H@~u_;M(_qW8{)IGxLHIi1Ow+k>cFG8dF5ba)&1A=y>OGp2?mkd2_Y>=P8Y> z@ECy5<$hu?r|C;{wCZ!FctGs_6CXC?x7kR&fDgjLAd(Qh{#IUuU}P=B^;8l#+cL<9 zu3L-Dq7I8@sxEmfFF*{UWwZ{9As2x7l4>1Z$DH)yUCdXpr~D5Eo!0IO z+^&)WWVOWj`B6~Fb+9~=tYO0m@PHT<;s6y*fQJWJR-!^_M<*-$4S!fm8_-wmXLoi$ zk4-jur{*v#C11(rB|Zbnz41WH;xP*Fs^f(b?~X=PfryiR2W}MrH$5(psHLNJ=9p@o z*287d_4SQCJ;&4ofX7!Jb2b39-48;AzkOwQpHCM3k^$bXQ=fmHLHVaopl-dR#wLDI zH^sClh`o*b$F1uhKeb7ZE(~6gF)v~?A+1}_(wYFPhI@#2DKgFR0&Q(=QRLgBl%*k{ zHj-^kY%Hnw(1(Y?7j7;t-`Bbmcw1em`#XT*1|Q|kdS08tfB@{9B#LTUq-Lq&cLb~z z2z#2E(j|A6c|laH{W%gBc5-?vVW!fUj8t4@sy6OO&o1*}@NH%1mG5!tvdLQ%AcFCz zQU-`QSE)TjysK^u3U3ABlom1#k_SihB z525Lr$Yer~0>Qz5F22vBrL~nqr_?`Za9|*J8wky~^3&3$N$ya1Tp+|LhBDwGEi+jH zfO$qr@)9Uv-zZi$V~?PDbZ~G$M&7Ia8G+7aWn)u#uMw~!8r9g?=->}jX%^j+`HH`C zU#&of<`iSYO_++kPS!f}OpsKzPZ&Cc63D)4ch8 zk8-#gmWs<6R%*uPc^lN32MT0m&5Hk8-J*c*A_Hzo6aGHo)WtOA$s!KDk5^aGvAfiSa<>mb%0%o zjg38g!I$IIPVIlobSaeN?9=G@7~k^SQ}T#`AHabXxYiCunpG8tZ#Fsv zb*Q6lHF05*k-#10R_c@*;q4!^QCgdztJ)%_&S}}H_`G*lt4!%N9hZhske&oyS+~zu zwYI>N3Ov@2AA?gSU=6iAbWqrFA!0ceaykl>KFxR7=jY~*@}p8xl*&|9Rhf#j<)WED z8KQkq2s?khb9_9BJbgJPhTOo%_1Bkc`*B0e8vXL-JYVxcMD_2L*g83pZTgtHQWZBEM&~zOIg)YD`{rK^t>0q`ekkGcS+m->6IbboC|Lt3(yKbd;1k~UW8nJs&=yZvu>XdebCrRGnW6NLaNCgYR z^-%Sx22b~uIiN*Vo&e*+vvL#S9;F1H zfb5PYlg~gO@7N3pu1C5Us_PEw*nv4D3ownsl?&3Qa<_pjuF2zj@7J2ZMj`7lAg){T zA&9AdKG9bk%vM|udp8w$Oc*zXTIK+_aHE257HBuH7#Z?UN=hoX0rW#%;6u6uhWo}V zvkr$x2R|-#^xwoe+_cId6L+84SoVr`ABPYg3OQ z&WP4R_TxuL6yXNyqU`n^qkCX;GD9i5d`UhgA&FXgC2{HVQM=^DT>CpcNs)*9Qd~HR z)O;qlofTGRCWRVTx{Ud_?Wp&0TmLk|rJUT&fUMyEgSNL0tFmj}KoLPeq_=c#kdjck zdrOIIQVGGJLlC4>QZ^x>G!lX!f^-X5G)M`GG$T3cwgSwJjlV&8?Ovld%m>m(okVL)E)^9QzjW?>MkD`Dy=p6y zr+K_zcgs9C7D-G@EH5t)DrLF8Cpf83!XxYRU@#F8*Bv|G4W|+n%75X~rAxxX2{BV0ig5f;-W!m%Ha`8%S@4#ug%rbI+U+^m0) zJpx6YV5JR~$LfT|#Hyc-CgnYn()X^g;cq@3GT}5>Yu@U`7biY@Hb*uze@~uNW3>8Y zkD2IOgw0@V!Iu67S=q+09faRzkGOA634nMEN%=HcT!X^VsGsayfb z=V@_zKR=4878=p<2a=pt@}-Bzktr~HaD7^^=}x4BSo1Yq5z9^l)ll^0n@1ASSAj9n zX3b*u)~_u?P@9%#5I__fgB;(k)B2DN}``-O2c5=TTL z()-ny&g8tyQ9hVm=a;{IFbf4W*Uw8C{^}PtcT=WlAdl#Q`E3r4j+VMAT^N5gm$^?F zU1vGEF`Xkp1(_T1d?wYv`~U%3685L~P@At-?5v=U zwsUcH-D?Vkn=s4-x^N4az43%D!y=7$+em*(Z-Z(Vpwn08*FH!>zu-FtLc+pEn2*gV z#!+$_aa)7x10OC7Du^3L_Eq0?nl^df7R_+|DFUUcDh)$fBqjT)qoSiPNYCK^3~X=K zwXn8sK7<#*KeG>*< z=)V3|X`4_=bF9Rcf`C%c(D3G6ud&xR8|Ip2L{+3Eh|V9WDqk!##DwfY6Fi;s(W+{3 zVN98MYG0GQS^pJ5C5%P(-SpZyl-cKOWrB?+RkBQ{C^|L9;OJ#u<~y=-sx*0Ne9OEkSGrgyu~>`Iz?dd<(T zWwVL=CL+Gt6i4>TN453!zZ~GI4E3;0%z@heiRW7S^N}jy94LBsUu*@Gju_a)Fj98| zb&8=+P+tF}&xfgw(M6Bk5xzfHh`i;(@w5x?hZXXxbP;;KIw&)4*IUeuVQqkz4 z>3DDl$|lxH))y(}ed|5eH*z}$z4qo~G3v@A_ikhoT6GPUVkWn7pcP7t~V-)6U+-z`nP}wWg z?t?4c_54cn;mGBto3ZYYXrEyyhK_{0EpNlFWM9WUvHqAT_sXtqo~*CTeZ`SP5hGPZY~j|ZIwnX zNr?LkUo}*fU1rnM)5GXqm}%%JpKQ|+aqSVU#Fszj?=#FQOg(Whva{oLG#U{bVl>7# zfAgxcnp9fuua|+m8#$q_Q#D0!JFj1wr;r-3u77TDq3XXV7l2!T{1N0`;y)-C7XA8T z;Qs3m)HWEEDw*=ce_oWt;}I(VzYnTytSeIW8$NJ_Fer*Efem#Al83id?yt$zemQRvx!eP&@U+mz))uf?rfQ<4Txm_J4S50IuS6N_{V%~WS$$%GnECCvJZ zLVYY*6uy{1H`kLD^$h?(-CSaT!QB7;eC8p-7MOQxaRLAieEs`#4E$}e0yskT5bD7P zu|MZwR*S2~g^ISHO8>uqq1t^OC5bRz6rpwbLyfqgGU$aUF3xiKPDuHODFw_Eh`~)@ zK^IYvM8dfK{U;WQf0}2&kQH@SgW|GJ>iNb5oQLliMbt5mrUI)ha20M{8m+m%J=y=^ zz;|@zRdvAL*3Me<=3wTrJ|~+~<6alp$+~~qa3*)tC@&Vy%(Z3Y(NI&nRE&h4o;H03 z;>N5f4Rs1-Csycln@iiONHt0`T0_c6YO+>#r`6;6K|ufOp{0^$&Wfy^iKCV4F9PT! zbH5yp%pUrs&ywEL_qFG(#l}-2F^D)Wz$GUpCP?ZRuMlHT!+t8u_Uo+!jR@I=r7F8b zfLW8wEB$Aanq!Unq_zAvEf3_$SPoalQYIbEYNph7&$NhVt~y|Y{%Cd-*qlh63(&{$ zrcF<(sHv{0=~&jQdzG+FPjl?^$*y-95E+OTsLP9*N6v8jmwwo8*>=oY@ez=hv1lul z9BgwJ7e4x3G7)do*RXc@!nLhAp1rP4g7ItP3khMwXbmqbtofHL3hq@tlm)3sT0(-b z0ra_i?R*-jOQ{z;G_GFFC313F17G#q$OwS04&(K15Q0-3fp_$NU|`@6L+NEf!J{sx z$)-lTAzoe)Vc|4UdqZdEbtvejpU+l=wE`A@d-Ji6QAe4{bwO9!zAsVfs{W|ujL8RU zc2a=9^h^+6`*`mF(nl=>MS=j*>Oe8VL}OQl&-QZMFyMb-p`kC$?JH_(@~r?i)eCK1`DhRg$Ks)T_-RyHqT z2oK>N5x7M}!on4RwZ*Z<@$?tL)B-f0-8vM+CnwW#apgm!c+pSF!3Zp~{x`>_=T!Yr z%!f062h%JQNdZ@ui^Zip9K<`^+xJCXDRYkQ-IOWZwIMkT$Bfa`(a<=XG*<6s4_=~3 zne8AauQd;J8vSl1f4F{HTpAj_0#)k)KGv21VMv^v4Fim1m&lm%b(MQEtoFSh~ial6(h* z5(c;qS1Q1bocRXBp#h68NA!4$eqQc;dhy=9dt+?Y{W++AH&&qvk-IWd>3*CXN1b`N zp$v~YoR?o(Z!vc$=5I9Y!N@L{xN~0F=%9*Acl=fvF$(j)GYi4{*jX9%07n~W01fr^z;=x96f~_>aQ_$yy;o}}nCU@_@Nm%i z0Ju)JjkFe?WXjoF=j!_b9)7&ezYU%YD*4cD1SJu3>3a0)JW7z$p8R-@>6 zT+0m&)k}c{6T7}%1c*zfXb#?)GuTG@PXI9*9U1AbEH38Hw6|Np-ibIkGY_tP1^8#V zmGn^a74;(Zme*+FF#Ex4vpMoAr*WwIY*8h~s!cg!{Eb&a;{`c6Gh+ccKKFxdM}gZs z<)x+2#y$QLl8pfZy+3ROaRoX5!{pAFCjI9v0k;J(;otp?O4!7omOe7&93jPmE}!`e zw3f7?HnhU!=Vz#)pt;7Z>szUC29AwsZ>0j}DI|OV;Ojb;GBn+{1;DU|rlw^iOoQ42 z)DMV?ck7!6MyfIJwyyn_mijd+r%jH#rAZ^$AwmG`lj z6xcD+D{eYoEOC0ncEVFO#}aVKLSg5^Cg<|_Ps8-8HFGl8^DLJNg#XA-4vYWpG_+v` zpCo>q)DK1|z>Tx?rR1V$DIf%RJh%!0bzYYRWWEmCw%&mBe8BJRG$CTU{qP-v?SodY znDJ*!7l{Xz{oiC3?*=}s4n(LwuSL*Z{j)Dyk3j!X%QvM1s>e{3)Xj#SHY2Ch3~`vMvA{W;g?$^1`VcL(_& zy{c(iIez*)_;-w!Ccx~OV;Tm&ZxtwDBY{+L9_#<%SbBL&uRvkrkH{D2N-rH0{9J?W zpG#|A9{wF^q^Ih%c{AOgZ)!T43PVP_fcu9+@y8dyQ()h$X`qX^eEcf$8k0=c0ZZDE zqLpGv^RaQ#wUrmzbU)W-)4d-Y9F?oUYd*kg(7&&lmA$v&@2)W>w)0~!oMeA{kea1~ zZ^2srz_v|r^lFBL?_fPX+~;d%>S$Q)&L3_z)FEc->gk{&VR>v9XxA zPUO$|fM<8aKYwvGo4I`=oiq!_+$a6t?^qn0QQI{--r;RADHd?7G9L7;YR}=&BiL7F zjxq9^@1|1v%yayGEM4OtU*!~9{Fko#f%m5>I^nFFqluuaNrwCdb{T22{mKj_nbJAC zHUJv&!~>5*_vDXue-26b$(HtopY~*2VX*h)a5MAds+D5Ee$fbAY0oZNGZRfZuWa20 z6+efCczyd+-~i%gq$O~q)YONY_!+3Rsu>10y2P42k3>`Ot9EC@*~7P8%rDD&H*4Ff zKYofpi(#g=XKdqXMUT-;E`J$ttqseb*=6`cKvhRLj@4 zE&0Avgp~&3=;ng^<_?UTw^MbdjC60thx%_{?Y+k|Vf)KI1;6pcEWy9Ap_VWC!L6Ot z3NHc#3qOv*DIVQkNtIC@?}R2m-EbW5uWU^N2Vkye|FuW&wPv4^i0+_NPzwDfo7dw_ zZ-=1r?BwA7vzgOpvN*vDhI}w#pp@&ZuLIPCDshcAH8vbh2k(GXzN(pA-qQ--t8i4p zH9T(_cD#8z2Ky<}e?OJ#dZ2swPS#9wPP=sZpr?)H*vv}N>F^iL*N%Dm>WiSO1$s;bK4 z?=|dv#zn~+KPKEJb?<`i1nMeac-Nzczs1~xj19#l=Ch#dBitgnPi;QS*z>%w!=azXUTK z?v2+U&51Yb$kSNZ+Sc7^!9b$$$+u11snP)88jX%kNEobvmso9B z0}*K4nOGXFSsTdH1iuYtj}&g*qWGu=6$y*f5MA1Q0yj=sNolS(a}o*^CQr%{9Q`bDPyL{l5Lo$#>a4X~NFfXf6v^fz$y1J|fme%d$WC_AOeALnh3HXmic-F_@z za*~Z2?QI}gb8JlZRfo^ui(9qs zZ%$BW+;Bb~ehdZyCyQ=lHF}MwFc4uYhz$l6<8^eI$-OOBDR)jOUkUhDUKL?{JT|u( zH%w(JdqmnYpU!`h2EvK+`RsoTg3!70-m!`E)XmN*^H~&kPd?Wd*W*_?3|TD{+!t7F zBDf=bEy;je?iP-MAJ)(|>u!PNh9N}*=6M9auwF#mvv`l=@TuT7nvyMMF_>jiy#jKX z*~wSXVqJWOq~^t$?0%jL*E6x-5eOMUbv-sg=i+%rnM;F=7l?-R273l%*_vZ3z>+&Uy5UyDT99Eu8=@2J+tx7(* zabsU?F0{NI?3L0grt;|ogd2WPx#iA-mv%h z_oxzVs_ltozWL!Vk`#Q=qk_7Yz}v1gB@=F}O;a-W>DG zOf3XNIB4v~e~Zt7VG2g)CA&Yr)T7}y{s_yf6Opzb=1XmYSuDZ4kb-c1@Bn)IKX7yF z>FT2DVwUlAp7|K@U0%Nd#u(UT#>d~Wx9?g?l@IW@wY9ae;dZM9%{I#|D(W5?8F~Lc zh(3x@(gol?2UWljD|4WW2DIphUtuFX7^Vk6{rTB^Ht^f~gJ1++#N)>$qnpsu3vdEv z9{lS*I}Avb1?}R|PsvN=q7u9nxuzRs5Rc&fT|DI1teem6LGOSNv}DJbnyYGRQ;8xs z&CJYxqH18U1bPK-7t0^uB}T=>aJo$!#TUT;)dVy9gDlYHd1{pfyaEE({7@FE`flJ| zuO?iL*#FL3Ua~Kf0Ht>x&TsL;n0{0!TgmQsn=DeK>E`Ul;^0UTLbb-3`uu_%D_xa4 z*v=x*hvQjKM@Jou)zo(SS~?9$7nlewqZw&an~yDGc+uf}mYA4D&UayczD+YOosb9f z_%V!N?guun*EURzgPf*}7PaaJnC<~0rCM0IPX7r1Sof>>q8#llXsDRmpNP=PkQ&;vgbWI=9VZIn zl3b&BmivieMPd2asgK$9iVO9;-!IYU+6?5XzbNdoGyU`O97v>+8il!57tsAj4j>4w zb5*^;NHi3LPu$+QxOY_vgP4Tu@ZCBG6{;6KI=+{xkZbSB-1V%w?%`%iIvI6TOU*2?~A>q>5mrD^5dC=k}1kvxHqtg#l4RhyfZQ|->p&ulWbuTS8s6TM1 zs!n^ct7eCvqU~BWNiyxbVKDD1ev-Ys4wnmbs8Ix_>5J*y1?WIv+TwAx*tD*kQ&->E zxN|UNA>;)C9bd1Y~xNq<`W-vI1|i1-Vyx(h!V$e3wTE6468yw_gVaWBM24EF!D54 z1`jF3?o&osmE3ILe2R=GFB5yp}&JiR-+LrobTOwlGSkI|@BY+ZMEj7(?atb0_bgiNSY zN%uP&eQla3{_Luw(`O<9qbm1E2U;bJnnXtiVCfXcoJ=@8=(5Ec9a83=#G4OhiG*q* z?UH)auNR%4O%iB1%`U(EwJvQrE{16hM-bNyx^ig&v^)ZstAKga)d2g3TSi7k_wK!} z&lv;K{hK#$1~O&X>yTYJc?DWM*Hu~#peipjQ`^31g*ERX078iTf-MRrM)@N>`xaGQ zlSD6v@9Yf{wwsf@9_HDysd)>_(amM+u}%^a{ACp$-wOrCy9G6J)e0z#s0}vwe297I zdaVllo+gC(U}VJboEdZmzPRj0)d?v1*bM304?h?0br)X~_xyOH|3(Euhifyj-ed7w zuysdh!LYqvZ1T-F0|$bRqnpc3A8avB8d)CLduWm+WT*;jIx$pFZz!l$PftGZG_@(>qWWv`slImL%n7h7f%lMOyui} zCf*u=I>VYCC{?fPR8WUVTvzTo%VYdzz@!Ly(CsLYF5!3&)LEuSe6Kxr!bXet0&1)p zNNFC*5!xF*`)4q=W!rByJ?xRFj=j9GChN6RoQ1D!MfhLi{QAJ*S*@FKS^>f^=s@rn zN|z9xtM7}8JLZ*({|4hA{S$?gz#kwRAfr69-(~PJ^vmZw8`T90Hq^bP>Qw!>@o6F@ zO!XO6_3J&?tJH+6S0d)l(6xwvR=ttHP+a$pBiyT;0Hw>ZYUr{#(ONY#;nD+?4Z9SR za|ob$IjV;UzCQH_K=~)`SG&J;9TT#G-kmkh6&Uz%q}pQ)CS^8-;pF1^h{*V~I5muW zXv?NZK%gdcnft|UTh|tbBQS5TYv6^F&Ew|Tq12AtRD+wZqyk}E+fAV0_+L?aOTRe} zGCloz`5BRu+#f<7lOaW$Gpl!~h8!iwq6E*!G2pRiMD#A$78iOcez6`0`4 zSP;Io0F+>@=uN?~R~nd)<8q8}*Jy-uquVlkEE9 zJoI<%PiD%NquH+Ks;ZDBIGZjM?z-c76Z|Kb=ZP`X?8m;DjG`=iYt(#0(g7K>f$ixg z=-zueR|E{1dC~q=3Ps8b^w*($HK&!Km| z;?rQm`=tu+aI1YB;mRqWBMZj?9flThob^exSU z--J2k3(W4XapziUSh39m{sny0(E9t2oI>-Oxddfn4g1Tkddr2-_q~hjbnW#sA3C)I zcla0Q#(e?Qxd;-v_c4u^x00Tz^BRE`_;x<)kPg`K+zu`DM>nni`k=fai6>~rKf@Qh z>)bdj8L-s0@G$|81fV0GF-!gV71R;O{7uq`tJgjWwtNu_lKRtZ&R zbdh>;eZP*a`DXY>>kmVlN!2U%wPqh;b6RtEJMSh9F?@W<^;0m)hK7>%zIH!uk|BNt|zXe{Xb4_-s`@jMGvhqfB|QqAgl>IeJYC{NQw8`rtklV58ODSfv@mip7I z_uiwmpnWU#eJ6Kz8K)Whg1oXcp|rK9xC+pk;NSgu)FI=R@YrJ}SZ79@eU8svSZACX z`TBs*<>pSy^^6|=Ga5cqOZL7#&c*OmiJuO$V=c8#3F;7&_C}@Jbtm)M8P{<2!^wjr6{rWoBQI%GVuU2;L3qlPX9^#Q2>aT zyG#0-Zzf+d$;+X;+1cq+j)9EkkDQ)|U%N+$4IfQfi|ZV9a-EcO0iuH=LJ!x`E<*Lz ztt_&fJO#{gW~0aB$i@1d=FQ%%Rmp=$;i-Hf%KYMlXg=r_;#fZbFPI)5q3M-44 z`UJ9wc_);Lagr-5J3LA;&OvC$Ho`0Z>pG8g zkIEysGQF>OTK4>0*p?kvNLIe?Z zB~6^%$PQIJRJjoLorb^Qp*Z#)WvLm$j}tofQ?nj%uuvd6I93?; zor1LpQXxjF|1Ls|3|zt+`#SBal+3~H7Lh`Varm(AtDH;rddhK=^<>i(`u86m+>X2| zmO+H!>-+mEX$UxtC(Xb1e-(bx9D8hJQLe6qws`pWwZEyL5l`1qE>@H~%I8xYn5hs( zS^qqY0@0G+W|=P6eS}7FrnTXrsBQkfSzc()?EK@CUtgLnc2eUygf9PQ8AREHEZw!X zZJevB+(IMWoZRIKfA@?3JYvs(rlI>j-OgRAlp29%@f}L=grMi!%uz9#cO$={@#x-;Eva)R@DF<{>fCqJ$=`n$HFVQpj7-_0 z{6}(DJLTACVgEIc=AEg>ELTJ*l_*NdKPvNnpElD5c`KtPLq*UL$shKU3A*WPxR9%N z$<-#R%&^(`;GwGR@Yu*%V?VsQ$Tl@qOZlEwJ!t@B@LpsH()ZGA+~K*Gq(~4zGbDHU z?b%!XvcwT#5$zVga;Q(E0-UQ{{df3n$&iF7Xz!*Qr#44fDUOv72EGAu5~{e4@2Fx1 z%<O)|(uWZ6A#@8uf#jYxA8TBV!> zI}S7TLKRDK^G?HL zu=c8`QQz1OJ!oq6sCD=khfkps4W*^%5}Nmvu-0pMD9&Hw%p@xHY1nI~YpbH9wz>9~ za-$icCOTJHBX8iI8I+~+$?@`HTFmQQP#$d1N)hI1GBGjj-(%Wly_eJ6JgJgaYKMI= zA!^R=Qr-DT(4}gHhS$dmFXxGG6Je;Il4GC{<;I&=uP#Q0%hVHZY;FcSK-azfdr()y zD1LwCn<$mz`$81j0x8UGhverB$r-XgEhx4fRJSHAloI9c)%(h4mjw?sph1Yrg3hRq z7+F|ou23R2;9`roismpQL^(B9NC}su!=vX`OC5c1<;Fi!6Jc`C2O|jx2zEm<5lG;f z7)6m|0B~w-Y-F1lAD5Mr^Y`{P&THxmle}JZ<5VA1a0yFGr#*YN`r`-whMb%n)cq?J z0no;$0ySH&&70+Gj{CR3>N;#Yq68~L+T3@3GF;^{ukQ+#D%C)U! z-}1`2CT;%Cq>`)ett~edHj;|@;s(|f4k!Ie(Vk!_TC%v=*-y*)`T4O4adQ*C2cFbzGz>NU$c@TILnqpq z87Pw_ICBR2J5s#bKRAF=+xz-3=9SS9s1V$94mLJ8$?rG<(*Z*RE*cxc&BNm^?F4nb|&HcD6-$3xA4`I^8(=@K* za!B1pU!UZ}cV04G_$BI&$*BNC5!>J`-(iGLBQPrgHN0x!75tFcsuAar1pB zajYv;rMnXoWj$h}D@|-@kk+gR^vo=+^uqt^F?bWTqJ|dN{)lhE>(`{zaL=sFu{ib! zXinS9;UqwE=Plt!0Q2D9Abwd?WFhSPB0qnmFFIpii17?m+~_Z|alWY{d77O3=pnpo zN($W-2{gTCXHU=2|7uQJmKtvdHRgDxN$l2*I6ypTuvq3(0`UifI)Od_^CE|UHOsH zDfU5w^{>ap30B&Z0udpu7^fdkhRCx>-`-!Ry8Hk6>(dDQ*OPA+g6Ue4|9WKqEx8@l z*$cW|Zo6B=q_5tLIr-ZD_2K>~vS1K0+x4uV;%xsV%_>LkQ~&e2cN+ES(;gb%Db1Z5~36p6|KG|w#VhXcy|9Q&yN3&51|_MYLdB^EV`FK7<2i5 zj$lC(#X{LX%+x{ay4T5Zh@dy5KZ{x#F1}c=w|kFWQ_sUgeCvF1aWVY{bpDKY(rio5 z;r!uw@RkwJ?pqj}?dYc`hr0MXS8_&2@9I5WBoI!`aSlSp`~IJ@_P@?$-z8S4eeJsS zeZ+W|N5tVABJafuJ1*$7yARW=4-U*5+)1dRK@Zn$8S zE?y*_#EpISj8q+HTF}JT!^P$O7ei_q8Xr&3ucM<4b#-uul&LGu_hyE>u`!>Gz+mot z&;vaI76|#;!UCZQ9}mwv&#y?tGf;xaJ~3Zk-}Mzuv_G`JZ2j`(i?RXK8M`0u-p6)6 zwze+d@+bCdXu{7Xq<$F58f*R{Q2!hkbLV(Hd#I0|QJ+C^u(r(a?g(K~PTtJ_k)+Db zAu9iejR&rQa5iT<5tK0qd^|i0Vz`*jsw*oiw==-WEL)DOUk~M&mgY`@su@;WV5h!s zUtPU7bs;((Mat$$Nmc?@uEGGAsj7s) zm~#vJAEOY85+%9J?tK9hCpftAKP2+kDdm#;bN>lRILkTETZfyyvj*oSnCXa^A)SK$ zWM9%~Wg+>3T(Li3Y{zdGf3e{nlHy&&$+(M#P(BzOGCg2Ag>1a#pnZ(STNoG{JNI>V zb~cy}O6zD5u+^t(p!|%OhzpzrIIiQL$_c^(^Acq(-Wt2qJh1zf8T)1JWc1kv?g*hl|7Z7jJ^T^i)Xx zs~ikuC?FDD4$g7%TQcd}WH`+Xk;_t0dDr@HCbOGHy5soG3ODBqV!A0m@hGi~R1ieG z9~wH3#$%_zh=I2iiNjBiMldtSsDm+EUNp#3fW+MCJJ8nKGC2gA0f$vV6|{)J2PS!c z(+h#fw+vBjOP0LvS|qqnWw*nwCwvVROHk@>X?^9hZRt2hZ(sjX8rj3#ZBxzv@69#h z$?g3;PBklpfW?=*&Ia;_v}E#g#C9+mv+3|xSM0ly5lLBDCOt?*?LvATOK;d2_n6ca zY{av`50BzJg?P89`8nu0+iP4^g&U?-c0==-0Zl5JPoF-G<&u)x|Cyh8M$Bg>f(z7f zikm17)VECrl2zf3jYr#~c&Ia;;J6*`ONJ&*(JU|@hsSh>DeBz0GxIUy{Y5SMP4}7E z+57xr`3sO2d@I(@%bLYW!hL7UWucGlMtW+h6mYD7|K;Q)7<)rS1rMitcUPiP>K1K# zVr(qVWpc8YXo66s=^sB@!k&dB^?vnpML&dmuFbXk79^T{K>fOPX>=?eW` z9$XAF0|R7gRR|@Wr*K+lpYokS#lGw4Ao>s&7gx9!F?VJ3BTY0%U35qe6tGSiRHUY+R@qtI`uZUYTQp&BV-q+_DDWaL z4}3D&{e|Z^l3)d6skrfHPCXRKvH?0m?b+{we0(3*p@ahFaWi}l`apLD$}=frB+w*; z;3F$jrvJRFYiLLvjQq;|mVt5Y+(9{72=VJhm-!DO)K`69T;=H{n(LK|pZM4ir^0X5 z*3@`-d((TJA;<5-%_iI?Ht~a?0!IgZOGPClW2!S%L-t0c+mfN7(33&9p*CV!*00p| z!z^}O>r-Ghh-UqKeZ#`T*_iXM&QcZ6&&|Pw8Zr`?b#JnXL>+X6N=fJD%?!FM!g0Av z*C*W)x@2tx1y5X^7sLd#o@A+UgTrN+a*&jz=E86q^_CeVo27_UwE#vOym;{f>4Vcv zn7z;*$6iXPif0Oi2JiJHC>ud5M<^*sqSrL*kK#_b zVpy%+6+`z-UY>Aee#qx2aThJc#Y5rV?kcM^HhRvtnZyB3SoFgj$h+H{x8lkkELSLT zD*pT~Z0Af6`m>1qqe|$s-Ax7e%?8v4+@SwBYr)~pww7Z3%uAhB z;-xkENoJ;#45DNE7oFJC9lzk|rKI8C3!fbPxyudc#PSK<_54%bJJk5`43Sxx)^!%= z?TEE;YM?ouSTc7O>+EVm{q_OyC_Rb8E8emfZE}Q|m=bXN97|!uRRqZ)IRqMd=MaJt}JI>ie^;K|#1@P~>1N zn0*eDT*9ycbc?SaIHeYQuAp{TFMs&s3u2G@r3xBggewY6YMg6L2Lh4ZU0uqDyWbPE zA1LVDyC)DfK0XfN@WbggmH<-*%;M4#olWDf&Wp4}l9%U>57q)6m|+RzOT{&pCi?s1 zqE!qG4K;{S&0gF1fEqr`0mX$dw#!#ORbR;STAY&P!`=q5jD|BOXEJ%hH(h#%?mJ~_ z1m#FW81K`vpQ(JXP1@hsTnItl@SV@_aHgpJAsDbn90oMN zjZZ?;t{CzJ1?UJt8(foZi|x7&X;oujvPYg0cpota+EC;>o2@pN)XR4k^PM#^Lk};_ zb9uu&b@S|a4n(RLaQrC{5P7Js+h@vnm6w(2cvQEyt66aQF7)Npdv4sha^(tS?{h9J zT@1-l^*{FfH7yf_98bYI@0Oo<-V+G7e$01uN(2y$=-+qXQ&CU^ZotRL2jFwOd9^9X z?Y*BeFQ&4LjZD?6Jh5ThaWD9Qhk~FJ8~wq~>VQ*aWiMm!pK}xG|Hd|K7ehYD93(e85RK zd!6Oj4pY(#CA5XrMuy(^GO<1-=5FrR|62M$&>i%5nl~G_w&_+6Vl?&GxP>Y_dw4np z_1;?lntn5EFFZE-ObxcvQi?k@BV%Q0=~YR|+TvpBvuBBKd^M>QDHJJe75je(pU!$@ z@upio-)1uI@PK8vB-NeR`_gk-_@rB>2pW&?J7mno1%YW6GphGTrQv1sxm((V$!uF=8J!ixQ&&%+pmr!K2tw8D>mO0TWo5Dl0aOC;4I9!G=XF`+;5G+ph1~Y z2rk=8$kD%eaRvz*Uv~RL7>se~Cdw+mH!jtFdIM&bSX@y1BYguwluG?2EB+q89Qb#} zt&g$CJ&m?lPtTF(3s~{h65p}1iUI)=!bfCCx}Mo+g-C{AvzbGvH&E-^CsWr%g}!gZ0fCB zIP-U*TvM1E_ftx8vIIbR0=x*!+?>_9q$C)b_KA~&;}%h%6d|_M4TrSOc^cP>n23mu zkuik-j@Vc3KspCPR%|U68rOy5v&wrXEx0;CnI7z2HRnc}G`%t=NFbZPUlPWF6&V$e z%!l+03~(N>)wDEFD~G$nDhNk^yBfe5z!N5G?*}Ua4#odb`I>3b%a}hpYK1U7kk)WI3;-}_cXM=fG@_7zg0zo&41 zBA^nzbFa{r<@iXG^koYE*el-bzb7&Da3`kt*vVCZ!>V)(kPUi!+pCo@ zDx@zl`iXy;DFgCf}SybLJZ$zFTc_LZbEbYS5LQhpFej8Wy z_DVIa0F;8MQ?y}k;Z}e!MxY)vqoLt0Ew>T@LhIVKna}Ld9tCF8sH6r4;tU8-=0xD)s)whrxY*6jt%}M9$_4{vlT#BC*ss11mm(!2o1Fnh znLaLe;mel~QLGnWBdRgY&GLz1=t?L? z{+54rWFRmnNT}sHySeLRu+=@@Efsr#0b)#F%*FKE0kQ|uKD)m6ItHBN%sp*2A0T(a zETpivY<#SOFxe5p-Ebg#Z2O_)cGUCK>f5^x zx-qU!n%JxHUn>c6FOpTT@~vp5lVeBLX6}!%x&0-7t)cg=gn+*Bm(v&Ll6b!6laRSk z{%e%K6QMXR4>b9Pl^~LWi7>V8INX>R%?~EM0)^%kx`&n*V<`I7z_4`SKstvUqc#Kn zM%$MRk9@`-O;kN2AZ6Wre%S>ttr^`18z+yLL$uRhhE+kh?d&8o2deOQZEE+>cwy{J zj7$7&&7Qw(7rss>_-Ij!NXv9sC#$KWKdSF5>G;2#sk@*#`QUyVih-GC;Qy?@njt!* z$>z3`Fv0q*yF!V!?w<90X|`Z}O|kcC>h?5j$!bq)`W!xL?cjCU+u>{fcucjU##sx# zT04&e_9w?YGQF;%Kce>kgtxI3mD_21LnHX-$I_xDdx$aq$6tFh($o2htNxRhwtL#V$ImhsTa3S&y5`=2~1t)zF;WC4Nj^;7cY}}uLJrq-z{_u>Jq=Qkk#mTM8ujdgyOm6GvhrJ>ua#SHBe4$IYJjukDEN|ciR73o zs;VoiNHQ|AK{xidN&t8|m_TnID13Ku z3t4W~f4-?ctKfy7#E~ZvZ00ve987&2&uSkU8dC8ijHN}#Zc}S&Xq>xPT2yq-(8Jj| zw)z=;jwEWvaUd>-9bdJG7=v~umsgF4be@GkwE*|8edlp524vm!51(Oq;C4jL;iFI4 zOfM}(MndwAb5R+}dg<^L@geYa?(gmGEyd-h!oVR^qPOzTmxQQb}-z zsd0++rz77ygRCFPbXnPD^D1E1T^5y=W}t=^6=$a*=b*R?J(?g>2xfGBW8-$xYaEyV z^E=p>cU66qf%eu8JvxMG6Cl4fJq;wF0-pu|Lcw4|{2%h3Z8MHtH486L{02>#-O>JU z$Wg>r)Yj4>*?>52qN_`J&&JugPn$om3~p;6mA1FHgMaez@+}t-Rm!3%{MJv}Qx69p6^CML#n<5gW9 zT?GIif87>VH=8k2EVVKVK_UU{5(aAjnw6O7=-V*z>x1VI*;`xSniNV4X7+S%%%wFQ#v-zOY3PiiZ#8C^VPRYI!>H4|biEMEoZ>|D{^&k{A$W_Syv?mbYZ?M1X^-5iKGSIJ>fP)5s_S##G>Q zobvfza3Ng?4-*}Y3VTPvfOc~eeNCw5TC?H~7?_F*aLCVB6F$Lz5b_cV%L!3%joe^~ zYP-zxbqg8;W|5-lE~Bx%>&9=g^zcyLWzSxaUE2{``wC8J`B|)F84{peN=LG6fEP~E zRb<}$tffK?`08Rhb+xsgE-sYB_a8ifv};Q)C{Z*?**nJA$VlP_k8`+?1N0cZ2T1iO z_pS4v+QJOx=H?*d`t>b^#b~)Du|QZzh=imhTVV9#$6y)YsyKz-n&x7mcm40ZpkVaV z6}skH5~l7b#;x1)crg(AGv45!)I_7n*>gZD0;!&QYdnT5>|N%7fRpTOwiU;r;o)-W zcG_M-SAp7bh_uPFf7En3YP|NOn8s^;IkaYX1)u2~Sh@lS2(aN6)zbDTr zIoCBDSxtia>Y}FGA+#f?76;uSn5)n8=?xWC0{ExQZ^wUi=n|At)wTypq7+mpkLilr z>$7e2p3!V=mK-#3^HUf}AdHNQi*yqAG$|443u`A+{u1?m%seA_$)Zi~&{LUa0bQj- z0h?pQrQQ|}ypd&BxN)0*{`#^hIu0iciSqk#W@tp!&J;lWAOgG z!w+2M$TD5-a!2gIJFI8}7m&){H`w(o?`39;_eyjP8?%Pz zV2fXG6JJyftSuwn(#?$;^49{!Jcxv(2NN6+KSFeSOv%v8DcW3fZL6*@hQNP|obR>i55qyZD0LQJgbSo{x88aL=LBkt zE7FSHFMC;2_+!79#Rr#KR(^Na^S)?Efa8wu=FrR4=>S%m>8W^r#s2!}(b3mE&Lv?y zhr`mZZs)(H0f;#NK{pTX@k82lEij{_O0~OP!<}=dR!ROJw*EVw>i>-&$L+nHvXYTF zNQjKc>|}2xTT(`5cDAgJO@(YxMo7q>k&#*UULgr(@9%Z$^?rR`pU>~tpWTjgp3leg z@w^__b-(WS`}OJQQ_;ACrM|bfq{|{mPuZt^ZvI_{Q8}&KCkbKKEP2KSKmGXV`i{t| zWzi9b`R*4;&f5bu7iRVdf@ZPMqtMY`8Io<}BLdXl=e^)>N{Zr=SoVe)fgC&8{=@nv zuHGSdbGcVHR+LA$XUBJG5J@`MG8AFn2pNFO$ot~SM3Kh3k18Ue`g>eQ^&$g?GQa^8 zoV7K7v7P66_c8sZQ~TN4vF9(G&&E?ZG*X_fmf&YDS2TE0H~aZao4(qZZymhT46D+7 z&yKao-b>Tsqwh&@g4r9}Xtm%c4&T1{P9`xRVj~jvVzZzMTl*RG%5*zyZzf!5?$6!Q zU?8;L{#n3tH8`{vs@U$uA!VzuJz%GJ?`ots8}aZk!IZl|&s=-?MXK_)7x0*SzV=w5$y^5%t$8-qemICKs!hSerSz6xlNm zq_SiJEVt#u_Ez$^i^b@+@4F`%M1P@H3mg>VK{{7jVD_z|v`QkemdlT3y?gZZ-hMGr zVM0G5;V+@_wcC{pQOTF6Sf7e^jx;-S&Kaw4+qAfh52^fFy!3mx$J$`b1TYsG&+m6T z^CR@in)|DY?&-Sw`m;M4DHELQ?ubjwShf$@d@E{vl0UE%{0=rEkh(=sQqpvJLxj!-R$I4xf7Wdz z7og?Ob=}`HB-m)Di7DXKW2Gn?i4&3ZMBVsA6Ck+ zdi)shtub|0NCKc&po%YteYZ&mYm&uphowAqXE~N*^e?T^D_pE&6 z>UnqrP=SlYm~Q^4HDGQ(p^F#;Kpkl~XgT|g6@eLH;q2(xhziQe$Y7m(SXEsu_W|a( z7T{?PjE&iJ4GXGb(Bo?p-mW~mM#i^3Z>RP9y|m}OO^zDBw>w{^!CU@Gk@`Ti_j17X zxdPu5SHsgwyUKC*f5fd^-isJdf0YVZ$ukbkGV@lFbIdF2>kWYJKz7iqbno7)PE90E zk$+t&a*8Mu(l&zK?|XZdf~IW(7`Vl$;+A}WN9k2Bol?ko6^Gp-h>FKJ&PhTZSz-!& z?5{;0>le>Z)84=+Dh@XcCq_XG5AK}FkTX9{wM5Z0^ z5=OpaKVanq&qO{xyT^~Kv$Al0-xCLXa{cGe?w+2V?QJzx)s`#0>C)@K)c}v9xjC9M zX9mA~!HI=&=mGS1@RgrD0$uvIf*0C(E#=@-13XSMdEk3MN)LG}6^!im+L)2$;^MkP zy^<6YBOokn(d7_^d2Vy|1tIL?0#o-nx1LD*?wuY-ANH86%$SJ2mz>N`n`$_h;;$v0Wq^5{6Lvo}7q)gx z!+FObd8dCEfe83%>WlA2l9C^uWE|bg&8}j#rWRO?IYS0Tr-o8P|Lon|{Cs6xX{>lK zhYiVV{%i@z8~}|n3gEW%4xHN1AFuMHI-~uCq0*@MFFWLI^l;=Sge+I;w>xiG~;yfuPhd%(RXqS}YiQ=1z zX(4Oox6J7J5g*oFBj#Nj#fK1#V1ie7B8Q-RdOvMp&hwN^0EjBuQvfGXa#fMSQu6in zg&k*dN&dd2WkJ~@0{caF4xr65do)p}xh`^AC=@>LSKxod6`9nbN7o2YSjP09RIH&a z7#;)?*wgUvdpX#d%wmpM%kAyO#eXkRQLJ{RTVb!|d-=%^wxcogH?}v&#xmPEBPH(7 z4~^VI-3ov3+C5zfnIzx8?t2>AE3!@%AD^L^t8-JnAGF7@24eRJ2L=a&trno=Q%#Gu zpQsGD6r>XY)|IW`@}g#KotK`T?kGUgG;gojLQq`B@={nzs@`E*D#U`w;6RE>$q?ct z5qdHGM(Rs_s*UfQJ3;IhWn;y-vQzKuyg4&bz*gw=Cdd}u(Z##Yc#yo-MTYj+J$IXb z^ip5?EyZ6_*1hiD_J^u5&OtvIFh_ifgO0-tRfYV zd25ABoc9^6N`iQx&J}fjzwYmC(mpM&+k{*_17XEa%H{j_U#?xK%K3Yvcs^&bH3z(1 zmEEE1low>2PB!gxTYK{U?$J8`z)qNHFl^xYOro;89|=TOaKu_3XePXPA!lQ5&i+N+ z!h)j@hw~@+pF)iIh$X_))WU%*Y-lhgLXB5ASSry|0_o!k2av`jwh?R4UyFP?MD*a@ zB#=B)(js5o61$1_JjlFcG3J`2O{*X5ofI zUl(w&N+L%baXN2#hJ}C>(w>a~k6=0MTD*-{vBHv)-at{re|S|ups%kl<2scvoQryT z!-IqOk9;hl#T&@}Q#S35@9{WfnBP)02b_%0(5A0#?RYg0`<0m_-_>+k@qVIxJSA+T zDs964ogm z*|{eNWON8s(9^yWnw9#L;abM>lW4h-Z>5QQE}!J!1)~p>aEu^le?G|j{82FbxDepS zqap~qu#s~6dAB^}3;*K_0C#EY(eTVPXn|lb z-3GG}o^#3GTbV4T2JhHV9ynv@ja8Q(o$N|7CN@4 zboJ2^;Db*u4jWz+**t8tVi}DMUdHRt?*NEYYSqNhTmO?T zW6=C@GDM-r=c}Ajsa{t(N;ouw96Yvmch~<-wzO|Rvh}MjND;nSjij8lNL53FuA6lF z@%MjFaROZ#Ik-&jHb5^}HNT3GDUG|mUP$1nte~}f^DX6SFR;D;B}h!z326pt*t{nP zW+Q;w?PRMoU)tNl-n@Ae8meGz&E1Ei*;eiPLx8xlS+5WTK%Je+($Wpf%gaEtba!{p z2#4iv%m>H`_r9l>7YuvJ#Z!HBuw{LRIxa&Pm{#DUZM*3S^3hsk?zb^Tkx z_@{{zfT6pSz*q^oHhG519zTrt;zdB2TESVft?dnHd)$XBpq0z+NR9XVGa?2(+ah0O z=$`H9z$e$R7y6CZnyc#_6!8Do$^}Ris~k~rnY^&bhEXiUjaZ6iK-GmW1?2QnUVc8w z)2zZma!eD#(aGEUTxWlM2!8N80hykG0mMC;NZUP<^??E1pSm1p0j8>|N;=b%FDNPb zwg#uU5bKh)1vqo`^>06A;0|;&QM$hR>a_M08;@hnq}W&8b;naF0}y%RV(ytfFeyZlUm z$sY`GC{^I7HB*Ir6}l3IXf%lt%p*DlI;+4r1_t71_`zb=t~HxmS>-DGAv?Ref)c~R z!ayY;QpH&RGKz$#qB$`iv<3C9a&kEDziw@9O;-P34BbLh-g!=AI28(F{SxiM|88<# zXyO>7|6KJN!!g(HHAVA0`wkXn=5C-DQ&P6=qjI4It%y;EyaTv311GA{sMK1B(y9h3qTKzZqs2bQ?c^ z4v$1&<9yHccjSe>c4D^%Uu~A9sPv5wX3!UPMjJeS^P|Ro!-nU3o647{BoJ;s*sVlRq!@ZazDt>7Si-?5KJ|C4_C@Q*qJKt<_!0LlEX=I2P+~o= zuu)^MIlo~%K>7Z#3nmJG&{Cr4n2Og%6Uv<)t^%dJ_N8)j6Ri_rRFYo}TpR@I+t9$; zgjRafa;5NxTyj(lc2`{*!ASR4QQL8Q01>C+WT=~EyuFVG2A(Nj-yUmgBQ(ds!4aUO zNS@jFJXPm4I_;;0w6V8;pc=U8e1~R(@*PzOyq~vLJbNnHf&_gs+gn?M$!}d17B=hX z^IFEN9#F4rT}Uy?Cg*|XX^W;J4N1hT{o`>vHD88MiGJYm=nac^7CfavSVKd^0Fb`! z`PXT`yc!Pc2>p%(vSO*w{=uy0wb>|lA2ZH;*5j%gmZed7w2yHKa@)i?*ZlN)#+NVJ zT`{St-B;eJ7$3;Rrat~eO*+3!RddYZNMw0&G>;1>Z!47j=&b)u@LCdcJ*Rv7_UjrP zTV7~RC6Y`dg@qBsF?VMJjnh4lfv2CHRbIS2CCjREXOub){8$}w@(FWwe{>|tqUW7u z{beYMi-P>0%qtV!%f4l*Tr(iqSVy7#ll4NT6(oBiB17t|SWh6KdJ%A4!C=q7rM6ZQ zXsFTxNb~4Bw$|3)S5}$@=2>RQ_jF3tNL zF)`^N5${|he=@SlWWb216+`Sy0s?}JDBTKKi9de69N}D|i0WX!#G0 z4k?gDU}PN}g#2Q6ECxT`unK;}F3}L^bv2@lAtP+E!xf0vwA&oFxqx4&Tj`x@;^m%87{b_jFPBA_~i3q4#2#Me#V$1FaIHSKrCp20@?zAhCcNea3 z0&|6=e}0N+;`-6<(HBm5Nxonh1lG>zIf`?Kt^DalwrfjMtmQxk=yNm+{79Q*>xsY=h#0d>F9oyn&|V<$*UJ>tGjv`*5SP*=$tn{lni^1gkUiI zM~k3Au5AJuT~&oVGw^dhf5k)5Fh;rjKdMuLqaHe&24zKw4j*@>KYR*)U=t!rE+lzv z(%u6;7w0&9-&t>As_*t!$k1{kt#3`38sW3kf4CR$*)77*VRgfg**P z_m(Qx?dOw8WMh+Boz%60=U$pR((i_FfsW7O{Vxp1h=w~Tt71yuZp1cnx^C}x&Wj%t zyOpsRhEvzvl{Z%7MLvhoXCFB{!YJHTMd0u7Q0(_P0CXaM!XEbvI5!+gI?o=7@vnR1 z?sC2<#)!Y+THyI;L*K9@OU1Bo;qrNAH55OFKCs!voxfiFEA*RR(fhGqs{h;`NV^6M z<}<9kY*%>nG}gQ(+F!GuM>jfgGdK!!shI2Za@!oOob@Isn(nI0VX*x_fAuU@)8gk4 zJl`1dEAct+_il$a)b1V0tp1^!G6}(c`1d9+Wabw7x#@kO{`2+!lK3W^T_?s9V__lU zBSBd&8>`45t-UqI3)AaXG!e@Lk!!8xl9d|pa_lJZTv%CLzuV+%Rk>DPN5#rm{ z5HGJdW=&*-@6ofH zElG@Px1JCE=Z~trZ)#ec@desbTl&!;WaXrbHf|NPWhC(71P)C^6|ZaguENA_P17GD z)p49>0iBx<><{*IogiP}u{8H6Uwbw#CD4}Uqzegbx>~2xnq9|@MG90c6RFb8=Cr#S zooS?zOF-9w2cuZa5s(Go0Tg~sJ`GG5VqMpz)q^dc-BD3Kwd?;cuL{nWRfZ3rY}I;I zxS(x@5yJlE2;*iTuU#g0xL%X%+pP>TXR(_%F2L)-kqrCjk2+ghHrBz+YTwvC;e!RH zTB_F^7!abU`98JotFhl5AC`Z&@Suokf+-r9O#jGEKt>%$f(8|L8UMWSuCc?NKcwNU z;H)FyL2&0w7(5J67kfr^JVXz8yqlpljF!mIGrHy$RRtj3KS1Lg@hnY4hu>d#>S8#1*Kis!Aps8zQOz z)>S8Qhf|^%@P;Sp1I4TJa&FA5$LlT(%VJVJ15)Ao?S4KsL;&!n*dBR%8_`<>?>jm9 zYiaZ$Gg+~moI;^W4`{_ z1!gO0j%oP*j<1+{Tu@eqn1@cTtC^60ndr1Pt%&6UUG`^7Jk(;sU1#*vE_n~X&OjNt zvxCTDSyv`fO6t6RGsw!MAUC;s1v5bJlx(T6uUJk`^2(LKUqEGrEDq=cjaUGe!MrdryCY{ z2in3gLhUd0Si4PxEHYmJ;w}T_B`dLdnD88n_eRt2-@ji=6fc8@Z~Dq_QZQpUj9TC~ z-r-vFn)ZKf?u&$;%9HcN2ZK#bvhylW(r78LH}ZxJbZqz@yIGT9xDPLc5VG?@^H@X^g=&qOrQ6|xaU>upi(+e^Q0yOocaw5RK!FjQ$I17<0{7j;WQTfCwP3L+UI31d91_Sbfs)aZzZD-m;C#Cc z-adl0vdF+Q*fSsx&6sVPgIzSu;hzQLT$)DlQut@gfl|zV)}jV>67UHeYc?v(Zv`g8 zFg2)w==bq4>Xl=e+tj;t4l+7|TnJhI(4~i}eY5g}!FeFC@vk3CEex5_rF$=1@bOQx zi?D~qwHW^=$q@91LN@>@n7rmD6o#Q6rex-plu#Rf2ft0=5QSu}w+__TOGSWhFmURg zhJ`hv_Dm6H&fKLxVT4)+*oKv2z<+0A3ZI1%ZrRn?$cS}yb(IXCy+TG>Qe2$)l+=R2 zjAl|vP7oQGHfrztzys(xK`_!SP$|X|iqMaMI&9Pd``qc*>S!s@>VYcmX7jeWf#(Sf zhNH?Am^+Kn=8yCOQoumGgAk?^6%|ztc7?%40%V@BN8TlvX+qN}B;3)2fT=Z|NbyU` zp`IbA9p2}08OW=Ia74yt!H7fodB6Q{38Ul>HLh>#ew9S<8CN4ngHHKX%ryu4`mn+` zhT+b1rroD~#I9`x8W5oXlDKy(@GHrDgK_*Sf4bdx6=nSiq)Mf-7)WWSipk}Eg~N1+ z?rI4HhKw4j?1#(5PSiCLR=(B4&7*0+5Yr>h$Sp4)Of0^a2oxrd&kX1hCvOW2JS;2) zj}f0cK-Eti8yw8hj`rrv!$+2b;_m24qe0BT*RM~Mu&~0&H5IE25^3?4E?}e)&&vHy zeUVF`4iIB~bwLS9fO5PN9sn;V2l6bg=f)}NHSaxvfdD>;uK8W4r?{hO%3s4hI4s z5EN3x2WjH{q}3-fBe>GAHx~|pF}89jl*Km}8}*Ka6S_10veepC&-UYF_9EXYI~rIV zRXltdn&o%8>%iP4e(z}Z0pO<4ICXTunyUkik+%;}p1TQ9eKe~BujCzh23{U{2Zv}z#+Z4HiyM?^<6ZMu>Ho1MmCRE}C$E1T6HEmpqKfQ!=}KxXS7tajEM zf_Ax~6%LK!;Y;fL`ug(+@{zh?r>(P$%&^sz^9l=5I1mgYQXjYHzJ3kr*4wv-0$m$p zbMP@o2{j$~hK3}msHlvV3n93bi4?M7g%=GN9%4brxRnFwO_TX1f^RK-Q{^ZOacId4=Riw|wv z*8R~On|fPUd1eqDc&_@zsLD8!kQrCNV6Czgu+}yc#cvA)SV_xJ$%)n=|%Sz-a$cw9aZU?p`j#&}Ge!f>|>#-#zEAlP+M3gaT(0-p}` zB|+S13K`+xT^3Y*&yT{Gxrj(3ny~RddX(aZ{2CJYn1Q@V5NYllG?Pvv$Zz{ak;pa$Z~bd13n3j7BjW~lu-m3y6i7wF`B*ZDEcZsbr}-)Q zyg@wc_z2q)o5H3^-(T5BkYZf&h}Nmgriml+8fhj`M(}ZRhP@opTmW}lH;>S6hJz^I z3DDO;Nkni^kXh2T45E69kXe@pV3~3j>=Hr*BO@ba^yx!r{QW==ss9H1TjSyenPSeO zlE~YZT_sISY+KO8<9L@r#!7PjI9~ z&#*=v0ezJ(0yx7Gub9owNVYZ-^q#1}HUxNb^|VzTxv?fB;>px$86Dgbpbw;9qaZ($ zdt52Qg%A+g5GA7nZZ=r7aOC}DK{0z!^j0*Wr088hlM5+`V-Jni~1X}O^TT1}c07wt25jE%{;WI=(NTTl?6 zuSzb6RPMecaN>x|_4|$TN|h*9UF{!difE}xU8DYdi8s0x3-Xjn;HQ87{N>u$(Uvug z&gcwNHOLOt6J70PxOq}FIciaK)e?&4(ZW?s2y7SPnulU`wMA!L+E*i(yw-6EDX^Fs z_P1{Vshj09EqY|)USx8M{^qA}Xj04N`k(Dt+?OG4)|bH?3Y%FF!g*_XS>iU#1>chT zU1yVS`Ex$(tzK9VTww4vh@CuoC^bWgyhOF41N!4&!2906#68z6T23oNJ%9(V z^UQST<@ij;0a77o7nBy^^59wyxov9Vwd2)AZn*oKQszYfcU0BlG22@Y;&rLk!tz=3rrNT>whwFEhky1%%R>lh#l@7YH@wD#&h2J?6BQP= zikB?As~AY3DM5Ua46aHf!E%S|85cSPnx?7p36ZUfa-}an>ObA%)!)m&%1zuE4v4E6 z{4#<^;!Bu|b@bSArR7`fea3%Ga-fD9cl$YBePOhnj7KG zp5Oq>nTBY~z&)8WLF|&-XXU7ChOi8E^Y}ML3-GT?t1m#h<$uCpMN&HJ{5X;tbIAR# zs>77t3%%T(lDCHXu79c{U;YM`Co$o^o2#UK9>QJkl^*S@ianqZYvGifF%zSBjvI61 z=FR2~=6_o05#&wcVt=GN2M`6baR(u`o7oQ$j` z)9YMevmkpe2O|7xsBZ0O+g?#LB!Ghdv9M9WMZ0mF^S%3>rD|PvQ{vHZJg?@BB4|aeQz(W}4$3LP~ z>u3bCl;>0;OG9PWc7xxJ^u)M-{TjVy6aTyK;d}6;IGMTSuo=NM3_NFWXRTEG12Alt zTGKl@A?MnwX1@8h919?cuQpxfJP;4H$YtE#$=tf{%AaMMVV# zD*1#{j0C7=A2ybM+$sO~C7rGHez34z%%6NvS%HmfG%QXcjZ>MSiY*-+@;pv1whN=T zN&b&%O*t##cte4b(bLjryqsaJyAcbQE>+Sn+=@e3Hn!{w9U8d3&sRh#;8=#oPyRn^ zGssWcGt)$;ouVVuzD<#l?R?{n7uL4W1}p&6{48{FSNIlI&*kOR3$u%hn&ksAm zz`5IS3KmXlc8v&(Qf{4>pi%HD9tY&2O?q9lRUR zpmm2oJ^JsuE1jgUI|2g@Ly=zVmH`||>0(_5Fln2@BlXxY{yj%(%AS#Q(5Cp;SIR{O z<4aT@kQwqb@WaE>iR6F<%+<_+r$gQqZ!=w;-o3oI$L;(TzcBaxKR2TU3gk7@uc?V{ zUuNpeazqz@2tZpKD8l}JGwG!1mr_g*y2qE#otqw^Dk0r$j|_VTHzfY_A3Hb{fBJV{ z=8?(O5)lCl9kSgQtl)2${UuP7!&~k`h@KZAa%sJ-*qZ6ZI4v5Wr2m7_OlrF1XP*SH zT(@exocoi0c8ZJx<99_Pt_}2VR?e&3ZNCDvgtak(hr9pShm|iYYR$Gj__!k{zpJ*! zCkrl&N*CeW^Y>#Vu++I%nd8pb+-mv(B&v=MxQl@Q{p-o4(lu#$anSf+4YMp5>!=CC zz%tCz{6EZF0CNAjp#UsJ0~zI|*3>AI- z;4BA=RL-yo(Vk2EdK-b%V*3?xO}r?T4*yr;`ZTl%J{xG?Ze(;g8MVH!DEVXCfeBNJ z$gx<*#7Pp4vD1Yq8r8cg%W{~;YBh*5|K~h${+dg!A>Y*Cegfv~z}yWnDrA2mhhi;K z7Pb;BC;(=EEC-G*fxOV5YiNxHS(_`o0M}b~mF+&@? zu{l?FeQY}Z^;Y<+rUZI7G)xYAF2>&nt?J)ybL>P0QSD2U=r}c2UtD?bqegCC>Yz-?k z21O}N*ZBUgKf%x`2r`I_3VRhX#=C%k|Kp?ZkN#1{pB;Xg_QIQ5Xc~pxc=bBXQ2$K> ztXjLpkE{P`J3}-rtUrC9200_=&4c3gL%WG;LyJXA-jkc2;lfx z`XAecmdOuz7@L5hU^}|_&8^^1iF@)BysDgrdH)v`Lj|+vrLLCBsP-i8ZJlwbdQ>cs zWSjHvf?{}}(5|iiIYzd7EvDlGfS#FTPcTLs5K(DwDb+qxzK=q+Ha23dqrJgQ4kNS_ zL|%(CJSbK0E3x{pHroJ%xMhsgj3mBe2zol1TY*_1f&nNF+%YA}SuwVOa7GaS&;enX ziOJOTG(Tr4)GoRg*w~uZ)Pc{r4kHP{EVCKS$Me4*z)3GyLB#)z_A_v*E_>=_*J1Gx z;;1Hgzyhy0^##bOxHvhp^P|D`^O@(--Y=9h&vWH=;(gG}l|94<1!8>uZF7tl2`m#K zYNk27d*X)-=lDBV8yZR!B$&4T%SZd0B*Es8&eQAMxr4mr(;^S1NbCz;+N3z3jmohB zumy|@aCko;Q@VP3ly#jj7kBbO33)Im)XT}z^EH)=Z)w#s*}PKkx$1|Tu%|AbM`Ox` zY%Q3(APBADFcukwcZr}$gzh5@hylF-&{lJ_!R#qDE^cma4yOe;fgeYRZ%caby@M)o z0~C0}Fd5R(qz~p4vvNh90+CX-tFMphM)PgJ+dv)wT(oz1{tRIODZuLj;yR*$XI?Z{ z=n~{z)I0>W7;QI_!5p~OX0J4r+Hr#&Km!T32*bhaI7z)J;A_H>EC5?ypD(< zYbUzH^5@2p~3H z_W%sIbb}dcFj;Y!{Q?F+AXTk|iJV4Ql2*8tVN56_FpX<`kFRCW75!LR;`$A)K6598 z38i*t&>{*KSCy}2Nod?q)iz#2?&imJ+_r>$-Xc#uhmIQkOoRd}fJ&nFfOoTR&YBTi z{!m_yP1kf5aH2+bSusO!bybwY61szv%#EQ!AcF4Q21F5bYBz8c@)rLfzVUJK@g;Xe zFa!d8a{$I~ZEETQ{a6X;$z#Dk_AWwe^t9=ltzj^`pzq=2Rfa0MO1&qlA25P@*m91m zt?+(r^kBD`@BbR|^%sWqN|o|?=_8BT`43CT<@*mdmMf;5mQ2X6 zjP5`#@yr7W!-8%;R#H^Nx(t>omyyUu`V<7v_W&(S1mswErK4BeW_x${o;NU;T3Ey! zWth+#os2beAxhwULPa^av9%Sn2h@;>7y<-#%*+;7A54IO1$dT_5Xt~A@@V3jOB7^e zGjq_&fd9rz#Jf9E1V*2i9(#F(AN_A>m_o9K>XYIvAG%tMK)Et>s1tP%9DEdj6)|{%jU2F(5~W#L&43->bOFE=PR@|$Pj>y-HX70X z*8qc0#LI3WNZ45&#Z7PId28U)jhh!2R=VYzhD?xoNn{M~bME=U?3TDkrC>_974e;p%DoJqn8#NT!kQ)J`1btuN zJ2-n`NThp@s7B7slLwt6I1gC7;w3;$dA1#Cap^U|`9dmfd8Fi?EiVLFMzMvZCHJ`& z;uqK|W_m3$FgdD~jrCc4JZ;TrIyO_m-A9kEU}d(O#$|*ZvP>u+54I2g#C2vKzV!)V zKCY=dDYE5v(%aC)wfho6Kb+Z_WFEVtKpz9HgpW^M`*1q;Rc-`2HkSJC0=S@L#?;h3 zz31UUuaK_0qCkUfgk7e%H(?7-#b)yIAHmEasOyO)l$#<#LSIKmxqV-O(KRs&qZabQ zdpx5+S-10P{Nmtrtyfm&;Ub)}{CnW|IWPJw&WTaB?v|X65ek2bJFyM1FBcLFN#|o{`~Ih8J5n{J=)yrejH2QQICl12GDE zw`P-mhZTOMu(K^wYWv1SOU=FjWt5mu!?Bnx$=3Mq;Rg~Y*c})CF#E2Snz?ssfyzkM zCE!VMnWpZr_)8EwMeT(YUS>f)21T1QSX|h|bcldi0G!Ig1Jl#fyAE7*kMW%i1+3Lw!>CAN~6O!iJF*9a}H`OGG}mpeRF585c;y*+Dyv2A7t?btSW zYlH?V%-^b-K3PLyCBEFb+m5Rg)712f+$g8eZwP1*7_ne(|RQ# z5RZ{_79lnmvZ%JYX7ju?wSl&p~PSN$M4Z~(?@+s&yT0WQ%N$noN^AQ%BLJ_z7D$| zZrAmC)s)ANyR{`eY)Fja6xB%fMTe3*1T3S<+3tc6xfiHw0WXx1T!a)Q@xXb=dU`psM3%xmFHdHz}bG%<6o5k15X%o)N3&FoxPWDZ!iIcPL=N)~gdzE+e#@AtE zzfvb*jpJPR>CC(av4If}syR>5)oO^ULQD5VzIaOab*v_vhNN1xQJk`il2wp7d+IH+eA=-Ej+Em zq-r}!ya$<8LdP%Sn9oT2WLxz#Ki6d~oD)PCIdpY3bXblqFRkKFlOMFBy!oOo6~Il0 zb?_Eh8;jdaJd0ZYMT8~r-#bqC6})ESz(bwxwrHz#z4uCW-@#G0mt@sdkw{KT%d{mW zDbt%F@@_Y_%l3q)QC}PmzVcjtc0EQpb~s{RH$&v+la4x zrbY@crBPJ?9dWcc@_E*}+y_T-p;JyZ^$gp6TQ#m;J>{I`fgFjq z+5-{Yp>hNlqog8}g^$1T-*n!O=5H{tP0+rvs@5r(PaLguZttWmpOOrV>A$O`(?D)@ zJzO=Uoa1UuV?G`2 z1gBYWV}BlI?C!jT41cO3o#=G=)L>lHk`tbSVo!DDQeVeWfqTxjXY19_D7xQv;Sqhi zrM*~KFJREs-)+r`(r*6Hg7zZ+W>RgJ`g|-CfnPjr??Ur)p3T%At*+~1hBZP0b4h{_ zY>R|26Pe~+y5@sJLVeF1w~MH+OP5UQoI=YMYht<&b$;uz&|`hTf#E7s*6hkK`{aP7 z@}?MEzKFPJX-3ye-g0?v^D)UJBCHr(_>fc!7(>CSowtN!}V#YTet=r?dSF%ZwIgvmO#> zx7(Dx$eLZFMMdhO&EuV0T@}2ApFaBSo#WB`R%>CJYP=^$v2(VI9rI?WVR6TOvT^tN zke++Y>%-&4h8mJn?6;I}tBj=I#YC=!y$&ICx|ENIU&o5{*2;C4Exfpkzh(~GuXD?x z>t8NAm(0z+jFj-a!YL)cNY9G$4L?jAi`hfyRT7v ze{uSfy{lUP3u7hS%}?Wg)Cy>kb;(QFVJwTcTsG?yFFKROWWDS5s`&I;>+aCeH_`@D z9Un<7D;Ahs==Ez==v&t4G=FwG2@yAY=+m)eM%r|z1>L8Kyw9^d1DYn{vK1xS)p$_c zT>M(4q~D}v(5e@fP zifpUqXZmvJor~!QKS{4w2@=zI^}|z7mo)8oo;TXs8pFOBrqZ1ZQ~6dtV#yr-^TUd< zsVZ8Q;)zac5U4tLh`DLbeXd!l@VI(rFKQ0Uf9Kx_p%}X&gErV)lWBvuC7%)(s~ZOS z!PGTf%m|~w07UtN4pY%D8pTPQqBmOer0B7PiT+GQ(#A}&(Eq~@h(m{ znT`sISV}uYRrnotM|mJL-+gViG+{fP_d7YFJKgGq99a5vzqiJHJuN9|Kiu>vko0uZ z^7Pe@;xwj)iT$djv>!VTlOI|gZsl)Ko^DV!98Rv6y=M9EFNZ`YRbH{>J>$lCPTD6Y ziX?%DB~OY6OP5dqGl-oVGfrI*f>?0hLQEaAVC zq5ii992b!;pWWFEby*0o7zttLZ0F?W<$VVW8}LH1pa4LreC#nF^FP0%{b60=JwLY| z=FoP6L;H?C8eD6^s_;PRz31j4Sn>aOJDK&*@^Xkd3Y%^V{&{J$OI~B9CTu_C1-GGh z&13(5qpJDifFGLfn78Tgi>f^^G9m`UCO%{Y1qFd`H;@j%Kix@9ODi^q(Cj;A#$P@z zHx0t5?bUUx7<_o31xgqT)I$xB!sK#z&U8YZ^K~JFoQYFCg&tU0&UqKrpq>AF0d#+u z*(K-B!Sw=O&dbA-`~K})+blM?Q^4Edhn6~?JwsS?k=7Hd0J1QSDQ% zlz>3Y3MgljC7!W^)f^AFNDe?%{^A8V@T7BslM_!iI1ce~bLW9q% zr5qYaM4wc^!pMnST-(J(_{_xQB;WDMOE!3|$g#M`Vq>T)&FkQm{c~F1yUGo;t$TXn zekYP@L&L-18U*r~&F?~I*)DJ!lv%~buzz5sqoZ?Bl9Q7QLKxS3>%V;nh2KOp_&J?% zTbr={t~X2u3t^3=*l#L&V|?>7VIv-9bE%<0u-vNP6++3Pj-#WmFYdCe>BOfA7R}}Q zCFb}nBQOJ)_2Jy-fIH~ixXuNw2}5&pb8uqbqH}Uyt-?RYMA};N(TM1&{yHdy|$=d z`&X#zJ*iD~bWKgAeGXiTxVY!u@157z)qQpcEr}L>$^+bqAp=+sljt0jzggaDgk}eBAPs7I-eu8H3s$0EaIQp`gP`)_nsPyZs(|N3h+X zp6&IvVr0>O1Ra<$p&P44=)K-0WR?v)JSFtB-fejdiNgX=x?+6${p6sfZwgWG|O_6_O5^I_$i8?*A|q5iA~WZ6W5 zd@<}dwnqvPcF>{Tm}# z*SS$X%G6FXI+mDZpb(eaCIrDv_h+e}W!bSvT=v>!#zY@Tp#T0sn5W|bLNmOIww{wa zD?LBD#pZ3Ipig#uJmzcNsMDt^HME4X6|4S4qI;p*cvu3o|2Z$B>XiETk4u`Sd$fJ% zu}0t=`8TiBrt^x=n&j0}bv!h}Y?rSPPG(a%v*5hSKMfcc+TlcDVQs=8`Y}|Dot^zQ z8wtKP-avCvHd2&xY@SpUbD5YMU>dE3WD4w@1sQ42_kv3tYBaGbXA?NN)Q*}X6;LW#Rh&ZiPG1gAWXvh@@XK{i7^mc6 zU-jC~N$|cIO;e9J95y?@nKUWE<+G1pp4qSctxiXr(-u4M?3*dKA|jHcd5M|rVbjBV z>#b8JdKBlW$klDb4{ZBbA165+e}A5m_){l@{8eU1&vuS}0?nMqg7qxZw}?oQ)@JUs z0ZQvx!u8s^$2%1w?hI$%QN^&g_~LNC=M_exo&e=`t%Fv3uX$$k@LrYdu&Jb8s#Ao8 zKi4tmnh-|BonP`vS(x$#il95M2T>F<}G~Wd`QwV z$z-*bQ87isuSWLvb7^*NcM0ke301u8*Ee+sUsNPLh1rd^cZoHbeH3H2(-wns5sK8y zmqgf5uxl0x7C27(OW(MhU4ONn+BvKA9>>0lyD|OY2R>8A1NEQjvhxp=}PV+n6f-IF3Oce!)i2dl@j{0)2T36#aA!;fc&znzlsYyK9`O3R}{CWAKf<$M2R8$Rajk#J7y^Up?%<#R`jS4=j%C z(QubsZ>1KPz@nAxOF$$|pUG<4SQlkyZ$3)CCy!nwAy?KSOkj`}Fygy(WO+%sFWclY@)WwL6c)O-MqgKL3EyDo z&!oz99nBTI6obgIKS=xh<7n}h5t|3IidOPHp=w*D>^fS%#v*?N8zbeZuU?_S{gZnG ztSGl&JEi{PS|g`Xvd{)AQ?EHOjRTD|4m;E5zAjb01LUNOn-j*odXC3qRnD9~`>ig! z{kn?}t$l~mGOCEk0)%HdhsC z6ls9lpP$YqN(>>$S#G6EGZl9Zq2~I4x*lV6{JF-qQ=+)O)0Ay#Q=&|sA3Z)ClUi=} zJG-dCV{J+Lkg!deyq|9XO_S^w4ZT9kyUg`O& z!{N8D6By{;zZ79CnX&$fw*$p}zXvO-Q5eF3-GdI%shkd_OF~=mTTgapjMKb>y)`yE zhg9m|efI7ssq9gB+48PjnAfGqA^9PUZ?5;){`xrjHG|Kg?cwkHc9+v=OOFHcv(5d4g=J zEW;J9z3~Q>*vk9n3vsPqU?gx{!|9=G|Fqg^jmN|_x1fh9(_Y2lO4>IJWrSyF{U|xo zXzK<1AF`k2Tfn_hd|a~2I6W%q4(m(FVM(yRqt<(Fyg$s*De^HLjQ*%ErNx3oWIh0a z{k88IB1uEuZ3qH&bdUqP7q?W*yDrOo7kleR+AFWuJIEQG)fxHjz|PXY?WM6+>Giv% z{3D_Dubs6UY7bE1ECwuYf>K3wA}>OQl4(m*k?8TZdsKQ1Nk@k{zLzhy~d)k_mt~ z!oZ@0!8#I7b3tRc-;sKmP>;2|g=S&C)1yZ#tLREz11&;3uqVk2=+d);%omKJSXk0aEqkR( z34uLl)8M?+LorrX<+Ff?xk*2$L!3RuW6I~)wQrc4J1LcKCW|#8Lm20Lmmno^KJ)f@XXEj`cPcrZRf7HEs zJd}O=KdiJ8QiE(^A|@mX*|Q8|XNc@&-(`(#m4uz81ih}nO~ z-RxFD^YajKC&b{p0#2MbL1_8;^C#)SgPumkmd(>;r-?~%z@$3PBi}F`5V3H>lw1zSLF+0mlLxYa|Slp5#F*7!X?)?EZ z>$y1-O-<<-5Ys!i^%K1l6acz_dYje>ssz<|D zTKc}GcM(b;XFn5$l4eesO7gG4io{rsiu8II061=?NbQB0xgVi+tSk0>lqr_ zm-vcK<^!XcaLkQ1S&^FLBiZ+K#A61*=fGe0DEvhN@?j^fJl@#%U`AWQL#9}jVlv^%Jl24vsoAzl*T3}ty3Dwp&q0Y z_IPzs9_)GuQGWQhEx-}|Pct&kwVpb9V;w_HuMI@Luen;#cT$#Oj9Kc$R*zRLRu#(J z&`E^V2HHQZu%y?bxpyL74={g$g4AIq`oONbs4Mmvvz^la>|bucXi5O1Wz-)c=3>+b9GwX9(y?~(I_;Bnk(uPeO(kbxkF^S2T= z=}4r&p^HZkA6}?Q-I2nc<`bjX#|o4&vFHiO4SjtW5FzV8h~Yu|ga=tFqLtkYfco@H zCIOln8H7y2%%fp-_2tTAfgmLL<(s8he8gK6M#07?-$}x}#CLT02!u`PRH)}_QoV(D z=KAQN@^hSIm4tvHk5HmEg)HO}g)fHM+1mHnv>C}qwR;E~ws1V3Oflj6Eib2ZK>o!_k)}J2uKSi>7@3&E|(u8u{(g0xpJ0xK>&4~C8olO7xeol zu%#qSer-APtu_t$im{8-LUZTNbKigBx*4pS-(KZ}|BlmhDp|BO=dK{$kQMFn&zJL+ zsc)isL%FCi5h}zl6$7-S^0_P1dZvS;Iom~>!gRx7r6kU<4(GK&j0KbUc_{KPZ%${c|6k*h7F600M%M{5zm|X@kPw zN*#PHyK*ZB{yh}4M*$}K^Pqh(c(cT+3WXE8zGnbCCKf0*CKYifE1;yN^3fyn*d=iH zMRZtpMtJM^xocn;UK#D@PuU*dR~#7WF%bZl*DJ7K<)C%%M&P6icV7C`Ex-7%R7I){D^csz@QBDEgqoW6MP=2N;C`7c({rrot`qwU_y;wxSHmT^6sK)Fp0SI|9msJA;aeR(@#FT8*v{9ye28JpXJ?nr^0P^ zZm8j4c;Y6#03iCBFn0K341nLvd?_%cCN|asMg|B?=;-LUNCAMUMA;km=dJzX3`f5I zJWk+pwIfWMT}mw~K~z@0yTTFT@d*HlXZ$Szjb_;HA@w$;O45bJU5}bp1(* z4lcT}u}H})2wGZsK@M{g8cU_EI*tvT}QZpxt_#29TGY@tftGgob89U0+{f1K|ol*kGD`+^nIY zx>-F{@-%jId^|QfTHDZ&Ck7Att**efi`fJy`AJ#0_tzXv?G!;f9f)Ob-AaGT_Emm? zuI~hD`ouq54})7i5&_ zm+bNOI(026<*}lHW`iecLG7HNq>NRfHUMG%&{B37eAzsUX@@ctWFv^cM~_@M^J{NI z%OgmKEeWETV1f}8%4$sPFB5Vu{|$D&q(NQ4^(g?M;8OI!FRe#ZFc?mJ}iy;oV0nMu%r4Vq#;NODwJP4;i^`LQz07Y2{y7>4CSK=BS2g|({ zM-``sWQN3k{;G*U_T+G(<>uer?3F!m(}ESfdQTM-u?roSQ|G{C1%!(BPEZ{a^m5&@cIA5AZI1&wV_{kp<0)dFhsI?;+pKLyo zTyV|oQp>!4R#nUQ1nnjbT%5+xnwP~r^B9RQeDj#k%N%8c_8+(KbbdQCT4MO?ERTwa zcfho7v4|A~o1@Mz>$vpAROG%|S^`Of+`%6ck@d%-?f*SUh>F%`{*tEt`uT6QH;=QT z8UFYNk|yvfD18>54!Hc@+6W77r0MRJXF_mEaGD!XUAzRYEbEf!4Z^1?IZ-ISf7-XX z2S0*))0e&b5)%*<@_D@Z+ly@Jzt@E18F&x7-~cp(xXRb)#GRfr>Fy}XT**_)#ATR| z@%j6*M?RTJYWM#NN2(rHAi8t7C{(qqrE;XRo;vSk7wloAuCs(tJ1u+l4D{%&3?)FP z4^Eld)Q=S{0Flp4$#A@o6UD=lIC``G>wEcn%gt$N2sWa7J_^wtRIczlX}v5PLQxmr zN#H^4M$%-birvGzC4-oalSL2h4Ok;iiL81hF67da6 zMzc(qxxY|R`SANMP z5)!2hHlQ*(>~=~sme>?FFBH>vEqD=87g!K*MnHdv@F_WmUS^TlLG3`>u8s7`K$mG# zM;FIa&YSXQ33X6Q8YKo%e*(Cp*oTu>g9IoYo(!lY@w7ZZ=uu)Kw)gkzeoP3qm^mnu zE925Y#~fs=|MfEvtUam1nom{95zFfT<4Z@e=FI=|QIr}awwrn8x=g?%cFH_ZWoa3@ zxJ~=#nKQgm36fuujX_UCGaNa42<~teg7J#ggbUv4a&biN6#T}dG46~1xDsER6F%to zegPC9400Gtr>d$dPXX-!IVpb6`QK&#ImYdFK+LK#N9n(Fvzgg)z#gGsFh9;_a~ZNd zz+H?>9h2R4vb|B8+W~7+hc7P0KNm!~I6F^qhGb;?T%PLWGc3C)qYEA25TAWLzM?2! z&730TpJEBq_q4pgcWE0}`iR89j6?+e2a)}q@NiO41_!5A*{le`Yg}HYin=>jvBty3 z?(irqxNH4|$xegli_Qu}#onsH^1<%)mMl6?5&lm}1<^6cqBOsJnHZmvY^_^;>ER5= zQ5Sf42zs(Xl$ndr-i$8J)ylpg;OF}9C-Xt+Z@LLN^zJ_tGX^Lh1tY}EudH|bXVR7N z|H$5%`&MrXHM8hT(M6tQ6V z?7|U)2gt51TiVaB68TdYB2PGg#LU24uqh7TRSX>jP)i#CO&afpXw(&YdisqwY>=B~ z);sjQX*U#zhmqmCo$6AV15AAAtt@Dp_yAHj&@KR-qFK9o_Blkcn#S7Npu~;M&5qHB z$4}>!>Kc0m*3N_CP-kW2xMNSz*kj>A&);t+aw5kg1sAa3ze~ObQ=bEoM0mq8xUj3Q zcyY!9u2Ne)JreIH4-aA_?bV!O!;zaDi{{s_(E!)9Lj!EEX@3R^kHAIUIQ|c1tBwS9pQER7=cs@^EZ+=CjU3 zbmk-wlz7kp7r@qzNzqD_@D>t085AkEZceW!tWG4y^vfqniUIM$bt}6Slc}FtZf0@; zg|d+dby@pugf{jh24COxN68Ur&vd zd*2Q8{i3(hxI@b>emnlVfyM*7f-gEt<=V*0-G2p(Fzd_U0AeLlCFW?c3?5IRb%qUn zCC)D&jw0ahKNBRpS7EcZu?VU!(tHJ)k~Zb><2FJ*D=L+Ub|pTON<-kq5JAD^`T3L0 z);cgVD?dSw97{k#W?T!FMmb4RAQ%{FcULxY_`Nu=O=pu-kThb2Z&e8s|H6S9y+)b zj--L0(($aMkLPT!;AZaXwGu{5g!?5EoInaC3l2=G7mU&-WVgb;kX!MwS$;oP*_hl` z0NfxE$m>9xut1sVZoTb#k4V6ow&mmF!$jbK7pewIQv{a75_gwxLmQUo0-i=k z)&FzAQ7YOOSx!EEnBfPKE4ME%(%ROq_=uFubGW3y3rcC^L60aAK%|L_pI@^#av((D*5`*> z>K98MA>6Oc=jkm^OL{qdoH7z?no~uikl&yVG@>LaUv1S1!)p^`AV(|cnpj~)+om(4 zir!kKMjU><%bu-t_o%*G1uzF4e#=cdIOzus`7BZJf`m)KudCOo!1f5joAqA(HGFWV zknJ@?Z$8F1H*}blYKrX6nX_kGbeWv3@5Dw$xi;K>6%OW4Hxfa?a3v>nSb(7QNcrjd z3x{Yh;}LGn&-i6UBO$eh+6^|07Rx>+)9-he4V$vH(dyq_;awARHr}w+fJP zDo0)bxw7`>kZktW%=0pKiV~TLO>!}?Mon#r{xCAoo(Bo=G^D)HUJ4#cKD4c1;^TeN zJVkjZc&REW;VrxtfkFl-_9$lDP;0=_jSszZcWrlWZ}vl`WAFE9*N&!^*Ym%ML8P^V z@ljsxN=9?E(yd!;zSeDh&-kXF1P8O8V}L&72QNNG5FMlnrE>e{Kyt65f`TVTfxBX9 z^&tEfxv@6=E!5Crd9r=^Kx6Y+iMw*45LpCG!$jmMDZ`@NoE)wc<>+3^k)9-E;zw_OC!y%N#0dyD!Pu9F` zC{^iSwx3t;E^#_;s9mORK&NjfwEU_m=#h`F@6_qUMtDtHw`umBzsi#c@B|3^^F(>N z`5y;ZDux4Q4Ow;5EkUL$zZDU_*Omvxf{eRxpfh5|$Hqcl2P?T`pOT$y=3{UZeAoyh zK)>d#W&L}uY8*+fs+rBw89#;+Ue0;))v8jd)LpnK*Xpg1mQe+c@=ic z@+Zg7q_(Mmrb1qHKD(Jb|HIaAu@pec%gpOfVac5d*5L9Lo-hpy^iT``EG!L27`=4~ zw5r(MoMB=390i$sN6dM4_S+J#ot$n0#0FJZhD$f^&6j|tqVu5;I`+Bemmc$l5alFYM`75-BG6ZlcWcXfQAao&=1r`@B2Of8^XPr6mARoIy-1jg zGNm+Sa@>&?psowxlz&-bz!49*1?F4sZg0roK-Q`Z9JS4|OxVSe)wMM`Y&ET1Ks05@ zyVq^!uZx#uymr5bsBhK65^4udf1sbt?mD}4tMwC{HSh)Yza`P~VE(I~GEVlD&JFFZ zmS$z0*9isf6hO{i#^)KnjZDbQ%35Ssq^@wvEdD+80a*G&PTyY+_rA`4k^Rq-ha8u^w)bNMfH~0oHiiqq3k)_cPx}l%H$)LPr9JL zz6ZZgbLvx82$@uwd`~HYbU!&Ru~CxykSi{=Eu z4w{JbsB&gr`u_G_QP}>qYHF#k5JnO{ZAk@Cy&x6>FcD)|ef_&jkDSimb{?SS(5wWMPqYwh4arZ#P|ASt1vIGsjiOe!iCM@AeJyoO^%h$aV+>h;HYnN z_{OK-nwXf-N)=j568vjDjF?gHlAhgE`_ED&u;reNyQm&7fc6msX;G!w^r_3?$QfvZ zVR;{kyi64+jUzUrN%lq{6TMgCZNNv+JbgKO8te0k3h(*1b=rTsN{3DAlTDU@1bqBjLZSceqLF12qpqsg8S;Q>er!;$;e4Zi=UpNbuz6nyuOCtZJk z_~qXiOa$|ICEGhG*P~#i;UfD`MI;hwiat5pd>{EE0dI8>CBO@DxKVFCP#nIUO?DQz zd>O(-7!lftm`IFTrPv5}ap?Wf<&Mcu7$Lm%(O2EabcjN{>VV z&JvqWJRU#jHNm{8coyX0RK|eSPI2ba$L{XZ^715opv?PQV+;-Z!`Wo-uKt=D9(D#y z@;8y8vK?LoUbO7}`?_CcZ1zA*v&$Sfp3|wmn0KBW!)*FxU(L^`VMO zb>gkd7#SZ8J$-$t8lKYTF&&@r=-ovL2FCnQop#vXa5xGL-!6jF?dmNd13oVixd=G7 z4=_c<+wh`C*q$c=xh7H@-V-g%Li8Ps3p~YOZS|j541)cy<}E^F zUaeo!q^o+2RD2hvmI`{1B`YK_Z=ip1RQi?t!rGdygTWoizYH7^tJ+^8W=#9bTU9V^ z8kR!PwECD!A_b#Ro1qJPiuc~G@2g+1X0XiV-fNC2qcixoNii`ikZpkyJaNjmfI#G3 z>q23_cM|cf0I4?uiAa^MO0gn#wGf5e8Y#-owgNVKAUWMSs|{6r*8JZ2 zqZAB;t}G$8zYfG7)Xm&dG#o!ZZdpL|_Zv-h5$7_g*3;ga>TTxO^NZ6z=7g^Hz=D^t z;U*C%_x?nXTOhVXyw!tA8!&@wrLzNAjoI0*8$ePi{~AVr**fxT&R7TngcKI?U}$fG z&r-8+e(^0DXj~&&2>=Z5#kM~|^h_4F_D11h4MKzfMGz^a7cdWoStNF-kw$O7?F9wM z1ie7wZg(4T>xE5WDZIM9WxXpFN;A@LIMF4@i%X>|C_pa|0amC}u{3ASSQ}s;is>6b z<6UqP5nkl!1<_6+_f%xWDj}HajRw%9-b%xi3{`kRNt}rZ3GmoplOpGN^omA7X%KGM zXL>sM&frv9DNOG0;5Rgp3vJ7j4mjdI+Z)4oz!Wud*%6CE(v7%aFqqKcGYA&n%^v|& zz}yc<_RRa8j9`HU?S)NcnSw$SxO~vdu&LqDoi7Lk4Kk>#R5^)aoRyVo%N4a1FEBM$=aAu5rNEW z0oQO%VOQPGRSLk+4&E3%ZPlH@z>4+FO_y5`xtR7uMn=Z!WVb^9Kj%)jOIw=s*UErF z>n_M238BGkFf=Rb!{_l|zjz9CM#S`m1qF}uO`Deap+Kh~E&)z`;u-ae0BVH}TgSKy z%HbPaiyn zHFZKGXFGu{8_I~*u4wQfu)c5{?JJ^Z!DK;wAvqSw1dn3&7vf~0{XxBu7r*JG>WuJb z<ki7UM@G_yARGo6`MoyTQQBrDCYOpIJ4kGucohQX zjN}HrMJ8s}iEJ8b+#+ogLiCz(*|NsfTZjcs^;Ofak8HzM$t~Z!YTrVz=~KdKAFYbJ=2;kT<2;v##Ham{8m2V{R-}|%KRIitwg1Q8))e<*h~PgD`V%?r zZ9=>6x;agr3C7EL4hRHnjtm~A^ipn0+JixfsgEi%yk`8H*gRf z&CSg8z0l>eZG&J~G3xxc?(U^L@3r3$?m@;;3N!woedlpV$VT7LZr5T7B3abEp(h;D z-n_g#$jk0E@IDgL{>NhHSW!_ug=pNR6-t|5*{*|54VbVUfYC}=rB-CsiFm*7KjjqV zY%>&+k^i0Z=@2rh4`#3{<%@Ol^}it_@Q^?)%C;pXC0SEXOI6S!%&x)$QBYE5$w2>E*)DhzAVk4G5`PLgD$<{&iW(^_m+)B+=TxkM zlq9zeR@?YF2zJ8;J9rIsg#*1z-#0gP_3BkPuCXyOs2rwVcr0Q-QX#pOM~X@ib~1*r zOAc@O@!kRL%PymsbM6seFt|Z5q%8j1uQDO#0WYVDC1pb|-@g_JY)cm>cKy3x1^U|= zNM1nQVR30`rd#I`C3ElBuY@#rBA=EROE2F|MY5qG$FPYU1r6TdkdR?Lp9ohkFA?>N z25)#ST);KL!|*;bQ-V|94P>!DOO-B- zLLOG)XBQHRZTN*=&b7<30ZE#Ko`7%r2zFfuHOLy!U$zwYoKJ&JK=Jz8;CE+QvIgF| zWRAlYNEqg)8I_XK zf(3kAE(&bQvU+A>q5*6<;CK;{A^PL3U~r+uF=q^l@6d8rOxV9K5Sy-2r2*&@SjUaHUgD z3K&gycfmMlOw><_MJ5oGcDoo+^r_i1$hSg@>R?(AycA%F!_R%#OUVqDJYoYwwWj2) zx`2xq9^s@2Qg~uN4!=P0z;bD^V%Fl5?)}$ay4L|9G>f+{^W- ztiT@(za$)bFJb1P08{1Zh&nh6M4tyNtvzY3*9A}wu6opFLZ&)CK3;@q|G=x1FC#`g zsusYO_wjwLZK+dWsOSp9PE}DhHa3t3LZWQX1IwKu{}{iRr&kQ+@JSW}7FmdbbPA1| z9jtpms}rzZ7b?0FURj9t2atcaG&YL)ZC`B#3?|cYqA6b10_eJwj}GnUQr_e{61dYI zaFoUb{CiCN_kSJO|2q`aSOhb(s_WXXo~o!Q3VKPOtLn4}1YqO<6bm`_V?5+S!&szJ zY%N=o>UTQ#iu=zC3F$mR-5GKQSdOX?a5UG%u!L@_RGUl7 zYZnVF@BqtXmzCX^l}16ukbz_-LmG7|yOl`C=CL;iWve`)R_Su|N%4C`i` zy@u+r2ihy>K)HK$g%PW`o7Q?bE^|J6W>Eg7r8AlUbSn?s^%+@M)PpBbJUl$y+^0j^ zQi-5JPcA0NIszQaVqQ`bpm`t<8e<8Zn9^80WZh?v(0Dq60FvsXz(7dF{Q)1DrN}KP zXmR6sfN(!TOgtbCdMOEffOLLyv1LBM0k~NX1E?;PYO=Jrh`00!3!Vm_{0ks&&L6M< zHN&h4OqVTB#MM4#tTkaet*aHPHw*M+y22;ZKz-n$6n4MF{!d|Q-y*m6dbv){IYL8= zemMRxIqexmO=IJ;jKD-ap-M|fw`3v1WLs~N1006Y(+)EC0W~ZO0+p{R!M?9y=88?H zbT8)Xzs8aKh=fC}CI)g;PWi~>WZqdT7~a19ZFlvNx!QiYlkF}A_~w~n;2llPIR;lj zxf5qT4l1DR%3!nPS75ptv5Eu0Ln_Z{Ht*rN!@4>;47?rCT#CiI0PR;~#sL~;596;h zt_*z&SNTJqOH{VBBr04_6<(jcZ!G}@={$9{CXdbcuAVchjKFgu)=wcZGEVi)^4wgV zRZbNFCK7PKgiaE-4!0JJBW2NWTAh;J8iJ15rYW+&XlPFm!;-{8b|f(->;i~*LwPd{ zXHfj^=G<9VAZ_edZ&}d^&9E1flahdI-UWW*t`u~IzOR}8aoO)YL#`&21itmZztalD z^z+`!ll&gPzQdI1j6TyoSE6rEZEs24UN*-cKdm;`drJ*cQE1vH){_(KTU&+i>gYqI zz|PioKSPnd2^KPEEKNEfOusyhvvumi4Ng&3Rfm9 z6Z`*l9eB1c3PF-r9%j~k`}P`|y~x0O3J8v80;^f!2s}8iU{FCO>{N|FhsM|dnMp2QGS?Z~3&Fi@=UuWSx<#2C`ug%YcQt<>@$Z1f;0Z{kX;0)>L;zmYH zmrT6>khZQcZf2Z3npE$Di7NAhP!+0l1SKo03EC=(g7=C~G+Pse=KyeePHD(Z^luu> zi}uC;XDMQv7RWn#j^reSbeE(krE=z9%+KK#$klq5m;IyNyuM?(WqSDQ8M4!Vb-FYVe1kml6WmllaL(H5lbK}OdaC|}nLkIMd=q@tp zn?t9`&WYCdPJT%AsQ9v?!GG$DX?oy;Gtp3&o+OQDdekkG%*nywyz%2xKnVuO{o%-A z&3~L_zZVFdh2y~^0!|$gplM0mZhzZj8z|a9Kl^fLZLJk>K(!&b4nK>6IuJP(RVoDf zp`qlZEP}S$OQ%ubsYeFuZdCIq-MzaB?grW(BJkiefmVmTzd0-fx!!4PiB;>_i|Rt= z>+{3ndZ1U2WiPn9&**@NZ~oV1IU>?S^8F%+O2LC3J$kf=u82Azub_Yy>jr1fgU$BR zo!^EKS)D`Sm3})w5zbV{+?;()1zTO~a|1{0AtH7P89#JW#WyWetgaJu5JRDH>UMJD5+Rp}v0j?4<0y zN6i+`0l6o(DG^DhEfx$kyw|bVQ)U=A9Q&#s!B2?Z(&JS)0`4h>SfhY$gNQODQ00TS z6)vbTVM{qQ5vn%(PS8e!9Bu3!<=zQW6um zS<~YvBpbCwW$5eW1U$^_S>joY=TMHl{T9lL%Zk$%d?ZepY`6kSwwoXocqpWT3sfj9 zjVgV-Kx}j8#M*%$%3)jpnqGMUq7&e9KSu*v3VM(^lB}%v&eQSSxcS$j!21R@8IqSq z+Fq-A?(Qpq$s_>i@LC%R_t&q-ln|ZQwvC3CRwvJ^BG_AseHs#}`Nh_M+?^;fLG7!_ zVmZcLc}EIr+?67yYQBBD9`je~_qn+Bqy^E;-R%5cf)He^e0IJiZ^2z5?xGRag8fgt z-||xu9Snjb2jDaB5Y8Y}0bZYA)%-UB+nBh*S?xhYKK)1T_xViJzmgd zJ3vCD&i(H<`*eA3lIsEMG1EpH8*W=FGHH9B^(yI;huWoC(Z@{v=7R!QkKqB4I$6EF z?XUGqOp?sP(cIO|#p0F&@t>SbZOJYRuwG&%{)4zUpS+E|n}rK2pS=AoHw!rnGbeKk zGCmaxM=LjLR(`=t0+N!fe|_0A>6^ij(@&ujerqPf69*>rBYAs1-oEB{#kW%S-mGsQ z&D9@5hHHDI#mwCv>optZ*VT&N1lehaB2l<)Ohdeo`&!I+^IEq3V*5AIou3Mem-)R? z-t1``ebrkkz3}Z#fk1|KeC1Be_Nwycou%j&vY)usLf*^;EwQC2f#*7UHi`5U^(xuf zsfBz~^ILs(i_4}R-TqOt{9dAn+zTB)OXZT!*zIvEXLtU5I@&GXT{E$~E@9Ixx*IP< zn&=>`5k@L6zF_E1x^n5I6h(B^HKSy8&Fg+@+PQ{pb&qe*rhIo8z4!ab*Q-`09U}54 zFggn%R_6$$I?1$J*MeR)3=>u-e%d^3Q1;eN^n02V@xXDE_BQ{$nUuqs;~_tiWQA`Q za(Oz&HHlx>ptCwF6lizITDx`Np26p>WS(C)54{+q%~~98(&HSL<3HYJnn{;vYV?wH zG@V2-p!jT6F#Zhw>zS+C8UlF-eLfalBhBKu^NY=jJ~r?id&u*CrJu3mAL*-#PuLFW zizv7pA$uFAHEew3Yxz*}mZQa;D@S@fCLf+&b?KhWJ%TFpJ?u$`qJ2R1d#$WeS7hx{ zpc&t@gL0)}4D^%3FbrP%*w*J{ag_tfpHF{FX-^x!eV?Bp&6zPL?!D@dL%!9=Zfy6v z6pw}<(dn_CJ~3$-RA!cI&PtX@p`NCe>wAw3^i!3Rdc5Ghs8VeJ{ z9J6c2)j$zimIV%qwxg{lP6lDRUB3sT*vRlnN3PmYedVwi<{CP8=c769y!noRvIebH zj1{gi|M%w$+S`5REjM2Nuzo~p{u}E#dCzzLl0TU^7cN|6U@UxcFiN!ew2I5Ih8Le; zEpG=Nt)5Q2;3Jgq!uI*ir?-tA<6W+~QBJcWYVURY5Z*|+twKk_UB+|ZisK=MgRJ)r zV|D!`itxv5u5{a6yMEYbDdQ^pv(K`~2S=#R+<*45D*e4-#l%&eFI6PuKfgOR1{dgzxp-bD5n!YplPCp@ARWUEVC~tva%2b#oeW=$m{kYuk-k#p! z?k~p>+h+>fUmRy^usFc5eKim3Ki(tMntVwzzwOylDC4{7pI;0$r*Ly}1vV6CRLw+W z%G4)O19AqMI5+-fuS6zq8czBuZ&`c!uy;%)N22pV@khFxu?WZQ_x)7=URKJ;CsB_e z>xvjz;ogf2OLG<*DRgxgNK4PB{5V9=Tw53nOqfIx&Ic^eop`opNODvod!B2MQ5f}E z8b^I#-nNS2)~aN4zZgyE`6rnoBF$^vam&k%eRlX>%6fG}5z8wO7qd!w%DAmYs=O z3=KMCW`qkFW}ufm|AnL_X7!}m!$nEc6E>%4GD6<5oHh%_;;t}=@_%_Ye(uY?Bl7%} z>{7rUbk7Ry?b)@lYRI!5GQ6Z@_1wjx_{MP2xYk;W^q%9V&N=zaEq~Ed5kogfRviWw zO$>hc{~p2fs?zv5^EV7g_F6o;c7pWMn9TdjM+liDDY{&-*{j?}3TqnFx;|m#(`Ro! z8@y3#&D+jYYNUNkohf~XjI>RJ5+f;cYK(^J=|#kwTZHXv53QCgS0j0^mp=&InGh!DBrBZ1np~Dt*=0X^xuR72CQE1E zp-V65jtq&Mo&9i>h0LDDL+k!HKU@5R5aY$|28@$W>;D1JSKxHvb1NapgZ= zLfh-M1uNeTbyHglGdD6mEq7D5{ePqAaLWq(g|3Y`xQnYo!eo3H3mYqIH`dFSg~<41 zo$Q@lv~J%rgYU{&c-WX(U=?KG+p4!*?N~1h!sW4Kd@?p}t{N6DvQ7@Sog6J3-B_=Z z{ogqyk^k~avjd@Vh?V&Hg^kWH@wZgIx*uPljg8|8jmtSiQ7BC79FpUyK-X-qbK~&Y~wFQ4Q5NbjhkWNJi^6grqLb*DXXJa&=E0PFB4*slHOQ!|x*HB3&4L z==A|b-j2LfeiAnrh4V29Fq7P|B`zp_Z94F zJh^()@HK>fp&6?aB+M|J?-I-1OGTMC%}d%mu7)v%o9bChxvQs}rB9xvG;@ewxqRdv zr3}}>XH=y-_9xn&7`|z4`+W=L@vPWEqf2T!u)xLVUGdZn{0{#=*HdS>e^-bxR90-Y zI&+Jw_iWg2%E$7*+md;UhRnKJX-J6U%>J_xsX?KS1S7`LlP)lSx9=g*%nGWH?)xCVROonJ%*70>xfmnVum(top-EXF#YYcD3TPcRdUn&CV| z0!O4NZuOQ18G$Bq0cXr_Qs?hyIX^iOM0HkbemG)n?ZmHM|K)&I1vRBpNA7PAEh(Y* zu!DP|bZRE!0gZ?Ki@3V&t|)OlTGPEy&uqCfN|KfVk3C++d?H$oNm7hmb$t#9+MR<{ z?}J)Tyk$(|I2L*ecRGb-eYfVp-us2QY`!IISVAZ^M3g6fMeESL7Bsw&M8k`^T%Frn z_LGypWyJ>MeCB>UiK_crYD9DjczXujg5T&KG`VX<#g5~!8}%S-U1hLz!4^L1N}$cE*5tD|H!GA9oSJdHIsFO?mr7>!RrwH^lRgk)X~Y4t!c3I;!L&Y+7AI}g1>5|8TbUM3QZI(gv8+mGBE z#y?tJWZqEHR$g5^CAeuMTU+FNIP=ugWYJSB%3qa|mM#WjL(m(kB_kWAs;DuMGD5m5;*iGJmmV$TpJ?EDf zJUPxvdetp`CRJBU5f_;Vh$gRj;ApzaM}yDzwCGPfw&s1}pjz$Y=t=2~oG)^3X^Q=7 zHT>anqOt2AOj0ZpY%c5-Eqdb~g$qIdMihdnIr*{f$ZiHp-BVo62ym3gT`w>W6l;F? z&ap1a;!xw{B8|$cF3zwLxkZUhJH8w@d@D-KxES710Q{jIIiZ*Zx_PmoRxcMQk7D!mFwYqT0&RT?fEo9e7`_cFeA z_@Sw4dfibC10xG$3H|KtMN!!mq4EbHJKz4S9#4-49=8vG4JwpntOghw|sc~ z2#Gk3K8c&Xvfzz-xAhxiS$Jkh%z18jO?=3%?KZwc>-R&2*VVEMjq})2@do z{{28Jf?$E{qP0gB(#;lztIe{XAa+J3zgz!QvpIAA`9xq)i4T3>CQql_jgW^(h9C`g zgf!Qi&gwbTwDMEh1s|u@j|HAw<797dSGU^PXDf0vWOz#K*r}2pU!tqfaoAc*;)tkY#%=fK{F^?mQa+{GNiKYLeZWUw@!YMU2u_nHov{t zWu>Q`%utG%yz&s4e&ay;vj=wrNBj>3Y!$n%x7EgTBGZH4iFvQupFH&Q?JG|XKhMz} znRN>9;p49uOG$1qy(1hs?V-iQH>V&H6TK)UB%9%P3@!8OpbY~gzwTx_+$6}husicfO=wMGZwZsj`3sB}@Zt7iw9scK~S`4+m` zWK)c=AsaH1_UIw15BM5~n$W|!)sOs!u87jZW9eMu%1Sd%Ur#+95THP1DVQuG67GdO zh075eP;hR|K5jCoyZo80(nm72_j6s<wKjF8DFvyM|=E#y}>b6#k}e~8O=juTJ{E9maorD z9{Sg14&BQ_!}`;l#Gzml0&`IR|NO@sU9MfP#b_y)lGj;9;PJc#xX>hwS`dr3W-6jhx8VNf( z1J1RHtx_MXW^^_R4ZH08{I|9U1m18lk@4Cy^8n_>tGE@Ka=u;0z#_r!|q`eqDwOn8n!9p?HztM)HF<}Eap2**;~&Vf#VvML zvy@W3Ww)l`9$*TW_Z!E2OUvUQth)1Z7gLVc$aiKZd9739 z*kqh{bE+RTY|bX1yYNc-SdF|2UxC3ID3tH5e$BPD^jn$A-lF4KzuhM!^NM~_t3`bK z7&=2e|F`qEeQRaQ@teD{N_!|bvzsHoy2+$!HL(O-xVKh}4A-D*pM%|8MQKBPCTFUC z#XG3g)i(<_29}z5-r5eJI#!eSXaCfAbz~=P9JSt3<*m5*O6R4{S7oyFGW!Kn)Vl;n zq;r|Y&RuZuc;}kMu}spV$7+`UtbL6a@;K(f)h@Cw++9UBMCMvoo}tJ@VyfpvGQ5m( zuXa3l?GyOuxj)8YN$ynmA?G}rJerkA87@b^<@V_{O_ijU--82veT-5H$n@{lf3Gp- zU?l64*AHIfjj_jS7N{h1c5^1;vtV=JSSVDvgc!hHu1ZdCIK>6Q%AWi8U0CRs+J0N< zHWHrkLa5L6zFr;l{yCw%z4{gP)@yqk&MyQ3DC@Z;F5?DfP6Xp)cGlw~H@I(bPje(| zY){ukWR~{3RDBNQ9Q5K`%ut7Jev44L)U9>1w}$KURlA4D+4qq7>(ia;Ypbih!jh+P zxorRY)2VUoW^VKPt@V2(E)N-@X(483k{t=5UdOGz<#ufu?y@UpJKRXJ70dpcobLJlZ$b&>x$fJvN@_t=BY@^9wn z#6`=#%FKRZBq2a0CDRVSV#=g=?Ns&tYbXE9Ufu&&JrVNH()?}mH3~JhSGRN9Ki#)X z^kX@33int?>y*9EOo)ij)!)r~=Y_bD>j3D_O-+nr{jmHoXm+xjNr(6=0${Ib~;i>D4=>yOnLRENKL_fG6n85okfGX zpVpSc%xT>F)=JHtS__{uoG#5ORP5b_eI@$SNS7V&bA_|bdzM{Wd9TSv)RoSn&%T(A zO3QjZ5}PU9GuBO6Y3QXIY(6Ss_q+d{mDwmwflag%^silbQJrA*`-Mx)?Lrl=80)Fm zpUKCsAKZ^+c+2lToRP0j#qLlX-+$?*-FN2fSz~Toy@Qh4K+l#Y|1a|1GAhodYZoLT zAp(H}2@b)6LvWYi?h-t~gL~tU;0=U8aBnmO3r?d+pmBF6xHnD%4b9x-eV*rizd2`} zb!N>sv(~KP554-nyJT0@-n*`S?J8Xch1%{@(3RoyBBAqzT0cp%TBw~(5nlT`xQWPp>SL3p2r|)}WPEvF4&$ zRQoNgKcXWEyGz{?z6VxoOTbNZ9{D}M(|x4(=+wbI;WOP>mcznDR7p<5_6!qMFJr)% zg*9-g8t%hixCiT6|B`BHllNw!nwl;102Tg?H+)Ucih;~;c^A^zB8qJCxu!f~@8KbM zDNzxrW_PT1()?C}sp6G0l}Urvj{OE1*uX=?ma@_SVK>$jH>eCn4fo&JQI2}I)qQeE zz`XpxMmcxXT`V?Ji@2X*KVLA;*^duCxU7% zkDD1#Ou)V`(a7JQ*I>L!Q1xmIxXulZ=8Xsi8&2%4Ye0_bwOiC!GwvDjlo1?(OZ{CE zn%s_lZ6kB!TOGF-pB*_q@LDUs8D8A#WmBeOFV9*r7$75Ea)tc;lypL&@@EJCQOmZKgkmJ-|Q~)ImVaCqWx+Q#OKJ_-BbE_$G zznzT|isG{Pwl;tRi6^0+Ar>F~=N&$fRJ)0<X3vK;U{=uuJo@ZqnagDwCkNwAep zpmaU-H@IG9B4;0w4~wF$Ggm>>V>CnhE~XO0=F=248nS+Kw)*#UQp!C(2sL=VH6fl- zhD6%(Y*TAz8)VI^#p_fctI0=JU%O8YY3K<46g}bF_g!z1)t&h=V(<(c@ujH=?htl_ zbH|UkZqM&Ewc57I;vsM^Zsb;I4$`Bm`XF@Y^cM{&Wqg2Jy+N3JoX@8cToU();$W27 z&qTdtu}TFws~!tI4m;x8>0hiFF|NJiPhmp0O$vt7+f`NIeyt%mUH_x*{9#GlL7?X6 z#%ginfJDcsuEjLv1RGkBQv%I zM@QdFutV9rF1(Eo$8kK`E$-dXv`rA$Taw&)#Qqa!p?}HZ^^d(vXBNsuL|L`D*b@U1XM4P@(jW=kCtn_j~)Oug{dEXF|g1!}&gJ2bTczZ#T_=_cJnVtav%*{Kb? zphN@*=T~b`*_XZ|AJM)Bg|1v{h8~ew2b@@K(-i~N&E{z)s^YRz+=hcw9O*^JZ5^za zC+sPy`s$8pUO_eOtGd1-r-hf#xi7T*n76UC*QXU#QM(TC^)sA?>fqj$#ykI0KwK-?xF8V4$Am0?f6epEl_HoZVJC zEk|!7DX^$1OOA*e-p#hw3kALBohmcL{o7ka#Yl^(A` z+Rc@%f=W&u;|erJ^{E`|THN9^;EYe~b$|G+RhWW6qAAL`ii*YygtpT2lj?Vs7I`j; z;KU5BDBsh;T?)5eM{{{M3n%nJokvh8T&}1%m@TU0?a`ash90JO4|p1j8|Kt>B}Cj{ zMszlpb&}eUDcpdzu@S)Uc@Vms^39;)QtfI7nsmED?Me3&dt*xV&SDgw%UBX!m^W8v zIap-6Q{!7nFbr_v8*Cv?ma~rVl0;f*6W*{qxAN2A@tdmoc>?$_%k)}B`(pwS{!#M> zPiop}Z?=bD^f{GU=eNH5<^X6`F3H2FQE+9ZmcPb$^)!4EzQ4bZoIjU=3X2|iF6*po zgBD#@V#>*6ag}J^y%|kTq8Ft{^QT4Eu`H zIgo?1J3%ARAgj>_w#V093O9lB`>$d``0*qvMkSoDt_WoCt5OAp@l)2n>r~Gzz5?vX zTO>pqXu~bivsdk%-h6JN5fK;8DbEbFtSWSx28YF{!E;6>0?rmmQ1H`l$NxMCRX91X z02rcRQ{Ts(SJyTOx`Az1*TE?R(nW8H8v}$;0HXa&KZZw+^--$fOIsM=HZ2G?XrqcZ zm<;RQ*HFO6vb#m#1@aT%iA&OsQmI=>@)})fR=BlnhLL74W6^tP6M|bw7pE2%=_@ue zkJSp0x@^YP2dR1>+4$smov1z;hJic@1{8v@knV@R>17r>iFU*u9+>bZUJNvi^U_iz zE}wZv%@;60WnSwoT~N(!9yTM<8J(-VA27Kj0 ze?GP25(3s^KBe)5DQSiYA^bEQ4t|6mor>YX$B|9XmR}1I@Gq$~CrrK>JwF#wu4`#K z?_H@Ehh!NBq_Xr^n2Mhi^z8eeS6PbmrfgirJy$qV^cEH7Q+b!@y5eR*4pwx$dGJ06 z*zWP&r0KOvPpZ6MR`GtmDdb<#RNy+3mv~Q^?Yd_9)~)8VT|D`Mr$9HOdk4-OBQ|&< zGg$qy>vAQteYUx~wwb+tWA=S$jRea7FvwCwvsm-)xUMN7WQrKFnJHBE%DMaaBXGM7 zWNB0zrL9TTjq-Tcn-elAT!*S{&ng3yzmQ||Yxc#K-A;o{D3|D9F?-otsaQ27cKozj z+};}z%>~vxwa17@j|OY~)^|%1rV`#W^mI0SI;RWG@og#BM;r$<*lyi=$T)5nD}c8> z+8^SgH+aboscr{$5X1Xd3B;>0-VMS~1Efal3BBJt+j9$tms{loz}DeS>%#C8 z7~tZKYqIpjm}|$z#q;UoNR2F+>hY~cPc##*7IJnQl+%k=p31h4!&I*PZ@t*k6Q65` zdJA>UQi3%FyR%kvn{VxG5*7ZCslKbCaT>nE{EY`k!(5y$1()R*Y3{;mt>pruTYQIG zP(0ApEEDu$9dVyCy_P>GrBmR`*&gwv+*PThR471~m}+bgwrRkuVtkbth(SVuJFz zhk#L1C=%In1nSK5E$EyuVs1ey7IY}=QsWCT;va96pRXyTpGHzp^ObxyFNU{R0E=5& zVBvf_lKB#e%){D;9$P|}Mqrp$g4-9M71BpAsbJH_&>WPRm~gw@a-_u8M*zC5>wks6 z)@IadNW+1@CTS>od0HPju38o+ww->YS8h^=nE9EMPwa+~LW>XhB|lp#l?yTUuBR5{ zhZ11=hpHgf8g{i7=R@l2Ov3A=k0ls_h9zH0u;M<(f^=l@_kq>Z4sp;+Bv^+$X&rKx zWZU%E>*{%NT-anKu~SB{=-25NeY>VSG#I?sYVzK&=;%clX$LPK;-D;+S2YPNi3AUN z0Vj4I@{9bYc6-acuBUn}MTmxe&7% zwsn5Br+-oPNO|p__r?Fx5!kKZhx9c8D-4)k&A?8TH%Q6FLKuH0E*5uqRzq%%=(Yak- z@{l)73G21qeWeqe>4IGNqXK_m5iZrAXIQWo3^=sLpLI%3Yl%abfo+`&FlG1C0I25C zT9t8($!F7e9PbMy6F~O>8{WgQ;?}X(($MkNmYXI+PsddrayI~jC>Nb6aV-$5*=)!g zF=2-LxaDUa2~q%-tP5E-8@~3W8NOd09hOtV>iw_^-pXVfd36ZvLdQ>Kb>iC9;u{lq z!fJ9zG$M=%7I(>H89d||Ff2yEH4C5B?5ZaOW0v#h4*N8}vr0OPa$-qeD7F6CETqt9 z{f6f~U;(g&N91C@zbkz5aVV$Hvoqc}o_;BIOWs_l0qsDT^`nkV5b;ql4k#-TDe7wg z0?R*{d@QuP37`*=S=Sm1I!1VT^lMKNh|M#+tlQY_FuLU~2tSkwPo&hMy3!l?a*_4} zBcB^CD|EQ=Aybba{8oPhP@GsWzi2VP=uCh5FNG5$I~td^Ax+9hmiZUq?9Iq{@|$1! z!~Fk&xuzG|Nje`7Se^XXkWq4^Sojw9!<@6`q#?dr-+5M~cSN{aY9jh%uwr_|7B+Va zy4=FZf8fN*CosRYblV}3R=y*BowkV5%zG&%1V@%ot3ij8la9jj?>c$6cntGjnEP?O z7enpN*vJm~Adj^fAnFFpGqo%=+X6`S#T@J}o}Yzu&sM$j-Bql*LPW zjnP@MSfRCrH~24jk@c9!cD-l<)cOQK9Iw!;uL}~5lm5VD(oC^RB^qiDu>ZaH0|6ZC z7NY()R`~eEKVAR3A$R3~0LU#){BM{F4Typj{8t$2Pf^yTqbuf|fbahlh?>X`b z8!zE!26!rgFQslVU5;OGdKLP&g&k+$3+4yG= z%5$BlE%wmcSX#xKlP_{~Kxl;UZzK+oO^$ZgH-dXD)u!h)5rmJAOp2ri5nz9gH9AK8 z#~KkKrb~-rUi6%M#8cWQKtxA$O1$6K_N2$`uoOV?uLoJ zlp$eLo;6X!XeSRB2#^tUtU&5-HXlHJ*-T#Gt5g1GmlTuv`KU^XKG7MogeH6)ofIg? z*I9n!UxP+(j-MQub$5D>7qVN6yU7PbN(dCFFH!^IEEhmX8|t>l|20! zuRIZ}354j3sR{FN65+twg2H|U=}pN#bLY%bWAQy-s(nIA zJsKhzw(je=Lh1Vydi3LL?`ZM-RM5|7drV1;8h_{GUvamTi+ra-DSPP~1vUt7DFJ;x zfrta-I_cl0I#^SkMVOLLRMIFm#L{I5Yt)vMPu?Z&~;y|7$Y zNKb8w^a@arOkxsVg{qyOS6u@kKtB&T3L{JF&AZvIV-};wKxfYE=C_Sq*QP22=3=z6 zgkIc+Sb%7X_JRix0n*``rF`(ff*Qa0&w`-EAMX35hIO@{(6p-Hw(_b)j0xQnR*a?a2H5spl$;*#gIKs?jW9h@Z;qr#c?L>d?{|BYVu-uvBh&9AbG zMn!}EUMAA@cf5ZcvlA>shzuEQ7z(}u>;AFkBi_>E(Ww31svx{ zdwh-zUbY<+MUHKc50>ef>FTdfp8!z_%Bhy?n4qAEV267>_CS2#V*Pk937c%lb{XQi zG(-h5>Fh;E7u;rmFht5SOa%Wm0k!d)k3h^>kr`C71?)#Nj=)(PPmDqVZ@r>|LobRZ z3kL`!1C*LdEEx4{hB}`+j`S8sqyO|W%q3d66*z_w5GqbHs= z^AT&cz%*x6LO2mci_5o=r}0jUC^|Jj>MhrL@b;T>lh&x*tYBi;*gdD0L&3{r)h-MZ z2MF5RxPRew9JTUNkzWgIy#o73&{@EnXh@((#*ItqOp4JOJb zjfhXL1OgawXch2!{CX_N8w@Lap~9atBM#ZuqGuv-WEpgR*A-}}VLgPYWSr31oHT}h z%HNPXSy>#U9$k~DpRV&Aat@*}!m&yu5f=6YWJwqOWz>EWaw%B=AbBoWscF=g`1$O^uGJYlLC6zdAMPrq1&kX+r_0W zaneS!PfyXYd>q1JjRsg7z>ikJYt6Ifj*v1z={~;deH2!Gj<+<8-`Ux@!*e8zEa}>f4SduxAxvDR}GYj9hpy-k_ z>!<&ggxzLOPrJ+(X97-6v==9;i@&b(ep4J-SeD`E0c>#f=RZ z>W=^qCc;D`uSjj|*b(`tP`il}t{jYf;{DolO0g8d0utinq%LJZyz=E;lG_+mAs=@9 zu!M;mQoA*h6OUm|H(q^8CRqTFCU8V$bpI5yCA?;tPeAOdfI(HiqF{W4J?oKdYw+61 z)Pc_fkd$h<-_d6sQ_(EPIX;ZLM!xVq`3Xmo?UE3Q*u@E(uHsu_&nx{1fWByWvAhB{ z*|>J03ta)f)?NYE4nnRFV_k23Sq_IHV%#vu2klPqq{3EQt*|ToN7bwV@)=s{5Ga9R z74H&@5qyg?6*6enBR`8v&(Amh7WI4%K9k4NdzdI$@Gi7 z&ymJXq8~I@$T!ZJwyD_GYhzlK78+VFxAQA(8;mFY6I~66K^Fg zGqjP{z^}zbzDeU~u;H+-=bB*nRs4QnT%m=9V>J+cL3aZj2bIqm-FMJF(}K9@>Yfkt z2ks>izPt#WD|NV6(z```2TdCwERTga2u*NNCbr_vV7d;I8^!l-h{#Jh<}g&<&lhjg zX!Tj!J>Tl;#zjyply3Zp6=lJHENC?`i2H|>n*Avc*Iid~K9vma%D)uS7Gbj9b`9_q zktXX`m@ucYil%+jdZFc^3Xv>$9Os;$^#;JiBNIFvj#wJ(=p{|PK8oe!RIQg!?e9nX zEGY3zd|rNX3oZK-@%Go>C3BLQr?pEz%${oT-@`z=9r!?uijm?10+BFm+Daj6H1kjY zmnqm^LnJCd4Vjfd+HuPIeu^ny%B;l8g{JcI!RNWDApou0S^yYMhoeXGJddi1%Nm$C z27#k?-xBB|)T-})4;uoYa95vOF7C@QeaL2LmPcw$PBi5<<(ljWm@@{Y&NgL_>^b?k zP<5?-fzhQwNN}zFz=$_a#gVpRrVn&1dQ3G)6M1s+Bt@ zjb`Gbq5Tl9v1ZU`?+)PkT8+WFFej$Z&~txFoZQZ>6MZk{NVCz{Rx|AJyjnFIaK}fC zt4)E-^EMkb%!x!+PC*-TTd^7nkF1)7*2FxKuM4%Y`$RxW-3wL{u+b<2H)Ovc^&R&8S!-7*^%}9GTy#TwSk=!#W2;tK(7U<(8nFh+)U$Td^H3nsPGzJ4 zVjaC|yf2e$OiX-I^`yolzazoNBJXF(s2j)f2g3J?q{3ra4G25*DBd6+M+b%m)gHJ` zrvwdGy`t@GE+EzQa(Bt$d7cWsjy9AWLTffPiT7H`uf%#{vs|#6$)vSkH#wOH+qw?MV>O<(WfWIw^t|ki)>3 z1mx>RSp^3-)XsTr;_V`S3d9FYep=ENXtbtQ4UtIusS-9N2&Hm8=hfe4NYj*T8R z(zF_`@H(-!H6ZHM2hC}0)J^QMS2acp6E?p*$_AL+?m)4dnT;|#a)Vw|JlYl>-jdg` zPufqFtO_Z$Vyg%nM5cGredsG+orUujSbebSByUu-P`4rN;gPaW9-hC;CbkW#pDkZW z>!QJVY(9We$Ad1AtCagk?|Jlb2VLj%+d}W4aoj2?{f|Nej=D1+E}D5V4@OmV92*0q z>aEC1+eXmQ0M^&gf}PuZXs10 z@4%gkg~aN8)HV}pcgm6ZUp(k9X)pyyX~%qAL5J=*t=p+!?P4LsZ2V<1Fyegzq?6HEL(~QwHUd1HD@v;cRt~RAbhyqRUIpkqAA81fKS`zfOboK)7GP*}* z>IktGbCK+AdCKLI?hk;#Xq(n~MvHr7BNmfWrYHPXzApRFNdc}vpX2kbyA6d|87-ho zLen!PvED=<%z~VkY8i38hr{EDw*PXAE7fjTM*4jt>92E^2qy8SM83qgU zC%c~*^p9Y5uMvXuqI<3?G>ZhJ>QQ};h?R-lMl{~m@gkr6jy!ud5MK>C8vbod>vrax zDtC|tS5_tg>t62+7RDCsu{&h zM}qZ(45&@Cp{0k?+0Av8mEqk>J_VFKb3)hR1haKR z9=2{({h?3h?MK;W$G_WdpYR{XRhN!CJZ|W9#Myw$Os)7P6#A@IHddd>dQrzdljg%d z9YesyEiU(;p>eZHJhKM0jE8bfPOl&g!ePi);pp4j*Na*bEao#)Bw$}!~R!E(tWhgA`;I6 zon@CQjh~~51H1D7ujTaopHh%=UIJW8v_J9!{{5As)N>0E{Wt&V8`aV5SOCS!$(Pgk zTLAZD8clQMgDeHxv~`Wv77VUF?5fNmW5j|N61rXunm=-vuf|=IFwslWjB)qn2Loo0 z8>em>(8H$+jWX!n>tiX*)j4xg>oMkqK3NXTHbF20ZqBqhNVTb8xlC{Z1bJ`IV)$d} ze&v>*<#{|%Z#-{IZHm@1Kp{Et-l6V=uvyiYOE7HrjR)swn=WVPv&Ka;7o4Tmb3e2o zUQKW&oW;0yJKRQw>vXv{835K?fBHKlI_*84OpaVGtdzeII&905#T1rfKRAxb8-skR zA29)L)|{=GkJ|;G7KY>-7|!_M`kd8!;2bi~v(CFrUl&=Hvc#RbmRHrx`wcDRl^*bN z7WLvSc2O`Mh=yUGZLe(mb>xavV#RI%X*+5n_5{(7?I~uLi613+7H*033EkT9>_I@Wo`vE!+S}K+84IkpdMLvzcA3yT;CckSsKvdvkFtUIe6nZeA(pD4$YZ$ z5gFi}=we7YO-_KwWHidpSSB*laP8zujj&bF&X>5zE>XWO#(WBWpseh}QL4CNu;gRr zW(hJM`8EzOoSYV?d?byGMaG@+cg{~4@wkWpecF@B?PBF1Z*}SHnWK)wDqacCiNNyG zqiaC7ohT*|OVv31AUZ2L%jK6_W#H`~zs==Rt~94+yOm!qV{X}KP}i^*82=dZwbb@K z$s&B0&DWGc<|9|7okv(~G!mGuRh8d|O)K~#$d+t%&-fe1$1SAPkJ(2o5BrN8f=-pl zKkl8qqs^ZyT!-TkL1)_w$pR8e5Y>&l(oRVVj+@w6GZF)WIa4cSTAAl@Gm&Zn_>Bc0 zc@G6%it+%%pQ05G?h;S*F?TbP;BayDP{}MK#xD7^>pjVH%^edP@3P-M54Riq>eJ8z z*>vdGOC<>%C!~!lKct5nuQDszSt8J$Ajd=Sc<=4;z>-tCbSg%YO_f#EIp83Ur4Frku?TqZl%@QV>Ir5^70T8La%4tgNN{IE`XFNso~BlS_3~>0B7fA^MI&J zqB-0trSaXQzd5?~1cgK0R7PJlUIwWT@5s2&b?|JWWNg#$HO}7pVPlQ-o&rfj6q)}B z*Y%G?Jz0Dj#QH>mkFqZV4-k^N@0#?wd{QW?DR6$^9b>cnY|@?xc(D*!2@Hjn!?R)1b|q#8_DZ{HGB7^bqz+r^|0(`lY?~F!`%7dWrx+k%6CG4e4hZQvRzizlyNWmhFii zcD)ll(?5k_cfzCn5ctd7HvJr8WT2o}jJ zs^wnGTpN!-d0+K;XJLeA%K0f*GyY+xdnY{&&e#f>HCcfCyr#Lqa~c0UJt8lF??t~5 zA?$20(~e*`y3~yLzoi~Or{3V?%!iWvPjYo{lWG4t(c!-e?Y;3WLleTKcn3ne1>+LH5P97*Sff&5OlMg9w@@L+k3Nert$~%4mBc^#8Tc11S?r+0dG*SZ4`#a3=`gzMW>r zX2ST5#)KjMm;n7n2>8xt2^g9j#1t)0jRQ^ND-jx_7FHns|Fe$^R5*_=dw*4j35YXl zDP@3Bm%XO2HnMDdrUIoL!30jx0WFPYu(CCaZpzpj79mlS$Heqd7lAEzna?-`nlGDq zldQI6*s6@VnV##Hd_U6oHui9fe)MuqaFq9SH+0=y?lU%lX4sSIAL zC)%j0aOWCZ-P=;*2+6s4in-BA-4VeuFeR5P-RVMAKnxn3k_(r9970u&2bA*PEAOHL z)xMjf0GtMq2PP%#%Jfa2?!~Kdd`y&JP1qRbKqGN` zWf9(1<+BfN)WP2=ueVb!$~QpRmTksi=+rArfiWc;o%EXxmEFd(pQx;(-VT4pYI$ z+Ec^ywh6YTw$s>ik?F(EZ(r^RbQ0hp=DX)&h1Jl!VM)KKc&&B z?5i&^u&=p4FEvwEciIt(_X%>JU&<(LzCG%3R$QLeE=L)wzvAiot3X!v%< zw7T+|{;9o*7_YNQlt|uSKGuaGKrJv}`{ipUaJLo|2^KiSa~3+(gLV9FG~F(2TZP6` znb6}aO52cVMd>K^tvjz@R+L-{am=VtqJQ+wP-7t|dauTzL3N0A2`|df<&Hr(W?%7n zy|U<g=l{&))>t;%`0f%Nol-s+8erB z=$_unLdAaceEhi_51dcU^Mpxeo6wY#URO+PIDV_O`2n!yJXwl=@lTVql$F*i9Lx?5 z=24iv4TS-U6w1$z+$Acs@(NzV-<0`dQOMI8K3884(;R2UM$W87bfK2!hQ9W3CW zvA_C_k@t@P(KY^Q^&Q6l-D1VE!G6*d7fl9ic#Arj=ZcK7m%vb66UzNgm+(a6JeJskQ4vInWh2#f|YCwU2B(ZE9cW`)LAUC9P^Th+s^ob&nRVI1#wV zUcu*jpljXRvAeHh3S5_JM5D^$hy7D=x1a3l|F=auB^^|~gWk$2poA6>;Yo(?;R@!< z?95_;d#?@?SI586x`7ts^leh_l)kA$CJ`DIq~AVrMnLAqzrC!kG1C~uvV%43n>eM| zl&wGg(LSG)`04(Oe?4%3wcK=h|MkGgwsS0W!R26-y&>_FyaElc<;?-3sGFll%`sfe z*YpZ;O;1%5QEEpyr-%(rim1!5|Lxnk+R>OC+}u3RA^*O;W7lL6T<(u(yhL&J^I=Xr zQ^gaS{{{WjjRhhm_VFn;iyH}#o^EX+FuBI{INk33+S9I6ivy<%o9k`=#CPb?No&~{ zAK#p@mz>9KYIauc2-9tiGHSkTE?HTR+6(gOrb2L59NRaIb9fE2(OWt4(S_6+M*i!) zc;G#acc#p<4-j#Za0^pHQm<1Jn~fJ1j_mZb|M}XUm2c%+@6=2Y6W+2z)9S_Wu3Sudp*hRs)tytreyYF- z9M&h8;;`CL7fAzNOCkAeCXc0iW5}O$XO`Hg$M)07*YgTWOSDC{s}cEwRGtydDM76g zz?%VexSIU(+azA5Tdog{O&!;Nmk%XIPcN3XR{&l_qe`Ty0@?24dz2 z`I1)_OP?qj_WQr5Xqy<(W4tYh5v<~W&>!d$+f-68Utypsil-VlR7~qDfc`iP{OtWu zorChW7Y~BRB*VcMo`C)kevwBZq1D$3^kVrA)#=k5Q_)O7HQ_ug3Ojh9Rq& z^N?mOVqZ~U3E;IPw{7l7Cbe)#qCUs~<~(LGUjHTPpV?2>dSvtCJV{8ul-}&8*RuZOWAAXs|eT z!8v2(jOJi0#*6mPPc>gS4~dY}2=V*B!2ZEzLHoz-{V6n;a4Dei@LbjIlgb_#-6@Sb zp6OPN|EVPw)3JH^*y~c$r|mRs8O`@Ksp222^{3vhaaA8Y!K)W;*Ka#I;F0JDgJVDc zSUg*48zQZ!nZ=AVE4ONhkyttb5&=u>&MguLOvwaz?A2GIilePGM)ClE6nm!e+p|2r zbL|k(^t=DmJAAn^TIK5P-Fd{P%8D|I5+Txj8bfhwp8j;R`Ybvnd{hH5bzbhgrz%X4 zn>Ee9a{rH|{`36g(vjrog@u{ZvRae}vV&3-@P_`XxETi)1s|9qGMpmJeXw#zS__Ln z^TkaLU+zY0EyuZ^^TwX%N8u#gf45{*3f2&|WZM~fNndfD_tE^1#jl~}!D1@-F=?uR zsGP*^KV1}ZR$CbU3>eLl1ad+>3WHC9d-FM_pPCU(eEDqvkN)zocwq<+0KY#5-a0s? z*`|-J2iwy#jS=Ea9xO7Vkqq)OF@HT_DhU_x1IXqmjM!uyoWo=uoms+wZn|-ibX^z2nb#ZQm;_`S&A@>q6;0Y$Fb) zBdI7svg9vBOrc=LqYs|#D{q&;ulFPARYeTc6C=rP{W$&Cu(5Emi1#AOhJu#WHz)1% zK}Sy1k}x$63zX7azqj*oWuYhSB_AH8kzW|=n_G8r@-b6Ek1TKkE2Z((^?|apfy+Lh zP0%plDzum|e@$kZN&GrAieS6+uvQj+d*eGgFpF~!<4;}Ah84(3)wzUyTGcV1L!-l) zz7?@*f6d|uzD#cqt5?S0X$1Kpz?^H!TumU;#aht2M474QiizBmb`>rg$l{a*?A&>X zucZW8Du>N}78OGb`EA=C-a|fUrTjek{fF(>WQE~G&$dg#8)6^Vt)a!;h_DmNGNaS9 z_YVQjzvy6h#a_L#6<1Yv>?H-54^fY>7B}9)vVxd`=}>FN`vj?r`#0`UA;+!4SL^u~ z`;J*xG!C@b)LKdceiy%cZ@5f!k=q~>fH)I$vwEi6=vuK8fPeQP!Lc}hYjwqVfQ)us zv$&*Q#K5rpRV}B1A#$e3jK?iWwC$!_q6p zqc>Z>pPxpM#*MMD?VeF18O6F5rD-CR2UPZEl{K0aG1=2W4z;9(?L+%NRbU&>+zI++ z1pAd%GE*|SGwu(I;vy=pT26G<6Efad>1*YPGg_vMlHhR)5tN!Y1qjufmsc)pJjT24YWdLpQCfBpSXFbFNHv{y3^IxfKVr@ zv~OpWRSOFvd<9I$oFIR^XBhgjhj=CHQNb2VCMO#akBUZ!!a}_OKPT6CfbFc)`!Zou z|Cox+u|6l>KFSqU;>7_jR$DixsrtI->tb)h3-n(1%%Sv<1Em|V z#21q*!{%Fnb?+Hdfj*Ce|A>B~7!HTA|218!&*Pvw1YfN91)rNO4YSHEdHU*W zvhZxN=CqVqLnWl5=(7Dtq3i1UUU8M0y?9=Cw|)#+wsmzPJ22$#?~qWliWI` z{Rf~K6~38XrP(*hC4`ZtMaLjCJ@u3InDuo<_>{k zZem9wlYwXivkoU*4>s|pm%sD!% znO_f`$DbuJCCNO+6$YAmmE>MQkX1VMu(gzEanr<9 z$1Xa+sK31qu>>oUMLp|BQSaS( zMc8D53z$o08Y`@{SVrEiyY>#~*!{GAZY#AyI&7If-j6iC1``+7h2->H1mXh?X?i^E zEeN%qYfqEMpV3aVvluOQfBv2jca&JCmu|gqY8C9&_*o2U;qZ=sQmLuzjW1UrEDgE@ zmG(DbnI4jv)F=x1{&h-?mCKiAe}4A|m6lQ_2kM=(F7nz7S%t-l_<*vRF z&u}`X-YYj=hn7nE4pm(+=j(^n2~=-LG!OrumrFiVk96OgBXP;ZX3%-=9ULNufvE-Gt5+6R2P)~(s^vu5 ztSvrHf#O|uI?vBH1|-t5{Dc@RUleSA(`Gi+0BBB9Ch1;rxO~ z#feRwU?(+c{@xe6_9pvOzS_M0eX_ES>E%)x_u$gw(Ji&e3g}43mS7~y{=P0v5B5E@ zh^dwi9WPBm@2~1jIi~u>=Kl8BU3F6@Iz9D2-|)6fFZaJ;u7bcGB`&B`c~-Tbj7FfE zicKLdI>731|35V+brmt@{`a`2L@EybNY%q1) zXu}Vb_oDFn$;2Z#P@BG1R@x=|Jw5q+_>;`deL( zNCGouf3vcwJ>94qo&Ngwy2d;t`5}W^eZEiW%y;SCaDAG)_tKs9N_(owQJ7WX>OY~V zp$LDNsZd+<{+)ZeE`=Zy>!M$Rz7!g7b5)OmeL-h<=3}Su+$@lvtuP zvr@So$1DmIJ2vIkPemo53UpTf32Zi|G%UOVT)31=k2*C{$>&tEd<`UXSARw_rh zDR%!^ga5M+gV$n=vM}=hNrZ&=-w~4k)tT3v0{`{Q>pdd_$5~!{e?a^gAB5cVEQ>{WN7T}swoj35=G!26NBM>KI^tI({r+#0sjYuUuQto?~x z6Xo2h)|RrCavbC27|H8E>2v!r0jAL6hOX}OBbDwewWjcO;}TVp>t8(SMvBPaDB6;xcqvf&{; z*+TOcxr3eVaY@wpOAph-`9a8gXXonPTBqMrLuS8=LtMRB=Gr_8?+<~Vh!}t{?`Sl- zFHP|~**z}A6mRfyDwy9h{zUm*Ke^VK@o@**&^!I~wV`Y#aaXNWNAaHUKLzQ!_Q-hku`UhO5 zwHI=)oo1#Ue4RIf^MpeETBx(4OFMA|>0?04b5p2yt6(+@=O_%}50YVA1)D`SAhkeA zF=+6rgCR*o=V9>ivt(%}Hmc_v(NW{4xvQ1L1y3MaY|(}>JGwRRzOp2HFcKaA-nJPE zvE4>vv~evP0|;RoPTZ>`oJl$AXYo5~fxE9T1F-|069n2{9*ZcOeKi~kF#34M0y_~i zt}F}DV9oZeqWbh%zw)S9Un~xq41Qsp>?;O0$m8NL?()~{c|`rstk?+A#y=0Ox~Js* zSjMUFeZljgb>q82MbmtJcY%OOlP9`MDFr&8id9AA+x%A5){&`Cn>IaJqm`gOIvyN~=SeXk}$nD#Cieqrv5$9}=%ErFRP zz!|`ufvwI#s^9CMcD^G2;v6;q4lBQ`JS!-*gAxNv>culaEuQ{-I^rjwp7)f41BWXC zEcoq3811(m67k$liFsLchVcU@G#4xg(W84PrDwayiaXHQJ^@Sx641pJ!+js7hl4T^ zuUCpF;TOJd^O_K6$3-T&hnG!&m4-m8F_T?4Op1p#_O^q5u9Z|B5V0 zz%sLckyqZH%4=bDdX zAB+Zanm`%jseHeE48N;JG~;rYJXkYd{C!C1)1U=aOH=#^A2Bv}Zn+nU%Gc=>#B{wp zmBagNLmD|!0V7hx602J5JkldHEtzuLPWFefFtKfK7H5$r%G#39_(!$s6cO6#qR<5L z&g6`+NsRCS&vSg?IEFjegt?Nd!kD!FPwDaRldDipg~}%TF;<3$S$+<1?KwTjzJH+j z$faMzCB=vIRcnjj>u(uhY?CTO^a)HL#1RnLW}RYLY;Ej$8z~q&VOcm0ryt@#5YUXqxo$=pYy*fCl~^rB%fhJ<8o-y;-~zkFomJj&eZ$zn=;uk`G@yfZDzm7 zI*pLEjXbjT5PLCr_gD^d1&Rx2K@)5>AX)XevkQ4~^=m7L-LgWrMPQSvY?D=*eWStd z$h>_~XHoat@OZ;SzDSp{rjF5)m~EBBR!wC=LU94-9-j25Hu`Qcs`;jhp{g(dK_kUZ zXI))%_vPpZNAM!`Q*&F)GI3KK*|=>IF6|HU@kY2G6G!>g?{|k%+>bJpyZ`9s?t|Y- z{58pT_vP0O`^0Uwo4^c($kB42i5DE@^)ZPC9C!OWX8RhS9n_hL1d`s!@JSxLP#8~p z`q(7(7Xww(9V#~44rpcno|r#QW@I@7)jKsQ=(g#;onYWhNL5>aBKa51*V?B$h zeE3G;t#^3v4m*W>pzxr1rcJJ*m%B>py@$en{9lhH9uvmj>aMnLlid@i)$U@i?$| z=cCs54g6|+ZMv5>F(vb8`uEuv&A>pCkFcil61V8fJ0^1RBBd^cy*xNO@yw$a1H>ox z4PzR3hs;f}uR^Hg;iNv3QqZ4c_q(aC-b!e-#Z*v$j++;N?^Yyy3$XxEkgqu9e zLvhck6-7Tj$b(&VHh-C-&5ep8(bvrD$o6Ez@a-2A+6B<&e&tu*bLo=xNI0Ze_WvO6 z9iuCW+CS0QPC9ndv2EMv*tTu6V_O|{$LQF$t&VLwx89jqYv#Z1+`Hy}x@WD_hdOnt zYVZ9#KU8+@=eJDni!iQRxCTiea|e8$e)=!%c=2Bm`1E%>1fWk7vF6SXVI!!~cix2B zF>5JiUiz{|rX7Z|M8Uxye0KTOAl#K9N?2p6PxKG5X%&CV9Eu+57C5*dM06n&Evmv0 z^Ew}jk>WKh#ao?&`w;p1Wg3{lY$K=NPO1OEZ7mGuM&}UoS%*&Ve?=!1)}_3$5*Ph> zg=^?ebx5Ex@RN#>1g^Yj)6>S>r4-)LuVpOajWU=o9@{}0 zR;7SnW?xV`MXvT;%y&<1M>3ELOy7Nbu_D=Yz2lGAa&xKyoC(aFY-<^dShRJ*Da$-$ zUEr@!7+tX)zu=!2Tizp+Pcku~UGd=sXIP#SOkePA%2|;8X&P@^RWzMgqtSDV|GbfXw1bl)3}ZW! zQ?pptXf)aXxJ1OOV|WU<2_bzh* z7EqG7dd*bW^Rx8H+B;X<67hdu5_Z~R5mF^%8%vRNj2j<~SlqYx+1mK+p|Hl^QZSZA zuO|Xkt%;+wdhOyQI{q(NKBsQ3@Hh92;9v{Jz~wTllSlBURtKC{zvKK9_aDoDN0I&^ z4|^{$O;~Q=oOx$B=_qVYxZ_o}&y6dt-L_O$O|2sasOaeJmuxkz+pqt_`-`_z`cE6z z1GCB*V#I@4n_bkR1SC#<0t%o1)X(4ny+SH`5xKtB;n@ zXd1=1pZzK8GEMu{)=rlZ$(vvXW0>D|ello_?MJ)01s^uz>$vq<-*@6+;I2EVQEn}^ zg1iywi5&O-Z`3*0|5WGyeg2ZLy}iT#T4_Me%H9Sj3_#2VOi6NZHU&!7=r94lVbX(R zQ~_!d5bLlqF*9&8v2k+}vobR?aBwoQaT2q!aWHUmv2n8#v$C)-urLApfa8UUfx0*U z<0>YuRu1++NdjUy@?s4sDr(!)qf5%vXyYN|8Ey@A+s`P zVBykaU=h^<{8EmHEyrvX49bX2&Ce` z4Q8*5Wcz#1}rfiQ03#&w}Myz?%m4lqAH)Mo&VEu<{QL5ODZmF|3J&|-Meqq0aHu}en!;3~USuh~ z>FRJ5VNg@L+vfe{zD+O9EUcb>`?5(e?MQgvMVq zb?BH<#K0Lo%PBGB>>HR^c0urzM^XHXL(IeVUa(KAaO7mM!2TXaJ)(>v%%PP(AMKge zYba%nCn-bss`?mp`l0knq)3DbxmTw@{;9!{K%!gpX5gv@fFwr6C{m)vAj3P`)S>2> z{UU22(i{u}^R-RL?_^Grap-|rpu{7y+7Cp%hFS^$lXE{xboK|ApvefAi(zX3m%ws@ zM7ZoBAzOg*jY(O3#;y&kV;sTV(-<9C-+I>U3=>@1zZ+hI4T#(2I}x-X2#J;nuwAyisB8sl59E^|Kw^bkw8n>-@#UJfB&vHW%);g3I2-cZ7-(*Ym!j= z$o;W+Rrx0~u2oqrKvpd*jDrNDJ21Zvp$PkC2_hEj8J#a70Pf*M>&=!4K0MRcD4!MH z`5%hy6x{%I^hB^&?=njF zcEpAIR_o}fJ8VJnuw0UUxV(xeqP4Ix>$fF?1BZ{rw5skoKhGw|$ClwG1H0ANisAph zb8KTISmjZVo;O$|FB6KWn|sTWO5fMC=OIw==zL<5&<2k3^dl=;VZmh-S7G6;3#;Ia z-LTf{!+fBMVE6k>jc?4m5HIy}*lKyjIYK_e?yK^U25d5;RYTjw6K9uy`^{wMsbMMu z(s}0U)*nHF1Doees?UM7f%@UU1=ULv_9>VhI$ zPD6-_JqNqDP=9w72c3Bj9W&;58ST$!D99^mnxQyDul|L33-U##H~ah9aEF5UCR<29 zfw9`~aaOu9oBO3pdrh(t#Beo-n{VE;8kJh&LdEwzSjv#!atr=qgxkTIQz~H(Dtg%!4HM?lIM1nS0~Z6oi;o5~6QRLh#aO-antri=ecEyFH%C-Js9Rm9&ej_<%Vt?E8##3qNoQH0<8-wwPw5pH z94~|TG0rqYXE`EHAuGr*QlS&R!vGsJGiL?%4TL`m^5TxxG7>Y`n;34^RFcWk-sf-D zxlb(v%~L@t78;DPdD)t~7jCg?q)4uQ`!_Lptf5J2oZuxRF`jX6TUIg7+!7Ed<4d)Y z9Fe|UQ1SzeIl$HxYR&M(1}wn^Umh?G~?bAW^`7`9RvP zA{G`wS#alQ(qq=qcM9qu&Fd;DZXXjvKvCM4@~1fHmq7WAmIY;ngj+9*p9&@}3da?O z0Bv`>$&F|r1x7+btEm|zZgN8RN{=(=b(ZJy zPVWh+3d7@esM30bpXik}NN>SfuZDBjNq!HIhR21$Jt8%3VXjby3R?AeQQmOMtr3ce zAMW1Snp@huG!_m+=PvDDr@3NAhTbReD?|~tjkmZxI>+;?^``AG)X{jHs0dPN_8$iE zsZY)96Ql&h(+0Og%toFzh6o=s-k7~COk_%9C!n%Yt-3L^JQP^vf*nj0>zj7|xj-Eu z1D9;q!XNoZW*@LdW{cmpllG7^3R854zdqAr4K7x-eUzouue(km@+e`64%W2Vd;Pp-b^OE*@~6Z^ySZvidhvc? zOJ=so2{Ad016sX;SqDi#`(I4jjz!Iy`h~nyQ96jkXtlidRW?V)%xaT-Xif=}cJEvV znjI}2*Tk<=rJRMh@S3)d>j%SP2f(rs*_?>B`p=_uX%DC1^fE^;>T(ZJvWBZq{^j`f zH;jW)=T#g(X4j1h%NgBPw8B{A4OLGhdq-gIPoaTjLz-lkdkRvOz7(Y-pVgYLZ1)K7 zFP^1v5`zA2q9OiNq?I#k1k^RdL}!*}7NKCIE2GqzKdmf_*m#yMb1$%4W8v`~zqE@{ z8vBT~VyR*6KynlwY35HeBcVZ80 zGQ8-boMKI)Vq0R&e0sbqArl2m?4n%p&lZJl>YyaHV%XVlYnM-IA_54z2>R}%ivIi! zgj^Cpa6qe?8&XzZV^adTmjK>IOvjw&49!}RMx96WgEp-l+eD^#5w4{Uyql~U*6FS< z8U1iIne`jAIRRbSkGOM9EE}(?$yGK0{dCsG?k&5kpSJ_?&=f|iORYT`vXzn!qaf>O%qJMV-6~MLA11s92-6#Q zQswfVZsA{tL4DV3g=ouuOL(V0!XHlmo*{+1%c?;59TsQi2A)jUfK69^ zbt3UQLQ;f1hCh6HF4O^LWobO`L|MM@tu-Lkb^~h}bu9BC4=)WgMajI8%K&+ALLr?& zx6h@Dam|p70_(KOAFOlnX=#0<=r3faUNLIzCtao?Uq=QC;VJJ`L2K;kiSguLw5k>< zMfSOQo#0v(XP_J^Mce_a^7${?#luHhitQzP`|5?q(5+aE z;^cWBWZ#CIso6w5wD$W;@M*s{|*vkmepXlWXi(b?RRTF�( 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 AUH||9 literal 0 HcmV?d00001 diff --git a/docs/experiment/ppl/cmd/head.rst b/docs/experiment/ppl/cmd/head.rst new file mode 100644 index 0000000000..d88dcc1800 --- /dev/null +++ b/docs/experiment/ppl/cmd/head.rst @@ -0,0 +1,102 @@ +============= +head +============= + +.. rubric:: Table of contents + +.. contents:: + :local: + :depth: 2 + + +Description +============ +| The ``head`` command returns the first N number of specified results in search order. + + +Syntax +============ +head [keeplast = (true | false)] [while "("")"] [N] + +* keeplast: optional. use in conjunction with the while argument to determine whether the last result in the result set is retained. The last result returned is the result that caused the to evaluate to false or NULL. Set keeplast=true to retain the last result in the result set. Set keeplast=false to discard the last result. **Default:** true +* while: optional. expression that evaluates to either true or false. statistical functions can not be used in the expression. **Default:** false +* N: optional. number of results to return. **Default:** 10 + +Example 1: Get first 10 results +=========================================== + +The example show first 10 results from accounts index. + +PPL query:: + + od> source=accounts | fields firstname, age | head; + fetched rows / total rows = 10/10 + +---------------+-----------+ + | firstname | age | + |---------------+-----------| + | Amber | 32 | + | Hattie | 36 | + | Nanette | 28 | + | Dale | 33 | + | Elinor | 36 | + | Virginia | 39 | + | Dillard | 34 | + | Mcgee | 39 | + | Aurelia | 37 | + | Fulton | 23 | + +---------------+-----------+ + +Example 2: Get first N results +=========================================== + +The example show first N results from accounts index. + +PPL query:: + + od> source=accounts | fields firstname, age | head 3; + fetched rows / total rows = 3/3 + +---------------+-----------+ + | firstname | age | + |---------------+-----------| + | Amber | 32 | + | Hattie | 36 | + | Nanette | 28 | + +---------------+-----------+ + +Example 3: Get first N results with while condition +=========================================================== + +The example show first N results from accounts index with while condition. + +PPL query:: + + od> source=accounts | fields firstname, age | sort age | head while(age < 21) 7; + fetched rows / total rows = 4/4 + +---------------+-----------+ + | firstname | age | + |---------------+-----------| + | Claudia | 20 | + | Copeland | 20 | + | Cornelia | 20 | + | Schultz | 20 | + | Simpson | 21 | + +---------------+-----------+ + +Example 4: Get first N results with while condition and last result which failed condition +========================================================================================== + +The example show first N results with while condition and last result which failed condition. + +PPL query:: + + od> source=accounts | fields firstname, age | sort age | head keeplast=false while(age < 21) 7; + fetched rows / total rows = 4/4 + +---------------+-----------+ + | firstname | age | + |---------------+-----------| + | Claudia | 20 | + | Copeland | 20 | + | Cornelia | 20 | + | Schultz | 20 | + +---------------+-----------+ + diff --git a/docs/experiment/ppl/cmd/stats.rst b/docs/experiment/ppl/cmd/stats.rst index ed4f45aada..3bdf67c818 100644 --- a/docs/experiment/ppl/cmd/stats.rst +++ b/docs/experiment/ppl/cmd/stats.rst @@ -13,6 +13,18 @@ Description ============ | Using ``stats`` command to calculate the aggregation from search result. +The following table catalogs the aggregation functions and also indicates how each one handles NULL/MISSING values is handled: + ++----------+-------------+-------------+ +| Function | NULL | MISSING | ++----------+-------------+-------------+ +| COUNT | Not counted | Not counted | ++----------+-------------+-------------+ +| SUM | Ignore | Ignore | ++----------+-------------+-------------+ +| AVG | Ignore | Ignore | ++----------+-------------+-------------+ + Syntax ============ diff --git a/docs/experiment/ppl/index.rst b/docs/experiment/ppl/index.rst index fb2964ccd7..0602c86bde 100644 --- a/docs/experiment/ppl/index.rst +++ b/docs/experiment/ppl/index.rst @@ -43,6 +43,8 @@ OpenDistro PPL Reference Manual - `where command `_ + - `head command `_ + - `rare command `_ - `top command `_ diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index fbb261464e..c6b1004dc4 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -1,4 +1,3 @@ - ============= SQL Functions ============= @@ -7,7 +6,7 @@ SQL Functions .. contents:: :local: - :depth: 1 + :depth: 2 Introduction ============ @@ -16,22 +15,38 @@ There is support for a wide variety of SQL functions. We are intend to generate Most of the specifications can be self explained just as a regular function with data type as argument. The only notation that needs elaboration is generic type ``T`` which binds to an actual type and can be used as return type. For example, ``ABS(NUMBER T) -> T`` means function ``ABS`` accepts an numerical argument of type ``T`` which could be any sub-type of ``NUMBER`` type and returns the actual type of ``T`` as return type. The actual type binds to generic type at runtime dynamically. + +Type Conversion +=============== + +CAST +---- + +Description +>>>>>>>>>>> + +Specification is undefined and type check is skipped for now + + +Mathematical Functions +====================== + ABS -=== +--- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. ABS(NUMBER T) -> T ACOS -==== +---- Description ------------ +>>>>>>>>>>> Usage: acos(x) calculate the arc cosine of x. Returns NULL if x is not in the range -1 to 1. @@ -51,32 +66,21 @@ Example:: ADD -=== +--- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. ADD(NUMBER T, NUMBER) -> T -ASCII -===== - -Description ------------ - -Specifications: - -1. ASCII(STRING T) -> INTEGER - - ASIN -==== +---- Description ------------ +>>>>>>>>>>> Usage: asin(x) calculate the arc sine of x. Returns NULL if x is not in the range -1 to 1. @@ -96,10 +100,10 @@ Example:: ATAN -==== +---- Description ------------ +>>>>>>>>>>> Usage: atan(x) calculates the arc tangent of x. atan(y, x) calculates the arc tangent of y / x, except that the signs of both arguments are used to determine the quadrant of the result. @@ -119,10 +123,10 @@ Example:: ATAN2 -===== +----- Description ------------ +>>>>>>>>>>> Usage: atan2(y, x) calculates the arc tangent of y / x, except that the signs of both arguments are used to determine the quadrant of the result. @@ -141,85 +145,33 @@ Example:: +--------------------+ -CAST -==== - -Description ------------ - -Specification is undefined and type check is skipped for now - CBRT -==== +---- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. CBRT(NUMBER T) -> T CEIL -==== +---- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. CEIL(NUMBER T) -> T -CONCAT -====== - -Description ------------ - -Usage: CONCAT(str1, str2) returns str1 and str strings concatenated together. - -Argument type: STRING, STRING - -Return Type: STRING - -Example:: - - od> SELECT CONCAT('hello', 'world') - fetched rows / total rows = 1/1 - +----------------------------+ - | CONCAT('hello', 'world') | - |----------------------------| - | helloworld | - +----------------------------+ - -CONCAT_WS -========= - -Description ------------ - -Usage: CONCAT_WS(sep, str1, str2) returns str1 concatenated with str2 using sep as a separator between them. - -Argument type: STRING, STRING, STRING - -Return Type: STRING - -Example:: - - od> SELECT CONCAT_WS(',', 'hello', 'world') - fetched rows / total rows = 1/1 - +------------------------------------+ - | CONCAT_WS(',', 'hello', 'world') | - |------------------------------------| - | hello,world | - +------------------------------------+ - CONV -==== +---- Description ------------ +>>>>>>>>>>> Usage: CONV(x, a, b) converts the number x from a base to b base. @@ -238,10 +190,10 @@ Example:: +----------------------+----------------------+-------------------+---------------------+ COS -=== +--- Description ------------ +>>>>>>>>>>> Usage: cos(x) calculate the cosine of x, where x is given in radians. @@ -261,21 +213,21 @@ Example:: COSH -==== +---- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. COSH(NUMBER T) -> DOUBLE COT -=== +--- Description ------------ +>>>>>>>>>>> Usage: cot(x) calculate the cotangent of x. Returns out-of-range error if x equals to 0. @@ -295,10 +247,10 @@ Example:: CRC32 -===== +----- Description ------------ +>>>>>>>>>>> Usage: Calculates a cyclic redundancy check value and returns a 32-bit unsigned value. @@ -317,56 +269,11 @@ Example:: +------------------+ -CURDATE -======= - -Description ------------ - -Specifications: - -1. CURDATE() -> DATE - - -DATE -==== - -Description ------------ - -Specifications: - -1. DATE(DATE) -> DATE - - -DATE_FORMAT -=========== - -Description ------------ - -Specifications: - -1. DATE_FORMAT(DATE, STRING) -> STRING -2. DATE_FORMAT(DATE, STRING, STRING) -> STRING - - -DAYOFMONTH -========== - -Description ------------ - -Specifications: - -1. DAYOFMONTH(DATE) -> INTEGER - - DEGREES -======= +------- Description ------------ +>>>>>>>>>>> Usage: degrees(x) converts x from radians to degrees. @@ -386,21 +293,21 @@ Example:: DIVIDE -====== +------ Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. DIVIDE(NUMBER T, NUMBER) -> T E -= +- Description ------------ +>>>>>>>>>>> Usage: E() returns the Euler's number @@ -418,38 +325,39 @@ Example:: EXP -=== +--- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. EXP(NUMBER T) -> T EXPM1 -===== +----- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. EXPM1(NUMBER T) -> T FLOOR -===== +----- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. FLOOR(NUMBER T) -> T +<<<<<<< HEAD IF == @@ -494,129 +402,54 @@ Specifications: 1. LEFT(STRING T, INTEGER) -> T -LENGTH -====== - -Description ------------ - -Usage: length(str) returns length of string measured in bytes. - -Argument type: STRING - -Return Type: INTEGER - -Example:: - - od> SELECT LENGTH('helloworld') - fetched rows / total rows = 1/1 - +------------------------+ - | LENGTH('helloworld') | - |------------------------| - | 10 | - +------------------------+ +======= +>>>>>>> 485208e069e8e916f39991cf9a79903dc5d0fa78 LN -== +-- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. LN(NUMBER T) -> DOUBLE -LOCATE -====== - -Description ------------ - -Specifications: - -1. LOCATE(STRING, STRING, INTEGER) -> INTEGER -2. LOCATE(STRING, STRING) -> INTEGER - - LOG -=== +--- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. LOG(NUMBER T) -> DOUBLE 2. LOG(NUMBER T, NUMBER) -> DOUBLE LOG2 -==== +---- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. LOG2(NUMBER T) -> DOUBLE LOG10 -===== +----- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. LOG10(NUMBER T) -> DOUBLE -LOWER -===== - -Description ------------ - -Usage: lower(string) converts the string to lowercase. - -Argument type: STRING - -Return Type: STRING - -Example:: - - od> SELECT LOWER('helloworld'), LOWER('HELLOWORLD') - fetched rows / total rows = 1/1 - +-----------------------+-----------------------+ - | LOWER('helloworld') | LOWER('HELLOWORLD') | - |-----------------------+-----------------------| - | helloworld | helloworld | - +-----------------------+-----------------------+ - -LTRIM -===== - -Description ------------ - -Usage: ltrim(str) trims leading space characters from the string. - -Argument type: STRING - -Return Type: STRING - -Example:: - - od> SELECT LTRIM(' hello'), LTRIM('hello ') - fetched rows / total rows = 1/1 - +---------------------+---------------------+ - | LTRIM(' hello') | LTRIM('hello ') | - |---------------------+---------------------| - | hello | hello | - +---------------------+---------------------+ - MAKETIME ======== @@ -628,11 +461,13 @@ Specifications: 1. MAKETIME(INTEGER, INTEGER, INTEGER) -> DATE -MOD ======= +>>>>>>> 485208e069e8e916f39991cf9a79903dc5d0fa78 +MOD +--- Description ------------ +>>>>>>>>>>> Usage: MOD(n, m) calculates the remainder of the number n divided by m. @@ -651,55 +486,22 @@ Example:: +-------------+---------------+ -MONTH -===== +MULTIPLY +-------- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: -1. MONTH(DATE) -> INTEGER - - -MONTHNAME -========= - -Description ------------ - -Specifications: - -1. MONTHNAME(DATE) -> STRING - - -MULTIPLY -======== - -Description ------------ - -Specifications: - -1. MULTIPLY(NUMBER T, NUMBER) -> NUMBER - - -NOW -=== - -Description ------------ - -Specifications: - -1. NOW() -> DATE +1. MULTIPLY(NUMBER T, NUMBER) -> NUMBER PI -== +-- Description ------------ +>>>>>>>>>>> Usage: PI() returns the constant pi @@ -717,10 +519,10 @@ Example:: POW -=== +--- Description ------------ +>>>>>>>>>>> Usage: POW(x, y) calculates the value of x raised to the power of y. Bad inputs return NULL result. @@ -728,6 +530,8 @@ Argument type: INTEGER/LONG/FLOAT/DOUBLE Return type: DOUBLE +Synonyms: `POWER`_ + Example:: od> SELECT POW(3, 2), POW(-3, 2), POW(3, -2) @@ -740,10 +544,10 @@ Example:: POWER -===== +----- Description ------------ +>>>>>>>>>>> Usage: POWER(x, y) calculates the value of x raised to the power of y. Bad inputs return NULL result. @@ -751,6 +555,8 @@ Argument type: INTEGER/LONG/FLOAT/DOUBLE Return type: DOUBLE +Synonyms: `POW`_ + Example:: od> SELECT POWER(3, 2), POWER(-3, 2), POWER(3, -2) @@ -763,10 +569,10 @@ Example:: RADIANS -======= +------- Description ------------ +>>>>>>>>>>> Usage: radians(x) converts x from degrees to radians. @@ -786,10 +592,10 @@ Example:: RAND -==== +---- Description ------------ +>>>>>>>>>>> Usage: RAND()/RAND(N) returns a random floating-point value in the range 0 <= value < 1.0. If integer N is specified, the seed is initialized prior to execution. One implication of this behavior is with identical argument N, rand(N) returns the same value each time, and thus produces a repeatable sequence of column values. @@ -808,44 +614,22 @@ Example:: +------------+ -REPLACE -======= - -Description ------------ - -Specifications: - -1. REPLACE(STRING T, STRING, STRING) -> T - - -RIGHT -===== - -Description ------------ - -Specifications: - -1. RIGHT(STRING T, INTEGER) -> T - - RINT -==== +---- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. RINT(NUMBER T) -> T ROUND -===== +----- Description ------------ +>>>>>>>>>>> Usage: ROUND(x, d) rounds the argument x to d decimal places, d defaults to 0 if not specified @@ -867,33 +651,11 @@ Example:: +----------------+-------------------+--------------------+----------------+ -RTRIM -===== - -Description ------------ - -Usage: rtrim(str) trims trailing space characters from the string. - -Argument type: STRING - -Return Type: STRING - -Example:: - - od> SELECT RTRIM(' hello'), RTRIM('hello ') - fetched rows / total rows = 1/1 - +---------------------+---------------------+ - | RTRIM(' hello') | RTRIM('hello ') | - |---------------------+---------------------| - | hello | hello | - +---------------------+---------------------+ - SIGN -==== +---- Description ------------ +>>>>>>>>>>> Usage: Returns the sign of the argument as -1, 0, or 1, depending on whether the number is negative, zero, or positive @@ -913,21 +675,21 @@ Example:: SIGNUM -====== +------ Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. SIGNUM(NUMBER T) -> T SIN -=== +--- Description ------------ +>>>>>>>>>>> Usage: sin(x) calculate the sine of x, where x is given in radians. @@ -947,21 +709,21 @@ Example:: SINH -==== +---- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. SINH(NUMBER T) -> DOUBLE SQRT -==== +---- Description ------------ +>>>>>>>>>>> Usage: Calculates the square root of a non-negative number @@ -984,10 +746,10 @@ Example:: STRCMP -========= +------ Description ------------ +>>>>>>>>>>> Usage: strcmp(str1, str2) returns 0 if strings are same, -1 if first arg < second arg according to current sort order, and 1 otherwise. @@ -1005,46 +767,24 @@ Example:: | -1 | 0 | +----------------------------+----------------------------+ -SUBSTRING -========= - -Description ------------ -Usage: substring(str, start) or substring(str, start, length) returns substring using start and length. With no length, entire string from start is returned. - -Argument type: STRING, INTEGER, INTEGER - -Return Type: STRING - -Synonyms: SUBSTR - -Example:: - - od> SELECT SUBSTRING('helloworld', 5), SUBSTRING('helloworld', 5, 3) - fetched rows / total rows = 1/1 - +------------------------------+---------------------------------+ - | SUBSTRING('helloworld', 5) | SUBSTRING('helloworld', 5, 3) | - |------------------------------+---------------------------------| - | oworld | owo | - +------------------------------+---------------------------------+ SUBTRACT -======== +-------- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: 1. SUBTRACT(NUMBER T, NUMBER) -> T TAN -=== +--- Description ------------ +>>>>>>>>>>> Usage: tan(x) calculate the tangent of x, where x is given in radians. @@ -1063,24 +803,343 @@ Example:: +----------+ -TIMESTAMP -========= +TRUNCATE +-------- + +Description +>>>>>>>>>>> + +Usage: TRUNCATE(x, d) returns the number x, truncated to d decimal place + +Argument type: INTEGER/LONG/FLOAT/DOUBLE + +Return type map: + +INTEGER/LONG -> LONG +FLOAT/DOUBLE -> DOUBLE + +Example:: + + fetched rows / total rows = 1/1 + +----------------------+-----------------------+-------------------+ + | TRUNCATE(56.78, 1) | TRUNCATE(56.78, -1) | TRUNCATE(56, 1) | + |----------------------+-----------------------+-------------------| + | 56.7 | 50 | 56 | + +----------------------+-----------------------+-------------------+ + + +Date and Time Functions +======================= + +CURDATE +------- Description +>>>>>>>>>>> + +Specifications: + +1. CURDATE() -> DATE + +DATE +---- + +Description +>>>>>>>>>>> + +Usage: date(expr) constructs a date type with the input string expr as a date. If the argument is of date/datetime/timestamp, it extracts the date value part from the expression. + +Argument type: STRING/DATE/DATETIME/TIMESTAMP + +Return type: DATE + +Example:: + + >od SELECT DATE('2020-08-26'), DATE(TIMESTAMP('2020-08-26 13:49:00')) + fetched rows / total rows = 1/1 + +----------------------+------------------------------------------+ + | DATE('2020-08-26') | DATE(TIMESTAMP('2020-08-26 13:49:00')) | + |----------------------+------------------------------------------| + | DATE '2020-08-26' | DATE '2020-08-26' | + +----------------------+------------------------------------------+ + + +ADDDATE +------- + +Description +>>>>>>>>>>> + +Usage: adddate(date, INTERVAL expr unit) adds the time interval of second argument to date; adddate(date, days) adds the second argument as integer number of days to date. + +Argument type: DATE/DATETIME/TIMESTAMP, INTERVAL/LONG + +Return type map: + +(DATE/DATETIME/TIMESTAMP, INTERVAL) -> DATETIME +(DATE, LONG) -> DATE +(DATETIME/TIMESTAMP, LONG) -> DATETIME + +Synonyms: `DATE_ADD`_ + +Example:: + + >od SELECT ADDDATE(DATE('2020-08-26'), INTERVAL 1 HOUR), ADDDATE(DATE('2020-08-26'), 1) + fetched rows / total rows = 1/1 + +------------------------------------------------+----------------------------------+ + | ADDDATE(DATE('2020-08-26'), INTERVAL 1 HOUR) | ADDDATE(DATE('2020-08-26'), 1) | + |------------------------------------------------+----------------------------------| + | DATETIME '2020-08-26 01:00:00' | DATE '2020-08-26' | + +------------------------------------------------+----------------------------------+ + + +DATE_ADD +-------- + +todo + + +DATE_FORMAT ----------- -Specifications: +Description +>>>>>>>>>>> -1. TIMESTAMP(DATE) -> DATE +Specifications: +1. DATE_FORMAT(DATE, STRING) -> STRING +2. DATE_FORMAT(DATE, STRING, STRING) -> STRING -TRIM -==== + +DAYOFMONTH +---------- + +Description +>>>>>>>>>>> + +Specifications: + +1. DAYOFMONTH(DATE) -> INTEGER + + +MAKETIME +-------- + +Description +>>>>>>>>>>> + +Specifications: + +1. MAKETIME(INTEGER, INTEGER, INTEGER) -> DATE + + +MONTH +----- + +Description +>>>>>>>>>>> + +Specifications: + +1. MONTH(DATE) -> INTEGER + + +MONTHNAME +--------- + +Description +>>>>>>>>>>> + +Specifications: + +1. MONTHNAME(DATE) -> STRING + + +NOW +--- + +Description +>>>>>>>>>>> + +Specifications: + +1. NOW() -> DATE + + +TIME +---- + +Description +>>>>>>>>>>> + +Usage: time(expr) constructs a time type with the input string expr as a time. If the argument is of date/datetime/time/timestamp, it extracts the time value part from the expression. + +Argument type: STRING/DATE/DATETIME/TIME/TIMESTAMP + +Return type: TIME + +Example:: + + >od SELECT TIME('13:49:00'), TIME(TIMESTAMP('2020-08-26 13:49:00')) + fetched rows / total rows = 1/1 + +--------------------+------------------------------------------+ + | TIME('13:49:00') | TIME(TIMESTAMP('2020-08-26 13:49:00')) | + |--------------------+------------------------------------------| + | TIME '13:49:00' | TIME '13:49:00' | + +--------------------+------------------------------------------+ + + +TIMESTAMP +--------- + +Description +>>>>>>>>>>> + +Usage: timestamp(expr) construct a timestamp type with the input string expr as an timestamp. If the argument is of date/datetime/timestamp type, cast expr to timestamp type with default timezone UTC. + +Argument type: STRING/DATE/DATETIME/TIMESTAMP + +Return type: TIMESTAMP + +Example:: + + >od SELECT TIMESTAMP('2020-08-26 13:49:00') + fetched rows / total rows = 1/1 + +------------------------------------+ + | TIMESTAMP('2020-08-26 13:49:00') | + |------------------------------------| + | TIMESTAMP '2020-08-26 13:49:00 | + +------------------------------------+ + + +YEAR +---- + +Description +>>>>>>>>>>> + +Specifications: + +1. YEAR(DATE) -> INTEGER + + + +String Functions +================ + +ASCII +----- + +Description +>>>>>>>>>>> + +Specifications: + +1. ASCII(STRING T) -> INTEGER + + +CONCAT +------ + +Description +>>>>>>>>>>> + +Usage: CONCAT(str1, str2) returns str1 and str strings concatenated together. + +Argument type: STRING, STRING + +Return Type: STRING + +Example:: + + od> SELECT CONCAT('hello', 'world') + fetched rows / total rows = 1/1 + +----------------------------+ + | CONCAT('hello', 'world') | + |----------------------------| + | helloworld | + +----------------------------+ + + +CONCAT_WS +--------- + +Description +>>>>>>>>>>> + +Usage: CONCAT_WS(sep, str1, str2) returns str1 concatenated with str2 using sep as a separator between them. + +Argument type: STRING, STRING, STRING + +Return Type: STRING + +Example:: + + od> SELECT CONCAT_WS(',', 'hello', 'world') + fetched rows / total rows = 1/1 + +------------------------------------+ + | CONCAT_WS(',', 'hello', 'world') | + |------------------------------------| + | hello,world | + +------------------------------------+ + + +LEFT +---- + +Description +>>>>>>>>>>> + +Specifications: + +1. LEFT(STRING T, INTEGER) -> T + +LENGTH +------ + +Description +>>>>>>>>>>> + +Specifications: + +1. LENGTH(STRING) -> INTEGER + +Usage: length(str) returns length of string measured in bytes. + +Argument type: STRING + +Return Type: INTEGER + +Example:: + + od> SELECT LENGTH('helloworld') + fetched rows / total rows = 1/1 + +------------------------+ + | LENGTH('helloworld') | + |------------------------| + | 10 | + +------------------------+ + + +LOCATE +------ + +Description +>>>>>>>>>>> + +Specifications: + +1. LOCATE(STRING, STRING, INTEGER) -> INTEGER +2. LOCATE(STRING, STRING) -> INTEGER + + +LOWER +===== Description ----------- -Usage: trim(str) trims leading and trailing space characters from the string. +Usage: lower(string) converts the string to lowercase. Argument type: STRING @@ -1088,44 +1147,132 @@ Return Type: STRING Example:: - od> SELECT TRIM(' hello'), TRIM('hello ') + od> SELECT LOWER('helloworld'), LOWER('HELLOWORLD') fetched rows / total rows = 1/1 - +--------------------+--------------------+ - | TRIM(' hello') | TRIM('hello ') | - |--------------------+--------------------| - | hello | hello | - +--------------------+--------------------+ + +-----------------------+-----------------------+ + | LOWER('helloworld') | LOWER('HELLOWORLD') | + |-----------------------+-----------------------| + | helloworld | helloworld | + +-----------------------+-----------------------+ -TRUNCATE -======== + +LTRIM +===== Description ----------- -Usage: TRUNCATE(x, d) returns the number x, truncated to d decimal place +Usage: ltrim(str) trims leading space characters from the string. -Argument type: INTEGER/LONG/FLOAT/DOUBLE +Argument type: STRING -Return type map: +Return Type: STRING -INTEGER/LONG -> LONG -FLOAT/DOUBLE -> DOUBLE +Example:: + + od> SELECT LTRIM(' hello'), LTRIM('hello ') + fetched rows / total rows = 1/1 + +---------------------+---------------------+ + | LTRIM(' hello') | LTRIM('hello ') | + |---------------------+---------------------| + | hello | hello | + +---------------------+---------------------+ + + +REPLACE +------- + +Description +>>>>>>>>>>> + +Specifications: + +1. REPLACE(STRING T, STRING, STRING) -> T + + +RIGHT +----- + +Description +>>>>>>>>>>> + +Specifications: + +1. RIGHT(STRING T, INTEGER) -> T + + +RTRIM +----- + +Description +>>>>>>>>>>> + +Usage: rtrim(str) trims trailing space characters from the string. + +Argument type: STRING + +Return Type: STRING Example:: + od> SELECT RTRIM(' hello'), RTRIM('hello ') fetched rows / total rows = 1/1 - +----------------------+-----------------------+-------------------+ - | TRUNCATE(56.78, 1) | TRUNCATE(56.78, -1) | TRUNCATE(56, 1) | - |----------------------+-----------------------+-------------------| - | 56.7 | 50 | 56 | - +----------------------+-----------------------+-------------------+ + +---------------------+---------------------+ + | RTRIM(' hello') | RTRIM('hello ') | + |---------------------+---------------------| + | hello | hello | + +---------------------+---------------------+ + + +SUBSTRING +--------- + +Description +>>>>>>>>>>> + +Usage: substring(str, start) or substring(str, start, length) returns substring using start and length. With no length, entire string from start is returned. + +Argument type: STRING, INTEGER, INTEGER + +Return Type: STRING + +Synonyms: SUBSTR + +Example:: + + od> SELECT SUBSTRING('helloworld', 5), SUBSTRING('helloworld', 5, 3) + fetched rows / total rows = 1/1 + +------------------------------+---------------------------------+ + | SUBSTRING('helloworld', 5) | SUBSTRING('helloworld', 5, 3) | + |------------------------------+---------------------------------| + | oworld | owo | + +------------------------------+---------------------------------+ + + +TRIM +---- + +Description +>>>>>>>>>>> + +Return Type: STRING + +Example:: + + od> SELECT TRIM(' hello'), TRIM('hello ') + fetched rows / total rows = 1/1 + +--------------------+--------------------+ + | TRIM(' hello') | TRIM('hello ') | + |--------------------+--------------------| + | hello | hello | + +--------------------+--------------------+ UPPER -===== +----- Description ------------ +>>>>>>>>>>> Usage: upper(string) converts the string to uppercase. @@ -1143,14 +1290,38 @@ Example:: | HELLOWORLD | HELLOWORLD | +-----------------------+-----------------------+ -YEAR -==== + +Conditional Functions +===================== + +IF +-- Description ------------ +>>>>>>>>>>> -Specifications: +Specifications: -1. YEAR(DATE) -> INTEGER +1. IF(BOOLEAN, ES_TYPE, ES_TYPE) -> ES_TYPE + + +IFNULL +------ +Description +>>>>>>>>>>> + +Specifications: + +1. IFNULL(ES_TYPE, ES_TYPE) -> ES_TYPE + +ISNULL +------ + +Description +>>>>>>>>>>> + +Specifications: + +1. ISNULL(ES_TYPE) -> INTEGER diff --git a/elasticsearch/build.gradle b/elasticsearch/build.gradle index 7aeebc5ab6..e18a3a9a7e 100644 --- a/elasticsearch/build.gradle +++ b/elasticsearch/build.gradle @@ -19,8 +19,8 @@ dependencies { testImplementation('org.junit.jupiter:junit-jupiter:5.6.2') testCompile group: 'org.hamcrest', name: 'hamcrest-library', version: '2.1' - testCompile group: 'org.mockito', name: 'mockito-core', version: '3.3.3' - testCompile group: 'org.mockito', name: 'mockito-junit-jupiter', version: '3.3.3' + testCompile group: 'org.mockito', name: 'mockito-core', version: '3.5.0' + testCompile group: 'org.mockito', name: 'mockito-junit-jupiter', version: '3.5.0' testCompile group: 'org.elasticsearch.client', name: 'elasticsearch-rest-high-level-client', version: "${es_version}" } diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/data/value/ElasticsearchExprValueFactory.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/data/value/ElasticsearchExprValueFactory.java index 5b2fe8c07f..95cf4bc670 100644 --- a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/data/value/ElasticsearchExprValueFactory.java +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/data/value/ElasticsearchExprValueFactory.java @@ -56,14 +56,17 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import lombok.RequiredArgsConstructor; +import java.util.function.Function; +import lombok.AllArgsConstructor; +import lombok.Setter; import org.elasticsearch.common.time.DateFormatters; /** Construct ExprValue from Elasticsearch response. */ -@RequiredArgsConstructor +@AllArgsConstructor public class ElasticsearchExprValueFactory { /** The Mapping of Field and ExprType. */ - private final Map typeMapping; + @Setter + private Map typeMapping; private static final DateTimeFormatter DATE_TIME_FORMATTER = new DateTimeFormatterBuilder() @@ -150,13 +153,13 @@ public ExprValue construct(String field, Object value) { ExprType type = type(field); if (type.equals(INTEGER)) { - return constructInteger((Integer) value); + return constructInteger(parseNumberValue(value, Integer::valueOf, Number::intValue)); } else if (type.equals(LONG)) { - return constructLong((Long) value); + return constructLong(parseNumberValue(value, Long::valueOf, Number::longValue)); } else if (type.equals(FLOAT)) { - return constructFloat((Float) value); + return constructFloat(parseNumberValue(value, Float::valueOf, Number::floatValue)); } else if (type.equals(DOUBLE)) { - return constructDouble((Double) value); + return constructDouble(parseNumberValue(value, Double::valueOf, Number::doubleValue)); } else if (type.equals(STRING)) { return constructString((String) value); } else if (type.equals(BOOLEAN)) { @@ -180,6 +183,18 @@ public ExprValue construct(String field, Object value) { } } + /** + * Elastisearch could return value String value even the type is Number. + */ + private T parseNumberValue(Object value, Function stringTFunction, + Function numberTFunction) { + if (value instanceof String) { + return stringTFunction.apply((String) value); + } else { + return numberTFunction.apply((Number) value); + } + } + private ExprType type(String field) { if (typeMapping.containsKey(field)) { return typeMapping.get(field); diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/executor/ElasticsearchExecutionEngine.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/executor/ElasticsearchExecutionEngine.java index 97163f7e5b..3f4cd2bf37 100644 --- a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/executor/ElasticsearchExecutionEngine.java +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/executor/ElasticsearchExecutionEngine.java @@ -20,8 +20,12 @@ import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.client.ElasticsearchClient; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.executor.protector.ExecutionProtector; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.ElasticsearchIndexScan; import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine; +import com.amazon.opendistroforelasticsearch.sql.executor.Explain; import com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlan; +import com.amazon.opendistroforelasticsearch.sql.storage.TableScanOperator; +import com.google.common.collect.ImmutableMap; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; @@ -56,4 +60,27 @@ public void execute(PhysicalPlan physicalPlan, ResponseListener l } }); } + + @Override + public void explain(PhysicalPlan plan, ResponseListener listener) { + client.schedule(() -> { + try { + Explain esExplain = new Explain() { + @Override + public ExplainResponseNode visitTableScan(TableScanOperator node, Object context) { + return explain(node, context, explainNode -> { + ElasticsearchIndexScan indexScan = (ElasticsearchIndexScan) node; + explainNode.setDescription(ImmutableMap.of( + "request", indexScan.getRequest().toString())); + }); + } + }; + + listener.onResponse(esExplain.apply(plan)); + } catch (Exception e) { + listener.onFailure(e); + } + }); + } + } diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/executor/protector/ElasticsearchExecutionProtector.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/executor/protector/ElasticsearchExecutionProtector.java index cef0164ca0..54630987d1 100644 --- a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/executor/protector/ElasticsearchExecutionProtector.java +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/executor/protector/ElasticsearchExecutionProtector.java @@ -22,6 +22,7 @@ import com.amazon.opendistroforelasticsearch.sql.planner.physical.DedupeOperator; import com.amazon.opendistroforelasticsearch.sql.planner.physical.EvalOperator; import com.amazon.opendistroforelasticsearch.sql.planner.physical.FilterOperator; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.HeadOperator; import com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlan; import com.amazon.opendistroforelasticsearch.sql.planner.physical.ProjectOperator; import com.amazon.opendistroforelasticsearch.sql.planner.physical.RareTopNOperator; @@ -97,6 +98,16 @@ public PhysicalPlan visitDedupe(DedupeOperator node, Object context) { node.getAllowedDuplication(), node.getKeepEmpty(), node.getConsecutive()); } + @Override + public PhysicalPlan visitHead(HeadOperator node, Object context) { + return new HeadOperator( + visitInput(node.getInput(), context), + node.getKeepLast(), + node.getWhileExpr(), + node.getNumber() + ); + } + @Override public PhysicalPlan visitSort(SortOperator node, Object context) { return new SortOperator(visitInput(node.getInput(), context), node.getCount(), diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchQueryRequest.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchQueryRequest.java index a599b63ec8..fc66e8a091 100644 --- a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchQueryRequest.java +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchQueryRequest.java @@ -17,10 +17,12 @@ package com.amazon.opendistroforelasticsearch.sql.elasticsearch.request; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.value.ElasticsearchExprValueFactory; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.response.ElasticsearchResponse; import com.google.common.annotations.VisibleForTesting; import java.util.function.Consumer; import java.util.function.Function; +import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @@ -57,6 +59,14 @@ public class ElasticsearchQueryRequest implements ElasticsearchRequest { */ private final SearchSourceBuilder sourceBuilder; + + /** + * ElasticsearchExprValueFactory. + */ + @EqualsAndHashCode.Exclude + @ToString.Exclude + private final ElasticsearchExprValueFactory exprValueFactory; + /** * Indicate the search already done. */ @@ -65,23 +75,24 @@ public class ElasticsearchQueryRequest implements ElasticsearchRequest { /** * Constructor of ElasticsearchQueryRequest. */ - public ElasticsearchQueryRequest(String indexName, int size) { + public ElasticsearchQueryRequest(String indexName, int size, + ElasticsearchExprValueFactory factory) { this.indexName = indexName; this.sourceBuilder = new SearchSourceBuilder(); sourceBuilder.from(0); sourceBuilder.size(size); sourceBuilder.timeout(DEFAULT_QUERY_TIMEOUT); - + this.exprValueFactory = factory; } @Override public ElasticsearchResponse search(Function searchAction, Function scrollAction) { if (searchDone) { - return new ElasticsearchResponse(SearchHits.empty()); + return new ElasticsearchResponse(SearchHits.empty(), exprValueFactory); } else { searchDone = true; - return new ElasticsearchResponse(searchAction.apply(searchRequest())); + return new ElasticsearchResponse(searchAction.apply(searchRequest()), exprValueFactory); } } diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchRequest.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchRequest.java index 60f851b100..4b38e8a0d9 100644 --- a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchRequest.java +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchRequest.java @@ -17,6 +17,7 @@ package com.amazon.opendistroforelasticsearch.sql.elasticsearch.request; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.value.ElasticsearchExprValueFactory; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.response.ElasticsearchResponse; import java.util.function.Consumer; import java.util.function.Function; @@ -53,4 +54,10 @@ ElasticsearchResponse search(Function searchActio * @return SearchSourceBuilder. */ SearchSourceBuilder getSourceBuilder(); + + /** + * Get the ElasticsearchExprValueFactory. + * @return ElasticsearchExprValueFactory. + */ + ElasticsearchExprValueFactory getExprValueFactory(); } diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchScrollRequest.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchScrollRequest.java index 87cb9ae2fd..bcbdf440c8 100644 --- a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchScrollRequest.java +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchScrollRequest.java @@ -16,6 +16,7 @@ package com.amazon.opendistroforelasticsearch.sql.elasticsearch.request; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.value.ElasticsearchExprValueFactory; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.response.ElasticsearchResponse; import java.util.Objects; import java.util.function.Consumer; @@ -49,6 +50,11 @@ public class ElasticsearchScrollRequest implements ElasticsearchRequest { /** Index name. */ private final String indexName; + /** Index name. */ + @EqualsAndHashCode.Exclude + @ToString.Exclude + private final ElasticsearchExprValueFactory exprValueFactory; + /** * Scroll id which is set after first request issued. Because ElasticsearchClient is shared by * multi-thread so this state has to be maintained here. @@ -70,7 +76,7 @@ public ElasticsearchResponse search(Function sear } setScrollId(esResponse.getScrollId()); - return new ElasticsearchResponse(esResponse); + return new ElasticsearchResponse(esResponse, exprValueFactory); } @Override diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/ElasticsearchAggregationResponseParser.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/ElasticsearchAggregationResponseParser.java new file mode 100644 index 0000000000..c37739b129 --- /dev/null +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/ElasticsearchAggregationResponseParser.java @@ -0,0 +1,92 @@ +/* + * + * 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.elasticsearch.response; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.experimental.UtilityClass; +import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregation; +import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; + +/** + * AggregationResponseParser. + */ +@UtilityClass +public class ElasticsearchAggregationResponseParser { + + /** + * Parse Aggregations as a list of field and value map. + * + * @param aggregations aggregations + * @return a list of field and value map + */ + public static List> parse(Aggregations aggregations) { + List aggregationList = aggregations.asList(); + ImmutableList.Builder> builder = new ImmutableList.Builder<>(); + + for (Aggregation aggregation : aggregationList) { + if (aggregation instanceof CompositeAggregation) { + for (CompositeAggregation.Bucket bucket : + ((CompositeAggregation) aggregation).getBuckets()) { + builder.add(parse(bucket)); + } + } else { + builder.add(parseInternal(aggregation)); + } + + } + return builder.build(); + } + + private static Map parse(CompositeAggregation.Bucket bucket) { + Map resultMap = new HashMap<>(); + // The NodeClient return InternalComposite + + // build pair + resultMap.putAll(bucket.getKey()); + + // build pair + for (Aggregation aggregation : bucket.getAggregations()) { + resultMap.putAll(parseInternal(aggregation)); + } + + return resultMap; + } + + private static Map parseInternal(Aggregation aggregation) { + Map resultMap = new HashMap<>(); + if (aggregation instanceof NumericMetricsAggregation.SingleValue) { + resultMap.put( + aggregation.getName(), + handleNanValue(((NumericMetricsAggregation.SingleValue) aggregation).value())); + } else { + throw new IllegalStateException("unsupported aggregation type " + aggregation.getType()); + } + return resultMap; + } + + @VisibleForTesting + protected static Object handleNanValue(double value) { + return Double.isNaN(value) ? null : value; + } +} diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/ElasticsearchResponse.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/ElasticsearchResponse.java index 11f88b7029..5aeb0f3a55 100644 --- a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/ElasticsearchResponse.java +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/ElasticsearchResponse.java @@ -16,27 +16,59 @@ package com.amazon.opendistroforelasticsearch.sql.elasticsearch.response; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.value.ElasticsearchExprValueFactory; +import com.google.common.collect.ImmutableMap; +import java.util.Arrays; import java.util.Iterator; +import java.util.Map; import lombok.EqualsAndHashCode; import lombok.ToString; import org.elasticsearch.action.search.SearchResponse; -import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; +import org.elasticsearch.search.aggregations.Aggregations; -/** Elasticsearch search response. */ +/** + * Elasticsearch search response. + */ @EqualsAndHashCode @ToString -public class ElasticsearchResponse implements Iterable { +public class ElasticsearchResponse implements Iterable { - /** Search query result (non-aggregation). */ + /** + * Search query result (non-aggregation). + */ private final SearchHits hits; - public ElasticsearchResponse(SearchResponse esResponse) { - this.hits = esResponse.getHits(); // TODO: aggregation result is separate and not in SearchHit[] + /** + * Search aggregation result. + */ + private final Aggregations aggregations; + + /** + * ElasticsearchExprValueFactory used to build ExprValue from search result. + */ + @EqualsAndHashCode.Exclude + private final ElasticsearchExprValueFactory exprValueFactory; + + /** + * Constructor of ElasticsearchResponse. + */ + public ElasticsearchResponse(SearchResponse esResponse, + ElasticsearchExprValueFactory exprValueFactory) { + this.hits = esResponse.getHits(); + this.aggregations = esResponse.getAggregations(); + this.exprValueFactory = exprValueFactory; } - public ElasticsearchResponse(SearchHits hits) { + /** + * Constructor of ElasticsearchResponse with SearchHits. + */ + public ElasticsearchResponse(SearchHits hits, ElasticsearchExprValueFactory exprValueFactory) { this.hits = hits; + this.aggregations = null; + this.exprValueFactory = exprValueFactory; } /** @@ -46,7 +78,11 @@ public ElasticsearchResponse(SearchHits hits) { * @return true for empty */ public boolean isEmpty() { - return (hits.getHits() == null) || (hits.getHits().length == 0); + return (hits.getHits() == null) || (hits.getHits().length == 0) && aggregations == null; + } + + public boolean isAggregationResponse() { + return aggregations != null; } /** @@ -54,8 +90,18 @@ public boolean isEmpty() { * * @return search hit iterator */ - @Override - public Iterator iterator() { - return hits.iterator(); + public Iterator iterator() { + if (isAggregationResponse()) { + return ElasticsearchAggregationResponseParser.parse(aggregations).stream().map(entry -> { + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + for (Map.Entry value : entry.entrySet()) { + builder.put(value.getKey(), exprValueFactory.construct(value.getKey(), value.getValue())); + } + return (ExprValue) ExprTupleValue.fromExprValueMap(builder.build()); + }).iterator(); + } else { + return Arrays.stream(hits.getHits()) + .map(hit -> (ExprValue) exprValueFactory.construct(hit.getSourceAsString())).iterator(); + } } } diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/ElasticsearchIndex.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/ElasticsearchIndex.java index ab06267c46..df737f3f3f 100644 --- a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/ElasticsearchIndex.java +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/ElasticsearchIndex.java @@ -21,12 +21,13 @@ import com.amazon.opendistroforelasticsearch.sql.data.type.ExprType; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.client.ElasticsearchClient; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.type.ElasticsearchDataType; -import com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.value.ElasticsearchExprIpValue; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.value.ElasticsearchExprValueFactory; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.mapping.IndexMapping; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.script.aggregation.AggregationQueryBuilder; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.script.filter.FilterQueryBuilder; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.serialization.DefaultExpressionSerializer; import com.amazon.opendistroforelasticsearch.sql.planner.DefaultImplementor; +import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalAggregation; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalFilter; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlan; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalRelation; @@ -34,9 +35,11 @@ import com.amazon.opendistroforelasticsearch.sql.storage.Table; import com.google.common.collect.ImmutableMap; import java.util.HashMap; +import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.aggregations.AggregationBuilder; /** Elasticsearch table (index) implementation. */ @RequiredArgsConstructor @@ -115,6 +118,33 @@ public PhysicalPlan visitFilter(LogicalFilter node, ElasticsearchIndexScan conte return visitChild(node, context); } + @Override + public PhysicalPlan visitAggregation(LogicalAggregation node, + ElasticsearchIndexScan context) { + // Todo, aggregation in the following pattern can be push down + // aggregation -> relation + // aggregation -> filter -> relation + if ((node.getChild().get(0) instanceof LogicalRelation) + || (node.getChild().get(0) instanceof LogicalFilter && node.getChild().get(0) + .getChild().get(0) instanceof LogicalRelation)) { + AggregationQueryBuilder builder = + new AggregationQueryBuilder(new DefaultExpressionSerializer()); + + List aggregationBuilder = + builder.buildAggregationBuilder(node.getAggregatorList(), + node.getGroupByList()); + + context.pushDownAggregation(aggregationBuilder); + context.pushTypeMapping( + builder.buildTypeMapping(node.getAggregatorList(), + node.getGroupByList())); + + return visitChild(node, context); + } else { + return super.visitAggregation(node, context); + } + } + @Override public PhysicalPlan visitRelation(LogicalRelation node, ElasticsearchIndexScan context) { return indexScan; diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/ElasticsearchIndexScan.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/ElasticsearchIndexScan.java index ccc31291f9..bac4bc82ed 100644 --- a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/ElasticsearchIndexScan.java +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/ElasticsearchIndexScan.java @@ -21,6 +21,7 @@ import com.amazon.opendistroforelasticsearch.sql.common.setting.Settings; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; +import com.amazon.opendistroforelasticsearch.sql.data.type.ExprType; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.client.ElasticsearchClient; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.value.ElasticsearchExprValueFactory; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.request.ElasticsearchQueryRequest; @@ -31,12 +32,14 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Map; import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.ToString; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; /** @@ -49,15 +52,14 @@ public class ElasticsearchIndexScan extends TableScanOperator { /** Elasticsearch client. */ private final ElasticsearchClient client; - private final ElasticsearchExprValueFactory exprValueFactory; - /** Search request. */ @EqualsAndHashCode.Include + @Getter @ToString.Include private final ElasticsearchRequest request; /** Search response for current batch. */ - private Iterator hits; + private Iterator iterator; /** * Todo. @@ -67,8 +69,7 @@ public ElasticsearchIndexScan(ElasticsearchClient client, ElasticsearchExprValueFactory exprValueFactory) { this.client = client; this.request = new ElasticsearchQueryRequest(indexName, - settings.getSettingValue(Settings.Key.QUERY_SIZE_LIMIT)); - this.exprValueFactory = exprValueFactory; + settings.getSettingValue(Settings.Key.QUERY_SIZE_LIMIT), exprValueFactory); } @Override @@ -82,17 +83,17 @@ public void open() { responses.add(response); response = client.search(request); } - hits = Iterables.concat(responses.toArray(new ElasticsearchResponse[0])).iterator(); + iterator = Iterables.concat(responses.toArray(new ElasticsearchResponse[0])).iterator(); } @Override public boolean hasNext() { - return hits.hasNext(); + return iterator.hasNext(); } @Override public ExprValue next() { - return exprValueFactory.construct(hits.next().getSourceAsString()); + return iterator.next(); } /** @@ -119,6 +120,20 @@ public void pushDown(QueryBuilder query) { } } + /** + * Push down aggregation to DSL request. + * @param aggregationBuilderList aggregation query. + */ + public void pushDownAggregation(List aggregationBuilderList) { + SearchSourceBuilder source = request.getSourceBuilder(); + aggregationBuilderList.forEach(aggregationBuilder -> source.aggregation(aggregationBuilder)); + source.size(0); + } + + public void pushTypeMapping(Map typeMapping) { + request.getExprValueFactory().setTypeMapping(typeMapping); + } + @Override public void close() { super.close(); diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/ExpressionScriptEngine.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/ExpressionScriptEngine.java index abebafc294..533ee62616 100644 --- a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/ExpressionScriptEngine.java +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/ExpressionScriptEngine.java @@ -16,6 +16,7 @@ package com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.script; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.script.aggregation.ExpressionAggregationScriptFactory; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.script.filter.ExpressionFilterScriptFactory; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.serialization.ExpressionSerializer; import com.amazon.opendistroforelasticsearch.sql.expression.Expression; @@ -24,6 +25,7 @@ import java.util.Set; import java.util.function.Function; import lombok.RequiredArgsConstructor; +import org.elasticsearch.script.AggregationScript; import org.elasticsearch.script.FilterScript; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptEngine; @@ -44,10 +46,10 @@ public class ExpressionScriptEngine implements ScriptEngine { * All supported script contexts and function to create factory from expression. */ private static final Map, Function> CONTEXTS = - ImmutableMap.of( - FilterScript.CONTEXT, - ExpressionFilterScriptFactory::new - ); + new ImmutableMap.Builder, Function>() + .put(FilterScript.CONTEXT, ExpressionFilterScriptFactory::new) + .put(AggregationScript.CONTEXT, ExpressionAggregationScriptFactory::new) + .build(); /** * Expression serializer that (de-)serializes expression. diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/ScriptUtils.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/ScriptUtils.java new file mode 100644 index 0000000000..1fe91e6f47 --- /dev/null +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/ScriptUtils.java @@ -0,0 +1,41 @@ +/* + * + * 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.elasticsearch.storage.script; + +import static com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.type.ElasticsearchDataType.ES_TEXT_KEYWORD; + +import com.amazon.opendistroforelasticsearch.sql.data.type.ExprType; +import lombok.experimental.UtilityClass; + +/** + * Script Utils. + */ +@UtilityClass +public class ScriptUtils { + + /** + * Text field doesn't have doc value (exception thrown even when you call "get") + * Limitation: assume inner field name is always "keyword". + */ + public static String convertTextToKeyword(String fieldName, ExprType fieldType) { + if (fieldType == ES_TEXT_KEYWORD) { + return fieldName + ".keyword"; + } + return fieldName; + } +} diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/AggregationQueryBuilder.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/AggregationQueryBuilder.java new file mode 100644 index 0000000000..3dd0db717f --- /dev/null +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/AggregationQueryBuilder.java @@ -0,0 +1,92 @@ +/* + * + * 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.elasticsearch.storage.script.aggregation; + +import com.amazon.opendistroforelasticsearch.sql.data.type.ExprType; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.script.aggregation.dsl.BucketAggregationBuilder; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.script.aggregation.dsl.MetricAggregationBuilder; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.serialization.ExpressionSerializer; +import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionNodeVisitor; +import com.amazon.opendistroforelasticsearch.sql.expression.NamedExpression; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.NamedAggregator; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.elasticsearch.search.aggregations.AggregationBuilder; +import org.elasticsearch.search.aggregations.AggregationBuilders; + +/** + * Build the AggregationBuilder from the list of {@link NamedAggregator} + * and list of {@link NamedExpression}. + */ +@RequiredArgsConstructor +public class AggregationQueryBuilder extends ExpressionNodeVisitor { + + /** + * How many composite buckets should be returned. + */ + public static final int AGGREGATION_BUCKET_SIZE = 1000; + + /** + * Bucket Aggregation builder. + */ + private final BucketAggregationBuilder bucketBuilder; + + /** + * Metric Aggregation builder. + */ + private final MetricAggregationBuilder metricBuilder; + + public AggregationQueryBuilder( + ExpressionSerializer serializer) { + this.bucketBuilder = new BucketAggregationBuilder(serializer); + this.metricBuilder = new MetricAggregationBuilder(serializer); + } + + /** + * Build AggregationBuilder. + */ + public List buildAggregationBuilder(List namedAggregatorList, + List groupByList) { + if (groupByList.isEmpty()) { + // no bucket + return ImmutableList + .copyOf(metricBuilder.build(namedAggregatorList).getAggregatorFactories()); + } else { + return Collections.singletonList(AggregationBuilders.composite("composite_buckets", + bucketBuilder.build(groupByList)) + .subAggregations(metricBuilder.build(namedAggregatorList)) + .size(AGGREGATION_BUCKET_SIZE)); + } + } + + /** + * Build ElasticsearchExprValueFactory. + */ + public Map buildTypeMapping( + List namedAggregatorList, + List groupByList) { + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + namedAggregatorList.forEach(agg -> builder.put(agg.getName(), agg.type())); + groupByList.forEach(group -> builder.put(group.getName(), group.type())); + return builder.build(); + } +} diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScript.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScript.java new file mode 100644 index 0000000000..64f7538e0d --- /dev/null +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScript.java @@ -0,0 +1,69 @@ +/* + * + * 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.elasticsearch.storage.script.aggregation; + +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprNullValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.script.core.ExpressionScript; +import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; +import java.util.Map; +import lombok.EqualsAndHashCode; +import org.apache.lucene.index.LeafReaderContext; +import org.elasticsearch.script.AggregationScript; +import org.elasticsearch.search.lookup.SearchLookup; + +/** + * Aggregation expression script that executed on each document. + */ +@EqualsAndHashCode(callSuper = false) +public class ExpressionAggregationScript extends AggregationScript { + + /** + * Expression Script. + */ + private final ExpressionScript expressionScript; + + /** + * Constructor of ExpressionAggregationScript. + */ + public ExpressionAggregationScript( + Expression expression, + SearchLookup lookup, + LeafReaderContext context, + Map params) { + super(params, lookup, context); + this.expressionScript = new ExpressionScript(expression); + } + + @Override + public Object execute() { + return expressionScript.execute(this::getDoc, this::evaluateExpression); + } + + private ExprValue evaluateExpression(Expression expression, Environment valueEnv) { + ExprValue result = expression.valueOf(valueEnv); + + // The missing value is treated as null value in doc_value, so we can't distinguish with them. + if (result.isNull()) { + return ExprNullValue.of(); + } + return result; + } +} diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScriptFactory.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScriptFactory.java new file mode 100644 index 0000000000..ec6528716b --- /dev/null +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScriptFactory.java @@ -0,0 +1,48 @@ +/* + * + * 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.elasticsearch.storage.script.aggregation; + +import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import java.util.Map; +import lombok.EqualsAndHashCode; +import org.elasticsearch.script.AggregationScript; +import org.elasticsearch.search.lookup.SearchLookup; + +/** + * Aggregation Expression script factory that generates leaf factory. + */ +@EqualsAndHashCode +public class ExpressionAggregationScriptFactory implements AggregationScript.Factory { + + private final Expression expression; + + public ExpressionAggregationScriptFactory(Expression expression) { + this.expression = expression; + } + + @Override + public boolean isResultDeterministic() { + // This implies the results are cacheable + return true; + } + + @Override + public AggregationScript.LeafFactory newFactory(Map params, SearchLookup lookup) { + return new ExpressionAggregationScriptLeafFactory(expression, params, lookup); + } +} diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScriptLeafFactory.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScriptLeafFactory.java new file mode 100644 index 0000000000..63c54fb8a7 --- /dev/null +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScriptLeafFactory.java @@ -0,0 +1,66 @@ +/* + * + * 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.elasticsearch.storage.script.aggregation; + +import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import java.io.IOException; +import java.util.Map; +import org.apache.lucene.index.LeafReaderContext; +import org.elasticsearch.script.AggregationScript; +import org.elasticsearch.search.lookup.SearchLookup; + +/** + * Expression script leaf factory that produces script executor for each leaf. + */ +public class ExpressionAggregationScriptLeafFactory implements AggregationScript.LeafFactory { + + /** + * Expression to execute. + */ + private final Expression expression; + + /** + * Expression to execute. + */ + private final Map params; + + /** + * Expression to execute. + */ + private final SearchLookup lookup; + + /** + * Constructor of ExpressionAggregationScriptLeafFactory. + */ + public ExpressionAggregationScriptLeafFactory( + Expression expression, Map params, SearchLookup lookup) { + this.expression = expression; + this.params = params; + this.lookup = lookup; + } + + @Override + public AggregationScript newInstance(LeafReaderContext ctx) { + return new ExpressionAggregationScript(expression, lookup, ctx, params); + } + + @Override + public boolean needs_score() { + return false; + } +} diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/AggregationBuilderHelper.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/AggregationBuilderHelper.java new file mode 100644 index 0000000000..0d441e6e1b --- /dev/null +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/AggregationBuilderHelper.java @@ -0,0 +1,63 @@ +/* + * + * 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.elasticsearch.storage.script.aggregation.dsl; + +import static com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.script.ExpressionScriptEngine.EXPRESSION_LANG_NAME; +import static java.util.Collections.emptyMap; +import static org.elasticsearch.script.Script.DEFAULT_SCRIPT_TYPE; + +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.script.ScriptUtils; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.serialization.ExpressionSerializer; +import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import com.amazon.opendistroforelasticsearch.sql.expression.FunctionExpression; +import com.amazon.opendistroforelasticsearch.sql.expression.ReferenceExpression; +import java.util.function.Function; +import lombok.RequiredArgsConstructor; +import org.elasticsearch.script.Script; + +/** + * Abstract Aggregation Builder. + * + * @param type of the actual AggregationBuilder to be built. + */ +@RequiredArgsConstructor +public class AggregationBuilderHelper { + + private final ExpressionSerializer serializer; + + /** + * Build AggregationBuilder from Expression. + * + * @param expression Expression + * @return AggregationBuilder + */ + public T build(Expression expression, Function fieldBuilder, + Function scriptBuilder) { + if (expression instanceof ReferenceExpression) { + String fieldName = ((ReferenceExpression) expression).getAttr(); + return fieldBuilder.apply(ScriptUtils.convertTextToKeyword(fieldName, expression.type())); + } else if (expression instanceof FunctionExpression) { + return scriptBuilder.apply(new Script( + DEFAULT_SCRIPT_TYPE, EXPRESSION_LANG_NAME, serializer.serialize(expression), + emptyMap())); + } else { + throw new IllegalStateException(String.format("metric aggregation doesn't support " + + "expression %s", expression)); + } + } +} diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/BucketAggregationBuilder.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/BucketAggregationBuilder.java new file mode 100644 index 0000000000..38aabcdeea --- /dev/null +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/BucketAggregationBuilder.java @@ -0,0 +1,54 @@ +/* + * + * 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.elasticsearch.storage.script.aggregation.dsl; + +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.serialization.ExpressionSerializer; +import com.amazon.opendistroforelasticsearch.sql.expression.NamedExpression; +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.elasticsearch.search.aggregations.bucket.composite.CompositeValuesSourceBuilder; +import org.elasticsearch.search.aggregations.bucket.composite.TermsValuesSourceBuilder; + +/** + * Bucket Aggregation Builder. + */ +public class BucketAggregationBuilder { + + private final AggregationBuilderHelper> helper; + + public BucketAggregationBuilder( + ExpressionSerializer serializer) { + this.helper = new AggregationBuilderHelper<>(serializer); + } + + /** + * Build the list of CompositeValuesSourceBuilder. + */ + public List> build(List expressions) { + ImmutableList.Builder> resultBuilder = + new ImmutableList.Builder<>(); + for (NamedExpression expression : expressions) { + TermsValuesSourceBuilder valuesSourceBuilder = + new TermsValuesSourceBuilder(expression.getName()).missingBucket(true); + resultBuilder + .add(helper.build(expression.getDelegated(), valuesSourceBuilder::field, + valuesSourceBuilder::script)); + } + return resultBuilder.build(); + } +} diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/MetricAggregationBuilder.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/MetricAggregationBuilder.java new file mode 100644 index 0000000000..1481161602 --- /dev/null +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/MetricAggregationBuilder.java @@ -0,0 +1,80 @@ +/* + * + * 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.elasticsearch.storage.script.aggregation.dsl; + +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.serialization.ExpressionSerializer; +import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionNodeVisitor; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.NamedAggregator; +import java.util.List; +import org.elasticsearch.search.aggregations.AggregationBuilder; +import org.elasticsearch.search.aggregations.AggregationBuilders; +import org.elasticsearch.search.aggregations.AggregatorFactories; +import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuilder; + +/** + * Build the Metric Aggregation from {@link NamedAggregator}. + */ +public class MetricAggregationBuilder + extends ExpressionNodeVisitor { + + private final AggregationBuilderHelper> helper; + + public MetricAggregationBuilder( + ExpressionSerializer serializer) { + this.helper = new AggregationBuilderHelper<>(serializer); + } + + /** + * Build AggregatorFactories.Builder from {@link NamedAggregator}. + * + * @param aggregatorList aggregator list + * @return AggregatorFactories.Builder + */ + public AggregatorFactories.Builder build(List aggregatorList) { + AggregatorFactories.Builder builder = new AggregatorFactories.Builder(); + for (NamedAggregator aggregator : aggregatorList) { + builder.addAggregator(aggregator.accept(this, null)); + } + return builder; + } + + @Override + public AggregationBuilder visitNamedAggregator(NamedAggregator node, + Object context) { + Expression expression = node.getArguments().get(0); + String name = node.getName(); + + switch (node.getFunctionName().getFunctionName()) { + case "avg": + return make(AggregationBuilders.avg(name), expression); + case "sum": + return make(AggregationBuilders.sum(name), expression); + case "count": + return make(AggregationBuilders.count(name), expression); + default: + throw new IllegalStateException( + String.format("unsupported aggregator %s", node.getFunctionName().getFunctionName())); + } + } + + private ValuesSourceAggregationBuilder make(ValuesSourceAggregationBuilder builder, + Expression expression) { + return helper.build(expression, builder::field, builder::script); + } +} diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/core/ExpressionScript.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/core/ExpressionScript.java new file mode 100644 index 0000000000..813f93e031 --- /dev/null +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/core/ExpressionScript.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.elasticsearch.storage.script.core; + +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.FLOAT; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; +import static com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.type.ElasticsearchDataType.ES_TEXT_KEYWORD; +import static java.util.stream.Collectors.reducing; +import static java.util.stream.Collectors.toMap; + +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprMissingValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; +import com.amazon.opendistroforelasticsearch.sql.data.type.ExprType; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.value.ElasticsearchExprValueFactory; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.script.ScriptUtils; +import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionNodeVisitor; +import com.amazon.opendistroforelasticsearch.sql.expression.ReferenceExpression; +import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.time.chrono.ChronoZonedDateTime; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Supplier; +import lombok.EqualsAndHashCode; +import org.elasticsearch.index.fielddata.ScriptDocValues; + +/** + * Expression script executor that executes the expression on each document + * and determine if the document is supposed to be filtered out or not. + */ +@EqualsAndHashCode(callSuper = false) +public class ExpressionScript { + + /** + * Expression to execute. + */ + private final Expression expression; + + /** + * ElasticsearchExprValueFactory. + */ + @EqualsAndHashCode.Exclude + private final ElasticsearchExprValueFactory valueFactory; + + /** + * Reference Fields. + */ + @EqualsAndHashCode.Exclude + private final Set fields; + + /** + * Expression constructor. + */ + public ExpressionScript(Expression expression) { + this.expression = expression; + this.fields = AccessController.doPrivileged((PrivilegedAction>) () -> + extractFields(expression)); + this.valueFactory = + AccessController.doPrivileged( + (PrivilegedAction) () -> buildValueFactory(fields)); + } + + /** + * Evaluate on the doc generate by the doc provider. + * @param docProvider doc provider. + * @param evaluator evaluator + * @return + */ + public Object execute(Supplier>> docProvider, + BiFunction, ExprValue> evaluator) { + return AccessController.doPrivileged((PrivilegedAction) () -> { + Environment valueEnv = + buildValueEnv(fields, valueFactory, docProvider); + ExprValue result = evaluator.apply(expression, valueEnv); + return result.value(); + }); + } + + private Set extractFields(Expression expr) { + Set fields = new HashSet<>(); + expr.accept(new ExpressionNodeVisitor>() { + @Override + public Object visitReference(ReferenceExpression node, Set context) { + context.add(node); + return null; + } + }, fields); + return fields; + } + + private ElasticsearchExprValueFactory buildValueFactory(Set fields) { + Map typeEnv = fields.stream() + .collect(toMap( + ReferenceExpression::getAttr, + ReferenceExpression::type)); + return new ElasticsearchExprValueFactory(typeEnv); + } + + private Environment buildValueEnv( + Set fields, ElasticsearchExprValueFactory valueFactory, + Supplier>> docProvider) { + + Map valueEnv = new HashMap<>(); + for (ReferenceExpression field : fields) { + String fieldName = field.getAttr(); + ExprValue exprValue = valueFactory.construct(fieldName, getDocValue(field, docProvider)); + valueEnv.put(field, exprValue); + } + // Encapsulate map data structure into anonymous Environment class + return valueEnv::get; + } + + private Object getDocValue(ReferenceExpression field, + Supplier>> docProvider) { + String fieldName = getDocValueName(field); + ScriptDocValues docValue = docProvider.get().get(fieldName); + if (docValue == null || docValue.isEmpty()) { + return null; + } + + Object value = docValue.get(0); + if (value instanceof ChronoZonedDateTime) { + return ((ChronoZonedDateTime) value).toInstant(); + } + return castNumberToFieldType(value, field.type()); + } + + /** + * Text field doesn't have doc value (exception thrown even when you call "get") + * Limitation: assume inner field name is always "keyword". + */ + private String getDocValueName(ReferenceExpression field) { + String fieldName = field.getAttr(); + return ScriptUtils.convertTextToKeyword(fieldName, field.type()); + } + + /** + * DocValue only support long and double so cast to integer and float if needed. + * The doc value must be Long and Double for expr type Long/Integer and Double/Float respectively. + * Otherwise there must be bugs in our engine that causes the mismatch. + */ + private Object castNumberToFieldType(Object value, ExprType type) { + if (value == null) { + return value; + } + + if (type == INTEGER) { + return ((Long) value).intValue(); + } else if (type == FLOAT) { + return ((Double) value).floatValue(); + } else { + return value; + } + } +} diff --git a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/ExpressionFilterScript.java b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/ExpressionFilterScript.java index 08cb490080..e64cf161bd 100644 --- a/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/ExpressionFilterScript.java +++ b/elasticsearch/src/main/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/filter/ExpressionFilterScript.java @@ -16,31 +16,15 @@ package com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.script.filter; -import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.FLOAT; -import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; -import static com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.type.ElasticsearchDataType.ES_TEXT_KEYWORD; -import static java.util.stream.Collectors.toMap; - import com.amazon.opendistroforelasticsearch.sql.data.model.ExprBooleanValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; import com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType; -import com.amazon.opendistroforelasticsearch.sql.data.type.ExprType; -import com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.value.ElasticsearchExprValueFactory; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.script.core.ExpressionScript; import com.amazon.opendistroforelasticsearch.sql.expression.Expression; -import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionNodeVisitor; -import com.amazon.opendistroforelasticsearch.sql.expression.ReferenceExpression; import com.amazon.opendistroforelasticsearch.sql.expression.env.Environment; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.time.chrono.ChronoZonedDateTime; -import java.util.HashMap; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import lombok.EqualsAndHashCode; import org.apache.lucene.index.LeafReaderContext; -import org.elasticsearch.SpecialPermission; -import org.elasticsearch.index.fielddata.ScriptDocValues; import org.elasticsearch.script.FilterScript; import org.elasticsearch.search.lookup.SearchLookup; @@ -52,118 +36,26 @@ class ExpressionFilterScript extends FilterScript { /** - * Expression to execute. - */ - private final Expression expression; - - /** - * ElasticsearchExprValueFactory. + * Expression Script. */ - @EqualsAndHashCode.Exclude - private final ElasticsearchExprValueFactory valueFactory; - - /** - * Reference Fields. - */ - @EqualsAndHashCode.Exclude - private final Set fields; + private final ExpressionScript expressionScript; public ExpressionFilterScript(Expression expression, SearchLookup lookup, LeafReaderContext context, Map params) { super(params, lookup, context); - this.expression = expression; - this.fields = AccessController.doPrivileged((PrivilegedAction>) () -> - extractFields(expression)); - this.valueFactory = - AccessController.doPrivileged( - (PrivilegedAction) () -> buildValueFactory(fields)); + this.expressionScript = new ExpressionScript(expression); } @Override public boolean execute() { - return AccessController.doPrivileged((PrivilegedAction) () -> { - Environment valueEnv = buildValueEnv(fields, valueFactory); - ExprValue result = evaluateExpression(valueEnv); - return (Boolean) result.value(); - }); - } - - private Set extractFields(Expression expr) { - Set fields = new HashSet<>(); - expr.accept(new ExpressionNodeVisitor>() { - @Override - public Object visitReference(ReferenceExpression node, Set context) { - context.add(node); - return null; - } - }, fields); - return fields; - } - - private ElasticsearchExprValueFactory buildValueFactory(Set fields) { - Map typeEnv = fields.stream() - .collect(toMap( - ReferenceExpression::getAttr, - ReferenceExpression::type)); - return new ElasticsearchExprValueFactory(typeEnv); - } - - private Environment buildValueEnv( - Set fields, ElasticsearchExprValueFactory valueFactory) { - - Map valueEnv = new HashMap<>(); - for (ReferenceExpression field : fields) { - String fieldName = field.getAttr(); - ExprValue exprValue = valueFactory.construct(fieldName, getDocValue(field)); - valueEnv.put(field, exprValue); - } - return valueEnv::get; // Encapsulate map data structure into anonymous Environment class + return (Boolean) expressionScript.execute(this::getDoc, this::evaluateExpression); } - private Object getDocValue(ReferenceExpression field) { - String fieldName = getDocValueName(field); - ScriptDocValues docValue = getDoc().get(fieldName); - if (docValue == null || docValue.isEmpty()) { - return null; - } - - Object value = docValue.get(0); - if (value instanceof ChronoZonedDateTime) { - return ((ChronoZonedDateTime) value).toInstant(); - } - return castNumberToFieldType(value, field.type()); - } - - /** - * Text field doesn't have doc value (exception thrown even when you call "get") - * Limitation: assume inner field name is always "keyword". - */ - private String getDocValueName(ReferenceExpression field) { - String fieldName = field.getAttr(); - if (field.type() == ES_TEXT_KEYWORD) { - fieldName += ".keyword"; - } - return fieldName; - } - - /** - * DocValue only support long and double so cast to integer and float if needed. - * The doc value must be Long and Double for expr type Long/Integer and Double/Float respectively. - * Otherwise there must be bugs in our engine that causes the mismatch. - */ - private Object castNumberToFieldType(Object value, ExprType type) { - if (type == INTEGER) { - return ((Long) value).intValue(); - } else if (type == FLOAT) { - return ((Double) value).floatValue(); - } else { - return value; - } - } - private ExprValue evaluateExpression(Environment valueEnv) { + private ExprValue evaluateExpression(Expression expression, + Environment valueEnv) { ExprValue result = expression.valueOf(valueEnv); if (result.isNull() || result.isMissing()) { return ExprBooleanValue.of(false); diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/client/ElasticsearchNodeClientTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/client/ElasticsearchNodeClientTest.java index e53fa1a08e..265bfa7c42 100644 --- a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/client/ElasticsearchNodeClientTest.java +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/client/ElasticsearchNodeClientTest.java @@ -28,10 +28,15 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprIntegerValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.value.ElasticsearchExprValueFactory; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.mapping.IndexMapping; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.request.ElasticsearchScrollRequest; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.response.ElasticsearchResponse; import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedMap; import com.google.common.io.Resources; import java.io.IOException; @@ -73,6 +78,15 @@ class ElasticsearchNodeClientTest { @Mock(answer = RETURNS_DEEP_STUBS) private NodeClient nodeClient; + @Mock + private ElasticsearchExprValueFactory factory; + + @Mock + private SearchHit searchHit; + + private ExprTupleValue exprTupleValue = ExprTupleValue.fromExprValueMap(ImmutableMap.of("id", + new ExprIntegerValue(1))); + @Test public void getIndexMappings() throws IOException { URL url = Resources.getResource(TEST_MAPPING_FILE); @@ -153,9 +167,11 @@ public void search() { when(searchResponse.getHits()) .thenReturn( new SearchHits( - new SearchHit[] {new SearchHit(1)}, + new SearchHit[] {searchHit}, new TotalHits(1L, TotalHits.Relation.EQUAL_TO), 1.0F)); + when(searchHit.getSourceAsString()).thenReturn("{\"id\", 1}"); + when(factory.construct(any())).thenReturn(exprTupleValue); // Mock second scroll request followed SearchResponse scrollResponse = mock(SearchResponse.class); @@ -164,13 +180,13 @@ public void search() { when(scrollResponse.getHits()).thenReturn(SearchHits.empty()); // Verify response for first scroll request - ElasticsearchScrollRequest request = new ElasticsearchScrollRequest("test"); + ElasticsearchScrollRequest request = new ElasticsearchScrollRequest("test", factory); ElasticsearchResponse response1 = client.search(request); assertFalse(response1.isEmpty()); - Iterator hits = response1.iterator(); + Iterator hits = response1.iterator(); assertTrue(hits.hasNext()); - assertEquals(new SearchHit(1), hits.next()); + assertEquals(exprTupleValue, hits.next()); assertFalse(hits.hasNext()); // Verify response for second scroll request @@ -208,7 +224,7 @@ void cleanup() { ElasticsearchNodeClient client = new ElasticsearchNodeClient(mock(ClusterService.class), nodeClient); - ElasticsearchScrollRequest request = new ElasticsearchScrollRequest("test"); + ElasticsearchScrollRequest request = new ElasticsearchScrollRequest("test", factory); request.setScrollId("scroll123"); client.cleanup(request); assertFalse(request.isScrollStarted()); @@ -224,7 +240,7 @@ void cleanupWithoutScrollId() { ElasticsearchNodeClient client = new ElasticsearchNodeClient(mock(ClusterService.class), nodeClient); - ElasticsearchScrollRequest request = new ElasticsearchScrollRequest("test"); + ElasticsearchScrollRequest request = new ElasticsearchScrollRequest("test", factory); client.cleanup(request); verify(nodeClient, never()).prepareClearScroll(); } diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/client/ElasticsearchRestClientTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/client/ElasticsearchRestClientTest.java index a91a2e861c..66b30f74a7 100644 --- a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/client/ElasticsearchRestClientTest.java +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/client/ElasticsearchRestClientTest.java @@ -27,6 +27,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprIntegerValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.value.ElasticsearchExprValueFactory; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.mapping.IndexMapping; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.request.ElasticsearchScrollRequest; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.response.ElasticsearchResponse; @@ -67,6 +71,15 @@ class ElasticsearchRestClientTest { private ElasticsearchRestClient client; + @Mock + private ElasticsearchExprValueFactory factory; + + @Mock + private SearchHit searchHit; + + private ExprTupleValue exprTupleValue = ExprTupleValue.fromExprValueMap(ImmutableMap.of("id", + new ExprIntegerValue(1))); + @BeforeEach void setUp() { client = new ElasticsearchRestClient(restClient); @@ -126,9 +139,11 @@ void search() throws IOException { when(searchResponse.getHits()) .thenReturn( new SearchHits( - new SearchHit[] {new SearchHit(1)}, + new SearchHit[] {searchHit}, new TotalHits(1L, TotalHits.Relation.EQUAL_TO), 1.0F)); + when(searchHit.getSourceAsString()).thenReturn("{\"id\", 1}"); + when(factory.construct(any())).thenReturn(exprTupleValue); // Mock second scroll request followed SearchResponse scrollResponse = mock(SearchResponse.class); @@ -137,13 +152,13 @@ void search() throws IOException { when(scrollResponse.getHits()).thenReturn(SearchHits.empty()); // Verify response for first scroll request - ElasticsearchScrollRequest request = new ElasticsearchScrollRequest("test"); + ElasticsearchScrollRequest request = new ElasticsearchScrollRequest("test", factory); ElasticsearchResponse response1 = client.search(request); assertFalse(response1.isEmpty()); - Iterator hits = response1.iterator(); + Iterator hits = response1.iterator(); assertTrue(hits.hasNext()); - assertEquals(new SearchHit(1), hits.next()); + assertEquals(exprTupleValue, hits.next()); assertFalse(hits.hasNext()); // Verify response for second scroll request @@ -155,7 +170,8 @@ void search() throws IOException { void searchWithIOException() throws IOException { when(restClient.search(any(), any())).thenThrow(new IOException()); assertThrows( - IllegalStateException.class, () -> client.search(new ElasticsearchScrollRequest("test"))); + IllegalStateException.class, + () -> client.search(new ElasticsearchScrollRequest("test", factory))); } @Test @@ -175,7 +191,7 @@ void scrollWithIOException() throws IOException { when(restClient.scroll(any(), any())).thenThrow(new IOException()); // First request run successfully - ElasticsearchScrollRequest scrollRequest = new ElasticsearchScrollRequest("test"); + ElasticsearchScrollRequest scrollRequest = new ElasticsearchScrollRequest("test", factory); client.search(scrollRequest); assertThrows( IllegalStateException.class, () -> client.search(scrollRequest)); @@ -193,7 +209,7 @@ void schedule() { @Test void cleanup() throws IOException { - ElasticsearchScrollRequest request = new ElasticsearchScrollRequest("test"); + ElasticsearchScrollRequest request = new ElasticsearchScrollRequest("test", factory); request.setScrollId("scroll123"); client.cleanup(request); verify(restClient).clearScroll(any(), any()); @@ -202,7 +218,7 @@ void cleanup() throws IOException { @Test void cleanupWithoutScrollId() throws IOException { - ElasticsearchScrollRequest request = new ElasticsearchScrollRequest("test"); + ElasticsearchScrollRequest request = new ElasticsearchScrollRequest("test", factory); client.cleanup(request); verify(restClient, never()).clearScroll(any(), any()); } @@ -211,7 +227,7 @@ void cleanupWithoutScrollId() throws IOException { void cleanupWithIOException() throws IOException { when(restClient.clearScroll(any(), any())).thenThrow(new IOException()); - ElasticsearchScrollRequest request = new ElasticsearchScrollRequest("test"); + ElasticsearchScrollRequest request = new ElasticsearchScrollRequest("test", factory); request.setScrollId("scroll123"); assertThrows(IllegalStateException.class, () -> client.cleanup(request)); } diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/data/value/ElasticsearchExprValueFactoryTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/data/value/ElasticsearchExprValueFactoryTest.java index c668a8d897..ac9b302442 100644 --- a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/data/value/ElasticsearchExprValueFactoryTest.java +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/data/value/ElasticsearchExprValueFactoryTest.java @@ -91,6 +91,11 @@ public void constructInteger() { assertEquals(integerValue(1), constructFromObject("intV", 1)); } + @Test + public void constructIntegerValueInStringValue() { + assertEquals(integerValue(1), constructFromObject("intV", "1")); + } + @Test public void constructLong() { assertEquals(longValue(1L), tupleValue("{\"longV\":1}").get("longV")); diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/executor/ElasticsearchExecutionEngineTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/executor/ElasticsearchExecutionEngineTest.java index c91ecd062d..828c8ea14a 100644 --- a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/executor/ElasticsearchExecutionEngineTest.java +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/executor/ElasticsearchExecutionEngineTest.java @@ -16,10 +16,12 @@ package com.amazon.opendistroforelasticsearch.sql.elasticsearch.executor; +import static com.amazon.opendistroforelasticsearch.sql.common.setting.Settings.Key.QUERY_SIZE_LIMIT; import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.tupleValue; import static com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.QueryResponse; import static com.google.common.collect.ImmutableMap.of; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; @@ -29,10 +31,14 @@ import static org.mockito.Mockito.when; import com.amazon.opendistroforelasticsearch.sql.common.response.ResponseListener; +import com.amazon.opendistroforelasticsearch.sql.common.setting.Settings; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.client.ElasticsearchClient; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.value.ElasticsearchExprValueFactory; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.executor.protector.ElasticsearchExecutionProtector; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.ElasticsearchIndexScan; import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine; +import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.ExplainResponse; import com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlan; import com.amazon.opendistroforelasticsearch.sql.storage.TableScanOperator; import java.util.ArrayList; @@ -124,6 +130,52 @@ public void onFailure(Exception e) { verify(plan).close(); } + @Test + void explainSuccessfully() { + ElasticsearchExecutionEngine executor = new ElasticsearchExecutionEngine(client, protector); + Settings settings = mock(Settings.class); + when(settings.getSettingValue(QUERY_SIZE_LIMIT)).thenReturn(100); + PhysicalPlan plan = new ElasticsearchIndexScan(mock(ElasticsearchClient.class), + settings, "test", mock(ElasticsearchExprValueFactory.class)); + + AtomicReference result = new AtomicReference<>(); + executor.explain(plan, new ResponseListener() { + @Override + public void onResponse(ExplainResponse response) { + result.set(response); + } + + @Override + public void onFailure(Exception e) { + fail(e); + } + }); + + assertNotNull(result.get()); + } + + @Test + void explainWithFailure() { + ElasticsearchExecutionEngine executor = new ElasticsearchExecutionEngine(client, protector); + PhysicalPlan plan = mock(PhysicalPlan.class); + when(plan.accept(any(), any())).thenThrow(IllegalStateException.class); + + AtomicReference result = new AtomicReference<>(); + executor.explain(plan, new ResponseListener() { + @Override + public void onResponse(ExplainResponse response) { + fail("Should fail as expected"); + } + + @Override + public void onFailure(Exception e) { + result.set(e); + } + }); + + assertNotNull(result.get()); + } + @RequiredArgsConstructor private static class FakePhysicalPlan extends TableScanOperator { private final Iterator it; 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 71389a4aed..de11d4a812 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 @@ -40,8 +40,8 @@ import com.amazon.opendistroforelasticsearch.sql.expression.Expression; 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.aggregation.AvgAggregator; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.NamedAggregator; import com.amazon.opendistroforelasticsearch.sql.monitor.ResourceMonitor; import com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlan; import com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlanDSL; @@ -88,9 +88,15 @@ public void testProtectIndexScan() { ReferenceExpression exclude = ref("name", STRING); ReferenceExpression dedupeField = ref("name", STRING); ReferenceExpression topField = ref("name", STRING); + List topExprs = Arrays.asList(ref("age", INTEGER)); Expression filterExpr = literal(ExprBooleanValue.of(true)); - List groupByExprs = Arrays.asList(ref("age", INTEGER)); - List aggregators = Arrays.asList(new AvgAggregator(groupByExprs, DOUBLE)); + Expression whileExpr = literal(ExprBooleanValue.of(true)); + Boolean keepLast = false; + Integer headNumber = 5; + List groupByExprs = Arrays.asList(named("age", ref("age", INTEGER))); + List aggregators = + Arrays.asList(named("avg(age)", new AvgAggregator(Arrays.asList(ref("age", INTEGER)), + DOUBLE))); Map mappings = ImmutableMap.of(ref("name", STRING), ref("lastname", STRING)); Pair newEvalField = @@ -108,11 +114,15 @@ public void testProtectIndexScan() { PhysicalPlanDSL.remove( PhysicalPlanDSL.rename( PhysicalPlanDSL.agg( - filter( - resourceMonitor( + PhysicalPlanDSL.head( + filter( + resourceMonitor( new ElasticsearchIndexScan( client, settings, indexName, exprValueFactory)), - filterExpr), + filterExpr), + keepLast, + whileExpr, + headNumber), aggregators, groupByExprs), mappings), @@ -121,7 +131,7 @@ public void testProtectIndexScan() { sortCount, sortField), CommandType.TOP, - groupByExprs, + topExprs, topField), dedupeField), include), @@ -134,10 +144,15 @@ public void testProtectIndexScan() { PhysicalPlanDSL.remove( PhysicalPlanDSL.rename( PhysicalPlanDSL.agg( - filter( - new ElasticsearchIndexScan( - client, settings, indexName, exprValueFactory), - filterExpr), + PhysicalPlanDSL.head( + filter( + new ElasticsearchIndexScan( + client, settings, indexName, + exprValueFactory), + filterExpr), + keepLast, + whileExpr, + headNumber), aggregators, groupByExprs), mappings), @@ -146,7 +161,7 @@ public void testProtectIndexScan() { sortCount, sortField), CommandType.TOP, - groupByExprs, + topExprs, topField), dedupeField), include))); diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchQueryRequestTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchQueryRequestTest.java index de6ab040f3..47b13f2a10 100644 --- a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchQueryRequestTest.java +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchQueryRequestTest.java @@ -21,13 +21,12 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.value.ElasticsearchExprValueFactory; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.response.ElasticsearchResponse; import java.util.function.Consumer; import java.util.function.Function; @@ -64,13 +63,17 @@ public class ElasticsearchQueryRequestTest { @Mock private SearchHit searchHit; - private final ElasticsearchQueryRequest request = new ElasticsearchQueryRequest("test", 200); + @Mock + private ElasticsearchExprValueFactory factory; + + private final ElasticsearchQueryRequest request = + new ElasticsearchQueryRequest("test", 200, factory); @Test void search() { when(searchAction.apply(any())).thenReturn(searchResponse); when(searchResponse.getHits()).thenReturn(searchHits); - when(searchHits.getHits()).thenReturn(new SearchHit[]{searchHit}); + when(searchHits.getHits()).thenReturn(new SearchHit[] {searchHit}); ElasticsearchResponse searchResponse = request.search(searchAction, scrollAction); assertFalse(searchResponse.isEmpty()); diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchScrollRequestTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchScrollRequestTest.java index b836c48f7b..cf78655a87 100644 --- a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchScrollRequestTest.java +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/request/ElasticsearchScrollRequestTest.java @@ -20,15 +20,24 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.value.ElasticsearchExprValueFactory; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchScrollRequest; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +@ExtendWith(MockitoExtension.class) class ElasticsearchScrollRequestTest { - private final ElasticsearchScrollRequest request = new ElasticsearchScrollRequest("test"); + @Mock + private ElasticsearchExprValueFactory factory; + + private final ElasticsearchScrollRequest request = + new ElasticsearchScrollRequest("test", factory); @Test void searchRequest() { diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/AggregationResponseUtils.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/AggregationResponseUtils.java new file mode 100644 index 0000000000..f75b618d23 --- /dev/null +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/AggregationResponseUtils.java @@ -0,0 +1,102 @@ +/* + * + * 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.elasticsearch.response; + +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ContextParser; +import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContent; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.composite.ParsedComposite; +import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.histogram.ParsedDateHistogram; +import org.elasticsearch.search.aggregations.bucket.terms.DoubleTerms; +import org.elasticsearch.search.aggregations.bucket.terms.LongTerms; +import org.elasticsearch.search.aggregations.bucket.terms.ParsedDoubleTerms; +import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms; +import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms; +import org.elasticsearch.search.aggregations.bucket.terms.StringTerms; +import org.elasticsearch.search.aggregations.metrics.AvgAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.MaxAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.MinAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.ParsedAvg; +import org.elasticsearch.search.aggregations.metrics.ParsedMax; +import org.elasticsearch.search.aggregations.metrics.ParsedMin; +import org.elasticsearch.search.aggregations.metrics.ParsedSum; +import org.elasticsearch.search.aggregations.metrics.ParsedValueCount; +import org.elasticsearch.search.aggregations.metrics.SumAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.ValueCountAggregationBuilder; +import org.elasticsearch.search.aggregations.pipeline.ParsedPercentilesBucket; +import org.elasticsearch.search.aggregations.pipeline.PercentilesBucketPipelineAggregationBuilder; + +public class AggregationResponseUtils { + private static final List entryList = + new ImmutableMap.Builder>().put( + MinAggregationBuilder.NAME, (p, c) -> ParsedMin.fromXContent(p, (String) c)) + .put(MaxAggregationBuilder.NAME, (p, c) -> ParsedMax.fromXContent(p, (String) c)) + .put(SumAggregationBuilder.NAME, (p, c) -> ParsedSum.fromXContent(p, (String) c)) + .put(AvgAggregationBuilder.NAME, (p, c) -> ParsedAvg.fromXContent(p, (String) c)) + .put(StringTerms.NAME, (p, c) -> ParsedStringTerms.fromXContent(p, (String) c)) + .put(LongTerms.NAME, (p, c) -> ParsedLongTerms.fromXContent(p, (String) c)) + .put(DoubleTerms.NAME, (p, c) -> ParsedDoubleTerms.fromXContent(p, (String) c)) + .put(ValueCountAggregationBuilder.NAME, + (p, c) -> ParsedValueCount.fromXContent(p, (String) c)) + .put(PercentilesBucketPipelineAggregationBuilder.NAME, + (p, c) -> ParsedPercentilesBucket.fromXContent(p, (String) c)) + .put(DateHistogramAggregationBuilder.NAME, + (p, c) -> ParsedDateHistogram.fromXContent(p, (String) c)) + .put(CompositeAggregationBuilder.NAME, + (p, c) -> ParsedComposite.fromXContent(p, (String) c)) + .build() + .entrySet() + .stream() + .map(entry -> new NamedXContentRegistry.Entry(Aggregation.class, + new ParseField(entry.getKey()), + entry.getValue())) + .collect(Collectors.toList()); + private static final NamedXContentRegistry namedXContentRegistry = + new NamedXContentRegistry(entryList); + private static final XContent xContent = XContentFactory.xContent(XContentType.JSON); + + /** + * Populate {@link Aggregations} from JSON string. + * + * @param json json string + * @return {@link Aggregations} + */ + public static Aggregations fromJson(String json) { + try { + XContentParser contentParser = + xContent.createParser(namedXContentRegistry, LoggingDeprecationHandler.INSTANCE, json); + contentParser.nextToken(); + return Aggregations.fromXContent(contentParser); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/ElasticsearchAggregationResponseParserTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/ElasticsearchAggregationResponseParserTest.java new file mode 100644 index 0000000000..f873f0cb06 --- /dev/null +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/ElasticsearchAggregationResponseParserTest.java @@ -0,0 +1,160 @@ +/* + * + * 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.elasticsearch.response; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.google.common.collect.ImmutableMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ElasticsearchAggregationResponseParserTest { + + /** + * SELECT MAX(age) as max FROM accounts. + */ + @Test + void no_bucket_one_metric_should_pass() { + String response = "{\n" + + " \"max#max\": {\n" + + " \"value\": 40\n" + + " }\n" + + "}"; + assertThat(parse(response), contains(entry("max", 40d))); + } + + /** + * SELECT MAX(age) as max, MIN(age) as min FROM accounts. + */ + @Test + void no_bucket_two_metric_should_pass() { + String response = "{\n" + + " \"max#max\": {\n" + + " \"value\": 40\n" + + " },\n" + + " \"min#min\": {\n" + + " \"value\": 20\n" + + " }\n" + + "}"; + assertThat(parse(response), + containsInAnyOrder(entry("max", 40d), entry("min", 20d))); + } + + @Test + void one_bucket_one_metric_should_pass() { + String response = "{\n" + + " \"composite#composite_buckets\": {\n" + + " \"after_key\": {\n" + + " \"type\": \"sale\"\n" + + " },\n" + + " \"buckets\": [\n" + + " {\n" + + " \"key\": {\n" + + " \"type\": \"cost\"\n" + + " },\n" + + " \"doc_count\": 2,\n" + + " \"avg#avg\": {\n" + + " \"value\": 20\n" + + " }\n" + + " },\n" + + " {\n" + + " \"key\": {\n" + + " \"type\": \"sale\"\n" + + " },\n" + + " \"doc_count\": 2,\n" + + " \"avg#avg\": {\n" + + " \"value\": 105\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + assertThat(parse(response), + containsInAnyOrder(ImmutableMap.of("type", "cost", "avg", 20d), + ImmutableMap.of("type", "sale", "avg", 105d))); + } + + @Test + void two_bucket_one_metric_should_pass() { + String response = "{\n" + + " \"composite#composite_buckets\": {\n" + + " \"after_key\": {\n" + + " \"type\": \"sale\",\n" + + " \"region\": \"us\"\n" + + " },\n" + + " \"buckets\": [\n" + + " {\n" + + " \"key\": {\n" + + " \"type\": \"cost\",\n" + + " \"region\": \"us\"\n" + + " },\n" + + " \"avg#avg\": {\n" + + " \"value\": 20\n" + + " }\n" + + " },\n" + + " {\n" + + " \"key\": {\n" + + " \"type\": \"sale\",\n" + + " \"region\": \"uk\"\n" + + " },\n" + + " \"avg#avg\": {\n" + + " \"value\": 130\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + assertThat(parse(response), + containsInAnyOrder(ImmutableMap.of("type", "cost", "region", "us", "avg", 20d), + ImmutableMap.of("type", "sale", "region", "uk", "avg", 130d))); + } + + @Test + void unsupported_aggregation_should_fail() { + String response = "{\n" + + " \"date_histogram#max\": {\n" + + " \"value\": 40\n" + + " }\n" + + "}"; + IllegalStateException exception = + assertThrows(IllegalStateException.class, () -> parse(response)); + assertEquals("unsupported aggregation type date_histogram", exception.getMessage()); + } + + @Test + void nan_value_should_return_null() { + assertNull(ElasticsearchAggregationResponseParser.handleNanValue(Double.NaN)); + } + + public List> parse(String json) { + return ElasticsearchAggregationResponseParser.parse(AggregationResponseUtils.fromJson(json)); + } + + public Map entry(String name, Object value) { + return ImmutableMap.of(name, value); + } +} \ No newline at end of file diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/ElasticsearchResponseTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/ElasticsearchResponseTest.java index 68ea1af73d..2746a64885 100644 --- a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/ElasticsearchResponseTest.java +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/response/ElasticsearchResponseTest.java @@ -20,60 +20,138 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprIntegerValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.value.ElasticsearchExprValueFactory; +import com.google.common.collect.ImmutableMap; +import java.util.Arrays; import org.apache.lucene.search.TotalHits; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; -import org.junit.jupiter.api.BeforeEach; +import org.elasticsearch.search.aggregations.Aggregations; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class ElasticsearchResponseTest { - @Mock private SearchResponse esResponse; + @Mock + private SearchResponse esResponse; - @BeforeEach - void setUp() { + @Mock + private ElasticsearchExprValueFactory factory; + + @Mock + private SearchHit searchHit1; + + @Mock + private SearchHit searchHit2; + + @Mock + private Aggregations aggregations; + + private ExprTupleValue exprTupleValue1 = ExprTupleValue.fromExprValueMap(ImmutableMap.of("id1", + new ExprIntegerValue(1))); + + private ExprTupleValue exprTupleValue2 = ExprTupleValue.fromExprValueMap(ImmutableMap.of("id2", + new ExprIntegerValue(2))); + + @Test + void isEmpty() { when(esResponse.getHits()) .thenReturn( new SearchHits( - new SearchHit[] {new SearchHit(1), new SearchHit(2)}, + new SearchHit[] {searchHit1, searchHit2}, new TotalHits(2L, TotalHits.Relation.EQUAL_TO), 1.0F)); - } - @Test - void isEmpty() { - ElasticsearchResponse response1 = new ElasticsearchResponse(esResponse); + ElasticsearchResponse response1 = new ElasticsearchResponse(esResponse, factory); assertFalse(response1.isEmpty()); when(esResponse.getHits()).thenReturn(SearchHits.empty()); - ElasticsearchResponse response2 = new ElasticsearchResponse(esResponse); + ElasticsearchResponse response2 = new ElasticsearchResponse(esResponse, factory); assertTrue(response2.isEmpty()); when(esResponse.getHits()) .thenReturn(new SearchHits(null, new TotalHits(0, TotalHits.Relation.EQUAL_TO), 0)); - ElasticsearchResponse response3 = new ElasticsearchResponse(esResponse); + ElasticsearchResponse response3 = new ElasticsearchResponse(esResponse, factory); assertTrue(response3.isEmpty()); } @Test void iterator() { + when(esResponse.getHits()) + .thenReturn( + new SearchHits( + new SearchHit[] {searchHit1, searchHit2}, + new TotalHits(2L, TotalHits.Relation.EQUAL_TO), + 1.0F)); + + when(searchHit1.getSourceAsString()).thenReturn("{\"id1\", 1}"); + when(searchHit2.getSourceAsString()).thenReturn("{\"id1\", 2}"); + when(factory.construct(any())).thenReturn(exprTupleValue1).thenReturn(exprTupleValue2); + int i = 0; - for (SearchHit hit : new ElasticsearchResponse(esResponse)) { + for (ExprValue hit : new ElasticsearchResponse(esResponse, factory)) { if (i == 0) { - assertEquals(new SearchHit(1), hit); + assertEquals(exprTupleValue1, hit); } else if (i == 1) { - assertEquals(new SearchHit(2), hit); + assertEquals(exprTupleValue2, hit); } else { fail("More search hits returned than expected"); } i++; } } + + @Test + void response_is_aggregation_when_aggregation_not_empty() { + when(esResponse.getAggregations()).thenReturn(aggregations); + + ElasticsearchResponse response = new ElasticsearchResponse(esResponse, factory); + assertTrue(response.isAggregationResponse()); + } + + @Test + void response_isnot_aggregation_when_aggregation_is_empty() { + when(esResponse.getAggregations()).thenReturn(null); + + ElasticsearchResponse response = new ElasticsearchResponse(esResponse, factory); + assertFalse(response.isAggregationResponse()); + } + + @Test + void aggregation_iterator() { + try ( + MockedStatic mockedStatic = Mockito + .mockStatic(ElasticsearchAggregationResponseParser.class)) { + when(ElasticsearchAggregationResponseParser.parse(any())) + .thenReturn(Arrays.asList(ImmutableMap.of("id1", 1), ImmutableMap.of("id2", 2))); + when(esResponse.getAggregations()).thenReturn(aggregations); + when(factory.construct(anyString(), any())).thenReturn(new ExprIntegerValue(1)) + .thenReturn(new ExprIntegerValue(2)); + + int i = 0; + for (ExprValue hit : new ElasticsearchResponse(esResponse, factory)) { + if (i == 0) { + assertEquals(exprTupleValue1, hit); + } else if (i == 1) { + assertEquals(exprTupleValue2, hit); + } else { + fail("More search hits returned than expected"); + } + i++; + } + } + } } diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/ElasticsearchIndexScanTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/ElasticsearchIndexScanTest.java index 5a41dfd2fe..aa9790068a 100644 --- a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/ElasticsearchIndexScanTest.java +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/ElasticsearchIndexScanTest.java @@ -81,21 +81,21 @@ void queryEmptyResult() { @Test void queryAllResults() { mockResponse( - new SearchHit[]{employee(1, "John", "IT"), employee(2, "Smith", "HR")}, - new SearchHit[]{employee(3, "Allen", "IT")}); + new ExprValue[]{employee(1, "John", "IT"), employee(2, "Smith", "HR")}, + new ExprValue[]{employee(3, "Allen", "IT")}); try (ElasticsearchIndexScan indexScan = new ElasticsearchIndexScan(client, settings, "employees", exprValueFactory)) { indexScan.open(); assertTrue(indexScan.hasNext()); - assertEquals(tupleValue(employee(1, "John", "IT")), indexScan.next()); + assertEquals(employee(1, "John", "IT"), indexScan.next()); assertTrue(indexScan.hasNext()); - assertEquals(tupleValue(employee(2, "Smith", "HR")), indexScan.next()); + assertEquals(employee(2, "Smith", "HR"), indexScan.next()); assertTrue(indexScan.hasNext()); - assertEquals(tupleValue(employee(3, "Allen", "IT")), indexScan.next()); + assertEquals(employee(3, "Allen", "IT"), indexScan.next()); assertFalse(indexScan.hasNext()); } @@ -128,6 +128,7 @@ private static class PushDownAssertion { private final ElasticsearchClient client; private final ElasticsearchIndexScan indexScan; private final ElasticsearchResponse response; + private final ElasticsearchExprValueFactory factory; public PushDownAssertion(ElasticsearchClient client, ElasticsearchExprValueFactory valueFactory, @@ -135,6 +136,7 @@ public PushDownAssertion(ElasticsearchClient client, this.client = client; this.indexScan = new ElasticsearchIndexScan(client, settings, "test", valueFactory); this.response = mock(ElasticsearchResponse.class); + this.factory = valueFactory; when(response.isEmpty()).thenReturn(true); } @@ -144,7 +146,7 @@ PushDownAssertion pushDown(QueryBuilder query) { } PushDownAssertion shouldQuery(QueryBuilder expected) { - ElasticsearchRequest request = new ElasticsearchQueryRequest("test", 200); + ElasticsearchRequest request = new ElasticsearchQueryRequest("test", 200, factory); request.getSourceBuilder() .query(expected) .sort(DOC_FIELD_NAME, ASC); @@ -155,7 +157,7 @@ PushDownAssertion shouldQuery(QueryBuilder expected) { } - private void mockResponse(SearchHit[]... searchHitBatches) { + private void mockResponse(ExprValue[]... searchHitBatches) { when(client.search(any())) .thenAnswer( new Answer() { @@ -167,7 +169,7 @@ public ElasticsearchResponse answer(InvocationOnMock invocation) { int totalBatch = searchHitBatches.length; if (batchNum < totalBatch) { when(response.isEmpty()).thenReturn(false); - SearchHit[] searchHit = searchHitBatches[batchNum]; + ExprValue[] searchHit = searchHitBatches[batchNum]; when(response.iterator()).thenReturn(Arrays.asList(searchHit).iterator()); } else if (batchNum == totalBatch) { when(response.isEmpty()).thenReturn(true); @@ -181,11 +183,11 @@ public ElasticsearchResponse answer(InvocationOnMock invocation) { }); } - protected SearchHit employee(int docId, String name, String department) { + protected ExprValue employee(int docId, String name, String department) { SearchHit hit = new SearchHit(docId); hit.sourceRef( new BytesArray("{\"name\":\"" + name + "\",\"department\":\"" + department + "\"}")); - return hit; + return tupleValue(hit); } private ExprValue tupleValue(SearchHit hit) { diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/ElasticsearchIndexTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/ElasticsearchIndexTest.java index 709ba51086..f204d6fba9 100644 --- a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/ElasticsearchIndexTest.java +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/ElasticsearchIndexTest.java @@ -51,11 +51,12 @@ import com.amazon.opendistroforelasticsearch.sql.expression.Expression; 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.aggregation.AvgAggregator; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.NamedAggregator; import com.amazon.opendistroforelasticsearch.sql.expression.config.ExpressionConfig; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlan; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlanDSL; +import com.amazon.opendistroforelasticsearch.sql.planner.physical.AggregationOperator; import com.amazon.opendistroforelasticsearch.sql.planner.physical.FilterOperator; import com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlan; import com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlanDSL; @@ -145,8 +146,10 @@ void implementOtherLogicalOperators() { ReferenceExpression exclude = ref("name", STRING); ReferenceExpression dedupeField = ref("name", STRING); Expression filterExpr = literal(ExprBooleanValue.of(true)); - List groupByExprs = Arrays.asList(ref("age", INTEGER)); - List aggregators = Arrays.asList(new AvgAggregator(groupByExprs, DOUBLE)); + List groupByExprs = Arrays.asList(named("age", ref("age", INTEGER))); + List aggregators = + Arrays.asList(named("avg(age)", new AvgAggregator(Arrays.asList(ref("age", INTEGER)), + DOUBLE))); Map mappings = ImmutableMap.of(ref("name", STRING), ref("lastname", STRING)); Pair newEvalField = @@ -162,10 +165,7 @@ void implementOtherLogicalOperators() { eval( remove( rename( - aggregation( - relation(indexName), - aggregators, - groupByExprs), + relation(indexName), mappings), exclude), newEvalField), @@ -182,11 +182,8 @@ void implementOtherLogicalOperators() { PhysicalPlanDSL.eval( PhysicalPlanDSL.remove( PhysicalPlanDSL.rename( - PhysicalPlanDSL.agg( - new ElasticsearchIndexScan( - client, settings, indexName, exprValueFactory), - aggregators, - groupByExprs), + new ElasticsearchIndexScan(client, settings, indexName, + exprValueFactory), mappings), exclude), newEvalField), @@ -225,21 +222,80 @@ void shouldNotPushDownFilterFarFromRelation() { ReferenceExpression field = ref("name", STRING); Expression filterExpr = dsl.equal(field, literal("John")); - List groupByExprs = Arrays.asList(ref("age", INTEGER)); - List aggregators = Arrays.asList(new AvgAggregator(groupByExprs, DOUBLE)); + List groupByExprs = Arrays.asList(named("age", ref("age", INTEGER))); + List aggregators = + Arrays.asList(named("avg(age)", new AvgAggregator(Arrays.asList(ref("age", INTEGER)), + DOUBLE))); String indexName = "test"; ElasticsearchIndex index = new ElasticsearchIndex(client, settings, indexName); PhysicalPlan plan = index.implement( - filter( - aggregation( - relation(indexName), - aggregators, - groupByExprs - ), - filterExpr)); + filter( + aggregation( + relation(indexName), + aggregators, + groupByExprs + ), + filterExpr)); assertTrue(plan instanceof FilterOperator); } + @Test + void shouldPushDownAggregation() { + when(settings.getSettingValue(Settings.Key.QUERY_SIZE_LIMIT)).thenReturn(200); + + ReferenceExpression field = ref("name", STRING); + Expression filterExpr = dsl.equal(field, literal("John")); + List groupByExprs = Arrays.asList(named("age", ref("age", INTEGER))); + List aggregators = + Arrays.asList(named("avg(age)", new AvgAggregator(Arrays.asList(ref("age", INTEGER)), + DOUBLE))); + + String indexName = "test"; + ElasticsearchIndex index = new ElasticsearchIndex(client, settings, indexName); + PhysicalPlan plan = index.implement( + filter( + aggregation( + relation(indexName), + aggregators, + groupByExprs + ), + filterExpr)); + + assertTrue(plan.getChild().get(0) instanceof ElasticsearchIndexScan); + + plan = index.implement( + aggregation( + filter( + relation(indexName), + filterExpr), + aggregators, + groupByExprs)); + assertTrue(plan instanceof ElasticsearchIndexScan); + } + + @Test + void shouldNotPushDownAggregationFarFromRelation() { + when(settings.getSettingValue(Settings.Key.QUERY_SIZE_LIMIT)).thenReturn(200); + + ReferenceExpression field = ref("name", STRING); + Expression filterExpr = dsl.equal(field, literal("John")); + List groupByExprs = Arrays.asList(named("age", ref("age", INTEGER))); + List aggregators = + Arrays.asList(named("avg(age)", new AvgAggregator(Arrays.asList(ref("age", INTEGER)), + DOUBLE))); + + String indexName = "test"; + ElasticsearchIndex index = new ElasticsearchIndex(client, settings, indexName); + + PhysicalPlan plan = index.implement( + aggregation( + filter(filter( + relation(indexName), + filterExpr), filterExpr), + aggregators, + groupByExprs)); + assertTrue(plan instanceof AggregationOperator); + } } diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/ExpressionScriptEngineTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/ExpressionScriptEngineTest.java index 9be9087527..eda964f6dd 100644 --- a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/ExpressionScriptEngineTest.java +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/ExpressionScriptEngineTest.java @@ -28,6 +28,7 @@ import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.serialization.ExpressionSerializer; import com.amazon.opendistroforelasticsearch.sql.expression.DSL; import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import org.elasticsearch.script.AggregationScript; import org.elasticsearch.script.FilterScript; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptEngine; @@ -65,7 +66,7 @@ void can_initialize_filter_script_factory_by_compiled_script() { when(serializer.deserialize("test code")).thenReturn(expression); assertThat(scriptEngine.getSupportedContexts(), - contains(FilterScript.CONTEXT)); + contains(FilterScript.CONTEXT, AggregationScript.CONTEXT)); Object actualFactory = scriptEngine.compile( "test", "test code", FilterScript.CONTEXT, emptyMap()); diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/AggregationQueryBuilderTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/AggregationQueryBuilderTest.java new file mode 100644 index 0000000000..537fe2f200 --- /dev/null +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/AggregationQueryBuilderTest.java @@ -0,0 +1,263 @@ +/* + * + * 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.elasticsearch.storage.script.aggregation; + +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.DOUBLE; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRING; +import static com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.type.ElasticsearchDataType.ES_TEXT_KEYWORD; +import static com.amazon.opendistroforelasticsearch.sql.expression.DSL.named; +import static com.amazon.opendistroforelasticsearch.sql.expression.DSL.ref; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; + +import com.amazon.opendistroforelasticsearch.sql.data.type.ExprType; +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.serialization.ExpressionSerializer; +import com.amazon.opendistroforelasticsearch.sql.expression.DSL; +import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import com.amazon.opendistroforelasticsearch.sql.expression.NamedExpression; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.AvgAggregator; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.NamedAggregator; +import com.amazon.opendistroforelasticsearch.sql.expression.config.ExpressionConfig; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@ExtendWith(MockitoExtension.class) +class AggregationQueryBuilderTest { + + private final DSL dsl = new ExpressionConfig().dsl(new ExpressionConfig().functionRepository()); + + @Mock + private ExpressionSerializer serializer; + + private AggregationQueryBuilder queryBuilder; + + @BeforeEach + void set_up() { + queryBuilder = new AggregationQueryBuilder(serializer); + } + + @Test + void should_build_composite_aggregation_for_field_reference() { + assertEquals( + "{\n" + + " \"composite_buckets\" : {\n" + + " \"composite\" : {\n" + + " \"size\" : 1000,\n" + + " \"sources\" : [ {\n" + + " \"name\" : {\n" + + " \"terms\" : {\n" + + " \"field\" : \"name\",\n" + + " \"missing_bucket\" : true,\n" + + " \"order\" : \"asc\"\n" + + " }\n" + + " }\n" + + " } ]\n" + + " },\n" + + " \"aggregations\" : {\n" + + " \"avg(age)\" : {\n" + + " \"avg\" : {\n" + + " \"field\" : \"age\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}", + buildQuery( + Arrays.asList( + named("avg(age)", new AvgAggregator(Arrays.asList(ref("age", INTEGER)), INTEGER))), + Arrays.asList(named("name", ref("name", STRING))))); + } + + @Test + void should_build_type_mapping_for_field_reference() { + assertThat( + buildTypeMapping(Arrays.asList( + named("avg(age)", new AvgAggregator(Arrays.asList(ref("age", INTEGER)), INTEGER))), + Arrays.asList(named("name", ref("name", STRING)))), + containsInAnyOrder( + map("avg(age)", INTEGER), + map("name", STRING) + )); + } + + @Test + void should_build_composite_aggregation_for_field_reference_of_keyword() { + assertEquals( + "{\n" + + " \"composite_buckets\" : {\n" + + " \"composite\" : {\n" + + " \"size\" : 1000,\n" + + " \"sources\" : [ {\n" + + " \"name\" : {\n" + + " \"terms\" : {\n" + + " \"field\" : \"name.keyword\",\n" + + " \"missing_bucket\" : true,\n" + + " \"order\" : \"asc\"\n" + + " }\n" + + " }\n" + + " } ]\n" + + " },\n" + + " \"aggregations\" : {\n" + + " \"avg(age)\" : {\n" + + " \"avg\" : {\n" + + " \"field\" : \"age\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}", + buildQuery( + Arrays.asList( + named("avg(age)", new AvgAggregator(Arrays.asList(ref("age", INTEGER)), INTEGER))), + Arrays.asList(named("name", ref("name", ES_TEXT_KEYWORD))))); + } + + @Test + void should_build_type_mapping_for_field_reference_of_keyword() { + assertThat( + buildTypeMapping(Arrays.asList( + named("avg(age)", new AvgAggregator(Arrays.asList(ref("age", INTEGER)), INTEGER))), + Arrays.asList(named("name", ref("name", ES_TEXT_KEYWORD)))), + containsInAnyOrder( + map("avg(age)", INTEGER), + map("name", ES_TEXT_KEYWORD) + )); + } + + @Test + void should_build_composite_aggregation_for_expression() { + doAnswer(invocation -> { + Expression expr = invocation.getArgument(0); + return expr.toString(); + }).when(serializer).serialize(any()); + assertEquals( + "{\n" + + " \"composite_buckets\" : {\n" + + " \"composite\" : {\n" + + " \"size\" : 1000,\n" + + " \"sources\" : [ {\n" + + " \"age\" : {\n" + + " \"terms\" : {\n" + + " \"script\" : {\n" + + " \"source\" : \"asin(age)\",\n" + + " \"lang\" : \"opendistro_expression\"\n" + + " },\n" + + " \"missing_bucket\" : true,\n" + + " \"order\" : \"asc\"\n" + + " }\n" + + " }\n" + + " } ]\n" + + " },\n" + + " \"aggregations\" : {\n" + + " \"avg(balance)\" : {\n" + + " \"avg\" : {\n" + + " \"script\" : {\n" + + " \"source\" : \"abs(balance)\",\n" + + " \"lang\" : \"opendistro_expression\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}", + buildQuery( + Arrays.asList( + named("avg(balance)", new AvgAggregator( + Arrays.asList(dsl.abs(ref("balance", INTEGER))), INTEGER))), + Arrays.asList(named("age", dsl.asin(ref("age", INTEGER)))))); + } + + @Test + void should_build_type_mapping_for_expression() { + assertThat( + buildTypeMapping(Arrays.asList( + named("avg(balance)", new AvgAggregator( + Arrays.asList(dsl.abs(ref("balance", INTEGER))), INTEGER))), + Arrays.asList(named("age", dsl.asin(ref("age", INTEGER))))), + containsInAnyOrder( + map("avg(balance)", INTEGER), + map("age", DOUBLE) + )); + } + + @Test + void should_build_aggregation_without_bucket() { + assertEquals( + "{\n" + + " \"avg(balance)\" : {\n" + + " \"avg\" : {\n" + + " \"field\" : \"balance\"\n" + + " }\n" + + " }\n" + + "}", + buildQuery( + Arrays.asList( + named("avg(balance)", new AvgAggregator( + Arrays.asList(ref("balance", INTEGER)), INTEGER))), + Collections.emptyList())); + } + + @Test + void should_build_type_mapping_without_bucket() { + assertThat( + buildTypeMapping(Arrays.asList( + named("avg(balance)", new AvgAggregator( + Arrays.asList(ref("balance", INTEGER)), INTEGER))), + Collections.emptyList()), + containsInAnyOrder( + map("avg(balance)", INTEGER) + )); + } + + @SneakyThrows + private String buildQuery(List namedAggregatorList, + List groupByList) { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readTree( + queryBuilder.buildAggregationBuilder(namedAggregatorList, groupByList).get(0).toString()) + .toPrettyString(); + } + + private Set> buildTypeMapping( + List namedAggregatorList, + List groupByList) { + return queryBuilder.buildTypeMapping(namedAggregatorList, groupByList).entrySet(); + } + + private Map.Entry map(String name, ExprType type) { + return new AbstractMap.SimpleEntry(name, type); + } +} \ No newline at end of file diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScriptFactoryTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScriptFactoryTest.java new file mode 100644 index 0000000000..afd5691ace --- /dev/null +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScriptFactoryTest.java @@ -0,0 +1,80 @@ +/* + * + * 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.elasticsearch.storage.script.aggregation; + +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 static org.mockito.Mockito.when; + +import com.amazon.opendistroforelasticsearch.sql.expression.DSL; +import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import org.apache.lucene.index.LeafReaderContext; +import org.elasticsearch.script.AggregationScript; +import org.elasticsearch.search.lookup.LeafSearchLookup; +import org.elasticsearch.search.lookup.SearchLookup; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@ExtendWith(MockitoExtension.class) +class ExpressionAggregationScriptFactoryTest { + + @Mock + private SearchLookup searchLookup; + + @Mock + private LeafSearchLookup leafSearchLookup; + + @Mock + private LeafReaderContext leafReaderContext; + + private final Expression expression = DSL.literal(true); + + private final Map params = Collections.emptyMap(); + + private final AggregationScript.Factory factory = + new ExpressionAggregationScriptFactory(expression); + + @Test + void should_return_deterministic_result() { + assertTrue(factory.isResultDeterministic()); + } + + @Test + void can_initialize_expression_filter_script() throws IOException { + when(searchLookup.getLeafSearchLookup(leafReaderContext)).thenReturn(leafSearchLookup); + + AggregationScript.LeafFactory leafFactory = factory.newFactory(params, searchLookup); + assertFalse(leafFactory.needs_score()); + + AggregationScript actualScript = leafFactory.newInstance(leafReaderContext); + + assertEquals( + new ExpressionAggregationScript(expression, searchLookup, leafReaderContext, params), + actualScript + ); + } +} \ No newline at end of file diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScriptTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScriptTest.java new file mode 100644 index 0000000000..0b8bb41187 --- /dev/null +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/ExpressionAggregationScriptTest.java @@ -0,0 +1,182 @@ +/* + * + * 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.elasticsearch.storage.script.aggregation; + +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRING; +import static com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.type.ElasticsearchDataType.ES_TEXT_KEYWORD; +import static com.amazon.opendistroforelasticsearch.sql.expression.DSL.literal; +import static com.amazon.opendistroforelasticsearch.sql.expression.DSL.ref; +import static java.util.Collections.emptyMap; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.amazon.opendistroforelasticsearch.sql.expression.DSL; +import com.amazon.opendistroforelasticsearch.sql.expression.Expression; +import com.amazon.opendistroforelasticsearch.sql.expression.config.ExpressionConfig; +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.apache.lucene.index.LeafReaderContext; +import org.elasticsearch.index.fielddata.ScriptDocValues; +import org.elasticsearch.search.lookup.LeafDocLookup; +import org.elasticsearch.search.lookup.LeafSearchLookup; +import org.elasticsearch.search.lookup.SearchLookup; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@ExtendWith(MockitoExtension.class) +class ExpressionAggregationScriptTest { + + private final DSL dsl = new ExpressionConfig().dsl(new ExpressionConfig().functionRepository()); + + @Mock + private SearchLookup lookup; + + @Mock + private LeafSearchLookup leafLookup; + + @Mock + private LeafReaderContext context; + + @Test + void can_execute_expression_with_integer_field() { + assertThat() + .docValues("age", 30L) // DocValue only supports long + .evaluate( + dsl.abs(ref("age", INTEGER))) + .shouldMatch(30); + } + + @Test + void can_execute_expression_with_integer_field_with_boolean_result() { + assertThat() + .docValues("age", 30L) // DocValue only supports long + .evaluate( + dsl.greater(ref("age", INTEGER), literal(20))) + .shouldMatch(true); + } + + @Test + void can_execute_expression_with_text_keyword_field() { + assertThat() + .docValues("name.keyword", "John") + .evaluate( + dsl.equal(ref("name", ES_TEXT_KEYWORD), literal("John"))) + .shouldMatch(true); + } + + @Test + void can_execute_expression_with_null_field() { + assertThat() + .docValues("age", null) + .evaluate(ref("age", INTEGER)) + .shouldMatch(null); + } + + @Test + void can_execute_expression_with_missing_field() { + assertThat() + .docValues("age", 30) + .evaluate(ref("name", STRING)) + .shouldMatch(null); + } + + private ExprScriptAssertion assertThat() { + return new ExprScriptAssertion(lookup, leafLookup, context); + } + + @RequiredArgsConstructor + private static class ExprScriptAssertion { + private final SearchLookup lookup; + private final LeafSearchLookup leafLookup; + private final LeafReaderContext context; + private Object actual; + + ExprScriptAssertion docValues() { + return this; + } + + ExprScriptAssertion docValues(String name, Object value) { + LeafDocLookup leafDocLookup = mockLeafDocLookup( + ImmutableMap.of(name, new FakeScriptDocValues<>(value))); + + when(lookup.getLeafSearchLookup(any())).thenReturn(leafLookup); + when(leafLookup.doc()).thenReturn(leafDocLookup); + return this; + } + + ExprScriptAssertion docValues(String name1, Object value1, + String name2, Object value2) { + LeafDocLookup leafDocLookup = mockLeafDocLookup( + ImmutableMap.of( + name1, new FakeScriptDocValues<>(value1), + name2, new FakeScriptDocValues<>(value2))); + + when(lookup.getLeafSearchLookup(any())).thenReturn(leafLookup); + when(leafLookup.doc()).thenReturn(leafDocLookup); + return this; + } + + ExprScriptAssertion evaluate(Expression expr) { + ExpressionAggregationScript script = + new ExpressionAggregationScript(expr, lookup, context, emptyMap()); + actual = script.execute(); + return this; + } + + void shouldMatch(Object expected) { + assertEquals(expected, actual); + } + + private LeafDocLookup mockLeafDocLookup(Map> docValueByNames) { + LeafDocLookup leafDocLookup = mock(LeafDocLookup.class); + when(leafDocLookup.get(anyString())) + .thenAnswer(invocation -> docValueByNames.get(invocation.getArgument(0))); + return leafDocLookup; + } + } + + @RequiredArgsConstructor + private static class FakeScriptDocValues extends ScriptDocValues { + private final T value; + + @Override + public void setNextDocId(int docId) { + throw new UnsupportedOperationException("Fake script doc values doesn't implement this yet"); + } + + @Override + public T get(int index) { + return value; + } + + @Override + public int size() { + return 1; + } + } +} \ No newline at end of file diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/BucketAggregationBuilderTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/BucketAggregationBuilderTest.java new file mode 100644 index 0000000000..85c8537a22 --- /dev/null +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/BucketAggregationBuilderTest.java @@ -0,0 +1,99 @@ +/* + * + * 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.elasticsearch.storage.script.aggregation.dsl; + +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; +import static com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.type.ElasticsearchDataType.ES_TEXT_KEYWORD; +import static com.amazon.opendistroforelasticsearch.sql.expression.DSL.named; +import static com.amazon.opendistroforelasticsearch.sql.expression.DSL.ref; +import static org.elasticsearch.common.xcontent.ToXContent.EMPTY_PARAMS; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.serialization.ExpressionSerializer; +import com.amazon.opendistroforelasticsearch.sql.expression.NamedExpression; +import java.util.Arrays; +import java.util.List; +import lombok.SneakyThrows; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.search.aggregations.bucket.composite.CompositeValuesSourceBuilder; +import org.junit.jupiter.api.BeforeEach; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@ExtendWith(MockitoExtension.class) +class BucketAggregationBuilderTest { + + @Mock + private ExpressionSerializer serializer; + + private BucketAggregationBuilder aggregationBuilder; + + @BeforeEach + void set_up() { + aggregationBuilder = new BucketAggregationBuilder(serializer); + } + + @Test + void should_build_bucket_with_field() { + assertEquals( + "{\n" + + " \"terms\" : {\n" + + " \"field\" : \"age\",\n" + + " \"missing_bucket\" : true,\n" + + " \"order\" : \"asc\"\n" + + " }\n" + + "}", + buildQuery( + Arrays.asList( + named("age", ref("age", INTEGER))))); + } + + @Test + void should_build_bucket_with_keyword_field() { + assertEquals( + "{\n" + + " \"terms\" : {\n" + + " \"field\" : \"name.keyword\",\n" + + " \"missing_bucket\" : true,\n" + + " \"order\" : \"asc\"\n" + + " }\n" + + "}", + buildQuery( + Arrays.asList( + named("name", ref("name", ES_TEXT_KEYWORD))))); + } + + @SneakyThrows + private String buildQuery(List groupByExpressions) { + XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON).prettyPrint(); + builder.startObject(); + CompositeValuesSourceBuilder sourceBuilder = + aggregationBuilder.build(groupByExpressions).get(0); + sourceBuilder.toXContent(builder, EMPTY_PARAMS); + builder.endObject(); + return BytesReference.bytes(builder).utf8ToString(); + } +} \ No newline at end of file diff --git a/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/MetricAggregationBuilderTest.java b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/MetricAggregationBuilderTest.java new file mode 100644 index 0000000000..094b74235c --- /dev/null +++ b/elasticsearch/src/test/java/com/amazon/opendistroforelasticsearch/sql/elasticsearch/storage/script/aggregation/dsl/MetricAggregationBuilderTest.java @@ -0,0 +1,139 @@ +/* + * + * 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.elasticsearch.storage.script.aggregation.dsl; + +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; +import static com.amazon.opendistroforelasticsearch.sql.expression.DSL.named; +import static com.amazon.opendistroforelasticsearch.sql.expression.DSL.ref; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +import com.amazon.opendistroforelasticsearch.sql.elasticsearch.storage.serialization.ExpressionSerializer; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.AvgAggregator; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.CountAggregator; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.NamedAggregator; +import com.amazon.opendistroforelasticsearch.sql.expression.aggregation.SumAggregator; +import com.amazon.opendistroforelasticsearch.sql.expression.function.FunctionName; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Arrays; +import java.util.List; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@ExtendWith(MockitoExtension.class) +class MetricAggregationBuilderTest { + + @Mock + private ExpressionSerializer serializer; + + @Mock + private NamedAggregator aggregator; + + private MetricAggregationBuilder aggregationBuilder; + + @BeforeEach + void set_up() { + aggregationBuilder = new MetricAggregationBuilder(serializer); + } + + @Test + void should_build_avg_aggregation() { + assertEquals( + "{\n" + + " \"avg(age)\" : {\n" + + " \"avg\" : {\n" + + " \"field\" : \"age\"\n" + + " }\n" + + " }\n" + + "}", + buildQuery( + Arrays.asList( + named("avg(age)", + new AvgAggregator(Arrays.asList(ref("age", INTEGER)), INTEGER))))); + } + + @Test + void should_build_sum_aggregation() { + assertEquals( + "{\n" + + " \"sum(age)\" : {\n" + + " \"sum\" : {\n" + + " \"field\" : \"age\"\n" + + " }\n" + + " }\n" + + "}", + buildQuery( + Arrays.asList( + named("sum(age)", + new SumAggregator(Arrays.asList(ref("age", INTEGER)), INTEGER))))); + } + + @Test + void should_build_count_aggregation() { + assertEquals( + "{\n" + + " \"count(age)\" : {\n" + + " \"value_count\" : {\n" + + " \"field\" : \"age\"\n" + + " }\n" + + " }\n" + + "}", + buildQuery( + Arrays.asList( + named("count(age)", + new CountAggregator(Arrays.asList(ref("age", INTEGER)), INTEGER))))); + } + + @Test + void should_throw_exception_for_unsupported_aggregator() { + when(aggregator.getFunctionName()).thenReturn(new FunctionName("max")); + when(aggregator.getArguments()).thenReturn(Arrays.asList(ref("age", INTEGER))); + + IllegalStateException exception = + assertThrows(IllegalStateException.class, () -> buildQuery(Arrays.asList(named("count(age)", + aggregator)))); + assertEquals("unsupported aggregator max", exception.getMessage()); + } + + @Test + void should_throw_exception_for_unsupported_exception() { + IllegalStateException exception = + assertThrows(IllegalStateException.class, () -> buildQuery(Arrays.asList( + named("count(age)", + new CountAggregator(Arrays.asList(named("age", ref("age", INTEGER))), INTEGER))))); + assertEquals( + "metric aggregation doesn't support expression age", + exception.getMessage()); + } + + @SneakyThrows + private String buildQuery(List namedAggregatorList) { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readTree( + aggregationBuilder.build(namedAggregatorList).toString()) + .toPrettyString(); + } +} \ No newline at end of file diff --git a/integ-test/build.gradle b/integ-test/build.gradle index 622497dcf2..7b5f621dbb 100644 --- a/integ-test/build.gradle +++ b/integ-test/build.gradle @@ -115,6 +115,12 @@ task integTestWithNewEngine(type: RestIntegTestTask) { exclude 'com/amazon/opendistroforelasticsearch/sql/doctest/**/*IT.class' exclude 'com/amazon/opendistroforelasticsearch/sql/correctness/**' + // Explain IT is dependent on internal implementation of old engine so it's not necessary + // to run these with new engine and not necessary to make this consistent with old engine. + exclude 'com/amazon/opendistroforelasticsearch/sql/legacy/ExplainIT.class' + exclude 'com/amazon/opendistroforelasticsearch/sql/legacy/PrettyFormatterIT.class' + exclude 'com/amazon/opendistroforelasticsearch/sql/legacy/TermQueryExplainIT.class' + // Skip old semantic analyzer IT because analyzer in new engine has different behavior exclude 'com/amazon/opendistroforelasticsearch/sql/legacy/QueryAnalysisIT.class' } diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/correctness/runner/connection/JDBCConnection.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/correctness/runner/connection/JDBCConnection.java index 2aff6b982c..bc0ca84626 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/correctness/runner/connection/JDBCConnection.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/correctness/runner/connection/JDBCConnection.java @@ -29,6 +29,7 @@ import java.sql.Statement; import java.util.Arrays; import java.util.List; +import org.json.JSONException; import org.json.JSONObject; /** @@ -135,8 +136,7 @@ private String parseColumnNameAndTypesInSchemaJson(String schema) { JSONObject json = (JSONObject) new JSONObject(schema).query("/mappings/properties"); return json.keySet().stream(). map(colName -> colName + " " + mapToJDBCType(json.getJSONObject(colName).getString("type"))) - . - collect(joining(",")); + .collect(joining(",")); } private String getValueList(Object[] fieldValues) { diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/correctness/testset/TestDataSet.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/correctness/testset/TestDataSet.java index 42036a2f2a..8d3cfc1d6a 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/correctness/testset/TestDataSet.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/correctness/testset/TestDataSet.java @@ -118,6 +118,8 @@ private Object convertStringToObject(String type, String str) { case "text": case "keyword": case "date": + case "time": + case "timestamp": return str; case "integer": return Integer.valueOf(str); diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/ExplainIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/ExplainIT.java new file mode 100644 index 0000000000..70729fe23a --- /dev/null +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/ExplainIT.java @@ -0,0 +1,51 @@ +/* + * 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.ppl; + +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; + +public class ExplainIT extends PPLIntegTestCase { + + @Override + public void init() throws IOException { + loadIndex(Index.ACCOUNT); + } + + @Test + public void testExplain() throws Exception { + URI uri = Resources.getResource("expectedOutput/ppl/explain_output.json").toURI(); + String expected = new String(Files.readAllBytes(Paths.get(uri))); + assertEquals( + expected, + explainQueryToString( + "source=elasticsearch-sql_test_index_account" + + "| where age > 30 " + + "| stats avg(age) AS avg_age by state, city " + + "| sort state " + + "| fields - city " + + "| eval age2 = avg_age + 2 " + + "| dedup age2 " + + "| fields age2") + ); + } + +} diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/HeadCommandIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/HeadCommandIT.java new file mode 100644 index 0000000000..48217127c8 --- /dev/null +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/HeadCommandIT.java @@ -0,0 +1,87 @@ +/* + * 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.ppl; + +import org.json.JSONObject; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static com.amazon.opendistroforelasticsearch.sql.legacy.TestsConstants.TEST_INDEX_ACCOUNT; +import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.rows; +import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.verifyDataRows; + +public class HeadCommandIT extends PPLIntegTestCase { + + @Override + public void init() throws IOException { + loadIndex(Index.ACCOUNT); + } + + @Test + public void testHead() throws IOException { + JSONObject result = + executeQuery(String.format("source=%s | fields firstname, age | head", TEST_INDEX_ACCOUNT)); + verifyDataRows(result, + rows("Amber", 32), + rows("Hattie", 36), + rows("Nanette", 28), + rows("Dale", 33), + rows("Elinor", 36), + rows("Virginia", 39), + rows("Dillard", 34), + rows("Mcgee", 39), + rows("Aurelia", 37), + rows("Fulton", 23)); + } + + @Test + public void testHeadWithNumber() throws IOException { + JSONObject result = + executeQuery(String.format("source=%s | fields firstname, age | head 3", TEST_INDEX_ACCOUNT)); + verifyDataRows(result, + rows("Amber", 32), + rows("Hattie", 36), + rows("Nanette", 28)); + } + + @Test + public void testHeadWithWhile() throws IOException { + JSONObject result = + executeQuery(String + .format("source=%s | fields firstname, age | sort age | head while(age < 21) 7", + TEST_INDEX_ACCOUNT)); + verifyDataRows(result, + rows("Claudia", 20), + rows("Copeland", 20), + rows("Cornelia", 20), + rows("Schultz", 20), + rows("Simpson", 21)); + } + + @Test + public void testHeadWithKeeplast() throws IOException { + JSONObject result = + executeQuery(String.format( + "source=%s | fields firstname, age | sort age | head keeplast=false while(age < 21) 7", + TEST_INDEX_ACCOUNT)); + verifyDataRows(result, + rows("Claudia", 20), + rows("Copeland", 20), + rows("Cornelia", 20), + rows("Schultz", 20)); + } +} diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/PPLIntegTestCase.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/PPLIntegTestCase.java index 40c2c604d0..6ac46a8168 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/PPLIntegTestCase.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/PPLIntegTestCase.java @@ -16,6 +16,7 @@ package com.amazon.opendistroforelasticsearch.sql.ppl; import static com.amazon.opendistroforelasticsearch.sql.legacy.TestUtils.getResponseBody; +import static com.amazon.opendistroforelasticsearch.sql.plugin.rest.RestPPLQueryAction.EXPLAIN_API_ENDPOINT; import static com.amazon.opendistroforelasticsearch.sql.plugin.rest.RestPPLQueryAction.QUERY_API_ENDPOINT; import com.amazon.opendistroforelasticsearch.sql.legacy.SQLIntegTestCase; @@ -38,13 +39,19 @@ protected JSONObject executeQuery(String query) throws IOException { } protected String executeQueryToString(String query) throws IOException { - Response response = client().performRequest(buildRequest(query)); + Response response = client().performRequest(buildRequest(query, QUERY_API_ENDPOINT)); Assert.assertEquals(200, response.getStatusLine().getStatusCode()); return getResponseBody(response, true); } - protected Request buildRequest(String query) { - Request request = new Request("POST", QUERY_API_ENDPOINT); + protected String explainQueryToString(String query) throws IOException { + Response response = client().performRequest(buildRequest(query, EXPLAIN_API_ENDPOINT)); + Assert.assertEquals(200, response.getStatusLine().getStatusCode()); + return getResponseBody(response, true); + } + + protected Request buildRequest(String query, String endpoint) { + Request request = new Request("POST", endpoint); request.setJsonEntity(String.format(Locale.ROOT, "{\n" + " \"query\": \"%s\"\n" + "}", query)); RequestOptions.Builder restOptionsBuilder = RequestOptions.DEFAULT.toBuilder(); diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/StandaloneIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/StandaloneIT.java index e76c0da6a1..03c04b81c6 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/StandaloneIT.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/StandaloneIT.java @@ -114,7 +114,7 @@ public void testSourceFieldQuery() throws IOException { private String executeByStandaloneQueryEngine(String query) { AtomicReference actual = new AtomicReference<>(); pplService.execute( - new PPLQueryRequest(query, null), + new PPLQueryRequest(query, null, null), new ResponseListener() { @Override diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/StatsCommandIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/StatsCommandIT.java index 74808a1436..3d9f5e9f4d 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/StatsCommandIT.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/StatsCommandIT.java @@ -16,6 +16,7 @@ package com.amazon.opendistroforelasticsearch.sql.ppl; import static com.amazon.opendistroforelasticsearch.sql.legacy.TestsConstants.TEST_INDEX_ACCOUNT; +import static com.amazon.opendistroforelasticsearch.sql.legacy.TestsConstants.TEST_INDEX_BANK_WITH_NULL_VALUES; import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.rows; import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.schema; import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.verifyDataRows; @@ -30,7 +31,7 @@ public class StatsCommandIT extends PPLIntegTestCase { @Override public void init() throws IOException { loadIndex(Index.ACCOUNT); - setQuerySizeLimit(2000); + loadIndex(Index.BANK_WITH_NULL_VALUES); } @Test @@ -71,10 +72,28 @@ public void testStatsNested() throws IOException { public void testStatsWhere() throws IOException { JSONObject response = executeQuery(String.format( - "source=%s | stats sum(balance) as a by gender | where a > 13000000", TEST_INDEX_ACCOUNT)); + "source=%s | stats sum(balance) as a by state | where a > 780000", + TEST_INDEX_ACCOUNT)); verifySchema(response, schema("a", null, "long"), - schema("gender", null, "string")); - verifyDataRows(response, rows(13082527, "M")); + schema("state", null, "string")); + verifyDataRows(response, rows(782199, "TX")); } + @Test + public void testGroupByNullValue() throws IOException { + JSONObject response = + executeQuery(String.format( + "source=%s | stats avg(balance) as a by age", + TEST_INDEX_BANK_WITH_NULL_VALUES)); + verifySchema(response, schema("a", null, "double"), + schema("age", null, "integer")); + verifyDataRows(response, + rows(null, null), + rows(32838D, 28), + rows(39225D, 32), + rows(4180D, 33), + rows(48086D, 34), + rows(null, 36) + ); + } } diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/DateTimeFunctionIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/DateTimeFunctionIT.java new file mode 100644 index 0000000000..62f2d6d639 --- /dev/null +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/DateTimeFunctionIT.java @@ -0,0 +1,67 @@ +/* + * 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.sql; + +import static com.amazon.opendistroforelasticsearch.sql.legacy.plugin.RestSqlAction.QUERY_API_ENDPOINT; +import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.rows; +import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.schema; +import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.verifyDataRows; +import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.verifySchema; +import static com.amazon.opendistroforelasticsearch.sql.util.TestUtils.getResponseBody; + +import com.amazon.opendistroforelasticsearch.sql.legacy.SQLIntegTestCase; +import com.amazon.opendistroforelasticsearch.sql.util.TestUtils; +import java.io.IOException; +import java.util.Locale; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.Response; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; + +public class DateTimeFunctionIT extends SQLIntegTestCase { + + @Override + public void init() throws Exception { + super.init(); + TestUtils.enableNewQueryEngine(client()); + } + + @Test + public void add_date() throws IOException { + JSONObject result = + executeQuery("select adddate(timestamp('2020-09-16 17:30:00'), interval 1 day)"); + verifySchema(result, + schema("adddate(timestamp('2020-09-16 17:30:00'), interval 1 day)", null, "datetime")); + verifyDataRows(result, rows("2020-09-17 17:30:00")); + + result = executeQuery("select adddate(date('2020-09-16'), 1)"); + verifySchema(result, schema("adddate(date('2020-09-16'), 1)", null, "date")); + verifyDataRows(result, rows("2020-09-17")); + } + + protected JSONObject executeQuery(String query) throws IOException { + Request request = new Request("POST", QUERY_API_ENDPOINT); + request.setJsonEntity(String.format(Locale.ROOT, "{\n" + " \"query\": \"%s\"\n" + "}", query)); + + RequestOptions.Builder restOptionsBuilder = RequestOptions.DEFAULT.toBuilder(); + restOptionsBuilder.addHeader("Content-Type", "application/json"); + request.setOptions(restOptionsBuilder); + + Response response = client().performRequest(request); + return new JSONObject(getResponseBody(response)); + } +} diff --git a/integ-test/src/test/resources/correctness/expressions/date_and_time_functions.txt b/integ-test/src/test/resources/correctness/expressions/date_and_time_functions.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integ-test/src/test/resources/correctness/expressions/functions.txt b/integ-test/src/test/resources/correctness/expressions/mathematical_functions.txt similarity index 96% rename from integ-test/src/test/resources/correctness/expressions/functions.txt rename to integ-test/src/test/resources/correctness/expressions/mathematical_functions.txt index fc6120d0ce..c893e625da 100644 --- a/integ-test/src/test/resources/correctness/expressions/functions.txt +++ b/integ-test/src/test/resources/correctness/expressions/mathematical_functions.txt @@ -73,3 +73,4 @@ sin(-1.57) tan(0) tan(1.57) tan(-1.57) +dayofmonth('2020-08-26') as dom diff --git a/integ-test/src/test/resources/expectedOutput/ppl/explain_output.json b/integ-test/src/test/resources/expectedOutput/ppl/explain_output.json new file mode 100644 index 0000000000..7e133bb24e --- /dev/null +++ b/integ-test/src/test/resources/expectedOutput/ppl/explain_output.json @@ -0,0 +1,60 @@ +{ + "root": { + "name": "ProjectOperator", + "description": { + "fields": "[age2]" + }, + "children": [ + { + "name": "DedupeOperator", + "description": { + "dedupeList": "[age2]", + "allowedDuplication": 1, + "keepEmpty": false, + "consecutive": false + }, + "children": [ + { + "name": "EvalOperator", + "description": { + "expressions": { + "age2": "+(avg_age, 2)" + } + }, + "children": [ + { + "name": "RemoveOperator", + "description": { + "removeList": "[city]" + }, + "children": [ + { + "name": "SortOperator", + "description": { + "count": 1000, + "sortList": { + "state": { + "sortOrder": "ASC", + "nullOrder": "NULL_FIRST" + } + } + }, + "children": [ + { + "name": "ElasticsearchIndexScan", + "description": { + "request": "ElasticsearchQueryRequest(indexName\u003delasticsearch-sql_test_index_account, sourceBuilder\u003d{\"from\":0,\"size\":0,\"timeout\":\"1m\",\"query\":{\"range\":{\"age\":{\"from\":30,\"to\":null,\"include_lower\":false,\"include_upper\":true,\"boost\":1.0}}},\"sort\":[{\"_doc\":{\"order\":\"asc\"}}],\"aggregations\":{\"composite_buckets\":{\"composite\":{\"size\":1000,\"sources\":[{\"state\":{\"terms\":{\"field\":\"state.keyword\",\"missing_bucket\":true,\"order\":\"asc\"}}},{\"city\":{\"terms\":{\"field\":\"city.keyword\",\"missing_bucket\":true,\"order\":\"asc\"}}}]},\"aggregations\":{\"avg_age\":{\"avg\":{\"field\":\"age\"}}}}}}, searchDone\u003dfalse)" + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + } + ] + } +} diff --git a/legacy/src/main/java/com/amazon/opendistroforelasticsearch/sql/legacy/plugin/RestSQLQueryAction.java b/legacy/src/main/java/com/amazon/opendistroforelasticsearch/sql/legacy/plugin/RestSQLQueryAction.java index 8683600d60..935c9c849f 100644 --- a/legacy/src/main/java/com/amazon/opendistroforelasticsearch/sql/legacy/plugin/RestSQLQueryAction.java +++ b/legacy/src/main/java/com/amazon/opendistroforelasticsearch/sql/legacy/plugin/RestSQLQueryAction.java @@ -25,9 +25,10 @@ import com.amazon.opendistroforelasticsearch.sql.common.response.ResponseListener; import com.amazon.opendistroforelasticsearch.sql.common.setting.Settings; import com.amazon.opendistroforelasticsearch.sql.elasticsearch.security.SecurityAccess; -import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlan; +import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.ExplainResponse; import com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlan; import com.amazon.opendistroforelasticsearch.sql.protocol.response.QueryResult; +import com.amazon.opendistroforelasticsearch.sql.protocol.response.format.JsonResponseFormatter; import com.amazon.opendistroforelasticsearch.sql.protocol.response.format.SimpleJsonResponseFormatter; import com.amazon.opendistroforelasticsearch.sql.sql.SQLService; import com.amazon.opendistroforelasticsearch.sql.sql.config.SQLServiceConfig; @@ -107,7 +108,11 @@ public RestChannelConsumer prepareRequest(SQLQueryRequest request, NodeClient no } catch (SyntaxCheckException e) { return NOT_SUPPORTED_YET; } - return channel -> sqlService.execute(plan, createListener(channel)); + + if (request.isExplainRequest()) { + return channel -> sqlService.explain(plan, createExplainResponseListener(channel)); + } + return channel -> sqlService.execute(plan, createQueryResponseListener(channel)); } private SQLService createSQLService(NodeClient client) { @@ -123,25 +128,41 @@ private SQLService createSQLService(NodeClient client) { }); } + private ResponseListener createExplainResponseListener(RestChannel channel) { + return new ResponseListener() { + @Override + public void onResponse(ExplainResponse response) { + sendResponse(channel, OK, new JsonResponseFormatter(PRETTY) { + @Override + protected Object buildJsonObject(ExplainResponse response) { + return response; + } + }.format(response)); + } + + @Override + public void onFailure(Exception e) { + LOG.error("Error happened during explain", e); + sendResponse(channel, INTERNAL_SERVER_ERROR, + "Failed to explain the query due to error: " + e.getMessage()); + } + }; + } + // TODO: duplicate code here as in RestPPLQueryAction - private ResponseListener createListener(RestChannel channel) { + private ResponseListener createQueryResponseListener(RestChannel channel) { SimpleJsonResponseFormatter formatter = new SimpleJsonResponseFormatter(PRETTY); return new ResponseListener() { @Override public void onResponse(QueryResponse response) { - sendResponse(OK, formatter.format(new QueryResult(response.getSchema(), - response.getResults()))); + sendResponse(channel, OK, + formatter.format(new QueryResult(response.getSchema(), response.getResults()))); } @Override public void onFailure(Exception e) { LOG.error("Error happened during query handling", e); - sendResponse(INTERNAL_SERVER_ERROR, formatter.format(e)); - } - - private void sendResponse(RestStatus status, String content) { - channel.sendResponse(new BytesRestResponse( - status, "application/json; charset=UTF-8", content)); + sendResponse(channel, INTERNAL_SERVER_ERROR, formatter.format(e)); } }; } @@ -154,4 +175,9 @@ private T doPrivileged(PrivilegedExceptionAction action) { } } + private void sendResponse(RestChannel channel, RestStatus status, String content) { + channel.sendResponse(new BytesRestResponse( + status, "application/json; charset=UTF-8", content)); + } + } diff --git a/legacy/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/plugin/RestSQLQueryActionTest.java b/legacy/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/plugin/RestSQLQueryActionTest.java index 50549e5884..2d420debf7 100644 --- a/legacy/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/plugin/RestSQLQueryActionTest.java +++ b/legacy/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/plugin/RestSQLQueryActionTest.java @@ -57,15 +57,15 @@ public void handleQueryThatCanSupport() { } @Test - public void skipExplainThatNotSupport() { + public void handleExplainThatCanSupport() { SQLQueryRequest request = new SQLQueryRequest( - new JSONObject("{\"query\": \"SELECT * FROM test\"}"), - "SELECT * FROM test", + new JSONObject("{\"query\": \"SELECT -123\"}"), + "SELECT -123", EXPLAIN_API_ENDPOINT, ""); RestSQLQueryAction queryAction = new RestSQLQueryAction(clusterService, settings); - assertSame(NOT_SUPPORTED_YET, queryAction.prepareRequest(request, nodeClient)); + assertNotSame(NOT_SUPPORTED_YET, queryAction.prepareRequest(request, nodeClient)); } @Test diff --git a/legacy/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/unittest/SqlRequestFactoryTest.java b/legacy/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/unittest/SqlRequestFactoryTest.java index 8bff20f5cc..39620eb567 100644 --- a/legacy/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/unittest/SqlRequestFactoryTest.java +++ b/legacy/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/unittest/SqlRequestFactoryTest.java @@ -15,12 +15,19 @@ package com.amazon.opendistroforelasticsearch.sql.legacy.unittest; +import static java.util.Collections.emptyList; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import com.amazon.opendistroforelasticsearch.sql.legacy.esdomain.LocalClusterState; +import com.amazon.opendistroforelasticsearch.sql.legacy.plugin.SqlSettings; import com.amazon.opendistroforelasticsearch.sql.legacy.request.PreparedStatementRequest; import com.amazon.opendistroforelasticsearch.sql.legacy.request.SqlRequest; import com.amazon.opendistroforelasticsearch.sql.legacy.request.SqlRequestFactory; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.rest.RestRequest; import org.junit.Assert; +import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -34,6 +41,15 @@ public class SqlRequestFactoryTest { @Mock private RestRequest restRequest; + @Before + public void setup() { + SqlSettings settings = spy(new SqlSettings()); + // Force return empty list to avoid ClusterSettings be invoked which is a final class and hard to mock. + // In this case, default value in Setting will be returned all the time. + doReturn(emptyList()).when(settings).getSettings(); + LocalClusterState.state().setSqlSettings(settings); + } + @Ignore("RestRequest is a final method, and Mockito 1.x cannot mock it." + "Ignore this test case till we can upgrade to Mockito 2.x") @Test diff --git a/legacy/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/util/AggregationUtils.java b/legacy/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/util/AggregationUtils.java index 0f270fb007..9f949a0ede 100644 --- a/legacy/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/util/AggregationUtils.java +++ b/legacy/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/util/AggregationUtils.java @@ -16,6 +16,9 @@ package com.amazon.opendistroforelasticsearch.sql.legacy.util; import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ContextParser; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; @@ -47,10 +50,6 @@ import org.elasticsearch.search.aggregations.pipeline.ParsedPercentilesBucket; import org.elasticsearch.search.aggregations.pipeline.PercentilesBucketPipelineAggregationBuilder; -import java.io.IOException; -import java.util.List; -import java.util.stream.Collectors; - public class AggregationUtils { private final static List entryList = new ImmutableMap.Builder>().put( diff --git a/plugin/src/main/java/com/amazon/opendistroforelasticsearch/sql/plugin/request/PPLQueryRequestFactory.java b/plugin/src/main/java/com/amazon/opendistroforelasticsearch/sql/plugin/request/PPLQueryRequestFactory.java index 99eb56e457..dde892e5af 100644 --- a/plugin/src/main/java/com/amazon/opendistroforelasticsearch/sql/plugin/request/PPLQueryRequestFactory.java +++ b/plugin/src/main/java/com/amazon/opendistroforelasticsearch/sql/plugin/request/PPLQueryRequestFactory.java @@ -51,7 +51,7 @@ private static PPLQueryRequest parsePPLRequestFromUrl(RestRequest restRequest) { if (ppl == null) { throw new IllegalArgumentException("Cannot find ppl parameter from the URL"); } - return new PPLQueryRequest(ppl, null); + return new PPLQueryRequest(ppl, null, restRequest.path()); } private static PPLQueryRequest parsePPLRequestFromPayload(RestRequest restRequest) { @@ -62,6 +62,7 @@ private static PPLQueryRequest parsePPLRequestFromPayload(RestRequest restReques } catch (JSONException e) { throw new IllegalArgumentException("Failed to parse request payload", e); } - return new PPLQueryRequest(jsonContent.getString(PPL_FIELD_NAME), jsonContent); + return new PPLQueryRequest(jsonContent.getString(PPL_FIELD_NAME), + jsonContent, restRequest.path()); } } diff --git a/plugin/src/main/java/com/amazon/opendistroforelasticsearch/sql/plugin/rest/RestPPLQueryAction.java b/plugin/src/main/java/com/amazon/opendistroforelasticsearch/sql/plugin/rest/RestPPLQueryAction.java index 8314462832..015f4aff16 100644 --- a/plugin/src/main/java/com/amazon/opendistroforelasticsearch/sql/plugin/rest/RestPPLQueryAction.java +++ b/plugin/src/main/java/com/amazon/opendistroforelasticsearch/sql/plugin/rest/RestPPLQueryAction.java @@ -17,6 +17,7 @@ import static com.amazon.opendistroforelasticsearch.sql.protocol.response.format.JsonResponseFormatter.Style.PRETTY; import static org.elasticsearch.rest.RestStatus.BAD_REQUEST; +import static org.elasticsearch.rest.RestStatus.INTERNAL_SERVER_ERROR; import static org.elasticsearch.rest.RestStatus.OK; import static org.elasticsearch.rest.RestStatus.SERVICE_UNAVAILABLE; @@ -28,6 +29,7 @@ import com.amazon.opendistroforelasticsearch.sql.exception.ExpressionEvaluationException; import com.amazon.opendistroforelasticsearch.sql.exception.QueryEngineException; import com.amazon.opendistroforelasticsearch.sql.exception.SemanticCheckException; +import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.ExplainResponse; import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.QueryResponse; import com.amazon.opendistroforelasticsearch.sql.legacy.metrics.MetricName; import com.amazon.opendistroforelasticsearch.sql.legacy.metrics.Metrics; @@ -35,11 +37,13 @@ import com.amazon.opendistroforelasticsearch.sql.plugin.request.PPLQueryRequestFactory; import com.amazon.opendistroforelasticsearch.sql.ppl.PPLService; import com.amazon.opendistroforelasticsearch.sql.ppl.config.PPLServiceConfig; +import com.amazon.opendistroforelasticsearch.sql.ppl.domain.PPLQueryRequest; import com.amazon.opendistroforelasticsearch.sql.protocol.response.QueryResult; +import com.amazon.opendistroforelasticsearch.sql.protocol.response.format.JsonResponseFormatter; import com.amazon.opendistroforelasticsearch.sql.protocol.response.format.SimpleJsonResponseFormatter; import java.io.IOException; import java.security.PrivilegedExceptionAction; -import java.util.Collections; +import java.util.Arrays; import java.util.List; import java.util.function.Supplier; import org.apache.logging.log4j.LogManager; @@ -57,6 +61,7 @@ public class RestPPLQueryAction extends BaseRestHandler { public static final String QUERY_API_ENDPOINT = "/_opendistro/_ppl"; + public static final String EXPLAIN_API_ENDPOINT = "/_opendistro/_ppl/_explain"; private static final Logger LOG = LogManager.getLogger(); @@ -88,8 +93,9 @@ public RestPPLQueryAction(RestController restController, ClusterService clusterS @Override public List routes() { - return Collections.singletonList( - new Route(RestRequest.Method.POST, QUERY_API_ENDPOINT) + return Arrays.asList( + new Route(RestRequest.Method.POST, QUERY_API_ENDPOINT), + new Route(RestRequest.Method.POST, EXPLAIN_API_ENDPOINT) ); } @@ -110,9 +116,13 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient nod "Either opendistro.ppl.enabled or rest.action.multi.allow_explicit_index setting is false" ), BAD_REQUEST); } + PPLService pplService = createPPLService(nodeClient); - return channel -> pplService.execute( - PPLQueryRequestFactory.getPPLRequest(request), createListener(channel)); + PPLQueryRequest pplRequest = PPLQueryRequestFactory.getPPLRequest(request); + if (pplRequest.isExplainRequest()) { + return channel -> pplService.explain(pplRequest, createExplainResponseListener(channel)); + } + return channel -> pplService.execute(pplRequest, createListener(channel)); } /** @@ -140,13 +150,40 @@ private PPLService createPPLService(NodeClient client) { }); } + /** + * TODO: need to extract an interface for both SQL and PPL action handler and move these + * common methods to the interface. This is not easy to do now because SQL action handler + * is still in legacy module. + */ + private ResponseListener createExplainResponseListener( + RestChannel channel) { + return new ResponseListener() { + @Override + public void onResponse(ExplainResponse response) { + sendResponse(channel, OK, new JsonResponseFormatter(PRETTY) { + @Override + protected Object buildJsonObject(ExplainResponse response) { + return response; + } + }.format(response)); + } + + @Override + public void onFailure(Exception e) { + LOG.error("Error happened during explain", e); + sendResponse(channel, INTERNAL_SERVER_ERROR, + "Failed to explain the query due to error: " + e.getMessage()); + } + }; + } + private ResponseListener createListener(RestChannel channel) { SimpleJsonResponseFormatter formatter = new SimpleJsonResponseFormatter(PRETTY); // TODO: decide format and pretty from URL param return new ResponseListener() { @Override public void onResponse(QueryResponse response) { - sendResponse(OK, formatter.format(new QueryResult(response.getSchema(), + sendResponse(channel, OK, formatter.format(new QueryResult(response.getSchema(), response.getResults()))); } @@ -161,11 +198,6 @@ public void onFailure(Exception e) { reportError(channel, e, SERVICE_UNAVAILABLE); } } - - private void sendResponse(RestStatus status, String content) { - channel.sendResponse( - new BytesRestResponse(status, "application/json; charset=UTF-8", content)); - } }; } @@ -177,6 +209,11 @@ private T doPrivileged(PrivilegedExceptionAction action) { } } + private void sendResponse(RestChannel channel, RestStatus status, String content) { + channel.sendResponse( + new BytesRestResponse(status, "application/json; charset=UTF-8", content)); + } + private void reportError(final RestChannel channel, final Exception e, final RestStatus status) { channel.sendResponse(new BytesRestResponse(status, ErrorMessageFactory.createErrorMessage(e, status.getStatus()).toString())); diff --git a/ppl/src/main/antlr/OpenDistroPPLLexer.g4 b/ppl/src/main/antlr/OpenDistroPPLLexer.g4 index 377bd38050..3c8377f103 100644 --- a/ppl/src/main/antlr/OpenDistroPPLLexer.g4 +++ b/ppl/src/main/antlr/OpenDistroPPLLexer.g4 @@ -28,6 +28,7 @@ STATS: 'STATS'; DEDUP: 'DEDUP'; SORT: 'SORT'; EVAL: 'EVAL'; +HEAD: 'HEAD'; TOP: 'TOP'; RARE: 'RARE'; @@ -50,11 +51,13 @@ NUM: 'NUM'; // ARGUMENT KEYWORDS KEEPEMPTY: 'KEEPEMPTY'; +KEEPLAST: 'KEEPLAST'; CONSECUTIVE: 'CONSECUTIVE'; DEDUP_SPLITVALUES: 'DEDUP_SPLITVALUES'; PARTITIONS: 'PARTITIONS'; ALLNUM: 'ALLNUM'; DELIM: 'DELIM'; +WHILE: 'WHILE'; // COMPARISON FUNCTION KEYWORDS CASE: 'CASE'; @@ -199,6 +202,7 @@ TAN: 'TAN'; DATE: 'DATE'; TIME: 'TIME'; TIMESTAMP: 'TIMESTAMP'; +ADDDATE: 'ADDDATE'; // TEXT FUNCTIONS SUBSTR: 'SUBSTR'; diff --git a/ppl/src/main/antlr/OpenDistroPPLParser.g4 b/ppl/src/main/antlr/OpenDistroPPLParser.g4 index 1de967b8aa..1c3f7f87bc 100644 --- a/ppl/src/main/antlr/OpenDistroPPLParser.g4 +++ b/ppl/src/main/antlr/OpenDistroPPLParser.g4 @@ -28,7 +28,7 @@ pplStatement /** commands */ commands - : whereCommand | fieldsCommand | renameCommand | statsCommand | dedupCommand | sortCommand | evalCommand + : whereCommand | fieldsCommand | renameCommand | statsCommand | dedupCommand | sortCommand | evalCommand | headCommand | topCommand | rareCommand; searchCommand @@ -75,6 +75,13 @@ evalCommand : EVAL evalClause (COMMA evalClause)* ; +headCommand + : HEAD + (KEEPLAST EQUAL keeplast=booleanLiteral)? + (WHILE LT_PRTHS whileExpr=logicalExpression RT_PRTHS)? + (number=integerLiteral)? + ; + topCommand : TOP (number=integerLiteral)? @@ -226,7 +233,7 @@ trigonometricFunctionName ; dateAndTimeFunctionBase - : DATE | TIME | TIMESTAMP + : DATE | TIME | TIMESTAMP | ADDDATE ; textFunctionBase diff --git a/ppl/src/main/java/com/amazon/opendistroforelasticsearch/sql/ppl/PPLService.java b/ppl/src/main/java/com/amazon/opendistroforelasticsearch/sql/ppl/PPLService.java index 08ca8f4383..8451a053b2 100644 --- a/ppl/src/main/java/com/amazon/opendistroforelasticsearch/sql/ppl/PPLService.java +++ b/ppl/src/main/java/com/amazon/opendistroforelasticsearch/sql/ppl/PPLService.java @@ -22,6 +22,7 @@ import com.amazon.opendistroforelasticsearch.sql.ast.tree.UnresolvedPlan; import com.amazon.opendistroforelasticsearch.sql.common.response.ResponseListener; import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine; +import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.ExplainResponse; import com.amazon.opendistroforelasticsearch.sql.planner.Planner; import com.amazon.opendistroforelasticsearch.sql.planner.logical.LogicalPlan; import com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlan; @@ -51,21 +52,39 @@ public class PPLService { */ public void execute(PPLQueryRequest request, ResponseListener listener) { try { - // 1.Parse query and convert parse tree (CST) to abstract syntax tree (AST) - ParseTree cst = parser.analyzeSyntax(request.getRequest()); - UnresolvedPlan ast = cst.accept(new AstBuilder(new AstExpressionBuilder())); - - // 2.Analyze abstract syntax to generate logical plan - LogicalPlan logicalPlan = analyzer.analyze(UnresolvedPlanHelper.addSelectAll(ast), - new AnalysisContext()); - - // 3.Generate optimal physical plan from logical plan - PhysicalPlan physicalPlan = new Planner(storageEngine).plan(logicalPlan); + executionEngine.execute(plan(request), listener); + } catch (Exception e) { + listener.onFailure(e); + } + } - // 4.Execute physical plan and send response - executionEngine.execute(physicalPlan, listener); + /** + * Explain the query in {@link PPLQueryRequest} using {@link ResponseListener} to + * get and format explain response. + * + * @param request {@link PPLQueryRequest} + * @param listener {@link ResponseListener} for explain response + */ + public void explain(PPLQueryRequest request, ResponseListener listener) { + try { + executionEngine.explain(plan(request), listener); } catch (Exception e) { listener.onFailure(e); } } + + private PhysicalPlan plan(PPLQueryRequest request) { + // 1.Parse query and convert parse tree (CST) to abstract syntax tree (AST) + ParseTree cst = parser.analyzeSyntax(request.getRequest()); + UnresolvedPlan ast = cst.accept( + new AstBuilder(new AstExpressionBuilder(), request.getRequest())); + + // 2.Analyze abstract syntax to generate logical plan + LogicalPlan logicalPlan = analyzer.analyze(UnresolvedPlanHelper.addSelectAll(ast), + new AnalysisContext()); + + // 3.Generate optimal physical plan from logical plan + return new Planner(storageEngine).plan(logicalPlan); + } + } diff --git a/ppl/src/main/java/com/amazon/opendistroforelasticsearch/sql/ppl/domain/PPLQueryRequest.java b/ppl/src/main/java/com/amazon/opendistroforelasticsearch/sql/ppl/domain/PPLQueryRequest.java index 9bb348fd43..074acf062d 100644 --- a/ppl/src/main/java/com/amazon/opendistroforelasticsearch/sql/ppl/domain/PPLQueryRequest.java +++ b/ppl/src/main/java/com/amazon/opendistroforelasticsearch/sql/ppl/domain/PPLQueryRequest.java @@ -20,12 +20,22 @@ @RequiredArgsConstructor public class PPLQueryRequest { - public static final PPLQueryRequest NULL = new PPLQueryRequest("", null); + public static final PPLQueryRequest NULL = new PPLQueryRequest("", null, ""); private final String pplQuery; private final JSONObject jsonContent; + private final String path; public String getRequest() { return pplQuery; } + + /** + * Check if request is to explain rather than execute the query. + * @return true if it is a explain request + */ + public boolean isExplainRequest() { + return path.endsWith("/_explain"); + } + } diff --git a/ppl/src/main/java/com/amazon/opendistroforelasticsearch/sql/ppl/parser/AstBuilder.java b/ppl/src/main/java/com/amazon/opendistroforelasticsearch/sql/ppl/parser/AstBuilder.java index 1058bacb96..3f094bfab7 100644 --- a/ppl/src/main/java/com/amazon/opendistroforelasticsearch/sql/ppl/parser/AstBuilder.java +++ b/ppl/src/main/java/com/amazon/opendistroforelasticsearch/sql/ppl/parser/AstBuilder.java @@ -19,6 +19,7 @@ import static com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.EvalCommandContext; import static com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.FieldsCommandContext; import static com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.FromClauseContext; +import static com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.HeadCommandContext; import static com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.PplStatementContext; import static com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.RareCommandContext; import static com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.RenameCommandContext; @@ -30,6 +31,7 @@ import static com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.TopCommandContext; import static com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.WhereCommandContext; +import com.amazon.opendistroforelasticsearch.sql.ast.expression.Alias; import com.amazon.opendistroforelasticsearch.sql.ast.expression.Argument; import com.amazon.opendistroforelasticsearch.sql.ast.expression.DataType; import com.amazon.opendistroforelasticsearch.sql.ast.expression.Field; @@ -41,6 +43,7 @@ import com.amazon.opendistroforelasticsearch.sql.ast.tree.Dedupe; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Eval; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Filter; +import com.amazon.opendistroforelasticsearch.sql.ast.tree.Head; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Project; import com.amazon.opendistroforelasticsearch.sql.ast.tree.RareTopN; import com.amazon.opendistroforelasticsearch.sql.ast.tree.RareTopN.CommandType; @@ -48,6 +51,7 @@ import com.amazon.opendistroforelasticsearch.sql.ast.tree.Rename; import com.amazon.opendistroforelasticsearch.sql.ast.tree.Sort; import com.amazon.opendistroforelasticsearch.sql.ast.tree.UnresolvedPlan; +import com.amazon.opendistroforelasticsearch.sql.common.utils.StringUtils; import com.amazon.opendistroforelasticsearch.sql.expression.Expression; import com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser; import com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.ByClauseContext; @@ -59,6 +63,8 @@ import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.tree.ParseTree; /** @@ -67,8 +73,15 @@ */ @RequiredArgsConstructor public class AstBuilder extends OpenDistroPPLParserBaseVisitor { + private final AstExpressionBuilder expressionBuilder; + /** + * PPL query to get original token text. This is necessary because token.getText() returns + * text without whitespaces or other characters discarded by lexer. + */ + private final String query; + @Override public UnresolvedPlan visitPplStatement(PplStatementContext ctx) { UnresolvedPlan search = visit(ctx.searchCommand()); @@ -138,25 +151,29 @@ public UnresolvedPlan visitRenameCommand(RenameCommandContext ctx) { @Override public UnresolvedPlan visitStatsCommand(StatsCommandContext ctx) { ImmutableList.Builder aggListBuilder = new ImmutableList.Builder<>(); - ImmutableList.Builder renameListBuilder = new ImmutableList.Builder<>(); for (OpenDistroPPLParser.StatsAggTermContext aggCtx : ctx.statsAggTerm()) { UnresolvedExpression aggExpression = visitExpression(aggCtx.statsFunction()); - aggListBuilder.add(aggExpression); - if (aggCtx.alias != null) { - renameListBuilder - .add(new Map(aggExpression, visitExpression(aggCtx.alias))); - } + String name = aggCtx.alias == null ? getTextInQuery(aggCtx) : StringUtils + .unquoteIdentifier(aggCtx.alias.getText()); + Alias alias = new Alias(name, aggExpression); + aggListBuilder.add(alias); } + List groupList = ctx.byClause() == null ? Collections.emptyList() : - getGroupByList(ctx.byClause()); + ctx.byClause() + .fieldList() + .fieldExpression() + .stream() + .map(groupCtx -> new Alias(getTextInQuery(groupCtx), visitExpression(groupCtx))) + .collect(Collectors.toList()); + Aggregation aggregation = new Aggregation( aggListBuilder.build(), Collections.emptyList(), groupList, ArgumentFactory.getArgumentList(ctx) ); - List renameList = renameListBuilder.build(); - return renameList.isEmpty() ? aggregation : new Rename(renameList, aggregation); + return aggregation; } /** @@ -170,6 +187,16 @@ public UnresolvedPlan visitDedupCommand(DedupCommandContext ctx) { ); } + /** + * Head command visitor. + */ + @Override + public UnresolvedPlan visitHeadCommand(HeadCommandContext ctx) { + UnresolvedExpression unresolvedExpr = + ctx.whileExpr != null ? visitExpression(ctx.logicalExpression()) : null; + return new Head(ArgumentFactory.getArgumentList(ctx, unresolvedExpr)); + } + /** * Sort command. */ @@ -266,4 +293,12 @@ protected UnresolvedPlan aggregateResult(UnresolvedPlan aggregate, UnresolvedPla return aggregate; } + /** + * Get original text in query. + */ + private String getTextInQuery(ParserRuleContext ctx) { + Token start = ctx.getStart(); + Token stop = ctx.getStop(); + return query.substring(start.getStartIndex(), stop.getStopIndex() + 1); + } } diff --git a/ppl/src/main/java/com/amazon/opendistroforelasticsearch/sql/ppl/utils/ArgumentFactory.java b/ppl/src/main/java/com/amazon/opendistroforelasticsearch/sql/ppl/utils/ArgumentFactory.java index abb525e06f..872e14c9dc 100644 --- a/ppl/src/main/java/com/amazon/opendistroforelasticsearch/sql/ppl/utils/ArgumentFactory.java +++ b/ppl/src/main/java/com/amazon/opendistroforelasticsearch/sql/ppl/utils/ArgumentFactory.java @@ -18,6 +18,7 @@ import static com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.BooleanLiteralContext; import static com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.DedupCommandContext; import static com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.FieldsCommandContext; +import static com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.HeadCommandContext; import static com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.IntegerLiteralContext; import static com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.RareCommandContext; import static com.amazon.opendistroforelasticsearch.sql.ppl.antlr.parser.OpenDistroPPLParser.SortCommandContext; @@ -28,10 +29,13 @@ import com.amazon.opendistroforelasticsearch.sql.ast.expression.Argument; import com.amazon.opendistroforelasticsearch.sql.ast.expression.DataType; import com.amazon.opendistroforelasticsearch.sql.ast.expression.Literal; +import com.amazon.opendistroforelasticsearch.sql.ast.expression.UnresolvedArgument; +import com.amazon.opendistroforelasticsearch.sql.ast.expression.UnresolvedExpression; import com.amazon.opendistroforelasticsearch.sql.common.utils.StringUtils; import java.util.Arrays; import java.util.Collections; import java.util.List; + import org.antlr.v4.runtime.ParserRuleContext; @@ -97,6 +101,27 @@ public static List getArgumentList(DedupCommandContext ctx) { ); } + /** + * Get list of {@link Argument}. + * + * @param ctx HeadCommandContext instance + * @return the list of arguments fetched from the head command + */ + public static List getArgumentList(HeadCommandContext ctx, + UnresolvedExpression unresolvedExpr) { + return Arrays.asList( + ctx.keeplast != null + ? new UnresolvedArgument("keeplast", getArgumentValue(ctx.keeplast)) + : new UnresolvedArgument("keeplast", new Literal(true, DataType.BOOLEAN)), + ctx.whileExpr != null && unresolvedExpr != null + ? new UnresolvedArgument("whileExpr", unresolvedExpr) + : new UnresolvedArgument("whileExpr", new Literal(true, DataType.BOOLEAN)), + ctx.number != null + ? new UnresolvedArgument("number", getArgumentValue(ctx.number)) + : new UnresolvedArgument("number", new Literal(10, DataType.INTEGER)) + ); + } + /** * Get list of {@link Argument}. * diff --git a/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/PPLServiceTest.java b/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/PPLServiceTest.java index ed8449bd0c..4d6acf62f0 100644 --- a/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/PPLServiceTest.java +++ b/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/PPLServiceTest.java @@ -22,6 +22,8 @@ import com.amazon.opendistroforelasticsearch.sql.common.response.ResponseListener; import com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType; import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine; +import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.ExplainResponse; +import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.ExplainResponseNode; import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.QueryResponse; import com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlan; import com.amazon.opendistroforelasticsearch.sql.ppl.config.PPLServiceConfig; @@ -83,7 +85,7 @@ public void testExecuteShouldPass() { return null; }).when(executionEngine).execute(any(), any()); - pplService.execute(new PPLQueryRequest("search source=t a=1", null), + pplService.execute(new PPLQueryRequest("search source=t a=1", null, null), new ResponseListener() { @Override public void onResponse(QueryResponse pplQueryResponse) { @@ -97,33 +99,72 @@ public void onFailure(Exception e) { }); } + @Test + public void testExplainShouldPass() { + doAnswer(invocation -> { + ResponseListener listener = invocation.getArgument(1); + listener.onResponse(new ExplainResponse(new ExplainResponseNode("test"))); + return null; + }).when(executionEngine).explain(any(), any()); + + pplService.explain(new PPLQueryRequest("search source=t a=1", null, null), + new ResponseListener() { + @Override + public void onResponse(ExplainResponse pplQueryResponse) { + } + + @Override + public void onFailure(Exception e) { + Assert.fail(); + } + }); + } + @Test public void testExecuteWithIllegalQueryShouldBeCaughtByHandler() { - pplService.execute(new PPLQueryRequest("search", null), new ResponseListener() { - @Override - public void onResponse(QueryResponse pplQueryResponse) { - Assert.fail(); - } + pplService.execute(new PPLQueryRequest("search", null, null), + new ResponseListener() { + @Override + public void onResponse(QueryResponse pplQueryResponse) { + Assert.fail(); + } + + @Override + public void onFailure(Exception e) { - @Override - public void onFailure(Exception e) { + } + }); + } + + @Test + public void testExplainWithIllegalQueryShouldBeCaughtByHandler() { + pplService.explain(new PPLQueryRequest("search", null, null), + new ResponseListener() { + @Override + public void onResponse(ExplainResponse pplQueryResponse) { + Assert.fail(); + } + + @Override + public void onFailure(Exception e) { - } - }); + } + }); } @Test public void test() { - pplService.execute(new PPLQueryRequest("search", null), new ResponseListener() { - @Override - public void onResponse(QueryResponse pplQueryResponse) { - Assert.fail(); - } + pplService.execute(new PPLQueryRequest("search", null, null), + new ResponseListener() { + @Override + public void onResponse(QueryResponse pplQueryResponse) { + Assert.fail(); + } - @Override - public void onFailure(Exception e) { + @Override + public void onFailure(Exception e) { - } - }); + } + }); } } \ No newline at end of file diff --git a/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/domain/PPLQueryRequestTest.java b/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/domain/PPLQueryRequestTest.java index 0b1e959d22..5e80f51158 100644 --- a/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/domain/PPLQueryRequestTest.java +++ b/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/domain/PPLQueryRequestTest.java @@ -15,12 +15,22 @@ package com.amazon.opendistroforelasticsearch.sql.ppl.domain; +import static org.junit.Assert.assertTrue; + import org.junit.Test; public class PPLQueryRequestTest { @Test public void getRequestShouldPass() { - PPLQueryRequest request = new PPLQueryRequest("source=t a=1", null); + PPLQueryRequest request = new PPLQueryRequest("source=t a=1", null, null); request.getRequest(); } + + @Test + public void testExplainRequest() { + PPLQueryRequest request = new PPLQueryRequest( + "source=t a=1", null, "/_opendistro/_ppl/_explain"); + assertTrue(request.isExplainRequest()); + } + } diff --git a/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/parser/AstBuilderTest.java b/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/parser/AstBuilderTest.java index 0bff1d62bf..ac174d9019 100644 --- a/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/parser/AstBuilderTest.java +++ b/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/parser/AstBuilderTest.java @@ -17,12 +17,14 @@ import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.agg; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.aggregate; +import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.alias; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.argument; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.booleanLiteral; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.compare; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.dedupe; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.defaultDedupArgs; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.defaultFieldsArgs; +import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.defaultHeadArgs; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.defaultSortFieldArgs; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.defaultSortOptions; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.defaultStatsArgs; @@ -31,6 +33,7 @@ import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.field; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.filter; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.function; +import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.head; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.intLiteral; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.let; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.map; @@ -42,6 +45,8 @@ import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.sort; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.sortOptions; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.stringLiteral; +import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.unresolvedArg; +import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.unresolvedArgList; import static java.util.Collections.emptyList; import static org.junit.Assert.assertEquals; @@ -54,7 +59,6 @@ public class AstBuilderTest { private PPLSyntaxParser parser = new PPLSyntaxParser(); - private AstBuilder astBuilder = new AstBuilder(new AstExpressionBuilder()); @Test public void testSearchCommand() { @@ -161,7 +165,10 @@ public void testStatsCommand() { agg( relation("t"), exprList( - aggregate("count", field("a")) + alias( + "count(a)", + aggregate("count", field("a")) + ) ), emptyList(), emptyList(), @@ -175,10 +182,17 @@ public void testStatsCommandWithByClause() { agg( relation("t"), exprList( - aggregate("count", field("a")) + alias( + "count(a)", + aggregate("count", field("a")) + ) ), emptyList(), - exprList(field("b")), + exprList( + alias( + "b", + field("b") + )), defaultStatsArgs() )); } @@ -186,17 +200,17 @@ public void testStatsCommandWithByClause() { @Test public void testStatsCommandWithAlias() { assertEqual("source=t | stats count(a) as alias", - rename( - agg( - relation("t"), - exprList( + agg( + relation("t"), + exprList( + alias( + "alias", aggregate("count", field("a")) - ), - emptyList(), - emptyList(), - defaultStatsArgs() + ) ), - map(aggregate("count", field("a")), field("alias")) + emptyList(), + emptyList(), + defaultStatsArgs() ) ); } @@ -207,10 +221,13 @@ public void testStatsCommandWithNestedFunctions() { agg( relation("t"), exprList( - aggregate( - "sum", - function("+", field("a"), field("b")) - )), + alias( + "sum(a+b)", + aggregate( + "sum", + function("+", field("a"), field("b")) + )) + ), emptyList(), emptyList(), defaultStatsArgs() @@ -219,12 +236,15 @@ public void testStatsCommandWithNestedFunctions() { agg( relation("t"), exprList( - aggregate( - "sum", - function( - "/", - function("abs", field("a")), - intLiteral(2) + alias( + "sum(abs(a)/2)", + aggregate( + "sum", + function( + "/", + function("abs", field("a")), + intLiteral(2) + ) ) ) ), @@ -259,6 +279,53 @@ public void testDedupCommandWithSortby() { )); } + @Test + public void testHeadCommand() { + assertEqual("source=t | head", + head( + relation("t"), + defaultHeadArgs() + )); + } + + @Test + public void testHeadCommandWithNumber() { + assertEqual("source=t | head 3", + head( + relation("t"), + unresolvedArgList( + unresolvedArg("keeplast", booleanLiteral(true)), + unresolvedArg("whileExpr", booleanLiteral(true)), + unresolvedArg("number", intLiteral(3))) + )); + } + + @Test + public void testHeadCommandWithWhileExpr() { + + assertEqual("source=t | head while(a < 5) 5", + head( + relation("t"), + unresolvedArgList( + unresolvedArg("keeplast", booleanLiteral(true)), + unresolvedArg("whileExpr", compare("<", field("a"), intLiteral(5))), + unresolvedArg("number", intLiteral(5))) + )); + } + + @Test + public void testHeadCommandWithKeepLast() { + + assertEqual("source=t | head keeplast=false while(a < 5) 5", + head( + relation("t"), + unresolvedArgList( + unresolvedArg("keeplast", booleanLiteral(false)), + unresolvedArg("whileExpr", compare("<", field("a"), intLiteral(5))), + unresolvedArg("number", intLiteral(5))) + )); + } + @Test public void testSortCommand() { assertEqual("source=t | sort f1, f2", @@ -400,6 +467,7 @@ protected void assertEqual(String query, String expected) { } private Node plan(String query) { + AstBuilder astBuilder = new AstBuilder(new AstExpressionBuilder(), query); return astBuilder.visit(parser.analyzeSyntax(query)); } } diff --git a/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/parser/AstExpressionBuilderTest.java b/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/parser/AstExpressionBuilderTest.java index b2598e3b1f..962cbcb34c 100644 --- a/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/parser/AstExpressionBuilderTest.java +++ b/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/parser/AstExpressionBuilderTest.java @@ -17,6 +17,7 @@ import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.agg; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.aggregate; +import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.alias; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.and; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.argument; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.booleanLiteral; @@ -295,11 +296,17 @@ public void testAggFuncCallExpr() { agg( relation("t"), exprList( - aggregate("avg", field("a")) - + alias( + "avg(a)", + aggregate("avg", field("a")) + ) ), emptyList(), - exprList(field("b")), + exprList( + alias( + "b", + field("b") + )), defaultStatsArgs() )); } @@ -310,10 +317,12 @@ public void testPercentileAggFuncExpr() { agg( relation("t"), exprList( - aggregate( - "percentile", - field("a"), - argument("rank", intLiteral(1)) + alias("percentile<1>(a)", + aggregate( + "percentile", + field("a"), + argument("rank", intLiteral(1)) + ) ) ), emptyList(), diff --git a/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/utils/ArgumentFactoryTest.java b/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/utils/ArgumentFactoryTest.java index f2400b5ade..9e8512fce8 100644 --- a/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/utils/ArgumentFactoryTest.java +++ b/ppl/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/utils/ArgumentFactoryTest.java @@ -17,6 +17,7 @@ import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.agg; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.aggregate; +import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.alias; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.argument; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.booleanLiteral; import static com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL.dedupe; @@ -57,7 +58,11 @@ public void testStatsCommandArgument() { "source=t | stats partitions=1 allnum=false delim=',' avg(a) dedup_splitvalues=true", agg( relation("t"), - exprList(aggregate("avg", field("a"))), + exprList( + alias( + "avg(a)", + aggregate("avg", field("a"))) + ), emptyList(), emptyList(), exprList( diff --git a/release-notes/opendistro-for-elasticsearch-sql.release-notes-1.10.1.0.md b/release-notes/opendistro-for-elasticsearch-sql.release-notes-1.10.1.0.md index 9db4808eee..9485c912a5 100644 --- a/release-notes/opendistro-for-elasticsearch-sql.release-notes-1.10.1.0.md +++ b/release-notes/opendistro-for-elasticsearch-sql.release-notes-1.10.1.0.md @@ -38,7 +38,8 @@ ### Documentation * update user documentation for testing odbc driver connection on windows([#722](https://github.com/opendistro-for-elasticsearch/sql/pull/722)) * Added workaround for identifiers with special characters in troubleshooting page([#718](https://github.com/opendistro-for-elasticsearch/sql/pull/718)) -* Update release notes for OD 1.10.1 release([#699](https://github.com/opendistro-for-elasticsearch/sql/pull/699)) +* Update release notes for OD 1.10 release([#699](https://github.com/opendistro-for-elasticsearch/sql/pull/699)) ### Maintenance +* Bumped ES and Kibana versions to v7.9.0 ([#697](https://github.com/opendistro-for-elasticsearch/sql/pull/697)) * Bump ES and Kibana to 7.9.1 and release ODFE 1.10.1.0 ([#732](https://github.com/opendistro-for-elasticsearch/sql/pull/732)) diff --git a/sql-jdbc/src/main/java/ESConnect.java b/sql-jdbc/src/main/java/ESConnect.java new file mode 100644 index 0000000000..c53911d47c --- /dev/null +++ b/sql-jdbc/src/main/java/ESConnect.java @@ -0,0 +1,36 @@ +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.Statement; + +public class ESConnect { + static final String URL = "jdbc:elasticsearch://http://localhost:9200"; + + public static void main(String[] args) throws Exception { + System.out.println("Connection"); + Connection con = DriverManager.getConnection(URL, null, null); + System.out.println("Creating statement"); + Statement st = con.createStatement(); + String sql = "SELECT SUBSTRING('hello', 2)"; + System.out.println("Executing query"); + ResultSet rs = st.executeQuery(sql); + + System.out.println("Reading results."); + ResultSetMetaData metaData = rs.getMetaData(); + for (int i = 1; i <= metaData.getColumnCount(); i++) { + System.out.println(metaData.getColumnTypeName(i)); + } + System.out.println(); + + while (rs.next()) { + for (int i = 1; i <= metaData.getColumnCount(); i++) { + System.out.print("Column: " + rs.getObject(i)); + } + System.out.println(); + } + rs.close(); + st.close(); + con.close(); + } +} \ No newline at end of file diff --git a/sql-jdbc/src/main/java/com/amazon/opendistroforelasticsearch/jdbc/types/BaseTypeConverter.java b/sql-jdbc/src/main/java/com/amazon/opendistroforelasticsearch/jdbc/types/BaseTypeConverter.java index 15a88006f3..dcb2058ccd 100644 --- a/sql-jdbc/src/main/java/com/amazon/opendistroforelasticsearch/jdbc/types/BaseTypeConverter.java +++ b/sql-jdbc/src/main/java/com/amazon/opendistroforelasticsearch/jdbc/types/BaseTypeConverter.java @@ -18,6 +18,7 @@ import java.sql.Date; import java.sql.SQLException; +import java.sql.Time; import java.sql.Timestamp; import java.util.HashMap; import java.util.Map; @@ -42,6 +43,7 @@ public abstract class BaseTypeConverter implements TypeConverter { typeHandlerMap.put(Timestamp.class, TimestampType.INSTANCE); typeHandlerMap.put(Date.class, DateType.INSTANCE); + typeHandlerMap.put(Time.class, TimeType.INSTANCE); } diff --git a/sql-jdbc/src/main/java/com/amazon/opendistroforelasticsearch/jdbc/types/ElasticsearchType.java b/sql-jdbc/src/main/java/com/amazon/opendistroforelasticsearch/jdbc/types/ElasticsearchType.java index 6f754ca094..8bbca3e710 100644 --- a/sql-jdbc/src/main/java/com/amazon/opendistroforelasticsearch/jdbc/types/ElasticsearchType.java +++ b/sql-jdbc/src/main/java/com/amazon/opendistroforelasticsearch/jdbc/types/ElasticsearchType.java @@ -16,7 +16,9 @@ package com.amazon.opendistroforelasticsearch.jdbc.types; +import java.sql.Date; import java.sql.JDBCType; +import java.sql.Time; import java.sql.Timestamp; import java.util.HashMap; import java.util.Locale; @@ -71,6 +73,8 @@ public enum ElasticsearchType { NESTED(JDBCType.STRUCT, null, 0, 0, false), OBJECT(JDBCType.STRUCT, null, 0, 0, false), DATE(JDBCType.TIMESTAMP, Timestamp.class, 24, 24, false), + TIME(JDBCType.TIME, Time.class, 24, 24, false), + TIMESTAMP(JDBCType.TIMESTAMP, Timestamp.class, 24, 24, false), NULL(JDBCType.NULL, null, 0, 0, false), UNSUPPORTED(JDBCType.OTHER, null, 0, 0, false); @@ -89,7 +93,8 @@ public enum ElasticsearchType { jdbcTypeToESTypeMap.put(JDBCType.REAL, FLOAT); jdbcTypeToESTypeMap.put(JDBCType.FLOAT, DOUBLE); jdbcTypeToESTypeMap.put(JDBCType.VARCHAR, KEYWORD); - jdbcTypeToESTypeMap.put(JDBCType.TIMESTAMP, DATE); + jdbcTypeToESTypeMap.put(JDBCType.TIMESTAMP, TIMESTAMP); + jdbcTypeToESTypeMap.put(JDBCType.TIME, TIME); jdbcTypeToESTypeMap.put(JDBCType.DATE, DATE); } diff --git a/sql-jdbc/src/main/java/com/amazon/opendistroforelasticsearch/jdbc/types/TimeType.java b/sql-jdbc/src/main/java/com/amazon/opendistroforelasticsearch/jdbc/types/TimeType.java new file mode 100644 index 0000000000..6546e5d168 --- /dev/null +++ b/sql-jdbc/src/main/java/com/amazon/opendistroforelasticsearch/jdbc/types/TimeType.java @@ -0,0 +1,73 @@ +/* + * 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.jdbc.types; + +import java.sql.SQLException; +import java.sql.Time; +import java.time.LocalTime; +import java.util.Map; + +public class TimeType implements TypeHelper