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        schema: JsValue,
115    ) -> Result<String, JsValue> {
116        let config: ViewConfig = serde_wasm_bindgen::from_value(config)
117            .map_err(|e| JsValue::from_str(&e.to_string()))?;
118
119        let schema = if schema.is_undefined() || schema.is_null() {
120            IndexMap::new()
121        } else {
122            self.parse_schema(schema)?
123        };
124
125        self.inner
126            .table_make_view(table_id, view_id, &config, &schema)
127            .map_err(|e| JsValue::from_str(&e.to_string()))
128    }
129
130    /// Returns the SQL query to fetch data from a view with the given viewport.
131    #[wasm_bindgen(js_name = "viewGetData")]
132    pub fn view_get_data(
133        &self,
134        view_id: &str,
135        config: JsValue,
136        viewport: JsValue,
137        schema: JsValue,
138    ) -> Result<String, JsValue> {
139        let config: ViewConfig = serde_wasm_bindgen::from_value(config)
140            .map_err(|e| JsValue::from_str(&e.to_string()))?;
141
142        let viewport: ViewPort = serde_wasm_bindgen::from_value(viewport)
143            .map_err(|e| JsValue::from_str(&e.to_string()))?;
144
145        let schema = self.parse_schema(schema)?;
146
147        self.inner
148            .view_get_data(view_id, &config, &viewport, &schema)
149            .map_err(|e| JsValue::from_str(&e.to_string()))
150    }
151
152    /// Returns the SQL query to describe a view's schema.
153    #[wasm_bindgen(js_name = "viewSchema")]
154    pub fn view_schema(&self, view_id: &str) -> Result<String, JsValue> {
155        self.inner
156            .view_schema(view_id)
157            .map_err(|e| JsValue::from_str(&e.to_string()))
158    }
159
160    /// Returns the SQL query to get the row count of a view.
161    #[wasm_bindgen(js_name = "viewSize")]
162    pub fn view_size(&self, view_id: &str) -> Result<String, JsValue> {
163        self.inner
164            .view_size(view_id)
165            .map_err(|e| JsValue::from_str(&e.to_string()))
166    }
167
168    /// Returns the SQL query to get the min and max values of a column.
169    #[wasm_bindgen(js_name = "viewGetMinMax")]
170    pub fn view_get_min_max(
171        &self,
172        view_id: &str,
173        column_name: &str,
174        config: JsValue,
175    ) -> Result<String, JsValue> {
176        let config: ViewConfig = serde_wasm_bindgen::from_value(config)
177            .map_err(|e| JsValue::from_str(&e.to_string()))?;
178
179        self.inner
180            .view_get_min_max(view_id, column_name, &config)
181            .map_err(|e| JsValue::from_str(&e.to_string()))
182    }
183}
184
185impl GenericSQLVirtualServerModel {
186    fn parse_schema(&self, schema: JsValue) -> Result<IndexMap<String, ColumnType>, JsValue> {
187        let obj = schema.dyn_ref::<Object>().ok_or_else(|| {
188            JsValue::from_str("Schema must be an object mapping column names to types")
189        })?;
190
191        let mut result = IndexMap::new();
192        let entries = Object::entries(obj);
193        for i in 0..entries.length() {
194            let entry = entries.get(i);
195            let entry_array = entry
196                .dyn_ref::<js_sys::Array>()
197                .ok_or_else(|| JsValue::from_str("Invalid schema entry"))?;
198            let key = entry_array
199                .get(0)
200                .as_string()
201                .ok_or_else(|| JsValue::from_str("Column name must be a string"))?;
202            let value = entry_array
203                .get(1)
204                .as_string()
205                .ok_or_else(|| JsValue::from_str("Column type must be a string"))?;
206            let column_type = ColumnType::from_str(&value)
207                .map_err(|_| JsValue::from_str(&format!("Unknown column type: {}", value)))?;
208            result.insert(key, column_type);
209        }
210        Ok(result)
211    }
212}