Skip to main content

perspective_client/
table.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::fmt::Display;
15
16use serde::{Deserialize, Serialize};
17use ts_rs::TS;
18
19use crate::assert_table_api;
20use crate::client::{Client, Features};
21use crate::config::{Expressions, ViewConfigUpdate};
22use crate::proto::make_table_req::MakeTableOptions;
23use crate::proto::make_table_req::make_table_options::MakeTableType;
24use crate::proto::request::ClientReq;
25use crate::proto::response::ClientResp;
26use crate::proto::*;
27use crate::table_data::UpdateData;
28use crate::utils::*;
29use crate::view::{View, ViewSource};
30
31pub type Schema = HashMap<String, ColumnType>;
32
33/// The format to interpret data preovided to [`Client::table`].
34///
35/// When serialized, these values are `"csv"`, `"json"`, `"columns"`, `"arrow"`
36/// and `"ndjson"`.
37#[derive(Clone, Copy, Debug, Serialize, Deserialize, TS)]
38pub enum TableReadFormat {
39    #[serde(rename = "csv")]
40    Csv,
41
42    #[serde(rename = "json")]
43    JsonString,
44
45    #[serde(rename = "columns")]
46    ColumnsString,
47
48    #[serde(rename = "arrow")]
49    Arrow,
50
51    #[serde(rename = "ndjson")]
52    Ndjson,
53}
54
55impl TableReadFormat {
56    pub fn parse(value: Option<String>) -> Result<Option<Self>, String> {
57        Ok(match value.as_deref() {
58            Some("csv") => Some(TableReadFormat::Csv),
59            Some("json") => Some(TableReadFormat::JsonString),
60            Some("columns") => Some(TableReadFormat::ColumnsString),
61            Some("arrow") => Some(TableReadFormat::Arrow),
62            Some("ndjson") => Some(TableReadFormat::Ndjson),
63            None => None,
64            Some(x) => return Err(format!("Unknown format \"{x}\"")),
65        })
66    }
67}
68
69/// Options which impact the behavior of [`Client::table`], as well as
70/// subsequent calls to [`Table::update`].
71#[derive(Clone, Debug, Default, Serialize, Deserialize, TS)]
72pub struct TableInitOptions {
73    #[serde(default)]
74    #[ts(optional)]
75    pub name: Option<String>,
76
77    #[serde(default)]
78    #[ts(optional)]
79    pub format: Option<TableReadFormat>,
80
81    /// This [`Table`] should use the column named by the `index` parameter as
82    /// the `index`, which causes [`Table::update`] and [`Client::table`] input
83    /// to either insert or update existing rows based on `index` column
84    /// value equality.
85    #[serde(default)]
86    #[ts(optional)]
87    pub index: Option<String>,
88
89    /// This [`Table`] should be limited to `limit` rows, after which the
90    /// _earliest_ rows will be overwritten (where _earliest_ is defined as
91    /// relative to insertion order).
92    #[serde(default)]
93    #[ts(optional)]
94    pub limit: Option<u32>,
95
96    /// Back this [`Table`]'s canonical data with the on-disk storage backend
97    /// instead of memory. On native targets this is a memory-mapped file; on
98    /// WASM it is OPFS (Worker only). Defaults to in-memory.
99    #[serde(default)]
100    #[ts(optional)]
101    pub page_to_disk: Option<bool>,
102
103    /// How Arrow `LIST` and JSON `Array` columns are ingested. `zip` (the
104    /// default) and `cartesian` expand a row into one row per list element,
105    /// and are incompatible with `index`, as the rows of an expansion
106    /// repeat their index. `stringify` encodes each list as a JSON array in
107    /// a single string column instead.
108    #[serde(default)]
109    #[ts(optional)]
110    pub list_flatten: Option<crate::proto::ListFlatten>,
111}
112
113impl TableInitOptions {
114    pub fn set_name<D: Display>(&mut self, name: D) {
115        self.name = Some(format!("{name}"))
116    }
117}
118
119impl TryFrom<TableOptions> for MakeTableOptions {
120    type Error = ClientError;
121
122    fn try_from(value: TableOptions) -> Result<Self, Self::Error> {
123        let page_to_disk = value.page_to_disk;
124        let list_flatten = value.list_flatten.map(|x| x as i32);
125        Ok(MakeTableOptions {
126            page_to_disk,
127            list_flatten,
128            make_table_type: match value {
129                TableOptions {
130                    index: Some(_),
131                    limit: Some(_),
132                    ..
133                } => Err(ClientError::BadTableOptions)?,
134                TableOptions {
135                    index: Some(index), ..
136                } => Some(MakeTableType::MakeIndexTable(index)),
137                TableOptions {
138                    limit: Some(limit), ..
139                } => Some(MakeTableType::MakeLimitTable(limit)),
140                _ => None,
141            },
142        })
143    }
144}
145
146#[derive(Clone, Debug)]
147pub(crate) struct TableOptions {
148    pub index: Option<String>,
149    pub limit: Option<u32>,
150    pub page_to_disk: Option<bool>,
151    pub list_flatten: Option<crate::proto::ListFlatten>,
152}
153
154/// The source [`View`] of a replica [`Table`] built by [`Client::table`],
155/// with the subscription tokens to release when the replica is deleted.
156#[derive(Clone)]
157pub(crate) struct ViewBinding {
158    pub view: View,
159    pub update_token: u32,
160    pub remove_token: Option<u32>,
161}
162
163impl From<TableInitOptions> for TableOptions {
164    fn from(value: TableInitOptions) -> Self {
165        TableOptions {
166            index: value.index,
167            limit: value.limit,
168            page_to_disk: value.page_to_disk,
169            list_flatten: value.list_flatten,
170        }
171    }
172}
173
174/// Options for [`Client::join`].
175#[derive(Clone, Debug, Default, Serialize, Deserialize, TS)]
176pub struct JoinOptions {
177    #[serde(default)]
178    #[ts(optional)]
179    pub join_type: Option<crate::proto::JoinType>,
180
181    #[serde(default)]
182    #[ts(optional)]
183    pub name: Option<String>,
184
185    #[serde(default)]
186    #[ts(optional)]
187    pub right_on: Option<String>,
188}
189
190/// Options for [`Table::delete`].
191#[derive(Clone, Debug, Default, Deserialize, TS)]
192pub struct DeleteOptions {
193    pub lazy: bool,
194}
195
196/// Options for [`Table::update`].
197#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
198pub struct UpdateOptions {
199    pub port_id: Option<u32>,
200    pub format: Option<TableReadFormat>,
201}
202
203/// Result of a call to [`Table::validate_expressions`], containing a schema
204/// for valid expressions and error messages for invalid ones.
205#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
206pub struct ExprValidationResult {
207    pub expression_schema: Schema,
208    pub errors: HashMap<String, table_validate_expr_resp::ExprValidationError>,
209    pub expression_alias: HashMap<String, String>,
210}
211
212/// [`Table`] is Perspective's columnar data frame, analogous to a Pandas/Polars
213/// `DataFrame` or Apache Arrow, supporting append & in-place updates, removal
214/// by index, and update notifications.
215///
216/// A [`Table`] contains columns, each of which have a unique name, are strongly
217/// and consistently typed, and contains rows of data conforming to the column's
218/// type. Each column in a [`Table`] must have the same number of rows, though
219/// not every row must contain data; null-values are used to indicate missing
220/// values in the dataset. The schema of a [`Table`] is _immutable after
221/// creation_, which means the column names and data types cannot be changed
222/// after the [`Table`] has been created. Columns cannot be added or deleted
223/// after creation either, but a [`View`] can be used to select an arbitrary set
224/// of columns from the [`Table`].
225#[derive(Clone)]
226pub struct Table {
227    name: String,
228    client: Client,
229    options: TableOptions,
230    pub(crate) view_binding: Option<ViewBinding>,
231}
232
233assert_table_api!(Table);
234
235impl PartialEq for Table {
236    fn eq(&self, other: &Self) -> bool {
237        self.name == other.name && self.client == other.client
238    }
239}
240
241impl Table {
242    pub(crate) fn new(name: String, client: Client, options: TableOptions) -> Self {
243        Table {
244            name,
245            client,
246            options,
247            view_binding: None,
248        }
249    }
250
251    fn client_message(&self, req: ClientReq) -> Request {
252        Request {
253            msg_id: self.client.gen_id(),
254            entity_id: self.name.clone(),
255            client_req: Some(req),
256        }
257    }
258
259    /// Get a copy of the [`Client`] this [`Table`] came from.
260    pub fn get_client(&self) -> Client {
261        self.client.clone()
262    }
263
264    /// Get a metadata dictionary of the `perspective_server::Server`'s
265    /// features, which is (currently) implementation specific, but there is
266    /// only one implementation.
267    pub async fn get_features(&self) -> ClientResult<Features> {
268        self.client.get_features().await
269    }
270
271    /// Returns the name of the index column for the table.
272    ///
273    /// # Examples
274    ///
275    /// ```no_run
276    /// # use perspective_client::{Client, TableData, TableInitOptions, UpdateData};
277    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
278    /// # let client: Client = todo!();
279    /// let options = TableInitOptions {
280    ///     index: Some("x".to_string()),
281    ///     ..TableInitOptions::default()
282    /// };
283    /// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
284    /// let table = client.table(data, options).await?;
285    /// let index = table.get_index();
286    /// # Ok(()) }
287    /// ```
288    pub fn get_index(&self) -> Option<String> {
289        self.options.index.as_ref().map(|index| index.to_owned())
290    }
291
292    /// Returns the user-specified row limit for this table.
293    pub fn get_limit(&self) -> Option<u32> {
294        self.options.limit.as_ref().map(|limit| *limit)
295    }
296
297    /// Returns the user-specified name for this table, or the auto-generated
298    /// name if a name was not specified when the table was created.
299    pub fn get_name(&self) -> &str {
300        self.name.as_str()
301    }
302
303    /// Removes all the rows in the [`Table`], but preserves everything else
304    /// including the schema, index, and any callbacks or registered
305    /// [`View`] instances.
306    ///
307    /// Calling [`Table::clear`], like [`Table::update`] and [`Table::remove`],
308    /// will trigger an update event to any registered listeners via
309    /// [`View::on_update`].
310    pub async fn clear(&self) -> ClientResult<()> {
311        self.replace(UpdateData::JsonRows("[]".to_owned())).await
312    }
313
314    /// Delete this [`Table`] and cleans up associated resources.
315    ///
316    /// [`Table`]s do not stop consuming resources or processing updates when
317    /// they are garbage collected in their host language - you must call
318    /// this method to reclaim these.
319    ///
320    /// # Arguments
321    ///
322    /// - `options` An options dictionary.
323    ///     - `lazy` Whether to delete this [`Table`] _lazily_. When false (the
324    ///       default), the delete will occur immediately, assuming it has no
325    ///       [`View`] instances registered to it (which must be deleted first,
326    ///       otherwise this method will throw an error). When true, the
327    ///       [`Table`] will only be marked for deltion once its [`View`]
328    ///       dependency count reaches 0.
329    ///
330    /// # Examples
331    ///
332    /// ```no_run
333    /// # use perspective_client::{Client, DeleteOptions, TableData, TableInitOptions, UpdateData};
334    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
335    /// # let client: Client = todo!();
336    /// let opts = TableInitOptions::default();
337    /// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
338    /// let table = client.table(data, opts).await?;
339    ///
340    /// // ...
341    ///
342    /// table.delete(DeleteOptions::default()).await?;
343    /// # Ok(()) }
344    /// ```
345    pub async fn delete(&self, options: DeleteOptions) -> ClientResult<()> {
346        if let Some(binding) = &self.view_binding {
347            binding.view.remove_update(binding.update_token).await?;
348            if let Some(token) = binding.remove_token {
349                binding.view.remove_remove(token).await?;
350            }
351        }
352
353        let msg = self.client_message(ClientReq::TableDeleteReq(TableDeleteReq {
354            is_immediate: !options.lazy,
355        }));
356
357        match self.client.oneshot(&msg).await? {
358            ClientResp::TableDeleteResp(_) => Ok(()),
359            resp => Err(resp.into()),
360        }
361    }
362
363    /// Returns the column names of this [`Table`] in "natural" order (the
364    /// ordering implied by the input format).
365    ///  
366    /// # Examples
367    ///
368    /// ```no_run
369    /// # use perspective_client::Table;
370    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
371    /// # let table: Table = todo!();
372    /// let columns = table.columns().await?;
373    /// # Ok(()) }
374    /// ```
375    pub async fn columns(&self) -> ClientResult<Vec<String>> {
376        let msg = self.client_message(ClientReq::TableSchemaReq(TableSchemaReq {}));
377        match self.client.oneshot(&msg).await? {
378            ClientResp::TableSchemaResp(TableSchemaResp { schema }) => Ok(schema
379                .map(|x| x.schema.into_iter().map(|x| x.name.to_owned()).collect())
380                .unwrap()),
381            resp => Err(resp.into()),
382        }
383    }
384
385    /// Returns the number of rows in a [`Table`].
386    pub async fn size(&self) -> ClientResult<usize> {
387        let msg = self.client_message(ClientReq::TableSizeReq(TableSizeReq {}));
388        match self.client.oneshot(&msg).await? {
389            ClientResp::TableSizeResp(TableSizeResp { size }) => Ok(size as usize),
390            resp => Err(resp.into()),
391        }
392    }
393
394    /// Returns a table's [`Schema`], a mapping of column names to column types.
395    ///
396    /// The mapping of a [`Table`]'s column names to data types is referred to
397    /// as a [`Schema`]. Each column has a unique name and a data type, one
398    /// of:
399    ///
400    /// - `"boolean"` - A boolean type
401    /// - `"date"` - A timesonze-agnostic date type (month/day/year)
402    /// - `"datetime"` - A millisecond-precision datetime type in the UTC
403    ///   timezone
404    /// - `"float"` - A 64 bit float
405    /// - `"integer"` - A signed 32 bit integer (the integer type supported by
406    ///   JavaScript)
407    /// - `"string"` - A [`String`] data type (encoded internally as a
408    ///   _dictionary_)
409    ///
410    /// Note that all [`Table`] columns are _nullable_, regardless of the data
411    /// type.
412    pub async fn schema(&self) -> ClientResult<Schema> {
413        let msg = self.client_message(ClientReq::TableSchemaReq(TableSchemaReq {}));
414        match self.client.oneshot(&msg).await? {
415            ClientResp::TableSchemaResp(TableSchemaResp { schema }) => Ok(schema
416                .map(|x| {
417                    x.schema
418                        .into_iter()
419                        .map(|x| (x.name, ColumnType::try_from(x.r#type).unwrap()))
420                        .collect()
421                })
422                .unwrap()),
423            resp => Err(resp.into()),
424        }
425    }
426
427    /// Create a unique channel ID on this [`Table`], which allows
428    /// `View::on_update` callback calls to be associated with the
429    /// `Table::update` which caused them.
430    pub async fn make_port(&self) -> ClientResult<i32> {
431        let msg = self.client_message(ClientReq::TableMakePortReq(TableMakePortReq {}));
432        match self.client.oneshot(&msg).await? {
433            ClientResp::TableMakePortResp(TableMakePortResp { port_id }) => Ok(port_id as i32),
434            _ => Err(ClientError::Unknown("make_port".to_string())),
435        }
436    }
437
438    /// Register a callback which is called exactly once, when this [`Table`] is
439    /// deleted with the [`Table::delete`] method.
440    ///
441    /// [`Table::on_delete`] resolves when the subscription message is sent, not
442    /// when the _delete_ event occurs.
443    pub async fn on_delete(
444        &self,
445        on_delete: Box<dyn Fn() + Send + Sync + 'static>,
446    ) -> ClientResult<u32> {
447        let callback = move |resp: Response| match resp.client_resp {
448            Some(ClientResp::TableOnDeleteResp(_)) => {
449                on_delete();
450                Ok(())
451            },
452            resp => Err(resp.into()),
453        };
454
455        let msg = self.client_message(ClientReq::TableOnDeleteReq(TableOnDeleteReq {}));
456        self.client.subscribe_once(&msg, Box::new(callback)).await?;
457        Ok(msg.msg_id)
458    }
459
460    /// Removes a listener with a given ID, as returned by a previous call to
461    /// [`Table::on_delete`].
462    pub async fn remove_delete(&self, callback_id: u32) -> ClientResult<()> {
463        let msg = self.client_message(ClientReq::TableRemoveDeleteReq(TableRemoveDeleteReq {
464            id: callback_id,
465        }));
466
467        match self.client.oneshot(&msg).await? {
468            ClientResp::TableRemoveDeleteResp(_) => Ok(()),
469            resp => Err(resp.into()),
470        }
471    }
472
473    /// Removes rows from this [`Table`] with the `index` column values
474    /// supplied.
475    ///
476    /// # Arguments
477    ///
478    /// - `indices` - A list of `index` column values for rows that should be
479    ///   removed.
480    ///
481    /// # Examples
482    ///
483    /// ```no_run
484    /// # use perspective_client::{Table, UpdateData};
485    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
486    /// # let table: Table = todo!();
487    /// table
488    ///     .remove(UpdateData::Csv("index\n1\n2\n3".into()))
489    ///     .await?;
490    /// # Ok(()) }
491    /// ```
492    pub async fn remove(&self, input: UpdateData) -> ClientResult<()> {
493        let msg = self.client_message(ClientReq::TableRemoveReq(TableRemoveReq {
494            data: Some(input.into()),
495        }));
496
497        match self.client.oneshot(&msg).await? {
498            ClientResp::TableRemoveResp(_) => Ok(()),
499            resp => Err(resp.into()),
500        }
501    }
502
503    /// Replace all rows in this [`Table`] with the input data, coerced to this
504    /// [`Table`]'s existing [`Schema`], notifying any derived [`View`] and
505    /// [`View::on_update`] callbacks.
506    ///
507    /// Calling [`Table::replace`] is an easy way to replace _all_ the data in a
508    /// [`Table`] without losing any derived [`View`] instances or
509    /// [`View::on_update`] callbacks. [`Table::replace`] does _not_ infer
510    /// data types like [`Client::table`] does, rather it _coerces_ input
511    /// data to the `Schema` like [`Table::update`]. If you need a [`Table`]
512    /// with a different `Schema`, you must create a new one.
513    ///
514    /// # Examples
515    ///
516    /// ```no_run
517    /// # use perspective_client::{Table, UpdateData};
518    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
519    /// # let table: Table = todo!();
520    /// let data = UpdateData::Csv("x,y\n1,2".into());
521    /// table.replace(data).await?;
522    /// # Ok(()) }
523    /// ```
524    pub async fn replace(&self, input: UpdateData) -> ClientResult<()> {
525        let msg = self.client_message(ClientReq::TableReplaceReq(TableReplaceReq {
526            data: Some(input.into()),
527        }));
528
529        match self.client.oneshot(&msg).await? {
530            ClientResp::TableReplaceResp(_) => Ok(()),
531            resp => Err(resp.into()),
532        }
533    }
534
535    /// Updates the rows of this table and any derived [`View`] instances.
536    ///
537    /// Calling [`Table::update`] will trigger the [`View::on_update`] callbacks
538    /// register to derived [`View`], and the call itself will not resolve until
539    /// _all_ derived [`View`]'s are notified.
540    ///
541    /// When updating a [`Table`] with an `index`, [`Table::update`] supports
542    /// partial updates, by omitting columns from the update data.
543    ///
544    /// # Arguments
545    ///
546    /// - `input` - The input data for this [`Table`]. The schema of a [`Table`]
547    ///   is immutable after creation, so this method cannot be called with a
548    ///   schema.
549    /// - `options` - Options for this update step - see [`UpdateOptions`].
550    ///
551    /// # Examples
552    ///
553    /// ```no_run
554    /// # use perspective_client::{Table, UpdateData, UpdateOptions};
555    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
556    /// # let table: Table = todo!();
557    /// let data = UpdateData::Csv("x,y\n1,2".into());
558    /// let opts = UpdateOptions::default();
559    /// table.update(data, opts).await?;
560    /// # Ok(()) }
561    /// ```
562    pub async fn update(&self, input: UpdateData, options: UpdateOptions) -> ClientResult<()> {
563        let msg = self.client_message(ClientReq::TableUpdateReq(TableUpdateReq {
564            data: Some(input.into()),
565            port_id: options.port_id.unwrap_or(0),
566        }));
567
568        match self.client.oneshot(&msg).await? {
569            ClientResp::TableUpdateResp(_) => Ok(()),
570            resp => Err(resp.into()),
571        }
572    }
573
574    /// Validates the given expressions.
575    pub async fn validate_expressions(
576        &self,
577        expressions: Expressions,
578    ) -> ClientResult<ExprValidationResult> {
579        let msg = self.client_message(ClientReq::TableValidateExprReq(TableValidateExprReq {
580            column_to_expr: expressions.0,
581        }));
582
583        match self.client.oneshot(&msg).await? {
584            ClientResp::TableValidateExprResp(result) => Ok(ExprValidationResult {
585                errors: result.errors,
586                expression_alias: result.expression_alias,
587                expression_schema: result
588                    .expression_schema
589                    .into_iter()
590                    .map(|(x, y)| (x, ColumnType::try_from(y).unwrap()))
591                    .collect(),
592            }),
593            resp => Err(resp.into()),
594        }
595    }
596
597    /// Create a new [`View`] from this table with a specified
598    /// [`ViewConfigUpdate`].
599    ///
600    /// See [`View`] struct.
601    ///
602    /// # Examples
603    ///
604    /// ```no_run
605    /// # use std::collections::HashMap;
606    /// # use perspective_client::Table;
607    /// # use perspective_client::config::*;
608    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
609    /// # let table: Table = todo!();
610    /// let view = table
611    ///     .view(Some(ViewConfigUpdate {
612    ///         columns: Some(vec![Some("Sales".into())]),
613    ///         aggregates: Some(HashMap::from_iter(vec![("Sales".into(), "sum".into())])),
614    ///         group_by: Some(vec!["Region".into(), "Country".into()]),
615    ///         filter: Some(vec![Filter::new("Category", "in", &[
616    ///             "Furniture",
617    ///             "Technology",
618    ///         ])]),
619    ///         ..ViewConfigUpdate::default()
620    ///     }))
621    ///     .await?;
622    /// # Ok(()) }
623    /// ```
624    pub async fn view(&self, config: Option<ViewConfigUpdate>) -> ClientResult<View> {
625        let view_name = randid();
626        let msg = Request {
627            msg_id: self.client.gen_id(),
628            entity_id: self.name.clone(),
629            client_req: ClientReq::TableMakeViewReq(TableMakeViewReq {
630                view_id: view_name.clone(),
631                config: config.map(|x| x.into()),
632            })
633            .into(),
634        };
635
636        match self.client.oneshot(&msg).await? {
637            ClientResp::TableMakeViewResp(TableMakeViewResp { view_id })
638                if view_id == view_name =>
639            {
640                Ok(View::new_with_source(
641                    view_name,
642                    self.client.clone(),
643                    ViewSource {
644                        options: self.options.clone(),
645                    },
646                ))
647            },
648            resp => Err(resp.into()),
649        }
650    }
651}