risingwavelabs/risingwave · error · StreamExecutorError

Gap interval must be positive

Error message

Gap interval must be positive

What it means

After evaluating the GAP interval expression, the gap-fill executor validates that the interval is strictly greater than zero. A zero or negative interval would cause an infinite/empty fill loop, so `execute_inner` fails with this anyhow error at src/stream/src/executor/gap_fill.rs:565.

Solutions

  1. Use a strictly positive INTERVAL literal, e.g. `GAP INTERVAL '1 minute'`.
  2. If the gap comes from an expression, add GREATEST to clamp it positive: `GAP GREATEST(expr, INTERVAL '1 second')`.
  3. Check the value bound to the gap (variable/config) before creating the materialized view.

Example fix

// before
SELECT GAP FILL(ts, INTERVAL '0 minutes') FROM t GROUP BY ts;
// after
SELECT GAP FILL(ts, INTERVAL '1 minute') FROM t GROUP BY ts;
Defensive patterns

Strategy: validation

Validate before calling

-- Check the interval is strictly positive before use:
SELECT gap_expr > INTERVAL '0 seconds' AS ok; -- must be true
-- Or clamp: GREATEST(gap_expr, INTERVAL '1 second')

Try / catch

match result {
    Err(e) if e.to_string().contains("Gap interval must be positive") => {
        // correct the GAP literal and re-create the materialized view
    }
    other => other?,
}

Prevention

When it happens

Trigger: A `GAP FILL` query with `GAP INTERVAL '0 seconds'`, a negative interval, or an expression that evaluates to a non-positive interval at MV creation time.

Common situations: Config mistakes where a user-supplied gap parameter defaults to 0; arithmetic expressions like `end_ts - start_ts` that evaluate to zero at creation.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11). Data as JSON: /api/errors/dc88953aa03e0af5. Report an issue: GitHub.

Appendix: source

Thrown at src/stream/src/executor/gap_fill.rs:565

            metrics,
        } = *self;

        let mut input = input.execute();

        let barrier = expect_first_barrier(&mut input).await?;
        let first_epoch = barrier.epoch;
        yield Message::Barrier(barrier);
        managed_state.init_epoch(first_epoch).await?;

        // Calculate and validate gap interval once at initialization
        let dummy_row = OwnedRow::new(vec![]);
        let interval_datum = gap_interval.eval_row_infallible(&dummy_row).await;
        let interval = interval_datum
            .ok_or_else(|| anyhow::anyhow!("Gap interval expression returned null"))?
            .into_interval();

        if interval <= Interval::from_month_day_usec(0, 0, 0) {
            Err(anyhow::anyhow!("Gap interval must be positive"))?;
        }
        let generation_context = GapFillGenerationContext {
            metrics: &metrics,
            high_amplification_threshold: high_gap_fill_amplification_threshold,
            actor_ctx: &ctx,
        };

        let partition_by_indices = managed_state.partition_by_indices.clone();
        let pointer_key_indices = managed_state.pointer_key_indices.clone();
        // Interpolation re-slopes every fill, so a changed anchor changes all of them; only
        // LOCF/NULL fills can be reused by the diff.
        let has_interpolate = fill_columns
            .values()
            .any(|strategy| matches!(strategy, FillStrategy::Interpolate));

        #[for_await]
        for msg in input {
            match msg? {

View on GitHub (pinned to 6469eb736d)