Jefffrey commented on code in PR #10626:
URL: https://github.com/apache/arrow-rs/pull/10626#discussion_r3800244656


##########
parquet/src/arrow/arrow_writer/mod.rs:
##########
@@ -360,74 +360,93 @@ impl<W: Write + Send> ArrowWriter<W> {
             return Ok(());
         }
 
-        let in_progress = match &mut self.in_progress {
-            Some(in_progress) => in_progress,
-            x => x.insert(
-                self.row_group_writer_factory
-                    
.create_row_group_writer(self.writer.flushed_row_groups().len())?,
-            ),
-        };
+        // Rows not yet handed to a row group writer. Splitting iterates here 
instead of
+        // recursing, so a small row group limit over a large batch cannot 
exhaust the stack.
+        let mut remaining = batch.clone();
+
+        loop {
+            if self.in_progress.is_none() {
+                let row_group_index = self.writer.flushed_row_groups().len();
+                self.in_progress = Some(
+                    self.row_group_writer_factory
+                        .create_row_group_writer(row_group_index)?,
+                );
+            }
+            let buffered_rows = 
self.in_progress.as_ref().unwrap().buffered_rows;
 
-        if let Some(max_rows) = self.max_row_group_row_count
-            && in_progress.buffered_rows + batch.num_rows() > max_rows
-        {
-            let to_write = max_rows - in_progress.buffered_rows;
-            let a = batch.slice(0, to_write);
-            let b = batch.slice(to_write, batch.num_rows() - to_write);
-            self.write(&a)?;
-            return self.write(&b);
-        }
+            // Leading rows of `remaining` that still fit in the current row 
group, when the
+            // rest has to go to a later one.
+            let mut split_at = match self.max_row_group_row_count {
+                Some(max_rows) if buffered_rows + remaining.num_rows() > 
max_rows => {
+                    Some(max_rows - buffered_rows)
+                }
+                _ => None,
+            };
 
-        // Check byte limit: if we have buffered data, use measured average 
row size
-        // to split batch proactively before exceeding byte limit
-        if let Some(max_bytes) = self.max_row_group_bytes
-            && in_progress.buffered_rows > 0
-        {
-            let current_bytes = in_progress.get_estimated_total_bytes();
+            // Check byte limit: if we have buffered data, use measured 
average row size
+            // to split batch proactively before exceeding byte limit
+            if split_at.is_none()

Review Comment:
   codex helped me identify a possible deviation: by checking `split_at` here 
it means the row count limit can win over the byte limit. for example this test 
will pass on main with recursive version:
   
   ```rust
       #[test]
       fn test123() {
           let props = WriterProperties::builder()
               .set_max_row_group_row_count(Some(15))
               .set_max_row_group_bytes(Some(1500))
               .build();
   
           let builder = write_batches(
               WriteBatchesShape {
                   num_batches: 2,
                   rows_per_batch: 10,
                   row_size: 100,
               },
               props,
           );
   
           let sizes = row_group_sizes(builder.metadata());
           assert_eq!(sizes, &[14, 6]);
       }
   ```
   
   but with this iterative version it fails because the sizes becomes `[15, 5]`



##########
parquet/src/arrow/arrow_writer/mod.rs:
##########
@@ -360,74 +360,93 @@ impl<W: Write + Send> ArrowWriter<W> {
             return Ok(());
         }
 
-        let in_progress = match &mut self.in_progress {
-            Some(in_progress) => in_progress,
-            x => x.insert(
-                self.row_group_writer_factory
-                    
.create_row_group_writer(self.writer.flushed_row_groups().len())?,
-            ),
-        };
+        // Rows not yet handed to a row group writer. Splitting iterates here 
instead of
+        // recursing, so a small row group limit over a large batch cannot 
exhaust the stack.
+        let mut remaining = batch.clone();
+
+        loop {
+            if self.in_progress.is_none() {
+                let row_group_index = self.writer.flushed_row_groups().len();
+                self.in_progress = Some(
+                    self.row_group_writer_factory
+                        .create_row_group_writer(row_group_index)?,
+                );
+            }
+            let buffered_rows = 
self.in_progress.as_ref().unwrap().buffered_rows;
 
-        if let Some(max_rows) = self.max_row_group_row_count
-            && in_progress.buffered_rows + batch.num_rows() > max_rows
-        {
-            let to_write = max_rows - in_progress.buffered_rows;
-            let a = batch.slice(0, to_write);
-            let b = batch.slice(to_write, batch.num_rows() - to_write);
-            self.write(&a)?;
-            return self.write(&b);
-        }
+            // Leading rows of `remaining` that still fit in the current row 
group, when the
+            // rest has to go to a later one.
+            let mut split_at = match self.max_row_group_row_count {
+                Some(max_rows) if buffered_rows + remaining.num_rows() > 
max_rows => {
+                    Some(max_rows - buffered_rows)
+                }
+                _ => None,
+            };
 
-        // Check byte limit: if we have buffered data, use measured average 
row size
-        // to split batch proactively before exceeding byte limit
-        if let Some(max_bytes) = self.max_row_group_bytes
-            && in_progress.buffered_rows > 0
-        {
-            let current_bytes = in_progress.get_estimated_total_bytes();
+            // Check byte limit: if we have buffered data, use measured 
average row size
+            // to split batch proactively before exceeding byte limit
+            if split_at.is_none()
+                && let Some(max_bytes) = self.max_row_group_bytes
+                && buffered_rows > 0
+            {
+                let current_bytes = self
+                    .in_progress
+                    .as_ref()
+                    .unwrap()
+                    .get_estimated_total_bytes();
 
-            if current_bytes >= max_bytes {
-                self.flush()?;
-                return self.write(batch);
-            }
+                if current_bytes >= max_bytes {
+                    self.flush()?;
+                    continue;
+                }
 
-            if let Some(avg_row_bytes) = current_bytes
-                .checked_div(in_progress.buffered_rows)
-                .filter(|avg_row_bytes| *avg_row_bytes > 0)
-            {
-                // At this point, `current_bytes < max_bytes` (checked above)
-                let remaining_bytes = max_bytes - current_bytes;
-                let rows_that_fit = 
remaining_bytes.checked_div(avg_row_bytes).unwrap_or(0);
-
-                if batch.num_rows() > rows_that_fit {
-                    if rows_that_fit > 0 {
-                        let a = batch.slice(0, rows_that_fit);
-                        let b = batch.slice(rows_that_fit, batch.num_rows() - 
rows_that_fit);
-                        self.write(&a)?;
-                        return self.write(&b);
-                    } else {
-                        self.flush()?;
-                        return self.write(batch);
+                if let Some(avg_row_bytes) = current_bytes
+                    .checked_div(buffered_rows)
+                    .filter(|avg_row_bytes| *avg_row_bytes > 0)
+                {
+                    // At this point, `current_bytes < max_bytes` (checked 
above)
+                    let remaining_bytes = max_bytes - current_bytes;
+                    let rows_that_fit = 
remaining_bytes.checked_div(avg_row_bytes).unwrap_or(0);
+
+                    if remaining.num_rows() > rows_that_fit {
+                        if rows_that_fit > 0 {
+                            split_at = Some(rows_that_fit);
+                        } else {
+                            self.flush()?;
+                            continue;
+                        }
                     }
                 }
             }
-        }
 
-        match self.cdc_chunkers.as_mut() {
-            Some(chunkers) => in_progress.write_with_chunkers(batch, 
chunkers)?,
-            None => in_progress.write(batch)?,
-        }
+            let rest = split_at.map(|to_write| {
+                let rest = remaining.slice(to_write, remaining.num_rows() - 
to_write);
+                remaining = remaining.slice(0, to_write);
+                rest
+            });
+
+            let in_progress = self.in_progress.as_mut().unwrap();

Review Comment:
   it would be nice if we could align with the previous version in not needing 
to do these unwraps multiple times



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to