2121namespace o2 ::framework
2222{
2323
24+ // / Select which tuples of the original grouping policy are yielded.
25+ // / IncludeEmpty preserves the existing stream, including empty associated slices.
26+ // / SkipEmpty omits tuples with any empty association. Use it only when omitting
27+ // / those tuples has no observable effect on the consumer, including counters,
28+ // / normalization, window bookkeeping, random-number streams and diagnostics.
29+ // / Window flags and neighbour counts retain their original-policy meaning: they
30+ // / are not recomputed for surviving tuples, and a window may yield no tuple.
31+ // / The grouping policy must terminate and advance without observable side effects;
32+ // / an entirely empty association can end iteration without visiting every step.
33+ enum class GroupedSlicePolicy { IncludeEmpty,
34+ SkipEmpty };
35+
36+ template <typename T>
37+ struct SupportsEmptyAssociatedGate : std::false_type {};
38+
39+ template <aod::is_aod_hash L, aod::is_aod_hash D, aod::is_origin_hash O, typename ... C>
40+ struct SupportsEmptyAssociatedGate <soa::Table<L, D, O, C...>>
41+ : std::bool_constant<O::hash != " JOIN" _h && O::hash != " CONC" _h> {};
42+
2443// Create an instance of a tuple interleaved from given tuples
2544template <typename ... T1s, typename ... T2s, std::size_t ... Is>
2645auto interleaveTuplesImpl (std::tuple<T1s...>& t1, std::tuple<T2s...>& t2, std::index_sequence<Is...>)
@@ -67,6 +86,12 @@ struct GroupedCombinationsGenerator {
6786 : GroupingPolicy(groupingPolicy), mIndexColumns {getMatchingIndexNode<G, As>()...}, cache{cache_}
6887 {
6988 }
89+ GroupedIterator (const GroupingPolicy& groupingPolicy, SliceCache* cache_, GroupedSlicePolicy slicePolicy)
90+ requires ((SupportsEmptyAssociatedGate<As>::value && ...))
91+ : GroupedIterator(groupingPolicy, cache_)
92+ {
93+ mSlicePolicy = slicePolicy;
94+ }
7095 template <typename ... T2s>
7196 GroupedIterator (const GroupingPolicy& groupingPolicy, const G& grouping, const std::tuple<T2s...>& associated, SliceCache* cache_)
7297 : GroupingPolicy(groupingPolicy),
@@ -172,8 +197,42 @@ struct GroupedCombinationsGenerator {
172197 return std::get<I>(*mAssociated ).sliceByCached (mIndexColumns [I], ind, *cache);
173198 }
174199
200+ template <std::size_t I>
201+ bool associatedHasRows () const
202+ {
203+ using A = std::tuple_element_t <I, std::tuple<As...>>;
204+ auto const & associated = std::get<I>(*mAssociated );
205+ if (associated.size () == 0 ) {
206+ return false ;
207+ }
208+ auto const & node = mIndexColumns [I];
209+ auto localCache = cache->ptr ->getCacheFor ({" " , soa::originReplacement (cache->ptr ->newOrigin )(soa::getMatcherFromTypeForKey<A>(node.name )), node.name });
210+ uint64_t index = *std::get<0 >(std::get<I>(this ->mCurrent ).getIndices ());
211+ return localCache.getSliceFor (index).second != 0 ;
212+ }
213+
214+ template <std::size_t ... I>
215+ bool allAssociatedHaveRows (std::index_sequence<I...>) const
216+ {
217+ return (associatedHasRows<I>() && ...);
218+ }
219+
175220 void setCurrentGroupedCombination ()
176221 {
222+ if constexpr ((SupportsEmptyAssociatedGate<As>::value && ...)) {
223+ if (mSlicePolicy == GroupedSlicePolicy::SkipEmpty && !this ->mIsEnd ) {
224+ // A globally empty association makes every original pair ineligible.
225+ if (std::apply ([](auto const &... tables) { return ((tables.size () == 0 ) || ...); }, *mAssociated )) {
226+ GroupingPolicy::moveToEnd ();
227+ return ;
228+ }
229+ // Advance the unchanged policy. Never remove grouping rows or refill
230+ // a window with a more distant partner after rejecting an empty pair.
231+ while (!this ->mIsEnd && !allAssociatedHaveRows (std::index_sequence_for<As...>{})) {
232+ this ->addOne ();
233+ }
234+ }
235+ }
177236 if (!this ->mIsEnd ) {
178237 auto & currentGrouping = GroupingPolicy::mCurrent ;
179238 mSlices .emplace (getAssociatedTables ());
@@ -187,6 +246,7 @@ struct GroupedCombinationsGenerator {
187246 std::optional<std::tuple<As...>> mSlices ;
188247 std::optional<GroupedIteratorType> mCurrentGrouped ;
189248 SliceCache* cache = nullptr ;
249+ GroupedSlicePolicy mSlicePolicy = GroupedSlicePolicy::IncludeEmpty;
190250 };
191251
192252 using iterator = GroupedIterator;
@@ -218,6 +278,24 @@ struct GroupedCombinationsGenerator {
218278 {
219279 setTables (grouping, associated);
220280 }
281+ // / Opt in to the tuple filter described by GroupedSlicePolicy.
282+ // / The type constraint accepts raw associated tables and excludes Join, Concat
283+ // / and filtered associations. It does not validate sorting or cache contents.
284+ // / As with sliceByCached, each matching association index must be sorted and
285+ // / have an initialized sorted SliceCache entry for the current table/dataframe.
286+ // / Grouping iterator indices must use the same coordinates as the cache keys.
287+ // / The cache and bound tables must remain valid and unchanged during iteration.
288+ // / Invalid/missing cache entries are not validated by this filter, and skipped
289+ // / typed-slice construction need not reproduce diagnostics for invalid inputs.
290+ template <typename ... T2s>
291+ GroupedCombinationsGenerator (const BP & binningPolicy, int catNeighbours, const T1 & outsider, const G& grouping, const std::tuple<T2s...>& associated, SliceCache* cache, GroupedSlicePolicy slicePolicy)
292+ requires((SupportsEmptyAssociatedGate<As>::value && ...))
293+ : mBegin(GroupingPolicy(binningPolicy, catNeighbours, outsider), cache, slicePolicy),
294+ mEnd(GroupingPolicy(binningPolicy, catNeighbours, outsider), cache, slicePolicy)
295+ {
296+ setTables (grouping, associated);
297+ }
298+
221299 GroupedCombinationsGenerator (GroupedCombinationsGenerator const &) = default;
222300 GroupedCombinationsGenerator& operator =(GroupedCombinationsGenerator const &) = default ;
223301 ~GroupedCombinationsGenerator () = default ;
0 commit comments