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    /// Only impacts [`View::to_arrow`]
109    #[ts(optional)]
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub compression: Option<String>,
112}
113
114impl From<ViewWindow> for ViewPort {
115    fn from(window: ViewWindow) -> Self {
116        ViewPort {
117            start_row: window.start_row.map(|x| x.floor() as u32),
118            start_col: window.start_col.map(|x| x.floor() as u32),
119            end_row: window.end_row.map(|x| x.ceil() as u32),
120            end_col: window.end_col.map(|x| x.ceil() as u32),
121        }
122    }
123}
124
125impl From<ViewPort> for ViewWindow {
126    fn from(window: ViewPort) -> Self {
127        ViewWindow {
128            start_row: window.start_row.map(|x| x as f64),
129            start_col: window.start_col.map(|x| x as f64),
130            end_row: window.end_row.map(|x| x as f64),
131            end_col: window.end_col.map(|x| x as f64),
132            ..ViewWindow::default()
133        }
134    }
135}
136
137/// Rows updated and port ID corresponding to an update batch, provided to the
138/// callback argument to [`View::on_update`] with the "rows" mode.
139#[derive(TS)]
140pub struct OnUpdateData(crate::proto::ViewOnUpdateResp);
141
142impl Deref for OnUpdateData {
143    type Target = crate::proto::ViewOnUpdateResp;
144
145    fn deref(&self) -> &Self::Target {
146        &self.0
147    }
148}
149
150/// The [`View`] struct is Perspective's query and serialization interface. It
151/// represents a query on the `Table`'s dataset and is always created from an
152/// existing `Table` instance via the [`Table::view`] method.
153///
154/// [`View`]s are immutable with respect to the arguments provided to the
155/// [`Table::view`] method; to change these parameters, you must create a new
156/// [`View`] on the same [`Table`]. However, each [`View`] is _live_ with
157/// respect to the [`Table`]'s data, and will (within a conflation window)
158/// update with the latest state as its parent [`Table`] updates, including
159/// incrementally recalculating all aggregates, pivots, filters, etc. [`View`]
160/// query parameters are composable, in that each parameter works independently
161/// _and_ in conjunction with each other, and there is no limit to the number of
162/// pivots, filters, etc. which can be applied.
163///
164/// To construct a [`View`], call the [`Table::view`] factory method. A
165/// [`Table`] can have as many [`View`]s associated with it as you need -
166/// Perspective conserves memory by relying on a single [`Table`] to power
167/// multiple [`View`]s concurrently.
168///
169/// # Examples
170///
171/// ```no_run
172/// # use perspective_client::{Client, TableData, TableInitOptions, UpdateData, ViewWindow};
173/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
174/// # let client: Client = todo!();
175/// let opts = TableInitOptions::default();
176/// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
177/// let table = client.table(data, opts).await?;
178///
179/// let view = table.view(None).await?;
180/// let arrow = view.to_arrow(ViewWindow::default()).await?;
181/// view.delete().await?;
182/// # Ok(()) }
183/// ```
184///
185/// ```no_run
186/// # use std::collections::HashMap;
187/// # use perspective_client::Table;
188/// # use perspective_client::config::*;
189/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
190/// # let table: Table = todo!();
191/// let view = table
192///     .view(Some(ViewConfigUpdate {
193///         columns: Some(vec![Some("Sales".into())]),
194///         aggregates: Some(HashMap::from_iter(vec![("Sales".into(), "sum".into())])),
195///         group_by: Some(vec!["Region".into(), "Country".into()]),
196///         filter: Some(vec![Filter::new("Category", "in", &[
197///             "Furniture",
198///             "Technology",
199///         ])]),
200///         ..ViewConfigUpdate::default()
201///     }))
202///     .await?;
203/// # Ok(()) }
204/// ```
205///
206///  Group By
207///
208/// ```no_run
209/// # use perspective_client::Table;
210/// # use perspective_client::config::*;
211/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
212/// # let table: Table = todo!();
213/// let view = table
214///     .view(Some(ViewConfigUpdate {
215///         group_by: Some(vec!["a".into(), "c".into()]),
216///         ..ViewConfigUpdate::default()
217///     }))
218///     .await?;
219/// # Ok(()) }
220/// ```
221///
222/// Split By
223///
224/// ```no_run
225/// # use perspective_client::Table;
226/// # use perspective_client::config::*;
227/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
228/// # let table: Table = todo!();
229/// let view = table
230///     .view(Some(ViewConfigUpdate {
231///         split_by: Some(vec!["a".into(), "c".into()]),
232///         ..ViewConfigUpdate::default()
233///     }))
234///     .await?;
235/// # Ok(()) }
236/// ```
237///
238/// In Javascript, a [`Table`] can be constructed on a [`Table::view`] instance,
239/// which will return a new [`Table`] based on the [`Table::view`]'s dataset,
240/// and all future updates that affect the [`Table::view`] will be forwarded to
241/// the new [`Table`]. This is particularly useful for implementing a
242/// [Client/Server Replicated](server.md#clientserver-replicated) design, by
243/// serializing the `View` to an arrow and setting up an `on_update` callback.
244///
245/// ```no_run
246/// # use perspective_client::{Client, TableData, TableInitOptions, UpdateData, UpdateOptions};
247/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
248/// # let client: Client = todo!();
249/// let opts = TableInitOptions::default();
250/// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
251/// let table = client.table(data, opts.clone()).await?;
252/// let view = table.view(None).await?;
253/// let table2 = client.table(TableData::View(view), opts).await?;
254/// let more = UpdateData::Csv("x,y\n5,6".into());
255/// table.update(more, UpdateOptions::default()).await?;
256/// # Ok(()) }
257/// ```
258#[derive(Clone, Debug)]
259pub struct View {
260    pub name: String,
261    client: Client,
262}
263
264assert_view_api!(View);
265
266impl View {
267    pub fn new(name: String, client: Client) -> Self {
268        View { name, client }
269    }
270
271    fn client_message(&self, req: ClientReq) -> Request {
272        crate::proto::Request {
273            msg_id: self.client.gen_id(),
274            entity_id: self.name.clone(),
275            client_req: Some(req),
276        }
277    }
278
279    /// Returns an array of strings containing the column paths of the [`View`]
280    /// without any of the source columns.
281    ///
282    /// A column path shows the columns that a given cell belongs to after
283    /// pivots are applied.
284    pub async fn column_paths(&self, window: ColumnWindow) -> ClientResult<Vec<String>> {
285        let msg = self.client_message(ClientReq::ViewColumnPathsReq(ViewColumnPathsReq {
286            start_col: window.start_col.map(|x| x as u32),
287            end_col: window.end_col.map(|x| x as u32),
288        }));
289
290        match self.client.oneshot(&msg).await? {
291            ClientResp::ViewColumnPathsResp(ViewColumnPathsResp { paths }) => {
292                // Ok(paths.into_iter().map(|x| x.path).collect())
293                Ok(paths)
294            },
295            resp => Err(resp.into()),
296        }
297    }
298
299    /// Returns this [`View`]'s _dimensions_, row and column count, as well as
300    /// those of the [`crate::Table`] from which it was derived.
301    ///
302    /// - `num_table_rows` - The number of rows in the underlying
303    ///   [`crate::Table`].
304    /// - `num_table_columns` - The number of columns in the underlying
305    ///   [`crate::Table`] (including the `index` column if this
306    ///   [`crate::Table`] was constructed with one).
307    /// - `num_view_rows` - The number of rows in this [`View`]. If this
308    ///   [`View`] has a `group_by` clause, `num_view_rows` will also include
309    ///   aggregated rows.
310    /// - `num_view_columns` - The number of columns in this [`View`]. If this
311    ///   [`View`] has a `split_by` clause, `num_view_columns` will include all
312    ///   _column paths_, e.g. the number of `columns` clause times the number
313    ///   of `split_by` groups.
314    pub async fn dimensions(&self) -> ClientResult<ViewDimensionsResp> {
315        let msg = self.client_message(ClientReq::ViewDimensionsReq(ViewDimensionsReq {}));
316        match self.client.oneshot(&msg).await? {
317            ClientResp::ViewDimensionsResp(resp) => Ok(resp),
318            resp => Err(resp.into()),
319        }
320    }
321
322    /// The expression schema of this [`View`], which contains only the
323    /// expressions created on this [`View`]. See [`View::schema`] for
324    /// details.
325    pub async fn expression_schema(&self) -> ClientResult<HashMap<String, ColumnType>> {
326        if self.client.get_features().await?.expressions {
327            let msg = self.client_message(ClientReq::ViewExpressionSchemaReq(
328                ViewExpressionSchemaReq {},
329            ));
330            match self.client.oneshot(&msg).await? {
331                ClientResp::ViewExpressionSchemaResp(ViewExpressionSchemaResp { schema }) => {
332                    Ok(schema
333                        .into_iter()
334                        .map(|(x, y)| (x, ColumnType::try_from(y).unwrap()))
335                        .collect())
336                },
337                resp => Err(resp.into()),
338            }
339        } else {
340            Ok([].into_iter().collect())
341        }
342    }
343
344    /// A copy of the [`ViewConfig`] object passed to the [`Table::view`] method
345    /// which created this [`View`].
346    pub async fn get_config(&self) -> ClientResult<crate::config::ViewConfig> {
347        let msg = self.client_message(ClientReq::ViewGetConfigReq(ViewGetConfigReq {}));
348        match self.client.oneshot(&msg).await? {
349            ClientResp::ViewGetConfigResp(ViewGetConfigResp {
350                config: Some(config),
351            }) => Ok(config.into()),
352            resp => Err(resp.into()),
353        }
354    }
355
356    /// The number of aggregated rows in this [`View`]. This is affected by the
357    /// "group_by" configuration parameter supplied to this view's contructor.
358    ///
359    /// # Returns
360    ///
361    /// The number of aggregated rows.
362    pub async fn num_rows(&self) -> ClientResult<u32> {
363        Ok(self.dimensions().await?.num_view_rows)
364    }
365
366    /// The schema of this [`View`].
367    ///
368    /// The [`View`] schema differs from the `schema` returned by
369    /// [`Table::schema`]; it may have different column names due to
370    /// `expressions` or `columns` configs, or it maye have _different
371    /// column types_ due to the application og `group_by` and `aggregates`
372    /// config. You can think of [`Table::schema`] as the _input_ schema and
373    /// [`View::schema`] as the _output_ schema of a Perspective pipeline.
374    pub async fn schema(&self) -> ClientResult<HashMap<String, ColumnType>> {
375        let msg = self.client_message(ClientReq::ViewSchemaReq(ViewSchemaReq {}));
376        match self.client.oneshot(&msg).await? {
377            ClientResp::ViewSchemaResp(ViewSchemaResp { schema }) => Ok(schema
378                .into_iter()
379                .map(|(x, y)| (x, ColumnType::try_from(y).unwrap()))
380                .collect()),
381            resp => Err(resp.into()),
382        }
383    }
384
385    /// Serializes a [`View`] to the Apache Arrow data format.
386    pub async fn to_arrow(&self, window: ViewWindow) -> ClientResult<Bytes> {
387        let msg = self.client_message(ClientReq::ViewToArrowReq(ViewToArrowReq {
388            viewport: Some(window.clone().into()),
389            compression: window.compression,
390        }));
391
392        match self.client.oneshot(&msg).await? {
393            ClientResp::ViewToArrowResp(ViewToArrowResp { arrow }) => Ok(arrow.into()),
394            resp => Err(resp.into()),
395        }
396    }
397
398    /// Serializes this [`View`] to a string of JSON data. Useful if you want to
399    /// save additional round trip serialize/deserialize cycles.    
400    pub async fn to_columns_string(&self, window: ViewWindow) -> ClientResult<String> {
401        let msg = self.client_message(ClientReq::ViewToColumnsStringReq(ViewToColumnsStringReq {
402            viewport: Some(window.clone().into()),
403            id: window.id,
404            index: window.index,
405            formatted: window.formatted,
406        }));
407
408        match self.client.oneshot(&msg).await? {
409            ClientResp::ViewToColumnsStringResp(ViewToColumnsStringResp { json_string }) => {
410                Ok(json_string)
411            },
412            resp => Err(resp.into()),
413        }
414    }
415
416    /// Render this `View` as a JSON string.
417    pub async fn to_json_string(&self, window: ViewWindow) -> ClientResult<String> {
418        let viewport = ViewPort::from(window.clone());
419        let msg = self.client_message(ClientReq::ViewToRowsStringReq(ViewToRowsStringReq {
420            viewport: Some(viewport),
421            id: window.id,
422            index: window.index,
423            formatted: window.formatted,
424        }));
425
426        match self.client.oneshot(&msg).await? {
427            ClientResp::ViewToRowsStringResp(ViewToRowsStringResp { json_string }) => {
428                Ok(json_string)
429            },
430            resp => Err(resp.into()),
431        }
432    }
433
434    /// Renders this [`View`] as an [NDJSON](https://github.com/ndjson/ndjson-spec)
435    /// formatted [`String`].
436    pub async fn to_ndjson(&self, window: ViewWindow) -> ClientResult<String> {
437        let viewport = ViewPort::from(window.clone());
438        let msg = self.client_message(ClientReq::ViewToNdjsonStringReq(ViewToNdjsonStringReq {
439            viewport: Some(viewport),
440            id: window.id,
441            index: window.index,
442            formatted: window.formatted,
443        }));
444
445        match self.client.oneshot(&msg).await? {
446            ClientResp::ViewToNdjsonStringResp(ViewToNdjsonStringResp { ndjson_string }) => {
447                Ok(ndjson_string)
448            },
449            resp => Err(resp.into()),
450        }
451    }
452
453    /// Serializes this [`View`] to CSV data in a standard format.
454    pub async fn to_csv(&self, window: ViewWindow) -> ClientResult<String> {
455        let msg = self.client_message(ClientReq::ViewToCsvReq(ViewToCsvReq {
456            viewport: Some(window.into()),
457        }));
458
459        match self.client.oneshot(&msg).await? {
460            ClientResp::ViewToCsvResp(ViewToCsvResp { csv }) => Ok(csv),
461            resp => Err(resp.into()),
462        }
463    }
464
465    /// Delete this [`View`] and clean up all resources associated with it.
466    /// [`View`] objects do not stop consuming resources or processing
467    /// updates when they are garbage collected - you must call this method
468    /// to reclaim these.
469    pub async fn delete(&self) -> ClientResult<()> {
470        let msg = self.client_message(ClientReq::ViewDeleteReq(ViewDeleteReq {}));
471        match self.client.oneshot(&msg).await? {
472            ClientResp::ViewDeleteResp(_) => Ok(()),
473            resp => Err(resp.into()),
474        }
475    }
476
477    /// Calculates the [min, max] of the leaf nodes of a column `column_name`.
478    ///
479    /// # Returns
480    ///
481    /// A tuple of [min, max], whose types are column and aggregate dependent.
482    pub async fn get_min_max(
483        &self,
484        column_name: String,
485    ) -> ClientResult<(crate::config::Scalar, crate::config::Scalar)> {
486        let msg = self.client_message(ClientReq::ViewGetMinMaxReq(ViewGetMinMaxReq {
487            column_name,
488        }));
489
490        match self.client.oneshot(&msg).await? {
491            ClientResp::ViewGetMinMaxResp(ViewGetMinMaxResp { min, max }) => {
492                let min = min.map(crate::config::Scalar::from).unwrap_or_default();
493                let max = max.map(crate::config::Scalar::from).unwrap_or_default();
494                Ok((min, max))
495            },
496            resp => Err(resp.into()),
497        }
498    }
499
500    /// Register a callback with this [`View`]. Whenever the view's underlying
501    /// table emits an update, this callback will be invoked with an object
502    /// containing `port_id`, indicating which port the update fired on, and
503    /// optionally `delta`, which is the new data that was updated for each
504    /// cell or each row.
505    ///
506    /// # Arguments
507    ///
508    /// - `on_update` - A callback function invoked on update, which receives an
509    ///   object with two keys: `port_id`, indicating which port the update was
510    ///   triggered on, and `delta`, whose value is dependent on the mode
511    ///   parameter.
512    /// - `options` - If this is provided as `OnUpdateOptions { mode:
513    ///   Some(OnUpdateMode::Row) }`, then `delta` is an Arrow of the updated
514    ///   rows. Otherwise `delta` will be [`Option::None`].
515    pub async fn on_update<T, U>(&self, on_update: T, options: OnUpdateOptions) -> ClientResult<u32>
516    where
517        T: Fn(OnUpdateData) -> U + Send + Sync + 'static,
518        U: Future<Output = ()> + Send + 'static,
519    {
520        let on_update = Arc::new(on_update);
521        let callback = move |resp: Response| {
522            let on_update = on_update.clone();
523            async move {
524                match resp.client_resp {
525                    Some(ClientResp::ViewOnUpdateResp(resp)) => {
526                        on_update(OnUpdateData(resp)).await;
527                        Ok(())
528                    },
529                    resp => Err(resp.into()),
530                }
531            }
532        };
533
534        let msg = self.client_message(ClientReq::ViewOnUpdateReq(ViewOnUpdateReq {
535            mode: options.mode.map(|OnUpdateMode::Row| Mode::Row as i32),
536        }));
537
538        self.client.subscribe(&msg, callback).await?;
539        Ok(msg.msg_id)
540    }
541
542    /// Unregister a previously registered update callback with this [`View`].
543    ///
544    /// # Arguments
545    ///
546    /// - `id` - A callback `id` as returned by a recipricol call to
547    ///   [`View::on_update`].
548    ///
549    /// # Examples
550    ///
551    /// ```no_run
552    /// # use perspective_client::{OnUpdateOptions, View};
553    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
554    /// # let view: View = todo!();
555    /// let callback = |_| async { print!("Updated!") };
556    /// let cid = view.on_update(callback, OnUpdateOptions::default()).await?;
557    /// view.remove_update(cid).await?;
558    /// # Ok(()) }
559    /// ```
560    pub async fn remove_update(&self, update_id: u32) -> ClientResult<()> {
561        let msg = self.client_message(ClientReq::ViewRemoveOnUpdateReq(ViewRemoveOnUpdateReq {
562            id: update_id,
563        }));
564
565        self.client.unsubscribe(update_id).await?;
566        match self.client.oneshot(&msg).await? {
567            ClientResp::ViewRemoveOnUpdateResp(_) => Ok(()),
568            resp => Err(resp.into()),
569        }
570    }
571
572    /// Register a callback with this [`View`]. Whenever the [`View`] is
573    /// deleted, this callback will be invoked.
574    pub async fn on_delete(
575        &self,
576        on_delete: Box<dyn Fn() + Send + Sync + 'static>,
577    ) -> ClientResult<u32> {
578        let callback = move |resp: Response| match resp.client_resp.unwrap() {
579            ClientResp::ViewOnDeleteResp(_) => {
580                on_delete();
581                Ok(())
582            },
583            resp => Err(resp.into()),
584        };
585
586        let msg = self.client_message(ClientReq::ViewOnDeleteReq(ViewOnDeleteReq {}));
587        self.client.subscribe_once(&msg, Box::new(callback)).await?;
588        Ok(msg.msg_id)
589    }
590
591    /// Unregister a previously registered [`View::on_delete`] callback.
592    pub async fn remove_delete(&self, callback_id: u32) -> ClientResult<()> {
593        let msg = self.client_message(ClientReq::ViewRemoveDeleteReq(ViewRemoveDeleteReq {
594            id: callback_id,
595        }));
596
597        match self.client.oneshot(&msg).await? {
598            ClientResp::ViewRemoveDeleteResp(ViewRemoveDeleteResp {}) => Ok(()),
599            resp => Err(resp.into()),
600        }
601    }
602
603    /// Collapses the `group_by` row at `row_index`.
604    pub async fn collapse(&self, row_index: u32) -> ClientResult<u32> {
605        let msg = self.client_message(ClientReq::ViewCollapseReq(ViewCollapseReq { row_index }));
606        match self.client.oneshot(&msg).await? {
607            ClientResp::ViewCollapseResp(ViewCollapseResp { num_changed }) => Ok(num_changed),
608            resp => Err(resp.into()),
609        }
610    }
611
612    /// Expand the `group_by` row at `row_index`.
613    pub async fn expand(&self, row_index: u32) -> ClientResult<u32> {
614        let msg = self.client_message(ClientReq::ViewExpandReq(ViewExpandReq { row_index }));
615        match self.client.oneshot(&msg).await? {
616            ClientResp::ViewExpandResp(ViewExpandResp { num_changed }) => Ok(num_changed),
617            resp => Err(resp.into()),
618        }
619    }
620
621    /// Set expansion `depth` of the `group_by` tree.
622    pub async fn set_depth(&self, depth: u32) -> ClientResult<()> {
623        let msg = self.client_message(ClientReq::ViewSetDepthReq(ViewSetDepthReq { depth }));
624        match self.client.oneshot(&msg).await? {
625            ClientResp::ViewSetDepthResp(_) => Ok(()),
626            resp => Err(resp.into()),
627        }
628    }
629}