Skip to main content

yo_kv/
setops.rs

1//! Set algebra, and the choice between probing and merging.
2//!
3//! `SINTER`, `SUNION`, `SDIFF`, `SINTERCARD` and the `*STORE` forms. This is the
4//! family aki lost worst on, at 0.75x for `SINTER` and 0.30x to 0.55x for the
5//! `*STORE` forms, and `08` section 4 sets the gate at ten times for all of them.
6//!
7//! # Two ways to do it
8//!
9//! **Probe.** Take the smallest set, and for each of its members ask every other
10//! set whether it has it. Work is `|smallest| * (k - 1)` questions in the worst
11//! case, and far fewer in practice because a member that is missing from the
12//! second set is never asked about the third. Every question is a random access
13//! into a different table.
14//!
15//! **Accumulate.** Walk every member of every set once, into one table that
16//! counts how many sets each member appeared in, and then read the answer off
17//! the counts. Work is `sum(|set|)` insertions, all of them into the same table.
18//!
19//! K11 pre-registers a crossover at k around 7: below that probe, above it merge.
20//! It does not reproduce, and it is worth being exact about why, because the
21//! reason is not that the number is a little out.
22//!
23//! # The crossover is not at seven and there is not one
24//!
25//! `benches/setops.rs` runs both plans over the same sets at k from 2 to 16, with
26//! sets of two hundred thousand and nine tenths of every set shared, which is the
27//! shape that gives probe the least help. Probe wins at every k. The gap narrows
28//! as k grows, from 2.95 times at k equals 2 to 1.24 times at k equals 16, and it
29//! narrows towards parity rather than towards a crossing.
30//!
31//! The arithmetic says the same thing once the cost of an operation is measured
32//! instead of assumed. Probe does `n * (k - 1)` table operations. Accumulate does
33//! `n * (k + 1)`, being one seeding insert and one count raise per member plus the
34//! read back. Those are 2.7 and 3.4 million at k equals 16, a ratio of 1.26
35//! against a measured 1.24. Probe does less work at every k and the ratio tends to
36//! one from above, so these two never cross.
37//!
38//! The pre-registered number assumed a probe question is much dearer than an
39//! accumulate touch, because a question is a random access into a table this
40//! operation has not otherwise touched and `08` section 4 floors that at about 40
41//! ns on a DRAM miss. Both come out at about 25 ns here. An accumulate touch is
42//! not the cheap sequential thing the model had in mind: it hashes the member and
43//! makes its own random access, into the counting table. Two random accesses that
44//! cost the same cannot trade off against each other, however many of them there
45//! are. This is L6's 70 ns positional probe again, which measured 13.
46//!
47//! # The third plan, which does change it
48//!
49//! `08` section 4 describes a merge that is neither of the two above: sorted
50//! arrays walked in lockstep, where a touch is a pointer step and a comparison
51//! with no hash anywhere. That genuinely is much cheaper than a probe question,
52//! and against it a crossover can exist. It was written down as needing the
53//! partitioned band and was therefore out of reach.
54//!
55//! It is in reach now, from the other direction. An all integer set is an
56//! [`Intset`], which is exactly a sorted array, and since #148 it stays one
57//! however big it gets rather than turning into a table at five hundred and
58//! twelve members. So whenever every operand is an intset there is something to
59//! merge, and that is most of what `SINTERSTORE` is called with: identifier
60//! sets, bitmap style tag sets, anything a numeric primary key went into.
61//!
62//! [`Plan::Merge`] is that, over [`Walk`], and it is why `plan_for` is a
63//! chooser with something to choose. The intersection is a leapfrog driven from
64//! the smallest set: take the value that set is on, pull the others up to it
65//! with [`Walk::seek`], and if they all land on it then it is in all of them.
66//! The seek is what makes the asymmetric case cheap, because a set of ten
67//! against a set of a million touches ten members of the big one and skips the
68//! rest.
69//!
70//! The counting plan stays reachable through [`inter_with`] and the benchmark
71//! keeps measuring it, because it is the control the merge has to beat.
72//!
73//! # What the merge is worth, measured
74//!
75//! `benches/setops.rs` builds the same four shapes as integer sets and runs
76//! every plan over them. Milliseconds per intersection, minimum per iteration,
77//! two hundred thousand members a set:
78//!
79//! ```text
80//!                        k=2      k=4      k=8     k=16
81//!   dense    merge      3.95     5.15    11.06    23.91
82//!            probe      6.93    11.93    22.63    41.95
83//!            count     16.32    25.91    45.03    84.90
84//!   sparse   merge      0.04     0.06     0.12     0.26
85//!            probe      6.17     6.21     6.38     6.54
86//!   striped  merge      4.70     5.07     5.13     5.27
87//!            probe      7.42     6.43     7.63     7.80
88//!   skewed   merge     0.002    0.003    0.007    0.015
89//!            probe     0.004    0.007    0.013    0.023
90//! ```
91//!
92//! And the other three commands, where the merge's opposite number is the table
93//! for the union and the probe for the other two:
94//!
95//! ```text
96//!                        k=2      k=4      k=8     k=16
97//!   union    merge      1.47     4.35    14.76    54.49
98//!            table     13.30    31.21    69.84   149.82
99//!   diff     merge      4.40     5.16     8.60    15.98
100//!            probe      6.94    10.02    16.65    29.07
101//!   store    merge      7.17    10.36    16.59    29.79
102//!            probe     13.88    23.95    36.69    63.24
103//! ```
104//!
105//! The merge wins every row at every k. The narrowest is 1.27 times and the
106//! widest is 141, which is a spread wide enough to be worth explaining rather
107//! than averaging.
108//!
109//! # Where the spread comes from, and the shape that nearly broke it
110//!
111//! `sparse` and `striped` hold the same sets with the same one percent overlap
112//! and differ only in where each set's unshared members sit. In `sparse` they
113//! are in a range of their own, so a cursor that lands in another set's range
114//! steps over the whole range in one binary search. In `striped` they are
115//! interleaved one for one, so there is nothing to skip and a step is worth a
116//! single member. That is 141 times against 1.6, on data that is identical by
117//! every summary statistic an optimiser could look at. Real data lies between
118//! the two and the number to quote is the striped one.
119//!
120//! Getting that row right took two goes and it is the reason the shape is in the
121//! benchmark. The first merge was symmetric: no set in charge, the largest value
122//! any cursor held as the target, every cursor visited in turn. On `striped` it
123//! was nine times slower than the probe at k of 16, and it deserved to be. A
124//! symmetric leapfrog costs a step per member of the union of every operand,
125//! because proving that nothing matches means looking at everything, and the
126//! union is `k` times the smallest set. The probe reads the smallest set once
127//! and fails on its first question, so it is flat in k, which is exactly what
128//! `sparse_probe` and `striped_probe` do at about 6 to 8 ms across the range.
129//!
130//! Driving the leapfrog from the smallest set fixes it, because it puts the
131//! merge on the probe's own bound: a step per member of the smallest operand,
132//! plus one per overshoot, over a step that is cheaper than a hash and a random
133//! access. `striped_merge` is 4.70 ms at k of 2 and 5.27 at k of 16, which is
134//! the same flatness the probe has with a smaller constant.
135//!
136//! So the honest claim is not that the merge is a different order of cost. It is
137//! that the merge is never worse than the probe by more than its constant and is
138//! sometimes better by two orders, and that the plan is free to take because the
139//! representation already sorted the data.
140//!
141//! The one row with a slope worth watching is the union, which finds the
142//! smallest value by looking at every cursor and is therefore quadratic in k
143//! where the table is linear. It wins by 9.1 times at k of 2 and 2.75 at k of 16,
144//! and extrapolating the two slopes they would cross somewhere past k of 50. A
145//! heap would make it `log k` at the cost of a comparison per push, and there is
146//! no point paying that until a `SUNION` with fifty keys turns up.
147//!
148//! # Ordering
149//!
150//! A probe or a count returns members in the order the first relevant set holds
151//! them, which is insertion order for a listpack or a table and ascending for an
152//! intset. A merge returns them ascending. Redis makes no ordering promise for
153//! any of these, and picking the order the data is already in means the walk is
154//! sequential and there is nothing to sort.
155//!
156//! For the intersection and the difference the two agree, because the plan only
157//! changes when every operand is an intset and the set being walked is then
158//! ascending either way. For the union they do not: the table walks the sets in
159//! turn and the merge interleaves them. That is the one place a plan is visible
160//! from outside, and it is visible only to a client that was relying on
161//! something Redis never promised.
162//!
163//! # The three representations
164//!
165//! The operand is a [`Set`], which is one of three things, and not the element
166//! table it used to be. The walked set gives up members through the same
167//! [`Set::iter`] everything else uses, and the questioned sets answer through
168//! [`Set::has`], which is [`Set::contains`] with the parse and the hash lifted
169//! out into a [`Needle`] so they happen once per member rather than once per
170//! question.
171//!
172//! What that buys is that the algebra never has to know what it is holding. It
173//! also means the members cross between representations correctly, which is not
174//! automatic: an intset member is a number that has no digits anywhere, and a
175//! table stores that same member as its digits, so `SINTER ints table` only
176//! finds anything because the needle carries both forms.
177//!
178//! # Presizing
179//!
180//! The `*STORE` forms hand the destination a size before they start filling it,
181//! taken from the smallest input, which is Y18's rule and an upper bound on any
182//! intersection. `05` section 3.1 wants that to be one arena bump. Until the
183//! arena is under this, the destination's own hint is the same promise with a
184//! different allocator behind it.
185
186use yo_common::Small;
187use yo_common::num::DIGITS_MAX;
188
189use crate::intset::Walk;
190use crate::set::{Limits, Needle, Set};
191use crate::{Elements, Intset};
192
193/// The tables a set operation fills in on its way to an answer.
194///
195/// A union walks everything into one table and lets the table be the duplicate
196/// check. An accumulating intersection counts into one. Both of those used to
197/// be built per call, which is a hash table out of the allocator on a command
198/// path, and it is the thing the text rows of the benchmark were mostly
199/// spending their time on.
200///
201/// So the tables belong to the caller now. A database keeps one of these and
202/// hands it in, the tables are cleared rather than dropped between calls, and a
203/// `SUNION` over sets no larger than the last one pays the allocator nothing at
204/// all. The memory that costs is one table as big as the largest union the
205/// database has been asked for, which is smaller than the answer it already had
206/// to build.
207///
208/// `setops_small`'s `union/text/k2` row, nanoseconds per operation over two
209/// text sets of eight members, went from 368.54 to 248.18 when the table
210/// stopped being built per call. That is 1.48 times on a command shaped like
211/// the ones people actually send. The integer rows do not move at all, because
212/// those take the merge plan and never build a table in the first place, which
213/// is the same split the `Small` work saw from the other side.
214///
215/// It is [`Default`], so a caller that does not care can pass
216/// `&mut Scratch::default()` and get exactly the old behaviour.
217#[derive(Debug, Default)]
218pub struct Scratch {
219    /// Where a union puts the members it has already emitted.
220    seen: Elements<()>,
221    /// Where an accumulating intersection counts how many sets have a member.
222    counts: Elements<u32>,
223}
224
225impl Scratch {
226    /// Empty tables that have not asked the allocator for anything yet.
227    #[must_use]
228    pub fn new() -> Scratch {
229        Scratch::default()
230    }
231
232    /// What the tables are holding on to, for `MEMORY USAGE` and for tests.
233    #[must_use]
234    pub fn memory_bytes(&self) -> usize {
235        self.seen.memory_bytes() + self.counts.memory_bytes()
236    }
237}
238
239/// How to answer a set operation.
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub enum Plan {
242    /// Walk the smallest set and question the others about each member.
243    Probe,
244    /// Walk everything once into one counting table.
245    Accumulate,
246    /// Step through every set at once, in order, comparing and never hashing.
247    ///
248    /// Only possible when every operand is an intset, because that is the only
249    /// representation that holds its members in order. [`inter_with`] will
250    /// refuse this plan for anything else rather than answer wrongly.
251    Merge,
252}
253
254/// The members every set has, in the order the smallest set holds them.
255///
256/// `limit` is `SINTERCARD`'s, and zero means no limit. The count comes back
257/// whether or not the caller collected anything, so `SINTERCARD` is this
258/// function with a callback that does nothing.
259///
260/// An empty input, or any empty set, is an empty intersection, which is what
261/// Redis says and is also the only sane reading.
262///
263/// A merge when every operand is an intset and a probe otherwise, which is a
264/// chooser with something to choose. See `plan_for`.
265pub fn inter<F>(scratch: &mut Scratch, sets: &[&Set], limit: usize, f: F) -> usize
266where
267    F: FnMut(&[u8]),
268{
269    inter_with(scratch, plan_for(sets), sets, limit, f)
270}
271
272/// Which plan the operands allow and deserve.
273///
274/// The merge is not a preference, it is a fact about the representation: two
275/// sorted arrays can be stepped through together and a table cannot, so a
276/// mixture of the two has nothing to merge and probes.
277///
278/// There is nothing to choose beyond that. A cost model that guessed at the
279/// overlap would be the obvious next thing to build and it is not needed,
280/// because the merge is driven from the smallest set and therefore carries the
281/// probe's own bound: it wins every shape in the benchmark, including the one
282/// laid out so that nothing can be skipped, and its worst row is still 1.27
283/// times ahead. See the module doc.
284fn plan_for(sets: &[&Set]) -> Plan {
285    if sets.iter().all(|s| s.ints().is_some()) {
286        Plan::Merge
287    } else {
288        Plan::Probe
289    }
290}
291
292/// How many operands fit without the allocator.
293///
294/// A set operation over more than eight keys is a thing somebody wrote on
295/// purpose and is rare enough that the spill is the right answer for it. Two or
296/// three is what almost every one of these is.
297pub(crate) const INLINE_KEYS: usize = 8;
298
299/// A list of one thing per operand, on the stack for the usual `k`.
300pub(crate) type PerSet<T> = Small<T, INLINE_KEYS>;
301
302/// Every operand as an intset, or `None` if any of them is something else.
303fn as_ints<'a>(sets: &[&'a Set]) -> Option<PerSet<&'a Intset>> {
304    sets.iter().map(|s| s.ints()).collect()
305}
306
307/// The same, with the plan named rather than assumed.
308///
309/// This is how the benchmark runs every plan over the same sets, which is the
310/// only way to find out where they cross and the only way to check that they
311/// agree on the answer. It is public because a caller that knows the shape of its
312/// own data knows more about it than [`inter`] can see from the sets alone.
313///
314/// [`Plan::Merge`] falls back to a probe when the operands are not all intsets,
315/// because a caller asking for it has stated a preference and not a fact, and
316/// the fact wins.
317pub fn inter_with<F>(scratch: &mut Scratch, how: Plan, sets: &[&Set], limit: usize, f: F) -> usize
318where
319    F: FnMut(&[u8]),
320{
321    if sets.is_empty() || sets.iter().any(|s| s.is_empty()) {
322        return 0;
323    }
324    match how {
325        Plan::Merge => match as_ints(sets) {
326            Some(ints) => inter_merge(&ints, limit, f),
327            None => inter_probe(sets, limit, f),
328        },
329        Plan::Probe => inter_probe(sets, limit, f),
330        Plan::Accumulate => inter_accumulate(&mut scratch.counts, sets, limit, f),
331    }
332}
333
334/// Step through every set at once, in order, and take what they all agree on.
335///
336/// Leapfrog, driven from the smallest set. Take the value that set is on, pull
337/// every other cursor up to it with [`Walk::seek`], and if they all land on it
338/// then it is in all of them. A cursor that lands past it has just proved that
339/// nothing between the two values is in the answer, so that value becomes the
340/// target and the driver is seeked to it as well, which is what lets a set of
341/// ten against a set of a million touch ten members of the big one rather than
342/// a million.
343///
344/// The others are seeked smallest first, and the loop restarts from the first of
345/// them the moment one of them overshoots, which is the probe's early exit in a
346/// different spelling: a member that is going to fail usually fails against the
347/// smallest of the others and never gets asked about the rest.
348///
349/// # Why it is driven rather than symmetric
350///
351/// The first version of this was symmetric. It held the largest value any cursor
352/// was on and went round them in turn, with no set in charge and no early exit,
353/// and on the shape that has nothing to skip it was nine times slower than the
354/// probe at k of 16 where this one is level with it. `benches/setops.rs` has the
355/// `striped` row that found it and the module doc has what it means.
356///
357/// The reason is that a symmetric leapfrog costs one step per member of the
358/// union of all the operands, because proving nothing matches means looking at
359/// everything. A driven one costs one step per member of the smallest operand
360/// plus one per overshoot, which is the same bound the probe has, over a step
361/// that is cheaper than the probe's. So it cannot lose by much and it can win by
362/// a lot.
363///
364/// The order is ascending, which is the order the probe plan produces on these
365/// same operands, since the set it walks is an intset and holds its members that
366/// way. So the answer does not change shape when the plan does.
367fn inter_merge<F>(sets: &[&Intset], limit: usize, mut f: F) -> usize
368where
369    F: FnMut(&[u8]),
370{
371    let mut order: PerSet<usize> = (0..sets.len()).collect();
372    order.sort_unstable_by_key(|&i| sets[i].len());
373    let mut driver = sets[order[0]].walk();
374    let mut others: PerSet<Walk<'_>> = order[1..].iter().map(|&i| sets[i].walk()).collect();
375
376    let mut digits = [0u8; DIGITS_MAX];
377    let mut found = 0usize;
378    'members: while let Some(target) = driver.peek() {
379        for w in &mut others {
380            w.seek(target);
381            match w.peek() {
382                // This set has nothing left, so neither has the answer.
383                None => break 'members,
384                Some(v) if v > target => {
385                    // Everything from `target` up to `v` is missing from this
386                    // set, so the driver can skip all of it in one search.
387                    driver.seek(v);
388                    continue 'members;
389                }
390                Some(_) => {}
391            }
392        }
393        f(yo_common::num::i64_digits(&mut digits, target));
394        found += 1;
395        if limit != 0 && found == limit {
396            break;
397        }
398        driver.bump();
399    }
400    found
401}
402
403/// Walk the smallest set, question the rest.
404///
405/// The other sets are asked smallest first. That is not tidiness: a member that
406/// is going to fail will usually fail against the smallest of the others, and
407/// asking that one first is what turns `k - 1` questions per member into closer
408/// to one.
409fn inter_probe<F>(sets: &[&Set], limit: usize, mut f: F) -> usize
410where
411    F: FnMut(&[u8]),
412{
413    let mut order: PerSet<usize> = (0..sets.len()).collect();
414    order.sort_unstable_by_key(|&i| sets[i].len());
415    let (&first, rest) = order.split_first().expect("not empty");
416
417    let mut digits = [0u8; DIGITS_MAX];
418    let mut found = 0usize;
419    for m in sets[first].iter() {
420        // Parsed and hashed once, asked k-1 times. Without this both are paid
421        // per question about the same member. See [`Needle`].
422        let needle = Needle::of(m, &mut digits);
423        if rest.iter().all(|&i| sets[i].has(&needle)) {
424            f(needle.bytes());
425            found += 1;
426            if limit != 0 && found == limit {
427                break;
428            }
429        }
430    }
431    found
432}
433
434/// Walk everything once into one counting table.
435///
436/// A member of the first set starts at one and every later set that has it
437/// raises it, so a member with the full count is in all of them. Members that
438/// are not in the first set are never entered at all, which keeps the table no
439/// bigger than the first set and is why the first set is the smallest one.
440fn inter_accumulate<F>(seen: &mut Elements<u32>, sets: &[&Set], limit: usize, mut f: F) -> usize
441where
442    F: FnMut(&[u8]),
443{
444    let mut order: PerSet<usize> = (0..sets.len()).collect();
445    order.sort_unstable_by_key(|&i| sets[i].len());
446    let (&first, rest) = order.split_first().expect("not empty");
447
448    let mut digits = [0u8; DIGITS_MAX];
449    seen.clear();
450    // One `yo_alloc::high_water` over the whole fill, which the union cannot
451    // have because it calls the caller back inside its walk and this does not.
452    // Same claim either way: the table is the database's and it grows when this
453    // intersection is bigger than every one before it.
454    yo_alloc::high_water(|| {
455        seen.reserve(sets[first].len());
456        for m in sets[first].iter() {
457            seen.insert(text(m, &mut digits), 1)
458                .expect("no larger than its source");
459        }
460    });
461    for &i in rest {
462        for m in sets[i].iter() {
463            if let Some(count) = seen.get_mut(text(m, &mut digits)) {
464                *count += 1;
465            }
466        }
467    }
468
469    // Read the answer off the first set rather than off the counting table, so
470    // the order the caller sees does not depend on which plan ran.
471    let k = sets.len() as u32;
472    let mut found = 0usize;
473    for m in sets[first].iter() {
474        let name = text(m, &mut digits);
475        if seen.get(name) == Some(&k) {
476            f(name);
477            found += 1;
478            if limit != 0 && found == limit {
479                break;
480            }
481        }
482    }
483    found
484}
485
486/// A member as the bytes a table keys on.
487///
488/// The counting plan and the union never ask another set a question, so they
489/// want a member's bytes and nothing else. Going through a [`Needle`] would
490/// parse and hash for nobody, since the table they are about to touch hashes it
491/// again on the way in.
492#[inline]
493fn text<'a>(m: crate::set::Member<'a>, digits: &'a mut [u8; DIGITS_MAX]) -> &'a [u8] {
494    match m {
495        crate::set::Member::Str(s) => s,
496        crate::set::Member::Int(n) => yo_common::num::i64_digits(digits, n),
497    }
498}
499
500/// Every member of any of the sets, each once.
501///
502/// A union has to read every member of every set whatever it does, so the only
503/// question is what it does with each one. Against a mixture of representations
504/// the answer is one insertion into a table that is also the duplicate check,
505/// and against intsets it is a merge, where the duplicate check is that two
506/// cursors are on the same value and costs a comparison rather than a hash.
507///
508/// The order differs between the two, and that is the one place a plan is
509/// visible from outside. The table walks the sets in turn, so it answers in the
510/// order each set holds its members, and the merge answers in ascending order
511/// across all of them. Redis promises neither.
512pub fn union<F>(scratch: &mut Scratch, sets: &[&Set], f: F) -> usize
513where
514    F: FnMut(&[u8]),
515{
516    union_with(scratch, plan_for(sets), sets, f)
517}
518
519/// The same, with the plan named rather than assumed.
520///
521/// [`inter_with`]'s reason for existing, applied here: the benchmark has to be
522/// able to run the table over the very sets the merge is fastest on, or the
523/// claim that the merge is worth having is a claim about two different inputs.
524///
525/// There are only two plans here, so anything that is not [`Plan::Merge`] is the
526/// table, and a merge asked for over operands that cannot merge is the table too.
527pub fn union_with<F>(scratch: &mut Scratch, how: Plan, sets: &[&Set], f: F) -> usize
528where
529    F: FnMut(&[u8]),
530{
531    match (how, as_ints(sets)) {
532        (Plan::Merge, Some(ints)) if !ints.is_empty() => union_merge(&ints, f),
533        _ => union_table(&mut scratch.seen, sets, f),
534    }
535}
536
537/// Step through every set at once and take the smallest value each round.
538///
539/// The smallest is found by looking at every cursor, which is `k` comparisons a
540/// member and no hashing at all. That makes this quadratic in `k` where the
541/// table is linear, so the win narrows from 9.1 times at k of 2 to 2.75 at k of
542/// 16 and the two would cross somewhere past k of 50. A heap would turn the scan
543/// into `log k` at the cost of a comparison per push, and it is not worth paying
544/// for a `SUNION` nobody writes.
545fn union_merge<F>(sets: &[&Intset], mut f: F) -> usize
546where
547    F: FnMut(&[u8]),
548{
549    let mut walks: PerSet<Walk<'_>> = sets.iter().map(|s| s.walk()).collect();
550    let mut digits = [0u8; DIGITS_MAX];
551    let mut found = 0usize;
552    while let Some(low) = walks.iter().filter_map(Walk::peek).min() {
553        f(yo_common::num::i64_digits(&mut digits, low));
554        found += 1;
555        // Every cursor sitting on it, because the same member in two sets is
556        // one member and this is where that is decided.
557        for w in &mut walks {
558            if w.peek() == Some(low) {
559                w.bump();
560            }
561        }
562    }
563    found
564}
565
566/// Walk everything into one table, where the table is the duplicate check.
567fn union_table<F>(seen: &mut Elements<()>, sets: &[&Set], mut f: F) -> usize
568where
569    F: FnMut(&[u8]),
570{
571    // The result is at most everything, and presizing to the largest input is
572    // the cheap half of that bound without pretending to know the overlap.
573    let biggest = sets.iter().map(|s| s.len()).max().unwrap_or(0);
574    let mut digits = [0u8; DIGITS_MAX];
575    seen.clear();
576    // `yo_alloc::high_water` on both of these, and on the insert below, because
577    // the table belongs to the database and is cleared rather than dropped. It
578    // grows when this union is bigger than every union before it and not
579    // otherwise, which is what
580    // `sets::tests::a_union_over_text_sets_does_not_allocate_once_its_table_is_warm`
581    // measures.
582    //
583    // The insert is the one that is per member rather than per call: the slot
584    // array and the rows are covered by the reserve, but the name blob is not,
585    // and it grows as the names go in. A guard around the whole walk instead
586    // would be one call rather than one per member, and it would hide whatever
587    // `f` does, which is the reply buffer for `SUNION` and the destination set
588    // for `SUNIONSTORE`. The report is worth more than that.
589    //
590    // A claim per member was worth being careful about, and `setops_small` on a
591    // laptop with other work on it could not tell the two versions apart: the
592    // spread between two runs of the same code was larger than the thing being
593    // looked for. So `yo_alloc::allow` grew a relaxed load of a static in front
594    // of its thread local work instead, and this is free in any process that has
595    // not armed a thread, which is every shipped binary.
596    yo_alloc::high_water(|| seen.reserve(biggest));
597    let mut found = 0usize;
598    for s in sets {
599        for m in s.iter() {
600            // The bytes are the duplicate check, which is what makes the same
601            // member found in two representations one member: an intset's 42
602            // and a table's `42` key the same, and `042` keys as itself,
603            // because that is the same rule that decided how each was stored.
604            let name = text(m, &mut digits);
605            let fresh = yo_alloc::high_water(|| seen.insert(name, ()));
606            if fresh.is_ok_and(|was| was.is_none()) {
607                f(name);
608                found += 1;
609            }
610        }
611    }
612    found
613}
614
615/// The members of the first set that no later set has.
616///
617/// The first set is the one being walked whether we like it or not, so the only
618/// choice is how each member is checked. Against a mixture that is a question
619/// per member, asked smallest set first because a member that is going to be
620/// found will usually be found there, and a member that is in the second set is
621/// never asked about the third. Against intsets it is a merge, and the order is
622/// the same either way because both walk the first set and the first set is
623/// ascending.
624pub fn diff<F>(sets: &[&Set], f: F) -> usize
625where
626    F: FnMut(&[u8]),
627{
628    diff_with(plan_for(sets), sets, f)
629}
630
631/// The same, with the plan named rather than assumed. See [`union_with`].
632pub fn diff_with<F>(how: Plan, sets: &[&Set], f: F) -> usize
633where
634    F: FnMut(&[u8]),
635{
636    match (how, as_ints(sets)) {
637        (Plan::Merge, Some(ints)) if !ints.is_empty() => diff_merge(&ints, f),
638        _ => diff_probe(sets, f),
639    }
640}
641
642/// Walk the first set, dragging a cursor through each of the others behind it.
643///
644/// The cursors only ever move forward, so the whole operation costs one pass
645/// over the first set and at most one pass over each of the others, however many
646/// members are in the answer. A probe pays a hash and a random access per member
647/// per set instead.
648fn diff_merge<F>(sets: &[&Intset], mut f: F) -> usize
649where
650    F: FnMut(&[u8]),
651{
652    let (first, rest) = sets.split_first().expect("not empty");
653    let mut walk = first.walk();
654    let mut others: PerSet<Walk<'_>> = rest.iter().map(|s| s.walk()).collect();
655    let mut digits = [0u8; DIGITS_MAX];
656    let mut found = 0usize;
657    while let Some(v) = walk.peek() {
658        let mut anyone = false;
659        for w in &mut others {
660            w.seek(v);
661            if w.peek() == Some(v) {
662                anyone = true;
663                break;
664            }
665        }
666        if !anyone {
667            f(yo_common::num::i64_digits(&mut digits, v));
668            found += 1;
669        }
670        walk.bump();
671    }
672    found
673}
674
675/// Walk the first set and ask the others about every member.
676fn diff_probe<F>(sets: &[&Set], mut f: F) -> usize
677where
678    F: FnMut(&[u8]),
679{
680    let Some((first, rest)) = sets.split_first() else {
681        return 0;
682    };
683    let mut order: PerSet<usize> = (0..rest.len()).collect();
684    order.sort_unstable_by_key(|&i| rest[i].len());
685
686    let mut digits = [0u8; DIGITS_MAX];
687    let mut found = 0usize;
688    for m in first.iter() {
689        let needle = Needle::of(m, &mut digits);
690        if !order.iter().any(|&i| rest[i].has(&needle)) {
691            f(needle.bytes());
692            found += 1;
693        }
694    }
695    found
696}
697
698/// The `*STORE` forms: run the operation and build the result as a set.
699///
700/// Presized once from `upper`, which the caller takes from the smallest input
701/// for an intersection or a difference and the sum for a union. Y18's rule, and
702/// the thing that stopped aki's `*STORE` family at 0.30x was that it was not
703/// applied.
704///
705/// Nothing comes back when nothing was found, because an empty set is not a
706/// thing that can exist. That is not a tidy up either: `SINTERSTORE d a b` with
707/// an empty intersection deletes `d` and answers zero, so the caller needs the
708/// difference between a set of no members and no set, and this is where it is.
709///
710/// The result picks its own representation from its first member and `upper`,
711/// through the same [`Set::with_hint`] `SADD` uses, so intersecting two intsets
712/// stores an intset rather than storing a table that happens to hold digits.
713/// The members arrive as bytes and that is enough to decide it, because the
714/// rule that made a member an integer on the way in is the rule that reads it
715/// as one on the way out.
716pub fn collect(
717    upper: usize,
718    limits: &Limits,
719    run: impl FnOnce(&mut dyn FnMut(&[u8])),
720) -> Option<Set> {
721    let mut out: Option<Set> = None;
722    run(&mut |name| match &mut out {
723        Some(s) => {
724            s.add(name, limits);
725        }
726        None => {
727            let mut s = Set::with_hint(name, upper, limits);
728            s.add(name, limits);
729            out = Some(s);
730        }
731    });
732    out
733}
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738    use crate::set::Encoding;
739
740    /// A set holding these members, in whatever representation it picks.
741    fn set(members: &[&str]) -> Set {
742        of(members.iter().map(|m| m.as_bytes()))
743    }
744
745    /// The same from bytes, for the members that are not text.
746    fn of<'a>(members: impl IntoIterator<Item = &'a [u8]>) -> Set {
747        let mut s = Set::new();
748        for m in members {
749            s.add(m, &Limits::DEFAULT);
750        }
751        s
752    }
753
754    /// A named way of building a set in one particular representation.
755    type Band = (&'static str, fn(&[&str]) -> Set);
756
757    /// A set forced past a band, so that a test can pick which representation
758    /// its operands are in rather than take whatever the member count gives.
759    fn banded(members: &[&str], limits: &Limits) -> Set {
760        let mut s = Set::new();
761        for m in members {
762            s.add(m.as_bytes(), limits);
763        }
764        s
765    }
766
767    /// Limits that put a set of any size in each of the three bands.
768    const AS_INTSET: Limits = Limits {
769        max_intset_entries: usize::MAX,
770        max_listpack_entries: usize::MAX,
771        max_listpack_value: usize::MAX,
772    };
773    const AS_LISTPACK: Limits = Limits {
774        max_intset_entries: 0,
775        max_listpack_entries: usize::MAX,
776        max_listpack_value: usize::MAX,
777    };
778    const AS_TABLE: Limits = Limits {
779        max_intset_entries: 0,
780        max_listpack_entries: 0,
781        max_listpack_value: 0,
782    };
783
784    /// A table holding these members, whatever they are.
785    ///
786    /// [`AS_TABLE`] is not enough on its own any more. Since #148 an all integer
787    /// set stays an intset past every ceiling and only changes the word
788    /// `OBJECT ENCODING` answers, so no configuration puts one in a table. What
789    /// still does is a member that is not an integer, and taking it out again
790    /// leaves the table behind, because every promotion here is one way.
791    fn tabled(members: &[&str]) -> Set {
792        let mut s = Set::new();
793        s.add(b"not a number", &AS_TABLE);
794        for m in members {
795            s.add(m.as_bytes(), &AS_TABLE);
796        }
797        s.remove(b"not a number");
798        assert_eq!(s.encoding(), Encoding::Hashtable);
799        assert!(s.ints().is_none(), "and a table underneath the word");
800        s
801    }
802
803    fn run<F>(op: F) -> Vec<String>
804    where
805        F: FnOnce(&mut dyn FnMut(&[u8])) -> usize,
806    {
807        let mut got = Vec::new();
808        let n = op(&mut |m| got.push(String::from_utf8_lossy(m).into_owned()));
809        assert_eq!(n, got.len(), "the count and the members disagree");
810        got
811    }
812
813    #[test]
814    fn an_intersection_is_what_they_all_have() {
815        let a = set(&["a", "b", "c", "d"]);
816        let b = set(&["b", "c", "d", "e"]);
817        let c = set(&["c", "d", "e", "f"]);
818        let got = run(|f| inter(&mut Scratch::new(), &[&a, &b, &c], 0, f));
819        assert_eq!(got, vec!["c", "d"]);
820    }
821
822    #[test]
823    fn an_intersection_of_one_set_is_that_set() {
824        let a = set(&["x", "y"]);
825        assert_eq!(
826            run(|f| inter(&mut Scratch::new(), &[&a], 0, f)),
827            vec!["x", "y"]
828        );
829    }
830
831    #[test]
832    fn an_empty_set_anywhere_empties_the_intersection() {
833        let a = set(&["a", "b"]);
834        let empty = set(&[]);
835        assert_eq!(
836            run(|f| inter(&mut Scratch::new(), &[&a, &empty], 0, f)),
837            Vec::<String>::new()
838        );
839        assert_eq!(
840            run(|f| inter(&mut Scratch::new(), &[&empty, &a], 0, f)),
841            Vec::<String>::new()
842        );
843        assert_eq!(
844            run(|f| inter(&mut Scratch::new(), &[], 0, f)),
845            Vec::<String>::new()
846        );
847    }
848
849    /// `SINTERCARD` stops as soon as it has enough, and stopping early must not
850    /// change the members it already handed over.
851    #[test]
852    fn a_limit_stops_the_intersection_early() {
853        let a = set(&["a", "b", "c", "d", "e"]);
854        let b = set(&["a", "b", "c", "d", "e"]);
855        assert_eq!(
856            run(|f| inter(&mut Scratch::new(), &[&a, &b], 2, f)),
857            vec!["a", "b"]
858        );
859        assert_eq!(
860            run(|f| inter(&mut Scratch::new(), &[&a, &b], 99, f)).len(),
861            5
862        );
863        assert_eq!(
864            run(|f| inter(&mut Scratch::new(), &[&a, &b], 0, f)).len(),
865            5,
866            "zero is no limit"
867        );
868    }
869
870    /// The two plans are two ways to compute the same thing, so they have to
871    /// agree on the members and on the order, or a client sees the answer change
872    /// when a set grows past a threshold it cannot see.
873    #[test]
874    fn both_plans_give_the_same_answer_in_the_same_order() {
875        let sets: Vec<Set> = (0..9)
876            .map(|s| {
877                let members: Vec<String> = (0..200)
878                    .filter(|i| i % (s + 2) != 1)
879                    .map(|i| format!("m{i}"))
880                    .collect();
881                set(&members.iter().map(String::as_str).collect::<Vec<_>>())
882            })
883            .collect();
884        let refs: Vec<&Set> = sets.iter().collect();
885
886        let probed = run(|f| inter_with(&mut Scratch::new(), Plan::Probe, &refs, 0, f));
887        let piled = run(|f| inter_with(&mut Scratch::new(), Plan::Accumulate, &refs, 0, f));
888        assert_eq!(probed, piled);
889        assert!(!probed.is_empty(), "the fixture should overlap");
890        assert_eq!(
891            run(|f| inter(&mut Scratch::new(), &refs, 0, f)),
892            probed,
893            "and so does the chooser"
894        );
895    }
896
897    #[test]
898    fn a_union_has_everything_once() {
899        let a = set(&["a", "b"]);
900        let b = set(&["b", "c"]);
901        let c = set(&["c", "d"]);
902        assert_eq!(
903            run(|f| union(&mut Scratch::new(), &[&a, &b, &c], f)),
904            vec!["a", "b", "c", "d"]
905        );
906        assert_eq!(
907            run(|f| union(&mut Scratch::new(), &[], f)),
908            Vec::<String>::new()
909        );
910    }
911
912    #[test]
913    fn a_difference_takes_the_others_out_of_the_first() {
914        let a = set(&["a", "b", "c", "d"]);
915        let b = set(&["b"]);
916        let c = set(&["d", "e"]);
917        assert_eq!(run(|f| diff(&[&a, &b, &c], f)), vec!["a", "c"]);
918        assert_eq!(run(|f| diff(&[&a], f)), vec!["a", "b", "c", "d"]);
919        assert_eq!(run(|f| diff(&[], f)), Vec::<String>::new());
920    }
921
922    /// Ten sets all holding the same members is the shape that gives probe the
923    /// least help, because nothing fails early and every member is asked about by
924    /// every other set. It is the shape the benchmark measures and the one K11's
925    /// number was about, so the plans have to agree on it in particular.
926    #[test]
927    fn the_plans_agree_where_every_set_holds_everything() {
928        let members: Vec<String> = (0..100).map(|i| format!("m{i}")).collect();
929        let names: Vec<&str> = members.iter().map(String::as_str).collect();
930        let sets: Vec<Set> = (0..10).map(|_| set(&names)).collect();
931        let refs: Vec<&Set> = sets.iter().collect();
932
933        let probed = run(|f| inter_with(&mut Scratch::new(), Plan::Probe, &refs, 0, f));
934        assert_eq!(probed, members, "everything is in all ten");
935        assert_eq!(
936            run(|f| inter_with(&mut Scratch::new(), Plan::Accumulate, &refs, 0, f)),
937            probed
938        );
939        assert_eq!(run(|f| inter(&mut Scratch::new(), &refs, 0, f)), probed);
940    }
941
942    #[test]
943    fn a_store_form_builds_a_set_of_the_result() {
944        let a = set(&["a", "b", "c"]);
945        let b = set(&["b", "c", "d"]);
946        let out = collect(a.len().min(b.len()), &Limits::DEFAULT, |f| {
947            inter(&mut Scratch::new(), &[&a, &b], 0, f);
948        })
949        .expect("two members is a set");
950        assert_eq!(out.len(), 2);
951        assert!(out.contains(b"b") && out.contains(b"c"));
952        assert!(!out.contains(b"a"));
953    }
954
955    /// A result of nothing is no set at all, which is the difference the STORE
956    /// forms need: an empty intersection deletes the destination rather than
957    /// leaving an empty set behind that EXISTS would answer one for.
958    #[test]
959    fn a_store_form_of_nothing_is_nothing() {
960        let a = set(&["a"]);
961        let b = set(&["b"]);
962        assert!(
963            collect(1, &Limits::DEFAULT, |f| {
964                inter(&mut Scratch::new(), &[&a, &b], 0, f);
965            })
966            .is_none()
967        );
968    }
969
970    /// The destination picks its own representation from what went into it, so
971    /// intersecting two intsets stores an intset and not a table of digits.
972    #[test]
973    fn a_store_form_keeps_the_representation_its_members_deserve() {
974        let a = set(&["1", "2", "3"]);
975        let b = set(&["2", "3", "4"]);
976        assert_eq!(a.encoding(), Encoding::Intset);
977        let out = collect(3, &Limits::DEFAULT, |f| {
978            inter(&mut Scratch::new(), &[&a, &b], 0, f);
979        })
980        .expect("two members");
981        assert_eq!(out.encoding(), Encoding::Intset);
982        assert!(out.contains(b"2") && out.contains(b"3"));
983
984        // And a union with one string in it does not, because one member that
985        // is not a number is all it takes.
986        let c = set(&["x"]);
987        let out = collect(4, &Limits::DEFAULT, |f| {
988            union(&mut Scratch::new(), &[&a, &c], f);
989        })
990        .expect("four members");
991        assert_ne!(out.encoding(), Encoding::Intset);
992        assert!(out.contains(b"1") && out.contains(b"x"));
993    }
994
995    /// The one that could not have worked before this: an intset member is a
996    /// number with no digits anywhere and a table stores that same member as
997    /// its digits, so every pairing of the three representations has to agree
998    /// about what a member is or the answers come back empty.
999    #[test]
1000    fn the_three_representations_intersect_each_other() {
1001        let names = ["1", "2", "3", "4"];
1002        let others = ["3", "4", "5", "6"];
1003        // The table is built rather than configured, because no ceiling puts an
1004        // all integer set in one any more. See [`tabled`].
1005        let bands: [Band; 3] = [
1006            ("intset", |m| banded(m, &AS_INTSET)),
1007            ("listpack", |m| banded(m, &AS_LISTPACK)),
1008            ("table", tabled),
1009        ];
1010        for (ln, left) in bands {
1011            for (rn, right) in bands {
1012                let a = left(&names);
1013                let b = right(&others);
1014                let mut got = run(|f| inter(&mut Scratch::new(), &[&a, &b], 0, f));
1015                got.sort();
1016                assert_eq!(got, ["3", "4"], "{ln} against {rn}");
1017
1018                let mut got = run(|f| union(&mut Scratch::new(), &[&a, &b], f));
1019                got.sort();
1020                assert_eq!(got, ["1", "2", "3", "4", "5", "6"], "{ln} with {rn}");
1021
1022                let mut got = run(|f| diff(&[&a, &b], f));
1023                got.sort();
1024                assert_eq!(got, ["1", "2"], "{ln} without {rn}");
1025            }
1026        }
1027    }
1028
1029    /// A member that looks like a number and a member that does not quite are
1030    /// two different members, and which one a set stored is decided by the same
1031    /// rule the algebra reads it back by.
1032    #[test]
1033    fn a_number_and_its_untidy_spelling_stay_two_members() {
1034        let a = banded(&["42", "042", "-0"], &AS_LISTPACK);
1035        let b = banded(&["42"], &AS_INTSET);
1036        assert_eq!(
1037            run(|f| inter(&mut Scratch::new(), &[&a, &b], 0, f)),
1038            vec!["42"]
1039        );
1040        let mut got = run(|f| diff(&[&a, &b], f));
1041        got.sort();
1042        assert_eq!(got, ["-0", "042"]);
1043        let mut got = run(|f| union(&mut Scratch::new(), &[&a, &b], f));
1044        got.sort();
1045        assert_eq!(
1046            got,
1047            ["-0", "042", "42"],
1048            "and the union does not merge them"
1049        );
1050    }
1051
1052    /// A set of the given integers, which is an intset and so is mergeable.
1053    fn ints(vals: &[i64]) -> Set {
1054        let mut s = Set::new();
1055        for v in vals {
1056            s.add(v.to_string().as_bytes(), &AS_INTSET);
1057        }
1058        assert_eq!(s.encoding(), Encoding::Intset);
1059        s
1060    }
1061
1062    /// Integers from a cheap scrambler, so the sets are not runs of consecutive
1063    /// values and the cursors have something to skip over.
1064    fn scattered(n: usize, seed: i64, span: i64) -> Vec<i64> {
1065        (0..n as i64)
1066            .map(|i| (i.wrapping_add(seed).wrapping_mul(2_654_435_761)).rem_euclid(span))
1067            .collect()
1068    }
1069
1070    /// The merge and the probe are two ways to compute the same thing, so they
1071    /// have to agree member for member and in order, on every shape.
1072    ///
1073    /// The shapes matter more than the count. Two sets of the same size that
1074    /// mostly overlap is what the seek never gets to help with, a small set
1075    /// against a huge one is what it exists for, and disjoint ranges are where a
1076    /// single seek is meant to cross the whole of the other set at once.
1077    #[test]
1078    fn the_merge_and_the_probe_agree_on_every_shape() {
1079        let shapes: [(&str, Vec<Vec<i64>>); 5] = [
1080            (
1081                "same size, mostly shared",
1082                vec![scattered(4_000, 0, 5_000), scattered(4_000, 7, 5_000)],
1083            ),
1084            (
1085                "ten against a hundred thousand",
1086                vec![scattered(10, 3, 100_000), scattered(100_000, 0, 200_000)],
1087            ),
1088            (
1089                "disjoint ranges",
1090                vec![(0..2_000).collect(), (900_000..902_000).collect()],
1091            ),
1092            (
1093                "five sets",
1094                vec![
1095                    scattered(3_000, 1, 4_000),
1096                    scattered(3_000, 2, 4_000),
1097                    scattered(3_000, 3, 4_000),
1098                    scattered(3_000, 4, 4_000),
1099                    scattered(3_000, 5, 4_000),
1100                ],
1101            ),
1102            (
1103                "negatives and a member too wide for a narrow run",
1104                vec![
1105                    vec![-9_000_000_000, -3, -2, -1, 0, 1, 2, 9_000_000_000],
1106                    vec![-9_000_000_000, -2, 0, 2, 4, 9_000_000_000],
1107                ],
1108            ),
1109        ];
1110
1111        for (what, vals) in shapes {
1112            let sets: Vec<Set> = vals.iter().map(|v| ints(v)).collect();
1113            let refs: Vec<&Set> = sets.iter().collect();
1114            assert_eq!(plan_for(&refs), Plan::Merge, "{what}");
1115
1116            let probed = run(|f| inter_with(&mut Scratch::new(), Plan::Probe, &refs, 0, f));
1117            assert_eq!(
1118                run(|f| inter(&mut Scratch::new(), &refs, 0, f)),
1119                probed,
1120                "intersect {what}"
1121            );
1122            assert_eq!(
1123                run(|f| inter_with(&mut Scratch::new(), Plan::Accumulate, &refs, 0, f)),
1124                probed,
1125                "and the count agrees, {what}"
1126            );
1127
1128            let subbed = diff_the_slow_way(&vals);
1129            assert_eq!(run(|f| diff(&refs, f)), subbed, "sub {what}");
1130            assert_eq!(
1131                run(|f| diff_with(Plan::Probe, &refs, f)),
1132                subbed,
1133                "and the probe agrees, {what}"
1134            );
1135
1136            let mut piled: Vec<String> = union_the_slow_way(&vals);
1137            piled.sort();
1138            for how in [Plan::Merge, Plan::Probe] {
1139                let mut got = run(|f| union_with(&mut Scratch::new(), how, &refs, f));
1140                got.sort();
1141                assert_eq!(got, piled, "union {what} by {how:?}");
1142            }
1143        }
1144    }
1145
1146    /// The difference worked out with a `BTreeSet`, which is the answer the
1147    /// merge has to match and shares no code with it.
1148    fn diff_the_slow_way(vals: &[Vec<i64>]) -> Vec<String> {
1149        let (first, rest) = vals.split_first().expect("not empty");
1150        let others: std::collections::BTreeSet<i64> =
1151            rest.iter().flat_map(|v| v.iter().copied()).collect();
1152        let mut left: Vec<i64> = first
1153            .iter()
1154            .copied()
1155            .filter(|v| !others.contains(v))
1156            .collect();
1157        left.sort_unstable();
1158        left.dedup();
1159        left.iter().map(i64::to_string).collect()
1160    }
1161
1162    fn union_the_slow_way(vals: &[Vec<i64>]) -> Vec<String> {
1163        let all: std::collections::BTreeSet<i64> =
1164            vals.iter().flat_map(|v| v.iter().copied()).collect();
1165        all.iter().map(i64::to_string).collect()
1166    }
1167
1168    /// A merged intersection comes back smallest first, which is the order the
1169    /// probe already produced on these operands, so a client cannot tell which
1170    /// plan ran.
1171    #[test]
1172    fn a_merged_intersection_is_ascending_and_so_was_the_probe() {
1173        let a = ints(&[900, 5, 40, 7, 1000, 3]);
1174        let b = ints(&[1000, 3, 900, 8, 5]);
1175        let got = run(|f| inter(&mut Scratch::new(), &[&a, &b], 0, f));
1176        assert_eq!(got, vec!["3", "5", "900", "1000"]);
1177        assert_eq!(
1178            run(|f| inter_with(&mut Scratch::new(), Plan::Probe, &[&a, &b], 0, f)),
1179            got
1180        );
1181    }
1182
1183    /// `SINTERCARD` stops early on the merge too, and stopping early does not
1184    /// change what it had already handed over.
1185    #[test]
1186    fn a_limit_stops_a_merged_intersection_early() {
1187        let vals: Vec<i64> = (0..2_000).collect();
1188        let a = ints(&vals);
1189        let b = ints(&vals);
1190        assert_eq!(plan_for(&[&a, &b]), Plan::Merge);
1191        assert_eq!(
1192            run(|f| inter(&mut Scratch::new(), &[&a, &b], 3, f)),
1193            vec!["0", "1", "2"]
1194        );
1195        assert_eq!(
1196            run(|f| inter(&mut Scratch::new(), &[&a, &b], 0, f)).len(),
1197            2_000
1198        );
1199        assert_eq!(
1200            run(|f| inter(&mut Scratch::new(), &[&a], 3, f)),
1201            vec!["0", "1", "2"]
1202        );
1203    }
1204
1205    /// One set that is not an intset takes the whole operation back to a probe,
1206    /// because there is nothing to walk in step with a table.
1207    #[test]
1208    fn one_unsorted_operand_takes_everything_back_to_a_probe() {
1209        let a = ints(&[1, 2, 3]);
1210        let b = tabled(&["2", "3", "4"]);
1211        assert_eq!(plan_for(&[&a, &b]), Plan::Probe);
1212        assert_eq!(
1213            run(|f| inter(&mut Scratch::new(), &[&a, &b], 0, f)),
1214            vec!["2", "3"]
1215        );
1216        // And asking for the merge anyway gets the right answer rather than a
1217        // wrong one, because the fact beats the preference.
1218        assert_eq!(
1219            run(|f| inter_with(&mut Scratch::new(), Plan::Merge, &[&a, &b], 0, f)),
1220            vec!["2", "3"]
1221        );
1222    }
1223
1224    /// A set past `set-max-intset-entries` is still an intset here, so it still
1225    /// merges. Before #148 it was a table by this size and this test would have
1226    /// been measuring the probe.
1227    #[test]
1228    fn a_set_past_the_intset_ceiling_still_merges() {
1229        let a: Set = {
1230            let mut s = Set::new();
1231            for i in 0..5_000i64 {
1232                s.add(i.to_string().as_bytes(), &Limits::DEFAULT);
1233            }
1234            s
1235        };
1236        assert_eq!(a.encoding(), Encoding::Hashtable, "the word a server uses");
1237        assert!(a.ints().is_some(), "and an intset underneath it");
1238        let b = ints(&[4_998, 4_999, 5_000]);
1239        assert_eq!(plan_for(&[&a, &b]), Plan::Merge);
1240        assert_eq!(
1241            run(|f| inter(&mut Scratch::new(), &[&a, &b], 0, f)),
1242            vec!["4998", "4999"]
1243        );
1244    }
1245
1246    /// The members are the same bytes whatever they contain, and a set holds
1247    /// arbitrary bytes rather than text.
1248    #[test]
1249    fn members_that_are_not_text_work_the_same() {
1250        let a = of([&b"\x00\xff"[..], b"\xc3\x28", b""]);
1251        let b = of([&b"\xc3\x28"[..], b""]);
1252        let mut got: Vec<Vec<u8>> = Vec::new();
1253        let n = inter(&mut Scratch::new(), &[&a, &b], 0, |m| got.push(m.to_vec()));
1254        assert_eq!(n, 2);
1255        assert_eq!(got, vec![b"\xc3\x28".to_vec(), b"".to_vec()]);
1256    }
1257}