Skip to main content

perspective_client/virtual_server/
data.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use std::error::Error;
14use std::sync::Arc;
15
16use arrow_array::builder::{
17    BooleanBuilder, Float64Builder, Int32Builder, StringDictionaryBuilder,
18    TimestampMillisecondBuilder,
19};
20use arrow_array::cast::AsArray;
21use arrow_array::types::Int32Type;
22use arrow_array::{
23    Array, ArrayRef, BooleanArray, Date32Array, Date64Array, Decimal128Array, DictionaryArray,
24    Float16Array, Float32Array, Float64Array, Int8Array, Int16Array, Int32Array, Int64Array,
25    LargeStringArray, RecordBatch, RecordBatchOptions, StringArray, Time32MillisecondArray,
26    Time32SecondArray, Time64MicrosecondArray, Time64NanosecondArray, TimestampMicrosecondArray,
27    TimestampMillisecondArray, TimestampNanosecondArray, TimestampSecondArray, UInt8Array,
28    UInt16Array, UInt32Array, UInt64Array,
29};
30use arrow_ipc::reader::{FileReader, StreamReader};
31use arrow_ipc::writer::StreamWriter;
32use arrow_schema::{DataType, Field, Schema, TimeUnit};
33use indexmap::IndexMap;
34use serde::Serialize;
35
36use crate::config::{GroupRollupMode, Scalar, ViewConfig};
37
38/// An Arrow column builder, used during the population phase of
39/// [`VirtualDataSlice`].
40pub enum ColumnBuilder {
41    Boolean(BooleanBuilder),
42    String(StringDictionaryBuilder<Int32Type>),
43    Float(Float64Builder),
44    Integer(Int32Builder),
45    Datetime(TimestampMillisecondBuilder),
46}
47
48fn dict_data_type() -> DataType {
49    DataType::Dictionary(Box::new(DataType::Int32), Box::new(DataType::Utf8))
50}
51
52/// Reads a cell from a canonical `Dictionary(Int32, Utf8)` column, or
53/// `None` for a null slot.
54fn dict_str_value(col: &ArrayRef, row_idx: usize) -> Option<&str> {
55    if col.is_null(row_idx) {
56        return None;
57    }
58
59    let typed = col
60        .as_any()
61        .downcast_ref::<DictionaryArray<Int32Type>>()
62        .and_then(|dict| {
63            let values = dict.values().as_any().downcast_ref::<StringArray>()?;
64            Some((dict, values))
65        });
66
67    match typed {
68        Some((dict, values)) => {
69            let key = dict.keys().value(row_idx) as usize;
70            (key < values.len() && !values.is_null(key)).then(|| values.value(key))
71        },
72        None => {
73            tracing::error!("Non-canonical dictionary column {}", col.data_type());
74            None
75        },
76    }
77}
78
79/// A single cell value in a row-oriented data representation.
80///
81/// Used when converting [`VirtualDataSlice`] to row format for JSON
82/// serialization.
83#[derive(Debug, Serialize)]
84#[serde(untagged)]
85pub enum VirtualDataCell {
86    Boolean(Option<bool>),
87    String(Option<String>),
88    Float(Option<f64>),
89    Integer(Option<i32>),
90    Datetime(Option<i64>),
91    RowPath(Vec<Scalar>),
92}
93
94#[derive(Copy, Clone, Debug, PartialEq, Eq)]
95pub enum RowPathStyle {
96    /// Legacy: emit a single `__ROW_PATH__` sidecar (per-row nested
97    /// array in `render_to_rows`, array-of-arrays in
98    /// `render_to_columns_json`). `__ROW_PATH_N__` per-level columns
99    /// are filtered out. Matches the native engine's `to_json` /
100    /// `to_columns` shape.
101    Sidecar,
102
103    /// Native: emit per-level `__ROW_PATH_0__`, `__ROW_PATH_1__`, …
104    /// columns directly. No `__ROW_PATH__` sidecar. Matches the native
105    /// engine's Arrow IPC, CSV, and NDJSON shapes.
106    PerLevel,
107}
108
109/// Trait for types that can be written to a [`ColumnBuilder`] which
110/// enforces sequential construction.
111///
112/// This trait enables type-safe insertion of values into virtual data columns,
113/// ensuring that values are written to columns of the correct type.
114pub trait SetVirtualDataColumn {
115    /// Writes this value (sequentially) to the given column builder.
116    ///
117    /// Returns an error if the column type does not match the value type.
118    fn write_to(self, col: &mut ColumnBuilder) -> Result<(), &'static str>;
119
120    /// Creates a new empty column builder of the appropriate type for this
121    /// value.
122    fn new_builder() -> ColumnBuilder;
123
124    /// Converts this value to a [`Scalar`] representation.
125    fn to_scalar(self) -> Scalar;
126}
127
128impl SetVirtualDataColumn for Option<String> {
129    fn write_to(self, col: &mut ColumnBuilder) -> Result<(), &'static str> {
130        if let ColumnBuilder::String(builder) = col {
131            match self {
132                Some(s) => builder.append_value(&s),
133                None => builder.append_null(),
134            }
135            Ok(())
136        } else {
137            Err("Bad type")
138        }
139    }
140
141    fn new_builder() -> ColumnBuilder {
142        ColumnBuilder::String(StringDictionaryBuilder::new())
143    }
144
145    fn to_scalar(self) -> Scalar {
146        if let Some(x) = self {
147            Scalar::String(x)
148        } else {
149            Scalar::Null
150        }
151    }
152}
153
154impl SetVirtualDataColumn for Option<f64> {
155    fn write_to(self, col: &mut ColumnBuilder) -> Result<(), &'static str> {
156        if let ColumnBuilder::Float(builder) = col {
157            match self {
158                Some(v) => builder.append_value(v),
159                None => builder.append_null(),
160            }
161            Ok(())
162        } else {
163            Err("Bad type")
164        }
165    }
166
167    fn new_builder() -> ColumnBuilder {
168        ColumnBuilder::Float(Float64Builder::new())
169    }
170
171    fn to_scalar(self) -> Scalar {
172        if let Some(x) = self {
173            Scalar::Float(x)
174        } else {
175            Scalar::Null
176        }
177    }
178}
179
180impl SetVirtualDataColumn for Option<i32> {
181    fn write_to(self, col: &mut ColumnBuilder) -> Result<(), &'static str> {
182        if let ColumnBuilder::Integer(builder) = col {
183            match self {
184                Some(v) => builder.append_value(v),
185                None => builder.append_null(),
186            }
187            Ok(())
188        } else {
189            Err("Bad type")
190        }
191    }
192
193    fn new_builder() -> ColumnBuilder {
194        ColumnBuilder::Integer(Int32Builder::new())
195    }
196
197    fn to_scalar(self) -> Scalar {
198        if let Some(x) = self {
199            Scalar::Float(x as f64)
200        } else {
201            Scalar::Null
202        }
203    }
204}
205
206impl SetVirtualDataColumn for Option<i64> {
207    fn write_to(self, col: &mut ColumnBuilder) -> Result<(), &'static str> {
208        if let ColumnBuilder::Datetime(builder) = col {
209            match self {
210                Some(v) => builder.append_value(v),
211                None => builder.append_null(),
212            }
213            Ok(())
214        } else {
215            Err("Bad type")
216        }
217    }
218
219    fn new_builder() -> ColumnBuilder {
220        ColumnBuilder::Datetime(TimestampMillisecondBuilder::new())
221    }
222
223    fn to_scalar(self) -> Scalar {
224        if let Some(x) = self {
225            Scalar::Float(x as f64)
226        } else {
227            Scalar::Null
228        }
229    }
230}
231
232impl SetVirtualDataColumn for Option<bool> {
233    fn write_to(self, col: &mut ColumnBuilder) -> Result<(), &'static str> {
234        if let ColumnBuilder::Boolean(builder) = col {
235            match self {
236                Some(v) => builder.append_value(v),
237                None => builder.append_null(),
238            }
239            Ok(())
240        } else {
241            Err("Bad type")
242        }
243    }
244
245    fn new_builder() -> ColumnBuilder {
246        ColumnBuilder::Boolean(BooleanBuilder::new())
247    }
248
249    fn to_scalar(self) -> Scalar {
250        if let Some(x) = self {
251            Scalar::Bool(x)
252        } else {
253            Scalar::Null
254        }
255    }
256}
257
258/// A columnar data slice returned from a virtual server view query.
259///
260/// This struct represents a rectangular slice of data from a view, stored
261/// internally as Arrow builders during population and frozen into a
262/// `RecordBatch` on first consumption.
263#[derive(Debug)]
264pub struct VirtualDataSlice {
265    config: ViewConfig,
266    builders: IndexMap<String, ColumnBuilder>,
267    row_path: Option<Vec<Vec<Scalar>>>,
268    frozen: Option<RecordBatch>,
269}
270
271impl std::fmt::Debug for ColumnBuilder {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        match self {
274            ColumnBuilder::Boolean(_) => write!(f, "ColumnBuilder::Boolean(..)"),
275            ColumnBuilder::String(_) => write!(f, "ColumnBuilder::String(..)"),
276            ColumnBuilder::Float(_) => write!(f, "ColumnBuilder::Float(..)"),
277            ColumnBuilder::Integer(_) => write!(f, "ColumnBuilder::Integer(..)"),
278            ColumnBuilder::Datetime(_) => write!(f, "ColumnBuilder::Datetime(..)"),
279        }
280    }
281}
282
283/// Extracts grouping ID values from an Arrow array as `i64`.
284fn cast_to_int64(array: &ArrayRef) -> Result<Vec<i64>, Box<dyn Error>> {
285    let num_rows = array.len();
286    let mut result = Vec::with_capacity(num_rows);
287    match array.data_type() {
288        DataType::Int32 => {
289            let arr = array.as_any().downcast_ref::<Int32Array>().unwrap();
290            for i in 0..num_rows {
291                result.push(if arr.is_null(i) {
292                    0
293                } else {
294                    arr.value(i) as i64
295                });
296            }
297        },
298        DataType::Int64 => {
299            let arr = array.as_any().downcast_ref::<Int64Array>().unwrap();
300            for i in 0..num_rows {
301                result.push(if arr.is_null(i) { 0 } else { arr.value(i) });
302            }
303        },
304        DataType::Float64 => {
305            let arr = array.as_any().downcast_ref::<Float64Array>().unwrap();
306            for i in 0..num_rows {
307                result.push(if arr.is_null(i) {
308                    0
309                } else {
310                    arr.value(i) as i64
311                });
312            }
313        },
314        dt => return Err(format!("Cannot cast {} to Int64", dt).into()),
315    }
316    Ok(result)
317}
318
319/// Extracts a single cell from a *coerced* Arrow array as a [`Scalar`].
320fn extract_scalar(array: &ArrayRef, row_idx: usize) -> Scalar {
321    if array.is_null(row_idx) {
322        return Scalar::Null;
323    }
324    match array.data_type() {
325        DataType::Dictionary(..) => dict_str_value(array, row_idx)
326            .map(|x| Scalar::String(x.to_string()))
327            .unwrap_or(Scalar::Null),
328        DataType::Float64 => {
329            let arr = array.as_any().downcast_ref::<Float64Array>().unwrap();
330            Scalar::Float(arr.value(row_idx))
331        },
332        DataType::Int32 => {
333            let arr = array.as_any().downcast_ref::<Int32Array>().unwrap();
334            Scalar::Float(arr.value(row_idx) as f64)
335        },
336        DataType::Boolean => {
337            let arr = array.as_any().downcast_ref::<BooleanArray>().unwrap();
338            Scalar::Bool(arr.value(row_idx))
339        },
340        DataType::Timestamp(TimeUnit::Millisecond, _) => {
341            let arr = array
342                .as_any()
343                .downcast_ref::<TimestampMillisecondArray>()
344                .unwrap();
345            Scalar::Float(arr.value(row_idx) as f64)
346        },
347        DataType::Date32 => {
348            let arr = array.as_any().downcast_ref::<Date32Array>().unwrap();
349            Scalar::Float(arr.value(row_idx) as f64 * 86_400_000.0)
350        },
351        dt => {
352            tracing::error!("Non-canonical row path type {}", dt);
353            Scalar::Null
354        },
355    }
356}
357
358/// Coerces an Arrow column to Perspective-compatible types, optionally
359/// renaming.
360/// Manually converts a timestamp array of any unit to milliseconds.
361fn timestamp_to_millis(array: &ArrayRef, unit: &TimeUnit) -> ArrayRef {
362    let millis: TimestampMillisecondArray = match unit {
363        TimeUnit::Second => {
364            let arr = array
365                .as_any()
366                .downcast_ref::<TimestampSecondArray>()
367                .unwrap();
368            arr.iter().map(|v| v.map(|v| v * 1_000)).collect()
369        },
370        TimeUnit::Microsecond => {
371            let arr = array
372                .as_any()
373                .downcast_ref::<TimestampMicrosecondArray>()
374                .unwrap();
375            arr.iter().map(|v| v.map(|v| v / 1_000)).collect()
376        },
377        TimeUnit::Nanosecond => {
378            let arr = array
379                .as_any()
380                .downcast_ref::<TimestampNanosecondArray>()
381                .unwrap();
382            arr.iter().map(|v| v.map(|v| v / 1_000_000)).collect()
383        },
384        TimeUnit::Millisecond => {
385            let arr = array
386                .as_any()
387                .downcast_ref::<TimestampMillisecondArray>()
388                .unwrap();
389
390            return Arc::new(arr.clone().with_timezone_opt(None::<Arc<str>>)) as ArrayRef;
391        },
392    };
393    Arc::new(millis) as ArrayRef
394}
395
396fn coerce_column(
397    name: &str,
398    field: &Field,
399    array: &ArrayRef,
400) -> Result<(Field, ArrayRef), Box<dyn Error>> {
401    match field.data_type() {
402        DataType::Boolean | DataType::Float64 | DataType::Int32 | DataType::Date32 => Ok((
403            Field::new(name, field.data_type().clone(), true),
404            array.clone(),
405        )),
406        DataType::Dictionary(key, value) => {
407            if key.as_ref() == &DataType::Int32 && value.as_ref() == &DataType::Utf8 {
408                return Ok((Field::new(name, dict_data_type(), true), array.clone()));
409            }
410
411            let dict = array
412                .as_any_dictionary_opt()
413                .ok_or_else(|| format!("Column '{}' is not a dictionary array", name))?;
414
415            let values = arrow_select::take::take(dict.values(), dict.keys(), None)?;
416            let field = Field::new(name, values.data_type().clone(), true);
417            coerce_column(name, &field, &values)
418        },
419        DataType::Utf8 => {
420            let arr = array.as_any().downcast_ref::<StringArray>().unwrap();
421            let mut builder = StringDictionaryBuilder::<Int32Type>::new();
422            for i in 0..arr.len() {
423                if arr.is_null(i) {
424                    builder.append_null();
425                } else {
426                    builder.append_value(arr.value(i));
427                }
428            }
429            Ok((
430                Field::new(name, dict_data_type(), true),
431                Arc::new(builder.finish()) as ArrayRef,
432            ))
433        },
434        DataType::Timestamp(TimeUnit::Millisecond, None) => Ok((
435            Field::new(name, DataType::Timestamp(TimeUnit::Millisecond, None), true),
436            array.clone(),
437        )),
438        DataType::Int8 => {
439            let arr = array.as_any().downcast_ref::<Int8Array>().unwrap();
440            let result: Int32Array = arr.iter().map(|v| v.map(|v| v as i32)).collect();
441            Ok((
442                Field::new(name, DataType::Int32, true),
443                Arc::new(result) as ArrayRef,
444            ))
445        },
446        DataType::Int16 => {
447            let arr = array.as_any().downcast_ref::<Int16Array>().unwrap();
448            let result: Int32Array = arr.iter().map(|v| v.map(|v| v as i32)).collect();
449            Ok((
450                Field::new(name, DataType::Int32, true),
451                Arc::new(result) as ArrayRef,
452            ))
453        },
454        DataType::UInt8 => {
455            let arr = array.as_any().downcast_ref::<UInt8Array>().unwrap();
456            let result: Int32Array = arr.iter().map(|v| v.map(|v| v as i32)).collect();
457            Ok((
458                Field::new(name, DataType::Int32, true),
459                Arc::new(result) as ArrayRef,
460            ))
461        },
462        DataType::UInt16 => {
463            let arr = array.as_any().downcast_ref::<UInt16Array>().unwrap();
464            let result: Int32Array = arr.iter().map(|v| v.map(|v| v as i32)).collect();
465            Ok((
466                Field::new(name, DataType::Int32, true),
467                Arc::new(result) as ArrayRef,
468            ))
469        },
470        DataType::UInt32 => {
471            let arr = array.as_any().downcast_ref::<UInt32Array>().unwrap();
472            let result: Int64Array = arr.iter().map(|v| v.map(|v| v as i64)).collect();
473            let result: Float64Array = result.iter().map(|v| v.map(|v| v as f64)).collect();
474            Ok((
475                Field::new(name, DataType::Float64, true),
476                Arc::new(result) as ArrayRef,
477            ))
478        },
479        DataType::Int64 => {
480            let arr = array.as_any().downcast_ref::<Int64Array>().unwrap();
481            let result: Float64Array = arr.iter().map(|v| v.map(|v| v as f64)).collect();
482            Ok((
483                Field::new(name, DataType::Float64, true),
484                Arc::new(result) as ArrayRef,
485            ))
486        },
487        DataType::UInt64 => {
488            let arr = array.as_any().downcast_ref::<UInt64Array>().unwrap();
489            let result: Float64Array = arr.iter().map(|v| v.map(|v| v as f64)).collect();
490            Ok((
491                Field::new(name, DataType::Float64, true),
492                Arc::new(result) as ArrayRef,
493            ))
494        },
495        DataType::Float32 => {
496            let arr = array.as_any().downcast_ref::<Float32Array>().unwrap();
497            let result: Float64Array = arr.iter().map(|v| v.map(|v| v as f64)).collect();
498            Ok((
499                Field::new(name, DataType::Float64, true),
500                Arc::new(result) as ArrayRef,
501            ))
502        },
503        DataType::Float16 => {
504            let arr = array.as_any().downcast_ref::<Float16Array>().unwrap();
505            let result: Float64Array = arr.iter().map(|v| v.map(|v| v.to_f64())).collect();
506            Ok((
507                Field::new(name, DataType::Float64, true),
508                Arc::new(result) as ArrayRef,
509            ))
510        },
511        DataType::Decimal128(_, scale) => {
512            let scale = *scale;
513            let arr = array.as_any().downcast_ref::<Decimal128Array>().unwrap();
514            let divisor = 10_f64.powi(scale as i32);
515            let result: Float64Array = arr.iter().map(|v| v.map(|v| v as f64 / divisor)).collect();
516            Ok((
517                Field::new(name, DataType::Float64, true),
518                Arc::new(result) as ArrayRef,
519            ))
520        },
521        DataType::Date64 => {
522            let arr = array.as_any().downcast_ref::<Date64Array>().unwrap();
523            let result: Date32Array = arr
524                .iter()
525                .map(|v| v.map(|v| (v / 86_400_000) as i32))
526                .collect();
527            Ok((
528                Field::new(name, DataType::Date32, true),
529                Arc::new(result) as ArrayRef,
530            ))
531        },
532        DataType::Timestamp(unit, _) => {
533            let casted = timestamp_to_millis(array, unit);
534            Ok((
535                Field::new(name, DataType::Timestamp(TimeUnit::Millisecond, None), true),
536                casted,
537            ))
538        },
539        DataType::Time32(TimeUnit::Second) => {
540            let arr = array.as_any().downcast_ref::<Time32SecondArray>().unwrap();
541            let result: TimestampMillisecondArray =
542                arr.iter().map(|v| v.map(|v| v as i64 * 1_000)).collect();
543            Ok((
544                Field::new(name, DataType::Timestamp(TimeUnit::Millisecond, None), true),
545                Arc::new(result) as ArrayRef,
546            ))
547        },
548        DataType::Time32(TimeUnit::Millisecond) => {
549            let arr = array
550                .as_any()
551                .downcast_ref::<Time32MillisecondArray>()
552                .unwrap();
553            let result: TimestampMillisecondArray =
554                arr.iter().map(|v| v.map(|v| v as i64)).collect();
555            Ok((
556                Field::new(name, DataType::Timestamp(TimeUnit::Millisecond, None), true),
557                Arc::new(result) as ArrayRef,
558            ))
559        },
560        DataType::Time64(TimeUnit::Microsecond) => {
561            let arr = array
562                .as_any()
563                .downcast_ref::<Time64MicrosecondArray>()
564                .unwrap();
565            let result: TimestampMillisecondArray =
566                arr.iter().map(|v| v.map(|v| v / 1_000)).collect();
567            Ok((
568                Field::new(name, DataType::Timestamp(TimeUnit::Millisecond, None), true),
569                Arc::new(result) as ArrayRef,
570            ))
571        },
572        DataType::Time64(TimeUnit::Nanosecond) => {
573            let arr = array
574                .as_any()
575                .downcast_ref::<Time64NanosecondArray>()
576                .unwrap();
577            let result: TimestampMillisecondArray =
578                arr.iter().map(|v| v.map(|v| v / 1_000_000)).collect();
579            Ok((
580                Field::new(name, DataType::Timestamp(TimeUnit::Millisecond, None), true),
581                Arc::new(result) as ArrayRef,
582            ))
583        },
584        DataType::LargeUtf8 => {
585            let arr = array.as_any().downcast_ref::<LargeStringArray>().unwrap();
586            let mut builder = StringDictionaryBuilder::<Int32Type>::new();
587            for i in 0..arr.len() {
588                if arr.is_null(i) {
589                    builder.append_null();
590                } else {
591                    builder.append_value(arr.value(i));
592                }
593            }
594            Ok((
595                Field::new(name, dict_data_type(), true),
596                Arc::new(builder.finish()) as ArrayRef,
597            ))
598        },
599        dt => {
600            tracing::warn!(
601                "Coercing unknown Arrow type {} to Dictionary for column '{}'",
602                dt,
603                name
604            );
605            let num_rows = array.len();
606            let mut builder = StringDictionaryBuilder::<Int32Type>::new();
607            for i in 0..num_rows {
608                if array.is_null(i) {
609                    builder.append_null();
610                } else {
611                    let scalar_arr = array.slice(i, 1);
612                    builder.append_value(format!("{:?}", scalar_arr));
613                }
614            }
615            Ok((
616                Field::new(name, dict_data_type(), true),
617                Arc::new(builder.finish()) as ArrayRef,
618            ))
619        },
620    }
621}
622
623impl VirtualDataSlice {
624    pub fn new(config: ViewConfig) -> Self {
625        VirtualDataSlice {
626            config,
627            builders: IndexMap::default(),
628            row_path: None,
629            frozen: None,
630        }
631    }
632
633    /// Loads data from Arrow IPC file format bytes, with automatic
634    /// post-processing based on the view configuration.
635    ///
636    /// When `group_by` is active, extracts `__GROUPING_ID__` and
637    /// `__ROW_PATH_N__` columns to build `self.row_path`, then removes
638    /// `__GROUPING_ID__` from the output `RecordBatch`. The
639    /// `__ROW_PATH_N__` columns are *kept* in the frozen batch so
640    /// downstream Arrow IPC consumers (`with_typed_arrays`, used by
641    /// viewer-charts to drive its categorical/numeric axis resolvers
642    /// and tree-hierarchy walkers) see them inline — matching the
643    /// native `perspective-server`'s `to_arrow` output when
644    /// `emit_legacy_row_path_names: false`.
645    pub fn from_arrow_ipc(&mut self, ipc: &[u8]) -> Result<(), Box<dyn Error>> {
646        let cursor = std::io::Cursor::new(ipc);
647        let (ipc_schema, batches) = if &ipc[0..6] == "ARROW1".as_bytes() {
648            let reader = FileReader::try_new(cursor, None)?;
649            let schema = reader.schema();
650            (schema, reader.collect::<Result<Vec<_>, _>>()?)
651        } else {
652            let reader = StreamReader::try_new(cursor, None)?;
653            let schema = reader.schema();
654            (schema, reader.collect::<Result<Vec<_>, _>>()?)
655        };
656
657        let batch = match batches.len() {
658            0 => RecordBatch::new_empty(ipc_schema),
659            1 => batches.into_iter().next().unwrap(),
660            _ => arrow_select::concat::concat_batches(&batches[0].schema(), &batches)?,
661        };
662
663        let has_group_by = !self.config.group_by.is_empty();
664        let num_rows = batch.num_rows();
665        let schema = batch.schema();
666
667        let coerced = schema
668            .fields()
669            .iter()
670            .enumerate()
671            .map(|(col_idx, field)| coerce_column(field.name(), field, batch.column(col_idx)))
672            .collect::<Result<Vec<_>, _>>()?;
673
674        // Phase A: Extract row_path from __GROUPING_ID__ and __ROW_PATH_N__
675        if has_group_by {
676            let group_by_len = self.config.group_by.len();
677            let is_flat = self.config.group_rollup_mode == GroupRollupMode::Flat;
678            let grouping_ids = if is_flat {
679                None
680            } else {
681                let grouping_id_idx = schema
682                    .index_of("__GROUPING_ID__")
683                    .map_err(|_| "Missing __GROUPING_ID__ column")?;
684                Some(cast_to_int64(&coerced[grouping_id_idx].1)?)
685            };
686
687            let mut row_paths: Vec<Vec<Scalar>> = (0..num_rows).map(|_| Vec::new()).collect();
688            for gidx in 0..group_by_len {
689                let col_name = format!("__ROW_PATH_{}__", gidx);
690                let col_idx = schema
691                    .index_of(&col_name)
692                    .map_err(|_| format!("Missing {} column", col_name))?;
693
694                let col = &coerced[col_idx].1;
695
696                // In flat mode, all rows are leaf rows
697                if is_flat {
698                    // TODO I may be dumb but I'm not exactly sure what Clippy
699                    // wants here. This could be an `enumerate` but how is this
700                    // better?
701                    #[allow(clippy::needless_range_loop)]
702                    for row_idx in 0..num_rows {
703                        row_paths[row_idx].push(extract_scalar(col, row_idx));
704                    }
705                } else {
706                    let gids = grouping_ids.as_ref().unwrap();
707                    let max_grouping_id = 2_i64.pow(group_by_len as u32 - gidx as u32) - 1;
708                    for row_idx in 0..num_rows {
709                        if gids[row_idx] < max_grouping_id {
710                            row_paths[row_idx].push(extract_scalar(col, row_idx));
711                        }
712                    }
713                }
714            }
715
716            self.row_path = Some(row_paths);
717        }
718
719        let mut new_fields = Vec::new();
720        let mut new_arrays: Vec<ArrayRef> = Vec::new();
721        for (field, array) in coerced {
722            let name = field.name();
723            // `__GROUPING_ID__` is an internal SQL-rollup discriminator
724            // (used in Phase A above to decide which row-path levels
725            // belong to each row). No JS consumer reads it, so it's
726            // dropped from the frozen batch.
727            //
728            // `__ROW_PATH_N__` columns are kept. Phase A copied their
729            // values into `self.row_path` for the JSON sidecar paths
730            // (`render_to_columns_json`, `render_to_rows`), but
731            // viewer-charts' `with_typed_arrays` callback needs the
732            // per-level columns inline in the Arrow stream — its
733            // categorical-axis resolver, numeric-position lookup, and
734            // tree hierarchy walker all do `columns.get(\`__ROW_PATH_${n}__\`)`.
735            // Keeping the columns here lets `render_to_arrow_ipc`
736            // serialize them naturally, matching native
737            // `perspective-server`'s `to_arrow` output.
738            if name == "__GROUPING_ID__" {
739                continue;
740            }
741
742            new_fields.push(field);
743            new_arrays.push(array);
744        }
745
746        let new_schema = Arc::new(Schema::new(new_fields));
747        self.frozen = Some(if new_arrays.is_empty() {
748            let options = RecordBatchOptions::new().with_row_count(Some(num_rows));
749            RecordBatch::try_new_with_options(new_schema, new_arrays, &options)?
750        } else {
751            RecordBatch::try_new(new_schema, new_arrays)?
752        });
753
754        Ok(())
755    }
756
757    /// Freezes the builders into a `RecordBatch`. Idempotent — subsequent
758    /// calls return the cached batch.
759    pub(crate) fn freeze(&mut self) -> &RecordBatch {
760        if self.frozen.is_none() {
761            let mut fields = Vec::new();
762            let mut arrays: Vec<ArrayRef> = Vec::new();
763
764            for (name, builder) in &mut self.builders {
765                let (field, array): (Field, ArrayRef) = match builder {
766                    ColumnBuilder::Boolean(b) => (
767                        Field::new(name, DataType::Boolean, true),
768                        Arc::new(b.finish()),
769                    ),
770                    ColumnBuilder::String(b) => (
771                        Field::new(name, dict_data_type(), true),
772                        Arc::new(b.finish()),
773                    ),
774                    ColumnBuilder::Float(b) => (
775                        Field::new(name, DataType::Float64, true),
776                        Arc::new(b.finish()),
777                    ),
778                    ColumnBuilder::Integer(b) => (
779                        Field::new(name, DataType::Int32, true),
780                        Arc::new(b.finish()),
781                    ),
782                    ColumnBuilder::Datetime(b) => (
783                        Field::new(name, DataType::Timestamp(TimeUnit::Millisecond, None), true),
784                        Arc::new(b.finish()),
785                    ),
786                };
787                fields.push(field);
788                arrays.push(array);
789            }
790
791            let schema = Arc::new(Schema::new(fields));
792            let batch = if arrays.is_empty() {
793                let num_rows = self.row_path.as_ref().map(|x| x.len()).unwrap_or(0);
794                let options = RecordBatchOptions::new().with_row_count(Some(num_rows));
795                RecordBatch::try_new_with_options(schema, arrays, &options)
796            } else {
797                RecordBatch::try_new(schema, arrays)
798            };
799
800            self.frozen = Some(
801                batch.expect("RecordBatch construction should not fail for well-formed builders"),
802            );
803        }
804
805        self.frozen.as_ref().unwrap()
806    }
807
808    /// Serializes the data to Arrow IPC streaming format.
809    pub(crate) fn render_to_arrow_ipc(&mut self) -> Result<Vec<u8>, Box<dyn Error>> {
810        let batch = self.freeze().clone();
811        let schema = batch.schema();
812        let mut buf = Vec::new();
813        {
814            let mut writer = StreamWriter::try_new(&mut buf, &schema)?;
815            writer.write(&batch)?;
816            writer.finish()?;
817        }
818        Ok(buf)
819    }
820
821    /// Converts the columnar data to a row-oriented representation for JSON
822    /// serialization.
823    ///
824    /// `style` selects between the legacy `__ROW_PATH__` sidecar
825    /// (`Sidecar`, used by `to_json`) and the native per-level
826    /// `__ROW_PATH_N__` columns (`PerLevel`, used by `to_csv` /
827    /// `to_ndjson`). See [`RowPathStyle`] for the deprecation plan.
828    pub(crate) fn render_to_rows(
829        &mut self,
830        style: RowPathStyle,
831    ) -> Vec<IndexMap<String, VirtualDataCell>> {
832        let batch = self.freeze().clone();
833        let num_rows = batch.num_rows();
834        let schema = batch.schema();
835
836        let synthesize_row_path = style == RowPathStyle::PerLevel
837            && self.row_path.is_some()
838            && !schema
839                .fields()
840                .iter()
841                .any(|x| x.name().starts_with("__ROW_PATH_"));
842
843        (0..num_rows)
844            .map(|row_idx| {
845                let mut row = IndexMap::new();
846                if style == RowPathStyle::Sidecar
847                    && let Some(ref rp) = self.row_path
848                    && row_idx < rp.len()
849                {
850                    row.insert(
851                        "__ROW_PATH__".to_string(),
852                        VirtualDataCell::RowPath(rp[row_idx].clone()),
853                    );
854                }
855
856                if synthesize_row_path
857                    && let Some(ref rp) = self.row_path
858                    && row_idx < rp.len()
859                {
860                    for level in 0..self.config.group_by.len() {
861                        row.insert(
862                            format!("__ROW_PATH_{}__", level),
863                            match rp[row_idx].get(level) {
864                                Some(Scalar::String(x)) => VirtualDataCell::String(Some(x.clone())),
865                                Some(Scalar::Float(x)) => VirtualDataCell::Float(Some(*x)),
866                                Some(Scalar::Bool(x)) => VirtualDataCell::Boolean(Some(*x)),
867                                Some(Scalar::Null) | None => VirtualDataCell::String(None),
868                            },
869                        );
870                    }
871                }
872
873                for (col_idx, field) in schema.fields().iter().enumerate() {
874                    if style == RowPathStyle::Sidecar && field.name().starts_with("__ROW_PATH_") {
875                        continue;
876                    }
877
878                    let col = batch.column(col_idx);
879                    let cell = if col.is_null(row_idx) {
880                        match field.data_type() {
881                            DataType::Boolean => VirtualDataCell::Boolean(None),
882                            DataType::Utf8 | DataType::Dictionary(..) => {
883                                VirtualDataCell::String(None)
884                            },
885                            DataType::Float64 => VirtualDataCell::Float(None),
886                            DataType::Int32 => VirtualDataCell::Integer(None),
887                            DataType::Timestamp(TimeUnit::Millisecond, _) => {
888                                VirtualDataCell::Datetime(None)
889                            },
890                            _ => continue,
891                        }
892                    } else {
893                        match field.data_type() {
894                            DataType::Boolean => {
895                                let arr = col.as_any().downcast_ref::<BooleanArray>().unwrap();
896                                VirtualDataCell::Boolean(Some(arr.value(row_idx)))
897                            },
898                            DataType::Utf8 => {
899                                let arr = col.as_any().downcast_ref::<StringArray>().unwrap();
900                                VirtualDataCell::String(Some(arr.value(row_idx).to_string()))
901                            },
902                            DataType::Dictionary(..) => VirtualDataCell::String(
903                                dict_str_value(col, row_idx).map(|x| x.to_string()),
904                            ),
905                            DataType::Float64 => {
906                                let arr = col.as_any().downcast_ref::<Float64Array>().unwrap();
907                                VirtualDataCell::Float(Some(arr.value(row_idx)))
908                            },
909                            DataType::Int32 => {
910                                let arr = col.as_any().downcast_ref::<Int32Array>().unwrap();
911                                VirtualDataCell::Integer(Some(arr.value(row_idx)))
912                            },
913                            DataType::Int64 => {
914                                // TODO ????
915                                let arr = col.as_any().downcast_ref::<Int64Array>().unwrap();
916                                VirtualDataCell::Float(Some(arr.value(row_idx) as f64))
917                            },
918                            DataType::Time64(TimeUnit::Microsecond) => {
919                                let arr = col
920                                    .as_any()
921                                    .downcast_ref::<Time64MicrosecondArray>()
922                                    .unwrap();
923                                VirtualDataCell::Float(Some(arr.value(row_idx) as f64))
924                            },
925                            DataType::Timestamp(TimeUnit::Microsecond, _) => {
926                                let arr = col
927                                    .as_any()
928                                    .downcast_ref::<Time64MicrosecondArray>()
929                                    .unwrap();
930                                VirtualDataCell::Datetime(Some(arr.value(row_idx) * 1000))
931                            },
932                            DataType::Timestamp(TimeUnit::Millisecond, _) => {
933                                let arr = col
934                                    .as_any()
935                                    .downcast_ref::<TimestampMillisecondArray>()
936                                    .unwrap();
937                                VirtualDataCell::Datetime(Some(arr.value(row_idx)))
938                            },
939                            DataType::Date32 => {
940                                let arr = col.as_any().downcast_ref::<Date32Array>().unwrap();
941                                VirtualDataCell::Datetime(Some(
942                                    arr.value(row_idx) as i64 * 86_400_000,
943                                ))
944                            },
945                            x => {
946                                tracing::error!("Unknown Arrow IPC type {}", x);
947                                continue;
948                            },
949                        }
950                    };
951                    row.insert(field.name().clone(), cell);
952                }
953
954                row
955            })
956            .collect()
957    }
958
959    /// Serializes the data to a column-oriented JSON string.
960    ///
961    /// `style` selects between the legacy `__ROW_PATH__` sidecar
962    /// (`Sidecar`, used by `to_columns`) and the native per-level
963    /// `__ROW_PATH_N__` columns (`PerLevel`, currently unused — reserved
964    /// for the future deprecation of `__ROW_PATH__`). See
965    /// [`RowPathStyle`] for context.
966    ///
967    /// `id` emits an `__ID__` column of per-row identities, matching the
968    /// native engine's `to_columns(id = true)` shape for grouped views
969    /// (each row's identity is its `__ROW_PATH__` prefix). Ungrouped
970    /// views have no `row_path` and emit no `__ID__` — consumers fall
971    /// back to positional identity, as before.
972    pub fn render_to_columns_json(
973        &mut self,
974        style: RowPathStyle,
975        id: bool,
976    ) -> Result<String, Box<dyn Error>> {
977        let batch = self.freeze().clone();
978        let schema = batch.schema();
979        let mut map = serde_json::Map::new();
980
981        if let Some(ref rp) = self.row_path {
982            if style == RowPathStyle::Sidecar {
983                map.insert("__ROW_PATH__".to_string(), serde_json::to_value(rp)?);
984            }
985
986            if id {
987                map.insert("__ID__".to_string(), serde_json::to_value(rp)?);
988            }
989        }
990
991        for (col_idx, field) in schema.fields().iter().enumerate() {
992            if style == RowPathStyle::Sidecar && field.name().starts_with("__ROW_PATH_") {
993                continue;
994            }
995
996            let col = batch.column(col_idx);
997            let num_rows = col.len();
998            let values: serde_json::Value = match field.data_type() {
999                DataType::Boolean => {
1000                    let arr = col.as_any().downcast_ref::<BooleanArray>().unwrap();
1001                    serde_json::to_value(
1002                        (0..num_rows)
1003                            .map(|i| {
1004                                if arr.is_null(i) {
1005                                    None
1006                                } else {
1007                                    Some(arr.value(i))
1008                                }
1009                            })
1010                            .collect::<Vec<_>>(),
1011                    )?
1012                },
1013                DataType::Utf8 => {
1014                    let arr = col.as_any().downcast_ref::<StringArray>().unwrap();
1015                    serde_json::to_value(
1016                        (0..num_rows)
1017                            .map(|i| {
1018                                if arr.is_null(i) {
1019                                    None
1020                                } else {
1021                                    Some(arr.value(i))
1022                                }
1023                            })
1024                            .collect::<Vec<_>>(),
1025                    )?
1026                },
1027                DataType::Dictionary(..) => serde_json::to_value(
1028                    (0..num_rows)
1029                        .map(|i| dict_str_value(col, i))
1030                        .collect::<Vec<_>>(),
1031                )?,
1032                DataType::Float64 => {
1033                    let arr = col.as_any().downcast_ref::<Float64Array>().unwrap();
1034                    serde_json::to_value(
1035                        (0..num_rows)
1036                            .map(|i| {
1037                                if arr.is_null(i) {
1038                                    None
1039                                } else {
1040                                    Some(arr.value(i))
1041                                }
1042                            })
1043                            .collect::<Vec<_>>(),
1044                    )?
1045                },
1046                DataType::Int32 => {
1047                    let arr = col.as_any().downcast_ref::<Int32Array>().unwrap();
1048                    serde_json::to_value(
1049                        (0..num_rows)
1050                            .map(|i| {
1051                                if arr.is_null(i) {
1052                                    None
1053                                } else {
1054                                    Some(arr.value(i))
1055                                }
1056                            })
1057                            .collect::<Vec<_>>(),
1058                    )?
1059                },
1060                DataType::Int64 => {
1061                    let arr = col.as_any().downcast_ref::<Int64Array>().unwrap();
1062                    serde_json::to_value(
1063                        (0..num_rows)
1064                            .map(|i| {
1065                                if arr.is_null(i) {
1066                                    None
1067                                } else {
1068                                    Some(arr.value(i) as f64)
1069                                }
1070                            })
1071                            .collect::<Vec<_>>(),
1072                    )?
1073                },
1074                DataType::Timestamp(TimeUnit::Millisecond, _) => {
1075                    let arr = col
1076                        .as_any()
1077                        .downcast_ref::<TimestampMillisecondArray>()
1078                        .unwrap();
1079                    serde_json::to_value(
1080                        (0..num_rows)
1081                            .map(|i| {
1082                                if arr.is_null(i) {
1083                                    None
1084                                } else {
1085                                    Some(arr.value(i))
1086                                }
1087                            })
1088                            .collect::<Vec<_>>(),
1089                    )?
1090                },
1091                DataType::Time64(TimeUnit::Microsecond) => {
1092                    let arr = col
1093                        .as_any()
1094                        .downcast_ref::<Time64MicrosecondArray>()
1095                        .unwrap();
1096                    serde_json::to_value(
1097                        (0..num_rows)
1098                            .map(|i| {
1099                                if arr.is_null(i) {
1100                                    None
1101                                } else {
1102                                    Some(arr.value(i) as f64)
1103                                }
1104                            })
1105                            .collect::<Vec<_>>(),
1106                    )?
1107                },
1108                DataType::Date32 => {
1109                    let arr = col.as_any().downcast_ref::<Date32Array>().unwrap();
1110                    serde_json::to_value(
1111                        (0..num_rows)
1112                            .map(|i| {
1113                                if arr.is_null(i) {
1114                                    None
1115                                } else {
1116                                    Some(arr.value(i) as i64 * 86_400_000)
1117                                }
1118                            })
1119                            .collect::<Vec<_>>(),
1120                    )?
1121                },
1122                x => {
1123                    tracing::error!("Unknown Arrow IPC type {}", x);
1124                    continue;
1125                },
1126            };
1127            map.insert(field.name().clone(), values);
1128        }
1129
1130        Ok(serde_json::to_string(&map)?)
1131    }
1132
1133    /// Sets a value in a column at the specified row index.
1134    ///
1135    /// If `group_by_index` is `Some`, the value is added to the `__ROW_PATH__`
1136    /// column as part of the row's group-by path. Otherwise, the value is
1137    /// inserted into the named column.
1138    ///
1139    /// Creates the column if it does not already exist.
1140    pub fn set_col<T: SetVirtualDataColumn>(
1141        &mut self,
1142        name: &str,
1143        grouping_id: Option<usize>,
1144        index: usize,
1145        value: T,
1146    ) -> Result<(), Box<dyn Error>> {
1147        if name == "__GROUPING_ID__" {
1148            return Ok(());
1149        }
1150
1151        if name.starts_with("__ROW_PATH_") {
1152            let group_by_index: u32 = name[11..name.len() - 2].parse()?;
1153            let max_grouping_id =
1154                2_i32.pow((self.config.group_by.len() as u32) - group_by_index) - 1;
1155
1156            if grouping_id.map(|x| x as i32).unwrap_or(i32::MAX) < max_grouping_id {
1157                let col = self.row_path.get_or_insert_with(Vec::new);
1158                if let Some(row) = col.get_mut(index) {
1159                    let scalar = value.to_scalar();
1160                    row.push(scalar);
1161                } else {
1162                    while col.len() < index {
1163                        col.push(vec![])
1164                    }
1165
1166                    let scalar = value.to_scalar();
1167                    col.push(vec![scalar]);
1168                }
1169            }
1170
1171            Ok(())
1172        } else {
1173            if !self.builders.contains_key(name) {
1174                self.builders.insert(name.to_owned(), T::new_builder());
1175            }
1176
1177            let col = self
1178                .builders
1179                .get_mut(name)
1180                .ok_or_else(|| format!("Column '{}' not found after insertion", name))?;
1181
1182            Ok(value.write_to(col)?)
1183        }
1184    }
1185}