{"record":{"id":"c3ff52b3e1fc995d","repo":"risingwavelabs/risingwave","slug":"position-delete-writer-produced-invalid-file-count","errorCode":null,"errorMessage":"position-delete writer produced invalid file count for {data_file_path}","messagePattern":"position-delete writer produced invalid file count for (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/connector/src/sink/iceberg/position_delete.rs","lineNumber":262,"sourceCode":"        if positions.len() == POSITION_DELETE_WRITE_CHUNK_SIZE {\n            write_position_delete_chunk(\n                &mut writer,\n                &arrow_schema,\n                &data_file_path,\n                std::mem::take(&mut positions),\n            )\n            .await?;\n            positions.reserve(POSITION_DELETE_WRITE_CHUNK_SIZE);\n        }\n    }\n    if !positions.is_empty() {\n        write_position_delete_chunk(&mut writer, &arrow_schema, &data_file_path, positions).await?;\n    }\n\n    let data_files = writer.close().await?;\n    // `close` will yield exactly one builder here.\n    let [mut builder] = data_files.try_into().map_err(|_| {\n        anyhow!(\"position-delete writer produced invalid file count for {data_file_path}\")\n    })?;\n\n    // `ParquetWriter` builds the file as `DataContentType::Data` with an empty partition; override\n    // those for a file-scoped V2 position-delete file and attach `referenced_data_file`.\n    builder\n        .content(DataContentType::PositionDeletes)\n        .referenced_data_file(Some(data_file_path));\n    if let Some(partition_key) = partition_key {\n        builder\n            .partition(partition_key.data().clone())\n            .partition_spec_id(partition_key.spec().spec_id());\n    }\n    builder\n        .build()\n        .context(\"Failed to build position-delete file metadata\")\n}\n\n/// Writes one chunk of `positions` as a `(file_path, pos)` batch into `writer`. Every row shares","sourceCodeStart":244,"sourceCodeEnd":280,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/connector/src/sink/iceberg/position_delete.rs#L244-L280","documentation":"This error is thrown when the iceberg-rs position-delete `DataFileWriter::close()` returns a number of DataFile builders other than exactly one. The code uses a slice pattern `let [mut builder] = data_files.try_into()` to destructurally assert the invariant that closing a position-delete writer yields exactly one file; any other count (0 or >1) breaks the writer contract and aborts the sink with this anyhow error.","triggerScenarios":"Calling write_parquet_position_delete_file when the underlying iceberg `DataFileWriter` (via RollingFileWriterBuilder) closes into zero files (empty write, IO failure swallowed) or multiple files because the rolling writer split output across the target file size, violating the code's one-builder assumption.","commonSituations":"A data file with zero delete positions being flushed; very large position-delete payloads exceeding target_file_size_mb causing the rolling writer to emit several parquet files; upgrading the iceberg-rs crate and its close() semantics changing.","solutions":["Check that the input `positions` collection is non-empty before calling write_parquet_position_delete_file, or short-circuit returning None for empty input","Verify target_file_size_mb for the sink is large enough that a single position-delete file never rolls into multiple files","Inspect the iceberg-rs version pinned in Cargo.toml for changes to DataFileWriter::close() semantics and pin to a version where close yields exactly one builder","If the invariant is intentionally relaxable, replace the slice pattern with a match on data_files.as_slice() handling 0 and n>1 cases explicitly"],"exampleFix":"// before\nlet [mut builder] = data_files.try_into().map_err(|_| {\n    anyhow!(\"position-delete writer produced invalid file count for {data_file_path}\")\n})?;\n// after\nlet mut builders = data_files;\nif builders.is_empty() {\n    return Ok(None); // nothing written\n}\nanyhow::ensure!(builders.len() == 1, \"expected 1 position-delete file, got {}\", builders.len());\nlet builder = builders.pop().unwrap();","handlingStrategy":"validation","validationCode":"if positions.is_empty() {\n    return Ok(None); // nothing to write, skip writer close entirely\n}","typeGuard":"fn single_builder(files: Vec<iceberg::spec::DataFileBuilder>) -> Option<iceberg::spec::DataFileBuilder> {\n    let mut it = files.into_iter();\n    match (it.next(), it.next()) {\n        (Some(b), None) => Some(b),\n        _ => None,\n    }\n}","tryCatchPattern":"match data_files.try_into() {\n    Ok([builder]) => builder,\n    _ => return Err(anyhow!(\"position-delete writer produced invalid file count\")),\n}","preventionTips":["Skip file creation entirely when the positions list is empty","Keep target_file_size_mb large enough that one position-delete batch never rolls into multiple files","Pin the iceberg-rs version and re-read close() semantics on every upgrade","Add a unit test asserting close() yields exactly one builder for a typical chunk"],"tags":["rust","iceberg","position-delete","invariant-violation"],"backgroundTag":"internal-invariant-violation","analyzedSha":"6469eb736d691e8e9b8a419a57edd6429ca77417","analyzedAt":"2026-09-11T21:06:21.487Z","contentChangedAt":"2026-09-11T21:06:21.487Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}