Skip to content

Commit 97c4894

Browse files
committed
ViewGetMinMaxReq API for Virtual Servers
Signed-off-by: Andrew Stein <steinlink@gmail.com>
1 parent d6e1b56 commit 97c4894

22 files changed

Lines changed: 403 additions & 31 deletions

File tree

rust/perspective-client/perspective.proto

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -462,11 +462,10 @@ message ViewGetMinMaxReq {
462462
}
463463

464464
message ViewGetMinMaxResp {
465-
string min = 1;
466-
string max = 2;
465+
Scalar min = 1;
466+
Scalar max = 2;
467467
}
468468

469-
470469
message ViewExpressionSchemaReq {}
471470
message ViewExpressionSchemaResp {
472471
map<string, ColumnType> schema = 1;

rust/perspective-client/src/rust/view.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,13 +455,20 @@ impl View {
455455
/// # Returns
456456
///
457457
/// A tuple of [min, max], whose types are column and aggregate dependent.
458-
pub async fn get_min_max(&self, column_name: String) -> ClientResult<(String, String)> {
458+
pub async fn get_min_max(
459+
&self,
460+
column_name: String,
461+
) -> ClientResult<(crate::config::Scalar, crate::config::Scalar)> {
459462
let msg = self.client_message(ClientReq::ViewGetMinMaxReq(ViewGetMinMaxReq {
460463
column_name,
461464
}));
462465

463466
match self.client.oneshot(&msg).await? {
464-
ClientResp::ViewGetMinMaxResp(ViewGetMinMaxResp { min, max }) => Ok((min, max)),
467+
ClientResp::ViewGetMinMaxResp(ViewGetMinMaxResp { min, max }) => {
468+
let min = min.map(crate::config::Scalar::from).unwrap_or_default();
469+
let max = max.map(crate::config::Scalar::from).unwrap_or_default();
470+
Ok((min, max))
471+
},
465472
resp => Err(resp.into()),
466473
}
467474
}

rust/perspective-client/src/rust/virtual_server/generic_sql_model.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
1818
// TODO(texodus): Missing these features
1919
//
20-
// - `min_max` API for value-coloring and value-sizing.
21-
//
2220
// - row expand/collapse in the datagrid needs datamodel support, this is likely
2321
// a "collapsed" boolean column in the temp table we `UPDATE`.
2422
//
@@ -287,6 +285,21 @@ impl GenericSQLVirtualServerModel {
287285
Ok(format!("SELECT COUNT(*) FROM {}", view_id))
288286
}
289287

288+
/// Returns the SQL query to get the min and max values of a column.
289+
///
290+
/// # Arguments
291+
/// * `view_id` - The identifier of the view.
292+
/// * `column_name` - The name of the column.
293+
///
294+
/// # Returns
295+
/// SQL: `SELECT MIN("column_name"), MAX("column_name") FROM {view_id}`
296+
pub fn view_get_min_max(&self, view_id: &str, column_name: &str) -> GenericSQLResult<String> {
297+
Ok(format!(
298+
"SELECT MIN(\"{}\"), MAX(\"{}\") FROM {}",
299+
column_name, column_name, view_id
300+
))
301+
}
302+
290303
fn filter_term_to_sql(term: &FilterTerm) -> Option<String> {
291304
match term {
292305
FilterTerm::Scalar(scalar) => Self::scalar_to_sql(scalar),

rust/perspective-client/src/rust/virtual_server/handler.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,18 @@ pub trait VirtualServerHandler {
145145
Box::pin(async { Ok(0) })
146146
}
147147

148+
/// Returns the min and max values of a column in a view.
149+
///
150+
/// Default implementation panics with "not implemented".
151+
fn view_get_min_max(
152+
&self,
153+
_view_id: &str,
154+
_column_name: &str,
155+
) -> VirtualServerFuture<'_, Result<(crate::config::Scalar, crate::config::Scalar), Self::Error>>
156+
{
157+
Box::pin(async { unimplemented!("view_get_min_max not implemented") })
158+
}
159+
148160
// Unused
149161

150162
/// Creates a new table with the given data.

rust/perspective-client/src/rust/virtual_server/server.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ use crate::proto::{
2525
ColumnType, GetFeaturesResp, GetHostedTablesResp, MakeTableResp, Request, Response,
2626
ServerError, TableMakePortResp, TableMakeViewResp, TableOnDeleteResp, TableRemoveDeleteResp,
2727
TableSchemaResp, TableSizeResp, TableValidateExprResp, ViewColumnPathsResp, ViewDeleteResp,
28-
ViewDimensionsResp, ViewExpressionSchemaResp, ViewGetConfigResp, ViewOnDeleteResp,
29-
ViewOnUpdateResp, ViewRemoveDeleteResp, ViewRemoveOnUpdateResp, ViewSchemaResp,
30-
ViewToColumnsStringResp, ViewToRowsStringResp,
28+
ViewDimensionsResp, ViewExpressionSchemaResp, ViewGetConfigResp, ViewGetMinMaxResp,
29+
ViewOnDeleteResp, ViewOnUpdateResp, ViewRemoveDeleteResp, ViewRemoveOnUpdateResp,
30+
ViewSchemaResp, ViewToColumnsStringResp, ViewToRowsStringResp,
3131
};
3232

3333
macro_rules! respond {
@@ -338,6 +338,16 @@ impl<T: VirtualServerHandler> VirtualServer<T> {
338338
.await?;
339339
respond!(msg, MakeTableResp {})
340340
},
341+
ViewGetMinMaxReq(req) => {
342+
let (min, max) = self
343+
.handler
344+
.view_get_min_max(&msg.entity_id, &req.column_name)
345+
.await?;
346+
respond!(msg, ViewGetMinMaxResp {
347+
min: Some(min.into()),
348+
max: Some(max.into()),
349+
})
350+
},
341351

342352
// Stub implementations for callback/update requests that VirtualServer doesn't support
343353
TableOnDeleteReq(_) => {
@@ -361,7 +371,6 @@ impl<T: VirtualServerHandler> VirtualServer<T> {
361371
ViewRemoveDeleteReq(_) => {
362372
respond!(msg, ViewRemoveDeleteResp {})
363373
},
364-
365374
x => {
366375
// Return an error response instead of empty bytes
367376
return Err(VirtualServerError::Other(format!(

rust/perspective-js/src/rust/generic_sql_model.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,14 @@ impl GenericSQLVirtualServerModel {
157157
.view_size(view_id)
158158
.map_err(|e| JsValue::from_str(&e.to_string()))
159159
}
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(&self, view_id: &str, column_name: &str) -> Result<String, JsValue> {
164+
self.inner
165+
.view_get_min_max(view_id, column_name)
166+
.map_err(|e| JsValue::from_str(&e.to_string()))
167+
}
160168
}
161169

162170
impl GenericSQLVirtualServerModel {

rust/perspective-js/src/rust/view.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ impl From<ViewWindow> for JsViewWindow {
4747
}
4848
}
4949

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+
5059
/// The [`View`] struct is Perspective's query and serialization interface. It
5160
/// represents a query on the `Table`'s dataset and is always created from an
5261
/// existing `Table` instance via the [`Table::view`] method.
@@ -147,10 +156,10 @@ impl View {
147156
#[wasm_bindgen]
148157
pub async fn get_min_max(&self, name: String) -> ApiResult<Array> {
149158
let result = self.0.get_min_max(name).await?;
150-
Ok([result.0, result.1]
151-
.iter()
152-
.map(|x| js_sys::JSON::parse(x))
153-
.collect::<Result<_, _>>()?)
159+
let arr = Array::new();
160+
arr.push(&scalar_to_jsvalue(&result.0));
161+
arr.push(&scalar_to_jsvalue(&result.1));
162+
Ok(arr)
154163
}
155164

156165
/// The number of aggregated rows in this [`View`]. This is affected by the

rust/perspective-js/src/rust/virtual_server.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@ impl From<serde_wasm_bindgen::Error> for JsError {
6060
}
6161
}
6262

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+
6377
pub struct JsServerHandler(Object);
6478

6579
impl JsServerHandler {
@@ -452,6 +466,45 @@ impl VirtualServerHandler for JsServerHandler {
452466
})
453467
}
454468

469+
fn view_get_min_max(
470+
&self,
471+
view_id: &str,
472+
column_name: &str,
473+
) -> HandlerFuture<
474+
Result<
475+
(
476+
perspective_client::config::Scalar,
477+
perspective_client::config::Scalar,
478+
),
479+
Self::Error,
480+
>,
481+
> {
482+
let has_method = Reflect::get(&self.0, &JsValue::from_str("viewGetMinMax"))
483+
.map(|val| !val.is_undefined())
484+
.unwrap_or(false);
485+
486+
if !has_method {
487+
return Box::pin(async {
488+
Err(JsError(JsValue::from_str("viewGetMinMax not implemented")))
489+
});
490+
}
491+
492+
let handler = self.0.clone();
493+
let view_id = view_id.to_string();
494+
let column_name = column_name.to_string();
495+
Box::pin(async move {
496+
let this = JsServerHandler(handler);
497+
let args = Array::new();
498+
args.push(&JsValue::from_str(&view_id));
499+
args.push(&JsValue::from_str(&column_name));
500+
let result = this.call_method_js_async("viewGetMinMax", &args).await?;
501+
let obj = result.dyn_ref::<Object>().unwrap();
502+
let min_val = Reflect::get(obj, &JsValue::from_str(wasm_bindgen::intern("min")))?;
503+
let max_val = Reflect::get(obj, &JsValue::from_str(wasm_bindgen::intern("max")))?;
504+
Ok((jsvalue_to_scalar(&min_val), jsvalue_to_scalar(&max_val)))
505+
})
506+
}
507+
455508
fn view_get_data(
456509
&self,
457510
view_id: &str,

rust/perspective-js/src/ts/virtual_server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ export interface VirtualServerHandler {
6868
tableId: string,
6969
expression: string,
7070
): ColumnType | Promise<ColumnType>;
71+
viewGetMinMax?(
72+
viewId: string,
73+
columnName: string,
74+
): { min: any; max: any } | Promise<{ min: any; max: any }>;
7175
getFeatures?(): ServerFeatures | Promise<ServerFeatures>;
7276
makeTable?(
7377
tableId: string,

rust/perspective-js/src/ts/virtual_servers/duckdb.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,16 @@ export class DuckDBHandler implements perspective.VirtualServerHandler {
290290
await runQuery(this.db, query);
291291
}
292292

293+
async viewGetMinMax(viewId: string, columnName: string) {
294+
const query = this.sqlBuilder.viewGetMinMax(viewId, columnName);
295+
const results = await runQuery(this.db, query);
296+
const row = results[0].toJSON();
297+
let [min, max] = Object.values(row);
298+
if (typeof min === "bigint") min = Number(min);
299+
if (typeof max === "bigint") max = Number(max);
300+
return { min: min ?? null, max: max ?? null };
301+
}
302+
293303
async viewGetData(
294304
viewId: string,
295305
config: ViewConfig,

0 commit comments

Comments
 (0)