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 CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ladybug
3
- Version: 0.20.2.dev20260908
3
+ Version: 0.21.0.dev20260909
4
4
  Summary: Highly scalable, extremely fast, easy-to-use embeddable graph database
5
5
  Home-page: https://github.com/LadybugDB/ladybug
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  cmake_minimum_required(VERSION 3.15)
2
2
 
3
- project(Lbug VERSION 0.20.2.20260908 LANGUAGES CXX C)
3
+ project(Lbug VERSION 0.21.0.20260909 LANGUAGES CXX C)
4
4
 
5
5
  option(SINGLE_THREADED "Single-threaded mode" FALSE)
6
6
  if(SINGLE_THREADED)
@@ -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
- return false;
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
- return false;
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;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ladybug
3
- Version: 0.20.2.dev20260908
3
+ Version: 0.21.0.dev20260909
4
4
  Summary: Highly scalable, extremely fast, easy-to-use embeddable graph database
5
5
  Home-page: https://github.com/LadybugDB/ladybug
6
6
  License: MIT
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.dev20260908"
7
+ version = "0.21.0.dev20260909"
8
8
  requires-python = ">=3.10,<3.15"
9
9
 
10
10
  [project.urls]