@@ -64,6 +64,21 @@ pub enum VirtualDataCell {
6464 RowPath ( Vec < Scalar > ) ,
6565}
6666
67+ #[ derive( Copy , Clone , Debug , PartialEq , Eq ) ]
68+ pub enum RowPathStyle {
69+ /// Legacy: emit a single `__ROW_PATH__` sidecar (per-row nested
70+ /// array in `render_to_rows`, array-of-arrays in
71+ /// `render_to_columns_json`). `__ROW_PATH_N__` per-level columns
72+ /// are filtered out. Matches the native engine's `to_json` /
73+ /// `to_columns` shape.
74+ Sidecar ,
75+
76+ /// Native: emit per-level `__ROW_PATH_0__`, `__ROW_PATH_1__`, …
77+ /// columns directly. No `__ROW_PATH__` sidecar. Matches the native
78+ /// engine's Arrow IPC, CSV, and NDJSON shapes.
79+ PerLevel ,
80+ }
81+
6782/// Trait for types that can be written to a [`ColumnBuilder`] which
6883/// enforces sequential construction.
6984///
@@ -593,14 +608,16 @@ impl VirtualDataSlice {
593608 /// to Perspective-compatible types.
594609 pub fn from_arrow_ipc ( & mut self , ipc : & [ u8 ] ) -> Result < ( ) , Box < dyn Error > > {
595610 let cursor = std:: io:: Cursor :: new ( ipc) ;
596- let batch = if & ipc[ 0 ..6 ] == "ARROW1" . as_bytes ( ) {
597- FileReader :: try_new ( cursor, None ) ?
598- . next ( )
599- . ok_or ( "Arrow IPC stream contained no record batches" ) ??
611+ let batches: Vec < RecordBatch > = if & ipc[ 0 ..6 ] == "ARROW1" . as_bytes ( ) {
612+ FileReader :: try_new ( cursor, None ) ?. collect :: < Result < Vec < _ > , _ > > ( ) ?
600613 } else {
601- StreamReader :: try_new ( cursor, None ) ?
602- . next ( )
603- . ok_or ( "Arrow IPC stream contained no record batches" ) ??
614+ StreamReader :: try_new ( cursor, None ) ?. collect :: < Result < Vec < _ > , _ > > ( ) ?
615+ } ;
616+
617+ let batch = match batches. len ( ) {
618+ 0 => return Err ( "Arrow IPC stream contained no record batches" . into ( ) ) ,
619+ 1 => batches. into_iter ( ) . next ( ) . unwrap ( ) ,
620+ _ => arrow_select:: concat:: concat_batches ( & batches[ 0 ] . schema ( ) , & batches) ?,
604621 } ;
605622
606623 let has_group_by = !self . config . group_by . is_empty ( ) ;
@@ -764,17 +781,24 @@ impl VirtualDataSlice {
764781
765782 /// Converts the columnar data to a row-oriented representation for JSON
766783 /// serialization.
767- pub ( crate ) fn render_to_rows ( & mut self ) -> Vec < IndexMap < String , VirtualDataCell > > {
784+ ///
785+ /// `style` selects between the legacy `__ROW_PATH__` sidecar
786+ /// (`Sidecar`, used by `to_json`) and the native per-level
787+ /// `__ROW_PATH_N__` columns (`PerLevel`, used by `to_csv` /
788+ /// `to_ndjson`). See [`RowPathStyle`] for the deprecation plan.
789+ pub ( crate ) fn render_to_rows (
790+ & mut self ,
791+ style : RowPathStyle ,
792+ ) -> Vec < IndexMap < String , VirtualDataCell > > {
768793 let batch = self . freeze ( ) . clone ( ) ;
769794 let num_rows = batch. num_rows ( ) ;
770795 let schema = batch. schema ( ) ;
771796
772797 ( 0 ..num_rows)
773798 . map ( |row_idx| {
774799 let mut row = IndexMap :: new ( ) ;
775-
776- // Add RowPath column first if present
777- if let Some ( ref rp) = self . row_path
800+ if style == RowPathStyle :: Sidecar
801+ && let Some ( ref rp) = self . row_path
778802 && row_idx < rp. len ( )
779803 {
780804 row. insert (
@@ -783,8 +807,11 @@ impl VirtualDataSlice {
783807 ) ;
784808 }
785809
786- // Add Arrow columns
787810 for ( col_idx, field) in schema. fields ( ) . iter ( ) . enumerate ( ) {
811+ if style == RowPathStyle :: Sidecar && field. name ( ) . starts_with ( "__ROW_PATH_" ) {
812+ continue ;
813+ }
814+
788815 let col = batch. column ( col_idx) ;
789816 let cell = if col. is_null ( row_idx) {
790817 match field. data_type ( ) {
@@ -869,17 +896,31 @@ impl VirtualDataSlice {
869896 }
870897
871898 /// Serializes the data to a column-oriented JSON string.
872- pub fn render_to_columns_json ( & mut self ) -> Result < String , Box < dyn Error > > {
899+ ///
900+ /// `style` selects between the legacy `__ROW_PATH__` sidecar
901+ /// (`Sidecar`, used by `to_columns`) and the native per-level
902+ /// `__ROW_PATH_N__` columns (`PerLevel`, currently unused — reserved
903+ /// for the future deprecation of `__ROW_PATH__`). See
904+ /// [`RowPathStyle`] for context.
905+ pub fn render_to_columns_json (
906+ & mut self ,
907+ style : RowPathStyle ,
908+ ) -> Result < String , Box < dyn Error > > {
873909 let batch = self . freeze ( ) . clone ( ) ;
874910 let schema = batch. schema ( ) ;
875911 let mut map = serde_json:: Map :: new ( ) ;
876912
877- // Add RowPath if present
878- if let Some ( ref rp) = self . row_path {
913+ if style == RowPathStyle :: Sidecar
914+ && let Some ( ref rp) = self . row_path
915+ {
879916 map. insert ( "__ROW_PATH__" . to_string ( ) , serde_json:: to_value ( rp) ?) ;
880917 }
881918
882919 for ( col_idx, field) in schema. fields ( ) . iter ( ) . enumerate ( ) {
920+ if style == RowPathStyle :: Sidecar && field. name ( ) . starts_with ( "__ROW_PATH_" ) {
921+ continue ;
922+ }
923+
883924 let col = batch. column ( col_idx) ;
884925 let num_rows = col. len ( ) ;
885926 let values: serde_json:: Value = match field. data_type ( ) {
0 commit comments