risingwavelabs/risingwave · error

file_scan_tasks must be Some

Error message

file_scan_tasks must be Some

What it means

After decoding a seq-id range for a stream chunk, no data rows were found (`ops.is_empty()`). A chunk read must yield at least one row; an empty result means the range selection pointed at seq ids that contain nothing (or only non-row ops), indicating a bug in the reader's range computation rather than a legitimate empty read.

Source

Thrown at src/batch/executors/src/executor/iceberg_scan.rs:101

            schema,
            file_scan_tasks: Some(file_scan_tasks),
            identity,
            file_scan_metrics,
            need_seq_num,
            need_file_path_and_pos,
            limit,
        }
    }

    #[try_stream(ok = DataChunk, error = BatchError)]
    async fn do_execute(mut self: Box<Self>) {
        let table = self.iceberg_config.load_table().await?;
        let data_types = self.schema.data_types();

        let data_file_scan_tasks = match Option::take(&mut self.file_scan_tasks) {
            Some(file_scan_tasks) => file_scan_tasks.into_tasks(),
            None => {
                bail!("file_scan_tasks must be Some")
            }
        };
        let mut remaining_limit = self
            .limit
            .map(|limit| usize::try_from(limit).unwrap_or(usize::MAX));

        for data_file_scan_task in data_file_scan_tasks {
            if matches!(remaining_limit, Some(0)) {
                return Ok(());
            }

            #[for_await]
            for chunk in scan_task_to_chunk_with_deletes(
                table.clone(),
                data_file_scan_task,
                IcebergScanOpts {
                    chunk_size: self.chunk_size,
                    need_seq_num: self.need_seq_num,

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Verify the range selection logic only issues reads over non-empty, non-truncated seq-id spans.
  2. Re-check truncation timing: if rows were truncated between range computation and read, re-select the range.
  3. Inspect the KV store at start_seq_id/end_seq_id to confirm entries exist.
  4. Ensure the writer never advances visible seq ids without writing rows.
Defensive patterns

Strategy: validation

Validate before calling

// rust
// confirm the range is non-empty and not yet truncated before decoding
anyhow::ensure!(end_seq_id > start_seq_id, "empty seq range [{}, {})", start_seq_id, end_seq_id);
anyhow::ensure!(start_seq_id > reader.current_truncate_seq_id(), "range [{}, {}) already truncated", start_seq_id, end_seq_id);

Try / catch

// rust
match reader.deserialize_stream_chunk(start, end, epoch).await {
    Ok(chunk) => process(chunk),
    Err(e) if e.to_string().contains("empty row") => {
        // re-select range from the store's current watermark before retrying
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling `deserialize_stream_chunk` with `[start_seq_id, end_seq_id)` whose decoded ops list is empty at the final check. Thrown at serde.rs:514.

Common situations: Read range where all entries are barriers or were truncated concurrently; stale start/end seq ids after truncation; writer skipped seq ids leaving gaps the reader assumed were populated.

Understand the failure class

Background: EmptyResultError / "no results found": when an API or scraper succeeds but returns zero rows — this error's family across 9 libraries.

Related errors


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