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}