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 /// The bytes behind a set that is stored the way Redis stores it.
315 ///
316 /// `DUMP` writes these straight out instead of walking the members, so this
317 /// exists for [`crate::rdb`] and for nothing else. The word this answers
318 /// against is [`Set::encoding`] and not the body, so a set that calls itself
319 /// a hashtable is walked even on the rare occasion its members are still in
320 /// one intset run. Keeping those two in step is what makes the rule sayable:
321 /// whatever `OBJECT ENCODING` says, that is the type byte the payload gets.
322 ///
323 /// `None` for a set with no such shape, which is the table, the partitioned
324 /// body and an intset that has split into runs.
325 #[inline]
326 pub(crate) fn packed_bytes(&self) -> Option<&[u8]> {
327 match &self.body {
328 Body::Ints(s) if !self.ints_past_limit => s.as_bytes(),
329 Body::Packed(lp) => Some(lp.as_bytes()),
330 _ => None,
331 }
332 }
333
334 /// How many members. This is `SCARD`.
335 #[inline]
336 pub fn len(&self) -> usize {
337 match &self.body {
338 Body::Ints(s) => s.len(),
339 Body::Packed(lp) => lp.len(),
340 Body::Table(t) => t.len(),
341 Body::Split(p) => p.len(),
342 }
343 }
344
345 /// Whether there are none.
346 ///
347 /// An empty set does not exist in Redis, so the caller deletes the key when
348 /// this turns true rather than storing an empty one.
349 #[inline]
350 pub fn is_empty(&self) -> bool {
351 self.len() == 0
352 }
353
354 /// Whether `member` is in the set. This is `SISMEMBER`.
355 #[must_use]
356 pub fn contains(&self, member: &[u8]) -> bool {
357 match &self.body {
358 // A member that is not an integer cannot be in a set of integers,
359 // and answering that costs a parse rather than a search.
360 Body::Ints(s) => parse_i64(member).is_some_and(|v| s.contains(v)),
361 Body::Packed(lp) => lp.find(member, 1).is_some(),
362 Body::Table(t) => t.contains(member),
363 Body::Split(p) => p.contains(member),
364 }
365 }
366
367 /// The same question asked with the work already done. See [`Needle`].
368 ///
369 /// This is what set algebra probes with. Every arm is the arm
370 /// [`Set::contains`] would have taken, with the parse and the hash lifted
371 /// out of it, so the two cannot disagree about what a member is.
372 #[must_use]
373 #[inline]
374 pub fn has(&self, needle: &Needle<'_>) -> bool {
375 match &self.body {
376 Body::Ints(s) => needle.int.is_some_and(|v| s.contains(v)),
377 Body::Packed(lp) => lp.find_parsed(needle.bytes, needle.int, 1).is_some(),
378 Body::Table(t) => t.contains_hashed(needle.hash, needle.bytes),
379 Body::Split(p) => p.contains_hashed(needle.hash, needle.bytes),
380 }
381 }
382
383 /// The member at `index`, in whatever order the representation holds them.
384 ///
385 /// Ascending for an intset, insertion order for the other two. Redis makes
386 /// no promise about set order and neither does this, but a uniform draw
387 /// needs positions and this is what gives it them (K9).
388 #[must_use]
389 pub fn at(&self, index: usize) -> Option<Member<'_>> {
390 match &self.body {
391 Body::Ints(s) => s.get(index).map(Member::Int),
392 Body::Packed(lp) => lp.get(index),
393 Body::Table(t) => t.at(index).map(|(name, _)| Member::Str(name)),
394 Body::Split(p) => p.at(index).map(|(name, _)| Member::Str(name)),
395 }
396 }
397
398 /// Every member.
399 pub fn iter(&self) -> impl Iterator<Item = Member<'_>> {
400 (0..self.len()).map(|i| self.at(i).expect("index is under the length"))
401 }
402
403 /// The members as a sorted array of integers, when that is what this is.
404 ///
405 /// The one place the representation is not an implementation detail, and it
406 /// is here for [`crate::setops`]: two sorted arrays intersect by stepping
407 /// through both of them with no hash anywhere, which is a different order of
408 /// cost from asking a table a question per member. That was worth nothing
409 /// while an all integer set turned into a table at five hundred and twelve
410 /// members, and it is worth a great deal now that it does not.
411 #[inline]
412 #[must_use]
413 pub const fn ints(&self) -> Option<&Intset> {
414 match &self.body {
415 Body::Ints(s) => Some(s),
416 _ => None,
417 }
418 }
419
420 /// Walk part of the set and say where to resume. This is `SSCAN`.
421 ///
422 /// Only the table and the partitioned band walk in windows. An intset or a
423 /// listpack hands back
424 /// every member in one call and a cursor of [`Cursor::END`], ignoring the
425 /// cursor it was given, which is what Redis does for the same two encodings
426 /// and for the same reason: a hundred and twenty eight members is smaller
427 /// than the reply header arithmetic to split them up, and a set that small
428 /// cannot block the loop long enough for the split to be worth anything.
429 ///
430 /// Ignoring the cursor is safe rather than merely convenient, because
431 /// promotion is one way. A set that gave a client a listpack cursor is not
432 /// going to be a listpack again, so the only way to arrive at those two arms
433 /// with a cursor from somewhere else is for the key to have been deleted and
434 /// remade underneath the scan, and returning everything to that client
435 /// returns a member twice at worst, which the guarantee allows.
436 ///
437 /// A table cursor arriving at the band is the one crossing that does happen,
438 /// because a set can split part way through a client's scan. That is handled
439 /// rather than ignored: a table cursor names one partition, and
440 /// [`Cursor::rebase`] reads the widening and restarts the walk at the top of
441 /// the new layout, so the client sees some members a second time and misses
442 /// none. Repeats are what the `SCAN` guarantee gives up in exchange for
443 /// surviving a resize, and a set only splits once.
444 pub fn scan<F>(&self, cursor: Cursor, count: usize, mut f: F) -> Cursor
445 where
446 F: FnMut(Member<'_>),
447 {
448 match &self.body {
449 Body::Table(t) => t.scan(cursor, count, |name, ()| f(Member::Str(name))),
450 Body::Split(p) => p.scan(cursor, count, |name, ()| f(Member::Str(name))),
451 // An intset past the ceiling is the one thing outside the table
452 // band that a single reply cannot hold. Redis has a dictionary by
453 // this point and walks it in windows, and a set of a million
454 // integers answering `SSCAN` with a million members in one go would
455 // be a several megabyte reply and a loop iteration nobody could
456 // measure. So it walks in windows too, and by index, which the runs
457 // answer in a walk down their tree rather than a walk along them.
458 //
459 // Downward, which is the direction the element table walks and for
460 // the same reason. Positions in a sorted array shift when a member
461 // below them goes, so an upward walk would miss a member for every
462 // one removed behind it, and `SSCAN` followed by `SREM` on what it
463 // found is the commonest thing anyone does with this command.
464 // Walking down means those removals are all above the cursor, where
465 // they cost nothing.
466 Body::Ints(s) if self.ints_past_limit && !s.is_empty() => {
467 let top = s.len() - 1;
468 let mut at = match cursor.rebase(1).idx() {
469 Some(idx) => (idx as usize).min(top),
470 None => top,
471 };
472 for _ in 0..count.max(1) {
473 f(Member::Int(s.at(at)));
474 if at == 0 {
475 return Cursor::END;
476 }
477 at -= 1;
478 }
479 Cursor::at(1, 0, at as u64)
480 }
481 _ => {
482 for m in self.iter() {
483 f(m);
484 }
485 Cursor::END
486 }
487 }
488 }
489
490 /// Bytes held by whichever representation this is.
491 #[must_use]
492 pub fn memory_bytes(&self) -> usize {
493 match &self.body {
494 Body::Ints(s) => s.memory_bytes(),
495 Body::Packed(lp) => lp.byte_len(),
496 Body::Table(t) => t.memory_bytes(),
497 Body::Split(p) => p.memory_bytes(),
498 }
499 }
500
501 /// What the slot array costs on its own, or nothing if there is not one.
502 ///
503 /// The three of these split [`Set::memory_bytes`] into the arrays it is made
504 /// of, which is what turns an argument about where the memory went into a
505 /// number. An intset and a listpack are one allocation with no index over
506 /// it, so they answer all of it under names and nothing under the other two.
507 #[must_use]
508 pub fn slot_bytes(&self) -> usize {
509 match &self.body {
510 Body::Ints(_) | Body::Packed(_) => 0,
511 Body::Table(t) => t.slot_bytes(),
512 Body::Split(p) => p.slot_bytes(),
513 }
514 }
515
516 /// What the row array costs on its own, capacity and not length.
517 #[must_use]
518 pub fn row_bytes(&self) -> usize {
519 match &self.body {
520 Body::Ints(_) | Body::Packed(_) => 0,
521 Body::Table(t) => t.row_bytes(),
522 Body::Split(p) => p.row_bytes(),
523 }
524 }
525
526 /// What the member bytes cost, live ones and dead ones together.
527 #[must_use]
528 pub fn name_bytes(&self) -> usize {
529 match &self.body {
530 Body::Ints(s) => s.memory_bytes(),
531 Body::Packed(lp) => lp.byte_len(),
532 Body::Table(t) => t.name_bytes(),
533 Body::Split(p) => p.name_bytes(),
534 }
535 }
536
537 /// Add `member`, promoting if it no longer fits. Answers whether it was new.
538 ///
539 /// This is `setTypeAdd`, arm for arm.
540 pub fn add(&mut self, member: &[u8], limits: &Limits) -> bool {
541 match &mut self.body {
542 Body::Table(t) => {
543 let new = t.insert(member, ()).is_ok_and(|old| old.is_none());
544 // Checked after the insert rather than before, so the set that
545 // splits is the one that has actually outgrown a table and not
546 // the one that is about to.
547 if t.len() > PARTITION_AT {
548 self.become_split();
549 }
550 return new;
551 }
552 Body::Split(p) => {
553 let new = p.insert(member, ()).is_ok_and(|old| old.is_none());
554 // Asked rather than decided, because growing is a rehash of the
555 // whole set and the band leaves the timing to whoever knows
556 // whether this is one write or the middle of a bulk load.
557 if let Some(want) = p.wants_parts() {
558 p.grow_to(want);
559 }
560 return new;
561 }
562 Body::Packed(lp) => {
563 if lp.find(member, 1).is_some() {
564 return false;
565 }
566 if lp.len() < limits.max_listpack_entries
567 && member.len() <= limits.max_listpack_value
568 {
569 lp.push(member);
570 return true;
571 }
572 // Too many members, or one too long. It falls out to a table.
573 }
574 Body::Ints(s) => {
575 if let Some(v) = parse_i64(member) {
576 if !s.add(v) {
577 return false;
578 }
579 // Strictly greater, so the 512th member is still an intset
580 // and the 513th is what a real server would call a
581 // hashtable. Nothing is rewritten, only the word changes.
582 //
583 // Unless the ceilings have been configured the wrong way
584 // round, where a set past the intset ceiling is still under
585 // the listpack one and a real server puts it in a listpack.
586 // That set is a handful of members and there is no memory
587 // argument for keeping it here, so it goes where it would
588 // have gone.
589 if s.len() > limits.max_intset_entries {
590 if self.ints_fit_a_listpack_alone(limits) {
591 self.become_listpack();
592 } else {
593 self.ints_past_limit = true;
594 }
595 }
596 return true;
597 }
598 // Not an integer, so it is certainly not in a set of integers
599 // already. If the set is still small enough it becomes a
600 // listpack, and otherwise it falls out to a table.
601 if self.ints_fit_a_listpack(member, limits) {
602 self.become_listpack();
603 self.push_new(member);
604 return true;
605 }
606 }
607 }
608 self.become_table(1);
609 self.push_new(member);
610 true
611 }
612
613 /// Put in a member already known to be new and known to fit where it is.
614 ///
615 /// Only ever called on the far side of a promotion, where both of those are
616 /// facts the promotion established and not things worth establishing twice.
617 fn push_new(&mut self, member: &[u8]) {
618 match &mut self.body {
619 Body::Packed(lp) => lp.push(member),
620 Body::Table(t) => {
621 t.insert(member, ())
622 .expect("the table was sized for this one");
623 }
624 Body::Split(p) => {
625 p.insert(member, ())
626 .expect("the band was sized for this one");
627 }
628 Body::Ints(_) => unreachable!("no promotion ever lands on an intset"),
629 }
630 }
631
632 /// Remove `member`. Answers whether it was there.
633 ///
634 /// Never demotes, which is Y4's one-way rule and Redis's behaviour.
635 pub fn remove(&mut self, member: &[u8]) -> bool {
636 match &mut self.body {
637 Body::Ints(s) => parse_i64(member).is_some_and(|v| s.remove(v)),
638 Body::Packed(lp) => match lp.find(member, 1) {
639 Some(at) => lp.delete(at, 1),
640 None => false,
641 },
642 Body::Table(t) => t.remove(member).is_some(),
643 Body::Split(p) => p.remove(member).is_some(),
644 }
645 }
646
647 /// Take out the member at `index` and hand it back.
648 ///
649 /// This is what `SPOP` runs on top of. The table moves its last row into the
650 /// hole rather than shifting, so the position of every other member is
651 /// stable except for one; the other two shift. Neither is a promise a caller
652 /// can lean on, and `SPOP` does not need one because it draws again from the
653 /// new length each time.
654 pub fn remove_at(&mut self, index: usize) -> Option<Vec<u8>> {
655 match &mut self.body {
656 Body::Ints(s) => {
657 let v = s.get(index)?;
658 s.remove(v);
659 let mut out = Vec::with_capacity(i64_len(v));
660 Member::Int(v).write_to(&mut out);
661 Some(out)
662 }
663 Body::Packed(lp) => {
664 let out = lp.get(index)?.to_vec();
665 lp.delete(index, 1);
666 Some(out)
667 }
668 Body::Table(t) => t.take_at(index).map(|(name, ())| name),
669 Body::Split(p) => p.take_at(index).map(|(name, ())| name),
670 }
671 }
672
673 /// Take out the member at `index` without building it into a `Vec` first.
674 ///
675 /// The same removal as [`Set::remove_at`] for a caller that has already read
676 /// the member and does not need it handed back. That caller is `SPOP` on the
677 /// wire, which reads with [`Set::at`], writes the bytes straight into the
678 /// reply buffer, and only then calls this. It is an allocation a member
679 /// saved on the one command in the set whose whole cost is the allocating.
680 ///
681 /// [`Set::remove_at`] stays for the embedded API, where the caller wants the
682 /// bytes and has nowhere to put them.
683 pub fn drop_at(&mut self, index: usize) -> bool {
684 match &mut self.body {
685 Body::Ints(s) => match s.get(index) {
686 Some(v) => {
687 s.remove(v);
688 true
689 }
690 None => false,
691 },
692 Body::Packed(lp) => {
693 if index >= lp.len() {
694 return false;
695 }
696 lp.delete(index, 1);
697 true
698 }
699 Body::Table(t) => t.remove_at(index).is_some(),
700 Body::Split(p) => p.remove_at(index).is_some(),
701 }
702 }
703
704 /// Whether an intset plus one non integer member would still be a listpack.
705 ///
706 /// Three tests, and the first is the asymmetric one: the count is compared
707 /// against the *listpack* ceiling and not the intset one, so an intset with
708 /// two hundred members is already too big to become a listpack even though
709 /// it is a perfectly legal intset. The other two are the new member's length
710 /// and the longest existing member's length once it is written as digits,
711 /// which only bites when `set-max-listpack-value` has been turned down,
712 /// because no integer is more than twenty characters.
713 fn ints_fit_a_listpack(&self, member: &[u8], limits: &Limits) -> bool {
714 let Body::Ints(s) = &self.body else {
715 return false;
716 };
717 s.len() < limits.max_listpack_entries
718 && member.len() <= limits.max_listpack_value
719 && self.ints_are_short_enough(limits)
720 }
721
722 /// Whether this intset on its own would fit a listpack.
723 ///
724 /// The same question with no new member in it, which is what an intset that
725 /// has just passed `set-max-intset-entries` asks. It only ever answers yes
726 /// when the two ceilings have been configured the wrong way round, because
727 /// 512 is not under 128, and a server run that way expects a listpack there.
728 fn ints_fit_a_listpack_alone(&self, limits: &Limits) -> bool {
729 let Body::Ints(s) = &self.body else {
730 return false;
731 };
732 s.len() <= limits.max_listpack_entries && self.ints_are_short_enough(limits)
733 }
734
735 /// Whether every member, written as digits, is under the listpack ceiling.
736 fn ints_are_short_enough(&self, limits: &Limits) -> bool {
737 let Body::Ints(s) = &self.body else {
738 return false;
739 };
740 // The two ends bound the digits of everything between them, so there is
741 // nothing to walk.
742 let widest = s
743 .min()
744 .map(i64_len)
745 .unwrap_or(0)
746 .max(s.max().map(i64_len).unwrap_or(0));
747 widest <= limits.max_listpack_value
748 }
749
750 /// Rewrite as a listpack, which only an intset ever does.
751 fn become_listpack(&mut self) {
752 let Body::Ints(s) = &self.body else {
753 return;
754 };
755 let mut lp = Listpack::new();
756 let mut buf = Vec::with_capacity(20);
757 for v in s.iter() {
758 buf.clear();
759 Member::Int(v).write_to(&mut buf);
760 lp.push(&buf);
761 }
762 self.body = Body::Packed(lp);
763 }
764
765 /// Rewrite as an element table, with room for `extra` more members.
766 fn become_table(&mut self, extra: usize) {
767 let mut t = Elements::with_capacity(self.len() + extra);
768 let mut buf = Vec::with_capacity(20);
769 for m in self.iter() {
770 match m {
771 Member::Str(b) => {
772 t.insert(b, ()).expect("room, and every member was unique");
773 }
774 Member::Int(v) => {
775 buf.clear();
776 Member::Int(v).write_to(&mut buf);
777 t.insert(&buf, ())
778 .expect("room, and every member was unique");
779 }
780 }
781 }
782 self.body = Body::Table(t);
783 }
784
785 /// Spread an element table over partitions.
786 ///
787 /// One way, like every other promotion here. A set that drops back under the
788 /// threshold keeps its partitions, which is Y4's rule and Redis's behaviour
789 /// for the encodings it does expose: the cost of a representation is paid
790 /// when it is entered, and paying it again on the way back out turns one
791 /// `SREM` at the boundary into a rehash of the whole set.
792 fn become_split(&mut self) {
793 if let Body::Table(t) = &self.body {
794 let p = Parts::from_table(t, parts_for(t.len()));
795 self.body = Body::Split(p);
796 }
797 }
798}
799
800impl Default for Set {
801 fn default() -> Set {
802 Set::new()
803 }
804}
805
806#[cfg(test)]
807mod tests {
808 use super::*;
809
810 fn of(members: &[&str]) -> Set {
811 let mut s = Set::new();
812 for m in members {
813 assert!(s.add(m.as_bytes(), &Limits::DEFAULT), "{m} was new");
814 }
815 s
816 }
817
818 /// What a set actually costs per member, which is half of M3's memory gate
819 /// row and was an argument rather than a number until this was written.
820 ///
821 /// Run it with `cargo test -p yo-kv --release measure_bytes_per_member --
822 /// --ignored --nocapture`. Ignored because a million members is not
823 /// something every `cargo test` should pay for, and it prints rather than
824 /// asserts because the number it prints is the thing being reported.
825 ///
826 /// Two shapes, because the gate names one of them and the other is what
827 /// most sets actually hold. Integers first, at every band an all integer
828 /// set passes through, and then strings, which never see the intset at all.
829 #[test]
830 #[ignore = "a measurement, run it by name"]
831 fn measure_bytes_per_member() {
832 let limits = Limits::DEFAULT;
833 for n in [512usize, 1_000, 100_000, 1_000_000] {
834 let mut s = Set::new();
835 for i in 0..n {
836 s.add(i.to_string().as_bytes(), &limits);
837 }
838 println!(
839 "int n={n:<9} band={:<10} total={:<10} per_member={:.2}",
840 band(&s),
841 s.memory_bytes(),
842 s.memory_bytes() as f64 / n as f64
843 );
844 }
845 // Sixteen byte members, so the payload is a round number and the
846 // overhead is whatever is above it.
847 for n in [128usize, 1_000, 100_000, 1_000_000] {
848 let mut s = Set::new();
849 let mut payload = 0usize;
850 for i in 0..n {
851 let m = format!("member:{i:09}");
852 payload += m.len();
853 s.add(m.as_bytes(), &limits);
854 }
855 let total = s.memory_bytes();
856 let per = |b: usize| b as f64 / n as f64;
857 println!(
858 "bytes n={n:<9} band={:<10} total={total:<10} payload={payload:<9} per_member={:.2} over_per_member={:.2} slots={:.2} rows={:.2} names={:.2} name_slack={:.2}",
859 band(&s),
860 per(total),
861 per(total - payload),
862 per(s.slot_bytes()),
863 per(s.row_bytes()),
864 per(s.name_bytes()),
865 per(s.name_bytes() - payload)
866 );
867 }
868 }
869
870 /// Which of the four a set is in, spelled out rather than through
871 /// [`Set::encoding`], which folds the two table bands into one word because
872 /// that is what `OBJECT ENCODING` has to say.
873 fn band(s: &Set) -> &'static str {
874 match &s.body {
875 Body::Ints(_) => "intset",
876 Body::Packed(_) => "listpack",
877 Body::Table(_) => "table",
878 Body::Split(_) => "split",
879 }
880 }
881
882 /// Everything about the partitioned band that needs a real set past the real
883 /// threshold, in one test, because building 262,145 members is the expensive
884 /// part and there is no reason to pay for it four times.
885 #[test]
886 fn a_set_past_the_threshold_splits_without_the_client_being_able_to_tell() {
887 let limits = Limits::DEFAULT;
888 let mut s = Set::new();
889 // One short of the threshold. Still one table: the check is strictly
890 // greater, so the set that splits is the one that has outgrown a table
891 // and not the one that is about to.
892 for i in 0..PARTITION_AT {
893 assert!(s.add(format!("m{i}").as_bytes(), &limits));
894 }
895 assert!(matches!(s.body, Body::Table(_)));
896 assert_eq!(s.len(), PARTITION_AT);
897
898 // The member that tips it over.
899 assert!(s.add(b"tipping", &limits));
900 assert!(matches!(s.body, Body::Split(_)), "it should have split");
901 assert_eq!(s.len(), PARTITION_AT + 1);
902
903 // And the client cannot tell. This is the whole point: Redis has three
904 // set encodings and a fourth word here breaks every suite that reads it.
905 assert_eq!(s.encoding(), Encoding::Hashtable);
906 assert_eq!(s.encoding().name(), "hashtable");
907
908 // Every member survived the rehash, asked both ways.
909 assert!(s.contains(b"tipping"));
910 assert!(s.contains(b"m0"));
911 assert!(s.contains(b"m262143"));
912 assert!(!s.contains(b"m262144"));
913 assert!(s.has(&Needle::new(b"m1000")));
914 assert!(!s.has(&Needle::new(b"nothing")));
915
916 // A rewrite is still not an add.
917 assert!(!s.add(b"m0", &limits));
918 assert_eq!(s.len(), PARTITION_AT + 1);
919
920 // Removing goes back through the same partition it went into, and the
921 // band never demotes however far the set shrinks.
922 assert!(s.remove(b"tipping"));
923 assert!(!s.remove(b"tipping"));
924 assert_eq!(s.len(), PARTITION_AT);
925 assert!(matches!(s.body, Body::Split(_)), "promotion is one way");
926 assert_eq!(s.encoding(), Encoding::Hashtable);
927
928 // The draw `SPOP` and `SRANDMEMBER` run on reaches the last position,
929 // which is the one that has to land in the highest non empty partition.
930 let last = s.at(s.len() - 1).expect("inside the set").to_vec();
931 assert!(s.contains(&last));
932 assert!(s.at(s.len()).is_none());
933 assert_eq!(s.remove_at(s.len() - 1), Some(last.clone()));
934 assert!(!s.contains(&last));
935 assert!(s.drop_at(0));
936 assert_eq!(s.len(), PARTITION_AT - 2);
937
938 // And a full scan sees every member exactly once.
939 let mut seen = 0usize;
940 let mut cursor = Cursor::START;
941 let mut rounds = 0;
942 loop {
943 cursor = s.scan(cursor, 1_000, |_| seen += 1);
944 rounds += 1;
945 assert!(rounds < 100_000, "the scan is not finishing");
946 if cursor.is_end() {
947 break;
948 }
949 }
950 assert_eq!(seen, s.len());
951 }
952
953 /// The crossing that actually happens in production: a client holding a
954 /// cursor from before the split. It has to see every member that stayed, and
955 /// repeats are what the `SCAN` guarantee gives up in exchange.
956 #[test]
957 fn a_scan_survives_the_set_splitting_underneath_it() {
958 let limits = Limits::DEFAULT;
959 let mut s = Set::new();
960 for i in 0..PARTITION_AT {
961 s.add(format!("m{i}").as_bytes(), &limits);
962 }
963 assert!(matches!(s.body, Body::Table(_)));
964
965 let mut seen = Vec::new();
966 let cursor = s.scan(Cursor::START, 5_000, |m| seen.push(m.to_vec()));
967 assert!(!cursor.is_end(), "the scan should have stopped part way");
968
969 s.add(b"tipping", &limits);
970 assert!(matches!(s.body, Body::Split(_)));
971
972 let mut cursor = cursor;
973 let mut rounds = 0;
974 loop {
975 cursor = s.scan(cursor, 5_000, |m| seen.push(m.to_vec()));
976 rounds += 1;
977 assert!(rounds < 100_000, "the scan is not finishing");
978 if cursor.is_end() {
979 break;
980 }
981 }
982 seen.sort_unstable();
983 seen.dedup();
984 assert_eq!(
985 seen.len(),
986 PARTITION_AT + 1,
987 "the split lost a member the client was entitled to"
988 );
989 }
990
991 #[test]
992 fn a_hint_past_the_threshold_builds_the_band_up_front() {
993 let limits = Limits::DEFAULT;
994 // A caller loading a million members should not fill one table, cross the
995 // threshold and then rehash the lot.
996 let s = Set::with_hint(b"first", 1_000_000, &limits);
997 assert!(matches!(s.body, Body::Split(_)));
998 assert_eq!(s.encoding(), Encoding::Hashtable);
999 assert!(s.is_empty());
1000
1001 // A hint at the threshold is still one table, matching the add path.
1002 let s = Set::with_hint(b"first", PARTITION_AT, &limits);
1003 assert!(matches!(s.body, Body::Table(_)));
1004
1005 // And a hint is only a hint: the band takes members like anything else.
1006 let mut s = Set::with_hint(b"a", 1_000_000, &limits);
1007 assert!(s.add(b"a", &limits));
1008 assert!(!s.add(b"a", &limits));
1009 assert!(s.contains(b"a"));
1010 assert_eq!(s.len(), 1);
1011 assert_eq!(s.at(0).map(|m| m.to_vec()), Some(b"a".to_vec()));
1012 }
1013
1014 fn members(s: &Set) -> Vec<String> {
1015 let mut v: Vec<String> = s
1016 .iter()
1017 .map(|m| String::from_utf8(m.to_vec()).expect("utf8 in these tests"))
1018 .collect();
1019 // Order is not part of the contract and the three representations do not
1020 // agree on it, so every assertion here is against a sorted list.
1021 v.sort();
1022 v
1023 }
1024
1025 #[test]
1026 fn a_new_set_is_an_empty_intset() {
1027 let s = Set::new();
1028 assert_eq!(s.encoding(), Encoding::Intset);
1029 assert_eq!(s.len(), 0);
1030 assert!(s.is_empty());
1031 assert!(!s.contains(b"1"));
1032 assert_eq!(s.at(0), None);
1033 }
1034
1035 #[test]
1036 fn integers_stay_an_intset_and_come_back_as_members() {
1037 let s = of(&["1", "2", "3"]);
1038 assert_eq!(s.encoding(), Encoding::Intset);
1039 assert_eq!(members(&s), ["1", "2", "3"]);
1040 assert!(s.contains(b"2"));
1041 assert!(!s.contains(b"4"));
1042 assert!(!s.contains(b"two"));
1043 }
1044
1045 #[test]
1046 fn adding_the_same_member_twice_says_so_in_all_three() {
1047 let mut ints = of(&["1", "2"]);
1048 assert!(!ints.add(b"2", &Limits::DEFAULT));
1049 assert_eq!(ints.len(), 2);
1050
1051 let mut packed = of(&["a", "b"]);
1052 assert_eq!(packed.encoding(), Encoding::Listpack);
1053 assert!(!packed.add(b"b", &Limits::DEFAULT));
1054 assert_eq!(packed.len(), 2);
1055
1056 let mut table = of(&["a", "b"]);
1057 table.become_table(0);
1058 assert!(!table.add(b"b", &Limits::DEFAULT));
1059 assert_eq!(table.len(), 2);
1060 }
1061
1062 #[test]
1063 fn a_string_turns_a_small_intset_into_a_listpack() {
1064 let mut s = of(&["1", "2", "3"]);
1065 assert!(s.add(b"hello", &Limits::DEFAULT));
1066 assert_eq!(s.encoding(), Encoding::Listpack);
1067 assert_eq!(members(&s), ["1", "2", "3", "hello"]);
1068 assert!(s.contains(b"1"), "the integers survived the rewrite");
1069 assert!(s.contains(b"hello"));
1070 }
1071
1072 #[test]
1073 fn a_string_turns_a_big_intset_straight_into_a_table() {
1074 // The asymmetric rule, and the one a natural implementation gets wrong.
1075 // Two hundred integers is a legal intset and is already past the
1076 // listpack ceiling, so this never passes through the listpack at all.
1077 let mut s = Set::new();
1078 for i in 0..200 {
1079 s.add(i.to_string().as_bytes(), &Limits::DEFAULT);
1080 }
1081 assert_eq!(s.encoding(), Encoding::Intset);
1082 assert_eq!(s.len(), 200);
1083
1084 assert!(s.add(b"hello", &Limits::DEFAULT));
1085 assert_eq!(s.encoding(), Encoding::Hashtable);
1086 assert_eq!(s.len(), 201);
1087 assert!(s.contains(b"199"));
1088 assert!(s.contains(b"hello"));
1089 }
1090
1091 #[test]
1092 fn an_intset_holds_five_hundred_and_twelve_and_converts_at_the_next_one() {
1093 let mut s = Set::new();
1094 for i in 0..512 {
1095 s.add(i.to_string().as_bytes(), &Limits::DEFAULT);
1096 }
1097 assert_eq!(s.encoding(), Encoding::Intset, "512 is still an intset");
1098 assert_eq!(s.len(), 512);
1099
1100 s.add(b"512", &Limits::DEFAULT);
1101 assert_eq!(s.encoding(), Encoding::Hashtable, "513 is not");
1102 assert_eq!(s.len(), 513);
1103 // And not a listpack on the way, because 513 is well past 128.
1104 for i in 0..513 {
1105 assert!(s.contains(i.to_string().as_bytes()), "{i} survived");
1106 }
1107 }
1108
1109 #[test]
1110 fn a_listpack_converts_at_a_hundred_and_twenty_eight_members() {
1111 let mut s = of(&["x"]);
1112 assert_eq!(s.encoding(), Encoding::Listpack);
1113 for i in 0..127 {
1114 s.add(format!("m{i}").as_bytes(), &Limits::DEFAULT);
1115 }
1116 assert_eq!(s.len(), 128);
1117 assert_eq!(s.encoding(), Encoding::Listpack, "128 is still a listpack");
1118
1119 s.add(b"one more", &Limits::DEFAULT);
1120 assert_eq!(s.len(), 129);
1121 assert_eq!(s.encoding(), Encoding::Hashtable);
1122 assert!(s.contains(b"x"));
1123 assert!(s.contains(b"m126"));
1124 assert!(s.contains(b"one more"));
1125 }
1126
1127 #[test]
1128 fn a_long_member_converts_a_listpack_whatever_the_count() {
1129 let mut s = of(&["a"]);
1130 let long = vec![b'z'; 65];
1131 assert!(s.add(&long, &Limits::DEFAULT));
1132 assert_eq!(s.encoding(), Encoding::Hashtable, "65 is past 64");
1133 assert_eq!(s.len(), 2);
1134 assert!(s.contains(&long));
1135
1136 // And exactly at the boundary it does not.
1137 let mut ok = of(&["a"]);
1138 ok.add(&[b'z'; 64], &Limits::DEFAULT);
1139 assert_eq!(ok.encoding(), Encoding::Listpack, "64 fits");
1140 }
1141
1142 #[test]
1143 fn a_long_member_sends_an_intset_to_a_table_and_not_a_listpack() {
1144 let mut s = of(&["1", "2"]);
1145 assert!(s.add(&[b'z'; 65], &Limits::DEFAULT));
1146 assert_eq!(s.encoding(), Encoding::Hashtable);
1147 assert_eq!(s.len(), 3);
1148 }
1149
1150 #[test]
1151 fn the_limits_are_configuration_and_moving_them_moves_the_encodings() {
1152 let tight = Limits {
1153 max_intset_entries: 2,
1154 max_listpack_entries: 2,
1155 max_listpack_value: 3,
1156 };
1157 let mut s = Set::new();
1158 s.add(b"1", &tight);
1159 s.add(b"2", &tight);
1160 assert_eq!(s.encoding(), Encoding::Intset);
1161 s.add(b"3", &tight);
1162 assert_eq!(s.encoding(), Encoding::Hashtable, "three is past two");
1163
1164 // And a member longer than three characters cannot go in a listpack.
1165 let mut t = Set::new();
1166 t.add(b"abc", &tight);
1167 assert_eq!(t.encoding(), Encoding::Listpack, "three characters fit");
1168 t.add(b"defg", &tight);
1169 assert_eq!(t.encoding(), Encoding::Hashtable, "four do not");
1170 assert!(t.contains(b"abc"));
1171 assert!(t.contains(b"defg"));
1172
1173 // Including as the first member, which is a table from the off rather
1174 // than a listpack that converts on the next thing to arrive. Redis gets
1175 // to the same place by the other road: it creates the listpack, tries
1176 // the add, and converts before the reply.
1177 let mut u = Set::new();
1178 u.add(b"abcd", &tight);
1179 assert_eq!(u.encoding(), Encoding::Hashtable);
1180 assert!(u.contains(b"abcd"));
1181 }
1182
1183 #[test]
1184 fn with_hint_picks_the_representation_up_front() {
1185 let d = &Limits::DEFAULT;
1186 assert_eq!(
1187 Set::with_hint(b"1", 10, d).encoding(),
1188 Encoding::Intset,
1189 "an integer and few enough of them"
1190 );
1191 assert_eq!(
1192 Set::with_hint(b"1", 1000, d).encoding(),
1193 Encoding::Hashtable,
1194 "an integer and too many"
1195 );
1196 assert_eq!(
1197 Set::with_hint(b"x", 10, d).encoding(),
1198 Encoding::Listpack,
1199 "not an integer and few enough"
1200 );
1201 assert_eq!(
1202 Set::with_hint(b"x", 1000, d).encoding(),
1203 Encoding::Hashtable,
1204 "not an integer and too many"
1205 );
1206 }
1207
1208 #[test]
1209 fn removing_works_in_all_three_and_never_demotes() {
1210 let mut ints = of(&["1", "2", "3"]);
1211 assert!(ints.remove(b"2"));
1212 assert!(!ints.remove(b"2"));
1213 assert!(!ints.remove(b"nope"), "not an integer, so not a member");
1214 assert_eq!(members(&ints), ["1", "3"]);
1215 assert_eq!(ints.encoding(), Encoding::Intset);
1216
1217 let mut packed = of(&["a", "b", "c"]);
1218 assert!(packed.remove(b"b"));
1219 assert!(!packed.remove(b"b"));
1220 assert_eq!(members(&packed), ["a", "c"]);
1221 assert_eq!(packed.encoding(), Encoding::Listpack);
1222
1223 let mut table = of(&["a", "b", "c"]);
1224 table.become_table(0);
1225 assert!(table.remove(b"b"));
1226 assert!(!table.remove(b"b"));
1227 assert_eq!(members(&table), ["a", "c"]);
1228 assert_eq!(
1229 table.encoding(),
1230 Encoding::Hashtable,
1231 "down to two members and still a table"
1232 );
1233 }
1234
1235 #[test]
1236 fn a_set_can_be_emptied_a_member_at_a_time() {
1237 for mut s in [of(&["1", "2", "3"]), of(&["a", "b", "c"])] {
1238 let all: Vec<Vec<u8>> = s.iter().map(|m| m.to_vec()).collect();
1239 for m in &all {
1240 assert!(s.remove(m));
1241 }
1242 assert!(s.is_empty());
1243 assert_eq!(s.at(0), None);
1244 }
1245 }
1246
1247 #[test]
1248 fn removing_by_position_hands_the_member_back() {
1249 // What `SPOP` runs on. Drawing position zero every time has to empty the
1250 // set rather than run off the end or repeat a member, in all three.
1251 let mut table = of(&["a", "b", "c", "d"]);
1252 table.become_table(0);
1253 for mut s in [
1254 of(&["10", "20", "30", "40"]),
1255 of(&["a", "b", "c", "d"]),
1256 table,
1257 ] {
1258 let mut got = Vec::new();
1259 while !s.is_empty() {
1260 got.push(String::from_utf8(s.remove_at(0).expect("not empty")).expect("utf8"));
1261 }
1262 got.sort();
1263 assert_eq!(got.len(), 4, "four members and no repeats");
1264 assert_eq!(s.len(), 0);
1265 assert_eq!(s.remove_at(0), None);
1266 }
1267 }
1268
1269 #[test]
1270 fn an_integer_member_is_the_same_member_however_it_is_written() {
1271 // An intset holds 42 as a number, so `SADD s 42` twice is one member.
1272 // `042` does not parse as an integer, so it is a different member and it
1273 // converts the set, which is what a real server does too.
1274 let mut s = of(&["42"]);
1275 assert!(!s.add(b"42", &Limits::DEFAULT));
1276 assert_eq!(s.len(), 1);
1277 assert!(s.add(b"042", &Limits::DEFAULT));
1278 assert_eq!(s.encoding(), Encoding::Listpack);
1279 assert_eq!(members(&s), ["042", "42"]);
1280 assert!(s.contains(b"42"));
1281 assert!(s.contains(b"042"));
1282 }
1283
1284 #[test]
1285 fn the_small_bands_answer_a_scan_in_one_go() {
1286 for s in [of(&["1", "2", "3"]), of(&["a", "b", "c"])] {
1287 let mut seen = Vec::new();
1288 // A count of one, which the table band would honour and these two
1289 // do not, and a cursor from nowhere, which these two ignore.
1290 let next = s.scan(Cursor::at(1, 0, 99), 1, |m| seen.push(m.to_vec()));
1291 assert!(next.is_end(), "{:?} split a scan up", s.encoding());
1292 assert_eq!(seen.len(), 3);
1293 }
1294 }
1295
1296 #[test]
1297 fn the_table_band_walks_a_scan_in_windows_and_misses_nothing() {
1298 let mut s = Set::new();
1299 for i in 0..300 {
1300 s.add(format!("m{i}").as_bytes(), &Limits::DEFAULT);
1301 }
1302 assert_eq!(s.encoding(), Encoding::Hashtable);
1303
1304 let mut seen = Vec::new();
1305 let mut c = Cursor::START;
1306 let mut turns = 0;
1307 loop {
1308 c = s.scan(c, 7, |m| seen.push(m.to_vec()));
1309 turns += 1;
1310 assert!(turns < 100, "the scan did not finish");
1311 if c.is_end() {
1312 break;
1313 }
1314 }
1315 assert!(
1316 turns > 1,
1317 "a window of seven over three hundred took one turn"
1318 );
1319 seen.sort();
1320 seen.dedup();
1321 assert_eq!(seen.len(), 300, "every member came back at least once");
1322 }
1323
1324 #[test]
1325 fn a_conversion_loses_no_member_at_any_of_the_three_boundaries() {
1326 // One walk over each path out of an intset and out of a listpack, each
1327 // checking every member is still findable after the rewrite rather than
1328 // only checking the count.
1329 let mut wide = Set::new();
1330 for v in [i64::MIN, -1, 0, 1, i64::MAX] {
1331 wide.add(v.to_string().as_bytes(), &Limits::DEFAULT);
1332 }
1333 wide.add(b"str", &Limits::DEFAULT);
1334 assert_eq!(wide.encoding(), Encoding::Listpack);
1335 for v in [i64::MIN, -1, 0, 1, i64::MAX] {
1336 assert!(wide.contains(v.to_string().as_bytes()), "{v} survived");
1337 }
1338
1339 wide.add(&[b'q'; 100], &Limits::DEFAULT);
1340 assert_eq!(wide.encoding(), Encoding::Hashtable);
1341 for v in [i64::MIN, -1, 0, 1, i64::MAX] {
1342 assert!(
1343 wide.contains(v.to_string().as_bytes()),
1344 "{v} survived twice"
1345 );
1346 }
1347 assert!(wide.contains(b"str"));
1348 assert_eq!(wide.len(), 7);
1349 }
1350}