Skip to content

feat: implement bulk_load API for all ART tree modes (#636)#864

Open
thompsonbry wants to merge 22 commits into
unodb-dev:masterfrom
thompsonbry:feat/bulk-load-636
Open

feat: implement bulk_load API for all ART tree modes (#636)#864
thompsonbry wants to merge 22 commits into
unodb-dev:masterfrom
thompsonbry:feat/bulk-load-636

Conversation

@thompsonbry

@thompsonbry thompsonbry commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements bulk_load for all three ART tree modes (db, mutex_db, olc_db) with std::execution_policy-based parallel execution.

API

tree.bulk_load(first, last);                      // sequential (default)
tree.bulk_load(std::execution::par, first, last); // parallel

Performance (96-core, 1M random uint64 keys, Release)

Mode seq par Speedup
db 11M keys/s 67M keys/s
mutex_db 11M keys/s 68M keys/s
olc_db 11M keys/s 65M keys/s

Design

  • Builds optimal-size inodes directly from sorted input (no grow events)
  • Parallel path dispatches one std::async per root-level partition
  • Shared algorithm in art_bulk_load_detail.hpp (single copy for all modes)
  • create_bulk static factories on inode_4/16/48/256

What was tested

  • 33 new tests: boundary, structural (node counts), operational (insert/remove/scan after bulk_load)
  • All 4 build configs: Debug±Stats, Release±Stats
  • clang-format-21, clang-tidy-21, cppcheck-2.21
  • UBSan, ASan, TSan (all clean)
  • Benchmarks: BM_BulkLoad_{seq,par} × {db, mutex_db, olc_db}

Blocked

  • Recursive parallelism for skewed distributions (future enhancement, tracked)

Summary by CodeRabbit

  • New Features

    • Added bulk loading for ART databases, with both sequential and parallel options.
    • Added examples and benchmarks showing bulk load usage and performance.
    • Added support for bulk loading to mutex-protected and concurrent database variants.
  • Bug Fixes

    • Bulk loading now validates empty-tree preconditions and preserves data on failure.
    • Improved handling of large shared prefixes and different internal node shapes during bulk loading.
  • Tests

    • Added extensive coverage for correctness, structure, out-of-memory behavior, and concurrent read scenarios.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds a bulk_load API to unodb::db, unodb::mutex_db, and unodb::olc_db, backed by a shared detail::bulk_load_impl template that partitions sorted key ranges and recursively constructs ART subtrees using new create_bulk inode factories, supporting both sequential and fork-based parallel execution, with stats accounting, tests, an example, and a benchmark.

Changes

ART Bulk-load Feature

Layer / File(s) Summary
Public APIs and wiring
art.hpp, mutex_art.hpp, olc_art.hpp, node_type.hpp
Adds bulk_load overloads (fork/max_tasks and sequential) to db, mutex_db, olc_db; adds build_result, bulk_subtree_guard, key_byte_at, merge_bulk_load_stats, friend declarations, and thread-local bulk_load_stats_accumulator wiring for stats.
Bulk inode construction
art_internal_impl.hpp
Adds bulk_load_tag constructors and create_bulk(...) factories for basic_inode_4/16/48/256, populating children/keys/value masks from a std::span<bulk_child>.
Shared bulk-load algorithm
art_bulk_load_detail.hpp
Implements detail::bulk_load_impl with prefix computation, byte-partitioning, recursive subtree_builder, leaf elimination, prefix-chain wrapping, and sequential/parallel (fork-based) root assembly with exception draining.
Tests and target registration
test/test_art_bulk_load*.cpp, test/test_art_oom.cpp, test/CMakeLists.txt
Adds three new test binaries covering empty/boundary/growth cases, operational/parallel/error paths, structural node-shape checks, and OOM exception-safety scenarios.
Example and benchmark surfaces
examples/example_bulk_load.cpp, examples/CMakeLists.txt, benchmark/micro_benchmark_bulk_load.cpp, benchmark/CMakeLists.txt
Adds a demo comparing sequential/parallel bulk-load usage and a benchmark suite comparing bulk_load vs insert across key types, scaling, memory, and scan scenarios.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant db as db/olc_db/mutex_db
  participant bulk_load_impl
  participant subtree_builder
  participant inode as create_bulk

  Caller->>db: bulk_load(fork, max_tasks, first, last)
  db->>db: check tree empty / range non-empty
  db->>bulk_load_impl: bulk_load_impl(self, fork, max_tasks, first, last)
  bulk_load_impl->>bulk_load_impl: partition_by_byte(range, dispatch_depth)
  alt fork provided
    bulk_load_impl->>subtree_builder: fork parallel tasks per partition
  else sequential
    bulk_load_impl->>subtree_builder: build recursively inline
  end
  subtree_builder->>inode: create_bulk(children, value_mask)
  inode-->>subtree_builder: node_ptr
  subtree_builder-->>bulk_load_impl: build_result
  bulk_load_impl->>db: assign self.root
  bulk_load_impl-->>Caller: return (or clear() on exception)
Loading

Possibly related PRs

  • unodb-dev/unodb#618: Adds the UNODB_DETAIL_WITH_STATS CMake switch that gates the bulk-load statistics code paths introduced here.
  • unodb-dev/unodb#845: Prior changes to basic_inode_* constructors/value-in-slot handling in art_internal_impl.hpp that the bulk create_bulk factories build on.

Suggested reviewers: laurynas-biveinis

Poem

A rabbit hops with sorted keys in paw,
Splitting bytes by ART's own law,
I4 to I256, subtrees take flight,
Forked or single-threaded, all builds just right,
clear() if it stumbles, then tries once more —
🐇 bulk-loaded and ready, straight to the core.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a bulk_load API across all ART tree modes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@art_bulk_load_detail.hpp`:
- Around line 182-245: The bulk-inode assembly logic is duplicated between the
root and recursive paths, so refactor the repeated child-count branching, vmask
construction, stats accounting, and create_bulk() calls into a shared helper.
Use a single helper around make_inode / the bulk assembly code to build inode_4,
inode_16, inode_48, and inode_256 consistently, then call it from both paths so
they do not drift. Keep the existing art_policy::can_eliminate_leaf handling and
UNODB_DETAIL_WITH_STATS accounting in that helper.
- Around line 172-175: The prefix chain depth logic is hard-coding the capacity
as 8 instead of using the existing prefix_cap value, so update the
chain_consumed calculation in both the sequential and parallel root assembly
paths to derive consumption from prefix_cap. Make the same change wherever this
pattern appears in art_bulk_load_detail.hpp, including the matching logic around
the other chain assembly block, so key_prefix_capacity changes do not produce
incorrect prefix lengths.
- Around line 18-19: The bulk-load path in art_bulk_load_detail.hpp can read
past the end of a key view when duplicate or terminal keys make
common_prefix_length() equal kv.size(), causing dispatch_depth to reach the end
before indexing kv[dd] in the bulk-load logic. Update the bulk-loading flow
around the detail routine that computes dispatch_depth and accesses kv[dd] to
reject duplicate/terminal keys up front or to handle terminal leaves explicitly
before any indexing occurs, using the existing bulk-load helpers rather than
assuming all inputs are strictly non-terminal and unique.
- Around line 288-310: The async subtree build loop in the builder logic is not
exception-safe, so a throw from futures[i].get() or later in the guard/children
assembly can leak raw node memory held by build_result. Update the future result
handling in this section to catch exceptions, drain any remaining std::future
results into a temporary guard collection before rethrowing, and only then
proceed with constructing bulk_child_t and bulk_subtree_guard state. Use the
existing builder lambda, build_result_t, guard_t, and children assembly as the
anchor points for the fix.

In `@art_internal_impl.hpp`:
- Line 3063: Remove the extra trailing blank lines at the end of each
create_bulk block so the linter stops flagging redundant whitespace; check the
create_bulk-related sections in art_internal_impl.hpp around the identified
spots and trim the final empty line(s) without changing any logic.

In `@benchmark/CMakeLists.txt`:
- Line 13: The *_quick_arg benchmark filter variables in
benchmark/CMakeLists.txt are using escaped quotes, so add_custom_target passes
the quotes literally and Google Benchmark never matches the intended subset.
Update the relevant set(...) definitions, including
micro_benchmark_bulk_load_quick_arg and the other *_quick_arg symbols in this
file, to use unquoted filter values like --benchmark_filter=/1024 so the quick
benchmarks actually run the smaller selection.

In `@benchmark/micro_benchmark_bulk_load.cpp`:
- Around line 28-30: The type alias for kv_mutex_db is inconsistent with kv_db
and kv_olc_db, which already use unodb::benchmark aliases. In
micro_benchmark_bulk_load.cpp, update the kv_mutex_db declaration to use the
existing unodb::benchmark::kv_mutex_db alias if it exists, so the alias style
matches the surrounding declarations; if that alias is not available, leave it
as-is.

In `@examples/example_bulk_load.cpp`:
- Around line 7-26: The example in make_sorted_data uses std::array without
including its defining header, so add the missing <array> include alongside the
other standard headers at the top of example_bulk_load.cpp. Keep the fix
localized to the includes so the dependency on std::array is explicit and no
longer relies on transitive includes from art.hpp or art_common.hpp.

In `@test/CMakeLists.txt`:
- Around line 62-64: The new bulk-load targets are registered via
add_db_test_target, but they are not included in the aggregate coverage and
valgrind lists, so they can bypass those checks. Update the test/CMakeLists.txt
aggregates that define tests_for_coverage and valgrind_tests to include
test_art_bulk_load, test_art_bulk_load_structural, and test_art_bulk_load_ops,
matching the existing pattern used for other db tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d3c0f1c0-e50e-4793-a31c-da32e2ef4694

📥 Commits

Reviewing files that changed from the base of the PR and between f52f55b and 9532faf.

📒 Files selected for processing (13)
  • art.hpp
  • art_bulk_load_detail.hpp
  • art_internal_impl.hpp
  • benchmark/CMakeLists.txt
  • benchmark/micro_benchmark_bulk_load.cpp
  • examples/CMakeLists.txt
  • examples/example_bulk_load.cpp
  • mutex_art.hpp
  • olc_art.hpp
  • test/CMakeLists.txt
  • test/test_art_bulk_load.cpp
  • test/test_art_bulk_load_ops.cpp
  • test/test_art_bulk_load_structural.cpp

Comment thread art_bulk_load_detail.hpp Outdated
Comment thread art_bulk_load_detail.hpp Outdated
Comment thread art_bulk_load_detail.hpp Outdated
Comment thread art_bulk_load_detail.hpp Outdated
Comment thread art_internal_impl.hpp Outdated
Comment thread benchmark/CMakeLists.txt
Comment thread benchmark/micro_benchmark_bulk_load.cpp
Comment thread examples/example_bulk_load.cpp
Comment thread test/CMakeLists.txt

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
art.hpp (1)

352-362: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the stale db::bulk_load execution-policy docs.

Line 357 says the policy is ignored for db, but Line 2736 forwards it into detail::bulk_load_impl, and the PR objective/tests expect std::execution::par to enable bulk construction. Update the comment so users don’t assume the parallel overload is a no-op.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@art.hpp` around lines 352 - 362, The db::bulk_load execution-policy comment
is stale and incorrectly says the policy is ignored, even though bulk_load
forwards the policy into detail::bulk_load_impl and parallel construction is
expected. Update the documentation on bulk_load to describe that the execution
policy is used, and remove the “ignored for db” wording so the behavior matches
the implementation and std::execution::par support.
test/test_art_bulk_load_ops.cpp (1)

133-150: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This test does not verify “no growth events.”

The body only checks that all keys are readable after bulk_load. A regression that reintroduced incremental inode growth would still pass here. Either assert an observable no-growth invariant under stats, or rename/drop this test so it does not claim coverage it does not provide.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_art_bulk_load_ops.cpp` around lines 133 - 150, The
BulkLoadOps.NoGrowthEvents test currently only verifies that keys can be read
after bulk_load and does not actually assert the no-growth invariant it claims.
Update the test to check an observable stats-based condition from the bulk_load
path (for example via existing growth/shrink counters or inode stats), using the
relevant bulk_load and stats APIs, or else rename/remove the test so its name
and comments do not overstate coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/test_art_oom.cpp`:
- Around line 672-676: The OOM regression check is too weak because `empty()`
only verifies the root is unreachable, not that all internal state and
allocations were fully cleaned up after `bulk_load` fails. In the
`std::bad_alloc` catch block, update the assertion to use the stronger
full-empty verification for `test_db` so this test also catches leaked nodes or
stale memory/stat counters, keeping the strong-guarantee check tied to the
`bulk_load` failure path.

---

Outside diff comments:
In `@art.hpp`:
- Around line 352-362: The db::bulk_load execution-policy comment is stale and
incorrectly says the policy is ignored, even though bulk_load forwards the
policy into detail::bulk_load_impl and parallel construction is expected. Update
the documentation on bulk_load to describe that the execution policy is used,
and remove the “ignored for db” wording so the behavior matches the
implementation and std::execution::par support.

In `@test/test_art_bulk_load_ops.cpp`:
- Around line 133-150: The BulkLoadOps.NoGrowthEvents test currently only
verifies that keys can be read after bulk_load and does not actually assert the
no-growth invariant it claims. Update the test to check an observable
stats-based condition from the bulk_load path (for example via existing
growth/shrink counters or inode stats), using the relevant bulk_load and stats
APIs, or else rename/remove the test so its name and comments do not overstate
coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bf7fa448-aabe-4faf-9d22-0661c79a5745

📥 Commits

Reviewing files that changed from the base of the PR and between 9532faf and 31bfbca.

📒 Files selected for processing (6)
  • art.hpp
  • benchmark/micro_benchmark_bulk_load.cpp
  • olc_art.hpp
  • test/CMakeLists.txt
  • test/test_art_bulk_load_ops.cpp
  • test/test_art_oom.cpp

Comment thread test/test_art_oom.cpp
Bryan Thompson added 6 commits June 30, 2026 17:03
Add bulk_load(first, last) and bulk_load(std::execution::par, first, last)
to db, mutex_db, and olc_db. Builds optimal-size internal nodes directly
from sorted input, avoiding incremental insert overhead.

Parallel execution dispatches one std::async task per root-level partition;
subtree construction within each partition is sequential.

API:
  tree.bulk_load(first, last);                    // sequential
  tree.bulk_load(std::execution::par, first, last); // parallel

Performance (96-core, 1M random uint64 keys, Release):
  db:       seq 11M/s, par 67M/s (6x)
  mutex_db: seq 11M/s, par 68M/s (6x)
  olc_db:   seq 11M/s, par 65M/s (6x)

Implementation:
- art_bulk_load_detail.hpp: shared recursive algorithm template
- art_internal_impl.hpp: create_bulk factories for inode_4/16/48/256
- art.hpp/olc_art.hpp/mutex_art.hpp: thin public API wrappers

Tests: 33 new tests (boundary, structural, operational) across all modes.
Benchmarks: BM_BulkLoad_seq/par for all tree types vs BM_Insert baseline.
- Add strong exception guarantee (try-catch + clear) to db::bulk_load
  and olc_db::bulk_load so tree is left empty on allocation failure
- Add 5 OOM typed tests (x3 db types = 15 tests) exercising single key,
  inode4, inode16, inode48, and nested inode topologies
- Add T44 OlcDbConcurrentReaders: after parallel bulk_load, spawn 4
  qsbr_threads reading all 10K keys to verify lock initialization
- Link test_art_bulk_load_ops against qsbr_test_utils
- Fix clang-format-21 whitespace in benchmark file
- Add portability_execution.hpp with __cpp_lib_execution detection;
  provides fallback sequential/parallel policy types when <execution>
  is unavailable (Apple libc++, older libstdc++)
- Replace all #include <execution> with portability header
- Fix GCC -Wuseless-cast by using uint64_t loop variable in
  test_art_bulk_load.cpp and DISABLE_GCC_WARNING in structural test
- Fix GCC -Wdangling-else by adding braces around EXPECT_GE/EXPECT_GT
  after bare if in test_art_bulk_load_structural.cpp
- Add UNODB_DETAIL_ASSERT bounds checks in partition_by_byte
- Replace hardcoded 8 with (prefix_cap + 1) in chain_consumed
- Extract make_bulk_inode shared lambda (eliminates duplication)
- Add exception safety for parallel futures drain
- Remove trailing blank lines in inode class closings
- Add missing #include <array> in example_bulk_load.cpp
- Wire bulk-load tests into coverage and valgrind targets
- Strengthen OOM test with memory_use == 0 assertion
The array-based value_bitmask_field wraps elements in
in_critical_section<T>, so direct array assignment fails.
Use element-by-element assignment for inode_48 and inode_256
bulk_load_from_children.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

♻️ Duplicate comments (1)
test/test_art_oom.cpp (1)

672-678: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use the full empty-state check after std::bad_alloc.

empty() plus current memory use is still weaker than the repository’s own empty-tree contract in test/db_test_utils.hpp: stale node counters would pass here. Assert zero node counts too, or route this through the same stronger helper.

Suggested assertion tightening
     } catch (const std::bad_alloc&) {
       unodb::test::allocation_failure_injector::reset();
       // Strong guarantee: tree must be empty after failed bulk_load
       UNODB_ASSERT_TRUE(test_db.empty());
 `#ifdef` UNODB_DETAIL_WITH_STATS
       UNODB_ASSERT_EQ(test_db.get_current_memory_use(), 0);
+      UNODB_ASSERT_THAT(test_db.get_node_counts(),
+                        ::testing::ElementsAre(0U, 0U, 0U, 0U, 0U));
 `#endif`
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_art_oom.cpp` around lines 672 - 678, The `std::bad_alloc` path in
the bulk_load OOM test is only checking `test_db.empty()` and memory use, which
is weaker than the repository’s full empty-tree contract. Update the assertion
in the catch block to use the stronger empty-state verification from
`test/db_test_utils.hpp` or add zero checks for the tree’s node counters
alongside `test_db.empty()` so stale counters cannot slip through; use the
existing `test_db`/`UNODB_ASSERT_*` test helpers to keep the check consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@art_bulk_load_detail.hpp`:
- Around line 306-320: The async launch path in the bulk-load builder can throw
after some futures have already been started, which leaves completed
build_result_t pointers uncollected. In the builder logic that creates the
std::future list, move the guards/children reserve work before launching tasks,
and wrap the std::async launch sequence so any successfully created futures are
drained through the existing get()/bulk_subtree_guard transfer path before
rethrowing. Keep the fix localized around the futures, guards, children, and
builder lambda handling.
- Around line 77-109: The build_prefix_chain lambda currently passes the raw
current child into inode_4::create() and then overwrites current, so an
exception during chain allocation can leak the child or a previously built
inode_4. Update build_prefix_chain() to transfer ownership of current into the
allocation path (for example via a guarded/owning local) until inode_4::create()
succeeds, then release it only after the new node_ptr_t is safely constructed.
Keep the same fix for both inode_4::create() calls and preserve the existing
stats accounting on the success path.
- Around line 311-313: The parallel worker path in the bulk-load builder is
updating shared db stats without synchronization, which races under
std::execution::par. In the code that launches the async work around builder,
make sure the lambdas that reach make_db_leaf_ptr() and build_chain() do not
mutate db<Key, Value>::current_memory_use, node_counts, or growing_inode_counts
unsafely for db/mutex_db; either route these types away from the parallel path
or guard those stats updates consistently, since olc_db already uses atomics but
this path does not.

In `@art_internal_impl.hpp`:
- Around line 3740-3742: The duplicated create_bulk declaration is making the
function signature invalid. In the create_bulk declarations referenced by
art_internal_impl.hpp, remove the extra repeated [[nodiscard]] static auto
create_bulk( line so each declaration appears only once, and apply the same
cleanup to the second create_bulk block as well.

In `@art.hpp`:
- Around line 2769-2774: The value_view upsert path in the update branch is
reusing the original value instead of the callback-mutated result from
fn(local). In the code around upsert_action::update and value_type/value_view
handling, make the reinsertion use the updated local value returned by the
lambda, not v. If local may alias leaf storage, copy it before calling
remove_internal(art_k) so the old leaf can be reclaimed safely, then pass that
copied updated value into insert_internal.
- Around line 363-373: The bulk_load policy handling in art.hpp is forwarding
std::execution::par into bulk_load_impl even though the ART/db allocator
contract says it is not thread-safe, so locate bulk_load and its policy dispatch
around bulk_load_impl and force the db path to use seq instead of async parallel
root partitions. If parallel behavior must remain, make the
allocator/thread-safety contract explicit and enforce it in the bulk_load policy
branch and any mutex_db-related path so concurrent allocations cannot occur.

In `@benchmark/micro_benchmark_bulk_load.cpp`:
- Around line 175-215: BM04 is mislabeled for the `db` and `mutex_db` cases
because `db::bulk_load` ignores the execution policy and `mutex_db::bulk_load`
just forwards it, so `BM_BulkLoad_par_db` and `BM_BulkLoad_par_mutex` are not
actually measuring parallel behavior. Update the benchmark family to keep the
`std::execution::par` test only for `olc_db`, and either remove the
`db`/`mutex_db` variants or rename them so they clearly measure
sequential/overhead-only behavior. Use the existing `BM_BulkLoad_par_db`,
`BM_BulkLoad_par_mutex`, and `BM_BulkLoad_par_olc` symbols to locate the
affected benchmark cases.

In `@examples/example_bulk_load.cpp`:
- Around line 50-69: The “parallel bulk_load” examples in example_bulk_load.cpp
are misleading because db::bulk_load ignores the execution policy and
mutex_db::bulk_load forwards to the same implementation, so neither actually
builds concurrently. Update the example to use olc_db if you want a true
parallel construction demo, or change the section comments and stderr labels in
the db and mutex_db blocks so they no longer claim parallel behavior. Keep the
references around bulk_load, db<std::uint64_t, unodb::value_view>, and
mutex_db<std::uint64_t, unodb::value_view> aligned with the actual semantics.
- Around line 26-33: make_sorted_data() is returning pairs that hold dangling
unodb::value_view references because the backing bytes are stack-allocated
inside the function; update the data construction so each value_view points to
storage that remains alive after make_sorted_data() returns, using the
make_sorted_data() helper and the kv vector as the key locations to adjust.
Ensure bulk_load() receives views backed by persistent memory, either by moving
the owned byte storage outside the function or by returning an owning container
alongside the views.

In `@olc_art.hpp`:
- Around line 2856-2879: In the replacement-leaf path inside the value_type ==
value_view branch, the new leaf is being created from the original insert
argument instead of the updated local value returned by fn(local), so the
callback’s requested change is lost. Update the leaf construction logic in this
branch (and the matching update branch at the other referenced location) to
build/copy the replacement leaf from local before reclaiming the old leaf, and
keep the pointer swap and reclaim flow in create_leaf_if_needed and the parent
write-lock section unchanged.

In `@portability_execution.hpp`:
- Around line 27-41: Move the fallback execution policy tags out of
std::execution and into a project-owned namespace in portability_execution.hpp,
since defining new standard-library types there is undefined behavior and may
clash with partial <execution> support. Update the fallback policy symbols
sequenced_policy, parallel_policy, parallel_unsequenced_policy, and the
seq/par/par_unseq constants to live under that namespace, then switch the
affected call sites that currently use seq or par to reference the new namespace
instead.

In `@test/test_art_bulk_load_ops.cpp`:
- Around line 133-151: The BulkLoadOps::NoGrowthEvents test currently only
verifies that keys can be read back, so it does not actually validate the
no-growth behavior it claims. Update the test in test_art_bulk_load_ops.cpp to
assert the relevant bulk_load growth/shrink counters or inode topology that
proves no growth events occurred during bulk loading, using the existing
db.bulk_load and db.get checks as needed; if those invariants are not available,
rename the test to reflect the actual behavior it verifies.

In `@test/test_art_oom.cpp`:
- Around line 653-728: Add an OOM regression test for the parallel bulk-load
path, since bulk_load_oom_test currently only exercises bulk_load(kv.begin(),
kv.end()). Introduce at least one case that drives the std::execution::par
overload and spans multiple root partitions so the std::async root-task setup is
covered. Reuse the existing ARTOOMTest helpers and add coverage for both db and
olc_db through the parallel bulk_load entry point to verify cleanup on
allocation failure.

---

Duplicate comments:
In `@test/test_art_oom.cpp`:
- Around line 672-678: The `std::bad_alloc` path in the bulk_load OOM test is
only checking `test_db.empty()` and memory use, which is weaker than the
repository’s full empty-tree contract. Update the assertion in the catch block
to use the stronger empty-state verification from `test/db_test_utils.hpp` or
add zero checks for the tree’s node counters alongside `test_db.empty()` so
stale counters cannot slip through; use the existing `test_db`/`UNODB_ASSERT_*`
test helpers to keep the check consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0e6588f3-80c5-4e00-92f9-c37458ed8614

📥 Commits

Reviewing files that changed from the base of the PR and between 31bfbca and 6a18e57.

📒 Files selected for processing (15)
  • art.hpp
  • art_bulk_load_detail.hpp
  • art_internal_impl.hpp
  • benchmark/CMakeLists.txt
  • benchmark/micro_benchmark_bulk_load.cpp
  • examples/CMakeLists.txt
  • examples/example_bulk_load.cpp
  • mutex_art.hpp
  • olc_art.hpp
  • portability_execution.hpp
  • test/CMakeLists.txt
  • test/test_art_bulk_load.cpp
  • test/test_art_bulk_load_ops.cpp
  • test/test_art_bulk_load_structural.cpp
  • test/test_art_oom.cpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 13

♻️ Duplicate comments (1)
test/test_art_oom.cpp (1)

672-678: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use the full empty-state check after std::bad_alloc.

empty() plus current memory use is still weaker than the repository’s own empty-tree contract in test/db_test_utils.hpp: stale node counters would pass here. Assert zero node counts too, or route this through the same stronger helper.

Suggested assertion tightening
     } catch (const std::bad_alloc&) {
       unodb::test::allocation_failure_injector::reset();
       // Strong guarantee: tree must be empty after failed bulk_load
       UNODB_ASSERT_TRUE(test_db.empty());
 `#ifdef` UNODB_DETAIL_WITH_STATS
       UNODB_ASSERT_EQ(test_db.get_current_memory_use(), 0);
+      UNODB_ASSERT_THAT(test_db.get_node_counts(),
+                        ::testing::ElementsAre(0U, 0U, 0U, 0U, 0U));
 `#endif`
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_art_oom.cpp` around lines 672 - 678, The `std::bad_alloc` path in
the bulk_load OOM test is only checking `test_db.empty()` and memory use, which
is weaker than the repository’s full empty-tree contract. Update the assertion
in the catch block to use the stronger empty-state verification from
`test/db_test_utils.hpp` or add zero checks for the tree’s node counters
alongside `test_db.empty()` so stale counters cannot slip through; use the
existing `test_db`/`UNODB_ASSERT_*` test helpers to keep the check consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@art_bulk_load_detail.hpp`:
- Around line 306-320: The async launch path in the bulk-load builder can throw
after some futures have already been started, which leaves completed
build_result_t pointers uncollected. In the builder logic that creates the
std::future list, move the guards/children reserve work before launching tasks,
and wrap the std::async launch sequence so any successfully created futures are
drained through the existing get()/bulk_subtree_guard transfer path before
rethrowing. Keep the fix localized around the futures, guards, children, and
builder lambda handling.
- Around line 77-109: The build_prefix_chain lambda currently passes the raw
current child into inode_4::create() and then overwrites current, so an
exception during chain allocation can leak the child or a previously built
inode_4. Update build_prefix_chain() to transfer ownership of current into the
allocation path (for example via a guarded/owning local) until inode_4::create()
succeeds, then release it only after the new node_ptr_t is safely constructed.
Keep the same fix for both inode_4::create() calls and preserve the existing
stats accounting on the success path.
- Around line 311-313: The parallel worker path in the bulk-load builder is
updating shared db stats without synchronization, which races under
std::execution::par. In the code that launches the async work around builder,
make sure the lambdas that reach make_db_leaf_ptr() and build_chain() do not
mutate db<Key, Value>::current_memory_use, node_counts, or growing_inode_counts
unsafely for db/mutex_db; either route these types away from the parallel path
or guard those stats updates consistently, since olc_db already uses atomics but
this path does not.

In `@art_internal_impl.hpp`:
- Around line 3740-3742: The duplicated create_bulk declaration is making the
function signature invalid. In the create_bulk declarations referenced by
art_internal_impl.hpp, remove the extra repeated [[nodiscard]] static auto
create_bulk( line so each declaration appears only once, and apply the same
cleanup to the second create_bulk block as well.

In `@art.hpp`:
- Around line 2769-2774: The value_view upsert path in the update branch is
reusing the original value instead of the callback-mutated result from
fn(local). In the code around upsert_action::update and value_type/value_view
handling, make the reinsertion use the updated local value returned by the
lambda, not v. If local may alias leaf storage, copy it before calling
remove_internal(art_k) so the old leaf can be reclaimed safely, then pass that
copied updated value into insert_internal.
- Around line 363-373: The bulk_load policy handling in art.hpp is forwarding
std::execution::par into bulk_load_impl even though the ART/db allocator
contract says it is not thread-safe, so locate bulk_load and its policy dispatch
around bulk_load_impl and force the db path to use seq instead of async parallel
root partitions. If parallel behavior must remain, make the
allocator/thread-safety contract explicit and enforce it in the bulk_load policy
branch and any mutex_db-related path so concurrent allocations cannot occur.

In `@benchmark/micro_benchmark_bulk_load.cpp`:
- Around line 175-215: BM04 is mislabeled for the `db` and `mutex_db` cases
because `db::bulk_load` ignores the execution policy and `mutex_db::bulk_load`
just forwards it, so `BM_BulkLoad_par_db` and `BM_BulkLoad_par_mutex` are not
actually measuring parallel behavior. Update the benchmark family to keep the
`std::execution::par` test only for `olc_db`, and either remove the
`db`/`mutex_db` variants or rename them so they clearly measure
sequential/overhead-only behavior. Use the existing `BM_BulkLoad_par_db`,
`BM_BulkLoad_par_mutex`, and `BM_BulkLoad_par_olc` symbols to locate the
affected benchmark cases.

In `@examples/example_bulk_load.cpp`:
- Around line 50-69: The “parallel bulk_load” examples in example_bulk_load.cpp
are misleading because db::bulk_load ignores the execution policy and
mutex_db::bulk_load forwards to the same implementation, so neither actually
builds concurrently. Update the example to use olc_db if you want a true
parallel construction demo, or change the section comments and stderr labels in
the db and mutex_db blocks so they no longer claim parallel behavior. Keep the
references around bulk_load, db<std::uint64_t, unodb::value_view>, and
mutex_db<std::uint64_t, unodb::value_view> aligned with the actual semantics.
- Around line 26-33: make_sorted_data() is returning pairs that hold dangling
unodb::value_view references because the backing bytes are stack-allocated
inside the function; update the data construction so each value_view points to
storage that remains alive after make_sorted_data() returns, using the
make_sorted_data() helper and the kv vector as the key locations to adjust.
Ensure bulk_load() receives views backed by persistent memory, either by moving
the owned byte storage outside the function or by returning an owning container
alongside the views.

In `@olc_art.hpp`:
- Around line 2856-2879: In the replacement-leaf path inside the value_type ==
value_view branch, the new leaf is being created from the original insert
argument instead of the updated local value returned by fn(local), so the
callback’s requested change is lost. Update the leaf construction logic in this
branch (and the matching update branch at the other referenced location) to
build/copy the replacement leaf from local before reclaiming the old leaf, and
keep the pointer swap and reclaim flow in create_leaf_if_needed and the parent
write-lock section unchanged.

In `@portability_execution.hpp`:
- Around line 27-41: Move the fallback execution policy tags out of
std::execution and into a project-owned namespace in portability_execution.hpp,
since defining new standard-library types there is undefined behavior and may
clash with partial <execution> support. Update the fallback policy symbols
sequenced_policy, parallel_policy, parallel_unsequenced_policy, and the
seq/par/par_unseq constants to live under that namespace, then switch the
affected call sites that currently use seq or par to reference the new namespace
instead.

In `@test/test_art_bulk_load_ops.cpp`:
- Around line 133-151: The BulkLoadOps::NoGrowthEvents test currently only
verifies that keys can be read back, so it does not actually validate the
no-growth behavior it claims. Update the test in test_art_bulk_load_ops.cpp to
assert the relevant bulk_load growth/shrink counters or inode topology that
proves no growth events occurred during bulk loading, using the existing
db.bulk_load and db.get checks as needed; if those invariants are not available,
rename the test to reflect the actual behavior it verifies.

In `@test/test_art_oom.cpp`:
- Around line 653-728: Add an OOM regression test for the parallel bulk-load
path, since bulk_load_oom_test currently only exercises bulk_load(kv.begin(),
kv.end()). Introduce at least one case that drives the std::execution::par
overload and spans multiple root partitions so the std::async root-task setup is
covered. Reuse the existing ARTOOMTest helpers and add coverage for both db and
olc_db through the parallel bulk_load entry point to verify cleanup on
allocation failure.

---

Duplicate comments:
In `@test/test_art_oom.cpp`:
- Around line 672-678: The `std::bad_alloc` path in the bulk_load OOM test is
only checking `test_db.empty()` and memory use, which is weaker than the
repository’s full empty-tree contract. Update the assertion in the catch block
to use the stronger empty-state verification from `test/db_test_utils.hpp` or
add zero checks for the tree’s node counters alongside `test_db.empty()` so
stale counters cannot slip through; use the existing `test_db`/`UNODB_ASSERT_*`
test helpers to keep the check consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0e6588f3-80c5-4e00-92f9-c37458ed8614

📥 Commits

Reviewing files that changed from the base of the PR and between 31bfbca and 6a18e57.

📒 Files selected for processing (15)
  • art.hpp
  • art_bulk_load_detail.hpp
  • art_internal_impl.hpp
  • benchmark/CMakeLists.txt
  • benchmark/micro_benchmark_bulk_load.cpp
  • examples/CMakeLists.txt
  • examples/example_bulk_load.cpp
  • mutex_art.hpp
  • olc_art.hpp
  • portability_execution.hpp
  • test/CMakeLists.txt
  • test/test_art_bulk_load.cpp
  • test/test_art_bulk_load_ops.cpp
  • test/test_art_bulk_load_structural.cpp
  • test/test_art_oom.cpp
🛑 Comments failed to post (13)
art_bulk_load_detail.hpp (3)

77-109: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Make prefix-chain construction own current while allocating.

build_prefix_chain() repeatedly passes a raw current child into inode_4::create(). If any chain allocation throws, the input child or the previously-created chain is no longer guarded and leaks, violating the bulk-load exception guarantee.

Proposed direction
   auto build_prefix_chain = [&self](art_key_type k, node_ptr_t child_inode,
                                     tree_depth_type start_depth,
                                     std::size_t end_depth) -> node_ptr_t {
     const auto full_key = k.get_key_view();
     const auto start = static_cast<std::size_t>(start_depth);
-    auto current = child_inode;
+    guard_t current_guard{self};
+    current_guard.ptr = child_inode;
+    current_guard.is_packed_value = false;
     std::size_t pos = end_depth;
     while (pos > start + prefix_cap) {
       const auto depth = pos - prefix_cap - 1;
       const auto dispatch = full_key[pos - 1];
       auto remaining = k;
       remaining.shift_right(depth);
       auto chain{
           inode_4::create(self, full_key, remaining,
                           tree_depth_type{static_cast<std::uint32_t>(depth)},
-                          dispatch, current)};
-      current = node_ptr_t{chain.release(), node_type::I4};
+                          dispatch, current_guard.ptr)};
+      current_guard.release();
+      current_guard.ptr = node_ptr_t{chain.release(), node_type::I4};
 `#ifdef` UNODB_DETAIL_WITH_STATS
       self.template account_growing_inode<node_type::I4>();
 `#endif`
       pos = depth;
     }
     if (pos > start) {
       const auto dispatch = full_key[pos - 1];
       auto chain{inode_4::create(
           self, full_key, tree_depth_type{static_cast<std::uint32_t>(start)},
-          static_cast<key_prefix_size>(pos - start - 1), dispatch, current)};
-      current = node_ptr_t{chain.release(), node_type::I4};
+          static_cast<key_prefix_size>(pos - start - 1), dispatch,
+          current_guard.ptr)};
+      current_guard.release();
+      current_guard.ptr = node_ptr_t{chain.release(), node_type::I4};
 `#ifdef` UNODB_DETAIL_WITH_STATS
       self.template account_growing_inode<node_type::I4>();
 `#endif`
     }
-    return current;
+    auto result = current_guard.ptr;
+    current_guard.release();
+    return result;
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  auto build_prefix_chain = [&self](art_key_type k, node_ptr_t child_inode,
                                    tree_depth_type start_depth,
                                    std::size_t end_depth) -> node_ptr_t {
    const auto full_key = k.get_key_view();
    const auto start = static_cast<std::size_t>(start_depth);
    guard_t current_guard{self};
    current_guard.ptr = child_inode;
    current_guard.is_packed_value = false;
    std::size_t pos = end_depth;
    while (pos > start + prefix_cap) {
      const auto depth = pos - prefix_cap - 1;
      const auto dispatch = full_key[pos - 1];
      auto remaining = k;
      remaining.shift_right(depth);
      auto chain{
          inode_4::create(self, full_key, remaining,
                          tree_depth_type{static_cast<std::uint32_t>(depth)},
                          dispatch, current_guard.ptr)};
      current_guard.release();
      current_guard.ptr = node_ptr_t{chain.release(), node_type::I4};
`#ifdef` UNODB_DETAIL_WITH_STATS
      self.template account_growing_inode<node_type::I4>();
`#endif`
      pos = depth;
    }
    if (pos > start) {
      const auto dispatch = full_key[pos - 1];
      auto chain{inode_4::create(
          self, full_key, tree_depth_type{static_cast<std::uint32_t>(start)},
          static_cast<key_prefix_size>(pos - start - 1), dispatch,
          current_guard.ptr)};
      current_guard.release();
      current_guard.ptr = node_ptr_t{chain.release(), node_type::I4};
`#ifdef` UNODB_DETAIL_WITH_STATS
      self.template account_growing_inode<node_type::I4>();
`#endif`
    }
    auto result = current_guard.ptr;
    current_guard.release();
    return result;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@art_bulk_load_detail.hpp` around lines 77 - 109, The build_prefix_chain
lambda currently passes the raw current child into inode_4::create() and then
overwrites current, so an exception during chain allocation can leak the child
or a previously built inode_4. Update build_prefix_chain() to transfer ownership
of current into the allocation path (for example via a guarded/owning local)
until inode_4::create() succeeds, then release it only after the new node_ptr_t
is safely constructed. Keep the same fix for both inode_4::create() calls and
preserve the existing stats accounting on the success path.

306-320: 🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Protect futures before every post-launch throw path.

After the first async task is launched, std::async, guards.reserve(), or children.reserve() can still throw before the guarded get() loop starts. Destroying already-completed futures at that point drops build_result_t raw pointers without transferring them to bulk_subtree_guard.

Reserve guard/child storage before launching tasks, and wrap the launch phase itself so any valid futures are drained before rethrowing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@art_bulk_load_detail.hpp` around lines 306 - 320, The async launch path in
the bulk-load builder can throw after some futures have already been started,
which leaves completed build_result_t pointers uncollected. In the builder logic
that creates the std::future list, move the guards/children reserve work before
launching tasks, and wrap the std::async launch sequence so any successfully
created futures are drained through the existing get()/bulk_subtree_guard
transfer path before rethrowing. Keep the fix localized around the futures,
guards, children, and builder lambda handling.

311-313: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect shared Db state touched by async bulk-load builders.
rg -n -C3 'account_growing_inode|make_db_leaf_ptr|build_chain|bulk_load_impl' .
rg -n -C3 'UNODB_DETAIL_WITH_STATS|struct.*stats|class.*stats|std::atomic|mutex' .

Repository: unodb-dev/unodb

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the bulk-load implementation and the shared state it calls into.
ast-grep outline art_bulk_load_detail.hpp --view expanded
printf '\n--- art_bulk_load_detail.hpp (relevant slice) ---\n'
sed -n '250,380p' art_bulk_load_detail.hpp

printf '\n--- builder-related symbols ---\n'
rg -n -C 2 'account_growing_inode|make_db_leaf_ptr|build_chain|bulk_load_impl|builder\(' art_bulk_load_detail.hpp art_internal_impl.hpp art_internal.hpp art.hpp mutex_art.hpp olc_art.hpp art_allocator.hpp qsbr.hpp

printf '\n--- stats / synchronization in qsbr.hpp ---\n'
rg -n -C 2 'dealloc_stats_lock|quiescent_state_stats_lock|std::mutex|std::atomic|register_dealloc_stats|register_quiescent_states' qsbr.hpp

printf '\n--- db / mutex_db / olc_db bulk-load entry points ---\n'
rg -n -C 2 'bulk_load\(' art.hpp mutex_art.hpp olc_art.hpp art_internal_impl.hpp

Repository: unodb-dev/unodb

Length of output: 40548


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- art_internal_impl.hpp: leaf allocation path ---\n'
sed -n '520,620p' art_internal_impl.hpp

printf '\n--- art_internal_impl.hpp: bulk-load helpers and build_chain context ---\n'
sed -n '820,980p' art_internal_impl.hpp

printf '\n--- art.hpp: build_chain / accounting definitions ---\n'
sed -n '1688,1775p' art.hpp
sed -n '2688,2715p' art.hpp

printf '\n--- olc_art.hpp: build_chain / accounting definitions ---\n'
sed -n '2368,2418p' olc_art.hpp
sed -n '4698,4720p' olc_art.hpp

printf '\n--- art_allocator.hpp: allocator thread-safety / shared state ---\n'
sed -n '1,220p' art_allocator.hpp

Repository: unodb-dev/unodb

Length of output: 21058


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- art.hpp: leaf / memory accounting helpers ---\n'
sed -n '2660,2745p' art.hpp

printf '\n--- olc_art.hpp: leaf / memory accounting helpers ---\n'
sed -n '4680,4745p' olc_art.hpp

printf '\n--- art.hpp: stats fields ---\n'
rg -n -C 2 'leaf_count|memory_use|node_counts|growing_inode_counts|shrinking_inode_counts|key_prefix_splits' art.hpp

printf '\n--- olc_art.hpp: stats fields ---\n'
rg -n -C 2 'leaf_count|memory_use|node_counts|growing_inode_counts|shrinking_inode_counts|key_prefix_splits' olc_art.hpp

Repository: unodb-dev/unodb

Length of output: 18038


db/mutex_db race here with std::execution::par. The worker lambdas call make_db_leaf_ptr() and build_chain(), which update db<Key, Value>::current_memory_use, node_counts, and growing_inode_counts as plain members; the outer mutex on mutex_db does not protect those worker threads. olc_db uses atomics, but this path is UB for db when UNODB_DETAIL_WITH_STATS is on. Either disable the parallel path for db/mutex_db or synchronize those stats updates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@art_bulk_load_detail.hpp` around lines 311 - 313, The parallel worker path in
the bulk-load builder is updating shared db stats without synchronization, which
races under std::execution::par. In the code that launches the async work around
builder, make sure the lambdas that reach make_db_leaf_ptr() and build_chain()
do not mutate db<Key, Value>::current_memory_use, node_counts, or
growing_inode_counts unsafely for db/mutex_db; either route these types away
from the parallel path or guard those stats updates consistently, since olc_db
already uses atomics but this path does not.
art_internal_impl.hpp (1)

3740-3742: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the duplicated create_bulk declaration lines.

The repeated [[nodiscard]] static auto create_bulk( lines make these function declarations syntactically invalid. Delete the extra duplicate in both blocks.

Proposed cleanup
   UNODB_DETAIL_DISABLE_CLANG_21_WARNING("-Wnrvo")
-  [[nodiscard]] static auto create_bulk(
   [[nodiscard]] static auto create_bulk(

Also applies to: 4553-4555

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@art_internal_impl.hpp` around lines 3740 - 3742, The duplicated create_bulk
declaration is making the function signature invalid. In the create_bulk
declarations referenced by art_internal_impl.hpp, remove the extra repeated
[[nodiscard]] static auto create_bulk( line so each declaration appears only
once, and apply the same cleanup to the second create_bulk block as well.
art.hpp (2)

363-373: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Don’t forward db parallel policies into async construction while the allocator contract says it is not thread-safe.

Line 368 says parallelism is ignored for db because the allocator is not thread-safe, but Line 2844 forwards std::execution::par to bulk_load_impl, whose policy branch uses async parallel root partitions. That can concurrently allocate through the same db instance, including via mutex_db. Force seq here or make the allocator/thread-safety contract explicit and enforced.

Also applies to: 2837-2845

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@art.hpp` around lines 363 - 373, The bulk_load policy handling in art.hpp is
forwarding std::execution::par into bulk_load_impl even though the ART/db
allocator contract says it is not thread-safe, so locate bulk_load and its
policy dispatch around bulk_load_impl and force the db path to use seq instead
of async parallel root partitions. If parallel behavior must remain, make the
allocator/thread-safety contract explicit and enforce it in the bulk_load policy
branch and any mutex_db-related path so concurrent allocations cannot occur.

2769-2774: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the lambda-updated value for value_view upserts.

This branch calls fn(local) but then removes and reinserts v, so upsert_action::update ignores the callback’s mutation. Reinsert from local, and copy it before reclaiming the old leaf if it may alias leaf storage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@art.hpp` around lines 2769 - 2774, The value_view upsert path in the update
branch is reusing the original value instead of the callback-mutated result from
fn(local). In the code around upsert_action::update and value_type/value_view
handling, make the reinsertion use the updated local value returned by the
lambda, not v. If local may alias leaf storage, copy it before calling
remove_internal(art_k) so the old leaf can be reclaimed safely, then pass that
copied updated value into insert_internal.
benchmark/micro_benchmark_bulk_load.cpp (1)

175-215: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

BM04 is benchmarking fake parallelism for db and mutex_db.

db::bulk_load ignores the execution policy, and mutex_db::bulk_load only forwards that same call. So BM_BulkLoad_par_db and BM_BulkLoad_par_mutex are just sequential runs mislabeled as parallel, which invalidates this benchmark family and any speedup claims derived from it. Keep BM04 on olc_db only, or rename the other cases to benchmark overload/policy overhead instead.

Also applies to: 397-400

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmark/micro_benchmark_bulk_load.cpp` around lines 175 - 215, BM04 is
mislabeled for the `db` and `mutex_db` cases because `db::bulk_load` ignores the
execution policy and `mutex_db::bulk_load` just forwards it, so
`BM_BulkLoad_par_db` and `BM_BulkLoad_par_mutex` are not actually measuring
parallel behavior. Update the benchmark family to keep the `std::execution::par`
test only for `olc_db`, and either remove the `db`/`mutex_db` variants or rename
them so they clearly measure sequential/overhead-only behavior. Use the existing
`BM_BulkLoad_par_db`, `BM_BulkLoad_par_mutex`, and `BM_BulkLoad_par_olc` symbols
to locate the affected benchmark cases.
examples/example_bulk_load.cpp (2)

26-33: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

make_sorted_data() returns dangling value_views.

Line 27 allocates the backing bytes on the stack, then every element in kv stores a value_view pointing at that buffer. Once the function returns, all those views dangle and the later bulk_load() calls read dead memory.

Fix the lifetime
 namespace {
 
 constexpr std::size_t key_count = 100'000;
+static constexpr std::array<std::byte, 8> value_bytes{};
 
 /// Build sorted key-value pairs. The caller is responsible for
 /// pre-encoding and pre-sorting keys before calling bulk_load.
 [[nodiscard]] auto make_sorted_data() {
-  constexpr std::array<std::byte, 8> val{};
-  const auto value = unodb::value_view{val};
+  const auto value = unodb::value_view{value_bytes};
   std::vector<std::pair<std::uint64_t, unodb::value_view>> kv;
   kv.reserve(key_count);
   for (std::size_t i = 0; i < key_count; ++i)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

namespace {

constexpr std::size_t key_count = 100'000;
static constexpr std::array<std::byte, 8> value_bytes{};

/// Build sorted key-value pairs. The caller is responsible for
/// pre-encoding and pre-sorting keys before calling bulk_load.
[[nodiscard]] auto make_sorted_data() {
  const auto value = unodb::value_view{value_bytes};
  std::vector<std::pair<std::uint64_t, unodb::value_view>> kv;
  kv.reserve(key_count);
  for (std::size_t i = 0; i < key_count; ++i)
    kv.emplace_back(static_cast<std::uint64_t>(i), value);
  return kv;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/example_bulk_load.cpp` around lines 26 - 33, make_sorted_data() is
returning pairs that hold dangling unodb::value_view references because the
backing bytes are stack-allocated inside the function; update the data
construction so each value_view points to storage that remains alive after
make_sorted_data() returns, using the make_sorted_data() helper and the kv
vector as the key locations to adjust. Ensure bulk_load() receives views backed
by persistent memory, either by moving the owned byte storage outside the
function or by returning an owning container alongside the views.

50-69: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

These “parallel” examples are wrong for db and mutex_db.

db::bulk_load documents that the execution policy is ignored, and mutex_db::bulk_load just forwards to that same call. So this file is currently claiming concurrent construction where none happens. Use olc_db for the real parallel example, or rename the sections/output so they stop advertising parallel behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/example_bulk_load.cpp` around lines 50 - 69, The “parallel
bulk_load” examples in example_bulk_load.cpp are misleading because
db::bulk_load ignores the execution policy and mutex_db::bulk_load forwards to
the same implementation, so neither actually builds concurrently. Update the
example to use olc_db if you want a true parallel construction demo, or change
the section comments and stderr labels in the db and mutex_db blocks so they no
longer claim parallel behavior. Keep the references around bulk_load,
db<std::uint64_t, unodb::value_view>, and mutex_db<std::uint64_t,
unodb::value_view> aligned with the actual semantics.
olc_art.hpp (1)

2856-2879: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Build replacement value_view leaves from local, not the insert argument.

Both update branches call fn(local) and then create the replacement leaf from v, discarding the callback’s requested update. Allocate/copy from local before reclaiming the current leaf.

Also applies to: 2931-2957

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@olc_art.hpp` around lines 2856 - 2879, In the replacement-leaf path inside
the value_type == value_view branch, the new leaf is being created from the
original insert argument instead of the updated local value returned by
fn(local), so the callback’s requested change is lost. Update the leaf
construction logic in this branch (and the matching update branch at the other
referenced location) to build/copy the replacement leaf from local before
reclaiming the old leaf, and keep the pointer swap and reclaim flow in
create_leaf_if_needed and the parent write-lock section unchanged.
portability_execution.hpp (1)

27-41: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect declared C++ targets/toolchains and all execution shim usage.
rg -n "CMAKE_CXX_STANDARD|cxx_std_|__cpp_lib_execution|__has_include\\(<execution>\\)|namespace std::execution|UNODB_DETAIL_HAS_EXECUTION_POLICIES" .

Repository: unodb-dev/unodb

Length of output: 1026


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== portability_execution.hpp =="
cat -n portability_execution.hpp

echo
echo "== usages of execution policy tags =="
rg -n "\b(std::execution::)?(seq|par|par_unseq)\b|UNODB_DETAIL_HAS_EXECUTION_POLICIES" .

Repository: unodb-dev/unodb

Length of output: 4151


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== include sites for portability_execution.hpp =="
rg -n '`#include` "portability_execution\.hpp"|`#include` <portability_execution\.hpp>' .

echo
echo "== surrounding declarations in mutex_art.hpp and art.hpp =="
sed -n '130,190p' mutex_art.hpp
echo
sed -n '350,410p' art.hpp
echo
sed -n '300,340p' olc_art.hpp

Repository: unodb-dev/unodb

Length of output: 7134


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== include sites for portability_execution.hpp =="
rg -n '`#include` "portability_execution\.hpp"|`#include` <portability_execution\.hpp>' .

echo
echo "== surrounding declarations in mutex_art.hpp =="
sed -n '130,190p' mutex_art.hpp

echo
echo "== surrounding declarations in art.hpp =="
sed -n '350,410p' art.hpp

echo
echo "== surrounding declarations in olc_art.hpp =="
sed -n '300,340p' olc_art.hpp

Repository: unodb-dev/unodb

Length of output: 7210


Move the fallback execution tags out of std::execution. portability_execution.hpp:27-41 defines new policy types in std, which is undefined behavior and can collide with partial <execution> implementations. Put the fallback in a project namespace and switch the few seq/par call sites to that namespace.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portability_execution.hpp` around lines 27 - 41, Move the fallback execution
policy tags out of std::execution and into a project-owned namespace in
portability_execution.hpp, since defining new standard-library types there is
undefined behavior and may clash with partial <execution> support. Update the
fallback policy symbols sequenced_policy, parallel_policy,
parallel_unsequenced_policy, and the seq/par/par_unseq constants to live under
that namespace, then switch the affected call sites that currently use seq or
par to reference the new namespace instead.
test/test_art_bulk_load_ops.cpp (1)

133-151: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

This test never checks “NoGrowthEvents”.

It only re-reads keys, so a regression that reintroduces grow/shrink behavior during bulk load would still pass. Either assert the relevant counters/topology here or rename the test to match what it actually verifies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_art_bulk_load_ops.cpp` around lines 133 - 151, The
BulkLoadOps::NoGrowthEvents test currently only verifies that keys can be read
back, so it does not actually validate the no-growth behavior it claims. Update
the test in test_art_bulk_load_ops.cpp to assert the relevant bulk_load
growth/shrink counters or inode topology that proves no growth events occurred
during bulk loading, using the existing db.bulk_load and db.get checks as
needed; if those invariants are not available, rename the test to reflect the
actual behavior it verifies.
test/test_art_oom.cpp (1)

653-728: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add an OOM case for the parallel overload.

These tests only hit bulk_load(kv.begin(), kv.end()). The new failure mode in this PR is the std::execution::par path with std::async root-task setup, so cleanup bugs there can still ship untested. Add at least one db/olc_db case that uses std::execution::par with input spanning multiple root partitions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_art_oom.cpp` around lines 653 - 728, Add an OOM regression test for
the parallel bulk-load path, since bulk_load_oom_test currently only exercises
bulk_load(kv.begin(), kv.end()). Introduce at least one case that drives the
std::execution::par overload and spans multiple root partitions so the
std::async root-task setup is covered. Reuse the existing ARTOOMTest helpers and
add coverage for both db and olc_db through the parallel bulk_load entry point
to verify cleanup on allocation failure.

In the build_single_leaf lambda, the else branch (classic leaf without
key/leaf elimination) does not use depth. Cast to void to silence
MSVC warning-as-error.
- C4100: guards/children unused when can_eliminate_leaf is false
- C4365: signed/unsigned mismatch in vmask index for inode_256
The bulk_load parallel API now takes a fork callable + max_tasks
instead of std::execution::par. The library never spawns threads
internally — the caller owns all threading decisions.

API:
  tree.bulk_load(first, last);                    // sequential
  tree.bulk_load(fork, max_tasks, first, last);   // parallel

Where fork(Callable) -> Future<R> with .get(). Works with
std::async, thread pools, fiber pools, QSBR threads, etc.

The implementation forks up to min(partitions, max_tasks) subtrees
via the callable; remaining partitions run inline on the calling
thread. Thread-local stats accumulators ensure correctness with
STATS=ON regardless of the executor type.

Also removes portability_execution.hpp (now unused) and dead
<future> includes from library headers.
@thompsonbry

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@art_internal_impl.hpp`:
- Around line 4549-4582: The bulk constructor in create_bulk currently leaves
child_indexes default-initialized, which bypasses the inode48_type empty-child
sentinel. Update create_bulk to explicitly initialize all 256 child_indexes
entries to inode48_type::empty_child before assigning entries from
children_span, while keeping the existing child population logic unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 618f3d98-6783-405e-92de-57f7a69d73ac

📥 Commits

Reviewing files that changed from the base of the PR and between 7f2e05b and fb7f636.

📒 Files selected for processing (15)
  • art.hpp
  • art_bulk_load_detail.hpp
  • art_internal_impl.hpp
  • benchmark/CMakeLists.txt
  • benchmark/micro_benchmark_bulk_load.cpp
  • examples/CMakeLists.txt
  • examples/example_bulk_load.cpp
  • mutex_art.hpp
  • node_type.hpp
  • olc_art.hpp
  • test/CMakeLists.txt
  • test/test_art_bulk_load.cpp
  • test/test_art_bulk_load_ops.cpp
  • test/test_art_bulk_load_structural.cpp
  • test/test_art_oom.cpp

Comment thread art_internal_impl.hpp
- Pass value_mask by const ref in Node48/Node256 create_bulk
  (cppcheck passedByValue for 6-byte and 32-byte arrays)
- Suppress functionStatic on olc_db::merge_bulk_load_stats
  (intentionally non-static for API consistency)
- Remove child_indexes{} from inode48 bulk_load_tag constructor init
  list so the default member initializer (all 0xFF / empty_child) is
  used. The previous {} zero-initialized, leaving absent key slots as
  0 instead of the empty_child sentinel. (CodeRabbit finding)
- Pass value_mask by const ref in Node48/Node256 create_bulk
  (cppcheck passedByValue on macOS)
- Suppress cppcheck functionStatic on olc_db::merge_bulk_load_stats
  (intentionally non-static for API consistency)
- Replace raw loop with std::transform for random_keys (cppcheck
  useStlAlgorithm on macOS)
- Suppress useStlAlgorithm for key_view_set index loops (no iterator)
- Add <iterator> for std::back_inserter
- clang-format fix in inode48 bulk_load_tag constructor
Move duplicated bulk_load infrastructure from art.hpp and olc_art.hpp
into shared ArtPolicy-parameterized templates in art_internal_impl.hpp:

- bulk_build_result<ArtPolicy>: tagged return type (ptr + is_packed)
- bulk_subtree_guard<ArtPolicy>: RAII guard with policy-aware cleanup
- bulk_build_chain<ArtPolicy>(): prefix chain builder (was build_chain)

db::build_chain and olc_db::build_chain now delegate to the shared
implementation. This eliminates ~80 lines of duplication.
@thompsonbry

Copy link
Copy Markdown
Contributor Author

@laurynas-biveinis can you do a review pass on this one? I think it is green modulo the various build timeouts.

Comment on lines +43 to +64
/// Generate sorted sequential keys [0..n).
[[nodiscard]] auto sequential_keys(std::int64_t n) {
std::vector<std::pair<std::uint64_t, unodb::value_view>> kv;
kv.reserve(static_cast<std::size_t>(n));
for (std::int64_t i = 0; i < n; ++i)
kv.emplace_back(static_cast<std::uint64_t>(i), value);
return kv;
}

/// Generate sorted random unique keys.
[[nodiscard]] auto random_keys(std::int64_t n) {
std::mt19937_64 rng(42); // NOLINT(cert-msc32-c,cert-msc51-cpp)
std::vector<std::uint64_t> keys(static_cast<std::size_t>(n));
std::generate(keys.begin(), keys.end(), rng);
std::sort(keys.begin(), keys.end());
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
std::vector<std::pair<std::uint64_t, unodb::value_view>> kv;
kv.reserve(keys.size());
std::transform(keys.begin(), keys.end(), std::back_inserter(kv),
[](auto k) { return std::make_pair(k, value); });
return kv;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not duplicated elsewhere?

Comment thread examples/example_bulk_load.cpp Outdated
tree.clear();
}

// ─── mutex_db: same API ────────────────────────────────────────────────────

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO mutex_db should not be emphasized in examples, olc_db should be preferred


UNODB_DETAIL_RESTORE_MSVC_WARNINGS()

#endif // UNODB_DETAIL_WITH_STATS

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The split between the three bulk load testcase files is rather arbitrary (a case can be made that all three files are "ops" and also all are "structural") - merge?

Comment thread test/test_art_oom.cpp Outdated
void bulk_load_oom_test(
unsigned fail_limit,
std::vector<std::pair<std::uint64_t, unodb::value_view>>
kv) { // NOLINT(performance-unnecessary-value-param)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why pass by value? it's large and not moved from. Pass by const reference?

Comment thread art.hpp

/// Forward declaration of shared bulk_load algorithm.
template <typename Db, typename Fork, typename RandomIt>
void bulk_load_impl(Db& self, Fork&& fork, std::size_t max_tasks,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a forward declaration at all if it's a template and its header has to be included anyway?

Comment thread art_bulk_load_detail.hpp Outdated
Comment on lines +83 to +87
UNODB_DETAIL_DISABLE_GCC_WARNING("-Wpessimizing-move")
UNODB_DETAIL_DISABLE_MSVC_WARNING(26479)
return std::move(parts); // NOLINT(performance-move-const-arg)
UNODB_DETAIL_RESTORE_MSVC_WARNINGS()
UNODB_DETAIL_RESTORE_GCC_WARNINGS()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This all goes away if parts is returned directly, also enabling NRVO

Comment thread art_bulk_load_detail.hpp Outdated
UNODB_DETAIL_RESTORE_GCC_WARNINGS()
};

auto build_prefix_chain = [&self](art_key_type k, node_ptr_t child_inode,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull-up to the top level this and everything pullable below

Comment thread art_bulk_load_detail.hpp Outdated
Comment on lines +112 to +114
// LCOV_EXCL_START — chain_consumed is always a multiple of (prefix_cap+1),
// so the while loop always decrements pos to exactly start.
if (pos > start) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is LCOV_EXCL_START and the comment saying that this is an always-false condition?

Comment thread art_bulk_load_detail.hpp Outdated
Comment on lines +404 to +405
} catch (...) {
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can only throw if emplace_back throws but that should not throw because guards got a reserve call? Can this be asserted as unreachable code?

Comment thread art_internal_impl.hpp Outdated
Comment on lines +195 to +196
: db_{other.db_}, ptr{other.ptr}, is_packed_value{other.is_packed_value} {
other.ptr = nullptr;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idiomatic is ptr{std::exchange(other.ptr), nullptr}

- Pull all lambdas to bulk_load_helpers<Db, RandomIt> struct with named
  static methods (common_prefix_length, partition_by_byte, build_prefix_chain,
  build_single_leaf, make_inode, build_subtree)
- partition_entry<RandomIt> struct at namespace scope
- Use std::exchange in bulk_subtree_guard move ctor
- Add ptr_type alias to bulk_build_result for cleaner signatures
- Make art_key_type, tree_depth_type, build_result, bulk_subtree_guard public
  in db and olc_db (needed for struct method signatures)
- Pass vector by const& in bulk_load_oom_test
- Replace mutex_db with olc_db in example
- Clarify LCOV_EXCL with UNODB_DETAIL_ASSERT(pos == start)
- Comment on inner catch: future.get() may rethrow task exception
- NRVO: return parts directly (explicit return type enables it)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants