Skip to main content

perspective_client/
view.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::collections::HashMap;
14use std::ops::Deref;
15use std::str::FromStr;
16use std::sync::Arc;
17
18use futures::Future;
19use prost::bytes::Bytes;
20use serde::{Deserialize, Serialize};
21use ts_rs::TS;
22
23use self::view_on_update_req::Mode;
24use crate::assert_view_api;
25use crate::client::Client;
26use crate::proto::request::ClientReq;
27use crate::proto::response::ClientResp;
28use crate::proto::*;
29#[cfg(doc)]
30use crate::table::Table;
31pub use crate::utils::*;
32
33/// Options for [`View::on_update`].
34#[derive(Default, Debug, Deserialize, TS)]
35pub struct OnUpdateOptions {
36    pub mode: Option<OnUpdateMode>,
37}
38
39/// The update mode for [`View::on_update`].
40///
41/// `Row` mode calculates and provides the update batch new rows/columns as an
42/// Apache Arrow to the callback provided to [`View::on_update`]. This allows
43/// incremental updates if your callbakc can read this format, but should be
44/// disabled otherwise.
45#[derive(Default, Debug, Deserialize, TS)]
46pub enum OnUpdateMode {
47    #[default]
48    #[serde(rename = "row")]
49    Row,
50}
51
52impl FromStr for OnUpdateMode {
53    type Err = ClientError;
54
55    fn from_str(s: &str) -> Result<Self, Self::Err> {
56        if s == "row" {
57            Ok(OnUpdateMode::Row)
58        } else {
59            Err(ClientError::Option)
60        }
61    }
62}
63
64#[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq)]
65pub struct ColumnWindow {
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub start_col: Option<f32>,
68
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub end_col: Option<f32>,
71}
72
73/// Options for serializing a window of data from a [`View`].
74///
75/// Some fields of [`ViewWindow`] are only applicable to specific methods of
76/// [`View`].
77#[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq)]
78pub struct ViewWindow {
79    #[ts(optional)]
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub start_row: Option<f64>,
82
83    #[ts(optional)]
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub start_col: Option<f64>,
86
87    #[ts(optional)]
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub end_row: Option<f64>,
90
91    #[ts(optional)]
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub end_col: Option<f64>,
94
95    #[ts(optional)]
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub id: Option<bool>,
98
99    #[ts(optional)]
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub index: Option<bool>,
102
103    /// Only impacts [`View::to_csv`]
104    #[ts(optional)]
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub formatted: Option<bool>,
107
108    /// Arrow IPC body compression for [`View::to_arrow`], `"lz4"` or `"zstd"`
109    /// (uncompressed when omitted).
110    #[ts(optional)]
111    #[ts(type = "\"lz4\" | \"zstd\"")]
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub compression: Option<String>,
114
115    /// When `true`, [`View::to_arrow`] names `group_by` key columns
116    /// `__ROW_PATH_N__` and never qualifies an aggregate, instead of the
117    /// human-readable names a key column and its aggregate would otherwise
118    /// take.
119    #[ts(optional)]
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub machine_column_names: Option<bool>,
122}
123
124impl From<ViewWindow> for ViewPort {
125    fn from(window: ViewWindow) -> Self {
126        ViewPort {
127            start_row: window.start_row.map(|x| x.floor() as u32),
128            start_col: window.start_col.map(|x| x.floor() as u32),
129            end_row: window.end_row.map(|x| x.ceil() as u32),
130            end_col: window.end_col.map(|x| x.ceil() as u32),
131            machine_column_names: window.machine_column_names,
132        }
133    }
134}
135
136impl From<ViewPort> for ViewWindow {
137    fn from(window: ViewPort) -> Self {
138        ViewWindow {
139            start_row: window.start_row.map(|x| x as f64),
140            start_col: window.start_col.map(|x| x as f64),
141            end_row: window.end_row.map(|x| x as f64),
142            end_col: window.end_col.map(|x| x as f64),
143            machine_column_names: window.machine_column_names,
144            ..ViewWindow::default()
145        }
146    }
147}
148
149/// Rows updated and port ID corresponding to an update batch, provided to the
150/// callback argument to [`View::on_update`] with the "rows" mode.
151#[derive(TS)]
152pub struct OnUpdateData(crate::proto::ViewOnUpdateResp);
153
154impl Deref for OnUpdateData {
155    type Target = crate::proto::ViewOnUpdateResp;
156
157    fn deref(&self) -> &Self::Target {
158        &self.0
159    }
160}
161
162/// The rule under which the rows reported by [`View::on_remove`] left the
163/// [`View`].
164#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, TS)]
165#[serde(rename_all = "lowercase")]
166pub enum RemoveCause {
167    Key,
168    Group,
169    Filter,
170}
171
172impl RemoveCause {
173    /// This cause as its lowercase name.
174    pub fn as_str(&self) -> &'static str {
175        match self {
176            RemoveCause::Key => "key",
177            RemoveCause::Group => "group",
178            RemoveCause::Filter => "filter",
179        }
180    }
181}
182
183impl From<view_on_remove_resp::Cause> for RemoveCause {
184    fn from(cause: view_on_remove_resp::Cause) -> Self {
185        match cause {
186            view_on_remove_resp::Cause::Key => RemoveCause::Key,
187            view_on_remove_resp::Cause::Group => RemoveCause::Group,
188            view_on_remove_resp::Cause::Filter => RemoveCause::Filter,
189        }
190    }
191}
192
193/// The keys of rows which left a [`View`], the port ID and the [`RemoveCause`]
194/// they left under, provided to the callback argument to [`View::on_remove`].
195#[derive(Clone, Debug, Serialize, TS)]
196pub struct OnRemoveData {
197    pub port_id: u32,
198    #[ts(type = "Uint8Array | undefined")]
199    #[serde(with = "serde_bytes")]
200    pub indices: Option<Vec<u8>>,
201    pub cause: RemoveCause,
202}
203
204impl From<ViewOnRemoveResp> for OnRemoveData {
205    fn from(resp: ViewOnRemoveResp) -> Self {
206        OnRemoveData {
207            port_id: resp.port_id,
208            cause: resp.cause().into(),
209            indices: resp.indices,
210        }
211    }
212}
213/// The [`View`] struct is Perspective's query and serialization interface. It
214/// represents a query on the `Table`'s dataset and is always created from an
215/// existing `Table` instance via the [`Table::view`] method.
216///
217/// [`View`]s are immutable with respect to the arguments provided to the
218/// [`Table::view`] method; to change these parameters, you must create a new
219/// [`View`] on the same [`Table`]. However, each [`View`] is _live_ with
220/// respect to the [`Table`]'s data, and will (within a conflation window)
221/// update with the latest state as its parent [`Table`] updates, including
222/// incrementally recalculating all aggregates, pivots, filters, etc. [`View`]
223/// query parameters are composable, in that each parameter works independently
224/// _and_ in conjunction with each other, and there is no limit to the number of
225/// pivots, filters, etc. which can be applied.
226///
227/// To construct a [`View`], call the [`Table::view`] factory method. A
228/// [`Table`] can have as many [`View`]s associated with it as you need -
229/// Perspective conserves memory by relying on a single [`Table`] to power
230/// multiple [`View`]s concurrently.
231///
232/// # Examples
233///
234/// ```no_run
235/// # use perspective_client::{Client, TableData, TableInitOptions, UpdateData, ViewWindow};
236/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
237/// # let client: Client = todo!();
238/// let opts = TableInitOptions::default();
239/// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
240/// let table = client.table(data, opts).await?;
241///
242/// let view = table.view(None).await?;
243/// let arrow = view.to_arrow(ViewWindow::default()).await?;
244/// view.delete().await?;
245/// # Ok(()) }
246/// ```
247///
248/// ```no_run
249/// # use std::collections::HashMap;
250/// # use perspective_client::Table;
251/// # use perspective_client::config::*;
252/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
253/// # let table: Table = todo!();
254/// let view = table
255///     .view(Some(ViewConfigUpdate {
256///         columns: Some(vec![Some("Sales".into())]),
257///         aggregates: Some(HashMap::from_iter(vec![("Sales".into(), "sum".into())])),
258///         group_by: Some(vec!["Region".into(), "Country".into()]),
259///         filter: Some(vec![Filter::new("Category", "in", &[
260///             "Furniture",
261///             "Technology",
262///         ])]),
263///         ..ViewConfigUpdate::default()
264///     }))
265///     .await?;
266/// # Ok(()) }
267/// ```
268///
269///  Group By
270///
271/// ```no_run
272/// # use perspective_client::Table;
273/// # use perspective_client::config::*;
274/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
275/// # let table: Table = todo!();
276/// let view = table
277///     .view(Some(ViewConfigUpdate {
278///         group_by: Some(vec!["a".into(), "c".into()]),
279///         ..ViewConfigUpdate::default()
280///     }))
281///     .await?;
282/// # Ok(()) }
283/// ```
284///
285/// Split By
286///
287/// ```no_run
288/// # use perspective_client::Table;
289/// # use perspective_client::config::*;
290/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
291/// # let table: Table = todo!();
292/// let view = table
293///     .view(Some(ViewConfigUpdate {
294///         split_by: Some(vec!["a".into(), "c".into()]),
295///         ..ViewConfigUpdate::default()
296///     }))
297///     .await?;
298/// # Ok(()) }
299/// ```
300///
301/// In Javascript, a [`Table`] can be constructed on a [`Table::view`] instance,
302/// which will return a new [`Table`] based on the [`Table::view`]'s dataset,
303/// and all future updates that affect the [`Table::view`] will be forwarded to
304/// the new [`Table`]. This is particularly useful for implementing a
305/// [Client/Server Replicated](server.md#clientserver-replicated) design, by
306/// serializing the `View` to an arrow and setting up an `on_update` callback.
307///
308/// ```no_run
309/// # use perspective_client::{Client, TableData, TableInitOptions, UpdateData, UpdateOptions};
310/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
311/// # let client: Client = todo!();
312/// let opts = TableInitOptions::default();
313/// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
314/// let table = client.table(data, opts.clone()).await?;
315/// let view = table.view(None).await?;
316/// let table2 = client.table(TableData::View(view), opts).await?;
317/// let more = UpdateData::Csv("x,y\n5,6".into());
318/// table.update(more, UpdateOptions::default()).await?;
319/// # Ok(()) }
320/// ```
321#[derive(Clone, Debug)]
322pub struct View {
323    pub name: String,
324    client: Client,
325    pub(crate) source: Option<ViewSource>,
326}
327
328/// The options of the [`Table`] a [`View`] was created from, as known to the
329/// [`Client`] which created it; a [`View`] opened by name alone has no source.
330#[derive(Clone, Debug)]
331pub(crate) struct ViewSource {
332    pub options: crate::table::TableOptions,
333}
334
335assert_view_api!(View);
336
337impl View {
338    pub(crate) fn client(&self) -> &Client {
339        &self.client
340    }
341
342    pub fn new(name: String, client: Client) -> Self {
343        View {
344            name,
345            client,
346            source: None,
347        }
348    }
349
350    pub(crate) fn new_with_source(name: String, client: Client, source: ViewSource) -> Self {
351        View {
352            name,
353            client,
354            source: Some(source),
355        }
356    }
357
358    fn client_message(&self, req: ClientReq) -> Request {
359        crate::proto::Request {
360            msg_id: self.client.gen_id(),
361            entity_id: self.name.clone(),
362            client_req: Some(req),
363        }
364    }
365
366    /// Returns this [`View`]'s column header area for `window`, transposed as
367    /// `area[level][column]`.
368    ///
369    /// A column path shows the columns that a given cell belongs to after
370    /// pivots are applied. The area is rectangular with one level per
371    /// `split_by` plus one: a column's split values occupy the leading levels
372    /// and its name always occupies the last, so a subtotal or grand total
373    /// under `split_rollup_mode: "rollup"` reads [`Scalar::Null`] at the levels
374    /// it does not pivot on. Values keep their column's type - a `datetime`
375    /// split value is epoch milliseconds, not text - so formatting them is the
376    /// caller's choice.
377    ///
378    /// `window` slices the *column* axis; the number of levels does not depend
379    /// on it.
380    pub async fn column_paths(
381        &self,
382        window: ColumnWindow,
383    ) -> ClientResult<Vec<Vec<crate::config::Scalar>>> {
384        let msg = self.client_message(ClientReq::ViewColumnPathsReq(ViewColumnPathsReq {
385            start_col: window.start_col.map(|x| x as u32),
386            end_col: window.end_col.map(|x| x as u32),
387        }));
388
389        match self.client.oneshot(&msg).await? {
390            ClientResp::ViewColumnPathsResp(ViewColumnPathsResp { area }) => Ok(area
391                .into_iter()
392                .map(|level| {
393                    level
394                        .values
395                        .into_iter()
396                        .map(crate::config::Scalar::from)
397                        .collect()
398                })
399                .collect()),
400            resp => Err(resp.into()),
401        }
402    }
403
404    /// Returns this [`View`]'s _dimensions_, row and column count, as well as
405    /// those of the [`crate::Table`] from which it was derived.
406    ///
407    /// - `num_table_rows` - The number of rows in the underlying
408    ///   [`crate::Table`].
409    /// - `num_table_columns` - The number of columns in the underlying
410    ///   [`crate::Table`] (including the `index` column if this
411    ///   [`crate::Table`] was constructed with one).
412    /// - `num_view_rows` - The number of rows in this [`View`]. If this
413    ///   [`View`] has a `group_by` clause, `num_view_rows` will also include
414    ///   aggregated rows.
415    /// - `num_view_columns` - The number of columns in this [`View`]. If this
416    ///   [`View`] has a `split_by` clause, `num_view_columns` will include all
417    ///   _column paths_, e.g. the number of `columns` clause times the number
418    ///   of `split_by` groups.
419    pub async fn dimensions(&self) -> ClientResult<ViewDimensionsResp> {
420        let msg = self.client_message(ClientReq::ViewDimensionsReq(ViewDimensionsReq {}));
421        match self.client.oneshot(&msg).await? {
422            ClientResp::ViewDimensionsResp(resp) => Ok(resp),
423            resp => Err(resp.into()),
424        }
425    }
426
427    /// The expression schema of this [`View`], which contains only the
428    /// expressions created on this [`View`]. See [`View::schema`] for
429    /// details.
430    pub async fn expression_schema(&self) -> ClientResult<HashMap<String, ColumnType>> {
431        if self.client.get_features().await?.expressions {
432            let msg = self.client_message(ClientReq::ViewExpressionSchemaReq(
433                ViewExpressionSchemaReq {},
434            ));
435            match self.client.oneshot(&msg).await? {
436                ClientResp::ViewExpressionSchemaResp(ViewExpressionSchemaResp { schema }) => {
437                    Ok(schema
438                        .into_iter()
439                        .map(|(x, y)| (x, ColumnType::try_from(y).unwrap()))
440                        .collect())
441                },
442                resp => Err(resp.into()),
443            }
444        } else {
445            Ok([].into_iter().collect())
446        }
447    }
448
449    /// A copy of the [`ViewConfig`] object passed to the [`Table::view`] method
450    /// which created this [`View`].
451    pub async fn get_config(&self) -> ClientResult<crate::config::ViewConfig> {
452        let msg = self.client_message(ClientReq::ViewGetConfigReq(ViewGetConfigReq {}));
453        match self.client.oneshot(&msg).await? {
454            ClientResp::ViewGetConfigResp(ViewGetConfigResp {
455                config: Some(config),
456            }) => Ok(config.into()),
457            resp => Err(resp.into()),
458        }
459    }
460
461    /// The number of aggregated rows in this [`View`]. This is affected by the
462    /// "group_by" configuration parameter supplied to this view's contructor.
463    ///
464    /// # Returns
465    ///
466    /// The number of aggregated rows.
467    pub async fn num_rows(&self) -> ClientResult<u32> {
468        Ok(self.dimensions().await?.num_view_rows)
469    }
470
471    /// The schema of this [`View`].
472    ///
473    /// The [`View`] schema differs from the `schema` returned by
474    /// [`Table::schema`]; it may have different column names due to
475    /// `expressions` or `columns` configs, or it maye have _different
476    /// column types_ due to the application og `group_by` and `aggregates`
477    /// config. You can think of [`Table::schema`] as the _input_ schema and
478    /// [`View::schema`] as the _output_ schema of a Perspective pipeline.
479    pub async fn schema(&self) -> ClientResult<HashMap<String, ColumnType>> {
480        let msg = self.client_message(ClientReq::ViewSchemaReq(ViewSchemaReq {}));
481        match self.client.oneshot(&msg).await? {
482            ClientResp::ViewSchemaResp(ViewSchemaResp { schema }) => Ok(schema
483                .into_iter()
484                .map(|(x, y)| (x, ColumnType::try_from(y).unwrap()))
485                .collect()),
486            resp => Err(resp.into()),
487        }
488    }
489
490    /// Serializes a [`View`] to the Apache Arrow data format, with IPC body
491    /// compression per [`ViewWindow::compression`].
492    pub async fn to_arrow(&self, window: ViewWindow) -> ClientResult<Bytes> {
493        let msg = self.client_message(ClientReq::ViewToArrowReq(ViewToArrowReq {
494            viewport: Some(window.clone().into()),
495            compression: window.compression,
496        }));
497
498        match self.client.oneshot(&msg).await? {
499            ClientResp::ViewToArrowResp(ViewToArrowResp { arrow }) => Ok(arrow.into()),
500            resp => Err(resp.into()),
501        }
502    }
503
504    /// Serializes this [`View`] to a string of JSON data. Useful if you want to
505    /// save additional round trip serialize/deserialize cycles.    
506    pub async fn to_columns_string(&self, window: ViewWindow) -> ClientResult<String> {
507        let msg = self.client_message(ClientReq::ViewToColumnsStringReq(ViewToColumnsStringReq {
508            viewport: Some(window.clone().into()),
509            id: window.id,
510            index: window.index,
511            formatted: window.formatted,
512        }));
513
514        match self.client.oneshot(&msg).await? {
515            ClientResp::ViewToColumnsStringResp(ViewToColumnsStringResp { json_string }) => {
516                Ok(json_string)
517            },
518            resp => Err(resp.into()),
519        }
520    }
521
522    /// Render this `View` as a JSON string.
523    pub async fn to_json_string(&self, window: ViewWindow) -> ClientResult<String> {
524        let viewport = ViewPort::from(window.clone());
525        let msg = self.client_message(ClientReq::ViewToRowsStringReq(ViewToRowsStringReq {
526            viewport: Some(viewport),
527            id: window.id,
528            index: window.index,
529            formatted: window.formatted,
530        }));
531
532        match self.client.oneshot(&msg).await? {
533            ClientResp::ViewToRowsStringResp(ViewToRowsStringResp { json_string }) => {
534                Ok(json_string)
535            },
536            resp => Err(resp.into()),
537        }
538    }
539
540    /// Renders this [`View`] as an [NDJSON](https://github.com/ndjson/ndjson-spec)
541    /// formatted [`String`].
542    pub async fn to_ndjson(&self, window: ViewWindow) -> ClientResult<String> {
543        let viewport = ViewPort::from(window.clone());
544        let msg = self.client_message(ClientReq::ViewToNdjsonStringReq(ViewToNdjsonStringReq {
545            viewport: Some(viewport),
546            id: window.id,
547            index: window.index,
548            formatted: window.formatted,
549        }));
550
551        match self.client.oneshot(&msg).await? {
552            ClientResp::ViewToNdjsonStringResp(ViewToNdjsonStringResp { ndjson_string }) => {
553                Ok(ndjson_string)
554            },
555            resp => Err(resp.into()),
556        }
557    }
558
559    /// Serializes this [`View`] to CSV data in a standard format.
560    pub async fn to_csv(&self, window: ViewWindow) -> ClientResult<String> {
561        let msg = self.client_message(ClientReq::ViewToCsvReq(ViewToCsvReq {
562            viewport: Some(window.into()),
563        }));
564
565        match self.client.oneshot(&msg).await? {
566            ClientResp::ViewToCsvResp(ViewToCsvResp { csv }) => Ok(csv),
567            resp => Err(resp.into()),
568        }
569    }
570
571    /// Delete this [`View`] and clean up all resources associated with it.
572    /// [`View`] objects do not stop consuming resources or processing
573    /// updates when they are garbage collected - you must call this method
574    /// to reclaim these.
575    pub async fn delete(&self) -> ClientResult<()> {
576        let msg = self.client_message(ClientReq::ViewDeleteReq(ViewDeleteReq {}));
577        match self.client.oneshot(&msg).await? {
578            ClientResp::ViewDeleteResp(_) => Ok(()),
579            resp => Err(resp.into()),
580        }
581    }
582
583    /// Calculates the [min, max] of the leaf nodes of a column `column_name`.
584    ///
585    /// # Returns
586    ///
587    /// A tuple of [min, max], whose types are column and aggregate dependent.
588    pub async fn get_min_max(
589        &self,
590        column_name: String,
591    ) -> ClientResult<(crate::config::Scalar, crate::config::Scalar)> {
592        let msg = self.client_message(ClientReq::ViewGetMinMaxReq(ViewGetMinMaxReq {
593            column_name,
594        }));
595
596        match self.client.oneshot(&msg).await? {
597            ClientResp::ViewGetMinMaxResp(ViewGetMinMaxResp { min, max }) => {
598                let min = min.map(crate::config::Scalar::from).unwrap_or_default();
599                let max = max.map(crate::config::Scalar::from).unwrap_or_default();
600                Ok((min, max))
601            },
602            resp => Err(resp.into()),
603        }
604    }
605
606    /// Register a callback with this [`View`]. Whenever the view's underlying
607    /// table emits an update, this callback will be invoked with an object
608    /// containing `port_id`, indicating which port the update fired on,
609    /// `reset`, which is `true` when the step began with [`Table::clear`] or
610    /// [`Table::replace`] so `delta` is relative to an empty table, and
611    /// optionally `delta`, which is the new data that was updated for each
612    /// cell or each row.
613    ///
614    /// # Arguments
615    ///
616    /// - `on_update` - A callback function invoked on update, which receives an
617    ///   object with three keys: `port_id`, indicating which port the update
618    ///   was triggered on, `reset`, and `delta`, whose value is dependent on
619    ///   the mode parameter.
620    /// - `options` - If this is provided as `OnUpdateOptions { mode:
621    ///   Some(OnUpdateMode::Row) }`, then `delta` is an Arrow of the updated
622    ///   rows. Otherwise `delta` will be [`Option::None`].
623    pub async fn on_update<T, U>(&self, on_update: T, options: OnUpdateOptions) -> ClientResult<u32>
624    where
625        T: Fn(OnUpdateData) -> U + Send + Sync + 'static,
626        U: Future<Output = ()> + Send + 'static,
627    {
628        let on_update = Arc::new(on_update);
629        let callback = move |resp: Response| {
630            let on_update = on_update.clone();
631            async move {
632                match resp.client_resp {
633                    Some(ClientResp::ViewOnUpdateResp(resp)) => {
634                        on_update(OnUpdateData(resp)).await;
635                        Ok(())
636                    },
637                    resp => Err(resp.into()),
638                }
639            }
640        };
641
642        let msg = self.client_message(ClientReq::ViewOnUpdateReq(ViewOnUpdateReq {
643            mode: options.mode.map(|OnUpdateMode::Row| Mode::Row as i32),
644        }));
645
646        self.client.subscribe(&msg, callback).await?;
647        Ok(msg.msg_id)
648    }
649
650    /// Unregister a previously registered update callback with this [`View`].
651    ///
652    /// # Arguments
653    ///
654    /// - `id` - A callback `id` as returned by a recipricol call to
655    ///   [`View::on_update`].
656    ///
657    /// # Examples
658    ///
659    /// ```no_run
660    /// # use perspective_client::{OnUpdateOptions, View};
661    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
662    /// # let view: View = todo!();
663    /// let callback = |_| async { print!("Updated!") };
664    /// let cid = view.on_update(callback, OnUpdateOptions::default()).await?;
665    /// view.remove_update(cid).await?;
666    /// # Ok(()) }
667    /// ```
668    pub async fn remove_update(&self, update_id: u32) -> ClientResult<()> {
669        let msg = self.client_message(ClientReq::ViewRemoveOnUpdateReq(ViewRemoveOnUpdateReq {
670            id: update_id,
671        }));
672
673        self.client.unsubscribe(update_id).await?;
674        match self.client.oneshot(&msg).await? {
675            ClientResp::ViewRemoveOnUpdateResp(_) => Ok(()),
676            resp => Err(resp.into()),
677        }
678    }
679
680    /// Register a callback which is invoked whenever rows leave this [`View`],
681    /// with their keys as an Apache Arrow of one column per key column named
682    /// after it and the [`RemoveCause`] they left under.
683    ///
684    /// For a [`View`] without `group_by` the keys are the `index` columns of
685    /// the [`Table`], which must have one: [`RemoveCause::Key`] when
686    /// [`Table::remove`] removed them ([`Table::replace`] reports the keys it
687    /// does not re-supply and [`Table::clear`] every key) and
688    /// [`RemoveCause::Filter`] when [`Table::update`] moved them outside the
689    /// [`View`]'s `filter`. For a [`View`] with `group_by` the keys are the
690    /// `group_by` columns of each group which no longer has any rows,
691    /// [`RemoveCause::Group`], `null` below the group's depth. It fires once
692    /// per cause per update step.
693    ///
694    /// # Examples
695    ///
696    /// ```no_run
697    /// # use perspective_client::{View, OnRemoveData};
698    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
699    /// # let view: View = todo!();
700    /// let callback = |removed: OnRemoveData| async move { println!("{:?}", removed.port_id) };
701    /// let cid = view.on_remove(callback).await?;
702    /// view.remove_remove(cid).await?;
703    /// # Ok(()) }
704    /// ```
705    pub async fn on_remove<T, U>(&self, on_remove: T) -> ClientResult<u32>
706    where
707        T: Fn(OnRemoveData) -> U + Send + Sync + 'static,
708        U: Future<Output = ()> + Send + 'static,
709    {
710        let on_remove = Arc::new(on_remove);
711        let callback = move |resp: Response| {
712            let on_remove = on_remove.clone();
713            async move {
714                match resp.client_resp {
715                    Some(ClientResp::ViewOnRemoveResp(resp)) => {
716                        on_remove(OnRemoveData::from(resp)).await;
717                        Ok(())
718                    },
719                    resp => Err(resp.into()),
720                }
721            }
722        };
723
724        let msg = self.client_message(ClientReq::ViewOnRemoveReq(ViewOnRemoveReq {}));
725        self.client.subscribe(&msg, callback).await?;
726        Ok(msg.msg_id)
727    }
728
729    /// Unregister a previously registered [`View::on_remove`] callback.
730    ///
731    /// # Arguments
732    ///
733    /// - `callback_id` - A callback `id` as returned by a reciprocal call to
734    ///   [`View::on_remove`].
735    pub async fn remove_remove(&self, callback_id: u32) -> ClientResult<()> {
736        let msg = self.client_message(ClientReq::ViewRemoveOnRemoveReq(ViewRemoveOnRemoveReq {
737            id: callback_id,
738        }));
739
740        self.client.unsubscribe(callback_id).await?;
741        match self.client.oneshot(&msg).await? {
742            ClientResp::ViewRemoveOnRemoveResp(_) => Ok(()),
743            resp => Err(resp.into()),
744        }
745    }
746
747    /// Register a callback with this [`View`]. Whenever the [`View`] is
748    /// deleted, this callback will be invoked.
749    pub async fn on_delete(
750        &self,
751        on_delete: Box<dyn Fn() + Send + Sync + 'static>,
752    ) -> ClientResult<u32> {
753        let callback = move |resp: Response| match resp.client_resp.unwrap() {
754            ClientResp::ViewOnDeleteResp(_) => {
755                on_delete();
756                Ok(())
757            },
758            resp => Err(resp.into()),
759        };
760
761        let msg = self.client_message(ClientReq::ViewOnDeleteReq(ViewOnDeleteReq {}));
762        self.client.subscribe_once(&msg, Box::new(callback)).await?;
763        Ok(msg.msg_id)
764    }
765
766    /// Unregister a previously registered [`View::on_delete`] callback.
767    pub async fn remove_delete(&self, callback_id: u32) -> ClientResult<()> {
768        let msg = self.client_message(ClientReq::ViewRemoveDeleteReq(ViewRemoveDeleteReq {
769            id: callback_id,
770        }));
771
772        match self.client.oneshot(&msg).await? {
773            ClientResp::ViewRemoveDeleteResp(ViewRemoveDeleteResp {}) => Ok(()),
774            resp => Err(resp.into()),
775        }
776    }
777
778    /// Collapses the `group_by` row at `row_index`.
779    pub async fn collapse(&self, row_index: u32) -> ClientResult<u32> {
780        let msg = self.client_message(ClientReq::ViewCollapseReq(ViewCollapseReq { row_index }));
781        match self.client.oneshot(&msg).await? {
782            ClientResp::ViewCollapseResp(ViewCollapseResp { num_changed }) => Ok(num_changed),
783            resp => Err(resp.into()),
784        }
785    }
786
787    /// Expand the `group_by` row at `row_index`.
788    pub async fn expand(&self, row_index: u32) -> ClientResult<u32> {
789        let msg = self.client_message(ClientReq::ViewExpandReq(ViewExpandReq { row_index }));
790        match self.client.oneshot(&msg).await? {
791            ClientResp::ViewExpandResp(ViewExpandResp { num_changed }) => Ok(num_changed),
792            resp => Err(resp.into()),
793        }
794    }
795
796    /// Set expansion `depth` of the `group_by` tree.
797    pub async fn set_depth(&self, depth: u32) -> ClientResult<()> {
798        let msg = self.client_message(ClientReq::ViewSetDepthReq(ViewSetDepthReq { depth }));
799        match self.client.oneshot(&msg).await? {
800            ClientResp::ViewSetDepthResp(_) => Ok(()),
801            resp => Err(resp.into()),
802        }
803    }
804}