Repository: kuzudb/kuzu
Pull request #5006: Refactor gds frontier
Author: login: andyfengHKU
Created: 2025-03-06T18:25:57Z | Merged: 2025-03-08T03:51:02Z

# Description

Remove unnecessary usage of std::atomic in `PathLengths`.

Remove `GDSFrontier` abstraction level, we don't have a second child class and I don't see we will have one in the foreseeable future.

Rename `getNumNodes` to `getMaxOffset` in graph interface. `NumNodes` is confusing/inaccurate when we introduce filtered graph. What we need is maxOffset that is used to initialize dense data structure.


Diff:
diff --git a/src/function/gds/asp_destinations.cpp b/src/function/gds/asp_destinations.cpp
index 954ef9bd76d..75816d5457d 100644
--- a/src/function/gds/asp_destinations.cpp
+++ b/src/function/gds/asp_destinations.cpp
@@ -14,13 +14,13 @@ namespace function {
 
 class Multiplicities {
 public:
-    Multiplicities(const std::unordered_map<table_id_t, uint64_t>& numNodesMap,
+    Multiplicities(const std::unordered_map<table_id_t, uint64_t>& maxOffsetMap,
         storage::MemoryManager* mm) {
-        for (auto& [tableID, numNodes] : numNodesMap) {
-            multiplicityArray.allocate(tableID, numNodes, mm);
+        for (auto& [tableID, maxOffset] : maxOffsetMap) {
+            multiplicityArray.allocate(tableID, maxOffset, mm);
             // Question to Trevor: Do I need to use atomics? If so, why?
             auto data = multiplicityArray.getData(tableID);
-            for (uint64_t i = 0; i < numNodes; ++i) {
+            for (uint64_t i = 0; i < maxOffset; ++i) {
                 data[i].store(0);
             }
         }
@@ -185,10 +185,11 @@ class AllSPDestinationsAlgorithm final : public RJAlgorithm {
 private:
     RJCompState getRJCompState(ExecutionContext* context, nodeID_t sourceNodeID) override {
         auto clientContext = context->clientContext;
-        auto frontier = PathLengths::getUnvisitedFrontier(context, sharedState->graph.get());
-        auto numNodesMap = sharedState->graph->getNumNodesMap(clientContext->getTransaction());
+        auto graph = sharedState->graph.get();
+        auto frontier = PathLengths::getUnvisitedFrontier(context, graph);
         auto mm = clientContext->getMemoryManager();
-        auto multiplicities = std::make_shared<Multiplicities>(numNodesMap, mm);
+        auto multiplicities = std::make_shared<Multiplicities>(
+            graph->getMaxOffsetMap(clientContext->getTransaction()), mm);
         auto outputWriter = std::make_unique<ASPDestinationsOutputWriter>(clientContext,
             sharedState->getOutputNodeMaskMap(), sourceNodeID, frontier, multiplicities);
         auto frontierPair = std::make_unique<SinglePathLengthsFrontierPair>(frontier);
diff --git a/src/function/gds/awsp_paths.cpp b/src/function/gds/awsp_paths.cpp
index b7d4fcf5c87..345fd5f3df1 100644
--- a/src/function/gds/awsp_paths.cpp
+++ b/src/function/gds/awsp_paths.cpp
@@ -141,9 +141,9 @@ class AllWeightedSPPathsAlgorithm : public RJAlgorithm {
     RJCompState getRJCompState(processor::ExecutionContext* context,
         nodeID_t sourceNodeID) override {
         auto clientContext = context->clientContext;
-        auto numNodes = sharedState->graph->getNumNodesMap(clientContext->getTransaction());
-        auto curFrontier = PathLengths::getUnvisitedFrontier(context, sharedState->graph.get());
-        auto nextFrontier = PathLengths::getUnvisitedFrontier(context, sharedState->graph.get());
+        auto graph = sharedState->graph.get();
+        auto curFrontier = PathLengths::getUnvisitedFrontier(context, graph);
+        auto nextFrontier = PathLengths::getUnvisitedFrontier(context, graph);
         auto frontierPair =
             std::make_unique<DoublePathLengthsFrontierPair>(curFrontier, nextFrontier);
         auto bfsGraph = getBFSGraph(context);
diff --git a/src/function/gds/degrees.h b/src/function/gds/degrees.h
index deb687f545d..9a6bb48986f 100644
--- a/src/function/gds/degrees.h
+++ b/src/function/gds/degrees.h
@@ -17,11 +17,11 @@ static constexpr degree_t INVALID_DEGREE = UINT64_MAX;
 
 class Degrees {
 public:
-    Degrees(const table_id_map_t<offset_t>& numNodesMap, MemoryManager* mm) {
-        for (const auto& [tableID, numNodes] : numNodesMap) {
-            degreeValuesMap.allocate(tableID, numNodes, mm);
+    Degrees(const table_id_map_t<offset_t>& maxOffsetMap, MemoryManager* mm) {
+        for (const auto& [tableID, maxOffset] : maxOffsetMap) {
+            degreeValuesMap.allocate(tableID, maxOffset, mm);
             pinTable(tableID);
-            for (auto i = 0u; i < numNodes; ++i) {
+            for (auto i = 0u; i < maxOffset; ++i) {
                 degreeValues[i].store(0, std::memory_order_relaxed);
             }
         }
diff --git a/src/function/gds/gds_frontier.cpp b/src/function/gds/gds_frontier.cpp
index fb9a8f0f6b6..b1f97ed5bc1 100644
--- a/src/function/gds/gds_frontier.cpp
+++ b/src/function/gds/gds_frontier.cpp
@@ -91,31 +91,22 @@ void SparseFrontier::mergeSparseFrontier(const SparseFrontier& other) {
     }
 }
 
-PathLengths::PathLengths(const common::table_id_map_t<common::offset_t>& numNodesMap_,
+PathLengths::PathLengths(const common::table_id_map_t<common::offset_t>& nodeMaxOffsetMap,
     storage::MemoryManager* mm)
-    : GDSFrontier{numNodesMap_} {
-    curIter.store(0);
-    for (const auto& [tableID, numNodes] : numNodesMap_) {
-        auto memBuffer = mm->allocateBuffer(false, numNodes * sizeof(frontier_entry_t));
+    : nodeMaxOffsetMap{nodeMaxOffsetMap}, curIter{0} {
+    for (const auto& [tableID, maxOffset] : nodeMaxOffsetMap) {
+        auto memBuffer = mm->allocateBuffer(false, maxOffset * sizeof(frontier_entry_t));
         masks.insert({tableID, std::move(memBuffer)});
     }
 }
 
 PathLengths::~PathLengths() = default;
 
-void PathLengths::pinCurFrontierTableID(common::table_id_t tableID) {
-    curFrontier.store(getMaskData(tableID), std::memory_order_relaxed);
-}
-
-void PathLengths::pinNextFrontierTableID(common::table_id_t tableID) {
-    nextFrontier.store(getMaskData(tableID), std::memory_order_relaxed);
-}
-
 std::shared_ptr<PathLengths> PathLengths::getUnvisitedFrontier(ExecutionContext* context,
     Graph* graph) {
     auto tx = context->clientContext->getTransaction();
     auto mm = context->clientContext->getMemoryManager();
-    auto frontier = std::make_shared<PathLengths>(graph->getNumNodesMap(tx), mm);
+    auto frontier = std::make_shared<PathLengths>(graph->getMaxOffsetMap(tx), mm);
     auto vc = std::make_unique<PathLengthsInitVertexCompute>(*frontier, UNVISITED);
     GDSUtils::runVertexCompute(context, graph, *vc);
     return frontier;
@@ -127,7 +118,7 @@ std::shared_ptr<PathLengths> PathLengths::getVisitedFrontier(processor::Executio
     graph::Graph* graph) {
     auto tx = context->clientContext->getTransaction();
     auto mm = context->clientContext->getMemoryManager();
-    auto frontier = std::make_shared<PathLengths>(graph->getNumNodesMap(tx), mm);
+    auto frontier = std::make_shared<PathLengths>(graph->getMaxOffsetMap(tx), mm);
     auto vc = std::make_unique<PathLengthsInitVertexCompute>(*frontier, INITIAL_VISITED);
     GDSUtils::runVertexCompute(context, graph, *vc);
     return frontier;
@@ -140,11 +131,11 @@ std::shared_ptr<PathLengths> PathLengths::getVisitedFrontier(processor::Executio
     }
     auto tx = context->clientContext->getTransaction();
     auto mm = context->clientContext->getMemoryManager();
-    auto frontier = std::make_shared<PathLengths>(graph->getNumNodesMap(tx), mm);
+    auto frontier = std::make_shared<PathLengths>(graph->getMaxOffsetMap(tx), mm);
     auto vc = std::make_unique<PathLengthsInitVertexCompute>(*frontier, UNVISITED);
     GDSUtils::runVertexCompute(context, graph, *vc);
     // TODO(Xiyang): we should use a vertex compute to do the following.
-    for (auto [tableID, numNodes] : graph->getNumNodesMap(tx)) {
+    for (auto [tableID, numNodes] : graph->getMaxOffsetMap(tx)) {
         frontier->pinTableID(tableID);
         auto mask = maskMap->getOffsetMask(tableID);
         for (auto i = 0u; i < numNodes; ++i) {
@@ -173,8 +164,8 @@ void PathLengthsInitVertexCompute::vertexCompute(common::offset_t startOffset,
     }
 }
 
-FrontierPair::FrontierPair(std::shared_ptr<GDSFrontier> curFrontier,
-    std::shared_ptr<GDSFrontier> nextFrontier)
+FrontierPair::FrontierPair(std::shared_ptr<PathLengths> curFrontier,
+    std::shared_ptr<PathLengths> nextFrontier)
     : curDenseFrontier{std::move(curFrontier)}, nextDenseFrontier{std::move(nextFrontier)} {
     curSparseFrontier = std::make_shared<SparseFrontier>();
     nextSparseFrontier = std::make_shared<SparseFrontier>();
@@ -235,17 +226,9 @@ bool FrontierPair::isCurFrontierSparse() {
     return curSparseFrontier->enabled();
 }
 
-void SinglePathLengthsFrontierPair::initSource(nodeID_t source) {
-    pathLengths->pinNextFrontierTableID(source.tableID);
-    pathLengths->setActive(source);
-    nextSparseFrontier->pinTableID(source.tableID);
-    nextSparseFrontier->addNode(source.offset);
-    hasActiveNodesForNextIter_.store(true);
-}
-
-void DoublePathLengthsFrontierPair::initSource(nodeID_t source) {
-    nextDenseFrontier->ptrCast<PathLengths>()->pinNextFrontierTableID(source.tableID);
-    nextDenseFrontier->ptrCast<PathLengths>()->setActive(source);
+void FrontierPair::initSource(common::nodeID_t source) {
+    nextDenseFrontier->pinNextFrontierTableID(source.tableID);
+    nextDenseFrontier->setActive(source);
     nextSparseFrontier->pinTableID(source.tableID);
     nextSparseFrontier->addNode(source.offset);
     hasActiveNodesForNextIter_.store(true);
@@ -253,8 +236,8 @@ void DoublePathLengthsFrontierPair::initSource(nodeID_t source) {
 
 void DoublePathLengthsFrontierPair::beginNewIterationInternalNoLock() {
     std::swap(curDenseFrontier, nextDenseFrontier);
-    curDenseFrontier->ptrCast<PathLengths>()->incrementCurIter();
-    nextDenseFrontier->ptrCast<PathLengths>()->incrementCurIter();
+    curDenseFrontier->incrementCurIter();
+    nextDenseFrontier->incrementCurIter();
 }
 
 static constexpr uint64_t EARLY_TERM_NUM_NODES_THRESHOLD = 100;
@@ -267,7 +250,7 @@ bool SPEdgeCompute::terminate(processor::NodeOffsetMaskMap& maskMap) {
     }
 
     auto& frontier = frontierPair->getCurDenseFrontier();
-    for (auto& [tableID, maxNumNodes] : frontier.getNumNodesMap()) {
+    for (auto& [tableID, maxNumNodes] : frontier.getNodeMaxOffsetMap()) {
         frontier.pinTableID(tableID);
         if (!maskMap.containsTableID(tableID)) {
             continue;
diff --git a/src/function/gds/gds_utils.cpp b/src/function/gds/gds_utils.cpp
index 463489537e0..ec3825ce22b 100644
--- a/src/function/gds/gds_utils.cpp
+++ b/src/function/gds/gds_utils.cpp
@@ -47,8 +47,8 @@ static void scheduleFrontierTask(catalog::TableCatalogEntry* fromEntry,
     // more generally decrease the number of worker threads by 1. Therefore, we instruct
     // scheduleTaskAndWaitOrError to start a new thread by passing true as the last
     // argument.
-    auto numNodes = graph->getNumNodes(transaction, fromEntry->getTableID());
-    sharedState->morselDispatcher.init(numNodes);
+    auto maxOffset = graph->getMaxOffset(transaction, fromEntry->getTableID());
+    sharedState->morselDispatcher.init(maxOffset);
     clientContext->getTaskScheduler()->scheduleTaskAndWaitOrError(task, context,
         true /* launchNewWorkerThread */);
 }
@@ -98,10 +98,10 @@ void GDSUtils::runFrontiersUntilConvergence(processor::ExecutionContext* context
 
 static void runVertexComputeInternal(catalog::TableCatalogEntry* currentEntry, graph::Graph* graph,
     std::shared_ptr<VertexComputeTask> task, processor::ExecutionContext* context) {
-    auto numNodes =
-        graph->getNumNodes(context->clientContext->getTransaction(), currentEntry->getTableID());
+    auto maxOffset =
+        graph->getMaxOffset(context->clientContext->getTransaction(), currentEntry->getTableID());
     auto sharedState = task->getSharedState();
-    sharedState->morselDispatcher.init(numNodes);
+    sharedState->morselDispatcher.init(maxOffset);
     context->clientContext->getTaskScheduler()->scheduleTaskAndWaitOrError(task, context,
         true /* launchNewWorkerThread */);
 }
diff --git a/src/function/gds/k_core_decomposition.cpp b/src/function/gds/k_core_decomposition.cpp
index 4a4187b679a..38c1a82e8fd 100644
--- a/src/function/gds/k_core_decomposition.cpp
+++ b/src/function/gds/k_core_decomposition.cpp
@@ -19,8 +19,8 @@ namespace function {
 
 class CoreValues {
 public:
-    CoreValues(const table_id_map_t<offset_t>& numNodesMap, MemoryManager* mm) {
-        init(numNodesMap, mm);
+    CoreValues(const table_id_map_t<offset_t>& maxOffsetMap, MemoryManager* mm) {
+        init(maxOffsetMap, mm);
     }
 
     void pinTable(table_id_t tableID) { coreValues = coreValuesMap.getData(tableID); }
@@ -36,11 +36,11 @@ class CoreValues {
     }
 
 private:
-    void init(const table_id_map_t<offset_t>& numNodesMap, MemoryManager* mm) {
-        for (const auto& [tableID, numNodes] : numNodesMap) {
-            coreValuesMap.allocate(tableID, numNodes, mm);
+    void init(const table_id_map_t<offset_t>& maxOffsetMap, MemoryManager* mm) {
+        for (const auto& [tableID, maxOffset] : maxOffsetMap) {
+            coreValuesMap.allocate(tableID, maxOffset, mm);
             pinTable(tableID);
-            for (auto i = 0u; i < numNodes; ++i) {
+            for (auto i = 0u; i < maxOffset; ++i) {
                 coreValues[i].store(INVALID_DEGREE, std::memory_order_relaxed);
             }
         }
@@ -206,11 +206,11 @@ class KCoreDecomposition final : public GDSAlgorithm {
         auto clientContext = context->clientContext;
         auto mm = clientContext->getMemoryManager();
         auto graph = sharedState->graph.get();
-        auto numNodesMap = graph->getNumNodesMap(clientContext->getTransaction());
-        auto degrees = Degrees(numNodesMap, mm);
+        auto transaction = clientContext->getTransaction();
+        auto degrees = Degrees(graph->getMaxOffsetMap(transaction), mm);
         DegreesUtils::computeDegree(context, graph, sharedState->getGraphNodeMaskMap(), &degrees,
             ExtendDirection::BOTH);
-        auto coreValues = CoreValues(numNodesMap, mm);
+        auto coreValues = CoreValues(graph->getMaxOffsetMap(transaction), mm);
         auto auxiliaryState = std::make_unique<KCoreAuxiliaryState>(degrees, coreValues);
         auto currentFrontier = PathLengths::getUnvisitedFrontier(context, sharedState->graph.get());
         auto nextFrontier = PathLengths::getUnvisitedFrontier(context, sharedState->graph.get());
diff --git a/src/function/gds/page_rank.cpp b/src/function/gds/page_rank.cpp
index 90620fcd7a2..df9ffaf67fe 100644
--- a/src/function/gds/page_rank.cpp
+++ b/src/function/gds/page_rank.cpp
@@ -91,11 +91,11 @@ static void addCAS(std::atomic<double>& origin, double valToAdd) {
 // Represents PageRank value for all nodes
 class PValues {
 public:
-    PValues(table_id_map_t<offset_t> numNodesMap, storage::MemoryManager* mm, double val) {
-        for (const auto& [tableID, numNodes] : numNodesMap) {
-            valueMap.allocate(tableID, numNodes, mm);
+    PValues(table_id_map_t<offset_t> maxOffsetMap, storage::MemoryManager* mm, double val) {
+        for (const auto& [tableID, maxOffset] : maxOffsetMap) {
+            valueMap.allocate(tableID, maxOffset, mm);
             pinTable(tableID);
-            for (auto i = 0u; i < numNodes; ++i) {
+            for (auto i = 0u; i < maxOffset; ++i) {
                 values[i].store(val, std::memory_order_relaxed);
             }
         }
@@ -308,10 +308,10 @@ class PageRank final : public GDSAlgorithm {
         auto clientContext = context->clientContext;
         auto transaction = clientContext->getTransaction();
         auto graph = sharedState->graph.get();
-        auto numNodesMap = graph->getNumNodesMap(transaction);
+        auto maxOffsetMap = graph->getMaxOffsetMap(transaction);
         auto numNodes = graph->getNumNodes(transaction);
-        auto p1 = PValues(numNodesMap, clientContext->getMemoryManager(), (double)1 / numNodes);
-        auto p2 = PValues(numNodesMap, clientContext->getMemoryManager(), 0);
+        auto p1 = PValues(maxOffsetMap, clientContext->getMemoryManager(), (double)1 / numNodes);
+        auto p2 = PValues(maxOffsetMap, clientContext->getMemoryManager(), 0);
         PValues* pCurrent = &p1;
         PValues* pNext = &p2;
         auto pageRankBindData = bindData->ptrCast<PageRankBindData>();
@@ -320,7 +320,7 @@ class PageRank final : public GDSAlgorithm {
         auto currentFrontier = PathLengths::getUnvisitedFrontier(context, graph);
         auto nextFrontier =
             PathLengths::getVisitedFrontier(context, graph, sharedState->getGraphNodeMaskMap());
-        auto degrees = Degrees(numNodesMap, clientContext->getMemoryManager());
+        auto degrees = Degrees(maxOffsetMap, clientContext->getMemoryManager());
         DegreesUtils::computeDegree(context, graph, sharedState->getGraphNodeMaskMap(), &degrees,
             ExtendDirection::FWD);
         auto frontierPair =
diff --git a/src/function/gds/rec_joins.cpp b/src/function/gds/rec_joins.cpp
index d231c5dbedc..24c013f556e 100644
--- a/src/function/gds/rec_joins.cpp
+++ b/src/function/gds/rec_joins.cpp
@@ -117,7 +117,7 @@ void RJAlgorithm::exec(processor::ExecutionContext* context) {
         totalNumNodes = inputNodeMaskMap->getNumMaskedNode();
     } else {
         for (auto& tableID : graph->getNodeTableIDs()) {
-            totalNumNodes += graph->getNumNodes(clientContext->getTransaction(), tableID);
+            totalNumNodes += graph->getMaxOffset(clientContext->getTransaction(), tableID);
         }
     }
     common::offset_t completedNumNodes = 0;
@@ -153,10 +153,10 @@ void RJAlgorithm::exec(processor::ExecutionContext* context) {
                 GDSUtils::runVertexCompute(context, graph, *vertexCompute);
             }
         };
-        auto numNodes = graph->getNumNodes(clientContext->getTransaction(), tableID);
+        auto maxOffset = graph->getMaxOffset(clientContext->getTransaction(), tableID);
         auto mask = inputNodeMaskMap->getOffsetMask(tableID);
         if (mask->isEnabled()) {
-            for (const auto& offset : mask->range(0, numNodes)) {
+            for (const auto& offset : mask->range(0, maxOffset)) {
                 calcFunc(offset);
                 clientContext->getProgressBar()->updateProgress(context->queryID,
                     getRJProgress(totalNumNodes, completedNumNodes++));
@@ -165,7 +165,7 @@ void RJAlgorithm::exec(processor::ExecutionContext* context) {
                 }
             }
         } else {
-            for (auto offset = 0u; offset < numNodes; ++offset) {
+            for (auto offset = 0u; offset < maxOffset; ++offset) {
                 calcFunc(offset);
                 clientContext->getProgressBar()->updateProgress(context->queryID,
                     getRJProgress(totalNumNodes, completedNumNodes++));
@@ -182,7 +182,7 @@ std::unique_ptr<BFSGraph> RJAlgorithm::getBFSGraph(processor::ExecutionContext*
     auto tx = context->clientContext->getTransaction();
     auto mm = context->clientContext->getMemoryManager();
     auto graph = sharedState->graph.get();
-    auto bfsGraph = std::make_unique<BFSGraph>(graph->getNumNodesMap(tx), mm);
+    auto bfsGraph = std::make_unique<BFSGraph>(graph->getMaxOffsetMap(tx), mm);
     auto vc = std::make_unique<BFSGraphInitVertexCompute>(*bfsGraph);
     GDSUtils::runVertexCompute(context, graph, *vc);
     return bfsGraph;
diff --git a/src/function/gds/strongly_connected_components_kosaraju.cpp b/src/function/gds/strongly_connected_components_kosaraju.cpp
index c564757975e..76bdf4bb466 100644
--- a/src/function/gds/strongly_connected_components_kosaraju.cpp
+++ b/src/function/gds/strongly_connected_components_kosaraju.cpp
@@ -219,14 +219,13 @@ class SCCKosaraju final : public GDSAlgorithm {
         auto clientContext = context->clientContext;
         auto mm = clientContext->getMemoryManager();
         auto graph = sharedState->graph.get();
-        auto numNodesMap = graph->getNumNodesMap(clientContext->getTransaction());
-        auto it = numNodesMap.begin();
-        const table_id_t tableID = it->first;
-        const offset_t numNodes = it->second;
+        KU_ASSERT(graph->getNodeTableIDs().size() == 1);
+        auto tableID = graph->getNodeTableIDs()[0];
+        auto maxOffset = graph->getMaxOffset(clientContext->getTransaction(), tableID);
 
-        auto sccState = SCCState(tableID, numNodes, mm);
+        auto sccState = SCCState(tableID, maxOffset, mm);
         auto edgeCompute = make_unique<SCCCompute>(graph, sccState);
-        edgeCompute->compute(tableID, numNodes);
+        edgeCompute->compute(tableID, maxOffset);
 
         auto writer = make_unique<SCCOutputWriter>(clientContext);
         auto vertexCompute =
diff --git a/src/function/gds/weakly_connected_components.cpp b/src/function/gds/weakly_connected_components.cpp
index c98632a247f..214a1b5d7b2 100644
--- a/src/function/gds/weakly_connected_components.cpp
+++ b/src/function/gds/weakly_connected_components.cpp
@@ -18,11 +18,11 @@ namespace function {
 
 class ComponentIDs {
 public:
-    ComponentIDs(const table_id_map_t<offset_t>& numNodesMap, MemoryManager* mm) {
-        for (const auto& [tableID, numNodes] : numNodesMap) {
-            componentIDsMap.allocate(tableID, numNodes, mm);
+    ComponentIDs(const table_id_map_t<offset_t>& maxOffsetMap, MemoryManager* mm) {
+        for (const auto& [tableID, maxOffset] : maxOffsetMap) {
+            componentIDsMap.allocate(tableID, maxOffset, mm);
             pinTable(tableID);
-            for (auto i = 0u; i < numNodes; ++i) {
+            for (auto i = 0u; i < maxOffset; ++i) {
                 componentIDs[i].store(i, std::memory_order_relaxed);
             }
         }
@@ -162,15 +162,14 @@ class WeaklyConnectedComponent final : public GDSAlgorithm {
     void exec(processor::ExecutionContext* context) override {
         auto clientContext = context->clientContext;
         auto graph = sharedState->graph.get();
-        auto numNodesMap = graph->getNumNodesMap(clientContext->getTransaction());
         auto currentFrontier = PathLengths::getUnvisitedFrontier(context, graph);
         auto nextFrontier =
             PathLengths::getVisitedFrontier(context, graph, sharedState->getGraphNodeMaskMap());
         auto frontierPair =
             std::make_unique<DoublePathLengthsFrontierPair>(currentFrontier, nextFrontier);
         frontierPair->initGDS();
-
-        auto componentIDs = ComponentIDs(numNodesMap, clientContext->getMemoryManager());
+        auto componentIDs = ComponentIDs(graph->getMaxOffsetMap(clientContext->getTransaction()),
+            clientContext->getMemoryManager());
         auto edgeCompute = std::make_unique<WCCEdgeCompute>(componentIDs);
         auto writer = std::make_unique<WCCOutputWriter>(clientContext);
         auto vertexCompute = std::make_unique<WCCVertexCompute>(clientContext->getMemoryManager(),
diff --git a/src/function/gds/wsp_destinations.cpp b/src/function/gds/wsp_destinations.cpp
index a7e92e46c7b..dd9294f41e0 100644
--- a/src/function/gds/wsp_destinations.cpp
+++ b/src/function/gds/wsp_destinations.cpp
@@ -13,7 +13,9 @@ namespace function {
 
 class Costs {
 public:
-    Costs(const table_id_map_t<offset_t>& numNodesMap, MemoryManager* mm) { init(numNodesMap, mm); }
+    Costs(const table_id_map_t<offset_t>& maxOffsetMap, MemoryManager* mm) {
+        init(maxOffsetMap, mm);
+    }
 
     void pinTable(table_id_t tableID) { costs = costsMap.getData(tableID); }
 
@@ -36,11 +38,11 @@ class Costs {
     }
 
 private:
-    void init(const table_id_map_t<offset_t>& numNodesMap, MemoryManager* mm) {
-        for (const auto& [tableID, numNodes] : numNodesMap) {
-            costsMap.allocate(tableID, numNodes, mm);
+    void init(const table_id_map_t<offset_t>& maxOffsetMap, MemoryManager* mm) {
+        for (const auto& [tableID, maxOffset] : maxOffsetMap) {
+            costsMap.allocate(tableID, maxOffset, mm);
             pinTable(tableID);
-            for (auto i = 0u; i < numNodes; ++i) {
+            for (auto i = 0u; i < maxOffset; ++i) {
                 costs[i].store(std::numeric_limits<double>::max(), std::memory_order_relaxed);
             }
         }
@@ -150,13 +152,15 @@ class WeightedSPDestinationsAlgorithm : public RJAlgorithm {
     RJCompState getRJCompState(processor::ExecutionContext* context,
         nodeID_t sourceNodeID) override {
         auto clientContext = context->clientContext;
-        auto numNodes = sharedState->graph->getNumNodesMap(clientContext->getTransaction());
+        auto graph = sharedState->graph.get();
         auto curFrontier = PathLengths::getUnvisitedFrontier(context, sharedState->graph.get());
         auto nextFrontier = PathLengths::getUnvisitedFrontier(context, sharedState->graph.get());
         auto frontierPair =
             std::make_unique<DoublePathLengthsFrontierPair>(curFrontier, nextFrontier);
         auto rjBindData = bindData->ptrCast<RJBindData>();
-        auto costs = std::make_shared<Costs>(numNodes, clientContext->getMemoryManager());
+        auto costs =
+            std::make_shared<Costs>(graph->getMaxOffsetMap(clientContext->getTransaction()),
+                clientContext->getMemoryManager());
         auto auxiliaryState = std::make_unique<WSPDestinationsAuxiliaryState>(costs);
         auto outputWriter = std::make_unique<WSPDestinationsOutputWriter>(clientContext,
             sharedState->getOutputNodeMaskMap(), sourceNodeID, costs);
diff --git a/src/function/gds/wsp_paths.cpp b/src/function/gds/wsp_paths.cpp
index 94d97aedfef..b7377a8d8bc 100644
--- a/src/function/gds/wsp_paths.cpp
+++ b/src/function/gds/wsp_paths.cpp
@@ -119,9 +119,9 @@ class WeightedSPPathsAlgorithm : public RJAlgorithm {
     RJCompState getRJCompState(processor::ExecutionContext* context,
         nodeID_t sourceNodeID) override {
         auto clientContext = context->clientContext;
-        auto numNodes = sharedState->graph->getNumNodesMap(clientContext->getTransaction());
-        auto curFrontier = PathLengths::getUnvisitedFrontier(context, sharedState->graph.get());
-        auto nextFrontier = PathLengths::getUnvisitedFrontier(context, sharedState->graph.get());
+        auto graph = sharedState->graph.get();
+        auto curFrontier = PathLengths::getUnvisitedFrontier(context, graph);
+        auto nextFrontier = PathLengths::getUnvisitedFrontier(context, graph);
         auto frontierPair =
             std::make_unique<DoublePathLengthsFrontierPair>(curFrontier, nextFrontier);
         auto bfsGraph = getBFSGraph(context);
diff --git a/src/graph/on_disk_graph.cpp b/src/graph/on_disk_graph.cpp
index 7b0c99f0b2a..f8e888ad397 100644
--- a/src/graph/on_disk_graph.cpp
+++ b/src/graph/on_disk_graph.cpp
@@ -152,12 +152,19 @@ OnDiskGraph::OnDiskGraph(ClientContext* context, GraphEntry entry)
     }
 }
 
-table_id_map_t<offset_t> OnDiskGraph::getNumNodesMap(transaction::Transaction* transaction) const {
-    table_id_map_t<offset_t> retVal;
+common::table_id_map_t<common::offset_t> OnDiskGraph::getMaxOffsetMap(
+    transaction::Transaction* transaction) const {
+    table_id_map_t<offset_t> result;
     for (auto tableID : getNodeTableIDs()) {
-        retVal[tableID] = getNumNodes(transaction, tableID);
+        result[tableID] = getMaxOffset(transaction, tableID);
     }
-    return retVal;
+    return result;
+}
+
+common::offset_t OnDiskGraph::getMaxOffset(transaction::Transaction* transaction,
+    table_id_t id) const {
+    KU_ASSERT(nodeIDToNodeTable.contains(id));
+    return nodeIDToNodeTable.at(id)->getNumTotalRows(transaction);
 }
 
 offset_t OnDiskGraph::getNumNodes(transaction::Transaction* transaction) const {
@@ -166,16 +173,11 @@ offset_t OnDiskGraph::getNumNodes(transaction::Transaction* transaction) const {
     }
     offset_t numNodes = 0u;
     for (auto id : getNodeTableIDs()) {
-        numNodes += getNumNodes(transaction, id);
+        numNodes += getMaxOffset(transaction, id);
     }
     return numNodes;
 }
 
-offset_t OnDiskGraph::getNumNodes(transaction::Transaction* transaction, table_id_t id) const {
-    KU_ASSERT(nodeIDToNodeTable.contains(id));
-    return nodeIDToNodeTable.at(id)->getNumTotalRows(transaction);
-}
-
 std::vector<NbrTableInfo> OnDiskGraph::getForwardNbrTableInfos(table_id_t srcNodeTableID) {
     KU_ASSERT(nodeIDToNbrTableInfos.contains(srcNodeTableID));
     return nodeIDToNbrTableInfos.at(srcNodeTableID);
diff --git a/src/include/function/gds/bfs_graph.h b/src/include/function/gds/bfs_graph.h
index 44abd8e9b1a..a2b529aa499 100644
--- a/src/include/function/gds/bfs_graph.h
+++ b/src/include/function/gds/bfs_graph.h
@@ -47,10 +47,10 @@ class BFSGraph {
     using parent_entry_t = std::atomic<ParentList*>;
 
 public:
-    BFSGraph(common::table_id_map_t<common::offset_t> numNodesMap, storage::MemoryManager* mm)
+    BFSGraph(common::table_id_map_t<common::offset_t> maxOffsetMap, storage::MemoryManager* mm)
         : mm{mm} {
-        for (auto& [tableID, numNodes] : numNodesMap) {
-            parentArray.allocate(tableID, numNodes, mm);
+        for (auto& [tableID, maxOffset] : maxOffsetMap) {
+            parentArray.allocate(tableID, maxOffset, mm);
         }
     }
 
diff --git a/src/include/function/gds/gds_frontier.h b/src/include/function/gds/gds_frontier.h
index d887518937b..4b2c3c91a0d 100644
--- a/src/include/function/gds/gds_frontier.h
+++ b/src/include/function/gds/gds_frontier.h
@@ -55,43 +55,6 @@ class KUZU_API FrontierMorselDispatcher {
     uint64_t morselSize;
 };
 
-/**
- * Interface for maintaining a frontier of nodes for GDS algorithms. The frontier is a set of
- * "active nodes" for which some computation should be done at a particular iteration of a
- * GDSAlgorithm.
- * TODO(Semih/Xiyang): This class should be renamed to simply Frontier after we remove the Frontier
- * classes in recursive_extend/frontier.h
- */
-class GDSFrontier {
-public:
-    explicit GDSFrontier(const common::table_id_map_t<common::offset_t>& numNodesMap)
-        : numNodesMap{numNodesMap} {}
-
-    virtual ~GDSFrontier() = default;
-    virtual void pinTableID(common::table_id_t tableID) = 0;
-    // isActive assumes a tableID has been pinned.
-    virtual bool isActive(common::offset_t offset) = 0;
-    virtual void setActive(std::span<const common::nodeID_t> nodeIDs) = 0;
-    virtual void setActive(common::nodeID_t nodeID) = 0;
-
-    const common::table_id_map_t<common::offset_t>& getNumNodesMap() const { return numNodesMap; }
-    common::offset_t getNumNodes(common::table_id_t tableID) const {
-        KU_ASSERT(numNodesMap.contains(tableID));
-        return numNodesMap.at(tableID);
-    }
-
-    template<class TARGET>
-    TARGET* ptrCast() {
-        return common::ku_dynamic_cast<TARGET*>(this);
-    }
-
-protected:
-    // We do not need to make nodeTableIDAndNumNodesMap and masks atomic because they should only
-    // be accessed by functions that are called by the "master GDS thread" (so not accessed inside
-    // the parallel functions in GDSUtils, which are called by other "worker threads").
-    common::table_id_map_t<common::offset_t> numNodesMap;
-};
-
 /*
  * Sparse frontier keeps a small frontier.
  * If enabled, this frontier represents a complete frontier.
@@ -155,45 +118,49 @@ class KUZU_API SparseFrontier {
  *
  * However, this is not necessary and the caller can also use this to represent a single frontier.
  */
-class KUZU_API PathLengths : public GDSFrontier {
+class KUZU_API PathLengths {
     using frontier_entry_t = std::atomic<uint16_t>;
 
 public:
     static constexpr uint16_t UNVISITED = UINT16_MAX;
 
-    PathLengths(const common::table_id_map_t<common::offset_t>& numNodesMap,
+    PathLengths(const common::table_id_map_t<common::offset_t>& nodeMaxOffsetMap,
         storage::MemoryManager* mm);
-    ~PathLengths() override;
+    PathLengths(const PathLengths& other) = delete;
+    PathLengths(const PathLengths&& other) = delete;
+    ~PathLengths();
+
+    const common::table_id_map_t<common::offset_t>& getNodeMaxOffsetMap() const {
+        return nodeMaxOffsetMap;
+    }
 
     uint16_t getMaskValueFromCurFrontier(common::offset_t offset) {
-        return getCurFrontier()[offset].load(std::memory_order_relaxed);
+        return curFrontier[offset].load(std::memory_order_relaxed);
     }
     uint16_t getMaskValueFromNextFrontier(common::offset_t offset) {
-        return getNextFrontier()[offset].load(std::memory_order_relaxed);
+        return nextFrontier[offset].load(std::memory_order_relaxed);
     }
 
-    bool isActive(common::offset_t offset) override {
-        return getCurFrontier()[offset] == curIter.load(std::memory_order_relaxed) - 1;
-    }
+    bool isActive(common::offset_t offset) { return curFrontier[offset] == curIter - 1; }
 
-    void setActive(std::span<const common::nodeID_t> nodeIDs) override {
+    void setActive(std::span<const common::nodeID_t> nodeIDs) {
         for (const auto nodeID : nodeIDs) {
             setActive(nodeID.offset);
         }
     }
-    void setActive(common::nodeID_t nodeID) override { setActive(nodeID.offset); }
+    void setActive(common::nodeID_t nodeID) { setActive(nodeID.offset); }
     void setActive(common::offset_t offset) {
-        getNextFrontier()[offset].store(getCurIter(), std::memory_order_relaxed);
+        nextFrontier[offset].store(curIter, std::memory_order_relaxed);
     }
     void setCurFrontierValue(common::offset_t offset, uint16_t val) {
-        getCurFrontier()[offset].store(val, std::memory_order_relaxed);
+        curFrontier[offset].store(val, std::memory_order_relaxed);
     }
 
-    void incrementCurIter() { curIter.fetch_add(1, std::memory_order_relaxed); }
+    void incrementCurIter() { curIter++; }
 
-    void pinTableID(common::table_id_t tableID) override { pinCurFrontierTableID(tableID); }
-    void pinCurFrontierTableID(common::table_id_t tableID);
-    void pinNextFrontierTableID(common::table_id_t tableID);
+    void pinTableID(common::table_id_t tableID) { pinCurFrontierTableID(tableID); }
+    void pinCurFrontierTableID(common::table_id_t tableID) { curFrontier = getMaskData(tableID); }
+    void pinNextFrontierTableID(common::table_id_t tableID) { nextFrontier = getMaskData(tableID); }
 
     // Init frontier to UNVISITED
     static std::shared_ptr<PathLengths> getUnvisitedFrontier(processor::ExecutionContext* context,
@@ -206,29 +173,16 @@ class KUZU_API PathLengths : public GDSFrontier {
         graph::Graph* graph, processor::NodeOffsetMaskMap* maskMap);
 
 private:
-    uint16_t getCurIter() { return curIter.load(std::memory_order_relaxed); }
-
-    frontier_entry_t* getCurFrontier() {
-        auto retVal = curFrontier.load(std::memory_order_relaxed);
-        KU_ASSERT(retVal != nullptr);
-        return retVal;
-    }
-
-    frontier_entry_t* getNextFrontier() {
-        auto retVal = nextFrontier.load(std::memory_order_relaxed);
-        KU_ASSERT(retVal != nullptr);
-        return retVal;
-    }
-
     frontier_entry_t* getMaskData(common::table_id_t tableID);
 
 private:
+    common::table_id_map_t<common::offset_t> nodeMaxOffsetMap;
     common::table_id_map_t<std::unique_ptr<storage::MemoryBuffer>> masks;
     // See FrontierPair::curIter. We keep a copy of curIter here because PathLengths stores
     // iteration numbers for vertices and uses them to identify which vertex is in the frontier.
-    std::atomic<uint16_t> curIter;
-    std::atomic<frontier_entry_t*> curFrontier;
-    std::atomic<frontier_entry_t*> nextFrontier;
+    uint16_t curIter = 0;
+    frontier_entry_t* curFrontier = nullptr;
+    frontier_entry_t* nextFrontier = nullptr;
 };
 
 class PathLengthsInitVertexCompute : public VertexCompute {
@@ -260,8 +214,8 @@ class PathLengthsInitVertexCompute : public VertexCompute {
  */
 class KUZU_API FrontierPair {
 public:
-    FrontierPair(std::shared_ptr<GDSFrontier> curFrontier,
-        std::shared_ptr<GDSFrontier> nextFrontier);
+    FrontierPair(std::shared_ptr<PathLengths> curFrontier,
+        std::shared_ptr<PathLengths> nextFrontier);
 
     virtual ~FrontierPair() = default;
 
@@ -270,23 +224,25 @@ class KUZU_API FrontierPair {
     void beginNewIteration();
 
     // Initialize for recursive computation which always starts from a single source.
-    virtual void initSource(common::nodeID_t source) = 0;
+    virtual void initSource(common::nodeID_t source);
     // Initialize for gds computation which usually starts from a large number of nodes;
     void initGDS();
 
     virtual void beginFrontierComputeBetweenTables(common::table_id_t curTableID,
         common::table_id_t nextTableID);
 
-    virtual void pinCurrFrontier(common::table_id_t tableID) {
+    void pinCurrFrontier(common::table_id_t tableID) {
         curSparseFrontier->pinTableID(tableID);
+        curDenseFrontier->pinCurFrontierTableID(tableID);
     }
-    virtual void pinNextFrontier(common::table_id_t tableID) {
+    void pinNextFrontier(common::table_id_t tableID) {
         nextSparseFrontier->pinTableID(tableID);
+        nextDenseFrontier->pinNextFrontierTableID(tableID);
     }
 
     uint16_t getCurrentIter() { return curIter.load(std::memory_order_relaxed); }
 
-    GDSFrontier& getCurDenseFrontier() const { return *curDenseFrontier; }
+    PathLengths& getCurDenseFrontier() const { return *curDenseFrontier; }
     SparseFrontier& getCurSparseFrontier() const { return *curSparseFrontier; }
     SparseFrontier& getNextSparseFrontier() const { return *nextSparseFrontier; }
     SparseFrontier& getVertexComputeCandidates() const { return *vertexComputeCandidates; }
@@ -315,8 +271,8 @@ class KUZU_API FrontierPair {
     std::atomic<bool> hasActiveNodesForNextIter_;
 
     // Dense frontiers are always updated.
-    std::shared_ptr<GDSFrontier> curDenseFrontier;
-    std::shared_ptr<GDSFrontier> nextDenseFrontier;
+    std::shared_ptr<PathLengths> curDenseFrontier;
+    std::shared_ptr<PathLengths> nextDenseFrontier;
     // Sparse frontiers
     std::shared_ptr<SparseFrontier> curSparseFrontier;
     std::shared_ptr<SparseFrontier> nextSparseFrontier;
@@ -331,17 +287,6 @@ class SinglePathLengthsFrontierPair : public FrontierPair {
 
     PathLengths* getPathLengths() const { return pathLengths.get(); }
 
-    void initSource(common::nodeID_t source) override;
-
-    void pinCurrFrontier(common::table_id_t tableID) override {
-        FrontierPair::pinCurrFrontier(tableID);
-        pathLengths->pinCurFrontierTableID(tableID);
-    }
-    void pinNextFrontier(common::table_id_t tableID) override {
-        FrontierPair::pinNextFrontier(tableID);
-        pathLengths->pinNextFrontierTableID(tableID);
-    }
-
     void beginNewIterationInternalNoLock() override { pathLengths->incrementCurIter(); }
 
 private:
@@ -354,17 +299,6 @@ class KUZU_API DoublePathLengthsFrontierPair : public FrontierPair {
         std::shared_ptr<PathLengths> nextFrontier)
         : FrontierPair{curFrontier, nextFrontier} {}
 
-    void initSource(common::nodeID_t source) override;
-
-    void pinCurrFrontier(common::table_id_t tableID) override {
-        FrontierPair::pinCurrFrontier(tableID);
-        curDenseFrontier->ptrCast<PathLengths>()->pinCurFrontierTableID(tableID);
-    }
-    void pinNextFrontier(common::table_id_t tableID) override {
-        FrontierPair::pinNextFrontier(tableID);
-        nextDenseFrontier->ptrCast<PathLengths>()->pinNextFrontierTableID(tableID);
-    }
-
     void beginNewIterationInternalNoLock() override;
 };
 
diff --git a/src/include/function/gds/gds_object_manager.h b/src/include/function/gds/gds_object_manager.h
index fb8969b4759..25b5a198d75 100644
--- a/src/include/function/gds/gds_object_manager.h
+++ b/src/include/function/gds/gds_object_manager.h
@@ -39,9 +39,9 @@ class ObjectBlock {
 template<typename T>
 class ObjectArraysMap {
 public:
-    void allocate(common::table_id_t tableID, common::offset_t numNodes,
+    void allocate(common::table_id_t tableID, common::offset_t maxOffset,
         storage::MemoryManager* mm) {
-        auto buffer = mm->allocateBuffer(false, numNodes * sizeof(T));
+        auto buffer = mm->allocateBuffer(false, maxOffset * sizeof(T));
         bufferPerTable.insert({tableID, std::move(buffer)});
     }
 
diff --git a/src/include/function/gds/gds_state.h b/src/include/function/gds/gds_state.h
index 1dfffde37be..6d1236a1f54 100644
--- a/src/include/function/gds/gds_state.h
+++ b/src/include/function/gds/gds_state.h
@@ -6,7 +6,7 @@
 namespace kuzu {
 namespace function {
 
-struct KUZU_API GDSComputeState {
+struct GDSComputeState {
     std::unique_ptr<function::FrontierPair> frontierPair = nullptr;
     std::unique_ptr<function::EdgeCompute> edgeCompute = nullptr;
     std::unique_ptr<function::GDSAuxiliaryState> auxiliaryState = nullptr;
diff --git a/src/include/graph/graph.h b/src/include/graph/graph.h
index ea3faa87d20..0797e3b4cdb 100644
--- a/src/include/graph/graph.h
+++ b/src/include/graph/graph.h
@@ -185,16 +185,17 @@ class Graph {
     // Get id for all relationship tables.
     virtual std::vector<common::table_id_t> getRelTableIDs() const = 0;
 
-    // Get num rows of each table as a map
-    virtual common::table_id_map_t<common::offset_t> getNumNodesMap(
+    // Get max offset of each table as a map.
+    virtual common::table_id_map_t<common::offset_t> getMaxOffsetMap(
         transaction::Transaction* transaction) const = 0;
 
-    // Get num rows for all node tables.
-    virtual common::offset_t getNumNodes(transaction::Transaction* transaction) const = 0;
-    // Get num rows for given node table.
-    virtual common::offset_t getNumNodes(transaction::Transaction* transaction,
+    // Get max offset of given table.
+    virtual common::offset_t getMaxOffset(transaction::Transaction* transaction,
         common::table_id_t id) const = 0;
 
+    // Get num nodes for all node tables.
+    virtual common::offset_t getNumNodes(transaction::Transaction* transaction) const = 0;
+
     // Get all possible forward (toNodeTable, relTable)s.
     virtual std::vector<NbrTableInfo> getForwardNbrTableInfos(
         common::table_id_t srcNodeTableID) = 0;
diff --git a/src/include/graph/on_disk_graph.h b/src/include/graph/on_disk_graph.h
index be4143383b1..1f71bdfb869 100644
--- a/src/include/graph/on_disk_graph.h
+++ b/src/include/graph/on_disk_graph.h
@@ -146,13 +146,14 @@ class KUZU_API OnDiskGraph final : public Graph {
         return graphEntry.getRelTableIDs();
     }
 
-    common::table_id_map_t<common::offset_t> getNumNodesMap(
+    common::table_id_map_t<common::offset_t> getMaxOffsetMap(
         transaction::Transaction* transaction) const override;
 
-    common::offset_t getNumNodes(transaction::Transaction* transaction) const override;
-    common::offset_t getNumNodes(transaction::Transaction* transaction,
+    common::offset_t getMaxOffset(transaction::Transaction* transaction,
         common::table_id_t id) const override;
 
+    common::offset_t getNumNodes(transaction::Transaction* transaction) const override;
+
     std::vector<NbrTableInfo> getForwardNbrTableInfos(common::table_id_t srcNodeTableID) override;
 
     std::unique_ptr<NbrScanState> prepareRelScan(catalog::TableCatalogEntry* tableEntry,


Reviews:
user: login: ray6080
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T02:31:14Z
user: login: andyfengHKU
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T02:59:14Z

Review comments:
diff_hunk: @@ -155,45 +118,46 @@ class KUZU_API SparseFrontier {
  *
  * However, this is not necessary and the caller can also use this to represent a single frontier.
  */
-class KUZU_API PathLengths : public GDSFrontier {
path: src/include/function/gds/gds_frontier.h
user: login: ray6080
body: Should this be renamed to `DenseFrontier` now? I don't follow why `PathLengths` is a generic name, as we should have cases where the frontier has nothing to do with path?
created_at: 2025-03-07T02:28:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -155,45 +118,46 @@ class KUZU_API SparseFrontier {
  *
  * However, this is not necessary and the caller can also use this to represent a single frontier.
  */
-class KUZU_API PathLengths : public GDSFrontier {
path: src/include/function/gds/gds_frontier.h
user: login: andyfengHKU
body: Maybe but I'm less motivated to do the renaming at this point.
created_at: 2025-03-07T02:59:13Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-06T18:58:46Z
body: # Benchmark Result

Master commit hash: `5752077575b162ab24d6a9ce6b733a16e8126219` 
Branch commit hash: `161ead1f2de63419be32d77bd54595823076c1ff` 

| Query Group | Query Name | Mean Time - Commit (ms) | Mean Time - Master (ms) | Diff |
| --- | --- | --- | --- | --- |
| aggregation | q24 | 728.43 | 791.18 | -62.75 (-7.93%) |
| aggregation | q28 | 7357.19 | 7322.02 | 35.17 (0.48%) |
| filter | q14 | 124.18 | 125.86 | -1.68 (-1.34%) |
| filter | q15 | 123.50 | 123.42 | 0.07 (0.06%) |
| filter | q16 | 338.43 | 341.85 | -3.42 (-1.00%) |
| filter | q17 | 445.27 | 444.64 | 0.63 (0.14%) |
| filter | q18 | 1924.95 | 1931.28 | -6.33 (-0.33%) |
| filter | zonemap-node | 88.55 | 88.94 | -0.39 (-0.44%) |
| filter | zonemap-node-lhs-cast | 90.05 | 89.04 | 1.00 (1.12%) |
| filter | zonemap-node-null | 89.31 | 88.83 | 0.48 (0.55%) |
| filter | zonemap-rel | 5664.23 | 5700.38 | -36.14 (-0.63%) |
| fixed_size_expr_evaluator | q07 | 688.93 | 680.33 | 8.59 (1.26%) |
| fixed_size_expr_evaluator | q08 | 973.26 | 965.43 | 7.82 (0.81%) |
| fixed_size_expr_evaluator | q09 | 972.13 | 962.71 | 9.42 (0.98%) |
| fixed_size_expr_evaluator | q10 | 260.87 | 254.93 | 5.94 (2.33%) |
| fixed_size_expr_evaluator | q11 | 263.46 | 254.75 | 8.71 (3.42%) |
| fixed_size_expr_evaluator | q12 | 239.92 | 232.98 | 6.94 (2.98%) |
| fixed_size_expr_evaluator | q13 | 1568.79 | 1566.27 | 2.52 (0.16%) |
| fixed_size_seq_scan | q23 | 117.24 | 116.14 | 1.11 (0.95%) |
| join | q29 | 968.87 | 955.16 | 13.71 (1.44%) |
| join | q30 | 2294.24 | 2235.86 | 58.37 (2.61%) |
| join | q31 | 4.83 | 4.19 | 0.64 (15.19%) |
| join | SelectiveTwoHopJoin | 40.68 | 38.32 | 2.36 (6.17%) |
| ldbc_snb_ic | q35 | 8.91 | 8.16 | 0.75 (9.22%) |
| ldbc_snb_ic | q36 | 85.91 | 104.48 | -18.56 (-17.77%) |
| ldbc_snb_is | q32 | 4.17 | 3.53 | 0.63 (17.93%) |
| ldbc_snb_is | q33 | 13.97 | 14.92 | -0.95 (-6.35%) |
| ldbc_snb_is | q34 | 1.20 | 1.28 | -0.08 (-6.10%) |
| multi-rel | multi-rel-large-scan | 1425.84 | 1515.23 | -89.39 (-5.90%) |
| multi-rel | multi-rel-lookup | 10.29 | 10.78 | -0.49 (-4.58%) |
| multi-rel | multi-rel-small-scan | 204.69 | 201.88 | 2.81 (1.39%) |
| order_by | q25 | 127.88 | 132.57 | -4.69 (-3.54%) |
| order_by | q26 | 458.50 | 458.79 | -0.29 (-0.06%) |
| order_by | q27 | 1411.61 | 1387.39 | 24.22 (1.75%) |
| recursive_join | recursive-join-bidirection | 311.19 | 305.81 | 5.38 (1.76%) |
| recursive_join | recursive-join-dense | 7157.96 | 7160.16 | -2.20 (-0.03%) |
| recursive_join | recursive-join-path | 24049.66 | 24079.45 | -29.79 (-0.12%) |
| recursive_join | recursive-join-sparse | 890.25 | 892.87 | -2.62 (-0.29%) |
| recursive_join | recursive-join-trail | 7060.35 | 7071.51 | -11.16 (-0.16%) |
| scan_after_filter | q01 | 167.17 | 171.32 | -4.14 (-2.42%) |
| scan_after_filter | q02 | 155.10 | 154.73 | 0.37 (0.24%) |
| shortest_path_ldbc100 | q37 | 107.02 | 89.87 | 17.15 (19.08%) |
| shortest_path_ldbc100 | q38 | 354.28 | 411.49 | -57.21 (-13.90%) |
| shortest_path_ldbc100 | q39 | 63.47 | 65.10 | -1.63 (-2.50%) |
| shortest_path_ldbc100 | q40 | 383.16 | 458.94 | -75.79 (-16.51%) |
| var_size_expr_evaluator | q03 | 2106.46 | 2102.04 | 4.42 (0.21%) |
| var_size_expr_evaluator | q04 | 2234.56 | 2225.98 | 8.59 (0.39%) |
| var_size_expr_evaluator | q05 | 2720.63 | 2674.94 | 45.69 (1.71%) |
| var_size_expr_evaluator | q06 | 1340.14 | 1364.77 | -24.63 (-1.80%) |
| var_size_seq_scan | q19 | 1446.69 | 1441.60 | 5.09 (0.35%) |
| var_size_seq_scan | q20 | 2682.66 | 2696.42 | -13.76 (-0.51%) |
| var_size_seq_scan | q21 | 2234.44 | 2253.54 | -19.10 (-0.85%) |
| var_size_seq_scan | q22 | 126.28 | 125.89 | 0.39 (0.31%) |

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-07T07:25:59Z
body: # Benchmark Result

Master commit hash: `8cfb6553038cd144cfb0211783441aac326e4919` 
Branch commit hash: `5167abfe52cc0c4dbd550a24efd6798a3184ed6f` 

| Query Group | Query Name | Mean Time - Commit (ms) | Mean Time - Master (ms) | Diff |
| --- | --- | --- | --- | --- |
| aggregation | q24 | 729.97 | 748.66 | -18.69 (-2.50%) |
| aggregation | q28 | 6768.40 | 7316.74 | -548.34 (-7.49%) |
| filter | q14 | 126.51 | 155.57 | -29.05 (-18.68%) |
| filter | q15 | 130.22 | 151.37 | -21.15 (-13.97%) |
| filter | q16 | 340.25 | 351.07 | -10.82 (-3.08%) |
| filter | q17 | 446.37 | 453.38 | -7.02 (-1.55%) |
| filter | q18 | 2011.38 | 1921.66 | 89.72 (4.67%) |
| filter | zonemap-node | 89.93 | 97.36 | -7.42 (-7.62%) |
| filter | zonemap-node-lhs-cast | 90.13 | 97.47 | -7.34 (-7.53%) |
| filter | zonemap-node-null | 89.29 | 97.22 | -7.94 (-8.16%) |
| filter | zonemap-rel | 5865.50 | 5742.78 | 122.73 (2.14%) |
| fixed_size_expr_evaluator | q07 | 681.49 | 695.10 | -13.61 (-1.96%) |
| fixed_size_expr_evaluator | q08 | 963.91 | 978.62 | -14.71 (-1.50%) |
| fixed_size_expr_evaluator | q09 | 962.44 | 974.26 | -11.82 (-1.21%) |
| fixed_size_expr_evaluator | q10 | 253.59 | 270.85 | -17.27 (-6.38%) |
| fixed_size_expr_evaluator | q11 | 254.60 | 270.22 | -15.61 (-5.78%) |
| fixed_size_expr_evaluator | q12 | 232.87 | 248.16 | -15.29 (-6.16%) |
| fixed_size_expr_evaluator | q13 | 1613.20 | 1572.27 | 40.93 (2.60%) |
| fixed_size_seq_scan | q23 | 111.12 | 126.42 | -15.30 (-12.10%) |
| **join** | **q29** | **764.01** | **962.57** | **-198.57 (-20.63%)** |
| **join** | **q30** | **1718.37** | **2178.69** | **-460.32 (-21.13%)** |
| **join** | **q31** | **6.91** | **4.82** | **2.08 (43.21%)** |
| join | SelectiveTwoHopJoin | 47.32 | 45.07 | 2.25 (4.99%) |
| ldbc_snb_ic | q35 | 10.88 | 10.16 | 0.72 (7.10%) |
| ldbc_snb_ic | q36 | 94.12 | 101.01 | -6.88 (-6.82%) |
| **ldbc_snb_is** | **q32** | **5.04** | **3.94** | **1.11 (28.12%)** |
| ldbc_snb_is | q33 | 12.92 | 13.69 | -0.76 (-5.57%) |
| ldbc_snb_is | q34 | 1.29 | 1.15 | 0.13 (11.41%) |
| multi-rel | multi-rel-large-scan | 1438.71 | 1415.65 | 23.06 (1.63%) |
| multi-rel | multi-rel-lookup | 12.73 | 11.19 | 1.54 (13.75%) |
| multi-rel | multi-rel-small-scan | 210.07 | 209.17 | 0.90 (0.43%) |
| order_by | q25 | 126.35 | 138.81 | -12.47 (-8.98%) |
| order_by | q26 | 450.08 | 464.55 | -14.46 (-3.11%) |
| order_by | q27 | 1391.11 | 1409.30 | -18.20 (-1.29%) |
| recursive_join | recursive-join-bidirection | 321.01 | 311.90 | 9.11 (2.92%) |
| recursive_join | recursive-join-dense | 7170.37 | 7142.15 | 28.22 (0.40%) |
| recursive_join | recursive-join-path | 23305.85 | 23743.69 | -437.85 (-1.84%) |
| recursive_join | recursive-join-sparse | 774.87 | 898.59 | -123.72 (-13.77%) |
| recursive_join | recursive-join-trail | 7082.51 | 7074.84 | 7.67 (0.11%) |
| scan_after_filter | q01 | 169.64 | 185.28 | -15.64 (-8.44%) |
| scan_after_filter | q02 | 155.81 | 166.52 | -10.71 (-6.43%) |
| shortest_path_ldbc100 | q37 | 94.30 | 102.05 | -7.75 (-7.60%) |
| shortest_path_ldbc100 | q38 | 329.85 | 336.07 | -6.21 (-1.85%) |
| shortest_path_ldbc100 | q39 | 59.40 | 65.48 | -6.08 (-9.28%) |
| shortest_path_ldbc100 | q40 | 413.37 | 406.31 | 7.07 (1.74%) |
| var_size_expr_evaluator | q03 | 2107.50 | 2113.38 | -5.88 (-0.28%) |
| var_size_expr_evaluator | q04 | 2299.96 | 2218.90 | 81.06 (3.65%) |
| var_size_expr_evaluator | q05 | 2655.55 | 2721.70 | -66.14 (-2.43%) |
| var_size_expr_evaluator | q06 | 1345.99 | 1359.69 | -13.70 (-1.01%) |
| var_size_seq_scan | q19 | 1444.80 | 1435.45 | 9.35 (0.65%) |
| var_size_seq_scan | q20 | 2635.59 | 2684.94 | -49.35 (-1.84%) |
| var_size_seq_scan | q21 | 2311.22 | 2246.30 | 64.93 (2.89%) |
| **var_size_seq_scan** | **q22** | **154.61** | **128.73** | **25.88 (20.10%)** |

author_association: NONE
user: login: codecov[bot]
created_at: 2025-03-07T17:31:53Z
body: ## [Codecov](https://app.codecov.io/gh/kuzudb/kuzu/pull/5006?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kuzudb) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 87.78%. Comparing base [(`4a46184`)](https://app.codecov.io/gh/kuzudb/kuzu/commit/4a46184ac8813986b47c1e13be5b73f2f52d7217?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kuzudb) to head [(`e2c68c3`)](https://app.codecov.io/gh/kuzudb/kuzu/commit/e2c68c35db7f2892bf8ae4ecf94b6bbf24c8140b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kuzudb).
> Report is 2 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5006      +/-   ##
==========================================
- Coverage   87.79%   87.78%   -0.01%     
==========================================
  Files        1412     1412              
  Lines       64547    64501      -46     
  Branches     7562     7563       +1     
==========================================
- Hits        56666    56622      -44     
+ Misses       7853     7851       -2     
  Partials       28       28              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/kuzudb/kuzu/pull/5006?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kuzudb).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kuzudb).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-07T19:22:08Z
body: # Benchmark Result

Master commit hash: `10a65b0486cba91c2e66d919c7570d87b3eecdf9` 
Branch commit hash: `5f26fc27149af8bf6add82c6169f76f987fe9e18` 

| Query Group | Query Name | Mean Time - Commit (ms) | Mean Time - Master (ms) | Diff |
| --- | --- | --- | --- | --- |
| aggregation | q24 | 732.92 | 729.98 | 2.94 (0.40%) |
| aggregation | q28 | 6471.81 | 6561.92 | -90.11 (-1.37%) |
| filter | q14 | 126.18 | 125.18 | 1.00 (0.80%) |
| filter | q15 | 129.49 | 126.24 | 3.25 (2.58%) |
| filter | q16 | 344.44 | 339.50 | 4.93 (1.45%) |
| filter | q17 | 447.83 | 446.60 | 1.24 (0.28%) |
| filter | q18 | 2030.09 | 1990.96 | 39.13 (1.97%) |
| filter | zonemap-node | 88.28 | 90.79 | -2.51 (-2.77%) |
| filter | zonemap-node-lhs-cast | 89.58 | 89.49 | 0.09 (0.10%) |
| filter | zonemap-node-null | 90.97 | 89.50 | 1.47 (1.65%) |
| filter | zonemap-rel | 5646.49 | 5764.45 | -117.97 (-2.05%) |
| fixed_size_expr_evaluator | q07 | 680.75 | 689.13 | -8.38 (-1.22%) |
| fixed_size_expr_evaluator | q08 | 962.26 | 968.61 | -6.35 (-0.66%) |
| fixed_size_expr_evaluator | q09 | 962.61 | 966.25 | -3.65 (-0.38%) |
| fixed_size_expr_evaluator | q10 | 255.50 | 260.67 | -5.17 (-1.98%) |
| fixed_size_expr_evaluator | q11 | 255.30 | 260.24 | -4.94 (-1.90%) |
| fixed_size_expr_evaluator | q12 | 232.38 | 241.60 | -9.22 (-3.82%) |
| fixed_size_expr_evaluator | q13 | 1600.02 | 1564.85 | 35.16 (2.25%) |
| fixed_size_seq_scan | q23 | 111.21 | 115.79 | -4.58 (-3.95%) |
| join | q29 | 747.01 | 750.44 | -3.43 (-0.46%) |
| join | q30 | 1727.02 | 1603.80 | 123.22 (7.68%) |
| join | q31 | 6.51 | 6.66 | -0.16 (-2.36%) |
| join | SelectiveTwoHopJoin | 46.25 | 51.08 | -4.82 (-9.45%) |
| ldbc_snb_ic | q35 | 9.74 | 10.23 | -0.49 (-4.81%) |
| ldbc_snb_ic | q36 | 97.84 | 100.49 | -2.66 (-2.64%) |
| **ldbc_snb_is** | **q32** | **4.80** | **6.82** | **-2.02 (-29.62%)** |
| ldbc_snb_is | q33 | 10.97 | 10.50 | 0.47 (4.51%) |
| ldbc_snb_is | q34 | 1.31 | 1.25 | 0.06 (4.87%) |
| multi-rel | multi-rel-large-scan | 1535.87 | 1413.33 | 122.54 (8.67%) |
| multi-rel | multi-rel-lookup | 11.59 | 11.47 | 0.12 (1.07%) |
| multi-rel | multi-rel-small-scan | 211.42 | 183.55 | 27.87 (15.19%) |
| order_by | q25 | 125.02 | 127.58 | -2.57 (-2.01%) |
| order_by | q26 | 443.38 | 449.13 | -5.74 (-1.28%) |
| order_by | q27 | 1399.14 | 1379.84 | 19.29 (1.40%) |
| recursive_join | recursive-join-bidirection | 303.37 | 286.81 | 16.56 (5.77%) |
| recursive_join | recursive-join-dense | 7145.45 | 7166.05 | -20.59 (-0.29%) |
| recursive_join | recursive-join-path | 23176.54 | 23199.43 | -22.88 (-0.10%) |
| recursive_join | recursive-join-sparse | 626.72 | 626.02 | 0.70 (0.11%) |
| recursive_join | recursive-join-trail | 7066.24 | 7091.45 | -25.21 (-0.36%) |
| scan_after_filter | q01 | 171.37 | 175.24 | -3.87 (-2.21%) |
| scan_after_filter | q02 | 154.79 | 156.08 | -1.29 (-0.83%) |
| shortest_path_ldbc100 | q37 | 86.90 | 92.77 | -5.88 (-6.34%) |
| shortest_path_ldbc100 | q38 | 315.66 | 315.75 | -0.09 (-0.03%) |
| shortest_path_ldbc100 | q39 | 60.46 | 64.18 | -3.73 (-5.80%) |
| shortest_path_ldbc100 | q40 | 412.25 | 421.73 | -9.49 (-2.25%) |
| var_size_expr_evaluator | q03 | 2108.13 | 2074.65 | 33.47 (1.61%) |
| var_size_expr_evaluator | q04 | 2313.27 | 2311.98 | 1.29 (0.06%) |
| var_size_expr_evaluator | q05 | 2629.99 | 2670.57 | -40.58 (-1.52%) |
| var_size_expr_evaluator | q06 | 1367.40 | 1373.09 | -5.69 (-0.41%) |
| var_size_seq_scan | q19 | 1452.48 | 1431.12 | 21.36 (1.49%) |
| var_size_seq_scan | q20 | 2819.94 | 2735.38 | 84.56 (3.09%) |
| var_size_seq_scan | q21 | 2347.04 | 2277.93 | 69.11 (3.03%) |
| var_size_seq_scan | q22 | 128.17 | 125.55 | 2.62 (2.08%) |

author_association: NONE