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}