perspective_js/
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 js_sys::Function;
14use perspective_client::config::*;
15use perspective_client::{DeleteOptions, UpdateData, UpdateOptions, assert_table_api};
16use wasm_bindgen::prelude::*;
17use wasm_bindgen_derive::TryFromJsValue;
18use wasm_bindgen_futures::spawn_local;
19
20use crate::Client;
21use crate::table_data::UpdateDataExt;
22use crate::utils::{ApiFuture, ApiResult, JsValueSerdeExt, LocalPollLoop};
23pub use crate::view::*;
24
25#[derive(TryFromJsValue, Clone, PartialEq)]
26#[wasm_bindgen]
27pub struct Table(pub(crate) perspective_client::Table);
28
29assert_table_api!(Table);
30
31impl From<perspective_client::Table> for Table {
32    fn from(value: perspective_client::Table) -> Self {
33        Table(value)
34    }
35}
36
37/// [`Table`] is Perspective's columnar data frame, analogous to a Pandas/Polars
38/// `DataFrame` or Apache Arrow, supporting append & in-place updates, removal
39/// by index, and update notifications.
40///
41/// A [`Table`] contains columns, each of which have a unique name, are strongly
42/// and consistently typed, and contains rows of data conforming to the column's
43/// type. Each column in a [`Table`] must have the same number of rows, though
44/// not every row must contain data; null-values are used to indicate missing
45/// values in the dataset. The schema of a [`Table`] is _immutable after
46/// creation_, which means the column names and data types cannot be changed
47/// after the [`Table`] has been created. Columns cannot be added or deleted
48/// after creation either, but a [`View`] can be used to select an arbitrary set
49/// of columns from the [`Table`].
50impl Table {
51    pub fn get_table(&self) -> &'_ perspective_client::Table {
52        &self.0
53    }
54}
55
56#[wasm_bindgen]
57extern "C" {
58    // TODO Fix me
59    #[wasm_bindgen(typescript_type = "\
60        string | ArrayBuffer | Record<string, unknown[]> | Record<string, unknown>[]")]
61    pub type JsTableInitData;
62
63    #[wasm_bindgen(typescript_type = "ViewConfigUpdate")]
64    pub type JsViewConfig;
65
66    #[wasm_bindgen(typescript_type = "UpdateOptions")]
67    pub type JsUpdateOptions;
68
69    #[wasm_bindgen(typescript_type = "DeleteOptions")]
70    pub type JsDeleteOptions;
71}
72
73#[wasm_bindgen]
74impl Table {
75    /// Returns the name of the index column for the table.
76    ///
77    /// # JavaScript Examples
78    ///
79    /// ```javascript
80    /// const table = await client.table("x,y\n1,2\n3,4", { index: "x" });
81    /// const index = table.get_index(); // "x"
82    /// ```
83    #[wasm_bindgen]
84    pub async fn get_index(&self) -> Option<String> {
85        self.0.get_index()
86    }
87
88    /// Get a copy of the [`Client`] this [`Table`] came from.
89    #[wasm_bindgen]
90    pub async fn get_client(&self) -> Client {
91        Client {
92            close: None,
93            client: self.0.get_client(),
94        }
95    }
96
97    /// Returns the user-specified name for this table, or the auto-generated
98    /// name if a name was not specified when the table was created.
99    #[wasm_bindgen]
100    pub async fn get_name(&self) -> String {
101        self.0.get_name().to_owned()
102    }
103
104    /// Returns the user-specified row limit for this table.
105    #[wasm_bindgen]
106    pub async fn get_limit(&self) -> Option<u32> {
107        self.0.get_limit()
108    }
109
110    /// Removes all the rows in the [`Table`], but preserves everything else
111    /// including the schema, index, and any callbacks or registered
112    /// [`View`] instances.
113    ///
114    /// Calling [`Table::clear`], like [`Table::update`] and [`Table::remove`],
115    /// will trigger an update event to any registered listeners via
116    /// [`View::on_update`].
117    #[wasm_bindgen]
118    pub async fn clear(&self) -> ApiResult<()> {
119        self.0.clear().await?;
120        Ok(())
121    }
122
123    /// Delete this [`Table`] and cleans up associated resources.
124    ///
125    /// [`Table`]s do not stop consuming resources or processing updates when
126    /// they are garbage collected in their host language - you must call
127    /// this method to reclaim these.
128    ///
129    /// # Arguments
130    ///
131    /// - `options` An options dictionary.
132    ///     - `lazy` Whether to delete this [`Table`] _lazily_. When false (the
133    ///       default), the delete will occur immediately, assuming it has no
134    ///       [`View`] instances registered to it (which must be deleted first,
135    ///       otherwise this method will throw an error). When true, the
136    ///       [`Table`] will only be marked for deltion once its [`View`]
137    ///       dependency count reaches 0.
138    ///
139    /// # JavaScript Examples
140    ///
141    /// ```javascript
142    /// const table = await client.table("x,y\n1,2\n3,4");
143    ///
144    /// // ...
145    ///
146    /// await table.delete({ lazy: true });
147    /// ```
148    #[wasm_bindgen]
149    pub async fn delete(self, options: Option<JsDeleteOptions>) -> ApiResult<()> {
150        let options = options
151            .into_serde_ext::<Option<DeleteOptions>>()?
152            .unwrap_or_default();
153
154        self.0.delete(options).await?;
155        Ok(())
156    }
157
158    /// Returns the number of rows in a [`Table`].
159    #[wasm_bindgen]
160    pub async fn size(&self) -> ApiResult<f64> {
161        Ok(self.0.size().await? as f64)
162    }
163
164    /// Returns a table's [`Schema`], a mapping of column names to column types.
165    ///
166    /// The mapping of a [`Table`]'s column names to data types is referred to
167    /// as a [`Schema`]. Each column has a unique name and a data type, one
168    /// of:
169    ///
170    /// - `"boolean"` - A boolean type
171    /// - `"date"` - A timesonze-agnostic date type (month/day/year)
172    /// - `"datetime"` - A millisecond-precision datetime type in the UTC
173    ///   timezone
174    /// - `"float"` - A 64 bit float
175    /// - `"integer"` - A signed 32 bit integer (the integer type supported by
176    ///   JavaScript)
177    /// - `"string"` - A [`String`] data type (encoded internally as a
178    ///   _dictionary_)
179    ///
180    /// Note that all [`Table`] columns are _nullable_, regardless of the data
181    /// type.
182    #[wasm_bindgen]
183    pub async fn schema(&self) -> ApiResult<JsValue> {
184        let schema = self.0.schema().await?;
185        Ok(JsValue::from_serde_ext(&schema)?)
186    }
187
188    /// Returns the column names of this [`Table`] in "natural" order (the
189    /// ordering implied by the input format).
190    ///  
191    ///  # JavaScript Examples
192    ///
193    ///  ```javascript
194    ///  const columns = await table.columns();
195    ///  ```   
196    #[wasm_bindgen]
197    pub async fn columns(&self) -> ApiResult<JsValue> {
198        let columns = self.0.columns().await?;
199        Ok(JsValue::from_serde_ext(&columns)?)
200    }
201
202    /// Create a unique channel ID on this [`Table`], which allows
203    /// `View::on_update` callback calls to be associated with the
204    /// `Table::update` which caused them.
205    #[wasm_bindgen]
206    pub async fn make_port(&self) -> ApiResult<i32> {
207        Ok(self.0.make_port().await?)
208    }
209
210    /// Register a callback which is called exactly once, when this [`Table`] is
211    /// deleted with the [`Table::delete`] method.
212    ///
213    /// [`Table::on_delete`] resolves when the subscription message is sent, not
214    /// when the _delete_ event occurs.
215    #[wasm_bindgen]
216    pub fn on_delete(&self, on_delete: Function) -> ApiFuture<u32> {
217        let table = self.clone();
218        ApiFuture::new(async move {
219            let emit = LocalPollLoop::new(move |()| on_delete.call0(&JsValue::UNDEFINED));
220            let on_delete = Box::new(move || spawn_local(emit.poll(())));
221            Ok(table.0.on_delete(on_delete).await?)
222        })
223    }
224
225    /// Removes a listener with a given ID, as returned by a previous call to
226    /// [`Table::on_delete`].
227    #[wasm_bindgen]
228    pub fn remove_delete(&self, callback_id: u32) -> ApiFuture<()> {
229        let client = self.0.clone();
230        ApiFuture::new(async move {
231            client.remove_delete(callback_id).await?;
232            Ok(())
233        })
234    }
235
236    /// Removes rows from this [`Table`] with the `index` column values
237    /// supplied.
238    ///
239    /// # Arguments
240    ///
241    /// - `indices` - A list of `index` column values for rows that should be
242    ///   removed.
243    ///
244    /// # JavaScript Examples
245    ///
246    /// ```javascript
247    /// await table.remove([1, 2, 3]);
248    /// ```
249    #[wasm_bindgen]
250    pub async fn remove(&self, value: &JsValue, options: Option<JsUpdateOptions>) -> ApiResult<()> {
251        let options = options
252            .into_serde_ext::<Option<UpdateOptions>>()?
253            .unwrap_or_default();
254
255        let input = UpdateData::from_js_value(value, options.format)?;
256        self.0.remove(input).await?;
257        Ok(())
258    }
259
260    /// Replace all rows in this [`Table`] with the input data, coerced to this
261    /// [`Table`]'s existing [`perspective_client::Schema`], notifying any
262    /// derived [`View`] and [`View::on_update`] callbacks.
263    ///
264    /// Calling [`Table::replace`] is an easy way to replace _all_ the data in a
265    /// [`Table`] without losing any derived [`View`] instances or
266    /// [`View::on_update`] callbacks. [`Table::replace`] does _not_ infer
267    /// data types like [`Client::table`] does, rather it _coerces_ input
268    /// data to the `Schema` like [`Table::update`]. If you need a [`Table`]
269    /// with a different `Schema`, you must create a new one.
270    ///
271    /// # JavaScript Examples
272    ///
273    /// ```javascript
274    /// await table.replace("x,y\n1,2");
275    /// ```
276    #[wasm_bindgen]
277    pub async fn replace(
278        &self,
279        input: &JsValue,
280        options: Option<JsUpdateOptions>,
281    ) -> ApiResult<()> {
282        let options = options
283            .into_serde_ext::<Option<UpdateOptions>>()?
284            .unwrap_or_default();
285
286        let input = UpdateData::from_js_value(input, options.format)?;
287        self.0.replace(input).await?;
288        Ok(())
289    }
290
291    /// Updates the rows of this table and any derived [`View`] instances.
292    ///
293    /// Calling [`Table::update`] will trigger the [`View::on_update`] callbacks
294    /// register to derived [`View`], and the call itself will not resolve until
295    /// _all_ derived [`View`]'s are notified.
296    ///
297    /// When updating a [`Table`] with an `index`, [`Table::update`] supports
298    /// partial updates, by omitting columns from the update data.
299    ///
300    /// # Arguments
301    ///
302    /// - `input` - The input data for this [`Table`]. The schema of a [`Table`]
303    ///   is immutable after creation, so this method cannot be called with a
304    ///   schema.
305    /// - `options` - Options for this update step - see [`UpdateOptions`].
306    ///
307    /// # JavaScript Examples
308    ///
309    /// ```javascript
310    /// await table.update("x,y\n1,2");
311    /// ```
312    #[wasm_bindgen]
313    pub fn update(
314        &self,
315        input: JsTableInitData,
316        options: Option<JsUpdateOptions>,
317    ) -> ApiFuture<()> {
318        let table = self.clone();
319        ApiFuture::new(async move {
320            let options = options
321                .into_serde_ext::<Option<UpdateOptions>>()?
322                .unwrap_or_default();
323
324            let input = UpdateData::from_js_value(&input, options.format)?;
325            Ok(table.0.update(input, options).await?)
326        })
327    }
328
329    /// Create a new [`View`] from this table with a specified
330    /// [`ViewConfigUpdate`].
331    ///
332    /// See [`View`] struct.
333    ///
334    /// # JavaScript Examples
335    ///
336    /// ```javascript
337    /// const view = await table.view({
338    ///     columns: ["Sales"],
339    ///     aggregates: { Sales: "sum" },
340    ///     group_by: ["Region", "Country"],
341    ///     filter: [["Category", "in", ["Furniture", "Technology"]]],
342    /// });
343    /// ```
344    #[wasm_bindgen]
345    pub async fn view(&self, config: Option<JsViewConfig>) -> ApiResult<View> {
346        let config = config
347            .map(|config| js_sys::JSON::stringify(&config))
348            .transpose()?
349            .and_then(|x| x.as_string())
350            .map(|x| serde_json::from_str(x.as_str()))
351            .transpose()?;
352
353        let view = self.0.view(config).await?;
354        Ok(View(view))
355    }
356
357    /// Validates the given expressions.
358    #[wasm_bindgen]
359    pub async fn validate_expressions(&self, exprs: &JsValue) -> ApiResult<JsValue> {
360        let exprs = JsValue::into_serde_ext::<Expressions>(exprs.clone())?;
361        let columns = self.0.validate_expressions(exprs).await?;
362        Ok(JsValue::from_serde_ext(&columns)?)
363    }
364}