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;
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
97impl TableInitOptions {
98    pub fn set_name<D: Display>(&mut self, name: D) {
99        self.name = Some(format!("{name}"))
100    }
101}
102
103impl TryFrom<TableOptions> for MakeTableOptions {
104    type Error = ClientError;
105
106    fn try_from(value: TableOptions) -> Result<Self, Self::Error> {
107        Ok(MakeTableOptions {
108            make_table_type: match value {
109                TableOptions {
110                    index: Some(_),
111                    limit: Some(_),
112                } => Err(ClientError::BadTableOptions)?,
113                TableOptions {
114                    index: Some(index), ..
115                } => Some(MakeTableType::MakeIndexTable(index)),
116                TableOptions {
117                    limit: Some(limit), ..
118                } => Some(MakeTableType::MakeLimitTable(limit)),
119                _ => None,
120            },
121        })
122    }
123}
124
125#[derive(Clone, Debug)]
126pub(crate) struct TableOptions {
127    pub index: Option<String>,
128    pub limit: Option<u32>,
129}
130
131impl From<TableInitOptions> for TableOptions {
132    fn from(value: TableInitOptions) -> Self {
133        TableOptions {
134            index: value.index,
135            limit: value.limit,
136        }
137    }
138}
139
140/// Options for [`Table::delete`].
141#[derive(Clone, Debug, Default, Deserialize, TS)]
142pub struct DeleteOptions {
143    pub lazy: bool,
144}
145
146/// Options for [`Table::update`].
147#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
148pub struct UpdateOptions {
149    pub port_id: Option<u32>,
150    pub format: Option<TableReadFormat>,
151}
152
153/// Result of a call to [`Table::validate_expressions`], containing a schema
154/// for valid expressions and error messages for invalid ones.
155#[derive(Clone, Debug, Serialize, Deserialize)]
156pub struct ExprValidationResult {
157    pub expression_schema: Schema,
158    pub errors: HashMap<String, table_validate_expr_resp::ExprValidationError>,
159    pub expression_alias: HashMap<String, String>,
160}
161
162/// [`Table`] is Perspective's columnar data frame, analogous to a Pandas/Polars
163/// `DataFrame` or Apache Arrow, supporting append & in-place updates, removal
164/// by index, and update notifications.
165///
166/// A [`Table`] contains columns, each of which have a unique name, are strongly
167/// and consistently typed, and contains rows of data conforming to the column's
168/// type. Each column in a [`Table`] must have the same number of rows, though
169/// not every row must contain data; null-values are used to indicate missing
170/// values in the dataset. The schema of a [`Table`] is _immutable after
171/// creation_, which means the column names and data types cannot be changed
172/// after the [`Table`] has been created. Columns cannot be added or deleted
173/// after creation either, but a [`View`] can be used to select an arbitrary set
174/// of columns from the [`Table`].
175#[derive(Clone)]
176pub struct Table {
177    name: String,
178    client: Client,
179    options: TableOptions,
180
181    /// If this table is constructed from a View, the view's on_update callback
182    /// is wired into this table. So, we store the token to clean it up properly
183    /// on destruction.
184    pub(crate) view_update_token: Option<u32>,
185}
186
187assert_table_api!(Table);
188
189impl PartialEq for Table {
190    fn eq(&self, other: &Self) -> bool {
191        self.name == other.name && self.client == other.client
192    }
193}
194
195impl Table {
196    pub(crate) fn new(name: String, client: Client, options: TableOptions) -> Self {
197        Table {
198            name,
199            client,
200            options,
201            view_update_token: None,
202        }
203    }
204
205    fn client_message(&self, req: ClientReq) -> Request {
206        Request {
207            msg_id: self.client.gen_id(),
208            entity_id: self.name.clone(),
209            client_req: Some(req),
210        }
211    }
212
213    /// Get a copy of the [`Client`] this [`Table`] came from.
214    pub fn get_client(&self) -> Client {
215        self.client.clone()
216    }
217
218    /// Get a metadata dictionary of the `perspective_server::Server`'s
219    /// features, which is (currently) implementation specific, but there is
220    /// only one implementation.
221    pub async fn get_features(&self) -> ClientResult<Features> {
222        self.client.get_features().await
223    }
224
225    /// Returns the name of the index column for the table.
226    ///
227    /// # Examples
228    ///
229    /// ```rust
230    /// let options = TableInitOptions {
231    ///     index: Some("x".to_string()),
232    ///     ..default()
233    /// };
234    /// let table = client.table("x,y\n1,2\n3,4", options).await;
235    /// let index = table.get_index()
236    /// ```
237    pub fn get_index(&self) -> Option<String> {
238        self.options.index.as_ref().map(|index| index.to_owned())
239    }
240
241    /// Returns the user-specified row limit for this table.
242    pub fn get_limit(&self) -> Option<u32> {
243        self.options.limit.as_ref().map(|limit| *limit)
244    }
245
246    /// Returns the user-specified name for this table, or the auto-generated
247    /// name if a name was not specified when the table was created.
248    pub fn get_name(&self) -> &str {
249        self.name.as_str()
250    }
251
252    /// Removes all the rows in the [`Table`], but preserves everything else
253    /// including the schema, index, and any callbacks or registered
254    /// [`View`] instances.
255    ///
256    /// Calling [`Table::clear`], like [`Table::update`] and [`Table::remove`],
257    /// will trigger an update event to any registered listeners via
258    /// [`View::on_update`].
259    pub async fn clear(&self) -> ClientResult<()> {
260        self.replace(UpdateData::JsonRows("[]".to_owned())).await
261    }
262
263    /// Delete this [`Table`] and cleans up associated resources.
264    ///
265    /// [`Table`]s do not stop consuming resources or processing updates when
266    /// they are garbage collected in their host language - you must call
267    /// this method to reclaim these.
268    ///
269    /// # Arguments
270    ///
271    /// - `options` An options dictionary.
272    ///     - `lazy` Whether to delete this [`Table`] _lazily_. When false (the
273    ///       default), the delete will occur immediately, assuming it has no
274    ///       [`View`] instances registered to it (which must be deleted first,
275    ///       otherwise this method will throw an error). When true, the
276    ///       [`Table`] will only be marked for deltion once its [`View`]
277    ///       dependency count reaches 0.
278    ///
279    /// # Examples
280    ///
281    /// ```rust
282    /// let opts = TableInitOptions::default();
283    /// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
284    /// let table = client.table(data, opts).await?;
285    ///
286    /// // ...
287    ///
288    /// table.delete(DeleteOptions::default()).await?;
289    /// ```
290    pub async fn delete(&self, options: DeleteOptions) -> ClientResult<()> {
291        let msg = self.client_message(ClientReq::TableDeleteReq(TableDeleteReq {
292            is_immediate: !options.lazy,
293        }));
294
295        match self.client.oneshot(&msg).await? {
296            ClientResp::TableDeleteResp(_) => Ok(()),
297            resp => Err(resp.into()),
298        }
299    }
300
301    /// Returns the column names of this [`Table`] in "natural" order (the
302    /// ordering implied by the input format).
303    ///  
304    /// # Examples
305    ///
306    /// ```rust
307    /// let columns = table.columns().await;
308    /// ```
309    pub async fn columns(&self) -> ClientResult<Vec<String>> {
310        let msg = self.client_message(ClientReq::TableSchemaReq(TableSchemaReq {}));
311        match self.client.oneshot(&msg).await? {
312            ClientResp::TableSchemaResp(TableSchemaResp { schema }) => Ok(schema
313                .map(|x| x.schema.into_iter().map(|x| x.name.to_owned()).collect())
314                .unwrap()),
315            resp => Err(resp.into()),
316        }
317    }
318
319    /// Returns the number of rows in a [`Table`].
320    pub async fn size(&self) -> ClientResult<usize> {
321        let msg = self.client_message(ClientReq::TableSizeReq(TableSizeReq {}));
322        match self.client.oneshot(&msg).await? {
323            ClientResp::TableSizeResp(TableSizeResp { size }) => Ok(size as usize),
324            resp => Err(resp.into()),
325        }
326    }
327
328    /// Returns a table's [`Schema`], a mapping of column names to column types.
329    ///
330    /// The mapping of a [`Table`]'s column names to data types is referred to
331    /// as a [`Schema`]. Each column has a unique name and a data type, one
332    /// of:
333    ///
334    /// - `"boolean"` - A boolean type
335    /// - `"date"` - A timesonze-agnostic date type (month/day/year)
336    /// - `"datetime"` - A millisecond-precision datetime type in the UTC
337    ///   timezone
338    /// - `"float"` - A 64 bit float
339    /// - `"integer"` - A signed 32 bit integer (the integer type supported by
340    ///   JavaScript)
341    /// - `"string"` - A [`String`] data type (encoded internally as a
342    ///   _dictionary_)
343    ///
344    /// Note that all [`Table`] columns are _nullable_, regardless of the data
345    /// type.
346    pub async fn schema(&self) -> ClientResult<Schema> {
347        let msg = self.client_message(ClientReq::TableSchemaReq(TableSchemaReq {}));
348        match self.client.oneshot(&msg).await? {
349            ClientResp::TableSchemaResp(TableSchemaResp { schema }) => Ok(schema
350                .map(|x| {
351                    x.schema
352                        .into_iter()
353                        .map(|x| (x.name, ColumnType::try_from(x.r#type).unwrap()))
354                        .collect()
355                })
356                .unwrap()),
357            resp => Err(resp.into()),
358        }
359    }
360
361    /// Create a unique channel ID on this [`Table`], which allows
362    /// `View::on_update` callback calls to be associated with the
363    /// `Table::update` which caused them.
364    pub async fn make_port(&self) -> ClientResult<i32> {
365        let msg = self.client_message(ClientReq::TableMakePortReq(TableMakePortReq {}));
366        match self.client.oneshot(&msg).await? {
367            ClientResp::TableMakePortResp(TableMakePortResp { port_id }) => Ok(port_id as i32),
368            _ => Err(ClientError::Unknown("make_port".to_string())),
369        }
370    }
371
372    /// Register a callback which is called exactly once, when this [`Table`] is
373    /// deleted with the [`Table::delete`] method.
374    ///
375    /// [`Table::on_delete`] resolves when the subscription message is sent, not
376    /// when the _delete_ event occurs.
377    pub async fn on_delete(
378        &self,
379        on_delete: Box<dyn Fn() + Send + Sync + 'static>,
380    ) -> ClientResult<u32> {
381        let callback = move |resp: Response| match resp.client_resp {
382            Some(ClientResp::TableOnDeleteResp(_)) => {
383                on_delete();
384                Ok(())
385            },
386            resp => Err(resp.into()),
387        };
388
389        let msg = self.client_message(ClientReq::TableOnDeleteReq(TableOnDeleteReq {}));
390        self.client.subscribe_once(&msg, Box::new(callback)).await?;
391        Ok(msg.msg_id)
392    }
393
394    /// Removes a listener with a given ID, as returned by a previous call to
395    /// [`Table::on_delete`].
396    pub async fn remove_delete(&self, callback_id: u32) -> ClientResult<()> {
397        let msg = self.client_message(ClientReq::TableRemoveDeleteReq(TableRemoveDeleteReq {
398            id: callback_id,
399        }));
400
401        match self.client.oneshot(&msg).await? {
402            ClientResp::TableRemoveDeleteResp(_) => Ok(()),
403            resp => Err(resp.into()),
404        }
405    }
406
407    /// Removes rows from this [`Table`] with the `index` column values
408    /// supplied.
409    ///
410    /// # Arguments
411    ///
412    /// - `indices` - A list of `index` column values for rows that should be
413    ///   removed.
414    ///
415    /// # Examples
416    ///
417    /// ```rust
418    /// table.remove(UpdateData::Csv("index\n1\n2\n3")).await?;
419    /// ```
420    pub async fn remove(&self, input: UpdateData) -> ClientResult<()> {
421        let msg = self.client_message(ClientReq::TableRemoveReq(TableRemoveReq {
422            data: Some(input.into()),
423        }));
424
425        match self.client.oneshot(&msg).await? {
426            ClientResp::TableRemoveResp(_) => Ok(()),
427            resp => Err(resp.into()),
428        }
429    }
430
431    /// Replace all rows in this [`Table`] with the input data, coerced to this
432    /// [`Table`]'s existing [`Schema`], notifying any derived [`View`] and
433    /// [`View::on_update`] callbacks.
434    ///
435    /// Calling [`Table::replace`] is an easy way to replace _all_ the data in a
436    /// [`Table`] without losing any derived [`View`] instances or
437    /// [`View::on_update`] callbacks. [`Table::replace`] does _not_ infer
438    /// data types like [`Client::table`] does, rather it _coerces_ input
439    /// data to the `Schema` like [`Table::update`]. If you need a [`Table`]
440    /// with a different `Schema`, you must create a new one.
441    ///
442    /// # Examples
443    ///
444    /// ```rust
445    /// let data = UpdateData::Csv("x,y\n1,2".into());
446    /// let opts = UpdateOptions::default();
447    /// table.replace(data, opts).await?;
448    /// ```
449    pub async fn replace(&self, input: UpdateData) -> ClientResult<()> {
450        let msg = self.client_message(ClientReq::TableReplaceReq(TableReplaceReq {
451            data: Some(input.into()),
452        }));
453
454        match self.client.oneshot(&msg).await? {
455            ClientResp::TableReplaceResp(_) => Ok(()),
456            resp => Err(resp.into()),
457        }
458    }
459
460    /// Updates the rows of this table and any derived [`View`] instances.
461    ///
462    /// Calling [`Table::update`] will trigger the [`View::on_update`] callbacks
463    /// register to derived [`View`], and the call itself will not resolve until
464    /// _all_ derived [`View`]'s are notified.
465    ///
466    /// When updating a [`Table`] with an `index`, [`Table::update`] supports
467    /// partial updates, by omitting columns from the update data.
468    ///
469    /// # Arguments
470    ///
471    /// - `input` - The input data for this [`Table`]. The schema of a [`Table`]
472    ///   is immutable after creation, so this method cannot be called with a
473    ///   schema.
474    /// - `options` - Options for this update step - see [`UpdateOptions`].
475    ///
476    /// # Examples
477    ///
478    /// ```rust
479    /// let data = UpdateData::Csv("x,y\n1,2".into());
480    /// let opts = UpdateOptions::default();
481    /// table.update(data, opts).await?;
482    /// ```  
483    pub async fn update(&self, input: UpdateData, options: UpdateOptions) -> ClientResult<()> {
484        let msg = self.client_message(ClientReq::TableUpdateReq(TableUpdateReq {
485            data: Some(input.into()),
486            port_id: options.port_id.unwrap_or(0),
487        }));
488
489        match self.client.oneshot(&msg).await? {
490            ClientResp::TableUpdateResp(_) => Ok(()),
491            resp => Err(resp.into()),
492        }
493    }
494
495    /// Validates the given expressions.
496    pub async fn validate_expressions(
497        &self,
498        expressions: Expressions,
499    ) -> ClientResult<ExprValidationResult> {
500        let msg = self.client_message(ClientReq::TableValidateExprReq(TableValidateExprReq {
501            column_to_expr: expressions.0,
502        }));
503
504        match self.client.oneshot(&msg).await? {
505            ClientResp::TableValidateExprResp(result) => Ok(ExprValidationResult {
506                errors: result.errors,
507                expression_alias: result.expression_alias,
508                expression_schema: result
509                    .expression_schema
510                    .into_iter()
511                    .map(|(x, y)| (x, ColumnType::try_from(y).unwrap()))
512                    .collect(),
513            }),
514            resp => Err(resp.into()),
515        }
516    }
517
518    /// Create a new [`View`] from this table with a specified
519    /// [`ViewConfigUpdate`].
520    ///
521    /// See [`View`] struct.
522    ///
523    /// # Examples
524    ///
525    /// ```rust
526    /// use crate::config::*;
527    /// let view = table
528    ///     .view(Some(ViewConfigUpdate {
529    ///         columns: Some(vec![Some("Sales".into())]),
530    ///         aggregates: Some(HashMap::from_iter(vec![("Sales".into(), "sum".into())])),
531    ///         group_by: Some(vec!["Region".into(), "Country".into()]),
532    ///         filter: Some(vec![Filter::new("Category", "in", &[
533    ///             "Furniture",
534    ///             "Technology",
535    ///         ])]),
536    ///         ..ViewConfigUpdate::default()
537    ///     }))
538    ///     .await?;
539    /// ```
540    pub async fn view(&self, config: Option<ViewConfigUpdate>) -> ClientResult<View> {
541        let view_name = randid();
542        let msg = Request {
543            msg_id: self.client.gen_id(),
544            entity_id: self.name.clone(),
545            client_req: ClientReq::TableMakeViewReq(TableMakeViewReq {
546                view_id: view_name.clone(),
547                config: config.map(|x| x.into()),
548            })
549            .into(),
550        };
551
552        match self.client.oneshot(&msg).await? {
553            ClientResp::TableMakeViewResp(TableMakeViewResp { view_id })
554                if view_id == view_name =>
555            {
556                Ok(View::new(view_name, self.client.clone()))
557            },
558            resp => Err(resp.into()),
559        }
560    }
561}