ladybug 0.20.2.dev20260907__tar.gz → 0.20.2.dev20260908__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sdist/PKG-INFO +1 -1
- sdist/ladybug-source/CMakeLists.txt +1 -1
- sdist/ladybug-source/src/include/processor/processor.h +38 -0
- sdist/ladybug-source/src/include/processor/result/result_set_descriptor.h +2 -2
- sdist/ladybug-source/src/include/storage/table/arrow_node_table.h +6 -1
- sdist/ladybug-source/src/include/storage/table/arrow_rel_table.h +4 -4
- sdist/ladybug-source/src/include/storage/table/arrow_table_support.h +18 -5
- sdist/ladybug-source/src/optimizer/optimizer.cpp +67 -0
- sdist/ladybug-source/src/planner/operator/logical_operator.cpp +6 -2
- sdist/ladybug-source/src/processor/processor.cpp +32 -0
- sdist/ladybug-source/src/processor/processor_task.cpp +14 -21
- sdist/ladybug-source/src/processor/result/result_set_descriptor.cpp +2 -2
- sdist/ladybug-source/src/storage/local_storage/local_node_table.cpp +12 -1
- sdist/ladybug-source/src/storage/storage_manager.cpp +12 -31
- sdist/ladybug-source/src/storage/table/arrow_node_table.cpp +13 -3
- sdist/ladybug-source/src/storage/table/arrow_rel_table.cpp +20 -7
- sdist/ladybug-source/src/storage/table/arrow_table_support.cpp +24 -20
- sdist/ladybug-source/src/storage/table/node_table.cpp +29 -1
- sdist/ladybug.egg-info/PKG-INFO +1 -1
- sdist/pyproject.toml +1 -1
sdist/PKG-INFO
CHANGED
|
@@ -1,15 +1,48 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
|
+
#include <mutex>
|
|
4
|
+
#include <thread>
|
|
5
|
+
#include <unordered_map>
|
|
6
|
+
|
|
3
7
|
#include "common/task_system/task_scheduler.h"
|
|
4
8
|
|
|
5
9
|
namespace lbug {
|
|
6
10
|
namespace main {
|
|
7
11
|
class QueryResult;
|
|
8
12
|
}
|
|
13
|
+
namespace storage {
|
|
14
|
+
class MemoryManager;
|
|
15
|
+
}
|
|
9
16
|
namespace processor {
|
|
10
17
|
class FactorizedTable;
|
|
11
18
|
class PhysicalPlan;
|
|
12
19
|
class PhysicalOperator;
|
|
20
|
+
class ResultSet;
|
|
21
|
+
struct ResultSetDescriptor;
|
|
22
|
+
|
|
23
|
+
// Per-database pool of reusable ResultSets, one slot per executing thread. A cached ResultSet
|
|
24
|
+
// owns ValueVectors whose buffers belong to this database's MemoryManager, so the pool must be
|
|
25
|
+
// owned by (and destroyed with) the database rather than living in a process-wide thread_local:
|
|
26
|
+
// a thread_local slot outlives Database::~Database and later releases (or worse, reuses) buffers
|
|
27
|
+
// through a dangling MemoryManager pointer when the next database runs a query on that thread.
|
|
28
|
+
class ResultSetPool {
|
|
29
|
+
public:
|
|
30
|
+
// Returns the ResultSet cached for the calling thread when its descriptor id matches,
|
|
31
|
+
// otherwise allocates a fresh one and caches it. The caller must keep the returned
|
|
32
|
+
// shared_ptr for as long as it uses the ResultSet: a nested query on the same thread may
|
|
33
|
+
// replace the slot, and the pool's own reference is dropped when this database closes.
|
|
34
|
+
std::shared_ptr<ResultSet> getOrCreate(ResultSetDescriptor* descriptor,
|
|
35
|
+
storage::MemoryManager* memoryManager);
|
|
36
|
+
|
|
37
|
+
private:
|
|
38
|
+
struct Entry {
|
|
39
|
+
uint64_t descriptorID = UINT64_MAX;
|
|
40
|
+
std::shared_ptr<ResultSet> resultSet;
|
|
41
|
+
};
|
|
42
|
+
std::mutex mtx;
|
|
43
|
+
std::unordered_map<std::thread::id, Entry> entries;
|
|
44
|
+
};
|
|
45
|
+
|
|
13
46
|
class QueryProcessor {
|
|
14
47
|
|
|
15
48
|
public:
|
|
@@ -20,6 +53,7 @@ public:
|
|
|
20
53
|
#endif
|
|
21
54
|
|
|
22
55
|
common::TaskScheduler* getTaskScheduler() { return taskScheduler.get(); }
|
|
56
|
+
ResultSetPool& getResultSetPool() { return resultSetPool; }
|
|
23
57
|
|
|
24
58
|
std::unique_ptr<main::QueryResult> execute(PhysicalPlan* physicalPlan,
|
|
25
59
|
ExecutionContext* context);
|
|
@@ -30,6 +64,10 @@ private:
|
|
|
30
64
|
void initTask(common::Task* task);
|
|
31
65
|
|
|
32
66
|
private:
|
|
67
|
+
// Declared before the scheduler so worker threads are joined before the pool is destroyed.
|
|
68
|
+
// The pool never outlives the buffers it caches because Database destroys its QueryProcessor
|
|
69
|
+
// before its MemoryManager.
|
|
70
|
+
ResultSetPool resultSetPool;
|
|
33
71
|
std::unique_ptr<common::TaskScheduler> taskScheduler;
|
|
34
72
|
};
|
|
35
73
|
|
|
@@ -27,13 +27,13 @@ struct DataChunkDescriptor {
|
|
|
27
27
|
|
|
28
28
|
struct LBUG_API ResultSetDescriptor {
|
|
29
29
|
// Monotonically increasing identity that survives pointer reuse (ABA
|
|
30
|
-
// prevention).
|
|
30
|
+
// prevention). Per-database ResultSet pooling in ProcessorTask::run()
|
|
31
31
|
// compares this ID instead of the raw pointer so that a descriptor
|
|
32
32
|
// allocated at the same address as a freed one is never confused with it.
|
|
33
33
|
// copy() preserves the id: a copy semantically represents the same descriptor
|
|
34
34
|
// slot (same plan position, same content). In particular the per-execution
|
|
35
35
|
// re-attach of cached-plan sink descriptors (ClientContext::attachSinkDescriptors)
|
|
36
|
-
// must carry the same id across executions, otherwise the
|
|
36
|
+
// must carry the same id across executions, otherwise the per-database ResultSetPool
|
|
37
37
|
// would miss on every execution of the same prepared statement.
|
|
38
38
|
static inline std::atomic<uint64_t> nextID{0};
|
|
39
39
|
uint64_t id;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
#include "storage/table/columnar_node_table_base.h"
|
|
14
14
|
|
|
15
15
|
namespace lbug {
|
|
16
|
+
struct ArrowTableData;
|
|
16
17
|
namespace storage {
|
|
17
18
|
|
|
18
19
|
struct ArrowNodeTableScanState final : ColumnarNodeTableScanState {
|
|
@@ -75,7 +76,7 @@ class ArrowNodeTable final : public ColumnarNodeTableBase {
|
|
|
75
76
|
public:
|
|
76
77
|
ArrowNodeTable(const StorageManager* storageManager,
|
|
77
78
|
const catalog::NodeTableCatalogEntry* nodeTableEntry, MemoryManager* memoryManager,
|
|
78
|
-
|
|
79
|
+
std::shared_ptr<ArrowTableData> arrowData, std::string arrowId);
|
|
79
80
|
|
|
80
81
|
~ArrowNodeTable();
|
|
81
82
|
|
|
@@ -121,6 +122,10 @@ private:
|
|
|
121
122
|
const std::vector<int64_t>& outputToArrowColumnIdx) const;
|
|
122
123
|
|
|
123
124
|
private:
|
|
125
|
+
// Pin on the registry entry: keeps the Arrow buffers alive even after
|
|
126
|
+
// unregisterArrowData() erases the registry map entry (issue #933).
|
|
127
|
+
// `schema`/`arrays` below are shallow non-owning views into `arrowData`.
|
|
128
|
+
std::shared_ptr<ArrowTableData> arrowData;
|
|
124
129
|
ArrowSchemaWrapper schema;
|
|
125
130
|
std::vector<ArrowArrayWrapper> arrays;
|
|
126
131
|
std::vector<std::optional<common::ArrowLogicalTypeInfo>> columnLogicalTypeInfos;
|
|
@@ -32,10 +32,7 @@ public:
|
|
|
32
32
|
ArrowRelTable(catalog::RelGroupCatalogEntry* relGroupEntry, common::table_id_t fromTableID,
|
|
33
33
|
common::table_id_t toTableID, const StorageManager* storageManager,
|
|
34
34
|
MemoryManager* memoryManager, const NodeTable* fromNodeTable, const NodeTable* toNodeTable,
|
|
35
|
-
|
|
36
|
-
std::vector<ArrowArrayWrapper> arrays, ArrowSchemaWrapper indptrSchema,
|
|
37
|
-
std::vector<ArrowArrayWrapper> indptrArrays, std::string arrowId,
|
|
38
|
-
std::string dstColumnName = "to");
|
|
35
|
+
std::shared_ptr<ArrowRelTableData> arrowData, std::string arrowId);
|
|
39
36
|
~ArrowRelTable();
|
|
40
37
|
|
|
41
38
|
void initScanState(transaction::Transaction* transaction, TableScanState& scanState,
|
|
@@ -78,6 +75,9 @@ private:
|
|
|
78
75
|
|
|
79
76
|
const NodeTable* fromNodeTable;
|
|
80
77
|
const NodeTable* toNodeTable;
|
|
78
|
+
// Pin on the registry entry (issue #933); `schema`/`arrays`/`indptrSchema`/
|
|
79
|
+
// `indptrArrays` below are shallow non-owning views into `arrowData`.
|
|
80
|
+
std::shared_ptr<ArrowRelTableData> arrowData;
|
|
81
81
|
ArrowRelTableLayout layout;
|
|
82
82
|
ArrowSchemaWrapper schema;
|
|
83
83
|
std::vector<ArrowArrayWrapper> arrays;
|
|
@@ -22,6 +22,16 @@ struct ArrowRelTableData {
|
|
|
22
22
|
std::string dstColumnName = "to";
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
// Registry-owned Arrow node-table payload. Stored as a shared_ptr in the
|
|
26
|
+
// process-wide registry so that tables can pin the data they view: the
|
|
27
|
+
// shared_ptr copy returned by getArrowData()/getArrowRelData() keeps the
|
|
28
|
+
// underlying Arrow buffers alive even if another thread unregisters (erases)
|
|
29
|
+
// the registry entry concurrently (CWE-416/667, issue #933).
|
|
30
|
+
struct ArrowTableData {
|
|
31
|
+
ArrowSchemaWrapper schema;
|
|
32
|
+
std::vector<ArrowArrayWrapper> arrays;
|
|
33
|
+
};
|
|
34
|
+
|
|
25
35
|
// Result of creating an arrow table view
|
|
26
36
|
struct ArrowTableCreationResult {
|
|
27
37
|
std::unique_ptr<main::QueryResult> queryResult;
|
|
@@ -37,12 +47,15 @@ public:
|
|
|
37
47
|
// Register Arrow relationship data and return an ID
|
|
38
48
|
static std::string registerArrowRelData(ArrowRelTableData data);
|
|
39
49
|
|
|
40
|
-
// Retrieve Arrow data by ID
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
// Retrieve Arrow data by ID. Returns a shared_ptr pinning the registry
|
|
51
|
+
// entry's lifetime: the caller may safely dereference it after the
|
|
52
|
+
// registry lock is released, even if another thread concurrently
|
|
53
|
+
// unregisters the same ID. Returns nullptr when the ID is unknown.
|
|
54
|
+
static std::shared_ptr<ArrowTableData> getArrowData(const std::string& id);
|
|
43
55
|
|
|
44
|
-
// Retrieve Arrow relationship data by ID (
|
|
45
|
-
|
|
56
|
+
// Retrieve Arrow relationship data by ID (same lifetime semantics as
|
|
57
|
+
// getArrowData above). Returns nullptr when the ID is unknown.
|
|
58
|
+
static std::shared_ptr<ArrowRelTableData> getArrowRelData(const std::string& id);
|
|
46
59
|
|
|
47
60
|
// Unregister Arrow data by ID
|
|
48
61
|
static void unregisterArrowData(const std::string& id);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#include <cstdlib>
|
|
5
5
|
#include <unordered_set>
|
|
6
6
|
|
|
7
|
+
#include "binder/expression/expression_util.h"
|
|
7
8
|
#include "common/enums/extend_direction_util.h"
|
|
8
9
|
#include "main/client_context.h"
|
|
9
10
|
#include "optimizer/acc_hash_join_optimizer.h"
|
|
@@ -32,6 +33,14 @@
|
|
|
32
33
|
#include "planner/operator/logical_filter.h"
|
|
33
34
|
#include "planner/operator/logical_hash_join.h"
|
|
34
35
|
#include "planner/operator/logical_operator.h"
|
|
36
|
+
#include "planner/operator/logical_partitioner.h"
|
|
37
|
+
#include "planner/operator/logical_unwind_deduplicate.h"
|
|
38
|
+
#include "planner/operator/scan/logical_count_anti_edge_chain.h"
|
|
39
|
+
#include "planner/operator/scan/logical_count_extend_chain.h"
|
|
40
|
+
#include "planner/operator/scan/logical_count_rel_table.h"
|
|
41
|
+
#include "planner/operator/scan/logical_query_primary_key_lookup.h"
|
|
42
|
+
#include "planner/operator/scan/logical_reachable_count.h"
|
|
43
|
+
#include "planner/operator/scan/logical_rel_degree_table.h"
|
|
35
44
|
#include "planner/operator/scan/logical_scan_node_table.h"
|
|
36
45
|
#include "transaction/transaction.h"
|
|
37
46
|
|
|
@@ -88,6 +97,64 @@ void dumpLogicalTree(const planner::LogicalOperator* op, int depth,
|
|
|
88
97
|
} else if (op->getOperatorType() == planner::LogicalOperatorType::SCAN_NODE_TABLE) {
|
|
89
98
|
fprintf(stderr, " [%s]",
|
|
90
99
|
op->constCast<planner::LogicalScanNodeTable>().getNodeID()->toString().c_str());
|
|
100
|
+
} else if (op->getOperatorType() == planner::LogicalOperatorType::COUNT_REL_TABLE) {
|
|
101
|
+
auto& count = op->constCast<planner::LogicalCountRelTable>();
|
|
102
|
+
fprintf(stderr, " [%s dir=%s bound=%s]", count.getRelGroupEntry()->getName().c_str(),
|
|
103
|
+
common::ExtendDirectionUtil::toString(count.getDirection()).c_str(),
|
|
104
|
+
count.getBoundNode()->getUniqueName().c_str());
|
|
105
|
+
} else if (op->getOperatorType() == planner::LogicalOperatorType::COUNT_EXTEND_CHAIN) {
|
|
106
|
+
auto& count = op->constCast<planner::LogicalCountExtendChain>();
|
|
107
|
+
fprintf(stderr, " [hops=%llu tables=", (unsigned long long)count.getHops().size());
|
|
108
|
+
bool first = true;
|
|
109
|
+
for (auto& hop : count.getHops()) {
|
|
110
|
+
for (auto& spec : hop.relScans) {
|
|
111
|
+
fprintf(stderr, "%s%s", first ? "" : ",", spec.relTableName.c_str());
|
|
112
|
+
first = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
fprintf(stderr, "]");
|
|
116
|
+
} else if (op->getOperatorType() == planner::LogicalOperatorType::COUNT_ANTI_EDGE_CHAIN) {
|
|
117
|
+
auto& count = op->constCast<planner::LogicalCountAntiEdgeChain>();
|
|
118
|
+
fprintf(stderr,
|
|
119
|
+
" [%s antiDir=%s hops=%llu id<>=", count.getAntiRelEntry()->getName().c_str(),
|
|
120
|
+
common::ExtendDirectionUtil::toString(count.getAntiEdgeDir()).c_str(),
|
|
121
|
+
(unsigned long long)count.getSuffixHops().size());
|
|
122
|
+
fprintf(stderr, "%s]", count.getHasNotEquals() ? "true" : "false");
|
|
123
|
+
} else if (op->getOperatorType() == planner::LogicalOperatorType::REACHABLE_COUNT) {
|
|
124
|
+
auto& count = op->constCast<planner::LogicalReachableCount>();
|
|
125
|
+
fprintf(stderr, " [%s dir=%s bound=%s nbr=%s range=%u..%u]",
|
|
126
|
+
count.getRelGroupEntry()->getName().c_str(),
|
|
127
|
+
common::ExtendDirectionUtil::toString(count.getDirection()).c_str(),
|
|
128
|
+
count.getBoundNode()->getUniqueName().c_str(),
|
|
129
|
+
count.getNbrNode()->getUniqueName().c_str(), count.getLowerBound(),
|
|
130
|
+
count.getUpperBound());
|
|
131
|
+
} else if (op->getOperatorType() == planner::LogicalOperatorType::REL_DEGREE_TABLE) {
|
|
132
|
+
auto& degree = op->constCast<planner::LogicalRelDegreeTable>();
|
|
133
|
+
fprintf(stderr, " [%s dir=%s mode=%s]", degree.getRelGroupEntry()->getName().c_str(),
|
|
134
|
+
common::ExtendDirectionUtil::toString(degree.getDirection()).c_str(),
|
|
135
|
+
degree.getMode() == planner::RelDegreeTableMode::ACTIVE_BOUND_COUNT ?
|
|
136
|
+
"ACTIVE_BOUND_COUNT" :
|
|
137
|
+
degree.getMode() == planner::RelDegreeTableMode::TOP_K_DEGREES ? "TOP_K_DEGREES" :
|
|
138
|
+
"OFFSET_COUNT");
|
|
139
|
+
} else if (op->getOperatorType() == planner::LogicalOperatorType::QUERY_PRIMARY_KEY_LOOKUP) {
|
|
140
|
+
auto& lookup = op->constCast<planner::LogicalQueryPrimaryKeyLookup>();
|
|
141
|
+
fprintf(stderr, " [table=%llu key=%s]", (unsigned long long)lookup.getTableID(),
|
|
142
|
+
lookup.getKey()->toString().c_str());
|
|
143
|
+
} else if (op->getOperatorType() == planner::LogicalOperatorType::UNWIND_DEDUPLICATE) {
|
|
144
|
+
auto& dedup = op->constCast<planner::LogicalUnwindDeduplicate>();
|
|
145
|
+
fprintf(stderr, " [keys=%s]",
|
|
146
|
+
binder::ExpressionUtil::toString(dedup.getKeyExpressions()).c_str());
|
|
147
|
+
} else if (op->getOperatorType() == planner::LogicalOperatorType::PARTITIONER) {
|
|
148
|
+
fprintf(stderr, " [keys=%llu]",
|
|
149
|
+
(unsigned long long)op->constCast<planner::LogicalPartitioner>()
|
|
150
|
+
.getInfo()
|
|
151
|
+
.getNumInfos());
|
|
152
|
+
} else {
|
|
153
|
+
// Generic fallback for operators without a dedicated handler above.
|
|
154
|
+
auto exprs = op->getExpressionsForPrinting();
|
|
155
|
+
if (!exprs.empty()) {
|
|
156
|
+
fprintf(stderr, " [%s]", exprs.c_str());
|
|
157
|
+
}
|
|
91
158
|
}
|
|
92
159
|
fprintf(stderr, "\n");
|
|
93
160
|
for (auto i = 0u; i < op->getNumChildren(); ++i) {
|
|
@@ -24,6 +24,8 @@ std::string LogicalOperatorUtils::logicalOperatorTypeToString(LogicalOperatorTyp
|
|
|
24
24
|
return "COPY_FROM";
|
|
25
25
|
case LogicalOperatorType::COPY_TO:
|
|
26
26
|
return "COPY_TO";
|
|
27
|
+
case LogicalOperatorType::COUNT_ANTI_EDGE_CHAIN:
|
|
28
|
+
return "COUNT_ANTI_EDGE_CHAIN";
|
|
27
29
|
case LogicalOperatorType::COUNT_EXTEND_CHAIN:
|
|
28
30
|
return "COUNT_EXTEND_CHAIN";
|
|
29
31
|
case LogicalOperatorType::COUNT_REL_TABLE:
|
|
@@ -132,9 +134,11 @@ std::string LogicalOperatorUtils::logicalOperatorTypeToString(LogicalOperatorTyp
|
|
|
132
134
|
return "EXTENSION_CLAUSE";
|
|
133
135
|
case LogicalOperatorType::UNWIND_DEDUPLICATE:
|
|
134
136
|
return "UNWIND_DEDUPLICATE";
|
|
135
|
-
default:
|
|
136
|
-
throw RuntimeException("Unknown logical operator type.");
|
|
137
137
|
}
|
|
138
|
+
// No default label: -Wswitch then enforces that every LogicalOperatorType enumerator is
|
|
139
|
+
// handled above, so adding a new operator fails the build here instead of failing at
|
|
140
|
+
// runtime. The throw only guards against out-of-range values (e.g. deserialized data).
|
|
141
|
+
throw RuntimeException("Unknown logical operator type.");
|
|
138
142
|
}
|
|
139
143
|
// LCOV_EXCL_STOP
|
|
140
144
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#include "processor/operator/sink.h"
|
|
7
7
|
#include "processor/physical_plan.h"
|
|
8
8
|
#include "processor/processor_task.h"
|
|
9
|
+
#include "processor/result/result_set.h"
|
|
9
10
|
|
|
10
11
|
using namespace lbug::common;
|
|
11
12
|
using namespace lbug::storage;
|
|
@@ -13,6 +14,37 @@ using namespace lbug::storage;
|
|
|
13
14
|
namespace lbug {
|
|
14
15
|
namespace processor {
|
|
15
16
|
|
|
17
|
+
std::shared_ptr<ResultSet> ResultSetPool::getOrCreate(ResultSetDescriptor* descriptor,
|
|
18
|
+
MemoryManager* memoryManager) {
|
|
19
|
+
const auto threadID = std::this_thread::get_id();
|
|
20
|
+
std::shared_ptr<ResultSet> reusable;
|
|
21
|
+
{
|
|
22
|
+
std::lock_guard lock{mtx};
|
|
23
|
+
auto& entry = entries[threadID];
|
|
24
|
+
if (entry.resultSet && entry.descriptorID == descriptor->id) {
|
|
25
|
+
reusable = entry.resultSet;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (reusable) {
|
|
29
|
+
// Same prepared statement on this thread: reuse the allocation. Only this thread touches
|
|
30
|
+
// its slot's ResultSet, so the reset happens after the lock is released.
|
|
31
|
+
reusable->resetForReuse();
|
|
32
|
+
return reusable;
|
|
33
|
+
}
|
|
34
|
+
// First time on this thread, or a different prepared statement: allocate fresh outside the
|
|
35
|
+
// lock, then swap it in and release the previous ResultSet after unlocking.
|
|
36
|
+
auto resultSet = std::make_shared<ResultSet>(descriptor, memoryManager);
|
|
37
|
+
std::shared_ptr<ResultSet> previous;
|
|
38
|
+
{
|
|
39
|
+
std::lock_guard lock{mtx};
|
|
40
|
+
auto& entry = entries[threadID];
|
|
41
|
+
previous = std::move(entry.resultSet);
|
|
42
|
+
entry.resultSet = resultSet;
|
|
43
|
+
entry.descriptorID = descriptor->id;
|
|
44
|
+
}
|
|
45
|
+
return resultSet;
|
|
46
|
+
}
|
|
47
|
+
|
|
16
48
|
#if defined(__APPLE__)
|
|
17
49
|
QueryProcessor::QueryProcessor(uint64_t numThreads, uint32_t threadQos) {
|
|
18
50
|
taskScheduler = std::make_unique<TaskScheduler>(numThreads, threadQos);
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
#include "common/task_system/progress_bar.h"
|
|
4
4
|
#include "main/client_context.h"
|
|
5
|
+
#include "main/database.h"
|
|
5
6
|
#include "main/settings.h"
|
|
6
7
|
#include "processor/execution_context.h"
|
|
8
|
+
#include "processor/processor.h"
|
|
7
9
|
#include "processor/result/result_set.h"
|
|
8
10
|
#include "storage/buffer_manager/memory_manager.h"
|
|
9
11
|
|
|
@@ -33,30 +35,21 @@ void ProcessorTask::run() {
|
|
|
33
35
|
// for i in range(n): conn.execute("RETURN $i", {"i": i})
|
|
34
36
|
// that's a 16KB+ calloc on every iteration.
|
|
35
37
|
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// (
|
|
40
|
-
//
|
|
41
|
-
//
|
|
38
|
+
// The reusable ResultSet is cached per executing thread in the database-owned
|
|
39
|
+
// ResultSetPool (see QueryProcessor), never in a process-wide thread_local: the cached
|
|
40
|
+
// vectors' buffers belong to this database's MemoryManager, and a thread_local slot would
|
|
41
|
+
// outlive Database::~Database and free (or reuse) them through a dangling MemoryManager the
|
|
42
|
+
// next time any database runs a query on this thread. Holding the shared_ptr here keeps the
|
|
43
|
+
// ResultSet alive even if a nested query on this thread replaces the slot.
|
|
42
44
|
ResultSet* resultSetPtr = nullptr;
|
|
43
45
|
std::unique_ptr<ResultSet> ownedResultSet;
|
|
46
|
+
std::shared_ptr<ResultSet> pooledResultSet;
|
|
47
|
+
auto* clientContext = executionContext->clientContext;
|
|
44
48
|
if (auto* desc = sink->getDescriptor()) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
cachedResultSet->resetForReuse();
|
|
50
|
-
resultSetPtr = cachedResultSet.get();
|
|
51
|
-
} else {
|
|
52
|
-
// First time on this thread, or a different prepared statement:
|
|
53
|
-
// allocate fresh. Owning shared_ptr; lifetime ends with the
|
|
54
|
-
// thread (or when the descriptor changes).
|
|
55
|
-
cachedResultSet = std::make_shared<processor::ResultSet>(desc,
|
|
56
|
-
storage::MemoryManager::Get(*executionContext->clientContext));
|
|
57
|
-
cachedDescID = desc->id;
|
|
58
|
-
resultSetPtr = cachedResultSet.get();
|
|
59
|
-
}
|
|
49
|
+
pooledResultSet =
|
|
50
|
+
clientContext->getDatabase()->getQueryProcessor()->getResultSetPool().getOrCreate(desc,
|
|
51
|
+
storage::MemoryManager::Get(*clientContext));
|
|
52
|
+
resultSetPtr = pooledResultSet.get();
|
|
60
53
|
} else {
|
|
61
54
|
// No descriptor (e.g. OrderByMerge): fall back to per-call allocation.
|
|
62
55
|
ownedResultSet =
|
|
@@ -28,8 +28,8 @@ std::unique_ptr<ResultSetDescriptor> ResultSetDescriptor::copy() const {
|
|
|
28
28
|
std::make_unique<ResultSetDescriptor>(std::move(dataChunkDescriptorsCopy));
|
|
29
29
|
// A copy is the same descriptor slot with identical content (copies are never mutated
|
|
30
30
|
// afterwards), so it inherits the source's identity. This keeps the id stable across
|
|
31
|
-
// executions of a prepared statement's cached plan, letting the
|
|
32
|
-
//
|
|
31
|
+
// executions of a prepared statement's cached plan, letting the per-database ResultSetPool
|
|
32
|
+
// used by ProcessorTask::run() actually hit. Two live descriptors with the same id are
|
|
33
33
|
// by construction content-identical, so cache reuse remains safe.
|
|
34
34
|
descriptorCopy->id = id;
|
|
35
35
|
return descriptorCopy;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#include "catalog/catalog_entry/node_table_catalog_entry.h"
|
|
4
4
|
#include "common/cast.h"
|
|
5
5
|
#include "common/exception/message.h"
|
|
6
|
+
#include "common/exception/runtime.h"
|
|
6
7
|
#include "common/types/types.h"
|
|
7
8
|
#include "common/types/value/value.h"
|
|
8
9
|
#include "storage/index/hash_index.h"
|
|
@@ -42,9 +43,19 @@ void LocalNodeTable::initLocalHashIndex(MemoryManager& mm) {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
bool LocalNodeTable::isVisible(const Transaction* transaction, offset_t offset) const {
|
|
46
|
+
if (offset < startOffset) {
|
|
47
|
+
throw RuntimeException(
|
|
48
|
+
"Primary-key index contains an invalid local node offset. Please drop and rebuild "
|
|
49
|
+
"the _PK index.");
|
|
50
|
+
}
|
|
45
51
|
auto [nodeGroupIdx, offsetInGroup] =
|
|
46
52
|
StorageUtils::getNodeGroupIdxAndOffsetInChunk(offset - startOffset);
|
|
47
|
-
auto* nodeGroup = nodeGroups.getNodeGroup(nodeGroupIdx);
|
|
53
|
+
auto* nodeGroup = nodeGroups.getNodeGroup(nodeGroupIdx, true /*mayOutOfBound*/);
|
|
54
|
+
if (nodeGroup == nullptr || offsetInGroup >= nodeGroup->getNumRows()) {
|
|
55
|
+
throw RuntimeException(
|
|
56
|
+
"Primary-key index contains an invalid local node offset. Please drop and rebuild "
|
|
57
|
+
"the _PK index.");
|
|
58
|
+
}
|
|
48
59
|
if (nodeGroup->isDeleted(transaction, offsetInGroup)) {
|
|
49
60
|
return false;
|
|
50
61
|
}
|
|
@@ -211,24 +211,19 @@ void StorageManager::createNodeTable(NodeTableCatalogEntry* entry, main::ClientC
|
|
|
211
211
|
// Extract Arrow ID from storage string
|
|
212
212
|
std::string arrowId = entry->getStorage().substr(8);
|
|
213
213
|
|
|
214
|
-
// Retrieve Arrow data from registry
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
// Retrieve Arrow data from registry. The returned shared_ptr pins
|
|
215
|
+
// the entry's lifetime past the registry lock, so a concurrent
|
|
216
|
+
// DROP TABLE / unregister cannot free the buffers while we build
|
|
217
|
+
// the table's shallow views below (issue #933).
|
|
218
|
+
auto arrowData = ArrowTableSupport::getArrowData(arrowId);
|
|
219
|
+
if (!arrowData) {
|
|
218
220
|
throw common::RuntimeException("Failed to retrieve Arrow data for ID: " + arrowId);
|
|
219
221
|
}
|
|
220
222
|
|
|
221
|
-
// Create
|
|
222
|
-
|
|
223
|
-
std::vector<ArrowArrayWrapper> arraysCopy;
|
|
224
|
-
arraysCopy.reserve(arrays->size());
|
|
225
|
-
for (const auto& arr : *arrays) {
|
|
226
|
-
arraysCopy.push_back(createShallowCopy(arr));
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Create Arrow-backed node table
|
|
223
|
+
// Create Arrow-backed node table (it keeps its own pin plus
|
|
224
|
+
// shallow non-owning views into the pinned data).
|
|
230
225
|
tables[entry->getTableID()] = std::make_unique<ArrowNodeTable>(this, entry,
|
|
231
|
-
&memoryManager, std::move(
|
|
226
|
+
&memoryManager, std::move(arrowData), arrowId);
|
|
232
227
|
} else {
|
|
233
228
|
throw common::RuntimeException(
|
|
234
229
|
"Unsupported storage option for node table: " + entry->getStorage());
|
|
@@ -262,8 +257,8 @@ void StorageManager::addRelTable(RelGroupCatalogEntry* entry, const RelTableCata
|
|
|
262
257
|
} else if (!entry->getStorage().empty()) {
|
|
263
258
|
if (entry->getStorage().substr(0, 8) == "arrow://") {
|
|
264
259
|
std::string arrowId = entry->getStorage().substr(8);
|
|
265
|
-
|
|
266
|
-
if (!
|
|
260
|
+
auto relData = ArrowTableSupport::getArrowRelData(arrowId);
|
|
261
|
+
if (!relData) {
|
|
267
262
|
throw common::RuntimeException("Failed to retrieve Arrow data for ID: " + arrowId);
|
|
268
263
|
}
|
|
269
264
|
if (!tables.contains(info.nodePair.srcTableID) ||
|
|
@@ -277,23 +272,9 @@ void StorageManager::addRelTable(RelGroupCatalogEntry* entry, const RelTableCata
|
|
|
277
272
|
throw common::RuntimeException(
|
|
278
273
|
"Arrow rel table currently supports only regular node tables");
|
|
279
274
|
}
|
|
280
|
-
ArrowSchemaWrapper schemaCopy = createShallowCopy(relData->schema);
|
|
281
|
-
std::vector<ArrowArrayWrapper> arraysCopy;
|
|
282
|
-
arraysCopy.reserve(relData->arrays.size());
|
|
283
|
-
for (const auto& arr : relData->arrays) {
|
|
284
|
-
arraysCopy.push_back(createShallowCopy(arr));
|
|
285
|
-
}
|
|
286
|
-
ArrowSchemaWrapper indptrSchemaCopy = createShallowCopy(relData->indptrSchema);
|
|
287
|
-
std::vector<ArrowArrayWrapper> indptrArraysCopy;
|
|
288
|
-
indptrArraysCopy.reserve(relData->indptrArrays.size());
|
|
289
|
-
for (const auto& arr : relData->indptrArrays) {
|
|
290
|
-
indptrArraysCopy.push_back(createShallowCopy(arr));
|
|
291
|
-
}
|
|
292
275
|
tables[info.oid] = std::make_unique<ArrowRelTable>(entry, info.nodePair.srcTableID,
|
|
293
276
|
info.nodePair.dstTableID, this, &memoryManager, fromNodeTable, toNodeTable,
|
|
294
|
-
|
|
295
|
-
std::move(indptrSchemaCopy), std::move(indptrArraysCopy), arrowId,
|
|
296
|
-
relData->dstColumnName);
|
|
277
|
+
std::move(relData), arrowId);
|
|
297
278
|
} else {
|
|
298
279
|
throw common::RuntimeException(
|
|
299
280
|
"Unsupported storage option for rel table: " + entry->getStorage());
|
|
@@ -41,11 +41,21 @@ static std::vector<std::optional<common::ArrowLogicalTypeInfo>> resolveColumnLog
|
|
|
41
41
|
|
|
42
42
|
ArrowNodeTable::ArrowNodeTable(const StorageManager* storageManager,
|
|
43
43
|
const catalog::NodeTableCatalogEntry* nodeTableEntry, MemoryManager* memoryManager,
|
|
44
|
-
|
|
44
|
+
std::shared_ptr<ArrowTableData> arrowData, std::string arrowId)
|
|
45
45
|
: ColumnarNodeTableBase{storageManager, nodeTableEntry, memoryManager,
|
|
46
46
|
std::make_unique<ArrowNodeTableScanSharedState>(scanMorselSize)},
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
arrowData{std::move(arrowData)}, totalRows{0}, arrowId{std::move(arrowId)} {
|
|
48
|
+
if (!this->arrowData) {
|
|
49
|
+
throw common::RuntimeException("Arrow data is null");
|
|
50
|
+
}
|
|
51
|
+
// Shallow non-owning views into the pinned registry data. Safe because
|
|
52
|
+
// `arrowData` keeps the underlying Arrow buffers alive for the table's
|
|
53
|
+
// lifetime, even after the registry entry is erased.
|
|
54
|
+
this->schema = createShallowCopy(this->arrowData->schema);
|
|
55
|
+
this->arrays.reserve(this->arrowData->arrays.size());
|
|
56
|
+
for (const auto& array : this->arrowData->arrays) {
|
|
57
|
+
this->arrays.push_back(createShallowCopy(array));
|
|
58
|
+
}
|
|
49
59
|
// Note: release may be nullptr if schema is managed by registry
|
|
50
60
|
if (!this->schema.format) {
|
|
51
61
|
throw common::RuntimeException("Arrow schema format cannot be null");
|
|
@@ -108,14 +108,27 @@ void ArrowRelTableScanState::setToTable(const transaction::Transaction* transact
|
|
|
108
108
|
|
|
109
109
|
ArrowRelTable::ArrowRelTable(catalog::RelGroupCatalogEntry* relGroupEntry, table_id_t fromTableID,
|
|
110
110
|
table_id_t toTableID, const StorageManager* storageManager, MemoryManager* memoryManager,
|
|
111
|
-
const NodeTable* fromNodeTable, const NodeTable* toNodeTable,
|
|
112
|
-
|
|
113
|
-
ArrowSchemaWrapper indptrSchema, std::vector<ArrowArrayWrapper> indptrArrays,
|
|
114
|
-
std::string arrowId, std::string dstColumnName)
|
|
111
|
+
const NodeTable* fromNodeTable, const NodeTable* toNodeTable,
|
|
112
|
+
std::shared_ptr<ArrowRelTableData> arrowData, std::string arrowId)
|
|
115
113
|
: ColumnarRelTableBase{relGroupEntry, fromTableID, toTableID, storageManager, memoryManager},
|
|
116
|
-
fromNodeTable{fromNodeTable}, toNodeTable{toNodeTable},
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
fromNodeTable{fromNodeTable}, toNodeTable{toNodeTable}, arrowData{std::move(arrowData)},
|
|
115
|
+
arrowId{std::move(arrowId)} {
|
|
116
|
+
if (!this->arrowData) {
|
|
117
|
+
throw RuntimeException("Arrow relationship data is null");
|
|
118
|
+
}
|
|
119
|
+
// Shallow non-owning views into the pinned registry data (see ArrowNodeTable).
|
|
120
|
+
this->layout = this->arrowData->layout;
|
|
121
|
+
const std::string dstColumnName = this->arrowData->dstColumnName;
|
|
122
|
+
this->schema = createShallowCopy(this->arrowData->schema);
|
|
123
|
+
this->arrays.reserve(this->arrowData->arrays.size());
|
|
124
|
+
for (const auto& array : this->arrowData->arrays) {
|
|
125
|
+
this->arrays.push_back(createShallowCopy(array));
|
|
126
|
+
}
|
|
127
|
+
this->indptrSchema = createShallowCopy(this->arrowData->indptrSchema);
|
|
128
|
+
this->indptrArrays.reserve(this->arrowData->indptrArrays.size());
|
|
129
|
+
for (const auto& array : this->arrowData->indptrArrays) {
|
|
130
|
+
this->indptrArrays.push_back(createShallowCopy(array));
|
|
131
|
+
}
|
|
119
132
|
if (!this->schema.format) {
|
|
120
133
|
throw RuntimeException("Arrow schema format cannot be null");
|
|
121
134
|
}
|
|
@@ -12,16 +12,18 @@ namespace lbug {
|
|
|
12
12
|
|
|
13
13
|
// Global registry for Arrow table data
|
|
14
14
|
// Memory Management:
|
|
15
|
-
// - Registry owns the Arrow data (ArrowSchemaWrapper/ArrowArrayWrapper with
|
|
16
|
-
//
|
|
15
|
+
// - Registry owns the Arrow data via shared_ptr (ArrowSchemaWrapper/ArrowArrayWrapper with
|
|
16
|
+
// release callbacks). getArrowData()/getArrowRelData() return a shared_ptr copy taken under
|
|
17
|
+
// the mutex, so callers pin the data past the lock scope; erasing the map entry only drops
|
|
18
|
+
// the registry's reference while live tables keep buffers valid (issue #933).
|
|
19
|
+
// - Arrow-backed tables hold a shared_ptr pin plus shallow copies (no release callbacks) and
|
|
20
|
+
// the arrowId
|
|
17
21
|
// - When a table is dropped (via DROP TABLE or unregisterArrowTable), the table's
|
|
18
22
|
// destructor automatically calls unregisterArrowData to clean up the registry entry
|
|
19
23
|
// - The wrappers' destructors call the release callbacks to free the actual Arrow memory
|
|
20
24
|
static std::mutex g_arrowRegistryMutex;
|
|
21
|
-
static std::unordered_map<std::string,
|
|
22
|
-
|
|
23
|
-
g_arrowRegistry;
|
|
24
|
-
static std::unordered_map<std::string, ArrowRelTableData> g_arrowRelRegistry;
|
|
25
|
+
static std::unordered_map<std::string, std::shared_ptr<ArrowTableData>> g_arrowRegistry;
|
|
26
|
+
static std::unordered_map<std::string, std::shared_ptr<ArrowRelTableData>> g_arrowRelRegistry;
|
|
25
27
|
|
|
26
28
|
std::string join(const std::vector<std::string>& strings, const std::string& delimiter) {
|
|
27
29
|
if (strings.empty())
|
|
@@ -58,8 +60,11 @@ std::string ArrowTableSupport::registerArrowData(ArrowSchemaWrapper schema,
|
|
|
58
60
|
static size_t nextId = 0;
|
|
59
61
|
std::string id = "arrow_" + std::to_string(nextId++);
|
|
60
62
|
|
|
61
|
-
// Store in registry
|
|
62
|
-
|
|
63
|
+
// Store in registry (shared ownership so live tables can pin the data).
|
|
64
|
+
auto entry = std::make_shared<ArrowTableData>();
|
|
65
|
+
entry->schema = std::move(schema);
|
|
66
|
+
entry->arrays = std::move(arrays);
|
|
67
|
+
g_arrowRegistry[id] = std::move(entry);
|
|
63
68
|
|
|
64
69
|
return id;
|
|
65
70
|
}
|
|
@@ -69,34 +74,33 @@ std::string ArrowTableSupport::registerArrowRelData(ArrowRelTableData data) {
|
|
|
69
74
|
|
|
70
75
|
static size_t nextRelId = 0;
|
|
71
76
|
std::string id = "arrow_rel_" + std::to_string(nextRelId++);
|
|
72
|
-
|
|
77
|
+
auto entry = std::make_shared<ArrowRelTableData>(std::move(data));
|
|
78
|
+
g_arrowRelRegistry[id] = std::move(entry);
|
|
73
79
|
return id;
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
|
|
77
|
-
std::vector<ArrowArrayWrapper>*& arrays) {
|
|
82
|
+
std::shared_ptr<ArrowTableData> ArrowTableSupport::getArrowData(const std::string& id) {
|
|
78
83
|
std::lock_guard<std::mutex> lock(g_arrowRegistryMutex);
|
|
79
84
|
|
|
80
85
|
auto it = g_arrowRegistry.find(id);
|
|
81
86
|
if (it == g_arrowRegistry.end()) {
|
|
82
|
-
return
|
|
87
|
+
return nullptr;
|
|
83
88
|
}
|
|
84
89
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return
|
|
90
|
+
// Copy the shared_ptr under the lock: the caller pins the data's lifetime
|
|
91
|
+
// past the lock scope, so a concurrent unregisterArrowData() (erase)
|
|
92
|
+
// cannot free the buffers out from under it.
|
|
93
|
+
return it->second;
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
|
|
96
|
+
std::shared_ptr<ArrowRelTableData> ArrowTableSupport::getArrowRelData(const std::string& id) {
|
|
92
97
|
std::lock_guard<std::mutex> lock(g_arrowRegistryMutex);
|
|
93
98
|
|
|
94
99
|
auto it = g_arrowRelRegistry.find(id);
|
|
95
100
|
if (it == g_arrowRelRegistry.end()) {
|
|
96
|
-
return
|
|
101
|
+
return nullptr;
|
|
97
102
|
}
|
|
98
|
-
|
|
99
|
-
return true;
|
|
103
|
+
return it->second;
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
void ArrowTableSupport::unregisterArrowData(const std::string& id) {
|
|
@@ -877,6 +877,9 @@ bool NodeTable::isVisibleNoLock(const Transaction* transaction, offset_t offset)
|
|
|
877
877
|
return false;
|
|
878
878
|
}
|
|
879
879
|
const auto* nodeGroup = getNodeGroupNoLock(nodeGroupIdx);
|
|
880
|
+
if (nodeGroup == nullptr) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
880
883
|
return nodeGroup->isVisibleNoLock(transaction, offsetInGroup);
|
|
881
884
|
}
|
|
882
885
|
|
|
@@ -928,7 +931,32 @@ bool NodeTable::lookupPK(const Transaction* transaction, ValueVector* keyVector,
|
|
|
928
931
|
}
|
|
929
932
|
if (auto* pkIndex = tryGetPrimaryKeyIndex()) {
|
|
930
933
|
return pkIndex->lookupPrimaryKey(transaction, keyVector, vectorPos, result,
|
|
931
|
-
[&](offset_t offset) {
|
|
934
|
+
[&](offset_t offset) {
|
|
935
|
+
// A stale/corrupt PK index entry must not reach isVisibleNoLock(), where an
|
|
936
|
+
// invalid node offset could otherwise be dereferenced in a release build. Keep
|
|
937
|
+
// this validation O(1) on the normal lookup path and fail the transaction instead
|
|
938
|
+
// of treating a corrupt entry as a missing key.
|
|
939
|
+
if (offset == INVALID_OFFSET) {
|
|
940
|
+
throw RuntimeException(
|
|
941
|
+
"Primary-key index contains an invalid node offset. Please drop and "
|
|
942
|
+
"rebuild the _PK index.");
|
|
943
|
+
}
|
|
944
|
+
const auto nodeGroupIdx = StorageUtils::getNodeGroupIdx(offset);
|
|
945
|
+
if (nodeGroupIdx >= nodeGroups->getNumNodeGroupsNoLock()) {
|
|
946
|
+
throw RuntimeException(
|
|
947
|
+
"Primary-key index points to a missing node group. Please drop and "
|
|
948
|
+
"rebuild the _PK index.");
|
|
949
|
+
}
|
|
950
|
+
const auto* nodeGroup = getNodeGroupNoLock(nodeGroupIdx);
|
|
951
|
+
const auto offsetInGroup =
|
|
952
|
+
offset - StorageUtils::getStartOffsetOfNodeGroup(nodeGroupIdx);
|
|
953
|
+
if (nodeGroup == nullptr || offsetInGroup >= nodeGroup->getNumRows()) {
|
|
954
|
+
throw RuntimeException(
|
|
955
|
+
"Primary-key index contains an out-of-range node offset. Please drop "
|
|
956
|
+
"and rebuild the _PK index.");
|
|
957
|
+
}
|
|
958
|
+
return isVisibleNoLock(transaction, offset);
|
|
959
|
+
});
|
|
932
960
|
}
|
|
933
961
|
auto keyToLookup = keyVector->getAsValue(vectorPos);
|
|
934
962
|
ColumnPredicateSet predicateSet;
|
sdist/ladybug.egg-info/PKG-INFO
CHANGED
sdist/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ description = "Highly scalable, extremely fast, easy-to-use embeddable graph dat
|
|
|
4
4
|
readme = "README.md"
|
|
5
5
|
license = { text = "MIT" }
|
|
6
6
|
keywords = ["graph", "database"]
|
|
7
|
-
version = "0.20.2.
|
|
7
|
+
version = "0.20.2.dev20260908"
|
|
8
8
|
requires-python = ">=3.10,<3.15"
|
|
9
9
|
|
|
10
10
|
[project.urls]
|