diff --git a/src/adapter/src/coord/sequencer/inner/subscribe.rs b/src/adapter/src/coord/sequencer/inner/subscribe.rs index 331398a83013e..a8167ba1347f4 100644 --- a/src/adapter/src/coord/sequencer/inner/subscribe.rs +++ b/src/adapter/src/coord/sequencer/inner/subscribe.rs @@ -573,6 +573,37 @@ impl Coordinator { }; active_subscribe.initialize(); + // A subscribe whose `as_of` equals its `up_to` covers the empty range + // `[as_of, up_to)` and is guaranteed to produce no data. The adapter + // reports this with the `EqualSubscribeBounds` notice. Skip installing a + // dataflow for it: the only observable output is the initial progress row + // that `initialize` already emitted (WITH PROGRESS only). Dropping + // `active_subscribe` closes the channel, which completes the subscribe on + // the client side. + let is_empty = df_desc + .as_of + .as_ref() + .is_some_and(|as_of| as_of == &df_desc.until); + if is_empty { + drop(active_subscribe); + drop(read_holds); + let resp = ExecuteResponse::Subscribing { + rx, + ctx_extra: std::mem::take(ctx_extra), + instance_id: cluster_id, + }; + let resp = match plan.copy_to { + None => resp, + Some(format) => ExecuteResponse::CopyTo { + format, + resp: Box::new(resp), + }, + }; + // No `mz_subscriptions` bookkeeping was deferred, so hand back an + // already-satisfied notify. + return Ok((resp, Box::pin(std::future::ready(())))); + } + // Register bookkeeping for the new SUBSCRIBE and ship its dataflow. The // `mz_subscriptions` write is deferred to a group commit (see // `add_active_compute_sink`) rather than committed inline, so it does not block diff --git a/src/compute-types/src/dataflows.rs b/src/compute-types/src/dataflows.rs index ccbc6190ca018..82a5c064759b9 100644 --- a/src/compute-types/src/dataflows.rs +++ b/src/compute-types/src/dataflows.rs @@ -87,19 +87,20 @@ impl
DataflowDescription
{ // Ensure that as_of <= until. soft_assert_or_log!( timely::PartialOrder::less_equal(as_of, until), - "expected empty `as_of ≤ until`, got `{as_of:?} ≰ {until:?}`", + "expected `as_of ≤ until`, got `{as_of:?} ≰ {until:?}`", ); // IF `as_of` is not a single timestamp this can't be a single time dataflow. let Some(as_of) = as_of.as_option() else { return false; }; - // Ensure that `as_of = MAX` implies `until.is_empty()`. - soft_assert_or_log!( - as_of != &mz_repr::Timestamp::MAX || until.is_empty(), - "expected `until = {{}}` due to `as_of = MAX`, got `until = {until:?}`", - ); - // Note that the `(as_of = MAX, until = {})` case also returns `true` - // here (as expected) since we are going to compare two `None` values. + // With `as_of = MAX` the `as_of <= until` invariant leaves only two valid + // frontiers, and both are handled correctly by the comparison below: + // * `until = {}` (unbounded) yields `None == None`, so single time. `MAX` + // is the last timestamp, so an unbounded dataflow starting there sees + // exactly one time. + // * `until = {MAX}` is the empty range produced by e.g. + // `SUBSCRIBE ... AS OF MAX UP TO MAX`. `try_step_forward` on `MAX` + // overflows to `None`, so `None == Some(MAX)` is false: not single time. as_of.try_step_forward().as_ref() == until.as_option() } } @@ -629,3 +630,50 @@ pub struct BuildDesc
{
/// TODO(database-issues#7533): Add documentation.
pub plan: P,
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ /// Builds a minimal dataflow description carrying only the `as_of` and
+ /// `until` frontiers that `is_single_time` inspects.
+ fn desc_with_bounds(
+ as_of: Antichain