risingwavelabs/risingwave · error · BatchError

GenerateSeriesExecutor should not have child!

Error message

GenerateSeriesExecutor should not have child!

What it means

TableFunctionExecutorBuilder (e.g. generate_series) requires zero child executors. Table functions are leaf operators whose output rows are generated from their expression arguments, so an input executor means the plan incorrectly wired a child under the table function node.

Source

Thrown at src/batch/executors/src/executor/table_function.rs:75

            // remove the first column and expand the second column if its data type is struct
            yield match chunk.column_at(1).as_ref() {
                ArrayImpl::Struct(struct_array) => struct_array.into(),
                _ => chunk.split_column_at(1).1,
            };
        }
    }
}

pub struct TableFunctionExecutorBuilder {}

impl TableFunctionExecutorBuilder {}

impl BoxedExecutorBuilder for TableFunctionExecutorBuilder {
    async fn new_boxed_executor(
        source: &ExecutorBuilder<'_>,
        inputs: Vec<BoxedExecutor>,
    ) -> Result<BoxedExecutor> {
        ensure!(
            inputs.is_empty(),
            "GenerateSeriesExecutor should not have child!"
        );
        let node = try_match_expand!(
            source.plan_node().get_node_body().unwrap(),
            NodeBody::TableFunction
        )?;

        let identity = source.plan_node().get_identity().clone();

        let chunk_size = source.context().get_config().developer.chunk_size;

        let table_function = build_from_prost(node.table_function.as_ref().unwrap(), chunk_size)?;

        let schema = if let DataType::Struct(fields) = table_function.return_type() {
            (&fields).into()
        } else {
            Schema {

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Check the plan: the TableFunction node must have no children; fix the planner code that produced a child.
  2. Ensure table-function arguments are passed as expressions (value_node.relation_expr), not as child executors.
  3. When building TableFunctionExecutor manually, supply an empty inputs vector.
  4. File an internal bug with the reproducing SQL if the planner looks correct.

Example fix

// before: child executor fed to table function
let inputs = vec![child_executor];
// after: table function as leaf, data via expression arguments
let inputs = vec![];
Defensive patterns

Strategy: try-catch

Validate before calling

// Rust: table functions must have no child executors
assert!(inputs.is_empty(), "TableFunction must be a leaf node");

Type guard

fn is_leaf(inputs: &[BoxedExecutor]) -> bool { inputs.is_empty() }

Try / catch

match builder.new_boxed_executor(&src, inputs).await {
    Err(e) if e.to_string().contains("should not have child") => /* fix plan arity, log plan tree */,
    other => other,
}

Prevention

When it happens

Trigger: new_boxed_executor is invoked with a non-empty `inputs` vec because a TableFunction plan node has children in the batch plan tree.

Common situations: Planner bugs when planning queries with generate_series/unnest-style table functions; incorrectly nesting a table function under another operator in hand-built plans; test fixtures wiring children into leaf nodes.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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