Skip to main content

yo_kv/
set.rs

1//! A set, in whichever of the three representations currently fits it.
2//!
3//! A set is one of an [`Intset`], a [`Listpack`] or an [`Elements`] table, and
4//! which one is not a choice this file gets to make freely. `OBJECT ENCODING`
5//! has to answer `intset`, `listpack` or `hashtable` at exactly the sizes a real
6//! server answers them, because clients and test suites read it (`08` ยง1), so
7//! the promotion rules here are Redis's rules and they were read off `t_set.c`
8//! in the 8.10.1 tarball rather than reasoned out from what each structure is
9//! good at.
10//!
11//! ```text
12//!   all integers          small, any bytes        everything else
13//! +-----------------+   +------------------+   +-------------------+
14//! | intset          |-->| listpack         |-->| element table     |
15//! | 2 B a member    |   | ~2 B + payload   |   | one probe, no cap |
16//! +-----------------+   +------------------+   +-------------------+
17//!    to 512 members         to 128 members
18//! ```
19//!
20//! Promotion is one-way and upward, which is Y4. A set that has been a hash
21//! table does not go back to an intset when it shrinks, and neither does Redis's:
22//! a set that demoted on the way down would rewrite itself on every second
23//! operation for a workload that adds and removes across a threshold.
24//!
25//! # The rules, and the two that are not obvious
26//!
27//! Adding an integer to an intset keeps it an intset until it holds more than
28//! `set-max-intset-entries`, and then it becomes a **hash table** and not a
29//! listpack, because the intset ceiling is 512 and the listpack ceiling is 128
30//! and something over the first is well over the second.
31//!
32//! Adding a non integer to an intset is the asymmetric one. It becomes a
33//! listpack only if the intset is currently under the *listpack* ceiling of 128,
34//! so an intset of 200 integers that receives one string goes straight to a hash
35//! table and is never a listpack at all. Reading that off the source was worth
36//! more than reasoning about it, because the natural implementation converts to
37//! a listpack whenever the members would fit and gets a different encoding name
38//! than a real server for a shape a test suite actually builds.
39//!
40//! # Members
41//!
42//! A member comes back as a [`Member`], which is [`listpack::Entry`] under
43//! another name: either the bytes as they lie or an integer that has not been
44//! formatted yet. All three representations can produce one without copying, and
45//! the formatting happens once, into the reply buffer, at the moment the reply is
46//! built. That is Y18, and it is the same reason [`crate::value::Str`] has two
47//! arms.
48
49use crate::elem::Elements;
50use crate::intset::Intset;
51use crate::listpack::{self, Listpack};
52use crate::scan::Cursor;
53use yo_common::num::{DIGITS_MAX, i64_digits, i64_len, parse_i64};
54
55/// A set member: bytes as they lie, or an integer not yet formatted.
56pub type Member<'a> = listpack::Entry<'a>;
57
58/// A member on its way to being asked about, in every form the three
59/// representations want it in.
60///
61/// Set algebra walks one set and asks every other set the same question about
62/// each member, and the three representations do not want the question in the
63/// same shape. An intset wants a number, a listpack wants bytes and a number
64/// because it holds both kinds, and an element table wants bytes and their
65/// hash. Asking through [`Set::contains`] would redo all of that per question:
66/// a parse for the intset, another parse inside the listpack, and a hash per
67/// table. This does each once per member and then asks `k - 1` times.
68///
69/// The hash is computed whether or not any operand is a table, which is waste
70/// when none is. It is waste worth taking, because the sets where it is wasted
71/// are an intset or a listpack, which are capped at a few hundred members, and
72/// an operation over sets that small is finished before the saving could have
73/// been measured. The sets where the hash pays are the large ones, and those
74/// are tables by definition.
75#[derive(Debug, Clone, Copy)]
76pub struct Needle<'a> {
77    /// The member as bytes, which for an integer member is a caller's buffer.
78    bytes: &'a [u8],
79    /// The number it is, if it is one, under the same rule that decides whether
80    /// a set stores it as one.
81    int: Option<i64>,
82    /// What an element table would key it under.
83    hash: u64,
84}
85
86impl<'a> Needle<'a> {
87    /// A needle from bytes, which is what a command line argument is.
88    #[must_use]
89    pub fn new(bytes: &'a [u8]) -> Needle<'a> {
90        Needle {
91            bytes,
92            int: parse_i64(bytes),
93            hash: Elements::<()>::hash_of(bytes),
94        }
95    }
96
97    /// A needle from a member walked out of a set.
98    ///
99    /// `digits` is where an integer member's text goes, because an intset holds
100    /// the number and the digits do not exist anywhere until somebody writes
101    /// them. It is the caller's buffer rather than a field so that the needle
102    /// stays a borrow and the buffer is written once per member rather than
103    /// allocated once per member.
104    ///
105    /// A member that came out as bytes is still parsed, because the set being
106    /// asked may be an intset and `SINTER ints strings` has to find the members
107    /// they share. A member that came out as a number is not, which is the
108    /// whole saving.
109    #[must_use]
110    pub fn of(member: Member<'a>, digits: &'a mut [u8; DIGITS_MAX]) -> Needle<'a> {
111        match member {
112            Member::Str(s) => Needle::new(s),
113            Member::Int(n) => {
114                let bytes = i64_digits(digits, n);
115                Needle {
116                    bytes,
117                    int: Some(n),
118                    hash: Elements::<()>::hash_of(bytes),
119                }
120            }
121        }
122    }
123
124    /// The member as bytes, which is what a caller collecting an answer wants.
125    #[must_use]
126    pub const fn bytes(&self) -> &'a [u8] {
127        self.bytes
128    }
129}
130
131/// Where the encodings change over.
132///
133/// These are `set-max-intset-entries`, `set-max-listpack-entries` and
134/// `set-max-listpack-value`, and they are runtime configuration in Redis, so
135/// they are a value passed in here rather than three constants. The defaults are
136/// Redis's defaults and a client that never touches `CONFIG SET` sees exactly
137/// the encodings a real server would give it.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub struct Limits {
140    /// Past this many members an all integer set stops being an intset.
141    pub max_intset_entries: usize,
142    /// At this many members a set stops being a listpack.
143    pub max_listpack_entries: usize,
144    /// A member longer than this cannot go in a listpack.
145    pub max_listpack_value: usize,
146}
147
148impl Limits {
149    /// Redis's defaults: 512, 128 and 64.
150    pub const DEFAULT: Limits = Limits {
151        max_intset_entries: 512,
152        max_listpack_entries: 128,
153        max_listpack_value: 64,
154    };
155}
156
157impl Default for Limits {
158    fn default() -> Limits {
159        Limits::DEFAULT
160    }
161}
162
163/// Which of the three a set is in, which is what `OBJECT ENCODING` reports.
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum Encoding {
166    /// All members are integers and there are few enough of them.
167    Intset,
168    /// One packed blob, walked linearly.
169    Listpack,
170    /// The element table.
171    Hashtable,
172}
173
174impl Encoding {
175    /// The word `OBJECT ENCODING` replies with.
176    #[inline]
177    pub const fn name(self) -> &'static str {
178        match self {
179            Encoding::Intset => "intset",
180            Encoding::Listpack => "listpack",
181            Encoding::Hashtable => "hashtable",
182        }
183    }
184}
185
186/// The three representations.
187#[derive(Debug, Clone)]
188enum Body {
189    Ints(Intset),
190    Packed(Listpack),
191    Table(Elements<()>),
192}
193
194/// A set of members.
195#[derive(Debug, Clone)]
196pub struct Set {
197    body: Body,
198}
199
200impl Set {
201    /// An empty set, which starts as an intset.
202    ///
203    /// This is what `SADD` on a missing key creates when it has no size hint,
204    /// and the first member decides nothing: an intset that receives a string
205    /// converts on the spot, and it costs a conversion of nothing.
206    #[must_use]
207    pub fn new() -> Set {
208        Set {
209            body: Body::Ints(Intset::new()),
210        }
211    }
212
213    /// An empty set sized for what is about to go in it.
214    ///
215    /// Redis's `setTypeCreate`, which picks the representation from the first
216    /// member and the count the caller expects, so that `SADD k a b c ...` with
217    /// a thousand arguments builds a table once rather than converting twice on
218    /// the way there. `hint` is only a hint and being wrong about it costs a
219    /// conversion and no correctness.
220    #[must_use]
221    pub fn with_hint(first: &[u8], hint: usize, limits: &Limits) -> Set {
222        if parse_i64(first).is_some() && hint <= limits.max_intset_entries {
223            Set {
224                body: Body::Ints(Intset::with_capacity(hint)),
225            }
226        } else if hint <= limits.max_listpack_entries {
227            Set {
228                body: Body::Packed(Listpack::new()),
229            }
230        } else {
231            Set {
232                body: Body::Table(Elements::with_capacity(hint)),
233            }
234        }
235    }
236
237    /// Which representation this is in.
238    #[inline]
239    #[must_use]
240    pub const fn encoding(&self) -> Encoding {
241        match self.body {
242            Body::Ints(_) => Encoding::Intset,
243            Body::Packed(_) => Encoding::Listpack,
244            Body::Table(_) => Encoding::Hashtable,
245        }
246    }
247
248    /// How many members. This is `SCARD`.
249    #[inline]
250    pub fn len(&self) -> usize {
251        match &self.body {
252            Body::Ints(s) => s.len(),
253            Body::Packed(lp) => lp.len(),
254            Body::Table(t) => t.len(),
255        }
256    }
257
258    /// Whether there are none.
259    ///
260    /// An empty set does not exist in Redis, so the caller deletes the key when
261    /// this turns true rather than storing an empty one.
262    #[inline]
263    pub fn is_empty(&self) -> bool {
264        self.len() == 0
265    }
266
267    /// Whether `member` is in the set. This is `SISMEMBER`.
268    #[must_use]
269    pub fn contains(&self, member: &[u8]) -> bool {
270        match &self.body {
271            // A member that is not an integer cannot be in a set of integers,
272            // and answering that costs a parse rather than a search.
273            Body::Ints(s) => parse_i64(member).is_some_and(|v| s.contains(v)),
274            Body::Packed(lp) => lp.find(member, 1).is_some(),
275            Body::Table(t) => t.contains(member),
276        }
277    }
278
279    /// The same question asked with the work already done. See [`Needle`].
280    ///
281    /// This is what set algebra probes with. Every arm is the arm
282    /// [`Set::contains`] would have taken, with the parse and the hash lifted
283    /// out of it, so the two cannot disagree about what a member is.
284    #[must_use]
285    #[inline]
286    pub fn has(&self, needle: &Needle<'_>) -> bool {
287        match &self.body {
288            Body::Ints(s) => needle.int.is_some_and(|v| s.contains(v)),
289            Body::Packed(lp) => lp.find_parsed(needle.bytes, needle.int, 1).is_some(),
290            Body::Table(t) => t.contains_hashed(needle.hash, needle.bytes),
291        }
292    }
293
294    /// The member at `index`, in whatever order the representation holds them.
295    ///
296    /// Ascending for an intset, insertion order for the other two. Redis makes
297    /// no promise about set order and neither does this, but a uniform draw
298    /// needs positions and this is what gives it them (K9).
299    #[must_use]
300    pub fn at(&self, index: usize) -> Option<Member<'_>> {
301        match &self.body {
302            Body::Ints(s) => s.get(index).map(Member::Int),
303            Body::Packed(lp) => lp.get(index),
304            Body::Table(t) => t.at(index).map(|(name, _)| Member::Str(name)),
305        }
306    }
307
308    /// Every member.
309    pub fn iter(&self) -> impl Iterator<Item = Member<'_>> {
310        (0..self.len()).map(|i| self.at(i).expect("index is under the length"))
311    }
312
313    /// Walk part of the set and say where to resume. This is `SSCAN`.
314    ///
315    /// Only the table band walks in windows. An intset or a listpack hands back
316    /// every member in one call and a cursor of [`Cursor::END`], ignoring the
317    /// cursor it was given, which is what Redis does for the same two encodings
318    /// and for the same reason: a hundred and twenty eight members is smaller
319    /// than the reply header arithmetic to split them up, and a set that small
320    /// cannot block the loop long enough for the split to be worth anything.
321    ///
322    /// Ignoring the cursor is safe rather than merely convenient, because
323    /// promotion is one way. A set that gave a client a table cursor is still a
324    /// table when the client comes back, so the only way to arrive here with a
325    /// cursor from somewhere else is for the key to have been deleted and
326    /// remade underneath the scan, and returning everything to that client
327    /// returns a member twice at worst, which the guarantee allows.
328    pub fn scan<F>(&self, cursor: Cursor, count: usize, mut f: F) -> Cursor
329    where
330        F: FnMut(Member<'_>),
331    {
332        match &self.body {
333            Body::Table(t) => t.scan(cursor, count, |name, ()| f(Member::Str(name))),
334            _ => {
335                for m in self.iter() {
336                    f(m);
337                }
338                Cursor::END
339            }
340        }
341    }
342
343    /// Bytes held by whichever representation this is.
344    #[must_use]
345    pub fn memory_bytes(&self) -> usize {
346        match &self.body {
347            Body::Ints(s) => s.memory_bytes(),
348            Body::Packed(lp) => lp.byte_len(),
349            Body::Table(t) => t.memory_bytes(),
350        }
351    }
352
353    /// Add `member`, promoting if it no longer fits. Answers whether it was new.
354    ///
355    /// This is `setTypeAdd`, arm for arm.
356    pub fn add(&mut self, member: &[u8], limits: &Limits) -> bool {
357        match &mut self.body {
358            Body::Table(t) => return t.insert(member, ()).is_ok_and(|old| old.is_none()),
359            Body::Packed(lp) => {
360                if lp.find(member, 1).is_some() {
361                    return false;
362                }
363                if lp.len() < limits.max_listpack_entries
364                    && member.len() <= limits.max_listpack_value
365                {
366                    lp.push(member);
367                    return true;
368                }
369                // Too many members, or one too long. It falls out to a table.
370            }
371            Body::Ints(s) => {
372                if let Some(v) = parse_i64(member) {
373                    if !s.add(v) {
374                        return false;
375                    }
376                    // Strictly greater, so the 512th member is still an intset
377                    // and the 513th is not. And it becomes a table rather than
378                    // a listpack, because anything past 512 is past 128 too.
379                    if s.len() > limits.max_intset_entries {
380                        self.become_table(0);
381                    }
382                    return true;
383                }
384                // Not an integer, so it is certainly not in a set of integers
385                // already. If the set is still small enough it becomes a
386                // listpack, and otherwise it falls out to a table.
387                if self.ints_fit_a_listpack(member, limits) {
388                    self.become_listpack();
389                    self.push_new(member);
390                    return true;
391                }
392            }
393        }
394        self.become_table(1);
395        self.push_new(member);
396        true
397    }
398
399    /// Put in a member already known to be new and known to fit where it is.
400    ///
401    /// Only ever called on the far side of a promotion, where both of those are
402    /// facts the promotion established and not things worth establishing twice.
403    fn push_new(&mut self, member: &[u8]) {
404        match &mut self.body {
405            Body::Packed(lp) => lp.push(member),
406            Body::Table(t) => {
407                t.insert(member, ())
408                    .expect("the table was sized for this one");
409            }
410            Body::Ints(_) => unreachable!("no promotion ever lands on an intset"),
411        }
412    }
413
414    /// Remove `member`. Answers whether it was there.
415    ///
416    /// Never demotes, which is Y4's one-way rule and Redis's behaviour.
417    pub fn remove(&mut self, member: &[u8]) -> bool {
418        match &mut self.body {
419            Body::Ints(s) => parse_i64(member).is_some_and(|v| s.remove(v)),
420            Body::Packed(lp) => match lp.find(member, 1) {
421                Some(at) => lp.delete(at, 1),
422                None => false,
423            },
424            Body::Table(t) => t.remove(member).is_some(),
425        }
426    }
427
428    /// Take out the member at `index` and hand it back.
429    ///
430    /// This is what `SPOP` runs on top of. The table moves its last row into the
431    /// hole rather than shifting, so the position of every other member is
432    /// stable except for one; the other two shift. Neither is a promise a caller
433    /// can lean on, and `SPOP` does not need one because it draws again from the
434    /// new length each time.
435    pub fn remove_at(&mut self, index: usize) -> Option<Vec<u8>> {
436        match &mut self.body {
437            Body::Ints(s) => {
438                let v = s.get(index)?;
439                s.remove(v);
440                let mut out = Vec::with_capacity(i64_len(v));
441                Member::Int(v).write_to(&mut out);
442                Some(out)
443            }
444            Body::Packed(lp) => {
445                let out = lp.get(index)?.to_vec();
446                lp.delete(index, 1);
447                Some(out)
448            }
449            Body::Table(t) => t.take_at(index).map(|(name, ())| name),
450        }
451    }
452
453    /// Take out the member at `index` without building it into a `Vec` first.
454    ///
455    /// The same removal as [`Set::remove_at`] for a caller that has already read
456    /// the member and does not need it handed back. That caller is `SPOP` on the
457    /// wire, which reads with [`Set::at`], writes the bytes straight into the
458    /// reply buffer, and only then calls this. It is an allocation a member
459    /// saved on the one command in the set whose whole cost is the allocating.
460    ///
461    /// [`Set::remove_at`] stays for the embedded API, where the caller wants the
462    /// bytes and has nowhere to put them.
463    pub fn drop_at(&mut self, index: usize) -> bool {
464        match &mut self.body {
465            Body::Ints(s) => match s.get(index) {
466                Some(v) => {
467                    s.remove(v);
468                    true
469                }
470                None => false,
471            },
472            Body::Packed(lp) => {
473                if index >= lp.len() {
474                    return false;
475                }
476                lp.delete(index, 1);
477                true
478            }
479            Body::Table(t) => t.remove_at(index).is_some(),
480        }
481    }
482
483    /// Whether an intset plus one non integer member would still be a listpack.
484    ///
485    /// Three tests, and the first is the asymmetric one: the count is compared
486    /// against the *listpack* ceiling and not the intset one, so an intset with
487    /// two hundred members is already too big to become a listpack even though
488    /// it is a perfectly legal intset. The other two are the new member's length
489    /// and the longest existing member's length once it is written as digits,
490    /// which only bites when `set-max-listpack-value` has been turned down,
491    /// because no integer is more than twenty characters.
492    fn ints_fit_a_listpack(&self, member: &[u8], limits: &Limits) -> bool {
493        let Body::Ints(s) = &self.body else {
494            return false;
495        };
496        let widest = s
497            .min()
498            .map(i64_len)
499            .unwrap_or(0)
500            .max(s.max().map(i64_len).unwrap_or(0));
501        s.len() < limits.max_listpack_entries
502            && member.len() <= limits.max_listpack_value
503            && widest <= limits.max_listpack_value
504    }
505
506    /// Rewrite as a listpack, which only an intset ever does.
507    fn become_listpack(&mut self) {
508        let Body::Ints(s) = &self.body else {
509            return;
510        };
511        let mut lp = Listpack::new();
512        let mut buf = Vec::with_capacity(20);
513        for v in s.iter() {
514            buf.clear();
515            Member::Int(v).write_to(&mut buf);
516            lp.push(&buf);
517        }
518        self.body = Body::Packed(lp);
519    }
520
521    /// Rewrite as an element table, with room for `extra` more members.
522    fn become_table(&mut self, extra: usize) {
523        let mut t = Elements::with_capacity(self.len() + extra);
524        let mut buf = Vec::with_capacity(20);
525        for m in self.iter() {
526            match m {
527                Member::Str(b) => {
528                    t.insert(b, ()).expect("room, and every member was unique");
529                }
530                Member::Int(v) => {
531                    buf.clear();
532                    Member::Int(v).write_to(&mut buf);
533                    t.insert(&buf, ())
534                        .expect("room, and every member was unique");
535                }
536            }
537        }
538        self.body = Body::Table(t);
539    }
540}
541
542impl Default for Set {
543    fn default() -> Set {
544        Set::new()
545    }
546}
547
548#[cfg(test)]
549mod tests {
550    use super::*;
551
552    fn of(members: &[&str]) -> Set {
553        let mut s = Set::new();
554        for m in members {
555            assert!(s.add(m.as_bytes(), &Limits::DEFAULT), "{m} was new");
556        }
557        s
558    }
559
560    fn members(s: &Set) -> Vec<String> {
561        let mut v: Vec<String> = s
562            .iter()
563            .map(|m| String::from_utf8(m.to_vec()).expect("utf8 in these tests"))
564            .collect();
565        // Order is not part of the contract and the three representations do not
566        // agree on it, so every assertion here is against a sorted list.
567        v.sort();
568        v
569    }
570
571    #[test]
572    fn a_new_set_is_an_empty_intset() {
573        let s = Set::new();
574        assert_eq!(s.encoding(), Encoding::Intset);
575        assert_eq!(s.len(), 0);
576        assert!(s.is_empty());
577        assert!(!s.contains(b"1"));
578        assert_eq!(s.at(0), None);
579    }
580
581    #[test]
582    fn integers_stay_an_intset_and_come_back_as_members() {
583        let s = of(&["1", "2", "3"]);
584        assert_eq!(s.encoding(), Encoding::Intset);
585        assert_eq!(members(&s), ["1", "2", "3"]);
586        assert!(s.contains(b"2"));
587        assert!(!s.contains(b"4"));
588        assert!(!s.contains(b"two"));
589    }
590
591    #[test]
592    fn adding_the_same_member_twice_says_so_in_all_three() {
593        let mut ints = of(&["1", "2"]);
594        assert!(!ints.add(b"2", &Limits::DEFAULT));
595        assert_eq!(ints.len(), 2);
596
597        let mut packed = of(&["a", "b"]);
598        assert_eq!(packed.encoding(), Encoding::Listpack);
599        assert!(!packed.add(b"b", &Limits::DEFAULT));
600        assert_eq!(packed.len(), 2);
601
602        let mut table = of(&["a", "b"]);
603        table.become_table(0);
604        assert!(!table.add(b"b", &Limits::DEFAULT));
605        assert_eq!(table.len(), 2);
606    }
607
608    #[test]
609    fn a_string_turns_a_small_intset_into_a_listpack() {
610        let mut s = of(&["1", "2", "3"]);
611        assert!(s.add(b"hello", &Limits::DEFAULT));
612        assert_eq!(s.encoding(), Encoding::Listpack);
613        assert_eq!(members(&s), ["1", "2", "3", "hello"]);
614        assert!(s.contains(b"1"), "the integers survived the rewrite");
615        assert!(s.contains(b"hello"));
616    }
617
618    #[test]
619    fn a_string_turns_a_big_intset_straight_into_a_table() {
620        // The asymmetric rule, and the one a natural implementation gets wrong.
621        // Two hundred integers is a legal intset and is already past the
622        // listpack ceiling, so this never passes through the listpack at all.
623        let mut s = Set::new();
624        for i in 0..200 {
625            s.add(i.to_string().as_bytes(), &Limits::DEFAULT);
626        }
627        assert_eq!(s.encoding(), Encoding::Intset);
628        assert_eq!(s.len(), 200);
629
630        assert!(s.add(b"hello", &Limits::DEFAULT));
631        assert_eq!(s.encoding(), Encoding::Hashtable);
632        assert_eq!(s.len(), 201);
633        assert!(s.contains(b"199"));
634        assert!(s.contains(b"hello"));
635    }
636
637    #[test]
638    fn an_intset_holds_five_hundred_and_twelve_and_converts_at_the_next_one() {
639        let mut s = Set::new();
640        for i in 0..512 {
641            s.add(i.to_string().as_bytes(), &Limits::DEFAULT);
642        }
643        assert_eq!(s.encoding(), Encoding::Intset, "512 is still an intset");
644        assert_eq!(s.len(), 512);
645
646        s.add(b"512", &Limits::DEFAULT);
647        assert_eq!(s.encoding(), Encoding::Hashtable, "513 is not");
648        assert_eq!(s.len(), 513);
649        // And not a listpack on the way, because 513 is well past 128.
650        for i in 0..513 {
651            assert!(s.contains(i.to_string().as_bytes()), "{i} survived");
652        }
653    }
654
655    #[test]
656    fn a_listpack_converts_at_a_hundred_and_twenty_eight_members() {
657        let mut s = of(&["x"]);
658        assert_eq!(s.encoding(), Encoding::Listpack);
659        for i in 0..127 {
660            s.add(format!("m{i}").as_bytes(), &Limits::DEFAULT);
661        }
662        assert_eq!(s.len(), 128);
663        assert_eq!(s.encoding(), Encoding::Listpack, "128 is still a listpack");
664
665        s.add(b"one more", &Limits::DEFAULT);
666        assert_eq!(s.len(), 129);
667        assert_eq!(s.encoding(), Encoding::Hashtable);
668        assert!(s.contains(b"x"));
669        assert!(s.contains(b"m126"));
670        assert!(s.contains(b"one more"));
671    }
672
673    #[test]
674    fn a_long_member_converts_a_listpack_whatever_the_count() {
675        let mut s = of(&["a"]);
676        let long = vec![b'z'; 65];
677        assert!(s.add(&long, &Limits::DEFAULT));
678        assert_eq!(s.encoding(), Encoding::Hashtable, "65 is past 64");
679        assert_eq!(s.len(), 2);
680        assert!(s.contains(&long));
681
682        // And exactly at the boundary it does not.
683        let mut ok = of(&["a"]);
684        ok.add(&[b'z'; 64], &Limits::DEFAULT);
685        assert_eq!(ok.encoding(), Encoding::Listpack, "64 fits");
686    }
687
688    #[test]
689    fn a_long_member_sends_an_intset_to_a_table_and_not_a_listpack() {
690        let mut s = of(&["1", "2"]);
691        assert!(s.add(&[b'z'; 65], &Limits::DEFAULT));
692        assert_eq!(s.encoding(), Encoding::Hashtable);
693        assert_eq!(s.len(), 3);
694    }
695
696    #[test]
697    fn the_limits_are_configuration_and_moving_them_moves_the_encodings() {
698        let tight = Limits {
699            max_intset_entries: 2,
700            max_listpack_entries: 2,
701            max_listpack_value: 3,
702        };
703        let mut s = Set::new();
704        s.add(b"1", &tight);
705        s.add(b"2", &tight);
706        assert_eq!(s.encoding(), Encoding::Intset);
707        s.add(b"3", &tight);
708        assert_eq!(s.encoding(), Encoding::Hashtable, "three is past two");
709
710        // And a member longer than three characters cannot go in a listpack.
711        let mut t = Set::new();
712        t.add(b"abc", &tight);
713        assert_eq!(t.encoding(), Encoding::Listpack, "three characters fit");
714        t.add(b"defg", &tight);
715        assert_eq!(t.encoding(), Encoding::Hashtable, "four do not");
716        assert!(t.contains(b"abc"));
717        assert!(t.contains(b"defg"));
718
719        // Including as the first member, which is a table from the off rather
720        // than a listpack that converts on the next thing to arrive. Redis gets
721        // to the same place by the other road: it creates the listpack, tries
722        // the add, and converts before the reply.
723        let mut u = Set::new();
724        u.add(b"abcd", &tight);
725        assert_eq!(u.encoding(), Encoding::Hashtable);
726        assert!(u.contains(b"abcd"));
727    }
728
729    #[test]
730    fn with_hint_picks_the_representation_up_front() {
731        let d = &Limits::DEFAULT;
732        assert_eq!(
733            Set::with_hint(b"1", 10, d).encoding(),
734            Encoding::Intset,
735            "an integer and few enough of them"
736        );
737        assert_eq!(
738            Set::with_hint(b"1", 1000, d).encoding(),
739            Encoding::Hashtable,
740            "an integer and too many"
741        );
742        assert_eq!(
743            Set::with_hint(b"x", 10, d).encoding(),
744            Encoding::Listpack,
745            "not an integer and few enough"
746        );
747        assert_eq!(
748            Set::with_hint(b"x", 1000, d).encoding(),
749            Encoding::Hashtable,
750            "not an integer and too many"
751        );
752    }
753
754    #[test]
755    fn removing_works_in_all_three_and_never_demotes() {
756        let mut ints = of(&["1", "2", "3"]);
757        assert!(ints.remove(b"2"));
758        assert!(!ints.remove(b"2"));
759        assert!(!ints.remove(b"nope"), "not an integer, so not a member");
760        assert_eq!(members(&ints), ["1", "3"]);
761        assert_eq!(ints.encoding(), Encoding::Intset);
762
763        let mut packed = of(&["a", "b", "c"]);
764        assert!(packed.remove(b"b"));
765        assert!(!packed.remove(b"b"));
766        assert_eq!(members(&packed), ["a", "c"]);
767        assert_eq!(packed.encoding(), Encoding::Listpack);
768
769        let mut table = of(&["a", "b", "c"]);
770        table.become_table(0);
771        assert!(table.remove(b"b"));
772        assert!(!table.remove(b"b"));
773        assert_eq!(members(&table), ["a", "c"]);
774        assert_eq!(
775            table.encoding(),
776            Encoding::Hashtable,
777            "down to two members and still a table"
778        );
779    }
780
781    #[test]
782    fn a_set_can_be_emptied_a_member_at_a_time() {
783        for mut s in [of(&["1", "2", "3"]), of(&["a", "b", "c"])] {
784            let all: Vec<Vec<u8>> = s.iter().map(|m| m.to_vec()).collect();
785            for m in &all {
786                assert!(s.remove(m));
787            }
788            assert!(s.is_empty());
789            assert_eq!(s.at(0), None);
790        }
791    }
792
793    #[test]
794    fn removing_by_position_hands_the_member_back() {
795        // What `SPOP` runs on. Drawing position zero every time has to empty the
796        // set rather than run off the end or repeat a member, in all three.
797        let mut table = of(&["a", "b", "c", "d"]);
798        table.become_table(0);
799        for mut s in [
800            of(&["10", "20", "30", "40"]),
801            of(&["a", "b", "c", "d"]),
802            table,
803        ] {
804            let mut got = Vec::new();
805            while !s.is_empty() {
806                got.push(String::from_utf8(s.remove_at(0).expect("not empty")).expect("utf8"));
807            }
808            got.sort();
809            assert_eq!(got.len(), 4, "four members and no repeats");
810            assert_eq!(s.len(), 0);
811            assert_eq!(s.remove_at(0), None);
812        }
813    }
814
815    #[test]
816    fn an_integer_member_is_the_same_member_however_it_is_written() {
817        // An intset holds 42 as a number, so `SADD s 42` twice is one member.
818        // `042` does not parse as an integer, so it is a different member and it
819        // converts the set, which is what a real server does too.
820        let mut s = of(&["42"]);
821        assert!(!s.add(b"42", &Limits::DEFAULT));
822        assert_eq!(s.len(), 1);
823        assert!(s.add(b"042", &Limits::DEFAULT));
824        assert_eq!(s.encoding(), Encoding::Listpack);
825        assert_eq!(members(&s), ["042", "42"]);
826        assert!(s.contains(b"42"));
827        assert!(s.contains(b"042"));
828    }
829
830    #[test]
831    fn the_small_bands_answer_a_scan_in_one_go() {
832        for s in [of(&["1", "2", "3"]), of(&["a", "b", "c"])] {
833            let mut seen = Vec::new();
834            // A count of one, which the table band would honour and these two
835            // do not, and a cursor from nowhere, which these two ignore.
836            let next = s.scan(Cursor::at(1, 0, 99), 1, |m| seen.push(m.to_vec()));
837            assert!(next.is_end(), "{:?} split a scan up", s.encoding());
838            assert_eq!(seen.len(), 3);
839        }
840    }
841
842    #[test]
843    fn the_table_band_walks_a_scan_in_windows_and_misses_nothing() {
844        let mut s = Set::new();
845        for i in 0..300 {
846            s.add(format!("m{i}").as_bytes(), &Limits::DEFAULT);
847        }
848        assert_eq!(s.encoding(), Encoding::Hashtable);
849
850        let mut seen = Vec::new();
851        let mut c = Cursor::START;
852        let mut turns = 0;
853        loop {
854            c = s.scan(c, 7, |m| seen.push(m.to_vec()));
855            turns += 1;
856            assert!(turns < 100, "the scan did not finish");
857            if c.is_end() {
858                break;
859            }
860        }
861        assert!(
862            turns > 1,
863            "a window of seven over three hundred took one turn"
864        );
865        seen.sort();
866        seen.dedup();
867        assert_eq!(seen.len(), 300, "every member came back at least once");
868    }
869
870    #[test]
871    fn a_conversion_loses_no_member_at_any_of_the_three_boundaries() {
872        // One walk over each path out of an intset and out of a listpack, each
873        // checking every member is still findable after the rewrite rather than
874        // only checking the count.
875        let mut wide = Set::new();
876        for v in [i64::MIN, -1, 0, 1, i64::MAX] {
877            wide.add(v.to_string().as_bytes(), &Limits::DEFAULT);
878        }
879        wide.add(b"str", &Limits::DEFAULT);
880        assert_eq!(wide.encoding(), Encoding::Listpack);
881        for v in [i64::MIN, -1, 0, 1, i64::MAX] {
882            assert!(wide.contains(v.to_string().as_bytes()), "{v} survived");
883        }
884
885        wide.add(&[b'q'; 100], &Limits::DEFAULT);
886        assert_eq!(wide.encoding(), Encoding::Hashtable);
887        for v in [i64::MIN, -1, 0, 1, i64::MAX] {
888            assert!(
889                wide.contains(v.to_string().as_bytes()),
890                "{v} survived twice"
891            );
892        }
893        assert!(wide.contains(b"str"));
894        assert_eq!(wide.len(), 7);
895    }
896}