miniextendr_api/dataframe.rs
1//! The unified owned R `data.frame` type and its conversion traits.
2//!
3//! [`DataFrame`] is **the** data-frame type: a single owned wrapper around a built
4//! `data.frame` SEXP that serves every direction —
5//!
6//! - **build** (Rust → R): [`IntoDataFrame::into_dataframe`] / `into_dataframe_par` (`feature = "rayon"`),
7//! - **read** (R → Rust): [`DataFrame::column`] / [`FromDataFrame::from_dataframe`],
8//! - **edit** (post-assembly): [`DataFrame::rename`] / [`DataFrame::drop`] / [`DataFrame::select`] / …
9//!
10//! The trait family mirrors the crate's existing [`IntoR`] /
11//! [`TryFromSexp`] pair, specialised to the data-frame SEXP:
12//!
13//! ```ignore
14//! use miniextendr_api::dataframe::{DataFrame, IntoDataFrame, FromDataFrame};
15//!
16//! // Rust → R
17//! let df: DataFrame = rows.into_dataframe()?; // sequential
18//! let df: DataFrame = rows.into_dataframe_par()?; // parallel (feature = "rayon")
19//!
20//! // R → Rust
21//! let rows: Vec<Row> = Vec::<Row>::from_dataframe(&df)?;
22//! ```
23//!
24//! `DataFrame` implements both `IntoR` and `TryFromSexp`, so it slots into
25//! `#[miniextendr]` function codegen with no special-casing — return it directly or accept
26//! it as an argument.
27//!
28//! # One error contract
29//!
30//! Every conversion failure surfaces as [`DataFrameError`]. The serde column assembler's
31//! internal `RSerdeError` is bridged via `From<RSerdeError>`; the parallel R→Rust reader
32//! reports through `DataFrameError` rather than a bare `String`.
33
34use crate::from_r::{SexpError, TryFromSexp};
35use crate::into_r::IntoR;
36use crate::list::{List, NamedList};
37use crate::typed_list::{TypedList, TypedListError, TypedListSpec, validate_list};
38use crate::{SEXP, SEXPTYPE, SexpExt};
39use std::ffi::CStr;
40
41pub mod group;
42pub use group::{GroupKey, GroupedDataFrame, group_rows};
43
44// region: Error type
45
46/// Error returned by any [`DataFrame`] construction, read, or conversion path.
47///
48/// This is the single data-frame error contract: the row-buffer build path, the serde
49/// columnar path, the parallel R→Rust reader, and validation all surface a `DataFrameError`.
50#[derive(Debug, Clone)]
51pub enum DataFrameError {
52 /// The SEXP is not a VECSXP.
53 NotList(String),
54 /// The object does not inherit from `data.frame`.
55 NotDataFrame,
56 /// The list has no `names` attribute (columns must be named).
57 NoNames,
58 /// Could not extract `nrow` from `row.names` attribute.
59 BadRowNames(String),
60 /// Columns have unequal lengths (when promoting from NamedList).
61 UnequalLengths {
62 /// First column length encountered.
63 expected: usize,
64 /// The column name that differs.
65 column: String,
66 /// The actual length of that column.
67 actual: usize,
68 },
69 /// A row could not be turned into named columns (e.g. unnamed list elements
70 /// in a `IntoList`-derived row). Replaces the old `panic!` on this path.
71 UnnamedColumns,
72 /// [`DataFrame::group_by`] referenced a column name that does not exist.
73 NoSuchColumn(String),
74 /// [`DataFrame::group_by`] on a column type with no sane grouping
75 /// semantics (doubles, list-columns, …).
76 UnsupportedGroupColumn {
77 /// The offending column name.
78 column: String,
79 /// Its SEXPTYPE, rendered for the message.
80 type_of: String,
81 },
82 /// A serde-driven schema/serialize/deserialize failure (the bridged
83 /// `RSerdeError` text) or another conversion failure carried as a message.
84 Conversion(String),
85}
86
87impl std::fmt::Display for DataFrameError {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 match self {
90 DataFrameError::NotList(msg) => write!(f, "not a list: {}", msg),
91 DataFrameError::NotDataFrame => write!(f, "object does not inherit from data.frame"),
92 DataFrameError::NoNames => write!(f, "data.frame has no column names"),
93 DataFrameError::BadRowNames(msg) => {
94 write!(f, "could not extract nrow from row.names: {}", msg)
95 }
96 DataFrameError::UnequalLengths {
97 expected,
98 column,
99 actual,
100 } => write!(
101 f,
102 "column {:?} has length {} (expected {})",
103 column, actual, expected
104 ),
105 DataFrameError::UnnamedColumns => {
106 write!(f, "cannot create data frame from unnamed list elements")
107 }
108 DataFrameError::NoSuchColumn(name) => {
109 write!(f, "no such column: {:?}", name)
110 }
111 DataFrameError::UnsupportedGroupColumn { column, type_of } => write!(
112 f,
113 "cannot group by column {:?} ({}): supported key types are factor, \
114 character, integer, and logical — cut() or factor() the column first",
115 column, type_of
116 ),
117 DataFrameError::Conversion(msg) => write!(f, "{}", msg),
118 }
119 }
120}
121
122impl std::error::Error for DataFrameError {}
123
124#[cfg(feature = "serde")]
125impl From<crate::serde::RSerdeError> for DataFrameError {
126 fn from(e: crate::serde::RSerdeError) -> Self {
127 DataFrameError::Conversion(e.to_string())
128 }
129}
130// endregion
131
132// region: DataFrame — the unified owned data.frame
133
134/// An owned, validated R `data.frame`. **The** data-frame type.
135///
136/// Wraps a built VECSXP carrying the `data.frame` class + `row.names`. A single coherent
137/// type for building (Rust → R), reading (R → Rust), and post-assembly editing — replacing
138/// the historical row-buffer / built-SEXP / read-wrapper trio with one coherent type.
139///
140/// # Building
141///
142/// Prefer the [`IntoDataFrame`] trait on your data:
143///
144/// ```ignore
145/// let df: DataFrame = rows.into_dataframe()?;
146/// ```
147///
148/// or the closure-fill `DataFrame::builder` for heterogeneous parallel column fill
149/// (`feature = "rayon"`).
150///
151/// # Reading
152///
153/// Wrap an incoming SEXP with [`DataFrame::from_sexp`] (or accept `DataFrame` directly as a
154/// `#[miniextendr]` argument), then pull typed columns with [`DataFrame::column`], or
155/// deserialize whole rows with [`FromDataFrame`].
156#[derive(Clone, Copy)]
157pub struct DataFrame {
158 sexp: SEXP,
159}
160
161impl DataFrame {
162 /// Wrap an already-built `data.frame` SEXP without re-validation.
163 ///
164 /// Used by the column assemblers, which produce a well-formed `data.frame` by
165 /// construction.
166 ///
167 /// # Safety
168 ///
169 /// `sexp` must be a VECSXP with the `data.frame` class and consistent `row.names`.
170 #[inline]
171 pub unsafe fn from_built_sexp(sexp: SEXP) -> Self {
172 Self { sexp }
173 }
174
175 /// Wrap an existing R `data.frame` SEXP, validating it.
176 ///
177 /// Validates that the object:
178 /// 1. Is a VECSXP (list)
179 /// 2. Inherits from `"data.frame"`
180 /// 3. Has a `names` attribute
181 /// 4. Has extractable `row.names` for nrow
182 ///
183 /// # Errors
184 ///
185 /// Returns [`DataFrameError`] if validation fails.
186 pub fn from_sexp(sexp: SEXP) -> Result<Self, DataFrameError> {
187 let stype = sexp.type_of();
188 if stype != SEXPTYPE::VECSXP {
189 return Err(DataFrameError::NotList(format!(
190 "expected VECSXP, got {:?}",
191 stype
192 )));
193 }
194 if !sexp.is_data_frame() {
195 return Err(DataFrameError::NotDataFrame);
196 }
197 // Require a names attribute (columns must be named).
198 let list = unsafe { List::from_raw(sexp) };
199 NamedList::new(list).ok_or(DataFrameError::NoNames)?;
200 // Confirm nrow is extractable.
201 extract_nrow(sexp)?;
202 Ok(Self { sexp })
203 }
204
205 // region: Read API (R → Rust column / row access)
206
207 /// Get a column by name, converting each element to type `T`.
208 ///
209 /// Returns `None` if the column name is not found or conversion fails.
210 #[inline]
211 pub fn column<T>(&self, name: &str) -> Option<T>
212 where
213 T: TryFromSexp<Error = SexpError>,
214 {
215 self.named_list().get(name)
216 }
217
218 /// Get a column by 0-based index, converting to type `T`.
219 #[inline]
220 pub fn column_index<T>(&self, idx: usize) -> Option<T>
221 where
222 T: TryFromSexp<Error = SexpError>,
223 {
224 let idx_isize: isize = idx.try_into().ok()?;
225 self.named_list().get_index(idx_isize)
226 }
227
228 /// Get the raw SEXP for a column by name.
229 #[inline]
230 pub fn column_raw(&self, name: &str) -> Option<SEXP> {
231 self.named_list().get_raw(name)
232 }
233
234 /// Number of rows.
235 #[inline]
236 pub fn nrow(&self) -> usize {
237 extract_nrow(self.sexp).unwrap_or(0)
238 }
239
240 /// Number of columns.
241 #[inline]
242 pub fn ncol(&self) -> usize {
243 self.sexp.len()
244 }
245
246 /// Collect column names in column order.
247 pub fn names(&self) -> Vec<String> {
248 let names_sexp = self.sexp.get_names();
249 if names_sexp.is_nil() {
250 return Vec::new();
251 }
252 let n = self.sexp.len() as isize;
253 (0..n)
254 .map(|i| names_sexp.string_elt_str(i).unwrap_or("").to_string())
255 .collect()
256 }
257
258 /// Check whether a column name exists.
259 #[inline]
260 pub fn contains_column(&self, name: &str) -> bool {
261 self.named_list().contains(name)
262 }
263
264 /// Validate the data frame's column types against a [`TypedListSpec`].
265 pub fn validate(&self, spec: &TypedListSpec) -> Result<TypedList, TypedListError> {
266 validate_list(unsafe { List::from_raw(self.sexp) }, spec)
267 }
268 // endregion
269
270 // region: Conversions
271
272 /// Get the underlying [`List`].
273 #[inline]
274 pub fn as_list(&self) -> List {
275 unsafe { List::from_raw(self.sexp) }
276 }
277
278 /// Get the underlying SEXP.
279 #[inline]
280 pub fn as_sexp(&self) -> SEXP {
281 self.sexp
282 }
283
284 /// Build the `NamedList` index for O(1) column-by-name access.
285 #[inline]
286 fn named_list(&self) -> NamedList {
287 NamedList::new(unsafe { List::from_raw(self.sexp) })
288 .expect("DataFrame always carries a names attribute")
289 }
290 // endregion
291
292 // region: Post-assembly editing (absorbed from the old serde columnar assembler)
293
294 /// Rename a column. No-op if `from` doesn't match any column name.
295 pub fn rename(self, from: &str, to: &str) -> Self {
296 unsafe {
297 // Root `self.sexp` so its `names` attribute survives the
298 // `SEXP::charsxp` (Rf_mkCharLenCE) allocation below, which can GC.
299 let _guard = crate::OwnedProtect::new(self.sexp);
300 let names_sexp = self.sexp.get_names();
301 if names_sexp == SEXP::nil() {
302 return self;
303 }
304 let ncol = names_sexp.xlength();
305 for i in 0..ncol {
306 if col_name(names_sexp, i) == from {
307 names_sexp.set_string_elt(i, SEXP::charsxp(to));
308 break;
309 }
310 }
311 }
312 self
313 }
314
315 /// Strip a prefix from all column names that start with it.
316 pub fn strip_prefix(self, prefix: &str) -> Self {
317 unsafe {
318 // Root `self.sexp` so its `names` attribute survives the
319 // `SEXP::charsxp` (Rf_mkCharLenCE) allocation below, which can GC.
320 let _guard = crate::OwnedProtect::new(self.sexp);
321 let names_sexp = self.sexp.get_names();
322 if names_sexp == SEXP::nil() {
323 return self;
324 }
325 let ncol = names_sexp.xlength();
326 for i in 0..ncol {
327 let name = col_name(names_sexp, i);
328 if let Some(stripped) = name.strip_prefix(prefix) {
329 names_sexp.set_string_elt(i, SEXP::charsxp(stripped));
330 }
331 }
332 }
333 self
334 }
335
336 /// Remove a column by name. No-op if the column doesn't exist.
337 pub fn drop(self, col: &str) -> Self {
338 unsafe {
339 let names_sexp = self.sexp.get_names();
340 if names_sexp == SEXP::nil() {
341 return self;
342 }
343 let ncol = names_sexp.xlength();
344 let drop_idx = (0..ncol).find(|&i| col_name(names_sexp, i) == col);
345 let Some(drop_idx) = drop_idx else {
346 return self;
347 };
348
349 let new_ncol = ncol - 1;
350 let new_list = crate::OwnedProtect::new(SEXP::alloc_list(new_ncol));
351 let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(new_ncol));
352
353 let mut j: isize = 0;
354 for i in 0..ncol {
355 if i == drop_idx {
356 continue;
357 }
358 new_list.set_vector_elt(j, self.sexp.vector_elt(i));
359 new_names.set_string_elt(j, names_sexp.string_elt(i));
360 j += 1;
361 }
362
363 new_list.set_names(*new_names);
364 copy_df_attrs(self.sexp, *new_list);
365
366 DataFrame { sexp: *new_list }
367 }
368 }
369
370 /// Keep only the named columns, in the order given. Unknown names are skipped.
371 pub fn select(self, cols: &[&str]) -> Self {
372 unsafe {
373 let names_sexp = self.sexp.get_names();
374 if names_sexp == SEXP::nil() {
375 return self;
376 }
377 let ncol = names_sexp.xlength();
378
379 let indices: Vec<isize> = cols
380 .iter()
381 .filter_map(|&want| (0..ncol).find(|&i| col_name(names_sexp, i) == want))
382 .collect();
383
384 let new_ncol: isize = indices.len().try_into().expect("ncol overflow");
385 let new_list = crate::OwnedProtect::new(SEXP::alloc_list(new_ncol));
386 let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(new_ncol));
387
388 for (j, &src_idx) in indices.iter().enumerate() {
389 let j_r: isize = j.try_into().expect("index overflow");
390 new_list.set_vector_elt(j_r, self.sexp.vector_elt(src_idx));
391 new_names.set_string_elt(j_r, names_sexp.string_elt(src_idx));
392 }
393
394 new_list.set_names(*new_names);
395 copy_df_attrs(self.sexp, *new_list);
396
397 DataFrame { sexp: *new_list }
398 }
399 }
400
401 /// Keep only the rows at the given 0-based indices, in order.
402 ///
403 /// Subsets every column (each a vector or list-column) to the specified rows
404 /// and rebuilds compact integer `row.names`. Used by the enum reader to
405 /// densify a flattened sub-frame before recursing into the inner type's reader.
406 ///
407 /// # PROTECT discipline
408 ///
409 /// Allocates one new column vector per column — `OwnedProtect`s the output list
410 /// across the loop so previously-built column SEXPs survive subsequent allocations.
411 pub fn select_rows(&self, idx: &[usize]) -> Self {
412 use crate::SexpExt as _;
413
414 unsafe {
415 let names_sexp = self.sexp.get_names();
416 let ncol = self.sexp.xlength();
417 let new_nrow = idx.len();
418
419 let new_list = crate::OwnedProtect::new(SEXP::alloc_list(ncol));
420 let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(ncol));
421
422 for col_j in 0..ncol {
423 let src_col = self.sexp.vector_elt(col_j);
424
425 // Gather the requested rows into a new dense column via the shared
426 // conversion helper (the row-selecting inverse of `scatter_column`).
427 // It returns an unprotected SEXP; we root it into the protected
428 // `new_list` immediately below, before any further allocation.
429 let new_col: SEXP = crate::convert::gather_column(src_col, idx);
430
431 // Root new_col in the protected output list BEFORE touching its
432 // attributes. `gather_column` returns an unprotected SEXP, and
433 // `set_class`/`set_levels` (Rf_setAttrib) allocate and can trigger
434 // GC. set_vector_elt does not allocate, so this ordering keeps
435 // new_col reachable (via new_list) across every allocating call.
436 new_list.set_vector_elt(col_j, new_col);
437 if names_sexp != SEXP::nil() {
438 new_names.set_string_elt(col_j, names_sexp.string_elt(col_j));
439 }
440
441 // Copy column attributes: class (for factor / Date / POSIXct) and
442 // levels (for factor columns). Safe now — new_col is rooted in the
443 // protected new_list, so GC during set_class/set_levels can't reap it.
444 let class_attr = src_col.get_class();
445 if class_attr != SEXP::nil() {
446 new_col.set_class(class_attr);
447 }
448 let levels_attr = src_col.get_levels();
449 if levels_attr != SEXP::nil() {
450 new_col.set_levels(levels_attr);
451 }
452 }
453
454 if names_sexp != SEXP::nil() {
455 new_list.set_names(*new_names);
456 }
457
458 // Set compact integer row.names (c(NA_integer_, -new_nrow)).
459 let (row_names, rn) = crate::into_r::alloc_r_vector::<i32>(2);
460 let _rn_guard = crate::OwnedProtect::new(row_names);
461 rn[0] = i32::MIN;
462 rn[1] = -(new_nrow as i32);
463 new_list.set_row_names(row_names);
464 // Copy the data.frame class attribute.
465 new_list.set_class(self.sexp.get_class());
466
467 DataFrame { sexp: *new_list }
468 }
469 }
470
471 /// Insert a column at index 0 (leftmost), removing any same-named column first.
472 pub fn prepend_column(self, name: &str, column: SEXP) -> Self {
473 let cleaned = self.drop(name);
474 unsafe {
475 let names_sexp = cleaned.sexp.get_names();
476 let ncol = if names_sexp == SEXP::nil() {
477 0
478 } else {
479 names_sexp.xlength()
480 };
481
482 let new_ncol = ncol + 1;
483 let new_list = crate::OwnedProtect::new(SEXP::alloc_list(new_ncol));
484 let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(new_ncol));
485
486 new_list.set_vector_elt(0, column);
487 new_names.set_string_elt(0, SEXP::charsxp(name));
488
489 for i in 0..ncol {
490 new_list.set_vector_elt(i + 1, cleaned.sexp.vector_elt(i));
491 new_names.set_string_elt(i + 1, names_sexp.string_elt(i));
492 }
493
494 new_list.set_names(*new_names);
495 copy_df_attrs(cleaned.sexp, *new_list);
496
497 DataFrame { sexp: *new_list }
498 }
499 }
500
501 /// Upsert a column: replace the column named `name` if it exists, else append.
502 pub fn with_column(self, name: &str, column: SEXP) -> Self {
503 unsafe {
504 let names_sexp = self.sexp.get_names();
505 if names_sexp == SEXP::nil() {
506 return self;
507 }
508 let ncol = names_sexp.xlength();
509 for i in 0..ncol {
510 if col_name(names_sexp, i) == name {
511 self.sexp.set_vector_elt(i, column);
512 return self;
513 }
514 }
515
516 let new_ncol = ncol + 1;
517 let new_list = crate::OwnedProtect::new(SEXP::alloc_list(new_ncol));
518 let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(new_ncol));
519
520 for i in 0..ncol {
521 new_list.set_vector_elt(i, self.sexp.vector_elt(i));
522 new_names.set_string_elt(i, names_sexp.string_elt(i));
523 }
524 new_list.set_vector_elt(ncol, column);
525 new_names.set_string_elt(ncol, SEXP::charsxp(name));
526
527 new_list.set_names(*new_names);
528 copy_df_attrs(self.sexp, *new_list);
529
530 DataFrame { sexp: *new_list }
531 }
532 }
533 // endregion
534
535 // region: builder (ex-RDataFrameBuilder, #768)
536
537 /// Start a closure-per-column builder yielding a [`DataFrame`].
538 ///
539 /// The heterogeneous-column analogue of `with_r_matrix`: each column buffer is R memory
540 /// filled by a per-column closure. Available regardless of the `rayon` feature (#1055);
541 /// the columns are filled **in parallel** when `rayon` is enabled and **serially**
542 /// otherwise — the resulting `data.frame` is identical either way.
543 ///
544 /// ```ignore
545 /// let df = DataFrame::builder(1000)
546 /// .column::<f64>("x", |chunk, off| for (i, v) in chunk.iter_mut().enumerate() { *v = (off + i) as f64 })
547 /// .column_str("label", |i| Some(format!("row{i}")))
548 /// .build();
549 /// ```
550 #[inline]
551 pub fn builder(nrow: usize) -> crate::dataframe_builder::RDataFrameBuilder {
552 crate::dataframe_builder::RDataFrameBuilder::new(nrow)
553 }
554 // endregion
555}
556// endregion
557
558// region: column-order name helper + attr copy (absorbed from columnar)
559
560/// Read the i-th column name from a STRSXP names vector.
561///
562/// # Safety
563/// `names_sexp` must be a valid STRSXP with at least `i + 1` elements.
564unsafe fn col_name(names_sexp: SEXP, i: isize) -> &'static str {
565 unsafe {
566 let s = names_sexp.string_elt(i);
567 let p = s.r_char();
568 std::ffi::CStr::from_ptr(p).to_str().unwrap_or("")
569 }
570}
571
572/// Copy class and row.names attributes from one data.frame SEXP to another.
573///
574/// # Safety
575/// Both SEXPs must be valid VECSXPs.
576unsafe fn copy_df_attrs(from: SEXP, to: SEXP) {
577 to.set_class(from.get_class());
578 to.set_row_names(from.get_row_names());
579}
580// endregion
581
582// region: IntoR / TryFromSexp for DataFrame
583
584impl TryFromSexp for DataFrame {
585 type Error = SexpError;
586
587 fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
588 DataFrame::from_sexp(sexp).map_err(|e| SexpError::InvalidValue(e.to_string()))
589 }
590}
591
592impl IntoR for DataFrame {
593 type Error = std::convert::Infallible;
594 fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
595 Ok(self.sexp)
596 }
597 unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
598 Ok(self.sexp)
599 }
600 #[inline]
601 fn into_sexp(self) -> SEXP {
602 self.sexp
603 }
604}
605// endregion
606
607// region: The conversion trait family (mirrors IntoR / TryFromSexp)
608
609/// Rust data → R `data.frame`. The data-frame analogue of [`IntoR`].
610///
611/// Implemented by `#[derive(DataFrameRow)]` on a row struct/enum (for `Vec<Row>`), by the
612/// blanket impl for any [`ColumnSource`] (`IntoList`-derived rows), and by the serde column
613/// path. Call it on your data: `rows.into_dataframe()?`.
614///
615/// # Parallel fast path
616///
617/// `into_dataframe_par` (present only with
618/// `feature = "rayon"`) produces the **same** [`DataFrame`] as
619/// [`into_dataframe`](IntoDataFrame::into_dataframe). It defaults to the sequential path, so
620/// every implementor gets a correct `_par` for free; `#[derive(DataFrameRow)]` row types
621/// override it with a genuinely parallel column fill (the #777 flattened `(column,row-range)`
622/// work-list). The verb is stable across feature sets — dropping `_par` degrades cleanly to
623/// the sequential call.
624pub trait IntoDataFrame: Sized {
625 /// Convert this value into a [`DataFrame`].
626 fn into_dataframe(self) -> Result<DataFrame, DataFrameError>;
627
628 /// Parallel column fill (`feature = "rayon"`). Same result as `into_dataframe()`.
629 ///
630 /// Defaults to the sequential path; overridden by the derive for a real parallel fill.
631 #[cfg(feature = "rayon")]
632 fn into_dataframe_par(self) -> Result<DataFrame, DataFrameError> {
633 self.into_dataframe()
634 }
635}
636
637/// R `data.frame` → Rust data. The data-frame analogue of
638/// [`TryFromSexp`].
639///
640/// Implemented by `#[derive(DataFrameRow)]` for `Vec<Row>` and by the serde row path.
641///
642/// # Parallel fast path
643///
644/// `from_dataframe_par` (`feature = "rayon"`) reads the
645/// same rows as [`from_dataframe`](FromDataFrame::from_dataframe), defaulting to the
646/// sequential reader; the derive overrides it with the #765 off-main-thread row assembly.
647pub trait FromDataFrame: Sized {
648 /// Read rows back out of a [`DataFrame`].
649 fn from_dataframe(df: &DataFrame) -> Result<Self, DataFrameError>;
650
651 /// Parallel row read (`feature = "rayon"`). Same result as `from_dataframe()`.
652 #[cfg(feature = "rayon")]
653 fn from_dataframe_par(df: &DataFrame) -> Result<Self, DataFrameError> {
654 Self::from_dataframe(df)
655 }
656}
657// endregion
658
659// region: ColumnSource — internal column-assembly engine (ex-public convert::IntoDataFrame)
660
661/// Internal engine that turns a value into a `data.frame`-shaped [`List`].
662///
663/// This was the historical public `convert::IntoDataFrame` (`-> List`). It is now an internal
664/// engine: the public [`IntoDataFrame`] (`-> Result<DataFrame, _>`) and the enum-flatten
665/// codegen both delegate to it. Not part of the public verb surface.
666#[doc(hidden)]
667pub trait ColumnSource {
668 /// Convert into a `data.frame`-shaped [`List`] (named columns, `data.frame` class,
669 /// `row.names`).
670 fn into_column_list(self) -> List;
671
672 /// Extract named column SEXPs from this value.
673 ///
674 /// Returns `(name, raw SEXP)` per column. The SEXPs are owned by the produced
675 /// data-frame SEXP and must be protected by the caller before it is released.
676 ///
677 /// # Safety
678 ///
679 /// Calls R API functions; must run on the R main thread.
680 fn into_named_columns(self) -> Vec<(String, crate::SEXP)>
681 where
682 Self: Sized,
683 {
684 use crate::SexpExt as _;
685 let list = self.into_column_list();
686 let sexp = list.as_sexp();
687 let n = sexp.len();
688 let mut out = Vec::with_capacity(n);
689 let names_sexp = sexp.get_names();
690 let has_names = !names_sexp.is_nil();
691 for i in 0..(n as isize) {
692 let col_sexp = sexp.vector_elt(i);
693 let col_name = if has_names {
694 names_sexp.string_elt_str(i).unwrap_or("").to_string()
695 } else {
696 i.to_string()
697 };
698 out.push((col_name, col_sexp));
699 }
700 out
701 }
702
703 /// Assemble this source into a validated [`DataFrame`].
704 ///
705 /// The column engine always sets the `data.frame` class (even for an empty frame); the one
706 /// exception is the unnamed-row degradation, which returns a bare unclassed empty list — the
707 /// old `panic!("unnamed list elements")` case, now a clean `Err(UnnamedColumns)`.
708 ///
709 /// This is the bridge from the internal column-assembly engine to the public [`DataFrame`].
710 /// We deliberately do **not** offer a blanket `impl<T: ColumnSource> IntoDataFrame for T`:
711 /// `#[derive(DataFrameRow)]` emits a *concrete* `impl IntoDataFrame for Vec<Row>` per row
712 /// type (and serde uses the `SerdeRows<T>` newtype), so a generic `for T` blanket would
713 /// coherence-conflict with every one of those (the compiler treats `Vec<Row>: ColumnSource`
714 /// as possibly-true). The derive's `into_dataframe` glue calls this method instead.
715 fn into_dataframe(self) -> Result<DataFrame, DataFrameError>
716 where
717 Self: Sized,
718 {
719 use crate::SexpExt as _;
720 let sexp = self.into_column_list().as_sexp();
721 if !sexp.is_data_frame() {
722 return Err(DataFrameError::UnnamedColumns);
723 }
724 Ok(unsafe { DataFrame::from_built_sexp(sexp) })
725 }
726}
727// endregion
728
729// region: DataFrameRowConvert — orphan-rule bridge for `Vec<Row>` conversions
730
731/// Row → DataFrame conversion glue emitted by `#[derive(DataFrameRow)]` on the **row type**.
732///
733/// The orphan rule forbids the derive from writing `impl IntoDataFrame for Vec<Row>` in the user
734/// crate: both `IntoDataFrame` and `Vec` are foreign there, and `Row` only appears *covered*
735/// inside `Vec<_>`, so there is no uncovered local type. Instead the derive implements this
736/// `#[doc(hidden)]` trait on the local `Row` type (legal — `Row` is local), and `miniextendr_api`
737/// carries the blanket [`IntoDataFrame`] / [`FromDataFrame`] impls for `Vec<T: DataFrameRowConvert>`
738/// below (legal — `IntoDataFrame` is local *here*). Users still call the public
739/// `rows.into_dataframe()?` / `Vec::<Row>::from_dataframe(&df)?` verbs.
740#[doc(hidden)]
741pub trait DataFrameRowConvert: Sized {
742 /// Build a [`DataFrame`] from a row vector (sequential).
743 fn rows_into_dataframe(rows: Vec<Self>) -> Result<DataFrame, DataFrameError>;
744
745 /// Build a [`DataFrame`] from a row vector (parallel; defaults to sequential).
746 #[cfg(feature = "rayon")]
747 fn rows_into_dataframe_par(rows: Vec<Self>) -> Result<DataFrame, DataFrameError> {
748 Self::rows_into_dataframe(rows)
749 }
750
751 /// Read a row vector out of a [`DataFrame`]. `None` means this row shape has no reader
752 /// (scalar, column-expansion, and struct-flatten struct shapes do; tagged enum shapes with
753 /// reader-capable fields do too; tagless/map-column/coerced/skip/`as_list` enum shapes and
754 /// opaque-map shapes do not); the blanket surfaces that as a clear error.
755 fn rows_from_dataframe(_df: &DataFrame) -> Option<Result<Vec<Self>, DataFrameError>> {
756 None
757 }
758
759 /// Parallel reader (defaults to the sequential reader).
760 #[cfg(feature = "rayon")]
761 fn rows_from_dataframe_par(df: &DataFrame) -> Option<Result<Vec<Self>, DataFrameError>> {
762 Self::rows_from_dataframe(df)
763 }
764}
765
766/// Error returned by `Vec::<Row>::from_dataframe` when the row shape has no R→Rust reader.
767fn no_reader_error() -> DataFrameError {
768 DataFrameError::Conversion(
769 "this DataFrameRow shape has no R→Rust reader (struct shapes with scalar/expansion/\
770 struct-flatten fields do; tagged enum shapes with reader-capable fields do; \
771 tagless/map-column/coerced/skip/as_list enum shapes and opaque-map shapes do not)"
772 .to_string(),
773 )
774}
775
776impl<T: DataFrameRowConvert> IntoDataFrame for Vec<T> {
777 fn into_dataframe(self) -> Result<DataFrame, DataFrameError> {
778 T::rows_into_dataframe(self)
779 }
780
781 #[cfg(feature = "rayon")]
782 fn into_dataframe_par(self) -> Result<DataFrame, DataFrameError> {
783 T::rows_into_dataframe_par(self)
784 }
785}
786
787impl<T: DataFrameRowConvert> FromDataFrame for Vec<T> {
788 fn from_dataframe(df: &DataFrame) -> Result<Self, DataFrameError> {
789 // Root the input across the read. A `.Call` caller gets this from R's
790 // argument frame, but a Rust caller may hand in a freshly-built,
791 // unprotected frame (`into_dataframe` returns an unrooted SEXP wrapper);
792 // reader-internal allocations would reclaim it mid-read under
793 // `gctorture(TRUE)` (caught by gc_stress_reader_nested_flatten).
794 // Mirrors the guard in serde's `dataframe_to_vec` — see
795 // reviews/2026-05-29-serde-deserialize-fixture-gctorture-input-protect.md.
796 // SAFETY: reader entry runs on the R main thread; `df` wraps a valid SEXP.
797 let _input = unsafe { crate::OwnedProtect::new(df.as_sexp()) };
798 T::rows_from_dataframe(df).unwrap_or_else(|| Err(no_reader_error()))
799 }
800
801 #[cfg(feature = "rayon")]
802 fn from_dataframe_par(df: &DataFrame) -> Result<Self, DataFrameError> {
803 // SAFETY: as in `from_dataframe` above.
804 let _input = unsafe { crate::OwnedProtect::new(df.as_sexp()) };
805 T::rows_from_dataframe_par(df).unwrap_or_else(|| Err(no_reader_error()))
806 }
807}
808// endregion
809
810// region: nrow extraction from row.names
811
812/// Extract `nrow` from R's `row.names` attribute.
813fn extract_nrow(sexp: SEXP) -> Result<usize, DataFrameError> {
814 let row_names = sexp.get_row_names();
815
816 if row_names.is_nil() {
817 return nrow_from_first_column(sexp);
818 }
819
820 let rn_type = row_names.type_of();
821 let rn_len = row_names.xlength();
822
823 if rn_type == SEXPTYPE::INTSXP && rn_len == 2 {
824 let rn: &[i32] = unsafe { row_names.as_slice() };
825 if rn[0] == i32::MIN && rn[1] < 0 {
826 return Ok((-rn[1]) as usize);
827 }
828 }
829
830 if let Ok(n) = usize::try_from(rn_len) {
831 Ok(n)
832 } else {
833 Err(DataFrameError::BadRowNames(format!(
834 "row.names has negative length: {}",
835 rn_len
836 )))
837 }
838}
839
840/// Fall back: extract nrow from the length of the first column.
841fn nrow_from_first_column(sexp: SEXP) -> Result<usize, DataFrameError> {
842 let ncol = sexp.xlength();
843 if ncol == 0 {
844 return Ok(0);
845 }
846 let first_col = sexp.vector_elt(0);
847 if first_col == SEXP::nil() {
848 return Ok(0);
849 }
850 let len = first_col.xlength();
851 if let Ok(n) = usize::try_from(len) {
852 Ok(n)
853 } else {
854 Err(DataFrameError::BadRowNames(
855 "first column has negative length".to_string(),
856 ))
857 }
858}
859// endregion
860
861// region: NamedList / List → DataFrame promotion
862
863/// Validate that all columns in a NamedList have equal length, returning the common length.
864fn validate_equal_lengths(named: &NamedList) -> Result<usize, DataFrameError> {
865 let list = named.as_list();
866 let n = list.len();
867
868 if n == 0 {
869 return Ok(0);
870 }
871
872 let first_col = list.as_sexp().vector_elt(0);
873 let expected: usize = first_col.len();
874
875 let names_sexp = list.names();
876 for i in 1..n {
877 let col = list.as_sexp().vector_elt(i);
878 let col_len: usize = col.len();
879 if col_len != expected {
880 let column = if let Some(names) = names_sexp {
881 let name_sexp = names.string_elt(i);
882 if name_sexp != SEXP::na_string() {
883 let name_ptr = name_sexp.r_char();
884 let name_cstr = unsafe { CStr::from_ptr(name_ptr) };
885 name_cstr.to_str().unwrap_or("<invalid>").to_string()
886 } else {
887 format!("column {}", i)
888 }
889 } else {
890 format!("column {}", i)
891 };
892
893 return Err(DataFrameError::UnequalLengths {
894 expected,
895 column,
896 actual: col_len,
897 });
898 }
899 }
900
901 Ok(expected)
902}
903
904impl NamedList {
905 /// Promote this named list to a [`DataFrame`].
906 ///
907 /// Validates equal column lengths, sets the `data.frame` class, and adds compact integer
908 /// `row.names`.
909 ///
910 /// # Errors
911 ///
912 /// Returns [`DataFrameError::UnequalLengths`] if columns differ in length.
913 pub fn as_data_frame(&self) -> Result<DataFrame, DataFrameError> {
914 let nrow = validate_equal_lengths(self)?;
915 self.as_list().set_data_frame_class();
916 self.as_list().set_row_names_int(nrow);
917 Ok(DataFrame {
918 sexp: self.as_list().as_sexp(),
919 })
920 }
921}
922
923impl List {
924 /// Promote this named list to a [`DataFrame`].
925 ///
926 /// # Errors
927 ///
928 /// Returns [`DataFrameError`] if the list has no names or columns differ in length.
929 pub fn as_data_frame(&self) -> Result<DataFrame, DataFrameError> {
930 let named = NamedList::new(*self).ok_or(DataFrameError::NoNames)?;
931 named.as_data_frame()
932 }
933}
934// endregion
935
936// region: NamedDataFrameListBuilder (moved from serde::columnar — no serde dependency)
937
938/// Assemble a named list whose elements are [`DataFrame`]s,
939/// without per-result `OwnedProtect` bookkeeping.
940///
941/// # Why this is distinct from [`DataFrame::builder`]
942///
943/// [`DataFrame::builder`](crate::dataframe::DataFrame::builder) and the serde
944/// `SerdeRowBuilder` both produce a *single* [`DataFrame`]. This builder
945/// produces a different shape — a named *list of* data.frames, e.g.
946/// `list(results = df, error = df)` — so it deliberately keeps its own name
947/// rather than folding into the `DataFrame::builder` vocabulary. Its inputs
948/// are [`DataFrame`]s (from any producer: [`IntoDataFrame`], the serde
949/// `vec_to_dataframe`, or [`GroupedDataFrame::frames`]); its output is a
950/// [`List`].
951///
952/// Each [`push`](NamedDataFrameListBuilder::push) protects the input
953/// data.frame's SEXP via an internal [`ProtectScope`](crate::ProtectScope);
954/// [`build`](NamedDataFrameListBuilder::build) consumes the builder and emits
955/// a named list via [`List::from_raw_pairs`](crate::list::List::from_raw_pairs).
956/// The scope drops at the end of `build`, releasing the per-input protects —
957/// by which point the children are reachable from the assembled list.
958///
959/// # Example
960///
961/// ```ignore
962/// let result = NamedDataFrameListBuilder::new()
963/// .push("results", oks.into_dataframe()?)
964/// .push("error", errs.into_dataframe()?)
965/// .build();
966/// ```
967pub struct NamedDataFrameListBuilder {
968 scope: crate::ProtectScope,
969 pairs: Vec<(String, SEXP)>,
970}
971
972impl NamedDataFrameListBuilder {
973 /// Create an empty builder.
974 ///
975 /// # Safety (caller)
976 ///
977 /// Must be called from the R main thread. The internal
978 /// [`ProtectScope`](crate::ProtectScope) carries `!Send + !Sync`
979 /// so the builder cannot be moved to another thread.
980 pub fn new() -> Self {
981 Self {
982 // SAFETY: ProtectScope requires the R main thread. The builder is
983 // constructible only on the R main thread; ProtectScope carries
984 // NoSendSync so it cannot be moved off-thread.
985 scope: unsafe { crate::ProtectScope::new() },
986 pairs: Vec::new(),
987 }
988 }
989
990 /// Create a builder pre-allocated for `n` entries.
991 ///
992 /// Equivalent to [`new`](Self::new) but avoids repeated re-allocations
993 /// when the number of partitions is known up front.
994 pub fn with_capacity(n: usize) -> Self {
995 Self {
996 scope: unsafe { crate::ProtectScope::new() },
997 pairs: Vec::with_capacity(n),
998 }
999 }
1000
1001 /// Append a named data.frame. The input's SEXP is protected
1002 /// internally for the lifetime of the builder.
1003 #[must_use]
1004 pub fn push<S: Into<String>>(mut self, name: S, df: DataFrame) -> Self {
1005 use crate::IntoR as _;
1006 let sexp = df.into_sexp();
1007 // SAFETY: R main thread (constructor invariant); sexp is a valid
1008 // VECSXP just produced by DataFrame::into_sexp.
1009 unsafe {
1010 self.scope.protect_raw(sexp);
1011 }
1012 self.pairs.push((name.into(), sexp));
1013 self
1014 }
1015
1016 /// Append an arbitrary SEXP under a name, protected like
1017 /// [`push`](Self::push). Used by the serde split-shape writer to carry
1018 /// the caller-supplied empty-`Ok` sentinel, which is deliberately not a
1019 /// `DataFrame`.
1020 ///
1021 /// # Safety
1022 ///
1023 /// `sexp` must be a valid R object; R main thread (constructor invariant).
1024 #[cfg(feature = "serde")]
1025 #[must_use]
1026 pub(crate) unsafe fn push_raw<S: Into<String>>(mut self, name: S, sexp: SEXP) -> Self {
1027 unsafe {
1028 self.scope.protect_raw(sexp);
1029 }
1030 self.pairs.push((name.into(), sexp));
1031 self
1032 }
1033
1034 /// Number of entries pushed so far.
1035 pub fn len(&self) -> usize {
1036 self.pairs.len()
1037 }
1038
1039 /// Whether no entries have been pushed yet.
1040 pub fn is_empty(&self) -> bool {
1041 self.pairs.is_empty()
1042 }
1043
1044 /// Consume the builder and return the assembled named [`List`].
1045 ///
1046 /// The returned `List`'s SEXP is *not* separately protected on return — the
1047 /// caller takes responsibility for protection (typically by immediately
1048 /// handing it back to R via the `.Call` return path). This matches the
1049 /// contract of [`List::from_raw_pairs`](crate::list::List::from_raw_pairs).
1050 pub fn build(self) -> crate::list::List {
1051 // pairs[i].1 is protected by self.scope; from_raw_pairs protects the
1052 // assembled VECSXP and STRSXP during construction. When self drops at
1053 // this function's exit, the input SEXPs are unprotected — but they are
1054 // now children of the returned list, so they remain reachable.
1055 crate::list::List::from_raw_pairs(self.pairs)
1056 }
1057}
1058
1059impl Default for NamedDataFrameListBuilder {
1060 fn default() -> Self {
1061 Self::new()
1062 }
1063}
1064// endregion
1065
1066// region: Debug impl
1067
1068impl std::fmt::Debug for DataFrame {
1069 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1070 f.debug_struct("DataFrame")
1071 .field("nrow", &self.nrow())
1072 .field("ncol", &self.ncol())
1073 .finish()
1074 }
1075}
1076
1077#[cfg(test)]
1078mod tests {
1079 use super::*;
1080
1081 #[test]
1082 fn test_data_frame_error_display() {
1083 let err = DataFrameError::NotDataFrame;
1084 assert_eq!(err.to_string(), "object does not inherit from data.frame");
1085
1086 let err = DataFrameError::NoNames;
1087 assert_eq!(err.to_string(), "data.frame has no column names");
1088
1089 let err = DataFrameError::UnequalLengths {
1090 expected: 3,
1091 column: "y".to_string(),
1092 actual: 5,
1093 };
1094 assert_eq!(err.to_string(), "column \"y\" has length 5 (expected 3)");
1095
1096 let err = DataFrameError::UnnamedColumns;
1097 assert_eq!(
1098 err.to_string(),
1099 "cannot create data frame from unnamed list elements"
1100 );
1101
1102 let err = DataFrameError::NoSuchColumn("g".to_string());
1103 assert_eq!(err.to_string(), "no such column: \"g\"");
1104 }
1105
1106 // region: NamedDataFrameListBuilder structural invariants
1107
1108 /// A new builder has zero length and reports is_empty().
1109 #[test]
1110 fn builder_new_is_empty() {
1111 let b = NamedDataFrameListBuilder::default();
1112 assert_eq!(b.len(), 0);
1113 assert!(b.is_empty());
1114 }
1115
1116 /// with_capacity reserves space but the builder is still empty.
1117 #[test]
1118 fn builder_with_capacity_starts_empty() {
1119 let b = NamedDataFrameListBuilder::with_capacity(8);
1120 assert_eq!(b.len(), 0);
1121 assert!(b.is_empty());
1122 }
1123
1124 /// The builder's scope count starts at zero (no protections yet).
1125 #[test]
1126 fn builder_scope_count_zero_before_push() {
1127 let b = NamedDataFrameListBuilder::new();
1128 assert_eq!(b.scope.count(), 0);
1129 }
1130 // endregion
1131}
1132// endregion