risingwavelabs/risingwave · error · StreamExecutorError

Gap interval expression returned null

Error message

Gap interval expression returned null

What it means

The gap-fill executor evaluates the GAP interval expression once at initialization against an empty dummy row (the interval must be constant). If the expression evaluates to NULL, `execute_inner` returns this anyhow error because there is no valid gap width to drive the filling logic.

Solutions

  1. Replace the GAP expression with a non-null constant INTERVAL literal, e.g. `GAP INTERVAL '1 minute'`.
  2. If the gap is computed by an expression, wrap it with COALESCE to a default: `GAP COALESCE(expr, INTERVAL '1 minute')`.
  3. Verify the referenced column/parameter is not NULL at the time the MV is created.

Example fix

// before
SELECT GAP FILL(ts, gap_col) 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

-- Validate the gap expression is non-null before creating the MV:
SELECT COALESCE(gap_expr, INTERVAL '1 minute') IS NOT NULL AS ok;
-- Prefer a literal: GAP INTERVAL '1 minute'

Try / catch

match gap_fill_mv_creation {
    Err(e) if e.to_string().contains("Gap interval expression returned null") => {
        // re-create with a constant non-null INTERVAL
    }
    other => other?,
}

Prevention

When it happens

Trigger: A `GAP FILL` query where the GAP expression (e.g. `GAP NULL::INTERVAL`, a column reference that is NULL, or an expression evaluating to NULL under constant folding) yields a NULL datum at src/stream/src/executor/gap_fill.rs:561.

Common situations: Users parameterizing the gap via a nullable column or a subquery instead of a literal; mis-typed expressions that constant-fold to NULL.

Related errors


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

Appendix: source

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

            gap_interval,
            high_gap_fill_amplification_threshold,
            ctx,
            input,
            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));

View on GitHub (pinned to 6469eb736d)