This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-mosaic.git


The following commit(s) were added to refs/heads/main by this push:
     new 7c22c0f  perf(core): defer dictionaries to bucket encoding (#100)
7c22c0f is described below

commit 7c22c0ffb0775ae86eadcdd1249c02414f75a5a8
Author: jianguotian <[email protected]>
AuthorDate: Mon Oct 19 18:50:42 2026 +0800

    perf(core): defer dictionaries to bucket encoding (#100)
---
 core/src/bucket_writer.rs | 390 +++++++++++++++++++++++++---------------------
 core/src/writer.rs        |  13 +-
 2 files changed, 224 insertions(+), 179 deletions(-)

diff --git a/core/src/bucket_writer.rs b/core/src/bucket_writer.rs
index f1da9e2..f011587 100644
--- a/core/src/bucket_writer.rs
+++ b/core/src/bucket_writer.rs
@@ -83,10 +83,6 @@ pub struct BucketWriter {
     const_tracking: Vec<bool>,
     first_value_len: Vec<usize>,
 
-    long_dict_maps: Vec<Option<HashMap<u64, usize>>>,
-    byte_dict_maps: Vec<Option<HashMap<Vec<u8>, usize>>>,
-    dict_tracking: Vec<DictTracking>,
-    dict_total_bytes: Vec<usize>,
     max_dict_total_bytes: usize,
     max_dict_entries: usize,
 
@@ -123,10 +119,6 @@ impl BucketWriter {
             non_null_counts: vec![0; total_columns],
             const_tracking: vec![true; total_columns],
             first_value_len: vec![0; total_columns],
-            long_dict_maps: (0..total_columns).map(|_| None).collect(),
-            byte_dict_maps: (0..total_columns).map(|_| None).collect(),
-            dict_tracking: vec![DictTracking::Pending; total_columns],
-            dict_total_bytes: vec![0; total_columns],
             max_dict_total_bytes,
             max_dict_entries,
             num_rows: 0,
@@ -181,14 +173,6 @@ impl BucketWriter {
         self.num_rows == 0
     }
 
-    pub fn estimated_raw_size(&self) -> usize {
-        if self.num_rows == 0 {
-            return 0;
-        }
-        let (encodings, has_nulls) = self.compute_encodings();
-        self.compute_out_size(&encodings, &has_nulls)
-    }
-
     pub fn write_columns(
         &mut self,
         arrays: &[&dyn Array],
@@ -423,7 +407,6 @@ impl BucketWriter {
         }
         let width = self.fixed_widths[col] as usize;
         let mask = u64::MAX >> (64 - width * 8);
-        let mut dict_start = 0;
         if self.const_tracking[col] {
             let first = self.integer_buffers[col]
                 .as_ref()
@@ -432,38 +415,8 @@ impl BucketWriter {
                 .next()
                 .unwrap_or(values[0].into() as u64 & mask);
             self.first_value_len[col] = width;
-            if let Some(first_different) = values
-                .iter()
-                .position(|&v| (v.into() as u64 & mask) != first)
-            {
-                // Preserve lazy dictionary activation: seed the constant 
prefix
-                // once, then track values starting at the first different key.
-                debug_assert_eq!(self.dict_tracking[col], 
DictTracking::Pending);
+            if values.iter().any(|&v| (v.into() as u64 & mask) != first) {
                 self.const_tracking[col] = false;
-                let dict = 
self.long_dict_maps[col].get_or_insert_with(HashMap::new);
-                dict.clear();
-                dict.insert(first, 0);
-                self.dict_tracking[col] = DictTracking::Active;
-                dict_start = first_different;
-            } else {
-                dict_start = values.len();
-            }
-        }
-        if self.dict_tracking[col] == DictTracking::Active {
-            let dict = self.long_dict_maps[col].as_mut().unwrap();
-            if dict.len() <= self.max_dict_entries {
-                for &value in &values[dict_start..] {
-                    let key = value.into() as u64 & mask;
-                    let next = dict.len();
-                    dict.entry(key).or_insert(next);
-                    if dict.len() > self.max_dict_entries {
-                        break;
-                    }
-                }
-            }
-            if dict.len() > self.max_dict_entries {
-                self.long_dict_maps[col] = None;
-                self.dict_tracking[col] = DictTracking::Disabled;
             }
         }
         self.integer_buffers[col]
@@ -596,13 +549,13 @@ impl BucketWriter {
 
         let col_size = buf.len() - before_all;
         let fw = self.fixed_widths[col] as usize;
-        if !self.const_tracking[col] && self.dict_tracking[col] == 
DictTracking::Disabled {
+        if !self.const_tracking[col] {
             self.non_null_counts[col] += num_rows;
         } else {
             for i in 0..num_rows {
                 self.non_null_counts[col] += 1;
                 self.track_encoding_value(col, before_all + i * fw, fw);
-                if !self.const_tracking[col] && self.dict_tracking[col] == 
DictTracking::Disabled {
+                if !self.const_tracking[col] {
                     self.non_null_counts[col] += num_rows - i - 1;
                     break;
                 }
@@ -633,8 +586,7 @@ impl BucketWriter {
         let valid_count = num_rows - nulls.null_count();
         self.value_buffers[col].reserve(valid_count * 8);
         let values = array.values();
-        let mut track_encoding =
-            self.const_tracking[col] || self.dict_tracking[col] != 
DictTracking::Disabled;
+        let mut track_encoding = self.const_tracking[col];
 
         for row in nulls.valid_indices() {
             let before = self.value_buffers[col].len();
@@ -643,8 +595,7 @@ impl BucketWriter {
 
             if track_encoding {
                 self.track_encoding_value(col, before, 8);
-                track_encoding =
-                    self.const_tracking[col] || self.dict_tracking[col] != 
DictTracking::Disabled;
+                track_encoding = self.const_tracking[col];
             }
         }
 
@@ -665,64 +616,151 @@ impl BucketWriter {
             }
 
             self.const_tracking[col] = false;
-            self.activate_dict_tracking(col, value_start, value_len);
-        } else {
-            self.track_dict_value(col, value_start, value_len);
         }
     }
 
-    fn activate_dict_tracking(&mut self, col: usize, value_start: usize, 
value_len: usize) {
-        debug_assert_eq!(self.dict_tracking[col], DictTracking::Pending);
-        self.dict_total_bytes[col] = 0;
-        if uses_long_dict(self.fixed_widths[col]) {
-            self.long_dict_maps[col]
-                .get_or_insert_with(HashMap::new)
-                .clear();
-        } else {
-            self.byte_dict_maps[col]
-                .get_or_insert_with(HashMap::new)
-                .clear();
-        }
-        self.dict_tracking[col] = DictTracking::Active;
+    pub(crate) fn has_integer_buffers(&self) -> bool {
+        self.integer_buffers.iter().any(Option::is_some)
+    }
 
-        self.track_dict_value(col, 0, self.first_value_len[col]);
-        if self.dict_tracking[col] == DictTracking::Active {
-            self.track_dict_value(col, value_start, value_len);
+    #[cfg(test)]
+    pub(crate) fn use_wide_integer_buffers(&mut self) {
+        assert_eq!(self.num_rows, 0);
+        for buffer in &mut self.integer_buffers {
+            *buffer = None;
         }
     }
 
-    fn track_dict_value(&mut self, col: usize, value_start: usize, value_len: 
usize) {
-        if self.dict_tracking[col] != DictTracking::Active {
-            return;
+    pub fn reset(&mut self) {
+        for i in 0..self.total_columns {
+            self.null_bitmaps[i].fill(0);
+            self.value_buffers[i].clear();
+            if let Some(buffer) = &mut self.integer_buffers[i] {
+                buffer.clear();
+            }
+            self.non_null_counts[i] = 0;
+            self.const_tracking[i] = true;
+            self.first_value_len[i] = 0;
         }
+        self.num_rows = 0;
+        for child in &mut self.children {
+            child.num_elements = 0;
+        }
+    }
 
-        let disable = if let Some(ref mut dict) = self.long_dict_maps[col] {
-            let key = values::extract_fixed_key(
-                &self.value_buffers[col],
-                value_start,
-                self.fixed_widths[col],
-            );
-            let len = dict.len();
-            dict.entry(key).or_insert(len);
-            dict.len() > self.max_dict_entries
-        } else if let Some(ref mut dict) = self.byte_dict_maps[col] {
-            let value = &self.value_buffers[col][value_start..value_start + 
value_len];
-            if !dict.contains_key(value) {
-                let len = dict.len();
-                dict.insert(value.to_vec(), len);
-                self.dict_total_bytes[col] += value_len;
-            }
-            dict.len() > self.max_dict_entries
-                || self.dict_total_bytes[col] > self.max_dict_total_bytes
-        } else {
-            unreachable!("active dictionary tracking must have a dictionary 
map")
+    /// Build dictionaries only for this bucket's encoding lifetime. Cached 
values are borrowed.
+    pub(crate) fn prepare(&self) -> PreparedBucket<'_> {
+        PreparedBucket::new(self)
+    }
+
+    pub fn estimated_raw_size(&self) -> usize {
+        self.prepare().estimated_raw_size()
+    }
+
+    pub fn finish(&self) -> Vec<u8> {
+        self.prepare().finish()
+    }
+
+    pub fn finish_paged(&self) -> PagedBucketOutput {
+        self.prepare().finish_paged()
+    }
+}
+
+/// Owns only transient dictionary tables, never a copy of the buffered values.
+/// Dropping this view releases tables even when encoding or the output sink 
fails.
+pub(crate) struct PreparedBucket<'a> {
+    bucket: &'a BucketWriter,
+    long_dict_maps: Vec<Option<HashMap<u64, usize>>>,
+    byte_dict_maps: Vec<Option<HashMap<Vec<u8>, usize>>>,
+    dict_tracking: Vec<DictTracking>,
+    dict_total_bytes: Vec<usize>,
+}
+
+impl std::ops::Deref for PreparedBucket<'_> {
+    type Target = BucketWriter;
+    fn deref(&self) -> &Self::Target {
+        self.bucket
+    }
+}
+
+impl<'a> PreparedBucket<'a> {
+    fn new(bucket: &'a BucketWriter) -> Self {
+        let mut prepared = Self {
+            bucket,
+            long_dict_maps: (0..bucket.total_columns).map(|_| None).collect(),
+            byte_dict_maps: (0..bucket.total_columns).map(|_| None).collect(),
+            dict_tracking: vec![DictTracking::Pending; bucket.total_columns],
+            dict_total_bytes: vec![0; bucket.total_columns],
         };
+        for col in 0..bucket.total_columns {
+            if bucket.const_tracking[col] || bucket.non_null_counts[col] == 0 {
+                continue;
+            }
+            prepared.dict_tracking[col] = DictTracking::Active;
+            let width = bucket.fixed_widths[col];
+            if uses_long_dict(width) {
+                let mut dict = HashMap::new();
+                let mut insert = |key| {
+                    let next = dict.len();
+                    dict.entry(key).or_insert(next);
+                    dict.len() > bucket.max_dict_entries
+                };
+                let disabled = if let Some(buffer) = 
&bucket.integer_buffers[col] {
+                    buffer.keys().any(&mut insert)
+                } else {
+                    (0..bucket.non_null_counts[col]).any(|i| {
+                        insert(values::extract_fixed_key(
+                            &bucket.value_buffers[col],
+                            i * width as usize,
+                            width,
+                        ))
+                    })
+                };
+                if disabled {
+                    prepared.dict_tracking[col] = DictTracking::Disabled;
+                } else {
+                    prepared.long_dict_maps[col] = Some(dict);
+                }
+            } else {
+                let mut dict = HashMap::new();
+                let buffer = &bucket.value_buffers[col];
+                let mut pos = 0;
+                let mut bytes = 0;
+                for _ in 0..bucket.non_null_counts[col] {
+                    let len = if width > 0 {
+                        width as usize
+                    } else {
+                        let len = varint::decode(buffer, &mut pos.clone())
+                            .expect("internal varint in value buffer");
+                        varint::encoded_size(len) + len as usize
+                    };
+                    let value = &buffer[pos..pos + len];
+                    pos += len;
+                    if !dict.contains_key(value) {
+                        let next = dict.len();
+                        dict.insert(value.to_vec(), next);
+                        bytes += len;
+                    }
+                    if dict.len() > bucket.max_dict_entries || bytes > 
bucket.max_dict_total_bytes {
+                        prepared.dict_tracking[col] = DictTracking::Disabled;
+                        break;
+                    }
+                }
+                if prepared.dict_tracking[col] == DictTracking::Active {
+                    prepared.byte_dict_maps[col] = Some(dict);
+                    prepared.dict_total_bytes[col] = bytes;
+                }
+            }
+        }
+        prepared
+    }
 
-        if disable {
-            self.dict_tracking[col] = DictTracking::Disabled;
-            self.long_dict_maps[col] = None;
-            self.byte_dict_maps[col] = None;
+    pub fn estimated_raw_size(&self) -> usize {
+        if self.num_rows == 0 {
+            return 0;
         }
+        let (encodings, has_nulls) = self.compute_encodings();
+        self.compute_out_size(&encodings, &has_nulls)
     }
 
     fn compute_encodings(&self) -> (Vec<u8>, Vec<bool>) {
@@ -805,18 +843,6 @@ impl BucketWriter {
         out
     }
 
-    pub(crate) fn has_integer_buffers(&self) -> bool {
-        self.integer_buffers.iter().any(Option::is_some)
-    }
-
-    #[cfg(test)]
-    pub(crate) fn use_wide_integer_buffers(&mut self) {
-        assert_eq!(self.num_rows, 0);
-        for buffer in &mut self.integer_buffers {
-            *buffer = None;
-        }
-    }
-
     pub(crate) fn encoding_plan(&self) -> BucketEncodingPlan {
         let (encodings, has_nulls) = self.compute_encodings();
         let page_sizes = (0..self.total_columns)
@@ -1020,32 +1046,6 @@ impl BucketWriter {
         out.write_all(&scratch[..used])
     }
 
-    pub fn reset(&mut self) {
-        for i in 0..self.total_columns {
-            self.null_bitmaps[i].fill(0);
-            self.value_buffers[i].clear();
-            if let Some(buffer) = &mut self.integer_buffers[i] {
-                buffer.clear();
-            }
-            self.non_null_counts[i] = 0;
-            self.const_tracking[i] = true;
-            self.first_value_len[i] = 0;
-            self.dict_tracking[i] = DictTracking::Pending;
-            self.dict_total_bytes[i] = 0;
-            if uses_long_dict(self.fixed_widths[i]) {
-                if let Some(ref mut dict) = self.long_dict_maps[i] {
-                    dict.clear();
-                }
-            } else if let Some(ref mut dict) = self.byte_dict_maps[i] {
-                dict.clear();
-            }
-        }
-        self.num_rows = 0;
-        for child in &mut self.children {
-            child.num_elements = 0;
-        }
-    }
-
     fn get_dict_size(&self, col: usize) -> usize {
         if self.dict_tracking[col] != DictTracking::Active {
             return 0;
@@ -2040,24 +2040,28 @@ mod tests {
                 };
             append(&mut compact, &mut wide, &[None; 65]);
             append(&mut compact, &mut wide, &vec![Some(255); 4095]);
-            assert_eq!(compact.dict_tracking[0], DictTracking::Pending);
-            assert!(compact.long_dict_maps[0].is_none());
+            assert_eq!(compact.prepare().dict_tracking[0], 
DictTracking::Pending);
+            assert!(compact.prepare().long_dict_maps[0].is_none());
             append(
                 &mut compact,
                 &mut wide,
                 &[None, Some(255), Some(-1), None, Some(-1)],
             );
-            assert_eq!(compact.dict_tracking[0], DictTracking::Active);
-            assert_eq!(compact.long_dict_maps[0].as_ref().unwrap().len(), 2);
-            assert_eq!(compact.compute_encodings().0, vec![ENCODING_DICT]);
+            assert_eq!(compact.prepare().dict_tracking[0], 
DictTracking::Active);
+            assert_eq!(
+                compact.prepare().long_dict_maps[0].as_ref().unwrap().len(),
+                2
+            );
+            assert_eq!(compact.prepare().compute_encodings().0, 
vec![ENCODING_DICT]);
 
             compact.reset();
             wide.reset();
             append(&mut compact, &mut wide, &vec![Some(7); 100]);
-            assert_eq!(compact.dict_tracking[0], DictTracking::Pending);
-            assert!(compact.long_dict_maps[0].as_ref().unwrap().is_empty());
+            assert_eq!(compact.prepare().dict_tracking[0], 
DictTracking::Pending);
+            assert!(compact.prepare().long_dict_maps[0].is_none());
             append(&mut compact, &mut wide, &[Some(8), None, Some(7)]);
-            let dict = compact.long_dict_maps[0].as_ref().unwrap();
+            let prepared = compact.prepare();
+            let dict = prepared.long_dict_maps[0].as_ref().unwrap();
             assert_eq!(dict.len(), 2);
             assert_eq!(dict[&7], 0);
             assert_eq!(dict[&8], 1);
@@ -2071,18 +2075,24 @@ mod tests {
             let mut compact = BucketWriter::new(&[&dt], 32768, limit);
             let prefix = Int64Array::from(vec![-1; 100]);
             compact.write_columns(&[&prefix], &[&dt]).unwrap();
-            assert!(compact.long_dict_maps[0].is_none());
+            assert!(compact.prepare().long_dict_maps[0].is_none());
             let varied = Int64Array::from(vec![Some(255), None, Some(256), 
Some(-1)]);
             compact.write_columns(&[&varied], &[&dt]).unwrap();
-            assert_eq!(compact.dict_tracking[0], DictTracking::Disabled);
-            assert!(compact.long_dict_maps[0].is_none());
+            assert_eq!(compact.prepare().dict_tracking[0], 
DictTracking::Disabled);
+            assert!(compact.prepare().long_dict_maps[0].is_none());
             compact.write_columns(&[&prefix], &[&dt]).unwrap();
-            assert_eq!(compact.dict_tracking[0], DictTracking::Disabled);
-            assert_eq!(compact.compute_encodings().0, vec![ENCODING_PLAIN]);
+            assert_eq!(compact.prepare().dict_tracking[0], 
DictTracking::Disabled);
+            assert_eq!(
+                compact.prepare().compute_encodings().0,
+                vec![ENCODING_PLAIN]
+            );
             compact.reset();
             compact.write_columns(&[&prefix], &[&dt]).unwrap();
-            assert_eq!(compact.dict_tracking[0], DictTracking::Pending);
-            assert_eq!(compact.compute_encodings().0, vec![ENCODING_CONST]);
+            assert_eq!(compact.prepare().dict_tracking[0], 
DictTracking::Pending);
+            assert_eq!(
+                compact.prepare().compute_encodings().0,
+                vec![ENCODING_CONST]
+            );
         }
     }
 
@@ -2098,9 +2108,9 @@ mod tests {
             .write_columns(&[&ints, &strings], &[&types[0], &types[1]])
             .unwrap();
 
-        assert!(writer.long_dict_maps[0].is_none());
-        assert!(writer.byte_dict_maps[1].is_none());
-        assert_eq!(writer.dict_total_bytes[1], 0);
+        assert!(writer.prepare().long_dict_maps[0].is_none());
+        assert!(writer.prepare().byte_dict_maps[1].is_none());
+        assert_eq!(writer.prepare().dict_total_bytes[1], 0);
     }
 
     #[test]
@@ -2121,9 +2131,18 @@ mod tests {
             .write_columns(&[&varied_ints, &varied_strings], &[&types[0], 
&types[1]])
             .unwrap();
 
-        assert_eq!(writer.dict_tracking, vec![DictTracking::Active; 2]);
-        assert_eq!(writer.long_dict_maps[0].as_ref().unwrap().len(), 2);
-        assert_eq!(writer.byte_dict_maps[1].as_ref().unwrap().len(), 2);
+        assert_eq!(
+            writer.prepare().dict_tracking,
+            vec![DictTracking::Active; 2]
+        );
+        assert_eq!(
+            writer.prepare().long_dict_maps[0].as_ref().unwrap().len(),
+            2
+        );
+        assert_eq!(
+            writer.prepare().byte_dict_maps[1].as_ref().unwrap().len(),
+            2
+        );
 
         let data = writer.finish();
         assert_eq!(data[0] & 0x03, ENCODING_DICT);
@@ -2146,9 +2165,18 @@ mod tests {
             .write_columns(&[&ints, &strings], &[&types[0], &types[1]])
             .unwrap();
 
-        assert_eq!(writer.dict_tracking, vec![DictTracking::Active; 2]);
-        assert_eq!(writer.long_dict_maps[0].as_ref().unwrap().len(), 255);
-        assert_eq!(writer.byte_dict_maps[1].as_ref().unwrap().len(), 255);
+        assert_eq!(
+            writer.prepare().dict_tracking,
+            vec![DictTracking::Active; 2]
+        );
+        assert_eq!(
+            writer.prepare().long_dict_maps[0].as_ref().unwrap().len(),
+            255
+        );
+        assert_eq!(
+            writer.prepare().byte_dict_maps[1].as_ref().unwrap().len(),
+            255
+        );
         let data = writer.finish();
         assert_eq!(data[0] & 0x03, ENCODING_DICT);
         assert_eq!((data[0] >> 2) & 0x03, ENCODING_DICT);
@@ -2159,9 +2187,12 @@ mod tests {
             .write_columns(&[&next_int, &next_string], &[&types[0], &types[1]])
             .unwrap();
 
-        assert_eq!(writer.dict_tracking, vec![DictTracking::Disabled; 2]);
-        assert!(writer.long_dict_maps.iter().all(Option::is_none));
-        assert!(writer.byte_dict_maps.iter().all(Option::is_none));
+        assert_eq!(
+            writer.prepare().dict_tracking,
+            vec![DictTracking::Disabled; 2]
+        );
+        assert!(writer.prepare().long_dict_maps.iter().all(Option::is_none));
+        assert!(writer.prepare().byte_dict_maps.iter().all(Option::is_none));
 
         let later_ints = Int32Array::from(vec![0, 256]);
         let later_strings = StringArray::from(vec!["value-000", "value-256"]);
@@ -2169,9 +2200,12 @@ mod tests {
             .write_columns(&[&later_ints, &later_strings], &[&types[0], 
&types[1]])
             .unwrap();
 
-        assert_eq!(writer.dict_tracking, vec![DictTracking::Disabled; 2]);
-        assert!(writer.long_dict_maps.iter().all(Option::is_none));
-        assert!(writer.byte_dict_maps.iter().all(Option::is_none));
+        assert_eq!(
+            writer.prepare().dict_tracking,
+            vec![DictTracking::Disabled; 2]
+        );
+        assert!(writer.prepare().long_dict_maps.iter().all(Option::is_none));
+        assert!(writer.prepare().byte_dict_maps.iter().all(Option::is_none));
         let data = writer.finish();
         assert_eq!(data[0] & 0x03, ENCODING_PLAIN);
         assert_eq!((data[0] >> 2) & 0x03, ENCODING_PLAIN);
@@ -2195,9 +2229,12 @@ mod tests {
             .write_columns(&[&within_budget], &[&DataType::Utf8])
             .unwrap();
 
-        assert_eq!(writer.dict_tracking[0], DictTracking::Active);
-        assert_eq!(writer.byte_dict_maps[0].as_ref().unwrap().len(), 2);
-        assert_eq!(writer.dict_total_bytes[0], 5);
+        assert_eq!(writer.prepare().dict_tracking[0], DictTracking::Active);
+        assert_eq!(
+            writer.prepare().byte_dict_maps[0].as_ref().unwrap().len(),
+            2
+        );
+        assert_eq!(writer.prepare().dict_total_bytes[0], 5);
         let data = writer.finish();
         assert_eq!(data[0] & 0x03, ENCODING_DICT);
 
@@ -2206,8 +2243,8 @@ mod tests {
             .write_columns(&[&beyond_budget], &[&DataType::Utf8])
             .unwrap();
 
-        assert_eq!(writer.dict_tracking[0], DictTracking::Disabled);
-        assert!(writer.byte_dict_maps[0].is_none());
+        assert_eq!(writer.prepare().dict_tracking[0], DictTracking::Disabled);
+        assert!(writer.prepare().byte_dict_maps[0].is_none());
         let data = writer.finish();
         assert_eq!(data[0] & 0x03, ENCODING_PLAIN);
     }
@@ -2287,8 +2324,11 @@ mod tests {
                 .collect::<Vec<_>>()
         );
         assert!(!writer.const_tracking[0]);
-        assert_eq!(writer.dict_tracking[0], DictTracking::Active);
-        assert_eq!(writer.long_dict_maps[0].as_ref().unwrap().len(), 2);
+        assert_eq!(writer.prepare().dict_tracking[0], DictTracking::Active);
+        assert_eq!(
+            writer.prepare().long_dict_maps[0].as_ref().unwrap().len(),
+            2
+        );
     }
 
     #[test]
diff --git a/core/src/writer.rs b/core/src/writer.rs
index 6ce79f4..033d9ca 100644
--- a/core/src/writer.rs
+++ b/core/src/writer.rs
@@ -20,7 +20,7 @@ use std::io;
 use arrow_array::*;
 use arrow_schema::Schema;
 
-use crate::bucket_writer::{BucketWriter, PagedBucketOutput};
+use crate::bucket_writer::{BucketWriter, PagedBucketOutput, PreparedBucket};
 use crate::schema::MosaicSchema;
 use crate::spec::*;
 use crate::stats::{self, ColumnStats, StatsCollector};
@@ -440,11 +440,14 @@ impl<S: OutputFile> MosaicWriter<S> {
             if bw.is_empty() {
                 continue;
             }
+            // Keep dictionaries only for the bucket currently being encoded.
+            // The view borrows buffered values and drops on output errors too.
+            let bw = bw.prepare();
             if bw.has_integer_buffers() {
                 let offset = self.out.pos();
                 let (layout, uncompressed_size) = write_integer_bucket(
                     &mut self.out,
-                    bw,
+                    &bw,
                     self.compression,
                     self.zstd_level,
                     self.page_size_threshold,
@@ -486,12 +489,14 @@ impl<S: OutputFile> MosaicWriter<S> {
                     .filter_map(|p| p.as_ref())
                     .map(|p| p.len())
                     .sum();
+                drop(bw);
                 let total_size = self.write_paged_bucket(&paged)?;
                 bucket_layouts[b] = BucketLayout::Paged { total_size };
                 actual_uncompressed_sizes[b] = paged_raw_size;
                 bucket_offsets[b] = self.out.pos() - total_size as u64;
             } else {
-                let raw = self.bucket_writers[b].as_ref().unwrap().finish();
+                let raw = bw.finish();
+                drop(bw);
                 let comp_size = self.write_compressed(&raw)?;
                 bucket_layouts[b] = BucketLayout::Monolithic {
                     compressed_size: comp_size,
@@ -848,7 +853,7 @@ fn compress_integer_unit<W: io::Write>(
 
 fn write_integer_bucket<S: OutputFile>(
     out: &mut S,
-    bucket: &BucketWriter,
+    bucket: &PreparedBucket<'_>,
     compression: u8,
     level: i32,
     page_threshold: usize,

Reply via email to