risingwavelabs/risingwave · error
StreamVectorIndexLookupJoin only support append only input
Error message
StreamVectorIndexLookupJoin only support append only input
What it means
StreamVectorIndexLookupJoin::new validates that the input of a stream vector index lookup join is append-only. RisingWave stream fragments must know the input never emits UPDATE/DELETE retraction records for this node; if the input's StreamKind is not AppendOnly, the constructor bails with this error instead of producing an invalid plan.
Solutions
- Rewrite the query so the join input is append-only (e.g. query an append-only source or MV that never deletes/updates rows).
- If input is an MV receiving updates, materialize an append-only derived table (e.g. filter/aggregation that is append-only) and join against that.
- Check the input's StreamKind before constructing the node and surface a user-friendly error suggesting append-only input.
Example fix
// before let node = StreamVectorIndexLookupJoin::new(core)?; // bails if input not append-only // after assert!(core.input.stream_kind() == StreamKind::AppendOnly, "vector index lookup join requires append-only input"); let node = StreamVectorIndexLookupJoin::new(core)?;
Defensive patterns
Strategy: validation
Validate before calling
if core.input.stream_kind() != StreamKind::AppendOnly {
return Err(anyhow!("vector index lookup join requires an append-only input"));
}
let node = StreamVectorIndexLookupJoin::new(core)?; Type guard
fn is_append_only(plan: &StreamPlanRef) -> bool { plan.stream_kind() == StreamKind::AppendOnly } Prevention
- Check StreamKind before planning vector index lookup joins.
- Keep inputs to such joins append-only sources or append-only MVs.
- Document the append-only requirement near the query pattern in your app code.
When it happens
Trigger: Calling StreamVectorIndexLookupJoin::new with a core whose core.input.stream_kind() != StreamKind::AppendOnly, i.e. planning a vector index lookup join over a stream input that can produce retractions (e.g. an MV with upserts/deletes or a non-append-only source).
Common situations: Building queries that use vector index time travel/lookup joins on top of mutable tables or sources with updates; users attempt AS OF joins over frequently updated MVs where only append-only streams are supported.
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
- IcebergWithPkIndexWriterExecutor requires exactly two inputs
- MATCH_RECOGNIZE node missing pattern
- MATCH_RECOGNIZE plan carries an empty ORDER BY
- missing downstream pk in iceberg sink desc
- should be append-only for vector index writer but receive op
AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11).
Data as JSON: /api/errors/09db13299b11d470.
Report an issue: GitHub.
Appendix: source
Thrown at src/frontend/src/optimizer/plan_node/stream_vector_index_lookup_join.rs:39
use crate::optimizer::plan_node::generic::{PhysicalPlanRef, VectorIndexLookupJoin};
use crate::optimizer::plan_node::stream::StreamPlanNodeMetadata;
use crate::optimizer::plan_node::utils::{Distill, childless_record};
use crate::optimizer::plan_node::{
ExprRewritable, PlanBase, PlanTreeNodeUnary, Stream, StreamNode, StreamPlanRef,
};
use crate::optimizer::property::StreamKind;
use crate::stream_fragmenter::BuildFragmentGraphState;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct StreamVectorIndexLookupJoin {
pub base: PlanBase<Stream>,
pub core: VectorIndexLookupJoin<StreamPlanRef>,
}
impl StreamVectorIndexLookupJoin {
pub fn new(core: VectorIndexLookupJoin<StreamPlanRef>) -> crate::error::Result<Self> {
if core.input.stream_kind() != StreamKind::AppendOnly {
bail!("StreamVectorIndexLookupJoin only support append only input")
}
Ok(Self::with_core(core))
}
fn with_core(core: VectorIndexLookupJoin<StreamPlanRef>) -> Self {
assert_eq!(core.input.stream_kind(), StreamKind::AppendOnly);
let base = PlanBase::new_stream_with_core(
&core,
core.input.distribution().clone(),
core.input.stream_kind(),
core.input.emit_on_window_close(),
core.input.watermark_columns().clone(),
core.input.columns_monotonicity().clone(),
);
Self { base, core }
}
}
View on GitHub (pinned to 6469eb736d)