Skip to main content

perspective_js/
generic_sql_model.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
13//! WASM bindings for the DuckDB SQL query builder.
14
15use std::str::FromStr;
16
17use indexmap::IndexMap;
18use js_sys::Object;
19use perspective_client::config::ViewConfig;
20use perspective_client::proto::{ColumnType, ViewPort};
21use perspective_client::virtual_server;
22use wasm_bindgen::prelude::*;
23
24use crate::utils::*;
25
26/// JavaScript-facing DuckDB SQL query builder.
27///
28/// This struct wraps the Rust `DuckDBSqlBuilder` and exposes it to JavaScript
29/// via wasm_bindgen.
30#[wasm_bindgen]
31pub struct GenericSQLVirtualServerModel {
32    inner: virtual_server::GenericSQLVirtualServerModel,
33}
34
35#[wasm_bindgen]
36extern "C" {
37    pub type JsGenericSQLVirtualServerModelArgs;
38}
39
40#[wasm_bindgen]
41impl GenericSQLVirtualServerModel {
42    /// Creates a new `JsDuckDBSqlBuilder` instance.
43    #[wasm_bindgen(constructor)]
44    pub fn new(args: Option<JsGenericSQLVirtualServerModelArgs>) -> Result<Self, JsValue> {
45        Ok(Self {
46            inner: virtual_server::GenericSQLVirtualServerModel::new(
47                args.map(|x| x.into_serde_ext())
48                    .transpose()?
49                    .unwrap_or_default(),
50            ),
51        })
52    }
53
54    /// Returns the SQL query to list all hosted tables.
55    #[wasm_bindgen(js_name = "getHostedTables")]
56    pub fn get_hosted_tables(&self) -> Result<String, JsValue> {
57        self.inner
58            .get_hosted_tables()
59            .map_err(|e| JsValue::from_str(&e.to_string()))
60    }
61
62    /// Returns the SQL query to describe a table's schema.
63    #[wasm_bindgen(js_name = "tableSchema")]
64    pub fn table_schema(&self, table_id: &str) -> Result<String, JsValue> {
65        self.inner
66            .table_schema(table_id)
67            .map_err(|e| JsValue::from_str(&e.to_string()))
68    }
69
70    /// Returns the SQL query to get the row count of a table.
71    #[wasm_bindgen(js_name = "tableSize")]
72    pub fn table_size(&self, table_id: &str) -> Result<String, JsValue> {
73        self.inner
74            .table_size(table_id)
75            .map_err(|e| JsValue::from_str(&e.to_string()))
76    }
77
78    /// Returns the SQL query to get the column count of a view.
79    #[wasm_bindgen(js_name = "viewColumnSize")]
80    pub fn view_column_size(&self, view_id: &str) -> Result<String, JsValue> {
81        self.inner
82            .view_column_size(view_id)
83            .map_err(|e| JsValue::from_str(&e.to_string()))
84    }
85
86    /// Returns the SQL query that plans every expression of `config` at once
87    /// against a table, one result column per expression alias, or `undefined`
88    /// when the config has no expressions.
89    #[wasm_bindgen(js_name = "expressionsDescribe")]
90    pub fn expressions_describe(
91        &self,
92        table_id: &str,
93        config: JsValue,
94    ) -> Result<Option<String>, JsValue> {
95        let config: ViewConfig = serde_wasm_bindgen::from_value(config)
96            .map_err(|e| JsValue::from_str(&e.to_string()))?;
97
98        self.inner
99            .expressions_describe(table_id, &config)
100            .map_err(|e| JsValue::from_str(&e.to_string()))
101    }
102
103    /// Returns the SQL query that plans ONE expression against a table - for
104    /// attributing a failed `expressionsDescribe` to the expressions at fault.
105    #[wasm_bindgen(js_name = "expressionDescribe")]
106    pub fn expression_describe(&self, table_id: &str, expression: &str) -> Result<String, JsValue> {
107        self.inner
108            .expression_describe(table_id, expression)
109            .map_err(|e| JsValue::from_str(&e.to_string()))
110    }
111
112    /// Returns the SQL query that plans the view `config` would build and
113    /// reports its result columns without materializing it (`split_by` folded
114    /// into `group_by`, no `PIVOT`), or `undefined` when the config selects no
115    /// columns.
116    #[wasm_bindgen(js_name = "tableDescribe")]
117    pub fn table_describe(
118        &self,
119        table_id: &str,
120        config: JsValue,
121        schema: JsValue,
122    ) -> Result<Option<String>, JsValue> {
123        let config: ViewConfig = serde_wasm_bindgen::from_value(config)
124            .map_err(|e| JsValue::from_str(&e.to_string()))?;
125
126        let schema = if schema.is_undefined() || schema.is_null() {
127            IndexMap::new()
128        } else {
129            self.parse_schema(schema)?
130        };
131
132        self.inner
133            .table_describe(table_id, &config, &schema)
134            .map_err(|e| JsValue::from_str(&e.to_string()))
135    }
136
137    /// Returns the SQL query to delete a view.
138    #[wasm_bindgen(js_name = "viewDelete")]
139    pub fn view_delete(&self, view_id: &str) -> Result<String, JsValue> {
140        self.inner
141            .view_delete(view_id)
142            .map_err(|e| JsValue::from_str(&e.to_string()))
143    }
144
145    /// Returns the SQL query to create a view from a table with the given
146    /// configuration.
147    #[wasm_bindgen(js_name = "tableMakeView")]
148    pub fn table_make_view(
149        &self,
150        table_id: &str,
151        view_id: &str,
152        config: JsValue,
153        schema: JsValue,
154    ) -> Result<String, JsValue> {
155        let config: ViewConfig = serde_wasm_bindgen::from_value(config)
156            .map_err(|e| JsValue::from_str(&e.to_string()))?;
157
158        let schema = if schema.is_undefined() || schema.is_null() {
159            IndexMap::new()
160        } else {
161            self.parse_schema(schema)?
162        };
163
164        self.inner
165            .table_make_view(table_id, view_id, &config, &schema)
166            .map_err(|e| JsValue::from_str(&e.to_string()))
167    }
168
169    /// Returns the SQL query to create a table from a view.
170    #[wasm_bindgen(js_name = "viewMakeTable")]
171    pub fn view_make_table(
172        &self,
173        view_id: &str,
174        table_id: &str,
175        config: JsValue,
176        view_schema: JsValue,
177        schema: JsValue,
178    ) -> Result<String, JsValue> {
179        let config: ViewConfig = serde_wasm_bindgen::from_value(config)
180            .map_err(|e| JsValue::from_str(&e.to_string()))?;
181
182        let view_schema = self.parse_schema(view_schema)?;
183        let schema = if schema.is_undefined() || schema.is_null() {
184            None
185        } else {
186            Some(self.parse_schema(schema)?)
187        };
188
189        self.inner
190            .view_make_table(view_id, table_id, &config, &view_schema, schema.as_ref())
191            .map_err(|e| JsValue::from_str(&e.to_string()))
192    }
193
194    /// Returns the SQL query to fetch data from a view with the given viewport.
195    #[wasm_bindgen(js_name = "viewGetData")]
196    pub fn view_get_data(
197        &self,
198        view_id: &str,
199        config: JsValue,
200        viewport: JsValue,
201        schema: JsValue,
202    ) -> Result<String, JsValue> {
203        let config: ViewConfig = serde_wasm_bindgen::from_value(config)
204            .map_err(|e| JsValue::from_str(&e.to_string()))?;
205
206        let viewport: ViewPort = serde_wasm_bindgen::from_value(viewport)
207            .map_err(|e| JsValue::from_str(&e.to_string()))?;
208
209        let schema = self.parse_schema(schema)?;
210
211        self.inner
212            .view_get_data(view_id, &config, &viewport, &schema)
213            .map_err(|e| JsValue::from_str(&e.to_string()))
214    }
215
216    /// Returns the SQL query to describe a view's schema.
217    #[wasm_bindgen(js_name = "viewSchema")]
218    pub fn view_schema(&self, view_id: &str) -> Result<String, JsValue> {
219        self.inner
220            .view_schema(view_id)
221            .map_err(|e| JsValue::from_str(&e.to_string()))
222    }
223
224    /// Returns the SQL query to get the row count of a view.
225    #[wasm_bindgen(js_name = "viewSize")]
226    pub fn view_size(&self, view_id: &str) -> Result<String, JsValue> {
227        self.inner
228            .view_size(view_id)
229            .map_err(|e| JsValue::from_str(&e.to_string()))
230    }
231
232    /// Returns the SQL query to get the min and max values of a column.
233    #[wasm_bindgen(js_name = "viewGetMinMax")]
234    pub fn view_get_min_max(
235        &self,
236        view_id: &str,
237        column_name: &str,
238        config: JsValue,
239    ) -> Result<String, JsValue> {
240        let config: ViewConfig = serde_wasm_bindgen::from_value(config)
241            .map_err(|e| JsValue::from_str(&e.to_string()))?;
242
243        self.inner
244            .view_get_min_max(view_id, column_name, &config)
245            .map_err(|e| JsValue::from_str(&e.to_string()))
246    }
247}
248
249impl GenericSQLVirtualServerModel {
250    fn parse_schema(&self, schema: JsValue) -> Result<IndexMap<String, ColumnType>, JsValue> {
251        let obj = schema.dyn_ref::<Object>().ok_or_else(|| {
252            JsValue::from_str("Schema must be an object mapping column names to types")
253        })?;
254
255        let mut result = IndexMap::new();
256        let entries = Object::entries(obj);
257        for i in 0..entries.length() {
258            let entry = entries.get(i);
259            let entry_array = entry
260                .dyn_ref::<js_sys::Array>()
261                .ok_or_else(|| JsValue::from_str("Invalid schema entry"))?;
262            let key = entry_array
263                .get(0)
264                .as_string()
265                .ok_or_else(|| JsValue::from_str("Column name must be a string"))?;
266            let value = entry_array
267                .get(1)
268                .as_string()
269                .ok_or_else(|| JsValue::from_str("Column type must be a string"))?;
270            let column_type = ColumnType::from_str(&value)
271                .map_err(|_| JsValue::from_str(&format!("Unknown column type: {}", value)))?;
272            result.insert(key, column_type);
273        }
274        Ok(result)
275    }
276}