Skip to content

Add nest() and unnest() for nested tables - #128

Merged
ielbadisy merged 12 commits into
mainfrom
feat/nest-unnest
Sep 23, 2026
Merged

ielbadisy merged 12 commits into
mainfrom
feat/nest-unnest

Conversation

@ielbadisy

@ielbadisy ielbadisy commented Sep 21, 2026 •

Copy link
Copy Markdown
Owner

Adds nest() and unnest() for nested tables, native kernels for both, and a benchmark against tidyr and dplyr.

  • nest(data, by, name = "data") collapses each group into a data frame held in a list-column, in first-appearance order. NA keys form their own group.
  • unnest(data, cols) expands list-columns of data frames or vectors back into rows and drops empty elements.
  • Both run through native kernels; unnest() falls back to the general fill-and-coerce path when the nested frames differ.
  • inst/benchmarks/benchmark-nest.R and a README section compare against tidyr and dplyr at 1e6 rows. basetable is faster in all six scenarios (10, 1e3, 1e5 groups).

Two general performance fixes found while profiling are separate commits in this branch and could be split into their own PR:

  • bt_default_threads() caches detectCores() (about 4 ms per verb call).
  • row_index() no longer calls INTEGER() per row, and single integer or logical keys are grouped with a dense table.

Checks: full test suite passes, R CMD check: Status OK.

Note: NAMESPACE is not roxygen-generated in this repo, so the exports and useDynLib entries were edited by hand.

parallel::detectCores() shells out on Linux and costs about 4 ms, and every verb calls bt_default_threads(). Cache the detected value for the session; the basetable.threads option is still read on every call.
The row-index conversion called INTEGER() once per row, so an indexed subset of 1e6 rows took about 37 ms. Hoisting the pointer brings it to about 5 ms.
Single integer or logical keys fell through to the generic byte-key hash map. Use a value-indexed table when the value range is small and an int hash map otherwise, and hoist the id copy in bt_group_id_. Grouping 1e6 integers drops from about 220 ms to about 12 ms.
nest() gathers each group's columns in one native pass (numeric columns on worker threads). unnest() computes element lengths and stacks the nested frames natively, falling back to the general fill-and-coerce path when the frames differ.
ATTRIB() is no longer part of the R API in R 4.6, which broke the build on
current R. Attribute presence now uses ANY_ATTRIB() (R >= 4.5, falling back
to ATTRIB() on older R), and unnest() compares column attributes through
reusable zero-length carriers filled by Rf_copyMostAttrib().
@ielbadisy
ielbadisy merged commit a2150c3 into main Sep 23, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant