From 684567cc9aa2076be2e624518ccd212bb49d7bfd Mon Sep 17 00:00:00 2001 From: nihilist-narwhal <240798405+nihilist-narwhal@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:34:26 -0500 Subject: [PATCH 1/3] Whole-sheet selection: Fill and Remove Duplicates stay within the data With the whole sheet selected (Ctrl+A on an empty sheet), Fill Down, Right, Up and Left tried to fill all 17 billion cells and ran out of memory, and Remove Duplicates scanned every row for minutes. Fill Up and Fill Left also took their source row or column before narrowing the selection to the data, so they filled up to row 1,048,576 even when the sheet had data. Whole rows and columns now narrow to the used range before anything else (nothing to do on an empty sheet), Remove Duplicates works on the active cell of an empty sheet, and a fill of more than 5,000,000 cells is refused, as Auto Fill already does. --- crates/engine/src/cmd/data.rs | 3 ++- crates/engine/src/cmd/edit.rs | 12 ++++++++++-- crates/engine/src/tests.rs | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/data.rs b/crates/engine/src/cmd/data.rs index 8961d8769..7f8467e8a 100644 --- a/crates/engine/src/cmd/data.rs +++ b/crates/engine/src/cmd/data.rs @@ -112,7 +112,8 @@ fn data_range(s: &Session, p: &Json) -> Result { } let sel = d.selection.current(); if !sel.is_single() { - let used = sh.used_range().unwrap_or(sel); + // An empty sheet has no data to work on: just the active cell. + let Some(used) = sh.used_range() else { return Ok(RangeRef::cell(d.selection.active)) }; return Ok(sel.intersection(&used).unwrap_or(sel)); } Ok(current_region(sh, d.selection.active)) diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index cffe15430..4c9f2d6ab 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -1034,6 +1034,16 @@ fn clear(s: &mut Session, p: &Json, what: &str) -> Result { fn fill_dir(s: &mut Session, p: &Json, dr: i64, dc: i64) -> Result { let sheet = target_sheet(s, p)?; let mut r = target_range(s, p)?; + // Whole rows or columns fill only where the sheet has content (nothing on an empty sheet). + if r.is_full_cols() || r.is_full_rows() { + match s.doc()?.wb.sheet(sheet).and_then(|sh| sh.used_range()).and_then(|u| r.intersection(&u)) { + Some(u) => r = u, + None => return Ok(Json::Null), + } + } + if r.count() > 5_000_000 { + return Err(bad("edit.fill", "range too large")); + } // With one row selected, Fill Down copies from the row above (Excel behaviour). if dr == 1 && r.height() == 1 { r.start.row = r.start.row.saturating_sub(1); @@ -1050,8 +1060,6 @@ fn fill_dir(s: &mut Session, p: &Json, dr: i64, dc: i64) -> Result { } else { RangeRef::new(CellRef::new(r.start.row, r.end.col), r.end) }; - let used = s.doc()?.wb.sheet(sheet).and_then(|sh| sh.used_range()); - let r = if r.is_full_cols() || r.is_full_rows() { used.and_then(|u| r.intersection(&u)).unwrap_or(r) } else { r }; edit(s, |cx| { crate::fill::fill(cx, sheet, src, r, crate::fill::FillMode::Copy)?; Ok(Json::Null) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index f3e36cdf4..9d0c7189f 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -167,6 +167,22 @@ fn every_command_survives_empty_params() { } } +#[test] +fn whole_sheet_selection_stays_within_the_data() { + // On an empty sheet with everything selected these filled or scanned every cell of the + // sheet, running out of memory or time. + let mut s = s(); + s.execute("edit.selectAll", json!({})).unwrap(); + for cmd in ["edit.fillDown", "edit.fillRight", "edit.fillUp", "edit.fillLeft", "data.removeDuplicates"] { + s.execute(cmd, json!({})).unwrap(); + } + s.execute("range.setValues", json!({"range": "A1", "values": [[1, 2], [3, 4], [5, 6]]})).unwrap(); + s.execute("selection.set", json!({"range": "A1:XFD1048576"})).unwrap(); + s.execute("edit.fillUp", json!({})).unwrap(); + assert_eq!(v(&s, "A1"), Value::Number(5.0)); + assert!(s.execute("edit.fillDown", json!({"range": "A1:Z1000000"})).is_err(), "too large to fill"); +} + #[test] fn parity_counts() { let (done, total) = crate::catalog::parity(); From 7fece30d49ed166735b05302334c779fb155fa7b Mon Sep 17 00:00:00 2001 From: nihilist-narwhal <240798405+nihilist-narwhal@users.noreply.github.com> Date: Sat, 10 Oct 2026 02:49:31 -0500 Subject: [PATCH 2/3] Remove Duplicates: compare only the part of the range with data Given whole columns ("range":"D:D"), Remove Duplicates compared a million blank rows and reported 1,048,573 removed duplicates; given the whole sheet it ran for minutes, as it also did on a table resized to the whole sheet. Rows and columns past the used range are blank, so they are left out: the result is the same and the counts match the data. --- crates/engine/src/cmd/data.rs | 3 +++ crates/engine/src/tests.rs | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/crates/engine/src/cmd/data.rs b/crates/engine/src/cmd/data.rs index 7f8467e8a..38bece3bc 100644 --- a/crates/engine/src/cmd/data.rs +++ b/crates/engine/src/cmd/data.rs @@ -580,6 +580,9 @@ fn remove_duplicates(s: &mut Session, p: &Json) -> Result { let r = data_range(s, p)?; let d = s.doc()?; let sh = d.wb.active().ok_or(EngineError::NoDocument)?; + // Rows and columns past the data are blank: only the part with data is compared (whole + // columns compared a million blank rows and reported them as removed duplicates). + let r = sh.used_range().and_then(|u| r.intersection(&u)).unwrap_or(RangeRef::cell(r.start)); let header = bool_param(p, "header").unwrap_or_else(|| guess_header(&d.wb, sh, r)); let cols: Vec = p .get("columns") diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 9d0c7189f..e673485fb 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -181,6 +181,10 @@ fn whole_sheet_selection_stays_within_the_data() { s.execute("edit.fillUp", json!({})).unwrap(); assert_eq!(v(&s, "A1"), Value::Number(5.0)); assert!(s.execute("edit.fillDown", json!({"range": "A1:Z1000000"})).is_err(), "too large to fill"); + s.execute("range.setValues", json!({"range": "D1", "values": [[1], [1], [2]]})).unwrap(); + let r = s.execute("data.removeDuplicates", json!({"range": "D:D", "header": false})).unwrap(); + assert_eq!((r["removed"].clone(), r["remaining"].clone()), (json!(1), json!(2))); + assert!(s.execute("data.removeDuplicates", json!({"range": "A1:XFD1048576", "header": false})).is_ok()); } #[test] From 60b60619b25737a290d840789fd476cd57104f8b Mon Sep 17 00:00:00 2001 From: nihilist-narwhal <240798405+nihilist-narwhal@users.noreply.github.com> Date: Sat, 10 Oct 2026 03:46:15 -0500 Subject: [PATCH 3/3] AutoFill: refuse a source as large as the sheet The target was capped at 5 million cells but the source wasn't: with the whole sheet as source and A1 as target, AutoFill read all 17 billion source cells one column at a time and never finished. --- crates/engine/src/cmd/edit.rs | 4 ++-- crates/engine/src/tests.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 4c9f2d6ab..b88470a37 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -1076,8 +1076,8 @@ fn auto_fill(s: &mut Session, p: &Json) -> Result { "values" => crate::fill::FillMode::ValuesOnly, _ => crate::fill::FillMode::Series, }; - if target.count() > 5_000_000 { - return Err(bad("edit.autoFill", "target too large")); + if target.count() > 5_000_000 || src.count() > 5_000_000 { + return Err(bad("edit.autoFill", "range too large")); } edit(s, |cx| { crate::fill::fill(cx, sheet, src, target, mode)?; diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index e673485fb..c68211437 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -181,6 +181,7 @@ fn whole_sheet_selection_stays_within_the_data() { s.execute("edit.fillUp", json!({})).unwrap(); assert_eq!(v(&s, "A1"), Value::Number(5.0)); assert!(s.execute("edit.fillDown", json!({"range": "A1:Z1000000"})).is_err(), "too large to fill"); + assert!(s.execute("edit.autoFill", json!({"source": "A1:XFD1048576", "target": "A1"})).is_err(), "too large to fill"); s.execute("range.setValues", json!({"range": "D1", "values": [[1], [1], [2]]})).unwrap(); let r = s.execute("data.removeDuplicates", json!({"range": "D:D", "header": false})).unwrap(); assert_eq!((r["removed"].clone(), r["remaining"].clone()), (json!(1), json!(2)));