diff --git a/crates/engine/src/cmd/pivot.rs b/crates/engine/src/cmd/pivot.rs index 0d73b0a..d44f2e0 100644 --- a/crates/engine/src/cmd/pivot.rs +++ b/crates/engine/src/cmd/pivot.rs @@ -2039,4 +2039,38 @@ mod tests { assert!(e.contains("field name is not valid"), "{e}"); assert!(s.execute("insert.pivotTable", json!({"source": "NoSuchTable"})).is_err()); } + + #[test] + fn too_many_value_columns_are_refused() { + let mut s = Session::new(); + s.new_workbook(); + let mut records: Vec = vec![json!(["Cat", "Sub", "V"])]; + for r in 0..64 { + for c in 0..64 { + records.push(json!([format!("c{r}"), format!("s{c}"), 1.0])); + } + } + s.execute("range.setValues", json!({"range": "A1", "values": records})).unwrap(); + let req = |n: usize| { + let values: Vec = (0..n).map(|_| json!({"field": "V"})).collect(); + json!({"source": "A1:C4097", "rows": ["Cat"], "columns": ["Sub"], "values": values}) + }; + let e = s.execute("insert.pivotTable", req(256)).unwrap_err().to_string(); + assert!(e.contains("will not fit"), "{e}"); + assert!(s.execute("insert.pivotTable", req(128)).is_ok()); + } + + #[test] + fn collapsed_column_items_count_as_one_column_group() { + let mut s = Session::new(); + s.new_workbook(); + let mut records: Vec = vec![json!(["Grp", "Sub", "V"])]; + records.extend((0..64).map(|c| json!(["g", format!("s{c}"), 1.0]))); + s.execute("range.setValues", json!({"range": "A1", "values": records})).unwrap(); + let values: Vec = (0..256).map(|_| json!({"field": "V"})).collect(); + s.execute("insert.pivotTable", json!({"source": "A1:C65", "columns": ["Grp"], "values": values})).unwrap(); + s.execute("pivot.collapse", json!({"field": "Grp"})).unwrap(); + // 64 Sub items under the collapsed "g" show as one column group: (1 + grand total) x 256 columns fit. + s.execute("pivot.addField", json!({"field": "Sub", "area": "columns"})).unwrap(); + } } diff --git a/crates/engine/src/pivot.rs b/crates/engine/src/pivot.rs index c76bf4b..a53ee40 100644 --- a/crates/engine/src/pivot.rs +++ b/crates/engine/src/pivot.rs @@ -693,6 +693,12 @@ enum ValSrc { Calc(usize), } +/// True when `col_groups` distinct column leaves (plus the grand-total column) times `values` value fields +/// (at least one data column per group) already exceed the sheet width. This is a lower bound on the final column count. +fn data_cols_exceed_sheet(col_groups: usize, grand_total: bool, values: usize) -> bool { + (col_groups + usize::from(grand_total)).saturating_mul(values.max(1)) > MAX_COLS as usize +} + /// Computes a PivotTable report from the workbook. pub fn compute(wb: &Workbook, pt: &PivotTable, pivot_sheet: usize) -> Result { let src = read_source(wb, &pt.source, pivot_sheet)?; @@ -752,6 +758,7 @@ pub fn compute(wb: &Workbook, pt: &PivotTable, pivot_sheet: usize) -> Result> = HashSet::new(); let mut col_set: HashSet> = HashSet::new(); + let mut col_leaves: HashSet> = HashSet::new(); let filter_fmts: Vec = filters.iter().map(|f| fmt_of(f.0)).collect(); let row_fmts: Vec = rows.iter().map(|(c, _)| fmt_of(*c)).collect(); let col_fmts: Vec = cols.iter().map(|(c, _)| fmt_of(*c)).collect(); @@ -788,8 +795,22 @@ pub fn compute(wb: &Workbook, pt: &PivotTable, pivot_sheet: usize) -> Result 5_000_000 { return Err("The PivotTable report is too large.".into()); } @@ -1428,3 +1449,17 @@ pub fn clear_output(wb: &mut Workbook, sheet: usize, idx: usize) { p.last_range = None; } } + +#[cfg(test)] +mod fit_tests { + use super::data_cols_exceed_sheet; + + #[test] + fn column_budget_counts_value_fields_and_grand_total() { + assert!(!data_cols_exceed_sheet(64, true, 128)); + assert!(data_cols_exceed_sheet(64, true, 256)); + assert!(!data_cols_exceed_sheet(64, false, 256)); + assert!(!data_cols_exceed_sheet(16_384, false, 0)); + assert!(data_cols_exceed_sheet(16_385, false, 0)); + } +}