Skip to main content

sheetkit_core/workbook/
sheet_ops.rs

1use super::*;
2
3impl Workbook {
4    /// Return the names of all sheets in workbook order.
5    pub fn sheet_names(&self) -> Vec<&str> {
6        self.worksheets
7            .iter()
8            .map(|(name, _)| name.as_str())
9            .collect()
10    }
11
12    /// Create a new empty sheet with the given name. Returns the 0-based sheet index.
13    pub fn new_sheet(&mut self, name: &str) -> Result<usize> {
14        let idx = crate::sheet::add_sheet(
15            &mut self.workbook_xml,
16            &mut self.workbook_rels,
17            &mut self.content_types,
18            &mut self.worksheets,
19            name,
20            WorksheetXml::default(),
21        )?;
22        if self.sheet_comments.len() < self.worksheets.len() {
23            self.sheet_comments.push(None);
24        }
25        if self.sheet_sparklines.len() < self.worksheets.len() {
26            self.sheet_sparklines.push(vec![]);
27        }
28        if self.sheet_vml.len() < self.worksheets.len() {
29            self.sheet_vml.push(None);
30        }
31        if self.raw_sheet_xml.len() < self.worksheets.len() {
32            self.raw_sheet_xml.push(None);
33        }
34        if self.sheet_dirty.len() < self.worksheets.len() {
35            self.sheet_dirty.push(true);
36        }
37        if self.sheet_threaded_comments.len() < self.worksheets.len() {
38            self.sheet_threaded_comments.push(None);
39        }
40        if self.sheet_form_controls.len() < self.worksheets.len() {
41            self.sheet_form_controls.push(vec![]);
42        }
43        self.rebuild_sheet_index();
44        Ok(idx)
45    }
46
47    /// Delete a sheet by name.
48    pub fn delete_sheet(&mut self, name: &str) -> Result<()> {
49        let idx = self.sheet_index(name)?;
50        self.assert_parallel_vecs_in_sync();
51
52        crate::sheet::delete_sheet(
53            &mut self.workbook_xml,
54            &mut self.workbook_rels,
55            &mut self.content_types,
56            &mut self.worksheets,
57            name,
58        )?;
59
60        // Remove all per-sheet parallel data at once. After delete_sheet
61        // above, worksheets has already been shortened by 1 so these
62        // vectors must follow.
63        self.sheet_comments.remove(idx);
64        self.sheet_sparklines.remove(idx);
65        self.sheet_vml.remove(idx);
66        self.raw_sheet_xml.remove(idx);
67        self.sheet_dirty.remove(idx);
68        self.sheet_threaded_comments.remove(idx);
69        self.sheet_form_controls.remove(idx);
70
71        // Remove tables belonging to the deleted sheet and re-index remaining.
72        self.tables.retain(|(_, _, si)| *si != idx);
73        for (_, _, si) in &mut self.tables {
74            if *si > idx {
75                *si -= 1;
76            }
77        }
78
79        // Remove and reindex streamed sheet data.
80        self.streamed_sheets.remove(&idx);
81        self.streamed_sheets = self
82            .streamed_sheets
83            .drain()
84            .map(|(i, data)| if i > idx { (i - 1, data) } else { (i, data) })
85            .collect();
86
87        self.reindex_sheet_maps_after_delete(idx);
88        self.rebuild_sheet_index();
89        Ok(())
90    }
91
92    /// Debug assertion that all per-sheet parallel vectors have the same
93    /// length as `worksheets`. Catching desync early prevents silent data
94    /// corruption from mismatched indices.
95    fn assert_parallel_vecs_in_sync(&self) {
96        let n = self.worksheets.len();
97        debug_assert_eq!(self.sheet_comments.len(), n, "sheet_comments desync");
98        debug_assert_eq!(self.sheet_sparklines.len(), n, "sheet_sparklines desync");
99        debug_assert_eq!(self.sheet_vml.len(), n, "sheet_vml desync");
100        debug_assert_eq!(self.raw_sheet_xml.len(), n, "raw_sheet_xml desync");
101        debug_assert_eq!(self.sheet_dirty.len(), n, "sheet_dirty desync");
102        debug_assert_eq!(
103            self.sheet_threaded_comments.len(),
104            n,
105            "sheet_threaded_comments desync"
106        );
107        debug_assert_eq!(
108            self.sheet_form_controls.len(),
109            n,
110            "sheet_form_controls desync"
111        );
112    }
113
114    /// Rename a sheet.
115    pub fn set_sheet_name(&mut self, old_name: &str, new_name: &str) -> Result<()> {
116        crate::sheet::rename_sheet(
117            &mut self.workbook_xml,
118            &mut self.worksheets,
119            old_name,
120            new_name,
121        )?;
122        self.rebuild_sheet_index();
123        Ok(())
124    }
125
126    /// Copy a sheet, returning the 0-based index of the new copy.
127    pub fn copy_sheet(&mut self, source: &str, target: &str) -> Result<usize> {
128        // Resolve the source index before copy_sheet changes the array.
129        let src_idx = self.sheet_index(source)?;
130        // Hydrate the source sheet so copy_sheet clones the real data,
131        // not an empty default.
132        self.ensure_hydrated(src_idx)?;
133        let idx = crate::sheet::copy_sheet(
134            &mut self.workbook_xml,
135            &mut self.workbook_rels,
136            &mut self.content_types,
137            &mut self.worksheets,
138            source,
139            target,
140        )?;
141        if self.sheet_comments.len() < self.worksheets.len() {
142            self.sheet_comments.push(None);
143        }
144        let source_sparklines = self
145            .sheet_sparklines
146            .get(src_idx)
147            .cloned()
148            .unwrap_or_default();
149        if self.sheet_sparklines.len() < self.worksheets.len() {
150            self.sheet_sparklines.push(source_sparklines);
151        }
152        if self.sheet_vml.len() < self.worksheets.len() {
153            self.sheet_vml.push(None);
154        }
155        if self.raw_sheet_xml.len() < self.worksheets.len() {
156            self.raw_sheet_xml.push(None);
157        }
158        if self.sheet_dirty.len() < self.worksheets.len() {
159            self.sheet_dirty.push(true);
160        }
161        if self.sheet_threaded_comments.len() < self.worksheets.len() {
162            self.sheet_threaded_comments.push(None);
163        }
164        if self.sheet_form_controls.len() < self.worksheets.len() {
165            self.sheet_form_controls.push(vec![]);
166        }
167        // Copy streamed data if the source sheet was streamed.
168        if let Some(src_streamed) = self.streamed_sheets.get(&src_idx) {
169            let cloned = src_streamed.try_clone()?;
170            self.streamed_sheets.insert(idx, cloned);
171        }
172        self.rebuild_sheet_index();
173        Ok(idx)
174    }
175
176    /// Get a sheet's 0-based index by name. Returns `None` if not found.
177    pub fn get_sheet_index(&self, name: &str) -> Option<usize> {
178        crate::sheet::find_sheet_index(&self.worksheets, name)
179    }
180
181    /// Get the name of the active sheet.
182    pub fn get_active_sheet(&self) -> &str {
183        let idx = crate::sheet::active_sheet_index(&self.workbook_xml);
184        self.worksheets
185            .get(idx)
186            .map(|(n, _)| n.as_str())
187            .unwrap_or_else(|| self.worksheets[0].0.as_str())
188    }
189
190    /// Set the active sheet by name.
191    pub fn set_active_sheet(&mut self, name: &str) -> Result<()> {
192        let idx = crate::sheet::find_sheet_index(&self.worksheets, name).ok_or_else(|| {
193            Error::SheetNotFound {
194                name: name.to_string(),
195            }
196        })?;
197        crate::sheet::set_active_sheet_index(&mut self.workbook_xml, idx as u32);
198        Ok(())
199    }
200
201    /// Create a [`StreamWriter`](crate::stream::StreamWriter) for a new sheet.
202    ///
203    /// The sheet will be added to the workbook when the StreamWriter is applied
204    /// via [`apply_stream_writer`](Self::apply_stream_writer).
205    pub fn new_stream_writer(&self, sheet_name: &str) -> Result<crate::stream::StreamWriter> {
206        crate::sheet::validate_sheet_name(sheet_name)?;
207        if self.worksheets.iter().any(|(n, _)| n == sheet_name) {
208            return Err(Error::SheetAlreadyExists {
209                name: sheet_name.to_string(),
210            });
211        }
212        Ok(crate::stream::StreamWriter::new(sheet_name))
213    }
214
215    /// Apply a completed [`StreamWriter`](crate::stream::StreamWriter) to the
216    /// workbook, adding it as a new sheet.
217    ///
218    /// The streamed row data stays on disk (in a temp file) and is written
219    /// directly to the ZIP archive during save, keeping memory usage constant
220    /// regardless of the number of rows.
221    ///
222    /// **Note:** Cell values in streamed sheets cannot be read back via
223    /// [`get_cell_value`](Self::get_cell_value) before saving. Save the
224    /// workbook and reopen it to read the data.
225    ///
226    /// Returns the 0-based index of the new sheet.
227    pub fn apply_stream_writer(&mut self, writer: crate::stream::StreamWriter) -> Result<usize> {
228        let (sheet_name, streamed_data) = writer.into_streamed_data()?;
229
230        // Add an empty WorksheetXml placeholder for sheet management
231        // (sheet names, indices, metadata). The actual data lives in the
232        // temp file and is streamed to the ZIP during save.
233        let idx = crate::sheet::add_sheet(
234            &mut self.workbook_xml,
235            &mut self.workbook_rels,
236            &mut self.content_types,
237            &mut self.worksheets,
238            &sheet_name,
239            WorksheetXml::default(),
240        )?;
241        if self.sheet_comments.len() < self.worksheets.len() {
242            self.sheet_comments.push(None);
243        }
244        if self.sheet_sparklines.len() < self.worksheets.len() {
245            self.sheet_sparklines.push(vec![]);
246        }
247        if self.sheet_vml.len() < self.worksheets.len() {
248            self.sheet_vml.push(None);
249        }
250        if self.raw_sheet_xml.len() < self.worksheets.len() {
251            self.raw_sheet_xml.push(None);
252        }
253        if self.sheet_dirty.len() < self.worksheets.len() {
254            self.sheet_dirty.push(true);
255        }
256        if self.sheet_threaded_comments.len() < self.worksheets.len() {
257            self.sheet_threaded_comments.push(None);
258        }
259        if self.sheet_form_controls.len() < self.worksheets.len() {
260            self.sheet_form_controls.push(vec![]);
261        }
262
263        // Store the streamed data for use during save.
264        self.streamed_sheets.insert(idx, streamed_data);
265
266        self.rebuild_sheet_index();
267        Ok(idx)
268    }
269
270    /// Insert `count` empty rows starting at `start_row` in the named sheet.
271    pub fn insert_rows(&mut self, sheet: &str, start_row: u32, count: u32) -> Result<()> {
272        let sheet_idx = self.sheet_index(sheet)?;
273        {
274            let ws = self.worksheet_mut_by_index(sheet_idx)?;
275            crate::row::insert_rows(ws, start_row, count)?;
276        }
277        self.apply_reference_shift_for_sheet(sheet_idx, |col, row| {
278            if row >= start_row {
279                (col, row + count)
280            } else {
281                (col, row)
282            }
283        })
284    }
285
286    /// Remove a single row from the named sheet, shifting rows below it up.
287    pub fn remove_row(&mut self, sheet: &str, row: u32) -> Result<()> {
288        let sheet_idx = self.sheet_index(sheet)?;
289        {
290            let ws = self.worksheet_mut_by_index(sheet_idx)?;
291            crate::row::remove_row(ws, row)?;
292        }
293        self.apply_reference_shift_for_sheet(sheet_idx, |col, r| {
294            if r > row {
295                (col, r - 1)
296            } else {
297                (col, r)
298            }
299        })
300    }
301
302    /// Duplicate a row, inserting the copy directly below.
303    pub fn duplicate_row(&mut self, sheet: &str, row: u32) -> Result<()> {
304        let ws = self.worksheet_mut(sheet)?;
305        crate::row::duplicate_row(ws, row)
306    }
307
308    /// Set the height of a row in points.
309    pub fn set_row_height(&mut self, sheet: &str, row: u32, height: f64) -> Result<()> {
310        let ws = self.worksheet_mut(sheet)?;
311        crate::row::set_row_height(ws, row, height)
312    }
313
314    /// Get the height of a row.
315    pub fn get_row_height(&self, sheet: &str, row: u32) -> Result<Option<f64>> {
316        let ws = self.worksheet_ref(sheet)?;
317        Ok(crate::row::get_row_height(ws, row))
318    }
319
320    /// Set the visibility of a row.
321    pub fn set_row_visible(&mut self, sheet: &str, row: u32, visible: bool) -> Result<()> {
322        let ws = self.worksheet_mut(sheet)?;
323        crate::row::set_row_visible(ws, row, visible)
324    }
325
326    /// Get the visibility of a row. Returns true if visible (not hidden).
327    pub fn get_row_visible(&self, sheet: &str, row: u32) -> Result<bool> {
328        let ws = self.worksheet_ref(sheet)?;
329        Ok(crate::row::get_row_visible(ws, row))
330    }
331
332    /// Set the outline level of a row.
333    pub fn set_row_outline_level(&mut self, sheet: &str, row: u32, level: u8) -> Result<()> {
334        let ws = self.worksheet_mut(sheet)?;
335        crate::row::set_row_outline_level(ws, row, level)
336    }
337
338    /// Get the outline level of a row. Returns 0 if not set.
339    pub fn get_row_outline_level(&self, sheet: &str, row: u32) -> Result<u8> {
340        let ws = self.worksheet_ref(sheet)?;
341        Ok(crate::row::get_row_outline_level(ws, row))
342    }
343
344    /// Set the style for an entire row.
345    ///
346    /// The `style_id` must be a valid index in cellXfs (returned by `add_style`).
347    pub fn set_row_style(&mut self, sheet: &str, row: u32, style_id: u32) -> Result<()> {
348        if style_id as usize >= self.stylesheet.cell_xfs.xfs.len() {
349            return Err(Error::StyleNotFound { id: style_id });
350        }
351        let ws = self.worksheet_mut(sheet)?;
352        crate::row::set_row_style(ws, row, style_id)
353    }
354
355    /// Get the style ID for a row. Returns 0 (default) if not set.
356    pub fn get_row_style(&self, sheet: &str, row: u32) -> Result<u32> {
357        let ws = self.worksheet_ref(sheet)?;
358        Ok(crate::row::get_row_style(ws, row))
359    }
360
361    /// Get all rows with their data from a sheet.
362    ///
363    /// Returns a Vec of `(row_number, Vec<(column_number, CellValue)>)` tuples.
364    /// Column numbers are 1-based (A=1, B=2, ...). Only rows that contain at
365    /// least one cell are included (sparse).
366    #[allow(clippy::type_complexity)]
367    pub fn get_rows(&self, sheet: &str) -> Result<Vec<(u32, Vec<(u32, CellValue)>)>> {
368        let ws = self.worksheet_ref(sheet)?;
369        let style_is_date = self.computed_style_is_date();
370        crate::row::get_rows(ws, &self.sst_runtime, &style_is_date)
371    }
372
373    /// Get all columns with their data from a sheet.
374    ///
375    /// Returns a Vec of `(column_name, Vec<(row_number, CellValue)>)` tuples.
376    /// Only columns that have data are included (sparse).
377    #[allow(clippy::type_complexity)]
378    pub fn get_cols(&self, sheet: &str) -> Result<Vec<(String, Vec<(u32, CellValue)>)>> {
379        let ws = self.worksheet_ref(sheet)?;
380        let style_is_date = self.computed_style_is_date();
381        crate::col::get_cols(ws, &self.sst_runtime, &style_is_date)
382    }
383
384    /// Set the width of a column.
385    pub fn set_col_width(&mut self, sheet: &str, col: &str, width: f64) -> Result<()> {
386        let ws = self.worksheet_mut(sheet)?;
387        crate::col::set_col_width(ws, col, width)
388    }
389
390    /// Get the width of a column.
391    pub fn get_col_width(&self, sheet: &str, col: &str) -> Result<Option<f64>> {
392        let ws = self.worksheet_ref(sheet)?;
393        Ok(crate::col::get_col_width(ws, col))
394    }
395
396    /// Set the visibility of a column.
397    pub fn set_col_visible(&mut self, sheet: &str, col: &str, visible: bool) -> Result<()> {
398        let ws = self.worksheet_mut(sheet)?;
399        crate::col::set_col_visible(ws, col, visible)
400    }
401
402    /// Get the visibility of a column. Returns true if visible (not hidden).
403    pub fn get_col_visible(&self, sheet: &str, col: &str) -> Result<bool> {
404        let ws = self.worksheet_ref(sheet)?;
405        crate::col::get_col_visible(ws, col)
406    }
407
408    /// Set the outline level of a column.
409    pub fn set_col_outline_level(&mut self, sheet: &str, col: &str, level: u8) -> Result<()> {
410        let ws = self.worksheet_mut(sheet)?;
411        crate::col::set_col_outline_level(ws, col, level)
412    }
413
414    /// Get the outline level of a column. Returns 0 if not set.
415    pub fn get_col_outline_level(&self, sheet: &str, col: &str) -> Result<u8> {
416        let ws = self.worksheet_ref(sheet)?;
417        crate::col::get_col_outline_level(ws, col)
418    }
419
420    /// Set the style for an entire column.
421    ///
422    /// The `style_id` must be a valid index in cellXfs (returned by `add_style`).
423    pub fn set_col_style(&mut self, sheet: &str, col: &str, style_id: u32) -> Result<()> {
424        if style_id as usize >= self.stylesheet.cell_xfs.xfs.len() {
425            return Err(Error::StyleNotFound { id: style_id });
426        }
427        let ws = self.worksheet_mut(sheet)?;
428        crate::col::set_col_style(ws, col, style_id)
429    }
430
431    /// Get the style ID for a column. Returns 0 (default) if not set.
432    pub fn get_col_style(&self, sheet: &str, col: &str) -> Result<u32> {
433        let ws = self.worksheet_ref(sheet)?;
434        crate::col::get_col_style(ws, col)
435    }
436
437    /// Insert `count` columns starting at `col` in the named sheet.
438    pub fn insert_cols(&mut self, sheet: &str, col: &str, count: u32) -> Result<()> {
439        let sheet_idx = self.sheet_index(sheet)?;
440        let start_col = column_name_to_number(col)?;
441        {
442            let ws = self.worksheet_mut_by_index(sheet_idx)?;
443            crate::col::insert_cols(ws, col, count)?;
444        }
445        self.apply_reference_shift_for_sheet(sheet_idx, |c, row| {
446            if c >= start_col {
447                (c + count, row)
448            } else {
449                (c, row)
450            }
451        })
452    }
453
454    /// Remove a single column from the named sheet.
455    pub fn remove_col(&mut self, sheet: &str, col: &str) -> Result<()> {
456        let sheet_idx = self.sheet_index(sheet)?;
457        let col_num = column_name_to_number(col)?;
458        {
459            let ws = self.worksheet_mut_by_index(sheet_idx)?;
460            crate::col::remove_col(ws, col)?;
461        }
462        self.apply_reference_shift_for_sheet(sheet_idx, |c, row| {
463            if c > col_num {
464                (c - 1, row)
465            } else {
466                (c, row)
467            }
468        })
469    }
470
471    /// Reindex per-sheet maps after deleting a sheet.
472    pub(crate) fn reindex_sheet_maps_after_delete(&mut self, removed_idx: usize) {
473        self.worksheet_rels = self
474            .worksheet_rels
475            .iter()
476            .filter_map(|(idx, rels)| {
477                if *idx == removed_idx {
478                    None
479                } else if *idx > removed_idx {
480                    Some((idx - 1, rels.clone()))
481                } else {
482                    Some((*idx, rels.clone()))
483                }
484            })
485            .collect();
486
487        self.worksheet_drawings = self
488            .worksheet_drawings
489            .iter()
490            .filter_map(|(idx, drawing_idx)| {
491                if *idx == removed_idx {
492                    None
493                } else if *idx > removed_idx {
494                    Some((idx - 1, *drawing_idx))
495                } else {
496                    Some((*idx, *drawing_idx))
497                }
498            })
499            .collect();
500    }
501
502    /// Apply a cell-reference shift transformation to sheet-scoped structures.
503    pub(crate) fn apply_reference_shift_for_sheet<F>(
504        &mut self,
505        sheet_idx: usize,
506        shift_cell: F,
507    ) -> Result<()>
508    where
509        F: Fn(u32, u32) -> (u32, u32) + Copy,
510    {
511        {
512            let ws = self.worksheet_mut_by_index(sheet_idx)?;
513
514            // Cell formulas.
515            for row in &mut ws.sheet_data.rows {
516                for cell in &mut row.cells {
517                    if let Some(ref mut f) = cell.f {
518                        if let Some(ref mut expr) = f.value {
519                            *expr = shift_cell_references_in_text(expr, shift_cell)?;
520                        }
521                    }
522                }
523            }
524
525            // Merged ranges.
526            if let Some(ref mut merges) = ws.merge_cells {
527                for mc in &mut merges.merge_cells {
528                    mc.reference = shift_cell_references_in_text(&mc.reference, shift_cell)?;
529                }
530                // Invalidate the coordinate cache since references changed.
531                merges.cached_coords.clear();
532            }
533
534            // Auto-filter.
535            if let Some(ref mut af) = ws.auto_filter {
536                af.reference = shift_cell_references_in_text(&af.reference, shift_cell)?;
537            }
538
539            // Data validations.
540            if let Some(ref mut dvs) = ws.data_validations {
541                for dv in &mut dvs.data_validations {
542                    dv.sqref = shift_cell_references_in_text(&dv.sqref, shift_cell)?;
543                    if let Some(ref mut f1) = dv.formula1 {
544                        *f1 = shift_cell_references_in_text(f1, shift_cell)?;
545                    }
546                    if let Some(ref mut f2) = dv.formula2 {
547                        *f2 = shift_cell_references_in_text(f2, shift_cell)?;
548                    }
549                }
550            }
551
552            // Conditional formatting ranges/formulas.
553            for cf in &mut ws.conditional_formatting {
554                cf.sqref = shift_cell_references_in_text(&cf.sqref, shift_cell)?;
555                for rule in &mut cf.cf_rules {
556                    for f in &mut rule.formulas {
557                        *f = shift_cell_references_in_text(f, shift_cell)?;
558                    }
559                }
560            }
561
562            // Hyperlinks.
563            if let Some(ref mut hyperlinks) = ws.hyperlinks {
564                for hl in &mut hyperlinks.hyperlinks {
565                    hl.reference = shift_cell_references_in_text(&hl.reference, shift_cell)?;
566                    if let Some(ref mut loc) = hl.location {
567                        *loc = shift_cell_references_in_text(loc, shift_cell)?;
568                    }
569                }
570            }
571
572            // Pane/selection references.
573            if let Some(ref mut views) = ws.sheet_views {
574                for view in &mut views.sheet_views {
575                    if let Some(ref mut pane) = view.pane {
576                        if let Some(ref mut top_left) = pane.top_left_cell {
577                            *top_left = shift_cell_references_in_text(top_left, shift_cell)?;
578                        }
579                    }
580                    for sel in &mut view.selection {
581                        if let Some(ref mut ac) = sel.active_cell {
582                            *ac = shift_cell_references_in_text(ac, shift_cell)?;
583                        }
584                        if let Some(ref mut sqref) = sel.sqref {
585                            *sqref = shift_cell_references_in_text(sqref, shift_cell)?;
586                        }
587                    }
588                }
589            }
590        }
591
592        // Drawing anchors attached to this sheet.
593        if let Some(&drawing_idx) = self.worksheet_drawings.get(&sheet_idx) {
594            if let Some((_, drawing)) = self.drawings.get_mut(drawing_idx) {
595                for anchor in &mut drawing.one_cell_anchors {
596                    let (new_col, new_row) = shift_cell(anchor.from.col + 1, anchor.from.row + 1);
597                    anchor.from.col = new_col - 1;
598                    anchor.from.row = new_row - 1;
599                }
600                for anchor in &mut drawing.two_cell_anchors {
601                    let (from_col, from_row) = shift_cell(anchor.from.col + 1, anchor.from.row + 1);
602                    anchor.from.col = from_col - 1;
603                    anchor.from.row = from_row - 1;
604                    let (to_col, to_row) = shift_cell(anchor.to.col + 1, anchor.to.row + 1);
605                    anchor.to.col = to_col - 1;
606                    anchor.to.row = to_row - 1;
607                }
608            }
609        }
610
611        Ok(())
612    }
613
614    /// Ensure a drawing exists for the given sheet index, creating one if needed.
615    /// Returns the drawing index.
616    pub(crate) fn ensure_drawing_for_sheet(&mut self, sheet_idx: usize) -> usize {
617        if let Some(&idx) = self.worksheet_drawings.get(&sheet_idx) {
618            return idx;
619        }
620
621        let idx = self.drawings.len();
622        let drawing_path = format!("xl/drawings/drawing{}.xml", idx + 1);
623        self.drawings.push((drawing_path, WsDr::default()));
624        self.worksheet_drawings.insert(sheet_idx, idx);
625
626        // Add drawing reference to the worksheet.
627        let ws_rid = self.next_worksheet_rid(sheet_idx);
628        // ensure_hydrated can only fail if the sheet was never loaded, which
629        // should not happen for a sheet we're actively attaching a drawing to.
630        // Use expect instead of `?` because this method returns `usize`.
631        self.ensure_hydrated(sheet_idx)
632            .expect("sheet must be hydrated before attaching a drawing");
633        self.mark_sheet_dirty(sheet_idx);
634        self.worksheets[sheet_idx].1.get_mut().unwrap().drawing = Some(DrawingRef {
635            r_id: ws_rid.clone(),
636        });
637
638        // Add worksheet->drawing relationship.
639        let drawing_rel_target = format!("../drawings/drawing{}.xml", idx + 1);
640        let ws_rels = self
641            .worksheet_rels
642            .entry(sheet_idx)
643            .or_insert_with(|| Relationships {
644                xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
645                relationships: vec![],
646            });
647        ws_rels.relationships.push(Relationship {
648            id: ws_rid,
649            rel_type: rel_types::DRAWING.to_string(),
650            target: drawing_rel_target,
651            target_mode: None,
652        });
653
654        // Add content type for the drawing.
655        self.content_types.overrides.push(ContentTypeOverride {
656            part_name: format!("/xl/drawings/drawing{}.xml", idx + 1),
657            content_type: mime_types::DRAWING.to_string(),
658        });
659
660        idx
661    }
662
663    /// Generate the next relationship ID for a worksheet's rels.
664    pub(crate) fn next_worksheet_rid(&self, sheet_idx: usize) -> String {
665        let existing = self
666            .worksheet_rels
667            .get(&sheet_idx)
668            .map(|r| r.relationships.as_slice())
669            .unwrap_or(&[]);
670        crate::sheet::next_rid(existing)
671    }
672
673    /// Generate the next relationship ID for a drawing's rels.
674    pub(crate) fn next_drawing_rid(&self, drawing_idx: usize) -> String {
675        let existing = self
676            .drawing_rels
677            .get(&drawing_idx)
678            .map(|r| r.relationships.as_slice())
679            .unwrap_or(&[]);
680        crate::sheet::next_rid(existing)
681    }
682}
683
684#[cfg(test)]
685#[allow(clippy::approx_constant)]
686mod tests {
687    use super::*;
688    use tempfile::TempDir;
689
690    #[test]
691    fn test_new_sheet_basic() {
692        let mut wb = Workbook::new();
693        let idx = wb.new_sheet("Sheet2").unwrap();
694        assert_eq!(idx, 1);
695        assert_eq!(wb.sheet_names(), vec!["Sheet1", "Sheet2"]);
696    }
697
698    #[test]
699    fn test_new_sheet_duplicate_returns_error() {
700        let mut wb = Workbook::new();
701        let result = wb.new_sheet("Sheet1");
702        assert!(result.is_err());
703        assert!(matches!(
704            result.unwrap_err(),
705            Error::SheetAlreadyExists { .. }
706        ));
707    }
708
709    #[test]
710    fn test_new_sheet_invalid_name_returns_error() {
711        let mut wb = Workbook::new();
712        let result = wb.new_sheet("Bad/Name");
713        assert!(result.is_err());
714        assert!(matches!(result.unwrap_err(), Error::InvalidSheetName(_)));
715    }
716
717    #[test]
718    fn test_delete_sheet_basic() {
719        let mut wb = Workbook::new();
720        wb.new_sheet("Sheet2").unwrap();
721        wb.delete_sheet("Sheet1").unwrap();
722        assert_eq!(wb.sheet_names(), vec!["Sheet2"]);
723    }
724
725    #[test]
726    fn test_delete_sheet_keeps_parallel_vecs_in_sync() {
727        let mut wb = Workbook::new();
728        wb.new_sheet("Sheet2").unwrap();
729        wb.new_sheet("Sheet3").unwrap();
730
731        // Add comments to Sheet2 (middle sheet).
732        wb.add_comment(
733            "Sheet2",
734            &crate::comment::CommentConfig {
735                cell: "A1".to_string(),
736                author: "Test".to_string(),
737                text: "note".to_string(),
738            },
739        )
740        .unwrap();
741
742        // Delete the middle sheet and verify no panic.
743        wb.delete_sheet("Sheet2").unwrap();
744        assert_eq!(wb.sheet_names(), vec!["Sheet1", "Sheet3"]);
745
746        // After deletion, adding a comment to Sheet3 (now index 1)
747        // should work without index mismatch.
748        wb.add_comment(
749            "Sheet3",
750            &crate::comment::CommentConfig {
751                cell: "B2".to_string(),
752                author: "Test".to_string(),
753                text: "note2".to_string(),
754            },
755        )
756        .unwrap();
757    }
758
759    #[test]
760    fn test_delete_last_sheet_returns_error() {
761        let mut wb = Workbook::new();
762        let result = wb.delete_sheet("Sheet1");
763        assert!(result.is_err());
764    }
765
766    #[test]
767    fn test_delete_nonexistent_sheet_returns_error() {
768        let mut wb = Workbook::new();
769        let result = wb.delete_sheet("NoSuchSheet");
770        assert!(result.is_err());
771        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
772    }
773
774    #[test]
775    fn test_set_sheet_name_basic() {
776        let mut wb = Workbook::new();
777        wb.set_sheet_name("Sheet1", "Renamed").unwrap();
778        assert_eq!(wb.sheet_names(), vec!["Renamed"]);
779    }
780
781    #[test]
782    fn test_set_sheet_name_to_existing_returns_error() {
783        let mut wb = Workbook::new();
784        wb.new_sheet("Sheet2").unwrap();
785        let result = wb.set_sheet_name("Sheet1", "Sheet2");
786        assert!(result.is_err());
787        assert!(matches!(
788            result.unwrap_err(),
789            Error::SheetAlreadyExists { .. }
790        ));
791    }
792
793    #[test]
794    fn test_copy_sheet_basic() {
795        let mut wb = Workbook::new();
796        let idx = wb.copy_sheet("Sheet1", "Sheet1 Copy").unwrap();
797        assert_eq!(idx, 1);
798        assert_eq!(wb.sheet_names(), vec!["Sheet1", "Sheet1 Copy"]);
799    }
800
801    #[test]
802    fn test_get_sheet_index() {
803        let mut wb = Workbook::new();
804        wb.new_sheet("Sheet2").unwrap();
805        assert_eq!(wb.get_sheet_index("Sheet1"), Some(0));
806        assert_eq!(wb.get_sheet_index("Sheet2"), Some(1));
807        assert_eq!(wb.get_sheet_index("Nonexistent"), None);
808    }
809
810    #[test]
811    fn test_get_active_sheet_default() {
812        let wb = Workbook::new();
813        assert_eq!(wb.get_active_sheet(), "Sheet1");
814    }
815
816    #[test]
817    fn test_set_active_sheet() {
818        let mut wb = Workbook::new();
819        wb.new_sheet("Sheet2").unwrap();
820        wb.set_active_sheet("Sheet2").unwrap();
821        assert_eq!(wb.get_active_sheet(), "Sheet2");
822    }
823
824    #[test]
825    fn test_set_active_sheet_not_found() {
826        let mut wb = Workbook::new();
827        let result = wb.set_active_sheet("NoSuchSheet");
828        assert!(result.is_err());
829        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
830    }
831
832    #[test]
833    fn test_sheet_management_roundtrip_save_open() {
834        let dir = TempDir::new().unwrap();
835        let path = dir.path().join("sheet_mgmt.xlsx");
836
837        let mut wb = Workbook::new();
838        wb.new_sheet("Data").unwrap();
839        wb.new_sheet("Summary").unwrap();
840        wb.set_sheet_name("Sheet1", "Overview").unwrap();
841        wb.save(&path).unwrap();
842
843        let wb2 = Workbook::open(&path).unwrap();
844        assert_eq!(wb2.sheet_names(), vec!["Overview", "Data", "Summary"]);
845    }
846
847    #[test]
848    fn test_workbook_insert_rows() {
849        let mut wb = Workbook::new();
850        wb.set_cell_value("Sheet1", "A1", "stay").unwrap();
851        wb.set_cell_value("Sheet1", "A2", "shift").unwrap();
852        wb.insert_rows("Sheet1", 2, 1).unwrap();
853
854        assert_eq!(
855            wb.get_cell_value("Sheet1", "A1").unwrap(),
856            CellValue::String("stay".to_string())
857        );
858        assert_eq!(
859            wb.get_cell_value("Sheet1", "A3").unwrap(),
860            CellValue::String("shift".to_string())
861        );
862        assert_eq!(wb.get_cell_value("Sheet1", "A2").unwrap(), CellValue::Empty);
863    }
864
865    #[test]
866    fn test_workbook_insert_rows_updates_formula_and_ranges() {
867        let mut wb = Workbook::new();
868        wb.set_cell_value(
869            "Sheet1",
870            "C1",
871            CellValue::Formula {
872                expr: "SUM(A2:B2)".to_string(),
873                result: None,
874            },
875        )
876        .unwrap();
877        wb.add_data_validation(
878            "Sheet1",
879            &crate::validation::DataValidationConfig::whole_number("A2:A5", 1, 9),
880        )
881        .unwrap();
882        wb.set_auto_filter("Sheet1", "A2:B10").unwrap();
883        wb.merge_cells("Sheet1", "A2", "B3").unwrap();
884
885        wb.insert_rows("Sheet1", 2, 1).unwrap();
886
887        match wb.get_cell_value("Sheet1", "C1").unwrap() {
888            CellValue::Formula { expr, .. } => assert_eq!(expr, "SUM(A3:B3)"),
889            other => panic!("expected formula, got {other:?}"),
890        }
891
892        let validations = wb.get_data_validations("Sheet1").unwrap();
893        assert_eq!(validations.len(), 1);
894        assert_eq!(validations[0].sqref, "A3:A6");
895
896        let merges = wb.get_merge_cells("Sheet1").unwrap();
897        assert_eq!(merges, vec!["A3:B4".to_string()]);
898
899        let ws = wb.worksheet_ref("Sheet1").unwrap();
900        assert_eq!(ws.auto_filter.as_ref().unwrap().reference, "A3:B11");
901    }
902
903    #[test]
904    fn test_workbook_insert_rows_sheet_not_found() {
905        let mut wb = Workbook::new();
906        let result = wb.insert_rows("NoSheet", 1, 1);
907        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
908    }
909
910    #[test]
911    fn test_workbook_remove_row() {
912        let mut wb = Workbook::new();
913        wb.set_cell_value("Sheet1", "A1", "first").unwrap();
914        wb.set_cell_value("Sheet1", "A2", "second").unwrap();
915        wb.set_cell_value("Sheet1", "A3", "third").unwrap();
916        wb.remove_row("Sheet1", 2).unwrap();
917
918        assert_eq!(
919            wb.get_cell_value("Sheet1", "A1").unwrap(),
920            CellValue::String("first".to_string())
921        );
922        assert_eq!(
923            wb.get_cell_value("Sheet1", "A2").unwrap(),
924            CellValue::String("third".to_string())
925        );
926    }
927
928    #[test]
929    fn test_workbook_remove_row_sheet_not_found() {
930        let mut wb = Workbook::new();
931        let result = wb.remove_row("NoSheet", 1);
932        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
933    }
934
935    #[test]
936    fn test_workbook_duplicate_row() {
937        let mut wb = Workbook::new();
938        wb.set_cell_value("Sheet1", "A1", "original").unwrap();
939        wb.duplicate_row("Sheet1", 1).unwrap();
940
941        assert_eq!(
942            wb.get_cell_value("Sheet1", "A1").unwrap(),
943            CellValue::String("original".to_string())
944        );
945        // The duplicated row at row 2 has the same SST index.
946        assert_eq!(
947            wb.get_cell_value("Sheet1", "A2").unwrap(),
948            CellValue::String("original".to_string())
949        );
950    }
951
952    #[test]
953    fn test_workbook_set_and_get_row_height() {
954        let mut wb = Workbook::new();
955        wb.set_row_height("Sheet1", 3, 25.0).unwrap();
956        assert_eq!(wb.get_row_height("Sheet1", 3).unwrap(), Some(25.0));
957    }
958
959    #[test]
960    fn test_workbook_get_row_height_sheet_not_found() {
961        let wb = Workbook::new();
962        let result = wb.get_row_height("NoSheet", 1);
963        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
964    }
965
966    #[test]
967    fn test_workbook_set_row_visible() {
968        let mut wb = Workbook::new();
969        wb.set_row_visible("Sheet1", 1, false).unwrap();
970    }
971
972    #[test]
973    fn test_workbook_set_row_visible_sheet_not_found() {
974        let mut wb = Workbook::new();
975        let result = wb.set_row_visible("NoSheet", 1, false);
976        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
977    }
978
979    #[test]
980    fn test_workbook_set_and_get_col_width() {
981        let mut wb = Workbook::new();
982        wb.set_col_width("Sheet1", "A", 18.0).unwrap();
983        assert_eq!(wb.get_col_width("Sheet1", "A").unwrap(), Some(18.0));
984    }
985
986    #[test]
987    fn test_workbook_get_col_width_sheet_not_found() {
988        let wb = Workbook::new();
989        let result = wb.get_col_width("NoSheet", "A");
990        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
991    }
992
993    #[test]
994    fn test_workbook_set_col_visible() {
995        let mut wb = Workbook::new();
996        wb.set_col_visible("Sheet1", "B", false).unwrap();
997    }
998
999    #[test]
1000    fn test_workbook_set_col_visible_sheet_not_found() {
1001        let mut wb = Workbook::new();
1002        let result = wb.set_col_visible("NoSheet", "A", false);
1003        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
1004    }
1005
1006    #[test]
1007    fn test_workbook_insert_cols() {
1008        let mut wb = Workbook::new();
1009        wb.set_cell_value("Sheet1", "A1", "a").unwrap();
1010        wb.set_cell_value("Sheet1", "B1", "b").unwrap();
1011        wb.insert_cols("Sheet1", "B", 1).unwrap();
1012
1013        assert_eq!(
1014            wb.get_cell_value("Sheet1", "A1").unwrap(),
1015            CellValue::String("a".to_string())
1016        );
1017        assert_eq!(wb.get_cell_value("Sheet1", "B1").unwrap(), CellValue::Empty);
1018        assert_eq!(
1019            wb.get_cell_value("Sheet1", "C1").unwrap(),
1020            CellValue::String("b".to_string())
1021        );
1022    }
1023
1024    #[test]
1025    fn test_workbook_insert_cols_updates_formula_and_ranges() {
1026        let mut wb = Workbook::new();
1027        wb.set_cell_value(
1028            "Sheet1",
1029            "D1",
1030            CellValue::Formula {
1031                expr: "SUM(A1:B1)".to_string(),
1032                result: None,
1033            },
1034        )
1035        .unwrap();
1036        wb.add_data_validation(
1037            "Sheet1",
1038            &crate::validation::DataValidationConfig::whole_number("B2:C3", 1, 9),
1039        )
1040        .unwrap();
1041        wb.set_auto_filter("Sheet1", "A1:C10").unwrap();
1042        wb.merge_cells("Sheet1", "B3", "C4").unwrap();
1043
1044        wb.insert_cols("Sheet1", "B", 2).unwrap();
1045
1046        match wb.get_cell_value("Sheet1", "F1").unwrap() {
1047            CellValue::Formula { expr, .. } => assert_eq!(expr, "SUM(A1:D1)"),
1048            other => panic!("expected formula, got {other:?}"),
1049        }
1050
1051        let validations = wb.get_data_validations("Sheet1").unwrap();
1052        assert_eq!(validations.len(), 1);
1053        assert_eq!(validations[0].sqref, "D2:E3");
1054
1055        let merges = wb.get_merge_cells("Sheet1").unwrap();
1056        assert_eq!(merges, vec!["D3:E4".to_string()]);
1057
1058        let ws = wb.worksheet_ref("Sheet1").unwrap();
1059        assert_eq!(ws.auto_filter.as_ref().unwrap().reference, "A1:E10");
1060    }
1061
1062    #[test]
1063    fn test_workbook_insert_cols_sheet_not_found() {
1064        let mut wb = Workbook::new();
1065        let result = wb.insert_cols("NoSheet", "A", 1);
1066        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
1067    }
1068
1069    #[test]
1070    fn test_workbook_remove_col() {
1071        let mut wb = Workbook::new();
1072        wb.set_cell_value("Sheet1", "A1", "a").unwrap();
1073        wb.set_cell_value("Sheet1", "B1", "b").unwrap();
1074        wb.set_cell_value("Sheet1", "C1", "c").unwrap();
1075        wb.remove_col("Sheet1", "B").unwrap();
1076
1077        assert_eq!(
1078            wb.get_cell_value("Sheet1", "A1").unwrap(),
1079            CellValue::String("a".to_string())
1080        );
1081        assert_eq!(
1082            wb.get_cell_value("Sheet1", "B1").unwrap(),
1083            CellValue::String("c".to_string())
1084        );
1085    }
1086
1087    #[test]
1088    fn test_workbook_remove_col_sheet_not_found() {
1089        let mut wb = Workbook::new();
1090        let result = wb.remove_col("NoSheet", "A");
1091        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
1092    }
1093
1094    #[test]
1095    fn test_new_stream_writer_validates_name() {
1096        let wb = Workbook::new();
1097        let result = wb.new_stream_writer("Bad[Name");
1098        assert!(result.is_err());
1099        assert!(matches!(result.unwrap_err(), Error::InvalidSheetName(_)));
1100    }
1101
1102    #[test]
1103    fn test_new_stream_writer_rejects_duplicate() {
1104        let wb = Workbook::new();
1105        let result = wb.new_stream_writer("Sheet1");
1106        assert!(result.is_err());
1107        assert!(matches!(
1108            result.unwrap_err(),
1109            Error::SheetAlreadyExists { .. }
1110        ));
1111    }
1112
1113    #[test]
1114    fn test_new_stream_writer_valid_name() {
1115        let wb = Workbook::new();
1116        let sw = wb.new_stream_writer("StreamSheet").unwrap();
1117        assert_eq!(sw.sheet_name(), "StreamSheet");
1118    }
1119
1120    #[test]
1121    fn test_apply_stream_writer_adds_sheet() {
1122        let mut wb = Workbook::new();
1123        let mut sw = wb.new_stream_writer("StreamSheet").unwrap();
1124        sw.write_row(1, &[CellValue::from("Hello"), CellValue::from(42)])
1125            .unwrap();
1126        let idx = wb.apply_stream_writer(sw).unwrap();
1127        assert_eq!(idx, 1);
1128        assert_eq!(wb.sheet_names(), vec!["Sheet1", "StreamSheet"]);
1129    }
1130
1131    #[test]
1132    fn test_apply_stream_writer_uses_inline_strings() {
1133        // Streamed sheets use inline strings, not the shared string table.
1134        let mut wb = Workbook::new();
1135        wb.set_cell_value("Sheet1", "A1", "Existing").unwrap();
1136        let sst_before = wb.sst_runtime.len();
1137
1138        let mut sw = wb.new_stream_writer("StreamSheet").unwrap();
1139        sw.write_row(1, &[CellValue::from("New"), CellValue::from("Existing")])
1140            .unwrap();
1141        wb.apply_stream_writer(sw).unwrap();
1142
1143        // SST should not grow because streamed sheets use inline strings.
1144        assert_eq!(wb.sst_runtime.len(), sst_before);
1145    }
1146
1147    #[test]
1148    fn test_stream_writer_save_and_reopen() {
1149        let dir = TempDir::new().unwrap();
1150        let path = dir.path().join("stream_test.xlsx");
1151
1152        let mut wb = Workbook::new();
1153        wb.set_cell_value("Sheet1", "A1", "Normal").unwrap();
1154
1155        let mut sw = wb.new_stream_writer("Streamed").unwrap();
1156        sw.write_row(1, &[CellValue::from("Name"), CellValue::from("Value")])
1157            .unwrap();
1158        sw.write_row(2, &[CellValue::from("Alice"), CellValue::from(100)])
1159            .unwrap();
1160        sw.write_row(3, &[CellValue::from("Bob"), CellValue::from(200)])
1161            .unwrap();
1162        wb.apply_stream_writer(sw).unwrap();
1163
1164        wb.save(&path).unwrap();
1165
1166        let wb2 = Workbook::open(&path).unwrap();
1167        assert_eq!(wb2.sheet_names(), vec!["Sheet1", "Streamed"]);
1168        assert_eq!(
1169            wb2.get_cell_value("Sheet1", "A1").unwrap(),
1170            CellValue::String("Normal".to_string())
1171        );
1172        assert_eq!(
1173            wb2.get_cell_value("Streamed", "A1").unwrap(),
1174            CellValue::String("Name".to_string())
1175        );
1176        assert_eq!(
1177            wb2.get_cell_value("Streamed", "B2").unwrap(),
1178            CellValue::Number(100.0)
1179        );
1180        assert_eq!(
1181            wb2.get_cell_value("Streamed", "A3").unwrap(),
1182            CellValue::String("Bob".to_string())
1183        );
1184    }
1185
1186    #[test]
1187    fn test_workbook_get_rows_empty_sheet() {
1188        let wb = Workbook::new();
1189        let rows = wb.get_rows("Sheet1").unwrap();
1190        assert!(rows.is_empty());
1191    }
1192
1193    #[test]
1194    fn test_workbook_get_rows_with_data() {
1195        let mut wb = Workbook::new();
1196        wb.set_cell_value("Sheet1", "A1", "Name").unwrap();
1197        wb.set_cell_value("Sheet1", "B1", 42.0).unwrap();
1198        wb.set_cell_value("Sheet1", "A2", "Alice").unwrap();
1199        wb.set_cell_value("Sheet1", "B2", true).unwrap();
1200
1201        let rows = wb.get_rows("Sheet1").unwrap();
1202        assert_eq!(rows.len(), 2);
1203        assert_eq!(rows[0].0, 1);
1204        assert_eq!(rows[0].1.len(), 2);
1205        assert_eq!(rows[0].1[0].0, 1);
1206        assert_eq!(rows[0].1[0].1, CellValue::String("Name".to_string()));
1207        assert_eq!(rows[0].1[1].0, 2);
1208        assert_eq!(rows[0].1[1].1, CellValue::Number(42.0));
1209        assert_eq!(rows[1].0, 2);
1210        assert_eq!(rows[1].1[0].1, CellValue::String("Alice".to_string()));
1211        assert_eq!(rows[1].1[1].1, CellValue::Bool(true));
1212    }
1213
1214    #[test]
1215    fn test_workbook_get_rows_sheet_not_found() {
1216        let wb = Workbook::new();
1217        assert!(wb.get_rows("NoSheet").is_err());
1218    }
1219
1220    #[test]
1221    fn test_workbook_get_cols_empty_sheet() {
1222        let wb = Workbook::new();
1223        let cols = wb.get_cols("Sheet1").unwrap();
1224        assert!(cols.is_empty());
1225    }
1226
1227    #[test]
1228    fn test_workbook_get_cols_with_data() {
1229        let mut wb = Workbook::new();
1230        wb.set_cell_value("Sheet1", "A1", "Name").unwrap();
1231        wb.set_cell_value("Sheet1", "B1", 42.0).unwrap();
1232        wb.set_cell_value("Sheet1", "A2", "Alice").unwrap();
1233        wb.set_cell_value("Sheet1", "B2", 30.0).unwrap();
1234
1235        let cols = wb.get_cols("Sheet1").unwrap();
1236        assert_eq!(cols.len(), 2);
1237        assert_eq!(cols[0].0, "A");
1238        assert_eq!(cols[0].1.len(), 2);
1239        assert_eq!(cols[0].1[0], (1, CellValue::String("Name".to_string())));
1240        assert_eq!(cols[0].1[1], (2, CellValue::String("Alice".to_string())));
1241        assert_eq!(cols[1].0, "B");
1242        assert_eq!(cols[1].1[0], (1, CellValue::Number(42.0)));
1243        assert_eq!(cols[1].1[1], (2, CellValue::Number(30.0)));
1244    }
1245
1246    #[test]
1247    fn test_workbook_get_cols_sheet_not_found() {
1248        let wb = Workbook::new();
1249        assert!(wb.get_cols("NoSheet").is_err());
1250    }
1251
1252    #[test]
1253    fn test_streamed_sheet_cells_empty_before_save() {
1254        // Streamed sheet data lives in a temp file, not in the WorksheetXml.
1255        // Reading cells before save returns Empty.
1256        let mut wb = Workbook::new();
1257        let mut sw = wb.new_stream_writer("Streamed").unwrap();
1258        sw.write_row(1, &[CellValue::from("Name"), CellValue::from("Age")])
1259            .unwrap();
1260        wb.apply_stream_writer(sw).unwrap();
1261
1262        assert_eq!(
1263            wb.get_cell_value("Streamed", "A1").unwrap(),
1264            CellValue::Empty
1265        );
1266        assert_eq!(
1267            wb.get_cell_value("Streamed", "B1").unwrap(),
1268            CellValue::Empty
1269        );
1270    }
1271
1272    #[test]
1273    fn test_streamed_sheet_readable_after_save_reopen() {
1274        let dir = TempDir::new().unwrap();
1275        let path = dir.path().join("stream_reopen.xlsx");
1276
1277        let mut wb = Workbook::new();
1278        let mut sw = wb.new_stream_writer("Streamed").unwrap();
1279        sw.write_row(1, &[CellValue::from("Name"), CellValue::from("Age")])
1280            .unwrap();
1281        sw.write_row(2, &[CellValue::from("Alice"), CellValue::from(30)])
1282            .unwrap();
1283        wb.apply_stream_writer(sw).unwrap();
1284        wb.save(&path).unwrap();
1285
1286        let wb2 = Workbook::open(&path).unwrap();
1287        assert_eq!(
1288            wb2.get_cell_value("Streamed", "A1").unwrap(),
1289            CellValue::String("Name".to_string())
1290        );
1291        assert_eq!(
1292            wb2.get_cell_value("Streamed", "B1").unwrap(),
1293            CellValue::String("Age".to_string())
1294        );
1295        assert_eq!(
1296            wb2.get_cell_value("Streamed", "A2").unwrap(),
1297            CellValue::String("Alice".to_string())
1298        );
1299        assert_eq!(
1300            wb2.get_cell_value("Streamed", "B2").unwrap(),
1301            CellValue::Number(30.0)
1302        );
1303    }
1304
1305    #[test]
1306    fn test_workbook_get_rows_roundtrip_save_open() {
1307        let mut wb = Workbook::new();
1308        wb.set_cell_value("Sheet1", "A1", "hello").unwrap();
1309        wb.set_cell_value("Sheet1", "B1", 99.0).unwrap();
1310        wb.set_cell_value("Sheet1", "A2", true).unwrap();
1311
1312        let tmp = std::env::temp_dir().join("test_get_rows_roundtrip.xlsx");
1313        wb.save(&tmp).unwrap();
1314
1315        let wb2 = Workbook::open(&tmp).unwrap();
1316        let rows = wb2.get_rows("Sheet1").unwrap();
1317        assert_eq!(rows.len(), 2);
1318        assert_eq!(rows[0].1[0].1, CellValue::String("hello".to_string()));
1319        assert_eq!(rows[0].1[1].1, CellValue::Number(99.0));
1320        assert_eq!(rows[1].1[0].1, CellValue::Bool(true));
1321
1322        let _ = std::fs::remove_file(&tmp);
1323    }
1324
1325    #[test]
1326    fn test_stream_save_reopen_basic() {
1327        let dir = TempDir::new().unwrap();
1328        let path = dir.path().join("stream_basic.xlsx");
1329
1330        let mut wb = Workbook::new();
1331        let mut sw = wb.new_stream_writer("Optimized").unwrap();
1332        sw.write_row(1, &[CellValue::from("Hello"), CellValue::from(42)])
1333            .unwrap();
1334        sw.write_row(2, &[CellValue::from("World"), CellValue::from(99)])
1335            .unwrap();
1336        let idx = wb.apply_stream_writer(sw).unwrap();
1337        assert_eq!(idx, 1);
1338
1339        wb.save(&path).unwrap();
1340        let wb2 = Workbook::open(&path).unwrap();
1341        assert_eq!(
1342            wb2.get_cell_value("Optimized", "A1").unwrap(),
1343            CellValue::String("Hello".to_string())
1344        );
1345        assert_eq!(
1346            wb2.get_cell_value("Optimized", "B1").unwrap(),
1347            CellValue::Number(42.0)
1348        );
1349        assert_eq!(
1350            wb2.get_cell_value("Optimized", "A2").unwrap(),
1351            CellValue::String("World".to_string())
1352        );
1353        assert_eq!(
1354            wb2.get_cell_value("Optimized", "B2").unwrap(),
1355            CellValue::Number(99.0)
1356        );
1357    }
1358
1359    #[test]
1360    fn test_stream_save_reopen_all_types() {
1361        let dir = TempDir::new().unwrap();
1362        let path = dir.path().join("stream_types.xlsx");
1363
1364        let mut wb = Workbook::new();
1365        let mut sw = wb.new_stream_writer("Types").unwrap();
1366        sw.write_row(
1367            1,
1368            &[
1369                CellValue::from("text"),
1370                CellValue::from(42),
1371                CellValue::from(3.14),
1372                CellValue::from(true),
1373                CellValue::Formula {
1374                    expr: "SUM(B1:C1)".to_string(),
1375                    result: None,
1376                },
1377                CellValue::Error("#N/A".to_string()),
1378                CellValue::Empty,
1379            ],
1380        )
1381        .unwrap();
1382        wb.apply_stream_writer(sw).unwrap();
1383
1384        wb.save(&path).unwrap();
1385        let wb2 = Workbook::open(&path).unwrap();
1386        assert_eq!(
1387            wb2.get_cell_value("Types", "A1").unwrap(),
1388            CellValue::String("text".to_string())
1389        );
1390        assert_eq!(
1391            wb2.get_cell_value("Types", "B1").unwrap(),
1392            CellValue::Number(42.0)
1393        );
1394        assert_eq!(
1395            wb2.get_cell_value("Types", "D1").unwrap(),
1396            CellValue::Bool(true)
1397        );
1398        match wb2.get_cell_value("Types", "E1").unwrap() {
1399            CellValue::Formula { expr, .. } => assert_eq!(expr, "SUM(B1:C1)"),
1400            other => panic!("expected formula, got {other:?}"),
1401        }
1402        assert_eq!(
1403            wb2.get_cell_value("Types", "F1").unwrap(),
1404            CellValue::Error("#N/A".to_string())
1405        );
1406        assert_eq!(wb2.get_cell_value("Types", "G1").unwrap(), CellValue::Empty);
1407    }
1408
1409    #[test]
1410    fn test_apply_stream_optimized_save_reopen() {
1411        let dir = TempDir::new().unwrap();
1412        let path = dir.path().join("stream_optimized.xlsx");
1413
1414        let mut wb = Workbook::new();
1415        wb.set_cell_value("Sheet1", "A1", "Normal").unwrap();
1416
1417        let mut sw = wb.new_stream_writer("Fast").unwrap();
1418        sw.write_row(1, &[CellValue::from("Name"), CellValue::from("Value")])
1419            .unwrap();
1420        sw.write_row(2, &[CellValue::from("Alice"), CellValue::from(100)])
1421            .unwrap();
1422        sw.write_row(3, &[CellValue::from("Bob"), CellValue::from(200)])
1423            .unwrap();
1424        wb.apply_stream_writer(sw).unwrap();
1425
1426        wb.save(&path).unwrap();
1427
1428        let wb2 = Workbook::open(&path).unwrap();
1429        assert_eq!(wb2.sheet_names(), vec!["Sheet1", "Fast"]);
1430        assert_eq!(
1431            wb2.get_cell_value("Fast", "A1").unwrap(),
1432            CellValue::String("Name".to_string())
1433        );
1434        assert_eq!(
1435            wb2.get_cell_value("Fast", "B2").unwrap(),
1436            CellValue::Number(100.0)
1437        );
1438        assert_eq!(
1439            wb2.get_cell_value("Fast", "A3").unwrap(),
1440            CellValue::String("Bob".to_string())
1441        );
1442    }
1443
1444    #[test]
1445    fn test_stream_freeze_panes_roundtrip() {
1446        let dir = TempDir::new().unwrap();
1447        let path = dir.path().join("stream_freeze.xlsx");
1448
1449        let mut wb = Workbook::new();
1450        let mut sw = wb.new_stream_writer("FreezeSheet").unwrap();
1451        sw.set_freeze_panes("B3").unwrap();
1452        sw.write_row(1, &[CellValue::from("A"), CellValue::from("B")])
1453            .unwrap();
1454        sw.write_row(2, &[CellValue::from("C"), CellValue::from("D")])
1455            .unwrap();
1456        wb.apply_stream_writer(sw).unwrap();
1457        wb.save(&path).unwrap();
1458
1459        let wb2 = Workbook::open(&path).unwrap();
1460        assert_eq!(
1461            wb2.get_panes("FreezeSheet").unwrap(),
1462            Some("B3".to_string())
1463        );
1464        assert_eq!(
1465            wb2.get_cell_value("FreezeSheet", "A1").unwrap(),
1466            CellValue::String("A".to_string())
1467        );
1468    }
1469
1470    #[test]
1471    fn test_stream_merge_cells_roundtrip() {
1472        let dir = TempDir::new().unwrap();
1473        let path = dir.path().join("stream_merge.xlsx");
1474
1475        let mut wb = Workbook::new();
1476        let mut sw = wb.new_stream_writer("MergeSheet").unwrap();
1477        sw.add_merge_cell("A1:C1").unwrap();
1478        sw.add_merge_cell("A3:B4").unwrap();
1479        sw.write_row(1, &[CellValue::from("Header")]).unwrap();
1480        sw.write_row(2, &[CellValue::from("Data")]).unwrap();
1481        wb.apply_stream_writer(sw).unwrap();
1482        wb.save(&path).unwrap();
1483
1484        let wb2 = Workbook::open(&path).unwrap();
1485        let merges = wb2.get_merge_cells("MergeSheet").unwrap();
1486        assert!(merges.contains(&"A1:C1".to_string()));
1487        assert!(merges.contains(&"A3:B4".to_string()));
1488        assert_eq!(
1489            wb2.get_cell_value("MergeSheet", "A1").unwrap(),
1490            CellValue::String("Header".to_string())
1491        );
1492    }
1493
1494    #[test]
1495    fn test_stream_col_widths_roundtrip() {
1496        let dir = TempDir::new().unwrap();
1497        let path = dir.path().join("stream_colw.xlsx");
1498
1499        let mut wb = Workbook::new();
1500        let mut sw = wb.new_stream_writer("ColSheet").unwrap();
1501        sw.set_col_width(1, 25.0).unwrap();
1502        sw.set_col_width(2, 12.5).unwrap();
1503        sw.write_row(1, &[CellValue::from("Wide"), CellValue::from("Narrow")])
1504            .unwrap();
1505        wb.apply_stream_writer(sw).unwrap();
1506        wb.save(&path).unwrap();
1507
1508        let wb2 = Workbook::open(&path).unwrap();
1509        let w1 = wb2.get_col_width("ColSheet", "A").unwrap().unwrap();
1510        let w2 = wb2.get_col_width("ColSheet", "B").unwrap().unwrap();
1511        assert!((w1 - 25.0).abs() < 0.01);
1512        assert!((w2 - 12.5).abs() < 0.01);
1513    }
1514
1515    #[test]
1516    fn test_stream_multiple_sheets() {
1517        let dir = TempDir::new().unwrap();
1518        let path = dir.path().join("stream_multi.xlsx");
1519
1520        let mut wb = Workbook::new();
1521        wb.set_cell_value("Sheet1", "A1", "Normal").unwrap();
1522
1523        let mut sw1 = wb.new_stream_writer("Stream1").unwrap();
1524        sw1.write_row(1, &[CellValue::from("S1R1")]).unwrap();
1525        sw1.write_row(2, &[CellValue::from("S1R2")]).unwrap();
1526        wb.apply_stream_writer(sw1).unwrap();
1527
1528        let mut sw2 = wb.new_stream_writer("Stream2").unwrap();
1529        sw2.write_row(1, &[CellValue::from("S2R1")]).unwrap();
1530        wb.apply_stream_writer(sw2).unwrap();
1531
1532        wb.save(&path).unwrap();
1533
1534        let wb2 = Workbook::open(&path).unwrap();
1535        assert_eq!(wb2.sheet_names(), vec!["Sheet1", "Stream1", "Stream2"]);
1536        assert_eq!(
1537            wb2.get_cell_value("Sheet1", "A1").unwrap(),
1538            CellValue::String("Normal".to_string())
1539        );
1540        assert_eq!(
1541            wb2.get_cell_value("Stream1", "A1").unwrap(),
1542            CellValue::String("S1R1".to_string())
1543        );
1544        assert_eq!(
1545            wb2.get_cell_value("Stream1", "A2").unwrap(),
1546            CellValue::String("S1R2".to_string())
1547        );
1548        assert_eq!(
1549            wb2.get_cell_value("Stream2", "A1").unwrap(),
1550            CellValue::String("S2R1".to_string())
1551        );
1552    }
1553
1554    #[test]
1555    fn test_stream_delete_sheet() {
1556        let dir = TempDir::new().unwrap();
1557        let path = dir.path().join("stream_delete.xlsx");
1558
1559        let mut wb = Workbook::new();
1560        let mut sw = wb.new_stream_writer("ToDelete").unwrap();
1561        sw.write_row(1, &[CellValue::from("Gone")]).unwrap();
1562        wb.apply_stream_writer(sw).unwrap();
1563
1564        let mut sw2 = wb.new_stream_writer("Kept").unwrap();
1565        sw2.write_row(1, &[CellValue::from("Stays")]).unwrap();
1566        wb.apply_stream_writer(sw2).unwrap();
1567
1568        wb.delete_sheet("ToDelete").unwrap();
1569        wb.save(&path).unwrap();
1570
1571        let wb2 = Workbook::open(&path).unwrap();
1572        assert_eq!(wb2.sheet_names(), vec!["Sheet1", "Kept"]);
1573        assert_eq!(
1574            wb2.get_cell_value("Kept", "A1").unwrap(),
1575            CellValue::String("Stays".to_string())
1576        );
1577    }
1578
1579    #[test]
1580    fn test_stream_combined_features_roundtrip() {
1581        let dir = TempDir::new().unwrap();
1582        let path = dir.path().join("stream_combined.xlsx");
1583
1584        let mut wb = Workbook::new();
1585        let mut sw = wb.new_stream_writer("Combined").unwrap();
1586        sw.set_freeze_panes("A2").unwrap();
1587        sw.set_col_width(1, 30.0).unwrap();
1588        sw.set_col_width_range(2, 3, 15.0).unwrap();
1589        sw.add_merge_cell("B1:C1").unwrap();
1590        sw.write_row(
1591            1,
1592            &[
1593                CellValue::from("Name"),
1594                CellValue::from("Merged Header"),
1595                CellValue::Empty,
1596            ],
1597        )
1598        .unwrap();
1599        sw.write_row(
1600            2,
1601            &[
1602                CellValue::from("Alice"),
1603                CellValue::from(100),
1604                CellValue::from(true),
1605            ],
1606        )
1607        .unwrap();
1608        wb.apply_stream_writer(sw).unwrap();
1609        wb.save(&path).unwrap();
1610
1611        let wb2 = Workbook::open(&path).unwrap();
1612        assert_eq!(wb2.get_panes("Combined").unwrap(), Some("A2".to_string()));
1613        let merges = wb2.get_merge_cells("Combined").unwrap();
1614        assert!(merges.contains(&"B1:C1".to_string()));
1615        let w1 = wb2.get_col_width("Combined", "A").unwrap().unwrap();
1616        assert!((w1 - 30.0).abs() < 0.01);
1617        assert_eq!(
1618            wb2.get_cell_value("Combined", "A1").unwrap(),
1619            CellValue::String("Name".to_string())
1620        );
1621        assert_eq!(
1622            wb2.get_cell_value("Combined", "B2").unwrap(),
1623            CellValue::Number(100.0)
1624        );
1625        assert_eq!(
1626            wb2.get_cell_value("Combined", "C2").unwrap(),
1627            CellValue::Bool(true)
1628        );
1629    }
1630
1631    // --- Regression tests for P1 bugs ---
1632
1633    #[test]
1634    fn test_stream_formula_result_types_roundtrip() {
1635        // Regression: formula cached results must preserve their type via the
1636        // cell t attribute (t="str", t="b", t="e"). Without it, string results
1637        // are dropped and bool results are decoded as Number(1.0).
1638        let dir = TempDir::new().unwrap();
1639        let path = dir.path().join("stream_formula_types.xlsx");
1640
1641        let mut wb = Workbook::new();
1642        let mut sw = wb.new_stream_writer("Formulas").unwrap();
1643        sw.write_row(
1644            1,
1645            &[
1646                CellValue::Formula {
1647                    expr: "A2&B2".to_string(),
1648                    result: Some(Box::new(CellValue::String("hello".to_string()))),
1649                },
1650                CellValue::Formula {
1651                    expr: "A2>0".to_string(),
1652                    result: Some(Box::new(CellValue::Bool(true))),
1653                },
1654                CellValue::Formula {
1655                    expr: "1/0".to_string(),
1656                    result: Some(Box::new(CellValue::Error("#DIV/0!".to_string()))),
1657                },
1658                CellValue::Formula {
1659                    expr: "SUM(A2:A10)".to_string(),
1660                    result: Some(Box::new(CellValue::Number(55.0))),
1661                },
1662            ],
1663        )
1664        .unwrap();
1665        wb.apply_stream_writer(sw).unwrap();
1666        wb.save(&path).unwrap();
1667
1668        let wb2 = Workbook::open(&path).unwrap();
1669        // String result
1670        assert_eq!(
1671            wb2.get_cell_value("Formulas", "A1").unwrap(),
1672            CellValue::Formula {
1673                expr: "A2&B2".to_string(),
1674                result: Some(Box::new(CellValue::String("hello".to_string()))),
1675            }
1676        );
1677        // Bool result
1678        assert_eq!(
1679            wb2.get_cell_value("Formulas", "B1").unwrap(),
1680            CellValue::Formula {
1681                expr: "A2>0".to_string(),
1682                result: Some(Box::new(CellValue::Bool(true))),
1683            }
1684        );
1685        // Error result
1686        assert_eq!(
1687            wb2.get_cell_value("Formulas", "C1").unwrap(),
1688            CellValue::Formula {
1689                expr: "1/0".to_string(),
1690                result: Some(Box::new(CellValue::Error("#DIV/0!".to_string()))),
1691            }
1692        );
1693        // Numeric result
1694        assert_eq!(
1695            wb2.get_cell_value("Formulas", "D1").unwrap(),
1696            CellValue::Formula {
1697                expr: "SUM(A2:A10)".to_string(),
1698                result: Some(Box::new(CellValue::Number(55.0))),
1699            }
1700        );
1701    }
1702
1703    #[test]
1704    fn test_stream_edit_after_apply_takes_effect() {
1705        // Regression: edits via set_cell_value after apply_stream_writer must
1706        // not be silently ignored. The edit invalidates the streamed data so
1707        // the normal WorksheetXml serialization path is used on save.
1708        let dir = TempDir::new().unwrap();
1709        let path = dir.path().join("stream_edit_after.xlsx");
1710
1711        let mut wb = Workbook::new();
1712        let mut sw = wb.new_stream_writer("S").unwrap();
1713        sw.write_row(1, &[CellValue::from("old")]).unwrap();
1714        wb.apply_stream_writer(sw).unwrap();
1715
1716        // Edit the streamed sheet: this should invalidate streamed data.
1717        wb.set_cell_value("S", "A1", "new").unwrap();
1718        wb.save(&path).unwrap();
1719
1720        let wb2 = Workbook::open(&path).unwrap();
1721        assert_eq!(
1722            wb2.get_cell_value("S", "A1").unwrap(),
1723            CellValue::String("new".to_string())
1724        );
1725    }
1726
1727    #[test]
1728    fn test_stream_copy_sheet_preserves_data() {
1729        // Regression: copy_sheet must clone the streamed payload so both
1730        // source and target sheets have the streamed data on save.
1731        let dir = TempDir::new().unwrap();
1732        let path = dir.path().join("stream_copy.xlsx");
1733
1734        let mut wb = Workbook::new();
1735        let mut sw = wb.new_stream_writer("Src").unwrap();
1736        sw.write_row(1, &[CellValue::from("x")]).unwrap();
1737        sw.write_row(2, &[CellValue::from("y")]).unwrap();
1738        wb.apply_stream_writer(sw).unwrap();
1739
1740        wb.copy_sheet("Src", "Dst").unwrap();
1741        wb.save(&path).unwrap();
1742
1743        let wb2 = Workbook::open(&path).unwrap();
1744        assert_eq!(
1745            wb2.get_cell_value("Src", "A1").unwrap(),
1746            CellValue::String("x".to_string())
1747        );
1748        assert_eq!(
1749            wb2.get_cell_value("Src", "A2").unwrap(),
1750            CellValue::String("y".to_string())
1751        );
1752        assert_eq!(
1753            wb2.get_cell_value("Dst", "A1").unwrap(),
1754            CellValue::String("x".to_string())
1755        );
1756        assert_eq!(
1757            wb2.get_cell_value("Dst", "A2").unwrap(),
1758            CellValue::String("y".to_string())
1759        );
1760    }
1761}