Skip to main content

ixa/entity/entity_set/
entity_set.rs

1//! A lazy, composable set type built from set-algebraic expressions.
2//!
3//! An [`EntitySet`] represents a set of [`EntityId`] values as a tree of union,
4//! intersection, and difference operations over leaf [`SourceSet`] nodes. The tree
5//! is constructed eagerly but evaluated lazily: membership tests ([`contains`]) walk
6//! the tree on demand, and iteration is deferred to [`EntitySetIterator`].
7//!
8//! Construction methods reorder operands by estimated size to improve
9//! short-circuit performance and apply only minimal structural simplification.
10//!
11//! [`contains`]: EntitySet::contains
12
13use std::borrow::Borrow;
14use std::ops::Range;
15
16use log::warn;
17use rand::{Rng, RngExt};
18
19use super::{EntitySetIterator, SourceSet};
20use crate::entity::{Entity, EntityId};
21#[cfg(feature = "profiling")]
22use crate::profiling::QueryProfileHandle;
23use crate::random::{
24    count_and_sample_single_l_reservoir, sample_multiple_from_known_length,
25    sample_multiple_l_reservoir, sample_single_excluding_l_reservoir, sample_single_l_reservoir,
26};
27
28/// Opaque public wrapper around the internal set-expression tree.
29pub struct EntitySet<'a, E: Entity> {
30    pub(super) inner: EntitySetInner<'a, E>,
31    #[cfg(feature = "profiling")]
32    pub(in crate::entity) query_profile: Option<QueryProfileHandle<'a>>,
33}
34
35/// Internal set-expression tree used to represent composed query sources.
36pub(super) enum EntitySetInner<'a, E: Entity> {
37    Source(SourceSet<'a, E>),
38    Union(Box<EntitySet<'a, E>>, Box<EntitySet<'a, E>>),
39    Intersection(Vec<EntitySet<'a, E>>),
40    Difference(Box<EntitySet<'a, E>>, Box<EntitySet<'a, E>>),
41}
42
43impl<'a, E: Entity> Clone for EntitySet<'a, E> {
44    fn clone(&self) -> Self {
45        Self {
46            inner: self.inner.clone(),
47            #[cfg(feature = "profiling")]
48            query_profile: self.query_profile,
49        }
50    }
51}
52
53impl<'a, E: Entity> Clone for EntitySetInner<'a, E> {
54    fn clone(&self) -> Self {
55        match self {
56            Self::Source(source) => Self::Source(source.clone()),
57            Self::Union(left, right) => Self::Union(left.clone(), right.clone()),
58            Self::Intersection(sets) => Self::Intersection(sets.clone()),
59            Self::Difference(left, right) => Self::Difference(left.clone(), right.clone()),
60        }
61    }
62}
63
64impl<'a, E: Entity> Default for EntitySet<'a, E> {
65    fn default() -> Self {
66        Self::empty()
67    }
68}
69
70impl<'a, E: Entity> EntitySet<'a, E> {
71    fn new(inner: EntitySetInner<'a, E>) -> Self {
72        Self {
73            inner,
74            #[cfg(feature = "profiling")]
75            query_profile: None,
76        }
77    }
78
79    /// Set algebra retains profiling only when all profiled operands have the
80    /// same query identity. This avoids charging a composed operation to an
81    /// arbitrary query while preserving a single identifiable query origin.
82    #[cfg(feature = "profiling")]
83    fn unique_query_profile(left: &Self, right: &Self) -> Option<QueryProfileHandle<'a>> {
84        match (left.query_profile, right.query_profile) {
85            (Some(left_profile), Some(right_profile)) if left_profile == right_profile => {
86                Some(left_profile)
87            }
88            (Some(_), Some(_)) => None,
89            (profile @ Some(_), None) | (None, profile @ Some(_)) => profile,
90            (None, None) => None,
91        }
92    }
93
94    pub(super) fn is_source_leaf(&self) -> bool {
95        matches!(self.inner, EntitySetInner::Source(_))
96    }
97
98    pub(super) fn into_source_leaf(self) -> Option<SourceSet<'a, E>> {
99        match self.inner {
100            EntitySetInner::Source(source) => Some(source),
101            _ => None,
102        }
103    }
104
105    /// Create an empty entity set.
106    #[must_use]
107    pub fn empty() -> Self {
108        Self::new(EntitySetInner::Source(SourceSet::empty()))
109    }
110
111    /// Create an entity set from a single source set.
112    pub(crate) fn from_source(source: SourceSet<'a, E>) -> Self {
113        Self::new(EntitySetInner::Source(source))
114    }
115
116    pub(crate) fn from_intersection_sources(mut sources: Vec<SourceSet<'a, E>>) -> Self {
117        match sources.len() {
118            0 => return Self::empty(),
119            1 => return Self::from_source(sources.pop().unwrap()),
120            _ => {}
121        }
122
123        // Keep intersections sorted smallest-to-largest so iterators can take the
124        // first source as the driver and membership checks short-circuit quickly.
125        sources.sort_unstable_by_key(SourceSet::sort_key);
126
127        let sets = sources.into_iter().map(Self::from_source).collect();
128
129        Self::new(EntitySetInner::Intersection(sets))
130    }
131
132    #[cfg(feature = "profiling")]
133    #[inline]
134    pub(in crate::entity) fn with_query_profile(mut self, profile: QueryProfileHandle<'a>) -> Self {
135        self.query_profile = Some(profile);
136        self
137    }
138
139    #[must_use]
140    pub fn union(self, other: Self) -> Self {
141        // Mutation only happens for profiling feature.
142        #![allow(unused_mut)]
143        let mut left = self;
144        let mut right = other;
145        #[cfg(feature = "profiling")]
146        let query_profile = Self::unique_query_profile(&left, &right);
147        #[cfg(feature = "profiling")]
148        {
149            left.query_profile = None;
150            right.query_profile = None;
151        }
152
153        let mut result = 'result: {
154            // Idempotence: A ∪ A = A  (same structure over same sources)
155            if left.structurally_eq(&right) {
156                break 'result left;
157            }
158
159            // Adjacent or overlapping intervals
160            if let (Some(a), Some(b)) = (left.as_range(), right.as_range()) {
161                if a.start <= b.end && b.start <= a.end {
162                    break 'result Self::from_source(SourceSet::population_range(
163                        a.start.min(b.start)..a.end.max(b.end),
164                    ));
165                }
166            }
167
168            // Union with empty set is identity: A ∪ ∅ = ∅ ∪ A = A
169            match (left.is_empty(), right.is_empty()) {
170                (true, _) => break 'result right,
171                (_, true) => break 'result left,
172                _ => { /* pass */ }
173            }
174
175            // Larger set on LHS: more likely to short-circuit `||`.
176            let (left, right) = if left.sort_key() >= right.sort_key() {
177                (left, right)
178            } else {
179                (right, left)
180            };
181            break 'result Self::new(EntitySetInner::Union(Box::new(left), Box::new(right)));
182        };
183        #[cfg(feature = "profiling")]
184        {
185            result.query_profile = query_profile;
186        }
187        result
188    }
189
190    #[must_use]
191    pub fn intersection(self, other: Self) -> Self {
192        // Mutation only happens for profiling feature.
193        #![allow(unused_mut)]
194        let mut left = self;
195        let mut right = other;
196        #[cfg(feature = "profiling")]
197        let query_profile = Self::unique_query_profile(&left, &right);
198        #[cfg(feature = "profiling")]
199        {
200            left.query_profile = None;
201            right.query_profile = None;
202        }
203
204        let mut result = 'result: {
205            // Idempotence: A ∩ A = A
206            if left.structurally_eq(&right) {
207                break 'result left;
208            }
209
210            // Intersection of overlapping intervals
211            if let (Some(a), Some(b)) = (left.as_range(), right.as_range()) {
212                let overlap = a.start.max(b.start)..a.end.min(b.end);
213                break 'result if overlap.is_empty() {
214                    Self::empty()
215                } else {
216                    Self::from_source(SourceSet::population_range(overlap))
217                };
218            }
219
220            // Intersection an empty set is empty: A ∩ ∅ = ∅ ∩ A = ∅
221            match (left.is_empty(), right.is_empty()) {
222                (true, _) => break 'result left,
223                (_, true) => break 'result right,
224                _ => { /* pass */ }
225            }
226
227            let mut sets = match left {
228                EntitySet {
229                    inner: EntitySetInner::Intersection(sets),
230                    ..
231                } => sets,
232                _ => vec![left],
233            };
234
235            sets.push(right);
236            // Keep intersections sorted smallest-to-largest so iterators can take the
237            // first source as the driver and membership checks short-circuit quickly.
238            sets.sort_unstable_by_key(EntitySet::sort_key);
239            break 'result Self::new(EntitySetInner::Intersection(sets));
240        };
241        #[cfg(feature = "profiling")]
242        {
243            result.query_profile = query_profile;
244        }
245        result
246    }
247
248    #[must_use]
249    pub fn difference(self, other: Self) -> Self {
250        // Mutation only happens for profiling feature.
251        #![allow(unused_mut)]
252        let mut left = self;
253        let mut right = other;
254        #[cfg(feature = "profiling")]
255        let query_profile = Self::unique_query_profile(&left, &right);
256        #[cfg(feature = "profiling")]
257        {
258            left.query_profile = None;
259            right.query_profile = None;
260        }
261
262        let mut result = 'result: {
263            // Self-subtraction: A \ A = ∅
264            if left.structurally_eq(&right) {
265                break 'result Self::empty();
266            }
267
268            if let (Some(a), Some(b)) = (left.as_range(), right.as_range()) {
269                let overlap = a.start.max(b.start)..a.end.min(b.end);
270                // Disjoint ranges leave the left operand unchanged.
271                if overlap.is_empty() {
272                    break 'result Self::from_source(SourceSet::population_range(a));
273                }
274                // A covering subtraction removes the entire left range.
275                if overlap.start == a.start && overlap.end == a.end {
276                    break 'result Self::empty();
277                }
278                // Trimming the left edge still leaves one contiguous suffix.
279                if overlap.start == a.start {
280                    break 'result Self::from_source(SourceSet::population_range(
281                        overlap.end..a.end,
282                    ));
283                }
284                // Trimming the right edge still leaves one contiguous prefix.
285                if overlap.end == a.end {
286                    break 'result Self::from_source(SourceSet::population_range(
287                        a.start..overlap.start,
288                    ));
289                }
290                // An interior subtraction would split the range, so keep the generic difference node.
291            }
292
293            // Subtraction involving an empty set is identity: A \ ∅ = A, ∅ \ A = ∅
294            if left.is_empty() || right.is_empty() {
295                break 'result left;
296            }
297
298            break 'result Self::new(EntitySetInner::Difference(Box::new(left), Box::new(right)));
299        };
300        #[cfg(feature = "profiling")]
301        {
302            result.query_profile = query_profile;
303        }
304        result
305    }
306
307    /// Test whether `entity_id` is a member of this set.
308    #[must_use]
309    pub fn contains(&self, entity_id: EntityId<E>) -> bool {
310        #[cfg(feature = "profiling")]
311        let _query_profile_scope = self.query_profile.map(QueryProfileHandle::scope);
312        self.contains_impl(entity_id)
313    }
314
315    fn contains_impl(&self, entity_id: EntityId<E>) -> bool {
316        match &self.inner {
317            EntitySetInner::Source(source) => source.contains(entity_id),
318            EntitySetInner::Union(a, b) => a.contains_impl(entity_id) || b.contains_impl(entity_id),
319            EntitySetInner::Intersection(sets) => {
320                sets.iter().all(|set| set.contains_impl(entity_id))
321            }
322            EntitySetInner::Difference(a, b) => {
323                a.contains_impl(entity_id) && !b.contains_impl(entity_id)
324            }
325        }
326    }
327
328    /// Collect this set's contents into an owned vector of `EntityId<E>`.
329    #[must_use]
330    #[allow(unused_mut)]
331    pub fn to_owned_vec(mut self) -> Vec<EntityId<E>> {
332        #[cfg(feature = "profiling")]
333        let _query_profile_scope = self.query_profile.take().map(QueryProfileHandle::scope);
334
335        self.into_iter().collect()
336    }
337
338    /// Sample a single entity uniformly from this set, excluding any entity
339    /// equal to `excluded`. Returns `None` if the set is empty or contains
340    /// only the excluded entity.
341    ///
342    /// For source-leaf sets with random-access backing (`PopulationRange`,
343    /// `IndexSet`), runs in O(1) with at most two index lookups and no
344    /// iterator construction. Falls back to O(n) reservoir sampling for
345    /// composite sets and `PropertySet` sources.
346    #[must_use]
347    pub fn sample_entity_excluding<R, X>(&self, rng: &mut R, excluded: X) -> Option<EntityId<E>>
348    where
349        R: Rng,
350        X: Borrow<EntityId<E>>,
351    {
352        #[cfg(feature = "profiling")]
353        let _query_profile_scope = self.query_profile.map(QueryProfileHandle::scope);
354
355        let excluded = *excluded.borrow();
356        if let Some(n) = self.try_len_impl() {
357            if n == 0 {
358                None
359            } else {
360                let p = rng.random_range(0..n as u32) as usize;
361                match self.try_nth(p) {
362                    Some(candidate) if candidate != excluded => Some(candidate),
363                    Some(_) if n == 1 => None,
364                    Some(_) => {
365                        // `excluded` is at position `p`. Resample from the n-1 remaining
366                        // positions: pick `k` uniform in `[0, n-1)`, then map it around
367                        // the hole at `p`.
368                        let k = rng.random_range(0..(n - 1) as u32) as usize;
369                        let target = if k < p { k } else { k + 1 };
370                        self.try_nth(target)
371                    }
372                    None => None,
373                }
374            }
375        } else {
376            #[allow(unused_mut)]
377            let mut set = self.clone();
378            #[cfg(feature = "profiling")]
379            {
380                set.query_profile = None;
381            }
382            sample_single_excluding_l_reservoir(rng, set, excluded)
383        }
384    }
385
386    /// Sample a single entity uniformly from this set. Returns `None` if the
387    /// set is empty.
388    #[must_use]
389    #[inline]
390    pub fn sample_entity<R>(&self, rng: &mut R) -> Option<EntityId<E>>
391    where
392        R: Rng,
393    {
394        #[cfg(feature = "profiling")]
395        let _query_profile_scope = self.query_profile.map(QueryProfileHandle::scope);
396
397        if let Some(n) = self.try_len_impl() {
398            if n == 0 {
399                warn!("Requested a sample entity from an empty population");
400                return None;
401            }
402            // The `u32` cast makes this function 30% faster than `usize`.
403            let index = rng.random_range(0..n as u32) as usize;
404            return self.try_nth(index);
405        }
406        #[allow(unused_mut)]
407        let mut set = self.clone();
408        #[cfg(feature = "profiling")]
409        {
410            set.query_profile = None;
411        }
412        sample_single_l_reservoir(rng, set)
413    }
414
415    /// Count the entities in this set and sample one uniformly from them.
416    ///
417    /// Returns `(count, sample)` where `sample` is `None` iff `count == 0`.
418    #[must_use]
419    pub fn count_and_sample_entity<R>(&self, rng: &mut R) -> (usize, Option<EntityId<E>>)
420    where
421        R: Rng,
422    {
423        #[cfg(feature = "profiling")]
424        let _query_profile_scope = self.query_profile.map(QueryProfileHandle::scope);
425
426        if let Some(n) = self.try_len_impl() {
427            if n == 0 {
428                return (0, None);
429            }
430            let index = rng.random_range(0..n as u32) as usize;
431            return (n, self.try_nth(index));
432        }
433        #[allow(unused_mut)]
434        let mut set = self.clone();
435        #[cfg(feature = "profiling")]
436        {
437            set.query_profile = None;
438        }
439        count_and_sample_single_l_reservoir(rng, set)
440    }
441
442    /// Sample up to `requested` entities uniformly from this set. If the set
443    /// has fewer than `requested` entities, the entire set is returned.
444    #[must_use]
445    pub fn sample_entities<R>(&self, rng: &mut R, requested: usize) -> Vec<EntityId<E>>
446    where
447        R: Rng,
448    {
449        #[cfg(feature = "profiling")]
450        let _query_profile_scope = self.query_profile.map(QueryProfileHandle::scope);
451
452        match self.try_len_impl() {
453            Some(0) => {
454                warn!("Requested a sample of entities from an empty population");
455                vec![]
456            }
457            Some(_) => {
458                #[allow(unused_mut)]
459                let mut set = self.clone();
460                #[cfg(feature = "profiling")]
461                {
462                    set.query_profile = None;
463                }
464                sample_multiple_from_known_length(rng, set, requested)
465            }
466            None => {
467                #[allow(unused_mut)]
468                let mut set = self.clone();
469                #[cfg(feature = "profiling")]
470                {
471                    set.query_profile = None;
472                }
473                sample_multiple_l_reservoir(rng, set, requested)
474            }
475        }
476    }
477
478    /// Returns `Some(length)` only when the set length is trivially known.
479    ///
480    /// This is true only for direct `SourceSet` leaves except `PropertySet`.
481    /// Composite expressions return `None`.
482    #[must_use]
483    pub fn try_len(&self) -> Option<usize> {
484        #[cfg(feature = "profiling")]
485        let _query_profile_scope = self.query_profile.map(QueryProfileHandle::scope);
486        self.try_len_impl()
487    }
488
489    fn try_len_impl(&self) -> Option<usize> {
490        match &self.inner {
491            EntitySetInner::Source(source) => source.try_len(),
492            _ => None,
493        }
494    }
495
496    fn try_nth(&self, idx: usize) -> Option<EntityId<E>> {
497        match &self.inner {
498            EntitySetInner::Source(source) => source.try_nth(idx),
499            _ => None,
500        }
501    }
502
503    fn as_range(&self) -> Option<Range<usize>> {
504        match &self.inner {
505            EntitySetInner::Source(SourceSet::PopulationRange(range)) => Some(range.clone()),
506            _ => None,
507        }
508    }
509
510    fn is_empty(&self) -> bool {
511        match &self.inner {
512            EntitySetInner::Source(SourceSet::PopulationRange(range)) => range.is_empty(),
513            _ => false,
514        }
515    }
516
517    fn sort_key(&self) -> (usize, u8) {
518        match &self.inner {
519            EntitySetInner::Source(source) => source.sort_key(),
520            EntitySetInner::Union(left, right) => {
521                // Union upper bound is additive; cost hint tracks the cheaper side.
522                let (left_upper, left_hint) = left.sort_key();
523                let (right_upper, right_hint) = right.sort_key();
524                (
525                    left_upper.saturating_add(right_upper),
526                    left_hint.min(right_hint),
527                )
528            }
529            EntitySetInner::Intersection(sets) => {
530                let mut upper = usize::MAX;
531                let mut hint = 0u8;
532                for set in sets {
533                    let (set_upper, set_hint) = set.sort_key();
534                    upper = upper.min(set_upper);
535                    hint = hint.saturating_add(set_hint);
536                }
537                if upper == usize::MAX {
538                    upper = 0;
539                }
540                (upper, hint)
541            }
542            EntitySetInner::Difference(left, right) => {
543                let (left_upper, left_hint) = left.sort_key();
544                let (_, right_hint) = right.sort_key();
545                (left_upper, left_hint.saturating_add(right_hint))
546            }
547        }
548    }
549
550    /// Structural equality check: same tree shape with same sources at leaves.
551    fn structurally_eq(&self, other: &Self) -> bool {
552        match (&self.inner, &other.inner) {
553            (EntitySetInner::Source(a), EntitySetInner::Source(b)) => a == b,
554            (EntitySetInner::Union(a1, a2), EntitySetInner::Union(b1, b2))
555            | (EntitySetInner::Difference(a1, a2), EntitySetInner::Difference(b1, b2)) => {
556                a1.structurally_eq(b1) && a2.structurally_eq(b2)
557            }
558            (EntitySetInner::Intersection(a_sets), EntitySetInner::Intersection(b_sets)) => {
559                a_sets.len() == b_sets.len()
560                    && a_sets
561                        .iter()
562                        .zip(b_sets.iter())
563                        .all(|(a_set, b_set)| a_set.structurally_eq(b_set))
564            }
565            _ => false,
566        }
567    }
568}
569
570impl<'a, E: Entity> IntoIterator for EntitySet<'a, E> {
571    type Item = EntityId<E>;
572    type IntoIter = EntitySetIterator<'a, E>;
573
574    fn into_iter(self) -> Self::IntoIter {
575        EntitySetIterator::new(self)
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use rand::rngs::StdRng;
582    use rand::SeedableRng;
583
584    use super::*;
585    use crate::entity::ContextEntitiesExt;
586    use crate::hashing::IndexSet;
587    use crate::{
588        define_derived_property, define_entity, define_multi_property, define_property, with,
589        Context,
590    };
591
592    define_entity!(Person);
593    define_property!(struct Age(u8), Person);
594    define_property!(struct County(u32), Person, default_const = County(0));
595    define_derived_property!(struct Senior(bool), Person, [Age], |age| Senior(age.0 >= 65));
596    define_multi_property!(Person, (Age, County));
597    define_multi_property!(Person, (County, Age));
598
599    fn finite_set(ids: &[usize]) -> IndexSet<EntityId<Person>> {
600        ids.iter()
601            .copied()
602            .map(EntityId::<Person>::new)
603            .collect::<IndexSet<_>>()
604    }
605
606    fn as_entity_set(set: &IndexSet<EntityId<Person>>) -> EntitySet<Person> {
607        EntitySet::from_source(SourceSet::IndexSet(set))
608    }
609
610    #[test]
611    fn from_source_empty_is_empty() {
612        let es = EntitySet::<Person>::empty();
613        assert_eq!(es.sort_key().0, 0);
614        for value in 0..10 {
615            assert!(!es.contains(EntityId::<Person>::new(value)));
616        }
617    }
618
619    #[test]
620    fn from_source_population_ranges() {
621        let population = EntitySet::from_source(SourceSet::<Person>::population_range(0..3));
622        assert!(population.contains(EntityId::<Person>::new(0)));
623        assert!(population.contains(EntityId::<Person>::new(2)));
624        assert!(!population.contains(EntityId::<Person>::new(3)));
625        assert_eq!(population.sort_key().0, 3);
626
627        let singleton = EntitySet::from_source(SourceSet::<Person>::singleton(EntityId::new(5)));
628        assert!(singleton.contains(EntityId::<Person>::new(5)));
629        assert!(!singleton.contains(EntityId::<Person>::new(4)));
630        assert_eq!(singleton.sort_key().0, 1);
631
632        let range = EntitySet::from_source(SourceSet::<Person>::population_range(2..5));
633        assert!(range.contains(EntityId::<Person>::new(2)));
634        assert!(range.contains(EntityId::<Person>::new(4)));
635        assert!(!range.contains(EntityId::<Person>::new(1)));
636        assert!(!range.contains(EntityId::<Person>::new(5)));
637        assert_eq!(range.try_len(), Some(3));
638    }
639
640    #[test]
641    fn union_basic_behavior_without_legacy_reductions() {
642        let a = finite_set(&[1, 2, 3]);
643        let e = EntitySet::<Person>::empty();
644        let u = EntitySet::from_source(SourceSet::<Person>::population_range(0..10));
645
646        let a_union_empty = as_entity_set(&a).union(e);
647        assert!(a_union_empty.contains(EntityId::<Person>::new(1)));
648        assert!(!a_union_empty.contains(EntityId::<Person>::new(4)));
649
650        let u_union_a = u.union(as_entity_set(&a));
651        assert!(u_union_a.contains(EntityId::<Person>::new(0)));
652        assert!(u_union_a.contains(EntityId::<Person>::new(9)));
653        assert!(!u_union_a.contains(EntityId::<Person>::new(10)));
654    }
655
656    #[test]
657    fn union_range_optimizations() {
658        let adjacent = EntitySet::from_source(SourceSet::<Person>::population_range(0..3)).union(
659            EntitySet::from_source(SourceSet::<Person>::population_range(3..6)),
660        );
661        assert!(matches!(
662            adjacent,
663            EntitySet { inner: EntitySetInner::Source(SourceSet::PopulationRange(ref range)), .. } if range == &(0..6)
664        ));
665
666        let overlapping = EntitySet::from_source(SourceSet::<Person>::population_range(2..6))
667            .union(EntitySet::from_source(
668                SourceSet::<Person>::population_range(4..8),
669            ));
670        assert!(matches!(
671            overlapping,
672            EntitySet { inner: EntitySetInner::Source(SourceSet::PopulationRange(ref range)), .. } if range == &(2..8)
673        ));
674
675        let disjoint = EntitySet::from_source(SourceSet::<Person>::singleton(EntityId::new(1)))
676            .union(EntitySet::from_source(SourceSet::<Person>::singleton(
677                EntityId::new(4),
678            )));
679        assert!(matches!(
680            disjoint,
681            EntitySet {
682                inner: EntitySetInner::Union(_, _),
683                ..
684            }
685        ));
686    }
687
688    #[test]
689    fn intersection_range_optimizations() {
690        let overlap = EntitySet::from_source(SourceSet::<Person>::population_range(2..6))
691            .intersection(EntitySet::from_source(
692                SourceSet::<Person>::population_range(4..8),
693            ));
694        assert!(matches!(
695            overlap,
696            EntitySet { inner: EntitySetInner::Source(SourceSet::PopulationRange(ref range)), .. } if range == &(4..6)
697        ));
698
699        let empty = EntitySet::from_source(SourceSet::<Person>::population_range(1..3))
700            .intersection(EntitySet::from_source(
701                SourceSet::<Person>::population_range(5..7),
702            ));
703        assert!(matches!(
704            empty,
705            EntitySet { inner: EntitySetInner::Source(SourceSet::PopulationRange(ref range)), .. } if range == &(0..0)
706        ));
707
708        let indexed_ids = finite_set(&[1, 2, 3]);
709        let mixed = EntitySet::from_source(SourceSet::<Person>::singleton(EntityId::new(2)))
710            .intersection(as_entity_set(&indexed_ids));
711        assert!(mixed.contains(EntityId::<Person>::new(2)));
712        assert!(!mixed.contains(EntityId::<Person>::new(1)));
713    }
714
715    #[test]
716    fn difference_range_optimizations() {
717        let unchanged = EntitySet::from_source(SourceSet::<Person>::population_range(2..6))
718            .difference(EntitySet::from_source(
719                SourceSet::<Person>::population_range(8..10),
720            ));
721        assert!(matches!(
722            unchanged,
723            EntitySet { inner: EntitySetInner::Source(SourceSet::PopulationRange(ref range)), .. } if range == &(2..6)
724        ));
725
726        let empty = EntitySet::from_source(SourceSet::<Person>::population_range(2..6)).difference(
727            EntitySet::from_source(SourceSet::<Person>::population_range(1..7)),
728        );
729        assert!(matches!(
730            empty,
731            EntitySet { inner: EntitySetInner::Source(SourceSet::PopulationRange(ref range)), .. } if range == &(0..0)
732        ));
733
734        let trim_left = EntitySet::from_source(SourceSet::<Person>::population_range(2..6))
735            .difference(EntitySet::from_source(
736                SourceSet::<Person>::population_range(1..4),
737            ));
738        assert!(matches!(
739            trim_left,
740            EntitySet { inner: EntitySetInner::Source(SourceSet::PopulationRange(ref range)), .. } if range == &(4..6)
741        ));
742
743        let trim_right = EntitySet::from_source(SourceSet::<Person>::population_range(2..6))
744            .difference(EntitySet::from_source(
745                SourceSet::<Person>::population_range(4..8),
746            ));
747        assert!(matches!(
748            trim_right,
749            EntitySet { inner: EntitySetInner::Source(SourceSet::PopulationRange(ref range)), .. } if range == &(2..4)
750        ));
751
752        let split = EntitySet::from_source(SourceSet::<Person>::population_range(2..8)).difference(
753            EntitySet::from_source(SourceSet::<Person>::population_range(4..6)),
754        );
755        assert!(matches!(
756            split,
757            EntitySet {
758                inner: EntitySetInner::Difference(_, _),
759                ..
760            }
761        ));
762        assert!(split.contains(EntityId::<Person>::new(2)));
763        assert!(split.contains(EntityId::<Person>::new(7)));
764        assert!(!split.contains(EntityId::<Person>::new(4)));
765        assert!(!split.contains(EntityId::<Person>::new(5)));
766    }
767
768    #[test]
769    fn difference_is_not_commutative() {
770        let a = finite_set(&[1, 2, 3]);
771        let b = finite_set(&[2, 3, 4]);
772
773        let d1 = as_entity_set(&a).difference(as_entity_set(&b));
774        let c = finite_set(&[2, 3, 4]);
775        let d = finite_set(&[1, 2, 3]);
776        let d2 = as_entity_set(&c).difference(as_entity_set(&d));
777
778        assert!(d1.contains(EntityId::<Person>::new(1)));
779        assert!(!d1.contains(EntityId::<Person>::new(4)));
780        assert!(d2.contains(EntityId::<Person>::new(4)));
781        assert!(!d2.contains(EntityId::<Person>::new(1)));
782    }
783
784    #[test]
785    fn sort_key_rules() {
786        let a = finite_set(&[1, 2]);
787        let b = finite_set(&[2, 3, 4]);
788
789        let union = as_entity_set(&a).union(as_entity_set(&b));
790        assert_eq!(union.sort_key(), (a.len() + b.len(), 3));
791
792        let intersection = as_entity_set(&a).intersection(as_entity_set(&b));
793        assert_eq!(intersection.sort_key(), (a.len().min(b.len()), 6));
794
795        let difference = as_entity_set(&a).difference(as_entity_set(&b));
796        assert_eq!(difference.sort_key(), (a.len(), 6));
797    }
798
799    #[test]
800    fn compound_expressions_membership() {
801        let a = finite_set(&[1, 2, 3, 4]);
802        let b = finite_set(&[3, 4, 5]);
803        let c = finite_set(&[10, 20]);
804        let d = finite_set(&[20]);
805
806        let union_of_intersections = as_entity_set(&a)
807            .intersection(as_entity_set(&b))
808            .union(as_entity_set(&c).intersection(as_entity_set(&d)));
809        assert!(union_of_intersections.contains(EntityId::<Person>::new(3)));
810        assert!(union_of_intersections.contains(EntityId::<Person>::new(4)));
811        assert!(union_of_intersections.contains(EntityId::<Person>::new(20)));
812        assert!(!union_of_intersections.contains(EntityId::<Person>::new(5)));
813
814        let a2 = finite_set(&[1, 2, 3]);
815        let b2 = finite_set(&[3, 4, 5]);
816        let a3 = finite_set(&[1, 2, 3]);
817        let law = as_entity_set(&a3).intersection(as_entity_set(&a2).union(as_entity_set(&b2)));
818        assert!(law.contains(EntityId::<Person>::new(1)));
819        assert!(law.contains(EntityId::<Person>::new(2)));
820        assert!(law.contains(EntityId::<Person>::new(3)));
821        assert!(!law.contains(EntityId::<Person>::new(4)));
822    }
823
824    #[test]
825    fn clone_preserves_composite_expression_behavior() {
826        let a = finite_set(&[1, 2, 3, 4]);
827        let b = finite_set(&[3, 4, 5]);
828        let c = finite_set(&[2]);
829
830        let set = as_entity_set(&a)
831            .difference(as_entity_set(&c))
832            .union(as_entity_set(&b));
833        let cloned = set.clone();
834
835        for value in 0..7 {
836            let entity_id = EntityId::<Person>::new(value);
837            assert_eq!(set.contains(entity_id), cloned.contains(entity_id));
838        }
839
840        assert_eq!(
841            set.into_iter().collect::<Vec<_>>(),
842            cloned.into_iter().collect::<Vec<_>>()
843        );
844    }
845
846    #[test]
847    fn population_zero_is_empty() {
848        let es = EntitySet::from_source(SourceSet::<Person>::empty());
849        assert_eq!(es.sort_key().0, 0);
850        assert!(!es.contains(EntityId::<Person>::new(0)));
851    }
852
853    #[test]
854    fn try_len_known_only_for_non_property_sources() {
855        let empty = EntitySet::<Person>::from_source(SourceSet::empty());
856        assert_eq!(empty.try_len(), Some(0));
857
858        let singleton = EntitySet::<Person>::from_source(SourceSet::singleton(EntityId::new(42)));
859        assert_eq!(singleton.try_len(), Some(1));
860
861        let population = EntitySet::<Person>::from_source(SourceSet::population_range(0..5));
862        assert_eq!(population.try_len(), Some(5));
863
864        let range = EntitySet::<Person>::from_source(SourceSet::population_range(4..9));
865        assert_eq!(range.try_len(), Some(5));
866
867        let index_data = [EntityId::new(1), EntityId::new(2), EntityId::new(3)]
868            .into_iter()
869            .collect::<IndexSet<_>>();
870        let indexed = EntitySet::<Person>::from_source(SourceSet::IndexSet(&index_data));
871        assert_eq!(indexed.try_len(), Some(3));
872
873        let mut context = Context::new();
874        context.add_entity(with!(Person, Age(10))).unwrap();
875        let property_source = SourceSet::<Person>::new(Age(10), &context).unwrap();
876        assert!(matches!(property_source, SourceSet::PropertySet(_)));
877        let property_set = EntitySet::<Person>::from_source(property_source);
878        assert_eq!(property_set.try_len(), None);
879
880        let composed = EntitySet::<Person>::from_source(SourceSet::population_range(0..3))
881            .difference(EntitySet::from_source(SourceSet::singleton(EntityId::new(
882                1,
883            ))));
884        assert_eq!(composed.try_len(), None);
885    }
886
887    #[test]
888    fn range_leaf_works_inside_composite_expressions() {
889        let indexed_ids = finite_set(&[1, 3, 5, 8]);
890        let indexed = as_entity_set(&indexed_ids);
891        let range = EntitySet::from_source(SourceSet::<Person>::population_range(2..8));
892
893        let intersection = range.intersection(indexed);
894        assert!(!intersection.contains(EntityId::new(1)));
895        assert!(intersection.contains(EntityId::new(3)));
896        assert!(intersection.contains(EntityId::new(5)));
897        assert!(!intersection.contains(EntityId::new(8)));
898    }
899
900    #[test]
901    fn clone_preserves_unindexed_concrete_property_query_results() {
902        let mut context = Context::new();
903        let p1 = context.add_entity(with!(Person, Age(10))).unwrap();
904        let p2 = context.add_entity(with!(Person, Age(10))).unwrap();
905        let _p3 = context.add_entity(with!(Person, Age(11))).unwrap();
906
907        let set = context.query::<Person, _>(with!(Person, Age(10)));
908        assert_eq!(set.try_len(), None);
909        let cloned = set.clone();
910
911        let mut iter = set.into_iter();
912        assert_eq!(iter.next(), Some(p1));
913        assert_eq!(iter.collect::<Vec<_>>(), vec![p2]);
914
915        assert!(cloned.contains(p1));
916        assert!(cloned.contains(p2));
917        assert_eq!(cloned.into_iter().collect::<Vec<_>>(), vec![p1, p2]);
918    }
919
920    #[test]
921    fn clone_preserves_unindexed_derived_property_query_results() {
922        let mut context = Context::new();
923        let _p1 = context.add_entity(with!(Person, Age(64))).unwrap();
924        let p2 = context.add_entity(with!(Person, Age(65))).unwrap();
925        let p3 = context.add_entity(with!(Person, Age(90))).unwrap();
926
927        let set = context.query::<Person, _>(with!(Person, Senior(true)));
928        assert_eq!(set.try_len(), None);
929        let cloned = set.clone();
930
931        assert!(set.contains(p2));
932        assert!(set.contains(p3));
933        assert_eq!(set.into_iter().collect::<Vec<_>>(), vec![p2, p3]);
934        assert_eq!(cloned.into_iter().collect::<Vec<_>>(), vec![p2, p3]);
935    }
936
937    #[test]
938    fn union_of_same_unindexed_property_query_deduplicates_to_one_source() {
939        let mut context = Context::new();
940        let p1 = context.add_entity(with!(Person, Age(10))).unwrap();
941        let p2 = context.add_entity(with!(Person, Age(10))).unwrap();
942        let _p3 = context.add_entity(with!(Person, Age(11))).unwrap();
943
944        let query = context.query::<Person, _>(with!(Person, Age(10)));
945        let union = query.clone().union(query);
946
947        assert!(matches!(
948            union,
949            EntitySet {
950                inner: EntitySetInner::Source(SourceSet::PropertySet(_)),
951                ..
952            }
953        ));
954        assert_eq!(union.into_iter().collect::<Vec<_>>(), vec![p1, p2]);
955    }
956
957    #[test]
958    fn union_of_equivalent_unindexed_multi_property_queries_deduplicates_to_one_source() {
959        let mut context = Context::new();
960        let matching = context
961            .add_entity(with!(Person, Age(28), County(7)))
962            .unwrap();
963        let _wrong_county = context
964            .add_entity(with!(Person, Age(28), County(8)))
965            .unwrap();
966        let _wrong_age = context
967            .add_entity(with!(Person, Age(29), County(7)))
968            .unwrap();
969
970        let age_county = context.query::<Person, _>(with!(Person, (Age(28), County(7))));
971        let county_age = context.query::<Person, _>(with!(Person, (County(7), Age(28))));
972        let union = age_county.union(county_age);
973
974        assert!(matches!(
975            union,
976            EntitySet {
977                inner: EntitySetInner::Source(SourceSet::PropertySet(_)),
978                ..
979            }
980        ));
981        assert_eq!(union.into_iter().collect::<Vec<_>>(), vec![matching]);
982    }
983
984    #[test]
985    fn sample_entity_excluding_skips_excluded() {
986        let set = EntitySet::from_source(SourceSet::<Person>::PopulationRange(0..5));
987        let excluded = EntityId::<Person>::new(2);
988        let mut rng = StdRng::seed_from_u64(42);
989        for _ in 0..200 {
990            let sampled = set.sample_entity_excluding(&mut rng, excluded).unwrap();
991            assert_ne!(sampled, excluded);
992            assert!(sampled.0 < 5);
993        }
994    }
995
996    #[test]
997    fn sample_entity_excluding_returns_none_when_only_excluded_present() {
998        let only = EntityId::<Person>::new(7);
999        let single = finite_set(&[7]);
1000        let mut rng = StdRng::seed_from_u64(42);
1001        assert_eq!(
1002            as_entity_set(&single).sample_entity_excluding(&mut rng, only),
1003            None
1004        );
1005    }
1006
1007    #[test]
1008    fn sample_entity_excluding_returns_none_on_empty() {
1009        let mut rng = StdRng::seed_from_u64(42);
1010        assert_eq!(
1011            EntitySet::<Person>::empty()
1012                .sample_entity_excluding(&mut rng, EntityId::<Person>::new(0)),
1013            None
1014        );
1015    }
1016
1017    #[test]
1018    fn sample_entity_excluding_excluded_not_in_set_uses_first_pick() {
1019        // When `excluded` is outside the set, every sample is the first
1020        // uniform pick — exercises the no-resample path.
1021        let set = EntitySet::from_source(SourceSet::<Person>::PopulationRange(0..10));
1022        let mut rng = StdRng::seed_from_u64(42);
1023        for _ in 0..200 {
1024            let sampled = set
1025                .sample_entity_excluding(&mut rng, EntityId::<Person>::new(999))
1026                .unwrap();
1027            assert!(sampled.0 < 10);
1028        }
1029    }
1030
1031    #[test]
1032    fn sample_entity_excluding_uniform_over_known_length() {
1033        // Chi-square test on PopulationRange (known-length, fast path).
1034        let excluded = EntityId::<Person>::new(7);
1035        let set = EntitySet::from_source(SourceSet::<Person>::PopulationRange(0..20));
1036        let num_runs = 50_000;
1037        let mut counts = [0usize; 20];
1038        let mut rng = StdRng::seed_from_u64(42);
1039        for _ in 0..num_runs {
1040            let id = set.sample_entity_excluding(&mut rng, excluded).unwrap();
1041            counts[id.0] += 1;
1042        }
1043        assert_eq!(counts[excluded.0], 0);
1044
1045        let expected = num_runs as f64 / 19.0;
1046        let chi_square: f64 = counts
1047            .iter()
1048            .enumerate()
1049            .filter(|(i, _)| *i != excluded.0)
1050            .map(|(_, &obs)| {
1051                let diff = obs as f64 - expected;
1052                diff * diff / expected
1053            })
1054            .sum();
1055        // df = 18, χ²_{0.999} ≈ 42.31
1056        assert!(chi_square < 42.31, "χ² = {chi_square}");
1057    }
1058}