ladybug 0.20.2.dev20260908__tar.gz → 0.21.0.dev20260909__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/storage/file_handle.h +13 -0
- sdist/ladybug-source/src/optimizer/count_rel_table_optimizer.cpp +30 -11
- sdist/ladybug-source/src/storage/disk_array.cpp +8 -0
- sdist/ladybug-source/src/storage/disk_array_collection.cpp +9 -0
- sdist/ladybug-source/src/storage/table/column_chunk_data.cpp +11 -0
- sdist/ladybug-source/src/storage/table/node_table.cpp +10 -29
- sdist/ladybug.egg-info/PKG-INFO +1 -1
- sdist/pyproject.toml +1 -1
sdist/PKG-INFO
CHANGED
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
#include "common/concurrent_vector.h"
|
|
13
13
|
#include "common/constants.h"
|
|
14
14
|
#include "common/copy_constructors.h"
|
|
15
|
+
#include "common/exception/runtime.h"
|
|
15
16
|
#include "common/file_system/file_info.h"
|
|
16
17
|
#include "common/types/types.h"
|
|
17
18
|
#include "storage/buffer_manager/page_state.h"
|
|
18
19
|
#include "storage/buffer_manager/vm_region.h"
|
|
19
20
|
#include "storage/enums/page_read_policy.h"
|
|
20
21
|
#include "storage/page_manager.h"
|
|
22
|
+
#include <format>
|
|
21
23
|
|
|
22
24
|
namespace lbug {
|
|
23
25
|
namespace main {
|
|
@@ -91,6 +93,17 @@ public:
|
|
|
91
93
|
void readPageFromDisk(uint8_t* frame, common::page_idx_t pageIdx) const {
|
|
92
94
|
DASSERT(!isInMemoryMode());
|
|
93
95
|
DASSERT(pageIdx < numPages);
|
|
96
|
+
// Fail fast with an actionable error instead of issuing a wild pread far
|
|
97
|
+
// beyond EOF (issue #843: a corrupted PIP chain turned into
|
|
98
|
+
// "Cannot read from file ... position: 4727899947008"). DASSERT alone is
|
|
99
|
+
// stripped in release builds.
|
|
100
|
+
if (pageIdx >= numPages) {
|
|
101
|
+
const auto numPagesSnapshot = numPages.load();
|
|
102
|
+
throw common::RuntimeException(
|
|
103
|
+
std::format("Cannot read page {} from disk: out of bounds for file with {} pages. "
|
|
104
|
+
"The database file may be corrupted.",
|
|
105
|
+
pageIdx, numPagesSnapshot));
|
|
106
|
+
}
|
|
94
107
|
fileInfo->readFromFile(frame, getPageSize(), pageIdx * getPageSize());
|
|
95
108
|
}
|
|
96
109
|
void writePageToFile(const uint8_t* buffer, common::page_idx_t pageIdx) {
|
|
@@ -63,6 +63,8 @@ std::shared_ptr<LogicalOperator> CountRelTableOptimizer::visitOperator(
|
|
|
63
63
|
|
|
64
64
|
static LogicalOperator* skipProjections(LogicalOperator* op);
|
|
65
65
|
static bool containsOp(const LogicalOperator* root, const LogicalOperator* target);
|
|
66
|
+
static bool isNativeRelGroupEntry(const RelGroupCatalogEntry* entry);
|
|
67
|
+
static bool isNativeNodeEntry(const NodeTableCatalogEntry* entry);
|
|
66
68
|
|
|
67
69
|
std::shared_ptr<LogicalOperator> CountRelTableOptimizer::tryRewriteAntiEdgeChainCount(
|
|
68
70
|
std::shared_ptr<LogicalOperator> op) {
|
|
@@ -234,7 +236,8 @@ std::shared_ptr<LogicalOperator> CountRelTableOptimizer::tryRewriteAntiEdgeChain
|
|
|
234
236
|
continue;
|
|
235
237
|
}
|
|
236
238
|
auto* relGroupEntry = rel->getEntry(0)->ptrCast<RelGroupCatalogEntry>();
|
|
237
|
-
if (relGroupEntry->getScanFunction().has_value()
|
|
239
|
+
if (relGroupEntry->getScanFunction().has_value() ||
|
|
240
|
+
!isNativeRelGroupEntry(relGroupEntry)) {
|
|
238
241
|
continue;
|
|
239
242
|
}
|
|
240
243
|
auto* extendChild = skipProjections((ext->getChild(0).get()));
|
|
@@ -405,7 +408,8 @@ std::shared_ptr<LogicalOperator> CountRelTableOptimizer::tryRewriteAntiEdgeChain
|
|
|
405
408
|
return op;
|
|
406
409
|
}
|
|
407
410
|
auto* relGroupEntry = rel->getEntry(0)->ptrCast<RelGroupCatalogEntry>();
|
|
408
|
-
if (relGroupEntry->getScanFunction().has_value()
|
|
411
|
+
if (relGroupEntry->getScanFunction().has_value() ||
|
|
412
|
+
!isNativeRelGroupEntry(relGroupEntry)) {
|
|
409
413
|
return op;
|
|
410
414
|
}
|
|
411
415
|
if (ext->getDirection() == ExtendDirection::BOTH) {
|
|
@@ -449,6 +453,14 @@ std::shared_ptr<LogicalOperator> CountRelTableOptimizer::tryRewriteAntiEdgeChain
|
|
|
449
453
|
toTableID != toNode->getTableIDs()[0]) {
|
|
450
454
|
return op;
|
|
451
455
|
}
|
|
456
|
+
// Parity with the extend-chain path, which gates every node it touches. The
|
|
457
|
+
// binder refuses to mix icebug-disk tables with native ones, but there is no
|
|
458
|
+
// equivalent guard for Arrow-backed tables, so the rel group's format alone
|
|
459
|
+
// cannot vouch for its endpoints.
|
|
460
|
+
if (!isNativeNodeEntry(fromNode->getEntry(0)->ptrCast<NodeTableCatalogEntry>()) ||
|
|
461
|
+
!isNativeNodeEntry(toNode->getEntry(0)->ptrCast<NodeTableCatalogEntry>())) {
|
|
462
|
+
return op;
|
|
463
|
+
}
|
|
452
464
|
hop.relScans.push_back({relGroupEntry->getRelEntryInfos()[0].oid, scanDirection,
|
|
453
465
|
fromTableID, toTableID, relGroupEntry->getName()});
|
|
454
466
|
matched = true;
|
|
@@ -468,6 +480,11 @@ std::shared_ptr<LogicalOperator> CountRelTableOptimizer::tryRewriteAntiEdgeChain
|
|
|
468
480
|
if (antiNodeA->getTableIDs()[0] != midTableID || antiNodeB->getTableIDs()[0] != midTableID) {
|
|
469
481
|
return op;
|
|
470
482
|
}
|
|
483
|
+
// n0, n1 and n2 all bind midTableID, so gating that one entry covers every node the
|
|
484
|
+
// operator's node-group arithmetic touches.
|
|
485
|
+
if (!isNativeNodeEntry(midNode->getEntry(0)->ptrCast<NodeTableCatalogEntry>())) {
|
|
486
|
+
return op;
|
|
487
|
+
}
|
|
471
488
|
|
|
472
489
|
std::vector<common::table_id_t> antiRelTableIDs;
|
|
473
490
|
antiRelTableIDs.push_back(antiRelEntry->getRelEntryInfos()[0].oid);
|
|
@@ -570,15 +587,6 @@ std::shared_ptr<LogicalOperator> CountRelTableOptimizer::tryRewriteExtendChainCo
|
|
|
570
587
|
|
|
571
588
|
// Validate the extends form a simple path over single-table nodes with single-entry,
|
|
572
589
|
// storage-backed rel groups.
|
|
573
|
-
// The fast path below iterates the committed node-group grid of native node tables and
|
|
574
|
-
// reads the CSR of native rel tables. Arrow-backed and icebug-disk tables have neither,
|
|
575
|
-
// so they must be left to the regular plan.
|
|
576
|
-
auto isNativeRelGroupEntry = [](const RelGroupCatalogEntry* entry) {
|
|
577
|
-
return entry->getStorage().empty() && entry->getStorageFormat() == StorageFormat::NONE;
|
|
578
|
-
};
|
|
579
|
-
auto isNativeNodeEntry = [](const NodeTableCatalogEntry* entry) {
|
|
580
|
-
return entry->getStorage().empty() && entry->getStorageFormat() == StorageFormat::NONE;
|
|
581
|
-
};
|
|
582
590
|
struct ChainEdge {
|
|
583
591
|
std::string u;
|
|
584
592
|
std::string v;
|
|
@@ -1143,6 +1151,17 @@ std::shared_ptr<LogicalOperator> CountRelTableOptimizer::visitAggregateReplace(
|
|
|
1143
1151
|
return projection;
|
|
1144
1152
|
}
|
|
1145
1153
|
|
|
1154
|
+
// The count-chain fast paths iterate the committed node-group grid of native node tables and
|
|
1155
|
+
// read the CSR of native rel tables. Arrow-backed and icebug-disk tables have neither, so they
|
|
1156
|
+
// must be left to the regular plan.
|
|
1157
|
+
static bool isNativeRelGroupEntry(const RelGroupCatalogEntry* entry) {
|
|
1158
|
+
return entry->getStorage().empty() && entry->getStorageFormat() == StorageFormat::NONE;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
static bool isNativeNodeEntry(const NodeTableCatalogEntry* entry) {
|
|
1162
|
+
return entry->getStorage().empty() && entry->getStorageFormat() == StorageFormat::NONE;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1146
1165
|
static LogicalOperator* skipProjections(LogicalOperator* op) {
|
|
1147
1166
|
while (op->getOperatorType() == LogicalOperatorType::PROJECTION) {
|
|
1148
1167
|
op = op->getChild(0).get();
|
|
@@ -33,6 +33,14 @@ PageStorageInfo::PageStorageInfo(uint64_t elementSize)
|
|
|
33
33
|
|
|
34
34
|
PIPWrapper::PIPWrapper(const FileHandle& fileHandle, page_idx_t pipPageIdx)
|
|
35
35
|
: pipPageIdx(pipPageIdx) {
|
|
36
|
+
// Validate before issuing the read so a corrupted PIP pointer fails fast with
|
|
37
|
+
// the offending index instead of a wild pread beyond EOF (issue #843).
|
|
38
|
+
if (pipPageIdx >= fileHandle.getNumPages()) {
|
|
39
|
+
throw RuntimeException(std::format("Cannot read PIP page {} from disk: out of bounds "
|
|
40
|
+
"for file with {} pages. The database file may be "
|
|
41
|
+
"corrupted.",
|
|
42
|
+
pipPageIdx, fileHandle.getNumPages()));
|
|
43
|
+
}
|
|
36
44
|
fileHandle.readPageFromDisk(reinterpret_cast<uint8_t*>(&pipContents), pipPageIdx);
|
|
37
45
|
}
|
|
38
46
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
#include "common/types/types.h"
|
|
8
8
|
#include "storage/file_handle.h"
|
|
9
9
|
#include "storage/shadow_utils.h"
|
|
10
|
+
#include <format>
|
|
10
11
|
|
|
11
12
|
using namespace lbug::common;
|
|
12
13
|
|
|
@@ -29,6 +30,14 @@ DiskArrayCollection::DiskArrayCollection(FileHandle& fileHandle, ShadowFile& sha
|
|
|
29
30
|
// Read headers from disk (no external state in lambda: optimistic read may run multiple times)
|
|
30
31
|
page_idx_t headerPageIdx = firstHeaderPage;
|
|
31
32
|
do {
|
|
33
|
+
// Fail fast on a corrupted nextHeaderPage link instead of issuing a wild
|
|
34
|
+
// read far beyond EOF (issue #843).
|
|
35
|
+
if (headerPageIdx >= fileHandle.getNumPages()) {
|
|
36
|
+
throw RuntimeException(std::format("Cannot read header page {} from disk: out of "
|
|
37
|
+
"bounds for file with {} pages. The database file "
|
|
38
|
+
"may be corrupted.",
|
|
39
|
+
headerPageIdx, fileHandle.getNumPages()));
|
|
40
|
+
}
|
|
32
41
|
std::unique_ptr<HeaderPage> headerPage;
|
|
33
42
|
page_idx_t nextHeaderPageIdx = INVALID_PAGE_IDX;
|
|
34
43
|
fileHandle.optimisticReadPage(headerPageIdx, [&](auto* frame) {
|
|
@@ -375,7 +375,15 @@ void ColumnChunkData::initializeScanState(SegmentState& state, const Column* col
|
|
|
375
375
|
|
|
376
376
|
void ColumnChunkData::scan(ValueVector& output, offset_t offset, length_t length,
|
|
377
377
|
sel_t posInOutputVector) const {
|
|
378
|
+
// Scanning zero rows must not touch data buffers: empty (e.g. freshly created)
|
|
379
|
+
// chunks may hold null/zero-size buffers, and memcpy with a null source is UB
|
|
380
|
+
// even when the byte count is 0 (issue #840: SIGSEGV in memmove under
|
|
381
|
+
// NodeTableScanState::scanNext on empty tables via a parallel scan morsel).
|
|
382
|
+
if (length == 0) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
378
385
|
DASSERT(offset + length <= numValues);
|
|
386
|
+
DASSERT(getData() != nullptr);
|
|
379
387
|
if (nullData) {
|
|
380
388
|
nullData->scan(output, offset, length, posInOutputVector);
|
|
381
389
|
}
|
|
@@ -785,6 +793,9 @@ std::unique_ptr<NullChunkData> NullChunkData::deserialize(MemoryManager& memoryM
|
|
|
785
793
|
|
|
786
794
|
void NullChunkData::scan(ValueVector& output, offset_t offset, length_t length,
|
|
787
795
|
sel_t posInOutputVector) const {
|
|
796
|
+
if (length == 0) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
788
799
|
output.setNullFromBits(getNullMask().getData(), offset, posInOutputVector, length);
|
|
789
800
|
}
|
|
790
801
|
|
|
@@ -867,6 +867,10 @@ bool NodeTable::isVisible(const Transaction* transaction, offset_t offset) const
|
|
|
867
867
|
}
|
|
868
868
|
|
|
869
869
|
bool NodeTable::isVisibleNoLock(const Transaction* transaction, offset_t offset) const {
|
|
870
|
+
if (offset == INVALID_OFFSET) {
|
|
871
|
+
throw RuntimeException(
|
|
872
|
+
"Index contains an invalid node offset. Please drop and rebuild the index.");
|
|
873
|
+
}
|
|
870
874
|
if (transaction && transaction->isUnCommitted(tableID, offset)) {
|
|
871
875
|
const auto localTable = transaction->getLocalStorage()->getLocalTable(tableID);
|
|
872
876
|
DASSERT(localTable);
|
|
@@ -874,11 +878,13 @@ bool NodeTable::isVisibleNoLock(const Transaction* transaction, offset_t offset)
|
|
|
874
878
|
}
|
|
875
879
|
auto [nodeGroupIdx, offsetInGroup] = StorageUtils::getNodeGroupIdxAndOffsetInChunk(offset);
|
|
876
880
|
if (nodeGroupIdx >= nodeGroups->getNumNodeGroupsNoLock()) {
|
|
877
|
-
|
|
881
|
+
throw RuntimeException(
|
|
882
|
+
"Index contains an invalid node offset. Please drop and rebuild the index.");
|
|
878
883
|
}
|
|
879
884
|
const auto* nodeGroup = getNodeGroupNoLock(nodeGroupIdx);
|
|
880
|
-
if (nodeGroup == nullptr) {
|
|
881
|
-
|
|
885
|
+
if (nodeGroup == nullptr || offsetInGroup >= nodeGroup->getNumRows()) {
|
|
886
|
+
throw RuntimeException(
|
|
887
|
+
"Index contains an invalid node offset. Please drop and rebuild the index.");
|
|
882
888
|
}
|
|
883
889
|
return nodeGroup->isVisibleNoLock(transaction, offsetInGroup);
|
|
884
890
|
}
|
|
@@ -931,32 +937,7 @@ bool NodeTable::lookupPK(const Transaction* transaction, ValueVector* keyVector,
|
|
|
931
937
|
}
|
|
932
938
|
if (auto* pkIndex = tryGetPrimaryKeyIndex()) {
|
|
933
939
|
return pkIndex->lookupPrimaryKey(transaction, keyVector, vectorPos, result,
|
|
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
|
-
});
|
|
940
|
+
[&](offset_t offset) { return isVisibleNoLock(transaction, offset); });
|
|
960
941
|
}
|
|
961
942
|
auto keyToLookup = keyVector->getAsValue(vectorPos);
|
|
962
943
|
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.
|
|
7
|
+
version = "0.21.0.dev20260909"
|
|
8
8
|
requires-python = ">=3.10,<3.15"
|
|
9
9
|
|
|
10
10
|
[project.urls]
|