This is an automated email from the ASF dual-hosted git repository.
airborne12 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new c31cd7f1cb8 [improvement](be) Read SNII dictionary keys in place in
walks and block opens (#69887)
c31cd7f1cb8 is described below
commit c31cd7f1cb8f3bf4847485baa762ac218a79a9e6
Author: Jack <[email protected]>
AuthorDate: Mon Nov 2 14:15:22 2026 +0800
[improvement](be) Read SNII dictionary keys in place in walks and block
opens (#69887)
### What problem does this PR solve?
Issue Number: None
Related PR: #69600
Problem Summary: A regexp without a literal prefix, such as `output
MATCH_REGEXP 'transi.*'`,
expands by walking every term of the SNII dictionary and testing each
key against the pattern. On
a 99M-row table, about 40% of the SNII stack samples of that query were
spent decoding dictionary
entries, twice as much as the pattern match:
- `visit_prefix_range` and `visit_term_range` decoded every key into a
fresh `DictEntry`: the whole
struct reset, a new `std::string`, and a `Status` for every varint.
- `DictBlockReader::open` fully decoded every anchor of the block, about
1 entry in 16, only to get
its key. Every block open pays for this, exact term lookups included.
Both now read an entry's length and front-coded key on the raw cursor. A
walk rebuilds the key in
one buffer it keeps across entries and a rejected key skips its body by
the entry length; `open()`
reads only the keys of the anchors. An entry that does not read cleanly
goes to the checked
decoders, which report what is wrong with it as before, and a walk still
decodes the body of every
entry it accepts with the checked decoder. The on-disk format and the
write path do not change.
`open()` no longer decodes the bodies of the anchors. The block's
crc32c, verified just before,
covers them, and they are checked when they are decoded, like the body
of every other entry.
| Query (99M rows) | Hot, new/previous | Cold, new/previous |
|---|---|---|
| regexp `'transi.*'` | 0.69 | 0.77 |
| regexp that matches no term | 0.67 | |
| regexp `'.*ing'` | 0.72 | |
| regexp `'e.*'` (stops at the expansion limit) | 0.90 | |
| phrase query, term lookup cache off | 0.78 | |
| phrase query, term lookup cache on | 0.99 | 1.01 |
| AND of two terms | 0.99 | 1.01 |
| the other 22 queries | around 1.0 (reruns of those above 1.05:
0.94-1.03) | |
Hot: both versions interleaved in one BE process. Cold: BE restarted and
the index files evicted
before every run, configuration order alternated, 5 runs each. The build
also had #69879 (the term
lookup cache) and #69882. With that cache off, which is master today,
queries open dictionary
blocks more often: the phrase query gains as the 0.78 row shows, and the
five other lookup-heavy
queries measured that way stayed at 0.96-1.04.
---
.../index/inverted/snii/format/dict_block.cpp | 212 ++++++++++++------
.../index/inverted/snii/format/dict_block_test.cpp | 242 +++++++++++++++++++++
2 files changed, 383 insertions(+), 71 deletions(-)
diff --git a/be/src/storage/index/inverted/snii/format/dict_block.cpp
b/be/src/storage/index/inverted/snii/format/dict_block.cpp
index 750a9f06014..96d76e9cfb7 100644
--- a/be/src/storage/index/inverted/snii/format/dict_block.cpp
+++ b/be/src/storage/index/inverted/snii/format/dict_block.cpp
@@ -19,6 +19,10 @@
#include <algorithm>
#include <atomic>
+#include <limits>
+#include <string>
+#include <string_view>
+#include <utility>
#include "storage/index/inverted/snii/encoding/byte_source.h"
#include "storage/index/inverted/snii/encoding/crc32c.h"
@@ -308,6 +312,107 @@ Status check_flags(uint8_t flags, bool has_positions) {
return Status::OK();
}
+// A LEB128 varint of at most five bytes and at most `max`, on the raw cursor.
Within five bytes
+// nothing is shifted out, so whatever this accepts the checked decoders
accept with the same value;
+// anything else, longer encodings included, returns false and is left to them.
+bool read_short_varint(const uint8_t** p, const uint8_t* end, uint64_t max,
uint64_t* v) {
+ uint64_t value = 0;
+ for (uint32_t shift = 0; shift < 35; shift += 7) {
+ if (*p == end) {
+ return false;
+ }
+ const uint8_t byte = *(*p)++;
+ value |= static_cast<uint64_t>(byte & 0x7F) << shift;
+ if (byte < 0x80) {
+ *v = value;
+ return value <= max;
+ }
+ }
+ return false;
+}
+
+// Where an entry lies in its ByteSource.
+struct EntryExtent {
+ size_t body_start = 0;
+ uint64_t total = 0; // entry_len
+ const uint8_t* end = nullptr;
+};
+
+// Reads one entry's length and front-coded key on the raw cursor, rebuilding
`key` from its
+// previous value in place: no DictEntry, no allocation once `key` has grown,
no Status per field. A
+// walk over a whole dictionary does this for every entry, and opening a block
for every anchor. On
+// anything it does not read cleanly it returns false and changes nothing, and
the caller decodes
+// that entry with the checked decoders, which report what is wrong with it.
+bool read_entry_key_fast(ByteSource* src, std::string* key, EntryExtent*
extent) {
+ const uint8_t* p = src->cursor();
+ const uint8_t* const end = src->end();
+ uint64_t total = 0;
+ if (!read_short_varint(&p, end, std::numeric_limits<uint64_t>::max(),
&total) ||
+ std::cmp_greater(total, end - p)) {
+ return false;
+ }
+ const uint8_t* const body = p;
+ const uint8_t* const entry_end = p + total;
+ uint64_t prefix = 0;
+ uint64_t suffix_len = 0;
+ if (!read_short_varint(&p, entry_end,
std::numeric_limits<uint32_t>::max(), &prefix) ||
+ !read_short_varint(&p, entry_end,
std::numeric_limits<uint32_t>::max(), &suffix_len) ||
+ prefix > key->size() || std::cmp_greater(suffix_len, entry_end - p)) {
+ return false;
+ }
+ key->resize(prefix);
+ key->append(reinterpret_cast<const char*>(p), suffix_len);
+ *extent = {.body_start = src->position() + static_cast<size_t>(body -
src->cursor()),
+ .total = total,
+ .end = entry_end};
+ src->set_cursor(p + suffix_len);
+ return true;
+}
+
+enum class WalkStep { kSkip, kStop, kVisit };
+
+// Walks the entries of `src` in key order. `classify(key)` skips an entry,
stops before it (the
+// range is exhausted: *exhausted = true) or hands it to `on_hit`; only handed
entries have their
+// bodies decoded. The key is rebuilt in one reused buffer, which also carries
the front-coding base
+// from entry to entry (an anchor segment starts from an empty one).
+template <typename Classify>
+Status walk_entries(ByteSource* src, IndexTier tier, const Classify& classify,
+ const std::function<Status(DictEntry&&, bool*)>& on_hit,
bool* exhausted) {
+ std::string key;
+ while (!src->eof()) {
+ EntryExtent extent;
+ const bool fast = read_entry_key_fast(src, &key, &extent);
+ if (!fast) {
+ DictEntry checked;
+ RETURN_IF_ERROR(
+ decode_dict_entry_key(src, key, &checked,
&extent.body_start, &extent.total));
+ key = std::move(checked.term);
+ }
+ const WalkStep step = classify(std::string_view(key));
+ if (step == WalkStep::kStop) {
+ *exhausted = true;
+ return Status::OK();
+ }
+ if (step == WalkStep::kSkip) {
+ if (fast) {
+ src->set_cursor(extent.end);
+ } else {
+ RETURN_IF_ERROR(skip_dict_entry_body(src, extent.body_start,
extent.total));
+ }
+ continue;
+ }
+ DictEntry entry;
+ RETURN_IF_ERROR(decode_dict_entry_rest(src, tier, extent.body_start,
extent.total, &entry));
+ entry.term = key;
+ bool stop = false;
+ RETURN_IF_ERROR(on_hit(std::move(entry), &stop));
+ if (stop) {
+ return Status::OK();
+ }
+ }
+ return Status::OK();
+}
+
} // namespace
//
NOLINTNEXTLINE(readability-function-cognitive-complexity,readability-function-size)
-- Keep invariant-sensitive state transitions together.
@@ -391,10 +496,15 @@ Status DictBlockReader::open(Slice block, IndexTier tier,
bool has_positions,
}
out->anchor_offsets_[i] = off;
// Anchor entries are encoded with prev_term="" and can be decoded
independently to retrieve their term.
+ // Only the key is needed here: the crc above covers the body, which
is checked when it is
+ // decoded, as for any other entry.
ByteSource e_src(covered.subslice(off, anchor_table_begin - off));
- DictEntry probe;
- RETURN_IF_ERROR(decode_dict_entry(&e_src, std::string_view {}, tier,
&probe));
- out->anchor_terms_[i] = std::move(probe.term);
+ EntryExtent extent;
+ if (!read_entry_key_fast(&e_src, &out->anchor_terms_[i], &extent)) {
+ DictEntry probe;
+ RETURN_IF_ERROR(decode_dict_entry(&e_src, std::string_view {},
tier, &probe));
+ out->anchor_terms_[i] = std::move(probe.term);
+ }
}
return Status::OK();
}
@@ -542,7 +652,7 @@ Status DictBlockReader::visit_prefix_range(std::string_view
prefix,
// Scan from the chosen anchor to the end of the entries region: the prefix
// range may span several anchor segments. Anchor entries are encoded with
- // prefix_len=0, so a single running `prev` reconstructs every term
correctly
+ // prefix_len=0, so a single running key reconstructs every term correctly
// even as the scan crosses segment boundaries.
const size_t seg_begin = anchor_offsets_[anchor_idx];
const size_t entries_end =
@@ -553,42 +663,21 @@ Status
DictBlockReader::visit_prefix_range(std::string_view prefix,
}
ByteSource src(block_.subslice(seg_begin, entries_end - seg_begin));
- std::string prev; // the chosen anchor segment starts at an anchor
(prev="")
- while (!src.eof()) {
- DictEntry e;
- size_t body_start = 0;
- uint64_t entry_total = 0;
- RETURN_IF_ERROR(
- decode_dict_entry_key(&src, std::string_view(prev), &e,
&body_start, &entry_total));
- const std::string_view t(e.term);
- if (t < prefix) {
- // Still before the range (only reachable inside the anchor segment
- // that straddles prefix): skip the body, keep the front-coding
base.
- RETURN_IF_ERROR(skip_dict_entry_body(&src, body_start,
entry_total));
- prev = std::move(e.term);
- continue;
- }
- const bool has_prefix = t.size() >= prefix.size() &&
t.starts_with(prefix);
- if (!has_prefix) {
- *prefix_exhausted = true; // sorted: no further matches here or
later
- return Status::OK();
- }
- if (accept_key && !accept_key(t)) {
- // Key-only rejection: never pay for this entry's body.
- RETURN_IF_ERROR(skip_dict_entry_body(&src, body_start,
entry_total));
- prev = std::move(e.term);
- continue;
- }
- // Accepted: materialize the body and hand the entry to the visitor.
- RETURN_IF_ERROR(decode_dict_entry_rest(&src, tier_, body_start,
entry_total, &e));
- prev = e.term; // copy the key before the entry is moved into on_hit
- bool stop = false;
- RETURN_IF_ERROR(on_hit(std::move(e), &stop));
- if (stop) {
- return Status::OK();
- }
- }
- return Status::OK();
+ return walk_entries(
+ &src, tier_,
+ [&](std::string_view term) {
+ // Still before the range: only inside the anchor segment that
straddles prefix.
+ if (term < prefix) {
+ return WalkStep::kSkip;
+ }
+ // Sorted: no further matches here or later.
+ if (!term.starts_with(prefix)) {
+ return WalkStep::kStop;
+ }
+ // Key-only rejection: never pay for this entry's body.
+ return accept_key && !accept_key(term) ? WalkStep::kSkip :
WalkStep::kVisit;
+ },
+ on_hit, prefix_exhausted);
}
Status DictBlockReader::visit_term_range(std::string_view lower_inclusive,
@@ -622,37 +711,18 @@ Status DictBlockReader::visit_term_range(std::string_view
lower_inclusive,
}
ByteSource src(block_.subslice(seg_begin, entries_end - seg_begin));
- std::string prev;
- while (!src.eof()) {
- DictEntry entry;
- size_t body_start = 0;
- uint64_t entry_total = 0;
- RETURN_IF_ERROR(decode_dict_entry_key(&src, std::string_view(prev),
&entry, &body_start,
- &entry_total));
- const std::string_view term(entry.term);
- if (term < lower_inclusive) {
- RETURN_IF_ERROR(skip_dict_entry_body(&src, body_start,
entry_total));
- prev = std::move(entry.term);
- continue;
- }
- if (upper_exclusive.has_value() && term >= *upper_exclusive) {
- *range_exhausted = true;
- return Status::OK();
- }
- if (accept_key && !accept_key(term)) {
- RETURN_IF_ERROR(skip_dict_entry_body(&src, body_start,
entry_total));
- prev = std::move(entry.term);
- continue;
- }
- RETURN_IF_ERROR(decode_dict_entry_rest(&src, tier_, body_start,
entry_total, &entry));
- prev = entry.term;
- bool stop = false;
- RETURN_IF_ERROR(on_hit(std::move(entry), &stop));
- if (stop) {
- return Status::OK();
- }
- }
- return Status::OK();
+ return walk_entries(
+ &src, tier_,
+ [&](std::string_view term) {
+ if (term < lower_inclusive) {
+ return WalkStep::kSkip;
+ }
+ if (upper_exclusive.has_value() && term >= *upper_exclusive) {
+ return WalkStep::kStop;
+ }
+ return accept_key && !accept_key(term) ? WalkStep::kSkip :
WalkStep::kVisit;
+ },
+ on_hit, range_exhausted);
}
} // namespace doris::snii::format
diff --git a/be/test/storage/index/inverted/snii/format/dict_block_test.cpp
b/be/test/storage/index/inverted/snii/format/dict_block_test.cpp
index 1effa3621ee..c4a2ab4f8f8 100644
--- a/be/test/storage/index/inverted/snii/format/dict_block_test.cpp
+++ b/be/test/storage/index/inverted/snii/format/dict_block_test.cpp
@@ -19,7 +19,10 @@
#include <gtest/gtest.h>
+#include <algorithm>
+#include <functional>
#include <limits>
+#include <optional>
#include <string>
#include <string_view>
#include <vector>
@@ -27,6 +30,7 @@
#include "common/status.h"
#include "storage/index/inverted/snii/common/slice.h"
#include "storage/index/inverted/snii/encoding/byte_sink.h"
+#include "storage/index/inverted/snii/encoding/byte_source.h"
#include "storage/index/inverted/snii/encoding/crc32c.h"
#include "storage/index/inverted/snii/format/dict_entry.h"
#include "storage/index/inverted/snii/format/format_constants.h"
@@ -605,3 +609,241 @@ TEST(SniiDictBlockTest,
FinishUnaffectedByDeadPrevTermRemoval) {
EXPECT_EQ(all[i].term, terms[i]) << i; // front-coding base rebuilt
without prev_term_
}
}
+
+namespace {
+
+// Pod-ref and inline entries whose keys run from a few bytes to tens, sharing
long prefixes.
+std::vector<DictEntry> MakeWalkEntries(int count) {
+ std::vector<DictEntry> entries;
+ for (int i = 0; i < count; ++i) {
+ std::string term =
+ "k" + std::string(static_cast<size_t>(i % 7) * 9, 'p') +
std::to_string(100000 + i);
+ entries.push_back(i % 3 == 0 ? MakeInline(std::move(term),
static_cast<uint32_t>(i + 1))
+ : MakePodRef(std::move(term),
static_cast<uint32_t>(i + 1),
+ static_cast<uint64_t>(i) *
10));
+ }
+ std::ranges::sort(entries, {}, &DictEntry::term);
+ return entries;
+}
+
+uint32_t LoadLe32(const std::vector<uint8_t>& bytes, size_t at) {
+ uint32_t v = 0;
+ for (size_t k = 0; k < 4; ++k) {
+ v |= static_cast<uint32_t>(bytes[at + k]) << (8 * k);
+ }
+ return v;
+}
+
+// Re-stamps the crc32c footer after a test edits a block, so that the edit
gets past the crc.
+void RestampCrc(std::vector<uint8_t>* bytes) {
+ const size_t n = bytes->size();
+ const uint32_t crc = doris::snii::crc32c(Slice(bytes->data(), n - 4));
+ for (size_t k = 0; k < 4; ++k) {
+ (*bytes)[n - 4 + k] = static_cast<uint8_t>(crc >> (8 * k));
+ }
+}
+
+// A block of three pod-ref entries that are all anchors, and the offset of
the second one:
+// [entry_len][prefix_len][suffix_len]["bbb"][flags][df][locator].
+std::vector<uint8_t> BuildAnchorsBlock(size_t* second) {
+ const std::vector<DictEntry> entries = {MakePodRef("aaa", 1, 0),
MakePodRef("bbb", 2, 10),
+ MakePodRef("ccc", 3, 20)};
+ std::vector<uint8_t> bytes = BuildBlock(entries, IndexTier::kT1, false,
100, 0, 1);
+ *second = LoadLe32(bytes, bytes.size() - 8 - 2 * 4); // [offsets u32 x
3][n_anchors][crc32c]
+ return bytes;
+}
+
+// The walk as it was before keys were read on a raw cursor: every entry
through the checked
+// decoders. The walks must report exactly what this reports, on valid and
corrupt blocks alike.
+Status CheckedWalk(Slice entries, std::optional<std::string_view> upper,
+ const std::function<bool(std::string_view)>& accept,
+ std::vector<std::string>* visited, bool* exhausted) {
+ ByteSource src(entries);
+ std::string prev;
+ while (!src.eof()) {
+ DictEntry entry;
+ size_t body_start = 0;
+ uint64_t total = 0;
+ RETURN_IF_ERROR(decode_dict_entry_key(&src, prev, &entry, &body_start,
&total));
+ if (upper.has_value() && entry.term >= *upper) {
+ *exhausted = true;
+ return Status::OK();
+ }
+ if (accept(entry.term)) {
+ RETURN_IF_ERROR(
+ decode_dict_entry_rest(&src, IndexTier::kT2, body_start,
total, &entry));
+ visited->push_back(entry.term);
+ } else {
+ RETURN_IF_ERROR(skip_dict_entry_body(&src, body_start, total));
+ }
+ prev = std::move(entry.term);
+ }
+ return Status::OK();
+}
+
+// A walk's on_hit that keeps every entry it is handed.
+std::function<Status(DictEntry&&, bool*)> Collect(std::vector<DictEntry>* got)
{
+ return [got](DictEntry&& entry, bool*) {
+ got->push_back(std::move(entry));
+ return Status::OK();
+ };
+}
+
+// `got` must be the entries of `all` that `wanted` picks, in order and with
their bodies.
+void ExpectVisited(const std::vector<DictEntry>& all, const
std::vector<DictEntry>& got,
+ const std::function<bool(const DictEntry&)>& wanted) {
+ size_t next = 0;
+ for (const DictEntry& want : all) {
+ if (!wanted(want)) {
+ continue;
+ }
+ ASSERT_LT(next, got.size());
+ ExpectCommon(got[next], want);
+ EXPECT_EQ(got[next].frq_off_delta, want.frq_off_delta);
+ EXPECT_EQ(got[next].frq_bytes, want.frq_bytes);
+ ++next;
+ }
+ EXPECT_EQ(next, got.size());
+}
+
+// Walks the block both ways and compares what they report; counts the walks
that found corruption.
+void ExpectWalkAgrees(const DictBlockReader& reader, Slice entries,
+ std::optional<std::string_view> upper,
+ const std::function<bool(std::string_view)>& accept,
size_t* corrupt) {
+ std::vector<std::string> want;
+ bool want_exhausted = false;
+ const Status want_st = CheckedWalk(entries, upper, accept, &want,
&want_exhausted);
+ std::vector<DictEntry> got;
+ bool got_exhausted = false;
+ const Status got_st = reader.visit_term_range("", upper, accept,
Collect(&got), &got_exhausted);
+ EXPECT_EQ(got_st.to_string(), want_st.to_string());
+ ASSERT_EQ(got.size(), want.size());
+ for (size_t i = 0; i < got.size(); ++i) {
+ EXPECT_EQ(got[i].term, want[i]);
+ }
+ EXPECT_EQ(got_exhausted, want_exhausted);
+ *corrupt += want_st.ok() ? 0 : 1;
+}
+
+} // namespace
+
+// The walks rebuild each key in one reused buffer. Short and long keys, long
prefixes shared across
+// anchors, and inline and pod-ref bodies all come out as decode_all() reads
them, for any prefix,
+// range and key filter.
+TEST(SniiDictBlock, WalksMatchDecodeAll) {
+ const std::vector<uint8_t> bytes =
+ BuildBlock(MakeWalkEntries(300), IndexTier::kT2, true, 8192,
16384, 8);
+ DictBlockReader reader;
+ ASSERT_TRUE(DictBlockReader::open(Slice(bytes), IndexTier::kT2, true,
&reader).ok());
+ std::vector<DictEntry> all;
+ ASSERT_TRUE(reader.decode_all(&all).ok());
+ ASSERT_EQ(all.size(), 300U);
+
+ const std::vector<std::function<bool(std::string_view)>> filters = {
+ {},
+ [](std::string_view term) { return !term.empty() && term.back() %
2 == 0; },
+ [](std::string_view term) { return term.size() > 20; },
+ [](std::string_view) { return false; }};
+ for (const std::string_view prefix :
+ {"", "k", "kppppppppp", "kpppppppppppppppppp1002", "k1001", "zzz"}) {
+ for (size_t f = 0; f < filters.size(); ++f) {
+ SCOPED_TRACE(std::string(prefix) + " filter " + std::to_string(f));
+ const auto& filter = filters[f];
+ std::vector<DictEntry> got;
+ bool exhausted = false;
+ ASSERT_TRUE(reader.visit_prefix_range(prefix, filter,
Collect(&got), &exhausted).ok());
+ ExpectVisited(all, got, [&](const DictEntry& entry) {
+ return entry.term.starts_with(prefix) && (!filter ||
filter(entry.term));
+ });
+ }
+ }
+
+ const std::string& lower = all[37].term;
+ const std::string& upper = all[201].term;
+ std::vector<DictEntry> got;
+ bool exhausted = false;
+ ASSERT_TRUE(reader.visit_term_range(lower, std::string_view(upper),
filters[1], Collect(&got),
+ &exhausted)
+ .ok());
+ ExpectVisited(all, got, [&](const DictEntry& entry) {
+ return entry.term >= lower && entry.term < upper &&
filters[1](entry.term);
+ });
+ EXPECT_TRUE(exhausted);
+}
+
+// Every one-byte change to the entries of a block, with the crc re-stamped so
that open() accepts
+// it, leaves a walk reporting exactly what a walk over the checked decoders
reports: the same
+// status, keys and end of range. An entry whose length or key the raw-cursor
reader cannot read
+// cleanly goes through the checked decoders, so its corruption is still
reported.
+TEST(SniiDictBlock, WalksAgreeWithCheckedDecodersOnCorruptBlocks) {
+ const std::vector<DictEntry> entries = MakeWalkEntries(24);
+ const std::vector<uint8_t> good = BuildBlock(entries, IndexTier::kT2,
true, 8192, 16384, 16);
+ // Tail: [anchor offsets u32 x n][n_anchors u32][crc32c u32]; the first
offset starts the entries.
+ const size_t n = good.size();
+ const size_t entries_end = n - 8 - 4 * static_cast<size_t>(LoadLe32(good,
n - 8));
+ const size_t entries_begin = LoadLe32(good, entries_end);
+ const std::optional<std::string_view> upper =
std::string_view(entries[19].term);
+ const std::vector<std::function<bool(std::string_view)>> filters = {
+ [](std::string_view) { return true; }, [](std::string_view) {
return false; },
+ [](std::string_view term) { return !term.empty() && term.back() %
2 == 0; }};
+
+ size_t corrupt = 0;
+ for (size_t at = entries_begin; at < entries_end; ++at) {
+ // A changed low bit, a larger value, and a varint that runs on into
the next byte.
+ for (const uint8_t mask : {0x01, 0x40, 0x80}) {
+ std::vector<uint8_t> bytes = good;
+ bytes[at] ^= mask;
+ RestampCrc(&bytes);
+ DictBlockReader reader;
+ if (!DictBlockReader::open(Slice(bytes), IndexTier::kT2, true,
&reader).ok()) {
+ continue; // open() decodes the anchor entries itself
+ }
+ SCOPED_TRACE("byte " + std::to_string(at) + " ^ " +
std::to_string(mask));
+ const Slice region(bytes.data() + entries_begin, entries_end -
entries_begin);
+ for (const auto& filter : filters) {
+ ExpectWalkAgrees(reader, region, std::nullopt, filter,
&corrupt);
+ ExpectWalkAgrees(reader, region, upper, filter, &corrupt);
+ }
+ ASSERT_FALSE(HasFailure());
+ }
+ }
+ EXPECT_GT(corrupt, 0U); // the changes do reach the checked decoders'
errors
+}
+
+// open() reads only the key of each anchor, and an anchor key it cannot read
is still an error.
+TEST(SniiDictBlock, OpenRejectsUnreadableAnchorKeys) {
+ size_t second = 0;
+ const std::vector<uint8_t> good = BuildAnchorsBlock(&second);
+ ASSERT_EQ(good[second + 1], 0); // an anchor has no previous key to share
a prefix with
+
+ auto expect_rejected = [&](size_t at, uint8_t value, const char* what) {
+ std::vector<uint8_t> bytes = good;
+ bytes[at] = value;
+ RestampCrc(&bytes);
+ DictBlockReader reader;
+ const Status st = DictBlockReader::open(Slice(bytes), IndexTier::kT1,
false, &reader);
+ EXPECT_TRUE(st.is<doris::ErrorCode::INVERTED_INDEX_FILE_CORRUPTED>())
+ << what << ": " << st.to_string();
+ };
+ expect_rejected(second + 1, 1, "front-coded anchor");
+ expect_rejected(second, 0x7F, "entry past the entries");
+ expect_rejected(second, 1, "key past its entry");
+ expect_rejected(second + 2, 0x7F, "key past the entries");
+}
+
+// An anchor's body is checked when it is decoded, as any other entry's:
open() does not read it.
+TEST(SniiDictBlock, AnchorBodyIsCheckedWhenDecoded) {
+ size_t second = 0;
+ std::vector<uint8_t> bytes = BuildAnchorsBlock(&second);
+ bytes[second + 7] |= 0x80; // df runs on into the locator
+ RestampCrc(&bytes);
+
+ DictBlockReader reader;
+ ASSERT_TRUE(DictBlockReader::open(Slice(bytes), IndexTier::kT1, false,
&reader).ok());
+ bool found = false;
+ DictEntry entry;
+ ASSERT_TRUE(reader.find_term("aaa", &found, &entry).ok());
+ EXPECT_TRUE(found);
+ const Status st = reader.find_term("bbb", &found, &entry);
+ EXPECT_TRUE(st.is<doris::ErrorCode::INVERTED_INDEX_FILE_CORRUPTED>()) <<
st.to_string();
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]