From 81b6699a022fa0946511085cf6bc193858e4dc92 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 09:38:33 +0100 Subject: [PATCH 01/71] Fixed range-handling for Modification Executors --- arangod/Aql/AqlItemBlockInputMatrix.cpp | 21 +++++++++------ arangod/Aql/AqlItemBlockInputMatrix.h | 3 ++- arangod/Aql/ModificationExecutor.cpp | 35 +++++++++++++++++-------- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/arangod/Aql/AqlItemBlockInputMatrix.cpp b/arangod/Aql/AqlItemBlockInputMatrix.cpp index bbd495784e27..3d4fc6839b7a 100644 --- a/arangod/Aql/AqlItemBlockInputMatrix.cpp +++ b/arangod/Aql/AqlItemBlockInputMatrix.cpp @@ -54,18 +54,22 @@ AqlItemBlockInputMatrix::AqlItemBlockInputMatrix(ExecutorState state, AqlItemMat } } -AqlItemBlockInputRange AqlItemBlockInputMatrix::getNextInputRange() { +AqlItemBlockInputRange& AqlItemBlockInputMatrix::getInputRange() { TRI_ASSERT(_aqlItemMatrix != nullptr); + if (_lastRange.hasDataRow()) { + return _lastRange; + } + // Need initialze lastRange if (_aqlItemMatrix->numberOfBlocks() == 0) { - return AqlItemBlockInputRange{upstreamState()}; + _lastRange = {AqlItemBlockInputRange{upstreamState()}}; + } else { + SharedAqlItemBlockPtr blockPtr = _aqlItemMatrix->getBlock(_currentBlockRowIndex); + auto [start, end] = blockPtr->getRelevantRange(); + ExecutorState state = incrBlockIndex(); + _lastRange = {state, 0, std::move(blockPtr), start}; } - - SharedAqlItemBlockPtr blockPtr = _aqlItemMatrix->getBlock(_currentBlockRowIndex); - auto [start, end] = blockPtr->getRelevantRange(); - ExecutorState state = incrBlockIndex(); - - return {state, 0, std::move(blockPtr), start}; + return _lastRange; } SharedAqlItemBlockPtr AqlItemBlockInputMatrix::getBlock() const noexcept { @@ -170,5 +174,6 @@ ExecutorState AqlItemBlockInputMatrix::incrBlockIndex() { } void AqlItemBlockInputMatrix::resetBlockIndex() noexcept { + _lastRange = {AqlItemBlockInputRange{upstreamState()}}; _currentBlockRowIndex = 0; } diff --git a/arangod/Aql/AqlItemBlockInputMatrix.h b/arangod/Aql/AqlItemBlockInputMatrix.h index 6e638aa2dbb6..01deb9852df7 100644 --- a/arangod/Aql/AqlItemBlockInputMatrix.h +++ b/arangod/Aql/AqlItemBlockInputMatrix.h @@ -51,7 +51,7 @@ class AqlItemBlockInputMatrix { // Will provide access to the first block (from _aqlItemMatrix) // After a block has been delivered, the block index will be increased. // Next call then will deliver the next block etc. - AqlItemBlockInputRange getNextInputRange(); + AqlItemBlockInputRange& getInputRange(); std::pair getMatrix() noexcept; ExecutorState upstreamState() const noexcept; @@ -70,6 +70,7 @@ class AqlItemBlockInputMatrix { // Only if _aqlItemMatrix is set (and NOT a nullptr), we have a valid and // usable DataRange object available to work with. AqlItemMatrix* _aqlItemMatrix; + AqlItemBlockInputRange _lastRange{ExecutorState::HASMORE}; size_t _currentBlockRowIndex = 0; ShadowAqlItemRow _shadowRow{CreateInvalidShadowRowHint{}}; }; diff --git a/arangod/Aql/ModificationExecutor.cpp b/arangod/Aql/ModificationExecutor.cpp index 5556ae39b93d..0b4061d025cd 100644 --- a/arangod/Aql/ModificationExecutor.cpp +++ b/arangod/Aql/ModificationExecutor.cpp @@ -173,16 +173,22 @@ template auto stats = ModificationStats{}; _modifier.reset(); - - ExecutorState upstreamState = ExecutorState::HASMORE; + if (!input.hasDataRow()) { + // Input is empty + return {input.upstreamState(), stats, AqlCall{}}; + } // only produce at most output.numRowsLeft() many results if constexpr (std::is_same_v) { - auto range = input.getNextInputRange(); + auto range = input.getInputRange(); doCollect(range, output.numRowsLeft()); - upstreamState = range.upstreamState(); + if (range.upstreamState() == ExecutorState::DONE) { + // We are done with this input. + // We need to forward it to the last ShadowRow. + input.skipAllRemainingDataRows(); + TRI_ASSERT(input.upstreamState() == ExecutorState::DONE); + } } else { doCollect(input, output.numRowsLeft()); - upstreamState = input.upstreamState(); } if (_modifier.nrOfOperations() > 0) { @@ -196,7 +202,7 @@ template doOutput(output, stats); } - return {upstreamState, stats, AqlCall{}}; + return {input.upstreamState(), stats, AqlCall{}}; } template @@ -205,16 +211,23 @@ template -> std::tuple { auto stats = ModificationStats{}; _modifier.reset(); + if (!input.hasDataRow()) { + // Input is empty + return {input.upstreamState(), stats, 0, AqlCall{}}; + } - ExecutorState upstreamState = ExecutorState::HASMORE; // only produce at most output.numRowsLeft() many results if constexpr (std::is_same_v) { - auto range = input.getNextInputRange(); + auto range = input.getInputRange(); doCollect(range, call.getOffset()); - upstreamState = range.upstreamState(); + if (range.upstreamState() == ExecutorState::DONE) { + // We are done with this input. + // We need to forward it to the last ShadowRow. + input.skipAllRemainingDataRows(); + TRI_ASSERT(input.upstreamState() == ExecutorState::DONE); + } } else { doCollect(input, call.getOffset()); - upstreamState = input.upstreamState(); } if (_modifier.nrOfOperations() > 0) { @@ -228,7 +241,7 @@ template call.didSkip(_modifier.nrOfOperations()); } - return {upstreamState, stats, _modifier.nrOfOperations(), AqlCall{}}; + return {input.upstreamState(), stats, _modifier.nrOfOperations(), AqlCall{}}; } using NoPassthroughSingleRowFetcher = SingleRowFetcher; From 01025a78f8b87a7a8d43c7954a5fd6d1ed0ff1df Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 10:09:21 +0100 Subject: [PATCH 02/71] DataRange handling in ModificationExecutor --- arangod/Aql/ModificationExecutor.cpp | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/arangod/Aql/ModificationExecutor.cpp b/arangod/Aql/ModificationExecutor.cpp index 0b4061d025cd..93e63f95ea7a 100644 --- a/arangod/Aql/ModificationExecutor.cpp +++ b/arangod/Aql/ModificationExecutor.cpp @@ -178,17 +178,19 @@ template return {input.upstreamState(), stats, AqlCall{}}; } // only produce at most output.numRowsLeft() many results + ExecutorState upstreamState = ExecutorState::HASMORE; if constexpr (std::is_same_v) { - auto range = input.getInputRange(); + auto& range = input.getInputRange(); doCollect(range, output.numRowsLeft()); - if (range.upstreamState() == ExecutorState::DONE) { + upstreamState = range.upstreamState(); + if (upstreamState == ExecutorState::DONE) { // We are done with this input. // We need to forward it to the last ShadowRow. input.skipAllRemainingDataRows(); - TRI_ASSERT(input.upstreamState() == ExecutorState::DONE); } } else { doCollect(input, output.numRowsLeft()); + upstreamState = input.upstreamState(); } if (_modifier.nrOfOperations() > 0) { @@ -202,7 +204,7 @@ template doOutput(output, stats); } - return {input.upstreamState(), stats, AqlCall{}}; + return {upstreamState, stats, AqlCall{}}; } template @@ -216,11 +218,19 @@ template return {input.upstreamState(), stats, 0, AqlCall{}}; } + size_t toSkip = call.getOffset(); + if (call.getLimit() == 0 && call.hasHardLimit()) { + // We need to produce all modification operations. + // If we are bound by limits or not! + toSkip = ExecutionBlock::SkipAllSize(); + } // only produce at most output.numRowsLeft() many results + ExecutorState upstreamState = ExecutorState::HASMORE; if constexpr (std::is_same_v) { - auto range = input.getInputRange(); + auto& range = input.getInputRange(); doCollect(range, call.getOffset()); - if (range.upstreamState() == ExecutorState::DONE) { + upstreamState = range.upstreamState(); + if (upstreamState == ExecutorState::DONE) { // We are done with this input. // We need to forward it to the last ShadowRow. input.skipAllRemainingDataRows(); @@ -228,6 +238,7 @@ template } } else { doCollect(input, call.getOffset()); + upstreamState = input.upstreamState(); } if (_modifier.nrOfOperations() > 0) { @@ -241,7 +252,7 @@ template call.didSkip(_modifier.nrOfOperations()); } - return {input.upstreamState(), stats, _modifier.nrOfOperations(), AqlCall{}}; + return {upstreamState, stats, _modifier.nrOfOperations(), AqlCall{}}; } using NoPassthroughSingleRowFetcher = SingleRowFetcher; From fe5331700978eadd0f93dd27c8141bdaa4b73d00 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 11:14:49 +0100 Subject: [PATCH 03/71] Honor batch-size defined by UpstreamExecutor --- arangod/Aql/ModificationExecutor.cpp | 35 +++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/arangod/Aql/ModificationExecutor.cpp b/arangod/Aql/ModificationExecutor.cpp index 93e63f95ea7a..ce432981bdee 100644 --- a/arangod/Aql/ModificationExecutor.cpp +++ b/arangod/Aql/ModificationExecutor.cpp @@ -169,14 +169,23 @@ template typename FetcherType::DataRange& input, OutputAqlItemRow& output) -> std::tuple { TRI_ASSERT(_infos._trx); - + AqlCall upstreamCall{}; + if constexpr (std::is_same_v && + !std::is_same_v) { + upstreamCall.softLimit = _modifier.getBatchSize(); + } auto stats = ModificationStats{}; _modifier.reset(); if (!input.hasDataRow()) { // Input is empty - return {input.upstreamState(), stats, AqlCall{}}; + return {input.upstreamState(), stats, upstreamCall}; + } + + TRI_IF_FAILURE("ModificationBlock::getSome") { + THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); } + // only produce at most output.numRowsLeft() many results ExecutorState upstreamState = ExecutorState::HASMORE; if constexpr (std::is_same_v) { @@ -204,18 +213,31 @@ template doOutput(output, stats); } - return {upstreamState, stats, AqlCall{}}; + return {upstreamState, stats, upstreamCall}; } template [[nodiscard]] auto ModificationExecutor::skipRowsRange( typename FetcherType::DataRange& input, AqlCall& call) -> std::tuple { + AqlCall upstreamCall{}; + if constexpr (std::is_same_v && + !std::is_same_v) { + upstreamCall.softLimit = _modifier.getBatchSize(); + } + auto stats = ModificationStats{}; + _modifier.reset(); + if (!input.hasDataRow()) { - // Input is empty - return {input.upstreamState(), stats, 0, AqlCall{}}; + LOG_DEVEL << "Initially no row" + // Input is empty + return {input.upstreamState(), stats, 0, upstreamCall}; + } + + TRI_IF_FAILURE("ModificationBlock::getSome") { + THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); } size_t toSkip = call.getOffset(); @@ -240,6 +262,7 @@ template doCollect(input, call.getOffset()); upstreamState = input.upstreamState(); } + LOG_DEVEL << "We skipped and got state: " << upstreamState; if (_modifier.nrOfOperations() > 0) { _modifier.transact(); @@ -252,7 +275,7 @@ template call.didSkip(_modifier.nrOfOperations()); } - return {upstreamState, stats, _modifier.nrOfOperations(), AqlCall{}}; + return {upstreamState, stats, _modifier.nrOfOperations(), upstreamCall}; } using NoPassthroughSingleRowFetcher = SingleRowFetcher; From 4bae2e4d3f5950c8a36cbd8aa4c284cd36737dd6 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 11:16:05 +0100 Subject: [PATCH 04/71] Fixed compile issue --- arangod/Aql/ModificationExecutor.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/arangod/Aql/ModificationExecutor.cpp b/arangod/Aql/ModificationExecutor.cpp index ce432981bdee..57e7f8513343 100644 --- a/arangod/Aql/ModificationExecutor.cpp +++ b/arangod/Aql/ModificationExecutor.cpp @@ -231,9 +231,8 @@ template _modifier.reset(); if (!input.hasDataRow()) { - LOG_DEVEL << "Initially no row" - // Input is empty - return {input.upstreamState(), stats, 0, upstreamCall}; + // Input is empty + return {input.upstreamState(), stats, 0, upstreamCall}; } TRI_IF_FAILURE("ModificationBlock::getSome") { From 8a6d636d9166e9c20d9b511ffa4e1bc70012e210 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 11:53:39 +0100 Subject: [PATCH 05/71] More fixes in modification --- arangod/Aql/ModificationExecutor.cpp | 71 ++++++++++++++-------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/arangod/Aql/ModificationExecutor.cpp b/arangod/Aql/ModificationExecutor.cpp index 57e7f8513343..881b4281f7c3 100644 --- a/arangod/Aql/ModificationExecutor.cpp +++ b/arangod/Aql/ModificationExecutor.cpp @@ -228,53 +228,52 @@ template auto stats = ModificationStats{}; - _modifier.reset(); - - if (!input.hasDataRow()) { - // Input is empty - return {input.upstreamState(), stats, 0, upstreamCall}; - } - TRI_IF_FAILURE("ModificationBlock::getSome") { THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); } - size_t toSkip = call.getOffset(); - if (call.getLimit() == 0 && call.hasHardLimit()) { - // We need to produce all modification operations. - // If we are bound by limits or not! - toSkip = ExecutionBlock::SkipAllSize(); - } // only produce at most output.numRowsLeft() many results - ExecutorState upstreamState = ExecutorState::HASMORE; - if constexpr (std::is_same_v) { - auto& range = input.getInputRange(); - doCollect(range, call.getOffset()); - upstreamState = range.upstreamState(); - if (upstreamState == ExecutorState::DONE) { - // We are done with this input. - // We need to forward it to the last ShadowRow. - input.skipAllRemainingDataRows(); - TRI_ASSERT(input.upstreamState() == ExecutorState::DONE); + ExecutorState upstreamState = input.upstreamState(); + while (input.hasDataRow() && call.needSkipMore()) { + _modifier.reset(); + size_t toSkip = call.getOffset(); + if (call.getLimit() == 0 && call.hasHardLimit()) { + // We need to produce all modification operations. + // If we are bound by limits or not! + toSkip = ExecutionBlock::SkipAllSize(); + } + if constexpr (std::is_same_v) { + auto& range = input.getInputRange(); + if (range.hasDataRow()) { + doCollect(range, toSkip); + } + upstreamState = range.upstreamState(); + if (upstreamState == ExecutorState::DONE) { + // We are done with this input. + // We need to forward it to the last ShadowRow. + input.skipAllRemainingDataRows(); + TRI_ASSERT(input.upstreamState() == ExecutorState::DONE); + } + } else { + doCollect(input, toSkip); + upstreamState = input.upstreamState(); } - } else { - doCollect(input, call.getOffset()); - upstreamState = input.upstreamState(); - } - LOG_DEVEL << "We skipped and got state: " << upstreamState; - if (_modifier.nrOfOperations() > 0) { - _modifier.transact(); + if (_modifier.nrOfOperations() > 0) { + _modifier.transact(); - if (_infos._doCount) { - stats.addWritesExecuted(_modifier.nrOfWritesExecuted()); - stats.addWritesIgnored(_modifier.nrOfWritesIgnored()); - } + if (_infos._doCount) { + stats.addWritesExecuted(_modifier.nrOfWritesExecuted()); + stats.addWritesIgnored(_modifier.nrOfWritesIgnored()); + } - call.didSkip(_modifier.nrOfOperations()); + call.didSkip(_modifier.nrOfOperations()); + } } - return {upstreamState, stats, _modifier.nrOfOperations(), upstreamCall}; + LOG_DEVEL << "We skipped and got state: " << upstreamState; + + return {upstreamState, stats, call.getSkipCount(), upstreamCall}; } using NoPassthroughSingleRowFetcher = SingleRowFetcher; From 9cc084b3be25ac10e87f2970b79044bc72d1263a Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 11:54:16 +0100 Subject: [PATCH 06/71] Remvoed log devel --- arangod/Aql/ModificationExecutor.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/arangod/Aql/ModificationExecutor.cpp b/arangod/Aql/ModificationExecutor.cpp index 881b4281f7c3..6554c6574fed 100644 --- a/arangod/Aql/ModificationExecutor.cpp +++ b/arangod/Aql/ModificationExecutor.cpp @@ -271,8 +271,6 @@ template } } - LOG_DEVEL << "We skipped and got state: " << upstreamState; - return {upstreamState, stats, call.getSkipCount(), upstreamCall}; } From 5c4e05e682faba2827ec17db01d5e6d406ecc29e Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 12:40:53 +0100 Subject: [PATCH 07/71] Fixed profiler Test. for NoResults node we cahnge the behaviour --- tests/js/server/aql/aql-profiler.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/js/server/aql/aql-profiler.js b/tests/js/server/aql/aql-profiler.js index 50eca3fee3a3..31c08f58d75c 100644 --- a/tests/js/server/aql/aql-profiler.js +++ b/tests/js/server/aql/aql-profiler.js @@ -571,13 +571,13 @@ function ahuacatlProfilerTestSuite () { testNoResultsBlock1: function() { const query = 'FOR i IN 1..@rows FILTER 1 == 0 RETURN i'; - // As the descendant blocks of NoResultsBlock don't get a single getSome - // call, they don't show up in the statistics. + // Also if we have no results, we do send a drop-all to dependecies + // potentielly we have modifiaction nodes that need to be executed. const genNodeList = () => [ - {type: SingletonBlock, calls: 0, items: 0}, - {type: CalculationBlock, calls: 0, items: 0}, - {type: EnumerateListBlock, calls: 0, items: 0}, + {type: SingletonBlock, calls: 1, items: 0}, + {type: CalculationBlock, calls: 1, items: 0}, + {type: EnumerateListBlock, calls: 1, items: 0}, {type: NoResultsBlock, calls: 1, items: 0}, {type: ReturnBlock, calls: 1, items: 0}, ]; From e25a5ca515055ae8839227b23597d8116f02013a Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 12:41:58 +0100 Subject: [PATCH 08/71] Activated getSome failure tests in ExecuteRestHandler --- arangod/Aql/RestAqlHandler.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/arangod/Aql/RestAqlHandler.cpp b/arangod/Aql/RestAqlHandler.cpp index 58346af21a67..fa57a645b8b2 100644 --- a/arangod/Aql/RestAqlHandler.cpp +++ b/arangod/Aql/RestAqlHandler.cpp @@ -734,6 +734,9 @@ RestStatus RestAqlHandler::handleUseQuery(std::string const& operation, generateError(std::move(maybeExecuteCall).result()); return RestStatus::DONE; } + TRI_IF_FAILURE("RestAqlHandler::getSome") { + THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); + } auto& executeCall = maybeExecuteCall.get(); auto items = SharedAqlItemBlockPtr{}; From 08709e972836923bfbc79c2d9f5cced7ea7aa32c Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 13:52:03 +0100 Subject: [PATCH 09/71] Fixed skipping in Index --- arangod/Aql/EnumerateCollectionExecutor.cpp | 15 +++------------ arangod/Utils/OperationCursor.cpp | 2 +- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/arangod/Aql/EnumerateCollectionExecutor.cpp b/arangod/Aql/EnumerateCollectionExecutor.cpp index 3ff3220d415c..7dfe8894fb87 100644 --- a/arangod/Aql/EnumerateCollectionExecutor.cpp +++ b/arangod/Aql/EnumerateCollectionExecutor.cpp @@ -195,12 +195,10 @@ std::tuple EnumerateCo AqlItemBlockInputRange& inputRange, AqlCall& call) { AqlCall upstreamCall{}; EnumerateCollectionStats stats{}; - bool offsetPhase = (call.getOffset() > 0); TRI_ASSERT(_documentProducingFunctionContext.getAndResetNumScanned() == 0); TRI_ASSERT(_documentProducingFunctionContext.getAndResetNumFiltered() == 0); - - while (inputRange.hasDataRow() && call.shouldSkip()) { + while ((inputRange.hasDataRow() || _cursorHasMore) && call.shouldSkip()) { uint64_t skipped = 0; if (!_cursorHasMore) { @@ -210,13 +208,8 @@ std::tuple EnumerateCo if (_cursorHasMore) { TRI_ASSERT(_currentRow.isInitialized()); // if offset is > 0, we're in offset skip phase - if (offsetPhase) { - if (skipped < call.getOffset()) { - skipped += skipEntries(call.getOffset(), stats); - } else { - // we skipped enough in our offset phase - break; - } + if (call.getOffset() > 0) { + skipped += skipEntries(call.getOffset(), stats); } else { // fullCount phase if (_infos.getFilter() == nullptr) { @@ -276,7 +269,6 @@ std::tuple EnumerateCollection TRI_ASSERT(_documentProducingFunctionContext.getAndResetNumScanned() == 0); TRI_ASSERT(_documentProducingFunctionContext.getAndResetNumFiltered() == 0); _documentProducingFunctionContext.setOutputRow(&output); - while (inputRange.hasDataRow() && !output.isFull()) { if (!_cursorHasMore) { initializeNewRow(inputRange); @@ -309,7 +301,6 @@ std::tuple EnumerateCollection if (!_cursorHasMore) { initializeNewRow(inputRange); } - return {inputRange.upstreamState(), stats, upstreamCall}; } diff --git a/arangod/Utils/OperationCursor.cpp b/arangod/Utils/OperationCursor.cpp index 0245935344e3..198d508b838a 100644 --- a/arangod/Utils/OperationCursor.cpp +++ b/arangod/Utils/OperationCursor.cpp @@ -146,7 +146,7 @@ void OperationCursor::skipAll(uint64_t& skipped) { while (_hasMore) { uint64_t skippedLocal = 0; _indexIterator->skip(toSkip, skippedLocal); - if (skipped != toSkip) { + if (skippedLocal != toSkip) { _hasMore = false; } skipped += skippedLocal; From e7b2b00abad92876206e16b77b0ed09049a7316a Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 14:44:51 +0100 Subject: [PATCH 10/71] Let the MultiDependencySingleROwFetcher return the correct states. --- arangod/Aql/MultiDependencySingleRowFetcher.cpp | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/arangod/Aql/MultiDependencySingleRowFetcher.cpp b/arangod/Aql/MultiDependencySingleRowFetcher.cpp index 517542e953e1..f2abc6a44528 100644 --- a/arangod/Aql/MultiDependencySingleRowFetcher.cpp +++ b/arangod/Aql/MultiDependencySingleRowFetcher.cpp @@ -373,6 +373,8 @@ auto MultiDependencySingleRowFetcher::executeForDependency(size_t const dependen if (state == ExecutionState::WAITING) { return {state, 0, AqlItemBlockInputRange{ExecutorState::HASMORE}}; } + ExecutorState execState = + state == ExecutionState::DONE ? ExecutorState::DONE : ExecutorState::HASMORE; _dependencyStates.at(dependency) = state; if (std::any_of(std::begin(_dependencyStates), std::end(_dependencyStates), @@ -384,18 +386,9 @@ auto MultiDependencySingleRowFetcher::executeForDependency(size_t const dependen state = ExecutionState::DONE; } if (block == nullptr) { - if (state == ExecutionState::HASMORE) { - return {state, skipped, AqlItemBlockInputRange{ExecutorState::HASMORE, skipped}}; - } - return {state, skipped, AqlItemBlockInputRange{ExecutorState::DONE, skipped}}; + return {state, skipped, AqlItemBlockInputRange{execState, skipped}}; } - + TRI_ASSERT(block != nullptr); auto [start, end] = block->getRelevantRange(); - if (state == ExecutionState::HASMORE) { - TRI_ASSERT(block != nullptr); - return {state, skipped, - AqlItemBlockInputRange{ExecutorState::DONE, skipped, block, start}}; - } - return {state, skipped, - AqlItemBlockInputRange{ExecutorState::DONE, skipped, block, start}}; + return {state, skipped, AqlItemBlockInputRange{execState, skipped, block, start}}; } From 368127b7fce98c89fc9670d95420343598703ae9 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 14:48:22 +0100 Subject: [PATCH 11/71] Fixed non-maintainer compilation --- arangod/Aql/ExecutionBlockImpl.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index ca8ae4c1d24a..18247be2d3a9 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -1249,15 +1249,11 @@ template auto ExecutionBlockImpl::executeProduceRows(typename Fetcher::DataRange& input, OutputAqlItemRow& output) -> std::tuple { - if constexpr (isNewStyleExecutor) { - if constexpr (is_one_of_v) { - return _executor.produceRows(input, output); - } else { - auto [state, stats, call] = _executor.produceRows(input, output); - return {state, stats, call, 0}; - } + if constexpr (is_one_of_v) { + return _executor.produceRows(input, output); } else { - TRI_ASSERT(false); + auto [state, stats, call] = _executor.produceRows(input, output); + return {state, stats, call, 0}; } } From 6eaad9f79aa04fc4487f4d8adfdd8690284db594 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 14:58:45 +0100 Subject: [PATCH 12/71] Attempt to fix windows compile issue --- arangod/Aql/SubqueryExecutor.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/arangod/Aql/SubqueryExecutor.cpp b/arangod/Aql/SubqueryExecutor.cpp index 72074816f0fe..1db91ec7f383 100644 --- a/arangod/Aql/SubqueryExecutor.cpp +++ b/arangod/Aql/SubqueryExecutor.cpp @@ -168,7 +168,7 @@ auto SubqueryExecutor::produceRows(AqlItemBlockInputRang return upstreamCall; }; - LOG_DEVEL_SQ << uint64_t(this) << "produceRows " << output.getClientCall(); + LOG_DEVEL_SQ << uint64_t(this) << "produceRows " << output.getClientCall(); if (_state == ExecutorState::DONE && !_input.isInitialized()) { // We have seen DONE upstream, and we have discarded our local reference @@ -183,7 +183,8 @@ auto SubqueryExecutor::produceRows(AqlItemBlockInputRang if (_infos.isConst() && !_input.isFirstDataRowInBlock()) { // Simply write writeOutput(output); - LOG_DEVEL_SQ << uint64_t(this) << "wrote output is const " << _state << " " << getUpstreamCall(); + LOG_DEVEL_SQ << uint64_t(this) << "wrote output is const " << _state + << " " << getUpstreamCall(); return {_state, NoStats{}, getUpstreamCall()}; } @@ -205,7 +206,8 @@ auto SubqueryExecutor::produceRows(AqlItemBlockInputRang // Subquery DONE if (state == ExecutionState::DONE) { writeOutput(output); - LOG_DEVEL_SQ << uint64_t(this) << "wrote output subquery done " << _state << " " << getUpstreamCall(); + LOG_DEVEL_SQ << uint64_t(this) << "wrote output subquery done " + << _state << " " << getUpstreamCall(); return {_state, NoStats{}, getUpstreamCall()}; } @@ -213,7 +215,8 @@ auto SubqueryExecutor::produceRows(AqlItemBlockInputRang // init new subquery if (!_input) { std::tie(_state, _input) = input.nextDataRow(); - LOG_DEVEL_SQ << uint64_t(this) << " nextDataRow: " << _state << " " << _input.isInitialized(); + LOG_DEVEL_SQ << uint64_t(this) << " nextDataRow: " << _state << " " + << _input.isInitialized(); if (!_input) { LOG_DEVEL_SQ << uint64_t(this) << "exit produce, no more input" << _state; return {_state, NoStats{}, getUpstreamCall()}; @@ -295,8 +298,8 @@ SubqueryExecutor::fetchBlockForPassthrough(size_t atMost } template <> -template > -auto SubqueryExecutor::skipRowsRange(AqlItemBlockInputRange& inputRange, AqlCall& call) +template <> +auto SubqueryExecutor::skipRowsRange<>(AqlItemBlockInputRange& inputRange, AqlCall& call) -> std::tuple { auto getUpstreamCall = [&]() { auto upstreamCall = AqlCall{}; @@ -305,7 +308,7 @@ auto SubqueryExecutor::skipRowsRange(AqlItemBlockInputRange& inputRange, A size_t skipped = 0; - LOG_DEVEL_SQ << uint64_t(this) << "skipRowsRange " << call; + LOG_DEVEL_SQ << uint64_t(this) << "skipRowsRange " << call; if (_state == ExecutorState::DONE && !_input.isInitialized()) { // We have seen DONE upstream, and we have discarded our local reference @@ -380,7 +383,4 @@ auto SubqueryExecutor::skipRowsRange(AqlItemBlockInputRange& inputRange, A } template class ::arangodb::aql::SubqueryExecutor; -template auto SubqueryExecutor::skipRowsRange(AqlItemBlockInputRange& inputRange, - AqlCall& call) - -> std::tuple; template class ::arangodb::aql::SubqueryExecutor; From 06c30993ecb653c9822f8378720b8069a0d1b969 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 14:59:24 +0100 Subject: [PATCH 13/71] Fixed the non-maintainer compile ina different way --- arangod/Aql/ExecutionBlockImpl.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index 18247be2d3a9..d25d5083e758 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -1249,11 +1249,15 @@ template auto ExecutionBlockImpl::executeProduceRows(typename Fetcher::DataRange& input, OutputAqlItemRow& output) -> std::tuple { - if constexpr (is_one_of_v) { - return _executor.produceRows(input, output); + if constexpr (isNewStyleExecutor) { + if constexpr (is_one_of_v) { + return _executor.produceRows(input, output); + } else { + auto [state, stats, call] = _executor.produceRows(input, output); + return {state, stats, call, 0}; + } } else { - auto [state, stats, call] = _executor.produceRows(input, output); - return {state, stats, call, 0}; + return {ExecutorState::DONE, typename Executor::Stats{}, AqlCall{}, 0}; } } From 68d6ebeb1e37fd7b46f1d79d48de9f20a7a8f7e8 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 16:10:20 +0100 Subject: [PATCH 14/71] Added API in MultiAqlItemBlockInputRange to get Number of dependencies --- arangod/Aql/MultiAqlItemBlockInputRange.cpp | 6 +++++- arangod/Aql/MultiAqlItemBlockInputRange.h | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/arangod/Aql/MultiAqlItemBlockInputRange.cpp b/arangod/Aql/MultiAqlItemBlockInputRange.cpp index 8b5aad8595d7..366d28b62fb4 100644 --- a/arangod/Aql/MultiAqlItemBlockInputRange.cpp +++ b/arangod/Aql/MultiAqlItemBlockInputRange.cpp @@ -130,7 +130,7 @@ auto MultiAqlItemBlockInputRange::isDone() const -> bool { return res; } -size_t MultiAqlItemBlockInputRange::skipAllRemainingDataRows() { +auto MultiAqlItemBlockInputRange::skipAllRemainingDataRows() -> size_t { for (size_t i = 0; i < _inputs.size(); i++) { _inputs.at(i).skipAllRemainingDataRows(); if (_inputs.at(i).upstreamState() == ExecutorState::HASMORE) { @@ -139,3 +139,7 @@ size_t MultiAqlItemBlockInputRange::skipAllRemainingDataRows() { } return 0; } + +auto MultiAqlItemBlockInputRange::numberDependencies() const noexcept -> size_t { + return _inputs.size(); +} \ No newline at end of file diff --git a/arangod/Aql/MultiAqlItemBlockInputRange.h b/arangod/Aql/MultiAqlItemBlockInputRange.h index e23a65f5ad7a..196fd9038788 100644 --- a/arangod/Aql/MultiAqlItemBlockInputRange.h +++ b/arangod/Aql/MultiAqlItemBlockInputRange.h @@ -65,7 +65,9 @@ class MultiAqlItemBlockInputRange { auto setDependency(size_t const dependency, AqlItemBlockInputRange& range) -> void; - size_t skipAllRemainingDataRows(); + auto skipAllRemainingDataRows() -> size_t; + + auto numberDependencies() const noexcept -> size_t; private: ExecutorState _finalState{ExecutorState::HASMORE}; From abd617eca2396f58d2f57e61c1a2cfad9f65f06d Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 16:10:37 +0100 Subject: [PATCH 15/71] Comments --- arangod/Aql/UnsortedGatherExecutor.h | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/arangod/Aql/UnsortedGatherExecutor.h b/arangod/Aql/UnsortedGatherExecutor.h index 5dc6d5c8afe8..2793c31be674 100644 --- a/arangod/Aql/UnsortedGatherExecutor.h +++ b/arangod/Aql/UnsortedGatherExecutor.h @@ -83,9 +83,32 @@ class UnsortedGatherExecutor { [[nodiscard]] auto skipRows(size_t atMost) -> std::tuple; - // TODO: This should really be the DataRange of the fetcher? + /** + * @brief Produce rows + * + * @param input DataRange delivered by the fetcher + * @param output place to write rows to + * @return std::tuple + * ExecutorState: DONE or HASMORE (only within a subquery) + * Stats: Stats gerenated here + * AqlCall: Request to upstream + * size:t: Dependency to request + */ [[nodiscard]] auto produceRows(typename Fetcher::DataRange& input, OutputAqlItemRow& output) -> std::tuple; + + /** + * @brief Skip rows + * + * @param input DataRange delivered by the fetcher + * @param call skip request form consumer + * @return std::tuple + * ExecutorState: DONE or HASMORE (only within a subquery) + * Stats: Stats gerenated here + * size_t: Number of rows skipped + * AqlCall: Request to upstream + * size:t: Dependency to request + */ [[nodiscard]] auto skipRowsRange(typename Fetcher::DataRange& input, AqlCall& call) -> std::tuple; From d149e39c9fb919828a532d4f50f35d896a96f8bc Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 16:11:18 +0100 Subject: [PATCH 16/71] Savepoint commit, does not compile, but no harm is done. Will start breaking things now --- arangod/Aql/SortingGatherExecutor.cpp | 135 ++++++++++++++++++++++---- arangod/Aql/SortingGatherExecutor.h | 77 +++++++++++++-- 2 files changed, 185 insertions(+), 27 deletions(-) diff --git a/arangod/Aql/SortingGatherExecutor.cpp b/arangod/Aql/SortingGatherExecutor.cpp index d6d366c0a08a..56ed5005e00c 100644 --- a/arangod/Aql/SortingGatherExecutor.cpp +++ b/arangod/Aql/SortingGatherExecutor.cpp @@ -167,9 +167,9 @@ SortingGatherExecutorInfos::SortingGatherExecutorInfos( std::shared_ptr> inputRegisters, std::shared_ptr> outputRegisters, RegisterId nrInputRegisters, RegisterId nrOutputRegisters, std::unordered_set registersToClear, - std::unordered_set registersToKeep, std::vector&& sortRegister, - arangodb::transaction::Methods* trx, GatherNode::SortMode sortMode, size_t limit, - GatherNode::Parallelism p) + std::unordered_set registersToKeep, + std::vector&& sortRegister, arangodb::transaction::Methods* trx, + GatherNode::SortMode sortMode, size_t limit, GatherNode::Parallelism p) : ExecutorInfos(std::move(inputRegisters), std::move(outputRegisters), nrInputRegisters, nrOutputRegisters, std::move(registersToClear), std::move(registersToKeep)), @@ -249,6 +249,101 @@ std::pair SortingGatherExecutor::produceRows(OutputAqlI return {state, NoStats{}}; } +auto SortingGatherExecutor::initialize(typename Fetcher::DataRange const& inputRange) -> void { + if (!_initialized) { + _strategy->prepare(inputRange); + _initialized = true; + _numberDependencies = inputRange.numberDependencies(); + } +} + +auto SortingGatherExecutor::requiresMoreInput(typename Fetcher::DataRange const& inputRange) const + -> std::optional> { + for (size_t dep = 0; dep < _numberDependencies; ++dep) { + auto const& [state, input] = inputRange.peekDataRow(dep); + if (!input && state != ExecutorState::DONE) { + // This dependency requires input + // TODO: This call requires limits + return std::tuple{AqlCall{}, dep}; + } + } + // No call required + return {}; +} + +auto SortingGatherExecutor::isDone(typename Fetcher::DataRange const& input) const -> bool { + // TODO: Include contrained sort + return input.isDone(); +} + +auto SortingGatherExecutor::nextRow(MultiAqlItemBlockInputRange& input) -> InputAqlItemRow { + if (isDone(input)) { + // No rows, there is a chance we get into this. + // If we requested data from upstream, but all if it is done. + return InputAqlItemRow{CreateInvalidInputRowHint{}}; + } +#ifdef ARANGODB_ENABLE_MAINTAINER_MODE + bool oneWithContent = false; + for (size_t dep = 0; dep < _numberDependencies; ++dep) { + auto const& [state, row] = input.peekDataRow(i); + if (row) { + oneWithContent = true; + } + } + TRI_ASSERT(oneWithContent); +#endif + auto nextVal = _strategy->nextValue(); + // TODO we might do some short-cuts here to maintain a list of requests + // to send in order to improve requires input + return nextVal.row; +} + +auto SortingGatherExecutor::produceRows(typename Fetcher::DataRange& input, + OutputAqlItemRow& output) + -> std::tuple { + while (!isDone(input) && !output.isFull()) { + auto maybeCall = requiresMoreInput(input); + if (maybeCall.has_value()) { + auto const& [request, dep] = maybeCall.value(); + return {ExecutorState::HASMORE, NoStats{}, request, dep}; + } + auto row = nextRow(input); + TRI_ASSERT(row.isInitialized() || isDone(input)); + if (row) { + output.copyRow(row); + output.advanceRow(); + } + } + + // Call and dependency unused + if (isDone(input)) { + return {ExecutorState::DONE, NoStats{}, AqlCall{}, 0}; + } + return {ExecutorState::HASMORE, NoStats{}, AqlCall{}, 0}; +} + +auto SortingGatherExecutor::skipRowsRange(typename Fetcher::DataRange& input, AqlCall& call) + -> std::tuple { + while (!isDone(input) && !call.needSkipMore()) { + auto maybeCall = requiresMoreInput(input); + if (maybeCall.has_value()) { + auto const& [request, dep] = maybeCall.value(); + return {ExecutorState::HASMORE, NoStats{}, call.getSkipCount(), request, dep}; + } + auto row = nextRow(input); + TRI_ASSERT(row.isInitialized() || isDone(input)); + if (row) { + call.didSkip(1); + } + } + + // Call and dependency unused + if (isDone(input)) { + return {ExecutorState::DONE, NoStats{}, call.getSkipCount(), AqlCall{}, 0}; + } + return {ExecutorState::HASMORE, NoStats{}, call.getSkipCount(), AqlCall{}, 0}; +} + std::pair SortingGatherExecutor::produceNextRow(size_t const atMost) { TRI_ASSERT(_strategy != nullptr); assertConstrainedDoesntOverfetch(atMost); @@ -261,8 +356,8 @@ std::pair SortingGatherExecutor::produceNextRow return {state, InputAqlItemRow{CreateInvalidInputRowHint{}}}; } } else { - // Activate this assert as soon as all blocks follow the done == no call api - // TRI_ASSERT(_nrDone < _numberDependencies); + // Activate this assert as soon as all blocks follow the done == no call + // api TRI_ASSERT(_nrDone < _numberDependencies); if (_inputRows[_dependencyToFetch].state == ExecutionState::DONE) { _inputRows[_dependencyToFetch].row = InputAqlItemRow{CreateInvalidInputRowHint()}; } else { @@ -345,13 +440,12 @@ ExecutionState SortingGatherExecutor::init(size_t const atMost) { size_t numWaiting = 0; for (size_t i = 0; i < _numberDependencies; i++) { - if (_inputRows[i].state == ExecutionState::DONE || - _inputRows[i].row) { + if (_inputRows[i].state == ExecutionState::DONE || _inputRows[i].row) { continue; } - - std::tie(_inputRows[i].state, - _inputRows[i].row) = _fetcher.fetchRowForDependency(i, atMost); + + std::tie(_inputRows[i].state, _inputRows[i].row) = + _fetcher.fetchRowForDependency(i, atMost); if (_inputRows[i].state == ExecutionState::WAITING) { if (!_fetchParallel) { return ExecutionState::WAITING; @@ -366,7 +460,7 @@ ExecutionState SortingGatherExecutor::init(size_t const atMost) { if (numWaiting > 0) { return ExecutionState::WAITING; } - + TRI_ASSERT(_numberDependencies > 0); _dependencyToFetch = _numberDependencies - 1; _initialized = true; @@ -430,15 +524,16 @@ bool SortingGatherExecutor::maySkip() const noexcept { return constrainedSort() && _rowsReturned >= _limit; } -std::tuple SortingGatherExecutor::skipRows(size_t const atMost) { +std::tuple SortingGatherExecutor::skipRows( + size_t const atMost) { if (!maySkip()) { // Until our limit, we must produce rows, because we might be asked later // to produce rows, in which case all rows have to have been skipped in // order. return produceAndSkipRows(atMost); } else { - // If we've reached our limit, we will never be asked to produce rows again. - // So we can just skip without sorting. + // If we've reached our limit, we will never be asked to produce rows + // again. So we can just skip without sorting. return reallySkipRows(atMost); } } @@ -464,7 +559,7 @@ std::tuple SortingGatherEx _dependencyToFetch = 0; } - { // Skip rows we had left in the heap first + { // Skip rows we had left in the heap first std::size_t const skip = std::min(atMost, _rowsLeftInHeap); _rowsLeftInHeap -= skip; _skipped += skip; @@ -512,11 +607,9 @@ std::tuple SortingGatherEx InputAqlItemRow row{CreateInvalidInputRowHint{}}; // We may not skip more rows in this method than we can produce! - auto const ourAtMost = constrainedSort() - ? std::min(atMost, rowsLeftToWrite()) - : atMost; + auto const ourAtMost = constrainedSort() ? std::min(atMost, rowsLeftToWrite()) : atMost; - while(state == ExecutionState::HASMORE && _skipped < ourAtMost) { + while (state == ExecutionState::HASMORE && _skipped < ourAtMost) { std::tie(state, row) = produceNextRow(ourAtMost - _skipped); // HASMORE => row has to be initialized TRI_ASSERT(state != ExecutionState::HASMORE || row.isInitialized()); @@ -533,8 +626,8 @@ std::tuple SortingGatherEx } // Note that _skipped *can* be larger than `ourAtMost`, due to WAITING, in - // which case we might get a lower `ourAtMost` on the second call than during - // the first. + // which case we might get a lower `ourAtMost` on the second call than + // during the first. TRI_ASSERT(_skipped <= atMost); TRI_ASSERT(state != ExecutionState::HASMORE || _skipped > 0); TRI_ASSERT(state != ExecutionState::WAITING || _skipped == 0); diff --git a/arangod/Aql/SortingGatherExecutor.h b/arangod/Aql/SortingGatherExecutor.h index 38c7aca7187e..1da3517c2fd2 100644 --- a/arangod/Aql/SortingGatherExecutor.h +++ b/arangod/Aql/SortingGatherExecutor.h @@ -36,7 +36,9 @@ class Methods; namespace aql { +struct AqlCall; class MultiDependencySingleRowFetcher; +class MultiAqlItemBlockInputRange; class NoStats; class OutputAqlItemRow; struct SortRegister; @@ -49,9 +51,8 @@ class SortingGatherExecutorInfos : public ExecutorInfos { std::unordered_set registersToClear, std::unordered_set registersToKeep, std::vector&& sortRegister, - arangodb::transaction::Methods* trx, - GatherNode::SortMode sortMode, size_t limit, - GatherNode::Parallelism p); + arangodb::transaction::Methods* trx, GatherNode::SortMode sortMode, + size_t limit, GatherNode::Parallelism p); SortingGatherExecutorInfos() = delete; SortingGatherExecutorInfos(SortingGatherExecutorInfos&&); SortingGatherExecutorInfos(SortingGatherExecutorInfos const&) = delete; @@ -62,7 +63,7 @@ class SortingGatherExecutorInfos : public ExecutorInfos { arangodb::transaction::Methods* trx() { return _trx; } GatherNode::SortMode sortMode() const noexcept { return _sortMode; } - + GatherNode::Parallelism parallelism() const noexcept { return _parallelism; } size_t limit() const noexcept { return _limit; } @@ -123,6 +124,35 @@ class SortingGatherExecutor { */ std::pair produceRows(OutputAqlItemRow& output); + /** + * @brief Produce rows + * + * @param input DataRange delivered by the fetcher + * @param output place to write rows to + * @return std::tuple + * ExecutorState: DONE or HASMORE (only within a subquery) + * Stats: Stats gerenated here + * AqlCall: Request to upstream + * size:t: Dependency to request + */ + [[nodiscard]] auto produceRows(MultiAqlItemBlockInputRange& input, OutputAqlItemRow& output) + -> std::tuple; + + /** + * @brief Skip rows + * + * @param input DataRange delivered by the fetcher + * @param call skip request form consumer + * @return std::tuple + * ExecutorState: DONE or HASMORE (only within a subquery) + * Stats: Stats gerenated here + * size_t: Number of rows skipped + * AqlCall: Request to upstream + * size:t: Dependency to request + */ + [[nodiscard]] auto skipRowsRange(MultiAqlItemBlockInputRange& input, AqlCall& call) + -> std::tuple; + void adjustNrDone(size_t dependency); std::pair expectedNumberOfRows(size_t atMost) const; @@ -148,6 +178,41 @@ class SortingGatherExecutor { // This also means that we may not produce rows anymore after that point. bool maySkip() const noexcept; + /** + * @brief Function that checks if all dependencies are either + * done, or have a row. + * The first one that does not match the condition + * will produce an upstream call to be fulfilled. + * + * @param inputRange Range of all input dependencies + * @return std::optional> optional call for the dependnecy requiring input + */ + auto requiresMoreInput(MultiAqlItemBlockInputRange const& inputRange) const + -> std::optional>; + + /** + * @brief Get the next row matching the sorting strategy + * + * @return InputAqlItemRow best fit row. Might be invalid if all input is done. + */ + auto nextRow(MultiAqlItemBlockInputRange& input) -> InputAqlItemRow; + + /** + * @brief Tests if this Executor is done producing + * => All inputs are fully consumed + * + * @return true we are done + * @return false we have more + */ + auto isDone(MultiAqlItemBlockInputRange const& input) const -> bool; + + /** + * @brief Initialize the Sorting strategy with the given input. + * This is known to be empty, but all prepared at this point. + * @param inputRange The input, no data included yet. + */ + auto initialize(typename Fetcher::DataRange const& inputRange) -> void; + private: Fetcher& _fetcher; @@ -165,7 +230,7 @@ class SortingGatherExecutor { // Counter for DONE states size_t _nrDone; - + /// @brief If we do a constrained sort, it holds the limit > 0. Otherwise, it's 0. size_t _limit; @@ -193,7 +258,7 @@ class SortingGatherExecutor { #endif std::tuple reallySkipRows(size_t atMost); std::tuple produceAndSkipRows(size_t atMost); - + const bool _fetchParallel; }; From 52dcde5ef8feaeb8cc7d2da6d8d47b4b27b7a303 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 17:34:29 +0100 Subject: [PATCH 17/71] Another savepoint commit. does not compile, yet. --- arangod/Aql/MultiAqlItemBlockInputRange.cpp | 5 + arangod/Aql/MultiAqlItemBlockInputRange.h | 1 + arangod/Aql/SortingGatherExecutor.cpp | 101 ++++++++++++++++---- arangod/Aql/SortingGatherExecutor.h | 3 +- 4 files changed, 93 insertions(+), 17 deletions(-) diff --git a/arangod/Aql/MultiAqlItemBlockInputRange.cpp b/arangod/Aql/MultiAqlItemBlockInputRange.cpp index 366d28b62fb4..16a13ec00e30 100644 --- a/arangod/Aql/MultiAqlItemBlockInputRange.cpp +++ b/arangod/Aql/MultiAqlItemBlockInputRange.cpp @@ -71,6 +71,11 @@ auto MultiAqlItemBlockInputRange::peekDataRow(size_t const dependency) const return _inputs.at(dependency).peekDataRow(); } +auto MultiAqlItemBlockInputRange::skipAll(size_t const dependency) noexcept -> std::size_t { + TRI_ASSERT(dependency < _inputs.size()); + return _inputs.at(dependency).skipAll() +} + auto MultiAqlItemBlockInputRange::nextDataRow(size_t const dependency) -> std::pair { TRI_ASSERT(dependency < _inputs.size()); diff --git a/arangod/Aql/MultiAqlItemBlockInputRange.h b/arangod/Aql/MultiAqlItemBlockInputRange.h index 196fd9038788..c35b8c93e055 100644 --- a/arangod/Aql/MultiAqlItemBlockInputRange.h +++ b/arangod/Aql/MultiAqlItemBlockInputRange.h @@ -51,6 +51,7 @@ class MultiAqlItemBlockInputRange { std::pair peekDataRow(size_t const dependency) const; std::pair nextDataRow(size_t const dependency); + auto skipAll(size_t const dependency) noexcept -> std::size_t; bool hasShadowRow() const noexcept; diff --git a/arangod/Aql/SortingGatherExecutor.cpp b/arangod/Aql/SortingGatherExecutor.cpp index 56ed5005e00c..fcbab500e2be 100644 --- a/arangod/Aql/SortingGatherExecutor.cpp +++ b/arangod/Aql/SortingGatherExecutor.cpp @@ -163,6 +163,10 @@ class MinElementSorting final : public SortingGatherExecutor::SortingStrategy, SortingGatherExecutor::ValueType::ValueType(size_t index) : dependencyIndex{index}, row{CreateInvalidInputRowHint()}, state{ExecutionState::HASMORE} {} +SortingGatherExecutor::ValueType::ValueType(size_t index, InputAqlItemRow prow, + ExecutionState pstate) + : dependencyIndex{index}, row{prow}, state{pstate} {} + SortingGatherExecutorInfos::SortingGatherExecutorInfos( std::shared_ptr> inputRegisters, std::shared_ptr> outputRegisters, RegisterId nrInputRegisters, @@ -249,18 +253,40 @@ std::pair SortingGatherExecutor::produceRows(OutputAqlI return {state, NoStats{}}; } -auto SortingGatherExecutor::initialize(typename Fetcher::DataRange const& inputRange) -> void { +auto SortingGatherExecutor::initialize(typename Fetcher::DataRange const& inputRange) + -> std::optional> { if (!_initialized) { - _strategy->prepare(inputRange); + // We cannot modify the number of dependencies, so we start + // with 0 dependencies, and will increase to whatever inputRange gives us. + TRI_ASSERT(_numberDependencies == 0 || + _numberDependencies == inputRange.numberDependencies()); + _numberDependencies = inputRange.numberDependencies(); + auto call = requiresMoreInput(inputRange); + if (call.has_value) { + return call; + } + // If we have collected all ranges once, we can prepare the local data-structure copy + _inputRows.reserve(_numberDependencies); + for (size_t dep = 0; dep < _numberDependencies; ++dep) { + auto const [state, row] = inputRange.peekDataRow(dep); + _inputRows.emplace_back(dep, row, state); + } + _strategy->prepare(_inputRows); _initialized = true; _numberDependencies = inputRange.numberDependencies(); } } -auto SortingGatherExecutor::requiresMoreInput(typename Fetcher::DataRange const& inputRange) const +auto SortingGatherExecutor::requiresMoreInput(typename Fetcher::DataRange const& inputRange) -> std::optional> { for (size_t dep = 0; dep < _numberDependencies; ++dep) { auto const& [state, input] = inputRange.peekDataRow(dep); + // Update the local copy, just to be sure it is up to date + // We might do too many copies here, but most likely this + // will not be a performance bottleneck. + ValueType& localDep = _inputRows[dep]; + localDep.row = input; + localDep.state = state; if (!input && state != ExecutorState::DONE) { // This dependency requires input // TODO: This call requires limits @@ -293,8 +319,18 @@ auto SortingGatherExecutor::nextRow(MultiAqlItemBlockInputRange& input) -> Input TRI_ASSERT(oneWithContent); #endif auto nextVal = _strategy->nextValue(); - // TODO we might do some short-cuts here to maintain a list of requests - // to send in order to improve requires input + _rowsReturned++; + { + // Consume the row, and set it to next input + std::ignore = input.nextDataRow(nextVal.dependencyIndex); + auto const& [state, row] = input.peekDataRow(nextVal.dependencyIndex); + _inputRows[nextVal.dependencyIndex].state = state; + _inputRows[nextVal.dependencyIndex].row = row; + + // TODO we might do some short-cuts here to maintain a list of requests + // to send in order to improve requires input + } + return nextVal.row; } @@ -302,6 +338,7 @@ auto SortingGatherExecutor::produceRows(typename Fetcher::DataRange& input, OutputAqlItemRow& output) -> std::tuple { while (!isDone(input) && !output.isFull()) { + TRI_ASSERT(!maySkip()); auto maybeCall = requiresMoreInput(input); if (maybeCall.has_value()) { auto const& [request, dep] = maybeCall.value(); @@ -315,33 +352,65 @@ auto SortingGatherExecutor::produceRows(typename Fetcher::DataRange& input, } } - // Call and dependency unused + // Call and dependency unused, so we return a too large dependency + // in order to trigger asserts if it is used. if (isDone(input)) { - return {ExecutorState::DONE, NoStats{}, AqlCall{}, 0}; + return {ExecutorState::DONE, NoStats{}, AqlCall{}, _numberDependencies + 1}; } - return {ExecutorState::HASMORE, NoStats{}, AqlCall{}, 0}; + return {ExecutorState::HASMORE, NoStats{}, AqlCall{}, _numberDependencies + 1}; } auto SortingGatherExecutor::skipRowsRange(typename Fetcher::DataRange& input, AqlCall& call) -> std::tuple { - while (!isDone(input) && !call.needSkipMore()) { + while (!isDone(input) && call.needSkipMore()) { auto maybeCall = requiresMoreInput(input); if (maybeCall.has_value()) { auto const& [request, dep] = maybeCall.value(); return {ExecutorState::HASMORE, NoStats{}, call.getSkipCount(), request, dep}; } - auto row = nextRow(input); - TRI_ASSERT(row.isInitialized() || isDone(input)); - if (row) { - call.didSkip(1); + if (call.getOffset() > 0) { + TRI_ASSERT(!maySkip()); + // We need to sort still + // And account the row in the limit + auto row = nextRow(input); + TRI_ASSERT(row.isInitialized() || isDone(input)); + if (row) { + call.didSkip(1); + } + } else { + // We are only called with fullcount. + // sorting does not matter. + // Start simply skip all from upstream. + for (size_t dep = 0; dep < input.numberDependencies(); ++dep) { + ExecutorState state = ExecutorState::HASMORE; + InputAqlItemRow row{CreateInvalidInputRowHint{}}; + while (state == ExecutorState::HASMORE) { + std::tie(state, row) = input.nextDataRow(dep); + if (row) { + call.didSkip(1); + } else { + // We have consumed all overfetched rows. + // We may still have a skip counter within the range. + call.didSkip(input.skipAll(dep)); + if (state == ExecutorState::HASMORE) { + // We need to fetch more data, but can fullCount now + AqlCall request{0, true, 0, AqlCall::LimitType::HARD}; + return {ExecutorState::HASMORE, NoStats{}, call.getSkipCount(), request, dep}; + } + } + } + } } } - // Call and dependency unused + // Call and dependency unused, so we return a too large dependency + // in order to trigger asserts if it is used. if (isDone(input)) { - return {ExecutorState::DONE, NoStats{}, call.getSkipCount(), AqlCall{}, 0}; + return {ExecutorState::DONE, NoStats{}, call.getSkipCount(), AqlCall{}, + _numberDependencies + 1}; } - return {ExecutorState::HASMORE, NoStats{}, call.getSkipCount(), AqlCall{}, 0}; + return {ExecutorState::HASMORE, NoStats{}, call.getSkipCount(), AqlCall{}, + _numberDependencies + 1}; } std::pair SortingGatherExecutor::produceNextRow(size_t const atMost) { diff --git a/arangod/Aql/SortingGatherExecutor.h b/arangod/Aql/SortingGatherExecutor.h index 1da3517c2fd2..df08b6439b49 100644 --- a/arangod/Aql/SortingGatherExecutor.h +++ b/arangod/Aql/SortingGatherExecutor.h @@ -81,9 +81,10 @@ class SortingGatherExecutor { struct ValueType { size_t dependencyIndex; InputAqlItemRow row; - ExecutionState state; + ExecutorState state; explicit ValueType(size_t index); + ValueType(size_t, InputAqlItemRow, ExecutorState); }; //////////////////////////////////////////////////////////////////////////////// From 5ae6cac9eadcb71d3d48b6b2f5836c237f0ce3c9 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 18:11:16 +0100 Subject: [PATCH 18/71] First draft of new Style SortingGather not yet implemented: Parallelism this needs to be handled in ExecutionBlockImpl now. --- arangod/Aql/ExecutionBlockImpl.cpp | 14 +- arangod/Aql/MultiAqlItemBlockInputRange.cpp | 2 +- arangod/Aql/SortingGatherExecutor.cpp | 364 +++----------------- arangod/Aql/SortingGatherExecutor.h | 54 +-- 4 files changed, 57 insertions(+), 377 deletions(-) diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index ca8ae4c1d24a..c02ab685b489 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -151,8 +151,8 @@ constexpr bool isNewStyleExecutor = is_one_of_v< ModificationExecutor, ModificationExecutor, UpdateReplaceModifier>, ModificationExecutor, - ModificationExecutor, UpsertModifier>, - SubqueryStartExecutor, UnsortedGatherExecutor, SubqueryEndExecutor, TraversalExecutor, + ModificationExecutor, UpsertModifier>, SubqueryStartExecutor, + UnsortedGatherExecutor, SortingGatherExecutor, SubqueryEndExecutor, TraversalExecutor, KShortestPathsExecutor, ShortestPathExecutor, EnumerateListExecutor, LimitExecutor, SortExecutor, IResearchViewExecutor, IResearchViewExecutor, @@ -1139,9 +1139,9 @@ static SkipRowsRangeVariant constexpr skipRowsType() { ModificationExecutor, ModificationExecutor, UpdateReplaceModifier>, ModificationExecutor, - ModificationExecutor, UpsertModifier>, - TraversalExecutor, EnumerateListExecutor, SubqueryStartExecutor, SubqueryEndExecutor, - SortedCollectExecutor, LimitExecutor, UnsortedGatherExecutor, SortExecutor, + ModificationExecutor, UpsertModifier>, TraversalExecutor, + EnumerateListExecutor, SubqueryStartExecutor, SubqueryEndExecutor, SortedCollectExecutor, + LimitExecutor, UnsortedGatherExecutor, SortingGatherExecutor, SortExecutor, IResearchViewExecutor, IResearchViewExecutor, IResearchViewExecutor, @@ -1250,7 +1250,7 @@ auto ExecutionBlockImpl::executeProduceRows(typename Fetcher::DataRang OutputAqlItemRow& output) -> std::tuple { if constexpr (isNewStyleExecutor) { - if constexpr (is_one_of_v) { + if constexpr (is_one_of_v) { return _executor.produceRows(input, output); } else { auto [state, stats, call] = _executor.produceRows(input, output); @@ -1268,7 +1268,7 @@ auto ExecutionBlockImpl::executeSkipRowsRange(typename Fetcher::DataRa if constexpr (isNewStyleExecutor) { call.skippedRows = 0; if constexpr (skipRowsType() == SkipRowsRangeVariant::EXECUTOR) { - if constexpr (is_one_of_v) { + if constexpr (is_one_of_v) { // If the executor has a method skipRowsRange, to skip outputs. // Every non-passthrough executor needs to implement this. auto res = _executor.skipRowsRange(inputRange, call); diff --git a/arangod/Aql/MultiAqlItemBlockInputRange.cpp b/arangod/Aql/MultiAqlItemBlockInputRange.cpp index 16a13ec00e30..226d082a708a 100644 --- a/arangod/Aql/MultiAqlItemBlockInputRange.cpp +++ b/arangod/Aql/MultiAqlItemBlockInputRange.cpp @@ -73,7 +73,7 @@ auto MultiAqlItemBlockInputRange::peekDataRow(size_t const dependency) const auto MultiAqlItemBlockInputRange::skipAll(size_t const dependency) noexcept -> std::size_t { TRI_ASSERT(dependency < _inputs.size()); - return _inputs.at(dependency).skipAll() + return _inputs.at(dependency).skipAll(); } auto MultiAqlItemBlockInputRange::nextDataRow(size_t const dependency) diff --git a/arangod/Aql/SortingGatherExecutor.cpp b/arangod/Aql/SortingGatherExecutor.cpp index fcbab500e2be..61c4ea8ebe80 100644 --- a/arangod/Aql/SortingGatherExecutor.cpp +++ b/arangod/Aql/SortingGatherExecutor.cpp @@ -161,10 +161,9 @@ class MinElementSorting final : public SortingGatherExecutor::SortingStrategy, } // namespace SortingGatherExecutor::ValueType::ValueType(size_t index) - : dependencyIndex{index}, row{CreateInvalidInputRowHint()}, state{ExecutionState::HASMORE} {} + : dependencyIndex{index}, row{CreateInvalidInputRowHint()}, state{ExecutorState::HASMORE} {} -SortingGatherExecutor::ValueType::ValueType(size_t index, InputAqlItemRow prow, - ExecutionState pstate) +SortingGatherExecutor::ValueType::ValueType(size_t index, InputAqlItemRow prow, ExecutorState pstate) : dependencyIndex{index}, row{prow}, state{pstate} {} SortingGatherExecutorInfos::SortingGatherExecutorInfos( @@ -187,17 +186,11 @@ SortingGatherExecutorInfos::SortingGatherExecutorInfos(SortingGatherExecutorInfo SortingGatherExecutorInfos::~SortingGatherExecutorInfos() = default; SortingGatherExecutor::SortingGatherExecutor(Fetcher& fetcher, Infos& infos) - : _fetcher(fetcher), - _initialized(false), + : _initialized(false), _numberDependencies(0), - _dependencyToFetch(0), _inputRows(), - _nrDone(0), _limit(infos.limit()), _rowsReturned(0), - _heapCounted(false), - _rowsLeftInHeap(0), - _skipped(0), _strategy(nullptr), _fetchParallel(infos.parallelism() == GatherNode::Parallelism::Parallel) { switch (infos.sortMode()) { @@ -217,42 +210,6 @@ SortingGatherExecutor::SortingGatherExecutor(Fetcher& fetcher, Infos& infos) SortingGatherExecutor::~SortingGatherExecutor() = default; -//////////////////////////////////////////////////////////////////////////////// -/// @brief Guarantees requiredby this this block: -/// 1) For every dependency the input is sorted, according to the same strategy. -/// -/// What this block does: -/// InitPhase: -/// Fetch 1 Block for every dependency. -/// ExecPhase: -/// Fetch row of scheduled block. -/// Pick the next (sorted) element (by strategy) -/// Schedule this block to fetch Row -/// -//////////////////////////////////////////////////////////////////////////////// - -std::pair SortingGatherExecutor::produceRows(OutputAqlItemRow& output) { - size_t const atMost = constrainedSort() ? output.numRowsLeft() - : ExecutionBlock::DefaultBatchSize; - ExecutionState state; - InputAqlItemRow row{CreateInvalidInputRowHint{}}; - std::tie(state, row) = produceNextRow(atMost); - - // HASMORE => row has to be initialized - TRI_ASSERT(state != ExecutionState::HASMORE || row.isInitialized()); - // WAITING => row may not be initialized - TRI_ASSERT(state != ExecutionState::WAITING || !row.isInitialized()); - - if (row) { - // NOTE: The original gatherBlock did referencing - // inside the outputblock by identical AQL values. - // This optimization is not in use anymore. - output.copyRow(row); - } - - return {state, NoStats{}}; -} - auto SortingGatherExecutor::initialize(typename Fetcher::DataRange const& inputRange) -> std::optional> { if (!_initialized) { @@ -262,7 +219,7 @@ auto SortingGatherExecutor::initialize(typename Fetcher::DataRange const& inputR _numberDependencies == inputRange.numberDependencies()); _numberDependencies = inputRange.numberDependencies(); auto call = requiresMoreInput(inputRange); - if (call.has_value) { + if (call.has_value()) { return call; } // If we have collected all ranges once, we can prepare the local data-structure copy @@ -275,6 +232,7 @@ auto SortingGatherExecutor::initialize(typename Fetcher::DataRange const& inputR _initialized = true; _numberDependencies = inputRange.numberDependencies(); } + return {}; } auto SortingGatherExecutor::requiresMoreInput(typename Fetcher::DataRange const& inputRange) @@ -311,7 +269,7 @@ auto SortingGatherExecutor::nextRow(MultiAqlItemBlockInputRange& input) -> Input #ifdef ARANGODB_ENABLE_MAINTAINER_MODE bool oneWithContent = false; for (size_t dep = 0; dep < _numberDependencies; ++dep) { - auto const& [state, row] = input.peekDataRow(i); + auto const& [state, row] = input.peekDataRow(dep); if (row) { oneWithContent = true; } @@ -334,9 +292,32 @@ auto SortingGatherExecutor::nextRow(MultiAqlItemBlockInputRange& input) -> Input return nextVal.row; } +//////////////////////////////////////////////////////////////////////////////// +/// @brief Guarantees requiredby this this block: +/// 1) For every dependency the input is sorted, according to the same strategy. +/// +/// What this block does: +/// InitPhase: +/// Fetch 1 Block for every dependency. +/// ExecPhase: +/// Fetch row of scheduled block. +/// Pick the next (sorted) element (by strategy) +/// Schedule this block to fetch Row +/// +//////////////////////////////////////////////////////////////////////////////// + auto SortingGatherExecutor::produceRows(typename Fetcher::DataRange& input, OutputAqlItemRow& output) -> std::tuple { + { + // First initialize + auto maybeCall = initialize(input); + if (maybeCall.has_value()) { + auto const& [request, dep] = maybeCall.value(); + return {ExecutorState::HASMORE, NoStats{}, request, dep}; + } + } + while (!isDone(input) && !output.isFull()) { TRI_ASSERT(!maySkip()); auto maybeCall = requiresMoreInput(input); @@ -362,6 +343,15 @@ auto SortingGatherExecutor::produceRows(typename Fetcher::DataRange& input, auto SortingGatherExecutor::skipRowsRange(typename Fetcher::DataRange& input, AqlCall& call) -> std::tuple { + { + // First initialize + auto maybeCall = initialize(input); + if (maybeCall.has_value()) { + auto const& [request, dep] = maybeCall.value(); + return {ExecutorState::HASMORE, NoStats{}, 0, request, dep}; + } + } + while (!isDone(input) && call.needSkipMore()) { auto maybeCall = requiresMoreInput(input); if (maybeCall.has_value()) { @@ -413,169 +403,8 @@ auto SortingGatherExecutor::skipRowsRange(typename Fetcher::DataRange& input, Aq _numberDependencies + 1}; } -std::pair SortingGatherExecutor::produceNextRow(size_t const atMost) { - TRI_ASSERT(_strategy != nullptr); - assertConstrainedDoesntOverfetch(atMost); - // We shouldn't be asked for more rows when we are allowed to skip - TRI_ASSERT(!maySkip()); - if (!_initialized) { - ExecutionState state = init(atMost); - if (state != ExecutionState::HASMORE) { - // Can be DONE(unlikely, no input) of WAITING - return {state, InputAqlItemRow{CreateInvalidInputRowHint{}}}; - } - } else { - // Activate this assert as soon as all blocks follow the done == no call - // api TRI_ASSERT(_nrDone < _numberDependencies); - if (_inputRows[_dependencyToFetch].state == ExecutionState::DONE) { - _inputRows[_dependencyToFetch].row = InputAqlItemRow{CreateInvalidInputRowHint()}; - } else { - // This is executed on every produceRows, and will replace the row that we have returned last time - std::tie(_inputRows[_dependencyToFetch].state, - _inputRows[_dependencyToFetch].row) = - _fetcher.fetchRowForDependency(_dependencyToFetch, atMost); - if (_inputRows[_dependencyToFetch].state == ExecutionState::WAITING) { - return {ExecutionState::WAITING, InputAqlItemRow{CreateInvalidInputRowHint{}}}; - } - if (!_inputRows[_dependencyToFetch].row) { - TRI_ASSERT(_inputRows[_dependencyToFetch].state == ExecutionState::DONE); - adjustNrDone(_dependencyToFetch); - } - } - } - if (_nrDone >= _numberDependencies) { - // We cannot return a row, because all are done - return {ExecutionState::DONE, InputAqlItemRow{CreateInvalidInputRowHint{}}}; - } -// if we get here, we have a valid row for every not done dependency. -// And we have atLeast 1 valid row left -#ifdef ARANGODB_ENABLE_MAINTAINER_MODE - bool oneWithContent = false; - for (auto const& inPair : _inputRows) { - // Waiting needs to bail out at fetch state - TRI_ASSERT(inPair.state != ExecutionState::WAITING); - // row.invalid => dependency is done - TRI_ASSERT(inPair.row || inPair.state == ExecutionState::DONE); - if (inPair.row) { - oneWithContent = true; - } - } - // We have at least one row to sort. - TRI_ASSERT(oneWithContent); -#endif - // get the index of the next best value. - ValueType val = _strategy->nextValue(); - _dependencyToFetch = val.dependencyIndex; - // We can never pick an invalid row! - TRI_ASSERT(val.row); - ++_rowsReturned; - adjustNrDone(_dependencyToFetch); - if (_nrDone >= _numberDependencies) { - return {ExecutionState::DONE, val.row}; - } - return {ExecutionState::HASMORE, val.row}; -} - -void SortingGatherExecutor::adjustNrDone(size_t const dependency) { - auto const& dep = _inputRows[dependency]; - if (dep.state == ExecutionState::DONE) { -#ifdef ARANGODB_ENABLE_MAINTAINER_MODE - TRI_ASSERT(_flaggedAsDone[dependency] == false); - _flaggedAsDone[dependency] = true; -#endif - ++_nrDone; - } -} - -void SortingGatherExecutor::initNumDepsIfNecessary() { - if (_numberDependencies == 0) { - // We need to initialize the dependencies once, they are injected - // after the fetcher is created. - _numberDependencies = _fetcher.numberDependencies(); - TRI_ASSERT(_numberDependencies > 0); - _inputRows.reserve(_numberDependencies); - for (size_t index = 0; index < _numberDependencies; ++index) { - _inputRows.emplace_back(ValueType{index}); -#ifdef ARANGODB_ENABLE_MAINTAINER_MODE - _flaggedAsDone.emplace_back(false); -#endif - } - } -} - -ExecutionState SortingGatherExecutor::init(size_t const atMost) { - assertConstrainedDoesntOverfetch(atMost); - initNumDepsIfNecessary(); - - size_t numWaiting = 0; - for (size_t i = 0; i < _numberDependencies; i++) { - if (_inputRows[i].state == ExecutionState::DONE || _inputRows[i].row) { - continue; - } - - std::tie(_inputRows[i].state, _inputRows[i].row) = - _fetcher.fetchRowForDependency(i, atMost); - if (_inputRows[i].state == ExecutionState::WAITING) { - if (!_fetchParallel) { - return ExecutionState::WAITING; - } - numWaiting++; - } else if (!_inputRows[i].row) { - TRI_ASSERT(_inputRows[i].state == ExecutionState::DONE); - adjustNrDone(i); - } - } - - if (numWaiting > 0) { - return ExecutionState::WAITING; - } - - TRI_ASSERT(_numberDependencies > 0); - _dependencyToFetch = _numberDependencies - 1; - _initialized = true; - if (_nrDone >= _numberDependencies) { - return ExecutionState::DONE; - } - _strategy->prepare(_inputRows); - return ExecutionState::HASMORE; -} - std::pair SortingGatherExecutor::expectedNumberOfRows(size_t const atMost) const { - assertConstrainedDoesntOverfetch(atMost); - // We shouldn't be asked for more rows when we are allowed to skip - TRI_ASSERT(!maySkip()); - ExecutionState state; - size_t expectedNumberOfRows; - std::tie(state, expectedNumberOfRows) = _fetcher.preFetchNumberOfRows(atMost); - if (state == ExecutionState::WAITING) { - return {state, 0}; - } - if (expectedNumberOfRows >= atMost) { - // We do not care, we have more than atMost anyways. - return {state, expectedNumberOfRows}; - } - // Now we need to figure out a more precise state - for (auto const& inRow : _inputRows) { - if (inRow.state == ExecutionState::HASMORE) { - // This block is not fully fetched, we do NOT know how many rows - // will be in the next batch, overestimate! - return {ExecutionState::HASMORE, atMost}; - } - if (inRow.row.isInitialized()) { - // This dependency is in owned by this Executor - expectedNumberOfRows++; - } - } - if (expectedNumberOfRows == 0) { - return {ExecutionState::DONE, 0}; - } - return {ExecutionState::HASMORE, expectedNumberOfRows}; -} - -size_t SortingGatherExecutor::rowsLeftToWrite() const noexcept { - TRI_ASSERT(constrainedSort()); - TRI_ASSERT(_limit >= _rowsReturned); - return _limit - _rowsReturned; + THROW_ARANGO_EXCEPTION(TRI_ERROR_NOT_IMPLEMENTED); } bool SortingGatherExecutor::constrainedSort() const noexcept { @@ -593,115 +422,8 @@ bool SortingGatherExecutor::maySkip() const noexcept { return constrainedSort() && _rowsReturned >= _limit; } -std::tuple SortingGatherExecutor::skipRows( - size_t const atMost) { - if (!maySkip()) { - // Until our limit, we must produce rows, because we might be asked later - // to produce rows, in which case all rows have to have been skipped in - // order. - return produceAndSkipRows(atMost); - } else { - // If we've reached our limit, we will never be asked to produce rows - // again. So we can just skip without sorting. - return reallySkipRows(atMost); - } -} - -std::tuple SortingGatherExecutor::reallySkipRows( - size_t const atMost) { - // Once, count all rows that are left in the heap (and free them) - if (!_heapCounted) { - initNumDepsIfNecessary(); - - // This row was just fetched: - _inputRows[_dependencyToFetch].row = InputAqlItemRow{CreateInvalidInputRowHint{}}; - _rowsLeftInHeap = 0; - for (auto& it : _inputRows) { - if (it.row) { - ++_rowsLeftInHeap; - it.row = InputAqlItemRow{CreateInvalidInputRowHint{}}; - } - } - _heapCounted = true; - - // Now we will just skip through all dependencies, starting with the first. - _dependencyToFetch = 0; - } - - { // Skip rows we had left in the heap first - std::size_t const skip = std::min(atMost, _rowsLeftInHeap); - _rowsLeftInHeap -= skip; - _skipped += skip; - } - - while (_dependencyToFetch < _numberDependencies && _skipped < atMost) { - auto& state = _inputRows[_dependencyToFetch].state; - while (state != ExecutionState::DONE && _skipped < atMost) { - std::size_t skippedNow; - std::tie(state, skippedNow) = - _fetcher.skipRowsForDependency(_dependencyToFetch, atMost - _skipped); - if (state == ExecutionState::WAITING) { - TRI_ASSERT(skippedNow == 0); - return {state, NoStats{}, 0}; - } - _skipped += skippedNow; - } - if (state == ExecutionState::DONE) { - ++_dependencyToFetch; - } - } - - // Skip dependencies which are DONE - while (_dependencyToFetch < _numberDependencies && - _inputRows[_dependencyToFetch].state == ExecutionState::DONE) { - ++_dependencyToFetch; - } - // The current dependency must now neither be DONE, nor WAITING. - TRI_ASSERT(_dependencyToFetch >= _numberDependencies || - _inputRows[_dependencyToFetch].state == ExecutionState::HASMORE); - - ExecutionState const state = _dependencyToFetch < _numberDependencies - ? ExecutionState::HASMORE - : ExecutionState::DONE; - - TRI_ASSERT(_skipped <= atMost); - std::size_t const skipped = _skipped; - _skipped = 0; - return {state, NoStats{}, skipped}; -} - -std::tuple SortingGatherExecutor::produceAndSkipRows( - size_t const atMost) { - ExecutionState state = ExecutionState::HASMORE; - InputAqlItemRow row{CreateInvalidInputRowHint{}}; - - // We may not skip more rows in this method than we can produce! - auto const ourAtMost = constrainedSort() ? std::min(atMost, rowsLeftToWrite()) : atMost; - - while (state == ExecutionState::HASMORE && _skipped < ourAtMost) { - std::tie(state, row) = produceNextRow(ourAtMost - _skipped); - // HASMORE => row has to be initialized - TRI_ASSERT(state != ExecutionState::HASMORE || row.isInitialized()); - // WAITING => row may not be initialized - TRI_ASSERT(state != ExecutionState::WAITING || !row.isInitialized()); - - if (row.isInitialized()) { - ++_skipped; - } - } - - if (state == ExecutionState::WAITING) { - return {state, NoStats{}, 0}; - } - - // Note that _skipped *can* be larger than `ourAtMost`, due to WAITING, in - // which case we might get a lower `ourAtMost` on the second call than - // during the first. - TRI_ASSERT(_skipped <= atMost); - TRI_ASSERT(state != ExecutionState::HASMORE || _skipped > 0); - TRI_ASSERT(state != ExecutionState::WAITING || _skipped == 0); - - std::size_t const skipped = _skipped; - _skipped = 0; - return {state, NoStats{}, skipped}; +auto SortingGatherExecutor::rowsLeftToWrite() const noexcept -> size_t { + TRI_ASSERT(constrainedSort()); + TRI_ASSERT(_limit >= _rowsReturned); + return _limit - std::min(_limit, _rowsReturned); } diff --git a/arangod/Aql/SortingGatherExecutor.h b/arangod/Aql/SortingGatherExecutor.h index df08b6439b49..440a507847c9 100644 --- a/arangod/Aql/SortingGatherExecutor.h +++ b/arangod/Aql/SortingGatherExecutor.h @@ -114,17 +114,9 @@ class SortingGatherExecutor { using Infos = SortingGatherExecutorInfos; using Stats = NoStats; - SortingGatherExecutor(Fetcher& fetcher, Infos& infos); + SortingGatherExecutor(Fetcher&, Infos& infos); ~SortingGatherExecutor(); - /** - * @brief produce the next Row of Aql Values. - * - * @return ExecutionState, - * if something was written output.hasValue() == true - */ - std::pair produceRows(OutputAqlItemRow& output); - /** * @brief Produce rows * @@ -154,23 +146,11 @@ class SortingGatherExecutor { [[nodiscard]] auto skipRowsRange(MultiAqlItemBlockInputRange& input, AqlCall& call) -> std::tuple; - void adjustNrDone(size_t dependency); - std::pair expectedNumberOfRows(size_t atMost) const; - std::tuple skipRows(size_t atMost); - private: - void initNumDepsIfNecessary(); - - ExecutionState init(size_t atMost); - - std::pair produceNextRow(size_t atMost); - bool constrainedSort() const noexcept; - size_t rowsLeftToWrite() const noexcept; - void assertConstrainedDoesntOverfetch(size_t atMost) const noexcept; // This is interesting in case this is a constrained sort and fullCount is @@ -188,7 +168,7 @@ class SortingGatherExecutor { * @param inputRange Range of all input dependencies * @return std::optional> optional call for the dependnecy requiring input */ - auto requiresMoreInput(MultiAqlItemBlockInputRange const& inputRange) const + auto requiresMoreInput(MultiAqlItemBlockInputRange const& inputRange) -> std::optional>; /** @@ -212,26 +192,21 @@ class SortingGatherExecutor { * This is known to be empty, but all prepared at this point. * @param inputRange The input, no data included yet. */ - auto initialize(typename Fetcher::DataRange const& inputRange) -> void; + auto initialize(MultiAqlItemBlockInputRange const& inputRange) + -> std::optional>; - private: - Fetcher& _fetcher; + auto rowsLeftToWrite() const noexcept -> size_t; + private: // Flag if we are past the initialize phase (fetched one block for every dependency). bool _initialized; // Total Number of dependencies size_t _numberDependencies; - // The Dependency we have to fetch next - size_t _dependencyToFetch; - // Input data to process std::vector _inputRows; - // Counter for DONE states - size_t _nrDone; - /// @brief If we do a constrained sort, it holds the limit > 0. Otherwise, it's 0. size_t _limit; @@ -240,26 +215,9 @@ class SortingGatherExecutor { /// dependencies. size_t _rowsReturned; - /// @brief When we reached the limit, we once count the rows that are left in - /// the heap (in _rowsLeftInHeap), so we can count them for skipping. - bool _heapCounted; - - /// @brief See comment for _heapCounted first. At the first real skip, this - /// is set to the number of rows left in the heap. It will be reduced while - /// skipping. - size_t _rowsLeftInHeap; - - size_t _skipped; - /// @brief sorting strategy std::unique_ptr _strategy; -#ifdef ARANGODB_ENABLE_MAINTAINER_MODE - std::vector _flaggedAsDone; -#endif - std::tuple reallySkipRows(size_t atMost); - std::tuple produceAndSkipRows(size_t atMost); - const bool _fetchParallel; }; From d46e51ce01fb1174641a84ad8e2e9705ff54334b Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 19:29:50 +0100 Subject: [PATCH 19/71] Allow waiting within old-style subquery --- arangod/Aql/ExecutionBlockImpl.cpp | 82 ++++++++++++++++++++++++++---- arangod/Aql/SubqueryExecutor.cpp | 59 +++++++++++++++------ arangod/Aql/SubqueryExecutor.h | 16 ++++-- 3 files changed, 126 insertions(+), 31 deletions(-) diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index d25d5083e758..abf241d61570 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -1252,6 +1252,11 @@ auto ExecutionBlockImpl::executeProduceRows(typename Fetcher::DataRang if constexpr (isNewStyleExecutor) { if constexpr (is_one_of_v) { return _executor.produceRows(input, output); + } else if constexpr (is_one_of_v, SubqueryExecutor>) { + // The SubqueryExecutor has it's own special handling outside. + // SO this code is in fact not reachable + TRI_ASSERT(false); + THROW_ARANGO_EXCEPTION(TRI_ERROR_INTERNAL_AQL); } else { auto [state, stats, call] = _executor.produceRows(input, output); return {state, stats, call, 0}; @@ -1274,6 +1279,11 @@ auto ExecutionBlockImpl::executeSkipRowsRange(typename Fetcher::DataRa auto res = _executor.skipRowsRange(inputRange, call); _executorReturnedDone = std::get(res) == ExecutorState::DONE; return res; + } else if constexpr (is_one_of_v, SubqueryExecutor>) { + // The SubqueryExecutor has it's own special handling outside. + // SO this code is in fact not reachable + TRI_ASSERT(false); + THROW_ARANGO_EXCEPTION(TRI_ERROR_INTERNAL_AQL); } else { auto [state, stats, skipped, localCall] = _executor.skipRowsRange(inputRange, call); @@ -1576,10 +1586,19 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { TRI_ASSERT(!(clientCall.getOffset() == 0 && clientCall.softLimit == AqlCall::Limit{0})); TRI_ASSERT(!(clientCall.hasSoftLimit() && clientCall.fullCount)); TRI_ASSERT(!(clientCall.hasSoftLimit() && clientCall.hasHardLimit())); + if constexpr (is_one_of_v, SubqueryExecutor>) { + // The old subquery executor can in-fact return waiting on produce call. + // if it needs to wait for the subquery. + // So we need to allow the return state here as well. + TRI_ASSERT(_execState == ExecState::CHECKCALL || + _execState == ExecState::SHADOWROWS || _execState == ExecState::UPSTREAM || + _execState == ExecState::PRODUCE || _execState == ExecState::SKIP); + } else { + // We can only have returned the following internal states + TRI_ASSERT(_execState == ExecState::CHECKCALL || _execState == ExecState::SHADOWROWS || + _execState == ExecState::UPSTREAM); + } - // We can only have returned the following internal states - TRI_ASSERT(_execState == ExecState::CHECKCALL || _execState == ExecState::SHADOWROWS || - _execState == ExecState::UPSTREAM); // Skip can only be > 0 if we are in upstream cases. TRI_ASSERT(_skipped == 0 || _execState == ExecState::UPSTREAM); @@ -1633,8 +1652,33 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { clientCall.getLimit() == 0 && clientCall.needsFullCount(); #endif LOG_QUERY("1f786", DEBUG) << printTypeInfo() << " call skipRows " << clientCall; - auto [state, stats, skippedLocal, call, dependency] = - executeSkipRowsRange(_lastRange, clientCall); + + ExecutorState state = ExecutorState::HASMORE; + typename Executor::Stats stats; + size_t skippedLocal = 0; + AqlCall call{}; + size_t dependency = 0; + if constexpr (is_one_of_v>) { + // NOTE: The subquery Executor will by itself call EXECUTE on it's + // subquery. This can return waiting => we can get a WAITING state + // here. We can only get the waiting state for SUbquery executors. + ExecutionState subqueryState = ExecutionState::HASMORE; + std::tie(subqueryState, stats, skippedLocal, call) = + _executor.skipRowsRange(_lastRange, clientCall); + if (subqueryState == ExecutionState::WAITING) { + TRI_ASSERT(skippedLocal == 0); + return {subqueryState, 0, nullptr}; + } else if (subqueryState == ExecutionState::DONE) { + state = ExecutorState::DONE; + } else { + state = ExecutorState::HASMORE; + } + } else { + // Execute skipSome + std::tie(state, stats, skippedLocal, call, dependency) = + executeSkipRowsRange(_lastRange, clientCall); + } + _requestedDependency = dependency; #ifdef ARANGODB_ENABLE_MAINTAINER_MODE // Assertion: We did skip 'skippedLocal' documents here. @@ -1695,11 +1739,29 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { } TRI_ASSERT(_outputItemRow); TRI_ASSERT(!_executorReturnedDone); - - // Execute getSome - auto const [state, stats, call, dependency] = - executeProduceRows(_lastRange, *_outputItemRow); - // TODO: Check + ExecutorState state = ExecutorState::HASMORE; + typename Executor::Stats stats; + AqlCall call{}; + size_t dependency = 0; + if constexpr (is_one_of_v, SubqueryExecutor>) { + // NOTE: The subquery Executor will by itself call EXECUTE on it's + // subquery. This can return waiting => we can get a WAITING state + // here. We can only get the waiting state for SUbquery executors. + ExecutionState subqueryState = ExecutionState::HASMORE; + std::tie(subqueryState, stats, call) = + _executor.produceRows(_lastRange, *_outputItemRow); + if (subqueryState == ExecutionState::WAITING) { + return {subqueryState, 0, nullptr}; + } else if (subqueryState == ExecutionState::DONE) { + state = ExecutorState::DONE; + } else { + state = ExecutorState::HASMORE; + } + } else { + // Execute getSome + std::tie(state, stats, call, dependency) = + executeProduceRows(_lastRange, *_outputItemRow); + } _requestedDependency = dependency; _executorReturnedDone = state == ExecutorState::DONE; _engine->_stats += stats; diff --git a/arangod/Aql/SubqueryExecutor.cpp b/arangod/Aql/SubqueryExecutor.cpp index 1db91ec7f383..c84b47f0346f 100644 --- a/arangod/Aql/SubqueryExecutor.cpp +++ b/arangod/Aql/SubqueryExecutor.cpp @@ -158,7 +158,7 @@ std::pair SubqueryExecutor::pro template auto SubqueryExecutor::produceRows(AqlItemBlockInputRange& input, OutputAqlItemRow& output) - -> std::tuple { + -> std::tuple { auto getUpstreamCall = [&]() { AqlCall upstreamCall = output.getClientCall(); if constexpr (isModificationSubquery) { @@ -173,7 +173,7 @@ auto SubqueryExecutor::produceRows(AqlItemBlockInputRang if (_state == ExecutorState::DONE && !_input.isInitialized()) { // We have seen DONE upstream, and we have discarded our local reference // to the last input, we will not be able to produce results anymore. - return {_state, NoStats{}, getUpstreamCall()}; + return {translatedReturnType(), NoStats{}, getUpstreamCall()}; } while (true) { if (_subqueryInitialized) { @@ -185,13 +185,17 @@ auto SubqueryExecutor::produceRows(AqlItemBlockInputRang writeOutput(output); LOG_DEVEL_SQ << uint64_t(this) << "wrote output is const " << _state << " " << getUpstreamCall(); - return {_state, NoStats{}, getUpstreamCall()}; + return {translatedReturnType(), NoStats{}, getUpstreamCall()}; } // Non const case, or first run in const auto [state, skipped, block] = _subquery.execute(AqlCallStack(AqlCall{})); TRI_ASSERT(skipped == 0); + if (state == ExecutionState::WAITING) { + return {state, NoStats{}, getUpstreamCall()}; + } // We get a result + LOG_DEVEL_SQ << uint64_t(this) << " we get subquery result"; if (block != nullptr) { TRI_IF_FAILURE("SubqueryBlock::executeSubquery") { THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); @@ -199,6 +203,8 @@ auto SubqueryExecutor::produceRows(AqlItemBlockInputRang if (_infos.returnsData()) { TRI_ASSERT(_subqueryResults != nullptr); + LOG_DEVEL_SQ << uint64_t(this) + << " store subquery result for writing " << block->size(); _subqueryResults->emplace_back(std::move(block)); } } @@ -208,7 +214,7 @@ auto SubqueryExecutor::produceRows(AqlItemBlockInputRang writeOutput(output); LOG_DEVEL_SQ << uint64_t(this) << "wrote output subquery done " << _state << " " << getUpstreamCall(); - return {_state, NoStats{}, getUpstreamCall()}; + return {translatedReturnType(), NoStats{}, getUpstreamCall()}; } } else { @@ -219,17 +225,22 @@ auto SubqueryExecutor::produceRows(AqlItemBlockInputRang << _input.isInitialized(); if (!_input) { LOG_DEVEL_SQ << uint64_t(this) << "exit produce, no more input" << _state; - return {_state, NoStats{}, getUpstreamCall()}; + return {translatedReturnType(), NoStats{}, getUpstreamCall()}; } } TRI_ASSERT(_input); if (!_infos.isConst() || _input.isFirstDataRowInBlock()) { - auto initRes = _subquery.initializeCursor(_input); + LOG_DEVEL_SQ << "Subquery: Initialize cursor"; + auto [state, result] = _subquery.initializeCursor(_input); + if (state == ExecutionState::WAITING) { + LOG_DEVEL_SQ << "Waiting on initialize cursor"; + return {state, NoStats{}, AqlCall{}}; + } - if (initRes.second.fail()) { + if (result.fail()) { // Error during initialize cursor - THROW_ARANGO_EXCEPTION(initRes.second); + THROW_ARANGO_EXCEPTION(result); } _subqueryResults = std::make_unique>(); } @@ -297,10 +308,19 @@ SubqueryExecutor::fetchBlockForPassthrough(size_t atMost return {rv.first, {}, std::move(rv.second)}; } +template +auto SubqueryExecutor::translatedReturnType() const + noexcept -> ExecutionState { + if (_state == ExecutorState::DONE) { + return ExecutionState::DONE; + } + return ExecutionState::HASMORE; +} + template <> template <> auto SubqueryExecutor::skipRowsRange<>(AqlItemBlockInputRange& inputRange, AqlCall& call) - -> std::tuple { + -> std::tuple { auto getUpstreamCall = [&]() { auto upstreamCall = AqlCall{}; return upstreamCall; @@ -313,7 +333,7 @@ auto SubqueryExecutor::skipRowsRange<>(AqlItemBlockInputRange& inputRange, if (_state == ExecutorState::DONE && !_input.isInitialized()) { // We have seen DONE upstream, and we have discarded our local reference // to the last input, we will not be able to produce results anymore. - return {_state, NoStats{}, 0, getUpstreamCall()}; + return {translatedReturnType(), NoStats{}, 0, getUpstreamCall()}; } while (true) { if (_subqueryInitialized) { @@ -327,12 +347,16 @@ auto SubqueryExecutor::skipRowsRange<>(AqlItemBlockInputRange& inputRange, skipped += 1; call.didSkip(1); LOG_DEVEL_SQ << uint64_t(this) << "did skip one"; - return {_state, NoStats{}, skipped, getUpstreamCall()}; + return {translatedReturnType(), NoStats{}, skipped, getUpstreamCall()}; } // Non const case, or first run in const auto [state, skipped, block] = _subquery.execute(AqlCallStack(AqlCall{})); TRI_ASSERT(skipped == 0); + if (state == ExecutionState::WAITING) { + return {state, NoStats{}, 0, getUpstreamCall()}; + } + // We get a result if (block != nullptr) { TRI_IF_FAILURE("SubqueryBlock::executeSubquery") { @@ -352,7 +376,7 @@ auto SubqueryExecutor::skipRowsRange<>(AqlItemBlockInputRange& inputRange, skipped += 1; call.didSkip(1); LOG_DEVEL_SQ << uint64_t(this) << "did skip one"; - return {_state, NoStats{}, skipped, getUpstreamCall()}; + return {translatedReturnType(), NoStats{}, skipped, getUpstreamCall()}; } } else { @@ -362,17 +386,20 @@ auto SubqueryExecutor::skipRowsRange<>(AqlItemBlockInputRange& inputRange, if (!_input) { LOG_DEVEL_SQ << uint64_t(this) << "skipped nothing waiting for input " << _state; - return {_state, NoStats{}, skipped, getUpstreamCall()}; + return {translatedReturnType(), NoStats{}, skipped, getUpstreamCall()}; } } TRI_ASSERT(_input); if (!_infos.isConst() || _input.isFirstDataRowInBlock()) { - auto initRes = _subquery.initializeCursor(_input); + auto [state, result] = _subquery.initializeCursor(_input); + if (state == ExecutionState::WAITING) { + return {state, NoStats{}, 0, getUpstreamCall()}; + } - if (initRes.second.fail()) { + if (result.fail()) { // Error during initialize cursor - THROW_ARANGO_EXCEPTION(initRes.second); + THROW_ARANGO_EXCEPTION(result); } _subqueryResults = std::make_unique>(); } diff --git a/arangod/Aql/SubqueryExecutor.h b/arangod/Aql/SubqueryExecutor.h index c7fbee012be2..b9e5c299827b 100644 --- a/arangod/Aql/SubqueryExecutor.h +++ b/arangod/Aql/SubqueryExecutor.h @@ -23,11 +23,11 @@ #ifndef ARANGOD_AQL_SUBQUERY_EXECUTOR_H #define ARANGOD_AQL_SUBQUERY_EXECUTOR_H +#include "Aql/AqlCall.h" +#include "Aql/AqlItemBlockInputRange.h" #include "Aql/ExecutionState.h" #include "Aql/ExecutorInfos.h" #include "Aql/InputAqlItemRow.h" -#include "Aql/AqlItemBlockInputRange.h" -#include "Aql/AqlCall.h" #include "Aql/Stats.h" #include "Basics/Result.h" @@ -97,13 +97,13 @@ class SubqueryExecutor { std::pair produceRows(OutputAqlItemRow& output); [[nodiscard]] auto produceRows(AqlItemBlockInputRange& input, OutputAqlItemRow& output) - -> std::tuple; + -> std::tuple; // skipRowsRange <=> isModificationSubquery - template = 0> + template = 0> auto skipRowsRange(AqlItemBlockInputRange& inputRange, AqlCall& call) - -> std::tuple; + -> std::tuple; std::tuple fetchBlockForPassthrough(size_t atMost); @@ -114,6 +114,12 @@ class SubqueryExecutor { */ void writeOutput(OutputAqlItemRow& output); + /** + * @brief Translate _state => to to execution allowing waiting. + * + */ + auto translatedReturnType() const noexcept -> ExecutionState; + private: Fetcher& _fetcher; SubqueryExecutorInfos& _infos; From 1e5947a82be3123ce008381b3c4404ad95b13e29 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sat, 29 Feb 2020 22:56:22 +0100 Subject: [PATCH 20/71] Fixed invalid skipRwos in unsorted gather --- arangod/Aql/UnsortedGatherExecutor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arangod/Aql/UnsortedGatherExecutor.cpp b/arangod/Aql/UnsortedGatherExecutor.cpp index e1b3221a640f..3535d505e754 100644 --- a/arangod/Aql/UnsortedGatherExecutor.cpp +++ b/arangod/Aql/UnsortedGatherExecutor.cpp @@ -86,7 +86,7 @@ auto UnsortedGatherExecutor::produceRows(typename Fetcher::DataRange& input, auto UnsortedGatherExecutor::skipRowsRange(typename Fetcher::DataRange& input, AqlCall& call) -> std::tuple { auto skipped = size_t{0}; - while (call.needSkipMore() && input.hasDataRow(currentDependency())) { + while (call.needSkipMore() && !done() && input.hasDataRow(currentDependency())) { auto [state, inputRow] = input.nextDataRow(currentDependency()); call.didSkip(1); From 56c41b10f947258d1327e9d52e8d4551c7766326 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sun, 1 Mar 2020 09:48:34 +0100 Subject: [PATCH 21/71] First draft of ParallelUnsortedGatherExecutor --- arangod/Aql/ExecutionBlockImpl.cpp | 8 +- arangod/Aql/MultiAqlItemBlockInputRange.cpp | 5 + arangod/Aql/MultiAqlItemBlockInputRange.h | 9 + .../Aql/ParallelUnsortedGatherExecutor.cpp | 163 +++++++----------- arangod/Aql/ParallelUnsortedGatherExecutor.h | 44 +++-- 5 files changed, 112 insertions(+), 117 deletions(-) diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index 836d7028522e..a81f1c6c3d7f 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -135,7 +135,7 @@ constexpr bool is_one_of_v = (std::is_same_v || ...); */ template constexpr bool isNewStyleExecutor = is_one_of_v< - Executor, FilterExecutor, SortedCollectExecutor, IdExecutor, + Executor, FilterExecutor, SortedCollectExecutor, IdExecutor, ParallelUnsortedGatherExecutor, IdExecutor>, ReturnExecutor, DistinctCollectExecutor, IndexExecutor, EnumerateCollectionExecutor, SubqueryExecutor, SubqueryExecutor, CountCollectExecutor, CalculationExecutor, CalculationExecutor, @@ -1125,7 +1125,7 @@ static SkipRowsRangeVariant constexpr skipRowsType() { static_assert( useExecutor == (is_one_of_v< - Executor, FilterExecutor, ShortestPathExecutor, ReturnExecutor, KShortestPathsExecutor, + Executor, FilterExecutor, ShortestPathExecutor, ReturnExecutor, KShortestPathsExecutor, ParallelUnsortedGatherExecutor, IdExecutor>, IdExecutor, HashedCollectExecutor, IndexExecutor, EnumerateCollectionExecutor, DistinctCollectExecutor, ConstrainedSortExecutor, CountCollectExecutor, SubqueryExecutor, @@ -1250,7 +1250,7 @@ auto ExecutionBlockImpl::executeProduceRows(typename Fetcher::DataRang OutputAqlItemRow& output) -> std::tuple { if constexpr (isNewStyleExecutor) { - if constexpr (is_one_of_v) { + if constexpr (is_one_of_v) { return _executor.produceRows(input, output); } else if constexpr (is_one_of_v, SubqueryExecutor>) { // The SubqueryExecutor has it's own special handling outside. @@ -1273,7 +1273,7 @@ auto ExecutionBlockImpl::executeSkipRowsRange(typename Fetcher::DataRa if constexpr (isNewStyleExecutor) { call.skippedRows = 0; if constexpr (skipRowsType() == SkipRowsRangeVariant::EXECUTOR) { - if constexpr (is_one_of_v) { + if constexpr (is_one_of_v) { // If the executor has a method skipRowsRange, to skip outputs. // Every non-passthrough executor needs to implement this. auto res = _executor.skipRowsRange(inputRange, call); diff --git a/arangod/Aql/MultiAqlItemBlockInputRange.cpp b/arangod/Aql/MultiAqlItemBlockInputRange.cpp index 226d082a708a..6bc50b79ff4e 100644 --- a/arangod/Aql/MultiAqlItemBlockInputRange.cpp +++ b/arangod/Aql/MultiAqlItemBlockInputRange.cpp @@ -65,6 +65,11 @@ auto MultiAqlItemBlockInputRange::hasDataRow() const noexcept -> bool { }); } +auto MultiAqlItemBlockInputRange::rangeForDependency(size_t const dependency) + -> AqlItemBlockInputRange& { + return _inputs.at(dependency); +} + auto MultiAqlItemBlockInputRange::peekDataRow(size_t const dependency) const -> std::pair { TRI_ASSERT(dependency < _inputs.size()); diff --git a/arangod/Aql/MultiAqlItemBlockInputRange.h b/arangod/Aql/MultiAqlItemBlockInputRange.h index c35b8c93e055..74aa1584c2ac 100644 --- a/arangod/Aql/MultiAqlItemBlockInputRange.h +++ b/arangod/Aql/MultiAqlItemBlockInputRange.h @@ -49,6 +49,15 @@ class MultiAqlItemBlockInputRange { bool hasDataRow() const noexcept; bool hasDataRow(size_t const dependency) const noexcept; + /** + * @brief Get a reference to the range of a given dependency + * NOTE: Modifing this range will modify the state of this class as well + * + * @param dependency index of the dependency + * @return AqlItemBlockInputRange& Modifyable reference to the input data stream + */ + auto rangeForDependency(size_t const dependency) -> AqlItemBlockInputRange&; + std::pair peekDataRow(size_t const dependency) const; std::pair nextDataRow(size_t const dependency); auto skipAll(size_t const dependency) noexcept -> std::size_t; diff --git a/arangod/Aql/ParallelUnsortedGatherExecutor.cpp b/arangod/Aql/ParallelUnsortedGatherExecutor.cpp index 629d8bea2893..9a3f205a4497 100644 --- a/arangod/Aql/ParallelUnsortedGatherExecutor.cpp +++ b/arangod/Aql/ParallelUnsortedGatherExecutor.cpp @@ -44,6 +44,11 @@ ParallelUnsortedGatherExecutor::ParallelUnsortedGatherExecutor(Fetcher& fetcher, ParallelUnsortedGatherExecutor::~ParallelUnsortedGatherExecutor() = default; +auto ParallelUnsortedGatherExecutor::upstreamCall() const noexcept -> AqlCall { + // TODO: Implement me + return AqlCall{}; +} + //////////////////////////////////////////////////////////////////////////////// /// @brief Guarantees requiredby this this block: /// 1) For every dependency the input is sorted, according to the same strategy. @@ -58,117 +63,73 @@ ParallelUnsortedGatherExecutor::~ParallelUnsortedGatherExecutor() = default; /// //////////////////////////////////////////////////////////////////////////////// -std::pair ParallelUnsortedGatherExecutor::produceRows(OutputAqlItemRow& output) { - initDependencies(); - - ExecutionState state; - InputAqlItemRow inputRow = InputAqlItemRow{CreateInvalidInputRowHint{}}; - - size_t x; - for (x = 0; x < _numberDependencies; ++x) { - size_t i = (_currentDependency + x) % _numberDependencies; - - if (_upstream[i] == ExecutionState::DONE) { - continue; - } - - size_t tmp = 0; - - state = ExecutionState::HASMORE; - while (!output.isFull() && state == ExecutionState::HASMORE) { - std::tie(state, inputRow) = _fetcher.fetchRowForDependency(i, output.numRowsLeft() /*atMost*/); - if (inputRow) { - output.copyRow(inputRow); - TRI_ASSERT(output.produced()); +auto ParallelUnsortedGatherExecutor::produceRows(typename Fetcher::DataRange& input, + OutputAqlItemRow& output) + -> std::tuple { + // Illegal dependency, on purpose to trigger asserts + size_t waitingDep = input.numberDependencies(); + for (size_t dep = 0; dep < input.numberDependencies(); ++dep) { + while (!output.isFull()) { + auto [state, row] = input.nextDataRow(dep); + if (row) { + output.copyRow(row); output.advanceRow(); - tmp++; + } else { + // This output did not produce anything + if (state == ExecutorState::HASMORE) { + waitingDep = dep; + } + break; } } - - _upstream[i] = state; - if (output.isFull()) { - break; - } - } - _currentDependency = x; - - NoStats stats; - - // fix assert in ExecutionBlockImpl::getSomeWithoutTrace - if (output.isFull()) { - return {ExecutionState::HASMORE, stats}; - } - - size_t numWaiting = 0; - for (x = 0; x < _numberDependencies; ++x) { - if (_upstream[x] == ExecutionState::HASMORE) { - return {ExecutionState::HASMORE, stats}; - } else if (_upstream[x] == ExecutionState::WAITING) { - numWaiting++; - } } - if (numWaiting > 0) { - return {ExecutionState::WAITING, stats}; + if (input.isDone()) { + // We cannot have one that we are waiting on, if we are done. + TRI_ASSERT(waitingDep == input.numberDependencies()); + return {ExecutorState::DONE, NoStats{}, AqlCall{}, waitingDep}; } - - TRI_ASSERT(std::all_of(_upstream.begin(), _upstream.end(), [](auto const& s) { return s == ExecutionState::DONE; } )); - return {ExecutionState::DONE, stats}; + return {ExecutorState::HASMORE, NoStats{}, upstreamCall(), waitingDep}; } -std::tuple -ParallelUnsortedGatherExecutor::skipRows(size_t const toSkip) { - initDependencies(); - TRI_ASSERT(_skipped <= toSkip); - - ExecutionState state = ExecutionState::HASMORE; - while (_skipped < toSkip) { - - const size_t i = _currentDependency; - if (_upstream[i] == ExecutionState::DONE) { - if (std::all_of(_upstream.begin(), _upstream.end(), - [](auto s) { return s == ExecutionState::DONE; })) { - state = ExecutionState::DONE; - break; - } - _currentDependency = (i + 1) % _numberDependencies; - continue; - } - - TRI_ASSERT(_skipped <= toSkip); +auto ParallelUnsortedGatherExecutor::skipRowsRange(typename Fetcher::DataRange& input, + AqlCall& call) + -> std::tuple { + size_t waitingDep = input.numberDependencies(); + for (size_t dep = 0; dep < input.numberDependencies(); ++dep) { + auto& range = input.rangeForDependency(dep); + while (call.needSkipMore()) { + if (!range.hasDataRow() && range.skippedInFlight() == 0) { + // Consumed this range, + // consume the next one - size_t skippedNow; - std::tie(state, skippedNow) = _fetcher.skipRowsForDependency(i, toSkip - _skipped); - _upstream[i] = state; - if (state == ExecutionState::WAITING) { - TRI_ASSERT(skippedNow == 0); - return {ExecutionState::WAITING, NoStats{}, 0}; - } - _skipped += skippedNow; - - if (_upstream[i] == ExecutionState::DONE) { - if (std::all_of(_upstream.begin(), _upstream.end(), - [](auto s) { return s == ExecutionState::DONE; })) { + // Guarantee: + // While in offsetPhase, we will only send requests to the first + // NON-DONE dependency. + if (range.upstreamState() == ExecutorState::HASMORE && + waitingDep == input.numberDependencies()) { + waitingDep = dep; + } break; } - _currentDependency = (i + 1) % _numberDependencies; - continue; - } + if (range.hasDataRow()) { + // We overfetched, skipLocally + // By gurantee we will only see data, if + // we are past the offset phase. + TRI_ASSERT(call.getOffset() == 0); + } else { + if (call.getOffset() > 0) { + call.didSkip(range.skip(call.getOffset())); + } else { + // Fullcount Case + call.didSkip(range.skipAll()); + } + } + } } - - size_t skipped = _skipped; - _skipped = 0; - - TRI_ASSERT(skipped <= toSkip); - return {state, NoStats{}, skipped}; -} - -void ParallelUnsortedGatherExecutor::initDependencies() { - if (_numberDependencies == 0) { - // We need to initialize the dependencies once, they are injected - // after the fetcher is created. - _numberDependencies = _fetcher.numberDependencies(); - TRI_ASSERT(_numberDependencies > 0); - _upstream.resize(_numberDependencies, ExecutionState::HASMORE); - TRI_ASSERT(std::all_of(_upstream.begin(), _upstream.end(), [](auto const& s) { return s == ExecutionState::HASMORE; } )); + if (input.isDone()) { + // We cannot have one that we are waiting on, if we are done. + TRI_ASSERT(waitingDep == input.numberDependencies()); + return {ExecutorState::DONE, NoStats{}, call.getSkipCount(), AqlCall{}, waitingDep}; } + return {ExecutorState::HASMORE, NoStats{}, call.getSkipCount(), upstreamCall(), waitingDep}; } diff --git a/arangod/Aql/ParallelUnsortedGatherExecutor.h b/arangod/Aql/ParallelUnsortedGatherExecutor.h index ed4a32e31333..bd3669d57c98 100644 --- a/arangod/Aql/ParallelUnsortedGatherExecutor.h +++ b/arangod/Aql/ParallelUnsortedGatherExecutor.h @@ -37,10 +37,11 @@ class Methods; namespace aql { +struct AqlCall; +class MultiAqlItemBlockInputRange; class MultiDependencySingleRowFetcher; class NoStats; class OutputAqlItemRow; -struct SortRegister; class ParallelUnsortedGatherExecutorInfos : public ExecutorInfos { public: @@ -55,7 +56,6 @@ class ParallelUnsortedGatherExecutorInfos : public ExecutorInfos { class ParallelUnsortedGatherExecutor { public: - public: struct Properties { static constexpr bool preservesOrder = true; @@ -71,19 +71,39 @@ class ParallelUnsortedGatherExecutor { ~ParallelUnsortedGatherExecutor(); /** - * @brief produce the next Row of Aql Values. + * @brief Produce rows + * + * @param input DataRange delivered by the fetcher + * @param output place to write rows to + * @return std::tuple + * ExecutorState: DONE or HASMORE (only within a subquery) + * Stats: Stats gerenated here + * AqlCall: Request to upstream + * size:t: Dependency to request + */ + [[nodiscard]] auto produceRows(MultiAqlItemBlockInputRange& input, OutputAqlItemRow& output) + -> std::tuple; + + /** + * @brief Skip rows * - * @return ExecutionState, - * if something was written output.hasValue() == true + * @param input DataRange delivered by the fetcher + * @param call skip request form consumer + * @return std::tuple + * ExecutorState: DONE or HASMORE (only within a subquery) + * Stats: Stats gerenated here + * size_t: Number of rows skipped + * AqlCall: Request to upstream + * size:t: Dependency to request */ - std::pair produceRows(OutputAqlItemRow& output); + [[nodiscard]] auto skipRowsRange(MultiAqlItemBlockInputRange& input, AqlCall& call) + -> std::tuple; - std::tuple skipRows(size_t atMost); - private: - void initDependencies(); - + + auto upstreamCall() const noexcept -> AqlCall; + private: Fetcher& _fetcher; // 64: default size of buffer; 8: Alignment size; computed to 4 but breaks in windows debug build. @@ -92,9 +112,9 @@ class ParallelUnsortedGatherExecutor { // Total Number of dependencies size_t _numberDependencies; - + size_t _currentDependency; - + size_t _skipped; }; From 474bf92117aec188d80db89132df828742542667 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sun, 1 Mar 2020 09:54:28 +0100 Subject: [PATCH 22/71] Removed unused local variables --- arangod/Aql/ParallelUnsortedGatherExecutor.cpp | 14 +++++++------- arangod/Aql/ParallelUnsortedGatherExecutor.h | 17 +---------------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/arangod/Aql/ParallelUnsortedGatherExecutor.cpp b/arangod/Aql/ParallelUnsortedGatherExecutor.cpp index 9a3f205a4497..cc5e83a32b04 100644 --- a/arangod/Aql/ParallelUnsortedGatherExecutor.cpp +++ b/arangod/Aql/ParallelUnsortedGatherExecutor.cpp @@ -39,14 +39,13 @@ ParallelUnsortedGatherExecutorInfos::ParallelUnsortedGatherExecutorInfos( nrInOutRegisters, nrInOutRegisters, std::move(registersToClear), std::move(registersToKeep)) {} -ParallelUnsortedGatherExecutor::ParallelUnsortedGatherExecutor(Fetcher& fetcher, Infos& infos) - : _fetcher(fetcher), _numberDependencies(0), _currentDependency(0), _skipped(0) {} +ParallelUnsortedGatherExecutor::ParallelUnsortedGatherExecutor(Fetcher&, Infos& infos) {} ParallelUnsortedGatherExecutor::~ParallelUnsortedGatherExecutor() = default; -auto ParallelUnsortedGatherExecutor::upstreamCall() const noexcept -> AqlCall { - // TODO: Implement me - return AqlCall{}; +auto ParallelUnsortedGatherExecutor::upstreamCall(AqlCall const& clientCall) const + noexcept -> AqlCall { + return clientCall; } //////////////////////////////////////////////////////////////////////////////// @@ -88,7 +87,7 @@ auto ParallelUnsortedGatherExecutor::produceRows(typename Fetcher::DataRange& in TRI_ASSERT(waitingDep == input.numberDependencies()); return {ExecutorState::DONE, NoStats{}, AqlCall{}, waitingDep}; } - return {ExecutorState::HASMORE, NoStats{}, upstreamCall(), waitingDep}; + return {ExecutorState::HASMORE, NoStats{}, upstreamCall(output.getClientCall()), waitingDep}; } auto ParallelUnsortedGatherExecutor::skipRowsRange(typename Fetcher::DataRange& input, @@ -131,5 +130,6 @@ auto ParallelUnsortedGatherExecutor::skipRowsRange(typename Fetcher::DataRange& TRI_ASSERT(waitingDep == input.numberDependencies()); return {ExecutorState::DONE, NoStats{}, call.getSkipCount(), AqlCall{}, waitingDep}; } - return {ExecutorState::HASMORE, NoStats{}, call.getSkipCount(), upstreamCall(), waitingDep}; + return {ExecutorState::HASMORE, NoStats{}, call.getSkipCount(), + upstreamCall(call), waitingDep}; } diff --git a/arangod/Aql/ParallelUnsortedGatherExecutor.h b/arangod/Aql/ParallelUnsortedGatherExecutor.h index bd3669d57c98..6df2900bce22 100644 --- a/arangod/Aql/ParallelUnsortedGatherExecutor.h +++ b/arangod/Aql/ParallelUnsortedGatherExecutor.h @@ -100,22 +100,7 @@ class ParallelUnsortedGatherExecutor { -> std::tuple; private: - void initDependencies(); - - auto upstreamCall() const noexcept -> AqlCall; - - private: - Fetcher& _fetcher; - // 64: default size of buffer; 8: Alignment size; computed to 4 but breaks in windows debug build. - ::arangodb::containers::SmallVector::allocator_type::arena_type _arena; - ::arangodb::containers::SmallVector _upstream{_arena}; - - // Total Number of dependencies - size_t _numberDependencies; - - size_t _currentDependency; - - size_t _skipped; + auto upstreamCall(AqlCall const& clientCall) const noexcept -> AqlCall; }; } // namespace aql From 854082d579939cb76ca799d469ef85bf07d789fc Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sun, 1 Mar 2020 12:15:43 +0100 Subject: [PATCH 23/71] Added some Assertions in MultiAqlItemBlockInputRange --- arangod/Aql/MultiAqlItemBlockInputRange.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/arangod/Aql/MultiAqlItemBlockInputRange.cpp b/arangod/Aql/MultiAqlItemBlockInputRange.cpp index 8b5aad8595d7..556ee2c174a7 100644 --- a/arangod/Aql/MultiAqlItemBlockInputRange.cpp +++ b/arangod/Aql/MultiAqlItemBlockInputRange.cpp @@ -36,12 +36,14 @@ MultiAqlItemBlockInputRange::MultiAqlItemBlockInputRange(ExecutorState state, std::size_t skipped, std::size_t nrInputRanges) { _inputs.resize(nrInputRanges, AqlItemBlockInputRange{state, skipped}); + TRI_ASSERT(nrInputRanges > 0); } auto MultiAqlItemBlockInputRange::resizeIfNecessary(ExecutorState state, size_t skipped, size_t nrInputRanges) -> void { // We never want to reduce the number of dependencies. TRI_ASSERT(_inputs.size() <= nrInputRanges); + TRI_ASSERT(nrInputRanges > 0); if (_inputs.size() < nrInputRanges) { _inputs.resize(nrInputRanges, AqlItemBlockInputRange{state, skipped}); } @@ -89,7 +91,7 @@ auto MultiAqlItemBlockInputRange::hasShadowRow() const noexcept -> bool { // * assert that all dependencies are on a shadow row? auto MultiAqlItemBlockInputRange::peekShadowRow() const -> arangodb::aql::ShadowAqlItemRow { TRI_ASSERT(!hasDataRow()); - + TRI_ASSERT(!_inputs.empty()); // TODO: Correct? return _inputs.at(0).peekShadowRow(); } From 692c3b74d61175542cb525bd91c18edc303366d5 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sun, 1 Mar 2020 12:16:25 +0100 Subject: [PATCH 24/71] Initialize dependdencies of MultiDependencyFetcher --- arangod/Aql/MultiDependencySingleRowFetcher.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/arangod/Aql/MultiDependencySingleRowFetcher.cpp b/arangod/Aql/MultiDependencySingleRowFetcher.cpp index f2abc6a44528..aac3b3b866ac 100644 --- a/arangod/Aql/MultiDependencySingleRowFetcher.cpp +++ b/arangod/Aql/MultiDependencySingleRowFetcher.cpp @@ -368,6 +368,9 @@ auto MultiDependencySingleRowFetcher::useStack(AqlCallStack const& stack) -> voi auto MultiDependencySingleRowFetcher::executeForDependency(size_t const dependency, AqlCallStack& stack) -> std::tuple { + if (_dependencyStates.empty()) { + initDependencies(); + } auto [state, skipped, block] = _dependencyProxy->executeForDependency(dependency, stack); if (state == ExecutionState::WAITING) { From 03a03dabdafdc6a6f1b7212a379f2506eacf8491 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sun, 1 Mar 2020 15:12:52 +0100 Subject: [PATCH 25/71] Fixed skipRows loop in UnsortingGatherNode --- arangod/Aql/UnsortedGatherExecutor.cpp | 28 +++++++++++++++++--------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/arangod/Aql/UnsortedGatherExecutor.cpp b/arangod/Aql/UnsortedGatherExecutor.cpp index 3535d505e754..2a8041bf7e79 100644 --- a/arangod/Aql/UnsortedGatherExecutor.cpp +++ b/arangod/Aql/UnsortedGatherExecutor.cpp @@ -85,25 +85,33 @@ auto UnsortedGatherExecutor::produceRows(typename Fetcher::DataRange& input, auto UnsortedGatherExecutor::skipRowsRange(typename Fetcher::DataRange& input, AqlCall& call) -> std::tuple { - auto skipped = size_t{0}; - while (call.needSkipMore() && !done() && input.hasDataRow(currentDependency())) { - auto [state, inputRow] = input.nextDataRow(currentDependency()); + while (call.needSkipMore() && !done()) { + if (input.hasDataRow(currentDependency())) { + auto [state, inputRow] = input.nextDataRow(currentDependency()); - call.didSkip(1); - skipped++; + call.didSkip(1); - if (state == ExecutorState::DONE) { - advanceDependency(); + if (state == ExecutorState::DONE) { + advanceDependency(); + } + } else { + if (input.upstreamState(currentDependency()) == ExecutorState::DONE) { + advanceDependency(); + } else { + // We need to fetch more first + break; + } } } if (done()) { // here currentDependency is invalid which will cause things to crash // if we ask upstream in ExecutionBlockImpl. yolo. - return {ExecutorState::DONE, Stats{}, skipped, AqlCall{}, currentDependency()}; + return {ExecutorState::DONE, Stats{}, call.getSkipCount(), AqlCall{}, + currentDependency()}; } else { - return {input.upstreamState(currentDependency()), Stats{}, skipped, - AqlCall{}, currentDependency()}; + return {input.upstreamState(currentDependency()), Stats{}, + call.getSkipCount(), AqlCall{}, currentDependency()}; } } From 01bca145fbd80592450e60f2391af86b53d50c87 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sun, 1 Mar 2020 16:57:09 +0100 Subject: [PATCH 26/71] Added an implementation for a SkipResult, in order to simplify exchange of it. --- arangod/Aql/SkipResult.cpp | 34 ++++++++++++++++++ arangod/Aql/SkipResult.h | 47 ++++++++++++++++++++++++ arangod/CMakeLists.txt | 1 + tests/Aql/SkipResultTest.cpp | 70 ++++++++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 1 + 5 files changed, 153 insertions(+) create mode 100644 arangod/Aql/SkipResult.cpp create mode 100644 arangod/Aql/SkipResult.h create mode 100644 tests/Aql/SkipResultTest.cpp diff --git a/arangod/Aql/SkipResult.cpp b/arangod/Aql/SkipResult.cpp new file mode 100644 index 000000000000..66d13bd466b6 --- /dev/null +++ b/arangod/Aql/SkipResult.cpp @@ -0,0 +1,34 @@ + +//////////////////////////////////////////////////////////////////////////////// +/// DISCLAIMER +/// +/// Copyright 2018 ArangoDB GmbH, Cologne, Germany +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License 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. +/// +/// Copyright holder is ArangoDB GmbH, Cologne, Germany +/// +/// @author Michael Hackstein +//////////////////////////////////////////////////////////////////////////////// + +#include "SkipResult.h" + +using namespace arangodb::aql; + +SkipResult::SkipResult() {} + +SkipResult::SkipResult(SkipResult const& other) : _skipped{other._skipped} {} + +auto SkipResult::getSkipCount() const noexcept -> size_t { return _skipped; } + +auto SkipResult::didSkip(size_t skipped) -> void { _skipped += skipped; } \ No newline at end of file diff --git a/arangod/Aql/SkipResult.h b/arangod/Aql/SkipResult.h new file mode 100644 index 000000000000..6eaefe651ec9 --- /dev/null +++ b/arangod/Aql/SkipResult.h @@ -0,0 +1,47 @@ +//////////////////////////////////////////////////////////////////////////////// +/// DISCLAIMER +/// +/// Copyright 2018 ArangoDB GmbH, Cologne, Germany +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License 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. +/// +/// Copyright holder is ArangoDB GmbH, Cologne, Germany +/// +/// @author Michael Hackstein +//////////////////////////////////////////////////////////////////////////////// + +#ifndef ARANGOD_AQL_SKIP_RESULT_H +#define ARANGOD_AQL_SKIP_RESULT_H + +// for size_t +#include + +namespace arangodb::aql { + +class SkipResult { + public: + SkipResult(); + + ~SkipResult() = default; + + SkipResult(SkipResult const& other); + + auto getSkipCount() const noexcept -> size_t; + + auto didSkip(size_t skipped) -> void; + + private: + size_t _skipped{0}; +}; +} // namespace arangodb::aql +#endif \ No newline at end of file diff --git a/arangod/CMakeLists.txt b/arangod/CMakeLists.txt index 785416e6a60a..c13cca6b74f1 100644 --- a/arangod/CMakeLists.txt +++ b/arangod/CMakeLists.txt @@ -348,6 +348,7 @@ set(LIB_ARANGO_AQL_SOURCES Aql/SimpleModifier.cpp Aql/SingleRemoteModificationExecutor.cpp Aql/SingleRowFetcher.cpp + Aql/SkipResult.cpp Aql/SortCondition.cpp Aql/SortExecutor.cpp Aql/SortNode.cpp diff --git a/tests/Aql/SkipResultTest.cpp b/tests/Aql/SkipResultTest.cpp new file mode 100644 index 000000000000..7bf8ac415001 --- /dev/null +++ b/tests/Aql/SkipResultTest.cpp @@ -0,0 +1,70 @@ +//////////////////////////////////////////////////////////////////////////////// +/// DISCLAIMER +/// +/// Copyright 2018 ArangoDB GmbH, Cologne, Germany +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License 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. +/// +/// Copyright holder is ArangoDB GmbH, Cologne, Germany +/// +/// @author Michael Hackstein +//////////////////////////////////////////////////////////////////////////////// + +#include "gtest/gtest.h" + +#include "Aql/SkipResult.h" + +using namespace arangodb; +using namespace arangodb::aql; + +namespace arangodb { +namespace tests { +namespace aql { + +class SkipResultTest : public ::testing::Test { + protected: + SkipResultTest() {} +}; + +TEST_F(SkipResultTest, defaults_to_0_skip) { + SkipResult testee{}; + EXPECT_EQ(testee.getSkipCount(), 0); +} + +TEST_F(SkipResultTest, counts_skip) { + SkipResult testee{}; + testee.didSkip(5); + EXPECT_EQ(testee.getSkipCount(), 5); +} + +TEST_F(SkipResultTest, accumulates_skips) { + SkipResult testee{}; + testee.didSkip(3); + testee.didSkip(6); + testee.didSkip(8); + EXPECT_EQ(testee.getSkipCount(), 17); +} + +TEST_F(SkipResultTest, is_copyable) { + SkipResult original{}; + original.didSkip(6); + SkipResult testee{original}; + + EXPECT_EQ(testee.getSkipCount(), original.getSkipCount()); + + original.didSkip(7); + EXPECT_NE(testee.getSkipCount(), original.getSkipCount()); +} +} // namespace aql +} // namespace tests +} // namespace arangodb diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9ed9e68f7a40..4c8d6934d0dd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -64,6 +64,7 @@ set(ARANGODB_TESTS_SOURCES Aql/RowFetcherHelper.cpp Aql/ScatterExecutorTest.cpp Aql/ShortestPathExecutorTest.cpp + Aql/SkipResultTest.cpp Aql/SingleRowFetcherTest.cpp Aql/SortedCollectExecutorTest.cpp Aql/SortExecutorTest.cpp From 0a441312383e71ece62100f7ab7c95a87d8f863e Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sun, 1 Mar 2020 19:07:29 +0100 Subject: [PATCH 27/71] Moved production API -> SkipResult --- arangod/Aql/AllRowsFetcher.cpp | 10 +-- arangod/Aql/AllRowsFetcher.h | 3 +- arangod/Aql/AqlExecuteResult.cpp | 29 ++------- arangod/Aql/AqlExecuteResult.h | 9 +-- arangod/Aql/BlocksWithClients.cpp | 18 +++--- arangod/Aql/BlocksWithClients.h | 9 +-- arangod/Aql/ConstFetcher.cpp | 19 ++++-- arangod/Aql/ConstFetcher.h | 3 +- arangod/Aql/DependencyProxy.cpp | 38 +++++------ arangod/Aql/DependencyProxy.h | 5 +- arangod/Aql/DistributeExecutor.cpp | 3 +- arangod/Aql/DistributeExecutor.h | 2 +- arangod/Aql/ExecutionBlock.cpp | 12 ++-- arangod/Aql/ExecutionBlock.h | 9 +-- arangod/Aql/ExecutionBlockImpl.cpp | 64 +++++++++++-------- arangod/Aql/ExecutionBlockImpl.h | 11 ++-- arangod/Aql/ExecutionEngine.cpp | 17 ++--- arangod/Aql/ExecutionEngine.h | 5 +- .../Aql/MultiDependencySingleRowFetcher.cpp | 9 +-- arangod/Aql/MultiDependencySingleRowFetcher.h | 3 +- arangod/Aql/RemoteExecutor.cpp | 42 ++++++------ arangod/Aql/RemoteExecutor.h | 18 ++++-- arangod/Aql/RestAqlHandler.cpp | 2 +- arangod/Aql/ScatterExecutor.cpp | 2 +- arangod/Aql/ScatterExecutor.h | 3 +- arangod/Aql/SingleRowFetcher.cpp | 16 +++-- arangod/Aql/SingleRowFetcher.h | 3 +- arangod/Aql/SkipResult.cpp | 40 +++++++++++- arangod/Aql/SkipResult.h | 18 ++++++ arangod/Aql/SubqueryExecutor.cpp | 6 +- tests/Aql/SkipResultTest.cpp | 41 ++++++++++++ 31 files changed, 297 insertions(+), 172 deletions(-) diff --git a/arangod/Aql/AllRowsFetcher.cpp b/arangod/Aql/AllRowsFetcher.cpp index 27702b4f5995..26712ae18b44 100644 --- a/arangod/Aql/AllRowsFetcher.cpp +++ b/arangod/Aql/AllRowsFetcher.cpp @@ -60,7 +60,7 @@ std::pair AllRowsFetcher::fetchAllRows() { return {ExecutionState::DONE, nullptr}; } -std::tuple AllRowsFetcher::execute(AqlCallStack& stack) { +std::tuple AllRowsFetcher::execute(AqlCallStack& stack) { if (!stack.isRelevant()) { auto [state, skipped, block] = _dependencyProxy->execute(stack); return {state, skipped, AqlItemBlockInputMatrix{block}}; @@ -79,13 +79,13 @@ std::tuple AllRowsFetcher::exec TRI_ASSERT(!_aqlItemMatrix->stoppedOnShadowRow()); while (true) { auto [state, skipped, block] = _dependencyProxy->execute(stack); - TRI_ASSERT(skipped == 0); + TRI_ASSERT(skipped.nothingSkipped()); // we will either build a complete fetched AqlItemBlockInputMatrix or return an empty one if (state == ExecutionState::WAITING) { TRI_ASSERT(block == nullptr); // On waiting we have nothing to return - return {state, 0, AqlItemBlockInputMatrix{ExecutorState::HASMORE}}; + return {state, SkipResult{}, AqlItemBlockInputMatrix{ExecutorState::HASMORE}}; } TRI_ASSERT(block != nullptr || state == ExecutionState::DONE); @@ -97,10 +97,10 @@ std::tuple AllRowsFetcher::exec // If we find a ShadowRow or ExecutionState == Done, we're done fetching. if (_aqlItemMatrix->stoppedOnShadowRow() || state == ExecutionState::DONE) { if (state == ExecutionState::HASMORE) { - return {state, 0, + return {state, SkipResult{}, AqlItemBlockInputMatrix{ExecutorState::HASMORE, _aqlItemMatrix.get()}}; } - return {state, 0, + return {state, SkipResult{}, AqlItemBlockInputMatrix{ExecutorState::DONE, _aqlItemMatrix.get()}}; } } diff --git a/arangod/Aql/AllRowsFetcher.h b/arangod/Aql/AllRowsFetcher.h index f285715c7e11..71c336ebbb88 100644 --- a/arangod/Aql/AllRowsFetcher.h +++ b/arangod/Aql/AllRowsFetcher.h @@ -44,6 +44,7 @@ enum class ExecutionState; template class DependencyProxy; class ShadowAqlItemRow; +class SkipResult; /** * @brief Interface for all AqlExecutors that do need all @@ -110,7 +111,7 @@ class AllRowsFetcher { * size_t => Amount of documents skipped * DataRange => Resulting data */ - std::tuple execute(AqlCallStack& stack); + std::tuple execute(AqlCallStack& stack); /** * @brief Fetch one new AqlItemRow from upstream. diff --git a/arangod/Aql/AqlExecuteResult.cpp b/arangod/Aql/AqlExecuteResult.cpp index db10deeaab96..26e92a3999f9 100644 --- a/arangod/Aql/AqlExecuteResult.cpp +++ b/arangod/Aql/AqlExecuteResult.cpp @@ -48,7 +48,7 @@ auto AqlExecuteResult::state() const noexcept -> ExecutionState { return _state; } -auto AqlExecuteResult::skipped() const noexcept -> std::size_t { +auto AqlExecuteResult::skipped() const noexcept -> SkipResult { return _skipped; } @@ -75,7 +75,8 @@ void AqlExecuteResult::toVelocyPack(velocypack::Builder& builder, builder.openObject(); builder.add(StaticStrings::AqlRemoteState, stateToValue(state())); - builder.add(StaticStrings::AqlRemoteSkipped, Value(skipped())); + builder.add(Value(StaticStrings::AqlRemoteSkipped)); + skipped().toVelocyPack(builder); if (block() != nullptr) { ObjectBuilder guard(&builder, StaticStrings::AqlRemoteBlock); block()->toVelocyPack(options, builder); @@ -101,7 +102,7 @@ auto AqlExecuteResult::fromVelocyPack(velocypack::Slice const slice, expectedPropertiesFound.emplace(StaticStrings::AqlRemoteBlock, false); auto state = ExecutionState::HASMORE; - auto skipped = std::size_t{}; + auto skipped = SkipResult{}; auto block = SharedAqlItemBlockPtr{}; auto const readState = [](velocypack::Slice slice) -> ResultT { @@ -127,24 +128,6 @@ auto AqlExecuteResult::fromVelocyPack(velocypack::Slice const slice, } }; - auto const readSkipped = [](velocypack::Slice slice) -> ResultT { - if (!slice.isInteger()) { - auto message = std::string{ - "When deserializating AqlExecuteResult: When reading skipped: " - "Unexpected type "}; - message += slice.typeName(); - return Result(TRI_ERROR_TYPE_ERROR, std::move(message)); - } - try { - return slice.getNumber(); - } catch (velocypack::Exception const& ex) { - auto message = std::string{ - "When deserializating AqlExecuteResult: When reading skipped: "}; - message += ex.what(); - return Result(TRI_ERROR_TYPE_ERROR, std::move(message)); - } - }; - auto const readBlock = [&itemBlockManager](velocypack::Slice slice) -> ResultT { if (slice.isNull()) { return SharedAqlItemBlockPtr{nullptr}; @@ -179,7 +162,7 @@ auto AqlExecuteResult::fromVelocyPack(velocypack::Slice const slice, } state = maybeState.get(); } else if (key == StaticStrings::AqlRemoteSkipped) { - auto maybeSkipped = readSkipped(it.value); + auto maybeSkipped = SkipResult::fromVelocyPack(it.value); if (maybeSkipped.fail()) { return std::move(maybeSkipped).result(); } @@ -214,6 +197,6 @@ auto AqlExecuteResult::fromVelocyPack(velocypack::Slice const slice, } auto AqlExecuteResult::asTuple() const noexcept - -> std::tuple { + -> std::tuple { return {state(), skipped(), block()}; } diff --git a/arangod/Aql/AqlExecuteResult.h b/arangod/Aql/AqlExecuteResult.h index 56eb448c83db..11ef415f32cf 100644 --- a/arangod/Aql/AqlExecuteResult.h +++ b/arangod/Aql/AqlExecuteResult.h @@ -25,6 +25,7 @@ #include "Aql/ExecutionState.h" #include "Aql/SharedAqlItemBlockPtr.h" +#include "Aql/SkipResult.h" #include @@ -42,7 +43,7 @@ namespace arangodb::aql { class AqlExecuteResult { public: - AqlExecuteResult(ExecutionState state, std::size_t skipped, SharedAqlItemBlockPtr&& block) + AqlExecuteResult(ExecutionState state, SkipResult skipped, SharedAqlItemBlockPtr&& block) : _state(state), _skipped(skipped), _block(std::move(block)) {} void toVelocyPack(velocypack::Builder&, velocypack::Options const*); @@ -50,15 +51,15 @@ class AqlExecuteResult { -> ResultT; [[nodiscard]] auto state() const noexcept -> ExecutionState; - [[nodiscard]] auto skipped() const noexcept -> std::size_t; + [[nodiscard]] auto skipped() const noexcept -> SkipResult; [[nodiscard]] auto block() const noexcept -> SharedAqlItemBlockPtr const&; [[nodiscard]] auto asTuple() const noexcept - -> std::tuple; + -> std::tuple; private: ExecutionState _state = ExecutionState::HASMORE; - std::size_t _skipped = 0; + SkipResult _skipped{}; SharedAqlItemBlockPtr _block = nullptr; }; diff --git a/arangod/Aql/BlocksWithClients.cpp b/arangod/Aql/BlocksWithClients.cpp index 8df29015729f..cbbab535d475 100644 --- a/arangod/Aql/BlocksWithClients.cpp +++ b/arangod/Aql/BlocksWithClients.cpp @@ -35,6 +35,7 @@ #include "Aql/InputAqlItemRow.h" #include "Aql/Query.h" #include "Aql/ScatterExecutor.h" +#include "Aql/SkipResult.h" #include "Basics/Exceptions.h" #include "Basics/StaticStrings.h" #include "Basics/StringBuffer.h" @@ -188,7 +189,8 @@ std::pair BlocksWithClientsImpl::skipSome(size } template -std::tuple BlocksWithClientsImpl::execute(AqlCallStack stack) { +std::tuple +BlocksWithClientsImpl::execute(AqlCallStack stack) { // This will not be implemented here! TRI_ASSERT(false); THROW_ARANGO_EXCEPTION(TRI_ERROR_NOT_IMPLEMENTED); @@ -197,7 +199,7 @@ std::tuple BlocksWithClientsImpl< template auto BlocksWithClientsImpl::executeForClient(AqlCallStack stack, std::string const& clientId) - -> std::tuple { + -> std::tuple { // traceExecuteBegin(stack); auto res = executeWithoutTraceForClient(stack, clientId); // traceExecuteEnd(res); @@ -207,7 +209,7 @@ auto BlocksWithClientsImpl::executeForClient(AqlCallStack stack, template auto BlocksWithClientsImpl::executeWithoutTraceForClient(AqlCallStack stack, std::string const& clientId) - -> std::tuple { + -> std::tuple { TRI_ASSERT(!clientId.empty()); if (clientId.empty()) { // Security bailout to avoid UB @@ -234,12 +236,12 @@ auto BlocksWithClientsImpl::executeWithoutTraceForClient(AqlCallStack while (!dataContainer.hasDataFor(call)) { if (_upstreamState == ExecutionState::DONE) { // We are done, with everything, we will not be able to fetch any more rows - return {_upstreamState, 0, nullptr}; + return {_upstreamState, SkipResult{}, nullptr}; } auto state = fetchMore(stack); if (state == ExecutionState::WAITING) { - return {state, 0, nullptr}; + return {state, SkipResult{}, nullptr}; } _upstreamState = state; } @@ -265,7 +267,7 @@ auto BlocksWithClientsImpl::fetchMore(AqlCallStack stack) -> Execution // We can never ever forward skip! // We could need the row in a different block, and once skipped // we cannot get it back. - TRI_ASSERT(skipped == 0); + TRI_ASSERT(skipped.nothingSkipped()); TRI_IF_FAILURE("ExecutionBlock::getBlock") { THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); @@ -287,7 +289,7 @@ std::pair BlocksWithClientsImpl size_t atMost, std::string const& shardId) { AqlCallStack stack(AqlCall::SimulateGetSome(atMost), true); auto [state, skipped, block] = executeForClient(stack, shardId); - TRI_ASSERT(skipped == 0); + TRI_ASSERT(skipped.nothingSkipped()); return {state, block}; } @@ -299,7 +301,7 @@ std::pair BlocksWithClientsImpl::skipSomeForSh AqlCallStack stack(AqlCall::SimulateSkipSome(atMost), true); auto [state, skipped, block] = executeForClient(stack, shardId); TRI_ASSERT(block == nullptr); - return {state, skipped}; + return {state, skipped.getSkipCount()}; } template class ::arangodb::aql::BlocksWithClientsImpl; diff --git a/arangod/Aql/BlocksWithClients.h b/arangod/Aql/BlocksWithClients.h index ead4589bf64f..75e68a37e306 100644 --- a/arangod/Aql/BlocksWithClients.h +++ b/arangod/Aql/BlocksWithClients.h @@ -48,6 +48,7 @@ class AqlItemBlock; struct Collection; class ExecutionEngine; class ExecutionNode; +class SkipResult; class ClientsExecutorInfos { public: @@ -87,7 +88,7 @@ class BlocksWithClients { * @return std::tuple */ virtual auto executeForClient(AqlCallStack stack, std::string const& clientId) - -> std::tuple = 0; + -> std::tuple = 0; }; /** @@ -130,7 +131,7 @@ class BlocksWithClientsImpl : public ExecutionBlock, public BlocksWithClients { std::pair skipSome(size_t atMost) final; /// @brief execute: shouldn't be used, use executeForClient - std::tuple execute(AqlCallStack stack) override; + std::tuple execute(AqlCallStack stack) override; /** * @brief Execute for client. @@ -141,7 +142,7 @@ class BlocksWithClientsImpl : public ExecutionBlock, public BlocksWithClients { * @return std::tuple */ auto executeForClient(AqlCallStack stack, std::string const& clientId) - -> std::tuple override; + -> std::tuple override; private: /** @@ -152,7 +153,7 @@ class BlocksWithClientsImpl : public ExecutionBlock, public BlocksWithClients { * @return std::tuple */ auto executeWithoutTraceForClient(AqlCallStack stack, std::string const& clientId) - -> std::tuple; + -> std::tuple; /** * @brief Load more data from upstream and distribute it into _clientBlockData diff --git a/arangod/Aql/ConstFetcher.cpp b/arangod/Aql/ConstFetcher.cpp index 14ba394b584e..42e858d1f9e8 100644 --- a/arangod/Aql/ConstFetcher.cpp +++ b/arangod/Aql/ConstFetcher.cpp @@ -25,6 +25,7 @@ #include "Aql/AqlCallStack.h" #include "Aql/DependencyProxy.h" #include "Aql/ShadowAqlItemRow.h" +#include "Aql/SkipResult.h" #include "Basics/Exceptions.h" #include "Basics/voc-errors.h" @@ -37,7 +38,7 @@ ConstFetcher::ConstFetcher(DependencyProxy& executionBlock) : _currentBlock{nullptr}, _rowIndex(0) {} auto ConstFetcher::execute(AqlCallStack& stack) - -> std::tuple { + -> std::tuple { // Note this fetcher can only be executed on top level (it is the singleton, or test) TRI_ASSERT(stack.isRelevant()); // We only peek the call here, as we do not take over ownership. @@ -45,7 +46,7 @@ auto ConstFetcher::execute(AqlCallStack& stack) auto call = stack.peek(); if (_blockForPassThrough == nullptr) { // we are done, nothing to move arround here. - return {ExecutionState::DONE, 0, AqlItemBlockInputRange{ExecutorState::DONE}}; + return {ExecutionState::DONE, SkipResult{}, AqlItemBlockInputRange{ExecutorState::DONE}}; } std::vector> sliceIndexes; sliceIndexes.emplace_back(_rowIndex, _blockForPassThrough->size()); @@ -152,7 +153,10 @@ auto ConstFetcher::execute(AqlCallStack& stack) SharedAqlItemBlockPtr resultBlock = _blockForPassThrough; _blockForPassThrough.reset(nullptr); _rowIndex = 0; - return {ExecutionState::DONE, call.getSkipCount(), + SkipResult skipped{}; + skipped.didSkip(call.getSkipCount()); + + return {ExecutionState::DONE, skipped, DataRange{ExecutorState::DONE, call.getSkipCount(), resultBlock, 0}}; } @@ -176,7 +180,9 @@ auto ConstFetcher::execute(AqlCallStack& stack) // No data to be returned // Block is dropped. resultBlock = nullptr; - return {ExecutionState::DONE, call.getSkipCount(), + SkipResult skipped{}; + skipped.didSkip(call.getSkipCount()); + return {ExecutionState::DONE, skipped, DataRange{ExecutorState::DONE, call.getSkipCount()}}; } @@ -187,8 +193,9 @@ auto ConstFetcher::execute(AqlCallStack& stack) _blockForPassThrough == nullptr ? ExecutorState::DONE : ExecutorState::HASMORE; resultBlock = resultBlock->slice(sliceIndexes); - return {resState, call.getSkipCount(), - DataRange{rangeState, call.getSkipCount(), resultBlock, 0}}; + SkipResult skipped{}; + skipped.didSkip(call.getSkipCount()); + return {resState, skipped, DataRange{rangeState, call.getSkipCount(), resultBlock, 0}}; } void ConstFetcher::injectBlock(SharedAqlItemBlockPtr block) { diff --git a/arangod/Aql/ConstFetcher.h b/arangod/Aql/ConstFetcher.h index 70fe73fa4c12..33e0215bee70 100644 --- a/arangod/Aql/ConstFetcher.h +++ b/arangod/Aql/ConstFetcher.h @@ -37,6 +37,7 @@ class AqlItemBlock; template class DependencyProxy; class ShadowAqlItemRow; +class SkipResult; /** * @brief Interface for all AqlExecutors that do only need one @@ -71,7 +72,7 @@ class ConstFetcher { * size_t => Amount of documents skipped * DataRange => Resulting data */ - auto execute(AqlCallStack& stack) -> std::tuple; + auto execute(AqlCallStack& stack) -> std::tuple; /** * @brief Fetch one new AqlItemRow from upstream. diff --git a/arangod/Aql/DependencyProxy.cpp b/arangod/Aql/DependencyProxy.cpp index cdbc955fac6c..0c9b8535987d 100644 --- a/arangod/Aql/DependencyProxy.cpp +++ b/arangod/Aql/DependencyProxy.cpp @@ -32,10 +32,10 @@ using namespace arangodb; using namespace arangodb::aql; template -std::tuple +std::tuple DependencyProxy::execute(AqlCallStack& stack) { ExecutionState state = ExecutionState::HASMORE; - size_t skipped = 0; + SkipResult skipped; SharedAqlItemBlockPtr block = nullptr; // Note: upstreamBlock will return next dependency // if we need to loop here @@ -47,16 +47,16 @@ DependencyProxy::execute(AqlCallStack& stack) { break; } } - } while (state != ExecutionState::WAITING && skipped == 0 && block == nullptr); + } while (state != ExecutionState::WAITING && skipped.nothingSkipped() && block == nullptr); return {state, skipped, block}; } template -std::tuple DependencyProxy::executeForDependency( - size_t dependency, AqlCallStack& stack) { - // TODO: assert dependency in range +std::tuple +DependencyProxy::executeForDependency(size_t dependency, + AqlCallStack& stack) { ExecutionState state = ExecutionState::HASMORE; - size_t skipped = 0; + SkipResult skipped; SharedAqlItemBlockPtr block = nullptr; if (!_distributeId.empty()) { @@ -81,7 +81,7 @@ std::tuple DependencyProxy::prefetchBlock(size_t atMost) { AqlCallStack stack = _injectedStack; stack.pushCall(AqlCall::SimulateGetSome(atMost)); // Also temporary, will not be used here. - size_t skipped = 0; + SkipResult skipped; do { // Note: upstreamBlock will return next dependency // if we need to loop here @@ -115,7 +115,7 @@ ExecutionState DependencyProxy::prefetchBlock(size_t atMost) { } // Cannot do skipping here // Temporary! - TRI_ASSERT(skipped == 0); + TRI_ASSERT(skipped.nothingSkipped()); if (state == ExecutionState::WAITING) { TRI_ASSERT(block == nullptr); @@ -190,7 +190,7 @@ DependencyProxy::fetchBlockForDependency(size_t dependency, si AqlCallStack stack = _injectedStack; stack.pushCall(AqlCall::SimulateGetSome(atMost)); // Also temporary, will not be used here. - size_t skipped = 0; + SkipResult skipped{}; if (_distributeId.empty()) { std::tie(state, skipped, block) = upstream.execute(stack); @@ -199,7 +199,7 @@ DependencyProxy::fetchBlockForDependency(size_t dependency, si std::tie(state, skipped, block) = upstreamWithClient->executeForClient(stack, _distributeId); } - TRI_ASSERT(skipped == 0); + TRI_ASSERT(skipped.nothingSkipped()); TRI_IF_FAILURE("ExecutionBlock::getBlock") { THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); } @@ -244,7 +244,7 @@ std::pair DependencyProxy::skipSomeFor SharedAqlItemBlockPtr block; while (state == ExecutionState::HASMORE && _skipped < atMost) { - size_t skippedNow; + SkipResult skippedNow; TRI_ASSERT(_skipped <= atMost); { // Make sure we call with the correct offset @@ -255,14 +255,14 @@ std::pair DependencyProxy::skipSomeFor } std::tie(state, skippedNow, block) = upstream.execute(stack); if (state == ExecutionState::WAITING) { - TRI_ASSERT(skippedNow == 0); + TRI_ASSERT(skippedNow.nothingSkipped()); return {state, 0}; } // Temporary. // If we return a block here it will be lost TRI_ASSERT(block == nullptr); - _skipped += skippedNow; + _skipped += skippedNow.getSkipCount(); TRI_ASSERT(_skipped <= atMost); } TRI_ASSERT(state != ExecutionState::WAITING); @@ -290,7 +290,7 @@ std::pair DependencyProxy::skipSome(si SharedAqlItemBlockPtr block; while (_skipped < toSkip) { - size_t skippedNow; + SkipResult skippedNow; // Note: upstreamBlock will return next dependency // if we need to loop here TRI_ASSERT(_skipped <= toSkip); @@ -309,10 +309,10 @@ std::pair DependencyProxy::skipSome(si upstreamWithClient->executeForClient(stack, _distributeId); } - TRI_ASSERT(skippedNow <= toSkip - _skipped); + TRI_ASSERT(skippedNow.getSkipCount() <= toSkip - _skipped); if (state == ExecutionState::WAITING) { - TRI_ASSERT(skippedNow == 0); + TRI_ASSERT(skippedNow.nothingSkipped()); return {state, 0}; } @@ -320,7 +320,7 @@ std::pair DependencyProxy::skipSome(si // If we return a block here it will be lost TRI_ASSERT(block == nullptr); - _skipped += skippedNow; + _skipped += skippedNow.getSkipCount(); // When the current dependency is done, advance. if (state == ExecutionState::DONE) { diff --git a/arangod/Aql/DependencyProxy.h b/arangod/Aql/DependencyProxy.h index 108e6bad1ac9..d28dda04a312 100644 --- a/arangod/Aql/DependencyProxy.h +++ b/arangod/Aql/DependencyProxy.h @@ -37,6 +37,7 @@ namespace arangodb::aql { class ExecutionBlock; class AqlItemBlockManager; +class SkipResult; /** * @brief Thin interface to access the methods of ExecutionBlock that are @@ -74,9 +75,9 @@ class DependencyProxy { TEST_VIRTUAL ~DependencyProxy() = default; // TODO Implement and document properly! - TEST_VIRTUAL std::tuple execute(AqlCallStack& stack); + TEST_VIRTUAL std::tuple execute(AqlCallStack& stack); - TEST_VIRTUAL std::tuple executeForDependency( + TEST_VIRTUAL std::tuple executeForDependency( size_t dependency, AqlCallStack& stack); // This is only TEST_VIRTUAL, so we ignore this lint warning: diff --git a/arangod/Aql/DistributeExecutor.cpp b/arangod/Aql/DistributeExecutor.cpp index c9b13abfa452..2f9f8b163c4e 100644 --- a/arangod/Aql/DistributeExecutor.cpp +++ b/arangod/Aql/DistributeExecutor.cpp @@ -31,6 +31,7 @@ #include "Aql/Query.h" #include "Aql/RegisterPlan.h" #include "Aql/ShadowAqlItemRow.h" +#include "Aql/SkipResult.h" #include "Basics/StaticStrings.h" #include "VocBase/LogicalCollection.h" @@ -212,7 +213,7 @@ auto DistributeExecutor::ClientBlockData::popJoinedBlock() -> SharedAqlItemBlock } auto DistributeExecutor::ClientBlockData::execute(AqlCall call, ExecutionState upstreamState) - -> std::tuple { + -> std::tuple { TRI_ASSERT(_executor != nullptr); // Make sure we actually have data before you call execute TRI_ASSERT(hasDataFor(call)); diff --git a/arangod/Aql/DistributeExecutor.h b/arangod/Aql/DistributeExecutor.h index 5d0073b45a37..5744c6f42071 100644 --- a/arangod/Aql/DistributeExecutor.h +++ b/arangod/Aql/DistributeExecutor.h @@ -95,7 +95,7 @@ class DistributeExecutor { auto hasDataFor(AqlCall const& call) -> bool; auto execute(AqlCall call, ExecutionState upstreamState) - -> std::tuple; + -> std::tuple; private: /** diff --git a/arangod/Aql/ExecutionBlock.cpp b/arangod/Aql/ExecutionBlock.cpp index ec4ca75d3433..56bf44defef0 100644 --- a/arangod/Aql/ExecutionBlock.cpp +++ b/arangod/Aql/ExecutionBlock.cpp @@ -316,15 +316,15 @@ void ExecutionBlock::traceExecuteBegin(AqlCallStack const& stack) { } } -auto ExecutionBlock::traceExecuteEnd(std::tuple const& result) - -> std::tuple { +auto ExecutionBlock::traceExecuteEnd(std::tuple const& result) + -> std::tuple { if (_profile >= PROFILE_LEVEL_BLOCKS) { auto const& [state, skipped, block] = result; auto const items = block != nullptr ? block->size() : 0; ExecutionNode const* en = getPlanNode(); ExecutionStats::Node stats; stats.calls = 1; - stats.items = skipped + items; + stats.items = skipped.getSkipCount() + items; if (state != ExecutionState::WAITING) { stats.runtime = TRI_microtime() - _getSomeBegin; _getSomeBegin = 0.0; @@ -339,9 +339,9 @@ auto ExecutionBlock::traceExecuteEnd(std::tuple= PROFILE_LEVEL_TRACE_1) { ExecutionNode const* node = getPlanNode(); - LOG_QUERY("60bbc", INFO) << "execute done " << printBlockInfo() - << " state=" << stateToString(state) - << " skipped=" << skipped << " produced=" << items; + LOG_QUERY("60bbc", INFO) + << "execute done " << printBlockInfo() << " state=" << stateToString(state) + << " skipped=" << skipped.getSkipCount() << " produced=" << items; if (_profile >= PROFILE_LEVEL_TRACE_2) { if (block == nullptr) { diff --git a/arangod/Aql/ExecutionBlock.h b/arangod/Aql/ExecutionBlock.h index b3652bc5576c..c1a73acfe081 100644 --- a/arangod/Aql/ExecutionBlock.h +++ b/arangod/Aql/ExecutionBlock.h @@ -26,6 +26,7 @@ #include "Aql/BlockCollector.h" #include "Aql/ExecutionState.h" +#include "Aql/SkipResult.h" #include "Basics/Result.h" #include @@ -144,9 +145,9 @@ class ExecutionBlock { /// * WAITING: We have async operation going on, nothing happend, please call again /// * HASMORE: Here is some data in the request range, there is still more, if required call again /// * DONE: Here is some data, and there will be no further data available. - /// 2. size_t: Amount of documents skipped. + /// 2. SkipResult: Amount of documents skipped. /// 3. SharedAqlItemBlockPtr: The next data block. - virtual std::tuple execute(AqlCallStack stack) = 0; + virtual std::tuple execute(AqlCallStack stack) = 0; [[nodiscard]] bool isInSplicedSubquery() const noexcept; @@ -155,8 +156,8 @@ class ExecutionBlock { void traceExecuteBegin(AqlCallStack const& stack); // Trace the end of a execute call, potentially with result - auto traceExecuteEnd(std::tuple const& result) - -> std::tuple; + auto traceExecuteEnd(std::tuple const& result) + -> std::tuple; [[nodiscard]] auto printBlockInfo() const -> std::string const; [[nodiscard]] auto printTypeInfo() const -> std::string const; diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index a81f1c6c3d7f..233eceb5b139 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -58,6 +58,7 @@ #include "Aql/ShortestPathExecutor.h" #include "Aql/SimpleModifier.h" #include "Aql/SingleRemoteModificationExecutor.h" +#include "Aql/SkipResult.h" #include "Aql/SortExecutor.h" #include "Aql/SortRegister.h" #include "Aql/SortedCollectExecutor.h" @@ -521,10 +522,10 @@ std::pair ExecutionBlockImpl::skipSome(size_t // If we indiscriminately return ExecutionState::HASMORE, then we end up in an infinite loop // // luckily we can dispose of this kludge once executors have been ported. - if (skipped < atMost && state == ExecutionState::DONE) { - return {ExecutionState::DONE, skipped}; + if (skipped.getSkipCount() < atMost && state == ExecutionState::DONE) { + return {ExecutionState::DONE, skipped.getSkipCount()}; } else { - return {ExecutionState::HASMORE, skipped}; + return {ExecutionState::HASMORE, skipped.getSkipCount()}; } } else { traceSkipSomeBegin(atMost); @@ -616,8 +617,8 @@ std::pair ExecutionBlockImpl::initializeCursor _rowFetcher.~Fetcher(); new (&_rowFetcher) Fetcher(_dependencyProxy); - TRI_ASSERT(_skipped == 0); - _skipped = 0; + TRI_ASSERT(_skipped.nothingSkipped()); + _skipped = SkipResult{}; TRI_ASSERT(_state == InternalState::DONE || _state == InternalState::FETCH_DATA); _state = InternalState::FETCH_DATA; @@ -640,7 +641,8 @@ std::pair ExecutionBlockImpl::shutdown(int err } template -std::tuple ExecutionBlockImpl::execute(AqlCallStack stack) { +std::tuple +ExecutionBlockImpl::execute(AqlCallStack stack) { // TODO remove this IF // These are new style executors if constexpr (isNewStyleExecutor) { @@ -673,7 +675,9 @@ std::tuple ExecutionBlockImpl ExecutionBlockImplsize()); if (myCall.getLimit() == 0) { - return {ExecutionState::DONE, 0, block}; + return {ExecutionState::DONE, SkipResult{}, block}; } } - return {state, 0, block}; + return {state, SkipResult{}, block}; } else if (AqlCall::IsFullCountCall(myCall)) { auto const [state, skipped] = skipSome(ExecutionBlock::SkipAllSize()); if (state != ExecutionState::WAITING) { myCall.didSkip(skipped); } - return {state, skipped, nullptr}; + SkipResult skipRes{}; + skipRes.didSkip(skipped); + return {state, skipRes, nullptr}; } else if (AqlCall::IsFastForwardCall(myCall)) { // No idea if DONE is correct here... - return {ExecutionState::DONE, 0, nullptr}; + return {ExecutionState::DONE, SkipResult{}, nullptr}; } // Should never get here! THROW_ARANGO_EXCEPTION(TRI_ERROR_NOT_IMPLEMENTED); @@ -717,8 +723,8 @@ auto ExecutionBlockImpl>::injectConstantBlock FastForwa template auto ExecutionBlockImpl::executeFetcher(AqlCallStack& stack, size_t const dependency) - -> std::tuple { + -> std::tuple { // Silence compiler about unused dependency (void)dependency; if constexpr (isNewStyleExecutor) { @@ -1565,7 +1571,7 @@ auto ExecutionBlockImpl::executeFastForward(typename Fetcher::DataRang * SharedAqlItemBlockPtr -> The resulting data */ template -std::tuple +std::tuple ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { if constexpr (isNewStyleExecutor) { if (!stack.isRelevant()) { @@ -1573,7 +1579,7 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { // We are bypassing subqueries. // This executor is not allowed to perform actions // However we need to maintain the upstream state. - size_t skippedLocal = 0; + SkipResult skippedLocal; typename Fetcher::DataRange bypassedRange{ExecutorState::HASMORE}; std::tie(_upstreamState, skippedLocal, bypassedRange) = executeFetcher(stack, _requestedDependency); @@ -1600,7 +1606,7 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { } // Skip can only be > 0 if we are in upstream cases. - TRI_ASSERT(_skipped == 0 || _execState == ExecState::UPSTREAM); + TRI_ASSERT(_skipped.nothingSkipped() || _execState == ExecState::UPSTREAM); if constexpr (std::is_same_v) { // TODO: implement forwarding of SKIP properly: @@ -1667,7 +1673,7 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { _executor.skipRowsRange(_lastRange, clientCall); if (subqueryState == ExecutionState::WAITING) { TRI_ASSERT(skippedLocal == 0); - return {subqueryState, 0, nullptr}; + return {subqueryState, SkipResult{}, nullptr}; } else if (subqueryState == ExecutionState::DONE) { state = ExecutorState::DONE; } else { @@ -1698,7 +1704,7 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { } #endif localExecutorState = state; - _skipped += skippedLocal; + _skipped.didSkip(skippedLocal); _engine->_stats += stats; // The execute might have modified the client call. if (state == ExecutorState::DONE) { @@ -1751,7 +1757,7 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { std::tie(subqueryState, stats, call) = _executor.produceRows(_lastRange, *_outputItemRow); if (subqueryState == ExecutionState::WAITING) { - return {subqueryState, 0, nullptr}; + return {subqueryState, SkipResult{}, nullptr}; } else if (subqueryState == ExecutionState::DONE) { state = ExecutorState::DONE; } else { @@ -1799,7 +1805,7 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { executeFastForward(_lastRange, clientCall); _requestedDependency = dependency; - _skipped += skippedLocal; + _skipped.didSkip(skippedLocal); _engine->_stats += stats; localExecutorState = state; @@ -1825,7 +1831,7 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { // We need to make sure _lastRange is all used TRI_ASSERT(!lastRangeHasDataRow()); TRI_ASSERT(!_lastRange.hasShadowRow()); - size_t skippedLocal = 0; + SkipResult skippedLocal; #ifdef ARANGODB_ENABLE_MAINTAINER_MODE size_t subqueryLevelBefore = stack.subqueryLevel(); @@ -1860,7 +1866,7 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { // We might have some local accounting to this call. _clientRequest = clientCall; // We do not return anything in WAITING state, also NOT skipped. - return {_upstreamState, 0, nullptr}; + return {_upstreamState, SkipResult{}, nullptr}; } if constexpr (Executor::Properties::allowsBlockPassthrough == BlockPassthrough::Enable) { @@ -1870,7 +1876,7 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { if constexpr (skipRowsType() == SkipRowsRangeVariant::FETCHER) { _skipped += skippedLocal; // We skipped through passthrough, so count that a skip was solved. - clientCall.didSkip(skippedLocal); + clientCall.didSkip(skippedLocal.getSkipCount()); } if (_lastRange.hasShadowRow() && !_lastRange.peekShadowRow().isRelevant()) { _execState = ExecState::SHADOWROWS; @@ -1936,17 +1942,19 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { _outputItemRow.reset(); // We return skipped here, reset member - size_t skipped = _skipped; - _skipped = 0; + SkipResult skipped = _skipped; + _skipped = SkipResult{}; if (localExecutorState == ExecutorState::HASMORE || _lastRange.hasDataRow() || _lastRange.hasShadowRow()) { // We have skipped or/and return data, otherwise we cannot return HASMORE - TRI_ASSERT(skipped > 0 || (outputBlock != nullptr && outputBlock->numEntries() > 0)); + TRI_ASSERT(!skipped.nothingSkipped() || + (outputBlock != nullptr && outputBlock->numEntries() > 0)); return {ExecutionState::HASMORE, skipped, std::move(outputBlock)}; } // We must return skipped and/or data when reportingHASMORE TRI_ASSERT(_upstreamState != ExecutionState::HASMORE || - (skipped > 0 || (outputBlock != nullptr && outputBlock->numEntries() > 0))); + (!skipped.nothingSkipped() || + (outputBlock != nullptr && outputBlock->numEntries() > 0))); return {_upstreamState, skipped, std::move(outputBlock)}; } else { // TODO this branch must never be taken with an executor that has not been diff --git a/arangod/Aql/ExecutionBlockImpl.h b/arangod/Aql/ExecutionBlockImpl.h index 0d2d3da66d1e..c5033cce6f39 100644 --- a/arangod/Aql/ExecutionBlockImpl.h +++ b/arangod/Aql/ExecutionBlockImpl.h @@ -51,6 +51,7 @@ class InputAqlItemRow; class OutputAqlItemRow; class Query; class ShadowAqlItemRow; +class SkipResult; /** * @brief This is the implementation class of AqlExecutionBlocks. @@ -220,9 +221,9 @@ class ExecutionBlockImpl final : public ExecutionBlock { /// * WAITING: We have async operation going on, nothing happend, please call again /// * HASMORE: Here is some data in the request range, there is still more, if required call again /// * DONE: Here is some data, and there will be no further data available. - /// 2. size_t: Amount of documents skipped. + /// 2. SkipResult: Amount of documents skipped. /// 3. SharedAqlItemBlockPtr: The next data block. - std::tuple execute(AqlCallStack stack) override; + std::tuple execute(AqlCallStack stack) override; template >>>> [[nodiscard]] RegisterId getOutputRegisterId() const noexcept; @@ -231,9 +232,9 @@ class ExecutionBlockImpl final : public ExecutionBlock { /** * @brief Inner execute() part, without the tracing calls. */ - std::tuple executeWithoutTrace(AqlCallStack stack); + std::tuple executeWithoutTrace(AqlCallStack stack); - std::tuple executeFetcher( + std::tuple executeFetcher( AqlCallStack& stack, size_t const dependency); std::tuple executeProduceRows( @@ -335,7 +336,7 @@ class ExecutionBlockImpl final : public ExecutionBlock { InternalState _state; - size_t _skipped{}; + SkipResult _skipped{}; DataRange _lastRange; diff --git a/arangod/Aql/ExecutionEngine.cpp b/arangod/Aql/ExecutionEngine.cpp index 2fa51ea000df..a4e30799654e 100644 --- a/arangod/Aql/ExecutionEngine.cpp +++ b/arangod/Aql/ExecutionEngine.cpp @@ -39,6 +39,7 @@ #include "Aql/QueryRegistry.h" #include "Aql/RemoteExecutor.h" #include "Aql/ReturnExecutor.h" +#include "Aql/SkipResult.h" #include "Aql/WalkerWorker.h" #include "Basics/ScopeGuard.h" #include "Cluster/ServerState.h" @@ -564,16 +565,16 @@ std::pair ExecutionEngine::initializeCursor(SharedAqlIte } auto ExecutionEngine::execute(AqlCallStack const& stack) - -> std::tuple { + -> std::tuple { if (_query.killed()) { THROW_ARANGO_EXCEPTION(TRI_ERROR_QUERY_KILLED); } auto const res = _root->execute(stack); #ifdef ARANGODB_ENABLE_MAINTAINER_MODE if (std::get(res) == ExecutionState::WAITING) { - auto const skipped = std::get(res); + auto const skipped = std::get(res); auto const block = std::get(res); - TRI_ASSERT(skipped == 0); + TRI_ASSERT(skipped.nothingSkipped()); TRI_ASSERT(block == nullptr); } #endif @@ -581,7 +582,7 @@ auto ExecutionEngine::execute(AqlCallStack const& stack) } auto ExecutionEngine::executeForClient(AqlCallStack const& stack, std::string const& clientId) - -> std::tuple { + -> std::tuple { if (_query.killed()) { THROW_ARANGO_EXCEPTION(TRI_ERROR_QUERY_KILLED); } @@ -597,9 +598,9 @@ auto ExecutionEngine::executeForClient(AqlCallStack const& stack, std::string co auto const res = rootBlock->executeForClient(stack, clientId); #ifdef ARANGODB_ENABLE_MAINTAINER_MODE if (std::get(res) == ExecutionState::WAITING) { - auto const skipped = std::get(res); + auto const skipped = std::get(res); auto const& block = std::get(res); - TRI_ASSERT(skipped == 0); + TRI_ASSERT(skipped.nothingSkipped()); TRI_ASSERT(block == nullptr); } #endif @@ -621,7 +622,7 @@ std::pair ExecutionEngine::getSome(size_t AqlCallStack compatibilityStack{AqlCall::SimulateGetSome(atMost), true}; auto const [state, skipped, block] = _root->execute(std::move(compatibilityStack)); // We cannot trigger a skip operation from here - TRI_ASSERT(skipped == 0); + TRI_ASSERT(skipped.nothingSkipped()); return {state, block}; } @@ -643,7 +644,7 @@ std::pair ExecutionEngine::skipSome(size_t atMost) { // We cannot be triggered within a subquery from earlier versions. // Also we cannot produce anything ourselfes here. TRI_ASSERT(block == nullptr); - return {state, skipped}; + return {state, skipped.getSkipCount()}; } Result ExecutionEngine::shutdownSync(int errorCode) noexcept try { diff --git a/arangod/Aql/ExecutionEngine.h b/arangod/Aql/ExecutionEngine.h index f9af9ff3d2c2..2b51c217ff6f 100644 --- a/arangod/Aql/ExecutionEngine.h +++ b/arangod/Aql/ExecutionEngine.h @@ -47,6 +47,7 @@ class ExecutionNode; class ExecutionPlan; class QueryRegistry; class Query; +class SkipResult; enum class SerializationFormat; class ExecutionEngine { @@ -98,10 +99,10 @@ class ExecutionEngine { std::pair shutdown(int errorCode); auto execute(AqlCallStack const& stack) - -> std::tuple; + -> std::tuple; auto executeForClient(AqlCallStack const& stack, std::string const& clientId) - -> std::tuple; + -> std::tuple; /// @brief getSome std::pair getSome(size_t atMost); diff --git a/arangod/Aql/MultiDependencySingleRowFetcher.cpp b/arangod/Aql/MultiDependencySingleRowFetcher.cpp index f2abc6a44528..c3eaa3095251 100644 --- a/arangod/Aql/MultiDependencySingleRowFetcher.cpp +++ b/arangod/Aql/MultiDependencySingleRowFetcher.cpp @@ -367,11 +367,11 @@ auto MultiDependencySingleRowFetcher::useStack(AqlCallStack const& stack) -> voi auto MultiDependencySingleRowFetcher::executeForDependency(size_t const dependency, AqlCallStack& stack) - -> std::tuple { + -> std::tuple { auto [state, skipped, block] = _dependencyProxy->executeForDependency(dependency, stack); if (state == ExecutionState::WAITING) { - return {state, 0, AqlItemBlockInputRange{ExecutorState::HASMORE}}; + return {state, SkipResult{}, AqlItemBlockInputRange{ExecutorState::HASMORE}}; } ExecutorState execState = state == ExecutionState::DONE ? ExecutorState::DONE : ExecutorState::HASMORE; @@ -386,9 +386,10 @@ auto MultiDependencySingleRowFetcher::executeForDependency(size_t const dependen state = ExecutionState::DONE; } if (block == nullptr) { - return {state, skipped, AqlItemBlockInputRange{execState, skipped}}; + return {state, skipped, AqlItemBlockInputRange{execState, skipped.getSkipCount()}}; } TRI_ASSERT(block != nullptr); auto [start, end] = block->getRelevantRange(); - return {state, skipped, AqlItemBlockInputRange{execState, skipped, block, start}}; + return {state, skipped, + AqlItemBlockInputRange{execState, skipped.getSkipCount(), block, start}}; } diff --git a/arangod/Aql/MultiDependencySingleRowFetcher.h b/arangod/Aql/MultiDependencySingleRowFetcher.h index a2c2ebdd911e..1e12f66b7e27 100644 --- a/arangod/Aql/MultiDependencySingleRowFetcher.h +++ b/arangod/Aql/MultiDependencySingleRowFetcher.h @@ -39,6 +39,7 @@ class AqlItemBlock; template class DependencyProxy; class ShadowAqlItemRow; +class SkipResult; /** * @brief Interface for all AqlExecutors that do need one @@ -135,7 +136,7 @@ class MultiDependencySingleRowFetcher { auto useStack(AqlCallStack const& stack) -> void; auto executeForDependency(size_t const dependency, AqlCallStack& stack) - -> std::tuple; + -> std::tuple; private: DependencyProxy* _dependencyProxy; diff --git a/arangod/Aql/RemoteExecutor.cpp b/arangod/Aql/RemoteExecutor.cpp index c231fc4cb91e..f2f4afebd700 100644 --- a/arangod/Aql/RemoteExecutor.cpp +++ b/arangod/Aql/RemoteExecutor.cpp @@ -31,6 +31,7 @@ #include "Aql/InputAqlItemRow.h" #include "Aql/Query.h" #include "Aql/RestAqlHandler.h" +#include "Aql/SkipResult.h" #include "Basics/MutexLocker.h" #include "Basics/StringBuffer.h" #include "Basics/VelocyPackHelper.h" @@ -153,8 +154,7 @@ std::pair ExecutionBlockImpl ExecutionBlockImpl::shutdown(i } auto ExecutionBlockImpl::executeViaOldApi(AqlCallStack stack) - -> std::tuple { + -> std::tuple { // Use the old getSome/SkipSome API. auto myCall = stack.popCall(); @@ -444,7 +444,9 @@ auto ExecutionBlockImpl::executeViaOldApi(AqlCallStack stack) if (state != ExecutionState::WAITING) { myCall.didSkip(skipped); } - return {state, skipped, nullptr}; + SkipResult skipRes{}; + skipRes.didSkip(skipped); + return {state, skipRes, nullptr}; } else if (AqlCall::IsGetSomeCall(myCall)) { auto const [state, block] = getSomeWithoutTrace(myCall.getLimit()); // We do not need to count as softLimit will be overwritten, and hard cannot be set. @@ -452,20 +454,22 @@ auto ExecutionBlockImpl::executeViaOldApi(AqlCallStack stack) // However we can do a short-cut here to report DONE on hardLimit if we are on the top-level query. myCall.didProduce(block->size()); if (myCall.getLimit() == 0) { - return {ExecutionState::DONE, 0, block}; + return {ExecutionState::DONE, SkipResult{}, block}; } } - return {state, 0, block}; + return {state, SkipResult{}, block}; } else if (AqlCall::IsFullCountCall(myCall)) { auto const [state, skipped] = skipSome(ExecutionBlock::SkipAllSize()); if (state != ExecutionState::WAITING) { myCall.didSkip(skipped); } - return {state, skipped, nullptr}; + SkipResult skipRes{}; + skipRes.didSkip(skipped); + return {state, skipRes, nullptr}; } else if (AqlCall::IsFastForwardCall(myCall)) { // No idea if DONE is correct here... - return {ExecutionState::DONE, 0, nullptr}; + return {ExecutionState::DONE, SkipResult{}, nullptr}; } // Should never get here! @@ -473,14 +477,14 @@ auto ExecutionBlockImpl::executeViaOldApi(AqlCallStack stack) } auto ExecutionBlockImpl::execute(AqlCallStack stack) - -> std::tuple { + -> std::tuple { traceExecuteBegin(stack); auto res = executeWithoutTrace(stack); return traceExecuteEnd(res); } auto ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) --> std::tuple { + -> std::tuple { if (ADB_UNLIKELY(api() == Api::GET_SOME)) { return executeViaOldApi(stack); } @@ -489,7 +493,7 @@ auto ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) } auto ExecutionBlockImpl::executeViaNewApi(AqlCallStack callStack) - -> std::tuple { + -> std::tuple { // silence tests -- we need to introduce new failure tests for fetchers TRI_IF_FAILURE("ExecutionBlock::getOrSkipSome1") { THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); @@ -509,7 +513,7 @@ auto ExecutionBlockImpl::executeViaNewApi(AqlCallStack callStack if (_requestInFlight) { // Already sent a shutdown request, but haven't got an answer yet. - return {ExecutionState::WAITING, 0, nullptr}; + return {ExecutionState::WAITING, SkipResult{}, nullptr}; } // For every call we simply forward via HTTP @@ -553,7 +557,7 @@ auto ExecutionBlockImpl::executeViaNewApi(AqlCallStack callStack THROW_ARANGO_EXCEPTION(res); } - return {ExecutionState::WAITING, 0, nullptr}; + return {ExecutionState::WAITING, SkipResult{}, nullptr}; } auto ExecutionBlockImpl::deserializeExecuteCallResultBody(VPackSlice const slice) const @@ -564,14 +568,16 @@ auto ExecutionBlockImpl::deserializeExecuteCallResultBody(VPackS if (ADB_UNLIKELY(!slice.isObject())) { using namespace std::string_literals; - return Result{TRI_ERROR_TYPE_ERROR, "When parsing execute result: expected object, got "s + slice.typeName()}; + return Result{TRI_ERROR_TYPE_ERROR, + "When parsing execute result: expected object, got "s + slice.typeName()}; } if (auto value = slice.get(StaticStrings::AqlRemoteResult); !value.isNone()) { return AqlExecuteResult::fromVelocyPack(value, _engine->itemBlockManager()); } - return Result{TRI_ERROR_TYPE_ERROR, "When parsing execute result: field result missing"}; + return Result{TRI_ERROR_TYPE_ERROR, + "When parsing execute result: field result missing"}; } auto ExecutionBlockImpl::serializeExecuteCallBody(AqlCallStack const& callStack) const @@ -661,10 +667,10 @@ Result ExecutionBlockImpl::sendAsyncRequest(fuerte::RestVerb typ req->header.addMeta("x-shard-id", _ownName); req->header.addMeta("shard-id", _ownName); // deprecated in 3.7, remove later } - + LOG_TOPIC("2713c", DEBUG, Logger::COMMUNICATION) - << "request to '" << _server - << "' '" << fuerte::to_string(type) << " " << req->header.path << "'"; + << "request to '" << _server << "' '" << fuerte::to_string(type) << " " + << req->header.path << "'"; network::ConnectionPtr conn = pool->leaseConnection(spec.endpoint); diff --git a/arangod/Aql/RemoteExecutor.h b/arangod/Aql/RemoteExecutor.h index 49244489cf00..cd8da9a7afbb 100644 --- a/arangod/Aql/RemoteExecutor.h +++ b/arangod/Aql/RemoteExecutor.h @@ -32,12 +32,16 @@ #include -namespace arangodb::fuerte { inline namespace v1 { +namespace arangodb::fuerte { +inline namespace v1 { enum class RestVerb; -}} +} +} // namespace arangodb::fuerte namespace arangodb::aql { +class SkipResult; + // The RemoteBlock is actually implemented by specializing ExecutionBlockImpl, // so this class only exists to identify the specialization. class RemoteExecutor final {}; @@ -67,7 +71,7 @@ class ExecutionBlockImpl : public ExecutionBlock { std::pair shutdown(int errorCode) override; - std::tuple execute(AqlCallStack stack) override; + std::tuple execute(AqlCallStack stack) override; [[nodiscard]] auto api() const noexcept -> Api; @@ -85,13 +89,13 @@ class ExecutionBlockImpl : public ExecutionBlock { std::pair skipSomeWithoutTrace(size_t atMost); auto executeWithoutTrace(AqlCallStack stack) - -> std::tuple; + -> std::tuple; auto executeViaOldApi(AqlCallStack stack) - -> std::tuple; + -> std::tuple; auto executeViaNewApi(AqlCallStack stack) - -> std::tuple; + -> std::tuple; [[nodiscard]] auto deserializeExecuteCallResultBody(velocypack::Slice) const -> ResultT; @@ -166,6 +170,6 @@ class ExecutionBlockImpl : public ExecutionBlock { Api _apiToUse = Api::EXECUTE; }; -} // namespace arangodb +} // namespace arangodb::aql #endif // ARANGOD_AQL_REMOTE_EXECUTOR_H diff --git a/arangod/Aql/RestAqlHandler.cpp b/arangod/Aql/RestAqlHandler.cpp index fa57a645b8b2..725eac165320 100644 --- a/arangod/Aql/RestAqlHandler.cpp +++ b/arangod/Aql/RestAqlHandler.cpp @@ -740,7 +740,7 @@ RestStatus RestAqlHandler::handleUseQuery(std::string const& operation, auto& executeCall = maybeExecuteCall.get(); auto items = SharedAqlItemBlockPtr{}; - auto skipped = size_t{}; + auto skipped = SkipResult{}; auto state = ExecutionState::HASMORE; // shardId is set IFF the root node is scatter or distribute diff --git a/arangod/Aql/ScatterExecutor.cpp b/arangod/Aql/ScatterExecutor.cpp index bdf56efdb391..a9edc4a2e55f 100644 --- a/arangod/Aql/ScatterExecutor.cpp +++ b/arangod/Aql/ScatterExecutor.cpp @@ -83,7 +83,7 @@ auto ScatterExecutor::ClientBlockData::hasDataFor(AqlCall const& call) -> bool { } auto ScatterExecutor::ClientBlockData::execute(AqlCall call, ExecutionState upstreamState) - -> std::tuple { + -> std::tuple { TRI_ASSERT(_executor != nullptr); // Make sure we actually have data before you call execute TRI_ASSERT(hasDataFor(call)); diff --git a/arangod/Aql/ScatterExecutor.h b/arangod/Aql/ScatterExecutor.h index 53bd1bb1b5c5..2b1b850c1e60 100644 --- a/arangod/Aql/ScatterExecutor.h +++ b/arangod/Aql/ScatterExecutor.h @@ -31,6 +31,7 @@ namespace arangodb { namespace aql { +class SkipResult; class ExecutionEngine; class ScatterNode; @@ -60,7 +61,7 @@ class ScatterExecutor { auto hasDataFor(AqlCall const& call) -> bool; auto execute(AqlCall call, ExecutionState upstreamState) - -> std::tuple; + -> std::tuple; private: std::deque _queue; diff --git a/arangod/Aql/SingleRowFetcher.cpp b/arangod/Aql/SingleRowFetcher.cpp index 62e054c5b875..44acf7bdc00a 100644 --- a/arangod/Aql/SingleRowFetcher.cpp +++ b/arangod/Aql/SingleRowFetcher.cpp @@ -30,6 +30,7 @@ #include "Aql/ExecutionBlock.h" #include "Aql/ExecutionState.h" #include "Aql/InputAqlItemRow.h" +#include "Aql/SkipResult.h" using namespace arangodb; using namespace arangodb::aql; @@ -77,28 +78,31 @@ SingleRowFetcher::fetchBlockForPassthrough(size_t atMost) { } template -std::tuple +std::tuple SingleRowFetcher::execute(AqlCallStack& stack) { auto [state, skipped, block] = _dependencyProxy->execute(stack); if (state == ExecutionState::WAITING) { // On waiting we have nothing to return - return {state, 0, AqlItemBlockInputRange{ExecutorState::HASMORE}}; + return {state, SkipResult{}, AqlItemBlockInputRange{ExecutorState::HASMORE}}; } if (block == nullptr) { if (state == ExecutionState::HASMORE) { - return {state, skipped, AqlItemBlockInputRange{ExecutorState::HASMORE, skipped}}; + return {state, skipped, + AqlItemBlockInputRange{ExecutorState::HASMORE, skipped.getSkipCount()}}; } - return {state, skipped, AqlItemBlockInputRange{ExecutorState::DONE, skipped}}; + return {state, skipped, + AqlItemBlockInputRange{ExecutorState::DONE, skipped.getSkipCount()}}; } auto [start, end] = block->getRelevantRange(); if (state == ExecutionState::HASMORE) { TRI_ASSERT(block != nullptr); return {state, skipped, - AqlItemBlockInputRange{ExecutorState::HASMORE, skipped, block, start}}; + AqlItemBlockInputRange{ExecutorState::HASMORE, + skipped.getSkipCount(), block, start}}; } return {state, skipped, - AqlItemBlockInputRange{ExecutorState::DONE, skipped, block, start}}; + AqlItemBlockInputRange{ExecutorState::DONE, skipped.getSkipCount(), block, start}}; } template diff --git a/arangod/Aql/SingleRowFetcher.h b/arangod/Aql/SingleRowFetcher.h index e33717eff395..f33447576c01 100644 --- a/arangod/Aql/SingleRowFetcher.h +++ b/arangod/Aql/SingleRowFetcher.h @@ -40,6 +40,7 @@ namespace arangodb::aql { class AqlItemBlock; template class DependencyProxy; +class SkipResult; /** * @brief Interface for all AqlExecutors that do only need one @@ -74,7 +75,7 @@ class SingleRowFetcher { * size_t => Amount of documents skipped * DataRange => Resulting data */ - std::tuple execute(AqlCallStack& stack); + std::tuple execute(AqlCallStack& stack); /** * @brief Fetch one new AqlItemRow from upstream. diff --git a/arangod/Aql/SkipResult.cpp b/arangod/Aql/SkipResult.cpp index 66d13bd466b6..53171e5eed0c 100644 --- a/arangod/Aql/SkipResult.cpp +++ b/arangod/Aql/SkipResult.cpp @@ -23,6 +23,11 @@ #include "SkipResult.h" +#include "Cluster/ResultT.h" + +#include +#include + using namespace arangodb::aql; SkipResult::SkipResult() {} @@ -31,4 +36,37 @@ SkipResult::SkipResult(SkipResult const& other) : _skipped{other._skipped} {} auto SkipResult::getSkipCount() const noexcept -> size_t { return _skipped; } -auto SkipResult::didSkip(size_t skipped) -> void { _skipped += skipped; } \ No newline at end of file +auto SkipResult::didSkip(size_t skipped) -> void { _skipped += skipped; } + +auto SkipResult::nothingSkipped() const noexcept -> bool { + return _skipped == 0; +} + +auto SkipResult::toVelocyPack(VPackBuilder& builder) const noexcept -> void { + builder.add(VPackValue(_skipped)); +} + +auto SkipResult::fromVelocyPack(VPackSlice slice) -> ResultT { + if (!slice.isInteger()) { + auto message = std::string{ + "When deserializating AqlExecuteResult: When reading skipped: " + "Unexpected type "}; + message += slice.typeName(); + return Result(TRI_ERROR_TYPE_ERROR, std::move(message)); + } + try { + SkipResult res; + res.didSkip(slice.getNumber()); + return res; + } catch (velocypack::Exception const& ex) { + auto message = std::string{ + "When deserializating AqlExecuteResult: When reading skipped: "}; + message += ex.what(); + return Result(TRI_ERROR_TYPE_ERROR, std::move(message)); + } +} + +auto arangodb::aql::operator+=(SkipResult& a, SkipResult const& b) noexcept -> SkipResult& { + a.didSkip(b.getSkipCount()); + return a; +} diff --git a/arangod/Aql/SkipResult.h b/arangod/Aql/SkipResult.h index 6eaefe651ec9..3bd3c936cd55 100644 --- a/arangod/Aql/SkipResult.h +++ b/arangod/Aql/SkipResult.h @@ -26,10 +26,21 @@ // for size_t #include +namespace arangodb { +template +class ResultT; +} +namespace arangodb::velocypack { +class Builder; +class Slice; +} // namespace arangodb::velocypack + namespace arangodb::aql { class SkipResult { public: + static auto fromVelocyPack(velocypack::Slice) -> ResultT; + SkipResult(); ~SkipResult() = default; @@ -40,8 +51,15 @@ class SkipResult { auto didSkip(size_t skipped) -> void; + auto nothingSkipped() const noexcept -> bool; + + auto toVelocyPack(arangodb::velocypack::Builder& builder) const noexcept -> void; + private: size_t _skipped{0}; }; + +auto operator+=(SkipResult& a, SkipResult const& b) noexcept -> SkipResult&; + } // namespace arangodb::aql #endif \ No newline at end of file diff --git a/arangod/Aql/SubqueryExecutor.cpp b/arangod/Aql/SubqueryExecutor.cpp index c84b47f0346f..6520cae4c352 100644 --- a/arangod/Aql/SubqueryExecutor.cpp +++ b/arangod/Aql/SubqueryExecutor.cpp @@ -190,7 +190,7 @@ auto SubqueryExecutor::produceRows(AqlItemBlockInputRang // Non const case, or first run in const auto [state, skipped, block] = _subquery.execute(AqlCallStack(AqlCall{})); - TRI_ASSERT(skipped == 0); + TRI_ASSERT(skipped.nothingSkipped()); if (state == ExecutionState::WAITING) { return {state, NoStats{}, getUpstreamCall()}; } @@ -351,8 +351,8 @@ auto SubqueryExecutor::skipRowsRange<>(AqlItemBlockInputRange& inputRange, } // Non const case, or first run in const - auto [state, skipped, block] = _subquery.execute(AqlCallStack(AqlCall{})); - TRI_ASSERT(skipped == 0); + auto [state, skipRes, block] = _subquery.execute(AqlCallStack(AqlCall{})); + TRI_ASSERT(skipRes.nothingSkipped()); if (state == ExecutionState::WAITING) { return {state, NoStats{}, 0, getUpstreamCall()}; } diff --git a/tests/Aql/SkipResultTest.cpp b/tests/Aql/SkipResultTest.cpp index 7bf8ac415001..f129075a9f38 100644 --- a/tests/Aql/SkipResultTest.cpp +++ b/tests/Aql/SkipResultTest.cpp @@ -24,6 +24,9 @@ #include "Aql/SkipResult.h" +#include +#include + using namespace arangodb; using namespace arangodb::aql; @@ -65,6 +68,44 @@ TEST_F(SkipResultTest, is_copyable) { original.didSkip(7); EXPECT_NE(testee.getSkipCount(), original.getSkipCount()); } + +TEST_F(SkipResultTest, can_report_if_we_skip) { + SkipResult testee{}; + EXPECT_TRUE(testee.nothingSkipped()); + testee.didSkip(3); + EXPECT_FALSE(testee.nothingSkipped()); + testee.didSkip(6); + EXPECT_FALSE(testee.nothingSkipped()); +} + +TEST_F(SkipResultTest, serialize_deserialize_empty) { + SkipResult original{}; + VPackBuilder builder; + original.toVelocyPack(builder); + auto testee = SkipResult::fromVelocyPack(builder.slice()); + EXPECT_EQ(testee.nothingSkipped(), original.nothingSkipped()); + EXPECT_EQ(testee.getSkipCount(), original.getSkipCount()); +} + +TEST_F(SkipResultTest, serialize_deserialize_with_count) { + SkipResult original{}; + original.didSkip(6); + VPackBuilder builder; + original.toVelocyPack(builder); + auto testee = SkipResult::fromVelocyPack(builder.slice()); + EXPECT_EQ(testee.nothingSkipped(), original.nothingSkipped()); + EXPECT_EQ(testee.getSkipCount(), original.getSkipCount()); +} + +TEST_F(SkipResult, can_be_added) { + SkipResult a{}; + a.didSkip(6); + SkipResult b{}; + b.didSkip(7); + a += b; + EXPECT_EQ(a.getSkipCount(), 13); +} + } // namespace aql } // namespace tests } // namespace arangodb From 250fab79a2c1f2b4db645b1c95f27b1c30ce3596 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sun, 1 Mar 2020 20:27:13 +0100 Subject: [PATCH 28/71] Made tests compile with new SkipResult --- arangod/Aql/SkipResult.cpp | 4 + arangod/Aql/SkipResult.h | 2 + tests/Aql/DependencyProxyMock.cpp | 7 +- tests/Aql/DependencyProxyMock.h | 8 +- tests/Aql/ExecutionBlockImplTest.cpp | 88 ++++++++++--------- tests/Aql/ExecutionBlockImplTestInstances.cpp | 4 +- tests/Aql/ExecutorTestHelper.h | 6 +- tests/Aql/HashedCollectExecutorTest.cpp | 4 +- tests/Aql/IdExecutorTest.cpp | 40 ++++----- tests/Aql/RemoteExecutorTest.cpp | 54 ++++++------ tests/Aql/ScatterExecutorTest.cpp | 46 +++++----- tests/Aql/SingleRowFetcherTest.cpp | 4 +- tests/Aql/SkipResultTest.cpp | 11 ++- tests/Aql/WaitingExecutionBlockMock.cpp | 27 ++++-- tests/Aql/WaitingExecutionBlockMock.h | 7 +- 15 files changed, 172 insertions(+), 140 deletions(-) diff --git a/arangod/Aql/SkipResult.cpp b/arangod/Aql/SkipResult.cpp index 53171e5eed0c..c9156a9e0e91 100644 --- a/arangod/Aql/SkipResult.cpp +++ b/arangod/Aql/SkipResult.cpp @@ -70,3 +70,7 @@ auto arangodb::aql::operator+=(SkipResult& a, SkipResult const& b) noexcept -> S a.didSkip(b.getSkipCount()); return a; } + +auto arangodb::aql::operator==(SkipResult const& a, SkipResult const& b) noexcept -> bool { + return a.getSkipCount() == b.getSkipCount(); +} \ No newline at end of file diff --git a/arangod/Aql/SkipResult.h b/arangod/Aql/SkipResult.h index 3bd3c936cd55..e05e8a8a58c7 100644 --- a/arangod/Aql/SkipResult.h +++ b/arangod/Aql/SkipResult.h @@ -61,5 +61,7 @@ class SkipResult { auto operator+=(SkipResult& a, SkipResult const& b) noexcept -> SkipResult&; +auto operator==(SkipResult const& a, SkipResult const& b) noexcept -> bool; + } // namespace arangodb::aql #endif \ No newline at end of file diff --git a/tests/Aql/DependencyProxyMock.cpp b/tests/Aql/DependencyProxyMock.cpp index 620038a51a0c..05a4dc621b0c 100644 --- a/tests/Aql/DependencyProxyMock.cpp +++ b/tests/Aql/DependencyProxyMock.cpp @@ -25,6 +25,8 @@ #include "gtest/gtest.h" +#include "Aql/SkipResult.h" + #include namespace arangodb::tests::aql { @@ -130,10 +132,11 @@ DependencyProxyMock& DependencyProxyMock:: } template -std::tuple +std::tuple DependencyProxyMock::execute(AqlCallStack& stack) { TRI_ASSERT(_block != nullptr); - return {arangodb::aql::ExecutionState::DONE, 0, _block}; + SkipResult res{}; + return {arangodb::aql::ExecutionState::DONE, res, _block}; } template diff --git a/tests/Aql/DependencyProxyMock.h b/tests/Aql/DependencyProxyMock.h index 2d7c6d8b89af..5d873b84ea47 100644 --- a/tests/Aql/DependencyProxyMock.h +++ b/tests/Aql/DependencyProxyMock.h @@ -33,6 +33,9 @@ #include namespace arangodb { +namespace aql { +class SkipResult; +} namespace tests { namespace aql { @@ -51,7 +54,7 @@ class DependencyProxyMock : public ::arangodb::aql::DependencyProxy skipSome(size_t atMost) override; - std::tuple execute( + std::tuple execute( arangodb::aql::AqlCallStack& stack) override; private: @@ -101,8 +104,7 @@ class MultiDependencyProxyMock // NOLINTNEXTLINE google-default-arguments std::pair fetchBlockForDependency( - size_t dependency, - size_t atMost = arangodb::aql::ExecutionBlock::DefaultBatchSize) override; + size_t dependency, size_t atMost = arangodb::aql::ExecutionBlock::DefaultBatchSize) override; std::pair skipSomeForDependency(size_t dependency, size_t atMost) override; diff --git a/tests/Aql/ExecutionBlockImplTest.cpp b/tests/Aql/ExecutionBlockImplTest.cpp index ea6133c55117..11e7adae37a2 100644 --- a/tests/Aql/ExecutionBlockImplTest.cpp +++ b/tests/Aql/ExecutionBlockImplTest.cpp @@ -58,7 +58,7 @@ using LambdaExe = TestLambdaSkipExecutor; // This test is supposed to only test getSome return values, // it is not supposed to test the fetch logic! - +#if 0 class ExecutionBlockImplTest : public ::testing::Test { protected: // ExecutionState state @@ -400,6 +400,7 @@ TEST_F(ExecutionBlockImplTest, ASSERT_EQ(state, ExecutionState::DONE); ASSERT_EQ(block, nullptr); } +#endif /** * @brief Shared Test case initializer to test the execute API @@ -691,7 +692,7 @@ class ExecutionBlockImplExecuteSpecificTest : public SharedExecutionBlockImplTes * @return std::tuple Response of execute(call); */ auto runTest(ProduceCall& prod, SkipCall& skip, AqlCall call) - -> std::tuple { + -> std::tuple { AqlCallStack stack{std::move(call)}; auto singleton = createSingleton(); if (GetParam()) { @@ -775,7 +776,7 @@ TEST_P(ExecutionBlockImplExecuteSpecificTest, test_toplevel_unlimited_call) { auto [state, skipped, block] = runTest(execImpl, skipCall, fullCall); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); // Once with empty, once with the line by Singleton EXPECT_EQ(nrCalls, 2); @@ -797,7 +798,7 @@ TEST_P(ExecutionBlockImplExecuteSpecificTest, test_toplevel_softlimit_call) { auto [state, skipped, block] = runTest(execImpl, skipCall, fullCall); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); // Once with empty, once with the line by Singleton EXPECT_EQ(nrCalls, 2); @@ -819,7 +820,7 @@ TEST_P(ExecutionBlockImplExecuteSpecificTest, test_toplevel_hardlimit_call) { auto [state, skipped, block] = runTest(execImpl, skipCall, fullCall); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); // Once with empty, once with the line by Singleton EXPECT_EQ(nrCalls, 2); @@ -838,7 +839,7 @@ TEST_P(ExecutionBlockImplExecuteSpecificTest, test_toplevel_offset_call) { auto [state, skipped, block] = runTest(execImpl, skipCall, fullCall); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 1); + EXPECT_EQ(skipped.getSkipCount(), 1); if (GetParam()) { // Do never call skip, pass through EXPECT_EQ(nrCalls, 0); @@ -866,7 +867,7 @@ TEST_P(ExecutionBlockImplExecuteSpecificTest, test_toplevel_offset_only_call) { auto [state, skipped, block] = runTest(execImpl, skipCall, fullCall); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 1); + EXPECT_EQ(skipped.getSkipCount(), 1); if (GetParam()) { // Do never call skip, pass through EXPECT_EQ(nrCalls, 0); @@ -902,7 +903,7 @@ TEST_P(ExecutionBlockImplExecuteSpecificTest, test_relevant_shadowrow_does_not_f // First call. Fetch all rows (data only) auto const& [state, skipped, block] = testee->execute(stack); EXPECT_EQ(state, ExecutionState::HASMORE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ASSERT_NE(block, nullptr); EXPECT_EQ(block->size(), ExecutionBlock::DefaultBatchSize); EXPECT_FALSE(block->hasShadowRows()); @@ -911,7 +912,7 @@ TEST_P(ExecutionBlockImplExecuteSpecificTest, test_relevant_shadowrow_does_not_f // Second call. only a single shadowRow left auto const& [state, skipped, block] = testee->execute(stack); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ASSERT_NE(block, nullptr); EXPECT_EQ(block->size(), 1); EXPECT_TRUE(block->hasShadowRows()); @@ -945,7 +946,7 @@ TEST_P(ExecutionBlockImplExecuteSpecificTest, set_of_shadowrows_does_not_fit_in_ // First call. Fetch all rows (data only) auto const& [state, skipped, block] = testee->execute(stack); EXPECT_EQ(state, ExecutionState::HASMORE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ASSERT_NE(block, nullptr); EXPECT_EQ(block->size(), ExecutionBlock::DefaultBatchSize); EXPECT_FALSE(block->hasShadowRows()); @@ -954,7 +955,7 @@ TEST_P(ExecutionBlockImplExecuteSpecificTest, set_of_shadowrows_does_not_fit_in_ // Second call. only the shadowRows are left auto const& [state, skipped, block] = testee->execute(stack); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ASSERT_NE(block, nullptr); ASSERT_EQ(block->size(), 2); EXPECT_TRUE(block->hasShadowRows()); @@ -995,7 +996,7 @@ TEST_P(ExecutionBlockImplExecuteSpecificTest, set_of_shadowrows_does_not_fit_ful // First call. Fetch all rows (data + relevant shadow row) auto const& [state, skipped, block] = testee->execute(stack); EXPECT_EQ(state, ExecutionState::HASMORE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ASSERT_NE(block, nullptr); EXPECT_EQ(block->size(), ExecutionBlock::DefaultBatchSize); EXPECT_TRUE(block->hasShadowRows()); @@ -1007,7 +1008,7 @@ TEST_P(ExecutionBlockImplExecuteSpecificTest, set_of_shadowrows_does_not_fit_ful // Second call. only the shadowRows are left auto const& [state, skipped, block] = testee->execute(stack); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ASSERT_NE(block, nullptr); EXPECT_EQ(block->size(), 1); EXPECT_TRUE(block->hasShadowRows()); @@ -1602,7 +1603,7 @@ class ExecutionBlockImplExecuteIntegrationTest * @param testReg The register to evaluate * @param numShadowRows Number of preceeding shadowRows in result. */ - void ValidateResult(std::shared_ptr data, size_t skipped, + void ValidateResult(std::shared_ptr data, SkipResult skipped, SharedAqlItemBlockPtr result, RegisterId testReg, size_t numShadowRows = 0) { auto const& call = getCall(); @@ -1611,7 +1612,8 @@ class ExecutionBlockImplExecuteIntegrationTest TRI_ASSERT(data->slice().isArray()); VPackSlice expected = data->slice(); - ValidateSkipMatches(call, static_cast(expected.length()), skipped); + ValidateSkipMatches(call, static_cast(expected.length()), + skipped.getSkipCount()); VPackArrayIterator expectedIt{expected}; // Skip Part @@ -1687,7 +1689,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, test_waiting_block_mock) { auto [state, skipped, block] = testee.execute(stack); if (doesWaiting()) { EXPECT_EQ(state, ExecutionState::WAITING); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); std::tie(state, skipped, block) = testee.execute(stack); } @@ -1721,7 +1723,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, test_produce_only) { if (doesWaiting()) { auto const [state, skipped, block] = producer->execute(stack); EXPECT_EQ(state, ExecutionState::WAITING); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); } auto const [state, skipped, block] = producer->execute(stack); @@ -1755,7 +1757,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, test_produce_using_two) { if (doesWaiting()) { auto const [state, skipped, block] = producer->execute(stack); EXPECT_EQ(state, ExecutionState::WAITING); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); } auto const [state, skipped, block] = producer->execute(stack); @@ -1816,7 +1818,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, test_call_forwarding_passthroug if (doesWaiting()) { auto const [state, skipped, block] = lower->execute(stack); EXPECT_EQ(state, ExecutionState::WAITING); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); // Reset call counters upperState.reset(); @@ -1900,7 +1902,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, test_call_forwarding_implement_ if (doesWaiting()) { auto const [state, skipped, block] = lower->execute(stack); EXPECT_EQ(state, ExecutionState::WAITING); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); } auto const [state, skipped, block] = lower->execute(stack); @@ -1945,7 +1947,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, test_multiple_upstream_calls) { size_t killSwitch = 0; while (state == ExecutionState::WAITING) { EXPECT_TRUE(doesWaiting()); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); std::tie(state, skipped, block) = testee->execute(stack); // Kill switch to avoid endless loop in case of error. @@ -2004,7 +2006,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, test_multiple_upstream_calls_pa size_t waited = 0; while (state == ExecutionState::WAITING && waited < 2 /* avoid endless waiting*/) { EXPECT_EQ(state, ExecutionState::WAITING); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); waited++; std::tie(state, skipped, block) = testee->execute(stack); @@ -2014,10 +2016,10 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, test_multiple_upstream_calls_pa EXPECT_EQ(block, nullptr); if (fullCount) { // We skipped everything - EXPECT_EQ(skipped, 1000); + EXPECT_EQ(skipped.getSkipCount(), 1000); EXPECT_EQ(state, ExecutionState::DONE); } else { - EXPECT_EQ(skipped, offset); + EXPECT_EQ(skipped.getSkipCount(), offset); EXPECT_EQ(state, ExecutionState::HASMORE); } } else { @@ -2033,7 +2035,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, test_multiple_upstream_calls_pa size_t waited = 0; while (state == ExecutionState::WAITING && waited < 3 /* avoid endless waiting*/) { EXPECT_EQ(state, ExecutionState::WAITING); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); waited++; std::tie(state, skipped, block) = testee->execute(stack); @@ -2051,8 +2053,8 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, test_multiple_upstream_calls_pa ASSERT_EQ(block->size(), 1); // Book-keeping for call. // We need to request data from above with the correct call. - if (skipped > 0) { - call.didSkip(skipped); + if (!skipped.nothingSkipped()) { + call.didSkip(skipped.getSkipCount()); } call.didProduce(1); auto got = block->getValueReference(0, outReg).slice(); @@ -2061,15 +2063,15 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, test_multiple_upstream_calls_pa << " in row " << i << " and register " << outReg; if (i == 0) { // The first data row includes skip - EXPECT_EQ(skipped, offset); + EXPECT_EQ(skipped.getSkipCount(), offset); } else { if (call.getLimit() == 0 && call.hasHardLimit() && call.needsFullCount()) { // The last row, with fullCount needs to contain data. - EXPECT_EQ(skipped, 1000 - limit - offset); + EXPECT_EQ(skipped.getSkipCount(), 1000 - limit - offset); } else { // Do not skip on later data rows // Except the last one on fullcount - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); } } // NOTE: We might want to get into this situation. @@ -2136,7 +2138,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, only_relevant_shadowRows) { if (doesWaiting()) { // We wait between lines EXPECT_EQ(state, ExecutionState::WAITING); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); std::tie(state, skipped, block) = testee->execute(stack); } @@ -2147,7 +2149,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, only_relevant_shadowRows) { EXPECT_EQ(state, ExecutionState::HASMORE); } // Cannot skip a shadowRow - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ASSERT_NE(block, nullptr); ASSERT_EQ(block->size(), 1); EXPECT_TRUE(block->hasShadowRows()); @@ -2194,7 +2196,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, input_and_relevant_shadowRow) { if (doesWaiting()) { auto const [state, skipped, block] = testee->execute(stack); EXPECT_EQ(state, ExecutionState::WAITING); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); } auto const [state, skipped, block] = testee->execute(stack); @@ -2246,7 +2248,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, input_and_non_relevant_shadowRo if (doesWaiting()) { auto const [state, skipped, block] = testee->execute(stack); EXPECT_EQ(state, ExecutionState::WAITING); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); } auto const [state, skipped, block] = testee->execute(stack); @@ -2315,7 +2317,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, multiple_subqueries) { if (doesWaiting()) { auto const [state, skipped, block] = testee->execute(stack); EXPECT_EQ(state, ExecutionState::WAITING); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); } auto const [state, skipped, block] = testee->execute(stack); @@ -2337,7 +2339,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, multiple_subqueries) { testee->execute(forwardStack); // We do not care for any data left EXPECT_EQ(forwardState, ExecutionState::HASMORE); - EXPECT_EQ(forwardSkipped, 0); + EXPECT_EQ(forwardSkipped.getSkipCount(), 0); // However there need to be two shadow rows ASSERT_NE(forwardBlock, nullptr); ASSERT_EQ(forwardBlock->size(), 2); @@ -2396,7 +2398,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, empty_subquery) { // we only wait exactly once, only one block upstream that is not sliced. auto const& [state, skipped, block] = testee->execute(stack); EXPECT_EQ(state, ExecutionState::WAITING); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); } auto call = getCall(); @@ -2408,10 +2410,10 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, empty_subquery) { EXPECT_EQ(state, ExecutionState::HASMORE); ASSERT_NE(block, nullptr); if (skip) { - EXPECT_EQ(skipped, 1); + EXPECT_EQ(skipped.getSkipCount(), 1); EXPECT_EQ(block->size(), 2); } else { - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block->size(), 3); } size_t row = 0; @@ -2446,7 +2448,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, empty_subquery) { auto const& [state, skipped, block] = testee->execute(stack); EXPECT_EQ(state, ExecutionState::HASMORE); ASSERT_NE(block, nullptr); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block->size(), 1); size_t row = 0; AssertIsShadowRowOfDepth(block, row, 0); @@ -2472,7 +2474,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, empty_subquery) { auto const& [state, skipped, block] = testee->execute(stack); EXPECT_EQ(state, ExecutionState::DONE); ASSERT_NE(block, nullptr); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block->size(), 2); size_t row = 0; AssertIsShadowRowOfDepth(block, row, 0); @@ -2541,7 +2543,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, test_outer_subquery_forwarding_ auto [state, skipped, block] = testee.execute(stack); if (doesWaiting()) { EXPECT_EQ(state, ExecutionState::WAITING); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); std::tie(state, skipped, block) = testee.execute(stack); } @@ -2600,7 +2602,7 @@ TEST_P(ExecutionBlockImplExecuteIntegrationTest, test_outer_subquery_forwarding) auto [state, skipped, block] = testee.execute(stack); if (doesWaiting()) { EXPECT_EQ(state, ExecutionState::WAITING); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); std::tie(state, skipped, block) = testee.execute(stack); } diff --git a/tests/Aql/ExecutionBlockImplTestInstances.cpp b/tests/Aql/ExecutionBlockImplTestInstances.cpp index b5bfc68a589b..a7af31f4c8f6 100644 --- a/tests/Aql/ExecutionBlockImplTestInstances.cpp +++ b/tests/Aql/ExecutionBlockImplTestInstances.cpp @@ -3,7 +3,7 @@ #include "TestExecutorHelper.h" #include "TestLambdaExecutor.h" -template class ::arangodb::aql::ExecutionBlockImpl; -template class ::arangodb::aql::ExecutionBlockImpl; +// template class ::arangodb::aql::ExecutionBlockImpl; +// template class ::arangodb::aql::ExecutionBlockImpl; template class ::arangodb::aql::ExecutionBlockImpl; template class ::arangodb::aql::ExecutionBlockImpl; diff --git a/tests/Aql/ExecutorTestHelper.h b/tests/Aql/ExecutorTestHelper.h index c3b2686246f1..bf11a6ff0408 100644 --- a/tests/Aql/ExecutorTestHelper.h +++ b/tests/Aql/ExecutorTestHelper.h @@ -387,7 +387,7 @@ struct ExecutorTestHelper { if (!loop) { auto const [state, skipped, result] = _pipeline.get().front()->execute(_callStack); - skippedTotal = skipped; + skippedTotal = skipped.getSkipCount(); finalState = state; if (result != nullptr) { allResults.add(result); @@ -397,8 +397,8 @@ struct ExecutorTestHelper { auto const [state, skipped, result] = _pipeline.get().front()->execute(_callStack); finalState = state; auto call = _callStack.popCall(); - skippedTotal += skipped; - call.didSkip(skipped); + skippedTotal += skipped.getSkipCount(); + call.didSkip(skipped.getSkipCount()); if (result != nullptr) { call.didProduce(result->size()); allResults.add(result); diff --git a/tests/Aql/HashedCollectExecutorTest.cpp b/tests/Aql/HashedCollectExecutorTest.cpp index 6aca1b82d975..63dffa7cd6d9 100644 --- a/tests/Aql/HashedCollectExecutorTest.cpp +++ b/tests/Aql/HashedCollectExecutorTest.cpp @@ -257,7 +257,7 @@ TEST_P(HashedCollectExecutorTest, collect_only_soft_less_second_call) { AqlCallStack stack{call}; auto const [state, skipped, result] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::HASMORE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ASSERT_NE(result, nullptr); asserthelper::ValidateBlocksAreEqualUnordered(result, buildExpectedOutput(), matchedRows, 2, registersToTest); @@ -270,7 +270,7 @@ TEST_P(HashedCollectExecutorTest, collect_only_soft_less_second_call) { AqlCallStack stack{call}; auto const [state, skipped, result] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ASSERT_NE(result, nullptr); asserthelper::ValidateBlocksAreEqualUnordered(result, buildExpectedOutput(), matchedRows, 0, registersToTest); diff --git a/tests/Aql/IdExecutorTest.cpp b/tests/Aql/IdExecutorTest.cpp index ac203cf39da3..407e999eb970 100644 --- a/tests/Aql/IdExecutorTest.cpp +++ b/tests/Aql/IdExecutorTest.cpp @@ -297,7 +297,7 @@ TEST_F(IdExecutionBlockTest, test_initialize_cursor_get) { AqlCallStack stack(std::move(call)); auto const& [state, skipped, block] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); } { @@ -312,7 +312,7 @@ TEST_F(IdExecutionBlockTest, test_initialize_cursor_get) { AqlCallStack stack(std::move(call)); auto const& [state, skipped, block] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ASSERT_NE(block, nullptr); EXPECT_EQ(block->size(), 1); auto const& val = block->getValueReference(0, 0); @@ -340,7 +340,7 @@ TEST_F(IdExecutionBlockTest, test_initialize_cursor_skip) { AqlCallStack stack(std::move(call)); auto const& [state, skipped, block] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); } { @@ -356,7 +356,7 @@ TEST_F(IdExecutionBlockTest, test_initialize_cursor_skip) { AqlCallStack stack(std::move(call)); auto const& [state, skipped, block] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 1); + EXPECT_EQ(skipped.getSkipCount(), 1); ASSERT_EQ(block, nullptr); } } @@ -381,7 +381,7 @@ TEST_F(IdExecutionBlockTest, test_initialize_cursor_fullCount) { AqlCallStack stack(std::move(call)); auto const& [state, skipped, block] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); } { @@ -398,7 +398,7 @@ TEST_F(IdExecutionBlockTest, test_initialize_cursor_fullCount) { AqlCallStack stack(std::move(call)); auto const& [state, skipped, block] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 1); + EXPECT_EQ(skipped.getSkipCount(), 1); ASSERT_EQ(block, nullptr); } } @@ -455,9 +455,9 @@ TEST_P(BlockOverloadTest, test_hardlimit_const_fetcher) { auto const& [state, skipped, block] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::DONE); if (useFullCount()) { - EXPECT_EQ(skipped, 4); + EXPECT_EQ(skipped.getSkipCount(), 4); } else { - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); } asserthelper::ValidateBlocksAreEqual(block, expectedOutputBlock); @@ -468,7 +468,7 @@ TEST_P(BlockOverloadTest, test_hardlimit_const_fetcher) { AqlCallStack stack(std::move(call)); auto const& [state, skipped, block] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); } } @@ -493,9 +493,9 @@ TEST_P(BlockOverloadTest, test_hardlimit_const_fetcher_shadow_rows_at_end) { auto const& [state, skipped, block] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::DONE); if (useFullCount()) { - EXPECT_EQ(skipped, 2); + EXPECT_EQ(skipped.getSkipCount(), 2); } else { - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); } asserthelper::ValidateBlocksAreEqual(block, expectedOutputBlock); } @@ -505,7 +505,7 @@ TEST_P(BlockOverloadTest, test_hardlimit_const_fetcher_shadow_rows_at_end) { AqlCallStack stack(std::move(call)); auto const& [state, skipped, block] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); } } @@ -530,9 +530,9 @@ TEST_P(BlockOverloadTest, test_hardlimit_const_fetcher_shadow_rows_in_between) { auto const& [state, skipped, block] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::HASMORE); if (useFullCount()) { - EXPECT_EQ(skipped, 1); + EXPECT_EQ(skipped.getSkipCount(), 1); } else { - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); } asserthelper::ValidateBlocksAreEqual(block, expectedOutputBlock); } @@ -544,7 +544,7 @@ TEST_P(BlockOverloadTest, test_hardlimit_const_fetcher_shadow_rows_in_between) { AqlCallStack stack(std::move(call)); auto const& [state, skipped, block] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); asserthelper::ValidateBlocksAreEqual(block, expectedOutputBlock); } } @@ -571,9 +571,9 @@ TEST_P(BlockOverloadTest, test_hardlimit_const_fetcher_consecutive_shadow_rows) auto const& [state, skipped, block] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::HASMORE); if (useFullCount()) { - EXPECT_EQ(skipped, 1); + EXPECT_EQ(skipped.getSkipCount(), 1); } else { - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); } asserthelper::ValidateBlocksAreEqual(block, expectedOutputBlock); } @@ -586,7 +586,7 @@ TEST_P(BlockOverloadTest, test_hardlimit_const_fetcher_consecutive_shadow_rows) AqlCallStack stack(std::move(call)); auto const& [state, skipped, block] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::HASMORE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); asserthelper::ValidateBlocksAreEqual(block, expectedOutputBlock); } { @@ -598,7 +598,7 @@ TEST_P(BlockOverloadTest, test_hardlimit_const_fetcher_consecutive_shadow_rows) AqlCallStack stack(std::move(call)); auto const& [state, skipped, block] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); asserthelper::ValidateBlocksAreEqual(block, expectedOutputBlock); } { @@ -607,7 +607,7 @@ TEST_P(BlockOverloadTest, test_hardlimit_const_fetcher_consecutive_shadow_rows) AqlCallStack stack(std::move(call)); auto const& [state, skipped, block] = testee.execute(stack); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(block, nullptr); } } diff --git a/tests/Aql/RemoteExecutorTest.cpp b/tests/Aql/RemoteExecutorTest.cpp index c5e4bef76825..74ce419dc35a 100644 --- a/tests/Aql/RemoteExecutorTest.cpp +++ b/tests/Aql/RemoteExecutorTest.cpp @@ -71,9 +71,7 @@ class DeSerializeAqlCallTest : public ::testing::TestWithParam { public: DeSerializeAqlCallTest() = default; - void SetUp() override { - aqlCall = GetParam(); - } + void SetUp() override { aqlCall = GetParam(); } protected: AqlCall aqlCall{}; @@ -118,20 +116,18 @@ class DeSerializeAqlCallStackTest : public ::testing::TestWithParam::error(-1); }); - ASSERT_TRUE(maybeDeSerializedCallStack.ok()) << maybeDeSerializedCallStack.errorMessage(); + ASSERT_TRUE(maybeDeSerializedCallStack.ok()) + << maybeDeSerializedCallStack.errorMessage(); auto const deSerializedCallStack = *maybeDeSerializedCallStack; ASSERT_EQ(aqlCallStack, deSerializedCallStack); } -INSTANTIATE_TEST_CASE_P(DeSerializeAqlCallStackTestVariations, DeSerializeAqlCallStackTest, testingAqlCallStacks); - +INSTANTIATE_TEST_CASE_P(DeSerializeAqlCallStackTestVariations, + DeSerializeAqlCallStackTest, testingAqlCallStacks); class DeSerializeAqlExecuteResultTest : public ::testing::TestWithParam { public: DeSerializeAqlExecuteResultTest() = default; - void SetUp() override { - aqlExecuteResult = GetParam(); - } + void SetUp() override { aqlExecuteResult = GetParam(); } protected: - AqlExecuteResult aqlExecuteResult{ExecutionState::DONE, 0, nullptr}; + AqlExecuteResult aqlExecuteResult{ExecutionState::DONE, SkipResult{}, nullptr}; }; ResourceMonitor resourceMonitor{}; AqlItemBlockManager manager{&resourceMonitor, SerializationFormat::SHADOWROWS}; +auto MakeSkipResult(size_t const i) -> SkipResult { + SkipResult res{}; + res.didSkip(i); + return res; +} + auto const testingAqlExecuteResults = ::testing::ValuesIn(std::array{ - AqlExecuteResult{ExecutionState::DONE, 0, nullptr}, - AqlExecuteResult{ExecutionState::HASMORE, 0, nullptr}, - AqlExecuteResult{ExecutionState::HASMORE, 4, nullptr}, - AqlExecuteResult{ExecutionState::DONE, 0, buildBlock<1>(manager, {{42}})}, - AqlExecuteResult{ExecutionState::HASMORE, 3, buildBlock<2>(manager, {{3, 42}, {4, 41}})}, + AqlExecuteResult{ExecutionState::DONE, MakeSkipResult(0), nullptr}, + AqlExecuteResult{ExecutionState::HASMORE, MakeSkipResult(0), nullptr}, + AqlExecuteResult{ExecutionState::HASMORE, MakeSkipResult(4), nullptr}, + AqlExecuteResult{ExecutionState::DONE, MakeSkipResult(0), buildBlock<1>(manager, {{42}})}, + AqlExecuteResult{ExecutionState::HASMORE, MakeSkipResult(3), + buildBlock<2>(manager, {{3, 42}, {4, 41}})}, }); TEST_P(DeSerializeAqlExecuteResultTest, testSuite) { @@ -203,7 +205,8 @@ TEST_P(DeSerializeAqlExecuteResultTest, testSuite) { ASSERT_EQ(aqlExecuteResult.state(), deSerializedAqlExecuteResult.state()); ASSERT_EQ(aqlExecuteResult.skipped(), deSerializedAqlExecuteResult.skipped()); - ASSERT_EQ(aqlExecuteResult.block() == nullptr, deSerializedAqlExecuteResult.block() == nullptr); + ASSERT_EQ(aqlExecuteResult.block() == nullptr, + deSerializedAqlExecuteResult.block() == nullptr); if (aqlExecuteResult.block() != nullptr) { ASSERT_EQ(*aqlExecuteResult.block(), *deSerializedAqlExecuteResult.block()) << "left: " << blockToString(aqlExecuteResult.block()) @@ -212,6 +215,7 @@ TEST_P(DeSerializeAqlExecuteResultTest, testSuite) { ASSERT_EQ(aqlExecuteResult, deSerializedAqlExecuteResult); } -INSTANTIATE_TEST_CASE_P(DeSerializeAqlExecuteResultTestVariations, DeSerializeAqlExecuteResultTest, testingAqlExecuteResults); +INSTANTIATE_TEST_CASE_P(DeSerializeAqlExecuteResultTestVariations, + DeSerializeAqlExecuteResultTest, testingAqlExecuteResults); } // namespace arangodb::tests::aql diff --git a/tests/Aql/ScatterExecutorTest.cpp b/tests/Aql/ScatterExecutorTest.cpp index 883a8b8498c0..d5cd0968fb98 100644 --- a/tests/Aql/ScatterExecutorTest.cpp +++ b/tests/Aql/ScatterExecutorTest.cpp @@ -159,7 +159,7 @@ TEST_P(RandomOrderTest, all_clients_should_get_the_block) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ValidateBlocksAreEqual(block, inputBlock); } } @@ -179,7 +179,7 @@ TEST_P(RandomOrderTest, all_clients_can_skip_the_block) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 3); + EXPECT_EQ(skipped.getSkipCount(), 3); EXPECT_EQ(block, nullptr); } } @@ -201,7 +201,7 @@ TEST_P(RandomOrderTest, all_clients_can_fullcount_the_block) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 2); + EXPECT_EQ(skipped.getSkipCount(), 2); ValidateBlocksAreEqual(block, expectedBlock); } } @@ -223,7 +223,7 @@ TEST_P(RandomOrderTest, all_clients_can_have_different_calls) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ValidateBlocksAreEqual(block, inputBlock); } else if (client == "b") { AqlCall call{}; @@ -232,7 +232,7 @@ TEST_P(RandomOrderTest, all_clients_can_have_different_calls) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 2); + EXPECT_EQ(skipped.getSkipCount(), 2); auto expectedBlock = buildBlock<1>(itemBlockManager, {{2}, {3}}); ValidateBlocksAreEqual(block, expectedBlock); } else if (client == "c") { @@ -242,7 +242,7 @@ TEST_P(RandomOrderTest, all_clients_can_have_different_calls) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::HASMORE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); auto expectedBlock = buildBlock<1>(itemBlockManager, {{0}, {1}}); ValidateBlocksAreEqual(block, expectedBlock); } @@ -254,7 +254,7 @@ TEST_P(RandomOrderTest, all_clients_can_have_different_calls) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::HASMORE); - EXPECT_EQ(skipped, 1); + EXPECT_EQ(skipped.getSkipCount(), 1); auto expectedBlock = buildBlock<1>(itemBlockManager, {{3}, {4}}); ValidateBlocksAreEqual(block, expectedBlock); } @@ -283,7 +283,7 @@ TEST_P(RandomOrderTest, get_does_not_jump_over_shadowrows) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::HASMORE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ValidateBlocksAreEqual(block, firstExpectedBlock); } @@ -295,7 +295,7 @@ TEST_P(RandomOrderTest, get_does_not_jump_over_shadowrows) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ValidateBlocksAreEqual(block, secondExpectedBlock); } } @@ -321,7 +321,7 @@ TEST_P(RandomOrderTest, handling_of_higher_depth_shadowrows_produce) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::HASMORE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ValidateBlocksAreEqual(block, firstExpectedBlock); } @@ -333,7 +333,7 @@ TEST_P(RandomOrderTest, handling_of_higher_depth_shadowrows_produce) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ValidateBlocksAreEqual(block, secondExpectedBlock); } } @@ -360,7 +360,7 @@ TEST_P(RandomOrderTest, handling_of_higher_depth_shadowrows_skip) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::HASMORE); - EXPECT_EQ(skipped, 2); + EXPECT_EQ(skipped.getSkipCount(), 2); ValidateBlocksAreEqual(block, firstExpectedBlock); } @@ -372,7 +372,7 @@ TEST_P(RandomOrderTest, handling_of_higher_depth_shadowrows_skip) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ValidateBlocksAreEqual(block, secondExpectedBlock); } } @@ -398,7 +398,7 @@ TEST_P(RandomOrderTest, handling_of_consecutive_shadow_rows) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::HASMORE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); auto expected = buildBlock<1>(itemBlockManager, {{0}, {1}, {2}, {3}}, {{2, 0}, {3, 1}}); ValidateBlocksAreEqual(block, expected); @@ -409,7 +409,7 @@ TEST_P(RandomOrderTest, handling_of_consecutive_shadow_rows) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); auto expected = buildBlock<1>(itemBlockManager, {{4}, {5}}, {{0, 0}, {1, 1}}); ValidateBlocksAreEqual(block, expected); } @@ -434,7 +434,7 @@ TEST_P(RandomOrderTest, shadowrows_with_different_call_types) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::HASMORE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); auto expectedBlock = buildBlock<1>(itemBlockManager, {{0}, {1}, {2}, {3}}, {{3, 0}}); ValidateBlocksAreEqual(block, expectedBlock); @@ -445,7 +445,7 @@ TEST_P(RandomOrderTest, shadowrows_with_different_call_types) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::HASMORE); - EXPECT_EQ(skipped, 2); + EXPECT_EQ(skipped.getSkipCount(), 2); auto expectedBlock = buildBlock<1>(itemBlockManager, {{2}, {3}}, {{1, 0}}); ValidateBlocksAreEqual(block, expectedBlock); } else if (client == "c") { @@ -455,7 +455,7 @@ TEST_P(RandomOrderTest, shadowrows_with_different_call_types) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::HASMORE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); auto expectedBlock = buildBlock<1>(itemBlockManager, {{0}, {1}}); ValidateBlocksAreEqual(block, expectedBlock); } @@ -467,7 +467,7 @@ TEST_P(RandomOrderTest, shadowrows_with_different_call_types) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::HASMORE); - EXPECT_EQ(skipped, 1); + EXPECT_EQ(skipped.getSkipCount(), 1); auto expectedBlock = buildBlock<1>(itemBlockManager, {{3}}, {{0, 0}}); ValidateBlocksAreEqual(block, expectedBlock); } @@ -484,7 +484,7 @@ TEST_P(RandomOrderTest, shadowrows_with_different_call_types) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); auto expectedBlock = buildBlock<1>(itemBlockManager, {{4}, {5}}, {{1, 0}}); ValidateBlocksAreEqual(block, expectedBlock); } else if (client == "b") { @@ -493,7 +493,7 @@ TEST_P(RandomOrderTest, shadowrows_with_different_call_types) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); auto expectedBlock = buildBlock<1>(itemBlockManager, {{4}, {5}}, {{1, 0}}); ValidateBlocksAreEqual(block, expectedBlock); } else if (client == "c") { @@ -503,7 +503,7 @@ TEST_P(RandomOrderTest, shadowrows_with_different_call_types) { AqlCallStack stack{call}; auto const [state, skipped, block] = testee.executeForClient(stack, client); EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 1); + EXPECT_EQ(skipped.getSkipCount(), 1); auto expectedBlock = buildBlock<1>(itemBlockManager, {{5}}, {{0, 0}}); ValidateBlocksAreEqual(block, expectedBlock); } @@ -579,7 +579,7 @@ TEST_F(ScatterExecutionBlockTest, any_ordering_of_calls_is_fine) { } else { EXPECT_EQ(state, ExecutionState::HASMORE); } - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); ASSERT_TRUE(callNr < blocks.size()); ValidateBlocksAreEqual(block, blocks[callNr]); callNr++; diff --git a/tests/Aql/SingleRowFetcherTest.cpp b/tests/Aql/SingleRowFetcherTest.cpp index f0ed040e2783..7ea984f8b9e6 100644 --- a/tests/Aql/SingleRowFetcherTest.cpp +++ b/tests/Aql/SingleRowFetcherTest.cpp @@ -1228,7 +1228,7 @@ TEST_F(SingleRowFetcherTestPassBlocks, handling_shadowrows_in_execute_oneAndDone // First no data row auto [state, skipped, input] = testee.execute(stack); EXPECT_EQ(input.getRowIndex(), 0); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(state, ExecutionState::DONE); } // testee is destroyed here } @@ -1263,7 +1263,7 @@ TEST_F(SingleRowFetcherTestPassBlocks, handling_shadowrows_in_execute_twoAndHasM auto [state, skipped, input] = testee.execute(stack); // We only have one block, no more calls to execute necessary EXPECT_EQ(state, ExecutionState::DONE); - EXPECT_EQ(skipped, 0); + EXPECT_EQ(skipped.getSkipCount(), 0); EXPECT_EQ(input.getRowIndex(), 0); // Now validate the input range diff --git a/tests/Aql/SkipResultTest.cpp b/tests/Aql/SkipResultTest.cpp index f129075a9f38..b79f918265c6 100644 --- a/tests/Aql/SkipResultTest.cpp +++ b/tests/Aql/SkipResultTest.cpp @@ -23,6 +23,7 @@ #include "gtest/gtest.h" #include "Aql/SkipResult.h" +#include "Cluster/ResultT.h" #include #include @@ -82,7 +83,9 @@ TEST_F(SkipResultTest, serialize_deserialize_empty) { SkipResult original{}; VPackBuilder builder; original.toVelocyPack(builder); - auto testee = SkipResult::fromVelocyPack(builder.slice()); + auto maybeTestee = SkipResult::fromVelocyPack(builder.slice()); + ASSERT_FALSE(maybeTestee.fail()); + auto testee = maybeTestee.get(); EXPECT_EQ(testee.nothingSkipped(), original.nothingSkipped()); EXPECT_EQ(testee.getSkipCount(), original.getSkipCount()); } @@ -92,12 +95,14 @@ TEST_F(SkipResultTest, serialize_deserialize_with_count) { original.didSkip(6); VPackBuilder builder; original.toVelocyPack(builder); - auto testee = SkipResult::fromVelocyPack(builder.slice()); + auto maybeTestee = SkipResult::fromVelocyPack(builder.slice()); + ASSERT_FALSE(maybeTestee.fail()); + auto testee = maybeTestee.get(); EXPECT_EQ(testee.nothingSkipped(), original.nothingSkipped()); EXPECT_EQ(testee.getSkipCount(), original.getSkipCount()); } -TEST_F(SkipResult, can_be_added) { +TEST_F(SkipResultTest, can_be_added) { SkipResult a{}; a.didSkip(6); SkipResult b{}; diff --git a/tests/Aql/WaitingExecutionBlockMock.cpp b/tests/Aql/WaitingExecutionBlockMock.cpp index c48de11e02e4..5ed988f8478e 100644 --- a/tests/Aql/WaitingExecutionBlockMock.cpp +++ b/tests/Aql/WaitingExecutionBlockMock.cpp @@ -27,6 +27,7 @@ #include "Aql/ExecutionEngine.h" #include "Aql/ExecutionState.h" #include "Aql/QueryOptions.h" +#include "Aql/SkipResult.h" #include "Logger/LogMacros.h" @@ -113,7 +114,7 @@ std::pair WaitingExecutionBlockMock::skip } } -std::tuple WaitingExecutionBlockMock::execute(AqlCallStack stack) { +std::tuple WaitingExecutionBlockMock::execute(AqlCallStack stack) { traceExecuteBegin(stack); auto res = executeWithoutTrace(stack); traceExecuteEnd(res); @@ -121,7 +122,7 @@ std::tuple WaitingExecutionBlockM } // NOTE: Does not care for shadowrows! -std::tuple WaitingExecutionBlockMock::executeWithoutTrace( +std::tuple WaitingExecutionBlockMock::executeWithoutTrace( AqlCallStack stack) { while (!stack.isRelevant()) { stack.pop(); @@ -135,7 +136,7 @@ std::tuple WaitingExecutionBlockM if (_variant != WaitingBehaviour::NEVER && !_hasWaited) { // If we ordered waiting check on _hasWaited and wait if not _hasWaited = true; - return {ExecutionState::WAITING, 0, nullptr}; + return {ExecutionState::WAITING, SkipResult{}, nullptr}; } if (_variant == WaitingBehaviour::ALWAYS) { // If we always wait, reset. @@ -154,7 +155,9 @@ std::tuple WaitingExecutionBlockM // Sorry we can only return one block. // This means we have prepared the first block. // But still need more data. - return {ExecutionState::HASMORE, skipped, result}; + SkipResult skipRes{}; + skipRes.didSkip(skipped); + return {ExecutionState::HASMORE, skipRes, result}; } else { dropBlock(); continue; @@ -177,7 +180,9 @@ std::tuple WaitingExecutionBlockM // Sorry we can only return one block. // This means we have prepared the first block. // But still need more data. - return {ExecutionState::HASMORE, skipped, result}; + SkipResult skipRes{}; + skipRes.didSkip(skipped); + return {ExecutionState::HASMORE, skipRes, result}; } size_t canReturn = _data.front()->size() - _inflight; @@ -212,16 +217,20 @@ std::tuple WaitingExecutionBlockM dropBlock(); } } + SkipResult skipRes{}; + skipRes.didSkip(skipped); if (!_data.empty()) { - return {ExecutionState::HASMORE, skipped, result}; + return {ExecutionState::HASMORE, skipRes, result}; } else if (result != nullptr && result->size() < myCall.hardLimit) { - return {ExecutionState::HASMORE, skipped, result}; + return {ExecutionState::HASMORE, skipRes, result}; } else { - return {ExecutionState::DONE, skipped, result}; + return {ExecutionState::DONE, skipRes, result}; } } } - return {ExecutionState::DONE, skipped, result}; + SkipResult skipRes{}; + skipRes.didSkip(skipped); + return {ExecutionState::DONE, skipRes, result}; } void WaitingExecutionBlockMock::dropBlock() { diff --git a/tests/Aql/WaitingExecutionBlockMock.h b/tests/Aql/WaitingExecutionBlockMock.h index 2c147ba08e91..b1ad73e01bd8 100644 --- a/tests/Aql/WaitingExecutionBlockMock.h +++ b/tests/Aql/WaitingExecutionBlockMock.h @@ -35,6 +35,7 @@ class AqlItemBlock; class ExecutionEngine; class ExecutionNode; struct ResourceMonitor; +class SkipResult; } // namespace aql namespace tests { @@ -106,15 +107,15 @@ class WaitingExecutionBlockMock final : public arangodb::aql::ExecutionBlock { */ std::pair skipSome(size_t atMost) override; - std::tuple execute( + std::tuple execute( arangodb::aql::AqlCallStack stack) override; private: void dropBlock(); // Implementation of execute - std::tuple executeWithoutTrace( - arangodb::aql::AqlCallStack stack); + std::tuple + executeWithoutTrace(arangodb::aql::AqlCallStack stack); private: std::deque _data; From bd53fa827f114d0db1cc2eea341cd6ec01df6c61 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Sun, 1 Mar 2020 23:21:54 +0100 Subject: [PATCH 29/71] Added a test using skip and limit on subqueries --- tests/Aql/SpliceSubqueryOptimizerRuleTest.cpp | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/Aql/SpliceSubqueryOptimizerRuleTest.cpp b/tests/Aql/SpliceSubqueryOptimizerRuleTest.cpp index 94e9940f4e42..9e84ab6b17e6 100644 --- a/tests/Aql/SpliceSubqueryOptimizerRuleTest.cpp +++ b/tests/Aql/SpliceSubqueryOptimizerRuleTest.cpp @@ -813,6 +813,46 @@ TEST_F(SpliceSubqueryNodeOptimizerRuleTest, verifyQueryResult(query, expected->slice()); } +TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_subquery_skip_nodes) { + auto query = R"aql( + FOR k IN 1..10 + LET sub1 = ( + FOR j IN 1..10 + LET sub2 = ( + FOR i IN 1..4 + LIMIT 2,10 + RETURN i + ) + LIMIT 2,10 + RETURN [j, sub2] + ) + LIMIT 3, 10 + RETURN [k, sub1])aql"; + verifySubquerySplicing(query, 2); + + VPackBuilder builder; + builder.openArray(); + for (size_t k = 4; k <= 10; ++k) { + builder.openArray(); + builder.add(VPackValue(k)); + builder.openArray(); + for (size_t j = 3; j <= 10; ++j) { + builder.openArray(); + builder.add(VPackValue(j)); + builder.openArray(); + for (size_t i = 3; i <= 4; ++i) { + builder.add(VPackValue(i)); + } + builder.close(); + builder.close(); + } + builder.close(); + builder.close(); + } + builder.close(); + verifyQueryResult(query, builder.slice()); +} + // TODO Check isInSplicedSubquery // TODO Test cluster rules From f72f45aaa21d22c23cf3b2facf49a93889ff77f4 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Mon, 2 Mar 2020 00:19:30 +0100 Subject: [PATCH 30/71] Prepared to use subqueries in SkipResult --- arangod/Aql/SkipResult.cpp | 81 +++++++++++++++++++++++++++----- arangod/Aql/SkipResult.h | 18 ++++++-- tests/Aql/SkipResultTest.cpp | 89 ++++++++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 17 deletions(-) diff --git a/arangod/Aql/SkipResult.cpp b/arangod/Aql/SkipResult.cpp index c9156a9e0e91..93dfd3d73534 100644 --- a/arangod/Aql/SkipResult.cpp +++ b/arangod/Aql/SkipResult.cpp @@ -26,6 +26,7 @@ #include "Cluster/ResultT.h" #include +#include #include using namespace arangodb::aql; @@ -34,30 +35,63 @@ SkipResult::SkipResult() {} SkipResult::SkipResult(SkipResult const& other) : _skipped{other._skipped} {} -auto SkipResult::getSkipCount() const noexcept -> size_t { return _skipped; } +auto SkipResult::getSkipCount() const noexcept -> size_t { + TRI_ASSERT(!_skipped.empty()); + return _skipped.back(); +} -auto SkipResult::didSkip(size_t skipped) -> void { _skipped += skipped; } +auto SkipResult::didSkip(size_t skipped) -> void { + TRI_ASSERT(!_skipped.empty()); + _skipped.back() += skipped; +} auto SkipResult::nothingSkipped() const noexcept -> bool { - return _skipped == 0; + TRI_ASSERT(!_skipped.empty()); + return std::all_of(_skipped.begin(), _skipped.end(), + [](size_t const& e) -> bool { return e == 0; }); } auto SkipResult::toVelocyPack(VPackBuilder& builder) const noexcept -> void { - builder.add(VPackValue(_skipped)); + VPackArrayBuilder guard(&builder); + TRI_ASSERT(!_skipped.empty()); + for (auto const& s : _skipped) { + builder.add(VPackValue(s)); + } } auto SkipResult::fromVelocyPack(VPackSlice slice) -> ResultT { - if (!slice.isInteger()) { + if (!slice.isArray()) { auto message = std::string{ "When deserializating AqlExecuteResult: When reading skipped: " "Unexpected type "}; message += slice.typeName(); return Result(TRI_ERROR_TYPE_ERROR, std::move(message)); } + if (slice.isEmptyArray()) { + auto message = std::string{ + "When deserializating AqlExecuteResult: When reading skipped: " + "Got an empty list of skipped values."}; + return Result(TRI_ERROR_TYPE_ERROR, std::move(message)); + } try { SkipResult res; - res.didSkip(slice.getNumber()); - return res; + auto it = VPackArrayIterator(slice); + while (it.valid()) { + auto val = it.value(); + if (!val.isInteger()) { + auto message = std::string{ + "When deserializating AqlExecuteResult: When reading skipped: " + "Unexpected type "}; + message += slice.typeName(); + return Result(TRI_ERROR_TYPE_ERROR, std::move(message)); + } + if (!it.isFirst()) { + res.incrementSubquery(); + } + res.didSkip(val.getNumber()); + ++it; + } + return {res}; } catch (velocypack::Exception const& ex) { auto message = std::string{ "When deserializating AqlExecuteResult: When reading skipped: "}; @@ -66,11 +100,34 @@ auto SkipResult::fromVelocyPack(VPackSlice slice) -> ResultT { } } -auto arangodb::aql::operator+=(SkipResult& a, SkipResult const& b) noexcept -> SkipResult& { - a.didSkip(b.getSkipCount()); - return a; +auto SkipResult::incrementSubquery() -> void { _skipped.emplace_back(0); } +auto SkipResult::decrementSubquery() -> void { + TRI_ASSERT(!_skipped.empty()); + _skipped.pop_back(); + TRI_ASSERT(!_skipped.empty()); +} +auto SkipResult::subqueryDepth() const noexcept -> size_t { + TRI_ASSERT(!_skipped.empty()); + return _skipped.size(); +} + +auto SkipResult::operator+=(SkipResult const& b) noexcept -> SkipResult& { + didSkip(b.getSkipCount()); + return *this; +} + +auto SkipResult::operator==(SkipResult const& b) const noexcept -> bool { + if (_skipped.size() != b._skipped.size()) { + return false; + } + for (size_t i = 0; i < _skipped.size(); ++i) { + if (_skipped[i] != b._skipped[i]) { + return false; + } + } + return true; } -auto arangodb::aql::operator==(SkipResult const& a, SkipResult const& b) noexcept -> bool { - return a.getSkipCount() == b.getSkipCount(); +auto SkipResult::operator!=(SkipResult const& b) const noexcept -> bool { + return !(*this == b); } \ No newline at end of file diff --git a/arangod/Aql/SkipResult.h b/arangod/Aql/SkipResult.h index e05e8a8a58c7..eff7a4f3efac 100644 --- a/arangod/Aql/SkipResult.h +++ b/arangod/Aql/SkipResult.h @@ -25,6 +25,7 @@ // for size_t #include +#include namespace arangodb { template @@ -55,13 +56,20 @@ class SkipResult { auto toVelocyPack(arangodb::velocypack::Builder& builder) const noexcept -> void; - private: - size_t _skipped{0}; -}; + auto incrementSubquery() -> void; + + auto decrementSubquery() -> void; + + auto subqueryDepth() const noexcept -> size_t; -auto operator+=(SkipResult& a, SkipResult const& b) noexcept -> SkipResult&; + auto operator+=(SkipResult const& b) noexcept -> SkipResult&; -auto operator==(SkipResult const& a, SkipResult const& b) noexcept -> bool; + auto operator==(SkipResult const& b) const noexcept -> bool; + auto operator!=(SkipResult const& b) const noexcept -> bool; + + private: + std::vector _skipped{0}; +}; } // namespace arangodb::aql #endif \ No newline at end of file diff --git a/tests/Aql/SkipResultTest.cpp b/tests/Aql/SkipResultTest.cpp index b79f918265c6..0f49039efaf7 100644 --- a/tests/Aql/SkipResultTest.cpp +++ b/tests/Aql/SkipResultTest.cpp @@ -88,6 +88,7 @@ TEST_F(SkipResultTest, serialize_deserialize_empty) { auto testee = maybeTestee.get(); EXPECT_EQ(testee.nothingSkipped(), original.nothingSkipped()); EXPECT_EQ(testee.getSkipCount(), original.getSkipCount()); + EXPECT_EQ(testee, original); } TEST_F(SkipResultTest, serialize_deserialize_with_count) { @@ -100,6 +101,7 @@ TEST_F(SkipResultTest, serialize_deserialize_with_count) { auto testee = maybeTestee.get(); EXPECT_EQ(testee.nothingSkipped(), original.nothingSkipped()); EXPECT_EQ(testee.getSkipCount(), original.getSkipCount()); + EXPECT_EQ(testee, original); } TEST_F(SkipResultTest, can_be_added) { @@ -111,6 +113,93 @@ TEST_F(SkipResultTest, can_be_added) { EXPECT_EQ(a.getSkipCount(), 13); } +TEST_F(SkipResultTest, can_add_a_subquery_depth) { + SkipResult a{}; + a.didSkip(5); + EXPECT_EQ(a.getSkipCount(), 5); + a.incrementSubquery(); + EXPECT_EQ(a.getSkipCount(), 0); + a.didSkip(7); + EXPECT_EQ(a.getSkipCount(), 7); + a.decrementSubquery(); + EXPECT_EQ(a.getSkipCount(), 5); +} + +TEST_F(SkipResultTest, nothing_skip_on_subquery) { + SkipResult a{}; + EXPECT_TRUE(a.nothingSkipped()); + a.didSkip(6); + EXPECT_FALSE(a.nothingSkipped()); + a.incrementSubquery(); + EXPECT_EQ(a.getSkipCount(), 0); + EXPECT_FALSE(a.nothingSkipped()); +} + +TEST_F(SkipResultTest, serialize_deserialize_with_a_subquery) { + SkipResult original{}; + original.didSkip(6); + original.incrementSubquery(); + original.didSkip(2); + + VPackBuilder builder; + original.toVelocyPack(builder); + auto maybeTestee = SkipResult::fromVelocyPack(builder.slice()); + ASSERT_FALSE(maybeTestee.fail()); + auto testee = maybeTestee.get(); + // Use built_in eq + EXPECT_EQ(testee, original); + // Manual test + EXPECT_EQ(testee.nothingSkipped(), original.nothingSkipped()); + EXPECT_EQ(testee.getSkipCount(), original.getSkipCount()); + EXPECT_EQ(testee.subqueryDepth(), original.subqueryDepth()); + original.decrementSubquery(); + testee.decrementSubquery(); + EXPECT_EQ(testee.nothingSkipped(), original.nothingSkipped()); + EXPECT_EQ(testee.getSkipCount(), original.getSkipCount()); + EXPECT_EQ(testee.subqueryDepth(), original.subqueryDepth()); +} + +TEST_F(SkipResultTest, equality) { + auto buildTestSet = []() -> std::vector { + SkipResult empty{}; + SkipResult skip1{}; + skip1.didSkip(6); + + SkipResult skip2{}; + skip2.didSkip(8); + + SkipResult subQuery1{}; + subQuery1.incrementSubquery(); + subQuery1.didSkip(4); + + SkipResult subQuery2{}; + subQuery2.didSkip(8); + subQuery2.incrementSubquery(); + subQuery2.didSkip(4); + + SkipResult subQuery3{}; + subQuery3.didSkip(8); + subQuery3.incrementSubquery(); + return {empty, skip1, skip2, subQuery1, subQuery2, subQuery3}; + }; + + // We create two identical sets with different entries + auto set1 = buildTestSet(); + auto set2 = buildTestSet(); + for (size_t i = 0; i < set1.size(); ++i) { + for (size_t j = 0; j < set2.size(); ++j) { + // Addresses are different + EXPECT_NE(&set1.at(i), &set2.at(j)); + // Identical index => Equal object + if (i == j) { + EXPECT_EQ(set1.at(i), set2.at(j)); + } else { + EXPECT_NE(set1.at(i), set2.at(j)); + } + } + } +} + } // namespace aql } // namespace tests } // namespace arangodb From 9224b4cb7fe66fbac04ea5c4464bce5dcf35961e Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Mon, 2 Mar 2020 08:36:12 +0100 Subject: [PATCH 31/71] Let subqueries modify the SkipResult subquery stack --- arangod/Aql/ExecutionBlockImpl.cpp | 60 +++++++++++++++++++++++------ arangod/Aql/SkipResult.cpp | 21 ++++++++++ arangod/Aql/SkipResult.h | 4 ++ tests/Aql/SkipResultTest.cpp | 62 ++++++++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 12 deletions(-) diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index 233eceb5b139..457dead9c7c5 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -618,7 +618,7 @@ std::pair ExecutionBlockImpl::initializeCursor new (&_rowFetcher) Fetcher(_dependencyProxy); TRI_ASSERT(_skipped.nothingSkipped()); - _skipped = SkipResult{}; + _skipped.reset(); TRI_ASSERT(_state == InternalState::DONE || _state == InternalState::FETCH_DATA); _state = InternalState::FETCH_DATA; @@ -724,7 +724,7 @@ auto ExecutionBlockImpl>::injectConstantBlock::executeWithoutTrace(AqlCallStack stack) { TRI_ASSERT(_skipped.nothingSkipped() || _execState == ExecState::UPSTREAM); if constexpr (std::is_same_v) { - // TODO: implement forwarding of SKIP properly: - // We need to modify the execute API to instead return a vector of skipped - // values. - // Then we can simply push a skip on the Stack here and let it forward. - // In case of a modifaction we need to NOT forward a skip, but instead do - // a limit := limit + offset call and a hardLimit 0 call on top of the stack. - TRI_ASSERT(!clientCall.needSkipMore()); - // In subqeryEndExecutor we actually manage two calls. // The clientClient is defined of what will go into the Executor. // on SubqueryEnd this call is generated based on the call from downstream @@ -1848,6 +1840,12 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { std::tie(_upstreamState, skippedLocal, _lastRange) = executeFetcher(stack, _requestedDependency); + { + VPackBuilder skipRes; + skippedLocal.toVelocyPack(skipRes); + LOG_DEVEL << getPlanNode()->getTypeString() + << " From Upstream: " << skipRes.toJson(); + } if constexpr (std::is_same_v) { // Do not pop the call, we did not put it on. // However we need it for accounting later. @@ -1873,11 +1871,33 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { // We have a new range, passthrough can use this range. _hasUsedDataRangeBlock = false; } + + if constexpr (std::is_same_v) { + // We need to pop the last subquery from the returned skip + // We have not asked for a subquery skip. + TRI_ASSERT(skippedLocal.getSkipCount() == 0); + skippedLocal.decrementSubquery(); + } + if constexpr (skipRowsType() == SkipRowsRangeVariant::FETCHER) { - _skipped += skippedLocal; // We skipped through passthrough, so count that a skip was solved. + _skipped.merge(skippedLocal, false); clientCall.didSkip(skippedLocal.getSkipCount()); + } else { + _skipped.merge(skippedLocal, true); + } + if constexpr (std::is_same_v) { + // For the subqueryStart, we need to increment the SkipLevel by one + // as we may trigger this multiple times, check if we need to do it. + LOG_DEVEL << "sq: " << stack.subqueryLevel() + << "skip: " << _skipped.subqueryDepth(); + while (_skipped.subqueryDepth() < stack.subqueryLevel() + 1) { + // In fact, we only need to increase by 1 + TRI_ASSERT(_skipped.subqueryDepth() == stack.subqueryLevel()); + _skipped.incrementSubquery(); + } } + if (_lastRange.hasShadowRow() && !_lastRange.peekShadowRow().isRelevant()) { _execState = ExecState::SHADOWROWS; } else { @@ -1943,7 +1963,23 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { // We return skipped here, reset member SkipResult skipped = _skipped; - _skipped = SkipResult{}; + + { + VPackBuilder skipRes; + skipped.toVelocyPack(skipRes); + LOG_DEVEL << getPlanNode()->getTypeString() << " Returning: " << skipRes.toJson() + << " stack.sq: " << stack.subqueryLevel(); + } + +#ifdef ARANGODB_ENABLE_MAINTAINER_MODE + if constexpr (std::is_same_v) { + TRI_ASSERT(skipped.subqueryDepth() == stack.subqueryLevel() /*we inected a call*/); + } else { + TRI_ASSERT(skipped.subqueryDepth() == stack.subqueryLevel() + 1 /*we took our call*/); + } +#endif + + _skipped.reset(); if (localExecutorState == ExecutorState::HASMORE || _lastRange.hasDataRow() || _lastRange.hasShadowRow()) { // We have skipped or/and return data, otherwise we cannot return HASMORE diff --git a/arangod/Aql/SkipResult.cpp b/arangod/Aql/SkipResult.cpp index 93dfd3d73534..768b95a525a1 100644 --- a/arangod/Aql/SkipResult.cpp +++ b/arangod/Aql/SkipResult.cpp @@ -111,6 +111,27 @@ auto SkipResult::subqueryDepth() const noexcept -> size_t { return _skipped.size(); } +auto SkipResult::reset() -> void { + for (size_t i = 0; i < _skipped.size(); ++i) { + _skipped[i] = 0; + } +} + +auto SkipResult::merge(SkipResult const& other, bool excludeTopLevel) noexcept -> void { + _skipped.reserve(other.subqueryDepth()); + while (other.subqueryDepth() > subqueryDepth()) { + incrementSubquery(); + } + TRI_ASSERT(other._skipped.size() <= _skipped.size()); + for (size_t i = 0; i < other._skipped.size(); ++i) { + if (excludeTopLevel && i + 1 == other._skipped.size()) { + // Do not copy top level + continue; + } + _skipped[i] += other._skipped[i]; + } +} + auto SkipResult::operator+=(SkipResult const& b) noexcept -> SkipResult& { didSkip(b.getSkipCount()); return *this; diff --git a/arangod/Aql/SkipResult.h b/arangod/Aql/SkipResult.h index eff7a4f3efac..c3d615b9799b 100644 --- a/arangod/Aql/SkipResult.h +++ b/arangod/Aql/SkipResult.h @@ -62,6 +62,10 @@ class SkipResult { auto subqueryDepth() const noexcept -> size_t; + auto reset() -> void; + + auto merge(SkipResult const& other, bool excludeTopLevel) noexcept -> void; + auto operator+=(SkipResult const& b) noexcept -> SkipResult&; auto operator==(SkipResult const& b) const noexcept -> bool; diff --git a/tests/Aql/SkipResultTest.cpp b/tests/Aql/SkipResultTest.cpp index 0f49039efaf7..9e2b9605503d 100644 --- a/tests/Aql/SkipResultTest.cpp +++ b/tests/Aql/SkipResultTest.cpp @@ -200,6 +200,68 @@ TEST_F(SkipResultTest, equality) { } } +TEST_F(SkipResultTest, merge_with_toplevel) { + SkipResult a{}; + a.didSkip(12); + a.incrementSubquery(); + a.didSkip(8); + + SkipResult b{}; + b.didSkip(9); + b.incrementSubquery(); + b.didSkip(2); + + a.merge(b, true); + + SkipResult expected{}; + expected.didSkip(12); + expected.didSkip(9); + expected.incrementSubquery(); + expected.didSkip(8); + expected.didSkip(2); + EXPECT_EQ(a, expected); +} + +TEST_F(SkipResultTest, merge_without_toplevel) { + SkipResult a{}; + a.didSkip(12); + a.incrementSubquery(); + a.didSkip(8); + + SkipResult b{}; + b.didSkip(9); + b.incrementSubquery(); + b.didSkip(2); + + a.merge(b, false); + + SkipResult expected{}; + expected.didSkip(12); + expected.didSkip(9); + expected.incrementSubquery(); + expected.didSkip(8); + EXPECT_EQ(a, expected); +} + +TEST_F(SkipResultTest, reset) { + SkipResult a{}; + a.didSkip(12); + a.incrementSubquery(); + a.didSkip(8); + + EXPECT_EQ(a.getSkipCount(), 8); + EXPECT_EQ(a.subqueryDepth(), 2); + EXPECT_FALSE(a.nothingSkipped()); + a.reset(); + + EXPECT_EQ(a.getSkipCount(), 0); + EXPECT_EQ(a.subqueryDepth(), 2); + EXPECT_TRUE(a.nothingSkipped()); + + a.decrementSubquery(); + EXPECT_EQ(a.getSkipCount(), 0); +} + } // namespace aql } // namespace tests } // namespace arangodb From fb6b9e1148eefa6c830e6ba1a2bd14d93eefd597 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Mon, 2 Mar 2020 10:03:24 +0100 Subject: [PATCH 32/71] Fixed return state of GatherNode --- arangod/Aql/UnsortedGatherExecutor.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/arangod/Aql/UnsortedGatherExecutor.cpp b/arangod/Aql/UnsortedGatherExecutor.cpp index 2a8041bf7e79..ef2babd051f2 100644 --- a/arangod/Aql/UnsortedGatherExecutor.cpp +++ b/arangod/Aql/UnsortedGatherExecutor.cpp @@ -88,7 +88,6 @@ auto UnsortedGatherExecutor::skipRowsRange(typename Fetcher::DataRange& input, A while (call.needSkipMore() && !done()) { if (input.hasDataRow(currentDependency())) { auto [state, inputRow] = input.nextDataRow(currentDependency()); - call.didSkip(1); if (state == ExecutorState::DONE) { @@ -104,6 +103,10 @@ auto UnsortedGatherExecutor::skipRowsRange(typename Fetcher::DataRange& input, A } } + while (!done() && input.upstreamState(currentDependency()) == ExecutorState::DONE) { + advanceDependency(); + } + if (done()) { // here currentDependency is invalid which will cause things to crash // if we ask upstream in ExecutionBlockImpl. yolo. From 6be31802c80ed21b89eb129377bd95e654555302 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Mon, 2 Mar 2020 10:47:51 +0100 Subject: [PATCH 33/71] Activate all spliced subquery tests \o/ --- arangod/Aql/AllRowsFetcher.cpp | 7 +-- arangod/Aql/ExecutionBlockImpl.cpp | 37 +++++++------ arangod/Aql/OptimizerRules.cpp | 32 +++++------ tests/Aql/SpliceSubqueryOptimizerRuleTest.cpp | 53 ++++++++----------- 4 files changed, 59 insertions(+), 70 deletions(-) diff --git a/arangod/Aql/AllRowsFetcher.cpp b/arangod/Aql/AllRowsFetcher.cpp index 26712ae18b44..98096c152342 100644 --- a/arangod/Aql/AllRowsFetcher.cpp +++ b/arangod/Aql/AllRowsFetcher.cpp @@ -79,10 +79,11 @@ std::tuple AllRowsFetcher:: TRI_ASSERT(!_aqlItemMatrix->stoppedOnShadowRow()); while (true) { auto [state, skipped, block] = _dependencyProxy->execute(stack); - TRI_ASSERT(skipped.nothingSkipped()); + TRI_ASSERT(skipped.getSkipCount() == 0); // we will either build a complete fetched AqlItemBlockInputMatrix or return an empty one if (state == ExecutionState::WAITING) { + TRI_ASSERT(skipped.nothingSkipped()); TRI_ASSERT(block == nullptr); // On waiting we have nothing to return return {state, SkipResult{}, AqlItemBlockInputMatrix{ExecutorState::HASMORE}}; @@ -97,10 +98,10 @@ std::tuple AllRowsFetcher:: // If we find a ShadowRow or ExecutionState == Done, we're done fetching. if (_aqlItemMatrix->stoppedOnShadowRow() || state == ExecutionState::DONE) { if (state == ExecutionState::HASMORE) { - return {state, SkipResult{}, + return {state, skipped, AqlItemBlockInputMatrix{ExecutorState::HASMORE, _aqlItemMatrix.get()}}; } - return {state, SkipResult{}, + return {state, skipped, AqlItemBlockInputMatrix{ExecutorState::DONE, _aqlItemMatrix.get()}}; } } diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index 457dead9c7c5..0b0e3d70ab30 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -1839,13 +1839,6 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { std::tie(_upstreamState, skippedLocal, _lastRange) = executeFetcher(stack, _requestedDependency); - - { - VPackBuilder skipRes; - skippedLocal.toVelocyPack(skipRes); - LOG_DEVEL << getPlanNode()->getTypeString() - << " From Upstream: " << skipRes.toJson(); - } if constexpr (std::is_same_v) { // Do not pop the call, we did not put it on. // However we need it for accounting later. @@ -1879,18 +1872,33 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { skippedLocal.decrementSubquery(); } + /* + { + VPackBuilder in; + VPackBuilder ex; + _skipped.toVelocyPack(ex); + skippedLocal.toVelocyPack(in); + LOG_DEVEL << "merging " << in.toJson() << " -> " << ex.toJson(); + } + */ if constexpr (skipRowsType() == SkipRowsRangeVariant::FETCHER) { // We skipped through passthrough, so count that a skip was solved. _skipped.merge(skippedLocal, false); clientCall.didSkip(skippedLocal.getSkipCount()); + } else if constexpr (is_one_of_v) { + // Subquery needs to include the topLevel Skip. + // But does not need to apply the count to clientCall. + _skipped.merge(skippedLocal, false); + // This is what has been asked for by the SubqueryEnd + auto subqueryCall = stack.popCall(); + subqueryCall.didSkip(skippedLocal.getSkipCount()); + stack.pushCall(std::move(subqueryCall)); } else { _skipped.merge(skippedLocal, true); } if constexpr (std::is_same_v) { // For the subqueryStart, we need to increment the SkipLevel by one // as we may trigger this multiple times, check if we need to do it. - LOG_DEVEL << "sq: " << stack.subqueryLevel() - << "skip: " << _skipped.subqueryDepth(); while (_skipped.subqueryDepth() < stack.subqueryLevel() + 1) { // In fact, we only need to increase by 1 TRI_ASSERT(_skipped.subqueryDepth() == stack.subqueryLevel()); @@ -1963,14 +1971,13 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { // We return skipped here, reset member SkipResult skipped = _skipped; - +/* { - VPackBuilder skipRes; - skipped.toVelocyPack(skipRes); - LOG_DEVEL << getPlanNode()->getTypeString() << " Returning: " << skipRes.toJson() - << " stack.sq: " << stack.subqueryLevel(); + VPackBuilder skippor; + skipped.toVelocyPack(skippor); + LOG_DEVEL << getPlanNode()->getTypeString() << " -> " << skippor.toJson(); } - +*/ #ifdef ARANGODB_ENABLE_MAINTAINER_MODE if constexpr (std::is_same_v) { TRI_ASSERT(skipped.subqueryDepth() == stack.subqueryLevel() /*we inected a call*/); diff --git a/arangod/Aql/OptimizerRules.cpp b/arangod/Aql/OptimizerRules.cpp index 0d344f22e0cb..9cd9e7b83bd2 100644 --- a/arangod/Aql/OptimizerRules.cpp +++ b/arangod/Aql/OptimizerRules.cpp @@ -74,9 +74,9 @@ namespace { -bool accessesCollectionVariable(arangodb::aql::ExecutionPlan const* plan, - arangodb::aql::ExecutionNode const* node, - ::arangodb::containers::HashSet& vars) { +bool accessesCollectionVariable( + arangodb::aql::ExecutionPlan const* plan, arangodb::aql::ExecutionNode const* node, + ::arangodb::containers::HashSet& vars) { using EN = arangodb::aql::ExecutionNode; if (node->getType() == EN::CALCULATION) { @@ -5796,9 +5796,9 @@ void arangodb::aql::optimizeTraversalsRule(Optimizer* opt, if (outVariable != nullptr && !n->isVarUsedLater(outVariable) && std::find(pruneVars.begin(), pruneVars.end(), outVariable) == pruneVars.end()) { outVariable = traversal->pathOutVariable(); - if (outVariable == nullptr || - (!n->isVarUsedLater(outVariable) && - std::find(pruneVars.begin(), pruneVars.end(), outVariable) == pruneVars.end())) { + if (outVariable == nullptr || (!n->isVarUsedLater(outVariable) && + std::find(pruneVars.begin(), pruneVars.end(), + outVariable) == pruneVars.end())) { // both traversal vertex and path outVariables not used later traversal->options()->setProduceVertices(false); modified = true; @@ -7275,12 +7275,12 @@ void arangodb::aql::moveFiltersIntoEnumerateRule(Optimizer* opt, ExecutionNode* filterParent = current->getFirstParent(); TRI_ASSERT(filterParent != nullptr); plan->unlinkNode(current); - + if (!current->isVarUsedLater(cn->outVariable())) { // also remove the calculation node plan->unlinkNode(cn); } - + current = filterParent; modified = true; } else if (current->getType() == EN::CALCULATION) { @@ -7403,19 +7403,11 @@ bool nodeMakesThisQueryLevelUnsuitableForSubquerySplicing(ExecutionNode const* n case ExecutionNode::DISTRIBUTE_CONSUMER: case ExecutionNode::SUBQUERY_START: case ExecutionNode::SUBQUERY_END: - // These nodes do not initiate a skip themselves, and thus are fine. - return false; case ExecutionNode::NORESULTS: - // no results currently cannot work, as they do not fetch from above. case ExecutionNode::LIMIT: - // limit blocks currently cannot work, both due to skipping and due to the - // limit and passthrough, which forbids passing shadow rows. - return true; - case ExecutionNode::COLLECT: { - auto const collectNode = ExecutionNode::castTo(node); - // Collect nodes skip iff using the COUNT method. - return collectNode->aggregationMethod() == CollectOptions::CollectMethod::COUNT; - } + case ExecutionNode::COLLECT: + // These nodes are fine + return false; case ExecutionNode::MAX_NODE_TYPE_VALUE: break; } @@ -7425,7 +7417,7 @@ bool nodeMakesThisQueryLevelUnsuitableForSubquerySplicing(ExecutionNode const* n "report this error. Try turning off the splice-subqueries rule to get " "your query working.", node->getTypeString().c_str()); -} +} // namespace void findSubqueriesSuitableForSplicing(ExecutionPlan const& plan, containers::SmallVector& result) { diff --git a/tests/Aql/SpliceSubqueryOptimizerRuleTest.cpp b/tests/Aql/SpliceSubqueryOptimizerRuleTest.cpp index 9e84ab6b17e6..30d1b648c61e 100644 --- a/tests/Aql/SpliceSubqueryOptimizerRuleTest.cpp +++ b/tests/Aql/SpliceSubqueryOptimizerRuleTest.cpp @@ -310,52 +310,44 @@ TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_subquery_with_sort) { verifySubquerySplicing(query, 1); auto expected = arangodb::velocypack::Parser::fromJson(R"([[6,5,4,3,2,1], [12,10,8,6,4,2]])"); - verifyQueryResult(query, expected->slice()); + verifyQueryResult(query, expected->slice(), R"({})", R"({"profile": 3})"); } -// Must be changed as soon as the subquery implementation with shadow rows handle skipping, -// and the splice-subqueries optimizer rule is changed to allow it. -TEST_F(SpliceSubqueryNodeOptimizerRuleTest, dont_splice_subquery_with_skip__inner_limit_offset) { +TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_subquery_with_skip__inner_limit_offset) { auto const queryString = R"aql(FOR i IN 0..2 LET a = (FOR j IN 0..2 LIMIT 1, 1 RETURN 3*i + j) RETURN FIRST(a))aql"; auto const expectedString = R"res([1, 4, 7])res"; - verifySubquerySplicing(queryString, 0, 1); + verifySubquerySplicing(queryString, 1); auto expected = arangodb::velocypack::Parser::fromJson(expectedString); verifyQueryResult(queryString, expected->slice()); } -// Must be changed as soon as the subquery implementation with shadow rows handle skipping, -// and the splice-subqueries optimizer rule is changed to allow it. -TEST_F(SpliceSubqueryNodeOptimizerRuleTest, dont_splice_subquery_with_skip__outer_limit_offset) { +TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_subquery_with_skip__outer_limit_offset) { auto const queryString = R"aql(FOR i IN 0..2 LET a = (FOR j IN 0..2 RETURN 3*i + j) LIMIT 1, 1 RETURN FIRST(a))aql"; auto const expectedString = R"res([3])res"; - verifySubquerySplicing(queryString, 0, 1); + verifySubquerySplicing(queryString, 1); auto expected = arangodb::velocypack::Parser::fromJson(expectedString); verifyQueryResult(queryString, expected->slice()); } -// Must be changed as soon as the subquery implementation with shadow rows handle skipping, -// and the splice-subqueries optimizer rule is changed to allow it. -TEST_F(SpliceSubqueryNodeOptimizerRuleTest, dont_splice_subquery_with_skip__inner_collect_count) { +TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_subquery_with_skip__inner_collect_count) { auto const queryString = R"aql(FOR i IN 0..2 LET a = (FOR j IN 0..i COLLECT WITH COUNT INTO n RETURN n) RETURN FIRST(a))aql"; auto const expectedString = R"res([1, 2, 3])res"; - verifySubquerySplicing(queryString, 0, 1); + verifySubquerySplicing(queryString, 1); auto expected = arangodb::velocypack::Parser::fromJson(expectedString); verifyQueryResult(queryString, expected->slice()); } -// Must be changed as soon as the subquery implementation with shadow rows handle skipping, -// and the splice-subqueries optimizer rule is changed to allow it. -TEST_F(SpliceSubqueryNodeOptimizerRuleTest, dont_splice_subquery_with_skip__outer_collect_count) { +TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_subquery_with_skip__outer_collect_count) { // the RAND() is there to avoid the subquery being removed auto const queryString = R"aql(FOR i IN 0..2 LET a = (FOR j IN 0..FLOOR(2*RAND()) RETURN 1) @@ -363,14 +355,14 @@ TEST_F(SpliceSubqueryNodeOptimizerRuleTest, dont_splice_subquery_with_skip__oute RETURN n)aql"; auto const expectedString = R"res([3])res"; - verifySubquerySplicing(queryString, 0, 1); + verifySubquerySplicing(queryString, 1); auto expected = arangodb::velocypack::Parser::fromJson(expectedString); verifyQueryResult(queryString, expected->slice()); } // Must be changed as soon as the subquery implementation with shadow rows handle skipping, // and the splice-subqueries optimizer rule is changed to allow it. -TEST_F(SpliceSubqueryNodeOptimizerRuleTest, dont_splice_subquery_with_skip__full_count) { +TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_subquery_with_skip__full_count) { // the RAND() is there to avoid the subquery being removed auto const queryString = R"aql(FOR i IN 0..2 LET a = (FOR j IN 0..FLOOR(2*RAND()) RETURN 1) @@ -378,7 +370,7 @@ TEST_F(SpliceSubqueryNodeOptimizerRuleTest, dont_splice_subquery_with_skip__full RETURN i)aql"; auto const expectedString = R"res([0])res"; - verifySubquerySplicing(queryString, 0, 1, "{}", R"opts({"fullCount": true})opts"); + verifySubquerySplicing(queryString, 1, 0, "{}", R"opts({"fullCount": true})opts"); auto expected = arangodb::velocypack::Parser::fromJson(expectedString); verifyQueryResult(queryString, expected->slice()); } @@ -412,7 +404,7 @@ TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_nested_subquery_with_innermos FOR i IN 0..1 LET js = ( // this subquery should be spliced FOR j IN 0..1 + FLOOR(RAND()) - LET ks = ( // this subquery should not be spliced + LET ks = ( // this subquery should be spliced FOR k IN 0..2 + FLOOR(RAND()) LIMIT 1, 2 RETURN 6*i + 3*j + k @@ -423,7 +415,7 @@ TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_nested_subquery_with_innermos )aql"; auto const expectedString = R"res([[[1, 2], [4, 5]], [[7, 8], [10, 11]]])res"; - verifySubquerySplicing(queryString, 1, 1); + verifySubquerySplicing(queryString, 2); auto expected = arangodb::velocypack::Parser::fromJson(expectedString); verifyQueryResult(queryString, expected->slice()); } @@ -440,7 +432,7 @@ TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_nested_subquery_with_innermos )aql"; auto const expectedString = R"res([{"a": 1, "b": [[3, 4]]}, {"a": 2, "b": [[3, 4]]}])res"; - verifySubquerySplicing(queryString, 1, 1); + verifySubquerySplicing(queryString, 2); auto expected = arangodb::velocypack::Parser::fromJson(expectedString); verifyQueryResult(queryString, expected->slice()); } @@ -450,7 +442,7 @@ TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_nested_subquery_with_innermos TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_nested_subquery_with_outermost_skip) { auto const queryString = R"aql( FOR i IN 0..2 - LET js = ( // this subquery should not be spliced + LET js = ( // this subquery should be spliced FOR j IN 0..1 + FLOOR(RAND()) LET ks = ( // this subquery should be spliced FOR k IN 0..1 + FLOOR(RAND()) @@ -463,19 +455,19 @@ TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_nested_subquery_with_outermos )aql"; auto const expectedString = R"res([[[4, 5], [6, 7]], [[8, 9], [10, 11]]])res"; - verifySubquerySplicing(queryString, 1, 1); + verifySubquerySplicing(queryString, 2); auto expected = arangodb::velocypack::Parser::fromJson(expectedString); verifyQueryResult(queryString, expected->slice()); } // Must be changed as soon as the subquery implementation with shadow rows handle skipping, // and the splice-subqueries optimizer rule is changed to allow it. -TEST_F(SpliceSubqueryNodeOptimizerRuleTest, dont_splice_subquery_with_limit_and_no_offset) { +TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_subquery_with_limit_and_no_offset) { auto query = R"aql( FOR i IN 2..4 LET a = (FOR j IN [i, i+10, i+20] LIMIT 0, 1 RETURN j) RETURN FIRST(a))aql"; - verifySubquerySplicing(query, 0, 1); + verifySubquerySplicing(query, 1); auto expected = arangodb::velocypack::Parser::fromJson(R"([2, 3, 4])"); verifyQueryResult(query, expected->slice()); @@ -521,7 +513,7 @@ TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_subquery_with_upsert) { auto const bindString = R"bind({"key": "myKey"})bind"; auto const expectedString = R"res([["UnitTestCollection/myKey"]])res"; - verifySubquerySplicing(queryString, 1, 1, bindString); + verifySubquerySplicing(queryString, 2, 0, bindString); auto expected = arangodb::velocypack::Parser::fromJson(expectedString); verifyQueryResult(queryString, expected->slice(), bindString); @@ -781,8 +773,7 @@ TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_subquery_with_collect_in_subq verifyQueryResult(queryString, expected->slice()); } -// Disabled as long as the subquery implementation with shadow rows cannot yet handle skipping. -TEST_F(SpliceSubqueryNodeOptimizerRuleTest, DISABLED_splice_subquery_with_limit_and_offset) { +TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_subquery_with_limit_and_offset) { auto query = R"aql( FOR i IN 2..4 LET a = (FOR j IN [0, i, i+10] LIMIT 1, 1 RETURN j) @@ -793,9 +784,7 @@ TEST_F(SpliceSubqueryNodeOptimizerRuleTest, DISABLED_splice_subquery_with_limit_ verifyQueryResult(query, expected->slice()); } -// Disabled as long as the subquery implementation with shadow rows cannot yet handle skipping. -TEST_F(SpliceSubqueryNodeOptimizerRuleTest, - DISABLED_splice_subquery_collect_within_empty_nested_subquery) { +TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_subquery_collect_within_empty_nested_subquery) { auto query = R"aql( FOR k IN 1..2 LET sub1 = ( From 6e8855b2c524c70afac33aa7b727e52e25b06c14 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Mon, 2 Mar 2020 14:40:16 +0100 Subject: [PATCH 34/71] Let SubqueryEnd honor the client request --- arangod/Aql/ExecutionBlockImpl.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index 0b0e3d70ab30..2df56f81dc7a 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -1407,6 +1407,10 @@ auto ExecutionBlockImpl::shadowRowForwarding() -> ExecState // We still have shadowRows, we // need to forward them return ExecState::SHADOWROWS; + } else if (_outputItemRow->isFull()) { + // Fullfilled the call + // Need to return! + return ExecState::DONE; } else { if (didConsume) { // We did only consume the input @@ -1979,10 +1983,12 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { } */ #ifdef ARANGODB_ENABLE_MAINTAINER_MODE - if constexpr (std::is_same_v) { - TRI_ASSERT(skipped.subqueryDepth() == stack.subqueryLevel() /*we inected a call*/); - } else { - TRI_ASSERT(skipped.subqueryDepth() == stack.subqueryLevel() + 1 /*we took our call*/); + if (!stack.is36Compatible()) { + if constexpr (std::is_same_v) { + TRI_ASSERT(skipped.subqueryDepth() == stack.subqueryLevel() /*we inected a call*/); + } else { + TRI_ASSERT(skipped.subqueryDepth() == stack.subqueryLevel() + 1 /*we took our call*/); + } } #endif From c03498eec2ee0b5e5b3fe9ec8c56ab00f310d3b4 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Mon, 2 Mar 2020 14:40:42 +0100 Subject: [PATCH 35/71] Added a Maintainer only test for the stack, if it is 36 compatible --- arangod/Aql/AqlCallStack.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arangod/Aql/AqlCallStack.h b/arangod/Aql/AqlCallStack.h index f67e505ddffc..1a6ef2c5ca21 100644 --- a/arangod/Aql/AqlCallStack.h +++ b/arangod/Aql/AqlCallStack.h @@ -92,6 +92,8 @@ class AqlCallStack { void toVelocyPack(velocypack::Builder& builder) const; + auto is36Compatible() const noexcept -> bool { return _compatibilityMode3_6; } + private: explicit AqlCallStack(std::stack&& operations); From d766db9f657e6376003c05f7779abd1bd547ffb2 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Tue, 3 Mar 2020 10:26:18 +0100 Subject: [PATCH 36/71] Added first part of side-effect executors. They now send upstream a fetch-all stack. Need to fix downstream reporting, and call the executor with hardLimit 0 --- arangod/Aql/AqlCallStack.cpp | 83 +++++++++++++++--------------- arangod/Aql/ExecutionBlockImpl.cpp | 33 ++++++++++++ arangod/Aql/SkipResult.cpp | 7 +++ arangod/Aql/SkipResult.h | 3 ++ tests/Aql/SkipResultTest.cpp | 4 +- 5 files changed, 86 insertions(+), 44 deletions(-) diff --git a/arangod/Aql/AqlCallStack.cpp b/arangod/Aql/AqlCallStack.cpp index 68c5cd9c5b8d..66a68d4b56fa 100644 --- a/arangod/Aql/AqlCallStack.cpp +++ b/arangod/Aql/AqlCallStack.cpp @@ -42,7 +42,7 @@ AqlCallStack::AqlCallStack(AqlCallStack const& other, AqlCall call) // We can only use this constructor on relevant levels // Alothers need to use passThrough constructor TRI_ASSERT(other._depth == 0); - _operations.push(std::move(call)); + _operations.emplace_back(std::move(call)); _compatibilityMode3_6 = other._compatibilityMode3_6; } @@ -51,7 +51,7 @@ AqlCallStack::AqlCallStack(AqlCallStack const& other) _depth(other._depth), _compatibilityMode3_6(other._compatibilityMode3_6) {} -AqlCallStack::AqlCallStack(std::stack&& operations) +AqlCallStack::AqlCallStack(std::vector&& operations) : _operations(std::move(operations)) {} bool AqlCallStack::isRelevant() const { return _depth == 0; } @@ -68,34 +68,23 @@ AqlCall AqlCallStack::popCall() { // to the upwards subquery. // => Simply put another fetchAll Call on the stack. // This code is to be removed in the next version after 3.7 - _operations.push(AqlCall{}); + _operations.emplace_back(AqlCall{}); } - auto call = _operations.top(); - _operations.pop(); + auto call = _operations.back(); + _operations.pop_back(); return call; } AqlCall const& AqlCallStack::peek() const { TRI_ASSERT(isRelevant()); TRI_ASSERT(!_operations.empty()); - return _operations.top(); + return _operations.back(); } void AqlCallStack::pushCall(AqlCall&& call) { // TODO is this correct on subqueries? TRI_ASSERT(isRelevant()); - _operations.push(call); -} - -void AqlCallStack::stackUpMissingCalls() { - while (!isRelevant()) { - // For every depth, we add an additional default call. - // The default is to produce unlimited many results, - // using DefaultBatchSize each. - _operations.emplace(AqlCall{}); - _depth--; - } - TRI_ASSERT(isRelevant()); + _operations.emplace_back(std::move(call)); } void AqlCallStack::pop() { @@ -127,8 +116,9 @@ auto AqlCallStack::fromVelocyPack(velocypack::Slice const slice) -> ResultT{}; + auto stack = std::vector{}; auto i = std::size_t{0}; + stack.reserve(slice.length()); for (auto const entry : VPackArrayIterator(slice)) { auto maybeAqlCall = AqlCall::fromVelocyPack(entry); @@ -140,7 +130,7 @@ auto AqlCallStack::fromVelocyPack(velocypack::Slice const slice) -> ResultT ResultT{}; - reverseStack.reserve(_operations.size()); - { - auto ops = _operations; - while (!ops.empty()) { - reverseStack.emplace_back(ops.top()); - ops.pop(); - } - } - builder.openArray(); - for (auto it = reverseStack.rbegin(); it != reverseStack.rend(); ++it) { - auto const& call = *it; + for (auto const& call : _operations) { call.toVelocyPack(builder); } builder.close(); @@ -172,19 +151,39 @@ void AqlCallStack::toVelocyPack(velocypack::Builder& builder) const { auto AqlCallStack::toString() const -> std::string { auto result = std::string{}; result += "["; - auto ops = _operations; - if (!ops.empty()) { - auto op = ops.top(); - ops.pop(); + bool isFirst = true; + for (auto const& op : _operations) { + if (!isFirst) { + result += ","; + } + isFirst = false; result += " "; result += op.toString(); - while (!ops.empty()) { - op = ops.top(); - ops.pop(); - result += ", "; - result += op.toString(); - } } result += " ]"; return result; } + +auto AqlCallStack::createEquivalentFetchAllShadowRowsStack() const -> AqlCallStack { + AqlCallStack res{*this}; + if (subqueryLevel() > 1) { + // We only replace the subquery levels. + // The releveant call may include a softLimit + // which needs to be honored here. + + std::replace_if( + res._operations.begin(), res._operations.end() - 1, + [](auto const&) -> bool { return true; }, AqlCall{}); + } + return res; +} + +auto AqlCallStack::needToSkipSubquery() const noexcept -> bool { + if (subqueryLevel() > 1) { + return std::any_of(_operations.begin(), _operations.end() - 1, + [](AqlCall const& call) -> bool { + return call.needSkipMore(); + }); + } + return false; +} \ No newline at end of file diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index 2df56f81dc7a..364e7cb20861 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -130,6 +130,23 @@ class TestLambdaSkipExecutor; template constexpr bool is_one_of_v = (std::is_same_v || ...); +/* + * Determine whether an executor cannot bypass subquery skips. + * This is if exection of this Executor does have side-effects + * other then it's own result. + */ + +template +constexpr bool executorHasSideEffects = + is_one_of_v, + ModificationExecutor, InsertModifier>, + ModificationExecutor, + ModificationExecutor, RemoveModifier>, + ModificationExecutor, + ModificationExecutor, UpdateReplaceModifier>, + ModificationExecutor, + ModificationExecutor, UpsertModifier>>; + /* * Determine whether we execute new style or old style skips, i.e. pre or post shadow row introduction * TODO: This should be removed once all executors and fetchers are ported to the new style. @@ -1232,6 +1249,12 @@ auto ExecutionBlockImpl::executeFetcher(AqlCallStack& stack, size_t co (void)dependency; if constexpr (isNewStyleExecutor) { if constexpr (is_one_of_v) { + static_assert( + !executorHasSideEffects, + "there is a special implementation for side-effect executors to " + "exchange the stack. For the MultiDependencyFetcher this special " + "case is not implemented. There is no reason to disallow this " + "case here however, it is just not needed thus far."); // TODO: This is a hack to guarantee we have enough space in our range // to fit all inputs, in particular the one executed below TRI_ASSERT(dependency < _dependencies.size()); @@ -1242,6 +1265,16 @@ auto ExecutionBlockImpl::executeFetcher(AqlCallStack& stack, size_t co _lastRange.setDependency(dependency, range); return {state, skipped, _lastRange}; + } else if constexpr (executorHasSideEffects) { + // If the executor has side effects, we cannot bypass any subqueries + // by skipping them. SO we need to fetch all shadow rows in order to + // trigger this Executor with everthing from above. + // NOTE: The Executor needs to discard shadowRows, and do the + auto fetchAllStack = stack.createEquivalentFetchAllShadowRowsStack(); + auto res = _rowFetcher.execute(fetchAllStack); + // Just make sure we did not Skip anything + TRI_ASSERT(std::get(res).nothingSkipped()); + return res; } else { return _rowFetcher.execute(stack); } diff --git a/arangod/Aql/SkipResult.cpp b/arangod/Aql/SkipResult.cpp index 768b95a525a1..6e721e6bd7fc 100644 --- a/arangod/Aql/SkipResult.cpp +++ b/arangod/Aql/SkipResult.cpp @@ -151,4 +151,11 @@ auto SkipResult::operator==(SkipResult const& b) const noexcept -> bool { auto SkipResult::operator!=(SkipResult const& b) const noexcept -> bool { return !(*this == b); +} + +std::ostream& operator<<(std::ostream& stream, arangodb::aql::SkipResult const& result) { + VPackBuilder temp; + result.toVelocyPack(temp); + stream << temp.toJson(); + return stream; } \ No newline at end of file diff --git a/arangod/Aql/SkipResult.h b/arangod/Aql/SkipResult.h index c3d615b9799b..b3883cac0907 100644 --- a/arangod/Aql/SkipResult.h +++ b/arangod/Aql/SkipResult.h @@ -76,4 +76,7 @@ class SkipResult { }; } // namespace arangodb::aql + +std::ostream& operator<<(std::ostream&, arangodb::aql::SkipResult const&); + #endif \ No newline at end of file diff --git a/tests/Aql/SkipResultTest.cpp b/tests/Aql/SkipResultTest.cpp index 9e2b9605503d..958ab0ad5563 100644 --- a/tests/Aql/SkipResultTest.cpp +++ b/tests/Aql/SkipResultTest.cpp @@ -211,7 +211,7 @@ TEST_F(SkipResultTest, merge_with_toplevel) { b.incrementSubquery(); b.didSkip(2); - a.merge(b, true); + a.merge(b, false); SkipResult expected{}; expected.didSkip(12); @@ -233,7 +233,7 @@ TEST_F(SkipResultTest, merge_without_toplevel) { b.incrementSubquery(); b.didSkip(2); - a.merge(b, false); + a.merge(b, true); SkipResult expected{}; expected.didSkip(12); From e6a8dcb66f75df2b375dc764065071eca932bc9b Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Tue, 3 Mar 2020 13:54:01 +0100 Subject: [PATCH 37/71] Add a fake FASTFORWARD call into a subquery-skipped ModificationExecutor. --- arangod/Aql/AqlCallStack.h | 35 +++++++++++++++++++-------- arangod/Aql/ExecutionBlockImpl.cpp | 38 +++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/arangod/Aql/AqlCallStack.h b/arangod/Aql/AqlCallStack.h index 1a6ef2c5ca21..f71b18e706ce 100644 --- a/arangod/Aql/AqlCallStack.h +++ b/arangod/Aql/AqlCallStack.h @@ -63,14 +63,6 @@ class AqlCallStack { // Put another call on top of the stack. void pushCall(AqlCall&& call); - // fill up all missing calls within this stack s.t. we reach depth == 0 - // This needs to be called if an executor requires to be fully executed, even if skipped, - // even if the subquery it is located in is skipped. - // The default operations added here will correspond to produce all Rows, unlimitted. - // e.g. every Modification Executor needs to call this functionality, as modifictions need to be - // performed even if skipped. - void stackUpMissingCalls(); - // Pops one subquery level. // if this isRelevent it pops the top-most call from the stack. // if this is not revelent it reduces the depth by 1. @@ -94,12 +86,35 @@ class AqlCallStack { auto is36Compatible() const noexcept -> bool { return _compatibilityMode3_6; } + /** + * @brief Create an equivalent call stack that does a full-produce + * of all Subquery levels. This is required for blocks + * that are not allowed to be bpassed. + * The top-most call remains unmodified, as the Executor might + * require some soft limit on it. + * + * @return AqlCallStack a stack of equivalent size, that does not skip + * on any lower subquery. + */ + auto createEquivalentFetchAllShadowRowsStack() const -> AqlCallStack; + + /** + * @brief Check if we are in a subquery that is in-fact required to + * be skipped. This is relevant for executors that have created + * an equivalentFetchAllShadowRows stack, in order to decide if + * the need to produce output or if they are skipped. + * + * @return true + * @return false + */ + auto needToSkipSubquery() const noexcept -> bool; + private: - explicit AqlCallStack(std::stack&& operations); + explicit AqlCallStack(std::vector&& operations); private: // The list of operations, stacked by depth (e.g. bottom element is from main query) - std::stack _operations; + std::vector _operations; // The depth of subqueries that have not issued calls into operations, // as they have been skipped. diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index 364e7cb20861..a6126eac68bb 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -1229,14 +1229,7 @@ static auto fastForwardType(AqlCall const& call, Executor const& e) -> FastForwa } // TODO: We only need to do this is the executor actually require to call. // e.g. Modifications will always need to be called. Limit only if it needs to report fullCount - if constexpr (is_one_of_v, - ModificationExecutor, InsertModifier>, - ModificationExecutor, - ModificationExecutor, RemoveModifier>, - ModificationExecutor, - ModificationExecutor, UpdateReplaceModifier>, - ModificationExecutor, - ModificationExecutor, UpsertModifier>>) { + if constexpr (std::is_same_v || executorHasSideEffects) { return FastForwardVariant::EXECUTOR; } return FastForwardVariant::FETCHER; @@ -1676,6 +1669,16 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { case ExecState::CHECKCALL: { LOG_QUERY("cfe46", DEBUG) << printTypeInfo() << " determine next action on call " << clientCall; + + if constexpr (executorHasSideEffects) { + // If the executor has sideEffects, and we need to skip the results we would + // produce here because we actually skip the subquery, we instead do a + // hardLimit 0 (aka FastForward) call instead to the local Executor + if (stack.needToSkipSubquery()) { + _execState = ExecState::FASTFORWARD; + break; + } + } _execState = nextState(clientCall); break; } @@ -1830,8 +1833,25 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { case ExecState::FASTFORWARD: { LOG_QUERY("96e2c", DEBUG) << printTypeInfo() << " all produced, fast forward to end up (sub-)query."; + AqlCall callCopy = clientCall; + if constexpr (executorHasSideEffects) { + if (stack.needToSkipSubquery()) { + // Fast Forward call. + callCopy = AqlCall{0, false, 0, AqlCall::LimitType::HARD}; + } + } auto [state, stats, skippedLocal, call, dependency] = - executeFastForward(_lastRange, clientCall); + executeFastForward(_lastRange, callCopy); + if constexpr (executorHasSideEffects) { + if (!stack.needToSkipSubquery()) { + // We need to modify the original call. + clientCall = callCopy; + } + // else: We are bypassing the results. + // Do not count them here. + } else { + clientCall = callCopy; + } _requestedDependency = dependency; _skipped.didSkip(skippedLocal); From 5659bc2cc3f64526d3b04f9898827093dc161eb0 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Tue, 3 Mar 2020 17:35:43 +0100 Subject: [PATCH 38/71] Added helper shadow row function for SideEffect executors --- arangod/Aql/ExecutionBlockImpl.cpp | 16 +++++++++++++++- arangod/Aql/ExecutionBlockImpl.h | 8 ++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index a6126eac68bb..c90739d83c40 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -1449,6 +1449,18 @@ auto ExecutionBlockImpl::shadowRowForwarding() -> ExecState } } +template +auto ExecutionBlockImpl::sideEffectShadowRowForwarding(AqlCallStack& stack) -> ExecState { + static_assert(executorHasSideEffects); + if (!stack.needToSkipSubquery()) { + // We need to really produce things here + // fall back to original version as any other executor. + return shadowRowForwarding(); + } + // TODO implemenet ShadowRowHandling + return shadowRowForwarding(); +} + template auto ExecutionBlockImpl::shadowRowForwarding() -> ExecState { TRI_ASSERT(_outputItemRow); @@ -1999,7 +2011,9 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { } TRI_ASSERT(!_outputItemRow->allRowsUsed()); - + if constexpr (executorHasSideEffects) { + _execState = sideEffectShadowRowForwarding(stack); + } // This may write one or more rows. _execState = shadowRowForwarding(); if constexpr (!std::is_same_v) { diff --git a/arangod/Aql/ExecutionBlockImpl.h b/arangod/Aql/ExecutionBlockImpl.h index c5033cce6f39..ce6b9468264b 100644 --- a/arangod/Aql/ExecutionBlockImpl.h +++ b/arangod/Aql/ExecutionBlockImpl.h @@ -308,6 +308,14 @@ class ExecutionBlockImpl final : public ExecutionBlock { void resetExecutor(); + // Forwarding of ShadowRows if the executor has SideEffects. + // This skips over ShadowRows, and counts them in the correct + // position of the callStack as "skipped". + // as soon as we reach a place where there is no skip + // ordered in the outer shadow rows, this call + // will fall back to shadowRowForwardning. + [[nodiscard]] auto sideEffectShadowRowForwarding(AqlCallStack& stack) -> ExecState; + private: /** * @brief Used to allow the row Fetcher to access selected methods of this From b1c6af5674de445f9e3d6343feb0e3cad422588c Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 4 Mar 2020 09:34:09 +0100 Subject: [PATCH 39/71] Let the Modification Executor also produce data, even if no FullCount is used. --- arangod/Aql/ModificationExecutor.cpp | 12 ++++++++---- arangod/Aql/SkipResult.cpp | 8 ++++++++ arangod/Aql/SkipResult.h | 2 ++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/arangod/Aql/ModificationExecutor.cpp b/arangod/Aql/ModificationExecutor.cpp index 6554c6574fed..2c052fca9980 100644 --- a/arangod/Aql/ModificationExecutor.cpp +++ b/arangod/Aql/ModificationExecutor.cpp @@ -201,7 +201,6 @@ template doCollect(input, output.numRowsLeft()); upstreamState = input.upstreamState(); } - if (_modifier.nrOfOperations() > 0) { _modifier.transact(); @@ -234,7 +233,10 @@ template // only produce at most output.numRowsLeft() many results ExecutorState upstreamState = input.upstreamState(); - while (input.hasDataRow() && call.needSkipMore()) { + // These executors need to be executed on HARD LIMIT 0. + // If we do a fullCount or not. + while (input.hasDataRow() && + (call.getOffset() > 0 || (call.getLimit() == 0 && call.hasHardLimit()))) { _modifier.reset(); size_t toSkip = call.getOffset(); if (call.getLimit() == 0 && call.hasHardLimit()) { @@ -266,8 +268,10 @@ template stats.addWritesExecuted(_modifier.nrOfWritesExecuted()); stats.addWritesIgnored(_modifier.nrOfWritesIgnored()); } - - call.didSkip(_modifier.nrOfOperations()); + if (call.needSkipMore()) { + // We only need to report skip, if we actually skip. + call.didSkip(_modifier.nrOfOperations()); + } } } diff --git a/arangod/Aql/SkipResult.cpp b/arangod/Aql/SkipResult.cpp index 6e721e6bd7fc..2b64063ba876 100644 --- a/arangod/Aql/SkipResult.cpp +++ b/arangod/Aql/SkipResult.cpp @@ -45,6 +45,14 @@ auto SkipResult::didSkip(size_t skipped) -> void { _skipped.back() += skipped; } +auto SkipResult::didSkipSubquery(size_t skipped, size_t depth) -> void { + TRI_ASSERT(!_skipped.empty()); + TRI_ASSERT(_skipped.size() > depth + 1); + size_t index = _skipped.size() - depth - 2; + size_t& localSkip = _skipped.at(index); + localSkip += skipped; +} + auto SkipResult::nothingSkipped() const noexcept -> bool { TRI_ASSERT(!_skipped.empty()); return std::all_of(_skipped.begin(), _skipped.end(), diff --git a/arangod/Aql/SkipResult.h b/arangod/Aql/SkipResult.h index b3883cac0907..1d21d07be470 100644 --- a/arangod/Aql/SkipResult.h +++ b/arangod/Aql/SkipResult.h @@ -52,6 +52,8 @@ class SkipResult { auto didSkip(size_t skipped) -> void; + auto didSkipSubquery(size_t skipped, size_t depth) -> void; + auto nothingSkipped() const noexcept -> bool; auto toVelocyPack(arangodb::velocypack::Builder& builder) const noexcept -> void; From ac94ae303c28b4ab3c0a138fc0aad1281fa5cca6 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 4 Mar 2020 09:38:27 +0100 Subject: [PATCH 40/71] Revert "Let the Modification Executor also produce data, even if no FullCount is used." This reverts commit b1c6af5674de445f9e3d6343feb0e3cad422588c. --- arangod/Aql/ModificationExecutor.cpp | 12 ++++-------- arangod/Aql/SkipResult.cpp | 8 -------- arangod/Aql/SkipResult.h | 2 -- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/arangod/Aql/ModificationExecutor.cpp b/arangod/Aql/ModificationExecutor.cpp index 2c052fca9980..6554c6574fed 100644 --- a/arangod/Aql/ModificationExecutor.cpp +++ b/arangod/Aql/ModificationExecutor.cpp @@ -201,6 +201,7 @@ template doCollect(input, output.numRowsLeft()); upstreamState = input.upstreamState(); } + if (_modifier.nrOfOperations() > 0) { _modifier.transact(); @@ -233,10 +234,7 @@ template // only produce at most output.numRowsLeft() many results ExecutorState upstreamState = input.upstreamState(); - // These executors need to be executed on HARD LIMIT 0. - // If we do a fullCount or not. - while (input.hasDataRow() && - (call.getOffset() > 0 || (call.getLimit() == 0 && call.hasHardLimit()))) { + while (input.hasDataRow() && call.needSkipMore()) { _modifier.reset(); size_t toSkip = call.getOffset(); if (call.getLimit() == 0 && call.hasHardLimit()) { @@ -268,10 +266,8 @@ template stats.addWritesExecuted(_modifier.nrOfWritesExecuted()); stats.addWritesIgnored(_modifier.nrOfWritesIgnored()); } - if (call.needSkipMore()) { - // We only need to report skip, if we actually skip. - call.didSkip(_modifier.nrOfOperations()); - } + + call.didSkip(_modifier.nrOfOperations()); } } diff --git a/arangod/Aql/SkipResult.cpp b/arangod/Aql/SkipResult.cpp index 2b64063ba876..6e721e6bd7fc 100644 --- a/arangod/Aql/SkipResult.cpp +++ b/arangod/Aql/SkipResult.cpp @@ -45,14 +45,6 @@ auto SkipResult::didSkip(size_t skipped) -> void { _skipped.back() += skipped; } -auto SkipResult::didSkipSubquery(size_t skipped, size_t depth) -> void { - TRI_ASSERT(!_skipped.empty()); - TRI_ASSERT(_skipped.size() > depth + 1); - size_t index = _skipped.size() - depth - 2; - size_t& localSkip = _skipped.at(index); - localSkip += skipped; -} - auto SkipResult::nothingSkipped() const noexcept -> bool { TRI_ASSERT(!_skipped.empty()); return std::all_of(_skipped.begin(), _skipped.end(), diff --git a/arangod/Aql/SkipResult.h b/arangod/Aql/SkipResult.h index 1d21d07be470..b3883cac0907 100644 --- a/arangod/Aql/SkipResult.h +++ b/arangod/Aql/SkipResult.h @@ -52,8 +52,6 @@ class SkipResult { auto didSkip(size_t skipped) -> void; - auto didSkipSubquery(size_t skipped, size_t depth) -> void; - auto nothingSkipped() const noexcept -> bool; auto toVelocyPack(arangodb::velocypack::Builder& builder) const noexcept -> void; From 7695b10eeb557e0fc21c7169d18a7751355e6ce5 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 4 Mar 2020 09:48:54 +0100 Subject: [PATCH 41/71] Revert "Revert "Let the Modification Executor also produce data, even if no FullCount is used."" This reverts commit ac94ae303c28b4ab3c0a138fc0aad1281fa5cca6. --- arangod/Aql/ModificationExecutor.cpp | 12 ++++++++---- arangod/Aql/SkipResult.cpp | 8 ++++++++ arangod/Aql/SkipResult.h | 2 ++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/arangod/Aql/ModificationExecutor.cpp b/arangod/Aql/ModificationExecutor.cpp index 6554c6574fed..2c052fca9980 100644 --- a/arangod/Aql/ModificationExecutor.cpp +++ b/arangod/Aql/ModificationExecutor.cpp @@ -201,7 +201,6 @@ template doCollect(input, output.numRowsLeft()); upstreamState = input.upstreamState(); } - if (_modifier.nrOfOperations() > 0) { _modifier.transact(); @@ -234,7 +233,10 @@ template // only produce at most output.numRowsLeft() many results ExecutorState upstreamState = input.upstreamState(); - while (input.hasDataRow() && call.needSkipMore()) { + // These executors need to be executed on HARD LIMIT 0. + // If we do a fullCount or not. + while (input.hasDataRow() && + (call.getOffset() > 0 || (call.getLimit() == 0 && call.hasHardLimit()))) { _modifier.reset(); size_t toSkip = call.getOffset(); if (call.getLimit() == 0 && call.hasHardLimit()) { @@ -266,8 +268,10 @@ template stats.addWritesExecuted(_modifier.nrOfWritesExecuted()); stats.addWritesIgnored(_modifier.nrOfWritesIgnored()); } - - call.didSkip(_modifier.nrOfOperations()); + if (call.needSkipMore()) { + // We only need to report skip, if we actually skip. + call.didSkip(_modifier.nrOfOperations()); + } } } diff --git a/arangod/Aql/SkipResult.cpp b/arangod/Aql/SkipResult.cpp index 6e721e6bd7fc..2b64063ba876 100644 --- a/arangod/Aql/SkipResult.cpp +++ b/arangod/Aql/SkipResult.cpp @@ -45,6 +45,14 @@ auto SkipResult::didSkip(size_t skipped) -> void { _skipped.back() += skipped; } +auto SkipResult::didSkipSubquery(size_t skipped, size_t depth) -> void { + TRI_ASSERT(!_skipped.empty()); + TRI_ASSERT(_skipped.size() > depth + 1); + size_t index = _skipped.size() - depth - 2; + size_t& localSkip = _skipped.at(index); + localSkip += skipped; +} + auto SkipResult::nothingSkipped() const noexcept -> bool { TRI_ASSERT(!_skipped.empty()); return std::all_of(_skipped.begin(), _skipped.end(), diff --git a/arangod/Aql/SkipResult.h b/arangod/Aql/SkipResult.h index b3883cac0907..1d21d07be470 100644 --- a/arangod/Aql/SkipResult.h +++ b/arangod/Aql/SkipResult.h @@ -52,6 +52,8 @@ class SkipResult { auto didSkip(size_t skipped) -> void; + auto didSkipSubquery(size_t skipped, size_t depth) -> void; + auto nothingSkipped() const noexcept -> bool; auto toVelocyPack(arangodb::velocypack::Builder& builder) const noexcept -> void; From 6bf1343ce30e82cc17784ab686136704500f6e6f Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 4 Mar 2020 10:07:02 +0100 Subject: [PATCH 42/71] Implemented proper fastForwarding and skipReporting in ExecutorsWithSideEffects. --- arangod/Aql/AqlCallStack.cpp | 31 ++++-- arangod/Aql/AqlCallStack.h | 18 ++++ arangod/Aql/ExecutionBlockImpl.cpp | 150 +++++++++++++++++++++------ arangod/Aql/ExecutionBlockImpl.h | 14 ++- arangod/Aql/ModificationExecutor.cpp | 10 +- 5 files changed, 177 insertions(+), 46 deletions(-) diff --git a/arangod/Aql/AqlCallStack.cpp b/arangod/Aql/AqlCallStack.cpp index 66a68d4b56fa..20ac7bce224c 100644 --- a/arangod/Aql/AqlCallStack.cpp +++ b/arangod/Aql/AqlCallStack.cpp @@ -179,11 +179,28 @@ auto AqlCallStack::createEquivalentFetchAllShadowRowsStack() const -> AqlCallSta } auto AqlCallStack::needToSkipSubquery() const noexcept -> bool { - if (subqueryLevel() > 1) { - return std::any_of(_operations.begin(), _operations.end() - 1, - [](AqlCall const& call) -> bool { - return call.needSkipMore(); - }); + return std::any_of(_operations.begin(), _operations.end(), [](AqlCall const& call) -> bool { + return call.needSkipMore(); + }); +} + +auto AqlCallStack::shadowRowDepthToSkip() const noexcept -> size_t { + TRI_ASSERT(needToSkipSubquery()); + for (size_t i = 0; i < _operations.size(); ++i) { + auto& call = _operations.at(i); + if (call.needSkipMore()) { + return _operations.size() - i - 1; + } } - return false; -} \ No newline at end of file + // unreachable + TRI_ASSERT(false); + THROW_ARANGO_EXCEPTION(TRI_ERROR_INTERNAL_AQL); +} + +auto AqlCallStack::modifyCallAtDepth(size_t depth) -> AqlCall& { + // depth 0 is back of vector + TRI_ASSERT(_operations.size() > depth); + // Take the depth-most from top of the vector. + auto& call = *(_operations.rbegin() + depth); + return call; +} diff --git a/arangod/Aql/AqlCallStack.h b/arangod/Aql/AqlCallStack.h index f71b18e706ce..ea94442f717a 100644 --- a/arangod/Aql/AqlCallStack.h +++ b/arangod/Aql/AqlCallStack.h @@ -109,6 +109,24 @@ class AqlCallStack { */ auto needToSkipSubquery() const noexcept -> bool; + /** + * @brief This is only valid if needToSkipSubquery is true. + * It will resolve to the heighest subquery level + * (outermost) that needs to be skipped. + * + * + * @return size_t Depth of the subquery that asks to be skipped. + */ + auto shadowRowDepthToSkip() const noexcept -> size_t; + + /** + * @brief Get a reference to the call at the given shadowRowDepth + * + * @param depth ShadowRow depth we need to work on + * @return AqlCall& reference to the call, can be modified. + */ + auto modifyCallAtDepth(size_t depth) -> AqlCall&; + private: explicit AqlCallStack(std::vector&& operations); diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index fe502ffc757c..c8b93113d01c 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -1462,7 +1462,35 @@ auto ExecutionBlockImpl::shadowRowForwarding() -> ExecState } template -auto ExecutionBlockImpl::sideEffectShadowRowForwarding(AqlCallStack& stack) +auto ExecutionBlockImpl::nextStateAfterShadowRows(ExecutorState const& state, + DataRange const& range) const + noexcept -> ExecState { + if (state == ExecutorState::DONE) { + // We have consumed everything, we are + // Done with this query + return ExecState::DONE; + } else if (range.hasDataRow()) { + // Multiple concatenated Subqueries + // This case is disallowed for now, as we do not know the + // look-ahead call + TRI_ASSERT(false); + // If we would know we could now go into a continue with next subquery + // state. + return ExecState::DONE; + } else if (range.hasShadowRow()) { + // We still have shadowRows, we + // need to forward them + return ExecState::SHADOWROWS; + } else { + // End of input, we are done for now + // Need to call again + return ExecState::DONE; + } +} + +template +auto ExecutionBlockImpl::sideEffectShadowRowForwarding(AqlCallStack& stack, + SkipResult& skipResult) -> ExecState { TRI_ASSERT(executorHasSideEffects); if (!stack.needToSkipSubquery()) { @@ -1470,8 +1498,75 @@ auto ExecutionBlockImpl::sideEffectShadowRowForwarding(AqlCallStack& s // fall back to original version as any other executor. return shadowRowForwarding(); } - // TODO implemenet ShadowRowHandling - return shadowRowForwarding(); + TRI_ASSERT(_outputItemRow); + TRI_ASSERT(_outputItemRow->isInitialized()); + TRI_ASSERT(!_outputItemRow->allRowsUsed()); + if (!_lastRange.hasShadowRow()) { + // We got back without a ShadowRow in the LastRange + // Let client call again + return ExecState::DONE; + } + + auto const& [state, shadowRow] = _lastRange.nextShadowRow(); + TRI_ASSERT(shadowRow.isInitialized()); + uint64_t depthSkippingNow = static_cast(stack.shadowRowDepthToSkip()); + uint64_t shadowDepth = shadowRow.getDepth(); + + bool didWriteRow = false; + if (shadowRow.isRelevant()) { + LOG_QUERY("1b257", DEBUG) << printTypeInfo() << " init executor."; + // We found a relevant shadow Row. + // We need to reset the Executor + resetExecutor(); + } + if (depthSkippingNow > shadowDepth) { + // We are skipping the outermost Subquery. + // Simply drop this ShadowRow + } else if (depthSkippingNow == shadowDepth) { + // We are skipping on this subquery level. + // Skip the row, but report skipped 1. + AqlCall& shadowCall = stack.modifyCallAtDepth(shadowDepth); + TRI_ASSERT(shadowCall.needSkipMore()); + shadowCall.didSkip(1); + skipResult.didSkipSubquery(1, shadowDepth); + } else { + // We got a shadowRow of a subquery we are not skipping here. + // Do proper reporting on it's call. + AqlCall& shadowCall = stack.modifyCallAtDepth(shadowDepth); + TRI_ASSERT(!shadowCall.needSkipMore() && shadowCall.getLimit() > 0); + _outputItemRow->copyRow(shadowRow); + shadowCall.didProduce(1); + + TRI_ASSERT(_outputItemRow->produced()); + _outputItemRow->advanceRow(); + didWriteRow = true; + } + if (state == ExecutorState::DONE) { + // We have consumed everything, we are + // Done with this query + return ExecState::DONE; + } else if (_lastRange.hasDataRow()) { + // Multiple concatenated Subqueries + // This case is disallowed for now, as we do not know the + // look-ahead call + TRI_ASSERT(false); + // If we would know we could now go into a continue with next subquery + // state. + return ExecState::DONE; + } else if (_lastRange.hasShadowRow()) { + // We still have shadowRows, we + // need to forward them + return ExecState::SHADOWROWS; + } else if (didWriteRow) { + // End of input, we are done for now + // Need to call again + return ExecState::DONE; + } else { + // Done with this subquery. + // We did not write any output yet. + // So we can continue with upstream. + return ExecState::UPSTREAM; + } } template @@ -1499,7 +1594,6 @@ auto ExecutionBlockImpl::shadowRowForwarding() -> ExecState { TRI_ASSERT(_outputItemRow->produced()); _outputItemRow->advanceRow(); - if (state == ExecutorState::DONE) { // We have consumed everything, we are // Done with this query @@ -1541,18 +1635,30 @@ auto ExecutionBlockImpl::executeFastForward(typename Fetcher::DataRang auto type = fastForwardType(clientCall, _executor); switch (type) { - case FastForwardVariant::FULLCOUNT: - case FastForwardVariant::EXECUTOR: { + case FastForwardVariant::FULLCOUNT: { LOG_QUERY("cb135", DEBUG) << printTypeInfo() << " apply full count."; auto [state, stats, skippedLocal, call, dependency] = executeSkipRowsRange(_lastRange, clientCall); _requestedDependency = dependency; - if (type == FastForwardVariant::EXECUTOR) { - // We do not report the skip - skippedLocal = 0; + if constexpr (is_one_of_v) { + // The executor will have used all Rows. + // However we need to drop them from the input + // here. + inputRange.skipAllRemainingDataRows(); } + return {state, stats, skippedLocal, call, dependency}; + } + case FastForwardVariant::EXECUTOR: { + LOG_QUERY("2890e", DEBUG) << printTypeInfo() << " fast forward."; + // We use a DUMMY Call to simulate fullCount. + AqlCall dummy; + dummy.hardLimit = 0; + dummy.fullCount = true; + auto [state, stats, skippedLocal, call, dependency] = + executeSkipRowsRange(_lastRange, dummy); + _requestedDependency = dependency; if constexpr (is_one_of_v) { // The executor will have used all Rows. // However we need to drop them from the input @@ -1560,7 +1666,7 @@ auto ExecutionBlockImpl::executeFastForward(typename Fetcher::DataRang inputRange.skipAllRemainingDataRows(); } - return {state, stats, skippedLocal, call, dependency}; + return {state, stats, 0, call, dependency}; } case FastForwardVariant::FETCHER: { LOG_QUERY("fa327", DEBUG) << printTypeInfo() << " bypass unused rows."; @@ -1953,16 +2059,6 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { TRI_ASSERT(skippedLocal.getSkipCount() == 0); skippedLocal.decrementSubquery(); } - - /* - { - VPackBuilder in; - VPackBuilder ex; - _skipped.toVelocyPack(ex); - skippedLocal.toVelocyPack(in); - LOG_DEVEL << "merging " << in.toJson() << " -> " << ex.toJson(); - } - */ if constexpr (skipRowsType() == SkipRowsRangeVariant::FETCHER) { // We skipped through passthrough, so count that a skip was solved. _skipped.merge(skippedLocal, false); @@ -2025,10 +2121,11 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { TRI_ASSERT(!_outputItemRow->allRowsUsed()); if constexpr (executorHasSideEffects) { - _execState = sideEffectShadowRowForwarding(stack); + _execState = sideEffectShadowRowForwarding(stack, _skipped); + } else { + // This may write one or more rows. + _execState = shadowRowForwarding(); } - // This may write one or more rows. - _execState = shadowRowForwarding(); if constexpr (!std::is_same_v) { // Produce might have modified the clientCall // But only do this if we are not subquery. @@ -2055,13 +2152,6 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { // We return skipped here, reset member SkipResult skipped = _skipped; -/* - { - VPackBuilder skippor; - skipped.toVelocyPack(skippor); - LOG_DEVEL << getPlanNode()->getTypeString() << " -> " << skippor.toJson(); - } -*/ #ifdef ARANGODB_ENABLE_MAINTAINER_MODE if (!stack.is36Compatible()) { if constexpr (std::is_same_v) { diff --git a/arangod/Aql/ExecutionBlockImpl.h b/arangod/Aql/ExecutionBlockImpl.h index a7358813fe94..66aaa893fc36 100644 --- a/arangod/Aql/ExecutionBlockImpl.h +++ b/arangod/Aql/ExecutionBlockImpl.h @@ -321,7 +321,19 @@ class ExecutionBlockImpl final : public ExecutionBlock { // as soon as we reach a place where there is no skip // ordered in the outer shadow rows, this call // will fall back to shadowRowForwardning. - [[nodiscard]] auto sideEffectShadowRowForwarding(AqlCallStack& stack) -> ExecState; + [[nodiscard]] auto sideEffectShadowRowForwarding(AqlCallStack& stack, + SkipResult& skipResult) -> ExecState; + + /** + * @brief Transition to the next state after shadowRows + * + * @param state the state returned by the getShadowRowCall + * @param range the current data range + * @return ExecState The next state + */ + [[nodiscard]] auto nextStateAfterShadowRows(ExecutorState const& state, + DataRange const& range) const + noexcept -> ExecState; private: /** diff --git a/arangod/Aql/ModificationExecutor.cpp b/arangod/Aql/ModificationExecutor.cpp index 2c052fca9980..f3712fd09199 100644 --- a/arangod/Aql/ModificationExecutor.cpp +++ b/arangod/Aql/ModificationExecutor.cpp @@ -233,10 +233,7 @@ template // only produce at most output.numRowsLeft() many results ExecutorState upstreamState = input.upstreamState(); - // These executors need to be executed on HARD LIMIT 0. - // If we do a fullCount or not. - while (input.hasDataRow() && - (call.getOffset() > 0 || (call.getLimit() == 0 && call.hasHardLimit()))) { + while (input.hasDataRow() && call.needSkipMore()) { _modifier.reset(); size_t toSkip = call.getOffset(); if (call.getLimit() == 0 && call.hasHardLimit()) { @@ -268,10 +265,7 @@ template stats.addWritesExecuted(_modifier.nrOfWritesExecuted()); stats.addWritesIgnored(_modifier.nrOfWritesIgnored()); } - if (call.needSkipMore()) { - // We only need to report skip, if we actually skip. - call.didSkip(_modifier.nrOfOperations()); - } + call.didSkip(_modifier.nrOfOperations()); } } From 6c743bcc207ee42675ffd0718f32b6f555697a66 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 4 Mar 2020 10:38:49 +0100 Subject: [PATCH 43/71] Removed unreachable code, somehow the G++ in our tests tries to comile it and fails now. --- arangod/Aql/ExecutionBlockImpl.cpp | 41 ++++++++---------------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index c8b93113d01c..ec6ad0cab2b6 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -528,38 +528,17 @@ static SkipVariants constexpr skipType() { template std::pair ExecutionBlockImpl::skipSome(size_t const atMost) { - if constexpr (isNewStyleExecutor) { - AqlCallStack stack{AqlCall::SimulateSkipSome(atMost)}; - auto const [state, skipped, block] = execute(stack); - - // execute returns ExecutionState::DONE here, which stops execution after simulating a skip. - // If we indiscriminately return ExecutionState::HASMORE, then we end up in an infinite loop - // - // luckily we can dispose of this kludge once executors have been ported. - if (skipped.getSkipCount() < atMost && state == ExecutionState::DONE) { - return {ExecutionState::DONE, skipped.getSkipCount()}; - } else { - return {ExecutionState::HASMORE, skipped.getSkipCount()}; - } + AqlCallStack stack{AqlCall::SimulateSkipSome(atMost)}; + auto const [state, skipped, block] = execute(stack); + + // execute returns ExecutionState::DONE here, which stops execution after simulating a skip. + // If we indiscriminately return ExecutionState::HASMORE, then we end up in an infinite loop + // + // luckily we can dispose of this kludge once executors have been ported. + if (skipped.getSkipCount() < atMost && state == ExecutionState::DONE) { + return {ExecutionState::DONE, skipped.getSkipCount()}; } else { - traceSkipSomeBegin(atMost); - auto state = ExecutionState::HASMORE; - - while (state == ExecutionState::HASMORE && _skipped < atMost) { - auto res = skipSomeOnceWithoutTrace(atMost - _skipped); - TRI_ASSERT(state != ExecutionState::WAITING || res.second == 0); - state = res.first; - _skipped += res.second; - TRI_ASSERT(_skipped <= atMost); - } - - size_t skipped = 0; - if (state != ExecutionState::WAITING) { - std::swap(skipped, _skipped); - } - - TRI_ASSERT(skipped <= atMost); - return traceSkipSomeEnd(state, skipped); + return {ExecutionState::HASMORE, skipped.getSkipCount()}; } } From 970d17f9810f1eff29923cb92cc54066168ce208 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 4 Mar 2020 10:39:14 +0100 Subject: [PATCH 44/71] noexcept -> throw is nono good. Thank you compiler for helping me here \o/ --- arangod/Aql/AqlCallStack.cpp | 2 +- arangod/Aql/AqlCallStack.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/arangod/Aql/AqlCallStack.cpp b/arangod/Aql/AqlCallStack.cpp index 20ac7bce224c..2ff544485568 100644 --- a/arangod/Aql/AqlCallStack.cpp +++ b/arangod/Aql/AqlCallStack.cpp @@ -184,7 +184,7 @@ auto AqlCallStack::needToSkipSubquery() const noexcept -> bool { }); } -auto AqlCallStack::shadowRowDepthToSkip() const noexcept -> size_t { +auto AqlCallStack::shadowRowDepthToSkip() const -> size_t { TRI_ASSERT(needToSkipSubquery()); for (size_t i = 0; i < _operations.size(); ++i) { auto& call = _operations.at(i); diff --git a/arangod/Aql/AqlCallStack.h b/arangod/Aql/AqlCallStack.h index ea94442f717a..775f7aa01fd4 100644 --- a/arangod/Aql/AqlCallStack.h +++ b/arangod/Aql/AqlCallStack.h @@ -117,7 +117,7 @@ class AqlCallStack { * * @return size_t Depth of the subquery that asks to be skipped. */ - auto shadowRowDepthToSkip() const noexcept -> size_t; + auto shadowRowDepthToSkip() const -> size_t; /** * @brief Get a reference to the call at the given shadowRowDepth From 3dba1bc5629975686451433ec8f6e34cc3037f6a Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 4 Mar 2020 12:37:34 +0100 Subject: [PATCH 45/71] Implment copy on SkipResult --- arangod/Aql/SkipResult.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/arangod/Aql/SkipResult.h b/arangod/Aql/SkipResult.h index 1d21d07be470..6083b664916b 100644 --- a/arangod/Aql/SkipResult.h +++ b/arangod/Aql/SkipResult.h @@ -25,6 +25,7 @@ // for size_t #include +#include #include namespace arangodb { @@ -47,6 +48,7 @@ class SkipResult { ~SkipResult() = default; SkipResult(SkipResult const& other); + SkipResult& operator=(const SkipResult&) = default; auto getSkipCount() const noexcept -> size_t; @@ -81,4 +83,4 @@ class SkipResult { std::ostream& operator<<(std::ostream&, arangodb::aql::SkipResult const&); -#endif \ No newline at end of file +#endif From 50c452f6152634e98a9b435e82dbe37db980610f Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 5 Mar 2020 07:47:03 +0100 Subject: [PATCH 46/71] Adapted SubqueryStartTest to allow easy testing for Skipping on outer levels. --- tests/Aql/ExecutorTestHelper.h | 31 +++-- tests/Aql/SubqueryStartExecutorTest.cpp | 165 +++++++++++++++++++++--- 2 files changed, 171 insertions(+), 25 deletions(-) diff --git a/tests/Aql/ExecutorTestHelper.h b/tests/Aql/ExecutorTestHelper.h index bf11a6ff0408..1a007f01782f 100644 --- a/tests/Aql/ExecutorTestHelper.h +++ b/tests/Aql/ExecutorTestHelper.h @@ -210,7 +210,7 @@ struct ExecutorTestHelper { ExecutorTestHelper(ExecutorTestHelper const&) = delete; ExecutorTestHelper(ExecutorTestHelper&&) = delete; explicit ExecutorTestHelper(arangodb::aql::Query& query) - : _expectedSkip{0}, + : _expectedSkip{}, _expectedState{ExecutionState::HASMORE}, _testStats{false}, _unorderedOutput{false}, @@ -300,8 +300,24 @@ struct ExecutorTestHelper { return *this; } - auto expectSkipped(std::size_t skip) -> ExecutorTestHelper& { - _expectedSkip = skip; + /** + * @brief + * + * @tparam Ts numeric type, can actually only be size_t + * @param skipOnLevel List of skip counters returned per level. subquery skips first, the last entry is the skip on the executor + * @return ExecutorTestHelper& chaining! + */ + template + auto expectSkipped(T skipFirst, Ts const... skipOnHigherLevel) -> ExecutorTestHelper& { + _expectedSkip = SkipResult{}; + // This is obvious, proof: Homework. + (_expectedSkip.didSkip(static_cast(skipFirst)), ..., + (_expectedSkip.incrementSubquery(), + _expectedSkip.didSkip(static_cast(skipOnHigherLevel)))); + + // NOTE: the above will increment didSkip by the first entry. + // For all following entries it will first increment the subquery depth + // and then add the didSkip on them. return *this; } @@ -377,7 +393,7 @@ struct ExecutorTestHelper { auto inputBlock = generateInputRanges(itemBlockManager); - auto skippedTotal = size_t{0}; + auto skippedTotal = SkipResult{}; auto finalState = ExecutionState::HASMORE; TRI_ASSERT(!_pipeline.empty()); @@ -387,7 +403,7 @@ struct ExecutorTestHelper { if (!loop) { auto const [state, skipped, result] = _pipeline.get().front()->execute(_callStack); - skippedTotal = skipped.getSkipCount(); + skippedTotal.merge(skipped, false); finalState = state; if (result != nullptr) { allResults.add(result); @@ -397,7 +413,7 @@ struct ExecutorTestHelper { auto const [state, skipped, result] = _pipeline.get().front()->execute(_callStack); finalState = state; auto call = _callStack.popCall(); - skippedTotal += skipped.getSkipCount(); + skippedTotal.merge(skipped, false); call.didSkip(skipped.getSkipCount()); if (result != nullptr) { call.didProduce(result->size()); @@ -409,7 +425,6 @@ struct ExecutorTestHelper { (!_callStack.peek().hasSoftLimit() || (_callStack.peek().getLimit() + _callStack.peek().getOffset()) > 0)); } - EXPECT_EQ(skippedTotal, _expectedSkip); EXPECT_EQ(finalState, _expectedState); SharedAqlItemBlockPtr result = allResults.steal(); @@ -500,7 +515,7 @@ struct ExecutorTestHelper { MatrixBuilder _output; std::vector> _outputShadowRows{}; std::array _outputRegisters; - std::size_t _expectedSkip; + SkipResult _expectedSkip; ExecutionState _expectedState; ExecutionStats _expectedStats; bool _testStats; diff --git a/tests/Aql/SubqueryStartExecutorTest.cpp b/tests/Aql/SubqueryStartExecutorTest.cpp index dc02b404e2d0..b1c14e0e5558 100644 --- a/tests/Aql/SubqueryStartExecutorTest.cpp +++ b/tests/Aql/SubqueryStartExecutorTest.cpp @@ -101,7 +101,7 @@ TEST_P(SubqueryStartExecutorTest, empty_input_does_not_add_shadow_rows) { .expectedStats(ExecutionStats{}) .expectedState(ExecutionState::DONE) .expectOutput({0}, {}) - .expectSkipped(0) + .expectSkipped(0, 0) .setCallStack(queryStack(AqlCall{}, AqlCall{})) .run(); } @@ -112,7 +112,7 @@ TEST_P(SubqueryStartExecutorTest, adds_a_shadowrow_after_single_input) { .setInputValue({{R"("a")"}}) .expectedStats(ExecutionStats{}) .expectedState(ExecutionState::DONE) - .expectSkipped(0) + .expectSkipped(0, 0) .expectOutput({0}, {{R"("a")"}, {R"("a")"}}, {{1, 0}}) .setCallStack(queryStack(AqlCall{}, AqlCall{})) .run(); @@ -128,7 +128,7 @@ TEST_P(SubqueryStartExecutorTest, .setInputValue({{{R"("a")"}}, {{R"("b")"}}, {{R"("c")"}}}) .expectedStats(ExecutionStats{}) .expectedState(ExecutionState::HASMORE) - .expectSkipped(0) + .expectSkipped(0, 0) .expectOutput({0}, {{R"("a")"}, {R"("a")"}}, {{1, 0}}) .setCallStack(queryStack(AqlCall{}, AqlCall{})) .run(); @@ -144,7 +144,7 @@ TEST_P(SubqueryStartExecutorTest, DISABLED_adds_a_shadowrow_after_every_input_li .setInputValue({{{R"("a")"}}, {{R"("b")"}}, {{R"("c")"}}}) .expectedStats(ExecutionStats{}) .expectedState(ExecutionState::DONE) - .expectSkipped(0) + .expectSkipped(0, 0) .expectOutput({0}, {{R"("a")"}, {R"("a")"}, {R"("b")"}, {R"("b")"}, {R"("c")"}, {R"("c")"}}, {{1, 0}, {3, 0}, {5, 0}}) .setCallStack(queryStack(AqlCall{}, AqlCall{})) @@ -159,7 +159,7 @@ TEST_P(SubqueryStartExecutorTest, adds_a_shadowrow_after_every_input_line) { .setInputValue({{{R"("a")"}}, {{R"("b")"}}, {{R"("c")"}}}) .expectedStats(ExecutionStats{}) .expectedState(ExecutionState::DONE) - .expectSkipped(0) + .expectSkipped(0, 0) .expectOutput({0}, {{R"("a")"}, {R"("a")"}, {R"("b")"}, {R"("b")"}, {R"("c")"}, {R"("c")"}}, {{1, 0}, {3, 0}, {5, 0}}) .setCallStack(queryStack(AqlCall{}, AqlCall{})) @@ -181,7 +181,7 @@ TEST_P(SubqueryStartExecutorTest, shadow_row_does_not_fit_in_current_block) { .setInputValue({{R"("a")"}}) .expectedStats(ExecutionStats{}) .expectedState(ExecutionState::HASMORE) - .expectSkipped(0) + .expectSkipped(0, 0) .expectOutput({0}, {{R"("a")"}}, {}) .setCallStack(queryStack(AqlCall{}, AqlCall{})) .run(); @@ -194,7 +194,7 @@ TEST_P(SubqueryStartExecutorTest, shadow_row_does_not_fit_in_current_block) { .setInputValue({{R"("a")"}}) .expectedStats(ExecutionStats{}) .expectedState(ExecutionState::DONE) - .expectSkipped(0) + .expectSkipped(0, 0) .expectOutput({0}, {{R"("a")"}, {R"("a")"}}, {{1, 0}}) .setCallStack(queryStack(AqlCall{}, AqlCall{})) .run(true); @@ -208,7 +208,7 @@ TEST_P(SubqueryStartExecutorTest, skip_in_subquery) { .expectedStats(ExecutionStats{}) .expectedState(ExecutionState::DONE) .expectOutput({0}, {{R"("a")"}}, {{0, 0}}) - .expectSkipped(1) + .expectSkipped(0, 1) .setCallStack(queryStack(AqlCall{}, AqlCall{10, false})) .run(); } @@ -220,7 +220,7 @@ TEST_P(SubqueryStartExecutorTest, fullCount_in_subquery) { .expectedStats(ExecutionStats{}) .expectedState(ExecutionState::DONE) .expectOutput({0}, {{R"("a")"}}, {{0, 0}}) - .expectSkipped(1) + .expectSkipped(0, 1) .setCallStack(queryStack(AqlCall{}, AqlCall{0, true, 0, AqlCall::LimitType::HARD})) .run(); } @@ -234,12 +234,20 @@ TEST_P(SubqueryStartExecutorTest, shadow_row_forwarding) { ExecutionNode::SUBQUERY_START)) .addConsumer(helper.createExecBlock(MakeBaseInfos(1), ExecutionNode::SUBQUERY_START)); + + if (GetCompatMode() == CompatibilityMode::VERSION36) { + // We will not get this infromation because the + // query stack is too small on purpose + helper.expectSkipped(0, 0); + } else { + helper.expectSkipped(0, 0, 0); + } + helper.setPipeline(std::move(pipe)) .setInputValue({{R"("a")"}}) .expectedStats(ExecutionStats{}) .expectedState(ExecutionState::DONE) .expectOutput({0}, {{R"("a")"}, {R"("a")"}, {R"("a")"}}, {{1, 0}, {2, 1}}) - .expectSkipped(0) .setCallStack(stack) .run(); } @@ -253,12 +261,20 @@ TEST_P(SubqueryStartExecutorTest, shadow_row_forwarding_many_inputs_single_call) ExecutionNode::SUBQUERY_START)) .addConsumer(helper.createExecBlock(MakeBaseInfos(1), ExecutionNode::SUBQUERY_START)); + + if (GetCompatMode() == CompatibilityMode::VERSION36) { + // We will not get this infromation because the + // query stack is too small on purpose + helper.expectSkipped(0, 0); + } else { + helper.expectSkipped(0, 0, 0); + } + helper.setPipeline(std::move(pipe)) .setInputValue({{R"("a")"}, {R"("b")"}, {R"("c")"}}) .expectedStats(ExecutionStats{}) .expectedState(ExecutionState::HASMORE) .expectOutput({0}, {{R"("a")"}, {R"("a")"}, {R"("a")"}}, {{1, 0}, {2, 1}}) - .expectSkipped(0) .setCallStack(stack) .run(); } @@ -272,6 +288,13 @@ TEST_P(SubqueryStartExecutorTest, shadow_row_forwarding_many_inputs_many_request ExecutionNode::SUBQUERY_START)) .addConsumer(helper.createExecBlock(MakeBaseInfos(1), ExecutionNode::SUBQUERY_START)); + if (GetCompatMode() == CompatibilityMode::VERSION36) { + // We will not get this infromation because the + // query stack is too small on purpose + helper.expectSkipped(0, 0); + } else { + helper.expectSkipped(0, 0, 0); + } helper.setPipeline(std::move(pipe)) .setInputValue({{R"("a")"}, {R"("b")"}, {R"("c")"}}) .expectedStats(ExecutionStats{}) @@ -280,7 +303,6 @@ TEST_P(SubqueryStartExecutorTest, shadow_row_forwarding_many_inputs_many_request {0}, {{R"("a")"}, {R"("a")"}, {R"("a")"}, {R"("b")"}, {R"("b")"}, {R"("b")"}, {R"("c")"}, {R"("c")"}, {R"("c")"}}, {{1, 0}, {2, 1}, {4, 0}, {5, 1}, {7, 0}, {8, 1}}) - .expectSkipped(0) .setCallStack(stack) .run(true); } @@ -303,12 +325,19 @@ TEST_P(SubqueryStartExecutorTest, shadow_row_forwarding_many_inputs_not_enough_s ExecutionNode::SUBQUERY_START)) .addConsumer(helper.createExecBlock(MakeBaseInfos(1), ExecutionNode::SUBQUERY_START)); + + if (GetCompatMode() == CompatibilityMode::VERSION36) { + // We will not get this infromation because the + // query stack is too small on purpose + helper.expectSkipped(0, 0); + } else { + helper.expectSkipped(0, 0, 0); + } helper.setPipeline(std::move(pipe)) .setInputValue({{R"("a")"}, {R"("b")"}, {R"("c")"}}) .expectedStats(ExecutionStats{}) .expectedState(ExecutionState::HASMORE) .expectOutput({0}, {{R"("a")"}, {R"("a")"}}, {{1, 0}}) - .expectSkipped(0) .setCallStack(stack) .run(); } @@ -323,6 +352,15 @@ TEST_P(SubqueryStartExecutorTest, shadow_row_forwarding_many_inputs_not_enough_s ExecutionNode::SUBQUERY_START)) .addConsumer(helper.createExecBlock(MakeBaseInfos(1), ExecutionNode::SUBQUERY_START)); + + if (GetCompatMode() == CompatibilityMode::VERSION36) { + // We will not get this infromation because the + // query stack is too small on purpose + helper.expectSkipped(0, 0); + } else { + helper.expectSkipped(0, 0, 0); + } + helper.setPipeline(std::move(pipe)) .setInputValue({{R"("a")"}, {R"("b")"}, {R"("c")"}}) .expectedStats(ExecutionStats{}) @@ -331,12 +369,105 @@ TEST_P(SubqueryStartExecutorTest, shadow_row_forwarding_many_inputs_not_enough_s {0}, {{R"("a")"}, {R"("a")"}, {R"("a")"}, {R"("b")"}, {R"("b")"}, {R"("b")"}, {R"("c")"}, {R"("c")"}, {R"("c")"}}, {{1, 0}, {2, 1}, {4, 0}, {5, 1}, {7, 0}, {8, 1}}) - .expectSkipped(0) .setCallStack(stack) .run(true); } } -// TODO: -// * Add tests for Skipping -// - on Higher level subquery +TEST_P(SubqueryStartExecutorTest, skip_in_outer_subquery) { + if (GetCompatMode() == CompatibilityMode::VERSION37) { + ExecutorTestHelper<1, 1>(*fakedQuery) + .setExecBlock(MakeBaseInfos(1), ExecutionNode::SUBQUERY_START) + .setInputValue({{R"("a")"}, {R"("b")"}}) + .expectedStats(ExecutionStats{}) + .expectedState(ExecutionState::DONE) + .expectOutput({0}, {{R"("b")"}, {R"("b")"}}, {{1, 0}}) + .expectSkipped(1, 0) + .setCallStack(queryStack(AqlCall{1, false, AqlCall::Infinity{}}, AqlCall{})) + .run(); + } else { + // The feature is not available in 3.7 or earlier. + } +} + +TEST_P(SubqueryStartExecutorTest, DISABLED_skip_only_in_outer_subquery) { + if (GetCompatMode() == CompatibilityMode::VERSION37) { + ExecutorTestHelper<1, 1>(*fakedQuery) + .setExecBlock(MakeBaseInfos(1), ExecutionNode::SUBQUERY_START) + .setInputValue({{R"("a")"}, {R"("b")"}}) + .expectedStats(ExecutionStats{}) + .expectedState(ExecutionState::DONE) + .expectOutput({0}, {}) + .expectSkipped(1, 0) + .setCallStack(queryStack(AqlCall{1, false}, AqlCall{})) + .run(); + } else { + // The feature is not available in 3.7 or earlier. + } +} + +TEST_P(SubqueryStartExecutorTest, fullCount_in_outer_subquery) { + if (GetCompatMode() == CompatibilityMode::VERSION37) { + ExecutorTestHelper<1, 1>(*fakedQuery) + .setExecBlock(MakeBaseInfos(1), ExecutionNode::SUBQUERY_START) + .setInputValue({{R"("a")"}, {R"("b")"}, {R"("c")"}, {R"("d")"}, {R"("e")"}, {R"("f")"}}) + .expectedStats(ExecutionStats{}) + .expectedState(ExecutionState::DONE) + .expectOutput({0}, {}) + .expectSkipped(6, 0) + .setCallStack(queryStack(AqlCall{0, true, 0, AqlCall::LimitType::HARD}, AqlCall{})) + .run(); + } else { + // The feature is not available in 3.7 or earlier. + } +} + +TEST_P(SubqueryStartExecutorTest, fastForward_in_inner_subquery) { + if (GetCompatMode() == CompatibilityMode::VERSION37) { + ExecutorTestHelper<1, 1>(*fakedQuery) + .setExecBlock(MakeBaseInfos(1), ExecutionNode::SUBQUERY_START) + .setInputValue({{R"("a")"}, {R"("b")"}, {R"("c")"}, {R"("d")"}, {R"("e")"}, {R"("f")"}}) + .expectedStats(ExecutionStats{}) + .expectedState(ExecutionState::HASMORE) + .expectOutput({0}, {{R"("a")"}}, {{0, 0}}) + .expectSkipped(0, 0) + .setCallStack(queryStack(AqlCall{0, false, AqlCall::Infinity{}}, + AqlCall{0, false, 0, AqlCall::LimitType::HARD})) + .run(); + } else { + // The feature is not available in 3.7 or earlier. + } +} + +TEST_P(SubqueryStartExecutorTest, skip_out_skip_in) { + if (GetCompatMode() == CompatibilityMode::VERSION37) { + ExecutorTestHelper<1, 1>(*fakedQuery) + .setExecBlock(MakeBaseInfos(1), ExecutionNode::SUBQUERY_START) + .setInputValue({{R"("a")"}, {R"("b")"}, {R"("c")"}, {R"("d")"}, {R"("e")"}, {R"("f")"}}) + .expectedStats(ExecutionStats{}) + .expectedState(ExecutionState::HASMORE) + .expectOutput({0}, {{R"("c")"}}, {{0, 0}}) + .expectSkipped(2, 1) + .setCallStack(queryStack(AqlCall{2, false, AqlCall::Infinity{}}, + AqlCall{10, false, AqlCall::Infinity{}})) + .run(); + } else { + // The feature is not available in 3.7 or earlier. + } +} + +TEST_P(SubqueryStartExecutorTest, fullbypass_in_outer_subquery) { + if (GetCompatMode() == CompatibilityMode::VERSION37) { + ExecutorTestHelper<1, 1>(*fakedQuery) + .setExecBlock(MakeBaseInfos(1), ExecutionNode::SUBQUERY_START) + .setInputValue({{R"("a")"}, {R"("b")"}, {R"("c")"}, {R"("d")"}, {R"("e")"}, {R"("f")"}}) + .expectedStats(ExecutionStats{}) + .expectedState(ExecutionState::DONE) + .expectOutput({0}, {}) + .expectSkipped(0, 0) + .setCallStack(queryStack(AqlCall{0, false, 0, AqlCall::LimitType::HARD}, AqlCall{})) + .run(); + } else { + // The feature is not available in 3.7 or earlier. + } +} \ No newline at end of file From 1874871125dfafed574b730cd0ec65c6b57115ae Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 5 Mar 2020 07:47:56 +0100 Subject: [PATCH 47/71] Fixed koenig lookup of SkipResult ostream operator --- arangod/Aql/SkipResult.cpp | 5 +++-- arangod/Aql/SkipResult.h | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/arangod/Aql/SkipResult.cpp b/arangod/Aql/SkipResult.cpp index 2b64063ba876..6ed007a195fa 100644 --- a/arangod/Aql/SkipResult.cpp +++ b/arangod/Aql/SkipResult.cpp @@ -160,10 +160,11 @@ auto SkipResult::operator==(SkipResult const& b) const noexcept -> bool { auto SkipResult::operator!=(SkipResult const& b) const noexcept -> bool { return !(*this == b); } - +namespace arangodb::aql { std::ostream& operator<<(std::ostream& stream, arangodb::aql::SkipResult const& result) { VPackBuilder temp; result.toVelocyPack(temp); stream << temp.toJson(); return stream; -} \ No newline at end of file +} +} // namespace arangodb::aql diff --git a/arangod/Aql/SkipResult.h b/arangod/Aql/SkipResult.h index 6083b664916b..7b36a7f46fe5 100644 --- a/arangod/Aql/SkipResult.h +++ b/arangod/Aql/SkipResult.h @@ -79,8 +79,8 @@ class SkipResult { std::vector _skipped{0}; }; -} // namespace arangodb::aql - std::ostream& operator<<(std::ostream&, arangodb::aql::SkipResult const&); +} // namespace arangodb::aql + #endif From 62898ce8f2db460bcee4d1e0cd902dbbe6dbaa45 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 5 Mar 2020 07:49:21 +0100 Subject: [PATCH 48/71] Removed special case of SubqueryStartExecutor and include it on the handling for SideEffect Executors --- arangod/Aql/ExecutionBlockImpl.cpp | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index ec6ad0cab2b6..1fd058e6b65b 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -1203,9 +1203,10 @@ static auto fastForwardType(AqlCall const& call, Executor const& e) -> FastForwa TRI_ASSERT(call.hasHardLimit()); return FastForwardVariant::FULLCOUNT; } - // TODO: We only need to do this is the executor actually require to call. - // e.g. Modifications will always need to be called. Limit only if it needs to report fullCount - if constexpr (std::is_same_v || executorHasSideEffects) { + // TODO: We only need to do this if the executor is required to call. + // e.g. Modifications and SubqueryStart will always need to be called. Limit only if it needs to report fullCount + if constexpr (is_one_of_v || + executorHasSideEffects) { return FastForwardVariant::EXECUTOR; } return FastForwardVariant::FETCHER; @@ -1601,16 +1602,6 @@ auto ExecutionBlockImpl::executeFastForward(typename Fetcher::DataRang AqlCall& clientCall) -> std::tuple { TRI_ASSERT(isNewStyleExecutor); - if constexpr (std::is_same_v) { - if (clientCall.needsFullCount() && clientCall.getOffset() == 0 && - clientCall.getLimit() == 0) { - // We can savely call skipRows. - // It will not report anything if the row is already consumed - return executeSkipRowsRange(_lastRange, clientCall); - } - // Do not fastForward anything, the Subquery start will handle it by itself - return {ExecutorState::DONE, NoStats{}, 0, AqlCall{}, 0}; - } auto type = fastForwardType(clientCall, _executor); switch (type) { From a4d4017ddd0aeb49824771ffeeaf5e8a72b10c41 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 5 Mar 2020 07:49:48 +0100 Subject: [PATCH 49/71] Sorry needed to make the _operations vector mutual because of 3.6 compatibility --- arangod/Aql/AqlCallStack.cpp | 12 ++++++++++++ arangod/Aql/AqlCallStack.h | 7 +++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/arangod/Aql/AqlCallStack.cpp b/arangod/Aql/AqlCallStack.cpp index 2ff544485568..13813c6ef74f 100644 --- a/arangod/Aql/AqlCallStack.cpp +++ b/arangod/Aql/AqlCallStack.cpp @@ -77,6 +77,18 @@ AqlCall AqlCallStack::popCall() { AqlCall const& AqlCallStack::peek() const { TRI_ASSERT(isRelevant()); + TRI_ASSERT(_compatibilityMode3_6 || !_operations.empty()); + if (is36Compatible() && _operations.empty()) { + // This is only for compatibility with 3.6 + // there we do not have the stack beeing passed-through + // in AQL, we only have a single call. + // We can only get into this state in the abscence of + // LIMIT => we always do an unlimted softLimit call + // to the upwards subquery. + // => Simply put another fetchAll Call on the stack. + // This code is to be removed in the next version after 3.7 + _operations.emplace_back(AqlCall{}); + } TRI_ASSERT(!_operations.empty()); return _operations.back(); } diff --git a/arangod/Aql/AqlCallStack.h b/arangod/Aql/AqlCallStack.h index 775f7aa01fd4..3611c07886e9 100644 --- a/arangod/Aql/AqlCallStack.h +++ b/arangod/Aql/AqlCallStack.h @@ -131,8 +131,11 @@ class AqlCallStack { explicit AqlCallStack(std::vector&& operations); private: - // The list of operations, stacked by depth (e.g. bottom element is from main query) - std::vector _operations; + // The list of operations, stacked by depth (e.g. bottom element is from main + // query) NOTE: This is only mutable on 3.6 compatibility mode. We need to + // inject an additional call in any const operation here just to pretend we + // are not empty. Can be removed after 3.7. + mutable std::vector _operations; // The depth of subqueries that have not issued calls into operations, // as they have been skipped. From 1fe6c3b9308abf6404a73a3d4139d5068d1a4dc0 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 5 Mar 2020 08:46:14 +0100 Subject: [PATCH 50/71] Attempt to fix windows compile issue --- arangod/Aql/SortingGatherExecutor.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arangod/Aql/SortingGatherExecutor.h b/arangod/Aql/SortingGatherExecutor.h index 440a507847c9..8b699342856d 100644 --- a/arangod/Aql/SortingGatherExecutor.h +++ b/arangod/Aql/SortingGatherExecutor.h @@ -28,6 +28,8 @@ #include "Aql/ExecutorInfos.h" #include "Aql/InputAqlItemRow.h" +#include + namespace arangodb { namespace transaction { From c7e25df43530b90f97a2c53eb9abb20757773c05 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 5 Mar 2020 13:57:49 +0100 Subject: [PATCH 51/71] Fixed behvaiour of SubqueryEndExecutor --- arangod/Aql/ExecutionBlockImpl.cpp | 7 +++ tests/Aql/SplicedSubqueryIntegrationTest.cpp | 49 ++++++++++++++++---- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index 1fd058e6b65b..e2ae7672f1d7 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -2095,6 +2095,13 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { } else { // This may write one or more rows. _execState = shadowRowForwarding(); + if constexpr (std::is_same_v) { + // we need to update the Top of the stack now + std::ignore = stack.popCall(); + // Copy the call + AqlCall modifiedCall = _outputItemRow->getClientCall(); + stack.pushCall(std::move(modifiedCall)); + } } if constexpr (!std::is_same_v) { // Produce might have modified the clientCall diff --git a/tests/Aql/SplicedSubqueryIntegrationTest.cpp b/tests/Aql/SplicedSubqueryIntegrationTest.cpp index f6f37d99f4b1..943afe458c2a 100644 --- a/tests/Aql/SplicedSubqueryIntegrationTest.cpp +++ b/tests/Aql/SplicedSubqueryIntegrationTest.cpp @@ -236,24 +236,34 @@ class SplicedSubqueryIntegrationTest auto createSkipCall() -> SkipCall { return [](AqlItemBlockInputRange& input, AqlCall& call) -> std::tuple { - auto skipped = size_t{0}; + while (call.shouldSkip() && input.skippedInFlight() > 0) { + if (call.getOffset() > 0) { + call.didSkip(input.skip(call.getOffset())); + } else { + EXPECT_TRUE(call.needsFullCount()); + EXPECT_EQ(call.getLimit(), 0); + EXPECT_TRUE(call.hasHardLimit()); + call.didSkip(input.skipAll()); + } + } + // If we overfetched and have data, throw it away while (input.hasDataRow() && call.shouldSkip()) { auto const& [state, inputRow] = input.nextDataRow(); EXPECT_TRUE(inputRow.isInitialized()); call.didSkip(1); - skipped++; } auto upstreamCall = AqlCall{call}; - return {input.upstreamState(), NoStats{}, skipped, upstreamCall}; + return {input.upstreamState(), NoStats{}, call.getSkipCount(), upstreamCall}; }; }; // Asserts if called. This is to check that when we use skip to // skip over a subquery, the subquery's produce is not invoked + // with data auto createAssertCall() -> ProduceCall { return [](AqlItemBlockInputRange& input, OutputAqlItemRow& output) -> std::tuple { - EXPECT_TRUE(false); + EXPECT_FALSE(input.hasDataRow()); NoStats stats{}; AqlCall call{}; @@ -333,7 +343,7 @@ TEST_P(SplicedSubqueryIntegrationTest, single_subquery) { .run(); }; -TEST_P(SplicedSubqueryIntegrationTest, DISABLED_single_subquery_skip_and_produce) { +TEST_P(SplicedSubqueryIntegrationTest, single_subquery_skip_and_produce) { auto call = AqlCall{5}; auto pipeline = createSubquery(); ExecutorTestHelper<1, 2>{*fakedQuery} @@ -347,7 +357,7 @@ TEST_P(SplicedSubqueryIntegrationTest, DISABLED_single_subquery_skip_and_produce .run(); }; -TEST_P(SplicedSubqueryIntegrationTest, DISABLED_single_subquery_skip_all) { +TEST_P(SplicedSubqueryIntegrationTest, single_subquery_skip_all) { auto call = AqlCall{20}; auto pipeline = createSubquery(); ExecutorTestHelper<1, 2>{*fakedQuery} @@ -361,7 +371,7 @@ TEST_P(SplicedSubqueryIntegrationTest, DISABLED_single_subquery_skip_all) { .run(); }; -TEST_P(SplicedSubqueryIntegrationTest, DISABLED_single_subquery_fullcount) { +TEST_P(SplicedSubqueryIntegrationTest, single_subquery_fullcount) { auto call = AqlCall{0, true, 0, AqlCall::LimitType::HARD}; auto pipeline = createSubquery(); ExecutorTestHelper<1, 2>{*fakedQuery} @@ -375,6 +385,8 @@ TEST_P(SplicedSubqueryIntegrationTest, DISABLED_single_subquery_fullcount) { .run(); }; +// NOTE: This test can be enabled if we can continue +// working on the second subquery without returning to consumer TEST_P(SplicedSubqueryIntegrationTest, DISABLED_single_subquery_skip_produce_count) { auto call = AqlCall{2, true, 2, AqlCall::LimitType::HARD}; auto pipeline = createSubquery(); @@ -442,7 +454,7 @@ TEST_P(SplicedSubqueryIntegrationTest, do_nothing_in_subquery) { .run(); }; -TEST_P(SplicedSubqueryIntegrationTest, DISABLED_check_call_passes_subquery) { +TEST_P(SplicedSubqueryIntegrationTest, check_call_passes_subquery) { auto call = AqlCall{10}; auto pipeline = concatPipelines(createCallAssertPipeline(call), createSubquery()); @@ -456,8 +468,9 @@ TEST_P(SplicedSubqueryIntegrationTest, DISABLED_check_call_passes_subquery) { .run(); }; -TEST_P(SplicedSubqueryIntegrationTest, DISABLED_check_skipping_subquery) { +TEST_P(SplicedSubqueryIntegrationTest, check_skipping_subquery) { auto call = AqlCall{10}; + LOG_DEVEL << call; auto pipeline = createSubquery(createAssertPipeline()); executorTestHelper.setPipeline(std::move(pipeline)) @@ -465,7 +478,23 @@ TEST_P(SplicedSubqueryIntegrationTest, DISABLED_check_skipping_subquery) { .setInputSplitType(getSplit()) .setCall(call) .expectOutput({0}, {}) - .expectSkipped(0) + .expectSkipped(8) .expectedState(ExecutionState::DONE) .run(); }; + +TEST_P(SplicedSubqueryIntegrationTest, check_soft_limit_subquery) { + auto call = AqlCall{0, false, 4, AqlCall::LimitType::SOFT}; + LOG_DEVEL << call; + auto pipeline = createSubquery(createAssertPipeline()); + + ExecutorTestHelper<1, 2>{*fakedQuery} + .setPipeline(std::move(pipeline)) + .setInputValueList(1, 2, 5, 2, 1, 5, 7, 1) + .setInputSplitType(getSplit()) + .setCall(call) + .expectOutput({0, 1}, {{1, R"([])"}, {2, R"([])"}, {5, R"([])"}, {2, R"([])"}}) + .expectSkipped(0) + .expectedState(ExecutionState::HASMORE) + .run(); +}; \ No newline at end of file From c5581e49258ecd316376e7d78fbd182f09a2d763 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 5 Mar 2020 14:06:04 +0100 Subject: [PATCH 52/71] Another windows attempt --- arangod/Aql/SkipResult.cpp | 2 +- arangod/Aql/SkipResult.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/arangod/Aql/SkipResult.cpp b/arangod/Aql/SkipResult.cpp index 6ed007a195fa..a37c9109a84d 100644 --- a/arangod/Aql/SkipResult.cpp +++ b/arangod/Aql/SkipResult.cpp @@ -67,7 +67,7 @@ auto SkipResult::toVelocyPack(VPackBuilder& builder) const noexcept -> void { } } -auto SkipResult::fromVelocyPack(VPackSlice slice) -> ResultT { +auto SkipResult::fromVelocyPack(VPackSlice slice) -> arangodb::ResultT { if (!slice.isArray()) { auto message = std::string{ "When deserializating AqlExecuteResult: When reading skipped: " diff --git a/arangod/Aql/SkipResult.h b/arangod/Aql/SkipResult.h index 7b36a7f46fe5..c00d6947f577 100644 --- a/arangod/Aql/SkipResult.h +++ b/arangod/Aql/SkipResult.h @@ -41,7 +41,7 @@ namespace arangodb::aql { class SkipResult { public: - static auto fromVelocyPack(velocypack::Slice) -> ResultT; + static auto fromVelocyPack(velocypack::Slice) -> arangodb::ResultT; SkipResult(); From 15a7d6e1891690e533e978ddeb3b2b0042458d3c Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 5 Mar 2020 16:47:37 +0100 Subject: [PATCH 53/71] Fixed modify test, which would actually iterate over too many documents if the LIMIT does not LIMIT the executed modification operations anymore. --- .../js/server/aql/aql-optimizer-rule-move-calculations-down.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/js/server/aql/aql-optimizer-rule-move-calculations-down.js b/tests/js/server/aql/aql-optimizer-rule-move-calculations-down.js index 54e7c8d0bd7d..37023b6157d1 100644 --- a/tests/js/server/aql/aql-optimizer-rule-move-calculations-down.js +++ b/tests/js/server/aql/aql-optimizer-rule-move-calculations-down.js @@ -371,7 +371,7 @@ function optimizerRuleTestSuite () { expected.push("test" + i + "-" + i); } - var query = "FOR i IN 0..100 LET result = (UPDATE {_key: CONCAT('test', TO_STRING(i))} WITH {updated: true} IN " + cn + " RETURN CONCAT(NEW._key, '-', NEW.value)) LIMIT 10 RETURN result[0]"; + var query = "FOR i IN 0..99 LET result = (UPDATE {_key: CONCAT('test', TO_STRING(i))} WITH {updated: true} IN " + cn + " RETURN CONCAT(NEW._key, '-', NEW.value)) LIMIT 10 RETURN result[0]"; var planDisabled = AQL_EXPLAIN(query, {}, paramDisabled); var planEnabled = AQL_EXPLAIN(query, {}, paramEnabled); From c30ac78545ae8904987171bd96058b65bd61b379 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 5 Mar 2020 16:48:06 +0100 Subject: [PATCH 54/71] Fixed tests that assert on existence of SubqueryNode, now there will be SubqueryStartNode! --- tests/js/server/aql/aql-optimizer-indexes.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/js/server/aql/aql-optimizer-indexes.js b/tests/js/server/aql/aql-optimizer-indexes.js index a08553b36d69..5707d153bcbf 100644 --- a/tests/js/server/aql/aql-optimizer-indexes.js +++ b/tests/js/server/aql/aql-optimizer-indexes.js @@ -862,7 +862,7 @@ function optimizerIndexesTestSuite () { }); assertEqual("SingletonNode", nodeTypes[0], query); - assertEqual("SubqueryNode", nodeTypes[1], query); + assertEqual("SubqueryStartNode", nodeTypes[1], query); var subNodeTypes = plan.nodes[1].subquery.nodes.map(function(node) { return node.type; @@ -3784,7 +3784,7 @@ function optimizerIndexesMultiCollectionTestSuite () { assertEqual(-1, nodeTypes.indexOf("IndexNode"), query); // no index for outer query assertNotEqual(-1, nodeTypes.indexOf("SortNode"), query); // sort node for outer query - var sub = nodeTypes.indexOf("SubqueryNode"); + var sub = nodeTypes.indexOf("SubqueryStartNode"); assertNotEqual(-1, sub); var subNodeTypes = plan.nodes[sub].subquery.nodes.map(function(node) { @@ -3815,7 +3815,7 @@ function optimizerIndexesMultiCollectionTestSuite () { assertEqual(-1, nodeTypes.indexOf("IndexNode"), query); // no index for outer query assertNotEqual(-1, nodeTypes.indexOf("SortNode"), query); // sort node for outer query - var sub = nodeTypes.indexOf("SubqueryNode"); + var sub = nodeTypes.indexOf("SubqueryStartNode"); assertNotEqual(-1, sub); var subNodeTypes = plan.nodes[sub].subquery.nodes.map(function(node) { @@ -3847,7 +3847,7 @@ function optimizerIndexesMultiCollectionTestSuite () { assertEqual(-1, nodeTypes.indexOf("IndexNode"), query); // no index for outer query assertNotEqual(-1, nodeTypes.indexOf("SortNode"), query); // sort node for outer query - var sub = nodeTypes.indexOf("SubqueryNode"); + var sub = nodeTypes.indexOf("SubqueryStartNode"); assertNotEqual(-1, sub); var subNodeTypes = plan.nodes[sub].subquery.nodes.map(function(node) { @@ -3879,7 +3879,7 @@ function optimizerIndexesMultiCollectionTestSuite () { assertEqual(-1, nodeTypes.indexOf("IndexNode"), query); // no index for outer query assertNotEqual(-1, nodeTypes.indexOf("SortNode"), query); // sort node for outer query - var sub = nodeTypes.indexOf("SubqueryNode"); + var sub = nodeTypes.indexOf("SubqueryStartNode"); assertNotEqual(-1, sub); var subNodeTypes = plan.nodes[sub].subquery.nodes.map(function(node) { @@ -3911,7 +3911,7 @@ function optimizerIndexesMultiCollectionTestSuite () { assertEqual(-1, nodeTypes.indexOf("IndexNode"), query); // no index for outer query assertNotEqual(-1, nodeTypes.indexOf("SortNode"), query); // sort node for outer query - var sub = nodeTypes.indexOf("SubqueryNode"); + var sub = nodeTypes.indexOf("SubqueryStartNode"); assertNotEqual(-1, sub); var subNodeTypes = plan.nodes[sub].subquery.nodes.map(function(node) { @@ -3943,7 +3943,7 @@ function optimizerIndexesMultiCollectionTestSuite () { assertEqual(-1, nodeTypes.indexOf("IndexNode"), query); // no index for outer query assertNotEqual(-1, nodeTypes.indexOf("SortNode"), query); // sort node for outer query - var sub = nodeTypes.indexOf("SubqueryNode"); + var sub = nodeTypes.indexOf("SubqueryStartNode"); assertNotEqual(-1, sub); var subNodeTypes = plan.nodes[sub].subquery.nodes.map(function(node) { From fe1757b7048059ebed351378cffbc3ab6221cf21 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 5 Mar 2020 16:48:55 +0100 Subject: [PATCH 55/71] Consider a hardLimitFastForward inside the Callstack like a needToSkipSubquery. But do not report the skipping of it. --- arangod/Aql/AqlCallStack.cpp | 4 ++-- arangod/Aql/ExecutionBlockImpl.cpp | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/arangod/Aql/AqlCallStack.cpp b/arangod/Aql/AqlCallStack.cpp index 13813c6ef74f..bcf0d434e3e0 100644 --- a/arangod/Aql/AqlCallStack.cpp +++ b/arangod/Aql/AqlCallStack.cpp @@ -192,7 +192,7 @@ auto AqlCallStack::createEquivalentFetchAllShadowRowsStack() const -> AqlCallSta auto AqlCallStack::needToSkipSubquery() const noexcept -> bool { return std::any_of(_operations.begin(), _operations.end(), [](AqlCall const& call) -> bool { - return call.needSkipMore(); + return call.needSkipMore() || call.hardLimit == 0; }); } @@ -200,7 +200,7 @@ auto AqlCallStack::shadowRowDepthToSkip() const -> size_t { TRI_ASSERT(needToSkipSubquery()); for (size_t i = 0; i < _operations.size(); ++i) { auto& call = _operations.at(i); - if (call.needSkipMore()) { + if (call.needSkipMore() || call.hardLimit == 0) { return _operations.size() - i - 1; } } diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index e2ae7672f1d7..6b7f42d215c6 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -1506,9 +1506,13 @@ auto ExecutionBlockImpl::sideEffectShadowRowForwarding(AqlCallStack& s // We are skipping on this subquery level. // Skip the row, but report skipped 1. AqlCall& shadowCall = stack.modifyCallAtDepth(shadowDepth); - TRI_ASSERT(shadowCall.needSkipMore()); - shadowCall.didSkip(1); - skipResult.didSkipSubquery(1, shadowDepth); + if (shadowCall.needSkipMore()) { + shadowCall.didSkip(1); + skipResult.didSkipSubquery(1, shadowDepth); + } else { + TRI_ASSERT(shadowCall.hardLimit == 0); + // Simply drop this shadowRow! + } } else { // We got a shadowRow of a subquery we are not skipping here. // Do proper reporting on it's call. From 3e4f6c7993af3e723228ae91c4ee622001397693 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Fri, 6 Mar 2020 02:05:53 +0100 Subject: [PATCH 56/71] Fixed all tests that are related to subqueries, which now need to assert spliced queries. --- tests/js/server/aql/aql-optimizer-indexes.js | 245 ++++++++++++------ ...o-document-materialization-arangosearch.js | 4 +- tests/js/server/aql/aql-subquery.js | 6 +- 3 files changed, 165 insertions(+), 90 deletions(-) diff --git a/tests/js/server/aql/aql-optimizer-indexes.js b/tests/js/server/aql/aql-optimizer-indexes.js index 5707d153bcbf..0f500f66d64b 100644 --- a/tests/js/server/aql/aql-optimizer-indexes.js +++ b/tests/js/server/aql/aql-optimizer-indexes.js @@ -863,12 +863,8 @@ function optimizerIndexesTestSuite () { assertEqual("SingletonNode", nodeTypes[0], query); assertEqual("SubqueryStartNode", nodeTypes[1], query); - - var subNodeTypes = plan.nodes[1].subquery.nodes.map(function(node) { - return node.type; - }); - assertNotEqual(-1, subNodeTypes.indexOf("IndexNode"), query); - assertEqual(-1, subNodeTypes.indexOf("SortNode"), query); + assertNotEqual(-1, nodeTypes.indexOf("IndexNode"), query); + assertEqual(-1, nodeTypes.indexOf("SortNode"), query); assertEqual("ReturnNode", nodeTypes[nodeTypes.length - 1], query); var results = AQL_EXECUTE(query, {}, opt); @@ -3776,9 +3772,29 @@ function optimizerIndexesMultiCollectionTestSuite () { var query = "FOR i IN " + c1.name() + " LET res = (FOR j IN " + c2.name() + " FILTER j.value == i.value SORT j.ref LIMIT 1 RETURN j) SORT res[0] RETURN i"; var plan = AQL_EXPLAIN(query, {}, opt).plan; - var nodeTypes = plan.nodes.map(function(node) { - return node.type; - }); + let idx = -1; + const nodeTypes = []; + const subqueryTypes = []; + { + let inSubquery = false; + for (const node of plan.nodes) { + const n = node.type; + if (n == "SubqueryStartNode" ) { + nodeTypes.push(n); + inSubquery = true; + } else if (n == "SubqueryEndNode" ) { + nodeTypes.push(n); + inSubquery = false; + } else if (inSubquery) { + if (n == "IndexNode") { + idx = node; + } + subqueryTypes.push(n); + } else { + nodeTypes.push(n); + } + } + } assertEqual("SingletonNode", nodeTypes[0], query); assertEqual(-1, nodeTypes.indexOf("IndexNode"), query); // no index for outer query @@ -3786,16 +3802,9 @@ function optimizerIndexesMultiCollectionTestSuite () { var sub = nodeTypes.indexOf("SubqueryStartNode"); assertNotEqual(-1, sub); - - var subNodeTypes = plan.nodes[sub].subquery.nodes.map(function(node) { - return node.type; - }); - - assertEqual("SingletonNode", subNodeTypes[0], query); - var idx = subNodeTypes.indexOf("IndexNode"); assertNotEqual(-1, idx, query); // index used for inner query - assertEqual("hash", plan.nodes[sub].subquery.nodes[idx].indexes[0].type); - assertNotEqual(-1, subNodeTypes.indexOf("SortNode"), query); // must have sort node for inner query + assertEqual("hash", idx.indexes[0].type); + assertNotEqual(-1, subqueryTypes.indexOf("SortNode"), query); // must have sort node for inner query }, //////////////////////////////////////////////////////////////////////////////// @@ -3807,10 +3816,29 @@ function optimizerIndexesMultiCollectionTestSuite () { var query = "FOR i IN " + c1.name() + " LET res = (FOR j IN " + c2.name() + " FILTER j.value == i.value SORT j.value LIMIT 1 RETURN j) SORT res[0] RETURN i"; var plan = AQL_EXPLAIN(query, {}, opt).plan; - var nodeTypes = plan.nodes.map(function(node) { - return node.type; - }); - + const nodeTypes = []; + const subqueryTypes = []; + let idx = -1; + { + let inSubquery = false; + for (const node of plan.nodes) { + const n = node.type; + if (n == "SubqueryStartNode" ) { + nodeTypes.push(n); + inSubquery = true; + } else if (n == "SubqueryEndNode" ) { + nodeTypes.push(n); + inSubquery = false; + } else if (inSubquery) { + if (n == "IndexNode") { + idx = node; + } + subqueryTypes.push(n); + } else { + nodeTypes.push(n); + } + } + } assertEqual("SingletonNode", nodeTypes[0], query); assertEqual(-1, nodeTypes.indexOf("IndexNode"), query); // no index for outer query assertNotEqual(-1, nodeTypes.indexOf("SortNode"), query); // sort node for outer query @@ -3818,15 +3846,9 @@ function optimizerIndexesMultiCollectionTestSuite () { var sub = nodeTypes.indexOf("SubqueryStartNode"); assertNotEqual(-1, sub); - var subNodeTypes = plan.nodes[sub].subquery.nodes.map(function(node) { - return node.type; - }); - - assertEqual("SingletonNode", subNodeTypes[0], query); - var idx = subNodeTypes.indexOf("IndexNode"); - assertNotEqual(-1, idx, query); // index used for inner query - assertEqual("hash", plan.nodes[sub].subquery.nodes[idx].indexes[0].type); - assertEqual(-1, subNodeTypes.indexOf("SortNode"), query); // must not have sort node for inner query + assertNotEqual(-1, subqueryTypes.indexOf("IndexNode"), query); // index used for inner query + assertEqual("hash", idx.indexes[0].type); + assertEqual(-1, subqueryTypes.indexOf("SortNode"), query); // must not have sort node for inner query }, //////////////////////////////////////////////////////////////////////////////// @@ -3839,9 +3861,29 @@ function optimizerIndexesMultiCollectionTestSuite () { var query = "FOR i IN " + c1.name() + " LET res = (FOR j IN " + c2.name() + " FILTER j.value == i.value SORT j.ref LIMIT 1 RETURN j) SORT res[0] RETURN i"; var plan = AQL_EXPLAIN(query, {}, opt).plan; - var nodeTypes = plan.nodes.map(function(node) { - return node.type; - }); + const nodeTypes = []; + const subqueryTypes = []; + let idx = -1; + { + let inSubquery = false; + for (const node of plan.nodes) { + const n = node.type; + if (n == "SubqueryStartNode" ) { + nodeTypes.push(n); + inSubquery = true; + } else if (n == "SubqueryEndNode" ) { + nodeTypes.push(n); + inSubquery = false; + } else if (inSubquery) { + if (n == "IndexNode") { + idx = node; + } + subqueryTypes.push(n); + } else { + nodeTypes.push(n); + } + } + } assertEqual("SingletonNode", nodeTypes[0], query); assertEqual(-1, nodeTypes.indexOf("IndexNode"), query); // no index for outer query @@ -3849,16 +3891,10 @@ function optimizerIndexesMultiCollectionTestSuite () { var sub = nodeTypes.indexOf("SubqueryStartNode"); assertNotEqual(-1, sub); - - var subNodeTypes = plan.nodes[sub].subquery.nodes.map(function(node) { - return node.type; - }); - - assertEqual("SingletonNode", subNodeTypes[0], query); - var idx = subNodeTypes.indexOf("IndexNode"); - assertNotEqual(-1, idx, query); // index used for inner query - assertEqual("hash", plan.nodes[sub].subquery.nodes[idx].indexes[0].type); - assertNotEqual(-1, subNodeTypes.indexOf("SortNode"), query); // must have sort node for inner query + assertNotEqual(-1, idx); + assertNotEqual(-1, subqueryTypes.indexOf("IndexNode"), query); // index used for inner query + assertEqual("hash", idx.indexes[0].type); + assertNotEqual(-1, subqueryTypes.indexOf("SortNode"), query); // must have sort node for inner query }, //////////////////////////////////////////////////////////////////////////////// @@ -3871,26 +3907,40 @@ function optimizerIndexesMultiCollectionTestSuite () { var query = "FOR i IN " + c1.name() + " LET res = (FOR j IN " + c2.name() + " FILTER j.value == i.value SORT j.value LIMIT 1 RETURN j) SORT res[0] RETURN i"; var plan = AQL_EXPLAIN(query, {}, opt).plan; - var nodeTypes = plan.nodes.map(function(node) { - return node.type; - }); + const nodeTypes = []; + const subqueryTypes = []; + let idx = -1; + { + let inSubquery = false; + for (const node of plan.nodes) { + const n = node.type; + if (n == "SubqueryStartNode" ) { + nodeTypes.push(n); + inSubquery = true; + } else if (n == "SubqueryEndNode" ) { + nodeTypes.push(n); + inSubquery = false; + } else if (inSubquery) { + if (n == "IndexNode") { + idx = node; + } + subqueryTypes.push(n); + } else { + nodeTypes.push(n); + } + } + } assertEqual("SingletonNode", nodeTypes[0], query); assertEqual(-1, nodeTypes.indexOf("IndexNode"), query); // no index for outer query assertNotEqual(-1, nodeTypes.indexOf("SortNode"), query); // sort node for outer query - var sub = nodeTypes.indexOf("SubqueryStartNode"); - assertNotEqual(-1, sub); + assertNotEqual(-1, nodeTypes.indexOf("SubqueryStartNode"), query); - var subNodeTypes = plan.nodes[sub].subquery.nodes.map(function(node) { - return node.type; - }); - - assertEqual("SingletonNode", subNodeTypes[0], query); - var idx = subNodeTypes.indexOf("IndexNode"); + assertNotEqual(-1, subqueryTypes.indexOf("IndexNode"), query); assertNotEqual(-1, idx, query); // index used for inner query - assertEqual("hash", plan.nodes[sub].subquery.nodes[idx].indexes[0].type); - assertEqual(-1, subNodeTypes.indexOf("SortNode"), query); // we're filtering on a constant, must not have sort node for inner query + assertEqual("hash", idx.indexes[0].type); + assertEqual(-1, subqueryTypes.indexOf("SortNode"), query); // we're filtering on a constant, must not have sort node for inner query }, //////////////////////////////////////////////////////////////////////////////// @@ -3903,26 +3953,40 @@ function optimizerIndexesMultiCollectionTestSuite () { var query = "FOR i IN " + c1.name() + " LET res = (FOR z IN 1..2 FOR j IN " + c2.name() + " FILTER j.value == i.value SORT j.value LIMIT 1 RETURN j) SORT res[0] RETURN i"; var plan = AQL_EXPLAIN(query, {}, opt).plan; - var nodeTypes = plan.nodes.map(function(node) { - return node.type; - }); + const nodeTypes = []; + const subqueryTypes = []; + let idx = -1; + { + let inSubquery = false; + for (const node of plan.nodes) { + const n = node.type; + if (n == "SubqueryStartNode" ) { + nodeTypes.push(n); + inSubquery = true; + } else if (n == "SubqueryEndNode" ) { + nodeTypes.push(n); + inSubquery = false; + } else if (inSubquery) { + if (n == "IndexNode") { + idx = node; + } + subqueryTypes.push(n); + } else { + nodeTypes.push(n); + } + } + } assertEqual("SingletonNode", nodeTypes[0], query); assertEqual(-1, nodeTypes.indexOf("IndexNode"), query); // no index for outer query assertNotEqual(-1, nodeTypes.indexOf("SortNode"), query); // sort node for outer query var sub = nodeTypes.indexOf("SubqueryStartNode"); - assertNotEqual(-1, sub); - - var subNodeTypes = plan.nodes[sub].subquery.nodes.map(function(node) { - return node.type; - }); - - assertEqual("SingletonNode", subNodeTypes[0], query); - var idx = subNodeTypes.indexOf("IndexNode"); + assertNotEqual(-1, sub, query); + assertNotEqual(-1, subqueryTypes.indexOf("IndexNode"), query); assertNotEqual(-1, idx, query); // index used for inner query - assertEqual("hash", plan.nodes[sub].subquery.nodes[idx].indexes[0].type); - assertNotEqual(-1, subNodeTypes.indexOf("SortNode"), query); // we're filtering on a constant, but we're in an inner loop + assertEqual("hash", idx.indexes[0].type); + assertNotEqual(-1, subqueryTypes.indexOf("SortNode"), query); // we're filtering on a constant, but we're in an inner loop }, //////////////////////////////////////////////////////////////////////////////// @@ -3935,26 +3999,39 @@ function optimizerIndexesMultiCollectionTestSuite () { var query = "FOR i IN " + c1.name() + " LET res = (FOR j IN " + c2.name() + " FILTER j.ref == i.ref SORT j.ref LIMIT 1 RETURN j) SORT res[0] RETURN i"; var plan = AQL_EXPLAIN(query, {}, opt).plan; - var nodeTypes = plan.nodes.map(function(node) { - return node.type; - }); + const nodeTypes = []; + const subqueryTypes = []; + let idx = -1; + { + let inSubquery = false; + for (const node of plan.nodes) { + const n = node.type; + if (n == "SubqueryStartNode" ) { + nodeTypes.push(n); + inSubquery = true; + } else if (n == "SubqueryEndNode" ) { + nodeTypes.push(n); + inSubquery = false; + } else if (inSubquery) { + if (n == "IndexNode") { + idx = node; + } + subqueryTypes.push(n); + } else { + nodeTypes.push(n); + } + } + } assertEqual("SingletonNode", nodeTypes[0], query); assertEqual(-1, nodeTypes.indexOf("IndexNode"), query); // no index for outer query assertNotEqual(-1, nodeTypes.indexOf("SortNode"), query); // sort node for outer query + assertNotEqual(-1, nodeTypes.indexOf("SubqueryStartNode"), query); - var sub = nodeTypes.indexOf("SubqueryStartNode"); - assertNotEqual(-1, sub); - - var subNodeTypes = plan.nodes[sub].subquery.nodes.map(function(node) { - return node.type; - }); - - assertEqual("SingletonNode", subNodeTypes[0], query); - var idx = subNodeTypes.indexOf("IndexNode"); + assertNotEqual(-1, subqueryTypes.indexOf("IndexNode"), query); assertNotEqual(-1, idx, query); // index used for inner query - assertEqual("skiplist", plan.nodes[sub].subquery.nodes[idx].indexes[0].type); - assertEqual(-1, subNodeTypes.indexOf("SortNode"), query); // must not have sort node for inner query + assertEqual("skiplist", idx.indexes[0].type); + assertEqual(-1, subqueryTypes.indexOf("SortNode"), query); // must not have sort node for inner query }, //////////////////////////////////////////////////////////////////////////////// diff --git a/tests/js/server/aql/aql-optimizer-rule-no-document-materialization-arangosearch.js b/tests/js/server/aql/aql-optimizer-rule-no-document-materialization-arangosearch.js index d6fb5cfe0102..59626c692b9a 100644 --- a/tests/js/server/aql/aql-optimizer-rule-no-document-materialization-arangosearch.js +++ b/tests/js/server/aql/aql-optimizer-rule-no-document-materialization-arangosearch.js @@ -145,10 +145,8 @@ function noDocumentMaterializationArangoSearchRuleTestSuite () { "SORT CONCAT(a, e) LIMIT 10 RETURN d.obj.e.e1"; let plan = AQL_EXPLAIN(query).plan; assertTrue(plan.nodes.filter(obj => { - return obj.type === "SubqueryNode"; - })[0].subquery.nodes.filter(obj => { return obj.type === "EnumerateViewNode"; - })[0].noMaterialization); + })[1].noMaterialization); let result = AQL_EXECUTE(query); assertEqual(2, result.json.length); let expectedKeys = new Set([14, 4]); diff --git a/tests/js/server/aql/aql-subquery.js b/tests/js/server/aql/aql-subquery.js index a2cc8ded83b7..d3002e80137f 100644 --- a/tests/js/server/aql/aql-subquery.js +++ b/tests/js/server/aql/aql-subquery.js @@ -343,8 +343,8 @@ function ahuacatlSubqueryTestSuite () { /// A count collect block will produce an output even if it does not get an input /// specifically it will rightfully count 0. /// The insert block will write into the collection if it gets an input. -/// So the assertion here is, that if a subquery has no input, than all it's -/// Parts do not have side-effects, but the subquery still prduces valid results +/// Even if the outer subquery is skipped. Henve we require to have documents +/// inserted here. //////////////////////////////////////////////////////////////////////////////// testCollectWithinEmptyNestedSubquery: function () { const colName = "UnitTestSubqueryCollection"; @@ -367,7 +367,7 @@ function ahuacatlSubqueryTestSuite () { var actual = getQueryResults(query); assertEqual(expected, actual); - assertEqual(db[colName].count(), 0); + assertEqual(db[colName].count(), 1); } finally { db._drop(colName); } From 435e84114d087e04d8da8570a3cf892340ea9bf8 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Fri, 6 Mar 2020 08:12:20 +0100 Subject: [PATCH 57/71] Fixed jslint --- tests/js/server/aql/aql-optimizer-indexes.js | 36 ++++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/js/server/aql/aql-optimizer-indexes.js b/tests/js/server/aql/aql-optimizer-indexes.js index 0f500f66d64b..b42fd4955654 100644 --- a/tests/js/server/aql/aql-optimizer-indexes.js +++ b/tests/js/server/aql/aql-optimizer-indexes.js @@ -3779,14 +3779,14 @@ function optimizerIndexesMultiCollectionTestSuite () { let inSubquery = false; for (const node of plan.nodes) { const n = node.type; - if (n == "SubqueryStartNode" ) { + if (n === "SubqueryStartNode" ) { nodeTypes.push(n); inSubquery = true; - } else if (n == "SubqueryEndNode" ) { + } else if (n === "SubqueryEndNode" ) { nodeTypes.push(n); inSubquery = false; } else if (inSubquery) { - if (n == "IndexNode") { + if (n === "IndexNode") { idx = node; } subqueryTypes.push(n); @@ -3823,14 +3823,14 @@ function optimizerIndexesMultiCollectionTestSuite () { let inSubquery = false; for (const node of plan.nodes) { const n = node.type; - if (n == "SubqueryStartNode" ) { + if (n === "SubqueryStartNode" ) { nodeTypes.push(n); inSubquery = true; - } else if (n == "SubqueryEndNode" ) { + } else if (n === "SubqueryEndNode" ) { nodeTypes.push(n); inSubquery = false; } else if (inSubquery) { - if (n == "IndexNode") { + if (n === "IndexNode") { idx = node; } subqueryTypes.push(n); @@ -3868,14 +3868,14 @@ function optimizerIndexesMultiCollectionTestSuite () { let inSubquery = false; for (const node of plan.nodes) { const n = node.type; - if (n == "SubqueryStartNode" ) { + if (n === "SubqueryStartNode" ) { nodeTypes.push(n); inSubquery = true; - } else if (n == "SubqueryEndNode" ) { + } else if (n === "SubqueryEndNode" ) { nodeTypes.push(n); inSubquery = false; } else if (inSubquery) { - if (n == "IndexNode") { + if (n === "IndexNode") { idx = node; } subqueryTypes.push(n); @@ -3914,14 +3914,14 @@ function optimizerIndexesMultiCollectionTestSuite () { let inSubquery = false; for (const node of plan.nodes) { const n = node.type; - if (n == "SubqueryStartNode" ) { + if (n === "SubqueryStartNode" ) { nodeTypes.push(n); inSubquery = true; - } else if (n == "SubqueryEndNode" ) { + } else if (n === "SubqueryEndNode" ) { nodeTypes.push(n); inSubquery = false; } else if (inSubquery) { - if (n == "IndexNode") { + if (n === "IndexNode") { idx = node; } subqueryTypes.push(n); @@ -3960,14 +3960,14 @@ function optimizerIndexesMultiCollectionTestSuite () { let inSubquery = false; for (const node of plan.nodes) { const n = node.type; - if (n == "SubqueryStartNode" ) { + if (n === "SubqueryStartNode" ) { nodeTypes.push(n); inSubquery = true; - } else if (n == "SubqueryEndNode" ) { + } else if (n === "SubqueryEndNode" ) { nodeTypes.push(n); inSubquery = false; } else if (inSubquery) { - if (n == "IndexNode") { + if (n === "IndexNode") { idx = node; } subqueryTypes.push(n); @@ -4006,14 +4006,14 @@ function optimizerIndexesMultiCollectionTestSuite () { let inSubquery = false; for (const node of plan.nodes) { const n = node.type; - if (n == "SubqueryStartNode" ) { + if (n === "SubqueryStartNode" ) { nodeTypes.push(n); inSubquery = true; - } else if (n == "SubqueryEndNode" ) { + } else if (n === "SubqueryEndNode" ) { nodeTypes.push(n); inSubquery = false; } else if (inSubquery) { - if (n == "IndexNode") { + if (n === "IndexNode") { idx = node; } subqueryTypes.push(n); From 1622f0873b1b5afba3feb401f83f19d10c633f35 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Tue, 10 Mar 2020 17:40:18 +0100 Subject: [PATCH 58/71] Fixed the callstack that has been seperated from the clientCall. In some places it was not handled correctly. --- arangod/Aql/AqlCallStack.cpp | 12 +++--------- arangod/Aql/ExecutionBlockImpl.cpp | 5 +++-- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/arangod/Aql/AqlCallStack.cpp b/arangod/Aql/AqlCallStack.cpp index 65992ab486c4..77ffed71ad4f 100644 --- a/arangod/Aql/AqlCallStack.cpp +++ b/arangod/Aql/AqlCallStack.cpp @@ -184,15 +184,9 @@ auto AqlCallStack::toString() const -> std::string { auto AqlCallStack::createEquivalentFetchAllShadowRowsStack() const -> AqlCallStack { AqlCallStack res{*this}; - if (subqueryLevel() > 1) { - // We only replace the subquery levels. - // The releveant call may include a softLimit - // which needs to be honored here. - - std::replace_if( - res._operations.begin(), res._operations.end() - 1, - [](auto const&) -> bool { return true; }, AqlCall{}); - } + std::replace_if( + res._operations.begin(), res._operations.end(), + [](auto const&) -> bool { return true; }, AqlCall{}); return res; } diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index b8fc7012bea1..7d397d706c94 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -1244,7 +1244,9 @@ auto ExecutionBlockImpl::executeFetcher(AqlCallStack& stack, AqlCallTy // by skipping them. SO we need to fetch all shadow rows in order to // trigger this Executor with everthing from above. // NOTE: The Executor needs to discard shadowRows, and do the + static_assert(std::is_same_v>); auto fetchAllStack = stack.createEquivalentFetchAllShadowRowsStack(); + fetchAllStack.pushCall(aqlCall); auto res = _rowFetcher.execute(fetchAllStack); // Just make sure we did not Skip anything TRI_ASSERT(std::get(res).nothingSkipped()); @@ -1255,8 +1257,7 @@ auto ExecutionBlockImpl::executeFetcher(AqlCallStack& stack, AqlCallTy // SubqueryStart and the partnered SubqueryEnd by *not* // pushing the upstream request. if constexpr (!std::is_same_v) { - auto callCopy = _upstreamRequest; - stack.pushCall(std::move(callCopy)); + stack.pushCall(std::move(aqlCall)); } auto const result = _rowFetcher.execute(stack); From aef48b62c82d6e7af7489d6e6f4dc85b092dd6d8 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 11 Mar 2020 13:05:56 +0100 Subject: [PATCH 59/71] Fixed skip result forwarding in Scatter/Gather --- arangod/Aql/BlocksWithClients.cpp | 8 ++--- arangod/Aql/DistributeExecutor.cpp | 32 +++++++++++++------ arangod/Aql/DistributeExecutor.h | 17 ++++++---- arangod/Aql/ExecutionBlockImpl.cpp | 20 +++++++++--- arangod/Aql/ExecutionBlockImpl.h | 2 +- .../Aql/MultiDependencySingleRowFetcher.cpp | 2 +- arangod/Aql/ScatterExecutor.cpp | 24 +++++++------- arangod/Aql/ScatterExecutor.h | 8 ++--- tests/Aql/IdExecutorTest.cpp | 9 +++--- 9 files changed, 76 insertions(+), 46 deletions(-) diff --git a/arangod/Aql/BlocksWithClients.cpp b/arangod/Aql/BlocksWithClients.cpp index 513577fae4ca..0e7579a8bc96 100644 --- a/arangod/Aql/BlocksWithClients.cpp +++ b/arangod/Aql/BlocksWithClients.cpp @@ -246,7 +246,8 @@ auto BlocksWithClientsImpl::executeWithoutTraceForClient(AqlCallStack _upstreamState = state; } // If we get here we have data and can return it. - return dataContainer.execute(call, _upstreamState); + stack.pushCall(call); + return dataContainer.execute(stack, _upstreamState); } template @@ -264,10 +265,9 @@ auto BlocksWithClientsImpl::fetchMore(AqlCallStack stack) -> Execution TRI_ASSERT(_dependencies.size() == 1); auto [state, skipped, block] = _dependencies[0]->execute(stack); - // We can never ever forward skip! // We could need the row in a different block, and once skipped // we cannot get it back. - TRI_ASSERT(skipped.nothingSkipped()); + TRI_ASSERT(skipped.getSkipCount() == 0); TRI_IF_FAILURE("ExecutionBlock::getBlock") { THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); @@ -276,7 +276,7 @@ auto BlocksWithClientsImpl::fetchMore(AqlCallStack stack) -> Execution // Waiting -> no block TRI_ASSERT(state != ExecutionState::WAITING || block == nullptr); if (block != nullptr) { - _executor.distributeBlock(block, _clientBlockData); + _executor.distributeBlock(block, skipped, _clientBlockData); } return state; diff --git a/arangod/Aql/DistributeExecutor.cpp b/arangod/Aql/DistributeExecutor.cpp index 6f4574830ef8..a528e68c3fc6 100644 --- a/arangod/Aql/DistributeExecutor.cpp +++ b/arangod/Aql/DistributeExecutor.cpp @@ -153,6 +153,12 @@ auto DistributeExecutor::ClientBlockData::addBlock(SharedAqlItemBlockPtr block, _queue.emplace_back(block, std::move(usedIndexes)); } +auto DistributeExecutor::ClientBlockData::addSkipResult(SkipResult const& skipResult) -> void { + TRI_ASSERT(_skipped.subqueryDepth() == 1 || + _skipped.subqueryDepth() == skipResult.subqueryDepth()); + _skipped.merge(skipResult, false); +} + auto DistributeExecutor::ClientBlockData::hasDataFor(AqlCall const& call) -> bool { return _executorHasMore || !_queue.empty(); } @@ -167,7 +173,8 @@ auto DistributeExecutor::ClientBlockData::hasDataFor(AqlCall const& call) -> boo * * @return SharedAqlItemBlockPtr a joind block from the queue. */ -auto DistributeExecutor::ClientBlockData::popJoinedBlock() -> SharedAqlItemBlockPtr { +auto DistributeExecutor::ClientBlockData::popJoinedBlock() + -> std::tuple { // There are some optimizations available in this implementation. // Namely we could apply good logic to cut the blocks at shadow rows // in order to allow the IDexecutor to hand them out en-block. @@ -209,14 +216,16 @@ auto DistributeExecutor::ClientBlockData::popJoinedBlock() -> SharedAqlItemBlock // Drop block form queue. _queue.pop_front(); } - return newBlock; + SkipResult skip = _skipped; + _skipped.reset(); + return {newBlock, skip}; } -auto DistributeExecutor::ClientBlockData::execute(AqlCall call, ExecutionState upstreamState) +auto DistributeExecutor::ClientBlockData::execute(AqlCallStack callStack, ExecutionState upstreamState) -> std::tuple { TRI_ASSERT(_executor != nullptr); // Make sure we actually have data before you call execute - TRI_ASSERT(hasDataFor(call)); + TRI_ASSERT(hasDataFor(callStack.peek())); if (!_executorHasMore) { // This cast is guaranteed, we create this a couple lines above and only // this executor is used here. @@ -225,16 +234,15 @@ auto DistributeExecutor::ClientBlockData::execute(AqlCall call, ExecutionState u auto casted = static_cast>*>(_executor.get()); TRI_ASSERT(casted != nullptr); - auto block = popJoinedBlock(); + auto [block, skipped] = popJoinedBlock(); // We will at least get one block, otherwise the hasDataFor would // be required to return false! TRI_ASSERT(block != nullptr); - casted->injectConstantBlock(block); + casted->injectConstantBlock(block, skipped); _executorHasMore = true; } - AqlCallStack stack{call}; - auto [state, skipped, result] = _executor->execute(stack); + auto [state, skipped, result] = _executor->execute(callStack); // We have all data locally cannot wait here. TRI_ASSERT(state != ExecutionState::WAITING); @@ -258,7 +266,7 @@ auto DistributeExecutor::ClientBlockData::execute(AqlCall call, ExecutionState u DistributeExecutor::DistributeExecutor(DistributeExecutorInfos const& infos) : _infos(infos){}; -auto DistributeExecutor::distributeBlock(SharedAqlItemBlockPtr block, +auto DistributeExecutor::distributeBlock(SharedAqlItemBlockPtr block, SkipResult skipped, std::unordered_map& blockMap) -> void { std::unordered_map> choosenMap; @@ -290,6 +298,12 @@ auto DistributeExecutor::distributeBlock(SharedAqlItemBlockPtr block, } target->second.addBlock(block, std::move(value)); } + + // Add the skipResult to all clients. + // It needs to be fetched once for every client. + for (auto& [key, map] : blockMap) { + map.addSkipResult(skipped); + } } auto DistributeExecutor::getClient(SharedAqlItemBlockPtr block, size_t rowIndex) diff --git a/arangod/Aql/DistributeExecutor.h b/arangod/Aql/DistributeExecutor.h index 83d2b58c4a8c..0e7bd1a618b6 100644 --- a/arangod/Aql/DistributeExecutor.h +++ b/arangod/Aql/DistributeExecutor.h @@ -92,9 +92,11 @@ class DistributeExecutor { auto clear() -> void; auto addBlock(SharedAqlItemBlockPtr block, std::vector usedIndexes) -> void; + + auto addSkipResult(SkipResult const& skipResult) -> void; auto hasDataFor(AqlCall const& call) -> bool; - auto execute(AqlCall call, ExecutionState upstreamState) + auto execute(AqlCallStack callStack, ExecutionState upstreamState) -> std::tuple; private: @@ -102,19 +104,21 @@ class DistributeExecutor { * @brief This call will join as many blocks as available from the queue * and return them in a SingleBlock. We then use the IdExecutor * to hand out the data contained in these blocks - * We do on purpose not give any kind of guarantees on the sizing of - * this block to be flexible with the implementation, and find a good + * We do on purpose not give any kind of guarantees on the sizing + * of this block to be flexible with the implementation, and find a good * trade-off between blocksize and block copy operations. * - * @return SharedAqlItemBlockPtr a joind block from the queue. + * @return SharedAqlItemBlockPtr a joined block from the queue. + * SkipResult the skip information matching to this block */ - auto popJoinedBlock() -> SharedAqlItemBlockPtr; + auto popJoinedBlock() -> std::tuple; private: AqlItemBlockManager& _blockManager; ExecutorInfos const& _infos; std::deque>> _queue; + SkipResult _skipped{}; // This is unique_ptr to get away with everything being forward declared... std::unique_ptr _executor; @@ -132,9 +136,10 @@ class DistributeExecutor { * Hence this method is not const ;( * * @param block The block to be distributed + * @param skipped The rows that have been skipped from upstream * @param blockMap Map client => Data. Will provide the required data to the correct client. */ - auto distributeBlock(SharedAqlItemBlockPtr block, + auto distributeBlock(SharedAqlItemBlockPtr block, SkipResult skipped, std::unordered_map& blockMap) -> void; private: diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index 7d397d706c94..4e4514dc0fe9 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -711,8 +711,8 @@ namespace arangodb::aql { template <> template <> -auto ExecutionBlockImpl>::injectConstantBlock>(SharedAqlItemBlockPtr block) - -> void { +auto ExecutionBlockImpl>::injectConstantBlock>( + SharedAqlItemBlockPtr block, SkipResult skipped) -> void { // reinitialize the DependencyProxy _dependencyProxy.reset(); @@ -721,8 +721,15 @@ auto ExecutionBlockImpl>::injectConstantBlock ExecutionBlockImpl>:: SharedAqlItemBlockPtr block = input.cloneToBlock(_engine->itemBlockManager(), *(infos().registersToKeep()), infos().numberOfOutputRegisters()); - - injectConstantBlock(block); + TRI_ASSERT(_skipped.nothingSkipped()); + _skipped.reset(); + // We inject an empty copy of our skipped here, + // This is resettet, but will maintain the size + injectConstantBlock(block, _skipped); // end of default initializeCursor return ExecutionBlock::initializeCursor(input); diff --git a/arangod/Aql/ExecutionBlockImpl.h b/arangod/Aql/ExecutionBlockImpl.h index dffc6fa8654c..1b4365c78c40 100644 --- a/arangod/Aql/ExecutionBlockImpl.h +++ b/arangod/Aql/ExecutionBlockImpl.h @@ -225,7 +225,7 @@ class ExecutionBlockImpl final : public ExecutionBlock { [[nodiscard]] std::pair initializeCursor(InputAqlItemRow const& input) override; template >>> - auto injectConstantBlock(SharedAqlItemBlockPtr block) -> void; + auto injectConstantBlock(SharedAqlItemBlockPtr block, SkipResult skipped) -> void; [[nodiscard]] Infos const& infos() const; diff --git a/arangod/Aql/MultiDependencySingleRowFetcher.cpp b/arangod/Aql/MultiDependencySingleRowFetcher.cpp index 2e475a395e1b..8a133e937687 100644 --- a/arangod/Aql/MultiDependencySingleRowFetcher.cpp +++ b/arangod/Aql/MultiDependencySingleRowFetcher.cpp @@ -439,7 +439,7 @@ auto MultiDependencySingleRowFetcher::execute(AqlCallStack const& stack, } else { TRI_ASSERT(skipped.nothingSkipped()); } - skippedTotal += skipped; + skippedTotal.merge(skipped, false); ranges.emplace_back(dependency, range); } } diff --git a/arangod/Aql/ScatterExecutor.cpp b/arangod/Aql/ScatterExecutor.cpp index a9edc4a2e55f..b81449906d9a 100644 --- a/arangod/Aql/ScatterExecutor.cpp +++ b/arangod/Aql/ScatterExecutor.cpp @@ -64,10 +64,11 @@ auto ScatterExecutor::ClientBlockData::clear() -> void { _executorHasMore = false; } -auto ScatterExecutor::ClientBlockData::addBlock(SharedAqlItemBlockPtr block) -> void { +auto ScatterExecutor::ClientBlockData::addBlock(SharedAqlItemBlockPtr block, + SkipResult skipped) -> void { // NOTE: - // There given ItemBlock will be reused in all requesting blocks. - // However, the next followwing block could be passthrough. + // The given ItemBlock will be reused in all requesting blocks. + // However, the next following block could be passthrough. // If it is, it will modify that data stored in block. // If now anther client requests the same block, it is not // the original any more, but a modified version. @@ -75,20 +76,20 @@ auto ScatterExecutor::ClientBlockData::addBlock(SharedAqlItemBlockPtr block) -> // is empty. If another peer-calculation has written to this value // this assertion does not hold true anymore. // Hence we are required to do an indepth cloning here. - _queue.emplace_back(block->slice(0, block->size())); + _queue.emplace_back(block->slice(0, block->size()), skipped); } auto ScatterExecutor::ClientBlockData::hasDataFor(AqlCall const& call) -> bool { return _executorHasMore || !_queue.empty(); } -auto ScatterExecutor::ClientBlockData::execute(AqlCall call, ExecutionState upstreamState) +auto ScatterExecutor::ClientBlockData::execute(AqlCallStack callStack, ExecutionState upstreamState) -> std::tuple { TRI_ASSERT(_executor != nullptr); // Make sure we actually have data before you call execute - TRI_ASSERT(hasDataFor(call)); + TRI_ASSERT(hasDataFor(callStack.peek())); if (!_executorHasMore) { - auto const& block = _queue.front(); + auto const& [block, skipResult] = _queue.front(); // This cast is guaranteed, we create this a couple lines above and only // this executor is used here. // Unfortunately i did not get a version compiled were i could only forward @@ -96,12 +97,11 @@ auto ScatterExecutor::ClientBlockData::execute(AqlCall call, ExecutionState upst auto casted = static_cast>*>(_executor.get()); TRI_ASSERT(casted != nullptr); - casted->injectConstantBlock(block); + casted->injectConstantBlock(block, skipResult); _executorHasMore = true; _queue.pop_front(); } - AqlCallStack stack{call}; - auto [state, skipped, result] = _executor->execute(stack); + auto [state, skipped, result] = _executor->execute(callStack); // We have all data locally cannot wait here. TRI_ASSERT(state != ExecutionState::WAITING); @@ -124,12 +124,12 @@ auto ScatterExecutor::ClientBlockData::execute(AqlCall call, ExecutionState upst ScatterExecutor::ScatterExecutor(ExecutorInfos const&){}; -auto ScatterExecutor::distributeBlock(SharedAqlItemBlockPtr block, +auto ScatterExecutor::distributeBlock(SharedAqlItemBlockPtr block, SkipResult skipped, std::unordered_map& blockMap) const -> void { // Scatter returns every block on every client as is. for (auto& [id, list] : blockMap) { - list.addBlock(block); + list.addBlock(block, skipped); } } diff --git a/arangod/Aql/ScatterExecutor.h b/arangod/Aql/ScatterExecutor.h index 2b1b850c1e60..10a1316cec02 100644 --- a/arangod/Aql/ScatterExecutor.h +++ b/arangod/Aql/ScatterExecutor.h @@ -57,14 +57,14 @@ class ScatterExecutor { ExecutorInfos const& scatterInfos); auto clear() -> void; - auto addBlock(SharedAqlItemBlockPtr block) -> void; + auto addBlock(SharedAqlItemBlockPtr block, SkipResult skipped) -> void; auto hasDataFor(AqlCall const& call) -> bool; - auto execute(AqlCall call, ExecutionState upstreamState) + auto execute(AqlCallStack callStack, ExecutionState upstreamState) -> std::tuple; private: - std::deque _queue; + std::deque> _queue; // This is unique_ptr to get away with everything beeing forward declared... std::unique_ptr _executor; bool _executorHasMore; @@ -73,7 +73,7 @@ class ScatterExecutor { ScatterExecutor(ExecutorInfos const&); ~ScatterExecutor() = default; - auto distributeBlock(SharedAqlItemBlockPtr block, + auto distributeBlock(SharedAqlItemBlockPtr block, SkipResult skipped, std::unordered_map& blockMap) const -> void; }; diff --git a/tests/Aql/IdExecutorTest.cpp b/tests/Aql/IdExecutorTest.cpp index 407e999eb970..4ec6f1b94df8 100644 --- a/tests/Aql/IdExecutorTest.cpp +++ b/tests/Aql/IdExecutorTest.cpp @@ -443,7 +443,8 @@ TEST_P(BlockOverloadTest, test_hardlimit_const_fetcher) { // Inject block auto inputBlock = buildBlock<1>(itemBlockManager, {{0}, {1}, {2}, {3}, {4}, {5}, {6}}); - testee.injectConstantBlock(inputBlock); + + testee.injectConstantBlock(inputBlock, SkipResult{}); } { // Now call with too small hardLimit @@ -480,7 +481,7 @@ TEST_P(BlockOverloadTest, test_hardlimit_const_fetcher_shadow_rows_at_end) { auto inputBlock = buildBlock<1>(itemBlockManager, {{0}, {1}, {2}, {3}, {4}, {5}, {6}}, {{5, 0}, {6, 1}}); - testee.injectConstantBlock(inputBlock); + testee.injectConstantBlock(inputBlock, SkipResult{}); } { // Now call with too small hardLimit @@ -517,7 +518,7 @@ TEST_P(BlockOverloadTest, test_hardlimit_const_fetcher_shadow_rows_in_between) { auto inputBlock = buildBlock<1>(itemBlockManager, {{0}, {1}, {2}, {3}, {4}, {5}, {6}}, {{3, 0}, {4, 1}, {6, 0}}); - testee.injectConstantBlock(inputBlock); + testee.injectConstantBlock(inputBlock, SkipResult{}); } { // Now call with too small hardLimit @@ -557,7 +558,7 @@ TEST_P(BlockOverloadTest, test_hardlimit_const_fetcher_consecutive_shadow_rows) auto inputBlock = buildBlock<1>(itemBlockManager, {{0}, {1}, {2}, {3}, {4}, {5}, {6}}, {{3, 0}, {4, 1}, {5, 0}, {6, 0}}); - testee.injectConstantBlock(inputBlock); + testee.injectConstantBlock(inputBlock, SkipResult{}); } // We can only return until the next top-level shadow row is reached. { From 666fe7d62458237e583887c514b743b380a999ca Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 11 Mar 2020 14:18:56 +0100 Subject: [PATCH 60/71] Fixed assertion if the ConstFetcher gets a block with subquery level skip injected --- arangod/Aql/ExecutionBlockImpl.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index 4e4514dc0fe9..2b49bfce826e 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -1777,8 +1777,9 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { _execState == ExecState::UPSTREAM); } - // Skip can only be > 0 if we are in upstream cases. - TRI_ASSERT(_skipped.nothingSkipped() || _execState == ExecState::UPSTREAM); + // Skip can only be > 0 if we are in upstream cases, or if we got injected a block + TRI_ASSERT(_skipped.nothingSkipped() || _execState == ExecState::UPSTREAM || + (std::is_same_v>)); if constexpr (std::is_same_v) { // In subqeryEndExecutor we actually manage two calls. From 680bce4429542b07155f3e0baf6346fe7d7378db Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 11 Mar 2020 20:35:58 +0100 Subject: [PATCH 61/71] Moved merging of SubquerySkips in MultiDependencies into the Fetcher --- .../Aql/MultiDependencySingleRowFetcher.cpp | 34 ++++++++++++++++++- arangod/Aql/MultiDependencySingleRowFetcher.h | 2 ++ arangod/Aql/SkipResult.cpp | 8 +++++ arangod/Aql/SkipResult.h | 1 + 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/arangod/Aql/MultiDependencySingleRowFetcher.cpp b/arangod/Aql/MultiDependencySingleRowFetcher.cpp index 8a133e937687..fba33607ad79 100644 --- a/arangod/Aql/MultiDependencySingleRowFetcher.cpp +++ b/arangod/Aql/MultiDependencySingleRowFetcher.cpp @@ -147,6 +147,8 @@ std::pair MultiDependencySingleRowFetcher::fet ++dep._rowIndex; } } + // We have delivered a shadowRow, we now may get additional subquery skip counters again. + _didReturnSubquerySkips = false; } ExecutionState const state = allDone ? ExecutionState::DONE : ExecutionState::HASMORE; @@ -436,16 +438,46 @@ auto MultiDependencySingleRowFetcher::execute(AqlCallStack const& stack, // Got a result, call is no longer in flight maybeCallInFlight = std::nullopt; allAskedDepsAreWaiting = false; + + // NOTE: + // in this fetcher case we do not have and do not want to have + // any control of the order the upstream responses are entering. + // Every of the upstream response will contain an identical skipped + // stack on the subqueries. + // We only need to forward the skipping of any one of those. + // So we implemented the following logic to return the skip + // information for the first on that arrives and all other + // subquery skip informations will be discarded. + if (!_didReturnSubquerySkips) { + // We have nothing skipped locally. + TRI_ASSERT(skippedTotal.subqueryDepth() == 1); + TRI_ASSERT(skippedTotal.getSkipCount() == 0); + + // We forward the skip block as is. + // This will also include the skips on subquery level + skippedTotal = skipped; + // Do this only once. + // The first response will contain the amount of rows skipped + // in subquery + _didReturnSubquerySkips = true; + } else { + // We only need the skip amount on the top level. + // Another dependency has forwarded the subquery level skips + // already + skippedTotal.mergeOnlyTopLevel(skipped); + } + } else { TRI_ASSERT(skipped.nothingSkipped()); } - skippedTotal.merge(skipped, false); + ranges.emplace_back(dependency, range); } } auto const state = std::invoke([&]() { if (askedAtLeastOneDep && allAskedDepsAreWaiting) { + TRI_ASSERT(skippedTotal.nothingSkipped()); return ExecutionState::WAITING; } else { return upstreamState(); diff --git a/arangod/Aql/MultiDependencySingleRowFetcher.h b/arangod/Aql/MultiDependencySingleRowFetcher.h index ccc173935428..13358f5e95c4 100644 --- a/arangod/Aql/MultiDependencySingleRowFetcher.h +++ b/arangod/Aql/MultiDependencySingleRowFetcher.h @@ -159,6 +159,8 @@ class MultiDependencySingleRowFetcher { /// in initOnce() to make sure that init() is called exactly once. std::vector> _callsInFlight; + bool _didReturnSubquerySkips{false}; + private: /** * @brief Delegates to ExecutionBlock::fetchBlock() diff --git a/arangod/Aql/SkipResult.cpp b/arangod/Aql/SkipResult.cpp index a37c9109a84d..7cbfc0545fc6 100644 --- a/arangod/Aql/SkipResult.cpp +++ b/arangod/Aql/SkipResult.cpp @@ -140,6 +140,14 @@ auto SkipResult::merge(SkipResult const& other, bool excludeTopLevel) noexcept - } } +auto SkipResult::mergeOnlyTopLevel(SkipResult const& other) noexcept -> void { + _skipped.reserve(other.subqueryDepth()); + while (other.subqueryDepth() > subqueryDepth()) { + incrementSubquery(); + } + _skipped.back() += other._skipped.back(); +} + auto SkipResult::operator+=(SkipResult const& b) noexcept -> SkipResult& { didSkip(b.getSkipCount()); return *this; diff --git a/arangod/Aql/SkipResult.h b/arangod/Aql/SkipResult.h index c00d6947f577..84db8ebdba8d 100644 --- a/arangod/Aql/SkipResult.h +++ b/arangod/Aql/SkipResult.h @@ -69,6 +69,7 @@ class SkipResult { auto reset() -> void; auto merge(SkipResult const& other, bool excludeTopLevel) noexcept -> void; + auto mergeOnlyTopLevel(SkipResult const& other) noexcept -> void; auto operator+=(SkipResult const& b) noexcept -> SkipResult&; From 3be7aac08a3ef195e1e703eac9a36cd57488ecea Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 11 Mar 2020 20:36:22 +0100 Subject: [PATCH 62/71] Removed dead code and fixed overproduction of Rows in Subquery Executor --- arangod/Aql/SubqueryExecutor.cpp | 78 ++------------------------------ 1 file changed, 3 insertions(+), 75 deletions(-) diff --git a/arangod/Aql/SubqueryExecutor.cpp b/arangod/Aql/SubqueryExecutor.cpp index 6520cae4c352..2319291c4bd2 100644 --- a/arangod/Aql/SubqueryExecutor.cpp +++ b/arangod/Aql/SubqueryExecutor.cpp @@ -77,80 +77,6 @@ SubqueryExecutor::~SubqueryExecutor() = default; template std::pair SubqueryExecutor::produceRows(OutputAqlItemRow& output) { -#if 0 - if (_state == ExecutorState::DONE && !_input.isInitialized()) { - // We have seen DONE upstream, and we have discarded our local reference - // to the last input, we will not be able to produce results anymore. - return {_state, NoStats{}}; - } - while (true) { - if (_subqueryInitialized) { - // Continue in subquery - - // Const case - if (_infos.isConst() && !_input.isFirstDataRowInBlock()) { - // Simply write - writeOutput(output); - return {_state, NoStats{}}; - } - - // Non const case, or first run in const - auto res = _subquery.getSome(ExecutionBlock::DefaultBatchSize); - if (res.first == ExecutionState::WAITING) { - TRI_ASSERT(res.second == nullptr); - return {res.first, NoStats{}}; - } - // We get a result - if (res.second != nullptr) { - TRI_IF_FAILURE("SubqueryBlock::executeSubquery") { - THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); - } - - if (_infos.returnsData()) { - TRI_ASSERT(_subqueryResults != nullptr); - _subqueryResults->emplace_back(std::move(res.second)); - } - } - - // Subquery DONE - if (res.first == ExecutionState::DONE) { - writeOutput(output); - return {_state, NoStats{}}; - } - - } else { - // init new subquery - if (!_input) { - std::tie(_state, _input) = _fetcher.fetchRow(); - if (_state == ExecutionState::WAITING) { - TRI_ASSERT(!_input); - return {_state, NoStats{}}; - } - if (!_input) { - TRI_ASSERT(_state == ExecutionState::DONE); - - // We are done! - return {_state, NoStats{}}; - } - } - - TRI_ASSERT(_input); - if (!_infos.isConst() || _input.isFirstDataRowInBlock()) { - auto initRes = _subquery.initializeCursor(_input); - if (initRes.first == ExecutionState::WAITING) { - return {ExecutionState::WAITING, NoStats{}}; - } - if (initRes.second.fail()) { - // Error during initialize cursor - THROW_ARANGO_EXCEPTION(initRes.second); - } - _subqueryResults = std::make_unique>(); - } - // on const subquery we can retoggle init as soon as we have new input. - _subqueryInitialized = true; - } - } -#endif TRI_ASSERT(false); THROW_ARANGO_EXCEPTION(TRI_ERROR_NOT_IMPLEMENTED); } @@ -175,7 +101,7 @@ auto SubqueryExecutor::produceRows(AqlItemBlockInputRang // to the last input, we will not be able to produce results anymore. return {translatedReturnType(), NoStats{}, getUpstreamCall()}; } - while (true) { + while (!output.isFull()) { if (_subqueryInitialized) { // Continue in subquery @@ -248,6 +174,8 @@ auto SubqueryExecutor::produceRows(AqlItemBlockInputRang _subqueryInitialized = true; } } + // Output full nothing to do. + return {ExecutionState::DONE, NoStats{}, AqlCall{}}; } template From 7b94bb7bba055fdacac9d3070b0c1710fa95956d Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 11 Mar 2020 22:41:06 +0100 Subject: [PATCH 63/71] Fixed bypassing of skip in SideEffect executors if they trigger waiting at some point --- arangod/Aql/ExecutionBlockImpl.cpp | 23 +++++++++++++++++++---- arangod/Aql/SkipResult.cpp | 6 ++++++ arangod/Aql/SkipResult.h | 2 ++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index 2b49bfce826e..e979ac6f97bb 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -1521,6 +1521,7 @@ auto ExecutionBlockImpl::sideEffectShadowRowForwarding(AqlCallStack& s // We need to reset the Executor resetExecutor(); } + if (depthSkippingNow > shadowDepth) { // We are skipping the outermost Subquery. // Simply drop this ShadowRow @@ -1781,14 +1782,29 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { TRI_ASSERT(_skipped.nothingSkipped() || _execState == ExecState::UPSTREAM || (std::is_same_v>)); + if constexpr (executorHasSideEffects) { + if (!_skipped.nothingSkipped()) { + // We get woken up on upstream, but we have not reported our + // local skip value to downstream + // In the sideEffect executor we need to apply the skip values on the + // incomming stack, which has not been modified yet. + // NOTE: We only apply the skipping on subquery level. + TRI_ASSERT(_skipped.subqueryDepth() == stack.subqueryLevel() + 1); + for (size_t i = 0; i < stack.subqueryLevel(); ++i) { + auto skippedSub = _skipped.getSkipOnSubqueryLevel(i); + if (skippedSub > 0) { + auto& call = stack.modifyCallAtDepth(i); + call.didSkip(skippedSub); + } + } + } + } + if constexpr (std::is_same_v) { // In subqeryEndExecutor we actually manage two calls. // The clientClient is defined of what will go into the Executor. // on SubqueryEnd this call is generated based on the call from downstream stack.pushCall(std::move(clientCall)); - // TODO: Implement different kind of calls we need to inject into Executor - // based on modification, or on forwarding. - // FOr now use a fetchUnlimited Call always clientCall = AqlCall{}; } if (_execState == ExecState::UPSTREAM) { @@ -2028,7 +2044,6 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { #endif std::tie(_upstreamState, skippedLocal, _lastRange) = executeFetcher(stack, _upstreamRequest); - #ifdef ARANGODB_ENABLE_MAINTAINER_MODE TRI_ASSERT(subqueryLevelBefore == stack.subqueryLevel()); #endif diff --git a/arangod/Aql/SkipResult.cpp b/arangod/Aql/SkipResult.cpp index 7cbfc0545fc6..094c13bb99fa 100644 --- a/arangod/Aql/SkipResult.cpp +++ b/arangod/Aql/SkipResult.cpp @@ -53,6 +53,12 @@ auto SkipResult::didSkipSubquery(size_t skipped, size_t depth) -> void { localSkip += skipped; } +auto SkipResult::getSkipOnSubqueryLevel(size_t depth) -> size_t { + TRI_ASSERT(!_skipped.empty()); + TRI_ASSERT(_skipped.size() > depth); + return _skipped.at(depth); +} + auto SkipResult::nothingSkipped() const noexcept -> bool { TRI_ASSERT(!_skipped.empty()); return std::all_of(_skipped.begin(), _skipped.end(), diff --git a/arangod/Aql/SkipResult.h b/arangod/Aql/SkipResult.h index 84db8ebdba8d..6850f26acda7 100644 --- a/arangod/Aql/SkipResult.h +++ b/arangod/Aql/SkipResult.h @@ -56,6 +56,8 @@ class SkipResult { auto didSkipSubquery(size_t skipped, size_t depth) -> void; + auto getSkipOnSubqueryLevel(size_t depth) -> size_t; + auto nothingSkipped() const noexcept -> bool; auto toVelocyPack(arangodb::velocypack::Builder& builder) const noexcept -> void; From 5165f5515c673c303361e8350605afbde3d109df Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Wed, 11 Mar 2020 23:38:22 +0100 Subject: [PATCH 64/71] Refactored old SubqueryExecutor, there has been some issue with WAITING within the cluster. --- arangod/Aql/ExecutionBlockImpl.cpp | 7 ++ arangod/Aql/SubqueryExecutor.cpp | 166 ++++++++++++++--------------- arangod/Aql/SubqueryExecutor.h | 15 +++ 3 files changed, 105 insertions(+), 83 deletions(-) diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index e979ac6f97bb..f8bcc6ef57a0 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -1772,6 +1772,13 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { TRI_ASSERT(_execState == ExecState::CHECKCALL || _execState == ExecState::SHADOWROWS || _execState == ExecState::UPSTREAM || _execState == ExecState::PRODUCE || _execState == ExecState::SKIP); + + // As it can return WAITING in produce call, there could be some rows written into the output. + // So let us update the call with the last state we have. + if (_execState == ExecState::PRODUCE && _outputItemRow != nullptr && + _outputItemRow->isInitialized()) { + clientCall = _outputItemRow->getClientCall(); + } } else { // We can only have returned the following internal states TRI_ASSERT(_execState == ExecState::CHECKCALL || _execState == ExecState::SHADOWROWS || diff --git a/arangod/Aql/SubqueryExecutor.cpp b/arangod/Aql/SubqueryExecutor.cpp index 2319291c4bd2..8457b36472dc 100644 --- a/arangod/Aql/SubqueryExecutor.cpp +++ b/arangod/Aql/SubqueryExecutor.cpp @@ -81,10 +81,47 @@ std::pair SubqueryExecutor::pro THROW_ARANGO_EXCEPTION(TRI_ERROR_NOT_IMPLEMENTED); } +template +auto SubqueryExecutor::initializeSubquery(AqlItemBlockInputRange& input) + -> std::tuple { + // init new subquery + if (!_input) { + std::tie(_state, _input) = input.nextDataRow(); + LOG_DEVEL_SQ << uint64_t(this) << " nextDataRow: " << _state << " " + << _input.isInitialized(); + if (!_input) { + LOG_DEVEL_SQ << uint64_t(this) << "exit, no more input" << _state; + return {translatedReturnType(), false}; + } + } + + TRI_ASSERT(_input); + if (!_infos.isConst() || _input.isFirstDataRowInBlock()) { + LOG_DEVEL_SQ << "Subquery: Initialize cursor"; + auto [state, result] = _subquery.initializeCursor(_input); + if (state == ExecutionState::WAITING) { + LOG_DEVEL_SQ << "Waiting on initialize cursor"; + return {state, false}; + } + + if (result.fail()) { + // Error during initialize cursor + THROW_ARANGO_EXCEPTION(result); + } + _subqueryResults = std::make_unique>(); + } + // on const subquery we can retoggle init as soon as we have new input. + _subqueryInitialized = true; + return {translatedReturnType(), true}; +} + template auto SubqueryExecutor::produceRows(AqlItemBlockInputRange& input, OutputAqlItemRow& output) -> std::tuple { + // We need to return skip in skipRows before + TRI_ASSERT(_skipped == 0); + auto getUpstreamCall = [&]() { AqlCall upstreamCall = output.getClientCall(); if constexpr (isModificationSubquery) { @@ -111,7 +148,7 @@ auto SubqueryExecutor::produceRows(AqlItemBlockInputRang writeOutput(output); LOG_DEVEL_SQ << uint64_t(this) << "wrote output is const " << _state << " " << getUpstreamCall(); - return {translatedReturnType(), NoStats{}, getUpstreamCall()}; + continue; } // Non const case, or first run in const @@ -140,42 +177,22 @@ auto SubqueryExecutor::produceRows(AqlItemBlockInputRang writeOutput(output); LOG_DEVEL_SQ << uint64_t(this) << "wrote output subquery done " << _state << " " << getUpstreamCall(); - return {translatedReturnType(), NoStats{}, getUpstreamCall()}; } - } else { - // init new subquery - if (!_input) { - std::tie(_state, _input) = input.nextDataRow(); - LOG_DEVEL_SQ << uint64_t(this) << " nextDataRow: " << _state << " " - << _input.isInitialized(); - if (!_input) { - LOG_DEVEL_SQ << uint64_t(this) << "exit produce, no more input" << _state; - return {translatedReturnType(), NoStats{}, getUpstreamCall()}; - } + auto const [state, initialized] = initializeSubquery(input); + if (state == ExecutionState::WAITING) { + LOG_DEVEL_SQ << "Waiting on initialize cursor"; + return {state, NoStats{}, AqlCall{}}; } - - TRI_ASSERT(_input); - if (!_infos.isConst() || _input.isFirstDataRowInBlock()) { - LOG_DEVEL_SQ << "Subquery: Initialize cursor"; - auto [state, result] = _subquery.initializeCursor(_input); - if (state == ExecutionState::WAITING) { - LOG_DEVEL_SQ << "Waiting on initialize cursor"; - return {state, NoStats{}, AqlCall{}}; - } - - if (result.fail()) { - // Error during initialize cursor - THROW_ARANGO_EXCEPTION(result); - } - _subqueryResults = std::make_unique>(); + if (!initialized) { + TRI_ASSERT(!_input); + return {state, NoStats{}, getUpstreamCall()}; } - // on const subquery we can retoggle init as soon as we have new input. - _subqueryInitialized = true; + TRI_ASSERT(_subqueryInitialized); } } - // Output full nothing to do. - return {ExecutionState::DONE, NoStats{}, AqlCall{}}; + + return {translatedReturnType(), NoStats{}, getUpstreamCall()}; } template @@ -184,6 +201,7 @@ void SubqueryExecutor::writeOutput(OutputAqlItemRow& out TRI_IF_FAILURE("SubqueryBlock::getSome") { THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); } + TRI_ASSERT(!output.isFull()); if (!_infos.isConst() || _input.isFirstDataRowInBlock()) { // In the non const case we need to move the data into the output for every // row. @@ -254,8 +272,6 @@ auto SubqueryExecutor::skipRowsRange<>(AqlItemBlockInputRange& inputRange, return upstreamCall; }; - size_t skipped = 0; - LOG_DEVEL_SQ << uint64_t(this) << "skipRowsRange " << call; if (_state == ExecutorState::DONE && !_input.isInitialized()) { @@ -263,78 +279,62 @@ auto SubqueryExecutor::skipRowsRange<>(AqlItemBlockInputRange& inputRange, // to the last input, we will not be able to produce results anymore. return {translatedReturnType(), NoStats{}, 0, getUpstreamCall()}; } - while (true) { + TRI_ASSERT(call.needSkipMore()); + // We cannot have a modifying subquery considered const + TRI_ASSERT(!_infos.isConst()); + bool isFullCount = call.getLimit() == 0 && call.getOffset() == 0; + while (isFullCount || _skipped < call.getOffset()) { if (_subqueryInitialized) { // Continue in subquery - // Const case - if (_infos.isConst() && !_input.isFirstDataRowInBlock()) { - // Simply write - _subqueryInitialized = false; - _input = InputAqlItemRow(CreateInvalidInputRowHint{}); - skipped += 1; - call.didSkip(1); - LOG_DEVEL_SQ << uint64_t(this) << "did skip one"; - return {translatedReturnType(), NoStats{}, skipped, getUpstreamCall()}; - } - - // Non const case, or first run in const - auto [state, skipRes, block] = _subquery.execute(AqlCallStack(AqlCall{})); + // While skipping we do not care for the result. + // Simply jump over it. + AqlCall subqueryCall{}; + subqueryCall.hardLimit = 0; + auto [state, skipRes, block] = _subquery.execute(AqlCallStack(subqueryCall)); TRI_ASSERT(skipRes.nothingSkipped()); if (state == ExecutionState::WAITING) { return {state, NoStats{}, 0, getUpstreamCall()}; } - - // We get a result - if (block != nullptr) { - TRI_IF_FAILURE("SubqueryBlock::executeSubquery") { - THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); - } - - if (_infos.returnsData()) { - TRI_ASSERT(_subqueryResults != nullptr); - _subqueryResults->emplace_back(std::move(block)); - } + // We get a result, but we asked for no rows. + // so please give us no rows. + TRI_ASSERT(block == nullptr); + TRI_IF_FAILURE("SubqueryBlock::executeSubquery") { + THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG); } // Subquery DONE if (state == ExecutionState::DONE) { _subqueryInitialized = false; _input = InputAqlItemRow(CreateInvalidInputRowHint{}); - skipped += 1; - call.didSkip(1); + _skipped += 1; LOG_DEVEL_SQ << uint64_t(this) << "did skip one"; - return {translatedReturnType(), NoStats{}, skipped, getUpstreamCall()}; } } else { - // init new subquery - if (!_input) { - std::tie(_state, _input) = inputRange.nextDataRow(); - - if (!_input) { - LOG_DEVEL_SQ << uint64_t(this) << "skipped nothing waiting for input " << _state; - return {translatedReturnType(), NoStats{}, skipped, getUpstreamCall()}; - } + auto const [state, initialized] = initializeSubquery(inputRange); + if (state == ExecutionState::WAITING) { + LOG_DEVEL_SQ << "Waiting on initialize cursor"; + return {state, NoStats{}, 0, AqlCall{}}; } - - TRI_ASSERT(_input); - if (!_infos.isConst() || _input.isFirstDataRowInBlock()) { - auto [state, result] = _subquery.initializeCursor(_input); - if (state == ExecutionState::WAITING) { - return {state, NoStats{}, 0, getUpstreamCall()}; + if (!initialized) { + TRI_ASSERT(!_input); + if (state == ExecutionState::DONE) { + // We are done, we will not get any more input. + break; } - - if (result.fail()) { - // Error during initialize cursor - THROW_ARANGO_EXCEPTION(result); - } - _subqueryResults = std::make_unique>(); + return {state, NoStats{}, 0, getUpstreamCall()}; } - // on const subquery we can retoggle init as soon as we have new input. - _subqueryInitialized = true; + TRI_ASSERT(_subqueryInitialized); } } + // If we get here, we are done with one set of skipping. + // We either skipped the offset + // or the fullCount + // or both if limit == 0. + call.didSkip(_skipped); + _skipped = 0; + return {translatedReturnType(), NoStats{}, call.getSkipCount(), getUpstreamCall()}; } template class ::arangodb::aql::SubqueryExecutor; diff --git a/arangod/Aql/SubqueryExecutor.h b/arangod/Aql/SubqueryExecutor.h index b9e5c299827b..b28823180849 100644 --- a/arangod/Aql/SubqueryExecutor.h +++ b/arangod/Aql/SubqueryExecutor.h @@ -120,6 +120,19 @@ class SubqueryExecutor { */ auto translatedReturnType() const noexcept -> ExecutionState; + /** + * @brief Initiliaze the subquery with next input row + * Throws if there was an error during initialize cursor + * + * + * @param input Container for more data + * @return std::tuple Result state (WAITING or + * translatedReturnType()) + * bool flag if we have initialized the query, if not, we require more data. + */ + auto initializeSubquery(AqlItemBlockInputRange& input) + -> std::tuple; + private: Fetcher& _fetcher; SubqueryExecutorInfos& _infos; @@ -144,6 +157,8 @@ class SubqueryExecutor { // Cache for the input row we are currently working on InputAqlItemRow _input; + + size_t _skipped = 0; }; } // namespace aql } // namespace arangodb From 9db1c3829852ea2bab725f5662c84c7f2917268d Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 12 Mar 2020 01:00:44 +0100 Subject: [PATCH 65/71] Removed debug logging in test --- tests/Aql/SpliceSubqueryOptimizerRuleTest.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Aql/SpliceSubqueryOptimizerRuleTest.cpp b/tests/Aql/SpliceSubqueryOptimizerRuleTest.cpp index 30d1b648c61e..19b3d524716f 100644 --- a/tests/Aql/SpliceSubqueryOptimizerRuleTest.cpp +++ b/tests/Aql/SpliceSubqueryOptimizerRuleTest.cpp @@ -310,7 +310,7 @@ TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_subquery_with_sort) { verifySubquerySplicing(query, 1); auto expected = arangodb::velocypack::Parser::fromJson(R"([[6,5,4,3,2,1], [12,10,8,6,4,2]])"); - verifyQueryResult(query, expected->slice(), R"({})", R"({"profile": 3})"); + verifyQueryResult(query, expected->slice()); } TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_subquery_with_skip__inner_limit_offset) { @@ -488,7 +488,6 @@ TEST_F(SpliceSubqueryNodeOptimizerRuleTest, splice_nested_empty_subqueries) { RETURN [results] )aql"; auto const expectedString = R"res([[[]]])res"; - verifySubquerySplicing(queryString, 2); auto expected = arangodb::velocypack::Parser::fromJson(expectedString); verifyQueryResult(queryString, expected->slice()); From 4ff1dd8f0ceb66c369d54a32fa83971991e8b84b Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 12 Mar 2020 01:01:11 +0100 Subject: [PATCH 66/71] Fixed empty subquery executor --- arangod/Aql/SubqueryExecutor.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/arangod/Aql/SubqueryExecutor.cpp b/arangod/Aql/SubqueryExecutor.cpp index 8457b36472dc..76be9ef6c6cc 100644 --- a/arangod/Aql/SubqueryExecutor.cpp +++ b/arangod/Aql/SubqueryExecutor.cpp @@ -138,6 +138,11 @@ auto SubqueryExecutor::produceRows(AqlItemBlockInputRang // to the last input, we will not be able to produce results anymore. return {translatedReturnType(), NoStats{}, getUpstreamCall()}; } + if (output.isFull()) { + // This can happen if there is no upstream + _state = input.upstreamState(); + } + while (!output.isFull()) { if (_subqueryInitialized) { // Continue in subquery From 7c79a700275223badda5fffd7b90ce93b72979c4 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 12 Mar 2020 11:01:28 +0100 Subject: [PATCH 67/71] Added an assertion in the AqlResult that no invalid block tries to be serialized --- arangod/Aql/AqlExecuteResult.cpp | 27 +++++++++++++++++++++------ arangod/Aql/AqlExecuteResult.h | 3 +-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/arangod/Aql/AqlExecuteResult.cpp b/arangod/Aql/AqlExecuteResult.cpp index 26e92a3999f9..be7d49d1cb98 100644 --- a/arangod/Aql/AqlExecuteResult.cpp +++ b/arangod/Aql/AqlExecuteResult.cpp @@ -44,6 +44,20 @@ auto getStringView(velocypack::Slice slice) -> std::string_view { } } // namespace +AqlExecuteResult::AqlExecuteResult(ExecutionState state, SkipResult skipped, + SharedAqlItemBlockPtr&& block) + : _state(state), _skipped(skipped), _block(std::move(block)) { + // Make sure we only produce a valid response + // The block should have checked as well. + // We must return skipped and/or data when reporting HASMORE + + // noskip && no data => state != HASMORE + // <=> skipped || data || state != HASMORE + TRI_ASSERT(!_skipped.nothingSkipped() || + (_block != nullptr && _block->numEntries() > 0) || + _state != ExecutionState::HASMORE); +} + auto AqlExecuteResult::state() const noexcept -> ExecutionState { return _state; } @@ -148,9 +162,9 @@ auto AqlExecuteResult::fromVelocyPack(velocypack::Slice const slice, if (auto propIt = expectedPropertiesFound.find(key); ADB_LIKELY(propIt != expectedPropertiesFound.end())) { if (ADB_UNLIKELY(propIt->second)) { - return Result( - TRI_ERROR_TYPE_ERROR, - "When deserializating AqlExecuteResult: Encountered duplicate key"); + return Result(TRI_ERROR_TYPE_ERROR, + "When deserializating AqlExecuteResult: " + "Encountered duplicate key"); } propIt->second = true; } @@ -175,11 +189,12 @@ auto AqlExecuteResult::fromVelocyPack(velocypack::Slice const slice, block = maybeBlock.get(); } else { LOG_TOPIC("cc6f4", WARN, Logger::AQL) - << "When deserializating AqlExecuteResult: Encountered unexpected " + << "When deserializating AqlExecuteResult: Encountered " + "unexpected " "key " << keySlice.toJson(); - // If you run into this assertion during rolling upgrades after adding a - // new attribute, remove it in the older version. + // If you run into this assertion during rolling upgrades after + // adding a new attribute, remove it in the older version. TRI_ASSERT(false); } } diff --git a/arangod/Aql/AqlExecuteResult.h b/arangod/Aql/AqlExecuteResult.h index 11ef415f32cf..409e598f9835 100644 --- a/arangod/Aql/AqlExecuteResult.h +++ b/arangod/Aql/AqlExecuteResult.h @@ -43,8 +43,7 @@ namespace arangodb::aql { class AqlExecuteResult { public: - AqlExecuteResult(ExecutionState state, SkipResult skipped, SharedAqlItemBlockPtr&& block) - : _state(state), _skipped(skipped), _block(std::move(block)) {} + AqlExecuteResult(ExecutionState state, SkipResult skipped, SharedAqlItemBlockPtr&& block); void toVelocyPack(velocypack::Builder&, velocypack::Options const*); static auto fromVelocyPack(velocypack::Slice, AqlItemBlockManager&) From ae61cee99e8ce88f2a9683815c460533fe4c2d73 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 12 Mar 2020 11:03:04 +0100 Subject: [PATCH 68/71] Added clientId into profile tracing. Fixed return of invalid blocks in blocks with client --- arangod/Aql/BlocksWithClients.cpp | 40 +++++++++++++++++++----------- arangod/Aql/ExecutionBlock.cpp | 12 ++++++--- arangod/Aql/ExecutionBlock.h | 6 +++-- arangod/Aql/ExecutionBlockImpl.cpp | 20 +++++++++------ 4 files changed, 49 insertions(+), 29 deletions(-) diff --git a/arangod/Aql/BlocksWithClients.cpp b/arangod/Aql/BlocksWithClients.cpp index 0e7579a8bc96..89ebd3ca26cb 100644 --- a/arangod/Aql/BlocksWithClients.cpp +++ b/arangod/Aql/BlocksWithClients.cpp @@ -200,9 +200,9 @@ template auto BlocksWithClientsImpl::executeForClient(AqlCallStack stack, std::string const& clientId) -> std::tuple { - // traceExecuteBegin(stack); + traceExecuteBegin(stack, clientId); auto res = executeWithoutTraceForClient(stack, clientId); - // traceExecuteEnd(res); + traceExecuteEnd(res, clientId); return res; } @@ -232,22 +232,32 @@ auto BlocksWithClientsImpl::executeWithoutTraceForClient(AqlCallStack // We do not have anymore data locally. // Need to fetch more from upstream auto& dataContainer = it->second; - - while (!dataContainer.hasDataFor(call)) { - if (_upstreamState == ExecutionState::DONE) { - // We are done, with everything, we will not be able to fetch any more rows - return {_upstreamState, SkipResult{}, nullptr}; + while (true) { + while (!dataContainer.hasDataFor(call)) { + if (_upstreamState == ExecutionState::DONE) { + // We are done, with everything, we will not be able to fetch any more rows + return {_upstreamState, SkipResult{}, nullptr}; + } + + auto state = fetchMore(stack); + if (state == ExecutionState::WAITING) { + return {state, SkipResult{}, nullptr}; + } + _upstreamState = state; } - - auto state = fetchMore(stack); - if (state == ExecutionState::WAITING) { - return {state, SkipResult{}, nullptr}; + { + // If we get here we have data and can return it. + // However the call might force us to drop everything (e.g. hardLimit == + // 0) So we need to refetch data eventually. + stack.pushCall(call); + auto [state, skipped, result] = dataContainer.execute(stack, _upstreamState); + if (state == ExecutionState::DONE || !skipped.nothingSkipped() || result != nullptr) { + // We have a valid result. + return {state, skipped, result}; + } + stack.popCall(); } - _upstreamState = state; } - // If we get here we have data and can return it. - stack.pushCall(call); - return dataContainer.execute(stack, _upstreamState); } template diff --git a/arangod/Aql/ExecutionBlock.cpp b/arangod/Aql/ExecutionBlock.cpp index 56bf44defef0..37538244f10f 100644 --- a/arangod/Aql/ExecutionBlock.cpp +++ b/arangod/Aql/ExecutionBlock.cpp @@ -298,7 +298,7 @@ bool ExecutionBlock::isInSplicedSubquery() const noexcept { return _isInSplicedSubquery; } -void ExecutionBlock::traceExecuteBegin(AqlCallStack const& stack) { +void ExecutionBlock::traceExecuteBegin(AqlCallStack const& stack, std::string const& clientId) { if (_profile >= PROFILE_LEVEL_BLOCKS) { if (_getSomeBegin <= 0.0) { _getSomeBegin = TRI_microtime(); @@ -311,12 +311,14 @@ void ExecutionBlock::traceExecuteBegin(AqlCallStack const& stack) { LOG_TOPIC("1e717", INFO, Logger::QUERIES) << "[query#" << queryId << "] " << "execute type=" << node->getTypeString() << " call= " << call - << " this=" << (uintptr_t)this << " id=" << node->id(); + << " this=" << (uintptr_t)this << " id=" << node->id() + << (clientId.empty() ? "" : " clientId=" + clientId); } } } -auto ExecutionBlock::traceExecuteEnd(std::tuple const& result) +auto ExecutionBlock::traceExecuteEnd(std::tuple const& result, + std::string const& clientId) -> std::tuple { if (_profile >= PROFILE_LEVEL_BLOCKS) { auto const& [state, skipped, block] = result; @@ -341,7 +343,9 @@ auto ExecutionBlock::traceExecuteEnd(std::tuple const& result) + auto traceExecuteEnd(std::tuple const& result, + std::string const& clientId = "") -> std::tuple; [[nodiscard]] auto printBlockInfo() const -> std::string const; diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index f8bcc6ef57a0..dd0a17936c0c 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -1772,19 +1772,23 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { TRI_ASSERT(_execState == ExecState::CHECKCALL || _execState == ExecState::SHADOWROWS || _execState == ExecState::UPSTREAM || _execState == ExecState::PRODUCE || _execState == ExecState::SKIP); - - // As it can return WAITING in produce call, there could be some rows written into the output. - // So let us update the call with the last state we have. - if (_execState == ExecState::PRODUCE && _outputItemRow != nullptr && - _outputItemRow->isInitialized()) { - clientCall = _outputItemRow->getClientCall(); - } } else { // We can only have returned the following internal states TRI_ASSERT(_execState == ExecState::CHECKCALL || _execState == ExecState::SHADOWROWS || _execState == ExecState::UPSTREAM); } + // In some executors we may write something into the output, but then return waiting. + // In this case we are not allowed to lose the call we have been working on, we have + // noted down created or skipped rows in there. + // The client is disallowed to change her mind anyways + // so we simply continue to work on the call we already have + // The guarantee is, if we have returned the block, and modified + // our local call, then the outputItemRow is not initialized + if (_outputItemRow != nullptr && _outputItemRow->isInitialized()) { + clientCall = _outputItemRow->getClientCall(); + } + // Skip can only be > 0 if we are in upstream cases, or if we got injected a block TRI_ASSERT(_skipped.nothingSkipped() || _execState == ExecState::UPSTREAM || (std::is_same_v>)); @@ -2192,7 +2196,7 @@ ExecutionBlockImpl::executeWithoutTrace(AqlCallStack stack) { (outputBlock != nullptr && outputBlock->numEntries() > 0)); return {ExecutionState::HASMORE, skipped, std::move(outputBlock)}; } - // We must return skipped and/or data when reportingHASMORE + // We must return skipped and/or data when reporting HASMORE TRI_ASSERT(_upstreamState != ExecutionState::HASMORE || (!skipped.nothingSkipped() || (outputBlock != nullptr && outputBlock->numEntries() > 0))); From ca243b2c2acd5c52f6c68336b1f65009ddf1d01d Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 12 Mar 2020 12:44:58 +0100 Subject: [PATCH 69/71] Removed invalid AqlExecuteResult from Test --- tests/Aql/RemoteExecutorTest.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Aql/RemoteExecutorTest.cpp b/tests/Aql/RemoteExecutorTest.cpp index 74ce419dc35a..8a82034d4b60 100644 --- a/tests/Aql/RemoteExecutorTest.cpp +++ b/tests/Aql/RemoteExecutorTest.cpp @@ -177,7 +177,6 @@ auto MakeSkipResult(size_t const i) -> SkipResult { auto const testingAqlExecuteResults = ::testing::ValuesIn(std::array{ AqlExecuteResult{ExecutionState::DONE, MakeSkipResult(0), nullptr}, - AqlExecuteResult{ExecutionState::HASMORE, MakeSkipResult(0), nullptr}, AqlExecuteResult{ExecutionState::HASMORE, MakeSkipResult(4), nullptr}, AqlExecuteResult{ExecutionState::DONE, MakeSkipResult(0), buildBlock<1>(manager, {{42}})}, AqlExecuteResult{ExecutionState::HASMORE, MakeSkipResult(3), From 48eec3eeb49dac2a862d8de24733b88d57421ac5 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 12 Mar 2020 14:07:19 +0100 Subject: [PATCH 70/71] Update tests/Aql/SubqueryStartExecutorTest.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Tobias Gödderz --- tests/Aql/SubqueryStartExecutorTest.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Aql/SubqueryStartExecutorTest.cpp b/tests/Aql/SubqueryStartExecutorTest.cpp index b1c14e0e5558..d73fbc9cc5e8 100644 --- a/tests/Aql/SubqueryStartExecutorTest.cpp +++ b/tests/Aql/SubqueryStartExecutorTest.cpp @@ -386,7 +386,7 @@ TEST_P(SubqueryStartExecutorTest, skip_in_outer_subquery) { .setCallStack(queryStack(AqlCall{1, false, AqlCall::Infinity{}}, AqlCall{})) .run(); } else { - // The feature is not available in 3.7 or earlier. + // The feature is not available in 3.6 or earlier. } } @@ -470,4 +470,4 @@ TEST_P(SubqueryStartExecutorTest, fullbypass_in_outer_subquery) { } else { // The feature is not available in 3.7 or earlier. } -} \ No newline at end of file +} From fe82b4bb9d10954e1591c09235842b921d7b1c18 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Thu, 12 Mar 2020 14:26:10 +0100 Subject: [PATCH 71/71] Fixed comment, thanks to reviewer --- arangod/Aql/ExecutionBlockImpl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arangod/Aql/ExecutionBlockImpl.cpp b/arangod/Aql/ExecutionBlockImpl.cpp index dd0a17936c0c..133df32b4677 100644 --- a/arangod/Aql/ExecutionBlockImpl.cpp +++ b/arangod/Aql/ExecutionBlockImpl.cpp @@ -1253,7 +1253,7 @@ auto ExecutionBlockImpl::executeFetcher(AqlCallStack& stack, AqlCallTy // If the executor has side effects, we cannot bypass any subqueries // by skipping them. SO we need to fetch all shadow rows in order to // trigger this Executor with everthing from above. - // NOTE: The Executor needs to discard shadowRows, and do the + // NOTE: The Executor needs to discard shadowRows, and do the accouting. static_assert(std::is_same_v>); auto fetchAllStack = stack.createEquivalentFetchAllShadowRowsStack(); fetchAllStack.pushCall(aqlCall);