hydro_lang/live_collections/keyed_singleton.rs
1//! Definitions for the [`KeyedSingleton`] live collection.
2
3use std::cell::RefCell;
4use std::collections::HashMap;
5use std::hash::Hash;
6use std::marker::PhantomData;
7use std::ops::Deref;
8use std::rc::Rc;
9
10use sealed::sealed;
11use stageleft::{IntoQuotedMut, QuotedWithContext, q};
12
13use super::boundedness::{Bounded, Boundedness, IsBounded, Unbounded};
14use super::keyed_stream::KeyedStream;
15use super::optional::Optional;
16use super::singleton::Singleton;
17use super::sliced::sliced;
18use super::stream::{ExactlyOnce, NoOrder, Stream, TotalOrder};
19use crate::compile::builder::{CycleId, FlowState};
20use crate::compile::ir::{
21 CollectionKind, HydroIrOpMetadata, HydroNode, HydroRoot, KeyedSingletonBoundKind, SharedNode,
22};
23#[cfg(stageleft_runtime)]
24use crate::forward_handle::{CycleCollection, ReceiverComplete};
25use crate::forward_handle::{ForwardRef, TickCycle};
26use crate::live_collections::stream::{Ordering, Retries};
27#[cfg(stageleft_runtime)]
28use crate::location::dynamic::{DynLocation, LocationId};
29use crate::location::tick::DeferTick;
30use crate::location::{Atomic, Location, Tick, check_matching_location};
31use crate::manual_expr::ManualExpr;
32use crate::nondet::{NonDet, nondet};
33use crate::properties::manual_proof;
34
35/// A marker trait indicating which components of a [`KeyedSingleton`] may change.
36///
37/// In addition to [`Bounded`] (all entries are fixed) and [`Unbounded`] (entries may be added /
38/// changed, but not removed), this also includes an additional variant [`BoundedValue`], which
39/// indicates that entries may be added over time, but once an entry is added it will never be
40/// removed and its value will never change.
41pub trait KeyedSingletonBound {
42 /// The [`Boundedness`] of the [`Stream`] underlying the keyed singleton.
43 type UnderlyingBound: Boundedness;
44 /// The [`Boundedness`] of each entry's value; [`Bounded`] means it is immutable.
45 type ValueBound: Boundedness;
46
47 /// The type of the keyed singleton if the value for each key is immutable.
48 type WithBoundedValue: KeyedSingletonBound<
49 UnderlyingBound = Self::UnderlyingBound,
50 ValueBound = Bounded,
51 EraseMonotonic = Self::WithBoundedValue,
52 >;
53
54 /// The [`Boundedness`] of this [`Singleton`] if it is produced from a [`KeyedStream`] with [`Self`] boundedness.
55 type KeyedStreamToMonotone: KeyedSingletonBound<UnderlyingBound = Self::UnderlyingBound, ValueBound = Self::ValueBound>;
56
57 /// The [`Boundedness`] of the keyed singleton produced by folding a [`KeyedStream`] with
58 /// [`Self`] boundedness when the aggregation does *not* have a monotonicity proof.
59 ///
60 /// Without a monotonicity proof, the per-key values may change arbitrarily, so an unbounded
61 /// input collapses to [`MonotonicKeys`] (keys are still only added, never removed).
62 type KeyedStreamToNonMonotone: KeyedSingletonBound<UnderlyingBound = Self::UnderlyingBound, ValueBound = Self::ValueBound>;
63
64 /// The type of the keyed singleton if the value for each key is no longer monotonic.
65 type EraseMonotonic: KeyedSingletonBound<UnderlyingBound = Self::UnderlyingBound, ValueBound = Self::ValueBound>;
66
67 /// Returns the [`KeyedSingletonBoundKind`] corresponding to this type.
68 fn bound_kind() -> KeyedSingletonBoundKind;
69}
70
71impl KeyedSingletonBound for Unbounded {
72 type UnderlyingBound = Unbounded;
73 type ValueBound = Unbounded;
74 type WithBoundedValue = BoundedValue;
75 type KeyedStreamToMonotone = MonotonicValue;
76 type KeyedStreamToNonMonotone = MonotonicKeys;
77 type EraseMonotonic = Unbounded;
78
79 fn bound_kind() -> KeyedSingletonBoundKind {
80 KeyedSingletonBoundKind::Unbounded
81 }
82}
83
84impl KeyedSingletonBound for Bounded {
85 type UnderlyingBound = Bounded;
86 type ValueBound = Bounded;
87 type WithBoundedValue = Bounded;
88 type KeyedStreamToMonotone = Bounded;
89 type KeyedStreamToNonMonotone = Bounded;
90 type EraseMonotonic = Bounded;
91
92 fn bound_kind() -> KeyedSingletonBoundKind {
93 KeyedSingletonBoundKind::Bounded
94 }
95}
96
97/// A variation of boundedness specific to [`KeyedSingleton`], which indicates that once a key appears,
98/// its value is bounded and will never change, but new entries may appear asynchronously
99pub struct BoundedValue;
100
101impl KeyedSingletonBound for BoundedValue {
102 type UnderlyingBound = Unbounded;
103 type ValueBound = Bounded;
104 type WithBoundedValue = BoundedValue;
105 type KeyedStreamToMonotone = BoundedValue;
106 type KeyedStreamToNonMonotone = BoundedValue;
107 type EraseMonotonic = BoundedValue;
108
109 fn bound_kind() -> KeyedSingletonBoundKind {
110 KeyedSingletonBoundKind::BoundedValue
111 }
112}
113
114/// A variation of boundedness specific to [`KeyedSingleton`], which indicates that once a key appears,
115/// it will never be removed, and the corresponding value will only increase monotonically.
116pub struct MonotonicValue;
117
118impl KeyedSingletonBound for MonotonicValue {
119 type UnderlyingBound = Unbounded;
120 type ValueBound = Unbounded;
121 type WithBoundedValue = BoundedValue;
122 type KeyedStreamToMonotone = MonotonicValue;
123 type KeyedStreamToNonMonotone = MonotonicKeys;
124 type EraseMonotonic = MonotonicKeys;
125
126 fn bound_kind() -> KeyedSingletonBoundKind {
127 KeyedSingletonBoundKind::MonotonicValue
128 }
129}
130
131/// A variation of boundedness specific to [`KeyedSingleton`], which indicates that once a key
132/// appears, it will never be removed, but the corresponding value may change arbitrarily.
133pub struct MonotonicKeys;
134
135impl KeyedSingletonBound for MonotonicKeys {
136 type UnderlyingBound = Unbounded;
137 type ValueBound = Unbounded;
138 type WithBoundedValue = BoundedValue;
139 type KeyedStreamToMonotone = MonotonicKeys;
140 type KeyedStreamToNonMonotone = MonotonicKeys;
141 type EraseMonotonic = MonotonicKeys;
142
143 fn bound_kind() -> KeyedSingletonBoundKind {
144 KeyedSingletonBoundKind::MonotonicKeys
145 }
146}
147
148#[sealed]
149#[diagnostic::on_unimplemented(
150 message = "The keyed singleton must have monotonic values (`MonotonicValue`) or be bounded (`Bounded`), but has bound `{Self}`. Strengthen the monotonicity upstream or consider a different API.",
151 label = "required here",
152 note = "To intentionally process a non-deterministic snapshot or batch, you may want to use a `sliced!` region. This introduces non-determinism so avoid unless necessary."
153)]
154/// Marker trait that is implemented for [`KeyedSingletonBound`] types whose per-key values
155/// are monotonically non-decreasing (or bounded).
156pub trait IsKeyedMonotonic: KeyedSingletonBound {}
157
158#[sealed]
159#[diagnostic::do_not_recommend]
160impl IsKeyedMonotonic for MonotonicValue {}
161
162#[sealed]
163#[diagnostic::do_not_recommend]
164impl IsKeyedMonotonic for BoundedValue {}
165
166#[sealed]
167#[diagnostic::do_not_recommend]
168impl<B: IsBounded + KeyedSingletonBound> IsKeyedMonotonic for B {}
169
170/// Mapping from keys of type `K` to values of type `V`.
171///
172/// Keyed Singletons capture an asynchronously updated mapping from keys of the `K` to values of
173/// type `V`, where the order of keys is non-deterministic. In addition to the standard boundedness
174/// variants ([`Bounded`] for finite and immutable, [`Unbounded`] for asynchronously changing),
175/// keyed singletons can use [`BoundedValue`] to declare that new keys may be added over time, but
176/// keys cannot be removed and the value for each key is immutable.
177///
178/// Type Parameters:
179/// - `K`: the type of the key for each entry
180/// - `V`: the type of the value for each entry
181/// - `Loc`: the [`Location`] where the keyed singleton is materialized
182/// - `Bound`: tracks whether the entries are:
183/// - [`Bounded`] (local and finite)
184/// - [`Unbounded`] (asynchronous with entries added / removed / changed over time)
185/// - [`BoundedValue`] (asynchronous with immutable values for each key and no removals)
186pub struct KeyedSingleton<K, V, Loc, Bound: KeyedSingletonBound> {
187 pub(crate) location: Loc,
188 pub(crate) ir_node: Rc<RefCell<HydroNode>>,
189 pub(crate) flow_state: FlowState,
190
191 _phantom: PhantomData<(K, V, Loc, Bound)>,
192}
193
194impl<K, V, L, B: KeyedSingletonBound> Drop for KeyedSingleton<K, V, L, B> {
195 fn drop(&mut self) {
196 let ir_node = self.ir_node.replace(HydroNode::Placeholder);
197 if !matches!(ir_node, HydroNode::Placeholder) && !ir_node.is_shared_with_others() {
198 self.flow_state.borrow_mut().try_push_root(HydroRoot::Null {
199 input: Box::new(ir_node),
200 op_metadata: HydroIrOpMetadata::new(),
201 });
202 }
203 }
204}
205
206impl<'a, K: Clone, V: Clone, Loc: Location<'a>, Bound: KeyedSingletonBound> Clone
207 for KeyedSingleton<K, V, Loc, Bound>
208{
209 fn clone(&self) -> Self {
210 if !matches!(self.ir_node.borrow().deref(), HydroNode::Tee { .. }) {
211 let orig_ir_node = self.ir_node.replace(HydroNode::Placeholder);
212 *self.ir_node.borrow_mut() = HydroNode::Tee {
213 inner: SharedNode(Rc::new(RefCell::new(orig_ir_node))),
214 metadata: self.location.new_node_metadata(Self::collection_kind()),
215 };
216 }
217
218 if let HydroNode::Tee { inner, metadata } = self.ir_node.borrow().deref() {
219 KeyedSingleton {
220 location: self.location.clone(),
221 flow_state: self.flow_state.clone(),
222 ir_node: super::tracked_ir_node(
223 &self.flow_state,
224 HydroNode::Tee {
225 inner: SharedNode(inner.0.clone()),
226 metadata: metadata.clone(),
227 },
228 ),
229 _phantom: PhantomData,
230 }
231 } else {
232 unreachable!()
233 }
234 }
235}
236
237impl<'a, K, V, L, B: KeyedSingletonBound> CycleCollection<'a, ForwardRef>
238 for KeyedSingleton<K, V, L, B>
239where
240 L: Location<'a>,
241{
242 type Location = L;
243
244 fn create_source(cycle_id: CycleId, location: L) -> Self {
245 let flow_state = location.flow_state().clone();
246 KeyedSingleton {
247 ir_node: super::tracked_ir_node(
248 &flow_state,
249 HydroNode::CycleSource {
250 cycle_id,
251 metadata: location.new_node_metadata(Self::collection_kind()),
252 },
253 ),
254 flow_state,
255 location,
256 _phantom: PhantomData,
257 }
258 }
259}
260
261impl<'a, K, V, L> CycleCollection<'a, TickCycle> for KeyedSingleton<K, V, Tick<L>, Bounded>
262where
263 L: Location<'a>,
264{
265 type Location = Tick<L>;
266
267 fn create_source(cycle_id: CycleId, location: Tick<L>) -> Self {
268 KeyedSingleton::new(
269 location.clone(),
270 HydroNode::CycleSource {
271 cycle_id,
272 metadata: location.new_node_metadata(Self::collection_kind()),
273 },
274 )
275 }
276}
277
278impl<'a, K, V, L> DeferTick for KeyedSingleton<K, V, Tick<L>, Bounded>
279where
280 L: Location<'a>,
281{
282 fn defer_tick(self) -> Self {
283 KeyedSingleton::defer_tick(self)
284 }
285}
286
287impl<'a, K, V, L, B: KeyedSingletonBound> ReceiverComplete<'a, ForwardRef>
288 for KeyedSingleton<K, V, L, B>
289where
290 L: Location<'a>,
291{
292 fn complete(self, cycle_id: CycleId, expected_location: LocationId) {
293 assert_eq!(
294 Location::id(&self.location),
295 expected_location,
296 "locations do not match"
297 );
298 self.location
299 .flow_state()
300 .borrow_mut()
301 .push_root(HydroRoot::CycleSink {
302 cycle_id,
303 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
304 op_metadata: HydroIrOpMetadata::new(),
305 });
306 }
307}
308
309impl<'a, K, V, L> ReceiverComplete<'a, TickCycle> for KeyedSingleton<K, V, Tick<L>, Bounded>
310where
311 L: Location<'a>,
312{
313 fn complete(self, cycle_id: CycleId, expected_location: LocationId) {
314 assert_eq!(
315 Location::id(&self.location),
316 expected_location,
317 "locations do not match"
318 );
319 self.location
320 .flow_state()
321 .borrow_mut()
322 .push_root(HydroRoot::CycleSink {
323 cycle_id,
324 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
325 op_metadata: HydroIrOpMetadata::new(),
326 });
327 }
328}
329
330impl<'a, K, V, L: Location<'a>, B: KeyedSingletonBound> KeyedSingleton<K, V, L, B> {
331 pub(crate) fn new(location: L, ir_node: HydroNode) -> Self {
332 debug_assert_eq!(ir_node.metadata().location_id, Location::id(&location));
333 debug_assert_eq!(ir_node.metadata().collection_kind, Self::collection_kind());
334
335 let flow_state = location.flow_state().clone();
336 let ir_node = super::tracked_ir_node(&flow_state, ir_node);
337 KeyedSingleton {
338 location,
339 flow_state,
340 ir_node,
341 _phantom: PhantomData,
342 }
343 }
344
345 /// Returns the [`Location`] where this keyed singleton is being materialized.
346 pub fn location(&self) -> &L {
347 &self.location
348 }
349
350 /// Weakens the consistency of this live collection to not guarantee any consistency across
351 /// cluster members (if this collection is on a cluster).
352 pub fn weaken_consistency(self) -> KeyedSingleton<K, V, L::DropConsistency, B>
353 where
354 L: Location<'a>,
355 {
356 if L::consistency()
357 .is_none_or(|c| c == crate::location::dynamic::ClusterConsistency::NoConsistency)
358 {
359 // already no consistency
360 KeyedSingleton::new(
361 self.location.drop_consistency(),
362 self.ir_node.replace(HydroNode::Placeholder),
363 )
364 } else {
365 KeyedSingleton::new(
366 self.location.drop_consistency(),
367 HydroNode::Cast {
368 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
369 metadata: self
370 .location
371 .drop_consistency()
372 .new_node_metadata(
373 KeyedSingleton::<K, V, L::DropConsistency, B>::collection_kind(),
374 ),
375 },
376 )
377 }
378 }
379
380 /// Casts this live collection to have the consistency guarantees specified in the given
381 /// location type parameter. The developer must ensure that the strengthened consistency
382 /// is actually guaranteed, via the proof field (see [`crate::prelude::manual_proof`]).
383 pub fn assert_has_consistency_of<L2: Location<'a, DropConsistency = L::DropConsistency>>(
384 self,
385 _proof: impl crate::properties::ConsistencyProof,
386 ) -> KeyedSingleton<K, V, L2, B>
387 where
388 L: Location<'a>,
389 {
390 if L::consistency() == L2::consistency() {
391 // already consistent
392 KeyedSingleton::new(
393 self.location.with_consistency_of(),
394 self.ir_node.replace(HydroNode::Placeholder),
395 )
396 } else {
397 KeyedSingleton::new(
398 self.location.with_consistency_of(),
399 HydroNode::AssertIsConsistent {
400 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
401 trusted: false,
402 metadata: self
403 .location
404 .clone()
405 .with_consistency_of::<L2>()
406 .new_node_metadata(KeyedSingleton::<K, V, L2, B>::collection_kind()),
407 },
408 )
409 }
410 }
411}
412
413#[cfg(stageleft_runtime)]
414fn key_count_inside_tick<'a, K, V, L: Location<'a>>(
415 me: KeyedSingleton<K, V, L, Bounded>,
416) -> Singleton<usize, L, Bounded> {
417 me.entries().count()
418}
419
420#[cfg(stageleft_runtime)]
421fn into_singleton_inside_tick<'a, K, V, L: Location<'a>>(
422 me: KeyedSingleton<K, V, L, Bounded>,
423) -> Singleton<HashMap<K, V>, L, Bounded>
424where
425 K: Eq + Hash,
426{
427 me.entries()
428 .assume_ordering_trusted(nondet!(
429 /// There is only one element associated with each key. The closure technically
430 /// isn't commutative in the case where both passed entries have the same key
431 /// but different values.
432 ///
433 /// In the future, we may want to have an `assume!(...)` statement in the UDF that
434 /// the key is never already present in the map.
435 ))
436 .fold(
437 q!(|| HashMap::new()),
438 q!(|map, (k, v)| {
439 map.insert(k, v);
440 }),
441 )
442}
443
444impl<'a, K, V, L: Location<'a>, B: KeyedSingletonBound> KeyedSingleton<K, V, L, B> {
445 pub(crate) fn collection_kind() -> CollectionKind {
446 CollectionKind::KeyedSingleton {
447 bound: B::bound_kind(),
448 key_type: stageleft::quote_type::<K>().into(),
449 value_type: stageleft::quote_type::<V>().into(),
450 }
451 }
452
453 /// Transforms each value by invoking `f` on each element, with keys staying the same
454 /// after transformation. If you need access to the key, see [`KeyedSingleton::map_with_key`].
455 ///
456 /// If you do not want to modify the stream and instead only want to view
457 /// each item use [`KeyedSingleton::inspect`] instead.
458 ///
459 /// # Example
460 /// ```rust
461 /// # #[cfg(feature = "deploy")] {
462 /// # use hydro_lang::prelude::*;
463 /// # use futures::StreamExt;
464 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
465 /// let keyed_singleton = // { 1: 2, 2: 4 }
466 /// # process
467 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
468 /// # .into_keyed()
469 /// # .first();
470 /// keyed_singleton.map(q!(|v| v + 1))
471 /// # .entries()
472 /// # }, |mut stream| async move {
473 /// // { 1: 3, 2: 5 }
474 /// # let mut results = Vec::new();
475 /// # for _ in 0..2 {
476 /// # results.push(stream.next().await.unwrap());
477 /// # }
478 /// # results.sort();
479 /// # assert_eq!(results, vec![(1, 3), (2, 5)]);
480 /// # }));
481 /// # }
482 /// ```
483 pub fn map<U, F>(
484 self,
485 f: impl IntoQuotedMut<'a, F, L> + Copy,
486 ) -> KeyedSingleton<K, U, L, B::EraseMonotonic>
487 where
488 F: Fn(V) -> U + 'a,
489 {
490 let f: ManualExpr<F, _> = ManualExpr::new(move |ctx: &L| f.splice_fn1_ctx(ctx));
491 let map_f = q!({
492 let orig = f;
493 move |(k, v)| (k, orig(v))
494 })
495 .splice_fn1_ctx::<(K, V), (K, U)>(&self.location)
496 .into();
497
498 KeyedSingleton::new(
499 self.location.clone(),
500 HydroNode::Map {
501 f: map_f,
502 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
503 metadata: self.location.new_node_metadata(KeyedSingleton::<
504 K,
505 U,
506 L,
507 B::EraseMonotonic,
508 >::collection_kind()),
509 },
510 )
511 }
512
513 /// Transforms each value by invoking `f` on each key-value pair, with keys staying the same
514 /// after transformation. Unlike [`KeyedSingleton::map`], this gives access to both the key and value.
515 ///
516 /// The closure `f` receives a tuple `(K, V)` containing both the key and value, and returns
517 /// the new value `U`. The key remains unchanged in the output.
518 ///
519 /// # Example
520 /// ```rust
521 /// # #[cfg(feature = "deploy")] {
522 /// # use hydro_lang::prelude::*;
523 /// # use futures::StreamExt;
524 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
525 /// let keyed_singleton = // { 1: 2, 2: 4 }
526 /// # process
527 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
528 /// # .into_keyed()
529 /// # .first();
530 /// keyed_singleton.map_with_key(q!(|(k, v)| k + v))
531 /// # .entries()
532 /// # }, |mut stream| async move {
533 /// // { 1: 3, 2: 6 }
534 /// # let mut results = Vec::new();
535 /// # for _ in 0..2 {
536 /// # results.push(stream.next().await.unwrap());
537 /// # }
538 /// # results.sort();
539 /// # assert_eq!(results, vec![(1, 3), (2, 6)]);
540 /// # }));
541 /// # }
542 /// ```
543 pub fn map_with_key<U, F>(
544 self,
545 f: impl IntoQuotedMut<'a, F, L> + Copy,
546 ) -> KeyedSingleton<K, U, L, B::EraseMonotonic>
547 where
548 F: Fn((K, V)) -> U + 'a,
549 K: Clone,
550 {
551 let f: ManualExpr<F, _> = ManualExpr::new(move |ctx: &L| f.splice_fn1_ctx(ctx));
552 let map_f = q!({
553 let orig = f;
554 move |(k, v)| {
555 let out = orig((Clone::clone(&k), v));
556 (k, out)
557 }
558 })
559 .splice_fn1_ctx::<(K, V), (K, U)>(&self.location)
560 .into();
561
562 KeyedSingleton::new(
563 self.location.clone(),
564 HydroNode::Map {
565 f: map_f,
566 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
567 metadata: self.location.new_node_metadata(KeyedSingleton::<
568 K,
569 U,
570 L,
571 B::EraseMonotonic,
572 >::collection_kind()),
573 },
574 )
575 }
576
577 /// Gets the number of keys in the keyed singleton.
578 ///
579 /// The output singleton will be unbounded if the input is [`Unbounded`] or [`BoundedValue`],
580 /// since keys may be added / removed over time. When the set of keys changes, the count will
581 /// be asynchronously updated.
582 ///
583 /// # Example
584 /// ```rust
585 /// # #[cfg(feature = "deploy")] {
586 /// # use hydro_lang::prelude::*;
587 /// # use futures::StreamExt;
588 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
589 /// # let tick = process.tick();
590 /// let keyed_singleton = // { 1: "a", 2: "b", 3: "c" }
591 /// # process
592 /// # .source_iter(q!(vec![(1, "a"), (2, "b"), (3, "c")]))
593 /// # .into_keyed()
594 /// # .batch(&tick, nondet!(/** test */))
595 /// # .first();
596 /// keyed_singleton.key_count()
597 /// # .all_ticks()
598 /// # }, |mut stream| async move {
599 /// // 3
600 /// # assert_eq!(stream.next().await.unwrap(), 3);
601 /// # }));
602 /// # }
603 /// ```
604 pub fn key_count(self) -> Singleton<usize, L, B::UnderlyingBound> {
605 if B::ValueBound::BOUNDED {
606 let me: KeyedSingleton<K, V, L, B::WithBoundedValue> = KeyedSingleton {
607 location: self.location.clone(),
608 flow_state: self.flow_state.clone(),
609 ir_node: super::tracked_ir_node(
610 &self.flow_state,
611 self.ir_node.replace(HydroNode::Placeholder),
612 ),
613 _phantom: PhantomData,
614 };
615
616 me.entries().count().ignore_monotonic()
617 } else if L::is_top_level()
618 && let Some(tick) = self.location.try_tick()
619 && (B::bound_kind() == KeyedSingletonBoundKind::Unbounded
620 || B::bound_kind() == KeyedSingletonBoundKind::MonotonicKeys
621 || B::bound_kind() == KeyedSingletonBoundKind::MonotonicValue)
622 {
623 let location = self.location.clone();
624 let ir_node = self.ir_node.replace(HydroNode::Placeholder);
625 let me: KeyedSingleton<K, V, L, MonotonicKeys> =
626 KeyedSingleton::new(location.clone(), ir_node);
627
628 let out =
629 key_count_inside_tick(me.snapshot(&tick, nondet!(/** eventually stabilizes */)))
630 .latest();
631 Singleton::new(location, out.ir_node.replace(HydroNode::Placeholder))
632 } else {
633 panic!("BoundedValue or Unbounded KeyedSingleton inside a tick, not supported");
634 }
635 }
636
637 /// Converts this keyed singleton into a [`Singleton`] containing a `HashMap` from keys to values.
638 ///
639 /// As the values for each key are updated asynchronously, the `HashMap` will be updated
640 /// asynchronously as well.
641 ///
642 /// # Example
643 /// ```rust
644 /// # #[cfg(feature = "deploy")] {
645 /// # use hydro_lang::prelude::*;
646 /// # use futures::StreamExt;
647 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
648 /// let keyed_singleton = // { 1: "a", 2: "b", 3: "c" }
649 /// # process
650 /// # .source_iter(q!(vec![(1, "a".to_owned()), (2, "b".to_owned()), (3, "c".to_owned())]))
651 /// # .into_keyed()
652 /// # .batch(&process.tick(), nondet!(/** test */))
653 /// # .first();
654 /// keyed_singleton.into_singleton()
655 /// # .all_ticks()
656 /// # }, |mut stream| async move {
657 /// // { 1: "a", 2: "b", 3: "c" }
658 /// # assert_eq!(stream.next().await.unwrap(), vec![(1, "a".to_owned()), (2, "b".to_owned()), (3, "c".to_owned())].into_iter().collect());
659 /// # }));
660 /// # }
661 /// ```
662 pub fn into_singleton(self) -> Singleton<HashMap<K, V>, L, B::UnderlyingBound>
663 where
664 K: Eq + Hash,
665 {
666 if B::ValueBound::BOUNDED {
667 let me: KeyedSingleton<K, V, L, B::WithBoundedValue> = KeyedSingleton {
668 location: self.location.clone(),
669 flow_state: self.flow_state.clone(),
670 ir_node: super::tracked_ir_node(
671 &self.flow_state,
672 self.ir_node.replace(HydroNode::Placeholder),
673 ),
674 _phantom: PhantomData,
675 };
676
677 me.entries()
678 .assume_ordering_trusted(nondet!(
679 /// There is only one element associated with each key. The closure technically
680 /// isn't commutative in the case where both passed entries have the same key
681 /// but different values.
682 ///
683 /// In the future, we may want to have an `assume!(...)` statement in the UDF that
684 /// the key is never already present in the map.
685 ))
686 .fold(
687 q!(|| HashMap::new()),
688 q!(|map, (k, v)| {
689 // TODO(shadaj): make this commutative but really-debug-assert that there is no key overlap
690 map.insert(k, v);
691 }),
692 )
693 } else if L::is_top_level()
694 && let Some(tick) = self.location.try_tick()
695 && (B::bound_kind() == KeyedSingletonBoundKind::Unbounded
696 || B::bound_kind() == KeyedSingletonBoundKind::MonotonicKeys
697 || B::bound_kind() == KeyedSingletonBoundKind::MonotonicValue)
698 {
699 let location = self.location.clone();
700 let ir_node = self.ir_node.replace(HydroNode::Placeholder);
701 let me: KeyedSingleton<K, V, L, MonotonicKeys> =
702 KeyedSingleton::new(location.clone(), ir_node);
703
704 let out = into_singleton_inside_tick(
705 me.snapshot(&tick, nondet!(/** eventually stabilizes */)),
706 )
707 .latest();
708 Singleton::new(location, out.ir_node.replace(HydroNode::Placeholder))
709 } else {
710 panic!("BoundedValue or Unbounded KeyedSingleton inside a tick, not supported");
711 }
712 }
713
714 /// An operator which allows you to "name" a `HydroNode`.
715 /// This is only used for testing, to correlate certain `HydroNode`s with IDs.
716 pub fn ir_node_named(self, name: &str) -> KeyedSingleton<K, V, L, B> {
717 {
718 let mut node = self.ir_node.borrow_mut();
719 let metadata = node.metadata_mut();
720 metadata.tag = Some(name.to_owned());
721 }
722 self
723 }
724
725 /// Strengthens the boundedness guarantee to `Bounded`, given that `B: IsBounded`, which
726 /// implies that `B == Bounded`.
727 pub fn make_bounded(self) -> KeyedSingleton<K, V, L, Bounded>
728 where
729 B: IsBounded,
730 {
731 KeyedSingleton::new(
732 self.location.clone(),
733 self.ir_node.replace(HydroNode::Placeholder),
734 )
735 }
736
737 /// Gets the value associated with a specific key from the keyed singleton.
738 /// Returns `None` if the key is `None` or there is no associated value.
739 ///
740 /// # Example
741 /// ```rust
742 /// # #[cfg(feature = "deploy")] {
743 /// # use hydro_lang::prelude::*;
744 /// # use futures::StreamExt;
745 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
746 /// let tick = process.tick();
747 /// let keyed_data = process
748 /// .source_iter(q!(vec![(1, 2), (2, 3)]))
749 /// .into_keyed()
750 /// .batch(&tick, nondet!(/** test */))
751 /// .first();
752 /// let key = tick.singleton(q!(1));
753 /// keyed_data.get(key).all_ticks()
754 /// # }, |mut stream| async move {
755 /// // 2
756 /// # assert_eq!(stream.next().await.unwrap(), 2);
757 /// # }));
758 /// # }
759 /// ```
760 pub fn get(self, key: impl Into<Optional<K, L, Bounded>>) -> Optional<V, L, Bounded>
761 where
762 B: IsBounded,
763 K: Hash + Eq + Clone,
764 V: Clone,
765 {
766 self.make_bounded()
767 .into_keyed_stream()
768 .get(key)
769 .cast_at_most_one_element()
770 }
771
772 /// Emit a keyed stream containing keys shared between the keyed singleton and the
773 /// keyed stream, where each value in the output keyed stream is a tuple of
774 /// (the keyed singleton's value, the keyed stream's value).
775 ///
776 /// # Example
777 /// ```rust
778 /// # #[cfg(feature = "deploy")] {
779 /// # use hydro_lang::prelude::*;
780 /// # use futures::StreamExt;
781 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
782 /// let tick = process.tick();
783 /// let keyed_data = process
784 /// .source_iter(q!(vec![(1, 10), (2, 20)]))
785 /// .into_keyed()
786 /// .batch(&tick, nondet!(/** test */))
787 /// .first();
788 /// let other_data = process
789 /// .source_iter(q!(vec![(1, 100), (2, 200), (1, 101)]))
790 /// .into_keyed()
791 /// .batch(&tick, nondet!(/** test */));
792 /// keyed_data.join_keyed_stream(other_data).entries().all_ticks()
793 /// # }, |mut stream| async move {
794 /// // { 1: [(10, 100), (10, 101)], 2: [(20, 200)] } in any order
795 /// # let mut results = vec![];
796 /// # for _ in 0..3 {
797 /// # results.push(stream.next().await.unwrap());
798 /// # }
799 /// # results.sort();
800 /// # assert_eq!(results, vec![(1, (10, 100)), (1, (10, 101)), (2, (20, 200))]);
801 /// # }));
802 /// # }
803 /// ```
804 pub fn join_keyed_stream<O2: Ordering, R2: Retries, V2, B2: Boundedness>(
805 self,
806 other: KeyedStream<K, V2, L, B2, O2, R2>,
807 ) -> KeyedStream<K, (V, V2), L, B2, O2, R2>
808 where
809 B: IsBounded,
810 K: Eq + Hash + Clone,
811 V: Clone,
812 V2: Clone,
813 {
814 // TODO(shadaj): if DFIR guarantees that joining unbounded keyed stream x bounded keyed stream
815 // always produces deterministic order per key (nested loop join), this could just use
816 // `join_keyed_stream` without constructing IRs manually
817 KeyedStream::new(
818 self.location.clone(),
819 HydroNode::Join {
820 left: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
821 right: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
822 metadata: self
823 .location
824 .new_node_metadata(KeyedStream::<K, (V, V2), L, B2, O2, R2>::collection_kind()),
825 },
826 )
827 }
828
829 /// Emit a keyed singleton containing all keys shared between two keyed singletons,
830 /// where each value in the output keyed singleton is a tuple of
831 /// (self.value, other.value).
832 ///
833 /// # Example
834 /// ```rust
835 /// # #[cfg(feature = "deploy")] {
836 /// # use hydro_lang::prelude::*;
837 /// # use futures::StreamExt;
838 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
839 /// # let tick = process.tick();
840 /// let requests = // { 1: 10, 2: 20, 3: 30 }
841 /// # process
842 /// # .source_iter(q!(vec![(1, 10), (2, 20), (3, 30)]))
843 /// # .into_keyed()
844 /// # .batch(&tick, nondet!(/** test */))
845 /// # .first();
846 /// let other = // { 1: 100, 2: 200, 4: 400 }
847 /// # process
848 /// # .source_iter(q!(vec![(1, 100), (2, 200), (4, 400)]))
849 /// # .into_keyed()
850 /// # .batch(&tick, nondet!(/** test */))
851 /// # .first();
852 /// requests.join_keyed_singleton(other)
853 /// # .entries().all_ticks()
854 /// # }, |mut stream| async move {
855 /// // { 1: (10, 100), 2: (20, 200) }
856 /// # let mut results = vec![];
857 /// # for _ in 0..2 {
858 /// # results.push(stream.next().await.unwrap());
859 /// # }
860 /// # results.sort();
861 /// # assert_eq!(results, vec![(1, (10, 100)), (2, (20, 200))]);
862 /// # }));
863 /// # }
864 /// ```
865 pub fn join_keyed_singleton<V2: Clone>(
866 self,
867 other: KeyedSingleton<K, V2, L, Bounded>,
868 ) -> KeyedSingleton<K, (V, V2), L, Bounded>
869 where
870 B: IsBounded,
871 K: Eq + Hash + Clone,
872 V: Clone,
873 {
874 let result_stream = self
875 .make_bounded()
876 .entries()
877 .join(other.entries())
878 .into_keyed();
879
880 // The cast is guaranteed to succeed, since each key (in both `self` and `other`) has at most one value.
881 result_stream.cast_at_most_one_entry_per_key()
882 }
883
884 /// For each value in `self`, find the matching key in `lookup`.
885 /// The output is a keyed singleton with the key from `self`, and a value
886 /// that is a tuple of (`self`'s value, Option<`lookup`'s value>).
887 /// If the key is not present in `lookup`, the option will be [`None`].
888 ///
889 /// # Example
890 /// ```rust
891 /// # #[cfg(feature = "deploy")] {
892 /// # use hydro_lang::prelude::*;
893 /// # use futures::StreamExt;
894 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
895 /// # let tick = process.tick();
896 /// let requests = // { 1: 10, 2: 20 }
897 /// # process
898 /// # .source_iter(q!(vec![(1, 10), (2, 20)]))
899 /// # .into_keyed()
900 /// # .batch(&tick, nondet!(/** test */))
901 /// # .first();
902 /// let other_data = // { 10: 100, 11: 110 }
903 /// # process
904 /// # .source_iter(q!(vec![(10, 100), (11, 110)]))
905 /// # .into_keyed()
906 /// # .batch(&tick, nondet!(/** test */))
907 /// # .first();
908 /// requests.lookup_keyed_singleton(other_data)
909 /// # .entries().all_ticks()
910 /// # }, |mut stream| async move {
911 /// // { 1: (10, Some(100)), 2: (20, None) }
912 /// # let mut results = vec![];
913 /// # for _ in 0..2 {
914 /// # results.push(stream.next().await.unwrap());
915 /// # }
916 /// # results.sort();
917 /// # assert_eq!(results, vec![(1, (10, Some(100))), (2, (20, None))]);
918 /// # }));
919 /// # }
920 /// ```
921 pub fn lookup_keyed_singleton<V2>(
922 self,
923 lookup: KeyedSingleton<V, V2, L, Bounded>,
924 ) -> KeyedSingleton<K, (V, Option<V2>), L, Bounded>
925 where
926 B: IsBounded,
927 K: Eq + Hash + Clone,
928 V: Eq + Hash + Clone,
929 V2: Clone,
930 {
931 let result_stream = self
932 .make_bounded()
933 .into_keyed_stream()
934 .lookup_keyed_stream(lookup.into_keyed_stream());
935
936 // The cast is guaranteed to succeed since both lookup and self contain at most 1 value per key
937 result_stream.cast_at_most_one_entry_per_key()
938 }
939
940 /// For each value in `self`, find the matching key in `lookup`.
941 /// The output is a keyed stream with the key from `self`, and a value
942 /// that is a tuple of (`self`'s value, Option<`lookup`'s value>).
943 /// If the key is not present in `lookup`, the option will be [`None`].
944 ///
945 /// # Example
946 /// ```rust
947 /// # #[cfg(feature = "deploy")] {
948 /// # use hydro_lang::prelude::*;
949 /// # use futures::StreamExt;
950 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
951 /// # let tick = process.tick();
952 /// let requests = // { 1: 10, 2: 20 }
953 /// # process
954 /// # .source_iter(q!(vec![(1, 10), (2, 20)]))
955 /// # .into_keyed()
956 /// # .batch(&tick, nondet!(/** test */))
957 /// # .first();
958 /// let other_data = // { 10: 100, 10: 110 }
959 /// # process
960 /// # .source_iter(q!(vec![(10, 100), (10, 110)]))
961 /// # .into_keyed()
962 /// # .batch(&tick, nondet!(/** test */));
963 /// requests.lookup_keyed_stream(other_data)
964 /// # .entries().all_ticks()
965 /// # }, |mut stream| async move {
966 /// // { 1: [(10, Some(100)), (10, Some(110))], 2: (20, None) }
967 /// # let mut results = vec![];
968 /// # for _ in 0..3 {
969 /// # results.push(stream.next().await.unwrap());
970 /// # }
971 /// # results.sort();
972 /// # assert_eq!(results, vec![(1, (10, Some(100))), (1, (10, Some(110))), (2, (20, None))]);
973 /// # }));
974 /// # }
975 /// ```
976 pub fn lookup_keyed_stream<V2, O: Ordering, R: Retries>(
977 self,
978 lookup: KeyedStream<V, V2, L, Bounded, O, R>,
979 ) -> KeyedStream<K, (V, Option<V2>), L, Bounded, NoOrder, R>
980 where
981 B: IsBounded,
982 K: Eq + Hash + Clone,
983 V: Eq + Hash + Clone,
984 V2: Clone,
985 {
986 self.make_bounded()
987 .entries()
988 .weaken_retries::<R>() // TODO: Once weaken_retries() is implemented for KeyedSingleton, remove entries() and into_keyed()
989 .into_keyed()
990 .lookup_keyed_stream(lookup)
991 }
992
993 /// For each key present in both `self` and `thresholds`, emits a [`KeyedStream`] event the first
994 /// time that key's value becomes greater than or equal to the corresponding threshold value.
995 /// The emitted value for each key is the threshold value itself.
996 ///
997 /// This requires the keyed singleton to have monotonic values ([`MonotonicValue`] or [`Bounded`]),
998 /// because otherwise the threshold detection would be non-deterministic.
999 ///
1000 /// The `thresholds` parameter is a [`BoundedValue`] keyed singleton mapping each key to its
1001 /// threshold. Thresholds may arrive asynchronously (new keys appear over time), but once set
1002 /// for a key, the threshold value is fixed. Late-arriving thresholds are checked against the
1003 /// current snapshot value immediately.
1004 ///
1005 /// # Example
1006 /// ```rust,ignore
1007 /// use hydro_lang::prelude::*;
1008 ///
1009 /// // Given a monotonically increasing keyed singleton (e.g. from fold with monotone proof)
1010 /// let counts: KeyedSingleton<u32, usize, _, MonotonicValue> = events.into_keyed()
1011 /// .fold(q!(|| 0), q!(|acc, _| *acc += 1, monotone = manual_proof!(/** +1 is monotone */)));
1012 ///
1013 /// // BoundedValue keyed singleton of thresholds (from .first())
1014 /// let thresholds = threshold_source.into_keyed().first();
1015 ///
1016 /// // Emits (key, threshold_value) the first time each key's value >= threshold
1017 /// let crossed = counts.threshold_greater_or_equal(thresholds);
1018 /// ```
1019 pub fn threshold_greater_or_equal(
1020 self,
1021 thresholds: KeyedSingleton<K, V, L, BoundedValue>,
1022 ) -> KeyedStream<K, V, L, B::UnderlyingBound, NoOrder, ExactlyOnce>
1023 where
1024 K: Clone + Eq + Hash,
1025 V: Clone + PartialOrd,
1026 B: IsKeyedMonotonic,
1027 {
1028 let self_location = self.location.clone();
1029 match B::bound_kind() {
1030 KeyedSingletonBoundKind::Bounded => {
1031 // Bounded case: self is already fixed, just join and filter
1032 let me: KeyedSingleton<K, V, L, Bounded> = KeyedSingleton::new(
1033 self.location.clone(),
1034 self.ir_node.replace(HydroNode::Placeholder),
1035 );
1036 let result = me
1037 .entries()
1038 .join(thresholds.entries())
1039 .filter_map(q!(|(k, (val, thresh))| {
1040 if val >= thresh {
1041 Some((k, thresh))
1042 } else {
1043 None
1044 }
1045 }))
1046 .into_keyed();
1047 KeyedStream::new(
1048 result.location.clone(),
1049 result.ir_node.replace(HydroNode::Placeholder),
1050 )
1051 }
1052 KeyedSingletonBoundKind::MonotonicValue => {
1053 let me: KeyedSingleton<K, V, L, MonotonicValue> = KeyedSingleton::new(
1054 self.location.clone(),
1055 self.ir_node.replace(HydroNode::Placeholder),
1056 );
1057
1058 let result = sliced! {
1059 let snapshot = use::snapshot(me, nondet!(/** thresholds are deterministic */));
1060 let thresh_snapshot =
1061 use::batch(thresholds, nondet!(/** thresholds are deterministic */));
1062 let mut already_crossed =
1063 use::state_null::<Stream<K, Tick<_>, Bounded, NoOrder>>();
1064
1065 let joined = thresh_snapshot.entries().join(snapshot.entries());
1066 let passed = joined
1067 .filter(q!(|(_, (thresh, val))| *val >= *thresh))
1068 .map(q!(|(k, (thresh, _))| (k, thresh)));
1069
1070 let newly_crossed = passed.anti_join(already_crossed.clone());
1071 already_crossed =
1072 already_crossed.chain(newly_crossed.clone().map(q!(|(k, _)| k)));
1073
1074 newly_crossed.into_keyed()
1075 };
1076
1077 KeyedStream::new(
1078 self_location,
1079 result.ir_node.replace(HydroNode::Placeholder),
1080 )
1081 }
1082 KeyedSingletonBoundKind::BoundedValue => {
1083 let me: KeyedSingleton<K, V, L, BoundedValue> = KeyedSingleton::new(
1084 self.location.clone(),
1085 self.ir_node.replace(HydroNode::Placeholder),
1086 );
1087
1088 let result = sliced! {
1089 let snapshot = use::batch(me, nondet!(/** thresholds are deterministic */));
1090 let thresh_snapshot =
1091 use::batch(thresholds, nondet!(/** thresholds are deterministic */));
1092 let mut already_crossed =
1093 use::state_null::<Stream<K, Tick<_>, Bounded, NoOrder>>();
1094
1095 let joined = thresh_snapshot.entries().join(snapshot.entries());
1096 let passed = joined
1097 .filter(q!(|(_, (thresh, val))| *val >= *thresh))
1098 .map(q!(|(k, (thresh, _))| (k, thresh)));
1099
1100 let newly_crossed = passed.anti_join(already_crossed.clone());
1101 already_crossed =
1102 already_crossed.chain(newly_crossed.clone().map(q!(|(k, _)| k)));
1103
1104 newly_crossed.into_keyed()
1105 };
1106
1107 KeyedStream::new(
1108 self_location,
1109 result.ir_node.replace(HydroNode::Placeholder),
1110 )
1111 }
1112 _ => {
1113 unreachable!(
1114 "IsKeyedMonotonic is only implemented for Bounded, BoundedValue, and MonotonicValue"
1115 )
1116 }
1117 }
1118 }
1119
1120 /// Like [`Self::threshold_greater_or_equal`], but uses a single [`Singleton`] threshold
1121 /// shared across all keys. Emits a `(K, V)` event for each key the first time that key's
1122 /// value becomes >= the threshold. The emitted value is the threshold itself.
1123 ///
1124 /// Because the threshold is a [`Bounded`] singleton, it is a compile-time constant and
1125 /// does not carry ongoing memory cost.
1126 ///
1127 /// # Example
1128 /// ```rust
1129 /// # #[cfg(feature = "deploy")] {
1130 /// # use hydro_lang::prelude::*;
1131 /// # use futures::StreamExt;
1132 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1133 /// // A keyed singleton of per-key values (in practice often a monotone counter): { 1: 6, 2: 4 }
1134 /// let counts = process
1135 /// .source_iter(q!(vec![(1, 6), (2, 4)]))
1136 /// .into_keyed()
1137 /// .first();
1138 ///
1139 /// // A single threshold value shared across all keys
1140 /// let threshold = process.singleton(q!(5));
1141 ///
1142 /// // Emits (key, threshold) the first time each key's value >= threshold
1143 /// counts.threshold_greater_or_equal_uniform(threshold)
1144 /// # .entries()
1145 /// # }, |mut stream| async move {
1146 /// // { 1: 5 } -- key 1's value 6 >= 5, but key 2's value 4 < 5
1147 /// # assert_eq!(stream.next().await.unwrap(), (1, 5));
1148 /// # }));
1149 /// # }
1150 /// ```
1151 pub fn threshold_greater_or_equal_uniform(
1152 self,
1153 threshold: Singleton<V, L, Bounded>,
1154 ) -> KeyedStream<K, V, L, B::UnderlyingBound, NoOrder, ExactlyOnce>
1155 where
1156 K: Clone + Eq + Hash,
1157 V: Clone + PartialOrd,
1158 B: IsKeyedMonotonic,
1159 {
1160 let self_location = self.location.clone();
1161 match B::bound_kind() {
1162 KeyedSingletonBoundKind::Bounded => {
1163 let me: KeyedSingleton<K, V, L, Bounded> = KeyedSingleton::new(
1164 self.location.clone(),
1165 self.ir_node.replace(HydroNode::Placeholder),
1166 );
1167 let result = me
1168 .entries()
1169 .cross_singleton(threshold)
1170 .filter_map(q!(|((k, val), thresh)| {
1171 if val >= thresh {
1172 Some((k, thresh))
1173 } else {
1174 None
1175 }
1176 }))
1177 .into_keyed();
1178 KeyedStream::new(
1179 result.location.clone(),
1180 result.ir_node.replace(HydroNode::Placeholder),
1181 )
1182 }
1183 KeyedSingletonBoundKind::MonotonicValue => {
1184 let me: KeyedSingleton<K, V, L, MonotonicValue> = KeyedSingleton::new(
1185 self.location.clone(),
1186 self.ir_node.replace(HydroNode::Placeholder),
1187 );
1188
1189 let result = sliced! {
1190 let snapshot = use::snapshot(me, nondet!(/** thresholds are deterministic */));
1191 let mut already_crossed =
1192 use::state_null::<Stream<K, Tick<_>, Bounded, NoOrder>>();
1193
1194 let tick = snapshot.location().clone();
1195 let thresh_in_tick = threshold.clone_into_tick(&tick);
1196
1197 let crossing = snapshot
1198 .entries()
1199 .cross_singleton(thresh_in_tick)
1200 .filter_map(q!(|((k, val), thresh)| {
1201 if val >= thresh {
1202 Some((k, thresh))
1203 } else {
1204 None
1205 }
1206 }));
1207
1208 let newly_crossed = crossing.anti_join(already_crossed.clone());
1209 already_crossed =
1210 already_crossed.chain(newly_crossed.clone().map(q!(|(k, _)| k)));
1211
1212 newly_crossed.into_keyed()
1213 };
1214
1215 KeyedStream::new(
1216 self_location,
1217 result.ir_node.replace(HydroNode::Placeholder),
1218 )
1219 }
1220 KeyedSingletonBoundKind::BoundedValue => {
1221 let me: KeyedSingleton<K, V, L, BoundedValue> = KeyedSingleton::new(
1222 self.location.clone(),
1223 self.ir_node.replace(HydroNode::Placeholder),
1224 );
1225
1226 let result = sliced! {
1227 let snapshot = use::batch(me, nondet!(/** thresholds are deterministic */));
1228 let mut already_crossed =
1229 use::state_null::<Stream<K, Tick<_>, Bounded, NoOrder>>();
1230
1231 let tick = snapshot.location().clone();
1232 let thresh_in_tick = threshold.clone_into_tick(&tick);
1233
1234 let crossing = snapshot
1235 .entries()
1236 .cross_singleton(thresh_in_tick)
1237 .filter_map(q!(|((k, val), thresh)| {
1238 if val >= thresh {
1239 Some((k, thresh))
1240 } else {
1241 None
1242 }
1243 }));
1244
1245 let newly_crossed = crossing.anti_join(already_crossed.clone());
1246 already_crossed =
1247 already_crossed.chain(newly_crossed.clone().map(q!(|(k, _)| k)));
1248
1249 newly_crossed.into_keyed()
1250 };
1251
1252 KeyedStream::new(
1253 self_location,
1254 result.ir_node.replace(HydroNode::Placeholder),
1255 )
1256 }
1257 _ => {
1258 unreachable!(
1259 "IsKeyedMonotonic is only implemented for Bounded, BoundedValue, and MonotonicValue"
1260 )
1261 }
1262 }
1263 }
1264}
1265
1266impl<'a, K, V, L: Location<'a>, B: KeyedSingletonBound<ValueBound = Bounded>>
1267 KeyedSingleton<K, V, L, B>
1268{
1269 /// Flattens the keyed singleton into an unordered stream of key-value pairs.
1270 ///
1271 /// The value for each key must be bounded, otherwise the resulting stream elements would be
1272 /// non-deterministic. As new entries are added to the keyed singleton, they will be streamed
1273 /// into the output.
1274 ///
1275 /// # Example
1276 /// ```rust
1277 /// # #[cfg(feature = "deploy")] {
1278 /// # use hydro_lang::prelude::*;
1279 /// # use futures::StreamExt;
1280 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1281 /// let keyed_singleton = // { 1: 2, 2: 4 }
1282 /// # process
1283 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1284 /// # .into_keyed()
1285 /// # .first();
1286 /// keyed_singleton.entries()
1287 /// # }, |mut stream| async move {
1288 /// // (1, 2), (2, 4) in any order
1289 /// # let mut results = Vec::new();
1290 /// # for _ in 0..2 {
1291 /// # results.push(stream.next().await.unwrap());
1292 /// # }
1293 /// # results.sort();
1294 /// # assert_eq!(results, vec![(1, 2), (2, 4)]);
1295 /// # }));
1296 /// # }
1297 /// ```
1298 pub fn entries(self) -> Stream<(K, V), L, B::UnderlyingBound, NoOrder, ExactlyOnce> {
1299 self.into_keyed_stream().entries()
1300 }
1301
1302 /// Flattens the keyed singleton into an unordered stream of just the values.
1303 ///
1304 /// The value for each key must be bounded, otherwise the resulting stream elements would be
1305 /// non-deterministic. As new entries are added to the keyed singleton, they will be streamed
1306 /// into the output.
1307 ///
1308 /// # Example
1309 /// ```rust
1310 /// # #[cfg(feature = "deploy")] {
1311 /// # use hydro_lang::prelude::*;
1312 /// # use futures::StreamExt;
1313 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1314 /// let keyed_singleton = // { 1: 2, 2: 4 }
1315 /// # process
1316 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1317 /// # .into_keyed()
1318 /// # .first();
1319 /// keyed_singleton.values()
1320 /// # }, |mut stream| async move {
1321 /// // 2, 4 in any order
1322 /// # let mut results = Vec::new();
1323 /// # for _ in 0..2 {
1324 /// # results.push(stream.next().await.unwrap());
1325 /// # }
1326 /// # results.sort();
1327 /// # assert_eq!(results, vec![2, 4]);
1328 /// # }));
1329 /// # }
1330 /// ```
1331 pub fn values(self) -> Stream<V, L, B::UnderlyingBound, NoOrder, ExactlyOnce> {
1332 let map_f = q!(|(_, v)| v)
1333 .splice_fn1_ctx::<(K, V), V>(&self.location)
1334 .into();
1335
1336 Stream::new(
1337 self.location.clone(),
1338 HydroNode::Map {
1339 f: map_f,
1340 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1341 metadata: self.location.new_node_metadata(Stream::<
1342 V,
1343 L,
1344 B::UnderlyingBound,
1345 NoOrder,
1346 ExactlyOnce,
1347 >::collection_kind()),
1348 },
1349 )
1350 }
1351
1352 /// Flattens the keyed singleton into an unordered stream of just the keys.
1353 ///
1354 /// The value for each key must be bounded, otherwise the removal of keys would result in
1355 /// non-determinism. As new entries are added to the keyed singleton, they will be streamed
1356 /// into the output.
1357 ///
1358 /// # Example
1359 /// ```rust
1360 /// # #[cfg(feature = "deploy")] {
1361 /// # use hydro_lang::prelude::*;
1362 /// # use futures::StreamExt;
1363 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1364 /// let keyed_singleton = // { 1: 2, 2: 4 }
1365 /// # process
1366 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1367 /// # .into_keyed()
1368 /// # .first();
1369 /// keyed_singleton.keys()
1370 /// # }, |mut stream| async move {
1371 /// // 1, 2 in any order
1372 /// # let mut results = Vec::new();
1373 /// # for _ in 0..2 {
1374 /// # results.push(stream.next().await.unwrap());
1375 /// # }
1376 /// # results.sort();
1377 /// # assert_eq!(results, vec![1, 2]);
1378 /// # }));
1379 /// # }
1380 /// ```
1381 pub fn keys(self) -> Stream<K, L, B::UnderlyingBound, NoOrder, ExactlyOnce> {
1382 self.entries().map(q!(|(k, _)| k))
1383 }
1384
1385 /// Given a bounded stream of keys `K`, returns a new keyed singleton containing only the
1386 /// entries whose keys are not in the provided stream.
1387 ///
1388 /// # Example
1389 /// ```rust
1390 /// # #[cfg(feature = "deploy")] {
1391 /// # use hydro_lang::prelude::*;
1392 /// # use futures::StreamExt;
1393 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1394 /// let tick = process.tick();
1395 /// let keyed_singleton = // { 1: 2, 2: 4 }
1396 /// # process
1397 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1398 /// # .into_keyed()
1399 /// # .first()
1400 /// # .batch(&tick, nondet!(/** test */));
1401 /// let keys_to_remove = process
1402 /// .source_iter(q!(vec![1]))
1403 /// .batch(&tick, nondet!(/** test */));
1404 /// keyed_singleton.filter_key_not_in(keys_to_remove)
1405 /// # .entries().all_ticks()
1406 /// # }, |mut stream| async move {
1407 /// // { 2: 4 }
1408 /// # for w in vec![(2, 4)] {
1409 /// # assert_eq!(stream.next().await.unwrap(), w);
1410 /// # }
1411 /// # }));
1412 /// # }
1413 /// ```
1414 pub fn filter_key_not_in<O2: Ordering, R2: Retries>(
1415 self,
1416 other: Stream<K, L, Bounded, O2, R2>,
1417 ) -> Self
1418 where
1419 K: Hash + Eq,
1420 {
1421 check_matching_location(&self.location, &other.location);
1422
1423 KeyedSingleton::new(
1424 self.location.clone(),
1425 HydroNode::AntiJoin {
1426 pos: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1427 neg: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
1428 metadata: self.location.new_node_metadata(Self::collection_kind()),
1429 },
1430 )
1431 }
1432
1433 /// An operator which allows you to "inspect" each value of a keyed singleton without
1434 /// modifying it. The closure `f` is called on a reference to each value. This is
1435 /// mainly useful for debugging, and should not be used to generate side-effects.
1436 ///
1437 /// # Example
1438 /// ```rust
1439 /// # #[cfg(feature = "deploy")] {
1440 /// # use hydro_lang::prelude::*;
1441 /// # use futures::StreamExt;
1442 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1443 /// let keyed_singleton = // { 1: 2, 2: 4 }
1444 /// # process
1445 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1446 /// # .into_keyed()
1447 /// # .first();
1448 /// keyed_singleton
1449 /// .inspect(q!(|v| println!("{}", v)))
1450 /// # .entries()
1451 /// # }, |mut stream| async move {
1452 /// // { 1: 2, 2: 4 }
1453 /// # for w in vec![(1, 2), (2, 4)] {
1454 /// # assert_eq!(stream.next().await.unwrap(), w);
1455 /// # }
1456 /// # }));
1457 /// # }
1458 /// ```
1459 pub fn inspect<F>(self, f: impl IntoQuotedMut<'a, F, L> + Copy) -> Self
1460 where
1461 F: Fn(&V) + 'a,
1462 {
1463 let f: ManualExpr<F, _> = ManualExpr::new(move |ctx: &L| f.splice_fn1_borrow_ctx(ctx));
1464 let inspect_f = q!({
1465 let orig = f;
1466 move |t: &(_, _)| orig(&t.1)
1467 })
1468 .splice_fn1_borrow_ctx::<(K, V), ()>(&self.location)
1469 .into();
1470
1471 KeyedSingleton::new(
1472 self.location.clone(),
1473 HydroNode::Inspect {
1474 f: inspect_f,
1475 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1476 metadata: self.location.new_node_metadata(Self::collection_kind()),
1477 },
1478 )
1479 }
1480
1481 /// An operator which allows you to "inspect" each entry of a keyed singleton without
1482 /// modifying it. The closure `f` is called on a reference to each key-value pair. This is
1483 /// mainly useful for debugging, and should not be used to generate side-effects.
1484 ///
1485 /// # Example
1486 /// ```rust
1487 /// # #[cfg(feature = "deploy")] {
1488 /// # use hydro_lang::prelude::*;
1489 /// # use futures::StreamExt;
1490 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1491 /// let keyed_singleton = // { 1: 2, 2: 4 }
1492 /// # process
1493 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1494 /// # .into_keyed()
1495 /// # .first();
1496 /// keyed_singleton
1497 /// .inspect_with_key(q!(|(k, v)| println!("{}: {}", k, v)))
1498 /// # .entries()
1499 /// # }, |mut stream| async move {
1500 /// // { 1: 2, 2: 4 }
1501 /// # for w in vec![(1, 2), (2, 4)] {
1502 /// # assert_eq!(stream.next().await.unwrap(), w);
1503 /// # }
1504 /// # }));
1505 /// # }
1506 /// ```
1507 pub fn inspect_with_key<F>(self, f: impl IntoQuotedMut<'a, F, L>) -> Self
1508 where
1509 F: Fn(&(K, V)) + 'a,
1510 {
1511 let inspect_f = f.splice_fn1_borrow_ctx::<(K, V), ()>(&self.location).into();
1512
1513 KeyedSingleton::new(
1514 self.location.clone(),
1515 HydroNode::Inspect {
1516 f: inspect_f,
1517 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1518 metadata: self.location.new_node_metadata(Self::collection_kind()),
1519 },
1520 )
1521 }
1522
1523 /// Gets the key-value tuple with the largest key among all entries in this [`KeyedSingleton`].
1524 ///
1525 /// Because this method requires values to be bounded, the output [`Optional`] will only be
1526 /// asynchronously updated if a new key is added that is higher than the previous max key.
1527 ///
1528 /// # Example
1529 /// ```rust
1530 /// # #[cfg(feature = "deploy")] {
1531 /// # use hydro_lang::prelude::*;
1532 /// # use futures::StreamExt;
1533 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1534 /// let tick = process.tick();
1535 /// let keyed_singleton = // { 1: 123, 2: 456, 0: 789 }
1536 /// # Stream::<_, _>::from(process.source_iter(q!(vec![(1, 123), (2, 456), (0, 789)])))
1537 /// # .into_keyed()
1538 /// # .first();
1539 /// keyed_singleton.get_max_key()
1540 /// # .sample_eager(nondet!(/** test */))
1541 /// # }, |mut stream| async move {
1542 /// // (2, 456)
1543 /// # assert_eq!(stream.next().await.unwrap(), (2, 456));
1544 /// # }));
1545 /// # }
1546 /// ```
1547 pub fn get_max_key(self) -> Optional<(K, V), L, B::UnderlyingBound>
1548 where
1549 K: Ord,
1550 {
1551 self.entries()
1552 .assume_ordering_trusted(nondet!(
1553 /// There is only one element associated with each key, and the keys are totallly
1554 /// ordered so we will produce a deterministic value. The closure technically
1555 /// isn't commutative in the case where both passed entries have the same key
1556 /// but different values.
1557 ///
1558 /// In the future, we may want to have an `assume!(...)` statement in the UDF that
1559 /// the two inputs do not have the same key.
1560 ))
1561 .reduce(q!(
1562 move |curr, new| {
1563 if new.0 > curr.0 {
1564 *curr = new;
1565 }
1566 },
1567 idempotent = manual_proof!(/** repeated elements are ignored */)
1568 ))
1569 }
1570
1571 /// Converts this keyed singleton into a [`KeyedStream`] with each group having a single
1572 /// element, the value.
1573 ///
1574 /// This is the equivalent of [`Singleton::into_stream`] but keyed.
1575 ///
1576 /// # Example
1577 /// ```rust
1578 /// # #[cfg(feature = "deploy")] {
1579 /// # use hydro_lang::prelude::*;
1580 /// # use futures::StreamExt;
1581 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1582 /// let keyed_singleton = // { 1: 2, 2: 4 }
1583 /// # Stream::<_, _>::from(process.source_iter(q!(vec![(1, 2), (2, 4)])))
1584 /// # .into_keyed()
1585 /// # .first();
1586 /// keyed_singleton
1587 /// .clone()
1588 /// .into_keyed_stream()
1589 /// .merge_unordered(
1590 /// keyed_singleton.into_keyed_stream()
1591 /// )
1592 /// # .entries()
1593 /// # }, |mut stream| async move {
1594 /// /// // { 1: [2, 2], 2: [4, 4] }
1595 /// # for w in vec![(1, 2), (2, 4), (1, 2), (2, 4)] {
1596 /// # assert_eq!(stream.next().await.unwrap(), w);
1597 /// # }
1598 /// # }));
1599 /// # }
1600 /// ```
1601 pub fn into_keyed_stream(
1602 self,
1603 ) -> KeyedStream<K, V, L, B::UnderlyingBound, TotalOrder, ExactlyOnce> {
1604 KeyedStream::new(
1605 self.location.clone(),
1606 HydroNode::Cast {
1607 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1608 metadata: self.location.new_node_metadata(KeyedStream::<
1609 K,
1610 V,
1611 L,
1612 B::UnderlyingBound,
1613 TotalOrder,
1614 ExactlyOnce,
1615 >::collection_kind()),
1616 },
1617 )
1618 }
1619}
1620
1621impl<'a, K, V, L, B: KeyedSingletonBound> KeyedSingleton<K, V, L, B>
1622where
1623 L: Location<'a>,
1624 B: KeyedSingletonBound<ValueBound = Bounded>,
1625{
1626 /// Shifts this bounded-value keyed singleton into an atomic context, which guarantees that any downstream logic
1627 /// will all be executed synchronously before any outputs are yielded (in [`KeyedSingleton::end_atomic`]).
1628 ///
1629 /// This is useful to enforce local consistency constraints, such as ensuring that a write is
1630 /// processed before an acknowledgement is emitted.
1631 pub fn atomic(self) -> KeyedSingleton<K, V, Atomic<L>, B> {
1632 let id = self.location.flow_state().borrow_mut().next_clock_id();
1633 let out_location = Atomic {
1634 tick: Tick {
1635 id,
1636 l: self.location.clone(),
1637 },
1638 };
1639 KeyedSingleton::new(
1640 out_location.clone(),
1641 HydroNode::BeginAtomic {
1642 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1643 metadata: out_location
1644 .new_node_metadata(KeyedSingleton::<K, V, Atomic<L>, B>::collection_kind()),
1645 },
1646 )
1647 }
1648}
1649
1650impl<'a, K, V, L, B: KeyedSingletonBound> KeyedSingleton<K, V, Atomic<L>, B>
1651where
1652 L: Location<'a>,
1653{
1654 /// Yields the elements of this keyed singleton back into a top-level, asynchronous execution context.
1655 /// See [`KeyedSingleton::atomic`] for more details.
1656 pub fn end_atomic(self) -> KeyedSingleton<K, V, L, B> {
1657 KeyedSingleton::new(
1658 self.location.tick.l.clone(),
1659 HydroNode::EndAtomic {
1660 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1661 metadata: self
1662 .location
1663 .tick
1664 .l
1665 .new_node_metadata(KeyedSingleton::<K, V, L, B>::collection_kind()),
1666 },
1667 )
1668 }
1669}
1670
1671impl<'a, K, V, L: Location<'a>> KeyedSingleton<K, V, Tick<L>, Bounded> {
1672 /// Shifts the state in `self` to the **next tick**, so that the returned keyed singleton at
1673 /// tick `T` always has the entries of `self` at tick `T - 1`.
1674 ///
1675 /// At tick `0`, the output has no entries, since there is no previous tick.
1676 ///
1677 /// This operator enables stateful iterative processing with ticks, by sending data from one
1678 /// tick to the next. For example, you can use it to compare state across consecutive batches.
1679 ///
1680 /// # Example
1681 /// ```rust
1682 /// # #[cfg(feature = "deploy")] {
1683 /// # use hydro_lang::prelude::*;
1684 /// # use futures::StreamExt;
1685 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1686 /// let tick = process.tick();
1687 /// # // ticks are lazy by default, forces the second tick to run
1688 /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1689 /// # let batch_first_tick = process
1690 /// # .source_iter(q!(vec![(1, 2), (2, 3)]))
1691 /// # .batch(&tick, nondet!(/** test */))
1692 /// # .into_keyed();
1693 /// # let batch_second_tick = process
1694 /// # .source_iter(q!(vec![(2, 4), (3, 5)]))
1695 /// # .batch(&tick, nondet!(/** test */))
1696 /// # .into_keyed()
1697 /// # .defer_tick(); // appears on the second tick
1698 /// let input_batch = // first tick: { 1: 2, 2: 3 }, second tick: { 2: 4, 3: 5 }
1699 /// # batch_first_tick.chain(batch_second_tick).first();
1700 /// input_batch.clone().filter_key_not_in(
1701 /// input_batch.defer_tick().keys() // keys present in the previous tick
1702 /// )
1703 /// # .entries().all_ticks()
1704 /// # }, |mut stream| async move {
1705 /// // { 1: 2, 2: 3 } (first tick), { 3: 5 } (second tick)
1706 /// # for w in vec![(1, 2), (2, 3), (3, 5)] {
1707 /// # assert_eq!(stream.next().await.unwrap(), w);
1708 /// # }
1709 /// # }));
1710 /// # }
1711 /// ```
1712 pub fn defer_tick(self) -> KeyedSingleton<K, V, Tick<L>, Bounded> {
1713 KeyedSingleton::new(
1714 self.location.clone(),
1715 HydroNode::DeferTick {
1716 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1717 metadata: self
1718 .location
1719 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
1720 },
1721 )
1722 }
1723}
1724
1725impl<'a, K, V, L, B: KeyedSingletonBound<ValueBound = Unbounded>> KeyedSingleton<K, V, L, B>
1726where
1727 L: Location<'a>,
1728{
1729 /// Returns a keyed singleton with a snapshot of each key-value entry at a non-deterministic
1730 /// point in time.
1731 ///
1732 /// # Non-Determinism
1733 /// Because this picks a snapshot of each entry, which is continuously changing, each output has a
1734 /// non-deterministic set of entries since each snapshot can be at an arbitrary point in time.
1735 pub fn snapshot<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1736 self,
1737 tick: &Tick<L2>,
1738 _nondet: NonDet,
1739 ) -> KeyedSingleton<K, V, Tick<L::DropConsistency>, Bounded> {
1740 assert_eq!(Location::id(tick.outer()), Location::id(&self.location));
1741 KeyedSingleton::new(
1742 tick.drop_consistency(),
1743 HydroNode::Batch {
1744 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1745 metadata: tick
1746 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
1747 },
1748 )
1749 }
1750}
1751
1752impl<'a, K, V, L, B: KeyedSingletonBound<ValueBound = Unbounded>> KeyedSingleton<K, V, Atomic<L>, B>
1753where
1754 L: Location<'a>,
1755{
1756 /// Returns a keyed singleton with a snapshot of each key-value entry, consistent with the
1757 /// state of the keyed singleton being atomically processed.
1758 ///
1759 /// # Non-Determinism
1760 /// Because this picks a snapshot of each entry, which is continuously changing, each output has a
1761 /// non-deterministic set of entries since each snapshot can be at an arbitrary point in time.
1762 pub fn snapshot_atomic<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1763 self,
1764 tick: &Tick<L2>,
1765 _nondet: NonDet,
1766 ) -> KeyedSingleton<K, V, Tick<L::DropConsistency>, Bounded> {
1767 KeyedSingleton::new(
1768 tick.drop_consistency(),
1769 HydroNode::Batch {
1770 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1771 metadata: tick
1772 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
1773 },
1774 )
1775 }
1776}
1777
1778impl<'a, K, V, L, B: KeyedSingletonBound<ValueBound = Bounded>> KeyedSingleton<K, V, L, B>
1779where
1780 L: Location<'a>,
1781{
1782 /// Creates a keyed singleton containing only the key-value pairs where the value satisfies a predicate `f`.
1783 ///
1784 /// The closure `f` receives a reference `&V` to each value and returns a boolean. If the predicate
1785 /// returns `true`, the key-value pair is included in the output. If it returns `false`, the pair
1786 /// is filtered out.
1787 ///
1788 /// The closure `f` receives a reference `&V` rather than an owned value `V` because filtering does
1789 /// not modify or take ownership of the values. If you need to modify the values while filtering
1790 /// use [`KeyedSingleton::filter_map`] instead.
1791 ///
1792 /// # Example
1793 /// ```rust
1794 /// # #[cfg(feature = "deploy")] {
1795 /// # use hydro_lang::prelude::*;
1796 /// # use futures::StreamExt;
1797 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1798 /// let keyed_singleton = // { 1: 2, 2: 4, 3: 1 }
1799 /// # process
1800 /// # .source_iter(q!(vec![(1, 2), (2, 4), (3, 1)]))
1801 /// # .into_keyed()
1802 /// # .first();
1803 /// keyed_singleton.filter(q!(|&v| v > 1))
1804 /// # .entries()
1805 /// # }, |mut stream| async move {
1806 /// // { 1: 2, 2: 4 }
1807 /// # let mut results = Vec::new();
1808 /// # for _ in 0..2 {
1809 /// # results.push(stream.next().await.unwrap());
1810 /// # }
1811 /// # results.sort();
1812 /// # assert_eq!(results, vec![(1, 2), (2, 4)]);
1813 /// # }));
1814 /// # }
1815 /// ```
1816 pub fn filter<F>(self, f: impl IntoQuotedMut<'a, F, L> + Copy) -> KeyedSingleton<K, V, L, B>
1817 where
1818 F: Fn(&V) -> bool + 'a,
1819 {
1820 let f: ManualExpr<F, _> = ManualExpr::new(move |ctx: &L| f.splice_fn1_borrow_ctx(ctx));
1821 let filter_f = q!({
1822 let orig = f;
1823 move |t: &(_, _)| orig(&t.1)
1824 })
1825 .splice_fn1_borrow_ctx::<(K, V), bool>(&self.location)
1826 .into();
1827
1828 KeyedSingleton::new(
1829 self.location.clone(),
1830 HydroNode::Filter {
1831 f: filter_f,
1832 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1833 metadata: self
1834 .location
1835 .new_node_metadata(KeyedSingleton::<K, V, L, B>::collection_kind()),
1836 },
1837 )
1838 }
1839
1840 /// An operator that both filters and maps values. It yields only the key-value pairs where
1841 /// the supplied closure `f` returns `Some(value)`.
1842 ///
1843 /// The closure `f` receives each value `V` and returns `Option<U>`. If the closure returns
1844 /// `Some(new_value)`, the key-value pair `(key, new_value)` is included in the output.
1845 /// If it returns `None`, the key-value pair is filtered out.
1846 ///
1847 /// # Example
1848 /// ```rust
1849 /// # #[cfg(feature = "deploy")] {
1850 /// # use hydro_lang::prelude::*;
1851 /// # use futures::StreamExt;
1852 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1853 /// let keyed_singleton = // { 1: "42", 2: "hello", 3: "100" }
1854 /// # process
1855 /// # .source_iter(q!(vec![(1, "42"), (2, "hello"), (3, "100")]))
1856 /// # .into_keyed()
1857 /// # .first();
1858 /// keyed_singleton.filter_map(q!(|s| s.parse::<i32>().ok()))
1859 /// # .entries()
1860 /// # }, |mut stream| async move {
1861 /// // { 1: 42, 3: 100 }
1862 /// # let mut results = Vec::new();
1863 /// # for _ in 0..2 {
1864 /// # results.push(stream.next().await.unwrap());
1865 /// # }
1866 /// # results.sort();
1867 /// # assert_eq!(results, vec![(1, 42), (3, 100)]);
1868 /// # }));
1869 /// # }
1870 /// ```
1871 pub fn filter_map<F, U>(
1872 self,
1873 f: impl IntoQuotedMut<'a, F, L> + Copy,
1874 ) -> KeyedSingleton<K, U, L, B::EraseMonotonic>
1875 where
1876 F: Fn(V) -> Option<U> + 'a,
1877 {
1878 let f: ManualExpr<F, _> = ManualExpr::new(move |ctx: &L| f.splice_fn1_ctx(ctx));
1879 let filter_map_f = q!({
1880 let orig = f;
1881 move |(k, v)| orig(v).map(|o| (k, o))
1882 })
1883 .splice_fn1_ctx::<(K, V), Option<(K, U)>>(&self.location)
1884 .into();
1885
1886 KeyedSingleton::new(
1887 self.location.clone(),
1888 HydroNode::FilterMap {
1889 f: filter_map_f,
1890 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1891 metadata: self.location.new_node_metadata(KeyedSingleton::<
1892 K,
1893 U,
1894 L,
1895 B::EraseMonotonic,
1896 >::collection_kind()),
1897 },
1898 )
1899 }
1900
1901 /// Returns a keyed singleton with entries consisting of _new_ key-value pairs that have
1902 /// arrived since the previous batch was released.
1903 ///
1904 /// Currently, there is no `all_ticks` dual on [`KeyedSingleton`], instead you may want to use
1905 /// [`KeyedSingleton::into_keyed_stream`] then yield with [`KeyedStream::all_ticks`].
1906 ///
1907 /// # Non-Determinism
1908 /// Because this picks a batch of asynchronously added entries, each output keyed singleton
1909 /// has a non-deterministic set of key-value pairs.
1910 pub fn batch<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1911 self,
1912 tick: &Tick<L2>,
1913 _nondet: NonDet,
1914 ) -> KeyedSingleton<K, V, Tick<L::DropConsistency>, Bounded> {
1915 assert_eq!(Location::id(tick.outer()), Location::id(&self.location));
1916 KeyedSingleton::new(
1917 tick.drop_consistency(),
1918 HydroNode::Batch {
1919 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1920 metadata: tick
1921 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
1922 },
1923 )
1924 }
1925}
1926
1927impl<'a, K, V, L, B: KeyedSingletonBound<ValueBound = Bounded>> KeyedSingleton<K, V, Atomic<L>, B>
1928where
1929 L: Location<'a>,
1930{
1931 /// Returns a keyed singleton with entries consisting of _new_ key-value pairs that are being
1932 /// atomically processed.
1933 ///
1934 /// Currently, there is no dual to asynchronously yield back outside the tick, instead you
1935 /// should use [`KeyedSingleton::into_keyed_stream`] and yield a [`KeyedStream`].
1936 ///
1937 /// # Non-Determinism
1938 /// Because this picks a batch of asynchronously added entries, each output keyed singleton
1939 /// has a non-deterministic set of key-value pairs.
1940 pub fn batch_atomic<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1941 self,
1942 tick: &Tick<L2>,
1943 nondet: NonDet,
1944 ) -> KeyedSingleton<K, V, Tick<L::DropConsistency>, Bounded> {
1945 let _ = nondet;
1946 KeyedSingleton::new(
1947 tick.drop_consistency(),
1948 HydroNode::Batch {
1949 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1950 metadata: tick
1951 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
1952 },
1953 )
1954 }
1955}
1956
1957#[cfg(test)]
1958mod tests {
1959 #[cfg(feature = "deploy")]
1960 use futures::{SinkExt, StreamExt};
1961 #[cfg(feature = "deploy")]
1962 use hydro_deploy::Deployment;
1963 #[cfg(any(feature = "deploy", feature = "sim"))]
1964 use stageleft::q;
1965
1966 #[cfg(any(feature = "deploy", feature = "sim"))]
1967 use crate::compile::builder::FlowBuilder;
1968 #[cfg(any(feature = "deploy", feature = "sim"))]
1969 use crate::location::Location;
1970 #[cfg(any(feature = "deploy", feature = "sim"))]
1971 use crate::nondet::nondet;
1972
1973 #[cfg(feature = "deploy")]
1974 #[tokio::test]
1975 async fn key_count_bounded_value() {
1976 let mut deployment = Deployment::new();
1977
1978 let mut flow = FlowBuilder::new();
1979 let node = flow.process::<()>();
1980 let external = flow.external::<()>();
1981
1982 let (input_port, input) = node.source_external_bincode(&external);
1983 let out = input
1984 .into_keyed()
1985 .first()
1986 .key_count()
1987 .sample_eager(nondet!(/** test */))
1988 .send_bincode_external(&external);
1989
1990 let nodes = flow
1991 .with_process(&node, deployment.Localhost())
1992 .with_external(&external, deployment.Localhost())
1993 .deploy(&mut deployment);
1994
1995 deployment.deploy().await.unwrap();
1996
1997 let mut external_in = nodes.connect(input_port).await;
1998 let mut external_out = nodes.connect(out).await;
1999
2000 deployment.start().await.unwrap();
2001
2002 assert_eq!(external_out.next().await.unwrap(), 0);
2003
2004 external_in.send((1, 1)).await.unwrap();
2005 assert_eq!(external_out.next().await.unwrap(), 1);
2006
2007 external_in.send((2, 2)).await.unwrap();
2008 assert_eq!(external_out.next().await.unwrap(), 2);
2009 }
2010
2011 #[cfg(feature = "deploy")]
2012 #[tokio::test]
2013 async fn key_count_unbounded_value() {
2014 let mut deployment = Deployment::new();
2015
2016 let mut flow = FlowBuilder::new();
2017 let node = flow.process::<()>();
2018 let external = flow.external::<()>();
2019
2020 let (input_port, input) = node.source_external_bincode(&external);
2021 let out = input
2022 .into_keyed()
2023 .fold(q!(|| 0), q!(|acc, _| *acc += 1))
2024 .key_count()
2025 .sample_eager(nondet!(/** test */))
2026 .send_bincode_external(&external);
2027
2028 let nodes = flow
2029 .with_process(&node, deployment.Localhost())
2030 .with_external(&external, deployment.Localhost())
2031 .deploy(&mut deployment);
2032
2033 deployment.deploy().await.unwrap();
2034
2035 let mut external_in = nodes.connect(input_port).await;
2036 let mut external_out = nodes.connect(out).await;
2037
2038 deployment.start().await.unwrap();
2039
2040 assert_eq!(external_out.next().await.unwrap(), 0);
2041
2042 external_in.send((1, 1)).await.unwrap();
2043 assert_eq!(external_out.next().await.unwrap(), 1);
2044
2045 external_in.send((1, 2)).await.unwrap();
2046 assert_eq!(external_out.next().await.unwrap(), 1);
2047
2048 external_in.send((2, 2)).await.unwrap();
2049 assert_eq!(external_out.next().await.unwrap(), 2);
2050
2051 external_in.send((1, 1)).await.unwrap();
2052 assert_eq!(external_out.next().await.unwrap(), 2);
2053
2054 external_in.send((3, 1)).await.unwrap();
2055 assert_eq!(external_out.next().await.unwrap(), 3);
2056 }
2057
2058 #[cfg(feature = "deploy")]
2059 #[tokio::test]
2060 async fn into_singleton_bounded_value() {
2061 let mut deployment = Deployment::new();
2062
2063 let mut flow = FlowBuilder::new();
2064 let node = flow.process::<()>();
2065 let external = flow.external::<()>();
2066
2067 let (input_port, input) = node.source_external_bincode(&external);
2068 let out = input
2069 .into_keyed()
2070 .first()
2071 .into_singleton()
2072 .sample_eager(nondet!(/** test */))
2073 .send_bincode_external(&external);
2074
2075 let nodes = flow
2076 .with_process(&node, deployment.Localhost())
2077 .with_external(&external, deployment.Localhost())
2078 .deploy(&mut deployment);
2079
2080 deployment.deploy().await.unwrap();
2081
2082 let mut external_in = nodes.connect(input_port).await;
2083 let mut external_out = nodes.connect(out).await;
2084
2085 deployment.start().await.unwrap();
2086
2087 assert_eq!(
2088 external_out.next().await.unwrap(),
2089 std::collections::HashMap::new()
2090 );
2091
2092 external_in.send((1, 1)).await.unwrap();
2093 assert_eq!(
2094 external_out.next().await.unwrap(),
2095 vec![(1, 1)].into_iter().collect()
2096 );
2097
2098 external_in.send((2, 2)).await.unwrap();
2099 assert_eq!(
2100 external_out.next().await.unwrap(),
2101 vec![(1, 1), (2, 2)].into_iter().collect()
2102 );
2103 }
2104
2105 #[cfg(feature = "deploy")]
2106 #[tokio::test]
2107 async fn into_singleton_unbounded_value() {
2108 let mut deployment = Deployment::new();
2109
2110 let mut flow = FlowBuilder::new();
2111 let node = flow.process::<()>();
2112 let external = flow.external::<()>();
2113
2114 let (input_port, input) = node.source_external_bincode(&external);
2115 let out = input
2116 .into_keyed()
2117 .fold(q!(|| 0), q!(|acc, _| *acc += 1))
2118 .into_singleton()
2119 .sample_eager(nondet!(/** test */))
2120 .send_bincode_external(&external);
2121
2122 let nodes = flow
2123 .with_process(&node, deployment.Localhost())
2124 .with_external(&external, deployment.Localhost())
2125 .deploy(&mut deployment);
2126
2127 deployment.deploy().await.unwrap();
2128
2129 let mut external_in = nodes.connect(input_port).await;
2130 let mut external_out = nodes.connect(out).await;
2131
2132 deployment.start().await.unwrap();
2133
2134 assert_eq!(
2135 external_out.next().await.unwrap(),
2136 std::collections::HashMap::new()
2137 );
2138
2139 external_in.send((1, 1)).await.unwrap();
2140 assert_eq!(
2141 external_out.next().await.unwrap(),
2142 vec![(1, 1)].into_iter().collect()
2143 );
2144
2145 external_in.send((1, 2)).await.unwrap();
2146 assert_eq!(
2147 external_out.next().await.unwrap(),
2148 vec![(1, 2)].into_iter().collect()
2149 );
2150
2151 external_in.send((2, 2)).await.unwrap();
2152 assert_eq!(
2153 external_out.next().await.unwrap(),
2154 vec![(1, 2), (2, 1)].into_iter().collect()
2155 );
2156
2157 external_in.send((1, 1)).await.unwrap();
2158 assert_eq!(
2159 external_out.next().await.unwrap(),
2160 vec![(1, 3), (2, 1)].into_iter().collect()
2161 );
2162
2163 external_in.send((3, 1)).await.unwrap();
2164 assert_eq!(
2165 external_out.next().await.unwrap(),
2166 vec![(1, 3), (2, 1), (3, 1)].into_iter().collect()
2167 );
2168 }
2169
2170 #[cfg(feature = "sim")]
2171 #[test]
2172 fn sim_unbounded_singleton_snapshot() {
2173 let mut flow = FlowBuilder::new();
2174 let node = flow.process::<()>();
2175
2176 let (input_port, input) = node.sim_input();
2177 let output = input
2178 .into_keyed()
2179 .fold(q!(|| 0), q!(|acc, _| *acc += 1))
2180 .snapshot(&node.tick(), nondet!(/** test */))
2181 .entries()
2182 .all_ticks()
2183 .sim_output();
2184
2185 let count = flow.sim().exhaustive(async || {
2186 input_port.send((1, 123));
2187 input_port.send((1, 456));
2188 input_port.send((2, 123));
2189
2190 let all = output.collect_sorted::<Vec<_>>().await;
2191 assert_eq!(all.last().unwrap(), &(2, 1));
2192 });
2193
2194 assert_eq!(count, 8);
2195 }
2196
2197 #[cfg(feature = "deploy")]
2198 #[tokio::test]
2199 async fn join_keyed_stream() {
2200 let mut deployment = Deployment::new();
2201
2202 let mut flow = FlowBuilder::new();
2203 let node = flow.process::<()>();
2204 let external = flow.external::<()>();
2205
2206 let tick = node.tick();
2207 let keyed_data = node
2208 .source_iter(q!(vec![(1, 10), (2, 20)]))
2209 .into_keyed()
2210 .batch(&tick, nondet!(/** test */))
2211 .first();
2212 let requests = node
2213 .source_iter(q!(vec![(1, 100), (2, 200), (3, 300)]))
2214 .into_keyed()
2215 .batch(&tick, nondet!(/** test */));
2216
2217 let out = keyed_data
2218 .join_keyed_stream(requests)
2219 .entries()
2220 .all_ticks()
2221 .send_bincode_external(&external);
2222
2223 let nodes = flow
2224 .with_process(&node, deployment.Localhost())
2225 .with_external(&external, deployment.Localhost())
2226 .deploy(&mut deployment);
2227
2228 deployment.deploy().await.unwrap();
2229
2230 let mut external_out = nodes.connect(out).await;
2231
2232 deployment.start().await.unwrap();
2233
2234 let mut results = vec![];
2235 for _ in 0..2 {
2236 results.push(external_out.next().await.unwrap());
2237 }
2238 results.sort();
2239
2240 assert_eq!(results, vec![(1, (10, 100)), (2, (20, 200))]);
2241 }
2242
2243 #[cfg(feature = "sim")]
2244 #[test]
2245 fn threshold_greater_or_equal_monotonic() {
2246 let mut flow = FlowBuilder::new();
2247 let node = flow.process::<()>();
2248
2249 let (input_port, input) = node.sim_input::<(u32, usize), _, _>();
2250 let (thresh_port, thresh_input) = node.sim_input::<(u32, usize), _, _>();
2251
2252 // Create a monotonically increasing keyed singleton via fold with monotone proof
2253 let counts: super::KeyedSingleton<u32, usize, _, super::MonotonicValue> =
2254 input.into_keyed().fold(
2255 q!(|| 0usize),
2256 q!(
2257 |acc, v| *acc += v,
2258 monotone = crate::properties::manual_proof!(/** += is monotonic */)
2259 ),
2260 );
2261
2262 // BoundedValue keyed singleton of thresholds (from .first() on unbounded stream)
2263 let thresholds = thresh_input.into_keyed().first();
2264
2265 let output = counts
2266 .threshold_greater_or_equal(thresholds)
2267 .entries()
2268 .sim_output();
2269
2270 let count = flow.sim().exhaustive(async || {
2271 // Set thresholds: key 1 needs value >= 5, key 2 needs value >= 10
2272 thresh_port.send((1, 5));
2273 thresh_port.send((2, 10));
2274
2275 // key 1 gets increments: 3 + 3 = 6, which is >= 5 ✓
2276 input_port.send((1, 3));
2277 input_port.send((1, 3));
2278 // key 2 gets increments: 3 + 3 = 6, which is < 10 ✗
2279 input_port.send((2, 3));
2280 input_port.send((2, 3));
2281
2282 let results = output.collect_sorted::<Vec<_>>().await;
2283 assert_eq!(results, vec![(1, 5)]);
2284 });
2285
2286 assert!(count > 0);
2287 }
2288
2289 #[cfg(feature = "sim")]
2290 #[test]
2291 fn threshold_greater_or_equal_uniform() {
2292 let mut flow = FlowBuilder::new();
2293 let node = flow.process::<()>();
2294
2295 let (input_port, input) = node.sim_input::<(u32, usize), _, _>();
2296
2297 let counts: super::KeyedSingleton<u32, usize, _, super::MonotonicValue> =
2298 input.into_keyed().fold(
2299 q!(|| 0usize),
2300 q!(
2301 |acc, v| *acc += v,
2302 monotone = crate::properties::manual_proof!(/** += is monotonic */)
2303 ),
2304 );
2305
2306 // Uniform threshold: all keys need value >= 5
2307 let threshold = node.singleton(q!(5usize));
2308
2309 let output = counts
2310 .threshold_greater_or_equal_uniform(threshold)
2311 .entries()
2312 .sim_output();
2313
2314 let count = flow.sim().exhaustive(async || {
2315 // key 1: 3 + 3 = 6 >= 5 ✓
2316 input_port.send((1, 3));
2317 input_port.send((1, 3));
2318 // key 2: 2 + 2 = 4 < 5 ✗
2319 input_port.send((2, 2));
2320 input_port.send((2, 2));
2321
2322 let results = output.collect_sorted::<Vec<_>>().await;
2323 assert_eq!(results, vec![(1, 5)]);
2324 });
2325
2326 assert!(count > 0);
2327 }
2328
2329 #[cfg(feature = "sim")]
2330 #[test]
2331 fn threshold_greater_or_equal_bounded_value() {
2332 let mut flow = FlowBuilder::new();
2333 let node = flow.process::<()>();
2334
2335 let (input_port, input) = node.sim_input::<(u32, usize), _, _>();
2336 let (thresh_port, thresh_input) = node.sim_input::<(u32, usize), _, _>();
2337
2338 // BoundedValue keyed singleton (values fixed once per key via .first())
2339 let values = input.into_keyed().first();
2340
2341 // BoundedValue keyed singleton of thresholds
2342 let thresholds = thresh_input.into_keyed().first();
2343
2344 let output = values
2345 .threshold_greater_or_equal(thresholds)
2346 .entries()
2347 .sim_output();
2348
2349 let count = flow.sim().exhaustive(async || {
2350 // Set thresholds: key 1 needs >= 3, key 2 needs >= 10
2351 thresh_port.send((1, 3));
2352 thresh_port.send((2, 10));
2353
2354 // key 1 gets value 5 >= 3 ✓, key 2 gets value 4 < 10 ✗
2355 input_port.send((1, 5));
2356 input_port.send((2, 4));
2357
2358 let results = output.collect_sorted::<Vec<_>>().await;
2359 assert_eq!(results, vec![(1, 3)]);
2360 });
2361
2362 assert!(count > 0);
2363 }
2364
2365 #[cfg(feature = "sim")]
2366 #[test]
2367 fn threshold_greater_or_equal_uniform_bounded_value() {
2368 let mut flow = FlowBuilder::new();
2369 let node = flow.process::<()>();
2370
2371 let (input_port, input) = node.sim_input::<(u32, usize), _, _>();
2372
2373 // BoundedValue keyed singleton (values fixed once per key via .first())
2374 let values = input.into_keyed().first();
2375
2376 // Uniform threshold: all keys need value >= 5
2377 let threshold = node.singleton(q!(5usize));
2378
2379 let output = values
2380 .threshold_greater_or_equal_uniform(threshold)
2381 .entries()
2382 .sim_output();
2383
2384 let count = flow.sim().exhaustive(async || {
2385 // key 1 gets value 7 >= 5 ✓, key 2 gets value 3 < 5 ✗
2386 input_port.send((1, 7));
2387 input_port.send((2, 3));
2388
2389 let results = output.collect_sorted::<Vec<_>>().await;
2390 assert_eq!(results, vec![(1, 5)]);
2391 });
2392
2393 assert!(count > 0);
2394 }
2395
2396 #[cfg(feature = "sim")]
2397 #[test]
2398 fn threshold_greater_or_equal_bounded() {
2399 let mut flow = FlowBuilder::new();
2400 let node = flow.process::<()>();
2401
2402 // Bounded keyed singleton (fully known upfront)
2403 let values = node
2404 .source_iter(q!(vec![(1, 6usize), (2, 4usize)]))
2405 .into_keyed()
2406 .first();
2407
2408 // BoundedValue thresholds (from async source)
2409 let (thresh_port, thresh_input) = node.sim_input::<(u32, usize), _, _>();
2410 let thresholds = thresh_input.into_keyed().first();
2411
2412 let output = values
2413 .threshold_greater_or_equal(thresholds)
2414 .entries()
2415 .sim_output();
2416
2417 let count = flow.sim().exhaustive(async || {
2418 thresh_port.send((1, 5));
2419 thresh_port.send((2, 10));
2420
2421 // key 1: 6 >= 5 ✓, key 2: 4 < 10 ✗
2422 let results = output.collect_sorted::<Vec<_>>().await;
2423 assert_eq!(results, vec![(1, 5)]);
2424 });
2425
2426 assert!(count > 0);
2427 }
2428
2429 #[cfg(feature = "sim")]
2430 #[test]
2431 fn threshold_greater_or_equal_uniform_bounded() {
2432 let mut flow = FlowBuilder::new();
2433 let node = flow.process::<()>();
2434
2435 let values = node
2436 .source_iter(q!(vec![(1, 6usize), (2, 4usize)]))
2437 .into_keyed()
2438 .first();
2439 let threshold = node.singleton(q!(5usize));
2440
2441 let output = values
2442 .threshold_greater_or_equal_uniform(threshold)
2443 .entries()
2444 .sim_output();
2445
2446 let count = flow.sim().exhaustive(async || {
2447 // key 1: 6 >= 5 ✓, key 2: 4 < 5 ✗
2448 let results = output.collect_sorted::<Vec<_>>().await;
2449 assert_eq!(results, vec![(1, 5)]);
2450 });
2451
2452 assert!(count > 0);
2453 }
2454}