@perspective-dev/client 4.3.0 → 4.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdn/perspective.js +2 -2
- package/dist/cdn/perspective.js.map +3 -3
- package/dist/esm/perspective.inline.js +2 -2
- package/dist/esm/perspective.inline.js.map +3 -3
- package/dist/esm/perspective.js +2 -2
- package/dist/esm/perspective.js.map +3 -3
- package/dist/esm/perspective.node.d.ts +10 -0
- package/dist/esm/perspective.node.js +99 -12
- package/dist/esm/perspective.node.js.map +2 -2
- package/dist/esm/ts-rs/JoinOptions.d.ts +9 -0
- package/dist/esm/ts-rs/JoinType.d.ts +1 -0
- package/dist/esm/virtual_server.d.ts +7 -0
- package/dist/esm/virtual_servers/clickhouse.js +1 -1
- package/dist/esm/virtual_servers/clickhouse.js.map +3 -3
- package/dist/esm/virtual_servers/duckdb.d.ts +4 -0
- package/dist/esm/virtual_servers/duckdb.js +1 -1
- package/dist/esm/virtual_servers/duckdb.js.map +3 -3
- package/dist/wasm/perspective-js.d.ts +41 -5
- package/dist/wasm/perspective-js.js +89 -10
- package/dist/wasm/perspective-js.wasm +0 -0
- package/dist/wasm/perspective-js.wasm.d.ts +8 -5
- package/package.json +5 -5
- package/src/rust/client.rs +70 -1
- package/src/rust/generic_sql_model.rs +16 -0
- package/src/rust/lib.rs +4 -0
- package/src/rust/utils/errors.rs +4 -0
- package/src/rust/utils/futures.rs +4 -0
- package/src/rust/view.rs +13 -4
- package/src/rust/virtual_server.rs +68 -1
- package/src/ts/perspective.node.ts +21 -2
- package/src/ts/ts-rs/JoinOptions.ts +7 -0
- package/src/ts/ts-rs/JoinType.ts +3 -0
- package/src/ts/virtual_server.ts +5 -0
- package/src/ts/virtual_servers/clickhouse.ts +2 -13
- package/src/ts/virtual_servers/duckdb.ts +18 -64
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@perspective-dev/client",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
"@perspective-dev/metadata": "",
|
|
50
50
|
"@perspective-dev/test": "",
|
|
51
51
|
"@clickhouse/client-web": "^1.12.0",
|
|
52
|
-
"@duckdb/duckdb-wasm": "
|
|
53
|
-
"@playwright/experimental-ct-react": "=1.
|
|
54
|
-
"@playwright/test": "=1.
|
|
52
|
+
"@duckdb/duckdb-wasm": "1.33.1-dev18.0",
|
|
53
|
+
"@playwright/experimental-ct-react": "=1.58.0",
|
|
54
|
+
"@playwright/test": "=1.58.0",
|
|
55
55
|
"@types/node": ">=22",
|
|
56
56
|
"@types/ws": ">=8",
|
|
57
57
|
"@types/stoppable": ">=1",
|
|
58
|
-
"apache-arrow": "
|
|
58
|
+
"apache-arrow": "17.0.0",
|
|
59
59
|
"superstore-arrow": "3.2.0",
|
|
60
60
|
"lodash": "^4.17.20",
|
|
61
61
|
"moment": "^2.30.1",
|
package/src/rust/client.rs
CHANGED
|
@@ -20,7 +20,7 @@ use js_sys::{Function, Uint8Array};
|
|
|
20
20
|
#[cfg(doc)]
|
|
21
21
|
use perspective_client::SystemInfo;
|
|
22
22
|
use perspective_client::{
|
|
23
|
-
ClientError, ReconnectCallback, Session, TableData, TableInitOptions, asyncfn,
|
|
23
|
+
ClientError, ReconnectCallback, Session, TableData, TableInitOptions, TableRef, asyncfn,
|
|
24
24
|
};
|
|
25
25
|
use wasm_bindgen::prelude::*;
|
|
26
26
|
use wasm_bindgen_derive::TryFromJsValue;
|
|
@@ -35,6 +35,35 @@ extern "C" {
|
|
|
35
35
|
#[derive(Clone)]
|
|
36
36
|
#[wasm_bindgen(typescript_type = "TableInitOptions")]
|
|
37
37
|
pub type JsTableInitOptions;
|
|
38
|
+
|
|
39
|
+
#[derive(Clone)]
|
|
40
|
+
#[wasm_bindgen(typescript_type = "JoinOptions")]
|
|
41
|
+
pub type JsJoinOptions;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async fn js_to_table_ref(val: &JsValue) -> ApiResult<TableRef> {
|
|
45
|
+
if let Some(name) = val.as_string() {
|
|
46
|
+
Ok(TableRef::from(name))
|
|
47
|
+
} else {
|
|
48
|
+
let get_name = js_sys::Reflect::get(val, &wasm_bindgen::intern("get_name").into())
|
|
49
|
+
.map_err(|_| apierror!(TableRefError))?
|
|
50
|
+
.dyn_into::<js_sys::Function>()
|
|
51
|
+
.map_err(|_| apierror!(TableRefError))?;
|
|
52
|
+
|
|
53
|
+
let promise = get_name
|
|
54
|
+
.call0(val)
|
|
55
|
+
.map_err(|_| apierror!(TableRefError))?
|
|
56
|
+
.dyn_into::<js_sys::Promise>()
|
|
57
|
+
.map_err(|_| apierror!(TableRefError))?;
|
|
58
|
+
|
|
59
|
+
let name = wasm_bindgen_futures::JsFuture::from(promise)
|
|
60
|
+
.await
|
|
61
|
+
.map_err(|_| apierror!(TableRefError))?
|
|
62
|
+
.as_string()
|
|
63
|
+
.ok_or_else(|| apierror!(TableRefError))?;
|
|
64
|
+
|
|
65
|
+
Ok(TableRef::from(name))
|
|
66
|
+
}
|
|
38
67
|
}
|
|
39
68
|
|
|
40
69
|
#[wasm_bindgen]
|
|
@@ -382,6 +411,46 @@ impl Client {
|
|
|
382
411
|
Ok(Table(self.client.table(args, options).await?))
|
|
383
412
|
}
|
|
384
413
|
|
|
414
|
+
/// Creates a new read-only [`Table`] by performing an INNER JOIN on two
|
|
415
|
+
/// source tables. The resulting table is reactive: when either source
|
|
416
|
+
/// table is updated, the join is automatically recomputed.
|
|
417
|
+
///
|
|
418
|
+
/// # Arguments
|
|
419
|
+
///
|
|
420
|
+
/// - `left` - The left source table (a [`Table`] instance or a table name
|
|
421
|
+
/// string).
|
|
422
|
+
/// - `right` - The right source table (a [`Table`] instance or a table name
|
|
423
|
+
/// string).
|
|
424
|
+
/// - `on` - The column name to join on. Must exist in both tables with the
|
|
425
|
+
/// same type.
|
|
426
|
+
/// - `options` - Optional join configuration: `{ join_type?: "inner" |
|
|
427
|
+
/// "left" | "outer", name?: string }`.
|
|
428
|
+
///
|
|
429
|
+
/// # JavaScript Examples
|
|
430
|
+
///
|
|
431
|
+
/// ```javascript
|
|
432
|
+
/// const joined = await client.join(orders_table, products_table, "Product ID", { join_type: "left" });
|
|
433
|
+
/// const joined = await client.join("orders", "products", "Product ID", { join_type: "left" });
|
|
434
|
+
/// ```
|
|
435
|
+
#[wasm_bindgen]
|
|
436
|
+
pub async fn join(
|
|
437
|
+
&self,
|
|
438
|
+
left: JsValue,
|
|
439
|
+
right: JsValue,
|
|
440
|
+
on: &str,
|
|
441
|
+
options: Option<JsJoinOptions>,
|
|
442
|
+
) -> ApiResult<Table> {
|
|
443
|
+
let options = options
|
|
444
|
+
.into_serde_ext::<Option<perspective_client::JoinOptions>>()?
|
|
445
|
+
.unwrap_or_default();
|
|
446
|
+
|
|
447
|
+
let left_ref = js_to_table_ref(&left).await?;
|
|
448
|
+
let right_ref = js_to_table_ref(&right).await?;
|
|
449
|
+
Ok(Table(
|
|
450
|
+
self.client.join(left_ref, right_ref, on, options).await?,
|
|
451
|
+
))
|
|
452
|
+
}
|
|
453
|
+
|
|
385
454
|
/// Terminates this [`Client`], cleaning up any [`crate::View`] handles the
|
|
386
455
|
/// [`Client`] has open as well as its callbacks.
|
|
387
456
|
#[wasm_bindgen]
|
|
@@ -157,6 +157,22 @@ impl GenericSQLVirtualServerModel {
|
|
|
157
157
|
.view_size(view_id)
|
|
158
158
|
.map_err(|e| JsValue::from_str(&e.to_string()))
|
|
159
159
|
}
|
|
160
|
+
|
|
161
|
+
/// Returns the SQL query to get the min and max values of a column.
|
|
162
|
+
#[wasm_bindgen(js_name = "viewGetMinMax")]
|
|
163
|
+
pub fn view_get_min_max(
|
|
164
|
+
&self,
|
|
165
|
+
view_id: &str,
|
|
166
|
+
column_name: &str,
|
|
167
|
+
config: JsValue,
|
|
168
|
+
) -> Result<String, JsValue> {
|
|
169
|
+
let config: ViewConfig = serde_wasm_bindgen::from_value(config)
|
|
170
|
+
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
|
171
|
+
|
|
172
|
+
self.inner
|
|
173
|
+
.view_get_min_max(view_id, column_name, &config)
|
|
174
|
+
.map_err(|e| JsValue::from_str(&e.to_string()))
|
|
175
|
+
}
|
|
160
176
|
}
|
|
161
177
|
|
|
162
178
|
impl GenericSQLVirtualServerModel {
|
package/src/rust/lib.rs
CHANGED
|
@@ -59,11 +59,15 @@ export type * from "../../src/ts/ts-rs/SystemInfo.d.ts";
|
|
|
59
59
|
export type * from "../../src/ts/ts-rs/SortDir.d.ts";
|
|
60
60
|
export type * from "../../src/ts/ts-rs/Filter.d.ts";
|
|
61
61
|
export type * from "../../src/ts/ts-rs/ViewConfig.d.ts";
|
|
62
|
+
export type * from "../../src/ts/ts-rs/JoinOptions.ts";
|
|
63
|
+
export type * from "../../src/ts/ts-rs/JoinType.ts";
|
|
62
64
|
|
|
63
65
|
import type {ColumnWindow} from "../../src/ts/ts-rs/ColumnWindow.d.ts";
|
|
64
66
|
import type {ColumnType} from "../../src/ts/ts-rs/ColumnType.d.ts";
|
|
65
67
|
import type {ViewWindow} from "../../src/ts/ts-rs/ViewWindow.d.ts";
|
|
66
68
|
import type {TableInitOptions} from "../../src/ts/ts-rs/TableInitOptions.d.ts";
|
|
69
|
+
import type {JoinOptions} from "../../src/ts/ts-rs/JoinOptions.ts";
|
|
70
|
+
import type {JoinType} from "../../src/ts/ts-rs/JoinType.ts";
|
|
67
71
|
import type {ViewConfigUpdate} from "../../src/ts/ts-rs/ViewConfigUpdate.d.ts";
|
|
68
72
|
import type * as on_update_args from "../../src/ts/ts-rs/ViewOnUpdateResp.d.ts";
|
|
69
73
|
import type {OnUpdateOptions} from "../../src/ts/ts-rs/OnUpdateOptions.d.ts";
|
package/src/rust/utils/errors.rs
CHANGED
|
@@ -97,6 +97,9 @@ pub enum ApiErrorType {
|
|
|
97
97
|
#[error("Invalid `expressions` {}", format_valid_exprs(.0))]
|
|
98
98
|
InvalidViewerConfigExpressionsError(Rc<ExprValidationResult>),
|
|
99
99
|
|
|
100
|
+
#[error("Expected a Table or string table name")]
|
|
101
|
+
TableRefError,
|
|
102
|
+
|
|
100
103
|
#[error("No `Table` attached")]
|
|
101
104
|
NoTableError,
|
|
102
105
|
|
|
@@ -134,6 +137,7 @@ impl ApiError {
|
|
|
134
137
|
ApiErrorType::ProstError(_) => "[ProstError]",
|
|
135
138
|
ApiErrorType::InvalidViewerConfigError(..) => "[InvalidViewerConfigError]",
|
|
136
139
|
ApiErrorType::InvalidViewerConfigExpressionsError(_) => "[InvalidViewerConfigError]",
|
|
140
|
+
ApiErrorType::TableRefError => "[TableRefError]",
|
|
137
141
|
ApiErrorType::NoTableError => "[NoTableError]",
|
|
138
142
|
ApiErrorType::SerdeWasmBindgenError(_) => "[SerdeWasmBindgenError]",
|
|
139
143
|
ApiErrorType::Utf8Error(_) => "[FromUtf8Error]",
|
|
@@ -64,6 +64,10 @@ where
|
|
|
64
64
|
pub fn spawn<U: Future<Output = ApiResult<T>> + 'static>(x: U) {
|
|
65
65
|
drop(js_sys::Promise::from(Self::new(x)))
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
pub fn spawn_throttled<U: Future<Output = ApiResult<T>> + 'static>(x: U) {
|
|
69
|
+
drop(js_sys::Promise::from(Self::new_throttled(x)))
|
|
70
|
+
}
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
impl<T> Default for ApiFuture<T>
|
package/src/rust/view.rs
CHANGED
|
@@ -47,6 +47,15 @@ impl From<ViewWindow> for JsViewWindow {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
fn scalar_to_jsvalue(scalar: &perspective_client::config::Scalar) -> JsValue {
|
|
51
|
+
match scalar {
|
|
52
|
+
perspective_client::config::Scalar::Float(x) => JsValue::from_f64(*x),
|
|
53
|
+
perspective_client::config::Scalar::String(x) => JsValue::from_str(x),
|
|
54
|
+
perspective_client::config::Scalar::Bool(x) => JsValue::from_bool(*x),
|
|
55
|
+
perspective_client::config::Scalar::Null => JsValue::NULL,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
/// The [`View`] struct is Perspective's query and serialization interface. It
|
|
51
60
|
/// represents a query on the `Table`'s dataset and is always created from an
|
|
52
61
|
/// existing `Table` instance via the [`Table::view`] method.
|
|
@@ -147,10 +156,10 @@ impl View {
|
|
|
147
156
|
#[wasm_bindgen]
|
|
148
157
|
pub async fn get_min_max(&self, name: String) -> ApiResult<Array> {
|
|
149
158
|
let result = self.0.get_min_max(name).await?;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
159
|
+
let arr = Array::new();
|
|
160
|
+
arr.push(&scalar_to_jsvalue(&result.0));
|
|
161
|
+
arr.push(&scalar_to_jsvalue(&result.1));
|
|
162
|
+
Ok(arr)
|
|
154
163
|
}
|
|
155
164
|
|
|
156
165
|
/// The number of aggregated rows in this [`View`]. This is affected by the
|
|
@@ -18,7 +18,7 @@ use std::str::FromStr;
|
|
|
18
18
|
use std::sync::{Arc, Mutex};
|
|
19
19
|
|
|
20
20
|
use indexmap::IndexMap;
|
|
21
|
-
use js_sys::{Array, Date, Object, Reflect};
|
|
21
|
+
use js_sys::{Array, Date, Object, Reflect, Uint8Array};
|
|
22
22
|
use perspective_client::proto::{ColumnType, HostedTable};
|
|
23
23
|
use perspective_client::virtual_server;
|
|
24
24
|
use perspective_client::virtual_server::{Features, ResultExt, VirtualServerHandler};
|
|
@@ -60,6 +60,20 @@ impl From<serde_wasm_bindgen::Error> for JsError {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
fn jsvalue_to_scalar(val: &JsValue) -> perspective_client::config::Scalar {
|
|
64
|
+
if val.is_null() || val.is_undefined() {
|
|
65
|
+
perspective_client::config::Scalar::Null
|
|
66
|
+
} else if let Some(b) = val.as_bool() {
|
|
67
|
+
perspective_client::config::Scalar::Bool(b)
|
|
68
|
+
} else if let Some(n) = val.as_f64() {
|
|
69
|
+
perspective_client::config::Scalar::Float(n)
|
|
70
|
+
} else if let Some(s) = val.as_string() {
|
|
71
|
+
perspective_client::config::Scalar::String(s)
|
|
72
|
+
} else {
|
|
73
|
+
perspective_client::config::Scalar::Null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
pub struct JsServerHandler(Object);
|
|
64
78
|
|
|
65
79
|
impl JsServerHandler {
|
|
@@ -452,6 +466,48 @@ impl VirtualServerHandler for JsServerHandler {
|
|
|
452
466
|
})
|
|
453
467
|
}
|
|
454
468
|
|
|
469
|
+
fn view_get_min_max(
|
|
470
|
+
&self,
|
|
471
|
+
view_id: &str,
|
|
472
|
+
column_name: &str,
|
|
473
|
+
config: &perspective_client::config::ViewConfig,
|
|
474
|
+
) -> HandlerFuture<
|
|
475
|
+
Result<
|
|
476
|
+
(
|
|
477
|
+
perspective_client::config::Scalar,
|
|
478
|
+
perspective_client::config::Scalar,
|
|
479
|
+
),
|
|
480
|
+
Self::Error,
|
|
481
|
+
>,
|
|
482
|
+
> {
|
|
483
|
+
let has_method = Reflect::get(&self.0, &JsValue::from_str("viewGetMinMax"))
|
|
484
|
+
.map(|val| !val.is_undefined())
|
|
485
|
+
.unwrap_or(false);
|
|
486
|
+
|
|
487
|
+
if !has_method {
|
|
488
|
+
return Box::pin(async {
|
|
489
|
+
Err(JsError(JsValue::from_str("viewGetMinMax not implemented")))
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
let handler = self.0.clone();
|
|
494
|
+
let view_id = view_id.to_string();
|
|
495
|
+
let column_name = column_name.to_string();
|
|
496
|
+
let config_js = serde_wasm_bindgen::to_value(config).unwrap();
|
|
497
|
+
Box::pin(async move {
|
|
498
|
+
let this = JsServerHandler(handler);
|
|
499
|
+
let args = Array::new();
|
|
500
|
+
args.push(&JsValue::from_str(&view_id));
|
|
501
|
+
args.push(&JsValue::from_str(&column_name));
|
|
502
|
+
args.push(&config_js);
|
|
503
|
+
let result = this.call_method_js_async("viewGetMinMax", &args).await?;
|
|
504
|
+
let obj = result.dyn_ref::<Object>().unwrap();
|
|
505
|
+
let min_val = Reflect::get(obj, &JsValue::from_str(wasm_bindgen::intern("min")))?;
|
|
506
|
+
let max_val = Reflect::get(obj, &JsValue::from_str(wasm_bindgen::intern("max")))?;
|
|
507
|
+
Ok((jsvalue_to_scalar(&min_val), jsvalue_to_scalar(&max_val)))
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
|
|
455
511
|
fn view_get_data(
|
|
456
512
|
&self,
|
|
457
513
|
view_id: &str,
|
|
@@ -531,6 +587,17 @@ impl VirtualDataSlice {
|
|
|
531
587
|
)
|
|
532
588
|
}
|
|
533
589
|
|
|
590
|
+
#[wasm_bindgen(js_name = "fromArrowIpc")]
|
|
591
|
+
pub fn from_arrow_ipc(&self, ipc: Uint8Array) -> Result<(), JsValue> {
|
|
592
|
+
self.1
|
|
593
|
+
.lock()
|
|
594
|
+
.unwrap()
|
|
595
|
+
.as_mut()
|
|
596
|
+
.unwrap()
|
|
597
|
+
.from_arrow_ipc(&ipc.to_vec())
|
|
598
|
+
.map_err(|e| JsValue::from_str(&e.to_string()))
|
|
599
|
+
}
|
|
600
|
+
|
|
534
601
|
#[wasm_bindgen(js_name = "setCol")]
|
|
535
602
|
pub fn set_col(
|
|
536
603
|
&self,
|
|
@@ -108,6 +108,7 @@ const CONTENT_TYPES: Record<string, string> = {
|
|
|
108
108
|
".arrow": "arraybuffer",
|
|
109
109
|
".feather": "arraybuffer",
|
|
110
110
|
".wasm": "application/wasm",
|
|
111
|
+
".svg": "image/svg+xml",
|
|
111
112
|
};
|
|
112
113
|
|
|
113
114
|
/**
|
|
@@ -147,9 +148,9 @@ export async function cwd_static_file_handler(
|
|
|
147
148
|
"Access-Control-Allow-Origin": "*",
|
|
148
149
|
});
|
|
149
150
|
if (extname === ".arrow" || extname === ".feather") {
|
|
150
|
-
response.end(content, "
|
|
151
|
+
response.end(content, "utf8");
|
|
151
152
|
} else {
|
|
152
|
-
response.end(content);
|
|
153
|
+
response.end(content, "utf8");
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
return;
|
|
@@ -273,6 +274,23 @@ export function on_error(callback: Function) {
|
|
|
273
274
|
return SYNC_CLIENT.on_error(callback);
|
|
274
275
|
}
|
|
275
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Create a read-only table from a JOIN of two source tables.
|
|
279
|
+
* @param left - The left source table (a Table instance or a table name string).
|
|
280
|
+
* @param right - The right source table (a Table instance or a table name string).
|
|
281
|
+
* @param on
|
|
282
|
+
* @param options - Optional join configuration: { join_type?: "inner"|"left"|"outer", name?: string }
|
|
283
|
+
* @returns
|
|
284
|
+
*/
|
|
285
|
+
export function join(
|
|
286
|
+
left: perspective_client.Table | string,
|
|
287
|
+
right: perspective_client.Table | string,
|
|
288
|
+
on: string,
|
|
289
|
+
options?: perspective_client.JoinOptions,
|
|
290
|
+
) {
|
|
291
|
+
return SYNC_CLIENT.join(left as any, right as any, on, options);
|
|
292
|
+
}
|
|
293
|
+
|
|
276
294
|
/**
|
|
277
295
|
* Create a table from the global Perspective instance.
|
|
278
296
|
* @param init_data
|
|
@@ -356,6 +374,7 @@ export { perspective_client as wasmModule };
|
|
|
356
374
|
|
|
357
375
|
export default {
|
|
358
376
|
table,
|
|
377
|
+
join,
|
|
359
378
|
websocket,
|
|
360
379
|
worker,
|
|
361
380
|
get_hosted_table_names,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
|
2
|
+
import type { JoinType } from "./JoinType.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Options for [`Client::join`].
|
|
6
|
+
*/
|
|
7
|
+
export type JoinOptions = { join_type?: JoinType, name?: string, right_on?: string, };
|
package/src/ts/virtual_server.ts
CHANGED
|
@@ -68,6 +68,11 @@ export interface VirtualServerHandler {
|
|
|
68
68
|
tableId: string,
|
|
69
69
|
expression: string,
|
|
70
70
|
): ColumnType | Promise<ColumnType>;
|
|
71
|
+
viewGetMinMax?(
|
|
72
|
+
viewId: string,
|
|
73
|
+
columnName: string,
|
|
74
|
+
config: ViewConfig,
|
|
75
|
+
): { min: any; max: any } | Promise<{ min: any; max: any }>;
|
|
71
76
|
getFeatures?(): ServerFeatures | Promise<ServerFeatures>;
|
|
72
77
|
makeTable?(
|
|
73
78
|
tableId: string,
|
|
@@ -309,8 +309,6 @@ export class ClickhouseHandler implements perspective.VirtualServerHandler {
|
|
|
309
309
|
viewport: ViewWindow,
|
|
310
310
|
dataSlice: perspective.VirtualDataSlice,
|
|
311
311
|
) {
|
|
312
|
-
const is_group_by = config.group_by?.length > 0;
|
|
313
|
-
const is_split_by = config.split_by?.length > 0;
|
|
314
312
|
const query = this.sqlBuilder.viewGetData(
|
|
315
313
|
viewId,
|
|
316
314
|
config,
|
|
@@ -323,23 +321,14 @@ export class ClickhouseHandler implements perspective.VirtualServerHandler {
|
|
|
323
321
|
});
|
|
324
322
|
|
|
325
323
|
for (let cidx = 0; cidx < columns.length; cidx++) {
|
|
326
|
-
|
|
327
|
-
// This is the grouping_id column, skip it
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
let col = columns[cidx];
|
|
332
|
-
if (is_split_by && !col.startsWith("__ROW_PATH_")) {
|
|
333
|
-
col = col.replaceAll("_", "|");
|
|
334
|
-
}
|
|
335
|
-
|
|
324
|
+
const col = columns[cidx];
|
|
336
325
|
const dtype = duckdbTypeToPsp(dtypes[cidx]) as ColumnType;
|
|
337
326
|
|
|
338
327
|
const isDecimal = dtypes[cidx].startsWith("Decimal");
|
|
339
328
|
for (let ridx = 0; ridx < rows.length; ridx++) {
|
|
340
329
|
const row = rows[ridx];
|
|
341
330
|
const grouping_id = row["__GROUPING_ID__"];
|
|
342
|
-
let value = row[
|
|
331
|
+
let value = row[col];
|
|
343
332
|
if (isDecimal) {
|
|
344
333
|
value = convertDecimalToNumber(value, dtypes[cidx]);
|
|
345
334
|
}
|
|
@@ -119,25 +119,6 @@ function duckdbTypeToPsp(name: string): ColumnType {
|
|
|
119
119
|
return "string";
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
function convertDecimalToNumber(value: any, dtypeString: string) {
|
|
123
|
-
if (!(value instanceof Uint32Array || value instanceof Int32Array)) {
|
|
124
|
-
return value;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
let bigIntValue = BigInt(0);
|
|
128
|
-
for (let i = 0; i < value.length; i++) {
|
|
129
|
-
bigIntValue |= BigInt(value[i]) << BigInt(i * 32);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const scaleMatch = dtypeString.match(/Decimal\[\d+e(\d+)\]/);
|
|
133
|
-
if (scaleMatch) {
|
|
134
|
-
const scale = parseInt(scaleMatch[1]);
|
|
135
|
-
return Number(bigIntValue) / Math.pow(10, scale);
|
|
136
|
-
} else {
|
|
137
|
-
return Number(bigIntValue);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
122
|
async function runQuery(
|
|
142
123
|
db: duckdb.AsyncDuckDBConnection,
|
|
143
124
|
query: string,
|
|
@@ -290,6 +271,20 @@ export class DuckDBHandler implements perspective.VirtualServerHandler {
|
|
|
290
271
|
await runQuery(this.db, query);
|
|
291
272
|
}
|
|
292
273
|
|
|
274
|
+
async viewGetMinMax(
|
|
275
|
+
viewId: string,
|
|
276
|
+
columnName: string,
|
|
277
|
+
config: ViewConfig,
|
|
278
|
+
) {
|
|
279
|
+
const query = this.sqlBuilder.viewGetMinMax(viewId, columnName, config);
|
|
280
|
+
const results = await runQuery(this.db, query);
|
|
281
|
+
const row = results[0].toJSON();
|
|
282
|
+
let [min, max] = Object.values(row);
|
|
283
|
+
if (typeof min === "bigint") min = Number(min);
|
|
284
|
+
if (typeof max === "bigint") max = Number(max);
|
|
285
|
+
return { min: min ?? null, max: max ?? null };
|
|
286
|
+
}
|
|
287
|
+
|
|
293
288
|
async viewGetData(
|
|
294
289
|
viewId: string,
|
|
295
290
|
config: ViewConfig,
|
|
@@ -297,10 +292,6 @@ export class DuckDBHandler implements perspective.VirtualServerHandler {
|
|
|
297
292
|
viewport: ViewWindow,
|
|
298
293
|
dataSlice: perspective.VirtualDataSlice,
|
|
299
294
|
) {
|
|
300
|
-
const is_group_by = config.group_by?.length > 0;
|
|
301
|
-
const is_split_by = config.split_by?.length > 0;
|
|
302
|
-
const is_flat = config.group_rollup_mode === "flat";
|
|
303
|
-
const has_grouping_id = is_group_by && !is_flat;
|
|
304
295
|
const query = this.sqlBuilder.viewGetData(
|
|
305
296
|
viewId,
|
|
306
297
|
config,
|
|
@@ -308,47 +299,10 @@ export class DuckDBHandler implements perspective.VirtualServerHandler {
|
|
|
308
299
|
schema,
|
|
309
300
|
);
|
|
310
301
|
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
for (let cidx = 0; cidx < columns.length; cidx++) {
|
|
316
|
-
if (cidx === 0 && has_grouping_id) {
|
|
317
|
-
// This is the grouping_id column, skip it
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
let col = columns[cidx];
|
|
322
|
-
if (is_split_by && !col.startsWith("__")) {
|
|
323
|
-
col = col.replaceAll("_", "|");
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const dtype = duckdbTypeToPsp(dtypes[cidx]) as ColumnType;
|
|
327
|
-
const isDecimal = dtypes[cidx].startsWith("Decimal");
|
|
328
|
-
for (let ridx = 0; ridx < rows.length; ridx++) {
|
|
329
|
-
const rowArray = rows[ridx].toArray();
|
|
330
|
-
const grouping_id = has_grouping_id
|
|
331
|
-
? Number(rowArray[0])
|
|
332
|
-
: undefined;
|
|
333
|
-
let value = rowArray[cidx];
|
|
334
|
-
if (isDecimal) {
|
|
335
|
-
value = convertDecimalToNumber(value, dtypes[cidx]);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (typeof value === "bigint") {
|
|
339
|
-
value = Number(value);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (typeof value !== "string" && dtype === "string") {
|
|
343
|
-
try {
|
|
344
|
-
value = JSON.stringify(value);
|
|
345
|
-
} catch (e) {
|
|
346
|
-
value = `${value}`;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
302
|
+
const ipc = await this.db.useUnsafe((bindings, conn) =>
|
|
303
|
+
bindings.runQuery(conn, query),
|
|
304
|
+
);
|
|
349
305
|
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
}
|
|
306
|
+
dataSlice.fromArrowIpc(ipc);
|
|
353
307
|
}
|
|
354
308
|
}
|