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.
512///
513/// `limit` is `SUNIONCARD`'s and works the way `SINTERCARD`'s does on [`inter`]:
514/// zero is no limit, and anything else stops the walk the moment it has that
515/// many. Stopping early is only sound because the count is the answer and the
516/// members are not, so which ones it happened to reach first does not matter.
517pub fn union<F>(scratch: &mut Scratch, sets: &[&Set], limit: usize, f: F) -> usize
518where
519    F: FnMut(&[u8]),
520{
521    union_with(scratch, plan_for(sets), sets, limit, f)
522}
523
524/// The same, with the plan named rather than assumed.
525///
526/// [`inter_with`]'s reason for existing, applied here: the benchmark has to be
527/// able to run the table over the very sets the merge is fastest on, or the
528/// claim that the merge is worth having is a claim about two different inputs.
529///
530/// There are only two plans here, so anything that is not [`Plan::Merge`] is the
531/// table, and a merge asked for over operands that cannot merge is the table too.
532pub fn union_with<F>(scratch: &mut Scratch, how: Plan, sets: &[&Set], limit: usize, f: F) -> usize
533where
534    F: FnMut(&[u8]),
535{
536    match (how, as_ints(sets)) {
537        (Plan::Merge, Some(ints)) if !ints.is_empty() => union_merge(&ints, limit, f),
538        _ => union_table(&mut scratch.seen, sets, limit, f),
539    }
540}
541
542/// Step through every set at once and take the smallest value each round.
543///
544/// The smallest is found by looking at every cursor, which is `k` comparisons a
545/// member and no hashing at all. That makes this quadratic in `k` where the
546/// table is linear, so the win narrows from 9.1 times at k of 2 to 2.75 at k of
547/// 16 and the two would cross somewhere past k of 50. A heap would turn the scan
548/// into `log k` at the cost of a comparison per push, and it is not worth paying
549/// for a `SUNION` nobody writes.
550fn union_merge<F>(sets: &[&Intset], limit: usize, mut f: F) -> usize
551where
552    F: FnMut(&[u8]),
553{
554    let mut walks: PerSet<Walk<'_>> = sets.iter().map(|s| s.walk()).collect();
555    let mut digits = [0u8; DIGITS_MAX];
556    let mut found = 0usize;
557    while let Some(low) = walks.iter().filter_map(Walk::peek).min() {
558        f(yo_common::num::i64_digits(&mut digits, low));
559        found += 1;
560        if limit != 0 && found == limit {
561            return found;
562        }
563        // Every cursor sitting on it, because the same member in two sets is
564        // one member and this is where that is decided.
565        for w in &mut walks {
566            if w.peek() == Some(low) {
567                w.bump();
568            }
569        }
570    }
571    found
572}
573
574/// Walk everything into one table, where the table is the duplicate check.
575fn union_table<F>(seen: &mut Elements<()>, sets: &[&Set], limit: usize, mut f: F) -> usize
576where
577    F: FnMut(&[u8]),
578{
579    // The result is at most everything, and presizing to the largest input is
580    // the cheap half of that bound without pretending to know the overlap.
581    let biggest = sets.iter().map(|s| s.len()).max().unwrap_or(0);
582    let mut digits = [0u8; DIGITS_MAX];
583    seen.clear();
584    // `yo_alloc::high_water` on both of these, and on the insert below, because
585    // the table belongs to the database and is cleared rather than dropped. It
586    // grows when this union is bigger than every union before it and not
587    // otherwise, which is what
588    // `sets::tests::a_union_over_text_sets_does_not_allocate_once_its_table_is_warm`
589    // measures.
590    //
591    // The insert is the one that is per member rather than per call: the slot
592    // array and the rows are covered by the reserve, but the name blob is not,
593    // and it grows as the names go in. A guard around the whole walk instead
594    // would be one call rather than one per member, and it would hide whatever
595    // `f` does, which is the reply buffer for `SUNION` and the destination set
596    // for `SUNIONSTORE`. The report is worth more than that.
597    //
598    // A claim per member was worth being careful about, and `setops_small` on a
599    // laptop with other work on it could not tell the two versions apart: the
600    // spread between two runs of the same code was larger than the thing being
601    // looked for. So `yo_alloc::allow` grew a relaxed load of a static in front
602    // of its thread local work instead, and this is free in any process that has
603    // not armed a thread, which is every shipped binary.
604    yo_alloc::high_water(|| seen.reserve(biggest));
605    let mut found = 0usize;
606    for s in sets {
607        for m in s.iter() {
608            // The bytes are the duplicate check, which is what makes the same
609            // member found in two representations one member: an intset's 42
610            // and a table's `42` key the same, and `042` keys as itself,
611            // because that is the same rule that decided how each was stored.
612            let name = text(m, &mut digits);
613            let fresh = yo_alloc::high_water(|| seen.insert(name, ()));
614            if fresh.is_ok_and(|was| was.is_none()) {
615                f(name);
616                found += 1;
617                if limit != 0 && found == limit {
618                    return found;
619                }
620            }
621        }
622    }
623    found
624}
625
626/// The members of the first set that no later set has.
627///
628/// The first set is the one being walked whether we like it or not, so the only
629/// choice is how each member is checked. Against a mixture that is a question
630/// per member, asked smallest set first because a member that is going to be
631/// found will usually be found there, and a member that is in the second set is
632/// never asked about the third. Against intsets it is a merge, and the order is
633/// the same either way because both walk the first set and the first set is
634/// ascending.
635///
636/// `limit` is `SDIFFCARD`'s, and is [`union`]'s in every respect.
637pub fn diff<F>(sets: &[&Set], limit: usize, f: F) -> usize
638where
639    F: FnMut(&[u8]),
640{
641    diff_with(plan_for(sets), sets, limit, f)
642}
643
644/// The same, with the plan named rather than assumed. See [`union_with`].
645pub fn diff_with<F>(how: Plan, sets: &[&Set], limit: usize, f: F) -> usize
646where
647    F: FnMut(&[u8]),
648{
649    match (how, as_ints(sets)) {
650        (Plan::Merge, Some(ints)) if !ints.is_empty() => diff_merge(&ints, limit, f),
651        _ => diff_probe(sets, limit, f),
652    }
653}
654
655/// Walk the first set, dragging a cursor through each of the others behind it.
656///
657/// The cursors only ever move forward, so the whole operation costs one pass
658/// over the first set and at most one pass over each of the others, however many
659/// members are in the answer. A probe pays a hash and a random access per member
660/// per set instead.
661fn diff_merge<F>(sets: &[&Intset], limit: usize, mut f: F) -> usize
662where
663    F: FnMut(&[u8]),
664{
665    let (first, rest) = sets.split_first().expect("not empty");
666    let mut walk = first.walk();
667    let mut others: PerSet<Walk<'_>> = rest.iter().map(|s| s.walk()).collect();
668    let mut digits = [0u8; DIGITS_MAX];
669    let mut found = 0usize;
670    while let Some(v) = walk.peek() {
671        let mut anyone = false;
672        for w in &mut others {
673            w.seek(v);
674            if w.peek() == Some(v) {
675                anyone = true;
676                break;
677            }
678        }
679        if !anyone {
680            f(yo_common::num::i64_digits(&mut digits, v));
681            found += 1;
682            if limit != 0 && found == limit {
683                return found;
684            }
685        }
686        walk.bump();
687    }
688    found
689}
690
691/// Walk the first set and ask the others about every member.
692fn diff_probe<F>(sets: &[&Set], limit: usize, mut f: F) -> usize
693where
694    F: FnMut(&[u8]),
695{
696    let Some((first, rest)) = sets.split_first() else {
697        return 0;
698    };
699    let mut order: PerSet<usize> = (0..rest.len()).collect();
700    order.sort_unstable_by_key(|&i| rest[i].len());
701
702    let mut digits = [0u8; DIGITS_MAX];
703    let mut found = 0usize;
704    for m in first.iter() {
705        let needle = Needle::of(m, &mut digits);
706        if !order.iter().any(|&i| rest[i].has(&needle)) {
707            f(needle.bytes());
708            found += 1;
709            if limit != 0 && found == limit {
710                return found;
711            }
712        }
713    }
714    found
715}
716
717/// The `*STORE` forms: run the operation and build the result as a set.
718///
719/// Presized once from `upper`, which the caller takes from the smallest input
720/// for an intersection or a difference and the sum for a union. Y18's rule, and
721/// the thing that stopped aki's `*STORE` family at 0.30x was that it was not
722/// applied.
723///
724/// Nothing comes back when nothing was found, because an empty set is not a
725/// thing that can exist. That is not a tidy up either: `SINTERSTORE d a b` with
726/// an empty intersection deletes `d` and answers zero, so the caller needs the
727/// difference between a set of no members and no set, and this is where it is.
728///
729/// The result picks its own representation from its first member and `upper`,
730/// through the same [`Set::with_hint`] `SADD` uses, so intersecting two intsets
731/// stores an intset rather than storing a table that happens to hold digits.
732/// The members arrive as bytes and that is enough to decide it, because the
733/// rule that made a member an integer on the way in is the rule that reads it
734/// as one on the way out.
735pub fn collect(
736    upper: usize,
737    limits: &Limits,
738    run: impl FnOnce(&mut dyn FnMut(&[u8])),
739) -> Option<Set> {
740    let mut out: Option<Set> = None;
741    run(&mut |name| match &mut out {
742        Some(s) => {
743            s.add(name, limits);
744        }
745        None => {
746            let mut s = Set::with_hint(name, upper, limits);
747            s.add(name, limits);
748            out = Some(s);
749        }
750    });
751    out
752}
753
754#[cfg(test)]
755mod tests {
756    use super::*;
757    use crate::set::Encoding;
758
759    /// A set holding these members, in whatever representation it picks.
760    fn set(members: &[&str]) -> Set {
761        of(members.iter().map(|m| m.as_bytes()))
762    }
763
764    /// The same from bytes, for the members that are not text.
765    fn of<'a>(members: impl IntoIterator<Item = &'a [u8]>) -> Set {
766        let mut s = Set::new();
767        for m in members {
768            s.add(m, &Limits::DEFAULT);
769        }
770        s
771    }
772
773    /// A named way of building a set in one particular representation.
774    type Band = (&'static str, fn(&[&str]) -> Set);
775
776    /// A set forced past a band, so that a test can pick which representation
777    /// its operands are in rather than take whatever the member count gives.
778    fn banded(members: &[&str], limits: &Limits) -> Set {
779        let mut s = Set::new();
780        for m in members {
781            s.add(m.as_bytes(), limits);
782        }
783        s
784    }
785
786    /// Limits that put a set of any size in each of the three bands.
787    const AS_INTSET: Limits = Limits {
788        max_intset_entries: usize::MAX,
789        max_listpack_entries: usize::MAX,
790        max_listpack_value: usize::MAX,
791    };
792    const AS_LISTPACK: Limits = Limits {
793        max_intset_entries: 0,
794        max_listpack_entries: usize::MAX,
795        max_listpack_value: usize::MAX,
796    };
797    const AS_TABLE: Limits = Limits {
798        max_intset_entries: 0,
799        max_listpack_entries: 0,
800        max_listpack_value: 0,
801    };
802
803    /// A table holding these members, whatever they are.
804    ///
805    /// [`AS_TABLE`] is not enough on its own any more. Since #148 an all integer
806    /// set stays an intset past every ceiling and only changes the word
807    /// `OBJECT ENCODING` answers, so no configuration puts one in a table. What
808    /// still does is a member that is not an integer, and taking it out again
809    /// leaves the table behind, because every promotion here is one way.
810    fn tabled(members: &[&str]) -> Set {
811        let mut s = Set::new();
812        s.add(b"not a number", &AS_TABLE);
813        for m in members {
814            s.add(m.as_bytes(), &AS_TABLE);
815        }
816        s.remove(b"not a number");
817        assert_eq!(s.encoding(), Encoding::Hashtable);
818        assert!(s.ints().is_none(), "and a table underneath the word");
819        s
820    }
821
822    fn run<F>(op: F) -> Vec<String>
823    where
824        F: FnOnce(&mut dyn FnMut(&[u8])) -> usize,
825    {
826        let mut got = Vec::new();
827        let n = op(&mut |m| got.push(String::from_utf8_lossy(m).into_owned()));
828        assert_eq!(n, got.len(), "the count and the members disagree");
829        got
830    }
831
832    #[test]
833    fn an_intersection_is_what_they_all_have() {
834        let a = set(&["a", "b", "c", "d"]);
835        let b = set(&["b", "c", "d", "e"]);
836        let c = set(&["c", "d", "e", "f"]);
837        let got = run(|f| inter(&mut Scratch::new(), &[&a, &b, &c], 0, f));
838        assert_eq!(got, vec!["c", "d"]);
839    }
840
841    #[test]
842    fn an_intersection_of_one_set_is_that_set() {
843        let a = set(&["x", "y"]);
844        assert_eq!(
845            run(|f| inter(&mut Scratch::new(), &[&a], 0, f)),
846            vec!["x", "y"]
847        );
848    }
849
850    #[test]
851    fn an_empty_set_anywhere_empties_the_intersection() {
852        let a = set(&["a", "b"]);
853        let empty = set(&[]);
854        assert_eq!(
855            run(|f| inter(&mut Scratch::new(), &[&a, &empty], 0, f)),
856            Vec::<String>::new()
857        );
858        assert_eq!(
859            run(|f| inter(&mut Scratch::new(), &[&empty, &a], 0, f)),
860            Vec::<String>::new()
861        );
862        assert_eq!(
863            run(|f| inter(&mut Scratch::new(), &[], 0, f)),
864            Vec::<String>::new()
865        );
866    }
867
868    /// `SINTERCARD` stops as soon as it has enough, and stopping early must not
869    /// change the members it already handed over.
870    #[test]
871    fn a_limit_stops_the_intersection_early() {
872        let a = set(&["a", "b", "c", "d", "e"]);
873        let b = set(&["a", "b", "c", "d", "e"]);
874        assert_eq!(
875            run(|f| inter(&mut Scratch::new(), &[&a, &b], 2, f)),
876            vec!["a", "b"]
877        );
878        assert_eq!(
879            run(|f| inter(&mut Scratch::new(), &[&a, &b], 99, f)).len(),
880            5
881        );
882        assert_eq!(
883            run(|f| inter(&mut Scratch::new(), &[&a, &b], 0, f)).len(),
884            5,
885            "zero is no limit"
886        );
887    }
888
889    /// `SUNIONCARD`'s limit, on both of the plans a union has, because the stop
890    /// is written once in each of them.
891    ///
892    /// The merge plan needs every operand to be an intset, which is why these
893    /// two are digits and the ones below are not.
894    #[test]
895    fn a_limit_stops_the_union_early() {
896        let a = set(&["1", "2", "3"]);
897        let b = set(&["3", "4", "5"]);
898        for how in [Plan::Merge, Plan::Accumulate] {
899            let mut s = Scratch::new();
900            assert_eq!(
901                run(|f| union_with(&mut s, how, &[&a, &b], 2, f)).len(),
902                2,
903                "{how:?} ignored the limit"
904            );
905            let mut s = Scratch::new();
906            assert_eq!(run(|f| union_with(&mut s, how, &[&a, &b], 99, f)).len(), 5);
907            let mut s = Scratch::new();
908            assert_eq!(
909                run(|f| union_with(&mut s, how, &[&a, &b], 0, f)).len(),
910                5,
911                "zero is no limit"
912            );
913        }
914    }
915
916    /// `SDIFFCARD`'s, the same way. The difference is four members here so that
917    /// a limit of two is a stop and not a coincidence.
918    #[test]
919    fn a_limit_stops_the_difference_early() {
920        let a = set(&["1", "2", "3", "4", "5"]);
921        let b = set(&["5"]);
922        for how in [Plan::Merge, Plan::Probe] {
923            assert_eq!(
924                run(|f| diff_with(how, &[&a, &b], 2, f)).len(),
925                2,
926                "{how:?} ignored the limit"
927            );
928            assert_eq!(run(|f| diff_with(how, &[&a, &b], 99, f)).len(), 4);
929            assert_eq!(
930                run(|f| diff_with(how, &[&a, &b], 0, f)).len(),
931                4,
932                "zero is no limit"
933            );
934        }
935    }
936
937    /// The two plans are two ways to compute the same thing, so they have to
938    /// agree on the members and on the order, or a client sees the answer change
939    /// when a set grows past a threshold it cannot see.
940    #[test]
941    fn both_plans_give_the_same_answer_in_the_same_order() {
942        let sets: Vec<Set> = (0..9)
943            .map(|s| {
944                let members: Vec<String> = (0..200)
945                    .filter(|i| i % (s + 2) != 1)
946                    .map(|i| format!("m{i}"))
947                    .collect();
948                set(&members.iter().map(String::as_str).collect::<Vec<_>>())
949            })
950            .collect();
951        let refs: Vec<&Set> = sets.iter().collect();
952
953        let probed = run(|f| inter_with(&mut Scratch::new(), Plan::Probe, &refs, 0, f));
954        let piled = run(|f| inter_with(&mut Scratch::new(), Plan::Accumulate, &refs, 0, f));
955        assert_eq!(probed, piled);
956        assert!(!probed.is_empty(), "the fixture should overlap");
957        assert_eq!(
958            run(|f| inter(&mut Scratch::new(), &refs, 0, f)),
959            probed,
960            "and so does the chooser"
961        );
962    }
963
964    #[test]
965    fn a_union_has_everything_once() {
966        let a = set(&["a", "b"]);
967        let b = set(&["b", "c"]);
968        let c = set(&["c", "d"]);
969        assert_eq!(
970            run(|f| union(&mut Scratch::new(), &[&a, &b, &c], 0, f)),
971            vec!["a", "b", "c", "d"]
972        );
973        assert_eq!(
974            run(|f| union(&mut Scratch::new(), &[], 0, f)),
975            Vec::<String>::new()
976        );
977    }
978
979    #[test]
980    fn a_difference_takes_the_others_out_of_the_first() {
981        let a = set(&["a", "b", "c", "d"]);
982        let b = set(&["b"]);
983        let c = set(&["d", "e"]);
984        assert_eq!(run(|f| diff(&[&a, &b, &c], 0, f)), vec!["a", "c"]);
985        assert_eq!(run(|f| diff(&[&a], 0, f)), vec!["a", "b", "c", "d"]);
986        assert_eq!(run(|f| diff(&[], 0, f)), Vec::<String>::new());
987    }
988
989    /// Ten sets all holding the same members is the shape that gives probe the
990    /// least help, because nothing fails early and every member is asked about by
991    /// every other set. It is the shape the benchmark measures and the one K11's
992    /// number was about, so the plans have to agree on it in particular.
993    #[test]
994    fn the_plans_agree_where_every_set_holds_everything() {
995        let members: Vec<String> = (0..100).map(|i| format!("m{i}")).collect();
996        let names: Vec<&str> = members.iter().map(String::as_str).collect();
997        let sets: Vec<Set> = (0..10).map(|_| set(&names)).collect();
998        let refs: Vec<&Set> = sets.iter().collect();
999
1000        let probed = run(|f| inter_with(&mut Scratch::new(), Plan::Probe, &refs, 0, f));
1001        assert_eq!(probed, members, "everything is in all ten");
1002        assert_eq!(
1003            run(|f| inter_with(&mut Scratch::new(), Plan::Accumulate, &refs, 0, f)),
1004            probed
1005        );
1006        assert_eq!(run(|f| inter(&mut Scratch::new(), &refs, 0, f)), probed);
1007    }
1008
1009    #[test]
1010    fn a_store_form_builds_a_set_of_the_result() {
1011        let a = set(&["a", "b", "c"]);
1012        let b = set(&["b", "c", "d"]);
1013        let out = collect(a.len().min(b.len()), &Limits::DEFAULT, |f| {
1014            inter(&mut Scratch::new(), &[&a, &b], 0, f);
1015        })
1016        .expect("two members is a set");
1017        assert_eq!(out.len(), 2);
1018        assert!(out.contains(b"b") && out.contains(b"c"));
1019        assert!(!out.contains(b"a"));
1020    }
1021
1022    /// A result of nothing is no set at all, which is the difference the STORE
1023    /// forms need: an empty intersection deletes the destination rather than
1024    /// leaving an empty set behind that EXISTS would answer one for.
1025    #[test]
1026    fn a_store_form_of_nothing_is_nothing() {
1027        let a = set(&["a"]);
1028        let b = set(&["b"]);
1029        assert!(
1030            collect(1, &Limits::DEFAULT, |f| {
1031                inter(&mut Scratch::new(), &[&a, &b], 0, f);
1032            })
1033            .is_none()
1034        );
1035    }
1036
1037    /// The destination picks its own representation from what went into it, so
1038    /// intersecting two intsets stores an intset and not a table of digits.
1039    #[test]
1040    fn a_store_form_keeps_the_representation_its_members_deserve() {
1041        let a = set(&["1", "2", "3"]);
1042        let b = set(&["2", "3", "4"]);
1043        assert_eq!(a.encoding(), Encoding::Intset);
1044        let out = collect(3, &Limits::DEFAULT, |f| {
1045            inter(&mut Scratch::new(), &[&a, &b], 0, f);
1046        })
1047        .expect("two members");
1048        assert_eq!(out.encoding(), Encoding::Intset);
1049        assert!(out.contains(b"2") && out.contains(b"3"));
1050
1051        // And a union with one string in it does not, because one member that
1052        // is not a number is all it takes.
1053        let c = set(&["x"]);
1054        let out = collect(4, &Limits::DEFAULT, |f| {
1055            union(&mut Scratch::new(), &[&a, &c], 0, f);
1056        })
1057        .expect("four members");
1058        assert_ne!(out.encoding(), Encoding::Intset);
1059        assert!(out.contains(b"1") && out.contains(b"x"));
1060    }
1061
1062    /// The one that could not have worked before this: an intset member is a
1063    /// number with no digits anywhere and a table stores that same member as
1064    /// its digits, so every pairing of the three representations has to agree
1065    /// about what a member is or the answers come back empty.
1066    #[test]
1067    fn the_three_representations_intersect_each_other() {
1068        let names = ["1", "2", "3", "4"];
1069        let others = ["3", "4", "5", "6"];
1070        // The table is built rather than configured, because no ceiling puts an
1071        // all integer set in one any more. See [`tabled`].
1072        let bands: [Band; 3] = [
1073            ("intset", |m| banded(m, &AS_INTSET)),
1074            ("listpack", |m| banded(m, &AS_LISTPACK)),
1075            ("table", tabled),
1076        ];
1077        for (ln, left) in bands {
1078            for (rn, right) in bands {
1079                let a = left(&names);
1080                let b = right(&others);
1081                let mut got = run(|f| inter(&mut Scratch::new(), &[&a, &b], 0, f));
1082                got.sort();
1083                assert_eq!(got, ["3", "4"], "{ln} against {rn}");
1084
1085                let mut got = run(|f| union(&mut Scratch::new(), &[&a, &b], 0, f));
1086                got.sort();
1087                assert_eq!(got, ["1", "2", "3", "4", "5", "6"], "{ln} with {rn}");
1088
1089                let mut got = run(|f| diff(&[&a, &b], 0, f));
1090                got.sort();
1091                assert_eq!(got, ["1", "2"], "{ln} without {rn}");
1092            }
1093        }
1094    }
1095
1096    /// A member that looks like a number and a member that does not quite are
1097    /// two different members, and which one a set stored is decided by the same
1098    /// rule the algebra reads it back by.
1099    #[test]
1100    fn a_number_and_its_untidy_spelling_stay_two_members() {
1101        let a = banded(&["42", "042", "-0"], &AS_LISTPACK);
1102        let b = banded(&["42"], &AS_INTSET);
1103        assert_eq!(
1104            run(|f| inter(&mut Scratch::new(), &[&a, &b], 0, f)),
1105            vec!["42"]
1106        );
1107        let mut got = run(|f| diff(&[&a, &b], 0, f));
1108        got.sort();
1109        assert_eq!(got, ["-0", "042"]);
1110        let mut got = run(|f| union(&mut Scratch::new(), &[&a, &b], 0, f));
1111        got.sort();
1112        assert_eq!(
1113            got,
1114            ["-0", "042", "42"],
1115            "and the union does not merge them"
1116        );
1117    }
1118
1119    /// A set of the given integers, which is an intset and so is mergeable.
1120    fn ints(vals: &[i64]) -> Set {
1121        let mut s = Set::new();
1122        for v in vals {
1123            s.add(v.to_string().as_bytes(), &AS_INTSET);
1124        }
1125        assert_eq!(s.encoding(), Encoding::Intset);
1126        s
1127    }
1128
1129    /// Integers from a cheap scrambler, so the sets are not runs of consecutive
1130    /// values and the cursors have something to skip over.
1131    fn scattered(n: usize, seed: i64, span: i64) -> Vec<i64> {
1132        (0..n as i64)
1133            .map(|i| (i.wrapping_add(seed).wrapping_mul(2_654_435_761)).rem_euclid(span))
1134            .collect()
1135    }
1136
1137    /// The merge and the probe are two ways to compute the same thing, so they
1138    /// have to agree member for member and in order, on every shape.
1139    ///
1140    /// The shapes matter more than the count. Two sets of the same size that
1141    /// mostly overlap is what the seek never gets to help with, a small set
1142    /// against a huge one is what it exists for, and disjoint ranges are where a
1143    /// single seek is meant to cross the whole of the other set at once.
1144    #[test]
1145    fn the_merge_and_the_probe_agree_on_every_shape() {
1146        let shapes: [(&str, Vec<Vec<i64>>); 5] = [
1147            (
1148                "same size, mostly shared",
1149                vec![scattered(4_000, 0, 5_000), scattered(4_000, 7, 5_000)],
1150            ),
1151            (
1152                "ten against a hundred thousand",
1153                vec![scattered(10, 3, 100_000), scattered(100_000, 0, 200_000)],
1154            ),
1155            (
1156                "disjoint ranges",
1157                vec![(0..2_000).collect(), (900_000..902_000).collect()],
1158            ),
1159            (
1160                "five sets",
1161                vec![
1162                    scattered(3_000, 1, 4_000),
1163                    scattered(3_000, 2, 4_000),
1164                    scattered(3_000, 3, 4_000),
1165                    scattered(3_000, 4, 4_000),
1166                    scattered(3_000, 5, 4_000),
1167                ],
1168            ),
1169            (
1170                "negatives and a member too wide for a narrow run",
1171                vec![
1172                    vec![-9_000_000_000, -3, -2, -1, 0, 1, 2, 9_000_000_000],
1173                    vec![-9_000_000_000, -2, 0, 2, 4, 9_000_000_000],
1174                ],
1175            ),
1176        ];
1177
1178        for (what, vals) in shapes {
1179            let sets: Vec<Set> = vals.iter().map(|v| ints(v)).collect();
1180            let refs: Vec<&Set> = sets.iter().collect();
1181            assert_eq!(plan_for(&refs), Plan::Merge, "{what}");
1182
1183            let probed = run(|f| inter_with(&mut Scratch::new(), Plan::Probe, &refs, 0, f));
1184            assert_eq!(
1185                run(|f| inter(&mut Scratch::new(), &refs, 0, f)),
1186                probed,
1187                "intersect {what}"
1188            );
1189            assert_eq!(
1190                run(|f| inter_with(&mut Scratch::new(), Plan::Accumulate, &refs, 0, f)),
1191                probed,
1192                "and the count agrees, {what}"
1193            );
1194
1195            let subbed = diff_the_slow_way(&vals);
1196            assert_eq!(run(|f| diff(&refs, 0, f)), subbed, "sub {what}");
1197            assert_eq!(
1198                run(|f| diff_with(Plan::Probe, &refs, 0, f)),
1199                subbed,
1200                "and the probe agrees, {what}"
1201            );
1202
1203            let mut piled: Vec<String> = union_the_slow_way(&vals);
1204            piled.sort();
1205            for how in [Plan::Merge, Plan::Probe] {
1206                let mut got = run(|f| union_with(&mut Scratch::new(), how, &refs, 0, f));
1207                got.sort();
1208                assert_eq!(got, piled, "union {what} by {how:?}");
1209            }
1210        }
1211    }
1212
1213    /// The difference worked out with a `BTreeSet`, which is the answer the
1214    /// merge has to match and shares no code with it.
1215    fn diff_the_slow_way(vals: &[Vec<i64>]) -> Vec<String> {
1216        let (first, rest) = vals.split_first().expect("not empty");
1217        let others: std::collections::BTreeSet<i64> =
1218            rest.iter().flat_map(|v| v.iter().copied()).collect();
1219        let mut left: Vec<i64> = first
1220            .iter()
1221            .copied()
1222            .filter(|v| !others.contains(v))
1223            .collect();
1224        left.sort_unstable();
1225        left.dedup();
1226        left.iter().map(i64::to_string).collect()
1227    }
1228
1229    fn union_the_slow_way(vals: &[Vec<i64>]) -> Vec<String> {
1230        let all: std::collections::BTreeSet<i64> =
1231            vals.iter().flat_map(|v| v.iter().copied()).collect();
1232        all.iter().map(i64::to_string).collect()
1233    }
1234
1235    /// A merged intersection comes back smallest first, which is the order the
1236    /// probe already produced on these operands, so a client cannot tell which
1237    /// plan ran.
1238    #[test]
1239    fn a_merged_intersection_is_ascending_and_so_was_the_probe() {
1240        let a = ints(&[900, 5, 40, 7, 1000, 3]);
1241        let b = ints(&[1000, 3, 900, 8, 5]);
1242        let got = run(|f| inter(&mut Scratch::new(), &[&a, &b], 0, f));
1243        assert_eq!(got, vec!["3", "5", "900", "1000"]);
1244        assert_eq!(
1245            run(|f| inter_with(&mut Scratch::new(), Plan::Probe, &[&a, &b], 0, f)),
1246            got
1247        );
1248    }
1249
1250    /// `SINTERCARD` stops early on the merge too, and stopping early does not
1251    /// change what it had already handed over.
1252    #[test]
1253    fn a_limit_stops_a_merged_intersection_early() {
1254        let vals: Vec<i64> = (0..2_000).collect();
1255        let a = ints(&vals);
1256        let b = ints(&vals);
1257        assert_eq!(plan_for(&[&a, &b]), Plan::Merge);
1258        assert_eq!(
1259            run(|f| inter(&mut Scratch::new(), &[&a, &b], 3, f)),
1260            vec!["0", "1", "2"]
1261        );
1262        assert_eq!(
1263            run(|f| inter(&mut Scratch::new(), &[&a, &b], 0, f)).len(),
1264            2_000
1265        );
1266        assert_eq!(
1267            run(|f| inter(&mut Scratch::new(), &[&a], 3, f)),
1268            vec!["0", "1", "2"]
1269        );
1270    }
1271
1272    /// One set that is not an intset takes the whole operation back to a probe,
1273    /// because there is nothing to walk in step with a table.
1274    #[test]
1275    fn one_unsorted_operand_takes_everything_back_to_a_probe() {
1276        let a = ints(&[1, 2, 3]);
1277        let b = tabled(&["2", "3", "4"]);
1278        assert_eq!(plan_for(&[&a, &b]), Plan::Probe);
1279        assert_eq!(
1280            run(|f| inter(&mut Scratch::new(), &[&a, &b], 0, f)),
1281            vec!["2", "3"]
1282        );
1283        // And asking for the merge anyway gets the right answer rather than a
1284        // wrong one, because the fact beats the preference.
1285        assert_eq!(
1286            run(|f| inter_with(&mut Scratch::new(), Plan::Merge, &[&a, &b], 0, f)),
1287            vec!["2", "3"]
1288        );
1289    }
1290
1291    /// A set past `set-max-intset-entries` is still an intset here, so it still
1292    /// merges. Before #148 it was a table by this size and this test would have
1293    /// been measuring the probe.
1294    #[test]
1295    fn a_set_past_the_intset_ceiling_still_merges() {
1296        let a: Set = {
1297            let mut s = Set::new();
1298            for i in 0..5_000i64 {
1299                s.add(i.to_string().as_bytes(), &Limits::DEFAULT);
1300            }
1301            s
1302        };
1303        assert_eq!(a.encoding(), Encoding::Hashtable, "the word a server uses");
1304        assert!(a.ints().is_some(), "and an intset underneath it");
1305        let b = ints(&[4_998, 4_999, 5_000]);
1306        assert_eq!(plan_for(&[&a, &b]), Plan::Merge);
1307        assert_eq!(
1308            run(|f| inter(&mut Scratch::new(), &[&a, &b], 0, f)),
1309            vec!["4998", "4999"]
1310        );
1311    }
1312
1313    /// The members are the same bytes whatever they contain, and a set holds
1314    /// arbitrary bytes rather than text.
1315    #[test]
1316    fn members_that_are_not_text_work_the_same() {
1317        let a = of([&b"\x00\xff"[..], b"\xc3\x28", b""]);
1318        let b = of([&b"\xc3\x28"[..], b""]);
1319        let mut got: Vec<Vec<u8>> = Vec::new();
1320        let n = inter(&mut Scratch::new(), &[&a, &b], 0, |m| got.push(m.to_vec()));
1321        assert_eq!(n, 2);
1322        assert_eq!(got, vec![b"\xc3\x28".to_vec(), b"".to_vec()]);
1323    }
1324}