Skip to main content

yo_kv/
hlls.rs

1//! The HyperLogLog commands, which are string commands over a documented value.
2//!
3//! Everything about the sketch itself is in [`hll`]. This file is where a key
4//! turns into one, where Redis's edges are kept and where the writes happen.
5//!
6//! There is no HyperLogLog type here for the same reason there is no bitmap
7//! type: in Redis there is not one either. A sketch is an ordinary string, `GET`
8//! hands the bytes to a client, `SET` takes them back, and `TYPE` says `string`.
9//! What makes these commands different from the other string commands is that
10//! they refuse a string that is not a sketch, with their own sentence, and that
11//! is the whole of the type discipline.
12//!
13//! Three edges, all measured on 8.10.1 rather than reasoned about.
14//!
15//! `PFCOUNT` is a `readonly` command that writes. The estimate is expensive
16//! enough that Redis caches it in the eight header bytes, so the first `PFCOUNT`
17//! after a `PFADD` walks the registers and every one after it reads eight bytes,
18//! and a client watching `GET` can see the header change under a read. We do the
19//! same, in place, without marking the key dirty.
20//!
21//! `PFADD` with no elements still creates the key and still answers 1, and the
22//! sketch it leaves behind has its cache marked stale even though nothing was
23//! added to it. A second `PFADD` of an element already in the sketch answers 0
24//! and leaves the bytes exactly as they were.
25//!
26//! `PFDEBUG GETREG` converts the sketch to dense and leaves it that way. It is
27//! a debugging command and it is allowed to, but it means a client cannot look
28//! at the registers of a sparse sketch without changing it.
29
30use crate::hll::{self, Encoding};
31use crate::keyspace::Keyspace;
32use crate::strings::check_len;
33use crate::value::{self, Str};
34use yo_common::{Code, Error, Result};
35use yo_index::RawMap;
36
37/// What Redis says about a `PFDEBUG` aimed at a key that is not there.
38const NO_SUCH_KEY: &str = "The specified key does not exist";
39/// What Redis says about `PFDEBUG DECODE` on a dense sketch.
40const NOT_SPARSE: &str = "HLL encoding is not sparse";
41
42impl Keyspace {
43    /// `PFADD key [element ...]`, answering whether anything changed.
44    ///
45    /// Creating the key counts as a change, so `PFADD fresh` with no elements
46    /// answers 1 and `PFADD fresh` again answers 0. An element that lands in a
47    /// register already holding at least as large a count is not a change
48    /// either, and in that case the value is not rewritten at all.
49    pub fn pfadd<'e, I>(&mut self, key: &[u8], eles: I) -> Result<bool>
50    where
51        I: Iterator<Item = &'e [u8]>,
52    {
53        self.reap(key);
54        self.string_only(key)?;
55        self.thaw(key)?;
56        check_len(key, hll::DENSE)?;
57
58        let mut buf = std::mem::take(&mut self.scratch);
59        buf.clear();
60        let (deadline, fresh) = match self.map.get(key) {
61            Some(rec) => {
62                value::read(rec).write_to(&mut buf);
63                (value::expire_at(rec), false)
64            }
65            None => (None, true),
66        };
67        if fresh {
68            hll::empty(&mut buf);
69        }
70
71        let outcome = 'work: {
72            if let Err(e) = hll::check(&buf) {
73                break 'work Err(e);
74            }
75            let mut changed = fresh;
76            for ele in eles {
77                let (index, count) = hll::place(ele);
78                match hll::set(&mut buf, index, count) {
79                    Some(hit) => changed |= hit,
80                    None => break 'work Err(hll::corrupt()),
81                }
82            }
83            Ok(changed)
84        };
85
86        // Only a change is written back, which is what makes a `PFADD` of an
87        // element that is already in the sketch free. The invalidation is here
88        // and not in the sketch because Redis puts it here too: a `PFADD` that
89        // creates an empty sketch and adds nothing to it still marks the cache
90        // stale on the way out.
91        if matches!(outcome, Ok(true)) {
92            hll::invalidate(&mut buf);
93            self.store_raw(key, &buf, deadline);
94        }
95        self.scratch = buf;
96        outcome
97    }
98
99    /// `PFCOUNT key [key ...]`.
100    ///
101    /// One key answers out of the header cache when it is good and fills it in
102    /// when it is not. Several keys are merged into one set of registers first,
103    /// and that answer is never cached, because there is nowhere to put it: the
104    /// union of two sketches is not a key.
105    ///
106    /// A key that is not there counts as an empty sketch rather than an error,
107    /// so `PFCOUNT missing` is 0 and a missing key among several is skipped.
108    pub fn pfcount<'k, I>(&mut self, keys: I) -> Result<u64>
109    where
110        I: Iterator<Item = &'k [u8]> + Clone,
111    {
112        for key in keys.clone() {
113            self.reap(key);
114            self.string_only(key)?;
115            // Every sketch is read in one pass below, borrowed out of the map
116            // at the same time, so they all have to be in memory rather than in
117            // the one buffer a served fault uses. A sketch is at most twelve
118            // kibibytes and a client counting them is going to count them
119            // again, so bringing them back is what it wanted anyway.
120            self.thaw(key)?;
121        }
122        let mut one = keys.clone();
123        if let (Some(key), None) = (one.next(), one.next()) {
124            return self.count_one(key);
125        }
126
127        // Sixteen kibibytes of registers on the stack, which is what Redis puts
128        // there for the same job. It does not go in the scratch buffer because
129        // the sketches being read are borrowed out of the map and the buffer is
130        // where a write would want to build its own copy.
131        let mut max = [0u8; hll::REGISTERS];
132        for key in keys {
133            let Some(bytes) = self.sketch(key)? else {
134                continue;
135            };
136            let enc = hll::check(bytes)?;
137            if !hll::merge(&mut max, bytes, enc) {
138                return Err(hll::corrupt());
139            }
140        }
141        let mut hist = [0u32; 64];
142        for &val in &max {
143            hist[val as usize] += 1;
144        }
145        Ok(hll::estimate(&hist))
146    }
147
148    /// `PFMERGE dest [source ...]`.
149    ///
150    /// The destination is one of the sources, so a merge never loses what was
151    /// already there, and `PFMERGE dest` with no sources at all is a no-op that
152    /// still answers OK. A destination that is not there is created.
153    ///
154    /// The result stays sparse when every input was sparse and it fits, which is
155    /// what a real server does: merging two hundred element sketches leaves a
156    /// two hundred and seventy nine byte one, not a dense one.
157    pub fn pfmerge<'k, I>(&mut self, dest: &'k [u8], srcs: I) -> Result<()>
158    where
159        I: Iterator<Item = &'k [u8]> + Clone,
160    {
161        self.reap(dest);
162        self.string_only(dest)?;
163        self.thaw(dest)?;
164        check_len(dest, hll::DENSE)?;
165        for src in srcs.clone() {
166            self.reap(src);
167            self.string_only(src)?;
168            self.thaw(src)?;
169        }
170
171        // Every input is read before anything is written, the destination
172        // included, because the write wants the database back and the sources
173        // are borrowed out of it.
174        let mut max = [0u8; hll::REGISTERS];
175        let mut dense = false;
176        for key in std::iter::once(dest).chain(srcs) {
177            let Some(bytes) = self.sketch(key)? else {
178                continue;
179            };
180            let enc = hll::check(bytes)?;
181            dense |= enc == Encoding::Dense;
182            if !hll::merge(&mut max, bytes, enc) {
183                return Err(hll::corrupt());
184            }
185        }
186
187        let mut buf = std::mem::take(&mut self.scratch);
188        buf.clear();
189        let deadline = match self.map.get(dest) {
190            Some(rec) => {
191                value::read(rec).write_to(&mut buf);
192                value::expire_at(rec)
193            }
194            None => {
195                hll::empty(&mut buf);
196                None
197            }
198        };
199
200        let outcome = 'work: {
201            // A dense input makes the result dense whatever the destination was,
202            // since the registers are coming from something that already needed
203            // the room. Everything else goes in one register at a time and turns
204            // dense on its own if it has to.
205            if dense && !hll::to_dense(&mut buf) {
206                break 'work Err(hll::corrupt());
207            }
208            for (i, &val) in max.iter().enumerate() {
209                if val != 0 && hll::set(&mut buf, i, val).is_none() {
210                    break 'work Err(hll::corrupt());
211                }
212            }
213            Ok(())
214        };
215
216        if outcome.is_ok() {
217            hll::invalidate(&mut buf);
218            self.store_raw(dest, &buf, deadline);
219        }
220        self.scratch = buf;
221        outcome
222    }
223
224    /// `PFDEBUG GETREG key`, which converts the sketch to dense first.
225    ///
226    /// The conversion is Redis's and it is not a side effect worth hiding: the
227    /// registers of a sparse sketch cannot be handed out one at a time without
228    /// walking the opcodes for each, so the debugging command that wants all
229    /// 16384 of them converts once and leaves it converted.
230    pub fn pfgetreg(&mut self, key: &[u8], regs: &mut [u8; hll::REGISTERS]) -> Result<()> {
231        self.pftodense(key)?;
232        let bytes = self.sketch(key)?.ok_or_else(no_such_key)?;
233        let body = &bytes[hll::HDR..];
234        for (i, slot) in regs.iter_mut().enumerate() {
235            *slot = hll::dense_get(body, i);
236        }
237        Ok(())
238    }
239
240    /// `PFDEBUG TODENSE key`, answering whether it had to convert anything.
241    pub fn pftodense(&mut self, key: &[u8]) -> Result<bool> {
242        self.reap(key);
243        self.string_only(key)?;
244        self.thaw(key)?;
245        let bytes = self.sketch(key)?.ok_or_else(no_such_key)?;
246        if hll::check(bytes)? == Encoding::Dense {
247            return Ok(false);
248        }
249
250        let mut buf = std::mem::take(&mut self.scratch);
251        buf.clear();
252        let deadline = match self.map.get(key) {
253            Some(rec) => {
254                value::read(rec).write_to(&mut buf);
255                value::expire_at(rec)
256            }
257            None => None,
258        };
259        let outcome = if hll::to_dense(&mut buf) {
260            self.store_raw(key, &buf, deadline);
261            Ok(true)
262        } else {
263            Err(hll::corrupt())
264        };
265        self.scratch = buf;
266        outcome
267    }
268
269    /// `PFDEBUG ENCODING key`, which is `sparse` or `dense`.
270    pub fn pfencoding(&mut self, key: &[u8]) -> Result<Encoding> {
271        self.reap(key);
272        self.string_only(key)?;
273        self.warm(key)?;
274        let bytes = self.sketch(key)?.ok_or_else(no_such_key)?;
275        hll::check(bytes)
276    }
277
278    /// `PFDEBUG DECODE key`, handing the opcodes to `run` as one line of text.
279    ///
280    /// The text goes into the scratch buffer and is lent out rather than
281    /// returned, the way [`Keyspace::bitfield_with`] lends its value out, so that
282    /// a debugging command does not allocate on a shard thread.
283    pub fn pfdecode<T>(&mut self, key: &[u8], run: impl FnOnce(&[u8]) -> T) -> Result<T> {
284        self.reap(key);
285        self.string_only(key)?;
286        self.warm(key)?;
287
288        // The buffer comes out of `self` before the sketch is read out of it, so
289        // that the text is being written into something the map does not own and
290        // the two borrows never meet.
291        let mut buf = std::mem::take(&mut self.scratch);
292        buf.clear();
293        let outcome = 'work: {
294            let bytes = match self.sketch(key) {
295                Ok(Some(bytes)) => bytes,
296                Ok(None) => break 'work Err(no_such_key()),
297                Err(e) => break 'work Err(e),
298            };
299            match hll::check(bytes) {
300                Ok(Encoding::Sparse) => hll::decode(bytes, &mut buf),
301                Ok(Encoding::Dense) => break 'work Err(Error::new(Code::Invalid, NOT_SPARSE)),
302                Err(e) => break 'work Err(e),
303            }
304            Ok(())
305        };
306        let out = outcome.map(|()| run(&buf));
307        self.scratch = buf;
308        out
309    }
310
311    /// The estimate for one key, out of the cache or into it.
312    fn count_one(&mut self, key: &[u8]) -> Result<u64> {
313        let Some(bytes) = self.sketch(key)? else {
314            return Ok(0);
315        };
316        let enc = hll::check(bytes)?;
317        if let Some(n) = hll::cached(bytes) {
318            return Ok(n);
319        }
320        let n = hll::count(bytes, enc)?;
321
322        // Written back in place, which is why `PFCOUNT` can be `readonly` and
323        // still leave the key's bytes different from how it found them. A sketch
324        // is always stored raw, so the fast path is the only path.
325        let hash = RawMap::hash_of(key);
326        if let Some(rec) = self.map.value_mut_hashed(hash, key)
327            && let Some(val) = value::raw_in_place(rec)
328        {
329            hll::cache(val, n);
330        }
331        Ok(n)
332    }
333
334    /// The bytes of a key, if it holds one, refusing a string that is not one.
335    ///
336    /// A missing key answers `None`, which every one of these commands treats as
337    /// an empty sketch. An int encoded string is refused rather than read as its
338    /// digits, since four digits cannot be a sketch and the sentence a client
339    /// wants is the one about a HyperLogLog.
340    fn sketch(&self, key: &[u8]) -> Result<Option<&[u8]>> {
341        match self.peek(key) {
342            None => Ok(None),
343            Some(Str::Bytes(b)) => Ok(Some(b)),
344            Some(Str::Int(_)) => Err(hll::not_hll()),
345        }
346    }
347}
348
349/// What `PFDEBUG` says about a key that is not there.
350fn no_such_key() -> Error {
351    Error::new(Code::NotFound, NO_SUCH_KEY)
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357    use crate::keyspace::Keyspace;
358
359    fn db() -> Keyspace {
360        Keyspace::new()
361    }
362
363    /// The key or element list these commands take, out of a test's own names.
364    fn names<'k>(list: &'k [&'k [u8]]) -> impl Iterator<Item = &'k [u8]> + Clone {
365        list.iter().copied()
366    }
367
368    /// What `GET` would hand a client, which is the whole point of the format.
369    fn bytes(db: &mut Keyspace, key: &[u8]) -> Vec<u8> {
370        db.get(key).expect("a value").expect("bytes").to_vec()
371    }
372
373    /// The three element sketch a real 8.10.1 writes, byte for byte.
374    #[test]
375    fn a_sketch_a_client_reads_is_the_one_a_real_server_wrote() {
376        let mut db = db();
377        assert!(db.pfadd(b"h", names(&[b"a", b"b", b"c"])).expect("an add"));
378        assert_eq!(
379            bytes(&mut db, b"h"),
380            b"HYLL\x01\0\0\0\0\0\0\0\0\0\0\x80\x60\xf3\x80\x50\xb1\x84\x4b\xfb\x80\x42\x5a"
381        );
382        assert_eq!(db.pfcount(names(&[b"h"])).expect("a count"), 3);
383        // Which a client can see: a `PFCOUNT` writes its answer into the header.
384        assert_eq!(bytes(&mut db, b"h")[8..16], [3, 0, 0, 0, 0, 0, 0, 0]);
385    }
386
387    /// An empty add creates the key and answers 1, and a second one answers 0.
388    #[test]
389    fn adding_nothing_still_creates_the_key() {
390        let mut db = db();
391        assert!(db.pfadd(b"h", names(&[])).expect("an add"));
392        assert!(db.exists(b"h"));
393        assert_eq!(db.strlen(b"h").expect("a length"), 18);
394        assert!(!db.pfadd(b"h", names(&[])).expect("an add"));
395        assert_eq!(db.pfcount(names(&[b"h"])).expect("a count"), 0);
396    }
397
398    /// An element already in the sketch changes neither the answer nor the bytes.
399    #[test]
400    fn adding_an_element_twice_leaves_the_value_alone() {
401        let mut db = db();
402        db.pfadd(b"h", names(&[b"a"])).expect("an add");
403        let before = bytes(&mut db, b"h");
404        assert!(!db.pfadd(b"h", names(&[b"a"])).expect("an add"));
405        assert_eq!(bytes(&mut db, b"h"), before);
406    }
407
408    /// The numbers a real server gives for the same elements, at three sizes.
409    #[test]
410    fn the_count_is_the_number_a_real_server_gives() {
411        for (n, want) in [(100usize, 100u64), (1000, 995), (10_000, 10_077)] {
412            let mut db = db();
413            for i in 0..n {
414                let ele = format!("e:{i}");
415                db.pfadd(b"h", names(&[ele.as_bytes()])).expect("an add");
416            }
417            assert_eq!(db.pfcount(names(&[b"h"])).expect("a count"), want, "{n}");
418        }
419    }
420
421    /// The two sizes the milestone gate names.
422    #[test]
423    fn a_sketch_is_sparse_until_it_is_not() {
424        let mut db = db();
425        for i in 0..1000 {
426            let ele = format!("e:{i}");
427            db.pfadd(b"k1", names(&[ele.as_bytes()])).expect("an add");
428        }
429        assert_eq!(db.strlen(b"k1").expect("a length"), 1880);
430        assert_eq!(db.pfencoding(b"k1").expect("an encoding"), Encoding::Sparse);
431        const { assert!(1880 <= hll::SPARSE_MAX) };
432
433        for i in 0..10_000 {
434            let ele = format!("e:{i}");
435            db.pfadd(b"k2", names(&[ele.as_bytes()])).expect("an add");
436        }
437        assert_eq!(db.strlen(b"k2").expect("a length"), 12304);
438        assert_eq!(db.pfencoding(b"k2").expect("an encoding"), Encoding::Dense);
439    }
440
441    /// Counting several keys is counting their union, and never caches.
442    ///
443    /// The three numbers are a real server's for the same elements, and not one
444    /// of them is the true count: 150 distinct elements are counted as 151 and
445    /// 50 as 49. That is what a HyperLogLog is, and agreeing with Redis about
446    /// which way it is wrong is the thing being tested.
447    #[test]
448    fn counting_several_keys_counts_their_union() {
449        let mut db = db();
450        for i in 0..200 {
451            let ele = format!("e:{i}");
452            let key: &[u8] = if i < 150 { b"a" } else { b"b" };
453            db.pfadd(key, names(&[ele.as_bytes()])).expect("an add");
454        }
455        assert_eq!(db.pfcount(names(&[b"a"])).expect("a count"), 151);
456        assert_eq!(db.pfcount(names(&[b"b"])).expect("a count"), 49);
457        assert_eq!(db.pfcount(names(&[b"a", b"b"])).expect("a count"), 199);
458        // A key that is not there is an empty sketch and not an error.
459        assert_eq!(db.pfcount(names(&[b"gone"])).expect("a count"), 0);
460        assert_eq!(db.pfcount(names(&[b"a", b"gone"])).expect("a count"), 151);
461        assert!(!db.exists(b"gone"));
462    }
463
464    /// A merge keeps what the destination had and stays sparse when it can.
465    #[test]
466    fn a_merge_is_a_union_and_keeps_the_smaller_form() {
467        let mut db = db();
468        for i in 0..100 {
469            let ele = format!("e:{i}");
470            db.pfadd(b"s1", names(&[ele.as_bytes()])).expect("an add");
471            db.pfadd(b"s2", names(&[ele.as_bytes()])).expect("an add");
472        }
473        db.pfmerge(b"m", names(&[b"s1", b"s2"])).expect("a merge");
474        assert_eq!(db.strlen(b"m").expect("a length"), 279);
475        assert_eq!(db.pfencoding(b"m").expect("an encoding"), Encoding::Sparse);
476        assert_eq!(db.pfcount(names(&[b"m"])).expect("a count"), 100);
477
478        // The destination is one of the sources, so nothing is ever lost.
479        for i in 100..200 {
480            let ele = format!("e:{i}");
481            db.pfadd(b"s3", names(&[ele.as_bytes()])).expect("an add");
482        }
483        db.pfmerge(b"m", names(&[b"s3"])).expect("a merge");
484        assert_eq!(db.pfcount(names(&[b"m"])).expect("a count"), 199);
485        assert_eq!(db.strlen(b"m").expect("a length"), 499);
486
487        // And a merge with no sources at all leaves it exactly as it was.
488        let before = bytes(&mut db, b"m");
489        db.pfmerge(b"m", names(&[])).expect("a merge");
490        assert_eq!(bytes(&mut db, b"m")[..8], before[..8]);
491        assert_eq!(db.pfcount(names(&[b"m"])).expect("a count"), 199);
492    }
493
494    /// A dense source makes the result dense, whatever the destination was.
495    #[test]
496    fn a_dense_source_makes_the_result_dense() {
497        let mut db = db();
498        for i in 0..100 {
499            let ele = format!("e:{i}");
500            db.pfadd(b"small", names(&[ele.as_bytes()]))
501                .expect("an add");
502        }
503        for i in 0..20_000 {
504            let ele = format!("e:{i}");
505            db.pfadd(b"big", names(&[ele.as_bytes()])).expect("an add");
506        }
507        db.pfmerge(b"m", names(&[b"small", b"big"]))
508            .expect("a merge");
509        assert_eq!(db.strlen(b"m").expect("a length"), 12304);
510        assert_eq!(db.pfencoding(b"m").expect("an encoding"), Encoding::Dense);
511        assert_eq!(db.pfcount(names(&[b"m"])).expect("a count"), 20096);
512    }
513
514    /// Every one of these keeps whatever deadline the key had.
515    #[test]
516    fn a_write_keeps_the_deadline() {
517        let mut db = db();
518        let mut fresh = Vec::new();
519        hll::empty(&mut fresh);
520        db.setex(b"h", 100, &fresh).expect("a set");
521        let had = db.expire_at(b"h").expect("a deadline");
522        db.pfadd(b"h", names(&[b"a"])).expect("an add");
523        assert_eq!(db.expire_at(b"h"), Some(had));
524        db.pfmerge(b"h", names(&[])).expect("a merge");
525        assert_eq!(db.expire_at(b"h"), Some(had));
526        db.pftodense(b"h").expect("a conversion");
527        assert_eq!(db.expire_at(b"h"), Some(had));
528        assert_eq!(db.pfcount(names(&[b"h"])).expect("a count"), 1);
529    }
530
531    /// The debugging commands, including the one that changes what it looks at.
532    #[test]
533    fn the_debug_commands_say_what_a_real_server_says() {
534        let mut db = db();
535        db.pfadd(b"h", names(&[b"a", b"b", b"c"])).expect("an add");
536        let text = db.pfdecode(b"h", <[u8]>::to_vec).expect("a decode");
537        assert_eq!(text, b"Z:8436 v:1,1 Z:4274 v:2,1 Z:3068 v:1,1 Z:603");
538
539        // Which is the three registers a real server has, and reading them
540        // converts the sketch and leaves it converted.
541        let mut regs = [0u8; hll::REGISTERS];
542        db.pfgetreg(b"h", &mut regs).expect("the registers");
543        assert_eq!(regs[8436], 1);
544        assert_eq!(regs[12711], 2);
545        assert_eq!(regs[15780], 1);
546        assert_eq!(regs.iter().filter(|&&v| v != 0).count(), 3);
547        assert_eq!(db.pfencoding(b"h").expect("an encoding"), Encoding::Dense);
548        assert_eq!(db.strlen(b"h").expect("a length"), 12304);
549        assert_eq!(db.pfcount(names(&[b"h"])).expect("a count"), 3);
550
551        // A converted sketch cannot be decoded and does not convert twice.
552        assert!(db.pfdecode(b"h", <[u8]>::to_vec).is_err());
553        assert!(!db.pftodense(b"h").expect("a conversion"));
554    }
555
556    /// A string that is not a sketch, and a key that is not a string.
557    #[test]
558    fn a_string_that_is_not_a_sketch_is_refused() {
559        let mut db = db();
560        db.set_plain(b"plain", b"not a sketch at all")
561            .expect("a set");
562        assert!(db.pfadd(b"plain", names(&[b"a"])).is_err());
563        assert!(db.pfcount(names(&[b"plain"])).is_err());
564        assert!(db.pfmerge(b"plain", names(&[])).is_err());
565        assert!(db.pfencoding(b"plain").is_err());
566        // An int encoded string is refused too, rather than read as its digits.
567        db.set_plain(b"n", b"12345").expect("a set");
568        assert!(db.pfcount(names(&[b"n"])).is_err());
569
570        // A key that is not there is not an error for the three real commands
571        // and is one for every `PFDEBUG` form.
572        assert!(db.pfencoding(b"gone").is_err());
573        assert!(db.pftodense(b"gone").is_err());
574        assert!(db.pfdecode(b"gone", <[u8]>::to_vec).is_err());
575        let mut regs = [0u8; hll::REGISTERS];
576        assert!(db.pfgetreg(b"gone", &mut regs).is_err());
577    }
578
579    /// A sketch whose opcodes do not add up says so rather than answering.
580    #[test]
581    fn a_corrupted_sketch_is_reported() {
582        let mut db = db();
583        db.pfadd(b"h", names(&[b"a", b"b", b"c"])).expect("an add");
584        let mut short = bytes(&mut db, b"h");
585        short.pop();
586        db.set_plain(b"h", &short).expect("a set");
587        let err = db.pfcount(names(&[b"h"])).expect_err("a complaint");
588        assert_eq!(err.code(), Code::Corrupt);
589        assert!(db.pfcount(names(&[b"h", b"h"])).is_err());
590        assert!(db.pfmerge(b"m", names(&[b"h"])).is_err());
591    }
592}