Skip to main content

yo_kv/
set.rs

1//! A set, in whichever representation currently fits it.
2//!
3//! A set is one of an [`Intset`], a [`Listpack`], an [`Elements`] table or a
4//! [`Parts`] band, and which one is not a choice this file gets to make freely.
5//! `OBJECT ENCODING` has to answer `intset`, `listpack` or `hashtable` at exactly
6//! the sizes a real server answers them, because clients and test suites read it
7//! (`08` ยง1), so the promotion rules here are Redis's rules and they were read
8//! off `t_set.c` in the 8.10.1 tarball rather than reasoned out from what each
9//! structure is good at.
10//!
11//! ```text
12//!  all integers        small, any bytes      everything else       large
13//! +-------------+   +------------------+   +---------------+   +-----------+
14//! | intset      |-->| listpack         |-->| element table |-->| partition |
15//! | 2 B member  |   | ~2 B + payload   |   | one probe     |   | band      |
16//! +-------------+   +------------------+   +---------------+   +-----------+
17//!  to 512 members     to 128 members                            past 262144
18//!                                          <------- both say `hashtable` ---->
19//! ```
20//!
21//! The fourth step is the one Redis does not have, and it is invisible on
22//! purpose. A set past 262,144 members becomes several element tables rather than
23//! one, which is what makes the merges and the growth pauses bounded, and it
24//! still answers `hashtable` because a client that gets a fourth word back from
25//! `OBJECT ENCODING` is a client whose assertions break. What partitioning
26//! changes is how a large set is stored, not what it is. See [`crate::parts`].
27//!
28//! Promotion is one-way and upward, which is Y4. A set that has been a hash
29//! table does not go back to an intset when it shrinks, and neither does Redis's:
30//! a set that demoted on the way down would rewrite itself on every second
31//! operation for a workload that adds and removes across a threshold. The band
32//! follows the same rule for the same reason, so a set hovering at the partition
33//! threshold rehashes once rather than on every other `SREM`.
34//!
35//! # The rules, and the two that are not obvious
36//!
37//! Adding an integer to an intset keeps it an intset until it holds more than
38//! `set-max-intset-entries`, and then it becomes a **hash table** and not a
39//! listpack, because the intset ceiling is 512 and the listpack ceiling is 128
40//! and something over the first is well over the second.
41//!
42//! Adding a non integer to an intset is the asymmetric one. It becomes a
43//! listpack only if the intset is currently under the *listpack* ceiling of 128,
44//! so an intset of 200 integers that receives one string goes straight to a hash
45//! table and is never a listpack at all. Reading that off the source was worth
46//! more than reasoning about it, because the natural implementation converts to
47//! a listpack whenever the members would fit and gets a different encoding name
48//! than a real server for a shape a test suite actually builds.
49//!
50//! # Members
51//!
52//! A member comes back as a [`Member`], which is [`listpack::Entry`] under
53//! another name: either the bytes as they lie or an integer that has not been
54//! formatted yet. All three representations can produce one without copying, and
55//! the formatting happens once, into the reply buffer, at the moment the reply is
56//! built. That is Y18, and it is the same reason [`crate::value::Str`] has two
57//! arms.
58
59use crate::elem::Elements;
60use crate::intset::Intset;
61use crate::listpack::{self, Listpack};
62use crate::parts::{PARTITION_AT, Parts, parts_for};
63use crate::scan::Cursor;
64use yo_common::num::{DIGITS_MAX, i64_digits, i64_len, parse_i64};
65
66/// A set member: bytes as they lie, or an integer not yet formatted.
67pub type Member<'a> = listpack::Entry<'a>;
68
69/// A member on its way to being asked about, in every form the three
70/// representations want it in.
71///
72/// Set algebra walks one set and asks every other set the same question about
73/// each member, and the three representations do not want the question in the
74/// same shape. An intset wants a number, a listpack wants bytes and a number
75/// because it holds both kinds, and an element table wants bytes and their
76/// hash. Asking through [`Set::contains`] would redo all of that per question:
77/// a parse for the intset, another parse inside the listpack, and a hash per
78/// table. This does each once per member and then asks `k - 1` times.
79///
80/// The hash is computed whether or not any operand is a table, which is waste
81/// when none is. It is waste worth taking, because the sets where it is wasted
82/// are an intset or a listpack, which are capped at a few hundred members, and
83/// an operation over sets that small is finished before the saving could have
84/// been measured. The sets where the hash pays are the large ones, and those
85/// are tables by definition.
86#[derive(Debug, Clone, Copy)]
87pub struct Needle<'a> {
88    /// The member as bytes, which for an integer member is a caller's buffer.
89    bytes: &'a [u8],
90    /// The number it is, if it is one, under the same rule that decides whether
91    /// a set stores it as one.
92    int: Option<i64>,
93    /// What an element table would key it under.
94    hash: u64,
95}
96
97impl<'a> Needle<'a> {
98    /// A needle from bytes, which is what a command line argument is.
99    #[must_use]
100    pub fn new(bytes: &'a [u8]) -> Needle<'a> {
101        Needle {
102            bytes,
103            int: parse_i64(bytes),
104            hash: Elements::<()>::hash_of(bytes),
105        }
106    }
107
108    /// A needle from a member walked out of a set.
109    ///
110    /// `digits` is where an integer member's text goes, because an intset holds
111    /// the number and the digits do not exist anywhere until somebody writes
112    /// them. It is the caller's buffer rather than a field so that the needle
113    /// stays a borrow and the buffer is written once per member rather than
114    /// allocated once per member.
115    ///
116    /// A member that came out as bytes is still parsed, because the set being
117    /// asked may be an intset and `SINTER ints strings` has to find the members
118    /// they share. A member that came out as a number is not, which is the
119    /// whole saving.
120    #[must_use]
121    pub fn of(member: Member<'a>, digits: &'a mut [u8; DIGITS_MAX]) -> Needle<'a> {
122        match member {
123            Member::Str(s) => Needle::new(s),
124            Member::Int(n) => {
125                let bytes = i64_digits(digits, n);
126                Needle {
127                    bytes,
128                    int: Some(n),
129                    hash: Elements::<()>::hash_of(bytes),
130                }
131            }
132        }
133    }
134
135    /// The member as bytes, which is what a caller collecting an answer wants.
136    #[must_use]
137    pub const fn bytes(&self) -> &'a [u8] {
138        self.bytes
139    }
140}
141
142/// Where the encodings change over.
143///
144/// These are `set-max-intset-entries`, `set-max-listpack-entries` and
145/// `set-max-listpack-value`, and they are runtime configuration in Redis, so
146/// they are a value passed in here rather than three constants. The defaults are
147/// Redis's defaults and a client that never touches `CONFIG SET` sees exactly
148/// the encodings a real server would give it.
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub struct Limits {
151    /// Past this many members an all integer set stops being an intset.
152    pub max_intset_entries: usize,
153    /// At this many members a set stops being a listpack.
154    pub max_listpack_entries: usize,
155    /// A member longer than this cannot go in a listpack.
156    pub max_listpack_value: usize,
157}
158
159impl Limits {
160    /// Redis's defaults: 512, 128 and 64.
161    pub const DEFAULT: Limits = Limits {
162        max_intset_entries: 512,
163        max_listpack_entries: 128,
164        max_listpack_value: 64,
165    };
166}
167
168impl Default for Limits {
169    fn default() -> Limits {
170        Limits::DEFAULT
171    }
172}
173
174/// Which of the three a set is in, which is what `OBJECT ENCODING` reports.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum Encoding {
177    /// All members are integers and there are few enough of them.
178    Intset,
179    /// One packed blob, walked linearly.
180    Listpack,
181    /// The element table.
182    Hashtable,
183}
184
185impl Encoding {
186    /// The word `OBJECT ENCODING` replies with.
187    #[inline]
188    pub const fn name(self) -> &'static str {
189        match self {
190            Encoding::Intset => "intset",
191            Encoding::Listpack => "listpack",
192            Encoding::Hashtable => "hashtable",
193        }
194    }
195}
196
197/// The four representations, of which `OBJECT ENCODING` can see three.
198///
199/// [`Body::Split`] is the partitioned band and it is deliberately invisible from
200/// outside. Redis has three set encodings and a client that gets a fourth word
201/// back from `OBJECT ENCODING` is a client whose assertions break, so a split set
202/// answers `hashtable` like the table it was. What partitioning changes is how a
203/// large set is stored and merged, not what it is.
204#[derive(Debug, Clone)]
205enum Body {
206    Ints(Intset),
207    Packed(Listpack),
208    Table(Elements<()>),
209    Split(Parts<()>),
210}
211
212/// A set of members.
213#[derive(Debug, Clone)]
214pub struct Set {
215    body: Body,
216    /// Whether an all integer set has passed `set-max-intset-entries`.
217    ///
218    /// This is where Redis rehashes the members into a dictionary and starts
219    /// answering `hashtable` to `OBJECT ENCODING`. Nothing is rehashed here,
220    /// because [`Intset`] holds a large set in runs and stays at two to eight
221    /// bytes a member where a table would cost thirty, so the only thing the
222    /// ceiling still decides is the word.
223    ///
224    /// It is a flag and not a comparison against the length because the ceiling
225    /// is configurable and [`Set::encoding`] is not handed the configuration. It
226    /// is also one way, like every promotion here: a set that has been called a
227    /// hashtable once does not go back to being called an intset when members
228    /// are removed, which is Y4 and is what Redis does.
229    ///
230    /// Only ever true while [`Body::Ints`] is the body. Every other body already
231    /// knows what to answer.
232    ints_past_limit: bool,
233}
234
235impl Set {
236    /// An empty set, which starts as an intset.
237    ///
238    /// This is what `SADD` on a missing key creates when it has no size hint,
239    /// and the first member decides nothing: an intset that receives a string
240    /// converts on the spot, and it costs a conversion of nothing.
241    #[must_use]
242    pub fn new() -> Set {
243        Set {
244            body: Body::Ints(Intset::new()),
245            ints_past_limit: false,
246        }
247    }
248
249    /// An empty set sized for what is about to go in it.
250    ///
251    /// Redis's `setTypeCreate`, which picks the representation from the first
252    /// member and the count the caller expects, so that `SADD k a b c ...` with
253    /// a thousand arguments builds a table once rather than converting twice on
254    /// the way there. `hint` is only a hint and being wrong about it costs a
255    /// conversion and no correctness.
256    ///
257    /// An integer first member sends it to the intset even when the count is
258    /// past `set-max-intset-entries`, where Redis would go straight to a
259    /// dictionary. A large set of integers is the case the runs exist for, and
260    /// building a table and never leaving it would give up the whole saving on
261    /// the one call that said in advance it was going to matter. The listpack
262    /// band in between is still honoured, because a set that small has nothing
263    /// to save and a server configured that way expects a listpack.
264    #[must_use]
265    pub fn with_hint(first: &[u8], hint: usize, limits: &Limits) -> Set {
266        let ints = parse_i64(first).is_some()
267            && (hint <= limits.max_intset_entries || hint > limits.max_listpack_entries);
268        if ints {
269            Set {
270                body: Body::Ints(Intset::with_capacity(hint)),
271                ints_past_limit: hint > limits.max_intset_entries,
272            }
273        } else if hint <= limits.max_listpack_entries {
274            Set {
275                body: Body::Packed(Listpack::new()),
276                ints_past_limit: false,
277            }
278        } else if hint > PARTITION_AT {
279            // A caller that says up front it is about to load a million members
280            // should not build one table, fill it past the threshold and then
281            // rehash the lot into partitions. The hint is only a hint, and being
282            // wrong about it here costs a set with more partitions than it needs
283            // rather than anything incorrect.
284            Set {
285                body: Body::Split(Parts::with_parts(parts_for(hint))),
286                ints_past_limit: false,
287            }
288        } else {
289            Set {
290                body: Body::Table(Elements::with_capacity(hint)),
291                ints_past_limit: false,
292            }
293        }
294    }
295
296    /// Which representation this is in.
297    ///
298    /// Four bodies and three words, and the intset accounts for two of the
299    /// missing ones. The partitioned body answers `hashtable` because it is one,
300    /// and an intset past `set-max-intset-entries` answers `hashtable` because
301    /// that is what a real server would have turned into by then, even though
302    /// nothing here was rehashed.
303    #[inline]
304    #[must_use]
305    pub const fn encoding(&self) -> Encoding {
306        match self.body {
307            Body::Ints(_) if self.ints_past_limit => Encoding::Hashtable,
308            Body::Ints(_) => Encoding::Intset,
309            Body::Packed(_) => Encoding::Listpack,
310            Body::Table(_) | Body::Split(_) => Encoding::Hashtable,
311        }
312    }
313
314    /// How many members. This is `SCARD`.
315    #[inline]
316    pub fn len(&self) -> usize {
317        match &self.body {
318            Body::Ints(s) => s.len(),
319            Body::Packed(lp) => lp.len(),
320            Body::Table(t) => t.len(),
321            Body::Split(p) => p.len(),
322        }
323    }
324
325    /// Whether there are none.
326    ///
327    /// An empty set does not exist in Redis, so the caller deletes the key when
328    /// this turns true rather than storing an empty one.
329    #[inline]
330    pub fn is_empty(&self) -> bool {
331        self.len() == 0
332    }
333
334    /// Whether `member` is in the set. This is `SISMEMBER`.
335    #[must_use]
336    pub fn contains(&self, member: &[u8]) -> bool {
337        match &self.body {
338            // A member that is not an integer cannot be in a set of integers,
339            // and answering that costs a parse rather than a search.
340            Body::Ints(s) => parse_i64(member).is_some_and(|v| s.contains(v)),
341            Body::Packed(lp) => lp.find(member, 1).is_some(),
342            Body::Table(t) => t.contains(member),
343            Body::Split(p) => p.contains(member),
344        }
345    }
346
347    /// The same question asked with the work already done. See [`Needle`].
348    ///
349    /// This is what set algebra probes with. Every arm is the arm
350    /// [`Set::contains`] would have taken, with the parse and the hash lifted
351    /// out of it, so the two cannot disagree about what a member is.
352    #[must_use]
353    #[inline]
354    pub fn has(&self, needle: &Needle<'_>) -> bool {
355        match &self.body {
356            Body::Ints(s) => needle.int.is_some_and(|v| s.contains(v)),
357            Body::Packed(lp) => lp.find_parsed(needle.bytes, needle.int, 1).is_some(),
358            Body::Table(t) => t.contains_hashed(needle.hash, needle.bytes),
359            Body::Split(p) => p.contains_hashed(needle.hash, needle.bytes),
360        }
361    }
362
363    /// The member at `index`, in whatever order the representation holds them.
364    ///
365    /// Ascending for an intset, insertion order for the other two. Redis makes
366    /// no promise about set order and neither does this, but a uniform draw
367    /// needs positions and this is what gives it them (K9).
368    #[must_use]
369    pub fn at(&self, index: usize) -> Option<Member<'_>> {
370        match &self.body {
371            Body::Ints(s) => s.get(index).map(Member::Int),
372            Body::Packed(lp) => lp.get(index),
373            Body::Table(t) => t.at(index).map(|(name, _)| Member::Str(name)),
374            Body::Split(p) => p.at(index).map(|(name, _)| Member::Str(name)),
375        }
376    }
377
378    /// Every member.
379    pub fn iter(&self) -> impl Iterator<Item = Member<'_>> {
380        (0..self.len()).map(|i| self.at(i).expect("index is under the length"))
381    }
382
383    /// The members as a sorted array of integers, when that is what this is.
384    ///
385    /// The one place the representation is not an implementation detail, and it
386    /// is here for [`crate::setops`]: two sorted arrays intersect by stepping
387    /// through both of them with no hash anywhere, which is a different order of
388    /// cost from asking a table a question per member. That was worth nothing
389    /// while an all integer set turned into a table at five hundred and twelve
390    /// members, and it is worth a great deal now that it does not.
391    #[inline]
392    #[must_use]
393    pub const fn ints(&self) -> Option<&Intset> {
394        match &self.body {
395            Body::Ints(s) => Some(s),
396            _ => None,
397        }
398    }
399
400    /// Walk part of the set and say where to resume. This is `SSCAN`.
401    ///
402    /// Only the table and the partitioned band walk in windows. An intset or a
403    /// listpack hands back
404    /// every member in one call and a cursor of [`Cursor::END`], ignoring the
405    /// cursor it was given, which is what Redis does for the same two encodings
406    /// and for the same reason: a hundred and twenty eight members is smaller
407    /// than the reply header arithmetic to split them up, and a set that small
408    /// cannot block the loop long enough for the split to be worth anything.
409    ///
410    /// Ignoring the cursor is safe rather than merely convenient, because
411    /// promotion is one way. A set that gave a client a listpack cursor is not
412    /// going to be a listpack again, so the only way to arrive at those two arms
413    /// with a cursor from somewhere else is for the key to have been deleted and
414    /// remade underneath the scan, and returning everything to that client
415    /// returns a member twice at worst, which the guarantee allows.
416    ///
417    /// A table cursor arriving at the band is the one crossing that does happen,
418    /// because a set can split part way through a client's scan. That is handled
419    /// rather than ignored: a table cursor names one partition, and
420    /// [`Cursor::rebase`] reads the widening and restarts the walk at the top of
421    /// the new layout, so the client sees some members a second time and misses
422    /// none. Repeats are what the `SCAN` guarantee gives up in exchange for
423    /// surviving a resize, and a set only splits once.
424    pub fn scan<F>(&self, cursor: Cursor, count: usize, mut f: F) -> Cursor
425    where
426        F: FnMut(Member<'_>),
427    {
428        match &self.body {
429            Body::Table(t) => t.scan(cursor, count, |name, ()| f(Member::Str(name))),
430            Body::Split(p) => p.scan(cursor, count, |name, ()| f(Member::Str(name))),
431            // An intset past the ceiling is the one thing outside the table
432            // band that a single reply cannot hold. Redis has a dictionary by
433            // this point and walks it in windows, and a set of a million
434            // integers answering `SSCAN` with a million members in one go would
435            // be a several megabyte reply and a loop iteration nobody could
436            // measure. So it walks in windows too, and by index, which the runs
437            // answer in a walk down their tree rather than a walk along them.
438            //
439            // Downward, which is the direction the element table walks and for
440            // the same reason. Positions in a sorted array shift when a member
441            // below them goes, so an upward walk would miss a member for every
442            // one removed behind it, and `SSCAN` followed by `SREM` on what it
443            // found is the commonest thing anyone does with this command.
444            // Walking down means those removals are all above the cursor, where
445            // they cost nothing.
446            Body::Ints(s) if self.ints_past_limit && !s.is_empty() => {
447                let top = s.len() - 1;
448                let mut at = match cursor.rebase(1).idx() {
449                    Some(idx) => (idx as usize).min(top),
450                    None => top,
451                };
452                for _ in 0..count.max(1) {
453                    f(Member::Int(s.at(at)));
454                    if at == 0 {
455                        return Cursor::END;
456                    }
457                    at -= 1;
458                }
459                Cursor::at(1, 0, at as u64)
460            }
461            _ => {
462                for m in self.iter() {
463                    f(m);
464                }
465                Cursor::END
466            }
467        }
468    }
469
470    /// Bytes held by whichever representation this is.
471    #[must_use]
472    pub fn memory_bytes(&self) -> usize {
473        match &self.body {
474            Body::Ints(s) => s.memory_bytes(),
475            Body::Packed(lp) => lp.byte_len(),
476            Body::Table(t) => t.memory_bytes(),
477            Body::Split(p) => p.memory_bytes(),
478        }
479    }
480
481    /// Add `member`, promoting if it no longer fits. Answers whether it was new.
482    ///
483    /// This is `setTypeAdd`, arm for arm.
484    pub fn add(&mut self, member: &[u8], limits: &Limits) -> bool {
485        match &mut self.body {
486            Body::Table(t) => {
487                let new = t.insert(member, ()).is_ok_and(|old| old.is_none());
488                // Checked after the insert rather than before, so the set that
489                // splits is the one that has actually outgrown a table and not
490                // the one that is about to.
491                if t.len() > PARTITION_AT {
492                    self.become_split();
493                }
494                return new;
495            }
496            Body::Split(p) => {
497                let new = p.insert(member, ()).is_ok_and(|old| old.is_none());
498                // Asked rather than decided, because growing is a rehash of the
499                // whole set and the band leaves the timing to whoever knows
500                // whether this is one write or the middle of a bulk load.
501                if let Some(want) = p.wants_parts() {
502                    p.grow_to(want);
503                }
504                return new;
505            }
506            Body::Packed(lp) => {
507                if lp.find(member, 1).is_some() {
508                    return false;
509                }
510                if lp.len() < limits.max_listpack_entries
511                    && member.len() <= limits.max_listpack_value
512                {
513                    lp.push(member);
514                    return true;
515                }
516                // Too many members, or one too long. It falls out to a table.
517            }
518            Body::Ints(s) => {
519                if let Some(v) = parse_i64(member) {
520                    if !s.add(v) {
521                        return false;
522                    }
523                    // Strictly greater, so the 512th member is still an intset
524                    // and the 513th is what a real server would call a
525                    // hashtable. Nothing is rewritten, only the word changes.
526                    //
527                    // Unless the ceilings have been configured the wrong way
528                    // round, where a set past the intset ceiling is still under
529                    // the listpack one and a real server puts it in a listpack.
530                    // That set is a handful of members and there is no memory
531                    // argument for keeping it here, so it goes where it would
532                    // have gone.
533                    if s.len() > limits.max_intset_entries {
534                        if self.ints_fit_a_listpack_alone(limits) {
535                            self.become_listpack();
536                        } else {
537                            self.ints_past_limit = true;
538                        }
539                    }
540                    return true;
541                }
542                // Not an integer, so it is certainly not in a set of integers
543                // already. If the set is still small enough it becomes a
544                // listpack, and otherwise it falls out to a table.
545                if self.ints_fit_a_listpack(member, limits) {
546                    self.become_listpack();
547                    self.push_new(member);
548                    return true;
549                }
550            }
551        }
552        self.become_table(1);
553        self.push_new(member);
554        true
555    }
556
557    /// Put in a member already known to be new and known to fit where it is.
558    ///
559    /// Only ever called on the far side of a promotion, where both of those are
560    /// facts the promotion established and not things worth establishing twice.
561    fn push_new(&mut self, member: &[u8]) {
562        match &mut self.body {
563            Body::Packed(lp) => lp.push(member),
564            Body::Table(t) => {
565                t.insert(member, ())
566                    .expect("the table was sized for this one");
567            }
568            Body::Split(p) => {
569                p.insert(member, ())
570                    .expect("the band was sized for this one");
571            }
572            Body::Ints(_) => unreachable!("no promotion ever lands on an intset"),
573        }
574    }
575
576    /// Remove `member`. Answers whether it was there.
577    ///
578    /// Never demotes, which is Y4's one-way rule and Redis's behaviour.
579    pub fn remove(&mut self, member: &[u8]) -> bool {
580        match &mut self.body {
581            Body::Ints(s) => parse_i64(member).is_some_and(|v| s.remove(v)),
582            Body::Packed(lp) => match lp.find(member, 1) {
583                Some(at) => lp.delete(at, 1),
584                None => false,
585            },
586            Body::Table(t) => t.remove(member).is_some(),
587            Body::Split(p) => p.remove(member).is_some(),
588        }
589    }
590
591    /// Take out the member at `index` and hand it back.
592    ///
593    /// This is what `SPOP` runs on top of. The table moves its last row into the
594    /// hole rather than shifting, so the position of every other member is
595    /// stable except for one; the other two shift. Neither is a promise a caller
596    /// can lean on, and `SPOP` does not need one because it draws again from the
597    /// new length each time.
598    pub fn remove_at(&mut self, index: usize) -> Option<Vec<u8>> {
599        match &mut self.body {
600            Body::Ints(s) => {
601                let v = s.get(index)?;
602                s.remove(v);
603                let mut out = Vec::with_capacity(i64_len(v));
604                Member::Int(v).write_to(&mut out);
605                Some(out)
606            }
607            Body::Packed(lp) => {
608                let out = lp.get(index)?.to_vec();
609                lp.delete(index, 1);
610                Some(out)
611            }
612            Body::Table(t) => t.take_at(index).map(|(name, ())| name),
613            Body::Split(p) => p.take_at(index).map(|(name, ())| name),
614        }
615    }
616
617    /// Take out the member at `index` without building it into a `Vec` first.
618    ///
619    /// The same removal as [`Set::remove_at`] for a caller that has already read
620    /// the member and does not need it handed back. That caller is `SPOP` on the
621    /// wire, which reads with [`Set::at`], writes the bytes straight into the
622    /// reply buffer, and only then calls this. It is an allocation a member
623    /// saved on the one command in the set whose whole cost is the allocating.
624    ///
625    /// [`Set::remove_at`] stays for the embedded API, where the caller wants the
626    /// bytes and has nowhere to put them.
627    pub fn drop_at(&mut self, index: usize) -> bool {
628        match &mut self.body {
629            Body::Ints(s) => match s.get(index) {
630                Some(v) => {
631                    s.remove(v);
632                    true
633                }
634                None => false,
635            },
636            Body::Packed(lp) => {
637                if index >= lp.len() {
638                    return false;
639                }
640                lp.delete(index, 1);
641                true
642            }
643            Body::Table(t) => t.remove_at(index).is_some(),
644            Body::Split(p) => p.remove_at(index).is_some(),
645        }
646    }
647
648    /// Whether an intset plus one non integer member would still be a listpack.
649    ///
650    /// Three tests, and the first is the asymmetric one: the count is compared
651    /// against the *listpack* ceiling and not the intset one, so an intset with
652    /// two hundred members is already too big to become a listpack even though
653    /// it is a perfectly legal intset. The other two are the new member's length
654    /// and the longest existing member's length once it is written as digits,
655    /// which only bites when `set-max-listpack-value` has been turned down,
656    /// because no integer is more than twenty characters.
657    fn ints_fit_a_listpack(&self, member: &[u8], limits: &Limits) -> bool {
658        let Body::Ints(s) = &self.body else {
659            return false;
660        };
661        s.len() < limits.max_listpack_entries
662            && member.len() <= limits.max_listpack_value
663            && self.ints_are_short_enough(limits)
664    }
665
666    /// Whether this intset on its own would fit a listpack.
667    ///
668    /// The same question with no new member in it, which is what an intset that
669    /// has just passed `set-max-intset-entries` asks. It only ever answers yes
670    /// when the two ceilings have been configured the wrong way round, because
671    /// 512 is not under 128, and a server run that way expects a listpack there.
672    fn ints_fit_a_listpack_alone(&self, limits: &Limits) -> bool {
673        let Body::Ints(s) = &self.body else {
674            return false;
675        };
676        s.len() <= limits.max_listpack_entries && self.ints_are_short_enough(limits)
677    }
678
679    /// Whether every member, written as digits, is under the listpack ceiling.
680    fn ints_are_short_enough(&self, limits: &Limits) -> bool {
681        let Body::Ints(s) = &self.body else {
682            return false;
683        };
684        // The two ends bound the digits of everything between them, so there is
685        // nothing to walk.
686        let widest = s
687            .min()
688            .map(i64_len)
689            .unwrap_or(0)
690            .max(s.max().map(i64_len).unwrap_or(0));
691        widest <= limits.max_listpack_value
692    }
693
694    /// Rewrite as a listpack, which only an intset ever does.
695    fn become_listpack(&mut self) {
696        let Body::Ints(s) = &self.body else {
697            return;
698        };
699        let mut lp = Listpack::new();
700        let mut buf = Vec::with_capacity(20);
701        for v in s.iter() {
702            buf.clear();
703            Member::Int(v).write_to(&mut buf);
704            lp.push(&buf);
705        }
706        self.body = Body::Packed(lp);
707    }
708
709    /// Rewrite as an element table, with room for `extra` more members.
710    fn become_table(&mut self, extra: usize) {
711        let mut t = Elements::with_capacity(self.len() + extra);
712        let mut buf = Vec::with_capacity(20);
713        for m in self.iter() {
714            match m {
715                Member::Str(b) => {
716                    t.insert(b, ()).expect("room, and every member was unique");
717                }
718                Member::Int(v) => {
719                    buf.clear();
720                    Member::Int(v).write_to(&mut buf);
721                    t.insert(&buf, ())
722                        .expect("room, and every member was unique");
723                }
724            }
725        }
726        self.body = Body::Table(t);
727    }
728
729    /// Spread an element table over partitions.
730    ///
731    /// One way, like every other promotion here. A set that drops back under the
732    /// threshold keeps its partitions, which is Y4's rule and Redis's behaviour
733    /// for the encodings it does expose: the cost of a representation is paid
734    /// when it is entered, and paying it again on the way back out turns one
735    /// `SREM` at the boundary into a rehash of the whole set.
736    fn become_split(&mut self) {
737        if let Body::Table(t) = &self.body {
738            let p = Parts::from_table(t, parts_for(t.len()));
739            self.body = Body::Split(p);
740        }
741    }
742}
743
744impl Default for Set {
745    fn default() -> Set {
746        Set::new()
747    }
748}
749
750#[cfg(test)]
751mod tests {
752    use super::*;
753
754    fn of(members: &[&str]) -> Set {
755        let mut s = Set::new();
756        for m in members {
757            assert!(s.add(m.as_bytes(), &Limits::DEFAULT), "{m} was new");
758        }
759        s
760    }
761
762    /// What a set actually costs per member, which is half of M3's memory gate
763    /// row and was an argument rather than a number until this was written.
764    ///
765    /// Run it with `cargo test -p yo-kv --release measure_bytes_per_member --
766    /// --ignored --nocapture`. Ignored because a million members is not
767    /// something every `cargo test` should pay for, and it prints rather than
768    /// asserts because the number it prints is the thing being reported.
769    ///
770    /// Two shapes, because the gate names one of them and the other is what
771    /// most sets actually hold. Integers first, at every band an all integer
772    /// set passes through, and then strings, which never see the intset at all.
773    #[test]
774    #[ignore = "a measurement, run it by name"]
775    fn measure_bytes_per_member() {
776        let limits = Limits::DEFAULT;
777        for n in [512usize, 1_000, 100_000, 1_000_000] {
778            let mut s = Set::new();
779            for i in 0..n {
780                s.add(i.to_string().as_bytes(), &limits);
781            }
782            println!(
783                "int   n={n:<9} band={:<10} total={:<10} per_member={:.2}",
784                band(&s),
785                s.memory_bytes(),
786                s.memory_bytes() as f64 / n as f64
787            );
788        }
789        // Sixteen byte members, so the payload is a round number and the
790        // overhead is whatever is above it.
791        for n in [128usize, 1_000, 100_000, 1_000_000] {
792            let mut s = Set::new();
793            let mut payload = 0usize;
794            for i in 0..n {
795                let m = format!("member:{i:09}");
796                payload += m.len();
797                s.add(m.as_bytes(), &limits);
798            }
799            let total = s.memory_bytes();
800            println!(
801                "bytes n={n:<9} band={:<10} total={total:<10} payload={payload:<9} per_member={:.2} over_per_member={:.2}",
802                band(&s),
803                total as f64 / n as f64,
804                (total as f64 - payload as f64) / n as f64
805            );
806        }
807    }
808
809    /// Which of the four a set is in, spelled out rather than through
810    /// [`Set::encoding`], which folds the two table bands into one word because
811    /// that is what `OBJECT ENCODING` has to say.
812    fn band(s: &Set) -> &'static str {
813        match &s.body {
814            Body::Ints(_) => "intset",
815            Body::Packed(_) => "listpack",
816            Body::Table(_) => "table",
817            Body::Split(_) => "split",
818        }
819    }
820
821    /// Everything about the partitioned band that needs a real set past the real
822    /// threshold, in one test, because building 262,145 members is the expensive
823    /// part and there is no reason to pay for it four times.
824    #[test]
825    fn a_set_past_the_threshold_splits_without_the_client_being_able_to_tell() {
826        let limits = Limits::DEFAULT;
827        let mut s = Set::new();
828        // One short of the threshold. Still one table: the check is strictly
829        // greater, so the set that splits is the one that has outgrown a table
830        // and not the one that is about to.
831        for i in 0..PARTITION_AT {
832            assert!(s.add(format!("m{i}").as_bytes(), &limits));
833        }
834        assert!(matches!(s.body, Body::Table(_)));
835        assert_eq!(s.len(), PARTITION_AT);
836
837        // The member that tips it over.
838        assert!(s.add(b"tipping", &limits));
839        assert!(matches!(s.body, Body::Split(_)), "it should have split");
840        assert_eq!(s.len(), PARTITION_AT + 1);
841
842        // And the client cannot tell. This is the whole point: Redis has three
843        // set encodings and a fourth word here breaks every suite that reads it.
844        assert_eq!(s.encoding(), Encoding::Hashtable);
845        assert_eq!(s.encoding().name(), "hashtable");
846
847        // Every member survived the rehash, asked both ways.
848        assert!(s.contains(b"tipping"));
849        assert!(s.contains(b"m0"));
850        assert!(s.contains(b"m262143"));
851        assert!(!s.contains(b"m262144"));
852        assert!(s.has(&Needle::new(b"m1000")));
853        assert!(!s.has(&Needle::new(b"nothing")));
854
855        // A rewrite is still not an add.
856        assert!(!s.add(b"m0", &limits));
857        assert_eq!(s.len(), PARTITION_AT + 1);
858
859        // Removing goes back through the same partition it went into, and the
860        // band never demotes however far the set shrinks.
861        assert!(s.remove(b"tipping"));
862        assert!(!s.remove(b"tipping"));
863        assert_eq!(s.len(), PARTITION_AT);
864        assert!(matches!(s.body, Body::Split(_)), "promotion is one way");
865        assert_eq!(s.encoding(), Encoding::Hashtable);
866
867        // The draw `SPOP` and `SRANDMEMBER` run on reaches the last position,
868        // which is the one that has to land in the highest non empty partition.
869        let last = s.at(s.len() - 1).expect("inside the set").to_vec();
870        assert!(s.contains(&last));
871        assert!(s.at(s.len()).is_none());
872        assert_eq!(s.remove_at(s.len() - 1), Some(last.clone()));
873        assert!(!s.contains(&last));
874        assert!(s.drop_at(0));
875        assert_eq!(s.len(), PARTITION_AT - 2);
876
877        // And a full scan sees every member exactly once.
878        let mut seen = 0usize;
879        let mut cursor = Cursor::START;
880        let mut rounds = 0;
881        loop {
882            cursor = s.scan(cursor, 1_000, |_| seen += 1);
883            rounds += 1;
884            assert!(rounds < 100_000, "the scan is not finishing");
885            if cursor.is_end() {
886                break;
887            }
888        }
889        assert_eq!(seen, s.len());
890    }
891
892    /// The crossing that actually happens in production: a client holding a
893    /// cursor from before the split. It has to see every member that stayed, and
894    /// repeats are what the `SCAN` guarantee gives up in exchange.
895    #[test]
896    fn a_scan_survives_the_set_splitting_underneath_it() {
897        let limits = Limits::DEFAULT;
898        let mut s = Set::new();
899        for i in 0..PARTITION_AT {
900            s.add(format!("m{i}").as_bytes(), &limits);
901        }
902        assert!(matches!(s.body, Body::Table(_)));
903
904        let mut seen = Vec::new();
905        let cursor = s.scan(Cursor::START, 5_000, |m| seen.push(m.to_vec()));
906        assert!(!cursor.is_end(), "the scan should have stopped part way");
907
908        s.add(b"tipping", &limits);
909        assert!(matches!(s.body, Body::Split(_)));
910
911        let mut cursor = cursor;
912        let mut rounds = 0;
913        loop {
914            cursor = s.scan(cursor, 5_000, |m| seen.push(m.to_vec()));
915            rounds += 1;
916            assert!(rounds < 100_000, "the scan is not finishing");
917            if cursor.is_end() {
918                break;
919            }
920        }
921        seen.sort_unstable();
922        seen.dedup();
923        assert_eq!(
924            seen.len(),
925            PARTITION_AT + 1,
926            "the split lost a member the client was entitled to"
927        );
928    }
929
930    #[test]
931    fn a_hint_past_the_threshold_builds_the_band_up_front() {
932        let limits = Limits::DEFAULT;
933        // A caller loading a million members should not fill one table, cross the
934        // threshold and then rehash the lot.
935        let s = Set::with_hint(b"first", 1_000_000, &limits);
936        assert!(matches!(s.body, Body::Split(_)));
937        assert_eq!(s.encoding(), Encoding::Hashtable);
938        assert!(s.is_empty());
939
940        // A hint at the threshold is still one table, matching the add path.
941        let s = Set::with_hint(b"first", PARTITION_AT, &limits);
942        assert!(matches!(s.body, Body::Table(_)));
943
944        // And a hint is only a hint: the band takes members like anything else.
945        let mut s = Set::with_hint(b"a", 1_000_000, &limits);
946        assert!(s.add(b"a", &limits));
947        assert!(!s.add(b"a", &limits));
948        assert!(s.contains(b"a"));
949        assert_eq!(s.len(), 1);
950        assert_eq!(s.at(0).map(|m| m.to_vec()), Some(b"a".to_vec()));
951    }
952
953    fn members(s: &Set) -> Vec<String> {
954        let mut v: Vec<String> = s
955            .iter()
956            .map(|m| String::from_utf8(m.to_vec()).expect("utf8 in these tests"))
957            .collect();
958        // Order is not part of the contract and the three representations do not
959        // agree on it, so every assertion here is against a sorted list.
960        v.sort();
961        v
962    }
963
964    #[test]
965    fn a_new_set_is_an_empty_intset() {
966        let s = Set::new();
967        assert_eq!(s.encoding(), Encoding::Intset);
968        assert_eq!(s.len(), 0);
969        assert!(s.is_empty());
970        assert!(!s.contains(b"1"));
971        assert_eq!(s.at(0), None);
972    }
973
974    #[test]
975    fn integers_stay_an_intset_and_come_back_as_members() {
976        let s = of(&["1", "2", "3"]);
977        assert_eq!(s.encoding(), Encoding::Intset);
978        assert_eq!(members(&s), ["1", "2", "3"]);
979        assert!(s.contains(b"2"));
980        assert!(!s.contains(b"4"));
981        assert!(!s.contains(b"two"));
982    }
983
984    #[test]
985    fn adding_the_same_member_twice_says_so_in_all_three() {
986        let mut ints = of(&["1", "2"]);
987        assert!(!ints.add(b"2", &Limits::DEFAULT));
988        assert_eq!(ints.len(), 2);
989
990        let mut packed = of(&["a", "b"]);
991        assert_eq!(packed.encoding(), Encoding::Listpack);
992        assert!(!packed.add(b"b", &Limits::DEFAULT));
993        assert_eq!(packed.len(), 2);
994
995        let mut table = of(&["a", "b"]);
996        table.become_table(0);
997        assert!(!table.add(b"b", &Limits::DEFAULT));
998        assert_eq!(table.len(), 2);
999    }
1000
1001    #[test]
1002    fn a_string_turns_a_small_intset_into_a_listpack() {
1003        let mut s = of(&["1", "2", "3"]);
1004        assert!(s.add(b"hello", &Limits::DEFAULT));
1005        assert_eq!(s.encoding(), Encoding::Listpack);
1006        assert_eq!(members(&s), ["1", "2", "3", "hello"]);
1007        assert!(s.contains(b"1"), "the integers survived the rewrite");
1008        assert!(s.contains(b"hello"));
1009    }
1010
1011    #[test]
1012    fn a_string_turns_a_big_intset_straight_into_a_table() {
1013        // The asymmetric rule, and the one a natural implementation gets wrong.
1014        // Two hundred integers is a legal intset and is already past the
1015        // listpack ceiling, so this never passes through the listpack at all.
1016        let mut s = Set::new();
1017        for i in 0..200 {
1018            s.add(i.to_string().as_bytes(), &Limits::DEFAULT);
1019        }
1020        assert_eq!(s.encoding(), Encoding::Intset);
1021        assert_eq!(s.len(), 200);
1022
1023        assert!(s.add(b"hello", &Limits::DEFAULT));
1024        assert_eq!(s.encoding(), Encoding::Hashtable);
1025        assert_eq!(s.len(), 201);
1026        assert!(s.contains(b"199"));
1027        assert!(s.contains(b"hello"));
1028    }
1029
1030    #[test]
1031    fn an_intset_holds_five_hundred_and_twelve_and_converts_at_the_next_one() {
1032        let mut s = Set::new();
1033        for i in 0..512 {
1034            s.add(i.to_string().as_bytes(), &Limits::DEFAULT);
1035        }
1036        assert_eq!(s.encoding(), Encoding::Intset, "512 is still an intset");
1037        assert_eq!(s.len(), 512);
1038
1039        s.add(b"512", &Limits::DEFAULT);
1040        assert_eq!(s.encoding(), Encoding::Hashtable, "513 is not");
1041        assert_eq!(s.len(), 513);
1042        // And not a listpack on the way, because 513 is well past 128.
1043        for i in 0..513 {
1044            assert!(s.contains(i.to_string().as_bytes()), "{i} survived");
1045        }
1046    }
1047
1048    #[test]
1049    fn a_listpack_converts_at_a_hundred_and_twenty_eight_members() {
1050        let mut s = of(&["x"]);
1051        assert_eq!(s.encoding(), Encoding::Listpack);
1052        for i in 0..127 {
1053            s.add(format!("m{i}").as_bytes(), &Limits::DEFAULT);
1054        }
1055        assert_eq!(s.len(), 128);
1056        assert_eq!(s.encoding(), Encoding::Listpack, "128 is still a listpack");
1057
1058        s.add(b"one more", &Limits::DEFAULT);
1059        assert_eq!(s.len(), 129);
1060        assert_eq!(s.encoding(), Encoding::Hashtable);
1061        assert!(s.contains(b"x"));
1062        assert!(s.contains(b"m126"));
1063        assert!(s.contains(b"one more"));
1064    }
1065
1066    #[test]
1067    fn a_long_member_converts_a_listpack_whatever_the_count() {
1068        let mut s = of(&["a"]);
1069        let long = vec![b'z'; 65];
1070        assert!(s.add(&long, &Limits::DEFAULT));
1071        assert_eq!(s.encoding(), Encoding::Hashtable, "65 is past 64");
1072        assert_eq!(s.len(), 2);
1073        assert!(s.contains(&long));
1074
1075        // And exactly at the boundary it does not.
1076        let mut ok = of(&["a"]);
1077        ok.add(&[b'z'; 64], &Limits::DEFAULT);
1078        assert_eq!(ok.encoding(), Encoding::Listpack, "64 fits");
1079    }
1080
1081    #[test]
1082    fn a_long_member_sends_an_intset_to_a_table_and_not_a_listpack() {
1083        let mut s = of(&["1", "2"]);
1084        assert!(s.add(&[b'z'; 65], &Limits::DEFAULT));
1085        assert_eq!(s.encoding(), Encoding::Hashtable);
1086        assert_eq!(s.len(), 3);
1087    }
1088
1089    #[test]
1090    fn the_limits_are_configuration_and_moving_them_moves_the_encodings() {
1091        let tight = Limits {
1092            max_intset_entries: 2,
1093            max_listpack_entries: 2,
1094            max_listpack_value: 3,
1095        };
1096        let mut s = Set::new();
1097        s.add(b"1", &tight);
1098        s.add(b"2", &tight);
1099        assert_eq!(s.encoding(), Encoding::Intset);
1100        s.add(b"3", &tight);
1101        assert_eq!(s.encoding(), Encoding::Hashtable, "three is past two");
1102
1103        // And a member longer than three characters cannot go in a listpack.
1104        let mut t = Set::new();
1105        t.add(b"abc", &tight);
1106        assert_eq!(t.encoding(), Encoding::Listpack, "three characters fit");
1107        t.add(b"defg", &tight);
1108        assert_eq!(t.encoding(), Encoding::Hashtable, "four do not");
1109        assert!(t.contains(b"abc"));
1110        assert!(t.contains(b"defg"));
1111
1112        // Including as the first member, which is a table from the off rather
1113        // than a listpack that converts on the next thing to arrive. Redis gets
1114        // to the same place by the other road: it creates the listpack, tries
1115        // the add, and converts before the reply.
1116        let mut u = Set::new();
1117        u.add(b"abcd", &tight);
1118        assert_eq!(u.encoding(), Encoding::Hashtable);
1119        assert!(u.contains(b"abcd"));
1120    }
1121
1122    #[test]
1123    fn with_hint_picks_the_representation_up_front() {
1124        let d = &Limits::DEFAULT;
1125        assert_eq!(
1126            Set::with_hint(b"1", 10, d).encoding(),
1127            Encoding::Intset,
1128            "an integer and few enough of them"
1129        );
1130        assert_eq!(
1131            Set::with_hint(b"1", 1000, d).encoding(),
1132            Encoding::Hashtable,
1133            "an integer and too many"
1134        );
1135        assert_eq!(
1136            Set::with_hint(b"x", 10, d).encoding(),
1137            Encoding::Listpack,
1138            "not an integer and few enough"
1139        );
1140        assert_eq!(
1141            Set::with_hint(b"x", 1000, d).encoding(),
1142            Encoding::Hashtable,
1143            "not an integer and too many"
1144        );
1145    }
1146
1147    #[test]
1148    fn removing_works_in_all_three_and_never_demotes() {
1149        let mut ints = of(&["1", "2", "3"]);
1150        assert!(ints.remove(b"2"));
1151        assert!(!ints.remove(b"2"));
1152        assert!(!ints.remove(b"nope"), "not an integer, so not a member");
1153        assert_eq!(members(&ints), ["1", "3"]);
1154        assert_eq!(ints.encoding(), Encoding::Intset);
1155
1156        let mut packed = of(&["a", "b", "c"]);
1157        assert!(packed.remove(b"b"));
1158        assert!(!packed.remove(b"b"));
1159        assert_eq!(members(&packed), ["a", "c"]);
1160        assert_eq!(packed.encoding(), Encoding::Listpack);
1161
1162        let mut table = of(&["a", "b", "c"]);
1163        table.become_table(0);
1164        assert!(table.remove(b"b"));
1165        assert!(!table.remove(b"b"));
1166        assert_eq!(members(&table), ["a", "c"]);
1167        assert_eq!(
1168            table.encoding(),
1169            Encoding::Hashtable,
1170            "down to two members and still a table"
1171        );
1172    }
1173
1174    #[test]
1175    fn a_set_can_be_emptied_a_member_at_a_time() {
1176        for mut s in [of(&["1", "2", "3"]), of(&["a", "b", "c"])] {
1177            let all: Vec<Vec<u8>> = s.iter().map(|m| m.to_vec()).collect();
1178            for m in &all {
1179                assert!(s.remove(m));
1180            }
1181            assert!(s.is_empty());
1182            assert_eq!(s.at(0), None);
1183        }
1184    }
1185
1186    #[test]
1187    fn removing_by_position_hands_the_member_back() {
1188        // What `SPOP` runs on. Drawing position zero every time has to empty the
1189        // set rather than run off the end or repeat a member, in all three.
1190        let mut table = of(&["a", "b", "c", "d"]);
1191        table.become_table(0);
1192        for mut s in [
1193            of(&["10", "20", "30", "40"]),
1194            of(&["a", "b", "c", "d"]),
1195            table,
1196        ] {
1197            let mut got = Vec::new();
1198            while !s.is_empty() {
1199                got.push(String::from_utf8(s.remove_at(0).expect("not empty")).expect("utf8"));
1200            }
1201            got.sort();
1202            assert_eq!(got.len(), 4, "four members and no repeats");
1203            assert_eq!(s.len(), 0);
1204            assert_eq!(s.remove_at(0), None);
1205        }
1206    }
1207
1208    #[test]
1209    fn an_integer_member_is_the_same_member_however_it_is_written() {
1210        // An intset holds 42 as a number, so `SADD s 42` twice is one member.
1211        // `042` does not parse as an integer, so it is a different member and it
1212        // converts the set, which is what a real server does too.
1213        let mut s = of(&["42"]);
1214        assert!(!s.add(b"42", &Limits::DEFAULT));
1215        assert_eq!(s.len(), 1);
1216        assert!(s.add(b"042", &Limits::DEFAULT));
1217        assert_eq!(s.encoding(), Encoding::Listpack);
1218        assert_eq!(members(&s), ["042", "42"]);
1219        assert!(s.contains(b"42"));
1220        assert!(s.contains(b"042"));
1221    }
1222
1223    #[test]
1224    fn the_small_bands_answer_a_scan_in_one_go() {
1225        for s in [of(&["1", "2", "3"]), of(&["a", "b", "c"])] {
1226            let mut seen = Vec::new();
1227            // A count of one, which the table band would honour and these two
1228            // do not, and a cursor from nowhere, which these two ignore.
1229            let next = s.scan(Cursor::at(1, 0, 99), 1, |m| seen.push(m.to_vec()));
1230            assert!(next.is_end(), "{:?} split a scan up", s.encoding());
1231            assert_eq!(seen.len(), 3);
1232        }
1233    }
1234
1235    #[test]
1236    fn the_table_band_walks_a_scan_in_windows_and_misses_nothing() {
1237        let mut s = Set::new();
1238        for i in 0..300 {
1239            s.add(format!("m{i}").as_bytes(), &Limits::DEFAULT);
1240        }
1241        assert_eq!(s.encoding(), Encoding::Hashtable);
1242
1243        let mut seen = Vec::new();
1244        let mut c = Cursor::START;
1245        let mut turns = 0;
1246        loop {
1247            c = s.scan(c, 7, |m| seen.push(m.to_vec()));
1248            turns += 1;
1249            assert!(turns < 100, "the scan did not finish");
1250            if c.is_end() {
1251                break;
1252            }
1253        }
1254        assert!(
1255            turns > 1,
1256            "a window of seven over three hundred took one turn"
1257        );
1258        seen.sort();
1259        seen.dedup();
1260        assert_eq!(seen.len(), 300, "every member came back at least once");
1261    }
1262
1263    #[test]
1264    fn a_conversion_loses_no_member_at_any_of_the_three_boundaries() {
1265        // One walk over each path out of an intset and out of a listpack, each
1266        // checking every member is still findable after the rewrite rather than
1267        // only checking the count.
1268        let mut wide = Set::new();
1269        for v in [i64::MIN, -1, 0, 1, i64::MAX] {
1270            wide.add(v.to_string().as_bytes(), &Limits::DEFAULT);
1271        }
1272        wide.add(b"str", &Limits::DEFAULT);
1273        assert_eq!(wide.encoding(), Encoding::Listpack);
1274        for v in [i64::MIN, -1, 0, 1, i64::MAX] {
1275            assert!(wide.contains(v.to_string().as_bytes()), "{v} survived");
1276        }
1277
1278        wide.add(&[b'q'; 100], &Limits::DEFAULT);
1279        assert_eq!(wide.encoding(), Encoding::Hashtable);
1280        for v in [i64::MIN, -1, 0, 1, i64::MAX] {
1281            assert!(
1282                wide.contains(v.to_string().as_bytes()),
1283                "{v} survived twice"
1284            );
1285        }
1286        assert!(wide.contains(b"str"));
1287        assert_eq!(wide.len(), 7);
1288    }
1289}