Skip to content

Commit 638cc26

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

File tree

22 files changed

+446
-31
lines changed

22 files changed

+446
-31
lines changed

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: 31 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,37 @@ 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+
/// * `config` - The view configuration.
294+
///
295+
/// # Returns
296+
/// SQL: `SELECT MIN("column_name"), MAX("column_name") FROM {view_id}`
297+
/// When the view uses ROLLUP grouping (non-flat mode with group_by),
298+
/// a `WHERE __GROUPING_ID__ = 0` clause is added to exclude non-leaf rows.
299+
pub fn view_get_min_max(
300+
&self,
301+
view_id: &str,
302+
column_name: &str,
303+
config: &ViewConfig,
304+
) -> GenericSQLResult<String> {
305+
let has_grouping_id =
306+
!config.group_by.is_empty() && config.group_rollup_mode != GroupRollupMode::Flat;
307+
let where_clause = if has_grouping_id {
308+
" WHERE __GROUPING_ID__ = 0"
309+
} else {
310+
""
311+
};
312+
313+
Ok(format!(
314+
"SELECT MIN(\"{}\"), MAX(\"{}\") FROM {}{}",
315+
column_name, column_name, view_id, where_clause
316+
))
317+
}
318+
290319
fn filter_term_to_sql(term: &FilterTerm) -> Option<String> {
291320
match term {
292321
FilterTerm::Scalar(scalar) => Self::scalar_to_sql(scalar),

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,19 @@ 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+
_config: &crate::config::ViewConfig,
156+
) -> VirtualServerFuture<'_, Result<(crate::config::Scalar, crate::config::Scalar), Self::Error>>
157+
{
158+
Box::pin(async { unimplemented!("view_get_min_max not implemented") })
159+
}
160+
148161
// Unused
149162

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

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

Lines changed: 14 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,17 @@ impl<T: VirtualServerHandler> VirtualServer<T> {
338338
.await?;
339339
respond!(msg, MakeTableResp {})
340340
},
341+
ViewGetMinMaxReq(req) => {
342+
let config = self.view_configs.get(&msg.entity_id).unwrap();
343+
let (min, max) = self
344+
.handler
345+
.view_get_min_max(&msg.entity_id, &req.column_name, config)
346+
.await?;
347+
respond!(msg, ViewGetMinMaxResp {
348+
min: Some(min.into()),
349+
max: Some(max.into()),
350+
})
351+
},
341352

342353
// Stub implementations for callback/update requests that VirtualServer doesn't support
343354
TableOnDeleteReq(_) => {
@@ -361,7 +372,6 @@ impl<T: VirtualServerHandler> VirtualServer<T> {
361372
ViewRemoveDeleteReq(_) => {
362373
respond!(msg, ViewRemoveDeleteResp {})
363374
},
364-
365375
x => {
366376
// Return an error response instead of empty bytes
367377
return Err(VirtualServerError::Other(format!(

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,22 @@ 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(
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+
}
160176
}
161177

162178
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: 56 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,48 @@ impl VirtualServerHandler for JsServerHandler {
452466
})
453467
}
454468

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+
455511
fn view_get_data(
456512
&self,
457513
view_id: &str,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ export interface VirtualServerHandler {
6868
tableId: string,
6969
expression: string,
7070
): ColumnType | Promise<ColumnType>;
71+
viewGetMinMax?(
72+
viewId: string,
73+
columnName: string,
74+
config: ViewConfig,
75+
): { min: any; max: any } | Promise<{ min: any; max: any }>;
7176
getFeatures?(): ServerFeatures | Promise<ServerFeatures>;
7277
makeTable?(
7378
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, config: ViewConfig) {
294+
const query = this.sqlBuilder.viewGetMinMax(viewId, columnName, config);
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)