FlatCityBuf C++ reader 0.8.0
Native C++17 reader for FlatCityBuf, the cloud-optimized CityJSON format
Loading...
Searching...
No Matches
btree_builder.cpp
Go to the documentation of this file.
1#include <fcb/error.hpp>
3
4#include <algorithm>
5#include <unordered_map>
6
7namespace fcb {
8
9namespace {
10
14struct UniqueLeaf {
15 KeyValue key;
16 std::uint64_t offset;
17};
18
24std::pair<std::vector<UniqueLeaf>, std::vector<std::uint8_t>>
25group_duplicates(std::vector<BtreeEntry> entries) {
26 std::stable_sort(entries.begin(), entries.end(), [](const BtreeEntry& a, const BtreeEntry& b) {
27 return compare_keys(a.key, b.key) < 0;
28 });
29
30 std::vector<UniqueLeaf> unique_leaves;
31 std::vector<std::uint8_t> payload_data;
32 std::size_t i = 0;
33 while (i < entries.size()) {
34 std::size_t j = i + 1;
35 while (j < entries.size() && compare_keys(entries[j].key, entries[i].key) == 0)
36 ++j;
37
38 if (j - i == 1) {
39 unique_leaves.push_back(UniqueLeaf{entries[i].key, entries[i].offset});
40 } else {
41 std::vector<std::uint64_t> offsets;
42 offsets.reserve(j - i);
43 for (std::size_t k = i; k < j; ++k)
44 offsets.push_back(entries[k].offset);
45 const std::uint64_t rel = payload_data.size();
46 encode_payload_entry(payload_data, offsets);
47 unique_leaves.push_back(UniqueLeaf{entries[i].key, kPayloadTag | rel});
48 }
49 i = j;
50 }
51 return {unique_leaves, payload_data};
52}
53
60void generate_nodes(std::vector<UniqueLeaf>& tree, const std::vector<StreeLevelBound>& level_bounds,
61 std::uint16_t branching_factor, std::size_t num_leaf_nodes, KeyKind kind) {
62 const std::uint64_t node_size = static_cast<std::uint64_t>(branching_factor) - 1;
63 const std::uint64_t skip_size = static_cast<std::uint64_t>(branching_factor) * node_size;
64 const std::uint64_t bf2 = static_cast<std::uint64_t>(branching_factor) * branching_factor;
65 const std::uint64_t num_nodes = tree.size();
66 const std::uint64_t leaf_start = num_nodes - num_leaf_nodes;
67
68 // Keyed by flat array index: the TRUE minimum key covered by that
69 // index's subtree (as opposed to `tree[index].key`, which for an
70 // INTERNAL index is a separator, not that subtree's minimum). Only
71 // ever read one level below where it was written; each level's pass
72 // must run to completion before the next level's pass starts.
73 std::unordered_map<std::uint64_t, KeyValue> parent_min_key;
74
75 auto require_min_key = [&](std::uint64_t idx) -> const KeyValue& {
76 auto it = parent_min_key.find(idx);
77 if (it == parent_min_key.end()) {
79 "static B+tree builder: missing parent_min_key entry -- this is a bug in "
80 "the builder itself, not malformed input");
81 }
82 return it->second;
83 };
84
85 for (std::size_t level = 0; level + 1 < level_bounds.size(); ++level) {
86 const StreeLevelBound& children_level = level_bounds[level];
87 const StreeLevelBound& parent_level = level_bounds[level + 1];
88
89 std::uint64_t parent_idx = parent_level.start;
90 std::uint64_t child_idx = children_level.start;
91
92 while (child_idx < children_level.end) {
93 if (parent_idx >= parent_level.end)
94 break;
95
96 const std::uint64_t child_idx_diff = child_idx - children_level.start;
97 const std::uint64_t m = child_idx_diff % skip_size;
98 const bool is_right_most_child = (node_size * node_size <= m) && (m < bf2);
99 const bool has_next_node = child_idx + node_size < children_level.end;
100
101 if (is_right_most_child) {
102 child_idx += node_size;
103 continue;
104 }
105
106 if (!has_next_node) {
107 const KeyValue parent_key = key_max(kind);
108 tree[parent_idx] = UniqueLeaf{parent_key, child_idx};
109
110 // `min(tree[child_idx].key, parent_min_key[child_idx] or max)`
111 // WITHOUT branching on whether `child_idx` is itself a leaf --
112 // and that omission is deliberate, not a shortcut this port
113 // is taking. For a LEAF child, `tree[child_idx].key` already
114 // IS that leaf's true min, and no `parent_min_key` entry
115 // exists for it (falls back to `key_max`, so `min` picks the
116 // real key). For an INTERNAL child, `tree[child_idx].key` is
117 // a right-sibling separator -- provably >= that subtree's
118 // true min (a separator is always the min of something to
119 // its OWN right, hence >= its own subtree's min) -- so `min`
120 // always resolves to the correct value already sitting in
121 // `parent_min_key`. Branching explicitly on `is_leaf_node`
122 // here would also be correct, but "simplifying" this by
123 // inverting to `max` (or by only handling one case) is the
124 // exact mistake this comment exists to head off. Confirmed
125 // during Fable consultation on this milestone.
126 const KeyValue& candidate =
127 parent_min_key.count(child_idx) ? parent_min_key.at(child_idx) : key_max(kind);
128 const KeyValue& own_min = compare_keys(tree[child_idx].key, candidate) < 0
129 ? tree[child_idx].key
130 : candidate;
131 parent_min_key.insert_or_assign(parent_idx, own_min);
132 ++parent_idx;
133 child_idx += node_size;
134 continue;
135 }
136
137 const std::uint64_t right_node_idx = child_idx + node_size;
138 const bool is_leaf_node = child_idx >= leaf_start;
139
140 if (is_leaf_node) {
141 const KeyValue parent_key =
142 right_node_idx < children_level.end ? tree[right_node_idx].key : key_max(kind);
143 tree[parent_idx] = UniqueLeaf{parent_key, child_idx};
144 parent_min_key.insert_or_assign(parent_idx, tree[child_idx].key);
145 ++parent_idx;
146 child_idx += node_size;
147 continue;
148 }
149
150 const KeyValue parent_key = right_node_idx < children_level.end
151 ? require_min_key(child_idx + node_size)
152 : key_max(kind);
153 tree[parent_idx] = UniqueLeaf{parent_key, child_idx};
154 parent_min_key.insert_or_assign(parent_idx, require_min_key(child_idx));
155 ++parent_idx;
156 child_idx += node_size;
157 }
158 }
159}
160
161} // namespace
162
163BuiltBtreeIndex build_static_btree(const std::vector<BtreeEntry>& entries, KeyKind kind,
164 std::uint16_t branching_factor) {
165 // `Stree::build` (stree.rs:638-640) clamps `branching_factor` to
166 // `[2, 65535]` BEFORE ever calling `init()` -- so `init()`'s own
167 // `if branching_factor < 2 { return Err(...) }` (stree.rs:451-455) is
168 // unreachable from `build`'s call path and a sub-2 value silently
169 // becomes 2, NOT an error. This is the OPPOSITE of the packed R-tree's
170 // `PackedRTree::build`, which `assert!`s (panics) on a sub-2 node size
171 // instead of pre-clamping -- a real asymmetry in Rust itself between
172 // the two builders, not a typo to "fix" into symmetry. Originally
173 // ported as a throw here (copying the R-tree's M5 behavior without
174 // re-verifying against the B+tree's OWN entry point); caught by the
175 // M6 codex review.
176 branching_factor = std::clamp<std::uint16_t>(branching_factor, 2, 65535);
177
178 // `init()`'s OTHER check -- `if self.num_leaf_nodes == 0 { return
179 // Err(...) }` -- is NOT preempted by anything in `build()`, so it DOES
180 // still throw through this call path; `num_leaf_nodes` there is
181 // `unique_leaves.len()` (post-dedup), but an empty `entries` can never
182 // produce a non-empty `unique_leaves` either way.
183 if (entries.empty()) {
185 "cannot build a static B+tree index with no entries");
186 }
187 // Rust's `Stree<K>` is generic over ONE concrete key type `K`, so a
188 // kind mismatch is unrepresentable there; this port's `KeyValue` is a
189 // runtime-tagged union, so nothing stops a caller from mixing kinds
190 // unless checked here explicitly. `compare_keys` (used below, during
191 // sorting) throws when it's given two DIFFERENT kinds to compare
192 // against EACH OTHER, but every entry sharing one (wrong) kind that
193 // simply differs from `kind` would sail through sorting undetected and
194 // silently encode a mismatched-width blob instead. Found during the
195 // M6 codex review.
196 for (const auto& e : entries) {
197 if (e.key.kind() != kind) {
199 "static B+tree: entry key kind does not match the column's declared kind");
200 }
201 }
202
203 auto [unique_leaves, payload_data] = group_duplicates(entries);
204
205 const auto level_bounds = stree_level_bounds(unique_leaves.size(), branching_factor);
206 const std::uint64_t num_nodes = level_bounds.front().end;
207
208 std::vector<UniqueLeaf> tree(static_cast<std::size_t>(num_nodes), UniqueLeaf{KeyValue{}, 0});
209 const std::uint64_t leaf_start = num_nodes - unique_leaves.size();
210 for (std::size_t i = 0; i < unique_leaves.size(); ++i)
211 tree[static_cast<std::size_t>(leaf_start) + i] = unique_leaves[i];
212
213 generate_nodes(tree, level_bounds, branching_factor, unique_leaves.size(), kind);
214
215 std::vector<std::uint8_t> bytes;
216 bytes.reserve(tree.size() * (key_serialized_size(kind) + 8) + payload_data.size());
217 for (const auto& node : tree) {
218 auto key_bytes = encode_key(node.key);
219 bytes.insert(bytes.end(), key_bytes.begin(), key_bytes.end());
220 for (int i = 0; i < 8; ++i)
221 bytes.push_back(static_cast<std::uint8_t>((node.offset >> (8 * i)) & 0xFF));
222 }
223 bytes.insert(bytes.end(), payload_data.begin(), payload_data.end());
224
225 return BuiltBtreeIndex{std::move(bytes), branching_factor,
226 static_cast<std::uint32_t>(unique_leaves.size())};
227}
228
229} // namespace fcb
Every failure the library reports is one of these.
Definition error.hpp:30
A decoded index key.
Definition key.hpp:40
std::vector< StreeLevelBound > stree_level_bounds(std::uint64_t num_items, std::uint16_t branching_factor)
Mirrors Stree::generate_level_bounds (stree.rs:474-508).
Definition stree.cpp:20
BuiltBtreeIndex build_static_btree(const std::vector< BtreeEntry > &entries, KeyKind kind, std::uint16_t branching_factor)
Builds one column's complete attribute index blob from its (key, offset) entries.
std::vector< std::uint8_t > encode_key(const KeyValue &v)
Definition key.cpp:147
void encode_payload_entry(std::vector< std::uint8_t > &out, const std::vector< std::uint64_t > &offsets)
Encode a payload entry: u32 count then count x u64, all little-endian (mirrors PayloadEntry::serializ...
Definition stree.cpp:350
KeyValue key_max(KeyKind kind)
Definition key.cpp:350
constexpr std::uint64_t kPayloadTag
The MSB of a leaf offset marks a payload reference rather than a direct feature offset (stree....
Definition stree.hpp:38
int compare_keys(const KeyValue &a, const KeyValue &b)
Three-way comparison.
Definition key.cpp:272
KeyKind
The concrete key types the B+tree index can hold.
Definition key.hpp:14
std::size_t key_serialized_size(KeyKind kind)
Serialized width in bytes. DateTime is 12: i64 seconds + u32 nanos.
Definition key.cpp:61
KeyKind kind
Definition stree.cpp:166
std::uint64_t offset
Definition stree.cpp:57
std::uint64_t node_size
Definition stree.cpp:167
KeyValue key
Definition stree.cpp:56
The finished index: the flat node array concatenated with the payload section (mirrors Stree::stream_...