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