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 to validate an expression against a table.
87    #[wasm_bindgen(js_name = "tableValidateExpression")]
88    pub fn table_validate_expression(
89        &self,
90        table_id: &str,
91        expression: &str,
92    ) -> Result<String, JsValue> {
93        self.inner
94            .table_validate_expression(table_id, expression)
95            .map_err(|e| JsValue::from_str(&e.to_string()))
96    }
97
98    /// Returns the SQL query to delete a view.
99    #[wasm_bindgen(js_name = "viewDelete")]
100    pub fn view_delete(&self, view_id: &str) -> Result<String, JsValue> {
101        self.inner
102            .view_delete(view_id)
103            .map_err(|e| JsValue::from_str(&e.to_string()))
104    }
105
106    /// Returns the SQL query to create a view from a table with the given
107    /// configuration.
108    #[wasm_bindgen(js_name = "tableMakeView")]
109    pub fn table_make_view(
110        &self,
111        table_id: &str,
112        view_id: &str,
113        config: JsValue,
114    ) -> Result<String, JsValue> {
115        let config: ViewConfig = serde_wasm_bindgen::from_value(config)
116            .map_err(|e| JsValue::from_str(&e.to_string()))?;
117
118        self.inner
119            .table_make_view(table_id, view_id, &config)
120            .map_err(|e| JsValue::from_str(&e.to_string()))
121    }
122
123    /// Returns the SQL query to fetch data from a view with the given viewport.
124    #[wasm_bindgen(js_name = "viewGetData")]
125    pub fn view_get_data(
126        &self,
127        view_id: &str,
128        config: JsValue,
129        viewport: JsValue,
130        schema: JsValue,
131    ) -> Result<String, JsValue> {
132        let config: ViewConfig = serde_wasm_bindgen::from_value(config)
133            .map_err(|e| JsValue::from_str(&e.to_string()))?;
134
135        let viewport: ViewPort = serde_wasm_bindgen::from_value(viewport)
136            .map_err(|e| JsValue::from_str(&e.to_string()))?;
137
138        let schema = self.parse_schema(schema)?;
139
140        self.inner
141            .view_get_data(view_id, &config, &viewport, &schema)
142            .map_err(|e| JsValue::from_str(&e.to_string()))
143    }
144
145    /// Returns the SQL query to describe a view's schema.
146    #[wasm_bindgen(js_name = "viewSchema")]
147    pub fn view_schema(&self, view_id: &str) -> Result<String, JsValue> {
148        self.inner
149            .view_schema(view_id)
150            .map_err(|e| JsValue::from_str(&e.to_string()))
151    }
152
153    /// Returns the SQL query to get the row count of a view.
154    #[wasm_bindgen(js_name = "viewSize")]
155    pub fn view_size(&self, view_id: &str) -> Result<String, JsValue> {
156        self.inner
157            .view_size(view_id)
158            .map_err(|e| JsValue::from_str(&e.to_string()))
159    }
160}
161
162impl GenericSQLVirtualServerModel {
163    fn parse_schema(&self, schema: JsValue) -> Result<IndexMap<String, ColumnType>, JsValue> {
164        let obj = schema.dyn_ref::<Object>().ok_or_else(|| {
165            JsValue::from_str("Schema must be an object mapping column names to types")
166        })?;
167
168        let mut result = IndexMap::new();
169        let entries = Object::entries(obj);
170        for i in 0..entries.length() {
171            let entry = entries.get(i);
172            let entry_array = entry
173                .dyn_ref::<js_sys::Array>()
174                .ok_or_else(|| JsValue::from_str("Invalid schema entry"))?;
175            let key = entry_array
176                .get(0)
177                .as_string()
178                .ok_or_else(|| JsValue::from_str("Column name must be a string"))?;
179            let value = entry_array
180                .get(1)
181                .as_string()
182                .ok_or_else(|| JsValue::from_str("Column type must be a string"))?;
183            let column_type = ColumnType::from_str(&value)
184                .map_err(|_| JsValue::from_str(&format!("Unknown column type: {}", value)))?;
185            result.insert(key, column_type);
186        }
187        Ok(result)
188    }
189}