From 2f2a4544c13f6919e5fce5967822921c24e01603 Mon Sep 17 00:00:00 2001 From: Benjamin Winger Date: Thu, 17 Oct 2024 10:16:25 -0400 Subject: [PATCH] Add support for scanning edge properties to GDS edgeCompute --- .../catalog_entry/table_catalog_entry.cpp | 4 + src/function/gds/all_shortest_paths.cpp | 9 +- src/function/gds/gds_task.cpp | 38 ++++---- src/function/gds/single_shortest_paths.cpp | 6 +- src/graph/on_disk_graph.cpp | 42 +++++++-- .../catalog_entry/table_catalog_entry.h | 2 + src/include/common/vector/value_vector.h | 2 + src/include/function/gds/gds_frontier.h | 3 +- src/include/graph/graph.h | 6 +- src/include/graph/on_disk_graph.h | 10 +- test/storage/rel_scan_test.cpp | 92 ++++++++++++++++--- 11 files changed, 162 insertions(+), 52 deletions(-) diff --git a/src/catalog/catalog_entry/table_catalog_entry.cpp b/src/catalog/catalog_entry/table_catalog_entry.cpp index e590737bbc..ce964a5ea9 100644 --- a/src/catalog/catalog_entry/table_catalog_entry.cpp +++ b/src/catalog/catalog_entry/table_catalog_entry.cpp @@ -76,6 +76,10 @@ column_id_t TableCatalogEntry::getColumnID(const std::string& propertyName) cons return propertyCollection.getColumnID(propertyName); } +common::column_id_t TableCatalogEntry::getColumnID(common::idx_t idx) const { + return propertyCollection.getColumnID(idx); +} + void TableCatalogEntry::addProperty(const PropertyDefinition& propertyDefinition) { propertyCollection.add(propertyDefinition); } diff --git a/src/function/gds/all_shortest_paths.cpp b/src/function/gds/all_shortest_paths.cpp index 862603bd5b..38100f380b 100644 --- a/src/function/gds/all_shortest_paths.cpp +++ b/src/function/gds/all_shortest_paths.cpp @@ -1,5 +1,6 @@ #include "binder/expression/expression_util.h" #include "common/data_chunk/sel_vector.h" +#include "common/vector/value_vector.h" #include "function/gds/bfs_graph.h" #include "function/gds/gds_frontier.h" #include "function/gds/gds_function_collection.h" @@ -208,7 +209,7 @@ class AllSPLengthsEdgeCompute : public EdgeCompute { : frontierPair{frontierPair}, multiplicities{multiplicities} {}; void edgeCompute(nodeID_t boundNodeID, std::span nbrIDs, - std::span, SelectionVector& mask, bool) override { + std::span, SelectionVector& mask, bool, const ValueVector*) override { size_t activeCount = 0; mask.forEach([&](auto i) { auto nbrVal = @@ -249,7 +250,8 @@ class AllSPPathsEdgeCompute : public EdgeCompute { } void edgeCompute(nodeID_t boundNodeID, std::span nbrNodeIDs, - std::span edgeIDs, SelectionVector& mask, bool fwdEdge) override { + std::span edgeIDs, SelectionVector& mask, bool fwdEdge, + const ValueVector*) override { size_t activeCount = 0; mask.forEach([&](auto i) { auto nbrLen = frontiersPair->pathLengths->getMaskValueFromNextFrontierFixedMask( @@ -399,7 +401,8 @@ struct VarLenJoinsEdgeCompute : public EdgeCompute { }; void edgeCompute(nodeID_t boundNodeID, std::span nbrNodeIDs, - std::span edgeIDs, SelectionVector& mask, bool isFwd) override { + std::span edgeIDs, SelectionVector& mask, bool isFwd, + const ValueVector*) override { mask.forEach([&](auto i) { // We should always update the nbrID in variable length joins if (!parentPtrsBlock->hasSpace()) { diff --git a/src/function/gds/gds_task.cpp b/src/function/gds/gds_task.cpp index 7e7e9a8500..acfb75f595 100644 --- a/src/function/gds/gds_task.cpp +++ b/src/function/gds/gds_task.cpp @@ -8,13 +8,13 @@ using namespace kuzu::common; namespace kuzu { namespace function { -static uint64_t computeScanResult(nodeID_t sourceNodeID, std::span nbrNodeIDs, - std::span edgeIDs, SelectionVector& mask, EdgeCompute& ec, - FrontierPair& frontierPair, bool isFwd) { - KU_ASSERT(nbrNodeIDs.size() == edgeIDs.size()); - ec.edgeCompute(sourceNodeID, nbrNodeIDs, edgeIDs, mask, isFwd); - frontierPair.getNextFrontierUnsafe().setActive(mask, nbrNodeIDs); - return mask.getSelSize(); +static uint64_t computeScanResult(nodeID_t sourceNodeID, graph::GraphScanState::Chunk& chunk, + EdgeCompute& ec, FrontierPair& frontierPair, bool isFwd) { + KU_ASSERT(chunk.nbrNodes.size() == chunk.edges.size()); + ec.edgeCompute(sourceNodeID, chunk.nbrNodes, chunk.edges, chunk.selVector, isFwd, + chunk.propertyVector); + frontierPair.getNextFrontierUnsafe().setActive(chunk.selVector, chunk.nbrNodes); + return chunk.selVector.getSelSize(); } void FrontierTask::run() { @@ -29,25 +29,25 @@ void FrontierTask::run() { if (sharedState->frontierPair.curFrontier->isActive(nodeID)) { switch (info.direction) { case ExtendDirection::FWD: { - for (auto [nodes, edges, mask] : graph->scanFwd(nodeID, *scanState)) { - numApproxActiveNodesForNextIter += computeScanResult(nodeID, nodes, edges, - mask, *localEc, sharedState->frontierPair, true); + for (auto chunk : graph->scanFwd(nodeID, *scanState)) { + numApproxActiveNodesForNextIter += computeScanResult(nodeID, chunk, + *localEc, sharedState->frontierPair, true); } } break; case ExtendDirection::BWD: { - for (auto [nodes, edges, mask] : graph->scanBwd(nodeID, *scanState)) { - numApproxActiveNodesForNextIter += computeScanResult(nodeID, nodes, edges, - mask, *localEc, sharedState->frontierPair, false); + for (auto chunk : graph->scanBwd(nodeID, *scanState)) { + numApproxActiveNodesForNextIter += computeScanResult(nodeID, chunk, + *localEc, sharedState->frontierPair, false); } } break; case ExtendDirection::BOTH: { - for (auto [nodes, edges, mask] : graph->scanFwd(nodeID, *scanState)) { - numApproxActiveNodesForNextIter += computeScanResult(nodeID, nodes, edges, - mask, *localEc, sharedState->frontierPair, true); + for (auto chunk : graph->scanFwd(nodeID, *scanState)) { + numApproxActiveNodesForNextIter += computeScanResult(nodeID, chunk, + *localEc, sharedState->frontierPair, true); } - for (auto [nodes, edges, mask] : graph->scanBwd(nodeID, *scanState)) { - numApproxActiveNodesForNextIter += computeScanResult(nodeID, nodes, edges, - mask, *localEc, sharedState->frontierPair, false); + for (auto chunk : graph->scanBwd(nodeID, *scanState)) { + numApproxActiveNodesForNextIter += computeScanResult(nodeID, chunk, + *localEc, sharedState->frontierPair, false); } } break; default: diff --git a/src/function/gds/single_shortest_paths.cpp b/src/function/gds/single_shortest_paths.cpp index 23e1ac6a47..4d15bf03b6 100644 --- a/src/function/gds/single_shortest_paths.cpp +++ b/src/function/gds/single_shortest_paths.cpp @@ -1,4 +1,5 @@ #include "common/data_chunk/sel_vector.h" +#include "common/vector/value_vector.h" #include "function/gds/bfs_graph.h" #include "function/gds/gds_frontier.h" #include "function/gds/gds_function_collection.h" @@ -63,7 +64,7 @@ class SingleSPLengthsEdgeCompute : public EdgeCompute { : frontierPair{frontierPair} {}; void edgeCompute(common::nodeID_t, std::span nbrIDs, - std::span, SelectionVector& mask, bool) override { + std::span, SelectionVector& mask, bool, const ValueVector*) override { size_t activeCount = 0; mask.forEach([&](auto i) { if (frontierPair->pathLengths->getMaskValueFromNextFrontierFixedMask( @@ -90,7 +91,8 @@ class SingleSPPathsEdgeCompute : public EdgeCompute { } void edgeCompute(nodeID_t boundNodeID, std::span nbrNodeIDs, - std::span edgeIDs, SelectionVector& mask, bool isFwd) override { + std::span edgeIDs, SelectionVector& mask, bool isFwd, + const ValueVector*) override { size_t activeCount = 0; mask.forEach([&](auto i) { auto shouldUpdate = frontierPair->pathLengths->getMaskValueFromNextFrontierFixedMask( diff --git a/src/graph/on_disk_graph.cpp b/src/graph/on_disk_graph.cpp index de19fdf9a6..434da09ebd 100644 --- a/src/graph/on_disk_graph.cpp +++ b/src/graph/on_disk_graph.cpp @@ -4,6 +4,7 @@ #include "binder/expression/property_expression.h" #include "common/assert.h" +#include "common/data_chunk/data_chunk_state.h" #include "common/enums/rel_direction.h" #include "common/types/types.h" #include "common/vector/value_vector.h" @@ -30,11 +31,15 @@ namespace graph { static std::unique_ptr getRelScanState(MemoryManager& mm, const TableCatalogEntry& relEntry, const RelTable& table, RelDataDirection direction, ValueVector* srcVector, ValueVector* dstVector, ValueVector* relIDVector, - expression_vector properties, const Schema& schema, const ResultSet& resultSet) { + expression_vector predicateEdgeProperties, std::optional edgePropertyID, + ValueVector* propertyVector, const Schema& schema, const ResultSet& resultSet) { auto columnIDs = std::vector{NBR_ID_COLUMN_ID, REL_ID_COLUMN_ID}; - for (auto property : properties) { + for (auto property : predicateEdgeProperties) { columnIDs.push_back(property->constCast().getColumnID(relEntry)); } + if (edgePropertyID) { + columnIDs.push_back(*edgePropertyID); + } auto columns = std::vector{}; for (const auto columnID : columnIDs) { columns.push_back(table.getColumn(columnID, direction)); @@ -44,17 +49,20 @@ static std::unique_ptr getRelScanState(MemoryManager& mm, scanState->nodeIDVector = srcVector; scanState->outputVectors.push_back(dstVector); scanState->outputVectors.push_back(relIDVector); - for (auto& property : properties) { + for (auto& property : predicateEdgeProperties) { auto pos = DataPos(schema.getExpressionPos(*property)); auto vector = resultSet.getValueVector(pos).get(); scanState->outputVectors.push_back(vector); } + if (edgePropertyID) { + scanState->outputVectors.push_back(propertyVector); + } scanState->outState = dstVector->state.get(); return scanState; } OnDiskGraphScanStates::OnDiskGraphScanStates(ClientContext* context, std::span tables, - const GraphEntry& graphEntry) + const GraphEntry& graphEntry, std::optional edgePropertyIndex) : iteratorIndex{0}, direction{RelDataDirection::INVALID} { auto schema = graphEntry.getRelPropertiesSchema(); auto descriptor = ResultSetDescriptor(&schema); @@ -70,6 +78,21 @@ OnDiskGraphScanStates::OnDiskGraphScanStates(ClientContext* context, std::span(LogicalType::INTERNAL_ID(), context->getMemoryManager()); relIDVector->state = state; + std::optional edgePropertyID; + if (edgePropertyIndex) { + // Edge property scans are only supported for single table scans at the moment + KU_ASSERT(tables.size() == 1); + // TODO(bmwinger): If there are both a predicate and a custom edgePropertyIndex, they will + // currently be scanned twice. The propertyVector could simply be one of the vectors used + // for the predicate. + auto catalogEntry = + context->getCatalog()->getTableCatalogEntry(context->getTx(), tables[0]->getTableID()); + propertyVector = std::make_unique( + catalogEntry->getProperty(*edgePropertyIndex).getType().copy(), + context->getMemoryManager()); + propertyVector->state = std::make_shared(); + edgePropertyID = catalogEntry->getColumnID(*edgePropertyIndex); + } if (graphEntry.hasRelPredicate()) { auto mapper = ExpressionMapper(&schema); relPredicateEvaluator = mapper.getEvaluator(graphEntry.getRelPredicate()); @@ -80,10 +103,12 @@ OnDiskGraphScanStates::OnDiskGraphScanStates(ClientContext* context, std::spangetTableID()); auto fwdState = getRelScanState(*context->getMemoryManager(), *relEntry, *table, RelDataDirection::FWD, srcNodeIDVector.get(), dstNodeIDVector.get(), relIDVector.get(), - graphEntry.getRelProperties(), schema, resultSet); + graphEntry.getRelProperties(), *edgePropertyID, propertyVector.get(), schema, + resultSet); auto bwdState = getRelScanState(*context->getMemoryManager(), *relEntry, *table, RelDataDirection::BWD, srcNodeIDVector.get(), dstNodeIDVector.get(), relIDVector.get(), - graphEntry.getRelProperties(), schema, resultSet); + graphEntry.getRelProperties(), *edgePropertyID, propertyVector.get(), schema, + resultSet); scanStates.emplace_back(table->getTableID(), OnDiskGraphScanState{context, *table, std::move(fwdState), std::move(bwdState)}); } @@ -165,10 +190,11 @@ std::vector OnDiskGraph::getRelTableIDInfos() { return result; } -std::unique_ptr OnDiskGraph::prepareScan(table_id_t relTableID) { +std::unique_ptr OnDiskGraph::prepareScan(table_id_t relTableID, + std::optional edgePropertyIndex) { auto relTable = context->getStorageManager()->getTable(relTableID)->ptrCast(); return std::unique_ptr( - new OnDiskGraphScanStates(context, std::span(&relTable, 1), graphEntry)); + new OnDiskGraphScanStates(context, std::span(&relTable, 1), graphEntry, edgePropertyIndex)); } std::unique_ptr OnDiskGraph::prepareMultiTableScanFwd( diff --git a/src/include/catalog/catalog_entry/table_catalog_entry.h b/src/include/catalog/catalog_entry/table_catalog_entry.h index f3311d8c7d..d3f849bd01 100644 --- a/src/include/catalog/catalog_entry/table_catalog_entry.h +++ b/src/include/catalog/catalog_entry/table_catalog_entry.h @@ -7,6 +7,7 @@ #include "catalog/catalog_entry/catalog_entry.h" #include "catalog/property_definition_collection.h" #include "common/enums/table_type.h" +#include "common/types/types.h" #include "function/table_functions.h" namespace kuzu { @@ -58,6 +59,7 @@ class KUZU_API TableCatalogEntry : public CatalogEntry { const binder::PropertyDefinition& getProperty(const std::string& propertyName) const; const binder::PropertyDefinition& getProperty(common::idx_t idx) const; virtual common::column_id_t getColumnID(const std::string& propertyName) const; + common::column_id_t getColumnID(common::idx_t idx) const; void addProperty(const binder::PropertyDefinition& propertyDefinition); void dropProperty(const std::string& propertyName); void renameProperty(const std::string& propertyName, const std::string& newName); diff --git a/src/include/common/vector/value_vector.h b/src/include/common/vector/value_vector.h index fc0891073d..cf02a4d4af 100644 --- a/src/include/common/vector/value_vector.h +++ b/src/include/common/vector/value_vector.h @@ -4,6 +4,7 @@ #include "common/assert.h" #include "common/cast.h" +#include "common/copy_constructors.h" #include "common/data_chunk/data_chunk_state.h" #include "common/null_mask.h" #include "common/types/ku_string.h" @@ -30,6 +31,7 @@ class KUZU_API ValueVector { KU_ASSERT(dataTypeID != LogicalTypeID::LIST); } + DELETE_COPY_AND_MOVE(ValueVector); ~ValueVector() = default; void setState(const std::shared_ptr& state_); diff --git a/src/include/function/gds/gds_frontier.h b/src/include/function/gds/gds_frontier.h index c6e1351fd8..bfdfa49870 100644 --- a/src/include/function/gds/gds_frontier.h +++ b/src/include/function/gds/gds_frontier.h @@ -5,6 +5,7 @@ #include "common/data_chunk/sel_vector.h" #include "common/types/types.h" +#include "common/vector/value_vector.h" #include "storage/buffer_manager/memory_manager.h" namespace kuzu { @@ -26,7 +27,7 @@ class EdgeCompute { // **do not** call setActive. Helper functions in GDSUtils will do that work. virtual void edgeCompute(common::nodeID_t boundNodeID, std::span nbrNodeID, std::span edgeID, - common::SelectionVector& mask, bool fwdEdge) = 0; + common::SelectionVector& mask, bool fwdEdge, const common::ValueVector* edgeProperty) = 0; virtual std::unique_ptr copy() = 0; }; diff --git a/src/include/graph/graph.h b/src/include/graph/graph.h index 2406dfc273..c05083465e 100644 --- a/src/include/graph/graph.h +++ b/src/include/graph/graph.h @@ -3,10 +3,12 @@ #include #include #include +#include #include "common/copy_constructors.h" #include "common/data_chunk/sel_vector.h" #include "common/types/types.h" +#include "common/vector/value_vector.h" #include namespace kuzu { @@ -26,6 +28,7 @@ class GraphScanState { // this reference can be modified, but the underlying data will be reset the next time next // is called common::SelectionVector& selVector; + const common::ValueVector* propertyVector; }; virtual ~GraphScanState() = default; virtual Chunk getChunk() = 0; @@ -115,7 +118,8 @@ class Graph { virtual std::vector getRelTableIDInfos() = 0; // Prepares scan on the specified relationship table (works for backwards and forwards scans) - virtual std::unique_ptr prepareScan(common::table_id_t relTableID) = 0; + virtual std::unique_ptr prepareScan(common::table_id_t relTableID, + std::optional edgePropertyID = std::nullopt) = 0; // Prepares scan on all connected relationship tables using forward adjList. virtual std::unique_ptr prepareMultiTableScanFwd( std::span nodeTableIDs) = 0; diff --git a/src/include/graph/on_disk_graph.h b/src/include/graph/on_disk_graph.h index 0834265f17..57f655a54b 100644 --- a/src/include/graph/on_disk_graph.h +++ b/src/include/graph/on_disk_graph.h @@ -82,7 +82,8 @@ class OnDiskGraphScanStates : public GraphScanState { public: GraphScanState::Chunk getChunk() override { auto& iter = getInnerIterator(); - return Chunk{iter.getNbrNodes(), iter.getEdges(), iter.getSelVectorUnsafe()}; + return Chunk{iter.getNbrNodes(), iter.getEdges(), iter.getSelVectorUnsafe(), + propertyVector.get()}; } bool next() override; @@ -110,13 +111,15 @@ class OnDiskGraphScanStates : public GraphScanState { std::unique_ptr srcNodeIDVector; std::unique_ptr dstNodeIDVector; std::unique_ptr relIDVector; + std::unique_ptr propertyVector; size_t iteratorIndex; common::RelDataDirection direction; std::unique_ptr relPredicateEvaluator; explicit OnDiskGraphScanStates(main::ClientContext* context, - std::span tableIDs, const GraphEntry& graphEntry); + std::span tableIDs, const GraphEntry& graphEntry, + std::optional edgePropertyIndex = std::nullopt); std::vector> scanStates; }; @@ -134,7 +137,8 @@ class OnDiskGraph final : public Graph { std::vector getRelTableIDInfos() override; - std::unique_ptr prepareScan(common::table_id_t relTableID) override; + std::unique_ptr prepareScan(common::table_id_t relTableID, + std::optional propertyID = std::nullopt) override; std::unique_ptr prepareMultiTableScanFwd( std::span nodeTableIDs) override; std::unique_ptr prepareMultiTableScanBwd( diff --git a/test/storage/rel_scan_test.cpp b/test/storage/rel_scan_test.cpp index e3eb4c70cb..bdae192530 100644 --- a/test/storage/rel_scan_test.cpp +++ b/test/storage/rel_scan_test.cpp @@ -3,6 +3,7 @@ #include #include "catalog/catalog.h" +#include "common/types/date_t.h" #include "common/types/types.h" #include "graph/graph_entry.h" #include "graph/on_disk_graph.h" @@ -10,9 +11,12 @@ #include "main/client_context.h" #include "main_test_helper/private_main_test_helper.h" -using kuzu::common::nodeID_t; - namespace kuzu { + +using common::Date; +using common::date_t; +using common::nodeID_t; +using common::offset_t; namespace testing { class RelScanTest : public PrivateApiTest { @@ -48,25 +52,83 @@ class RelScanTestAmazon : public RelScanTest { TEST_F(RelScanTest, ScanFwd) { auto tableID = catalog->getTableID(context->getTx(), "person"); auto relTableID = catalog->getTableID(context->getTx(), "knows"); - auto scanState = graph->prepareScan(relTableID); + auto datePropertyIndex = + catalog->getTableCatalogEntry(context->getTx(), relTableID)->getPropertyIdx("date"); + auto scanState = graph->prepareScan(relTableID, datePropertyIndex); + + std::unordered_map expectedDates = { + {0, Date::fromDate(2021, 6, 30)}, + {1, Date::fromDate(2021, 6, 30)}, + {2, Date::fromDate(2021, 6, 30)}, + {3, Date::fromDate(2021, 6, 30)}, + {4, Date::fromDate(1950, 5, 14)}, + {5, Date::fromDate(1950, 5, 14)}, + {6, Date::fromDate(2021, 6, 30)}, + {7, Date::fromDate(1950, 5, 14)}, + {8, Date::fromDate(2000, 1, 1)}, + {9, Date::fromDate(2021, 6, 30)}, + {10, Date::fromDate(1950, 05, 14)}, + {11, Date::fromDate(2000, 1, 1)}, + }; + + const auto compare = [&](uint64_t node, std::vector expectedNodeOffsets, + std::vector expectedFwdRelOffsets, + std::vector expectedBwdRelOffsets) { + std::vector resultNodeOffsets; + std::vector expectedNodes; + std::transform(expectedNodeOffsets.begin(), expectedNodeOffsets.end(), + std::back_inserter(expectedNodes), + [&](auto offset) { return nodeID_t{offset, tableID}; }); - const auto compare = [&](uint64_t node, std::vector expected) { - std::vector result; + std::vector resultRelOffsets; + std::vector resultDates; for (const auto chunk : graph->scanFwd(nodeID_t{node, tableID}, *scanState)) { - chunk.selVector.forEach([&](auto i) { result.push_back(chunk.nbrNodes[i]); }); + chunk.selVector.forEach([&](auto i) { + EXPECT_EQ(chunk.nbrNodes[i].tableID, tableID); + resultNodeOffsets.push_back(chunk.nbrNodes[i].offset); + EXPECT_EQ(chunk.edges[i].tableID, relTableID); + resultRelOffsets.push_back(chunk.edges[i].offset); + resultDates.push_back(chunk.propertyVector->getValue(i)); + }); } - EXPECT_EQ(result, expected); - EXPECT_EQ(graph->scanFwd(nodeID_t{node, tableID}, *scanState).collectNbrNodes(), expected); - result.clear(); + EXPECT_EQ(resultNodeOffsets, expectedNodeOffsets); + EXPECT_EQ(resultRelOffsets, expectedFwdRelOffsets); + for (size_t i = 0; i < resultRelOffsets.size(); i++) { + EXPECT_EQ(expectedDates[resultRelOffsets[i]], resultDates[i]) + << " Result " << i << " (rel offset " << resultRelOffsets[i] << ") was " + << Date::toString(resultDates[i]) << " but we expected " + << Date::toString(expectedDates[resultRelOffsets[i]]); + } + EXPECT_EQ(graph->scanFwd(nodeID_t{node, tableID}, *scanState).collectNbrNodes(), + expectedNodes); + + resultNodeOffsets.clear(); + resultRelOffsets.clear(); + resultDates.clear(); + for (const auto chunk : graph->scanBwd(nodeID_t{node, tableID}, *scanState)) { - chunk.selVector.forEach([&](auto i) { result.push_back(chunk.nbrNodes[i]); }); + chunk.selVector.forEach([&](auto i) { + EXPECT_EQ(chunk.nbrNodes[i].tableID, tableID); + resultNodeOffsets.push_back(chunk.nbrNodes[i].offset); + EXPECT_EQ(chunk.edges[i].tableID, relTableID); + resultRelOffsets.push_back(chunk.edges[i].offset); + resultDates.push_back(chunk.propertyVector->getValue(i)); + }); + } + EXPECT_EQ(resultNodeOffsets, expectedNodeOffsets); + EXPECT_EQ(resultRelOffsets, expectedBwdRelOffsets); + for (size_t i = 0; i < resultRelOffsets.size(); i++) { + EXPECT_EQ(expectedDates[resultRelOffsets[i]], resultDates[i]) + << " Result " << i << " (rel offset " << resultRelOffsets[i] << ") was " + << Date::toString(resultDates[i]) << " but we expected " + << Date::toString(expectedDates[resultRelOffsets[i]]); } - EXPECT_EQ(result, expected); - EXPECT_EQ(graph->scanFwd(nodeID_t{node, tableID}, *scanState).collectNbrNodes(), expected); + EXPECT_EQ(graph->scanFwd(nodeID_t{node, tableID}, *scanState).collectNbrNodes(), + expectedNodes); }; - compare(0, {nodeID_t{1, tableID}, nodeID_t{2, tableID}, nodeID_t{3, tableID}}); - compare(1, {nodeID_t{0, tableID}, nodeID_t{2, tableID}, nodeID_t{3, tableID}}); - compare(2, {nodeID_t{0, tableID}, nodeID_t{1, tableID}, nodeID_t{3, tableID}}); + compare(0, {1, 2, 3}, {0, 1, 2}, {3, 6, 9}); + compare(1, {0, 2, 3}, {3, 4, 5}, {0, 7, 10}); + compare(2, {0, 1, 3}, {6, 7, 8}, {1, 4, 11}); } } // namespace testing