risingwavelabs/risingwave · error

Expected at most 1 clean_watermark_index per table, got {:?}

Error message

Expected at most 1 clean_watermark_index per table, got {:?}

What it means

In stream HashJoin watermark inference, each table may have at most one column used for state cleaning (clean_watermark_index). If the collected `clean_watermark_indices` vector has more than one entry, `infer_clean_watermark_indices` returns this error via `bail!`. The invariant is supposed to be guaranteed upstream by `derive_watermark_for_hash_join`, so this error means the join's inequality/equality conditions yielded multiple cleanable columns per table.

Source

Thrown at src/frontend/src/optimizer/plan_node/stream_hash_join.rs:486

            if is_left && *clean_left {
                let col_idx = pair.left_idx;
                if !clean_watermark_indices.contains(&col_idx) {
                    inequal_clean_watermark_indices.push(col_idx);
                }
            } else if !is_left && *clean_right {
                let col_idx = pair.right_idx;
                if !clean_watermark_indices.contains(&col_idx) {
                    inequal_clean_watermark_indices.push(col_idx);
                }
            }
        }

        clean_watermark_indices.extend(inequal_clean_watermark_indices.clone());

        // Verify: only 1 column per table is allowed to do state cleaning.
        // This invariant is enforced by `derive_watermark_for_hash_join`.
        if clean_watermark_indices.len() > 1 {
            bail!(
                "Expected at most 1 clean_watermark_index per table, got {:?}",
                clean_watermark_indices
            )
        }

        Ok((
            clean_watermark_indices,
            eq_join_key_clean_watermark_indices,
            inequal_clean_watermark_indices,
        ))
    }

    /// Infer which join keys can be used for state cleaning based on equal conditions.
    fn infer_eq_join_key_clean_watermark_indices(&self, join_key_indices: &[usize]) -> Vec<usize> {
        let mut clean_indices = vec![];
        for (idx_in_jk, do_state_cleaning) in &self.watermark_indices_in_jk {
            if *do_state_cleaning {
                let col_idx = join_key_indices[*idx_in_jk];

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Rewrite the query so each joined table has at most one inequality/watermark-cleaning condition (drop or merge extra non-equi predicates).
  2. Check `derive_watermark_for_hash_join` output for the failing join to see which conditions produced multiple indices.
  3. If the invariant should hold, fix the derivation logic so it enforces at most one cleaning column per table before inference runs.
  4. Simplify the join (e.g. pre-filter inputs or split the join) to avoid multiple cleanable columns.

Example fix

-- before: multiple cleaning columns per table
CREATE MV AS SELECT * FROM t1 JOIN t2 ON t1.ts > t2.ts AND t1.id > t2.id;
-- after: keep only one inequality suitable for watermark cleaning
CREATE MV AS SELECT * FROM t1 JOIN t2 ON t1.ts > t2.ts AND t1.id = t2.id;
Defensive patterns

Strategy: validation

Validate before calling

-- SQL: ensure at most one non-equi (watermark-cleaning) condition per joined table
-- Inspect the join predicates before creating the MV:
-- SELECT pg_get_expr(ev.ev, ev.oid) FROM ...  -- or review the ON clause manually
-- Count inequality predicates per table; rewrite if > 1 per side.

Try / catch

// Rust caller: treat bail! result as an error, not panic
match node.infer_clean_watermark_indices() {
    Ok(indices) => proceed(indices),
    Err(e) => return Err(context!(e, "hash join watermark inference")),
}

Prevention

When it happens

Trigger: Creating a `CREATE MATERIALIZED VIEW` with a stream hash join whose watermark derivation produces more than one clean_watermark_index for one side of the join, i.e. multiple inequality conditions each mapping to cleanable columns on the same table, when `derive_watermark_for_hash_join` did not collapse them.

Common situations: Queries with multiple non-equi join conditions on watermark columns; upgrading RisingWave after changes to watermark/now-offset derivation rules; writing SQL that combines several inequality predicates (e.g. `t1.ts > t2.ts AND t1.id > t2.id`) on streaming joins with watermarks.

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/9df6f68cf8d047d3. Report an issue: GitHub.