diff --git a/src/engine/TransitivePathBase.cpp b/src/engine/TransitivePathBase.cpp index 63899fdb28..a833bfdfbd 100644 --- a/src/engine/TransitivePathBase.cpp +++ b/src/engine/TransitivePathBase.cpp @@ -63,76 +63,80 @@ TransitivePathBase::decideDirection() { } // _____________________________________________________________________________ -void TransitivePathBase::fillTableWithHull(IdTable& table, const Map& hull, - std::vector& nodes, - size_t startSideCol, - size_t targetSideCol, - const IdTable& startSideTable, - size_t skipCol) const { - CALL_FIXED_SIZE((std::array{table.numColumns(), startSideTable.numColumns()}), - &TransitivePathBase::fillTableWithHullImpl, this, table, hull, - nodes, startSideCol, targetSideCol, startSideTable, skipCol); +Result::Generator TransitivePathBase::fillTableWithHull( + NodeGenerator hull, size_t startSideCol, size_t targetSideCol, + size_t skipCol, bool yieldOnce, size_t inputWidth) const { + return ad_utility::callFixedSize( + std::array{inputWidth, getResultWidth()}, + [&]() { + return fillTableWithHullImpl( + std::move(hull), startSideCol, targetSideCol, yieldOnce, skipCol); + }); } // _____________________________________________________________________________ -template -void TransitivePathBase::fillTableWithHullImpl( - IdTable& tableDyn, const Map& hull, std::vector& nodes, - size_t startSideCol, size_t targetSideCol, const IdTable& startSideTable, - size_t skipCol) const { - IdTableStatic table = std::move(tableDyn).toStatic(); - IdTableView startView = - startSideTable.asStaticView(); - - size_t rowIndex = 0; - for (size_t i = 0; i < nodes.size(); i++) { - Id node = nodes[i]; - auto it = hull.find(node); - if (it == hull.end()) { - continue; - } - - for (Id otherNode : it->second) { - table.emplace_back(); - table(rowIndex, startSideCol) = node; - table(rowIndex, targetSideCol) = otherNode; - - copyColumns(startView, table, i, rowIndex, skipCol); - - rowIndex++; - } - } - - tableDyn = std::move(table).toDynamic(); -} - -// _____________________________________________________________________________ -void TransitivePathBase::fillTableWithHull(IdTable& table, const Map& hull, - size_t startSideCol, - size_t targetSideCol) const { - CALL_FIXED_SIZE((std::array{table.numColumns()}), - &TransitivePathBase::fillTableWithHullImpl, this, table, hull, - startSideCol, targetSideCol); +Result::Generator TransitivePathBase::fillTableWithHull(NodeGenerator hull, + size_t startSideCol, + size_t targetSideCol, + bool yieldOnce) const { + return ad_utility::callFixedSize(getResultWidth(), [&]() { + return fillTableWithHullImpl<0, WIDTH>(std::move(hull), startSideCol, + targetSideCol, yieldOnce); + }); } // _____________________________________________________________________________ -template -void TransitivePathBase::fillTableWithHullImpl(IdTable& tableDyn, - const Map& hull, - size_t startSideCol, - size_t targetSideCol) const { - IdTableStatic table = std::move(tableDyn).toStatic(); - size_t rowIndex = 0; - for (auto const& [node, linkedNodes] : hull) { +template +Result::Generator TransitivePathBase::fillTableWithHullImpl( + NodeGenerator hull, size_t startSideCol, size_t targetSideCol, + bool yieldOnce, size_t skipCol) const { + ad_utility::Timer timer{ad_utility::Timer::Stopped}; + size_t outputRow = 0; + IdTableStatic table{getResultWidth(), allocator()}; + std::vector storedLocalVocabs; + for (auto& [node, linkedNodes, localVocab, idTable, inputRow] : hull) { + timer.cont(); + // As an optimization nodes without any linked nodes should not get yielded + // in the first place. + AD_CONTRACT_CHECK(!linkedNodes.empty()); + if (!yieldOnce) { + table.reserve(linkedNodes.size()); + } + std::optional> inputView = std::nullopt; + if (idTable != nullptr) { + inputView = idTable->template asStaticView(); + } for (Id linkedNode : linkedNodes) { table.emplace_back(); - table(rowIndex, startSideCol) = node; - table(rowIndex, targetSideCol) = linkedNode; + table(outputRow, startSideCol) = node; + table(outputRow, targetSideCol) = linkedNode; - rowIndex++; + if (inputView.has_value()) { + copyColumns(inputView.value(), table, + inputRow, outputRow, skipCol); + } + + outputRow++; } + + if (yieldOnce) { + storedLocalVocabs.emplace_back(std::move(localVocab)); + } else { + timer.stop(); + runtimeInfo().addDetail("IdTable fill time", timer.msecs()); + co_yield {std::move(table).toDynamic(), std::move(localVocab)}; + table = IdTableStatic{getResultWidth(), allocator()}; + outputRow = 0; + } + timer.stop(); + } + if (yieldOnce) { + timer.start(); + LocalVocab mergedVocab{}; + mergedVocab.mergeWith(storedLocalVocabs); + runtimeInfo().addDetail("IdTable fill time", timer.msecs()); + co_yield {std::move(table).toDynamic(), std::move(mergedVocab)}; } - tableDyn = std::move(table).toDynamic(); } // _____________________________________________________________________________ @@ -405,7 +409,7 @@ void TransitivePathBase::copyColumns(const IdTableView& inputTable, continue; } - outputTable(outputRow, outCol) = inputTable(inputRow, inCol); + outputTable.at(outputRow, outCol) = inputTable.at(inputRow, inCol); inCol++; outCol++; } diff --git a/src/engine/TransitivePathBase.h b/src/engine/TransitivePathBase.h index ce7c32ac3e..a223e06d95 100644 --- a/src/engine/TransitivePathBase.h +++ b/src/engine/TransitivePathBase.h @@ -69,6 +69,31 @@ using Map = std::unordered_map< Id, Set, HashId, std::equal_to, ad_utility::AllocatorWithLimit>>; +// Helper struct, that allows a generator to yield a a node and all its +// connected nodes (the `targets`), along with a local vocabulary and the row +// index of the node in the input table. The `IdTable` pointer might be null if +// the `Id` is not associated with a table. In this case the `row` value does +// not represent anything meaningful and should not be used. +struct NodeWithTargets { + Id node_; + Set targets_; + LocalVocab localVocab_; + const IdTable* idTable_; + size_t row_; + + // Explicit to prevent issues with co_yield and lifetime. + // See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=103909 for more info. + NodeWithTargets(Id node, Set targets, LocalVocab localVocab, + const IdTable* idTable, size_t row) + : node_{node}, + targets_{std::move(targets)}, + localVocab_{std::move(localVocab)}, + idTable_{idTable}, + row_{row} {} +}; + +using NodeGenerator = cppcoro::generator; + /** * @class TransitivePathBase * @brief A common base class for different implementations of the Transitive @@ -147,37 +172,36 @@ class TransitivePathBase : public Operation { * startSideTable to fill in the rest of the columns. * This function is called if the start side is bound and a variable. * - * @param table The result table which will be filled. - * @param hull The transitive hull. - * @param nodes The start nodes of the transitive hull. These need to be in - * the same order and amount as the starting side nodes in the startTable. + * @param hull The transitive hull, represented by a generator that yields + * sets of connected nodes with some metadata. * @param startSideCol The column of the result table for the startSide of the * hull * @param targetSideCol The column of the result table for the targetSide of * the hull - * @param startSideTable An IdTable that holds other results. The other - * results will be transferred to the new result table. * @param skipCol This column contains the Ids of the start side in the * startSideTable and will be skipped. + * @param yieldOnce If true, the generator will yield only a single time. + * @param inputWidth The width of the input table that is referenced by the + * elements of `hull`. */ - void fillTableWithHull(IdTable& table, const Map& hull, - std::vector& nodes, size_t startSideCol, - size_t targetSideCol, const IdTable& startSideTable, - size_t skipCol) const; + Result::Generator fillTableWithHull(NodeGenerator hull, size_t startSideCol, + size_t targetSideCol, size_t skipCol, + bool yieldOnce, size_t inputWidth) const; /** * @brief Fill the given table with the transitive hull. * This function is called if the sides are unbound or ids. * - * @param table The result table which will be filled. * @param hull The transitive hull. * @param startSideCol The column of the result table for the startSide of the * hull * @param targetSideCol The column of the result table for the targetSide of * the hull + * @param yieldOnce If true, the generator will yield only a single time. */ - void fillTableWithHull(IdTable& table, const Map& hull, size_t startSideCol, - size_t targetSideCol) const; + Result::Generator fillTableWithHull(NodeGenerator hull, size_t startSideCol, + size_t targetSideCol, + bool yieldOnce) const; // Copy the columns from the input table to the output table template @@ -204,16 +228,11 @@ class TransitivePathBase : public Operation { private: uint64_t getSizeEstimateBeforeLimit() override; - template - void fillTableWithHullImpl(IdTable& table, const Map& hull, - std::vector& nodes, size_t startSideCol, - size_t targetSideCol, - const IdTable& startSideTable, - size_t skipCol) const; - - template - void fillTableWithHullImpl(IdTable& table, const Map& hull, - size_t startSideCol, size_t targetSideCol) const; + template + Result::Generator fillTableWithHullImpl(NodeGenerator hull, + size_t startSideCol, + size_t targetSideCol, bool yieldOnce, + size_t skipCol = 0) const; public: size_t getCostEstimate() override; diff --git a/src/engine/TransitivePathImpl.h b/src/engine/TransitivePathImpl.h index 55ce45ba4d..407b63a298 100644 --- a/src/engine/TransitivePathImpl.h +++ b/src/engine/TransitivePathImpl.h @@ -11,6 +11,25 @@ #include "util/Exception.h" #include "util/Timer.h" +namespace detail { + +// Helper struct that allows to group a read-only view of a column of a table +// with a reference to the table itself and a local vocabulary (used to ensure +// the correct lifetime). +template +struct TableColumnWithVocab { + const IdTable* table_; + ColumnType column_; + LocalVocab vocab_; + + // Explicit to prevent issues with co_yield and lifetime. + // See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=103909 for more info. + TableColumnWithVocab(const IdTable* table, ColumnType column, + LocalVocab vocab) + : table_{table}, column_{std::move(column)}, vocab_{std::move(vocab)} {}; +}; +}; // namespace detail + /** * @class TransitivePathImpl * @brief This class implements common functions for the concrete TransitivePath @@ -22,6 +41,9 @@ */ template class TransitivePathImpl : public TransitivePathBase { + using TableColumnWithVocab = + detail::TableColumnWithVocab>; + public: TransitivePathImpl(QueryExecutionContext* qec, std::shared_ptr child, @@ -36,100 +58,88 @@ class TransitivePathImpl : public TransitivePathBase { * it is a variable. The other IdTable contains the result * of the start side and will be used to get the start nodes. * - * @tparam RES_WIDTH Number of columns of the result table - * @tparam SUB_WIDTH Number of columns of the sub table - * @tparam SIDE_WIDTH Number of columns of the - * @param res The result table which will be filled in-place - * @param sub The IdTable for the sub result + * @param sub A shared pointer to the sub result. Needs to be kept alive for + * the lifetime of this generator. * @param startSide The start side for the transitive hull * @param targetSide The target side for the transitive hull - * @param startSideTable The IdTable of the startSide + * @param startSideResult The Result of the startSide + * @param yieldOnce If true, the generator will yield only a single time. */ - template - void computeTransitivePathBound(IdTable* dynRes, const IdTable& dynSub, - const TransitivePathSide& startSide, - const TransitivePathSide& targetSide, - const IdTable& startSideTable) const { - auto timer = ad_utility::Timer(ad_utility::Timer::Stopped); - timer.start(); - - auto [edges, nodes] = setupMapAndNodes( - dynSub, startSide, targetSide, startSideTable); - - timer.stop(); - auto initTime = timer.msecs(); - timer.start(); - - Map hull(allocator()); - if (!targetSide.isVariable()) { - hull = transitiveHull(edges, nodes, std::get(targetSide.value_)); - } else { - hull = transitiveHull(edges, nodes, std::nullopt); + Result::Generator computeTransitivePathBound( + std::shared_ptr sub, const TransitivePathSide& startSide, + const TransitivePathSide& targetSide, + std::shared_ptr startSideResult, bool yieldOnce) const { + ad_utility::Timer timer{ad_utility::Timer::Started}; + + auto edges = setupEdgesMap(sub->idTable(), startSide, targetSide); + auto nodes = setupNodes(startSide, std::move(startSideResult)); + // Setup nodes returns a generator, so this time measurement won't include + // the time for each iteration, but every iteration step should have + // constant overhead, which should be safe to ignore. + runtimeInfo().addDetail("Initialization time", timer.msecs().count()); + + NodeGenerator hull = + transitiveHull(edges, sub->getCopyOfLocalVocab(), std::move(nodes), + targetSide.isVariable() + ? std::nullopt + : std::optional{std::get(targetSide.value_)}); + + auto result = fillTableWithHull( + std::move(hull), startSide.outputCol_, targetSide.outputCol_, + startSide.treeAndCol_.value().second, yieldOnce, + startSide.treeAndCol_.value().first->getResultWidth()); + + // Iterate over generator to prevent lifetime issues + for (auto& pair : result) { + co_yield pair; } - - timer.stop(); - auto hullTime = timer.msecs(); - timer.start(); - - fillTableWithHull(*dynRes, hull, nodes, startSide.outputCol_, - targetSide.outputCol_, startSideTable, - startSide.treeAndCol_.value().second); - - timer.stop(); - auto fillTime = timer.msecs(); - - auto& info = runtimeInfo(); - info.addDetail("Initialization time", initTime.count()); - info.addDetail("Hull time", hullTime.count()); - info.addDetail("IdTable fill time", fillTime.count()); }; /** * @brief Compute the transitive hull. * This function is called when no side is bound (or an id). * - * @tparam RES_WIDTH Number of columns of the result table - * @tparam SUB_WIDTH Number of columns of the sub table - * @param res The result table which will be filled in-place - * @param sub The IdTable for the sub result + * @param sub A shared pointer to the sub result. Needs to be kept alive for + * the lifetime of this generator. * @param startSide The start side for the transitive hull * @param targetSide The target side for the transitive hull + * @param yieldOnce If true, the generator will yield only a single time. */ - template - void computeTransitivePath(IdTable* dynRes, const IdTable& dynSub, - const TransitivePathSide& startSide, - const TransitivePathSide& targetSide) const { - auto timer = ad_utility::Timer(ad_utility::Timer::Stopped); - timer.start(); + Result::Generator computeTransitivePath(std::shared_ptr sub, + const TransitivePathSide& startSide, + const TransitivePathSide& targetSide, + bool yieldOnce) const { + ad_utility::Timer timer{ad_utility::Timer::Started}; + + auto edges = setupEdgesMap(sub->idTable(), startSide, targetSide); + auto nodesWithDuplicates = + setupNodes(sub->idTable(), startSide, targetSide); + Set nodesWithoutDuplicates{allocator()}; + for (const auto& span : nodesWithDuplicates) { + nodesWithoutDuplicates.insert(span.begin(), span.end()); + } - auto [edges, nodes] = - setupMapAndNodes(dynSub, startSide, targetSide); + runtimeInfo().addDetail("Initialization time", timer.msecs()); - timer.stop(); - auto initTime = timer.msecs(); - timer.start(); + // Technically we should pass the localVocab of `sub` here, but this will + // just lead to a merge with itself later on in the pipeline. + detail::TableColumnWithVocab tableInfo{ + nullptr, nodesWithoutDuplicates, LocalVocab{}}; - Map hull{allocator()}; - if (!targetSide.isVariable()) { - hull = transitiveHull(edges, nodes, std::get(targetSide.value_)); - } else { - hull = transitiveHull(edges, nodes, std::nullopt); - } - - timer.stop(); - auto hullTime = timer.msecs(); - timer.start(); + NodeGenerator hull = transitiveHull( + edges, sub->getCopyOfLocalVocab(), std::span{&tableInfo, 1}, + targetSide.isVariable() + ? std::nullopt + : std::optional{std::get(targetSide.value_)}); - fillTableWithHull(*dynRes, hull, startSide.outputCol_, - targetSide.outputCol_); - timer.stop(); - auto fillTime = timer.msecs(); + auto result = fillTableWithHull(std::move(hull), startSide.outputCol_, + targetSide.outputCol_, yieldOnce); - auto& info = runtimeInfo(); - info.addDetail("Initialization time", initTime.count()); - info.addDetail("Hull time", hullTime.count()); - info.addDetail("IdTable fill time", fillTime.count()); + // Iterate over generator to prevent lifetime issues + for (auto& pair : result) { + co_yield pair; + } }; protected: @@ -142,7 +152,7 @@ class TransitivePathImpl : public TransitivePathBase { * * @return Result The result of the TransitivePath operation */ - ProtoResult computeResult([[maybe_unused]] bool requestLaziness) override { + ProtoResult computeResult(bool requestLaziness) override { if (minDist_ == 0 && !isBoundOrId() && lhs_.isVariable() && rhs_.isVariable()) { AD_THROW( @@ -151,161 +161,170 @@ class TransitivePathImpl : public TransitivePathBase { "not supported"); } auto [startSide, targetSide] = decideDirection(); - std::shared_ptr subRes = subtree_->getResult(); - - IdTable idTable{allocator()}; - - idTable.setNumColumns(getResultWidth()); - - size_t subWidth = subRes->idTable().numColumns(); + // In order to traverse the graph represented by this result, we need random + // access across the whole table, so it doesn't make sense to lazily compute + // the result. + std::shared_ptr subRes = subtree_->getResult(false); if (startSide.isBoundVariable()) { std::shared_ptr sideRes = - startSide.treeAndCol_.value().first->getResult(); - size_t sideWidth = sideRes->idTable().numColumns(); + startSide.treeAndCol_.value().first->getResult(true); - CALL_FIXED_SIZE((std::array{resultWidth_, subWidth, sideWidth}), - &TransitivePathImpl::computeTransitivePathBound, this, - &idTable, subRes->idTable(), startSide, targetSide, - sideRes->idTable()); + auto gen = + computeTransitivePathBound(std::move(subRes), startSide, targetSide, + std::move(sideRes), !requestLaziness); - return {std::move(idTable), resultSortedOn(), - Result::getMergedLocalVocab(*sideRes, *subRes)}; + return requestLaziness + ? ProtoResult{std::move(gen), resultSortedOn()} + : ProtoResult{cppcoro::getSingleElement(std::move(gen)), + resultSortedOn()}; } - CALL_FIXED_SIZE((std::array{resultWidth_, subWidth}), - &TransitivePathImpl::computeTransitivePath, this, - &idTable, subRes->idTable(), startSide, targetSide); - - // NOTE: The only place, where the input to a transitive path operation is - // not an index scan (which has an empty local vocabulary by default) is the - // `LocalVocabTest`. But it doesn't harm to propagate the local vocab here - // either. - return {std::move(idTable), resultSortedOn(), - subRes->getSharedLocalVocab()}; - }; + auto gen = computeTransitivePath(std::move(subRes), startSide, targetSide, + !requestLaziness); + return requestLaziness + ? ProtoResult{std::move(gen), resultSortedOn()} + : ProtoResult{cppcoro::getSingleElement(std::move(gen)), + resultSortedOn()}; + } /** - * @brief Compute the transitive hull starting at the given nodes, - * using the given Map. - * - * @param edges Adjacency lists, mapping Ids (nodes) to their connected + * @brief Depth-first search to find connected nodes in the graph. + * @param edges The adjacency lists, mapping Ids (nodes) to their connected * Ids. - * @param nodes A list of Ids. These Ids are used as starting points for the - * transitive hull. Thus, this parameter guides the performance of this - * algorithm. - * @param target Optional target Id. If supplied, only paths which end - * in this Id are added to the hull. - * @return Map Maps each Id to its connected Ids in the transitive hull + * @param startNode The node to start the search from. + * @param target Optional target Id. If supplied, only paths which end in this + * Id are added to the result. + * @return A set of connected nodes in the graph. */ - Map transitiveHull(const T& edges, const std::vector& startNodes, - std::optional target) const { - // For every node do a dfs on the graph - Map hull{allocator()}; - + Set findConnectedNodes(const T& edges, Id startNode, + const std::optional& target) const { std::vector> stack; ad_utility::HashSetWithMemoryLimit marks{ getExecutionContext()->getAllocator()}; - for (auto startNode : startNodes) { - if (hull.contains(startNode)) { - // We have already computed the hull for this node - continue; - } + Set connectedNodes{getExecutionContext()->getAllocator()}; + stack.emplace_back(startNode, 0); - marks.clear(); - stack.clear(); - stack.push_back({startNode, 0}); + if (minDist_ == 0 && (!target.has_value() || startNode == target.value())) { + connectedNodes.insert(startNode); + } - if (minDist_ == 0 && - (!target.has_value() || startNode == target.value())) { - insertIntoMap(hull, startNode, startNode); - } + while (!stack.empty()) { + checkCancellation(); + auto [node, steps] = stack.back(); + stack.pop_back(); - while (!stack.empty()) { - checkCancellation(); - auto [node, steps] = stack.back(); - stack.pop_back(); - - if (steps <= maxDist_ && marks.count(node) == 0) { - if (steps >= minDist_) { - marks.insert(node); - if (!target.has_value() || node == target.value()) { - insertIntoMap(hull, startNode, node); - } + if (steps <= maxDist_ && marks.count(node) == 0) { + if (steps >= minDist_) { + marks.insert(node); + if (!target.has_value() || node == target.value()) { + connectedNodes.insert(node); } + } - const auto& successors = edges.successors(node); - for (auto successor : successors) { - stack.push_back({successor, steps + 1}); - } + const auto& successors = edges.successors(node); + for (auto successor : successors) { + stack.emplace_back(successor, steps + 1); + } + } + } + return connectedNodes; + } + + /** + * @brief Compute the transitive hull starting at the given nodes, + * using the given Map. + * + * @param edges Adjacency lists, mapping Ids (nodes) to their connected + * Ids. + * @param startNodes A range that yields an instantiation of + * `TableColumnWithVocab` that can be consumed to create a transitive hull. + * @param target Optional target Id. If supplied, only paths which end + * in this Id are added to the hull. + * @return Map Maps each Id to its connected Ids in the transitive hull + */ + NodeGenerator transitiveHull(const T& edges, LocalVocab edgesVocab, + std::ranges::range auto startNodes, + std::optional target) const { + ad_utility::Timer timer{ad_utility::Timer::Stopped}; + for (auto&& tableColumn : startNodes) { + timer.cont(); + LocalVocab mergedVocab = std::move(tableColumn.vocab_); + mergedVocab.mergeWith(std::span{&edgesVocab, 1}); + size_t currentRow = 0; + for (Id startNode : tableColumn.column_) { + Set connectedNodes = findConnectedNodes(edges, startNode, target); + if (!connectedNodes.empty()) { + runtimeInfo().addDetail("Hull time", timer.msecs()); + timer.stop(); + co_yield NodeWithTargets{startNode, std::move(connectedNodes), + mergedVocab.clone(), tableColumn.table_, + currentRow}; + timer.cont(); } + currentRow++; } + timer.stop(); } - return hull; } /** * @brief Prepare a Map and a nodes vector for the transitive hull * computation. * - * @tparam SUB_WIDTH Number of columns of the sub table * @param sub The sub table result * @param startSide The TransitivePathSide where the edges start * @param targetSide The TransitivePathSide where the edges end - * @return std::pair> A Map and Id vector (nodes) for the - * transitive hull computation + * @return std::vector> An vector of spans of (nodes) for + * the transitive hull computation */ - template - std::pair> setupMapAndNodes( + std::vector> setupNodes( const IdTable& sub, const TransitivePathSide& startSide, const TransitivePathSide& targetSide) const { - std::vector nodes; - auto edges = setupEdgesMap(sub, startSide, targetSide); + std::vector> result; // id -> var|id if (!startSide.isVariable()) { - nodes.push_back(std::get(startSide.value_)); + result.emplace_back(&std::get(startSide.value_), 1); // var -> var } else { std::span startNodes = sub.getColumn(startSide.subCol_); - // TODO Use ranges::to. - nodes.insert(nodes.end(), startNodes.begin(), startNodes.end()); + result.emplace_back(startNodes); if (minDist_ == 0) { std::span targetNodes = sub.getColumn(targetSide.subCol_); - nodes.insert(nodes.end(), targetNodes.begin(), targetNodes.end()); + result.emplace_back(targetNodes); } } - return {std::move(edges), std::move(nodes)}; + return result; }; /** * @brief Prepare a Map and a nodes vector for the transitive hull * computation. * - * @tparam SUB_WIDTH Number of columns of the sub table - * @tparam SIDE_WIDTH Number of columns of the startSideTable - * @param sub The sub table result * @param startSide The TransitivePathSide where the edges start - * @param targetSide The TransitivePathSide where the edges end * @param startSideTable An IdTable containing the Ids for the startSide - * @return std::pair> A Map and Id vector (nodes) for the - * transitive hull computation + * @return cppcoro::generator An generator for + * the transitive hull computation */ - template - std::pair> setupMapAndNodes( - const IdTable& sub, const TransitivePathSide& startSide, - const TransitivePathSide& targetSide, - const IdTable& startSideTable) const { - std::vector nodes; - auto edges = setupEdgesMap(sub, startSide, targetSide); - - // Bound -> var|id - std::span startNodes = - startSideTable.getColumn(startSide.treeAndCol_.value().second); - // TODO Use ranges::to. - nodes.insert(nodes.end(), startNodes.begin(), startNodes.end()); - - return {std::move(edges), std::move(nodes)}; + cppcoro::generator setupNodes( + const TransitivePathSide& startSide, + std::shared_ptr startSideResult) const { + if (startSideResult->isFullyMaterialized()) { + // Bound -> var|id + std::span startNodes = startSideResult->idTable().getColumn( + startSide.treeAndCol_.value().second); + co_yield TableColumnWithVocab{&startSideResult->idTable(), startNodes, + startSideResult->getCopyOfLocalVocab()}; + } else { + for (auto& [idTable, localVocab] : startSideResult->idTables()) { + // Bound -> var|id + std::span startNodes = + idTable.getColumn(startSide.treeAndCol_.value().second); + co_yield TableColumnWithVocab{&idTable, startNodes, + std::move(localVocab)}; + } + } }; virtual T setupEdgesMap(const IdTable& dynSub, diff --git a/src/engine/idTable/IdTable.h b/src/engine/idTable/IdTable.h index c615e8350c..c76ee1b9d6 100644 --- a/src/engine/idTable/IdTable.h +++ b/src/engine/idTable/IdTable.h @@ -330,9 +330,16 @@ class IdTable { T& at(size_t row, size_t column) requires(!isView) { return data().at(column).at(row); } - const T& at(size_t row, size_t column) const { + // TODO Remove overload for `isView` and drop requires clause. + const T& at(size_t row, size_t column) const requires(!isView) { return data().at(column).at(row); } + // `std::span::at` is a C++26 feature, so we have to implement it ourselves. + const T& at(size_t row, size_t column) const requires(isView) { + const auto& col = data().at(column); + AD_CONTRACT_CHECK(row < col.size()); + return col[row]; + } // Get a reference to the `i`-th row. The returned proxy objects can be // implicitly and trivially converted to `row_reference`. For the design diff --git a/test/TransitivePathTest.cpp b/test/TransitivePathTest.cpp index e616ad2e2b..2e6da1855b 100644 --- a/test/TransitivePathTest.cpp +++ b/test/TransitivePathTest.cpp @@ -4,7 +4,6 @@ // Johannes Herrmann (johannes.r.herrmann(at)gmail.com) #include -#include #include #include @@ -14,7 +13,6 @@ #include "engine/QueryExecutionTree.h" #include "engine/TransitivePathBase.h" #include "engine/ValuesForTesting.h" -#include "gtest/gtest.h" #include "util/GTestHelpers.h" #include "util/IdTableHelpers.h" #include "util/IndexTestHelpers.h" @@ -26,13 +24,17 @@ using Vars = std::vector>; } // namespace -class TransitivePathTest : public testing::TestWithParam { +// The first bool indicates if binary search should be used (true) or hash map +// based search (false). The second bool indicates if the result should be +// requested lazily. +class TransitivePathTest + : public testing::TestWithParam> { public: [[nodiscard]] static std::pair, QueryExecutionContext*> makePath(IdTable input, Vars vars, TransitivePathSide left, TransitivePathSide right, size_t minDist, size_t maxDist) { - bool useBinSearch = GetParam(); + bool useBinSearch = std::get<0>(GetParam()); auto qec = getQec(); auto subtree = ad_utility::makeExecutionTree( qec, std::move(input), vars); @@ -42,6 +44,7 @@ class TransitivePathTest : public testing::TestWithParam { qec}; } + // ___________________________________________________________________________ [[nodiscard]] static std::shared_ptr makePathUnbound( IdTable input, Vars vars, TransitivePathSide left, TransitivePathSide right, size_t minDist, size_t maxDist) { @@ -50,29 +53,75 @@ class TransitivePathTest : public testing::TestWithParam { return T; } - [[nodiscard]] static std::shared_ptr makePathLeftBound( - IdTable input, Vars vars, IdTable sideTable, size_t sideTableCol, - Vars sideVars, TransitivePathSide left, TransitivePathSide right, - size_t minDist, size_t maxDist) { + // Create bound transitive path with a side table that is either a single + // table or multiple ones. + [[nodiscard]] static std::shared_ptr makePathBound( + bool isLeft, IdTable input, Vars vars, + std::variant> sideTable, + size_t sideTableCol, Vars sideVars, TransitivePathSide left, + TransitivePathSide right, size_t minDist, size_t maxDist, + bool forceFullyMaterialized = false) { auto [T, qec] = makePath(std::move(input), vars, std::move(left), std::move(right), minDist, maxDist); - auto leftOp = ad_utility::makeExecutionTree( - qec, std::move(sideTable), sideVars); - return T->bindLeftSide(leftOp, sideTableCol); + auto operation = + std::holds_alternative(sideTable) + ? ad_utility::makeExecutionTree( + qec, std::move(std::get(sideTable)), sideVars, false, + std::vector{sideTableCol}, LocalVocab{}, + std::nullopt, forceFullyMaterialized) + : ad_utility::makeExecutionTree( + qec, std::move(std::get>(sideTable)), + sideVars, false, std::vector{sideTableCol}); + return isLeft ? T->bindLeftSide(operation, sideTableCol) + : T->bindRightSide(operation, sideTableCol); } - [[nodiscard]] static std::shared_ptr makePathRightBound( - IdTable input, Vars vars, IdTable sideTable, size_t sideTableCol, - Vars sideVars, TransitivePathSide left, TransitivePathSide right, - size_t minDist, size_t maxDist) { - auto [T, qec] = makePath(std::move(input), vars, std::move(left), - std::move(right), minDist, maxDist); - auto rightOp = ad_utility::makeExecutionTree( - qec, std::move(sideTable), sideVars); - return T->bindRightSide(rightOp, sideTableCol); + // ___________________________________________________________________________ + static std::vector split(const IdTable& idTable) { + std::vector result; + for (const auto& row : idTable) { + result.emplace_back(idTable.numColumns(), idTable.getAllocator()); + result.back().push_back(row); + } + return result; + } + + // ___________________________________________________________________________ + static bool requestLaziness() { return std::get<1>(GetParam()); } + + // ___________________________________________________________________________ + void assertResultMatchesIdTable(const Result& result, const IdTable& expected, + ad_utility::source_location loc = + ad_utility::source_location::current()) { + auto t = generateLocationTrace(loc); + using ::testing::UnorderedElementsAreArray; + ASSERT_NE(result.isFullyMaterialized(), requestLaziness()); + if (requestLaziness()) { + const auto& [idTable, localVocab] = + aggregateTables(std::move(result.idTables()), expected.numColumns()); + EXPECT_THAT(idTable, UnorderedElementsAreArray(expected)); + } else { + EXPECT_THAT(result.idTable(), UnorderedElementsAreArray(expected)); + } + } + + // Call testCase three times with differing arguments. This is used to test + // scenarios where the same input table is delivered in different splits + // either wrapped within a generator or as a single table. + static void runTestWithForcedSideTableScenarios( + const std::invocable>, + bool> auto& testCase, + IdTable idTable, + ad_utility::source_location loc = + ad_utility::source_location::current()) { + auto trace = generateLocationTrace(loc); + testCase(idTable.clone(), false); + testCase(split(idTable), false); + testCase(idTable.clone(), true); } }; +// _____________________________________________________________________________ TEST_P(TransitivePathTest, idToId) { auto sub = makeIdTableFromVector({{0, 1}, {1, 2}, {1, 3}, {2, 3}}); @@ -84,11 +133,11 @@ TEST_P(TransitivePathTest, idToId) { makePathUnbound(std::move(sub), {Variable{"?start"}, Variable{"?target"}}, left, right, 1, std::numeric_limits::max()); - auto resultTable = T->computeResultOnlyForTesting(); - ASSERT_THAT(resultTable.idTable(), - ::testing::UnorderedElementsAreArray(expected)); + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); } +// _____________________________________________________________________________ TEST_P(TransitivePathTest, idToVar) { auto sub = makeIdTableFromVector({{0, 1}, {1, 2}, {1, 3}, {2, 3}}); @@ -100,11 +149,11 @@ TEST_P(TransitivePathTest, idToVar) { makePathUnbound(std::move(sub), {Variable{"?start"}, Variable{"?target"}}, left, right, 1, std::numeric_limits::max()); - auto resultTable = T->computeResultOnlyForTesting(); - ASSERT_THAT(resultTable.idTable(), - ::testing::UnorderedElementsAreArray(expected)); + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); } +// _____________________________________________________________________________ TEST_P(TransitivePathTest, varToId) { auto sub = makeIdTableFromVector({{0, 1}, {1, 2}, {1, 3}, {2, 3}}); @@ -120,11 +169,11 @@ TEST_P(TransitivePathTest, varToId) { makePathUnbound(std::move(sub), {Variable{"?start"}, Variable{"?target"}}, left, right, 1, std::numeric_limits::max()); - auto resultTable = T->computeResultOnlyForTesting(); - ASSERT_THAT(resultTable.idTable(), - ::testing::UnorderedElementsAreArray(expected)); + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); } +// _____________________________________________________________________________ TEST_P(TransitivePathTest, idToVarMinLengthZero) { auto sub = makeIdTableFromVector({{0, 1}, {1, 2}, {1, 3}, {2, 3}}); @@ -136,11 +185,11 @@ TEST_P(TransitivePathTest, idToVarMinLengthZero) { makePathUnbound(std::move(sub), {Variable{"?start"}, Variable{"?target"}}, left, right, 0, std::numeric_limits::max()); - auto resultTable = T->computeResultOnlyForTesting(); - ASSERT_THAT(resultTable.idTable(), - ::testing::UnorderedElementsAreArray(expected)); + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); } +// _____________________________________________________________________________ TEST_P(TransitivePathTest, varToIdMinLengthZero) { auto sub = makeIdTableFromVector({{0, 1}, {1, 2}, {1, 3}, {2, 3}}); @@ -157,11 +206,11 @@ TEST_P(TransitivePathTest, varToIdMinLengthZero) { makePathUnbound(std::move(sub), {Variable{"?start"}, Variable{"?target"}}, left, right, 0, std::numeric_limits::max()); - auto resultTable = T->computeResultOnlyForTesting(); - ASSERT_THAT(resultTable.idTable(), - ::testing::UnorderedElementsAreArray(expected)); + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); } +// _____________________________________________________________________________ TEST_P(TransitivePathTest, varTovar) { auto sub = makeIdTableFromVector({ {0, 1}, @@ -185,11 +234,11 @@ TEST_P(TransitivePathTest, varTovar) { makePathUnbound(std::move(sub), {Variable{"?start"}, Variable{"?target"}}, left, right, 1, std::numeric_limits::max()); - auto resultTable = T->computeResultOnlyForTesting(); - ASSERT_THAT(resultTable.idTable(), - ::testing::UnorderedElementsAreArray(expected)); + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); } +// _____________________________________________________________________________ TEST_P(TransitivePathTest, unlimitedMaxLength) { auto sub = makeIdTableFromVector({{0, 2}, {2, 4}, @@ -225,11 +274,11 @@ TEST_P(TransitivePathTest, unlimitedMaxLength) { makePathUnbound(std::move(sub), {Variable{"?start"}, Variable{"?target"}}, left, right, 1, std::numeric_limits::max()); - auto resultTable = T->computeResultOnlyForTesting(); - ASSERT_THAT(resultTable.idTable(), - ::testing::UnorderedElementsAreArray(expected)); + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); } +// _____________________________________________________________________________ TEST_P(TransitivePathTest, idToLeftBound) { auto sub = makeIdTableFromVector({{0, 1}, {1, 2}, {1, 3}, {2, 3}, {3, 4}}); @@ -247,29 +296,33 @@ TEST_P(TransitivePathTest, idToLeftBound) { TransitivePathSide left(std::nullopt, 0, Variable{"?start"}, 0); TransitivePathSide right(std::nullopt, 1, V(4), 1); - { - auto T = makePathLeftBound( - sub.clone(), {Variable{"?start"}, Variable{"?target"}}, - leftOpTable.clone(), 1, {Variable{"?x"}, Variable{"?start"}}, left, - right, 0, std::numeric_limits::max()); - - auto resultTable = T->computeResultOnlyForTesting(); - ASSERT_THAT(resultTable.idTable(), - ::testing::UnorderedElementsAreArray(expected)); - } - { - auto T = makePathLeftBound( - std::move(sub), {Variable{"?start"}, Variable{"?target"}}, - std::move(leftOpTable), 1, {std::nullopt, Variable{"?start"}}, - std::move(left), std::move(right), 0, - std::numeric_limits::max()); - - auto resultTable = T->computeResultOnlyForTesting(); - ASSERT_THAT(resultTable.idTable(), - ::testing::UnorderedElementsAreArray(expected)); - } + runTestWithForcedSideTableScenarios( + [&](auto tableVariant, bool forceFullyMaterialized) { + auto T = makePathBound( + true, sub.clone(), {Variable{"?start"}, Variable{"?target"}}, + std::move(tableVariant), 1, {Variable{"?x"}, Variable{"?start"}}, + left, right, 0, std::numeric_limits::max(), + forceFullyMaterialized); + + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); + }, + leftOpTable.clone()); + runTestWithForcedSideTableScenarios( + [&](auto tableVariant, bool forceFullyMaterialized) { + auto T = makePathBound( + true, sub.clone(), {Variable{"?start"}, Variable{"?target"}}, + std::move(tableVariant), 1, {std::nullopt, Variable{"?start"}}, + left, right, 0, std::numeric_limits::max(), + forceFullyMaterialized); + + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); + }, + std::move(leftOpTable)); } +// _____________________________________________________________________________ TEST_P(TransitivePathTest, idToRightBound) { auto sub = makeIdTableFromVector({ {0, 1}, @@ -293,29 +346,33 @@ TEST_P(TransitivePathTest, idToRightBound) { TransitivePathSide left(std::nullopt, 0, V(0), 0); TransitivePathSide right(std::nullopt, 1, Variable{"?target"}, 1); - { - auto T = makePathRightBound( - sub.clone(), {Variable{"?start"}, Variable{"?target"}}, - rightOpTable.clone(), 0, {Variable{"?target"}, Variable{"?x"}}, left, - right, 0, std::numeric_limits::max()); - - auto resultTable = T->computeResultOnlyForTesting(); - ASSERT_THAT(resultTable.idTable(), - ::testing::UnorderedElementsAreArray(expected)); - } - { - auto T = makePathRightBound( - std::move(sub), {Variable{"?start"}, Variable{"?target"}}, - std::move(rightOpTable), 0, {Variable{"?target"}, std::nullopt}, - std::move(left), std::move(right), 0, - std::numeric_limits::max()); - - auto resultTable = T->computeResultOnlyForTesting(); - ASSERT_THAT(resultTable.idTable(), - ::testing::UnorderedElementsAreArray(expected)); - } + runTestWithForcedSideTableScenarios( + [&](auto tableVariant, bool forceFullyMaterialized) { + auto T = makePathBound( + false, sub.clone(), {Variable{"?start"}, Variable{"?target"}}, + std::move(tableVariant), 0, {Variable{"?target"}, Variable{"?x"}}, + left, right, 0, std::numeric_limits::max(), + forceFullyMaterialized); + + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); + }, + rightOpTable.clone()); + runTestWithForcedSideTableScenarios( + [&](auto tableVariant, bool forceFullyMaterialized) { + auto T = makePathBound( + false, sub.clone(), {Variable{"?start"}, Variable{"?target"}}, + std::move(tableVariant), 0, {Variable{"?target"}, std::nullopt}, + left, right, 0, std::numeric_limits::max(), + forceFullyMaterialized); + + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); + }, + std::move(rightOpTable)); } +// _____________________________________________________________________________ TEST_P(TransitivePathTest, leftBoundToVar) { auto sub = makeIdTableFromVector({ {1, 2}, @@ -344,19 +401,21 @@ TEST_P(TransitivePathTest, leftBoundToVar) { TransitivePathSide left(std::nullopt, 0, Variable{"?start"}, 0); TransitivePathSide right(std::nullopt, 1, Variable{"?target"}, 1); - { - auto T = makePathLeftBound( - std::move(sub), {Variable{"?start"}, Variable{"?target"}}, - std::move(leftOpTable), 1, {Variable{"?x"}, Variable{"?start"}}, - std::move(left), std::move(right), 0, - std::numeric_limits::max()); - - auto resultTable = T->computeResultOnlyForTesting(); - ASSERT_THAT(resultTable.idTable(), - ::testing::UnorderedElementsAreArray(expected)); - } + runTestWithForcedSideTableScenarios( + [&](auto tableVariant, bool forceFullyMaterialized) { + auto T = makePathBound( + true, sub.clone(), {Variable{"?start"}, Variable{"?target"}}, + std::move(tableVariant), 1, {Variable{"?x"}, Variable{"?start"}}, + left, right, 0, std::numeric_limits::max(), + forceFullyMaterialized); + + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); + }, + std::move(leftOpTable)); } +// _____________________________________________________________________________ TEST_P(TransitivePathTest, rightBoundToVar) { auto sub = makeIdTableFromVector({ {1, 2}, @@ -385,16 +444,98 @@ TEST_P(TransitivePathTest, rightBoundToVar) { TransitivePathSide left(std::nullopt, 0, Variable{"?start"}, 0); TransitivePathSide right(std::nullopt, 1, Variable{"?target"}, 1); - auto T = makePathRightBound( - std::move(sub), {Variable{"?start"}, Variable{"?target"}}, - std::move(rightOpTable), 0, {Variable{"?target"}, Variable{"?x"}}, - std::move(left), std::move(right), 0, std::numeric_limits::max()); - - auto resultTable = T->computeResultOnlyForTesting(); - ASSERT_THAT(resultTable.idTable(), - ::testing::UnorderedElementsAreArray(expected)); + runTestWithForcedSideTableScenarios( + [&](auto tableVariant, bool forceFullyMaterialized) { + auto T = makePathBound( + false, sub.clone(), {Variable{"?start"}, Variable{"?target"}}, + std::move(tableVariant), 0, {Variable{"?target"}, Variable{"?x"}}, + left, right, 0, std::numeric_limits::max(), + forceFullyMaterialized); + + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); + }, + std::move(rightOpTable)); +} + +// _____________________________________________________________________________ +TEST_P(TransitivePathTest, startNodesWithNoMatchesRightBound) { + auto sub = makeIdTableFromVector({ + {1, 2}, + {3, 4}, + }); + + auto rightOpTable = makeIdTableFromVector({ + {2, 5}, + {3, 6}, + {4, 7}, + }); + + auto expected = makeIdTableFromVector({ + {1, 2, 5}, + {3, 4, 7}, + }); + + TransitivePathSide left(std::nullopt, 0, Variable{"?start"}, 0); + TransitivePathSide right(std::nullopt, 1, Variable{"?target"}, 1); + auto T = makePathBound( + false, sub.clone(), {Variable{"?start"}, Variable{"?target"}}, + split(rightOpTable), 0, {Variable{"?target"}, Variable{"?x"}}, left, + right, 1, std::numeric_limits::max()); + + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); +} + +// _____________________________________________________________________________ +TEST_P(TransitivePathTest, emptySideTable) { + auto sub = makeIdTableFromVector({ + {1, 2}, + {3, 4}, + }); + + auto expected = makeIdTableFromVector({}); + + TransitivePathSide left(std::nullopt, 0, Variable{"?start"}, 0); + TransitivePathSide right(std::nullopt, 1, Variable{"?target"}, 1); + auto T = makePathBound(true, sub.clone(), + {Variable{"?start"}, Variable{"?target"}}, + std::vector{}, 0, {Variable{"?start"}}, left, + right, 0, std::numeric_limits::max()); + + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); +} + +// _____________________________________________________________________________ +TEST_P(TransitivePathTest, startNodesWithNoMatchesLeftBound) { + auto sub = makeIdTableFromVector({ + {1, 2}, + {3, 4}, + }); + + auto leftOpTable = makeIdTableFromVector({ + {2, 5}, + {3, 6}, + {4, 7}, + }); + + auto expected = makeIdTableFromVector({ + {3, 4, 6}, + }); + + TransitivePathSide left(std::nullopt, 0, Variable{"?start"}, 0); + TransitivePathSide right(std::nullopt, 1, Variable{"?target"}, 1); + auto T = makePathBound( + true, sub.clone(), {Variable{"?start"}, Variable{"?target"}}, + split(leftOpTable), 0, {Variable{"?start"}, Variable{"?x"}}, left, right, + 1, std::numeric_limits::max()); + + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); } +// _____________________________________________________________________________ TEST_P(TransitivePathTest, maxLength2FromVariable) { auto sub = makeIdTableFromVector({ {0, 2}, @@ -426,11 +567,11 @@ TEST_P(TransitivePathTest, maxLength2FromVariable) { auto T = makePathUnbound(std::move(sub), {Variable{"?start"}, Variable{"?target"}}, left, right, 1, 2); - auto resultTable = T->computeResultOnlyForTesting(); - ASSERT_THAT(resultTable.idTable(), - ::testing::UnorderedElementsAreArray(expected)); + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); } +// _____________________________________________________________________________ TEST_P(TransitivePathTest, maxLength2FromId) { auto sub = makeIdTableFromVector({ {0, 2}, @@ -454,11 +595,11 @@ TEST_P(TransitivePathTest, maxLength2FromId) { auto T = makePathUnbound(std::move(sub), {Variable{"?start"}, Variable{"?target"}}, left, right, 1, 2); - auto resultTable = T->computeResultOnlyForTesting(); - ASSERT_THAT(resultTable.idTable(), - ::testing::UnorderedElementsAreArray(expected)); + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); } +// _____________________________________________________________________________ TEST_P(TransitivePathTest, maxLength2ToId) { auto sub = makeIdTableFromVector({ {0, 2}, @@ -481,11 +622,11 @@ TEST_P(TransitivePathTest, maxLength2ToId) { auto T = makePathUnbound(std::move(sub), {Variable{"?start"}, Variable{"?target"}}, left, right, 1, 2); - auto resultTable = T->computeResultOnlyForTesting(); - ASSERT_THAT(resultTable.idTable(), - ::testing::UnorderedElementsAreArray(expected)); + auto resultTable = T->computeResultOnlyForTesting(requestLaziness()); + assertResultMatchesIdTable(resultTable, expected); } +// _____________________________________________________________________________ TEST_P(TransitivePathTest, zeroLengthException) { auto sub = makeIdTableFromVector({ {0, 2}, @@ -504,15 +645,19 @@ TEST_P(TransitivePathTest, zeroLengthException) { makePathUnbound(std::move(sub), {Variable{"?start"}, Variable{"?target"}}, left, right, 0, std::numeric_limits::max()); AD_EXPECT_THROW_WITH_MESSAGE( - T->computeResultOnlyForTesting(), + T->computeResultOnlyForTesting(requestLaziness()), ::testing::ContainsRegex("This query might have to evaluate the empty " "path, which is currently " "not supported")); } -INSTANTIATE_TEST_SUITE_P(TransitivePathTestSuite, TransitivePathTest, - testing::Bool(), - [](const testing::TestParamInfo& info) { - return info.param ? "TransitivePathBinSearch" - : "TransitivePathHashMap"; - }); +// _____________________________________________________________________________ +INSTANTIATE_TEST_SUITE_P( + TransitivePathTestSuite, TransitivePathTest, + ::testing::Combine(::testing::Bool(), ::testing::Bool()), + [](const testing::TestParamInfo>& info) { + std::string result = std::get<0>(info.param) ? "TransitivePathBinSearch" + : "TransitivePathHashMap"; + result += std::get<1>(info.param) ? "Lazy" : "FullyMaterialized"; + return result; + }); diff --git a/test/util/IdTableHelpers.cpp b/test/util/IdTableHelpers.cpp index d643476256..0b3b0a6a2e 100644 --- a/test/util/IdTableHelpers.cpp +++ b/test/util/IdTableHelpers.cpp @@ -248,3 +248,15 @@ std::shared_ptr idTableToExecutionTree( return ad_utility::makeExecutionTree(qec, input.clone(), std::move(vars)); } + +// _____________________________________________________________________________ +std::pair> aggregateTables( + Result::Generator generator, size_t numColumns) { + IdTable aggregateTable{numColumns, ad_utility::makeUnlimitedAllocator()}; + std::vector localVocabs; + for (auto& [idTable, localVocab] : generator) { + localVocabs.emplace_back(std::move(localVocab)); + aggregateTable.insertAtEnd(idTable); + } + return {std::move(aggregateTable), std::move(localVocabs)}; +} diff --git a/test/util/IdTableHelpers.h b/test/util/IdTableHelpers.h index 474c0dfd03..bc7035cd2f 100644 --- a/test/util/IdTableHelpers.h +++ b/test/util/IdTableHelpers.h @@ -256,3 +256,8 @@ IdTable createRandomlyFilledIdTable( /// and filling it with dummy variables. std::shared_ptr idTableToExecutionTree( QueryExecutionContext*, const IdTable&); + +// Fully consume a given generator and store it in an `IdTable` and store the +// local vocabs in a vector. +std::pair> aggregateTables( + Result::Generator generator, size_t numColumns);