FlatCityBuf C++ reader 0.8.0
Native C++17 reader for FlatCityBuf, the cloud-optimized CityJSON format
Loading...
Searching...
No Matches
stree.cpp
Go to the documentation of this file.
1#include <fcb/reader.hpp>
2#include <fcb/stree.hpp>
3
4#include <algorithm>
5#include <deque>
6#include <utility>
7
8#include "detail/checked.hpp"
9
10namespace fcb {
11
20std::vector<StreeLevelBound> stree_level_bounds(std::uint64_t num_items,
21 std::uint16_t branching_factor) {
22 if (branching_factor < 2) {
24 "invalid branching factor " + std::to_string(branching_factor));
25 }
26 if (num_items == 0) {
27 throw Error(ErrorCode::AttributeIndexNotFound, "empty attribute index");
28 }
29
30 std::vector<std::uint64_t> level_num_nodes;
31 std::uint64_t n = num_items;
32 std::uint64_t num_nodes = n;
33 level_num_nodes.push_back(n);
34 for (;;) {
35 n = detail::ceil_div(n, branching_factor);
36 num_nodes = detail::checked_add(num_nodes, n, "stree num_nodes");
37 level_num_nodes.push_back(n);
38 if (n < branching_factor)
39 break;
40 }
41
42 std::vector<StreeLevelBound> bounds;
43 bounds.reserve(level_num_nodes.size());
44 std::uint64_t acc = num_nodes;
45 for (std::uint64_t size : level_num_nodes) {
46 acc -= size;
47 bounds.push_back(StreeLevelBound{acc, acc + size});
48 }
49 return bounds;
50}
51
52namespace {
53
55struct Entry {
56 KeyValue key;
57 std::uint64_t offset;
58};
59
60std::uint64_t entry_size(KeyKind kind) { return key_serialized_size(kind) + 8; }
61
62std::uint64_t read_u64_le(bytes_view b, std::size_t at) {
63 std::uint64_t v = 0;
64 for (std::size_t i = 0; i < 8; ++i) {
65 v |= static_cast<std::uint64_t>(b[at + i]) << (8 * i);
66 }
67 return v;
68}
69
71std::vector<Entry> read_entries(RangeReader& reader, std::uint64_t index_begin, KeyKind kind,
72 std::uint64_t first, std::uint64_t last) {
73 std::vector<Entry> out;
74 if (last <= first)
75 return out;
76
77 const std::uint64_t esz = entry_size(kind);
78 const std::uint64_t at = detail::checked_add(
79 index_begin, detail::checked_mul(first, esz, "entry offset"), "entry base");
80 const std::uint64_t len = detail::checked_mul(last - first, esz, "entry span");
81
82 auto block = reader.read(at, len);
83 if (block.size() < len) {
84 throw Error(ErrorCode::AttributeIndexNotFound, "truncated attribute index node");
85 }
86
87 const std::size_t ksz = key_serialized_size(kind);
88 out.reserve(static_cast<std::size_t>(last - first));
89 for (std::uint64_t i = 0; i < last - first; ++i) {
90 const std::size_t base = static_cast<std::size_t>(i * esz);
91 Entry e{};
92 e.key = decode_key(kind, bytes_view(block).subspan(base, ksz));
93 e.offset = read_u64_le(bytes_view(block), base + ksz);
94 out.push_back(std::move(e));
95 }
96 return out;
97}
98
101struct BinarySearch {
102 bool found;
103 std::size_t index;
104};
105
106BinarySearch binary_search(const std::vector<Entry>& items, const KeyValue& key) {
107 std::size_t lo = 0, hi = items.size();
108 while (lo < hi) {
109 const std::size_t mid = lo + (hi - lo) / 2;
110 const int c = compare_keys(items[mid].key, key);
111 if (c == 0)
112 return {true, mid};
113 if (c < 0) {
114 lo = mid + 1;
115 } else {
116 hi = mid;
117 }
118 }
119 return {false, lo};
120}
121
124void emit_offset(std::uint64_t off, std::uint64_t index, RangeReader& reader,
125 std::uint64_t payload_begin, std::uint64_t payload_size,
126 std::vector<SearchResultItem>& out) {
127 if (!is_payload_ref(off)) {
128 out.push_back(SearchResultItem{off, index});
129 return;
130 }
131
132 const std::uint64_t rel = payload_offset(off);
133 if (rel + 4 > payload_size) {
134 throw Error(ErrorCode::AttributeIndexNotFound, "payload reference out of range");
135 }
136
137 auto head = reader.read(detail::checked_add(payload_begin, rel, "payload"), 4);
138 if (head.size() < 4) {
139 throw Error(ErrorCode::AttributeIndexNotFound, "truncated payload entry");
140 }
141 std::uint32_t count = 0;
142 for (std::size_t i = 0; i < 4; ++i)
143 count |= static_cast<std::uint32_t>(head[i]) << (8 * i);
144
145 const std::uint64_t want =
146 detail::checked_add(4, detail::checked_mul(count, 8, "payload"), "payload entry");
147 if (rel + want > payload_size) {
148 throw Error(ErrorCode::AttributeIndexNotFound, "payload entry overruns its section");
149 }
150
151 auto body = reader.read(detail::checked_add(payload_begin, rel, "payload"), want);
152 if (body.size() < want) {
153 throw Error(ErrorCode::AttributeIndexNotFound, "truncated payload entry body");
154 }
155 for (std::uint32_t i = 0; i < count; ++i) {
156 out.push_back(SearchResultItem{read_u64_le(bytes_view(body), 4 + i * 8), index});
157 }
158}
159
161struct Tree {
162 RangeReader& reader;
163 std::uint64_t index_begin;
164 std::uint64_t payload_begin;
165 std::uint64_t payload_size;
167 std::uint64_t node_size; // branching_factor - 1
168 std::vector<StreeLevelBound> levels;
169
170 std::uint64_t leaf_start() const { return levels.front().start; }
171 std::uint64_t leaf_end() const { return levels.front().end; }
172
173 std::vector<Entry> node_at(std::uint64_t node_index, std::size_t level) const {
174 const std::uint64_t end =
175 std::min<std::uint64_t>(node_index + node_size, levels[level].end);
176 return read_entries(reader, index_begin, kind, node_index, end);
177 }
178};
179
181std::vector<SearchResultItem> find_exact(const Tree& t, const KeyValue& key) {
182 std::vector<SearchResultItem> out;
183 std::deque<std::pair<std::uint64_t, std::size_t>> queue;
184 queue.emplace_back(0, t.levels.size() - 1);
185
186 while (!queue.empty()) {
187 const auto [node_index, level] = queue.front();
188 queue.pop_front();
189
190 auto items = t.node_at(node_index, level);
191 if (items.empty())
192 continue;
193
194 const auto hit = binary_search(items, key);
195
196 if (level != 0) {
197 // Internal descent. On an exact hit the search key belongs to the
198 // RIGHT of that separator, hence the + node_size; find_partition
199 // deliberately omits it (see below).
200 std::uint64_t child = 0;
201 if (hit.found) {
202 child = items[hit.index].offset + t.node_size;
203 } else if (hit.index == 0) {
204 child = items[0].offset;
205 } else if (hit.index >= items.size()) {
206 child = items.back().offset + t.node_size;
207 } else {
208 child = items[hit.index].offset;
209 }
210
211 // Separator entries with no right sibling carry K::max_value() as
212 // a sentinel, whose offset ALREADY points at the last child group.
213 // Adding node_size would walk off the end of the level for any
214 // query whose key equals the type maximum -- Eq(true) on a bool
215 // column is enough. Clamping back to `offset` is a no-op for
216 // ordinary keys. The same fix has been applied upstream.
217 const std::uint64_t child_level = level - 1;
218 if (child >= t.levels[child_level].end) {
219 child = items[hit.index < items.size() ? hit.index : items.size() - 1].offset;
220 }
221 queue.emplace_back(child, child_level);
222 continue;
223 }
224
225 if (hit.found) {
226 emit_offset(items[hit.index].offset, node_index + hit.index - t.leaf_start(), t.reader,
227 t.payload_begin, t.payload_size, out);
228 }
229 }
230 return out;
231}
232
237std::uint64_t find_partition(const Tree& t, const KeyValue& key) {
238 std::uint64_t node_index = 0;
239 for (std::size_t level = t.levels.size(); level-- > 1;) {
240 auto items = t.node_at(node_index, level);
241 if (items.empty())
242 continue;
243
244 const auto hit = binary_search(items, key);
245 if (hit.found) {
246 node_index = items[hit.index].offset;
247 } else if (hit.index == 0) {
248 node_index = items[0].offset;
249 } else if (hit.index >= items.size()) {
250 node_index = items.back().offset + t.node_size;
251 } else {
252 node_index = items[hit.index].offset;
253 }
254 }
255 return node_index;
256}
257
268std::vector<SearchResultItem> scan_range(const Tree& t, const KeyValue& lower, bool lower_strict,
269 const KeyValue& upper, bool upper_strict) {
270 const int lu = compare_keys(lower, upper);
271 if (lu > 0)
272 return {};
273 if (lu == 0 && (lower_strict || upper_strict))
274 return {};
275
276 const std::uint64_t lower_idx = find_partition(t, lower);
277 const std::uint64_t upper_idx = find_partition(t, upper);
278
279 const std::uint64_t start = std::max<std::uint64_t>(lower_idx, t.leaf_start());
280
281 // Widened by one extra node versus the reference's `upper_idx + node_size`.
282 //
283 // find_partition descends LEFT on an exact hit, so when `upper` is itself
284 // a separator key its matching leaf entry sits at exactly
285 // upper_idx + node_size -- one past the un-widened scan end, and was
286 // silently dropped. Widening is safe because the filter below rejects
287 // out-of-range keys; it costs at most one extra node read. The same fix
288 // has been applied upstream.
289 const std::uint64_t end = std::min<std::uint64_t>(upper_idx + 2 * t.node_size, t.leaf_end());
290
291 std::vector<SearchResultItem> out;
292 std::uint64_t cur = start;
293 while (cur < end) {
294 const std::uint64_t node_end = std::min<std::uint64_t>(cur + t.node_size, end);
295 auto items = read_entries(t.reader, t.index_begin, t.kind, cur, node_end);
296 for (std::size_t i = 0; i < items.size(); ++i) {
297 const int cl = compare_keys(items[i].key, lower);
298 const int cu = compare_keys(items[i].key, upper);
299 if (lower_strict ? cl > 0 : cl >= 0) {
300 if (upper_strict ? cu < 0 : cu <= 0) {
301 emit_offset(items[i].offset, cur + i - t.leaf_start(), t.reader,
302 t.payload_begin, t.payload_size, out);
303 }
304 }
305 }
306 cur = node_end;
307 }
308 return out;
309}
310
311} // namespace
312
313std::uint64_t stree_num_nodes(std::uint64_t num_items, std::uint16_t branching_factor) {
314 if (branching_factor < 2) {
315 throw Error(ErrorCode::AttributeIndexNotFound, "invalid branching factor");
316 }
317 if (num_items == 0)
318 return 0;
319
320 std::uint64_t n = num_items;
321 std::uint64_t num_nodes = n;
322 for (;;) {
323 n = detail::ceil_div(n, branching_factor);
324 num_nodes = detail::checked_add(num_nodes, n, "stree num_nodes");
325 if (n < branching_factor)
326 break;
327 }
328 return num_nodes;
329}
330
331std::vector<std::uint64_t> decode_payload_entry(bytes_view b) {
332 if (b.size() < 4) {
333 throw Error(ErrorCode::AttributeIndexNotFound, "short payload entry");
334 }
335 std::uint32_t count = 0;
336 for (std::size_t i = 0; i < 4; ++i)
337 count |= static_cast<std::uint32_t>(b[i]) << (8 * i);
338
339 if (b.size() < 4 + static_cast<std::size_t>(count) * 8) {
340 throw Error(ErrorCode::AttributeIndexNotFound, "truncated payload entry");
341 }
342 std::vector<std::uint64_t> out;
343 out.reserve(count);
344 for (std::uint32_t i = 0; i < count; ++i) {
345 out.push_back(read_u64_le(b, 4 + static_cast<std::size_t>(i) * 8));
346 }
347 return out;
348}
349
350void encode_payload_entry(std::vector<std::uint8_t>& out,
351 const std::vector<std::uint64_t>& offsets) {
352 const std::uint32_t count = static_cast<std::uint32_t>(offsets.size());
353 for (int i = 0; i < 4; ++i)
354 out.push_back(static_cast<std::uint8_t>((count >> (8 * i)) & 0xFF));
355 for (std::uint64_t off : offsets)
356 for (int i = 0; i < 8; ++i)
357 out.push_back(static_cast<std::uint8_t>((off >> (8 * i)) & 0xFF));
358}
359
360std::vector<SearchResultItem> stree_query(RangeReader& reader, const AttrIndexInfo& index,
361 KeyKind kind, Operator op, const KeyValue& value) {
362 const std::uint64_t num_nodes = stree_num_nodes(index.num_unique_items, index.branching_factor);
363 const std::uint64_t tree_bytes = detail::checked_mul(num_nodes, entry_size(kind), "stree size");
364 if (tree_bytes > index.length) {
366 "attribute index node region exceeds its declared length");
367 }
368
369 Tree t{reader,
370 index.begin,
371 detail::checked_add(index.begin, tree_bytes, "payload begin"),
372 index.length - tree_bytes,
373 kind,
374 static_cast<std::uint64_t>(index.branching_factor) - 1,
375 stree_level_bounds(index.num_unique_items, index.branching_factor)};
376
377 // Fixed-width string keys are truncated, so ordering AFTER the truncation
378 // point is invisible to the index: two values sharing a 50-byte prefix
379 // compare equal here but may order either way in full. Every string
380 // comparison is therefore widened to include the equal-prefix band, and
381 // select_attr's post-filter applies the real operator to the untruncated
382 // value. Ne in particular must be a FULL scan -- excluding the prefix
383 // matches would drop features whose value merely shares a prefix.
384 const bool is_string =
386
387 switch (op) {
388 case Operator::Eq:
389 // Equal-prefix collisions are candidates, not answers; the
390 // post-filter narrows them.
391 return find_exact(t, value);
392 case Operator::Ge:
393 return scan_range(t, value, false, key_max(kind), false);
394 case Operator::Le:
395 return scan_range(t, key_min(kind), false, value, false);
396 case Operator::Gt:
397 // Strict, except for strings where equal-prefix keys must survive
398 // to be judged on their full value.
399 return scan_range(t, value, !is_string, key_max(kind), false);
400 case Operator::Lt:
401 return scan_range(t, key_min(kind), false, value, !is_string);
402 case Operator::Ne: {
403 if (is_string) {
404 return scan_range(t, key_min(kind), false, key_max(kind), false);
405 }
406 // Two half-open scans rather than a full scan minus the equal set:
407 // subtraction on feature offsets is wrong when one feature carries
408 // several values of the attribute.
409 auto lo = scan_range(t, key_min(kind), false, value, true);
410 auto hi = scan_range(t, value, true, key_max(kind), false);
411 lo.insert(lo.end(), hi.begin(), hi.end());
412 return lo;
413 }
414 }
415 throw Error(ErrorCode::QueryExecutionError, "unknown operator");
416}
417
418} // namespace fcb
Every failure the library reports is one of these.
Definition error.hpp:30
A decoded index key.
Definition key.hpp:40
Synchronous byte-range source.
virtual std::vector< std::uint8_t > read(std::uint64_t offset, std::uint64_t length)=0
Read length bytes at offset, subject to the contract above.
Minimal C++17 stand-in for std::span: a non-owning view over contiguous memory.
Definition span.hpp:13
std::size_t size() const noexcept
Definition span.hpp:26
std::size_t index
Definition geometry.cpp:70
std::uint64_t checked_mul(std::uint64_t a, std::uint64_t b, const char *what="mul")
Definition checked.hpp:24
std::uint64_t checked_add(std::uint64_t a, std::uint64_t b, const char *what="add")
Definition checked.hpp:16
std::uint64_t ceil_div(std::uint64_t a, std::uint64_t b)
ceil(a / b) without the (a + b - 1) overflow hazard.
Definition checked.hpp:34
span< const std::uint8_t > bytes_view
The workhorse alias: a read-only view over bytes.
Definition span.hpp:43
bool is_payload_ref(std::uint64_t off)
Definition stree.hpp:41
std::uint64_t stree_num_nodes(std::uint64_t num_items, std::uint16_t branching_factor)
Total node count.
Definition stree.cpp:313
std::vector< std::uint64_t > decode_payload_entry(bytes_view b)
Decode a payload entry: u32 count then count x u64, all little-endian.
Definition stree.cpp:331
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
std::uint64_t payload_offset(std::uint64_t off)
Definition stree.hpp:42
std::vector< SearchResultItem > stree_query(RangeReader &reader, const AttrIndexInfo &index, KeyKind kind, Operator op, const KeyValue &value)
Run one condition against one column's index blob, returning candidate feature offsets (relative to t...
Definition stree.cpp:360
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
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
KeyValue decode_key(KeyKind kind, bytes_view b)
Definition key.cpp:219
KeyValue key_min(KeyKind kind)
Sentinels used to lower open-ended range queries.
Definition key.cpp:313
Operator
Comparison operators the attribute index supports.
Definition stree.hpp:17
KeyKind kind
Definition stree.cpp:166
std::uint64_t offset
Definition stree.cpp:57
std::uint64_t payload_begin
Definition stree.cpp:164
std::uint64_t node_size
Definition stree.cpp:167
std::uint64_t index_begin
Definition stree.cpp:163
std::uint64_t payload_size
Definition stree.cpp:165
bool found
Definition stree.cpp:102
RangeReader & reader
Definition stree.cpp:162
std::vector< StreeLevelBound > levels
Definition stree.cpp:168
KeyValue key
Definition stree.cpp:56
Where one column's B+tree index lives, and how it is shaped.
Definition header.hpp:35
Half-open [start, end) node index range for one tree level, in the flat node array shared by every le...
Definition stree.hpp:53