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 indexmap::IndexMap;
17use serde::{Deserialize, Serialize};
18use ts_rs::TS;
19
20use crate::assert_table_api;
21use crate::client::{Client, Features};
22use crate::config::{Expressions, ViewConfigUpdate};
23use crate::proto::make_table_req::MakeTableOptions;
24use crate::proto::make_table_req::make_table_options::MakeTableType;
25use crate::proto::request::ClientReq;
26use crate::proto::response::ClientResp;
27use crate::proto::*;
28use crate::table_data::UpdateData;
29use crate::utils::*;
30use crate::view::{View, ViewSource};
31
32pub type Schema = HashMap<String, ColumnType>;
33
34/// The format to interpret data preovided to [`Client::table`].
35///
36/// When serialized, these values are `"csv"`, `"json"`, `"columns"`, `"arrow"`
37/// and `"ndjson"`.
38#[derive(Clone, Copy, Debug, Serialize, Deserialize, TS)]
39pub enum TableReadFormat {
40    #[serde(rename = "csv")]
41    Csv,
42
43    #[serde(rename = "json")]
44    JsonString,
45
46    #[serde(rename = "columns")]
47    ColumnsString,
48
49    #[serde(rename = "arrow")]
50    Arrow,
51
52    #[serde(rename = "ndjson")]
53    Ndjson,
54}
55
56impl TableReadFormat {
57    pub fn parse(value: Option<String>) -> Result<Option<Self>, String> {
58        Ok(match value.as_deref() {
59            Some("csv") => Some(TableReadFormat::Csv),
60            Some("json") => Some(TableReadFormat::JsonString),
61            Some("columns") => Some(TableReadFormat::ColumnsString),
62            Some("arrow") => Some(TableReadFormat::Arrow),
63            Some("ndjson") => Some(TableReadFormat::Ndjson),
64            None => None,
65            Some(x) => return Err(format!("Unknown format \"{x}\"")),
66        })
67    }
68}
69
70/// The column, or columns in key order, a [`Table`] is keyed by.
71///
72/// When serialized, a single column is a `string` and a composite key is an
73/// array of `string`.
74#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TS)]
75#[serde(untagged)]
76pub enum TableIndex {
77    Column(String),
78    Columns(Vec<String>),
79}
80
81impl TableIndex {
82    /// The index columns in key order.
83    pub fn columns(&self) -> &[String] {
84        match self {
85            TableIndex::Column(column) => std::slice::from_ref(column),
86            TableIndex::Columns(columns) => columns,
87        }
88    }
89
90    /// Fold a single-column list into [`TableIndex::Column`] and reject an
91    /// empty one.
92    pub fn normalize(self) -> ClientResult<Self> {
93        match self {
94            TableIndex::Columns(columns) if columns.is_empty() => Err(ClientError::BadTableOptions),
95            TableIndex::Columns(mut columns) if columns.len() == 1 => {
96                Ok(TableIndex::Column(columns.remove(0)))
97            },
98            index => Ok(index),
99        }
100    }
101
102    /// The normalized index for a list of columns, `None` when empty.
103    pub fn from_columns(mut columns: Vec<String>) -> Option<Self> {
104        match columns.len() {
105            0 => None,
106            1 => Some(TableIndex::Column(columns.remove(0))),
107            _ => Some(TableIndex::Columns(columns)),
108        }
109    }
110}
111
112impl From<&str> for TableIndex {
113    fn from(value: &str) -> Self {
114        TableIndex::Column(value.to_string())
115    }
116}
117
118impl From<String> for TableIndex {
119    fn from(value: String) -> Self {
120        TableIndex::Column(value)
121    }
122}
123
124impl From<Vec<String>> for TableIndex {
125    fn from(value: Vec<String>) -> Self {
126        TableIndex::Columns(value)
127    }
128}
129
130/// Options which impact the behavior of [`Client::table`], as well as
131/// subsequent calls to [`Table::update`].
132#[derive(Clone, Debug, Default, Serialize, Deserialize, TS)]
133pub struct TableInitOptions {
134    #[serde(default)]
135    #[ts(optional)]
136    pub name: Option<String>,
137
138    #[serde(default)]
139    #[ts(optional)]
140    pub format: Option<TableReadFormat>,
141
142    /// This [`Table`] should use the column, or columns, named by the `index`
143    /// parameter as the `index`, which causes [`Table::update`] and
144    /// [`Client::table`] input to either insert or update existing rows based
145    /// on `index` column value equality. A list of columns keys rows by the
146    /// combination of their values, in list order.
147    #[serde(default)]
148    #[ts(optional)]
149    pub index: Option<TableIndex>,
150
151    /// This [`Table`] should be limited to `limit` rows, after which the
152    /// _earliest_ rows will be overwritten (where _earliest_ is defined as
153    /// relative to insertion order).
154    #[serde(default)]
155    #[ts(optional)]
156    pub limit: Option<u32>,
157
158    /// Back this [`Table`]'s canonical data with the on-disk storage backend
159    /// instead of memory. On native targets this is a memory-mapped file; on
160    /// WASM it is OPFS (Worker only). Defaults to in-memory.
161    #[serde(default)]
162    #[ts(optional)]
163    pub page_to_disk: Option<bool>,
164
165    /// How Arrow `LIST` and JSON `Array` columns are ingested. `zip` (the
166    /// default) and `cartesian` expand a row into one row per list element,
167    /// and are incompatible with `index`, as the rows of an expansion
168    /// repeat their index. `stringify` encodes each list as a JSON array in
169    /// a single string column instead, with datetimes as epoch milliseconds,
170    /// and `stringify_formatted` writes them as the text `to_json` emits
171    /// under `formatted`. `STRUCT` and `MAP` columns are governed by
172    /// `struct_flatten` and `map_flatten`.
173    #[serde(default)]
174    #[ts(optional)]
175    pub list_flatten: Option<crate::proto::ListFlatten>,
176
177    /// How Arrow `STRUCT` columns and JSON objects are ingested. `pivot` (the
178    /// default) hoists each field into its own dotted column, and `stringify`
179    /// keeps the whole value as JSON text in a single string column, with
180    /// datetimes as epoch milliseconds; `stringify_formatted` writes them as
181    /// the text `to_json` emits under `formatted`.
182    #[serde(default)]
183    #[ts(optional)]
184    pub struct_flatten: Option<crate::proto::StructFlatten>,
185
186    /// How Arrow `MAP` columns are ingested. `pivot` hoists each distinct key
187    /// into its own dotted column, and `stringify` or `stringify_formatted`
188    /// keeps the whole value as JSON text in a single string column, as for
189    /// `struct_flatten`. When unset, follows `struct_flatten`. JSON objects
190    /// always follow `struct_flatten`.
191    #[serde(default)]
192    #[ts(optional)]
193    pub map_flatten: Option<crate::proto::MapFlatten>,
194
195    /// The columns of a [`Table`] made from a [`View`], in place of the ones
196    /// inferred from it.
197    #[serde(default)]
198    #[ts(optional)]
199    pub schema: Option<IndexMap<String, ColumnType>>,
200}
201
202impl TableInitOptions {
203    pub fn set_name<D: Display>(&mut self, name: D) {
204        self.name = Some(format!("{name}"))
205    }
206}
207
208impl TryFrom<TableOptions> for MakeTableOptions {
209    type Error = ClientError;
210
211    fn try_from(value: TableOptions) -> Result<Self, Self::Error> {
212        let page_to_disk = value.page_to_disk;
213        let list_flatten = value.list_flatten.map(|x| x as i32);
214        let struct_flatten = value.struct_flatten.map(|x| x as i32);
215        let map_flatten = value.map_flatten.map(|x| x as i32);
216        let view_schema = value.schema.clone().map(|schema| crate::proto::Schema {
217            schema: schema
218                .into_iter()
219                .map(|(name, r#type)| crate::proto::schema::KeyTypePair {
220                    name,
221                    r#type: r#type as i32,
222                })
223                .collect(),
224        });
225
226        Ok(MakeTableOptions {
227            page_to_disk,
228            list_flatten,
229            struct_flatten,
230            map_flatten,
231            view_schema,
232            make_table_type: match value {
233                TableOptions {
234                    index: Some(_),
235                    limit: Some(_),
236                    ..
237                } => Err(ClientError::BadTableOptions)?,
238                TableOptions {
239                    index: Some(TableIndex::Column(index)),
240                    ..
241                } => Some(MakeTableType::MakeIndexTable(index)),
242                TableOptions {
243                    index: Some(TableIndex::Columns(columns)),
244                    ..
245                } => Some(MakeTableType::MakeCompositeIndexTable(IndexColumns {
246                    columns,
247                })),
248                TableOptions {
249                    limit: Some(limit), ..
250                } => Some(MakeTableType::MakeLimitTable(limit)),
251                _ => None,
252            },
253        })
254    }
255}
256
257#[derive(Clone, Debug)]
258pub(crate) struct TableOptions {
259    pub index: Option<TableIndex>,
260    pub limit: Option<u32>,
261    pub page_to_disk: Option<bool>,
262    pub list_flatten: Option<crate::proto::ListFlatten>,
263    pub struct_flatten: Option<crate::proto::StructFlatten>,
264    pub map_flatten: Option<crate::proto::MapFlatten>,
265    pub schema: Option<IndexMap<String, ColumnType>>,
266}
267
268/// The source [`View`] of a replica [`Table`] built by [`Client::table`],
269/// with the subscription tokens to release when the replica is deleted.
270#[derive(Clone)]
271pub(crate) struct ViewBinding {
272    pub view: View,
273    pub update_token: u32,
274    pub remove_token: Option<u32>,
275}
276
277impl From<TableInitOptions> for TableOptions {
278    fn from(value: TableInitOptions) -> Self {
279        TableOptions {
280            index: value.index,
281            limit: value.limit,
282            page_to_disk: value.page_to_disk,
283            list_flatten: value.list_flatten,
284            struct_flatten: value.struct_flatten,
285            map_flatten: value.map_flatten,
286            schema: value.schema,
287        }
288    }
289}
290
291/// Options for [`Client::join`].
292#[derive(Clone, Debug, Default, Serialize, Deserialize, TS)]
293pub struct JoinOptions {
294    #[serde(default)]
295    #[ts(optional)]
296    pub join_type: Option<crate::proto::JoinType>,
297
298    #[serde(default)]
299    #[ts(optional)]
300    pub name: Option<String>,
301
302    #[serde(default)]
303    #[ts(optional)]
304    pub right_on: Option<String>,
305}
306
307/// Options for [`Table::delete`].
308#[derive(Clone, Debug, Default, Deserialize, TS)]
309pub struct DeleteOptions {
310    pub lazy: bool,
311}
312
313/// Options for [`Table::update`].
314#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
315pub struct UpdateOptions {
316    pub port_id: Option<u32>,
317    pub format: Option<TableReadFormat>,
318}
319
320/// Result of a call to [`Table::validate_expressions`], containing a schema
321/// for valid expressions and error messages for invalid ones.
322#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
323pub struct ExprValidationResult {
324    pub expression_schema: Schema,
325    pub errors: HashMap<String, ExpressionError>,
326    pub expression_alias: HashMap<String, String>,
327}
328
329/// The successful result of [`Table::describe`]: what a [`View`] built from the
330/// described config would report, WITHOUT building one.
331#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
332pub struct Description {
333    /// The type of every expression, before aggregation.
334    pub expression_schema: Schema,
335
336    /// Equal to [`View::schema`] of a [`View`] built from the same config.
337    pub view_schema: Schema,
338}
339
340/// Why [`Table::describe`] rejected a config.
341#[derive(Clone, Debug, PartialEq)]
342pub enum DescribeError {
343    /// At least one expression failed to compile.
344    Expressions {
345        expression_schema: Schema,
346        errors: HashMap<String, ExpressionError>,
347    },
348
349    /// Every expression compiled but the rest of the config is invalid.
350    Config(String),
351}
352
353impl Display for DescribeError {
354    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
355        match self {
356            Self::Expressions { errors, .. } => {
357                let mut names = errors.keys().collect::<Vec<_>>();
358                names.sort();
359                write!(f, "Invalid expressions: ")?;
360                for (i, name) in names.iter().enumerate() {
361                    if i > 0 {
362                        write!(f, "; ")?;
363                    }
364
365                    write!(f, "{}: {}", name, errors[*name].error_message)?;
366                }
367
368                Ok(())
369            },
370            Self::Config(msg) => write!(f, "{}", msg),
371        }
372    }
373}
374
375impl std::error::Error for DescribeError {}
376
377/// The wire/serde shape of a [`Table::describe`] verdict, as exchanged with
378/// JavaScript and Python (both client results and virtual-server handler
379/// returns): exactly one of the three arms, discriminated by its keys.
380#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
381#[serde(untagged)]
382pub enum DescribeVerdict {
383    Ok {
384        expression_schema: Schema,
385        view_schema: Schema,
386    },
387    Expressions {
388        #[serde(default)]
389        expression_schema: Schema,
390        expression_errors: HashMap<String, ExpressionError>,
391    },
392    Config {
393        config_error: String,
394    },
395}
396
397impl From<Result<Description, DescribeError>> for DescribeVerdict {
398    fn from(value: Result<Description, DescribeError>) -> Self {
399        match value {
400            Ok(d) => Self::Ok {
401                expression_schema: d.expression_schema,
402                view_schema: d.view_schema,
403            },
404            Err(DescribeError::Expressions {
405                expression_schema,
406                errors,
407            }) => Self::Expressions {
408                expression_schema,
409                expression_errors: errors,
410            },
411            Err(DescribeError::Config(config_error)) => Self::Config { config_error },
412        }
413    }
414}
415
416impl TryFrom<DescribeVerdict> for Result<Description, DescribeError> {
417    type Error = ClientError;
418
419    /// Fails only for the unrepresentable `Expressions` arm with no errors.
420    fn try_from(value: DescribeVerdict) -> Result<Self, ClientError> {
421        Ok(match value {
422            DescribeVerdict::Ok {
423                expression_schema,
424                view_schema,
425            } => Ok(Description {
426                expression_schema,
427                view_schema,
428            }),
429            DescribeVerdict::Expressions {
430                expression_schema,
431                expression_errors,
432            } => {
433                if expression_errors.is_empty() {
434                    return Err(ClientError::Unknown(
435                        "Describe verdict has an empty `expression_errors`".to_string(),
436                    ));
437                }
438
439                Err(DescribeError::Expressions {
440                    expression_schema,
441                    errors: expression_errors,
442                })
443            },
444            DescribeVerdict::Config { config_error } => Err(DescribeError::Config(config_error)),
445        })
446    }
447}
448
449fn decode_schema(schema: HashMap<String, i32>) -> ClientResult<Schema> {
450    schema
451        .into_iter()
452        .map(|(name, ty)| {
453            ColumnType::try_from(ty)
454                .map(|ty| (name, ty))
455                .map_err(|e| ClientError::Unknown(e.to_string()))
456        })
457        .collect()
458}
459
460/// [`Table`] is Perspective's columnar data frame, analogous to a Pandas/Polars
461/// `DataFrame` or Apache Arrow, supporting append & in-place updates, removal
462/// by index, and update notifications.
463///
464/// A [`Table`] contains columns, each of which have a unique name, are strongly
465/// and consistently typed, and contains rows of data conforming to the column's
466/// type. Each column in a [`Table`] must have the same number of rows, though
467/// not every row must contain data; null-values are used to indicate missing
468/// values in the dataset. The schema of a [`Table`] is _immutable after
469/// creation_, which means the column names and data types cannot be changed
470/// after the [`Table`] has been created. Columns cannot be added or deleted
471/// after creation either, but a [`View`] can be used to select an arbitrary set
472/// of columns from the [`Table`].
473#[derive(Clone)]
474pub struct Table {
475    name: String,
476    client: Client,
477    options: TableOptions,
478    pub(crate) view_binding: Option<ViewBinding>,
479}
480
481assert_table_api!(Table);
482
483impl PartialEq for Table {
484    fn eq(&self, other: &Self) -> bool {
485        self.name == other.name && self.client == other.client
486    }
487}
488
489impl Table {
490    pub(crate) fn new(name: String, client: Client, options: TableOptions) -> Self {
491        Table {
492            name,
493            client,
494            options,
495            view_binding: None,
496        }
497    }
498
499    fn client_message(&self, req: ClientReq) -> Request {
500        Request {
501            msg_id: self.client.gen_id(),
502            entity_id: self.name.clone(),
503            client_req: Some(req),
504        }
505    }
506
507    /// Get a copy of the [`Client`] this [`Table`] came from.
508    pub fn get_client(&self) -> Client {
509        self.client.clone()
510    }
511
512    /// Get a metadata dictionary of the `perspective_server::Server`'s
513    /// features, which is (currently) implementation specific, but there is
514    /// only one implementation.
515    pub async fn get_features(&self) -> ClientResult<Features> {
516        self.client.get_features().await
517    }
518
519    /// Returns the name of the index column for the table.
520    ///
521    /// # Examples
522    ///
523    /// ```no_run
524    /// # use perspective_client::{Client, TableData, TableInitOptions, UpdateData};
525    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
526    /// # let client: Client = todo!();
527    /// let options = TableInitOptions {
528    ///     index: Some("x".into()),
529    ///     ..TableInitOptions::default()
530    /// };
531    /// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
532    /// let table = client.table(data, options).await?;
533    /// let index = table.get_index();
534    /// # Ok(()) }
535    /// ```
536    pub fn get_index(&self) -> Option<TableIndex> {
537        self.options.index.clone()
538    }
539
540    /// Returns the user-specified row limit for this table.
541    pub fn get_limit(&self) -> Option<u32> {
542        self.options.limit.as_ref().map(|limit| *limit)
543    }
544
545    /// Returns the user-specified name for this table, or the auto-generated
546    /// name if a name was not specified when the table was created.
547    pub fn get_name(&self) -> &str {
548        self.name.as_str()
549    }
550
551    /// Removes all the rows in the [`Table`], but preserves everything else
552    /// including the schema, index, and any callbacks or registered
553    /// [`View`] instances.
554    ///
555    /// Calling [`Table::clear`], like [`Table::update`] and [`Table::remove`],
556    /// will trigger an update event to any registered listeners via
557    /// [`View::on_update`].
558    pub async fn clear(&self) -> ClientResult<()> {
559        self.replace(UpdateData::JsonRows("[]".to_owned())).await
560    }
561
562    /// Delete this [`Table`] and cleans up associated resources.
563    ///
564    /// [`Table`]s do not stop consuming resources or processing updates when
565    /// they are garbage collected in their host language - you must call
566    /// this method to reclaim these.
567    ///
568    /// # Arguments
569    ///
570    /// - `options` An options dictionary.
571    ///     - `lazy` Whether to delete this [`Table`] _lazily_. When false (the
572    ///       default), the delete will occur immediately, assuming it has no
573    ///       [`View`] instances registered to it (which must be deleted first,
574    ///       otherwise this method will throw an error). When true, the
575    ///       [`Table`] will only be marked for deltion once its [`View`]
576    ///       dependency count reaches 0.
577    ///
578    /// # Examples
579    ///
580    /// ```no_run
581    /// # use perspective_client::{Client, DeleteOptions, TableData, TableInitOptions, UpdateData};
582    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
583    /// # let client: Client = todo!();
584    /// let opts = TableInitOptions::default();
585    /// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
586    /// let table = client.table(data, opts).await?;
587    ///
588    /// // ...
589    ///
590    /// table.delete(DeleteOptions::default()).await?;
591    /// # Ok(()) }
592    /// ```
593    pub async fn delete(&self, options: DeleteOptions) -> ClientResult<()> {
594        if let Some(binding) = &self.view_binding {
595            binding.view.remove_update(binding.update_token).await?;
596            if let Some(token) = binding.remove_token {
597                binding.view.remove_remove(token).await?;
598            }
599        }
600
601        let msg = self.client_message(ClientReq::TableDeleteReq(TableDeleteReq {
602            is_immediate: !options.lazy,
603        }));
604
605        match self.client.oneshot(&msg).await? {
606            ClientResp::TableDeleteResp(_) => Ok(()),
607            resp => Err(resp.into()),
608        }
609    }
610
611    /// Returns the column names of this [`Table`] in "natural" order (the
612    /// ordering implied by the input format).
613    ///  
614    /// # Examples
615    ///
616    /// ```no_run
617    /// # use perspective_client::Table;
618    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
619    /// # let table: Table = todo!();
620    /// let columns = table.columns().await?;
621    /// # Ok(()) }
622    /// ```
623    pub async fn columns(&self) -> ClientResult<Vec<String>> {
624        let msg = self.client_message(ClientReq::TableSchemaReq(TableSchemaReq {}));
625        match self.client.oneshot(&msg).await? {
626            ClientResp::TableSchemaResp(TableSchemaResp { schema }) => Ok(schema
627                .map(|x| x.schema.into_iter().map(|x| x.name.to_owned()).collect())
628                .unwrap()),
629            resp => Err(resp.into()),
630        }
631    }
632
633    /// Returns the number of rows in a [`Table`].
634    pub async fn size(&self) -> ClientResult<usize> {
635        let msg = self.client_message(ClientReq::TableSizeReq(TableSizeReq {}));
636        match self.client.oneshot(&msg).await? {
637            ClientResp::TableSizeResp(TableSizeResp { size }) => Ok(size as usize),
638            resp => Err(resp.into()),
639        }
640    }
641
642    /// Returns a table's [`Schema`], a mapping of column names to column types.
643    ///
644    /// The mapping of a [`Table`]'s column names to data types is referred to
645    /// as a [`Schema`]. Each column has a unique name and a data type, one
646    /// of:
647    ///
648    /// - `"boolean"` - A boolean type
649    /// - `"date"` - A timesonze-agnostic date type (month/day/year)
650    /// - `"datetime"` - A millisecond-precision datetime type in the UTC
651    ///   timezone
652    /// - `"float"` - A 64 bit float
653    /// - `"integer"` - A signed 32 bit integer (the integer type supported by
654    ///   JavaScript)
655    /// - `"string"` - A [`String`] data type (encoded internally as a
656    ///   _dictionary_)
657    ///
658    /// Note that all [`Table`] columns are _nullable_, regardless of the data
659    /// type.
660    pub async fn schema(&self) -> ClientResult<Schema> {
661        let msg = self.client_message(ClientReq::TableSchemaReq(TableSchemaReq {}));
662        match self.client.oneshot(&msg).await? {
663            ClientResp::TableSchemaResp(TableSchemaResp { schema }) => Ok(schema
664                .map(|x| {
665                    x.schema
666                        .into_iter()
667                        .map(|x| (x.name, ColumnType::try_from(x.r#type).unwrap()))
668                        .collect()
669                })
670                .unwrap()),
671            resp => Err(resp.into()),
672        }
673    }
674
675    /// Create a unique channel ID on this [`Table`], which allows
676    /// `View::on_update` callback calls to be associated with the
677    /// `Table::update` which caused them.
678    pub async fn make_port(&self) -> ClientResult<i32> {
679        let msg = self.client_message(ClientReq::TableMakePortReq(TableMakePortReq {}));
680        match self.client.oneshot(&msg).await? {
681            ClientResp::TableMakePortResp(TableMakePortResp { port_id }) => Ok(port_id as i32),
682            _ => Err(ClientError::Unknown("make_port".to_string())),
683        }
684    }
685
686    /// Register a callback which is called exactly once, when this [`Table`] is
687    /// deleted with the [`Table::delete`] method.
688    ///
689    /// [`Table::on_delete`] resolves when the subscription message is sent, not
690    /// when the _delete_ event occurs.
691    pub async fn on_delete(
692        &self,
693        on_delete: Box<dyn Fn() + Send + Sync + 'static>,
694    ) -> ClientResult<u32> {
695        let callback = move |resp: Response| match resp.client_resp {
696            Some(ClientResp::TableOnDeleteResp(_)) => {
697                on_delete();
698                Ok(())
699            },
700            resp => Err(resp.into()),
701        };
702
703        let msg = self.client_message(ClientReq::TableOnDeleteReq(TableOnDeleteReq {}));
704        self.client.subscribe_once(&msg, Box::new(callback)).await?;
705        Ok(msg.msg_id)
706    }
707
708    /// Removes a listener with a given ID, as returned by a previous call to
709    /// [`Table::on_delete`].
710    pub async fn remove_delete(&self, callback_id: u32) -> ClientResult<()> {
711        let msg = self.client_message(ClientReq::TableRemoveDeleteReq(TableRemoveDeleteReq {
712            id: callback_id,
713        }));
714
715        match self.client.oneshot(&msg).await? {
716            ClientResp::TableRemoveDeleteResp(_) => Ok(()),
717            resp => Err(resp.into()),
718        }
719    }
720
721    /// Removes rows from this [`Table`] with the `index` column values
722    /// supplied.
723    ///
724    /// # Arguments
725    ///
726    /// - `indices` - A list of `index` column values for rows that should be
727    ///   removed.
728    ///
729    /// # Examples
730    ///
731    /// ```no_run
732    /// # use perspective_client::{Table, UpdateData};
733    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
734    /// # let table: Table = todo!();
735    /// table
736    ///     .remove(UpdateData::Csv("index\n1\n2\n3".into()))
737    ///     .await?;
738    /// # Ok(()) }
739    /// ```
740    pub async fn remove(&self, input: UpdateData) -> ClientResult<()> {
741        let msg = self.client_message(ClientReq::TableRemoveReq(TableRemoveReq {
742            data: Some(input.into()),
743        }));
744
745        match self.client.oneshot(&msg).await? {
746            ClientResp::TableRemoveResp(_) => Ok(()),
747            resp => Err(resp.into()),
748        }
749    }
750
751    /// Replace all rows in this [`Table`] with the input data, coerced to this
752    /// [`Table`]'s existing [`Schema`], notifying any derived [`View`] and
753    /// [`View::on_update`] callbacks.
754    ///
755    /// Calling [`Table::replace`] is an easy way to replace _all_ the data in a
756    /// [`Table`] without losing any derived [`View`] instances or
757    /// [`View::on_update`] callbacks. [`Table::replace`] does _not_ infer
758    /// data types like [`Client::table`] does, rather it _coerces_ input
759    /// data to the `Schema` like [`Table::update`]. If you need a [`Table`]
760    /// with a different `Schema`, you must create a new one.
761    ///
762    /// # Examples
763    ///
764    /// ```no_run
765    /// # use perspective_client::{Table, UpdateData};
766    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
767    /// # let table: Table = todo!();
768    /// let data = UpdateData::Csv("x,y\n1,2".into());
769    /// table.replace(data).await?;
770    /// # Ok(()) }
771    /// ```
772    pub async fn replace(&self, input: UpdateData) -> ClientResult<()> {
773        let msg = self.client_message(ClientReq::TableReplaceReq(TableReplaceReq {
774            data: Some(input.into()),
775        }));
776
777        match self.client.oneshot(&msg).await? {
778            ClientResp::TableReplaceResp(_) => Ok(()),
779            resp => Err(resp.into()),
780        }
781    }
782
783    /// Updates the rows of this table and any derived [`View`] instances.
784    ///
785    /// Calling [`Table::update`] will trigger the [`View::on_update`] callbacks
786    /// register to derived [`View`], and the call itself will not resolve until
787    /// _all_ derived [`View`]'s are notified.
788    ///
789    /// When updating a [`Table`] with an `index`, [`Table::update`] supports
790    /// partial updates, by omitting columns from the update data.
791    ///
792    /// # Arguments
793    ///
794    /// - `input` - The input data for this [`Table`]. The schema of a [`Table`]
795    ///   is immutable after creation, so this method cannot be called with a
796    ///   schema.
797    /// - `options` - Options for this update step - see [`UpdateOptions`].
798    ///
799    /// # Examples
800    ///
801    /// ```no_run
802    /// # use perspective_client::{Table, UpdateData, UpdateOptions};
803    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
804    /// # let table: Table = todo!();
805    /// let data = UpdateData::Csv("x,y\n1,2".into());
806    /// let opts = UpdateOptions::default();
807    /// table.update(data, opts).await?;
808    /// # Ok(()) }
809    /// ```
810    pub async fn update(&self, input: UpdateData, options: UpdateOptions) -> ClientResult<()> {
811        let msg = self.client_message(ClientReq::TableUpdateReq(TableUpdateReq {
812            data: Some(input.into()),
813            port_id: options.port_id.unwrap_or(0),
814        }));
815
816        match self.client.oneshot(&msg).await? {
817            ClientResp::TableUpdateResp(_) => Ok(()),
818            resp => Err(resp.into()),
819        }
820    }
821
822    /// Validate a complete [`ViewConfigUpdate`] against this table and report
823    /// the schema a [`View`] built from it would have, without creating one.
824    pub async fn describe(
825        &self,
826        config: ViewConfigUpdate,
827    ) -> ClientResult<Result<Description, DescribeError>> {
828        let msg = self.client_message(ClientReq::TableDescribeReq(TableDescribeReq {
829            config: Some(config.into()),
830        }));
831
832        let resp = match self.client.oneshot(&msg).await? {
833            ClientResp::TableDescribeResp(resp) => resp,
834            resp => return Err(resp.into()),
835        };
836
837        let expression_schema = decode_schema(resp.expression_schema)?;
838        let has_errors = !resp.expression_errors.is_empty();
839        Ok(match (resp.result, has_errors) {
840            (Some(table_describe_resp::Result::View(view)), false) => Ok(Description {
841                expression_schema,
842                view_schema: decode_schema(view.schema)?,
843            }),
844            (Some(table_describe_resp::Result::ConfigError(msg)), false) => {
845                Err(DescribeError::Config(msg))
846            },
847            (None, true) => Err(DescribeError::Expressions {
848                expression_schema,
849                errors: resp.expression_errors,
850            }),
851            (result, _) => {
852                return Err(ClientError::Unknown(format!(
853                    "Malformed describe response: result={:?} expression_errors={:?}",
854                    result.is_some(),
855                    resp.expression_errors.keys().collect::<Vec<_>>()
856                )));
857            },
858        })
859    }
860
861    /// Validates the given expressions.
862    pub async fn validate_expressions(
863        &self,
864        expressions: Expressions,
865    ) -> ClientResult<ExprValidationResult> {
866        let expression_alias = expressions.0.clone();
867        let config = ViewConfigUpdate {
868            expressions: Some(expressions),
869            columns: Some(vec![]),
870            ..ViewConfigUpdate::default()
871        };
872
873        match self.describe(config).await? {
874            Ok(d) => Ok(ExprValidationResult {
875                expression_schema: d.expression_schema,
876                errors: HashMap::new(),
877                expression_alias,
878            }),
879            Err(DescribeError::Expressions {
880                expression_schema,
881                errors,
882            }) => Ok(ExprValidationResult {
883                expression_schema,
884                errors,
885                expression_alias,
886            }),
887            Err(DescribeError::Config(msg)) => Err(ClientError::Internal(msg)),
888        }
889    }
890
891    /// Create a new [`View`] from this table with a specified
892    /// [`ViewConfigUpdate`].
893    ///
894    /// See [`View`] struct.
895    ///
896    /// # Examples
897    ///
898    /// ```no_run
899    /// # use std::collections::HashMap;
900    /// # use perspective_client::Table;
901    /// # use perspective_client::config::*;
902    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
903    /// # let table: Table = todo!();
904    /// let view = table
905    ///     .view(Some(ViewConfigUpdate {
906    ///         columns: Some(vec![Some("Sales".into())]),
907    ///         aggregates: Some(HashMap::from_iter(vec![("Sales".into(), "sum".into())])),
908    ///         group_by: Some(vec!["Region".into(), "Country".into()]),
909    ///         filter: Some(vec![Filter::new("Category", "in", &[
910    ///             "Furniture",
911    ///             "Technology",
912    ///         ])]),
913    ///         ..ViewConfigUpdate::default()
914    ///     }))
915    ///     .await?;
916    /// # Ok(()) }
917    /// ```
918    pub async fn view(&self, config: Option<ViewConfigUpdate>) -> ClientResult<View> {
919        let view_name = randid();
920        let msg = Request {
921            msg_id: self.client.gen_id(),
922            entity_id: self.name.clone(),
923            client_req: ClientReq::TableMakeViewReq(TableMakeViewReq {
924                view_id: view_name.clone(),
925                config: config.map(|x| x.into()),
926            })
927            .into(),
928        };
929
930        match self.client.oneshot(&msg).await? {
931            ClientResp::TableMakeViewResp(TableMakeViewResp { view_id })
932                if view_id == view_name =>
933            {
934                Ok(View::new_with_source(
935                    view_name,
936                    self.client.clone(),
937                    ViewSource {
938                        options: self.options.clone(),
939                    },
940                ))
941            },
942            resp => Err(resp.into()),
943        }
944    }
945}