Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 8 additions & 22 deletions rust/perspective-client/src/rust/virtual_server/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -601,11 +601,10 @@ impl VirtualDataSlice {
/// native `perspective-server`'s `to_arrow` output when
/// `emit_legacy_row_path_names: false`.
///
/// When `split_by` is active, renames data columns by replacing `_`
/// with `|` (the DuckDB PIVOT separator).
///
/// Also coerces non-standard Arrow types (e.g. `Decimal128`, `Int64`)
/// to Perspective-compatible types.
/// to Perspective-compatible types. Data column names are passed
/// through verbatim — pivoted views already name columns with
/// Perspective's column-path separator.
pub fn from_arrow_ipc(&mut self, ipc: &[u8]) -> Result<(), Box<dyn Error>> {
let cursor = std::io::Cursor::new(ipc);
let batches: Vec<RecordBatch> = if &ipc[0..6] == "ARROW1".as_bytes() {
Expand Down Expand Up @@ -702,14 +701,7 @@ impl VirtualDataSlice {
continue;
}

let new_name = if has_split_by && !name.starts_with("__") {
name.replace('_', "|")
} else {
name.clone()
};

let (coerced_field, coerced_array) =
coerce_column(&new_name, field, batch.column(col_idx))?;
let (coerced_field, coerced_array) = coerce_column(name, field, batch.column(col_idx))?;
new_fields.push(coerced_field);
new_arrays.push(coerced_array);
}
Expand Down Expand Up @@ -1108,20 +1100,14 @@ impl VirtualDataSlice {

Ok(())
} else {
let col_name = if !self.config.split_by.is_empty() && !name.starts_with("__") {
name.replace('_', "|")
} else {
name.to_owned()
};

if !self.builders.contains_key(&col_name) {
self.builders.insert(col_name.clone(), T::new_builder());
if !self.builders.contains_key(name) {
self.builders.insert(name.to_owned(), T::new_builder());
}

let col = self
.builders
.get_mut(&col_name)
.ok_or_else(|| format!("Column '{}' not found after insertion", col_name))?;
.get_mut(name)
.ok_or_else(|| format!("Column '{}' not found after insertion", name))?;

Ok(value.write_to(col)?)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,57 @@ pub type GenericSQLResult<T> = Result<T, GenericSQLError>;
pub struct GenericSQLVirtualServerModelArgs {
create_entity: Option<String>,
grouping_fn: Option<String>,

/// Separator joining `split_by` values and the column name in pivoted
/// view column names, e.g. `"CA|Sales"` for separator `"|"`. Perspective's
/// column-path separator is `"|"`, so any other value produces views the
/// client will not interpret as column paths.
column_separator: Option<String>,
}

/// Recovers the source column of a pivoted view column name — the longest
/// `config.columns` entry that is a strict suffix of `name` — with its index
/// in `config.columns`. Requires no separator knowledge, so it works at
/// protocol boundaries where the SQL model's `column_separator` is unknown.
/// Returns `None` for non-path names (e.g. flat-view columns, which equal a
/// `config.columns` entry exactly rather than strictly containing one).
pub(crate) fn column_path_source<'a>(
name: &str,
config: &'a ViewConfig,
) -> Option<(usize, &'a str)> {
let mut best: Option<(usize, &'a str)> = None;
for (idx, col) in config.columns.iter().flatten().enumerate() {
if name.len() > col.len()
&& name.ends_with(col.as_str())
&& best.is_none_or(|(_, b)| col.len() > b.len())
{
best = Some((idx, col));
}
}

best
}

/// Sorts pivoted view column names into Perspective's column-path order:
/// `split_by` value paths ascending, then `config.columns` order within each
/// path (e.g. `CA|price, CA|qty, NY|price, NY|qty`).
///
/// The per-column `PIVOT` join in [`ViewQueryContext`] emits columns grouped
/// by source column instead, so every egress of view column names re-sorts
/// with this. Internal `__`-prefixed columns sort first, unmatched names
/// last, both preserving relative order.
pub(crate) fn sort_column_paths<T: AsRef<str>>(names: &mut [T], config: &ViewConfig) {
names.sort_by_cached_key(|name| {
let name = name.as_ref();
if name.starts_with("__") {
return (0u8, String::new(), 0usize);
}

match column_path_source(name, config) {
Some((idx, col)) => (1, name[..name.len() - col.len()].to_string(), idx),
None => (2, String::new(), 0),
}
});
}

/// A stateless SQL query builder virtual server operations.
Expand Down Expand Up @@ -230,6 +281,8 @@ impl GenericSQLVirtualServerModel {
} else {
data_columns.sort_by(|a, b| b.cmp(a));
}
} else if !config.split_by.is_empty() {
sort_column_paths(&mut data_columns, config);
}

let data_columns: Vec<&String> = data_columns
Expand Down
Loading
Loading