{"record":{"id":"a10050c0e85bd038","repo":"influxdata/influxdb","slug":"if-you-call-drop-last-value-the-tag-buffer-must","errorCode":null,"errorMessage":"If you call `drop_last_value`, the tag buffer must contain at least one value, but it contained zero","messagePattern":"If you call `drop_last_value`, the tag buffer must contain at least one value, but it contained zero","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/table_batch/src/builder/column_writer/dictionary.rs","lineNumber":74,"sourceCode":"\n    /// Remove the last wrote value.\n    ///\n    /// # Reference Leak\n    ///\n    /// This call removes the encoded value from the buffer.\n    ///\n    /// If this call removes the sole reference to a value in this buffer, the value will not appear\n    /// in the decoded result, and is removed from the buffer containing all previously-pushed\n    /// strings. This allows us to avoid storing and transmitting unnecessary data, at the cost of\n    /// a slightly more expensive operation when we have to remove a value due to recovering from a\n    /// partial write.\n    ///\n    /// # Panics\n    ///\n    /// Panics if no values remain in `self`.\n    pub(crate) fn drop_last_value(&mut self) {\n        let last_pushed = self.encoded.pop()\n            .expect(\"If you call `drop_last_value`, the tag buffer must contain at least one value, but it contained zero\");\n\n        // And we should remove it from the packed strings as well if it failed, just to get more\n        // predictable behavior and avoid sending some data if possible.\n        if !self.encoded.contains(&last_pushed) {\n            let removed = self.keys.pop()\n                .expect(\"If we can remove a value from the interned strings, we must also be able to remove a value from the packed strings.\");\n\n            self.id_map.remove(&removed);\n        }\n    }\n\n    pub(crate) fn finish(self) -> Option<InternedStrings> {\n        let dictionary = self.keys.finish()?;\n\n        // Invariant: the key -> ID map and the buffer containing the encoded\n        // keys must always agree on the number of unique keys observed.\n        debug_assert_eq!(\n            self.id_map.len(),","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/influxdata/influxdb/blob/d28e26e048401c53cbb98cf2d6ab0cf1e98048ca/core/table_batch/src/builder/column_writer/dictionary.rs#L56-L92","documentation":"DictionaryBuffer::drop_last_value() removes the most recently pushed dictionary-encoded value; it is the rollback primitive used when recovering from a partial line-protocol write. The .expect fires when self.encoded (the tag->dictionary-ID buffer) is empty, i.e. more values were dropped than were ever pushed. It is pub(crate), so it can only be triggered by code inside the table_batch builder, not by external callers.","triggerScenarios":"Builder rollback logic calling drop_last_value() on a column that had zero successful pushes, or calling the rollback twice for the same row (double-undo), or undoing a row after the buffer was already drained by a previous undo. Any code path in core/table_batch that recovers from a partial write and unconditionally drops N values without tracking how many were actually written.","commonSituations":"Refactoring the partial-write recovery logic so the number of drop_last_value calls no longer matches the number of push_str calls; a malformed line-protocol payload where some columns of a row parsed and others errored, combined with an off-by-one in the undo loop; test code exercising rollback on an empty buffer.","solutions":["Check the buffer is non-empty before dropping: DictionaryBuffer exposes len(), so guard with len() > 0.","Audit the caller: count pushes per column and only drop that many — make undo symmetric with the successful writes of the failed row.","Reproduce with the failing line-protocol payload in a unit test (the module has proptest tests for exactly this push/drop behavior) and fix the off-by-one.","If the rollback order across columns can vary, switch the caller to snapshot/restore of buffer lengths instead of repeated pops."],"exampleFix":"// before\ncol.drop_last_value(); // panics: 'the tag buffer must contain at least one value, but it contained zero'\n\n// after\nif col.len() > 0 {\n    col.drop_last_value();\n}","handlingStrategy":"type-guard","validationCode":"// drop_last_value is pub(crate): guard inside the builder rollback code.\nif column.dictionary.len() > 0 {\n    column.dictionary.drop_last_value();\n} else {\n    debug!(\"skip drop_last_value: buffer already empty\");\n}","typeGuard":"// Language-appropriate guard: use the existing len() accessor.\nimpl DictionaryBuffer {\n    pub(crate) fn can_drop_last(&self) -> bool {\n        self.len() > 0\n    }\n}","tryCatchPattern":null,"preventionTips":["Make undo symmetric with push: track successful pushes per column and drop exactly that many.","Prefer snapshot/restore of buffer lengths over repeated pops when rollback order can vary.","Keep property tests covering interleaved push_str/drop_last_value sequences in CI."],"tags":["rust","influxdb","line-protocol","buffer","rollback","panic","internal-api"],"backgroundTag":"buffer-underflow","analyzedSha":"d28e26e048401c53cbb98cf2d6ab0cf1e98048ca","analyzedAt":"2026-08-16T19:53:34.623Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}