chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
# ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
[package]
|
||||
name = "perspective-client"
|
||||
version = "4.5.2"
|
||||
authors = ["Andrew Stein <steinlink@gmail.com>"]
|
||||
edition = "2024"
|
||||
description = "A data visualization and analytics component, especially well-suited for large and/or streaming datasets."
|
||||
repository = "https://github.com/perspective-dev/perspective"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://perspective-dev.github.io"
|
||||
keywords = []
|
||||
links = "perspective_client"
|
||||
include = [
|
||||
"src/**/*",
|
||||
"Cargo.toml",
|
||||
"package.json",
|
||||
"build.rs",
|
||||
"docs/**/*",
|
||||
"perspective.proto",
|
||||
]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
# Removes the `Send` restriction from core `Future` traits, allowing easier
|
||||
# integration with wasm while respecting compat with rust-analyzer and metadata.
|
||||
sendable = []
|
||||
|
||||
# Should the project build the `proto.rs` via protoc from source or assume it
|
||||
# already exists?
|
||||
generate-proto = []
|
||||
|
||||
# Should the project build protobuf from source? If not, building this crate
|
||||
# requires PROTOC be set in the environment
|
||||
protobuf-src = ["dep:protobuf-src"]
|
||||
|
||||
# When generating metadata, we can't rely on its existence - so enable this
|
||||
# to skip metadata generation. This currently only affects docs.
|
||||
omit_metadata = []
|
||||
|
||||
# Should `talc` be used as a global allocator? Allows enhanced runtime metrics
|
||||
# via `Client::system_info`.
|
||||
talc-allocator = ["talc"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib"]
|
||||
path = "src/rust/lib.rs"
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = { version = "0.12.3" }
|
||||
|
||||
# https://github.com/abseil/abseil-cpp/issues/1241#issuecomment-2138616329
|
||||
protobuf-src = { version = "2.1.1", optional = true }
|
||||
|
||||
[dependencies]
|
||||
arrow-array = { version = "57.3.0", default-features = false }
|
||||
arrow-ipc = { version = "57.3.0", default-features = false }
|
||||
arrow-schema = { version = "57.3.0", default-features = false }
|
||||
arrow-select = { version = "57.3.0", default-features = false }
|
||||
async-lock = { version = "2.5.0" }
|
||||
futures = { version = "0.3.28" }
|
||||
indexmap = { version = "2.2.6", features = ["serde"] }
|
||||
itertools = { version = "0.10.1" }
|
||||
paste = { version = "1.0.12" }
|
||||
prost-types = { version = "0.12.3" }
|
||||
|
||||
# `rand` dependency that needs features unified.
|
||||
getrandom = { version = "0", features = ["wasm_js"] }
|
||||
num-traits = "0.2.19"
|
||||
rand = { version = ">=0.9,<1" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_bytes = { version = "0.11" }
|
||||
serde_json = { version = "1.0.107", features = ["raw_value", "preserve_order"] }
|
||||
talc = { version = "4.4.3", features = ["counters"], optional = true }
|
||||
thiserror = { version = "1.0.55" }
|
||||
tracing = { version = "0.1.36" }
|
||||
|
||||
[dependencies.prost]
|
||||
version = "0.12.3"
|
||||
default-features = false
|
||||
features = ["prost-derive", "std"]
|
||||
|
||||
[dependencies.ts-rs]
|
||||
version = "11.0.1"
|
||||
features = ["serde-json-impl", "no-serde-warnings", "import-esm"]
|
||||
@@ -0,0 +1,92 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use std::io::Result;
|
||||
use std::path::Path;
|
||||
|
||||
fn prost_build() -> Result<()> {
|
||||
println!(
|
||||
"cargo::metadata=PROTO_PATH={}",
|
||||
std::env::var("CARGO_MANIFEST_DIR").unwrap()
|
||||
);
|
||||
|
||||
// This source file is included at `publish` time, but not `build` time
|
||||
// because it is initially generated from the `perspective.proto` definition
|
||||
// in the source.
|
||||
if std::env::var("CARGO_FEATURE_GENERATE_PROTO").is_ok() {
|
||||
println!("cargo:warning=MESSAGE Building in development mode");
|
||||
// let root_dir_env = std::env::var("PSP_ROOT_DIR").expect("Must set
|
||||
// PSP_ROOT_DIR"); let root_dir = Path::new(root_dir_env.as_str());
|
||||
// let proto_file = Path::join(root_dir, "cpp/protos/perspective.proto");
|
||||
let proto_file = Path::new("./perspective.proto");
|
||||
let include_path = proto_file
|
||||
.parent()
|
||||
.expect("Couldn't determine parent directory of proto_file")
|
||||
.to_path_buf();
|
||||
|
||||
println!("cargo:rerun-if-changed={}", proto_file.to_str().unwrap());
|
||||
|
||||
// prost_build reads PROTOC from the environment. When the `protobuf-src`
|
||||
// feature is enabled, the build script sets PROTOC to the one built by
|
||||
// that crate. When protobuf-src is disabled, builders must set PROTOC
|
||||
// in the environment to a protocol buffer compiler.
|
||||
#[cfg(feature = "protobuf-src")]
|
||||
unsafe {
|
||||
std::env::set_var("PROTOC", protobuf_src::protoc())
|
||||
};
|
||||
#[cfg(not(feature = "protobuf-src"))]
|
||||
if std::env::var("PROTOC").is_err() {
|
||||
panic!(
|
||||
"generate-proto is enabled and protobuf-src is disabled. PROTOC must be set in \
|
||||
the environment to the path of a protocol buffer compiler"
|
||||
)
|
||||
}
|
||||
|
||||
prost_build::Config::new()
|
||||
// .bytes(["ViewToArrowResp.arrow", "from_arrow"])
|
||||
.type_attribute("ViewOnUpdateResp", "#[derive(ts_rs::TS)]")
|
||||
.field_attribute("ViewOnUpdateResp.delta", "#[ts(as = \"Vec::<u8>\")]")
|
||||
.field_attribute("ViewOnUpdateResp.delta", "#[serde(with = \"serde_bytes\")]")
|
||||
.type_attribute("ColumnType", "#[derive(ts_rs::TS)]")
|
||||
.type_attribute(
|
||||
"JoinType",
|
||||
"#[derive(serde::Deserialize, ts_rs::TS)] #[serde(rename_all = \"snake_case\")]",
|
||||
)
|
||||
.field_attribute("ViewToArrowResp.arrow", "#[serde(skip)]")
|
||||
.field_attribute("from_arrow", "#[serde(skip)]")
|
||||
.type_attribute(".", "#[derive(serde::Serialize)]")
|
||||
.type_attribute("ViewPort", "#[derive(serde::Deserialize)]")
|
||||
.type_attribute("ViewDimensionsResp", "#[derive(serde::Deserialize)]")
|
||||
.type_attribute("TableValidateExprResp", "#[derive(serde::Deserialize)]")
|
||||
.type_attribute(
|
||||
"ColumnType",
|
||||
"#[derive(serde::Deserialize)] #[serde(rename_all = \"snake_case\")]",
|
||||
)
|
||||
.type_attribute("ExprValidationError", "#[derive(serde::Deserialize)]")
|
||||
.compile_protos(&[proto_file], &[include_path])
|
||||
.unwrap();
|
||||
|
||||
std::fs::copy(
|
||||
std::env::var("OUT_DIR").unwrap() + "/perspective.proto.rs",
|
||||
"src/rust/proto.rs",
|
||||
)?;
|
||||
|
||||
std::fs::remove_file(std::env::var("OUT_DIR").unwrap() + "/perspective.proto.rs")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
prost_build()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@perspective-dev/client-rust",
|
||||
"version": "4.5.2",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/perspective-dev/perspective"
|
||||
},
|
||||
"type": "module",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"clean": "rm src/rust/proto.rs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,584 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
import "google/protobuf/struct.proto";
|
||||
|
||||
package perspective.proto;
|
||||
|
||||
option optimize_for = LITE_RUNTIME;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Common
|
||||
|
||||
enum StatusCode {
|
||||
SERVER_ERROR = 0;
|
||||
VIEW_NOT_FOUND = 1;
|
||||
TRANSPORT_ERROR = 2;
|
||||
}
|
||||
|
||||
// Recoverable, user-readable error reporting from the engine.
|
||||
message ServerError {
|
||||
string message = 1;
|
||||
StatusCode status_code = 2;
|
||||
}
|
||||
|
||||
message Schema {
|
||||
repeated KeyTypePair schema = 1;
|
||||
message KeyTypePair {
|
||||
string name = 1;
|
||||
ColumnType type = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// The data type constructors Perspective supports.
|
||||
message MakeTableData {
|
||||
oneof data {
|
||||
Schema from_schema = 1;
|
||||
string from_csv = 2;
|
||||
bytes from_arrow = 3;
|
||||
string from_rows = 4;
|
||||
string from_cols = 5;
|
||||
string from_view = 6;
|
||||
string from_ndjson = 7;
|
||||
};
|
||||
}
|
||||
|
||||
// Filter type scalars - this is _not_ the same as a Columns scalar, as this
|
||||
// value is used in the view config and must be JSON safe!
|
||||
message Scalar {
|
||||
oneof scalar {
|
||||
bool bool = 1;
|
||||
// int64 date = 2; // TODO these are the wrong type
|
||||
// int64 datetime = 3;
|
||||
double float = 4;
|
||||
// int32 int = 5;
|
||||
string string = 6;
|
||||
google.protobuf.NullValue null = 7;
|
||||
}
|
||||
}
|
||||
|
||||
// View types
|
||||
enum ColumnType {
|
||||
STRING = 0;
|
||||
DATE = 1;
|
||||
DATETIME = 2;
|
||||
INTEGER = 3;
|
||||
FLOAT = 4;
|
||||
BOOLEAN = 5;
|
||||
}
|
||||
|
||||
// Options for requresting a slice of data, starting with the rectangular
|
||||
// viewport.
|
||||
message ViewPort {
|
||||
optional uint32 start_row = 1;
|
||||
optional uint32 start_col = 2;
|
||||
optional uint32 end_row = 3;
|
||||
optional uint32 end_col = 4;
|
||||
optional bool emit_legacy_row_path_names = 5;
|
||||
// optional bool id = 5;
|
||||
// optional bool index = 3;
|
||||
// optional bool formatted = 6;
|
||||
// optional bool leaves_only = 7;
|
||||
// optional bool compression = 3;
|
||||
}
|
||||
|
||||
// TODO This belongs in features
|
||||
enum SortOp {
|
||||
SORT_NONE = 0;
|
||||
SORT_ASC = 1;
|
||||
SORT_DESC = 2;
|
||||
SORT_COL_ASC = 3;
|
||||
SORT_COL_DESC = 4;
|
||||
SORT_ASC_ABS = 5;
|
||||
SORT_DESC_ABS = 6;
|
||||
SORT_COL_ASC_ABS = 7;
|
||||
SORT_COL_DESC_ABS = 8;
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// RPC
|
||||
|
||||
message Request {
|
||||
uint32 msg_id = 1;
|
||||
string entity_id = 2;
|
||||
oneof client_req {
|
||||
// Minimum Virtual API (theoretical).
|
||||
GetFeaturesReq get_features_req = 3;
|
||||
GetHostedTablesReq get_hosted_tables_req = 4;
|
||||
RemoveHostedTablesUpdateReq remove_hosted_tables_update_req = 37;
|
||||
TableMakePortReq table_make_port_req = 5;
|
||||
TableMakeViewReq table_make_view_req = 6;
|
||||
TableSchemaReq table_schema_req = 7;
|
||||
TableSizeReq table_size_req = 8;
|
||||
TableValidateExprReq table_validate_expr_req = 9;
|
||||
ViewColumnPathsReq view_column_paths_req = 10;
|
||||
ViewDeleteReq view_delete_req = 11;
|
||||
ViewDimensionsReq view_dimensions_req = 12;
|
||||
ViewExpressionSchemaReq view_expression_schema_req = 13;
|
||||
ViewGetConfigReq view_get_config_req = 14;
|
||||
ViewSchemaReq view_schema_req = 15;
|
||||
ViewToArrowReq view_to_arrow_req = 16;
|
||||
|
||||
// Optional (we can enable real-time/autocomplete/etc with these, but
|
||||
// not required).
|
||||
ServerSystemInfoReq server_system_info_req = 17;
|
||||
ViewCollapseReq view_collapse_req = 18;
|
||||
ViewExpandReq view_expand_req = 19;
|
||||
ViewGetMinMaxReq view_get_min_max_req = 20;
|
||||
ViewOnUpdateReq view_on_update_req = 21;
|
||||
ViewRemoveOnUpdateReq view_remove_on_update_req = 22;
|
||||
ViewSetDepthReq view_set_depth_req = 23;
|
||||
ViewToColumnsStringReq view_to_columns_string_req = 24;
|
||||
ViewToCSVReq view_to_csv_req = 25;
|
||||
ViewToRowsStringReq view_to_rows_string_req = 26;
|
||||
ViewToNdjsonStringReq view_to_ndjson_string_req = 36;
|
||||
|
||||
// External (we don't need these for viewer, but the developer may).
|
||||
MakeTableReq make_table_req = 27;
|
||||
TableDeleteReq table_delete_req = 28;
|
||||
TableOnDeleteReq table_on_delete_req = 29;
|
||||
TableRemoveDeleteReq table_remove_delete_req = 30;
|
||||
TableRemoveReq table_remove_req = 31;
|
||||
TableReplaceReq table_replace_req = 32;
|
||||
TableUpdateReq table_update_req = 33;
|
||||
ViewOnDeleteReq view_on_delete_req = 34;
|
||||
ViewRemoveDeleteReq view_remove_delete_req = 35;
|
||||
MakeJoinTableReq make_join_table_req = 38;
|
||||
}
|
||||
}
|
||||
|
||||
message Response {
|
||||
uint32 msg_id = 1;
|
||||
string entity_id = 2;
|
||||
oneof client_resp {
|
||||
GetFeaturesResp get_features_resp = 3;
|
||||
GetHostedTablesResp get_hosted_tables_resp = 4;
|
||||
RemoveHostedTablesUpdateResp remove_hosted_tables_update_resp = 37;
|
||||
TableMakePortResp table_make_port_resp = 5;
|
||||
TableMakeViewResp table_make_view_resp = 6;
|
||||
TableSchemaResp table_schema_resp = 7;
|
||||
TableSizeResp table_size_resp = 8;
|
||||
TableValidateExprResp table_validate_expr_resp = 9;
|
||||
ViewColumnPathsResp view_column_paths_resp = 10;
|
||||
ViewDeleteResp view_delete_resp = 11;
|
||||
ViewDimensionsResp view_dimensions_resp = 12;
|
||||
ViewExpressionSchemaResp view_expression_schema_resp = 13;
|
||||
ViewGetConfigResp view_get_config_resp = 14;
|
||||
ViewSchemaResp view_schema_resp = 15;
|
||||
ViewToArrowResp view_to_arrow_resp = 16;
|
||||
ServerSystemInfoResp server_system_info_resp = 17;
|
||||
ViewCollapseResp view_collapse_resp = 18;
|
||||
ViewExpandResp view_expand_resp = 19;
|
||||
ViewGetMinMaxResp view_get_min_max_resp = 20;
|
||||
ViewOnUpdateResp view_on_update_resp = 21;
|
||||
ViewRemoveOnUpdateResp view_remove_on_update_resp = 22;
|
||||
ViewSetDepthResp view_set_depth_resp = 23;
|
||||
ViewToColumnsStringResp view_to_columns_string_resp = 24;
|
||||
ViewToCSVResp view_to_csv_resp = 25;
|
||||
ViewToRowsStringResp view_to_rows_string_resp = 26;
|
||||
ViewToNdjsonStringResp view_to_ndjson_string_resp = 36;
|
||||
MakeTableResp make_table_resp = 27;
|
||||
TableDeleteResp table_delete_resp = 28;
|
||||
TableOnDeleteResp table_on_delete_resp = 29;
|
||||
TableRemoveDeleteResp table_remove_delete_resp = 30;
|
||||
TableRemoveResp table_remove_resp = 31;
|
||||
TableReplaceResp table_replace_resp = 32;
|
||||
TableUpdateResp table_update_resp = 33;
|
||||
ViewOnDeleteResp view_on_delete_resp = 34;
|
||||
ViewRemoveDeleteResp view_remove_delete_resp = 35;
|
||||
MakeJoinTableResp make_join_table_resp = 38;
|
||||
ServerError server_error = 50;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Virtual API
|
||||
|
||||
|
||||
enum GroupRollupMode {
|
||||
ROLLUP = 0;
|
||||
FLAT = 1;
|
||||
TOTAL = 2;
|
||||
}
|
||||
|
||||
// Informs the client of the feature set, e.g. what to expect in the
|
||||
// `ViewConfig` message.
|
||||
message GetFeaturesReq {}
|
||||
message GetFeaturesResp {
|
||||
bool group_by = 1;
|
||||
bool split_by = 2;
|
||||
bool expressions = 3;
|
||||
bool on_update = 4;
|
||||
bool sort = 5;
|
||||
map<uint32, ColumnTypeOptions> filter_ops = 6;
|
||||
map<uint32, AggregateOptions> aggregates = 7;
|
||||
repeated GroupRollupMode group_rollup_mode = 8;
|
||||
|
||||
message ColumnTypeOptions {
|
||||
repeated string options = 1;
|
||||
}
|
||||
|
||||
message AggregateOptions {
|
||||
repeated AggregateArgs aggregates = 1;
|
||||
}
|
||||
|
||||
message AggregateArgs {
|
||||
string name = 1;
|
||||
repeated ColumnType args = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// `Client::get_hosted_tables`
|
||||
message GetHostedTablesReq {
|
||||
bool subscribe = 1;
|
||||
}
|
||||
|
||||
message GetHostedTablesResp {
|
||||
repeated HostedTable table_infos = 1;
|
||||
}
|
||||
|
||||
message HostedTable {
|
||||
string entity_id = 1;
|
||||
optional string index = 2;
|
||||
optional uint32 limit = 3;
|
||||
}
|
||||
|
||||
message RemoveHostedTablesUpdateReq {
|
||||
uint32 id = 1;
|
||||
}
|
||||
message RemoveHostedTablesUpdateResp {}
|
||||
|
||||
// `Table::size`
|
||||
message TableSizeReq {}
|
||||
message TableSizeResp {
|
||||
uint32 size = 2;
|
||||
}
|
||||
|
||||
// `Table::schema`
|
||||
message TableSchemaReq {}
|
||||
message TableSchemaResp {
|
||||
Schema schema = 1;
|
||||
}
|
||||
|
||||
// `Table::validate_expressions`
|
||||
// TODO: This should be just `validate()`
|
||||
message TableValidateExprReq {
|
||||
map<string, string> column_to_expr = 1;
|
||||
}
|
||||
message TableValidateExprResp {
|
||||
map<string, ColumnType> expression_schema = 1;
|
||||
map<string, ExprValidationError> errors = 2;
|
||||
map<string, string> expression_alias = 3;
|
||||
message ExprValidationError {
|
||||
string error_message = 1;
|
||||
uint32 line = 2;
|
||||
uint32 column = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// `Table::view`
|
||||
message TableMakeViewReq {
|
||||
string view_id = 1;
|
||||
ViewConfig config = 2;
|
||||
}
|
||||
message TableMakeViewResp {
|
||||
string view_id = 1;
|
||||
}
|
||||
|
||||
// `View::schema`
|
||||
message ViewSchemaReq {}
|
||||
message ViewSchemaResp {
|
||||
map<string, ColumnType> schema = 1;
|
||||
}
|
||||
|
||||
// `View::dimensions`
|
||||
message ViewDimensionsReq {}
|
||||
message ViewDimensionsResp {
|
||||
uint32 num_table_rows = 1;
|
||||
uint32 num_table_columns = 2;
|
||||
uint32 num_view_rows = 3;
|
||||
uint32 num_view_columns = 4;
|
||||
}
|
||||
|
||||
// `View::get_config`
|
||||
message ViewGetConfigReq {}
|
||||
message ViewGetConfigResp {
|
||||
ViewConfig config = 1;
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// `Client::table`.
|
||||
message MakeTableReq {
|
||||
MakeTableData data = 1;
|
||||
optional MakeTableOptions options = 2;
|
||||
message MakeTableOptions {
|
||||
oneof make_table_type {
|
||||
string make_index_table = 1;
|
||||
uint32 make_limit_table = 2;
|
||||
};
|
||||
|
||||
// Back this Table's canonical data with the on-disk storage backend
|
||||
// (memory-mapped file on native; OPFS on WASM) instead of memory.
|
||||
// Orthogonal to `make_table_type`, so it is a standalone field.
|
||||
optional bool page_to_disk = 3;
|
||||
}
|
||||
}
|
||||
message MakeTableResp {}
|
||||
|
||||
enum JoinType {
|
||||
INNER = 0;
|
||||
LEFT = 1;
|
||||
OUTER = 2;
|
||||
}
|
||||
|
||||
// `Client::join` — create a read-only table from a JOIN of two tables.
|
||||
message MakeJoinTableReq {
|
||||
string left_table_id = 1;
|
||||
string right_table_id = 2;
|
||||
string on_column = 3;
|
||||
JoinType join_type = 4;
|
||||
string right_on_column = 5;
|
||||
}
|
||||
message MakeJoinTableResp {}
|
||||
|
||||
// `Table::delete`
|
||||
message TableDeleteReq {
|
||||
bool is_immediate = 1;
|
||||
}
|
||||
message TableDeleteResp {}
|
||||
|
||||
// `Table::on_delete`
|
||||
message TableOnDeleteReq {}
|
||||
message TableOnDeleteResp {}
|
||||
|
||||
// `Table::make_port`
|
||||
message TableMakePortReq {}
|
||||
message TableMakePortResp {
|
||||
uint32 port_id = 1;
|
||||
}
|
||||
|
||||
// `Table::remove_delete`
|
||||
message TableRemoveDeleteReq {
|
||||
uint32 id = 1;
|
||||
}
|
||||
message TableRemoveDeleteResp {}
|
||||
|
||||
// `Table::update`
|
||||
message TableUpdateReq {
|
||||
MakeTableData data = 1;
|
||||
uint32 port_id = 2;
|
||||
}
|
||||
message TableUpdateResp {}
|
||||
|
||||
// `Table::replace`
|
||||
message TableReplaceReq {
|
||||
MakeTableData data = 1;
|
||||
}
|
||||
message TableReplaceResp {}
|
||||
|
||||
// `Table::remove`
|
||||
message TableRemoveReq {
|
||||
MakeTableData data = 1;
|
||||
}
|
||||
message TableRemoveResp {}
|
||||
|
||||
message ViewOnUpdateReq {
|
||||
enum Mode {
|
||||
ROW = 0;
|
||||
}
|
||||
optional Mode mode = 1;
|
||||
}
|
||||
message ViewOnUpdateResp {
|
||||
optional bytes delta = 1;
|
||||
uint32 port_id = 2;
|
||||
}
|
||||
|
||||
message ViewOnDeleteReq {}
|
||||
message ViewOnDeleteResp {}
|
||||
|
||||
message ViewRemoveDeleteReq {
|
||||
uint32 id = 1;
|
||||
}
|
||||
message ViewRemoveDeleteResp {}
|
||||
|
||||
message ViewToColumnsStringReq {
|
||||
ViewPort viewport = 1;
|
||||
optional bool id = 2;
|
||||
optional bool index = 3;
|
||||
optional bool formatted = 4;
|
||||
}
|
||||
|
||||
message ViewToColumnsStringResp {
|
||||
string json_string = 1;
|
||||
}
|
||||
|
||||
message ViewToRowsStringReq {
|
||||
ViewPort viewport = 1;
|
||||
optional bool id = 2;
|
||||
optional bool index = 3;
|
||||
optional bool formatted = 4;
|
||||
}
|
||||
|
||||
message ViewToRowsStringResp {
|
||||
string json_string = 1;
|
||||
}
|
||||
|
||||
message ViewToNdjsonStringReq {
|
||||
ViewPort viewport = 1;
|
||||
optional bool id = 2;
|
||||
optional bool index = 3;
|
||||
optional bool formatted = 4;
|
||||
}
|
||||
|
||||
message ViewToNdjsonStringResp {
|
||||
string ndjson_string = 1;
|
||||
}
|
||||
|
||||
message ViewToArrowReq {
|
||||
ViewPort viewport = 1;
|
||||
optional string compression = 2;
|
||||
}
|
||||
|
||||
message ViewToArrowResp {
|
||||
bytes arrow = 1;
|
||||
}
|
||||
|
||||
message ViewColumnPathsReq {
|
||||
optional uint32 start_col = 1;
|
||||
optional uint32 end_col = 2;
|
||||
}
|
||||
|
||||
// // TODO This is a better paths representations but its not compatible with
|
||||
// // the legacy API. Let's do this when we can fix the API.
|
||||
|
||||
// message ColumnPath {
|
||||
// repeated string path = 1;
|
||||
// }
|
||||
|
||||
message ViewColumnPathsResp {
|
||||
repeated string paths = 1;
|
||||
// repeated ColumnPath paths = 1;
|
||||
}
|
||||
|
||||
message ViewDeleteReq {}
|
||||
message ViewDeleteResp {}
|
||||
|
||||
message ViewGetMinMaxReq {
|
||||
string column_name = 1;
|
||||
}
|
||||
|
||||
message ViewGetMinMaxResp {
|
||||
Scalar min = 1;
|
||||
Scalar max = 2;
|
||||
}
|
||||
|
||||
message ViewExpressionSchemaReq {}
|
||||
message ViewExpressionSchemaResp {
|
||||
map<string, ColumnType> schema = 1;
|
||||
}
|
||||
|
||||
message ViewToCSVReq {
|
||||
ViewPort viewport = 1;
|
||||
}
|
||||
|
||||
message ViewToCSVResp {
|
||||
string csv = 1;
|
||||
}
|
||||
|
||||
message ViewRemoveOnUpdateReq {
|
||||
uint32 id = 1;
|
||||
}
|
||||
message ViewRemoveOnUpdateResp {}
|
||||
|
||||
message ViewCollapseReq {
|
||||
uint32 row_index = 1;
|
||||
}
|
||||
|
||||
message ViewCollapseResp {
|
||||
uint32 num_changed = 1;
|
||||
}
|
||||
|
||||
message ViewExpandReq {
|
||||
uint32 row_index = 1;
|
||||
}
|
||||
|
||||
message ViewExpandResp {
|
||||
uint32 num_changed = 1;
|
||||
}
|
||||
|
||||
// `View::set_depth`
|
||||
message ViewSetDepthReq {
|
||||
uint32 depth = 1;
|
||||
}
|
||||
message ViewSetDepthResp {}
|
||||
|
||||
message ServerSystemInfoReq {}
|
||||
message ServerSystemInfoResp {
|
||||
uint64 heap_size = 1;
|
||||
uint64 used_size = 2;
|
||||
uint32 cpu_time = 3;
|
||||
uint32 cpu_time_epoch = 4;
|
||||
}
|
||||
|
||||
|
||||
message ViewConfig {
|
||||
repeated string group_by = 1;
|
||||
repeated string split_by = 2;
|
||||
ColumnsUpdate columns = 3;
|
||||
repeated Filter filter = 4;
|
||||
repeated Sort sort = 5;
|
||||
map<string, string> expressions = 6;
|
||||
map<string, AggList> aggregates = 7;
|
||||
FilterReducer filter_op = 8;
|
||||
optional uint32 group_by_depth = 9;
|
||||
optional GroupRollupMode group_rollup_mode = 10;
|
||||
|
||||
message AggList {
|
||||
repeated string aggregations = 1;
|
||||
}
|
||||
|
||||
message Sort {
|
||||
string column = 1;
|
||||
SortOp op = 2;
|
||||
}
|
||||
|
||||
message Filter {
|
||||
string column = 1;
|
||||
string op = 2;
|
||||
repeated Scalar value = 3;
|
||||
}
|
||||
|
||||
enum FilterReducer {
|
||||
AND = 0;
|
||||
OR = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message ColumnsUpdate {
|
||||
oneof opt_columns {
|
||||
google.protobuf.NullValue default_columns = 1;
|
||||
Columns columns = 2;
|
||||
}
|
||||
|
||||
message Columns {
|
||||
repeated string columns = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,809 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_lock::{Mutex, RwLock};
|
||||
use futures::Future;
|
||||
use futures::future::{BoxFuture, LocalBoxFuture, join_all};
|
||||
use prost::Message;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::proto::request::ClientReq;
|
||||
use crate::proto::response::ClientResp;
|
||||
use crate::proto::{
|
||||
ColumnType, GetFeaturesReq, GetFeaturesResp, GetHostedTablesReq, GetHostedTablesResp,
|
||||
HostedTable, JoinType, MakeJoinTableReq, MakeTableReq, RemoveHostedTablesUpdateReq, Request,
|
||||
Response, ServerError, ServerSystemInfoReq,
|
||||
};
|
||||
use crate::table::{JoinOptions, Table, TableInitOptions, TableOptions};
|
||||
use crate::table_data::{TableData, UpdateData};
|
||||
use crate::table_ref::TableRef;
|
||||
use crate::utils::*;
|
||||
use crate::view::{OnUpdateData, ViewWindow};
|
||||
use crate::{OnUpdateMode, OnUpdateOptions, asyncfn, clone};
|
||||
|
||||
/// Metadata about the engine runtime (such as total heap utilization).
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
|
||||
pub struct SystemInfo<T = u64> {
|
||||
/// Total available bytes for allocation on the [`Server`].
|
||||
pub heap_size: T,
|
||||
|
||||
/// Bytes allocated for use on the [`Server`].
|
||||
pub used_size: T,
|
||||
|
||||
/// Wall-clock time spent processing requests on the [`Server`], in
|
||||
/// milliseconds (estimated). This does not properly account for the
|
||||
/// internal thread pool (which enables column-parallel processing of
|
||||
/// individual requests).
|
||||
pub cpu_time: u32,
|
||||
|
||||
/// Milliseconds since internal CPU time accumulator was reset.
|
||||
pub cpu_time_epoch: u32,
|
||||
|
||||
/// Timestamp (POSIX) this request was made. This field may be omitted
|
||||
/// for wasm due to `perspective-client` lacking a dependency on
|
||||
/// `wasm_bindgen`.
|
||||
pub timestamp: Option<T>,
|
||||
|
||||
/// Total available bytes for allocation on the [`Client`]. This is only
|
||||
/// available if `trace-allocator` is enabled.
|
||||
pub client_heap: Option<T>,
|
||||
|
||||
/// Bytes allocated for use on the [`Client`]. This is only
|
||||
/// available if `trace-allocator` is enabled.
|
||||
pub client_used: Option<T>,
|
||||
}
|
||||
|
||||
impl<U: Copy + 'static> SystemInfo<U> {
|
||||
/// Convert the numeric representation for `T` to something else, which is
|
||||
/// useful for JavaScript where there is no `u64` native type.
|
||||
pub fn cast<T: Copy + 'static>(&self) -> SystemInfo<T>
|
||||
where
|
||||
U: num_traits::AsPrimitive<T>,
|
||||
{
|
||||
SystemInfo {
|
||||
heap_size: self.heap_size.as_(),
|
||||
used_size: self.used_size.as_(),
|
||||
cpu_time: self.cpu_time,
|
||||
cpu_time_epoch: self.cpu_time_epoch,
|
||||
timestamp: self.timestamp.map(|x| x.as_()),
|
||||
client_heap: self.client_heap.map(|x| x.as_()),
|
||||
client_used: self.client_used.map(|x| x.as_()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata about what features are supported by the `Server` to which this
|
||||
/// [`Client`] connects.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Features(Arc<GetFeaturesResp>);
|
||||
|
||||
impl Features {
|
||||
pub fn get_group_rollup_modes(&self) -> Vec<crate::config::GroupRollupMode> {
|
||||
self.group_rollup_mode
|
||||
.iter()
|
||||
.map(|x| {
|
||||
crate::config::GroupRollupMode::from(
|
||||
crate::proto::GroupRollupMode::try_from(*x).unwrap(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Features {
|
||||
type Target = GetFeaturesResp;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl GetFeaturesResp {
|
||||
pub fn default_op(&self, col_type: ColumnType) -> Option<&str> {
|
||||
self.filter_ops
|
||||
.get(&(col_type as u32))?
|
||||
.options
|
||||
.first()
|
||||
.map(|x| x.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
type BoxFn<I, O> = Box<dyn Fn(I) -> O + Send + Sync + 'static>;
|
||||
type Box2Fn<I, J, O> = Box<dyn Fn(I, J) -> O + Send + Sync + 'static>;
|
||||
|
||||
type Subscriptions<C> = Arc<RwLock<HashMap<u32, C>>>;
|
||||
type OnErrorCallback =
|
||||
Box2Fn<ClientError, Option<ReconnectCallback>, BoxFuture<'static, Result<(), ClientError>>>;
|
||||
|
||||
type OnceCallback = Box<dyn FnOnce(Response) -> ClientResult<()> + Send + Sync + 'static>;
|
||||
type SendCallback = Arc<
|
||||
dyn for<'a> Fn(&'a Request) -> BoxFuture<'a, Result<(), Box<dyn Error + Send + Sync>>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
>;
|
||||
|
||||
/// The client-side representation of a connection to a `Server`.
|
||||
pub trait ClientHandler: Clone + Send + Sync + 'static {
|
||||
fn send_request(
|
||||
&self,
|
||||
msg: Vec<u8>,
|
||||
) -> impl Future<Output = Result<(), Box<dyn Error + Send + Sync>>> + Send;
|
||||
}
|
||||
|
||||
mod name_registry {
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
|
||||
use crate::ClientError;
|
||||
use crate::view::ClientResult;
|
||||
|
||||
static CLIENT_ID_GEN: LazyLock<Arc<Mutex<u32>>> = LazyLock::new(Arc::default);
|
||||
static REGISTERED_CLIENTS: LazyLock<Arc<Mutex<HashSet<String>>>> = LazyLock::new(Arc::default);
|
||||
|
||||
pub(crate) fn generate_name(name: Option<&str>) -> ClientResult<String> {
|
||||
if let Some(name) = name {
|
||||
if let Some(name) = REGISTERED_CLIENTS
|
||||
.lock()
|
||||
.map_err(ClientError::from)?
|
||||
.get(name)
|
||||
{
|
||||
Err(ClientError::DuplicateNameError(name.to_owned()))
|
||||
} else {
|
||||
Ok(name.to_owned())
|
||||
}
|
||||
} else {
|
||||
let mut guard = CLIENT_ID_GEN.lock()?;
|
||||
*guard += 1;
|
||||
Ok(format!("client-{guard}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of the `reconnect` parameter passed to [`Client::handle_error`},
|
||||
/// and to the callback closure of [`Client::on_error`].
|
||||
///
|
||||
/// Calling this function from a [`Client::on_error`] closure should run the
|
||||
/// (implementation specific) client reconnect logic, e.g. rebindign a
|
||||
/// websocket.
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct ReconnectCallback(
|
||||
Arc<dyn Fn() -> LocalBoxFuture<'static, Result<(), Box<dyn Error>>> + Send + Sync>,
|
||||
);
|
||||
|
||||
impl Deref for ReconnectCallback {
|
||||
type Target = dyn Fn() -> LocalBoxFuture<'static, Result<(), Box<dyn Error>>> + Send + Sync;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ReconnectCallback {
|
||||
pub fn new(
|
||||
f: impl Fn() -> LocalBoxFuture<'static, Result<(), Box<dyn Error>>> + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
ReconnectCallback(Arc::new(f))
|
||||
}
|
||||
}
|
||||
|
||||
/// An instance of a [`Client`] is a connection to a single
|
||||
/// `perspective_server::Server`, whether locally in-memory or remote over some
|
||||
/// transport like a WebSocket.
|
||||
#[derive(Clone)]
|
||||
pub struct Client {
|
||||
name: Arc<String>,
|
||||
features: Arc<Mutex<Option<Features>>>,
|
||||
send: SendCallback,
|
||||
id_gen: IDGen,
|
||||
subscriptions_errors: Subscriptions<OnErrorCallback>,
|
||||
subscriptions_once: Subscriptions<OnceCallback>,
|
||||
subscriptions: Subscriptions<BoxFn<Response, BoxFuture<'static, Result<(), ClientError>>>>,
|
||||
}
|
||||
|
||||
impl PartialEq for Client {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Client {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Client").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client instance with a closure that handles message
|
||||
/// dispatch. See [`Client::new`] for details.
|
||||
pub fn new_with_callback<T, U>(name: Option<&str>, send_request: T) -> ClientResult<Self>
|
||||
where
|
||||
T: Fn(Vec<u8>) -> U + 'static + Sync + Send,
|
||||
U: Future<Output = Result<(), Box<dyn Error + Send + Sync>>> + Send + 'static,
|
||||
{
|
||||
let name = name_registry::generate_name(name)?;
|
||||
let send_request = Arc::new(send_request);
|
||||
let send: SendCallback = Arc::new(move |req| {
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
req.encode(&mut bytes).unwrap();
|
||||
let send_request = send_request.clone();
|
||||
Box::pin(async move { send_request(bytes).await })
|
||||
});
|
||||
|
||||
Ok(Client {
|
||||
name: Arc::new(name),
|
||||
features: Arc::default(),
|
||||
id_gen: IDGen::default(),
|
||||
send,
|
||||
subscriptions: Subscriptions::default(),
|
||||
subscriptions_errors: Arc::default(),
|
||||
subscriptions_once: Arc::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new [`Client`] instance with [`ClientHandler`].
|
||||
pub fn new<T>(name: Option<&str>, client_handler: T) -> ClientResult<Self>
|
||||
where
|
||||
T: ClientHandler + 'static + Sync + Send,
|
||||
{
|
||||
Self::new_with_callback(
|
||||
name,
|
||||
asyncfn!(client_handler, async move |req| {
|
||||
client_handler.send_request(req).await
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> &'_ str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
/// Handle a message from the external message queue.
|
||||
/// [`Client::handle_response`] is part of the low-level message-handling
|
||||
/// API necessary to implement new transports for a [`Client`]
|
||||
/// connection to a local-or-remote `perspective_server::Server`, and
|
||||
/// doesn't generally need to be called directly by "users" of a
|
||||
/// [`Client`] once connected.
|
||||
pub async fn handle_response<'a>(&'a self, msg: &'a [u8]) -> ClientResult<bool> {
|
||||
let msg = Response::decode(msg)?;
|
||||
tracing::debug!("RECV {}", msg);
|
||||
let mut wr = self.subscriptions_once.write().await;
|
||||
if let Some(handler) = (*wr).remove(&msg.msg_id) {
|
||||
drop(wr);
|
||||
handler(msg)?;
|
||||
return Ok(true);
|
||||
} else if let Some(handler) = self.subscriptions.try_read().unwrap().get(&msg.msg_id) {
|
||||
drop(wr);
|
||||
handler(msg).await?;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if let Response {
|
||||
client_resp: Some(ClientResp::ServerError(ServerError { message, .. })),
|
||||
..
|
||||
} = &msg
|
||||
{
|
||||
tracing::error!("{}", message);
|
||||
} else {
|
||||
tracing::debug!("Received unsolicited server response: {}", msg);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Handle an exception from the underlying transport.
|
||||
pub async fn handle_error<T, U>(
|
||||
&self,
|
||||
message: ClientError,
|
||||
reconnect: Option<T>,
|
||||
) -> ClientResult<()>
|
||||
where
|
||||
T: Fn() -> U + Clone + Send + Sync + 'static,
|
||||
U: Future<Output = ClientResult<()>>,
|
||||
{
|
||||
let subs = self.subscriptions_errors.read().await;
|
||||
let tasks = join_all(subs.values().map(|callback| {
|
||||
callback(
|
||||
message.clone(),
|
||||
reconnect.clone().map(move |f| {
|
||||
ReconnectCallback(Arc::new(move || {
|
||||
clone!(f);
|
||||
Box::pin(async move { Ok(f().await?) }) as LocalBoxFuture<'static, _>
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}));
|
||||
|
||||
tasks.await.into_iter().collect::<Result<(), _>>()?;
|
||||
self.close_and_error_subscriptions(&message).await
|
||||
}
|
||||
|
||||
/// TODO Synthesize an error to provide to the caller, since the
|
||||
/// server did not respond and the other option is to just drop the call
|
||||
/// which results in a non-descript error message. It would be nice to
|
||||
/// have client-side failures be a native part of the Client API.
|
||||
async fn close_and_error_subscriptions(&self, message: &ClientError) -> ClientResult<()> {
|
||||
let synthetic_error = |msg_id| Response {
|
||||
msg_id,
|
||||
entity_id: "".to_string(),
|
||||
client_resp: Some(ClientResp::ServerError(ServerError {
|
||||
message: format!("{message}"),
|
||||
status_code: 2,
|
||||
})),
|
||||
};
|
||||
|
||||
self.subscriptions.write().await.clear();
|
||||
let callbacks_once = self
|
||||
.subscriptions_once
|
||||
.write()
|
||||
.await
|
||||
.drain()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
callbacks_once
|
||||
.into_iter()
|
||||
.try_for_each(|(msg_id, f)| f(synthetic_error(msg_id)))
|
||||
}
|
||||
|
||||
pub async fn on_error<T, U, V>(&self, on_error: T) -> ClientResult<u32>
|
||||
where
|
||||
T: Fn(ClientError, Option<ReconnectCallback>) -> U + Clone + Send + Sync + 'static,
|
||||
U: Future<Output = V> + Send + 'static,
|
||||
V: Into<Result<(), ClientError>> + Sync + 'static,
|
||||
{
|
||||
let id = self.gen_id();
|
||||
let callback = asyncfn!(on_error, async move |x, y| on_error(x, y).await.into());
|
||||
self.subscriptions_errors
|
||||
.write()
|
||||
.await
|
||||
.insert(id, Box::new(move |x, y| Box::pin(callback(x, y))));
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Generate a message ID unique to this client.
|
||||
pub(crate) fn gen_id(&self) -> u32 {
|
||||
self.id_gen.next()
|
||||
}
|
||||
|
||||
pub(crate) async fn unsubscribe(&self, update_id: u32) -> ClientResult<()> {
|
||||
let callback = self
|
||||
.subscriptions
|
||||
.write()
|
||||
.await
|
||||
.remove(&update_id)
|
||||
.ok_or(ClientError::Unknown("remove_update".to_string()))?;
|
||||
|
||||
drop(callback);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register a callback which is expected to respond exactly once.
|
||||
pub(crate) async fn subscribe_once(
|
||||
&self,
|
||||
msg: &Request,
|
||||
on_update: Box<dyn FnOnce(Response) -> ClientResult<()> + Send + Sync + 'static>,
|
||||
) -> ClientResult<()> {
|
||||
self.subscriptions_once
|
||||
.write()
|
||||
.await
|
||||
.insert(msg.msg_id, on_update);
|
||||
|
||||
tracing::debug!("SEND {}", msg);
|
||||
if let Err(e) = (self.send)(msg).await {
|
||||
self.subscriptions_once.write().await.remove(&msg.msg_id);
|
||||
Err(ClientError::Unknown(e.to_string()))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn subscribe<T, U>(&self, msg: &Request, on_update: T) -> ClientResult<()>
|
||||
where
|
||||
T: Fn(Response) -> U + Send + Sync + 'static,
|
||||
U: Future<Output = Result<(), ClientError>> + Send + 'static,
|
||||
{
|
||||
self.subscriptions
|
||||
.write()
|
||||
.await
|
||||
.insert(msg.msg_id, Box::new(move |x| Box::pin(on_update(x))));
|
||||
|
||||
tracing::debug!("SEND {}", msg);
|
||||
if let Err(e) = (self.send)(msg).await {
|
||||
self.subscriptions.write().await.remove(&msg.msg_id);
|
||||
Err(ClientError::Unknown(e.to_string()))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a `ClientReq` and await both the successful completion of the
|
||||
/// `send`, _and_ the `ClientResp` which is returned.
|
||||
pub(crate) async fn oneshot(&self, req: &Request) -> ClientResult<ClientResp> {
|
||||
let (sender, receiver) = futures::channel::oneshot::channel::<ClientResp>();
|
||||
let on_update = Box::new(move |res: Response| {
|
||||
sender.send(res.client_resp.unwrap()).map_err(|x| x.into())
|
||||
});
|
||||
|
||||
self.subscribe_once(req, on_update).await?;
|
||||
receiver
|
||||
.await
|
||||
.map_err(|_| ClientError::Unknown(format!("Internal error for req {req}")))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_features(&self) -> ClientResult<Features> {
|
||||
let mut guard = self.features.lock().await;
|
||||
let features = if let Some(features) = &*guard {
|
||||
features.clone()
|
||||
} else {
|
||||
let msg = Request {
|
||||
msg_id: self.gen_id(),
|
||||
entity_id: "".to_owned(),
|
||||
client_req: Some(ClientReq::GetFeaturesReq(GetFeaturesReq {})),
|
||||
};
|
||||
|
||||
let features = Features(Arc::new(match self.oneshot(&msg).await? {
|
||||
ClientResp::GetFeaturesResp(features) => Ok(features),
|
||||
resp => Err(resp),
|
||||
}?));
|
||||
|
||||
*guard = Some(features.clone());
|
||||
features
|
||||
};
|
||||
|
||||
Ok(features)
|
||||
}
|
||||
|
||||
/// Creates a new [`Table`] from either a _schema_ or _data_.
|
||||
///
|
||||
/// The [`Client::table`] factory function can be initialized with either a
|
||||
/// _schema_ (see [`Table::schema`]), or data in one of these formats:
|
||||
///
|
||||
/// - Apache Arrow
|
||||
/// - CSV
|
||||
/// - JSON row-oriented
|
||||
/// - JSON column-oriented
|
||||
/// - NDJSON
|
||||
///
|
||||
/// When instantiated with _data_, the schema is inferred from this data.
|
||||
/// While this is convenient, inferrence is sometimes imperfect e.g.
|
||||
/// when the input is empty, null or ambiguous. For these cases,
|
||||
/// [`Client::table`] can first be instantiated with a explicit schema.
|
||||
///
|
||||
/// When instantiated with a _schema_, the resulting [`Table`] is empty but
|
||||
/// with known column names and column types. When subsqeuently
|
||||
/// populated with [`Table::update`], these columns will be _coerced_ to
|
||||
/// the schema's type. This behavior can be useful when
|
||||
/// [`Client::table`]'s column type inferences doesn't work.
|
||||
///
|
||||
/// The resulting [`Table`] is _virtual_, and invoking its methods
|
||||
/// dispatches events to the `perspective_server::Server` this
|
||||
/// [`Client`] connects to, where the data is stored and all calculation
|
||||
/// occurs.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `arg` - Either _schema_ or initialization _data_.
|
||||
/// - `options` - Optional configuration which provides one of:
|
||||
/// - `limit` - The max number of rows the resulting [`Table`] can
|
||||
/// store.
|
||||
/// - `index` - The column name to use as an _index_ column. If this
|
||||
/// `Table` is being instantiated by _data_, this column name must be
|
||||
/// present in the data.
|
||||
/// - `name` - The name of the table. This will be generated if it is
|
||||
/// not provided.
|
||||
/// - `format` - The explicit format of the input data, can be one of
|
||||
/// `"json"`, `"columns"`, `"csv"` or `"arrow"`. This overrides
|
||||
/// language-specific type dispatch behavior, which allows stringified
|
||||
/// and byte array alternative inputs.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Load a CSV from a `String`:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use perspective_client::*;
|
||||
/// # async fn run(client: Client) -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let opts = TableInitOptions::default();
|
||||
/// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
|
||||
/// let table = client.table(data, opts).await?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub async fn table(&self, input: TableData, options: TableInitOptions) -> ClientResult<Table> {
|
||||
let entity_id = match options.name.clone() {
|
||||
Some(x) => x.to_owned(),
|
||||
None => randid(),
|
||||
};
|
||||
|
||||
if let TableData::View(view) = &input {
|
||||
let window = ViewWindow::default();
|
||||
let arrow = view.to_arrow(window).await?;
|
||||
let mut table = self
|
||||
.crate_table_inner(UpdateData::Arrow(arrow).into(), options.into(), entity_id)
|
||||
.await?;
|
||||
|
||||
let table_ = table.clone();
|
||||
let callback = asyncfn!(table_, update, async move |update: OnUpdateData| {
|
||||
let update = UpdateData::Arrow(update.delta.expect("Malformed message").into());
|
||||
let options = crate::UpdateOptions::default();
|
||||
table_.update(update, options).await.unwrap_or_log();
|
||||
});
|
||||
|
||||
let options = OnUpdateOptions {
|
||||
mode: Some(OnUpdateMode::Row),
|
||||
};
|
||||
|
||||
let on_update_token = view.on_update(callback, options).await?;
|
||||
table.view_update_token = Some(on_update_token);
|
||||
Ok(table)
|
||||
} else {
|
||||
self.crate_table_inner(input, options.into(), entity_id)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn crate_table_inner(
|
||||
&self,
|
||||
input: TableData,
|
||||
options: TableOptions,
|
||||
entity_id: String,
|
||||
) -> ClientResult<Table> {
|
||||
let msg = Request {
|
||||
msg_id: self.gen_id(),
|
||||
entity_id: entity_id.clone(),
|
||||
client_req: Some(ClientReq::MakeTableReq(MakeTableReq {
|
||||
data: Some(input.into()),
|
||||
options: Some(options.clone().try_into()?),
|
||||
})),
|
||||
};
|
||||
|
||||
let client = self.clone();
|
||||
match self.oneshot(&msg).await? {
|
||||
ClientResp::MakeTableResp(_) => Ok(Table::new(entity_id, client, options)),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new read-only [`Table`] by performing a JOIN on two source
|
||||
/// tables. The resulting table is reactive: when either source table is
|
||||
/// updated, the join is automatically recomputed.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `left` - The left source table (as a [`Table`] or name string).
|
||||
/// * `right` - The right source table (as a [`Table`] or name string).
|
||||
/// * `on` - The column name to join on. Must exist in both tables with the
|
||||
/// same type.
|
||||
/// * `options` - Join configuration (join type, table name).
|
||||
pub async fn join(
|
||||
&self,
|
||||
left: TableRef,
|
||||
right: TableRef,
|
||||
on: &str,
|
||||
options: JoinOptions,
|
||||
) -> ClientResult<Table> {
|
||||
let entity_id = options.name.unwrap_or_else(randid);
|
||||
let join_type: JoinType = options.join_type.unwrap_or_default();
|
||||
let right_on_column = options.right_on.unwrap_or_default();
|
||||
let msg = Request {
|
||||
msg_id: self.gen_id(),
|
||||
entity_id: entity_id.clone(),
|
||||
client_req: Some(ClientReq::MakeJoinTableReq(MakeJoinTableReq {
|
||||
left_table_id: left.table_name().to_owned(),
|
||||
right_table_id: right.table_name().to_owned(),
|
||||
on_column: on.to_owned(),
|
||||
join_type: join_type.into(),
|
||||
right_on_column,
|
||||
})),
|
||||
};
|
||||
|
||||
let client = self.clone();
|
||||
match self.oneshot(&msg).await? {
|
||||
ClientResp::MakeJoinTableResp(_) => Ok(Table::new(entity_id, client, TableOptions {
|
||||
index: Some(on.to_owned()),
|
||||
limit: None,
|
||||
page_to_disk: None,
|
||||
})),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_table_infos(&self) -> ClientResult<Vec<HostedTable>> {
|
||||
let msg = Request {
|
||||
msg_id: self.gen_id(),
|
||||
entity_id: "".to_owned(),
|
||||
client_req: Some(ClientReq::GetHostedTablesReq(GetHostedTablesReq {
|
||||
subscribe: false,
|
||||
})),
|
||||
};
|
||||
|
||||
match self.oneshot(&msg).await? {
|
||||
ClientResp::GetHostedTablesResp(GetHostedTablesResp { table_infos }) => Ok(table_infos),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a [`Table`] that is hosted on the `perspective_server::Server`
|
||||
/// that is connected to this [`Client`].
|
||||
///
|
||||
/// The `name` property of [`TableInitOptions`] is used to identify each
|
||||
/// [`Table`]. [`Table`] `name`s can be looked up for each [`Client`]
|
||||
/// via [`Client::get_hosted_table_names`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use perspective_client::Client;
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let client: Client = todo!();
|
||||
/// let table = client.open_table("table_one".to_owned()).await?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub async fn open_table(&self, entity_id: String) -> ClientResult<Table> {
|
||||
let infos = self.get_table_infos().await?;
|
||||
|
||||
// TODO fix this - name is repeated 2x
|
||||
if let Some(info) = infos.into_iter().find(|i| i.entity_id == entity_id) {
|
||||
let options = TableOptions {
|
||||
index: info.index,
|
||||
limit: info.limit,
|
||||
// `page_to_disk` is a server-side property not surfaced in table
|
||||
// info; it does not affect client-side behavior.
|
||||
page_to_disk: None,
|
||||
};
|
||||
|
||||
let client = self.clone();
|
||||
Ok(Table::new(entity_id, client, options))
|
||||
} else {
|
||||
Err(ClientError::Unknown(format!(
|
||||
"Unknown table \"{}\"",
|
||||
entity_id
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the names of all tables that this client has access to.
|
||||
///
|
||||
/// `name` is a string identifier unique to the [`Table`] (per [`Client`]),
|
||||
/// which can be used in conjunction with [`Client::open_table`] to get
|
||||
/// a [`Table`] instance without the use of [`Client::table`]
|
||||
/// constructor directly (e.g., one created by another [`Client`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use perspective_client::Client;
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let client: Client = todo!();
|
||||
/// let tables = client.get_hosted_table_names().await?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub async fn get_hosted_table_names(&self) -> ClientResult<Vec<String>> {
|
||||
let msg = Request {
|
||||
msg_id: self.gen_id(),
|
||||
entity_id: "".to_owned(),
|
||||
client_req: Some(ClientReq::GetHostedTablesReq(GetHostedTablesReq {
|
||||
subscribe: false,
|
||||
})),
|
||||
};
|
||||
|
||||
match self.oneshot(&msg).await? {
|
||||
ClientResp::GetHostedTablesResp(GetHostedTablesResp { table_infos }) => {
|
||||
Ok(table_infos.into_iter().map(|i| i.entity_id).collect())
|
||||
},
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a callback which is invoked whenever [`Client::table`] (on this
|
||||
/// [`Client`]) or [`Table::delete`] (on a [`Table`] belinging to this
|
||||
/// [`Client`]) are called.
|
||||
pub async fn on_hosted_tables_update<T, U>(&self, on_update: T) -> ClientResult<u32>
|
||||
where
|
||||
T: Fn() -> U + Send + Sync + 'static,
|
||||
U: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
let on_update = Arc::new(on_update);
|
||||
let callback = asyncfn!(on_update, async move |resp: Response| {
|
||||
match resp.client_resp {
|
||||
Some(ClientResp::GetHostedTablesResp(_)) | None => {
|
||||
on_update().await;
|
||||
Ok(())
|
||||
},
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
});
|
||||
|
||||
let msg = Request {
|
||||
msg_id: self.gen_id(),
|
||||
entity_id: "".to_owned(),
|
||||
client_req: Some(ClientReq::GetHostedTablesReq(GetHostedTablesReq {
|
||||
subscribe: true,
|
||||
})),
|
||||
};
|
||||
|
||||
self.subscribe(&msg, callback).await?;
|
||||
Ok(msg.msg_id)
|
||||
}
|
||||
|
||||
/// Remove a callback previously registered via
|
||||
/// `Client::on_hosted_tables_update`.
|
||||
pub async fn remove_hosted_tables_update(&self, update_id: u32) -> ClientResult<()> {
|
||||
let msg = Request {
|
||||
msg_id: self.gen_id(),
|
||||
entity_id: "".to_owned(),
|
||||
client_req: Some(ClientReq::RemoveHostedTablesUpdateReq(
|
||||
RemoveHostedTablesUpdateReq { id: update_id },
|
||||
)),
|
||||
};
|
||||
|
||||
self.unsubscribe(update_id).await?;
|
||||
match self.oneshot(&msg).await? {
|
||||
ClientResp::RemoveHostedTablesUpdateResp(_) => Ok(()),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the [`SystemInfo`] struct, implementation-specific metadata
|
||||
/// about the [`perspective_server::Server`] runtime such as Memory and
|
||||
/// CPU usage.
|
||||
pub async fn system_info(&self) -> ClientResult<SystemInfo> {
|
||||
let msg = Request {
|
||||
msg_id: self.gen_id(),
|
||||
entity_id: "".to_string(),
|
||||
client_req: Some(ClientReq::ServerSystemInfoReq(ServerSystemInfoReq {})),
|
||||
};
|
||||
|
||||
match self.oneshot(&msg).await? {
|
||||
ClientResp::ServerSystemInfoResp(resp) => {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let timestamp = Some(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)?
|
||||
.as_millis() as u64,
|
||||
);
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
let timestamp = None;
|
||||
|
||||
#[cfg(feature = "talc-allocator")]
|
||||
let (client_used, client_heap) = {
|
||||
let (client_used, client_heap) = crate::utils::get_used();
|
||||
(Some(client_used as u64), Some(client_heap as u64))
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "talc-allocator"))]
|
||||
let (client_used, client_heap) = (None, None);
|
||||
|
||||
let info = SystemInfo {
|
||||
heap_size: resp.heap_size,
|
||||
used_size: resp.used_size,
|
||||
cpu_time: resp.cpu_time,
|
||||
cpu_time_epoch: resp.cpu_time_epoch,
|
||||
timestamp,
|
||||
client_heap,
|
||||
client_used,
|
||||
};
|
||||
|
||||
Ok(info)
|
||||
},
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::proto::view_config;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, TS)]
|
||||
#[serde(untagged)]
|
||||
pub enum Aggregate {
|
||||
SingleAggregate(String),
|
||||
MultiAggregate(String, Vec<String>),
|
||||
}
|
||||
|
||||
impl From<&'static str> for Aggregate {
|
||||
fn from(value: &'static str) -> Self {
|
||||
if value.contains(" by ") {
|
||||
let mut parts = value.split(" by ");
|
||||
Aggregate::MultiAggregate(
|
||||
parts.next().unwrap().into(),
|
||||
parts.map(|x| x.into()).collect(),
|
||||
)
|
||||
} else {
|
||||
Aggregate::SingleAggregate(value.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Aggregate {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
match self {
|
||||
Self::SingleAggregate(x) => write!(fmt, "{x}")?,
|
||||
Self::MultiAggregate(agg, x) => write!(fmt, "{} by {}", agg, x.join(", "))?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Aggregate> for view_config::AggList {
|
||||
fn from(value: Aggregate) -> Self {
|
||||
view_config::AggList {
|
||||
aggregations: match value {
|
||||
Aggregate::SingleAggregate(x) => vec![x],
|
||||
Aggregate::MultiAggregate(x, y) => {
|
||||
vec![x, y.join(",")]
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<view_config::AggList> for Aggregate {
|
||||
fn from(value: view_config::AggList) -> Self {
|
||||
if value.aggregations.len() == 1 {
|
||||
Aggregate::SingleAggregate(value.aggregations.first().unwrap().clone())
|
||||
} else {
|
||||
Aggregate::MultiAggregate(
|
||||
value.aggregations.first().unwrap().clone(),
|
||||
value
|
||||
.aggregations
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.split(",")
|
||||
.map(|x| x.to_owned())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::ClientError;
|
||||
use crate::proto::ColumnType;
|
||||
|
||||
impl Display for ColumnType {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
write!(fmt, "{}", match self {
|
||||
Self::String => "string",
|
||||
Self::Integer => "integer",
|
||||
Self::Float => "float",
|
||||
Self::Boolean => "boolean",
|
||||
Self::Date => "date",
|
||||
Self::Datetime => "datetime",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ColumnType {
|
||||
type Err = ClientError;
|
||||
|
||||
fn from_str(val: &str) -> Result<Self, Self::Err> {
|
||||
val.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ColumnType {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(val: &str) -> Result<Self, Self::Error> {
|
||||
if val == "string" {
|
||||
Ok(Self::String)
|
||||
} else if val == "integer" {
|
||||
Ok(Self::Integer)
|
||||
} else if val == "float" {
|
||||
Ok(Self::Float)
|
||||
} else if val == "boolean" {
|
||||
Ok(Self::Boolean)
|
||||
} else if val == "date" {
|
||||
Ok(Self::Date)
|
||||
} else if val == "datetime" {
|
||||
Ok(Self::Datetime)
|
||||
} else {
|
||||
Err(ClientError::Internal(format!("Unknown type {val}")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColumnType {
|
||||
pub fn to_capitalized(&self) -> String {
|
||||
match self {
|
||||
ColumnType::String => "String",
|
||||
ColumnType::Datetime => "Datetime",
|
||||
ColumnType::Date => "Date",
|
||||
ColumnType::Integer => "Integer",
|
||||
ColumnType::Float => "Float",
|
||||
ColumnType::Boolean => "Boolean",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,685 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
//! Perspective supports _expression columns_, which are virtual columns
|
||||
//! calculated as part of the [`crate::View`], optionally using values from its
|
||||
//! underlying [`crate::Table`]'s columns. Such expression columns are defined
|
||||
//! in Perspective's expression language, an extended version of
|
||||
//! [ExprTK](https://github.com/ArashPartow/exprtk), which is itself quite similar
|
||||
//! (in design and features) to expressions in Excel.
|
||||
//!
|
||||
//! ## UI
|
||||
//!
|
||||
//! Expression columns can be created in `<perspective-viewer>` by clicking the
|
||||
//! "New Column" button at the bottom of the column list, or via the API by
|
||||
//! adding the expression to the `expressions` config key when calling
|
||||
//! `viewer.restore()`.
|
||||
//!
|
||||
//! By default, such expression columns are not "used", and will appear above
|
||||
//! the `Table`'s other deselected columns in the column list, with an
|
||||
//! additional set of buttons for:
|
||||
//!
|
||||
//! - _Editing_ the column's expression. Doing so will update the definitions of
|
||||
//! _all_ usage of the expression column.
|
||||
//! - _Deleting_ the column. Clicking `Reset` (or calling the `reset()` method)
|
||||
//! will not delete expressions unless the `Shift` key is held (or `true`
|
||||
//! parameter supplied, respectively). This button only appears if the
|
||||
//! expression column i unused.
|
||||
//!
|
||||
//! To use the column, just drag/select the column as you would a normal column,
|
||||
//! e.g. as a "Filter", "Group By", etc. Expression columns will recalculate
|
||||
//! whenever their dependent columns update.
|
||||
//!
|
||||
//! ## Perspective Extensions to ExprTK
|
||||
//!
|
||||
//! ExprTK has its own
|
||||
//! [excellent documentation](http://www.partow.net/programming/exprtk/) which
|
||||
//! covers the core langauge in depth, which is an excellent place to start in
|
||||
//! learning the basics. In addition to these features, Perspective adds a few
|
||||
//! of its own custom extensions and syntax.
|
||||
//!
|
||||
//! #### Static Typing
|
||||
//!
|
||||
//! In addition to `float` values which ExprTK supports natively, Perspective's
|
||||
//! expression language also supports Perspective's other types `date`,
|
||||
//! `datetime`, `integer`, `boolean`; as well as rudimentary type-checking,
|
||||
//! which will report an <span>error</span> when the values/columns supplied as
|
||||
//! arguments cannot be resolved to the expected type, e.g. `length(x)` expects
|
||||
//! an argument `x` of type `string` and is not a valid expression for an `x` of
|
||||
//! another type. Perspective supplies a set of _cast_ functions for converting
|
||||
//! between types where possible e.g. `string(x)` to cast a variable `x` to a
|
||||
//! `string`.
|
||||
//!
|
||||
//! #### Expression Column Name
|
||||
//!
|
||||
//! Expressions are named by their key in the `ViewConfig::expressions`
|
||||
//! `HashMap`. This name will be used in the `<perspective-viewer>` UI when
|
||||
//! referring to the column, but will also be used in the API when specifying
|
||||
//! e.g. `group_by` or `sort` fields. When creating a new column via
|
||||
//! `<oerspective-viewer>`'s expression editor, new columns will get a default
|
||||
//! name (which you may delete or change):
|
||||
//!
|
||||
//! ```html
|
||||
//! // New Column 1
|
||||
//! ```
|
||||
//!
|
||||
//! Without such a comment, an expression will show up in the
|
||||
//! `<perspective-viewer>` API and UI as itself (clipped to a reasonable length
|
||||
//! for the latter).
|
||||
//!
|
||||
//! #### Referencing [`crate::Table`] Columns
|
||||
//!
|
||||
//! Columns from the [`crate::Table`] can be referenced in an expression with
|
||||
//! _double quotes_.
|
||||
//!
|
||||
//! ```text
|
||||
//! // Expected Sales ("Sales" * 10) + "Profit"
|
||||
//! ```
|
||||
//!
|
||||
//! #### String Literals
|
||||
//!
|
||||
//! In contrast to standard ExprTK, string literals are declared with _single
|
||||
//! quotes_:
|
||||
//!
|
||||
//! ```text
|
||||
//! // Profitable
|
||||
//! if ("Profit" > 0) {
|
||||
//! 'Stonks'
|
||||
//! } else {
|
||||
//! 'Not Stonks'
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! #### Extended Library
|
||||
//!
|
||||
//! Perspective adds many of its own functions in addition to `ExprTK`'s
|
||||
//! standard ones, including common functions for `datetime` and `string` types
|
||||
//! such as `substring()`, `bucket()`, `day_of_week()`, etc. A full list of
|
||||
//! available functions is available in the
|
||||
//! [Expression Columns API](../obj/perspective-viewer-exprtk).
|
||||
//!
|
||||
//! ## Examples
|
||||
//!
|
||||
//! #### Casting
|
||||
//!
|
||||
//! Just `2`, as an `integer` (numeric literals currently default to `float`
|
||||
//! unless cast).
|
||||
//!
|
||||
//! ```text
|
||||
//! integer(2)
|
||||
//! ```
|
||||
//!
|
||||
//! #### Variables
|
||||
//!
|
||||
//! ```text
|
||||
//! // My Column Name
|
||||
//! var incrementedBy200 := "Sales" + 200;
|
||||
//! var half := incrementedBy200 / 2;
|
||||
//! half
|
||||
//! ```
|
||||
//!
|
||||
//! ```text
|
||||
//! // Complex Expression
|
||||
//! var upperCustomer := upper("Customer Name");
|
||||
//! var separator := concat(upperCustomer, ' | ');
|
||||
//! var profitRatio := floor(percent_of("Profit", "Sales")); // Remove trailing decimal.
|
||||
//! var combined := concat(separator, string(profitRatio));
|
||||
//! var percentDisplay := concat(combined, '%');
|
||||
//! percentDisplay
|
||||
//! ```
|
||||
//!
|
||||
//! #### Conditionals
|
||||
//!
|
||||
//! ```text
|
||||
//! // Conditional
|
||||
//! var priceAdjustmentDate := date(2016, 6, 18);
|
||||
//! var finalPrice := "Sales" - "Discount";
|
||||
//! var additionalModifier := 0;
|
||||
//!
|
||||
//! if("Order Date" > priceAdjustmentDate) {
|
||||
//! finalPrice -= 5;
|
||||
//! additionalModifier -= 2;
|
||||
//! }
|
||||
//! else
|
||||
//! finalPrice += 5;
|
||||
//!
|
||||
//! finalPrice + additionalModifier
|
||||
//! ```
|
||||
|
||||
#![cfg_attr(not(feature = "omit_metadata"), doc = include_str!("../../../docs/expression_gen.md"))]
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Deserialize, Clone, PartialEq, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum ExpressionsDeserde {
|
||||
Array(Vec<String>),
|
||||
Map(HashMap<String, String>),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Debug, Default, TS)]
|
||||
#[serde(from = "ExpressionsDeserde")]
|
||||
pub struct Expressions(pub HashMap<String, String>);
|
||||
|
||||
impl std::ops::Deref for Expressions {
|
||||
type Target = HashMap<String, String>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for Expressions {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade_legacy_format(expressions: &[String]) -> HashMap<String, String> {
|
||||
tracing::debug!("Legacy `expressions` format: {:?}", expressions);
|
||||
expressions
|
||||
.iter()
|
||||
.map(|s| {
|
||||
if let Some((name, expression)) = s.split_once('\n') {
|
||||
if !expression.is_empty() && name.starts_with("//") {
|
||||
(name.split_at(2).1.trim().to_owned(), expression.to_owned())
|
||||
} else {
|
||||
(s.to_owned(), s.to_owned())
|
||||
}
|
||||
} else {
|
||||
(s.to_owned(), s.to_owned())
|
||||
}
|
||||
})
|
||||
.collect::<HashMap<_, _>>()
|
||||
}
|
||||
|
||||
impl From<ExpressionsDeserde> for Expressions {
|
||||
fn from(value: ExpressionsDeserde) -> Self {
|
||||
match value {
|
||||
ExpressionsDeserde::Array(arr) => Self(upgrade_legacy_format(&arr)),
|
||||
ExpressionsDeserde::Map(map) => Self(map),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, TS)]
|
||||
pub struct Expression<'a> {
|
||||
pub name: Cow<'a, str>,
|
||||
pub expression: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl<'a> Expression<'a> {
|
||||
/// If name is None, the expression is used as the name.
|
||||
pub fn new(name: Option<Cow<'a, str>>, expression: Cow<'a, str>) -> Self {
|
||||
Self {
|
||||
name: name.unwrap_or_else(|| expression.clone()),
|
||||
expression,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromIterator<Expression<'a>> for Expressions {
|
||||
fn from_iter<T: IntoIterator<Item = Expression<'a>>>(iter: T) -> Self {
|
||||
Self(
|
||||
iter.into_iter()
|
||||
.map(|x| (x.name.as_ref().to_owned(), x.expression.as_ref().to_owned()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Expressions {
|
||||
pub fn insert(&mut self, expr: &Expression) {
|
||||
self.0.insert(
|
||||
expr.name.as_ref().to_owned(),
|
||||
expr.expression.as_ref().to_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Serialize, Clone, Copy, PartialEq)]
|
||||
pub struct CompletionItemSuggestion {
|
||||
pub label: &'static str,
|
||||
pub insert_text: &'static str,
|
||||
pub documentation: &'static str,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub static COMPLETIONS: [CompletionItemSuggestion; 79] = [
|
||||
CompletionItemSuggestion {
|
||||
label: "var",
|
||||
insert_text: "var ${1:x := 1}",
|
||||
documentation: "Declare a new local variable",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "abs",
|
||||
insert_text: "abs(${1:x})",
|
||||
documentation: "Absolute value of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "avg",
|
||||
insert_text: "avg(${1:x})",
|
||||
documentation: "Average of all inputs",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "bucket",
|
||||
insert_text: "bucket(${1:x}, ${2:y})",
|
||||
documentation: "Bucket x by y",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "ceil",
|
||||
insert_text: "ceil(${1:x})",
|
||||
documentation: "Smallest integer >= x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "exp",
|
||||
insert_text: "exp(${1:x})",
|
||||
documentation: "Natural exponent of x (e ^ x)",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "floor",
|
||||
insert_text: "floor(${1:x})",
|
||||
documentation: "Largest integer <= x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "frac",
|
||||
insert_text: "frac(${1:x})",
|
||||
documentation: "Fractional portion (after the decimal) of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "iclamp",
|
||||
insert_text: "iclamp(${1:x})",
|
||||
documentation: "Inverse clamp x within a range",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "inrange",
|
||||
insert_text: "inrange(${1:x})",
|
||||
documentation: "Returns whether x is within a range",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "log",
|
||||
insert_text: "log(${1:x})",
|
||||
documentation: "Natural log of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "log10",
|
||||
insert_text: "log10(${1:x})",
|
||||
documentation: "Base 10 log of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "log1p",
|
||||
insert_text: "log1p(${1:x})",
|
||||
documentation: "Natural log of 1 + x where x is very small",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "log2",
|
||||
insert_text: "log2(${1:x})",
|
||||
documentation: "Base 2 log of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "logn",
|
||||
insert_text: "logn(${1:x}, ${2:N})",
|
||||
documentation: "Base N log of x where N >= 0",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "max",
|
||||
insert_text: "max(${1:x})",
|
||||
documentation: "Maximum value of all inputs",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "min",
|
||||
insert_text: "min(${1:x})",
|
||||
documentation: "Minimum value of all inputs",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "mul",
|
||||
insert_text: "mul(${1:x})",
|
||||
documentation: "Product of all inputs",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "percent_of",
|
||||
insert_text: "percent_of(${1:x})",
|
||||
documentation: "Percent y of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "pow",
|
||||
insert_text: "pow(${1:x}, ${2:y})",
|
||||
documentation: "x to the power of y",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "root",
|
||||
insert_text: "root(${1:x}, ${2:N})",
|
||||
documentation: "N-th root of x where N >= 0",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "round",
|
||||
insert_text: "round(${1:x})",
|
||||
documentation: "Round x to the nearest integer",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "sgn",
|
||||
insert_text: "sgn(${1:x})",
|
||||
documentation: "Sign of x: -1, 1, or 0",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "sqrt",
|
||||
insert_text: "sqrt(${1:x})",
|
||||
documentation: "Square root of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "sum",
|
||||
insert_text: "sum(${1:x})",
|
||||
documentation: "Sum of all inputs",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "trunc",
|
||||
insert_text: "trunc(${1:x})",
|
||||
documentation: "Integer portion of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "acos",
|
||||
insert_text: "acos(${1:x})",
|
||||
documentation: "Arc cosine of x in radians",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "acosh",
|
||||
insert_text: "acosh(${1:x})",
|
||||
documentation: "Inverse hyperbolic cosine of x in radians",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "asin",
|
||||
insert_text: "asin(${1:x})",
|
||||
documentation: "Arc sine of x in radians",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "asinh",
|
||||
insert_text: "asinh(${1:x})",
|
||||
documentation: "Inverse hyperbolic sine of x in radians",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "atan",
|
||||
insert_text: "atan(${1:x})",
|
||||
documentation: "Arc tangent of x in radians",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "atanh",
|
||||
insert_text: "atanh(${1:x})",
|
||||
documentation: "Inverse hyperbolic tangent of x in radians",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "cos",
|
||||
insert_text: "cos(${1:x})",
|
||||
documentation: "Cosine of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "cosh",
|
||||
insert_text: "cosh(${1:x})",
|
||||
documentation: "Hyperbolic cosine of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "cot",
|
||||
insert_text: "cot(${1:x})",
|
||||
documentation: "Cotangent of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "sin",
|
||||
insert_text: "sin(${1:x})",
|
||||
documentation: "Sine of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "sinc",
|
||||
insert_text: "sinc(${1:x})",
|
||||
documentation: "Sine cardinal of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "sinh",
|
||||
insert_text: "sinh(${1:x})",
|
||||
documentation: "Hyperbolic sine of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "tan",
|
||||
insert_text: "tan(${1:x})",
|
||||
documentation: "Tangent of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "tanh",
|
||||
insert_text: "tanh(${1:x})",
|
||||
documentation: "Hyperbolic tangent of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "deg2rad",
|
||||
insert_text: "deg2rad(${1:x})",
|
||||
documentation: "Convert x from degrees to radians",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "deg2grad",
|
||||
insert_text: "deg2grad(${1:x})",
|
||||
documentation: "Convert x from degrees to gradians",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "rad2deg",
|
||||
insert_text: "rad2deg(${1:x})",
|
||||
documentation: "Convert x from radians to degrees",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "grad2deg",
|
||||
insert_text: "grad2deg(${1:x})",
|
||||
documentation: "Convert x from gradians to degrees",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "concat",
|
||||
insert_text: "concat(${1:x}, ${2:y})",
|
||||
documentation: "Concatenate string columns and string literals, such \
|
||||
as:\nconcat(\"State\" ', ', \"City\")",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "order",
|
||||
insert_text: "order(${1:input column}, ${2:value}, ...)",
|
||||
documentation: "Generates a sort order for a string column based on the input order of \
|
||||
the parameters, such as:\norder(\"State\", 'Texas', 'New York')",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "upper",
|
||||
insert_text: "upper(${1:x})",
|
||||
documentation: "Uppercase of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "lower",
|
||||
insert_text: "lower(${1:x})",
|
||||
documentation: "Lowercase of x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "hour_of_day",
|
||||
insert_text: "hour_of_day(${1:x})",
|
||||
documentation: "Return a datetime's hour of the day as a string",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "month_of_year",
|
||||
insert_text: "month_of_year(${1:x})",
|
||||
documentation: "Return a datetime's month of the year as a string",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "day_of_week",
|
||||
insert_text: "day_of_week(${1:x})",
|
||||
documentation: "Return a datetime's day of week as a string",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "now",
|
||||
insert_text: "now()",
|
||||
documentation: "The current datetime in local time",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "today",
|
||||
insert_text: "today()",
|
||||
documentation: "The current date in local time",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "is_null",
|
||||
insert_text: "is_null(${1:x})",
|
||||
documentation: "Whether x is a null value",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "is_not_null",
|
||||
insert_text: "is_not_null(${1:x})",
|
||||
documentation: "Whether x is not a null value",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "coalesce",
|
||||
insert_text: "coalesce(${1:x}, ${2:y})",
|
||||
documentation: "Returns the first non-null argument.",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "contains",
|
||||
insert_text: "contains(${1:x}, ${2:'substr'})",
|
||||
documentation: "Whether the string column or value contains the literal substring.",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "not",
|
||||
insert_text: "not(${1:x})",
|
||||
documentation: "not x",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "true",
|
||||
insert_text: "true",
|
||||
documentation: "Boolean value true",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "false",
|
||||
insert_text: "false",
|
||||
documentation: "Boolean value false",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "if",
|
||||
insert_text: "if (${1:condition}) {} else if (${2:condition}) {} else {}",
|
||||
documentation: "An if/else conditional, which evaluates a condition such as:\n if \
|
||||
(\"Sales\" > 100) { true } else { false }",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "for",
|
||||
insert_text: "for (${1:expression}) {}",
|
||||
documentation: "A for loop, which repeatedly evaluates an incrementing expression such \
|
||||
as:\nvar x := 0; var y := 1; for (x < 10; x += 1) { y := x + y }",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "string",
|
||||
insert_text: "string(${1:x})",
|
||||
documentation: "Converts the given argument to a string",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "integer",
|
||||
insert_text: "integer(${1:x})",
|
||||
documentation: "Converts the given argument to a 32-bit integer. If the result \
|
||||
over/under-flows, null is returned",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "float",
|
||||
insert_text: "float(${1:x})",
|
||||
documentation: "Converts the argument to a float",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "date",
|
||||
insert_text: "date(${1:year}, ${1:month}, ${1:day})",
|
||||
documentation: "Given a year, month (1-12) and day, create a new date",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "datetime",
|
||||
insert_text: "datetime(${1:timestamp})",
|
||||
documentation: "Given a POSIX timestamp of milliseconds since epoch, create a new datetime",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "boolean",
|
||||
insert_text: "boolean(${1:x})",
|
||||
documentation: "Converts the given argument to a boolean",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "random",
|
||||
insert_text: "random()",
|
||||
documentation: "Returns a random float between 0 and 1, inclusive.",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "match",
|
||||
insert_text: "match(${1:string}, ${2:pattern})",
|
||||
documentation: "Returns True if any part of string matches pattern, and False otherwise.",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "match_all",
|
||||
insert_text: "match_all(${1:string}, ${2:pattern})",
|
||||
documentation: "Returns True if the whole string matches pattern, and False otherwise.",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "search",
|
||||
insert_text: "search(${1:string}, ${2:pattern})",
|
||||
documentation: "Returns the substring that matches the first capturing group in pattern, \
|
||||
or null if there are no capturing groups in the pattern or if there are \
|
||||
no matches.",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "indexof",
|
||||
insert_text: "indexof(${1:string}, ${2:pattern}, ${3:output_vector})",
|
||||
documentation: "Writes into index 0 and 1 of output_vector the start and end indices of \
|
||||
the substring that matches the first capturing group in \
|
||||
pattern.\n\nReturns true if there is a match and output was written, or \
|
||||
false if there are no capturing groups in the pattern, if there are no \
|
||||
matches, or if the indices are invalid.",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "substring",
|
||||
insert_text: "substring(${1:string}, ${2:start_idx}, ${3:length})",
|
||||
documentation: "Returns a substring of string from start_idx with the given length. If \
|
||||
length is not passed in, returns substring from start_idx to the end of \
|
||||
the string. Returns null if the string or any indices are invalid.",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "replace",
|
||||
insert_text: "replace(${1:string}, ${2:pattern}, ${3:replacer})",
|
||||
documentation: "Replaces the first match of pattern in string with replacer, or return \
|
||||
the original string if no replaces were made.",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "replace_all",
|
||||
insert_text: "replace_all(${1:string}, ${2:pattern}, ${3:replacer})",
|
||||
documentation: "Replaces all non-overlapping matches of pattern in string with replacer, \
|
||||
or return the original string if no replaces were made.",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "index",
|
||||
insert_text: "index()",
|
||||
documentation: "Looks up the index value of the current row",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "col",
|
||||
insert_text: "col(${1:string})",
|
||||
documentation: "Looks up a column value by name",
|
||||
},
|
||||
CompletionItemSuggestion {
|
||||
label: "vlookup",
|
||||
insert_text: "vlookup(${1:string}, ${2:uint64})",
|
||||
documentation: "Looks up a value in another column by index",
|
||||
},
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn test_completions_insert_text_matches_label() {
|
||||
for comp in COMPLETIONS {
|
||||
let label = comp.label;
|
||||
let insert_text = comp.insert_text;
|
||||
assert!(
|
||||
insert_text.starts_with(label),
|
||||
"insert_text for label {label} does not start with {label}:\n {insert_text}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::proto;
|
||||
use crate::proto::scalar;
|
||||
|
||||
/// This type represents the ViewConfig serializable type, which must be JSON
|
||||
/// safe.
|
||||
#[derive(Clone, Default, Deserialize, Debug, PartialEq, Serialize, TS)]
|
||||
#[serde(untagged)]
|
||||
pub enum Scalar {
|
||||
Float(f64),
|
||||
String(String),
|
||||
Bool(bool),
|
||||
// DateTime(i64),
|
||||
// Date(String),
|
||||
// Int(i32),
|
||||
#[default]
|
||||
Null,
|
||||
}
|
||||
|
||||
impl From<&str> for Scalar {
|
||||
fn from(value: &str) -> Self {
|
||||
Self::String(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Scalar {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
match self {
|
||||
Self::Float(x) => write!(fmt, "{x}"),
|
||||
Self::String(x) => write!(fmt, "{x}"),
|
||||
Self::Bool(x) => write!(fmt, "{x}"),
|
||||
Self::Null => write!(fmt, ""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Debug, PartialEq, Serialize, TS)]
|
||||
#[serde(untagged)]
|
||||
pub enum FilterTerm {
|
||||
Array(Vec<Scalar>),
|
||||
Scalar(#[serde(default)] Scalar),
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for FilterTerm
|
||||
where
|
||||
T: AsRef<[&'a str]>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self::Array(value.as_ref().iter().map(|x| (*x).into()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FilterTerm {
|
||||
fn default() -> Self {
|
||||
Self::Scalar(Scalar::Null)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FilterTerm {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
match self {
|
||||
Self::Scalar(x) => {
|
||||
write!(fmt, "{x}")?;
|
||||
},
|
||||
Self::Array(xs) => write!(
|
||||
fmt,
|
||||
"{}",
|
||||
Itertools::intersperse(xs.iter().map(|x| format!("{x}")), ",".to_owned())
|
||||
.collect::<String>()
|
||||
)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FilterTerm {
|
||||
pub fn is_null(&self) -> bool {
|
||||
matches!(self, FilterTerm::Scalar(Scalar::Null))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Debug, PartialEq, Serialize, TS)]
|
||||
#[serde()]
|
||||
pub struct Filter(String, String, #[serde(default)] FilterTerm);
|
||||
|
||||
impl Filter {
|
||||
pub fn new<T>(column: &str, op: &str, term: T) -> Self
|
||||
where
|
||||
FilterTerm: From<T>,
|
||||
{
|
||||
Filter(column.to_string(), op.to_string(), term.into())
|
||||
}
|
||||
|
||||
pub fn column(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
|
||||
pub fn op(&self) -> &str {
|
||||
self.1.as_str()
|
||||
}
|
||||
|
||||
pub fn term(&self) -> &FilterTerm {
|
||||
&self.2
|
||||
}
|
||||
|
||||
pub fn column_mut(&mut self) -> &mut String {
|
||||
&mut self.0
|
||||
}
|
||||
|
||||
pub fn op_mut(&mut self) -> &mut String {
|
||||
&mut self.1
|
||||
}
|
||||
|
||||
pub fn term_mut(&mut self) -> &mut FilterTerm {
|
||||
&mut self.2
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq, TS)]
|
||||
pub enum FilterReducer {
|
||||
#[default]
|
||||
#[serde(rename = "and")]
|
||||
And,
|
||||
#[serde(rename = "or")]
|
||||
Or,
|
||||
}
|
||||
|
||||
impl From<Scalar> for proto::Scalar {
|
||||
fn from(value: Scalar) -> Self {
|
||||
match value {
|
||||
Scalar::Float(x) => proto::Scalar {
|
||||
scalar: Some(scalar::Scalar::Float(x)),
|
||||
},
|
||||
Scalar::String(x) => proto::Scalar {
|
||||
scalar: Some(scalar::Scalar::String(x)),
|
||||
},
|
||||
Scalar::Bool(x) => proto::Scalar {
|
||||
scalar: Some(scalar::Scalar::Bool(x)),
|
||||
},
|
||||
Scalar::Null => proto::Scalar {
|
||||
scalar: Some(scalar::Scalar::Null(0)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::Scalar> for Scalar {
|
||||
fn from(value: proto::Scalar) -> Self {
|
||||
match value.scalar {
|
||||
Some(scalar::Scalar::Bool(x)) => Scalar::Bool(x),
|
||||
Some(scalar::Scalar::String(x)) => Scalar::String(x),
|
||||
Some(scalar::Scalar::Float(x)) => Scalar::Float(x),
|
||||
Some(scalar::Scalar::Null(_)) => Scalar::Null,
|
||||
None => Scalar::Null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Filter> for proto::view_config::Filter {
|
||||
fn from(value: Filter) -> Self {
|
||||
proto::view_config::Filter {
|
||||
column: value.0,
|
||||
op: value.1,
|
||||
value: match value.2 {
|
||||
FilterTerm::Scalar(x) => vec![x.into()],
|
||||
FilterTerm::Array(x) => x.into_iter().map(|x| x.into()).collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::view_config::Filter> for Filter {
|
||||
fn from(value: proto::view_config::Filter) -> Self {
|
||||
Filter(
|
||||
value.column,
|
||||
value.op,
|
||||
if value.value.len() == 1 {
|
||||
FilterTerm::Scalar(value.value.into_iter().next().unwrap().into())
|
||||
} else {
|
||||
FilterTerm::Array(value.value.into_iter().map(|x| x.into()).collect())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
//! Options for [`Table::view`], and details about fields like `expressions`.
|
||||
//!
|
||||
//! [`ViewConfig`] for the top level argument to [`Table::view`].
|
||||
//!
|
||||
//! [`expressions`] module for details about the ExprTK format.
|
||||
|
||||
#[cfg(doc)]
|
||||
use crate::Table;
|
||||
|
||||
mod aggregates;
|
||||
mod column_type;
|
||||
pub mod expressions;
|
||||
mod filters;
|
||||
mod plugin;
|
||||
mod sort;
|
||||
mod view_config;
|
||||
|
||||
pub use aggregates::*;
|
||||
pub use expressions::*;
|
||||
pub use filters::*;
|
||||
pub use plugin::*;
|
||||
pub use sort::*;
|
||||
pub use view_config::*;
|
||||
|
||||
pub use crate::proto::{ColumnType, SortOp};
|
||||
@@ -0,0 +1,69 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
pub struct Symbol {
|
||||
/// the name of the symbol
|
||||
pub name: String,
|
||||
/// unescaped HTML string
|
||||
pub html: String,
|
||||
}
|
||||
impl std::fmt::Display for Symbol {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
pub struct SymbolAttributes {
|
||||
/// a vec of SVGs to render
|
||||
pub symbols: Vec<Symbol>,
|
||||
}
|
||||
|
||||
/// The default style configurations per type, as retrived by
|
||||
/// plugin.plugin_attributes
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct DefaultStyleAttributes {
|
||||
pub string: serde_json::Value,
|
||||
pub datetime: serde_json::Value,
|
||||
pub date: serde_json::Value,
|
||||
pub integer: serde_json::Value,
|
||||
pub float: serde_json::Value,
|
||||
pub bool: serde_json::Value,
|
||||
}
|
||||
|
||||
/// The data needed to populate a column's settings.
|
||||
///
|
||||
/// These are typically default values, a listing of possible values, or other
|
||||
/// basic configuration settings for the plugin. This is the result of calling
|
||||
/// plugin.plugin_attributes
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct PluginAttributes {
|
||||
pub symbol: Option<SymbolAttributes>,
|
||||
pub style: Option<DefaultStyleAttributes>,
|
||||
// color: ColorAttributes,
|
||||
// axis: AxisAttributes,
|
||||
//...
|
||||
}
|
||||
|
||||
/// The configuration which is created as the result of calling plugin.save
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
pub struct PluginConfig {
|
||||
/// Refers to the currently active columns. Maps name to configuration.
|
||||
#[serde(default)]
|
||||
pub columns: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::proto;
|
||||
|
||||
#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Serialize, TS)]
|
||||
#[serde()]
|
||||
pub struct Sort(pub String, pub SortDir);
|
||||
|
||||
#[derive(Clone, Copy, Deserialize, Debug, Eq, PartialEq, Serialize, TS)]
|
||||
#[serde()]
|
||||
pub enum SortDir {
|
||||
#[serde(rename = "none")]
|
||||
None,
|
||||
|
||||
#[serde(rename = "desc")]
|
||||
Desc,
|
||||
|
||||
#[serde(rename = "asc")]
|
||||
Asc,
|
||||
|
||||
#[serde(rename = "col desc")]
|
||||
ColDesc,
|
||||
|
||||
#[serde(rename = "col asc")]
|
||||
ColAsc,
|
||||
|
||||
#[serde(rename = "desc abs")]
|
||||
DescAbs,
|
||||
|
||||
#[serde(rename = "asc abs")]
|
||||
AscAbs,
|
||||
|
||||
#[serde(rename = "col desc abs")]
|
||||
ColDescAbs,
|
||||
|
||||
#[serde(rename = "col asc abs")]
|
||||
ColAscAbs,
|
||||
}
|
||||
|
||||
impl Display for SortDir {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
write!(fmt, "{}", match self {
|
||||
Self::None => "none",
|
||||
Self::Desc => "desc",
|
||||
Self::Asc => "asc",
|
||||
Self::ColDesc => "col desc",
|
||||
Self::ColAsc => "col asc",
|
||||
Self::DescAbs => "desc abs",
|
||||
Self::AscAbs => "asc abs",
|
||||
Self::ColDescAbs => "col desc abs",
|
||||
Self::ColAscAbs => "col asc abs",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SortDir {
|
||||
/// Increment the `SortDir` in logical order, given an `abs()` modifier.
|
||||
pub fn cycle(&self, split_by: bool, abs: bool) -> Self {
|
||||
let order: &[Self] = match (split_by, abs) {
|
||||
(false, false) => &[Self::None, Self::Asc, Self::Desc],
|
||||
(false, true) => &[Self::None, Self::AscAbs, Self::DescAbs],
|
||||
(true, false) => &[
|
||||
Self::None,
|
||||
Self::Asc,
|
||||
Self::Desc,
|
||||
Self::ColAsc,
|
||||
Self::ColDesc,
|
||||
],
|
||||
(true, true) => &[
|
||||
Self::None,
|
||||
Self::AscAbs,
|
||||
Self::DescAbs,
|
||||
Self::ColAscAbs,
|
||||
Self::ColDescAbs,
|
||||
],
|
||||
};
|
||||
|
||||
let index = order.iter().position(|x| x == self).unwrap_or(0);
|
||||
order[(index + 1) % order.len()]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SortDir> for proto::SortOp {
|
||||
fn from(value: SortDir) -> Self {
|
||||
match value {
|
||||
SortDir::None => proto::SortOp::SortNone,
|
||||
SortDir::Desc => proto::SortOp::SortDesc,
|
||||
SortDir::Asc => proto::SortOp::SortAsc,
|
||||
SortDir::ColDesc => proto::SortOp::SortColDesc,
|
||||
SortDir::ColAsc => proto::SortOp::SortColAsc,
|
||||
SortDir::DescAbs => proto::SortOp::SortDescAbs,
|
||||
SortDir::AscAbs => proto::SortOp::SortAscAbs,
|
||||
SortDir::ColDescAbs => proto::SortOp::SortColDescAbs,
|
||||
SortDir::ColAscAbs => proto::SortOp::SortColAscAbs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::SortOp> for SortDir {
|
||||
fn from(value: proto::SortOp) -> Self {
|
||||
match value {
|
||||
proto::SortOp::SortNone => SortDir::None,
|
||||
proto::SortOp::SortDesc => SortDir::Desc,
|
||||
proto::SortOp::SortAsc => SortDir::Asc,
|
||||
proto::SortOp::SortColAsc => SortDir::ColAsc,
|
||||
proto::SortOp::SortColDesc => SortDir::ColDesc,
|
||||
proto::SortOp::SortAscAbs => SortDir::AscAbs,
|
||||
proto::SortOp::SortDescAbs => SortDir::DescAbs,
|
||||
proto::SortOp::SortColAscAbs => SortDir::ColAscAbs,
|
||||
proto::SortOp::SortColDescAbs => SortDir::ColDescAbs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Sort> for proto::view_config::Sort {
|
||||
fn from(value: Sort) -> Self {
|
||||
proto::view_config::Sort {
|
||||
column: value.0,
|
||||
op: proto::SortOp::from(value.1).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::view_config::Sort> for Sort {
|
||||
fn from(value: proto::view_config::Sort) -> Self {
|
||||
Sort(
|
||||
value.column,
|
||||
proto::SortOp::try_from(value.op).unwrap().into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use super::aggregates::*;
|
||||
use super::expressions::*;
|
||||
use super::filters::*;
|
||||
use super::sort::*;
|
||||
use crate::proto;
|
||||
use crate::proto::columns_update;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq, TS)]
|
||||
pub enum GroupRollupMode {
|
||||
#[default]
|
||||
#[serde(rename = "rollup")]
|
||||
Rollup,
|
||||
|
||||
#[serde(rename = "flat")]
|
||||
Flat,
|
||||
|
||||
#[serde(rename = "total")]
|
||||
Total,
|
||||
}
|
||||
|
||||
impl Display for GroupRollupMode {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
write!(fmt, "{}", match self {
|
||||
Self::Rollup => "Rollup",
|
||||
Self::Flat => "Flat",
|
||||
Self::Total => "Total",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::GroupRollupMode> for GroupRollupMode {
|
||||
fn from(value: proto::GroupRollupMode) -> Self {
|
||||
match value {
|
||||
proto::GroupRollupMode::Rollup => Self::Rollup,
|
||||
proto::GroupRollupMode::Flat => Self::Flat,
|
||||
proto::GroupRollupMode::Total => Self::Total,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GroupRollupMode> for proto::GroupRollupMode {
|
||||
fn from(value: GroupRollupMode) -> Self {
|
||||
match value {
|
||||
GroupRollupMode::Rollup => proto::GroupRollupMode::Rollup,
|
||||
GroupRollupMode::Flat => proto::GroupRollupMode::Flat,
|
||||
GroupRollupMode::Total => proto::GroupRollupMode::Total,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default, PartialEq, Serialize, TS)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ViewConfig {
|
||||
#[serde(default)]
|
||||
pub group_by: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub split_by: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub sort: Vec<Sort>,
|
||||
|
||||
#[serde(default)]
|
||||
pub filter: Vec<Filter>,
|
||||
|
||||
// #[serde(skip_serializing_if = "is_default_value")]
|
||||
#[serde(default)]
|
||||
pub group_rollup_mode: GroupRollupMode,
|
||||
|
||||
#[serde(skip_serializing_if = "is_default_value")]
|
||||
#[serde(default)]
|
||||
pub filter_op: FilterReducer,
|
||||
|
||||
#[serde(default)]
|
||||
pub expressions: Expressions,
|
||||
|
||||
#[serde(default)]
|
||||
pub columns: Vec<Option<String>>,
|
||||
|
||||
#[serde(default)]
|
||||
pub aggregates: HashMap<String, Aggregate>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
pub group_by_depth: Option<u32>,
|
||||
}
|
||||
|
||||
fn is_default_value<A: Default + PartialEq>(value: &A) -> bool {
|
||||
value == &A::default()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default, PartialEq, Serialize, TS)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ViewConfigUpdate {
|
||||
/// A group by _groups_ the dataset by the unique values of each column used
|
||||
/// as a group by - a close analogue in SQL to the `GROUP BY` statement.
|
||||
/// The underlying dataset is aggregated to show the values belonging to
|
||||
/// each group, and a total row is calculated for each group, showing
|
||||
/// the currently selected aggregated value (e.g. `sum`) of the column.
|
||||
/// Group by are useful for hierarchies, categorizing data and
|
||||
/// attributing values, i.e. showing the number of units sold based on
|
||||
/// State and City. In Perspective, group by are represented as an array
|
||||
/// of string column names to pivot, are applied in the order provided;
|
||||
/// For example, a group by of `["State", "City", "Postal Code"]` shows
|
||||
/// the values for each Postal Code, which are grouped by City,
|
||||
/// which are in turn grouped by State.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub group_by: Option<Vec<String>>,
|
||||
|
||||
/// A split by _splits_ the dataset by the unique values of each column used
|
||||
/// as a split by. The underlying dataset is not aggregated, and a new
|
||||
/// column is created for each unique value of the split by. Each newly
|
||||
/// created column contains the parts of the dataset that correspond to
|
||||
/// the column header, i.e. a `View` that has `["State"]` as its split
|
||||
/// by will have a new column for each state. In Perspective, Split By
|
||||
/// are represented as an array of string column names to pivot.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub split_by: Option<Vec<String>>,
|
||||
|
||||
/// The `columns` property specifies which columns should be included in the
|
||||
/// [`crate::View`]'s output. This allows users to show or hide a specific
|
||||
/// subset of columns, as well as control the order in which columns
|
||||
/// appear to the user. This is represented in Perspective as an array
|
||||
/// of string column names.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub columns: Option<Vec<Option<String>>>,
|
||||
|
||||
/// The `filter` property specifies columns on which the query can be
|
||||
/// filtered, returning rows that pass the specified filter condition.
|
||||
/// This is analogous to the `WHERE` clause in SQL. There is no limit on
|
||||
/// the number of columns where `filter` is applied, but the resulting
|
||||
/// dataset is one that passes all the filter conditions, i.e. the
|
||||
/// filters are joined with an `AND` condition.
|
||||
///
|
||||
/// Perspective represents `filter` as an array of arrays, with the values
|
||||
/// of each inner array being a string column name, a string filter
|
||||
/// operator, and a filter operand in the type of the column.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub filter: Option<Vec<Filter>>,
|
||||
|
||||
/// The `sort` property specifies columns on which the query should be
|
||||
/// sorted, analogous to `ORDER BY` in SQL. A column can be sorted
|
||||
/// regardless of its data type, and sorts can be applied in ascending
|
||||
/// or descending order. Perspective represents `sort` as an array of
|
||||
/// arrays, with the values of each inner array being a string column
|
||||
/// name and a string sort direction. When `column-pivots` are applied,
|
||||
/// the additional sort directions `"col asc"` and `"col desc"` will
|
||||
/// determine the order of pivot columns groups.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub sort: Option<Vec<Sort>>,
|
||||
|
||||
/// The `expressions` property specifies _new_ columns in Perspective that
|
||||
/// are created using existing column values or arbitary scalar values
|
||||
/// defined within the expression. In `<perspective-viewer>`,
|
||||
/// expressions are added using the "New Column" button in the side
|
||||
/// panel.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub expressions: Option<Expressions>,
|
||||
|
||||
/// Aggregates perform a calculation over an entire column, and are
|
||||
/// displayed when one or more [Group By](#group-by) are applied to the
|
||||
/// `View`. Aggregates can be specified by the user, or Perspective will
|
||||
/// use the following sensible default aggregates based on column type:
|
||||
///
|
||||
/// - "sum" for `integer` and `float` columns
|
||||
/// - "count" for all other columns
|
||||
///
|
||||
/// Perspective provides a selection of aggregate functions that can be
|
||||
/// applied to columns in the `View` constructor using a dictionary of
|
||||
/// column name to aggregate function name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub aggregates: Option<HashMap<String, Aggregate>>,
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub group_by_depth: Option<u32>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub filter_op: Option<FilterReducer>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub group_rollup_mode: Option<GroupRollupMode>,
|
||||
}
|
||||
|
||||
impl From<ViewConfigUpdate> for proto::ViewConfig {
|
||||
fn from(value: ViewConfigUpdate) -> Self {
|
||||
proto::ViewConfig {
|
||||
group_by: value.group_by.unwrap_or_default(),
|
||||
split_by: value.split_by.unwrap_or_default(),
|
||||
columns: value.columns.map(|x| proto::ColumnsUpdate {
|
||||
opt_columns: Some(columns_update::OptColumns::Columns(
|
||||
proto::columns_update::Columns {
|
||||
columns: x.into_iter().flatten().collect(),
|
||||
},
|
||||
)),
|
||||
}),
|
||||
filter: value
|
||||
.filter
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|x| x.into())
|
||||
.collect(),
|
||||
filter_op: value
|
||||
.filter_op
|
||||
.map(proto::view_config::FilterReducer::from)
|
||||
.unwrap_or_default() as i32,
|
||||
sort: value
|
||||
.sort
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|x| x.into())
|
||||
.collect(),
|
||||
expressions: value.expressions.unwrap_or_default().0,
|
||||
aggregates: value
|
||||
.aggregates
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(x, y)| (x, y.into()))
|
||||
.collect(),
|
||||
group_by_depth: value.group_by_depth,
|
||||
group_rollup_mode: value
|
||||
.group_rollup_mode
|
||||
.map(|x| proto::GroupRollupMode::from(x).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FilterReducer> for proto::view_config::FilterReducer {
|
||||
fn from(value: FilterReducer) -> Self {
|
||||
match value {
|
||||
FilterReducer::And => proto::view_config::FilterReducer::And,
|
||||
FilterReducer::Or => proto::view_config::FilterReducer::Or,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::view_config::FilterReducer> for FilterReducer {
|
||||
fn from(value: proto::view_config::FilterReducer) -> Self {
|
||||
match value {
|
||||
proto::view_config::FilterReducer::And => FilterReducer::And,
|
||||
proto::view_config::FilterReducer::Or => FilterReducer::Or,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ViewConfig> for ViewConfigUpdate {
|
||||
fn from(value: ViewConfig) -> Self {
|
||||
ViewConfigUpdate {
|
||||
group_by: Some(value.group_by),
|
||||
split_by: Some(value.split_by),
|
||||
columns: Some(value.columns),
|
||||
filter: Some(value.filter),
|
||||
filter_op: Some(value.filter_op),
|
||||
sort: Some(value.sort),
|
||||
expressions: Some(value.expressions),
|
||||
aggregates: Some(value.aggregates),
|
||||
group_by_depth: value.group_by_depth,
|
||||
group_rollup_mode: Some(value.group_rollup_mode),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::ViewConfig> for ViewConfig {
|
||||
fn from(value: proto::ViewConfig) -> Self {
|
||||
ViewConfig {
|
||||
group_by: value.group_by,
|
||||
split_by: value.split_by,
|
||||
columns: match value.columns.unwrap_or_default().opt_columns {
|
||||
Some(columns_update::OptColumns::Columns(x)) => {
|
||||
x.columns.into_iter().map(Some).collect()
|
||||
},
|
||||
_ => {
|
||||
vec![]
|
||||
},
|
||||
},
|
||||
filter: value.filter.into_iter().map(|x| x.into()).collect(),
|
||||
filter_op: proto::view_config::FilterReducer::try_from(value.filter_op)
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
sort: value.sort.into_iter().map(|x| x.into()).collect(),
|
||||
expressions: Expressions(value.expressions),
|
||||
aggregates: value
|
||||
.aggregates
|
||||
.into_iter()
|
||||
.map(|(x, y)| (x, y.into()))
|
||||
.collect(),
|
||||
group_by_depth: value.group_by_depth,
|
||||
group_rollup_mode: value
|
||||
.group_rollup_mode
|
||||
.map(proto::GroupRollupMode::try_from)
|
||||
.and_then(|x| x.ok())
|
||||
.map(|x| x.into())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ViewConfigUpdate> for ViewConfig {
|
||||
fn from(value: ViewConfigUpdate) -> Self {
|
||||
ViewConfig {
|
||||
group_by: value.group_by.unwrap_or_default(),
|
||||
split_by: value.split_by.unwrap_or_default(),
|
||||
columns: value.columns.unwrap_or_default(),
|
||||
filter: value.filter.unwrap_or_default(),
|
||||
filter_op: value.filter_op.unwrap_or_default(),
|
||||
sort: value.sort.unwrap_or_default(),
|
||||
expressions: value.expressions.unwrap_or_default(),
|
||||
aggregates: value.aggregates.unwrap_or_default(),
|
||||
group_by_depth: value.group_by_depth,
|
||||
group_rollup_mode: value.group_rollup_mode.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::ViewConfig> for ViewConfigUpdate {
|
||||
fn from(value: proto::ViewConfig) -> Self {
|
||||
ViewConfigUpdate {
|
||||
group_by: Some(value.group_by),
|
||||
split_by: Some(value.split_by),
|
||||
columns: match value.columns.unwrap_or_default().opt_columns {
|
||||
Some(columns_update::OptColumns::Columns(x)) => {
|
||||
Some(x.columns.into_iter().map(Some).collect())
|
||||
},
|
||||
_ => None,
|
||||
},
|
||||
filter: Some(value.filter.into_iter().map(|x| x.into()).collect()),
|
||||
filter_op: Some(
|
||||
proto::view_config::FilterReducer::try_from(value.filter_op)
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
),
|
||||
sort: Some(value.sort.into_iter().map(|x| x.into()).collect()),
|
||||
expressions: Some(Expressions(value.expressions)),
|
||||
aggregates: Some(
|
||||
value
|
||||
.aggregates
|
||||
.into_iter()
|
||||
.map(|(x, y)| (x, y.into()))
|
||||
.collect(),
|
||||
),
|
||||
group_by_depth: value.group_by_depth,
|
||||
group_rollup_mode: value
|
||||
.group_rollup_mode
|
||||
.and_then(|x| proto::GroupRollupMode::try_from(x).ok())
|
||||
.map(|x| x.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewConfig {
|
||||
fn _apply<T>(field: &mut T, update: Option<T>) -> bool {
|
||||
match update {
|
||||
None => false,
|
||||
Some(update) => {
|
||||
*field = update;
|
||||
true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self, reset_expressions: bool) {
|
||||
let mut config = Self::default();
|
||||
if !reset_expressions {
|
||||
config.expressions = self.expressions.clone();
|
||||
}
|
||||
std::mem::swap(self, &mut config);
|
||||
}
|
||||
|
||||
/// Apply `ViewConfigUpdate` to a `ViewConfig`, ignoring any fields in
|
||||
/// `update` which were unset.
|
||||
pub fn apply_update(&mut self, mut update: ViewConfigUpdate) -> bool {
|
||||
let mut changed = false;
|
||||
if ((self.group_rollup_mode == GroupRollupMode::Total
|
||||
&& update.group_rollup_mode.is_none())
|
||||
|| update.group_rollup_mode == Some(GroupRollupMode::Total))
|
||||
&& update
|
||||
.group_by
|
||||
.as_ref()
|
||||
.map(|x| !x.is_empty())
|
||||
.unwrap_or_default()
|
||||
{
|
||||
tracing::info!("`total` incompatible with `group_by`");
|
||||
changed = true;
|
||||
update.group_rollup_mode = Some(GroupRollupMode::Rollup);
|
||||
}
|
||||
|
||||
if update.group_rollup_mode == Some(GroupRollupMode::Total) && !self.group_by.is_empty() {
|
||||
tracing::info!("`group_by` incompatible with `total`");
|
||||
changed = true;
|
||||
update.group_by = Some(vec![]);
|
||||
}
|
||||
|
||||
changed = Self::_apply(&mut self.group_by, update.group_by) || changed;
|
||||
changed = Self::_apply(&mut self.split_by, update.split_by) || changed;
|
||||
changed = Self::_apply(&mut self.columns, update.columns) || changed;
|
||||
changed = Self::_apply(&mut self.filter, update.filter) || changed;
|
||||
changed = Self::_apply(&mut self.sort, update.sort) || changed;
|
||||
changed = Self::_apply(&mut self.aggregates, update.aggregates) || changed;
|
||||
changed = Self::_apply(&mut self.expressions, update.expressions) || changed;
|
||||
changed = Self::_apply(&mut self.group_rollup_mode, update.group_rollup_mode) || changed;
|
||||
if self.group_rollup_mode == GroupRollupMode::Total && !self.group_by.is_empty() {
|
||||
tracing::info!("`total` incompatible with `group_by`");
|
||||
changed = true;
|
||||
self.group_by = vec![];
|
||||
}
|
||||
|
||||
changed
|
||||
}
|
||||
|
||||
pub fn is_aggregated(&self) -> bool {
|
||||
!self.group_by.is_empty() || self.group_rollup_mode == GroupRollupMode::Total
|
||||
}
|
||||
|
||||
pub fn is_column_expression_in_use(&self, name: &str) -> bool {
|
||||
let name = name.to_owned();
|
||||
self.group_by.contains(&name)
|
||||
|| self.split_by.contains(&name)
|
||||
|| self.sort.iter().any(|x| x.0 == name)
|
||||
|| self.filter.iter().any(|x| x.column() == name)
|
||||
|| self.columns.contains(&Some(name))
|
||||
}
|
||||
|
||||
/// `ViewConfig` carries additional metadata in the form of `None` columns
|
||||
/// which are filtered befor ebeing passed to the engine, but whose position
|
||||
/// is a placeholder for Viewer functionality. `is_equivalent` tests
|
||||
/// equivalency from the perspective of the engine.
|
||||
pub fn is_equivalent(&self, other: &Self) -> bool {
|
||||
let _self = self.clone();
|
||||
let _self = ViewConfig {
|
||||
columns: _self.columns.into_iter().filter(|x| x.is_some()).collect(),
|
||||
.._self
|
||||
};
|
||||
|
||||
let _other = other.clone();
|
||||
let _other = ViewConfig {
|
||||
columns: _other.columns.into_iter().filter(|x| x.is_some()).collect(),
|
||||
..other.clone()
|
||||
};
|
||||
|
||||
_self == _other
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
//! `perspective_client` is the client implementation of
|
||||
//! [Perspective](https://perspective-dev.github.io), designed to be used from Rust
|
||||
//! directly, and as a core to `perspective-js` and `perspective-python` crates
|
||||
//! which wrap language-specific bindings for this module.
|
||||
//!
|
||||
//! # See also
|
||||
//!
|
||||
//! - [`perspective-rs`](https://docs.rs/perspective/latest/) for the Rust
|
||||
//! Client and Server APIs.
|
||||
//! - [`perspective-js`](https://docs.rs/perspective-js/latest/) for the
|
||||
//! JavaScript API.
|
||||
//! - [`perspective-python`](https://docs.rs/perspective-python/latest/) for the
|
||||
//! Python API.
|
||||
//! - [`perspective-server`](https://docs.rs/perspective-server/latest/) for
|
||||
//! Data Binding details.
|
||||
//! - [`perspective-viewer`](https://docs.rs/perspective-viewer/latest/) for the
|
||||
//! WebAssembly `<perspective-viewer>` Custom Element API.
|
||||
|
||||
#![warn(
|
||||
clippy::all,
|
||||
clippy::panic_in_result_fn,
|
||||
clippy::await_holding_refcell_ref
|
||||
)]
|
||||
|
||||
mod client;
|
||||
mod session;
|
||||
mod table;
|
||||
mod table_data;
|
||||
mod table_ref;
|
||||
mod view;
|
||||
pub mod virtual_server;
|
||||
|
||||
pub mod config;
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[allow(clippy::all)]
|
||||
pub mod proto;
|
||||
|
||||
pub mod utils;
|
||||
|
||||
pub use crate::client::{Client, ClientHandler, Features, ReconnectCallback, SystemInfo};
|
||||
use crate::proto::HostedTable;
|
||||
pub use crate::proto::JoinType;
|
||||
pub use crate::session::{ProxySession, Session};
|
||||
pub use crate::table::{
|
||||
DeleteOptions, ExprValidationResult, JoinOptions, Table, TableInitOptions, TableReadFormat,
|
||||
UpdateOptions,
|
||||
};
|
||||
pub use crate::table_data::{TableData, UpdateData};
|
||||
pub use crate::table_ref::TableRef;
|
||||
pub use crate::view::{
|
||||
ColumnWindow, OnUpdateData, OnUpdateMode, OnUpdateOptions, View, ViewWindow,
|
||||
};
|
||||
|
||||
pub type ClientError = utils::ClientError;
|
||||
pub type ExprValidationError = crate::proto::table_validate_expr_resp::ExprValidationError;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod vendor {
|
||||
pub use paste;
|
||||
}
|
||||
|
||||
impl From<&str> for HostedTable {
|
||||
fn from(entity_id: &str) -> Self {
|
||||
HostedTable {
|
||||
entity_id: entity_id.to_string(),
|
||||
index: None,
|
||||
limit: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert that an implementation of domain language wrapper for [`Table`]
|
||||
/// implements the expected API. As domain languages have different API needs,
|
||||
/// a trait isn't useful for asserting that the entire API is implemented,
|
||||
/// because the signatures will not match exactly (unless every method is
|
||||
/// made heavily generic). Instead, this macro complains when a method name
|
||||
/// is missing.
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! assert_table_api {
|
||||
($x:ty) => {
|
||||
$crate::vendor::paste::paste! {
|
||||
#[cfg(debug_assertions)]
|
||||
fn [< _assert_table_api_ $x:lower >]() {
|
||||
let _ = (
|
||||
&$x::clear,
|
||||
&$x::columns,
|
||||
&$x::delete,
|
||||
&$x::get_index,
|
||||
&$x::get_limit,
|
||||
&$x::get_client,
|
||||
&$x::make_port,
|
||||
&$x::on_delete,
|
||||
&$x::remove_delete,
|
||||
&$x::replace,
|
||||
&$x::schema,
|
||||
&$x::size,
|
||||
&$x::update,
|
||||
&$x::validate_expressions,
|
||||
&$x::view,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Similar to [`assert_table_api`], but for [`View`]. See [`assert_table_api`].
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! assert_view_api {
|
||||
($x:ty) => {
|
||||
$crate::vendor::paste::paste! {
|
||||
#[cfg(debug_assertions)]
|
||||
fn [< _assert_table_api_ $x:lower >]() {
|
||||
let _ = (
|
||||
&$x::column_paths,
|
||||
&$x::delete,
|
||||
&$x::dimensions,
|
||||
&$x::expression_schema,
|
||||
&$x::get_config,
|
||||
&$x::get_min_max,
|
||||
&$x::num_rows,
|
||||
// &$x::on_update,
|
||||
&$x::remove_update,
|
||||
&$x::on_delete,
|
||||
&$x::remove_delete,
|
||||
&$x::schema,
|
||||
&$x::to_arrow,
|
||||
&$x::to_columns_string,
|
||||
&$x::to_json_string,
|
||||
&$x::to_csv,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use std::error::Error as StdError;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::Future;
|
||||
use prost::Message;
|
||||
|
||||
use crate::proto::request::ClientReq;
|
||||
use crate::proto::{Request, Response};
|
||||
use crate::{Client, ClientError, asyncfn};
|
||||
#[cfg(doc)]
|
||||
use crate::{Table, View};
|
||||
|
||||
/// The server-side representation of a connection to a [`Client`].
|
||||
///
|
||||
/// For each [`Client`] that wants to connect to a `perspective_server::Server`,
|
||||
/// a dedicated [`Session`] must be created. The [`Session`] handles routing
|
||||
/// messages emitted by the `perspective_server::Server`ve_server::Server`, as
|
||||
/// well as owning any resources the [`Client`] may request.
|
||||
pub trait Session<E> {
|
||||
/// Handle an incoming request from the [`Client`]. Calling
|
||||
/// [`Session::handle_request`] will result in the `send_response` parameter
|
||||
/// which was used to construct this [`Session`] to fire one or more times.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `request` An incoming request message, generated from a
|
||||
/// [`Client::new`]'s `send_request` handler (which may-or-may-not be
|
||||
/// local).
|
||||
fn handle_request(&self, request: &[u8]) -> impl Future<Output = Result<(), E>>;
|
||||
|
||||
/// Close this [`Session`], cleaning up any callbacks (e.g. arguments
|
||||
/// provided to [`Session::handle_request`]) and resources (e.g. views
|
||||
/// returned by a call to [`Table::view`]).
|
||||
///
|
||||
/// Dropping a [`Session`] outside of the context of [`Session::close`]
|
||||
/// will cause a [`tracing`] error-level log to be emitted, but won't fail.
|
||||
/// They will, however, leak.
|
||||
fn close(self) -> impl Future<Output = ()>;
|
||||
}
|
||||
|
||||
type ProxyCallbackError = Box<dyn StdError + Send + Sync>;
|
||||
type ProxyCallback = Arc<dyn Fn(&[u8]) -> Result<(), ProxyCallbackError> + Send + Sync>;
|
||||
|
||||
/// A [`Session`] implementation which tunnels through another [`Client`].
|
||||
/// @private
|
||||
#[derive(Clone)]
|
||||
pub struct ProxySession {
|
||||
parent: Client,
|
||||
callback: ProxyCallback,
|
||||
}
|
||||
|
||||
impl ProxySession {
|
||||
pub fn new(
|
||||
client: Client,
|
||||
send_response: impl Fn(&[u8]) -> Result<(), ProxyCallbackError> + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
ProxySession {
|
||||
parent: client,
|
||||
callback: Arc::new(send_response),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn encode(response: Response, callback: ProxyCallback) -> Result<(), ClientError> {
|
||||
let mut enc = vec![];
|
||||
response.encode(&mut enc)?;
|
||||
callback(&enc).map_err(|x| ClientError::Unknown(x.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Session<ClientError> for ProxySession {
|
||||
async fn handle_request(&self, request: &[u8]) -> Result<(), ClientError> {
|
||||
let req = Request::decode(request)?;
|
||||
let callback = self.callback.clone();
|
||||
match req.client_req.as_ref() {
|
||||
Some(ClientReq::ViewOnUpdateReq(_)) => {
|
||||
let on_update =
|
||||
asyncfn!(callback, async move |response| encode(response, callback));
|
||||
self.parent.subscribe(&req, on_update).await?
|
||||
},
|
||||
Some(_) => {
|
||||
let on_update = move |response| encode(response, callback);
|
||||
self.parent
|
||||
.subscribe_once(&req, Box::new(on_update))
|
||||
.await?
|
||||
},
|
||||
None => {
|
||||
return Err(ClientError::Internal(
|
||||
"ProxySession::handle_request: invalid request".to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn close(self) {}
|
||||
}
|
||||
@@ -0,0 +1,620 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::assert_table_api;
|
||||
use crate::client::{Client, Features};
|
||||
use crate::config::{Expressions, ViewConfigUpdate};
|
||||
use crate::proto::make_table_req::MakeTableOptions;
|
||||
use crate::proto::make_table_req::make_table_options::MakeTableType;
|
||||
use crate::proto::request::ClientReq;
|
||||
use crate::proto::response::ClientResp;
|
||||
use crate::proto::*;
|
||||
use crate::table_data::UpdateData;
|
||||
use crate::utils::*;
|
||||
use crate::view::View;
|
||||
|
||||
pub type Schema = HashMap<String, ColumnType>;
|
||||
|
||||
/// The format to interpret data preovided to [`Client::table`].
|
||||
///
|
||||
/// When serialized, these values are `"csv"`, `"json"`, `"columns"`, `"arrow"`
|
||||
/// and `"ndjson"`.
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, TS)]
|
||||
pub enum TableReadFormat {
|
||||
#[serde(rename = "csv")]
|
||||
Csv,
|
||||
|
||||
#[serde(rename = "json")]
|
||||
JsonString,
|
||||
|
||||
#[serde(rename = "columns")]
|
||||
ColumnsString,
|
||||
|
||||
#[serde(rename = "arrow")]
|
||||
Arrow,
|
||||
|
||||
#[serde(rename = "ndjson")]
|
||||
Ndjson,
|
||||
}
|
||||
|
||||
impl TableReadFormat {
|
||||
pub fn parse(value: Option<String>) -> Result<Option<Self>, String> {
|
||||
Ok(match value.as_deref() {
|
||||
Some("csv") => Some(TableReadFormat::Csv),
|
||||
Some("json") => Some(TableReadFormat::JsonString),
|
||||
Some("columns") => Some(TableReadFormat::ColumnsString),
|
||||
Some("arrow") => Some(TableReadFormat::Arrow),
|
||||
Some("ndjson") => Some(TableReadFormat::Ndjson),
|
||||
None => None,
|
||||
Some(x) => return Err(format!("Unknown format \"{x}\"")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Options which impact the behavior of [`Client::table`], as well as
|
||||
/// subsequent calls to [`Table::update`].
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, TS)]
|
||||
pub struct TableInitOptions {
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub name: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub format: Option<TableReadFormat>,
|
||||
|
||||
/// This [`Table`] should use the column named by the `index` parameter as
|
||||
/// the `index`, which causes [`Table::update`] and [`Client::table`] input
|
||||
/// to either insert or update existing rows based on `index` column
|
||||
/// value equality.
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub index: Option<String>,
|
||||
|
||||
/// This [`Table`] should be limited to `limit` rows, after which the
|
||||
/// _earliest_ rows will be overwritten (where _earliest_ is defined as
|
||||
/// relative to insertion order).
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub limit: Option<u32>,
|
||||
|
||||
/// Back this [`Table`]'s canonical data with the on-disk storage backend
|
||||
/// instead of memory. On native targets this is a memory-mapped file; on
|
||||
/// WASM it is OPFS (Worker only). Defaults to in-memory.
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub page_to_disk: Option<bool>,
|
||||
}
|
||||
|
||||
impl TableInitOptions {
|
||||
pub fn set_name<D: Display>(&mut self, name: D) {
|
||||
self.name = Some(format!("{name}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<TableOptions> for MakeTableOptions {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: TableOptions) -> Result<Self, Self::Error> {
|
||||
let page_to_disk = value.page_to_disk;
|
||||
Ok(MakeTableOptions {
|
||||
page_to_disk,
|
||||
make_table_type: match value {
|
||||
TableOptions {
|
||||
index: Some(_),
|
||||
limit: Some(_),
|
||||
..
|
||||
} => Err(ClientError::BadTableOptions)?,
|
||||
TableOptions {
|
||||
index: Some(index), ..
|
||||
} => Some(MakeTableType::MakeIndexTable(index)),
|
||||
TableOptions {
|
||||
limit: Some(limit), ..
|
||||
} => Some(MakeTableType::MakeLimitTable(limit)),
|
||||
_ => None,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct TableOptions {
|
||||
pub index: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
pub page_to_disk: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<TableInitOptions> for TableOptions {
|
||||
fn from(value: TableInitOptions) -> Self {
|
||||
TableOptions {
|
||||
index: value.index,
|
||||
limit: value.limit,
|
||||
page_to_disk: value.page_to_disk,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Options for [`Client::join`].
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, TS)]
|
||||
pub struct JoinOptions {
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub join_type: Option<crate::proto::JoinType>,
|
||||
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub name: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub right_on: Option<String>,
|
||||
}
|
||||
|
||||
/// Options for [`Table::delete`].
|
||||
#[derive(Clone, Debug, Default, Deserialize, TS)]
|
||||
pub struct DeleteOptions {
|
||||
pub lazy: bool,
|
||||
}
|
||||
|
||||
/// Options for [`Table::update`].
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
|
||||
pub struct UpdateOptions {
|
||||
pub port_id: Option<u32>,
|
||||
pub format: Option<TableReadFormat>,
|
||||
}
|
||||
|
||||
/// Result of a call to [`Table::validate_expressions`], containing a schema
|
||||
/// for valid expressions and error messages for invalid ones.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ExprValidationResult {
|
||||
pub expression_schema: Schema,
|
||||
pub errors: HashMap<String, table_validate_expr_resp::ExprValidationError>,
|
||||
pub expression_alias: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// [`Table`] is Perspective's columnar data frame, analogous to a Pandas/Polars
|
||||
/// `DataFrame` or Apache Arrow, supporting append & in-place updates, removal
|
||||
/// by index, and update notifications.
|
||||
///
|
||||
/// A [`Table`] contains columns, each of which have a unique name, are strongly
|
||||
/// and consistently typed, and contains rows of data conforming to the column's
|
||||
/// type. Each column in a [`Table`] must have the same number of rows, though
|
||||
/// not every row must contain data; null-values are used to indicate missing
|
||||
/// values in the dataset. The schema of a [`Table`] is _immutable after
|
||||
/// creation_, which means the column names and data types cannot be changed
|
||||
/// after the [`Table`] has been created. Columns cannot be added or deleted
|
||||
/// after creation either, but a [`View`] can be used to select an arbitrary set
|
||||
/// of columns from the [`Table`].
|
||||
#[derive(Clone)]
|
||||
pub struct Table {
|
||||
name: String,
|
||||
client: Client,
|
||||
options: TableOptions,
|
||||
|
||||
/// If this table is constructed from a View, the view's on_update callback
|
||||
/// is wired into this table. So, we store the token to clean it up properly
|
||||
/// on destruction.
|
||||
pub(crate) view_update_token: Option<u32>,
|
||||
}
|
||||
|
||||
assert_table_api!(Table);
|
||||
|
||||
impl PartialEq for Table {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name && self.client == other.client
|
||||
}
|
||||
}
|
||||
|
||||
impl Table {
|
||||
pub(crate) fn new(name: String, client: Client, options: TableOptions) -> Self {
|
||||
Table {
|
||||
name,
|
||||
client,
|
||||
options,
|
||||
view_update_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn client_message(&self, req: ClientReq) -> Request {
|
||||
Request {
|
||||
msg_id: self.client.gen_id(),
|
||||
entity_id: self.name.clone(),
|
||||
client_req: Some(req),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a copy of the [`Client`] this [`Table`] came from.
|
||||
pub fn get_client(&self) -> Client {
|
||||
self.client.clone()
|
||||
}
|
||||
|
||||
/// Get a metadata dictionary of the `perspective_server::Server`'s
|
||||
/// features, which is (currently) implementation specific, but there is
|
||||
/// only one implementation.
|
||||
pub async fn get_features(&self) -> ClientResult<Features> {
|
||||
self.client.get_features().await
|
||||
}
|
||||
|
||||
/// Returns the name of the index column for the table.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use perspective_client::{Client, TableData, TableInitOptions, UpdateData};
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let client: Client = todo!();
|
||||
/// let options = TableInitOptions {
|
||||
/// index: Some("x".to_string()),
|
||||
/// ..TableInitOptions::default()
|
||||
/// };
|
||||
/// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
|
||||
/// let table = client.table(data, options).await?;
|
||||
/// let index = table.get_index();
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub fn get_index(&self) -> Option<String> {
|
||||
self.options.index.as_ref().map(|index| index.to_owned())
|
||||
}
|
||||
|
||||
/// Returns the user-specified row limit for this table.
|
||||
pub fn get_limit(&self) -> Option<u32> {
|
||||
self.options.limit.as_ref().map(|limit| *limit)
|
||||
}
|
||||
|
||||
/// Returns the user-specified name for this table, or the auto-generated
|
||||
/// name if a name was not specified when the table was created.
|
||||
pub fn get_name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
/// Removes all the rows in the [`Table`], but preserves everything else
|
||||
/// including the schema, index, and any callbacks or registered
|
||||
/// [`View`] instances.
|
||||
///
|
||||
/// Calling [`Table::clear`], like [`Table::update`] and [`Table::remove`],
|
||||
/// will trigger an update event to any registered listeners via
|
||||
/// [`View::on_update`].
|
||||
pub async fn clear(&self) -> ClientResult<()> {
|
||||
self.replace(UpdateData::JsonRows("[]".to_owned())).await
|
||||
}
|
||||
|
||||
/// Delete this [`Table`] and cleans up associated resources.
|
||||
///
|
||||
/// [`Table`]s do not stop consuming resources or processing updates when
|
||||
/// they are garbage collected in their host language - you must call
|
||||
/// this method to reclaim these.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `options` An options dictionary.
|
||||
/// - `lazy` Whether to delete this [`Table`] _lazily_. When false (the
|
||||
/// default), the delete will occur immediately, assuming it has no
|
||||
/// [`View`] instances registered to it (which must be deleted first,
|
||||
/// otherwise this method will throw an error). When true, the
|
||||
/// [`Table`] will only be marked for deltion once its [`View`]
|
||||
/// dependency count reaches 0.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use perspective_client::{Client, DeleteOptions, TableData, TableInitOptions, UpdateData};
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let client: Client = todo!();
|
||||
/// let opts = TableInitOptions::default();
|
||||
/// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
|
||||
/// let table = client.table(data, opts).await?;
|
||||
///
|
||||
/// // ...
|
||||
///
|
||||
/// table.delete(DeleteOptions::default()).await?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub async fn delete(&self, options: DeleteOptions) -> ClientResult<()> {
|
||||
let msg = self.client_message(ClientReq::TableDeleteReq(TableDeleteReq {
|
||||
is_immediate: !options.lazy,
|
||||
}));
|
||||
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::TableDeleteResp(_) => Ok(()),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the column names of this [`Table`] in "natural" order (the
|
||||
/// ordering implied by the input format).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use perspective_client::Table;
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let table: Table = todo!();
|
||||
/// let columns = table.columns().await?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub async fn columns(&self) -> ClientResult<Vec<String>> {
|
||||
let msg = self.client_message(ClientReq::TableSchemaReq(TableSchemaReq {}));
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::TableSchemaResp(TableSchemaResp { schema }) => Ok(schema
|
||||
.map(|x| x.schema.into_iter().map(|x| x.name.to_owned()).collect())
|
||||
.unwrap()),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of rows in a [`Table`].
|
||||
pub async fn size(&self) -> ClientResult<usize> {
|
||||
let msg = self.client_message(ClientReq::TableSizeReq(TableSizeReq {}));
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::TableSizeResp(TableSizeResp { size }) => Ok(size as usize),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a table's [`Schema`], a mapping of column names to column types.
|
||||
///
|
||||
/// The mapping of a [`Table`]'s column names to data types is referred to
|
||||
/// as a [`Schema`]. Each column has a unique name and a data type, one
|
||||
/// of:
|
||||
///
|
||||
/// - `"boolean"` - A boolean type
|
||||
/// - `"date"` - A timesonze-agnostic date type (month/day/year)
|
||||
/// - `"datetime"` - A millisecond-precision datetime type in the UTC
|
||||
/// timezone
|
||||
/// - `"float"` - A 64 bit float
|
||||
/// - `"integer"` - A signed 32 bit integer (the integer type supported by
|
||||
/// JavaScript)
|
||||
/// - `"string"` - A [`String`] data type (encoded internally as a
|
||||
/// _dictionary_)
|
||||
///
|
||||
/// Note that all [`Table`] columns are _nullable_, regardless of the data
|
||||
/// type.
|
||||
pub async fn schema(&self) -> ClientResult<Schema> {
|
||||
let msg = self.client_message(ClientReq::TableSchemaReq(TableSchemaReq {}));
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::TableSchemaResp(TableSchemaResp { schema }) => Ok(schema
|
||||
.map(|x| {
|
||||
x.schema
|
||||
.into_iter()
|
||||
.map(|x| (x.name, ColumnType::try_from(x.r#type).unwrap()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap()),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a unique channel ID on this [`Table`], which allows
|
||||
/// `View::on_update` callback calls to be associated with the
|
||||
/// `Table::update` which caused them.
|
||||
pub async fn make_port(&self) -> ClientResult<i32> {
|
||||
let msg = self.client_message(ClientReq::TableMakePortReq(TableMakePortReq {}));
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::TableMakePortResp(TableMakePortResp { port_id }) => Ok(port_id as i32),
|
||||
_ => Err(ClientError::Unknown("make_port".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a callback which is called exactly once, when this [`Table`] is
|
||||
/// deleted with the [`Table::delete`] method.
|
||||
///
|
||||
/// [`Table::on_delete`] resolves when the subscription message is sent, not
|
||||
/// when the _delete_ event occurs.
|
||||
pub async fn on_delete(
|
||||
&self,
|
||||
on_delete: Box<dyn Fn() + Send + Sync + 'static>,
|
||||
) -> ClientResult<u32> {
|
||||
let callback = move |resp: Response| match resp.client_resp {
|
||||
Some(ClientResp::TableOnDeleteResp(_)) => {
|
||||
on_delete();
|
||||
Ok(())
|
||||
},
|
||||
resp => Err(resp.into()),
|
||||
};
|
||||
|
||||
let msg = self.client_message(ClientReq::TableOnDeleteReq(TableOnDeleteReq {}));
|
||||
self.client.subscribe_once(&msg, Box::new(callback)).await?;
|
||||
Ok(msg.msg_id)
|
||||
}
|
||||
|
||||
/// Removes a listener with a given ID, as returned by a previous call to
|
||||
/// [`Table::on_delete`].
|
||||
pub async fn remove_delete(&self, callback_id: u32) -> ClientResult<()> {
|
||||
let msg = self.client_message(ClientReq::TableRemoveDeleteReq(TableRemoveDeleteReq {
|
||||
id: callback_id,
|
||||
}));
|
||||
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::TableRemoveDeleteResp(_) => Ok(()),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes rows from this [`Table`] with the `index` column values
|
||||
/// supplied.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `indices` - A list of `index` column values for rows that should be
|
||||
/// removed.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use perspective_client::{Table, UpdateData};
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let table: Table = todo!();
|
||||
/// table
|
||||
/// .remove(UpdateData::Csv("index\n1\n2\n3".into()))
|
||||
/// .await?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub async fn remove(&self, input: UpdateData) -> ClientResult<()> {
|
||||
let msg = self.client_message(ClientReq::TableRemoveReq(TableRemoveReq {
|
||||
data: Some(input.into()),
|
||||
}));
|
||||
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::TableRemoveResp(_) => Ok(()),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace all rows in this [`Table`] with the input data, coerced to this
|
||||
/// [`Table`]'s existing [`Schema`], notifying any derived [`View`] and
|
||||
/// [`View::on_update`] callbacks.
|
||||
///
|
||||
/// Calling [`Table::replace`] is an easy way to replace _all_ the data in a
|
||||
/// [`Table`] without losing any derived [`View`] instances or
|
||||
/// [`View::on_update`] callbacks. [`Table::replace`] does _not_ infer
|
||||
/// data types like [`Client::table`] does, rather it _coerces_ input
|
||||
/// data to the `Schema` like [`Table::update`]. If you need a [`Table`]
|
||||
/// with a different `Schema`, you must create a new one.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use perspective_client::{Table, UpdateData};
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let table: Table = todo!();
|
||||
/// let data = UpdateData::Csv("x,y\n1,2".into());
|
||||
/// table.replace(data).await?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub async fn replace(&self, input: UpdateData) -> ClientResult<()> {
|
||||
let msg = self.client_message(ClientReq::TableReplaceReq(TableReplaceReq {
|
||||
data: Some(input.into()),
|
||||
}));
|
||||
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::TableReplaceResp(_) => Ok(()),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the rows of this table and any derived [`View`] instances.
|
||||
///
|
||||
/// Calling [`Table::update`] will trigger the [`View::on_update`] callbacks
|
||||
/// register to derived [`View`], and the call itself will not resolve until
|
||||
/// _all_ derived [`View`]'s are notified.
|
||||
///
|
||||
/// When updating a [`Table`] with an `index`, [`Table::update`] supports
|
||||
/// partial updates, by omitting columns from the update data.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `input` - The input data for this [`Table`]. The schema of a [`Table`]
|
||||
/// is immutable after creation, so this method cannot be called with a
|
||||
/// schema.
|
||||
/// - `options` - Options for this update step - see [`UpdateOptions`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use perspective_client::{Table, UpdateData, UpdateOptions};
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let table: Table = todo!();
|
||||
/// let data = UpdateData::Csv("x,y\n1,2".into());
|
||||
/// let opts = UpdateOptions::default();
|
||||
/// table.update(data, opts).await?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub async fn update(&self, input: UpdateData, options: UpdateOptions) -> ClientResult<()> {
|
||||
let msg = self.client_message(ClientReq::TableUpdateReq(TableUpdateReq {
|
||||
data: Some(input.into()),
|
||||
port_id: options.port_id.unwrap_or(0),
|
||||
}));
|
||||
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::TableUpdateResp(_) => Ok(()),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates the given expressions.
|
||||
pub async fn validate_expressions(
|
||||
&self,
|
||||
expressions: Expressions,
|
||||
) -> ClientResult<ExprValidationResult> {
|
||||
let msg = self.client_message(ClientReq::TableValidateExprReq(TableValidateExprReq {
|
||||
column_to_expr: expressions.0,
|
||||
}));
|
||||
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::TableValidateExprResp(result) => Ok(ExprValidationResult {
|
||||
errors: result.errors,
|
||||
expression_alias: result.expression_alias,
|
||||
expression_schema: result
|
||||
.expression_schema
|
||||
.into_iter()
|
||||
.map(|(x, y)| (x, ColumnType::try_from(y).unwrap()))
|
||||
.collect(),
|
||||
}),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`View`] from this table with a specified
|
||||
/// [`ViewConfigUpdate`].
|
||||
///
|
||||
/// See [`View`] struct.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::collections::HashMap;
|
||||
/// # use perspective_client::Table;
|
||||
/// # use perspective_client::config::*;
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let table: Table = todo!();
|
||||
/// let view = table
|
||||
/// .view(Some(ViewConfigUpdate {
|
||||
/// columns: Some(vec![Some("Sales".into())]),
|
||||
/// aggregates: Some(HashMap::from_iter(vec![("Sales".into(), "sum".into())])),
|
||||
/// group_by: Some(vec!["Region".into(), "Country".into()]),
|
||||
/// filter: Some(vec![Filter::new("Category", "in", &[
|
||||
/// "Furniture",
|
||||
/// "Technology",
|
||||
/// ])]),
|
||||
/// ..ViewConfigUpdate::default()
|
||||
/// }))
|
||||
/// .await?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub async fn view(&self, config: Option<ViewConfigUpdate>) -> ClientResult<View> {
|
||||
let view_name = randid();
|
||||
let msg = Request {
|
||||
msg_id: self.client.gen_id(),
|
||||
entity_id: self.name.clone(),
|
||||
client_req: ClientReq::TableMakeViewReq(TableMakeViewReq {
|
||||
view_id: view_name.clone(),
|
||||
config: config.map(|x| x.into()),
|
||||
})
|
||||
.into(),
|
||||
};
|
||||
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::TableMakeViewResp(TableMakeViewResp { view_id })
|
||||
if view_id == view_name =>
|
||||
{
|
||||
Ok(View::new(view_name, self.client.clone()))
|
||||
},
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use prost::bytes::Bytes;
|
||||
|
||||
use crate::proto;
|
||||
use crate::proto::*;
|
||||
use crate::view::View;
|
||||
#[cfg(doc)]
|
||||
use crate::{Client, Table};
|
||||
|
||||
/// The possible formats of input data which [`Client::table`] may take as an
|
||||
/// argument.
|
||||
#[derive(Debug)]
|
||||
pub enum TableData {
|
||||
Schema(Vec<(String, ColumnType)>),
|
||||
Update(UpdateData),
|
||||
View(View),
|
||||
}
|
||||
|
||||
/// The possible formats of input data which [`Table::update`] may take as an
|
||||
/// argument.
|
||||
#[derive(Debug)]
|
||||
pub enum UpdateData {
|
||||
Csv(String),
|
||||
Arrow(Bytes),
|
||||
JsonRows(String),
|
||||
JsonColumns(String),
|
||||
Ndjson(String),
|
||||
}
|
||||
|
||||
impl From<UpdateData> for TableData {
|
||||
fn from(value: UpdateData) -> Self {
|
||||
TableData::Update(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TableData> for proto::MakeTableData {
|
||||
fn from(value: TableData) -> Self {
|
||||
let data = match value {
|
||||
TableData::Update(x) => return x.into(),
|
||||
TableData::View(view) => make_table_data::Data::FromView(view.name),
|
||||
TableData::Schema(x) => make_table_data::Data::FromSchema(proto::Schema {
|
||||
schema: x
|
||||
.into_iter()
|
||||
.map(|(name, r#type)| schema::KeyTypePair {
|
||||
name,
|
||||
r#type: r#type as i32,
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
};
|
||||
|
||||
MakeTableData { data: Some(data) }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UpdateData> for proto::MakeTableData {
|
||||
fn from(value: UpdateData) -> Self {
|
||||
let data = match value {
|
||||
UpdateData::Csv(x) => make_table_data::Data::FromCsv(x),
|
||||
UpdateData::Arrow(x) => make_table_data::Data::FromArrow(x.into()),
|
||||
UpdateData::JsonRows(x) => make_table_data::Data::FromRows(x),
|
||||
UpdateData::JsonColumns(x) => make_table_data::Data::FromCols(x),
|
||||
UpdateData::Ndjson(x) => make_table_data::Data::FromNdjson(x),
|
||||
};
|
||||
|
||||
MakeTableData { data: Some(data) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use crate::Table;
|
||||
|
||||
/// A reference to a table, either by handle or by name.
|
||||
#[derive(Clone)]
|
||||
pub enum TableRef {
|
||||
Table(Table),
|
||||
Name(String),
|
||||
}
|
||||
|
||||
impl TableRef {
|
||||
pub fn table_name(&self) -> &str {
|
||||
match self {
|
||||
TableRef::Table(table) => table.get_name(),
|
||||
TableRef::Name(name) => name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Table> for TableRef {
|
||||
fn from(table: &Table) -> Self {
|
||||
TableRef::Table(table.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Table> for TableRef {
|
||||
fn from(table: Table) -> Self {
|
||||
TableRef::Table(table)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for TableRef {
|
||||
fn from(name: String) -> Self {
|
||||
TableRef::Name(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for TableRef {
|
||||
fn from(name: &str) -> Self {
|
||||
TableRef::Name(name.to_owned())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
/// A helper to for the pattern `let x2 = x;` necessary to clone structs
|
||||
/// destined for an `async` or `'static` closure stack. This is like `move || {
|
||||
/// .. }` or `move async { .. }`, but for clone semantics. `clone!()` works
|
||||
/// with symbols as well as properties and methods, using the last symbol name
|
||||
/// in the method chain, or an alias via `x = ...` syntax.
|
||||
/// ```
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! clone {
|
||||
(impl @bind $i:tt { $($orig:tt)* } { }) => {
|
||||
let $i = $($orig)*.clone();
|
||||
};
|
||||
|
||||
(impl @bind $i:tt { $($orig:tt)* } { @mut }) => {
|
||||
let mut $i = $($orig)*.clone();
|
||||
};
|
||||
|
||||
(impl @bind $i:tt { $($orig:tt)* } { $binder:tt }) => {
|
||||
let $binder = $($orig)*.clone();
|
||||
};
|
||||
|
||||
(impl @bind $i:tt { $($orig:tt)* } { @mut $binder:tt }) => {
|
||||
let mut $binder = $($orig)*.clone();
|
||||
};
|
||||
|
||||
(impl @expand { $($orig:tt)* } { $($binder:tt)* } $i:tt) => {
|
||||
$crate::clone!(impl @bind $i { $($orig)* $i } { $($binder)* });
|
||||
};
|
||||
|
||||
|
||||
(impl @expand { $($orig:tt)* } { $($binder:tt)* } mut $i:tt) => {
|
||||
$crate::clone!(impl @bind $i { $($orig)* $i } { @mut $($binder)* });
|
||||
};
|
||||
|
||||
(impl @expand { $($orig:tt)* } { $($binder:tt)* } $i:tt ()) => {
|
||||
$crate::clone!(impl @bind $i { $($orig)* $i () } { $($binder)* });
|
||||
};
|
||||
|
||||
(impl @expand { $($orig:tt)* } { $($binder:tt)* } $i:tt . 0) => {
|
||||
$crate::clone!(impl @bind $i { $($orig)* $i . 0 } { $($binder)* });
|
||||
};
|
||||
|
||||
(impl @expand { $($orig:tt)* } { $($binder:tt)* } $i:tt . 1) => {
|
||||
$crate::clone!(impl @bind $i { $($orig)* $i . 1 } { $($binder)* });
|
||||
};
|
||||
|
||||
(impl @expand { $($orig:tt)* } { $($binder:tt)* } $i:tt . 2) => {
|
||||
$crate::clone!(impl @bind $i { $($orig)* $i . 2 } { $($binder)* });
|
||||
};
|
||||
|
||||
(impl @expand { $($orig:tt)* } { $($binder:tt)* } $i:tt . 3) => {
|
||||
$crate::clone!(impl @bind $i { $($orig)* $i . 3 } { $($binder)* });
|
||||
};
|
||||
|
||||
(impl @expand { $($orig:tt)* } { $($binder:tt)* } mut $i:tt = $($tail:tt)+) => {
|
||||
$crate::clone!(impl @expand { $($orig)* } { @mut $i } $($tail)+);
|
||||
};
|
||||
|
||||
(impl @expand { $($orig:tt)* } { $($binder:tt)* } $i:tt = $($tail:tt)+) => {
|
||||
$crate::clone!(impl @expand { $($orig)* } { $i } $($tail)+);
|
||||
};
|
||||
|
||||
(impl @expand { $($orig:tt)* } { $($binder:tt)* } $i:tt $($tail:tt)+) => {
|
||||
$crate::clone!(impl @expand { $($orig)* $i } { $($binder)* } $($tail)+)
|
||||
};
|
||||
|
||||
(impl @context { $($orig:tt)* } $tail:tt) => {
|
||||
$crate::clone!(impl @expand { } { } $($orig)* $tail)
|
||||
};
|
||||
|
||||
(impl @context { $($orig:tt)* } , $($tail:tt)+) => {
|
||||
$crate::clone!(impl @expand { } { } $($orig)*);
|
||||
$crate::clone!(impl @context { } $($tail)+);
|
||||
};
|
||||
|
||||
(impl @context { $($orig:tt)* } $i:tt $($tail:tt)+) => {
|
||||
$crate::clone!(impl @context { $($orig)* $i } $($tail)+)
|
||||
};
|
||||
|
||||
($($tail:tt)+) => {
|
||||
$crate::clone!(impl @context { } $($tail)+);
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! asyncfn {
|
||||
// Munch function args
|
||||
(impl @bindings { $($args:tt)* } { $($clones:tt)* } !{ } | $($tail:tt)+) => {{
|
||||
move |$($args)*| {
|
||||
$($clones)*
|
||||
async move {
|
||||
$($tail)+
|
||||
}
|
||||
}
|
||||
}};
|
||||
|
||||
(impl @bindings { $($args:tt)* } { $($clones:tt)* } !{ } $i:tt $($tail:tt)+) => {
|
||||
$crate::asyncfn!(impl @bindings { $($args)* $i } { $($clones)* } !{ } $($tail)+)
|
||||
};
|
||||
|
||||
// Munch function start
|
||||
(impl @context { $($clones:tt)* } !{ } async move || $($tail:tt)+) => {{
|
||||
move || {
|
||||
$($clones)*
|
||||
async move {
|
||||
$($tail)+
|
||||
}
|
||||
}
|
||||
}};
|
||||
|
||||
(impl @context { $($clones:tt)* } !{ } async move | $($tail:tt)+) => {
|
||||
$crate::asyncfn!(impl @bindings { } { $($clones)* } !{ } $($tail)+)
|
||||
};
|
||||
|
||||
// Munch clone bindings
|
||||
(impl @context { $($clones:tt)* } !{ $($stack:tt)* } , $($tail:tt)+) => {{
|
||||
$crate::asyncfn!(impl @context { $crate::clone!($($stack)*); $($clones)* } !{ } $($tail)+)
|
||||
}};
|
||||
|
||||
(impl @context { $($clones:tt)* } !{ $($stack:tt)* } $i:tt $($tail:tt)+) => {
|
||||
$crate::asyncfn!(impl @context { $($clones)* } !{ $($stack)* $i } $($tail)+)
|
||||
};
|
||||
|
||||
// Root
|
||||
($($tail:tt)+) => {
|
||||
$crate::asyncfn!(impl @context { } !{ } $($tail)+)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use prost::Message;
|
||||
|
||||
use crate::proto::make_table_data::Data;
|
||||
use crate::proto::request::ClientReq;
|
||||
use crate::proto::response::ClientResp;
|
||||
use crate::proto::{
|
||||
MakeTableData, MakeTableReq, Request, Response, TableUpdateReq, ViewOnUpdateResp,
|
||||
ViewToArrowResp, ViewToColumnsStringResp, ViewToCsvResp, ViewToNdjsonStringResp,
|
||||
ViewToRowsStringResp,
|
||||
};
|
||||
|
||||
fn replace(x: Data) -> Data {
|
||||
match x {
|
||||
Data::FromArrow(_) => Data::FromArrow("<< redacted >>".to_string().encode_to_vec()),
|
||||
Data::FromRows(_) => Data::FromRows("<< redacted >>".to_string()),
|
||||
Data::FromCols(_) => Data::FromCols("<< redacted >>".to_string()),
|
||||
Data::FromCsv(_) => Data::FromCsv("<< redacted >>".to_string()),
|
||||
x => x,
|
||||
}
|
||||
}
|
||||
|
||||
/// `prost` generates `Debug` implementations that includes the `data` field,
|
||||
/// which makes logs output unreadable. This `Display` implementation hides
|
||||
/// fields that we don't want ot display in the logs.
|
||||
impl std::fmt::Display for Request {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut msg = self.clone();
|
||||
msg = match msg {
|
||||
Request {
|
||||
client_req:
|
||||
Some(ClientReq::MakeTableReq(MakeTableReq {
|
||||
ref options,
|
||||
data:
|
||||
Some(MakeTableData {
|
||||
data: Some(ref data),
|
||||
}),
|
||||
})),
|
||||
..
|
||||
} => Request {
|
||||
client_req: Some(ClientReq::MakeTableReq(MakeTableReq {
|
||||
options: options.clone(),
|
||||
data: Some(MakeTableData {
|
||||
data: Some(replace(data.clone())),
|
||||
}),
|
||||
})),
|
||||
..msg.clone()
|
||||
},
|
||||
Request {
|
||||
client_req:
|
||||
Some(ClientReq::TableUpdateReq(TableUpdateReq {
|
||||
port_id,
|
||||
data:
|
||||
Some(MakeTableData {
|
||||
data: Some(ref data),
|
||||
}),
|
||||
})),
|
||||
..
|
||||
} => Request {
|
||||
client_req: Some(ClientReq::TableUpdateReq(TableUpdateReq {
|
||||
port_id,
|
||||
data: Some(MakeTableData {
|
||||
data: Some(replace(data.clone())),
|
||||
}),
|
||||
})),
|
||||
..msg.clone()
|
||||
},
|
||||
x => x,
|
||||
};
|
||||
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
serde_json::to_string(&msg).unwrap_or("Can't deserialize".to_string())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Response {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut msg = self.clone();
|
||||
msg = match msg {
|
||||
Response {
|
||||
client_resp: Some(ClientResp::ViewToColumnsStringResp(_)),
|
||||
..
|
||||
} => Response {
|
||||
client_resp: Some(ClientResp::ViewToColumnsStringResp(
|
||||
ViewToColumnsStringResp {
|
||||
json_string: "<< redacted >>".to_owned(),
|
||||
},
|
||||
)),
|
||||
..msg.clone()
|
||||
},
|
||||
Response {
|
||||
client_resp: Some(ClientResp::ViewToRowsStringResp(_)),
|
||||
..
|
||||
} => Response {
|
||||
client_resp: Some(ClientResp::ViewToRowsStringResp(ViewToRowsStringResp {
|
||||
json_string: "<< redacted >>".to_owned(),
|
||||
})),
|
||||
..msg.clone()
|
||||
},
|
||||
Response {
|
||||
client_resp: Some(ClientResp::ViewToNdjsonStringResp(_)),
|
||||
..
|
||||
} => Response {
|
||||
client_resp: Some(ClientResp::ViewToNdjsonStringResp(ViewToNdjsonStringResp {
|
||||
ndjson_string: "<< redacted >>".to_owned(),
|
||||
})),
|
||||
..msg.clone()
|
||||
},
|
||||
Response {
|
||||
client_resp: Some(ClientResp::ViewToArrowResp(_)),
|
||||
..
|
||||
} => Response {
|
||||
client_resp: Some(ClientResp::ViewToArrowResp(ViewToArrowResp {
|
||||
arrow: vec![],
|
||||
})),
|
||||
..msg.clone()
|
||||
},
|
||||
Response {
|
||||
client_resp: Some(ClientResp::ViewToCsvResp(_)),
|
||||
..
|
||||
} => Response {
|
||||
client_resp: Some(ClientResp::ViewToCsvResp(ViewToCsvResp {
|
||||
csv: "<< redacted >>".to_owned(),
|
||||
})),
|
||||
..msg.clone()
|
||||
},
|
||||
Response {
|
||||
client_resp: Some(ClientResp::ViewOnUpdateResp(ref x)),
|
||||
..
|
||||
} => Response {
|
||||
client_resp: Some(ClientResp::ViewOnUpdateResp(ViewOnUpdateResp {
|
||||
delta: x.delta.as_ref().map(|_| vec![]),
|
||||
port_id: x.port_id,
|
||||
})),
|
||||
..msg.clone()
|
||||
},
|
||||
x => x,
|
||||
};
|
||||
|
||||
write!(f, "{}", serde_json::to_string(&msg).unwrap())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
//! Utility functions that are common to the `perspective` crates.
|
||||
|
||||
mod clone;
|
||||
mod logging;
|
||||
mod rand_sequence;
|
||||
|
||||
#[cfg(feature = "talc-allocator")]
|
||||
mod talc_allocator;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTimeError;
|
||||
|
||||
use rand::prelude::*;
|
||||
use rand::rngs::StdRng;
|
||||
#[cfg(feature = "talc-allocator")]
|
||||
pub(crate) use talc_allocator::get_used;
|
||||
use thiserror::*;
|
||||
|
||||
use crate::proto;
|
||||
use crate::utils::rand_sequence::RandomSequence;
|
||||
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum ClientError {
|
||||
#[error("View not found")]
|
||||
ViewNotFound,
|
||||
|
||||
#[error("Abort(): {0}")]
|
||||
Internal(String),
|
||||
|
||||
#[error("Transport error: {0}")]
|
||||
TransportError(String),
|
||||
|
||||
#[error("Client not yet initialized")]
|
||||
NotInitialized,
|
||||
|
||||
#[error("Unknown error: {0}")]
|
||||
Unknown(String),
|
||||
|
||||
#[error("Unwrapped option")]
|
||||
Option,
|
||||
|
||||
#[error("Bad string")]
|
||||
Utf8(#[from] std::str::Utf8Error),
|
||||
|
||||
#[error("Undecipherable server message {0:?}")]
|
||||
DecodeError(#[from] prost::DecodeError),
|
||||
|
||||
#[error("Response aborted")]
|
||||
ResponseAborted,
|
||||
|
||||
#[error("Unexpected response {0:?}")]
|
||||
ResponseFailed(Box<proto::response::ClientResp>),
|
||||
|
||||
#[error("Not yet implemented {0:?}")]
|
||||
NotImplemented(&'static str),
|
||||
|
||||
#[error("Can't use both `limit` and `index` arguments")]
|
||||
BadTableOptions,
|
||||
|
||||
#[error("External error: {0}")]
|
||||
ExternalError(Arc<Box<dyn std::error::Error + Send + Sync>>),
|
||||
|
||||
#[error("Undecipherable proto message")]
|
||||
ProtoError(#[from] prost::EncodeError),
|
||||
|
||||
#[error("Duplicate name {0}")]
|
||||
DuplicateNameError(String),
|
||||
|
||||
#[error("{0}")]
|
||||
TimeError(#[from] SystemTimeError),
|
||||
}
|
||||
|
||||
pub type ClientResult<T> = Result<T, ClientError>;
|
||||
|
||||
impl From<Box<dyn std::error::Error + Send + Sync>> for ClientError {
|
||||
fn from(value: Box<dyn std::error::Error + Send + Sync>) -> Self {
|
||||
ClientError::ExternalError(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, A> From<std::sync::PoisonError<std::sync::MutexGuard<'a, A>>> for ClientError {
|
||||
fn from(_: std::sync::PoisonError<std::sync::MutexGuard<'a, A>>) -> Self {
|
||||
ClientError::Internal("Lock Error".to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<proto::response::ClientResp>> for ClientError {
|
||||
fn from(value: Option<proto::response::ClientResp>) -> Self {
|
||||
match value {
|
||||
Some(proto::response::ClientResp::ServerError(x)) => match x.status_code() {
|
||||
proto::StatusCode::ServerError => ClientError::Internal(x.message),
|
||||
proto::StatusCode::ViewNotFound => ClientError::ViewNotFound,
|
||||
proto::StatusCode::TransportError => ClientError::TransportError(x.message),
|
||||
},
|
||||
Some(x) => ClientError::ResponseFailed(Box::new(x)),
|
||||
None => ClientError::ResponseAborted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::response::ClientResp> for ClientError {
|
||||
fn from(value: proto::response::ClientResp) -> Self {
|
||||
match value {
|
||||
proto::response::ClientResp::ServerError(x) => match x.status_code() {
|
||||
proto::StatusCode::ServerError => ClientError::Internal(x.message),
|
||||
proto::StatusCode::ViewNotFound => ClientError::ViewNotFound,
|
||||
proto::StatusCode::TransportError => ClientError::TransportError(x.message),
|
||||
},
|
||||
x => ClientError::ResponseFailed(Box::new(x)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PerspectiveResultExt {
|
||||
fn unwrap_or_log(&self);
|
||||
}
|
||||
|
||||
impl<T, E> PerspectiveResultExt for Result<T, E>
|
||||
where
|
||||
E: std::error::Error,
|
||||
{
|
||||
fn unwrap_or_log(&self) {
|
||||
if let Err(e) = self {
|
||||
tracing::warn!("{}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a sequence of IDs
|
||||
#[derive(Clone)]
|
||||
pub struct IDGen(Arc<std::sync::Mutex<RandomSequence>>);
|
||||
|
||||
impl Default for IDGen {
|
||||
fn default() -> Self {
|
||||
Self(Arc::new(std::sync::Mutex::new(Self::new_seq())))
|
||||
}
|
||||
}
|
||||
|
||||
impl IDGen {
|
||||
fn new_seq() -> RandomSequence {
|
||||
let mut rng = rand::rngs::ThreadRng::default();
|
||||
let config = RandomSequence::new(rng.next_u32(), rng.next_u32());
|
||||
config.into_iter()
|
||||
}
|
||||
|
||||
pub fn next(&self) -> u32 {
|
||||
let mut idgen = self.0.lock().unwrap();
|
||||
if let Some(x) = idgen.next() {
|
||||
x
|
||||
} else {
|
||||
*idgen = Self::new_seq();
|
||||
idgen.next().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SIZE: usize = 21;
|
||||
|
||||
const CHARACTERS: [char; 52] = [
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
|
||||
't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
|
||||
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||
];
|
||||
|
||||
/// Generate a random identifier `String`
|
||||
pub fn randid() -> String {
|
||||
let mask = CHARACTERS.len().next_power_of_two() - 1;
|
||||
let step: usize = 8 * SIZE / 5;
|
||||
let mut id = String::with_capacity(SIZE);
|
||||
loop {
|
||||
let mut rng = rand::rngs::ThreadRng::default();
|
||||
let mut random = StdRng::from_rng(&mut rng);
|
||||
let mut bytes: Vec<u8> = vec![0; step];
|
||||
random.fill(&mut bytes[..]);
|
||||
for &byte in &bytes {
|
||||
let byte = byte as usize & mask;
|
||||
if CHARACTERS.len() > byte {
|
||||
id.push(CHARACTERS[byte]);
|
||||
if id.len() == SIZE {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
//! This implementation liberally borrows from the excellent
|
||||
//! [`rand-unique`](https://github.com/hoxxep/rand-unique) crate, MIT licensed.
|
||||
//! Unlike `rand-unique`, this hacked up version only implements u32 (for wasm),
|
||||
//! but is compatible with `rand>0.8` and ~20kb smaller (in wasm).
|
||||
|
||||
const INIT_BASE: u32 = 0x682f0161;
|
||||
const INIT_OFFSET: u32 = 0x46790905;
|
||||
const PRIME: u32 = 4294967291;
|
||||
const INTERMEDIATE_XOR: u32 = 0x5bf03635;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RandomSequence {
|
||||
start_index: u32,
|
||||
current_index: u32,
|
||||
intermediate_offset: u32,
|
||||
ended: bool,
|
||||
}
|
||||
|
||||
impl RandomSequence {
|
||||
pub fn new(seed_base: u32, seed_offset: u32) -> Self {
|
||||
let start_index = permute_qpr(permute_qpr(seed_base).wrapping_add(INIT_BASE));
|
||||
let intermediate_offset = permute_qpr(permute_qpr(seed_offset).wrapping_add(INIT_OFFSET));
|
||||
RandomSequence {
|
||||
start_index,
|
||||
current_index: 0,
|
||||
intermediate_offset,
|
||||
ended: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RandomSequence {
|
||||
type Item = u32;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let index = self.start_index.wrapping_add(self.current_index);
|
||||
let inner_residue = permute_qpr(index).wrapping_add(self.intermediate_offset);
|
||||
let next = permute_qpr(inner_residue ^ INTERMEDIATE_XOR);
|
||||
self.current_index = match self.current_index.checked_add(1) {
|
||||
Some(v) => {
|
||||
self.ended = false;
|
||||
v
|
||||
},
|
||||
None => {
|
||||
if !self.ended {
|
||||
self.ended = true;
|
||||
self.current_index
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Some(next)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
(u32::MAX as usize - self.current_index as usize, None)
|
||||
}
|
||||
}
|
||||
|
||||
fn permute_qpr(x: u32) -> u32 {
|
||||
if x >= PRIME {
|
||||
return x;
|
||||
}
|
||||
|
||||
let residue = ((x as u64 * x as u64) % PRIME as u64) as u32;
|
||||
if x <= PRIME >> 1 {
|
||||
residue
|
||||
} else {
|
||||
PRIME - residue
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
#[global_allocator]
|
||||
static TALC: talc::TalckWasm = unsafe { talc::TalckWasm::new_global() };
|
||||
|
||||
pub fn get_used() -> (usize, usize) {
|
||||
return {
|
||||
let counters = TALC.lock();
|
||||
(
|
||||
counters.get_counters().allocated_bytes,
|
||||
(counters.get_counters().available_bytes + counters.get_counters().allocated_bytes)
|
||||
as usize,
|
||||
)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct X {
|
||||
x: i32,
|
||||
}
|
||||
|
||||
impl X {
|
||||
const fn test(&self) -> i32 {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Y {
|
||||
y: X,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_one_ident() {
|
||||
let x = 0;
|
||||
clone!(x);
|
||||
assert_eq!(x, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_two_idents() {
|
||||
let x = 0;
|
||||
let y = 1;
|
||||
clone!(x, y);
|
||||
assert_eq!(x, 0);
|
||||
assert_eq!(y, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_struct_field() {
|
||||
let x = X { x: 0 };
|
||||
clone!(x.x);
|
||||
assert_eq!(x, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_struct_struct_field() {
|
||||
let x = Y { y: X { x: 0 } };
|
||||
clone!(x.y.x);
|
||||
assert_eq!(x, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple() {
|
||||
let x = (0, 1);
|
||||
clone!(x.0);
|
||||
assert_eq!(x, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alias() {
|
||||
let x = 0;
|
||||
clone!(y = x);
|
||||
assert_eq!(y, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex() {
|
||||
let x = Y { y: X { x: 0 } };
|
||||
let w = (0, x.clone());
|
||||
clone!(z = x.y.x, x.y.test(), w.1.y.x);
|
||||
assert_eq!(z, 0);
|
||||
assert_eq!(x, 0);
|
||||
assert_eq!(test, 1);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
mod clone;
|
||||
@@ -0,0 +1,638 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::Future;
|
||||
use prost::bytes::Bytes;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use self::view_on_update_req::Mode;
|
||||
use crate::assert_view_api;
|
||||
use crate::client::Client;
|
||||
use crate::proto::request::ClientReq;
|
||||
use crate::proto::response::ClientResp;
|
||||
use crate::proto::*;
|
||||
#[cfg(doc)]
|
||||
use crate::table::Table;
|
||||
pub use crate::utils::*;
|
||||
|
||||
/// Options for [`View::on_update`].
|
||||
#[derive(Default, Debug, Deserialize, TS)]
|
||||
pub struct OnUpdateOptions {
|
||||
pub mode: Option<OnUpdateMode>,
|
||||
}
|
||||
|
||||
/// The update mode for [`View::on_update`].
|
||||
///
|
||||
/// `Row` mode calculates and provides the update batch new rows/columns as an
|
||||
/// Apache Arrow to the callback provided to [`View::on_update`]. This allows
|
||||
/// incremental updates if your callbakc can read this format, but should be
|
||||
/// disabled otherwise.
|
||||
#[derive(Default, Debug, Deserialize, TS)]
|
||||
pub enum OnUpdateMode {
|
||||
#[default]
|
||||
#[serde(rename = "row")]
|
||||
Row,
|
||||
}
|
||||
|
||||
impl FromStr for OnUpdateMode {
|
||||
type Err = ClientError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s == "row" {
|
||||
Ok(OnUpdateMode::Row)
|
||||
} else {
|
||||
Err(ClientError::Option)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq)]
|
||||
pub struct ColumnWindow {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub start_col: Option<f32>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub end_col: Option<f32>,
|
||||
}
|
||||
|
||||
/// Options for serializing a window of data from a [`View`].
|
||||
///
|
||||
/// Some fields of [`ViewWindow`] are only applicable to specific methods of
|
||||
/// [`View`].
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq)]
|
||||
pub struct ViewWindow {
|
||||
#[ts(optional)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub start_row: Option<f64>,
|
||||
|
||||
#[ts(optional)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub start_col: Option<f64>,
|
||||
|
||||
#[ts(optional)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub end_row: Option<f64>,
|
||||
|
||||
#[ts(optional)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub end_col: Option<f64>,
|
||||
|
||||
#[ts(optional)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<bool>,
|
||||
|
||||
#[ts(optional)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub index: Option<bool>,
|
||||
|
||||
/// Only impacts [`View::to_csv`]
|
||||
#[ts(optional)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub formatted: Option<bool>,
|
||||
|
||||
/// Only impacts [`View::to_arrow`]
|
||||
#[ts(optional)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub compression: Option<String>,
|
||||
|
||||
/// When `true`, group-by columns use legacy `"colname (Group by N)"`
|
||||
/// naming. When `false`, they use `__ROW_PATH_N__` naming consistent
|
||||
/// with the SQL backend. Defaults to `true` for backwards compatibility.
|
||||
#[ts(optional)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub emit_legacy_row_path_names: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<ViewWindow> for ViewPort {
|
||||
fn from(window: ViewWindow) -> Self {
|
||||
ViewPort {
|
||||
start_row: window.start_row.map(|x| x.floor() as u32),
|
||||
start_col: window.start_col.map(|x| x.floor() as u32),
|
||||
end_row: window.end_row.map(|x| x.ceil() as u32),
|
||||
end_col: window.end_col.map(|x| x.ceil() as u32),
|
||||
emit_legacy_row_path_names: window.emit_legacy_row_path_names,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ViewPort> for ViewWindow {
|
||||
fn from(window: ViewPort) -> Self {
|
||||
ViewWindow {
|
||||
start_row: window.start_row.map(|x| x as f64),
|
||||
start_col: window.start_col.map(|x| x as f64),
|
||||
end_row: window.end_row.map(|x| x as f64),
|
||||
end_col: window.end_col.map(|x| x as f64),
|
||||
emit_legacy_row_path_names: window.emit_legacy_row_path_names,
|
||||
..ViewWindow::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rows updated and port ID corresponding to an update batch, provided to the
|
||||
/// callback argument to [`View::on_update`] with the "rows" mode.
|
||||
#[derive(TS)]
|
||||
pub struct OnUpdateData(crate::proto::ViewOnUpdateResp);
|
||||
|
||||
impl Deref for OnUpdateData {
|
||||
type Target = crate::proto::ViewOnUpdateResp;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`View`] struct is Perspective's query and serialization interface. It
|
||||
/// represents a query on the `Table`'s dataset and is always created from an
|
||||
/// existing `Table` instance via the [`Table::view`] method.
|
||||
///
|
||||
/// [`View`]s are immutable with respect to the arguments provided to the
|
||||
/// [`Table::view`] method; to change these parameters, you must create a new
|
||||
/// [`View`] on the same [`Table`]. However, each [`View`] is _live_ with
|
||||
/// respect to the [`Table`]'s data, and will (within a conflation window)
|
||||
/// update with the latest state as its parent [`Table`] updates, including
|
||||
/// incrementally recalculating all aggregates, pivots, filters, etc. [`View`]
|
||||
/// query parameters are composable, in that each parameter works independently
|
||||
/// _and_ in conjunction with each other, and there is no limit to the number of
|
||||
/// pivots, filters, etc. which can be applied.
|
||||
///
|
||||
/// To construct a [`View`], call the [`Table::view`] factory method. A
|
||||
/// [`Table`] can have as many [`View`]s associated with it as you need -
|
||||
/// Perspective conserves memory by relying on a single [`Table`] to power
|
||||
/// multiple [`View`]s concurrently.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use perspective_client::{Client, TableData, TableInitOptions, UpdateData, ViewWindow};
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let client: Client = todo!();
|
||||
/// let opts = TableInitOptions::default();
|
||||
/// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
|
||||
/// let table = client.table(data, opts).await?;
|
||||
///
|
||||
/// let view = table.view(None).await?;
|
||||
/// let arrow = view.to_arrow(ViewWindow::default()).await?;
|
||||
/// view.delete().await?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::collections::HashMap;
|
||||
/// # use perspective_client::Table;
|
||||
/// # use perspective_client::config::*;
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let table: Table = todo!();
|
||||
/// let view = table
|
||||
/// .view(Some(ViewConfigUpdate {
|
||||
/// columns: Some(vec![Some("Sales".into())]),
|
||||
/// aggregates: Some(HashMap::from_iter(vec![("Sales".into(), "sum".into())])),
|
||||
/// group_by: Some(vec!["Region".into(), "Country".into()]),
|
||||
/// filter: Some(vec![Filter::new("Category", "in", &[
|
||||
/// "Furniture",
|
||||
/// "Technology",
|
||||
/// ])]),
|
||||
/// ..ViewConfigUpdate::default()
|
||||
/// }))
|
||||
/// .await?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
///
|
||||
/// Group By
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use perspective_client::Table;
|
||||
/// # use perspective_client::config::*;
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let table: Table = todo!();
|
||||
/// let view = table
|
||||
/// .view(Some(ViewConfigUpdate {
|
||||
/// group_by: Some(vec!["a".into(), "c".into()]),
|
||||
/// ..ViewConfigUpdate::default()
|
||||
/// }))
|
||||
/// .await?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
///
|
||||
/// Split By
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use perspective_client::Table;
|
||||
/// # use perspective_client::config::*;
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let table: Table = todo!();
|
||||
/// let view = table
|
||||
/// .view(Some(ViewConfigUpdate {
|
||||
/// split_by: Some(vec!["a".into(), "c".into()]),
|
||||
/// ..ViewConfigUpdate::default()
|
||||
/// }))
|
||||
/// .await?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
///
|
||||
/// In Javascript, a [`Table`] can be constructed on a [`Table::view`] instance,
|
||||
/// which will return a new [`Table`] based on the [`Table::view`]'s dataset,
|
||||
/// and all future updates that affect the [`Table::view`] will be forwarded to
|
||||
/// the new [`Table`]. This is particularly useful for implementing a
|
||||
/// [Client/Server Replicated](server.md#clientserver-replicated) design, by
|
||||
/// serializing the `View` to an arrow and setting up an `on_update` callback.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use perspective_client::{Client, TableData, TableInitOptions, UpdateData, UpdateOptions};
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let client: Client = todo!();
|
||||
/// let opts = TableInitOptions::default();
|
||||
/// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
|
||||
/// let table = client.table(data, opts.clone()).await?;
|
||||
/// let view = table.view(None).await?;
|
||||
/// let table2 = client.table(TableData::View(view), opts).await?;
|
||||
/// let more = UpdateData::Csv("x,y\n5,6".into());
|
||||
/// table.update(more, UpdateOptions::default()).await?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct View {
|
||||
pub name: String,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
assert_view_api!(View);
|
||||
|
||||
impl View {
|
||||
pub fn new(name: String, client: Client) -> Self {
|
||||
View { name, client }
|
||||
}
|
||||
|
||||
fn client_message(&self, req: ClientReq) -> Request {
|
||||
crate::proto::Request {
|
||||
msg_id: self.client.gen_id(),
|
||||
entity_id: self.name.clone(),
|
||||
client_req: Some(req),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an array of strings containing the column paths of the [`View`]
|
||||
/// without any of the source columns.
|
||||
///
|
||||
/// A column path shows the columns that a given cell belongs to after
|
||||
/// pivots are applied.
|
||||
pub async fn column_paths(&self, window: ColumnWindow) -> ClientResult<Vec<String>> {
|
||||
let msg = self.client_message(ClientReq::ViewColumnPathsReq(ViewColumnPathsReq {
|
||||
start_col: window.start_col.map(|x| x as u32),
|
||||
end_col: window.end_col.map(|x| x as u32),
|
||||
}));
|
||||
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewColumnPathsResp(ViewColumnPathsResp { paths }) => {
|
||||
// Ok(paths.into_iter().map(|x| x.path).collect())
|
||||
Ok(paths)
|
||||
},
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns this [`View`]'s _dimensions_, row and column count, as well as
|
||||
/// those of the [`crate::Table`] from which it was derived.
|
||||
///
|
||||
/// - `num_table_rows` - The number of rows in the underlying
|
||||
/// [`crate::Table`].
|
||||
/// - `num_table_columns` - The number of columns in the underlying
|
||||
/// [`crate::Table`] (including the `index` column if this
|
||||
/// [`crate::Table`] was constructed with one).
|
||||
/// - `num_view_rows` - The number of rows in this [`View`]. If this
|
||||
/// [`View`] has a `group_by` clause, `num_view_rows` will also include
|
||||
/// aggregated rows.
|
||||
/// - `num_view_columns` - The number of columns in this [`View`]. If this
|
||||
/// [`View`] has a `split_by` clause, `num_view_columns` will include all
|
||||
/// _column paths_, e.g. the number of `columns` clause times the number
|
||||
/// of `split_by` groups.
|
||||
pub async fn dimensions(&self) -> ClientResult<ViewDimensionsResp> {
|
||||
let msg = self.client_message(ClientReq::ViewDimensionsReq(ViewDimensionsReq {}));
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewDimensionsResp(resp) => Ok(resp),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The expression schema of this [`View`], which contains only the
|
||||
/// expressions created on this [`View`]. See [`View::schema`] for
|
||||
/// details.
|
||||
pub async fn expression_schema(&self) -> ClientResult<HashMap<String, ColumnType>> {
|
||||
if self.client.get_features().await?.expressions {
|
||||
let msg = self.client_message(ClientReq::ViewExpressionSchemaReq(
|
||||
ViewExpressionSchemaReq {},
|
||||
));
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewExpressionSchemaResp(ViewExpressionSchemaResp { schema }) => {
|
||||
Ok(schema
|
||||
.into_iter()
|
||||
.map(|(x, y)| (x, ColumnType::try_from(y).unwrap()))
|
||||
.collect())
|
||||
},
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
} else {
|
||||
Ok([].into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// A copy of the [`ViewConfig`] object passed to the [`Table::view`] method
|
||||
/// which created this [`View`].
|
||||
pub async fn get_config(&self) -> ClientResult<crate::config::ViewConfig> {
|
||||
let msg = self.client_message(ClientReq::ViewGetConfigReq(ViewGetConfigReq {}));
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewGetConfigResp(ViewGetConfigResp {
|
||||
config: Some(config),
|
||||
}) => Ok(config.into()),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of aggregated rows in this [`View`]. This is affected by the
|
||||
/// "group_by" configuration parameter supplied to this view's contructor.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The number of aggregated rows.
|
||||
pub async fn num_rows(&self) -> ClientResult<u32> {
|
||||
Ok(self.dimensions().await?.num_view_rows)
|
||||
}
|
||||
|
||||
/// The schema of this [`View`].
|
||||
///
|
||||
/// The [`View`] schema differs from the `schema` returned by
|
||||
/// [`Table::schema`]; it may have different column names due to
|
||||
/// `expressions` or `columns` configs, or it maye have _different
|
||||
/// column types_ due to the application og `group_by` and `aggregates`
|
||||
/// config. You can think of [`Table::schema`] as the _input_ schema and
|
||||
/// [`View::schema`] as the _output_ schema of a Perspective pipeline.
|
||||
pub async fn schema(&self) -> ClientResult<HashMap<String, ColumnType>> {
|
||||
let msg = self.client_message(ClientReq::ViewSchemaReq(ViewSchemaReq {}));
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewSchemaResp(ViewSchemaResp { schema }) => Ok(schema
|
||||
.into_iter()
|
||||
.map(|(x, y)| (x, ColumnType::try_from(y).unwrap()))
|
||||
.collect()),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes a [`View`] to the Apache Arrow data format.
|
||||
pub async fn to_arrow(&self, window: ViewWindow) -> ClientResult<Bytes> {
|
||||
let msg = self.client_message(ClientReq::ViewToArrowReq(ViewToArrowReq {
|
||||
viewport: Some(window.clone().into()),
|
||||
compression: window.compression,
|
||||
}));
|
||||
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewToArrowResp(ViewToArrowResp { arrow }) => Ok(arrow.into()),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes this [`View`] to a string of JSON data. Useful if you want to
|
||||
/// save additional round trip serialize/deserialize cycles.
|
||||
pub async fn to_columns_string(&self, window: ViewWindow) -> ClientResult<String> {
|
||||
let msg = self.client_message(ClientReq::ViewToColumnsStringReq(ViewToColumnsStringReq {
|
||||
viewport: Some(window.clone().into()),
|
||||
id: window.id,
|
||||
index: window.index,
|
||||
formatted: window.formatted,
|
||||
}));
|
||||
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewToColumnsStringResp(ViewToColumnsStringResp { json_string }) => {
|
||||
Ok(json_string)
|
||||
},
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Render this `View` as a JSON string.
|
||||
pub async fn to_json_string(&self, window: ViewWindow) -> ClientResult<String> {
|
||||
let viewport = ViewPort::from(window.clone());
|
||||
let msg = self.client_message(ClientReq::ViewToRowsStringReq(ViewToRowsStringReq {
|
||||
viewport: Some(viewport),
|
||||
id: window.id,
|
||||
index: window.index,
|
||||
formatted: window.formatted,
|
||||
}));
|
||||
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewToRowsStringResp(ViewToRowsStringResp { json_string }) => {
|
||||
Ok(json_string)
|
||||
},
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders this [`View`] as an [NDJSON](https://github.com/ndjson/ndjson-spec)
|
||||
/// formatted [`String`].
|
||||
pub async fn to_ndjson(&self, window: ViewWindow) -> ClientResult<String> {
|
||||
let viewport = ViewPort::from(window.clone());
|
||||
let msg = self.client_message(ClientReq::ViewToNdjsonStringReq(ViewToNdjsonStringReq {
|
||||
viewport: Some(viewport),
|
||||
id: window.id,
|
||||
index: window.index,
|
||||
formatted: window.formatted,
|
||||
}));
|
||||
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewToNdjsonStringResp(ViewToNdjsonStringResp { ndjson_string }) => {
|
||||
Ok(ndjson_string)
|
||||
},
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes this [`View`] to CSV data in a standard format.
|
||||
pub async fn to_csv(&self, window: ViewWindow) -> ClientResult<String> {
|
||||
let msg = self.client_message(ClientReq::ViewToCsvReq(ViewToCsvReq {
|
||||
viewport: Some(window.into()),
|
||||
}));
|
||||
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewToCsvResp(ViewToCsvResp { csv }) => Ok(csv),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete this [`View`] and clean up all resources associated with it.
|
||||
/// [`View`] objects do not stop consuming resources or processing
|
||||
/// updates when they are garbage collected - you must call this method
|
||||
/// to reclaim these.
|
||||
pub async fn delete(&self) -> ClientResult<()> {
|
||||
let msg = self.client_message(ClientReq::ViewDeleteReq(ViewDeleteReq {}));
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewDeleteResp(_) => Ok(()),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the [min, max] of the leaf nodes of a column `column_name`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A tuple of [min, max], whose types are column and aggregate dependent.
|
||||
pub async fn get_min_max(
|
||||
&self,
|
||||
column_name: String,
|
||||
) -> ClientResult<(crate::config::Scalar, crate::config::Scalar)> {
|
||||
let msg = self.client_message(ClientReq::ViewGetMinMaxReq(ViewGetMinMaxReq {
|
||||
column_name,
|
||||
}));
|
||||
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewGetMinMaxResp(ViewGetMinMaxResp { min, max }) => {
|
||||
let min = min.map(crate::config::Scalar::from).unwrap_or_default();
|
||||
let max = max.map(crate::config::Scalar::from).unwrap_or_default();
|
||||
Ok((min, max))
|
||||
},
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a callback with this [`View`]. Whenever the view's underlying
|
||||
/// table emits an update, this callback will be invoked with an object
|
||||
/// containing `port_id`, indicating which port the update fired on, and
|
||||
/// optionally `delta`, which is the new data that was updated for each
|
||||
/// cell or each row.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `on_update` - A callback function invoked on update, which receives an
|
||||
/// object with two keys: `port_id`, indicating which port the update was
|
||||
/// triggered on, and `delta`, whose value is dependent on the mode
|
||||
/// parameter.
|
||||
/// - `options` - If this is provided as `OnUpdateOptions { mode:
|
||||
/// Some(OnUpdateMode::Row) }`, then `delta` is an Arrow of the updated
|
||||
/// rows. Otherwise `delta` will be [`Option::None`].
|
||||
pub async fn on_update<T, U>(&self, on_update: T, options: OnUpdateOptions) -> ClientResult<u32>
|
||||
where
|
||||
T: Fn(OnUpdateData) -> U + Send + Sync + 'static,
|
||||
U: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
let on_update = Arc::new(on_update);
|
||||
let callback = move |resp: Response| {
|
||||
let on_update = on_update.clone();
|
||||
async move {
|
||||
match resp.client_resp {
|
||||
Some(ClientResp::ViewOnUpdateResp(resp)) => {
|
||||
on_update(OnUpdateData(resp)).await;
|
||||
Ok(())
|
||||
},
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let msg = self.client_message(ClientReq::ViewOnUpdateReq(ViewOnUpdateReq {
|
||||
mode: options.mode.map(|OnUpdateMode::Row| Mode::Row as i32),
|
||||
}));
|
||||
|
||||
self.client.subscribe(&msg, callback).await?;
|
||||
Ok(msg.msg_id)
|
||||
}
|
||||
|
||||
/// Unregister a previously registered update callback with this [`View`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `id` - A callback `id` as returned by a recipricol call to
|
||||
/// [`View::on_update`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use perspective_client::{OnUpdateOptions, View};
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # let view: View = todo!();
|
||||
/// let callback = |_| async { print!("Updated!") };
|
||||
/// let cid = view.on_update(callback, OnUpdateOptions::default()).await?;
|
||||
/// view.remove_update(cid).await?;
|
||||
/// # Ok(()) }
|
||||
/// ```
|
||||
pub async fn remove_update(&self, update_id: u32) -> ClientResult<()> {
|
||||
let msg = self.client_message(ClientReq::ViewRemoveOnUpdateReq(ViewRemoveOnUpdateReq {
|
||||
id: update_id,
|
||||
}));
|
||||
|
||||
self.client.unsubscribe(update_id).await?;
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewRemoveOnUpdateResp(_) => Ok(()),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a callback with this [`View`]. Whenever the [`View`] is
|
||||
/// deleted, this callback will be invoked.
|
||||
pub async fn on_delete(
|
||||
&self,
|
||||
on_delete: Box<dyn Fn() + Send + Sync + 'static>,
|
||||
) -> ClientResult<u32> {
|
||||
let callback = move |resp: Response| match resp.client_resp.unwrap() {
|
||||
ClientResp::ViewOnDeleteResp(_) => {
|
||||
on_delete();
|
||||
Ok(())
|
||||
},
|
||||
resp => Err(resp.into()),
|
||||
};
|
||||
|
||||
let msg = self.client_message(ClientReq::ViewOnDeleteReq(ViewOnDeleteReq {}));
|
||||
self.client.subscribe_once(&msg, Box::new(callback)).await?;
|
||||
Ok(msg.msg_id)
|
||||
}
|
||||
|
||||
/// Unregister a previously registered [`View::on_delete`] callback.
|
||||
pub async fn remove_delete(&self, callback_id: u32) -> ClientResult<()> {
|
||||
let msg = self.client_message(ClientReq::ViewRemoveDeleteReq(ViewRemoveDeleteReq {
|
||||
id: callback_id,
|
||||
}));
|
||||
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewRemoveDeleteResp(ViewRemoveDeleteResp {}) => Ok(()),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Collapses the `group_by` row at `row_index`.
|
||||
pub async fn collapse(&self, row_index: u32) -> ClientResult<u32> {
|
||||
let msg = self.client_message(ClientReq::ViewCollapseReq(ViewCollapseReq { row_index }));
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewCollapseResp(ViewCollapseResp { num_changed }) => Ok(num_changed),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand the `group_by` row at `row_index`.
|
||||
pub async fn expand(&self, row_index: u32) -> ClientResult<u32> {
|
||||
let msg = self.client_message(ClientReq::ViewExpandReq(ViewExpandReq { row_index }));
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewExpandResp(ViewExpandResp { num_changed }) => Ok(num_changed),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set expansion `depth` of the `group_by` tree.
|
||||
pub async fn set_depth(&self, depth: u32) -> ClientResult<()> {
|
||||
let msg = self.client_message(ClientReq::ViewSetDepthReq(ViewSetDepthReq { depth }));
|
||||
match self.client.oneshot(&msg).await? {
|
||||
ClientResp::ViewSetDepthResp(_) => Ok(()),
|
||||
resp => Err(resp.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use prost::{DecodeError, EncodeError};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Error type for virtual server operations.
|
||||
///
|
||||
/// This enum represents the various errors that can occur when processing
|
||||
/// requests through a [`VirtualServer`](super::VirtualServer).
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum VirtualServerError<T: std::fmt::Debug> {
|
||||
#[error("External Error: {0:?}")]
|
||||
InternalError(#[from] T),
|
||||
|
||||
#[error("{0}")]
|
||||
DecodeError(DecodeError),
|
||||
|
||||
#[error("{0}")]
|
||||
EncodeError(EncodeError),
|
||||
|
||||
#[error("View not found '{0}'")]
|
||||
UnknownViewId(String),
|
||||
|
||||
#[error("Invalid JSON'{0}'")]
|
||||
InvalidJSON(std::sync::Arc<serde_json::Error>),
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
/// Extension trait for extracting internal errors from [`VirtualServerError`]
|
||||
/// results.
|
||||
///
|
||||
/// Provides a method to distinguish between internal handler errors and other
|
||||
/// virtual server errors.
|
||||
pub trait ResultExt<X, T> {
|
||||
fn get_internal_error(self) -> Result<X, Result<T, String>>;
|
||||
}
|
||||
|
||||
impl<X, T: std::fmt::Debug> ResultExt<X, T> for Result<X, VirtualServerError<T>> {
|
||||
fn get_internal_error(self) -> Result<X, Result<T, String>> {
|
||||
match self {
|
||||
Ok(x) => Ok(x),
|
||||
Err(VirtualServerError::InternalError(x)) => Err(Ok(x)),
|
||||
Err(x) => Err(Err(x.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::GroupRollupMode;
|
||||
use crate::proto::get_features_resp::{AggregateArgs, AggregateOptions, ColumnTypeOptions};
|
||||
use crate::proto::{ColumnType, GetFeaturesResp};
|
||||
|
||||
/// Describes the capabilities supported by a virtual server handler.
|
||||
///
|
||||
/// This struct is returned by
|
||||
/// [`VirtualServerHandler::get_features`](super::VirtualServerHandler::get_features)
|
||||
/// to inform clients about which operations are available.
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
|
||||
pub struct Features<'a> {
|
||||
/// Whether group-by aggregation is supported.
|
||||
#[serde(default)]
|
||||
pub group_by: bool,
|
||||
|
||||
/// Which `group_by_rollup_mode` options are supported
|
||||
#[serde(default)]
|
||||
pub group_rollup_mode: Vec<GroupRollupMode>,
|
||||
|
||||
/// Whether split-by (pivot) operations are supported.
|
||||
#[serde(default)]
|
||||
pub split_by: bool,
|
||||
|
||||
/// Available filter operators per column type.
|
||||
#[serde(default)]
|
||||
pub filter_ops: IndexMap<ColumnType, Vec<Cow<'a, str>>>,
|
||||
|
||||
/// Available aggregate functions per column type.
|
||||
#[serde(default)]
|
||||
pub aggregates: IndexMap<ColumnType, Vec<AggSpec<'a>>>,
|
||||
|
||||
/// Whether sorting is supported.
|
||||
#[serde(default)]
|
||||
pub sort: bool,
|
||||
|
||||
/// Whether computed expressions are supported.
|
||||
#[serde(default)]
|
||||
pub expressions: bool,
|
||||
|
||||
/// Whether update callbacks are supported.
|
||||
#[serde(default)]
|
||||
pub on_update: bool,
|
||||
}
|
||||
|
||||
/// Specification for an aggregate function.
|
||||
///
|
||||
/// Aggregates can either take no additional arguments ([`AggSpec::Single`])
|
||||
/// or require column type arguments ([`AggSpec::Multiple`]).
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum AggSpec<'a> {
|
||||
/// An aggregate function with no additional arguments.
|
||||
Single(Cow<'a, str>),
|
||||
/// An aggregate function that requires column type arguments.
|
||||
Multiple(Cow<'a, str>, Vec<ColumnType>),
|
||||
}
|
||||
|
||||
impl<'a> From<Features<'a>> for GetFeaturesResp {
|
||||
fn from(value: Features<'a>) -> GetFeaturesResp {
|
||||
GetFeaturesResp {
|
||||
group_by: value.group_by,
|
||||
group_rollup_mode: value
|
||||
.group_rollup_mode
|
||||
.iter()
|
||||
.map(|x| crate::proto::GroupRollupMode::from(*x) as i32)
|
||||
.collect(),
|
||||
split_by: value.split_by,
|
||||
expressions: value.expressions,
|
||||
on_update: value.on_update,
|
||||
sort: value.sort,
|
||||
aggregates: value
|
||||
.aggregates
|
||||
.iter()
|
||||
.map(|(dtype, aggs)| {
|
||||
(*dtype as u32, AggregateOptions {
|
||||
aggregates: aggs
|
||||
.iter()
|
||||
.map(|agg| match agg {
|
||||
AggSpec::Single(cow) => AggregateArgs {
|
||||
name: cow.to_string(),
|
||||
args: vec![],
|
||||
},
|
||||
AggSpec::Multiple(cow, column_types) => AggregateArgs {
|
||||
name: cow.to_string(),
|
||||
args: column_types.iter().map(|x| *x as i32).collect(),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
filter_ops: value
|
||||
.filter_ops
|
||||
.iter()
|
||||
.map(|(ty, options)| {
|
||||
(*ty as u32, ColumnTypeOptions {
|
||||
options: options.iter().map(|x| (*x).to_string()).collect(),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
//! SQL query builder for virtual server operations.
|
||||
//!
|
||||
//! This module provides a stateless SQL query generator that produces
|
||||
//! generic SQL strings for perspective virtual server operations.
|
||||
|
||||
// TODO(texodus): Missing these features
|
||||
//
|
||||
// - row expand/collapse in the datagrid needs datamodel support, this is likely
|
||||
// a "collapsed" boolean column in the temp table we `UPDATE`.
|
||||
//
|
||||
// - `on_update` real-time support will be method which takes sa view name and a
|
||||
// handler and calls the handler when the view needs to be recalculated.
|
||||
//
|
||||
// Nice to have:
|
||||
//
|
||||
// - Optional `view_change` method can be implemented for engine optimization,
|
||||
// defaulting to just delete & recreate (as Perspective engine does now).
|
||||
//
|
||||
// - Would like to add a metadata API so that e.g. Viewer debug panel could show
|
||||
// internal generated SQL.
|
||||
|
||||
mod table_make_view;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::config::{FilterTerm, GroupRollupMode, Scalar, Sort, SortDir, ViewConfig};
|
||||
use crate::proto::{ColumnType, ViewPort};
|
||||
use crate::virtual_server::generic_sql_model::table_make_view::ViewQueryContext;
|
||||
|
||||
/// Error type for SQL generation operations.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GenericSQLError {
|
||||
/// A required column was not found in the schema.
|
||||
ColumnNotFound(String),
|
||||
/// An invalid configuration was provided.
|
||||
InvalidConfig(String),
|
||||
/// An unsupported operation was requested.
|
||||
UnsupportedOperation(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for GenericSQLError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::ColumnNotFound(col) => write!(f, "Column not found: {}", col),
|
||||
Self::InvalidConfig(msg) => write!(f, "Invalid configuration: {}", msg),
|
||||
Self::UnsupportedOperation(msg) => write!(f, "Unsupported operation: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for GenericSQLError {}
|
||||
|
||||
/// Result type alias for SQL operations.
|
||||
pub type GenericSQLResult<T> = Result<T, GenericSQLError>;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct GenericSQLVirtualServerModelArgs {
|
||||
create_entity: Option<String>,
|
||||
grouping_fn: Option<String>,
|
||||
}
|
||||
|
||||
/// A stateless SQL query builder virtual server operations.
|
||||
///
|
||||
/// This struct generates SQL query strings without executing them, allowing
|
||||
/// the caller to execute the queries against a SQL connection.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct GenericSQLVirtualServerModel(GenericSQLVirtualServerModelArgs);
|
||||
|
||||
impl GenericSQLVirtualServerModel {
|
||||
/// Creates a new `GenericSQLVirtualServerModel` instance.
|
||||
pub fn new(args: GenericSQLVirtualServerModelArgs) -> Self {
|
||||
Self(args)
|
||||
}
|
||||
|
||||
/// Returns the SQL query to list all hosted tables.
|
||||
///
|
||||
/// # Returns
|
||||
/// SQL: `SHOW ALL TABLES`
|
||||
pub fn get_hosted_tables(&self) -> GenericSQLResult<String> {
|
||||
Ok("SHOW ALL TABLES".to_string())
|
||||
}
|
||||
|
||||
/// Returns the SQL query to describe a table's schema.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `table_id` - The identifier of the table to describe.
|
||||
///
|
||||
/// # Returns
|
||||
/// SQL: `DESCRIBE {table_id}`
|
||||
pub fn table_schema(&self, table_id: &str) -> GenericSQLResult<String> {
|
||||
Ok(format!("DESCRIBE {}", table_id))
|
||||
}
|
||||
|
||||
/// Returns the SQL query to get the row count of a table.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `table_id` - The identifier of the table.
|
||||
///
|
||||
/// # Returns
|
||||
/// SQL: `SELECT COUNT(*) FROM {table_id}`
|
||||
pub fn table_size(&self, table_id: &str) -> GenericSQLResult<String> {
|
||||
Ok(format!("SELECT COUNT(*) FROM {}", table_id))
|
||||
}
|
||||
|
||||
/// Returns the SQL query to get the column count of a view.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `view_id` - The identifier of the view.
|
||||
///
|
||||
/// # Returns
|
||||
/// SQL: `SELECT COUNT(*) FROM (DESCRIBE {view_id})`
|
||||
pub fn view_column_size(&self, view_id: &str) -> GenericSQLResult<String> {
|
||||
Ok(format!("SELECT COUNT(*) FROM (DESCRIBE {})", view_id))
|
||||
}
|
||||
|
||||
/// Returns the SQL query to validate an expression against a table.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `table_id` - The identifier of the table.
|
||||
/// * `expression` - The SQL expression to validate.
|
||||
///
|
||||
/// # Returns
|
||||
/// SQL: `DESCRIBE (SELECT {expression} FROM {table_id})`
|
||||
pub fn table_validate_expression(
|
||||
&self,
|
||||
table_id: &str,
|
||||
expression: &str,
|
||||
) -> GenericSQLResult<String> {
|
||||
Ok(format!(
|
||||
"DESCRIBE (SELECT {} FROM {})",
|
||||
expression, table_id
|
||||
))
|
||||
}
|
||||
|
||||
/// Returns the SQL query to delete a view.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `view_id` - The identifier of the view to delete.
|
||||
///
|
||||
/// # Returns
|
||||
/// SQL: `DROP TABLE IF EXISTS {view_id}`
|
||||
pub fn view_delete(&self, view_id: &str) -> GenericSQLResult<String> {
|
||||
Ok(format!("DROP TABLE IF EXISTS {}", view_id))
|
||||
}
|
||||
|
||||
/// Returns the SQL query to create a view from a table with the given
|
||||
/// configuration.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `table_id` - The identifier of the source table.
|
||||
/// * `view_id` - The identifier for the new view.
|
||||
/// * `config` - The view configuration specifying columns, group_by,
|
||||
/// split_by, etc.
|
||||
///
|
||||
/// # Returns
|
||||
/// SQL: `CREATE TABLE {view_id} AS (...)`
|
||||
pub fn table_make_view(
|
||||
&self,
|
||||
table_id: &str,
|
||||
view_id: &str,
|
||||
config: &ViewConfig,
|
||||
) -> GenericSQLResult<String> {
|
||||
let ctx = ViewQueryContext::new(self, table_id, config);
|
||||
let query = ctx.build_query();
|
||||
let template = self.0.create_entity.as_deref().unwrap_or("TABLE");
|
||||
Ok(format!("CREATE {} {} AS ({})", template, view_id, query))
|
||||
}
|
||||
|
||||
/// Returns the SQL query to fetch data from a view with the given viewport.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `view_id` - The identifier of the view.
|
||||
/// * `config` - The view configuration.
|
||||
/// * `viewport` - The viewport specifying row/column ranges.
|
||||
/// * `schema` - The schema of the view (column names to types).
|
||||
///
|
||||
/// # Returns
|
||||
/// SQL: `SELECT ... FROM {view_id} LIMIT ... OFFSET ...`
|
||||
pub fn view_get_data(
|
||||
&self,
|
||||
view_id: &str,
|
||||
config: &ViewConfig,
|
||||
viewport: &ViewPort,
|
||||
schema: &IndexMap<String, ColumnType>,
|
||||
) -> GenericSQLResult<String> {
|
||||
let group_by = &config.group_by;
|
||||
let sort = &config.sort;
|
||||
let start_col = viewport.start_col.unwrap_or(0) as usize;
|
||||
let end_col = viewport.end_col.map(|x| x as usize);
|
||||
let start_row = viewport.start_row.unwrap_or(0);
|
||||
let end_row = viewport.end_row;
|
||||
let limit_clause = if let Some(end) = end_row {
|
||||
format!("LIMIT {} OFFSET {}", end - start_row, start_row)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let mut data_columns: Vec<&String> = schema
|
||||
.keys()
|
||||
.filter(|col_name| !col_name.starts_with("__"))
|
||||
.collect();
|
||||
|
||||
let col_sort_dir = sort.iter().find_map(|Sort(_, dir)| match dir {
|
||||
SortDir::ColAsc | SortDir::ColAscAbs => Some(true),
|
||||
SortDir::ColDesc | SortDir::ColDescAbs => Some(false),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
if let Some(ascending) = col_sort_dir {
|
||||
if ascending {
|
||||
data_columns.sort();
|
||||
} else {
|
||||
data_columns.sort_by(|a, b| b.cmp(a));
|
||||
}
|
||||
}
|
||||
|
||||
let data_columns: Vec<&String> = data_columns
|
||||
.into_iter()
|
||||
.skip(start_col)
|
||||
.take(end_col.map(|e| e - start_col).unwrap_or(usize::MAX))
|
||||
.collect();
|
||||
|
||||
let mut group_by_cols: Vec<String> = Vec::new();
|
||||
if !group_by.is_empty() {
|
||||
if config.group_rollup_mode != GroupRollupMode::Flat {
|
||||
group_by_cols.push("\"__GROUPING_ID__\"".to_string());
|
||||
}
|
||||
for idx in 0..group_by.len() {
|
||||
group_by_cols.push(format!("\"__ROW_PATH_{}__\"", idx));
|
||||
}
|
||||
}
|
||||
|
||||
let all_columns: Vec<String> = group_by_cols
|
||||
.into_iter()
|
||||
.chain(data_columns.iter().map(|col| format!("\"{}\"", col)))
|
||||
.collect();
|
||||
|
||||
Ok(format!(
|
||||
"SELECT {} FROM {} {}",
|
||||
all_columns.join(", "),
|
||||
view_id,
|
||||
limit_clause
|
||||
)
|
||||
.trim()
|
||||
.to_string())
|
||||
}
|
||||
|
||||
/// Returns the SQL query to describe a view's schema.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `view_id` - The identifier of the view.
|
||||
///
|
||||
/// # Returns
|
||||
/// SQL: `DESCRIBE {view_id}`
|
||||
pub fn view_schema(&self, view_id: &str) -> GenericSQLResult<String> {
|
||||
Ok(format!("DESCRIBE {}", view_id))
|
||||
}
|
||||
|
||||
/// Returns the SQL query to get the row count of a view.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `view_id` - The identifier of the view.
|
||||
///
|
||||
/// # Returns
|
||||
/// SQL: `SELECT COUNT(*) FROM {view_id}`
|
||||
pub fn view_size(&self, view_id: &str) -> GenericSQLResult<String> {
|
||||
Ok(format!("SELECT COUNT(*) FROM {}", view_id))
|
||||
}
|
||||
|
||||
/// Returns the SQL query to get the min and max values of a column.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `view_id` - The identifier of the view.
|
||||
/// * `column_name` - The name of the column.
|
||||
/// * `config` - The view configuration.
|
||||
///
|
||||
/// # Returns
|
||||
/// SQL: `SELECT MIN("column_name"), MAX("column_name") FROM {view_id}`
|
||||
/// When the view uses ROLLUP grouping (non-flat mode with group_by),
|
||||
/// a `WHERE __GROUPING_ID__ = 0` clause is added to exclude non-leaf rows.
|
||||
pub fn view_get_min_max(
|
||||
&self,
|
||||
view_id: &str,
|
||||
column_name: &str,
|
||||
config: &ViewConfig,
|
||||
) -> GenericSQLResult<String> {
|
||||
let has_grouping_id =
|
||||
!config.group_by.is_empty() && config.group_rollup_mode != GroupRollupMode::Flat;
|
||||
let where_clause = if has_grouping_id {
|
||||
" WHERE __GROUPING_ID__ = 0"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
Ok(format!(
|
||||
"SELECT MIN(\"{}\"), MAX(\"{}\") FROM {}{}",
|
||||
column_name, column_name, view_id, where_clause
|
||||
))
|
||||
}
|
||||
|
||||
fn filter_term_to_sql(term: &FilterTerm) -> Option<String> {
|
||||
match term {
|
||||
FilterTerm::Scalar(scalar) => Self::scalar_to_sql(scalar),
|
||||
FilterTerm::Array(scalars) => {
|
||||
let values: Vec<String> = scalars.iter().filter_map(Self::scalar_to_sql).collect();
|
||||
if values.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(format!("({})", values.join(", ")))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn scalar_to_sql(scalar: &Scalar) -> Option<String> {
|
||||
match scalar {
|
||||
Scalar::Null => None,
|
||||
Scalar::Bool(b) => Some(if *b { "TRUE" } else { "FALSE" }.to_string()),
|
||||
Scalar::Float(f) => Some(f.to_string()),
|
||||
Scalar::String(s) => Some(format!("'{}'", s.replace('\'', "''"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,649 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use crate::config::{Aggregate, GroupRollupMode, Sort, SortDir, ViewConfig};
|
||||
|
||||
fn aggregate_to_string(agg: &Aggregate) -> String {
|
||||
match agg {
|
||||
Aggregate::SingleAggregate(name) => name.clone(),
|
||||
Aggregate::MultiAggregate(name, _args) => name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn sort_dir_to_string(dir: &SortDir) -> &'static str {
|
||||
match dir {
|
||||
SortDir::None => "",
|
||||
SortDir::Asc | SortDir::ColAsc | SortDir::AscAbs | SortDir::ColAscAbs => "ASC",
|
||||
SortDir::Desc | SortDir::ColDesc | SortDir::DescAbs | SortDir::ColDescAbs => "DESC",
|
||||
}
|
||||
}
|
||||
|
||||
fn is_col_sort(dir: &SortDir) -> bool {
|
||||
matches!(
|
||||
dir,
|
||||
SortDir::ColAsc | SortDir::ColDesc | SortDir::ColAscAbs | SortDir::ColDescAbs
|
||||
)
|
||||
}
|
||||
|
||||
enum QueryOrientation {
|
||||
/// Default
|
||||
Flat,
|
||||
|
||||
/// `group_by` set
|
||||
Grouped,
|
||||
|
||||
/// `split_by` set
|
||||
Pivoted,
|
||||
|
||||
/// `group_by` and `split_by` set
|
||||
GroupedAndPivoted,
|
||||
|
||||
/// `total` set
|
||||
Total,
|
||||
|
||||
/// `total` and `split_by`
|
||||
TotalPivoted,
|
||||
}
|
||||
|
||||
/// Precomputed context for building a SQL view query from a [`ViewConfig`].
|
||||
///
|
||||
/// Holds the resolved column names, grouping function, and row-path aliases
|
||||
/// needed to emit the correct `SELECT`, `GROUP BY`, `PIVOT`, `ORDER BY`, and
|
||||
/// `WINDOW` clauses for every combination of `group_by` / `split_by`.
|
||||
pub(crate) struct ViewQueryContext<'a> {
|
||||
table: &'a str,
|
||||
config: &'a ViewConfig,
|
||||
group_col_names: Vec<String>,
|
||||
grouping_fn: &'a str,
|
||||
row_path_aliases: Vec<String>,
|
||||
}
|
||||
|
||||
impl<'a> ViewQueryContext<'a> {
|
||||
/// Creates a new query context by resolving expressions, the grouping
|
||||
/// function, and row-path aliases from the given model and config.
|
||||
pub(crate) fn new(
|
||||
model: &'a super::GenericSQLVirtualServerModel,
|
||||
table: &'a str,
|
||||
config: &'a ViewConfig,
|
||||
) -> Self {
|
||||
let expressions = &config.expressions.0;
|
||||
let col_name_resolve = |col: &str| -> String {
|
||||
expressions
|
||||
.get(col)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("\"{}\"", col))
|
||||
};
|
||||
|
||||
let grouping_fn = model.0.grouping_fn.as_deref().unwrap_or("GROUPING_ID");
|
||||
let group_col_names: Vec<String> = config
|
||||
.group_by
|
||||
.iter()
|
||||
.map(|c| col_name_resolve(c))
|
||||
.collect();
|
||||
|
||||
let row_path_aliases: Vec<String> = (0..config.group_by.len())
|
||||
.map(|i| format!("__ROW_PATH_{}__", i))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
table,
|
||||
config,
|
||||
group_col_names,
|
||||
grouping_fn,
|
||||
row_path_aliases,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the inner `SELECT` query (without the outer `CREATE TABLE`
|
||||
/// wrapper) for the four `group_by` x `split_by` combinations, appending
|
||||
/// `WINDOW` and `ORDER BY` clauses as needed.
|
||||
pub(crate) fn build_query(&self) -> String {
|
||||
let where_sql = self.where_sql();
|
||||
let order_by = self.order_by_clauses();
|
||||
let windows = self.window_clauses();
|
||||
let mut query = match self.query_orientation() {
|
||||
QueryOrientation::Flat => {
|
||||
let select = self.select_clauses().join(", ");
|
||||
format!("SELECT {} FROM {}{}", select, self.table, where_sql)
|
||||
},
|
||||
QueryOrientation::Grouped => {
|
||||
let mut clauses = self.select_clauses();
|
||||
clauses.extend(self.row_path_select_clauses());
|
||||
if self.is_flat_mode() {
|
||||
format!(
|
||||
"SELECT {} FROM {}{} GROUP BY {}",
|
||||
clauses.join(", "),
|
||||
self.table,
|
||||
where_sql,
|
||||
self.group_col_names.join(", ")
|
||||
)
|
||||
} else {
|
||||
clauses.push(self.grouping_id_clause());
|
||||
format!(
|
||||
"SELECT {} FROM {}{} GROUP BY ROLLUP({})",
|
||||
clauses.join(", "),
|
||||
self.table,
|
||||
where_sql,
|
||||
self.group_col_names.join(", ")
|
||||
)
|
||||
}
|
||||
},
|
||||
QueryOrientation::Pivoted => {
|
||||
let select = self.select_clauses();
|
||||
let pivot_using: Vec<String> = self
|
||||
.config
|
||||
.columns
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|col| {
|
||||
let escaped = col.replace('"', "\"\"").replace('_', "-");
|
||||
format!("first(\"{}\") as \"{}\"", escaped, escaped)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let split_cols: String = self
|
||||
.config
|
||||
.split_by
|
||||
.iter()
|
||||
.map(|c| format!("\"{}\"", c))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let row_num_order = self.pivot_row_num_order();
|
||||
format!(
|
||||
"SELECT * EXCLUDE (__ROW_NUM__) FROM (PIVOT (SELECT {}, {}, ROW_NUMBER() OVER \
|
||||
(ORDER BY {}) as __ROW_NUM__ FROM {}{}) ON {} USING {} GROUP BY __ROW_NUM__)",
|
||||
select.join(", "),
|
||||
split_cols,
|
||||
row_num_order,
|
||||
self.table,
|
||||
where_sql,
|
||||
self.pivot_on_expr(),
|
||||
pivot_using.join(", "),
|
||||
)
|
||||
},
|
||||
QueryOrientation::GroupedAndPivoted => {
|
||||
let groups_joined = self.group_col_names.join(", ");
|
||||
let split_cols_joined = self.pivot_on_expr();
|
||||
let mut inner_clauses = self.select_clauses();
|
||||
inner_clauses.extend(self.row_path_select_clauses());
|
||||
if !self.is_flat_mode() {
|
||||
inner_clauses.push(self.grouping_id_clause());
|
||||
}
|
||||
for sb_col in &self.config.split_by {
|
||||
inner_clauses.push(self.col_name(sb_col));
|
||||
}
|
||||
|
||||
for (sidx, Sort(sort_col, sort_dir)) in self.config.sort.iter().enumerate() {
|
||||
if *sort_dir != SortDir::None && !is_col_sort(sort_dir) {
|
||||
let agg = self.get_aggregate(sort_col);
|
||||
if self.is_flat_mode() {
|
||||
inner_clauses.push(format!(
|
||||
"sum({}({})) OVER (PARTITION BY {}) AS __SORT_{}__",
|
||||
agg,
|
||||
self.col_name(sort_col),
|
||||
groups_joined,
|
||||
sidx,
|
||||
));
|
||||
} else {
|
||||
inner_clauses.push(format!(
|
||||
"sum({}({})) OVER (PARTITION BY {}({}), {}) AS __SORT_{}__",
|
||||
agg,
|
||||
self.col_name(sort_col),
|
||||
self.grouping_fn,
|
||||
groups_joined,
|
||||
groups_joined,
|
||||
sidx,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let inner_query = if self.is_flat_mode() {
|
||||
format!(
|
||||
"SELECT {} FROM {}{} GROUP BY {}, {}",
|
||||
inner_clauses.join(", "),
|
||||
self.table,
|
||||
where_sql,
|
||||
groups_joined,
|
||||
split_cols_joined,
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"SELECT {} FROM {}{} GROUP BY ROLLUP({}), {}",
|
||||
inner_clauses.join(", "),
|
||||
self.table,
|
||||
where_sql,
|
||||
groups_joined,
|
||||
split_cols_joined,
|
||||
)
|
||||
};
|
||||
|
||||
let pivot_using: String = self
|
||||
.config
|
||||
.columns
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|col| {
|
||||
let escaped = col.replace('"', "\"\"").replace('_', "-");
|
||||
format!("first(\"{}\") as \"{}\"", escaped, escaped)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let mut row_id_cols = self.row_path_aliases.clone();
|
||||
if !self.is_flat_mode() {
|
||||
row_id_cols.push("__GROUPING_ID__".to_string());
|
||||
}
|
||||
for (sidx, Sort(_, sort_dir)) in self.config.sort.iter().enumerate() {
|
||||
if *sort_dir != SortDir::None && !is_col_sort(sort_dir) {
|
||||
row_id_cols.push(format!("__SORT_{}__", sidx));
|
||||
}
|
||||
}
|
||||
|
||||
format!(
|
||||
"SELECT * FROM (PIVOT ({}) ON {} USING {} GROUP BY {})",
|
||||
inner_query,
|
||||
self.pivot_on_expr(),
|
||||
pivot_using,
|
||||
row_id_cols.join(", ")
|
||||
)
|
||||
},
|
||||
QueryOrientation::Total => {
|
||||
let select = self.select_clauses().join(", ");
|
||||
format!("SELECT {} FROM {}{}", select, self.table, where_sql)
|
||||
},
|
||||
QueryOrientation::TotalPivoted => {
|
||||
let raw_cols: Vec<String> = self
|
||||
.config
|
||||
.columns
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|col| self.col_name(col))
|
||||
.collect();
|
||||
|
||||
let split_cols: String = self
|
||||
.config
|
||||
.split_by
|
||||
.iter()
|
||||
.map(|c| format!("\"{}\"", c))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let pivot_using: Vec<String> = self
|
||||
.config
|
||||
.columns
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|col| {
|
||||
let agg = self.get_aggregate(col);
|
||||
let escaped = col.replace('"', "\"\"").replace('_', "-");
|
||||
format!("{}(\"{}\") as \"{}\"", agg, escaped, escaped)
|
||||
})
|
||||
.collect();
|
||||
|
||||
format!(
|
||||
"SELECT * FROM (PIVOT (SELECT {}, {} FROM {}{}) ON {} USING {})",
|
||||
raw_cols.join(", "),
|
||||
split_cols,
|
||||
self.table,
|
||||
where_sql,
|
||||
self.pivot_on_expr(),
|
||||
pivot_using.join(", "),
|
||||
)
|
||||
},
|
||||
};
|
||||
|
||||
if !windows.is_empty() {
|
||||
query = format!("{} WINDOW {}", query, windows.join(", "));
|
||||
}
|
||||
|
||||
if !order_by.is_empty() {
|
||||
query = format!("{} ORDER BY {}", query, order_by.join(", "));
|
||||
} else if self.is_flat_mode() && !self.config.group_by.is_empty() {
|
||||
let default_order: Vec<String> = self
|
||||
.row_path_aliases
|
||||
.iter()
|
||||
.map(|alias| format!("{} ASC", alias))
|
||||
.collect();
|
||||
query = format!("{} ORDER BY {}", query, default_order.join(", "));
|
||||
} else if self.config.group_by.is_empty()
|
||||
&& self.config.group_rollup_mode != GroupRollupMode::Total
|
||||
{
|
||||
let default_order = if self.config.split_by.is_empty() {
|
||||
"rowid"
|
||||
} else {
|
||||
"__ROW_NUM__"
|
||||
};
|
||||
|
||||
query = format!("{} ORDER BY {}", query, default_order);
|
||||
}
|
||||
|
||||
query
|
||||
}
|
||||
|
||||
fn is_flat_mode(&self) -> bool {
|
||||
self.config.group_rollup_mode == GroupRollupMode::Flat
|
||||
}
|
||||
|
||||
fn needs_aggregation(&self) -> bool {
|
||||
!self.config.group_by.is_empty() || self.config.group_rollup_mode == GroupRollupMode::Total
|
||||
}
|
||||
|
||||
fn query_orientation(&self) -> QueryOrientation {
|
||||
if self.config.group_rollup_mode == GroupRollupMode::Total {
|
||||
return if self.config.split_by.is_empty() {
|
||||
QueryOrientation::Total
|
||||
} else {
|
||||
QueryOrientation::TotalPivoted
|
||||
};
|
||||
}
|
||||
|
||||
match (
|
||||
self.config.group_by.is_empty(),
|
||||
self.config.split_by.is_empty(),
|
||||
) {
|
||||
(true, true) => QueryOrientation::Flat,
|
||||
(false, true) => QueryOrientation::Grouped,
|
||||
(true, false) => QueryOrientation::Pivoted,
|
||||
(false, false) => QueryOrientation::GroupedAndPivoted,
|
||||
}
|
||||
}
|
||||
|
||||
fn col_name(&self, col: &str) -> String {
|
||||
self.config
|
||||
.expressions
|
||||
.0
|
||||
.get(col)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("\"{}\"", col))
|
||||
}
|
||||
|
||||
fn get_aggregate(&self, col: &str) -> String {
|
||||
self.config
|
||||
.aggregates
|
||||
.get(col)
|
||||
.map(aggregate_to_string)
|
||||
.unwrap_or_else(|| "any_value".to_string())
|
||||
}
|
||||
|
||||
fn select_clauses(&self) -> Vec<String> {
|
||||
let mut clauses = Vec::new();
|
||||
if self.needs_aggregation() {
|
||||
for col in self.config.columns.iter().flatten() {
|
||||
let agg = self.get_aggregate(col);
|
||||
let escaped = col.replace('"', "\"\"").replace("_", "-");
|
||||
clauses.push(format!(
|
||||
"{}({}) as \"{}\"",
|
||||
agg,
|
||||
self.col_name(col),
|
||||
escaped
|
||||
));
|
||||
}
|
||||
} else if !self.config.columns.is_empty() {
|
||||
for col in self.config.columns.iter().flatten() {
|
||||
let escaped = col.replace('"', "\"\"").replace("_", "-");
|
||||
clauses.push(format!("{} as \"{}\"", self.col_name(col), escaped));
|
||||
}
|
||||
}
|
||||
|
||||
clauses
|
||||
}
|
||||
|
||||
fn where_sql(&self) -> String {
|
||||
let clauses: Vec<String> = self
|
||||
.config
|
||||
.filter
|
||||
.iter()
|
||||
.filter_map(|flt| {
|
||||
super::GenericSQLVirtualServerModel::filter_term_to_sql(flt.term()).map(
|
||||
|term_lit| format!("{} {} {}", self.col_name(flt.column()), flt.op(), term_lit),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if clauses.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" WHERE {}", clauses.join(" AND "))
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the `ORDER BY` expression for the `ROW_NUMBER()` window
|
||||
/// function used inside `PIVOT` queries. Uses sort config if available,
|
||||
/// otherwise falls back to `rowid`.
|
||||
fn pivot_row_num_order(&self) -> String {
|
||||
let sort_exprs: Vec<String> = self
|
||||
.config
|
||||
.sort
|
||||
.iter()
|
||||
.filter(|Sort(_, dir)| *dir != SortDir::None && !is_col_sort(dir))
|
||||
.map(|Sort(col, dir)| format!("{} {}", self.col_name(col), sort_dir_to_string(dir)))
|
||||
.collect();
|
||||
|
||||
if sort_exprs.is_empty() {
|
||||
"rowid".to_string()
|
||||
} else {
|
||||
sort_exprs.join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
fn pivot_on_expr(&self) -> String {
|
||||
self.config
|
||||
.split_by
|
||||
.iter()
|
||||
.map(|c| format!("\"{}\"", c))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
fn grouping_id_clause(&self) -> String {
|
||||
format!(
|
||||
"{}({}) AS __GROUPING_ID__",
|
||||
self.grouping_fn,
|
||||
self.group_col_names.join(", ")
|
||||
)
|
||||
}
|
||||
|
||||
fn row_path_select_clauses(&self) -> Vec<String> {
|
||||
self.config
|
||||
.group_by
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, col)| format!("{} as __ROW_PATH_{}__", self.col_name(col), i))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn order_by_clauses(&self) -> Vec<String> {
|
||||
let mut clauses = Vec::new();
|
||||
if !self.config.group_by.is_empty() && self.is_flat_mode() {
|
||||
let has_row_sort = self
|
||||
.config
|
||||
.sort
|
||||
.iter()
|
||||
.any(|Sort(_, dir)| *dir != SortDir::None && !is_col_sort(dir));
|
||||
if self.config.group_by.len() > 1 && has_row_sort {
|
||||
// Hierarchical flat sort — mirrors rollup logic but without GROUPING_ID
|
||||
for gidx in 0..self.config.group_by.len() {
|
||||
let is_leaf = gidx >= self.config.group_by.len() - 1;
|
||||
for (sidx, Sort(sort_col, sort_dir)) in self.config.sort.iter().enumerate() {
|
||||
if *sort_dir == SortDir::None || is_col_sort(sort_dir) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dir = sort_dir_to_string(sort_dir);
|
||||
if !self.config.split_by.is_empty() {
|
||||
if is_leaf {
|
||||
clauses.push(format!("__SORT_{}__ {}", sidx, dir));
|
||||
} else {
|
||||
clauses.push(format!(
|
||||
"first(__SORT_{}__) OVER __WINDOW_{}__ {}",
|
||||
sidx, gidx, dir
|
||||
));
|
||||
}
|
||||
} else {
|
||||
let agg = self.get_aggregate(sort_col);
|
||||
if is_leaf {
|
||||
clauses.push(format!(
|
||||
"{}({}) {}",
|
||||
agg,
|
||||
self.col_name(sort_col),
|
||||
dir
|
||||
));
|
||||
} else {
|
||||
clauses.push(format!(
|
||||
"first({}({})) OVER __WINDOW_{}__ {}",
|
||||
agg,
|
||||
self.col_name(sort_col),
|
||||
gidx,
|
||||
dir
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clauses.push(format!("{} ASC", self.row_path_aliases[gidx]));
|
||||
}
|
||||
} else {
|
||||
// Single group level — simple sort, no window needed
|
||||
for (sidx, Sort(sort_col, sort_dir)) in self.config.sort.iter().enumerate() {
|
||||
if *sort_dir != SortDir::None && !is_col_sort(sort_dir) {
|
||||
let dir = sort_dir_to_string(sort_dir);
|
||||
if !self.config.split_by.is_empty() {
|
||||
clauses.push(format!("__SORT_{}__ {}", sidx, dir));
|
||||
} else {
|
||||
let agg = self.get_aggregate(sort_col);
|
||||
clauses.push(format!("{}({}) {}", agg, self.col_name(sort_col), dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !self.config.group_by.is_empty() {
|
||||
for gidx in 0..self.config.group_by.len() {
|
||||
if !self.config.split_by.is_empty() {
|
||||
let shift = self.config.group_by.len() - 1 - gidx;
|
||||
if shift > 0 {
|
||||
clauses.push(format!("(__GROUPING_ID__ >> {}) DESC", shift));
|
||||
} else {
|
||||
clauses.push("__GROUPING_ID__ DESC".to_string());
|
||||
}
|
||||
} else {
|
||||
let groups_up_to = self.config.group_by[..=gidx]
|
||||
.iter()
|
||||
.map(|c| self.col_name(c))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
clauses.push(format!("{}({}) DESC", self.grouping_fn, groups_up_to));
|
||||
}
|
||||
|
||||
let is_leaf = gidx >= self.config.group_by.len() - 1;
|
||||
for (sidx, Sort(sort_col, sort_dir)) in self.config.sort.iter().enumerate() {
|
||||
if *sort_dir == SortDir::None || is_col_sort(sort_dir) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dir = sort_dir_to_string(sort_dir);
|
||||
if !self.config.split_by.is_empty() {
|
||||
if is_leaf {
|
||||
clauses.push(format!("__SORT_{}__ {}", sidx, dir));
|
||||
} else {
|
||||
clauses.push(format!(
|
||||
"first(__SORT_{}__) OVER __WINDOW_{}__ {}",
|
||||
sidx, gidx, dir
|
||||
));
|
||||
}
|
||||
} else {
|
||||
let agg = self.get_aggregate(sort_col);
|
||||
if is_leaf {
|
||||
clauses.push(format!("{}({}) {}", agg, self.col_name(sort_col), dir));
|
||||
} else {
|
||||
clauses.push(format!(
|
||||
"first({}({})) OVER __WINDOW_{}__ {}",
|
||||
agg,
|
||||
self.col_name(sort_col),
|
||||
gidx,
|
||||
dir
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clauses.push(format!("{} ASC", self.row_path_aliases[gidx]));
|
||||
}
|
||||
} else if self.config.split_by.is_empty() {
|
||||
for Sort(sort_col, sort_dir) in &self.config.sort {
|
||||
if *sort_dir != SortDir::None && !is_col_sort(sort_dir) {
|
||||
let dir = sort_dir_to_string(sort_dir);
|
||||
clauses.push(format!("{} {}", self.col_name(sort_col), dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clauses
|
||||
}
|
||||
|
||||
fn window_clauses(&self) -> Vec<String> {
|
||||
if self.config.sort.is_empty() || self.config.group_by.len() <= 1 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut clauses = Vec::new();
|
||||
for gidx in 0..(self.config.group_by.len() - 1) {
|
||||
let partition = self.row_path_aliases[..=gidx].join(", ");
|
||||
if self.is_flat_mode() {
|
||||
// Flat mode: partition by row path only (no GROUPING_ID)
|
||||
if !self.config.split_by.is_empty() {
|
||||
let order = self.row_path_aliases.join(", ");
|
||||
clauses.push(format!(
|
||||
"__WINDOW_{}__ AS (PARTITION BY {} ORDER BY {})",
|
||||
gidx, partition, order,
|
||||
));
|
||||
} else {
|
||||
clauses.push(format!(
|
||||
"__WINDOW_{}__ AS (PARTITION BY {} ORDER BY {})",
|
||||
gidx,
|
||||
partition,
|
||||
self.group_col_names.join(", ")
|
||||
));
|
||||
}
|
||||
} else if !self.config.split_by.is_empty() {
|
||||
let shift = self.config.group_by.len() - 1 - gidx;
|
||||
let grouping_expr = if shift > 0 {
|
||||
format!("(__GROUPING_ID__ >> {})", shift)
|
||||
} else {
|
||||
"__GROUPING_ID__".to_string()
|
||||
};
|
||||
|
||||
let order = self.row_path_aliases.join(", ");
|
||||
clauses.push(format!(
|
||||
"__WINDOW_{}__ AS (PARTITION BY {}, {} ORDER BY {})",
|
||||
gidx, grouping_expr, partition, order,
|
||||
));
|
||||
} else {
|
||||
let sub_groups = self.config.group_by[..=gidx]
|
||||
.iter()
|
||||
.map(|c| self.col_name(c))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
clauses.push(format!(
|
||||
"__WINDOW_{}__ AS (PARTITION BY {}({}), {} ORDER BY {})",
|
||||
gidx,
|
||||
self.grouping_fn,
|
||||
sub_groups,
|
||||
partition,
|
||||
self.group_col_names.join(", ")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
clauses
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,620 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::*;
|
||||
use crate::config::{Aggregate, GroupRollupMode};
|
||||
|
||||
#[test]
|
||||
fn test_get_hosted_tables() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
assert_eq!(builder.get_hosted_tables().unwrap(), "SHOW ALL TABLES");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_schema() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
assert_eq!(
|
||||
builder.table_schema("my_table").unwrap(),
|
||||
"DESCRIBE my_table"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_size() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
assert_eq!(
|
||||
builder.table_size("my_table").unwrap(),
|
||||
"SELECT COUNT(*) FROM my_table"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_view_delete() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
assert_eq!(
|
||||
builder.view_delete("my_view").unwrap(),
|
||||
"DROP TABLE IF EXISTS my_view"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_make_view_simple() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.columns = vec![Some("col1".to_string()), Some("col2".to_string())];
|
||||
let sql = builder
|
||||
.table_make_view("source_table", "dest_view", &config)
|
||||
.unwrap();
|
||||
|
||||
assert!(sql.starts_with("CREATE TABLE dest_view AS"));
|
||||
assert!(sql.contains("\"col1\""));
|
||||
assert!(sql.contains("\"col2\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_make_view_with_group_by() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.columns = vec![Some("value".to_string())];
|
||||
config.group_by = vec!["category".to_string()];
|
||||
let sql = builder
|
||||
.table_make_view("source_table", "dest_view", &config)
|
||||
.unwrap();
|
||||
|
||||
assert!(sql.contains("GROUP BY ROLLUP"));
|
||||
assert!(sql.contains("__ROW_PATH_0__"));
|
||||
assert!(sql.contains("__GROUPING_ID__"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_make_view_with_group_by_and_split_by() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.columns = vec![Some("value".to_string())];
|
||||
config.group_by = vec!["category".to_string()];
|
||||
config.split_by = vec!["quarter".to_string()];
|
||||
let sql = builder
|
||||
.table_make_view("source_table", "dest_view", &config)
|
||||
.unwrap();
|
||||
|
||||
assert!(sql.contains("GROUP BY ROLLUP"), "expected ROLLUP: {}", sql);
|
||||
assert!(sql.contains("PIVOT"), "expected PIVOT: {}", sql);
|
||||
assert!(
|
||||
sql.contains("__ROW_PATH_0__"),
|
||||
"expected __ROW_PATH_0__: {}",
|
||||
sql
|
||||
);
|
||||
|
||||
assert!(
|
||||
sql.contains("__GROUPING_ID__"),
|
||||
"expected __GROUPING_ID__: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_make_view_with_sort_group_by_and_split_by() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.columns = vec![Some("value".to_string())];
|
||||
config.group_by = vec!["category".to_string()];
|
||||
config.split_by = vec!["quarter".to_string()];
|
||||
config.sort = vec![Sort("value".to_string(), SortDir::Asc)];
|
||||
config.aggregates = HashMap::from([(
|
||||
"value".to_string(),
|
||||
Aggregate::SingleAggregate("sum".to_string()),
|
||||
)]);
|
||||
|
||||
let sql = builder
|
||||
.table_make_view("source_table", "dest_view", &config)
|
||||
.unwrap();
|
||||
|
||||
assert!(sql.contains("__SORT_0__"), "expected __SORT_0__: {}", sql);
|
||||
assert!(
|
||||
sql.contains("__GROUPING_ID__, __SORT_0__"),
|
||||
"expected __SORT_0__ in GROUP BY: {}",
|
||||
sql
|
||||
);
|
||||
|
||||
assert!(
|
||||
sql.contains("__SORT_0__ ASC"),
|
||||
"expected __SORT_0__ ASC in ORDER BY: {}",
|
||||
sql
|
||||
);
|
||||
|
||||
assert!(
|
||||
!sql.contains("sum(\"value\") ASC"),
|
||||
"should not have raw aggregate in ORDER BY: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_make_view_with_sort_multi_group_by_and_split_by() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.columns = vec![Some("value".to_string())];
|
||||
config.group_by = vec!["region".to_string(), "category".to_string()];
|
||||
config.split_by = vec!["quarter".to_string()];
|
||||
config.sort = vec![Sort("value".to_string(), SortDir::Asc)];
|
||||
config.aggregates = HashMap::from([(
|
||||
"value".to_string(),
|
||||
Aggregate::SingleAggregate("sum".to_string()),
|
||||
)]);
|
||||
|
||||
let sql = builder
|
||||
.table_make_view("source_table", "dest_view", &config)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
sql.contains("PARTITION BY (__GROUPING_ID__ >> 1)"),
|
||||
"expected shifted __GROUPING_ID__ in WINDOW: {}",
|
||||
sql
|
||||
);
|
||||
|
||||
assert!(
|
||||
sql.contains("first(__SORT_0__) OVER __WINDOW_0__"),
|
||||
"expected first(__SORT_0__) OVER __WINDOW_0__: {}",
|
||||
sql
|
||||
);
|
||||
|
||||
assert!(
|
||||
!sql.contains("GROUPING_ID(\"region\")"),
|
||||
"should not have GROUPING_ID function in WINDOW: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_make_view_with_sort_and_group_by_no_split_by() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.columns = vec![Some("value".to_string())];
|
||||
config.group_by = vec!["category".to_string()];
|
||||
config.sort = vec![Sort("value".to_string(), SortDir::Asc)];
|
||||
config.aggregates = HashMap::from([(
|
||||
"value".to_string(),
|
||||
Aggregate::SingleAggregate("sum".to_string()),
|
||||
)]);
|
||||
|
||||
let sql = builder
|
||||
.table_make_view("source_table", "dest_view", &config)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
sql.contains("sum(\"value\") ASC"),
|
||||
"expected raw aggregate in ORDER BY: {}",
|
||||
sql
|
||||
);
|
||||
|
||||
assert!(
|
||||
!sql.contains("__SORT_0__"),
|
||||
"should not have __SORT_0__ without split_by: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_make_view_col_sort_excludes_row_order_by() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.columns = vec![Some("value".to_string())];
|
||||
config.group_by = vec!["category".to_string()];
|
||||
config.split_by = vec!["quarter".to_string()];
|
||||
config.sort = vec![Sort("value".to_string(), SortDir::ColAsc)];
|
||||
config.aggregates = HashMap::from([(
|
||||
"value".to_string(),
|
||||
Aggregate::SingleAggregate("sum".to_string()),
|
||||
)]);
|
||||
|
||||
let sql = builder
|
||||
.table_make_view("source_table", "dest_view", &config)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
!sql.contains("__SORT_0__"),
|
||||
"col sort should not produce __SORT_0__: {}",
|
||||
sql
|
||||
);
|
||||
|
||||
assert!(
|
||||
!sql.contains("sum(\"value\") ASC"),
|
||||
"col sort should not produce row ORDER BY: {}",
|
||||
sql
|
||||
);
|
||||
|
||||
assert!(sql.contains("PIVOT"), "should still have PIVOT: {}", sql);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_make_view_mixed_row_and_col_sort() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.columns = vec![Some("value".to_string()), Some("qty".to_string())];
|
||||
config.group_by = vec!["category".to_string()];
|
||||
config.split_by = vec!["quarter".to_string()];
|
||||
config.sort = vec![
|
||||
Sort("value".to_string(), SortDir::ColDesc),
|
||||
Sort("qty".to_string(), SortDir::Asc),
|
||||
];
|
||||
|
||||
config.aggregates = HashMap::from([
|
||||
(
|
||||
"value".to_string(),
|
||||
Aggregate::SingleAggregate("sum".to_string()),
|
||||
),
|
||||
(
|
||||
"qty".to_string(),
|
||||
Aggregate::SingleAggregate("sum".to_string()),
|
||||
),
|
||||
]);
|
||||
|
||||
let sql = builder
|
||||
.table_make_view("source_table", "dest_view", &config)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
!sql.contains("__SORT_0__"),
|
||||
"col sort (idx 0) should not produce __SORT_0__: {}",
|
||||
sql
|
||||
);
|
||||
|
||||
assert!(
|
||||
sql.contains("__SORT_1__"),
|
||||
"row sort (idx 1) should produce __SORT_1__: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_make_view_pivoted_with_sort() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.columns = vec![Some("value".to_string())];
|
||||
config.split_by = vec!["quarter".to_string()];
|
||||
config.sort = vec![Sort("value".to_string(), SortDir::Desc)];
|
||||
let sql = builder
|
||||
.table_make_view("source_table", "dest_view", &config)
|
||||
.unwrap();
|
||||
|
||||
assert!(sql.contains("PIVOT"), "expected PIVOT: {}", sql);
|
||||
assert!(
|
||||
sql.contains("ROW_NUMBER() OVER (ORDER BY \"value\" DESC)"),
|
||||
"expected sort in ROW_NUMBER window: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
sql.ends_with("ORDER BY __ROW_NUM__)"),
|
||||
"should end with ORDER BY __ROW_NUM__: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_view_get_data_col_sort_ascending() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.sort = vec![Sort("value".to_string(), SortDir::ColAsc)];
|
||||
let viewport = ViewPort {
|
||||
start_row: Some(0),
|
||||
end_row: Some(100),
|
||||
start_col: Some(0),
|
||||
end_col: None,
|
||||
..ViewPort::default()
|
||||
};
|
||||
|
||||
let mut schema = IndexMap::new();
|
||||
schema.insert("C_value".to_string(), ColumnType::Float);
|
||||
schema.insert("A_value".to_string(), ColumnType::Float);
|
||||
schema.insert("B_value".to_string(), ColumnType::Float);
|
||||
let sql = builder
|
||||
.view_get_data("my_view", &config, &viewport, &schema)
|
||||
.unwrap();
|
||||
|
||||
let a_pos = sql.find("\"A_value\"").unwrap();
|
||||
let b_pos = sql.find("\"B_value\"").unwrap();
|
||||
let c_pos = sql.find("\"C_value\"").unwrap();
|
||||
assert!(
|
||||
a_pos < b_pos && b_pos < c_pos,
|
||||
"col asc should order columns A < B < C: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_view_get_data_col_sort_descending() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.sort = vec![Sort("value".to_string(), SortDir::ColDesc)];
|
||||
let viewport = ViewPort {
|
||||
start_row: Some(0),
|
||||
end_row: Some(100),
|
||||
start_col: Some(0),
|
||||
end_col: None,
|
||||
..ViewPort::default()
|
||||
};
|
||||
|
||||
let mut schema = IndexMap::new();
|
||||
schema.insert("A_value".to_string(), ColumnType::Float);
|
||||
schema.insert("C_value".to_string(), ColumnType::Float);
|
||||
schema.insert("B_value".to_string(), ColumnType::Float);
|
||||
let sql = builder
|
||||
.view_get_data("my_view", &config, &viewport, &schema)
|
||||
.unwrap();
|
||||
|
||||
let a_pos = sql.find("\"A_value\"").unwrap();
|
||||
let b_pos = sql.find("\"B_value\"").unwrap();
|
||||
let c_pos = sql.find("\"C_value\"").unwrap();
|
||||
assert!(
|
||||
c_pos < b_pos && b_pos < a_pos,
|
||||
"col desc should order columns C > B > A: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_view_get_data() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let config = ViewConfig::default();
|
||||
let viewport = ViewPort {
|
||||
start_row: Some(0),
|
||||
end_row: Some(100),
|
||||
start_col: Some(0),
|
||||
end_col: Some(5),
|
||||
..ViewPort::default()
|
||||
};
|
||||
|
||||
let mut schema = IndexMap::new();
|
||||
schema.insert("col1".to_string(), ColumnType::String);
|
||||
schema.insert("col2".to_string(), ColumnType::Integer);
|
||||
let sql = builder
|
||||
.view_get_data("my_view", &config, &viewport, &schema)
|
||||
.unwrap();
|
||||
|
||||
assert!(sql.contains("SELECT"));
|
||||
assert!(sql.contains("FROM my_view"));
|
||||
assert!(sql.contains("LIMIT 100 OFFSET 0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_make_view_flat_group_by() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.columns = vec![Some("value".to_string())];
|
||||
config.group_by = vec!["category".to_string()];
|
||||
config.group_rollup_mode = GroupRollupMode::Flat;
|
||||
let sql = builder
|
||||
.table_make_view("source_table", "dest_view", &config)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
sql.contains("GROUP BY \"category\""),
|
||||
"expected plain GROUP BY: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
!sql.contains("ROLLUP"),
|
||||
"should not contain ROLLUP: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
!sql.contains("__GROUPING_ID__"),
|
||||
"should not contain __GROUPING_ID__: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
sql.contains("__ROW_PATH_0__"),
|
||||
"should contain __ROW_PATH_0__: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_make_view_flat_group_by_with_split_by() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.columns = vec![Some("value".to_string())];
|
||||
config.group_by = vec!["category".to_string()];
|
||||
config.split_by = vec!["quarter".to_string()];
|
||||
config.group_rollup_mode = GroupRollupMode::Flat;
|
||||
let sql = builder
|
||||
.table_make_view("source_table", "dest_view", &config)
|
||||
.unwrap();
|
||||
|
||||
assert!(sql.contains("PIVOT"), "expected PIVOT: {}", sql);
|
||||
assert!(
|
||||
!sql.contains("ROLLUP"),
|
||||
"should not contain ROLLUP: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
!sql.contains("__GROUPING_ID__"),
|
||||
"should not contain __GROUPING_ID__: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
sql.contains("__ROW_PATH_0__"),
|
||||
"should contain __ROW_PATH_0__: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_make_view_flat_group_by_with_sort() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.columns = vec![Some("value".to_string())];
|
||||
config.group_by = vec!["category".to_string()];
|
||||
config.sort = vec![Sort("value".to_string(), SortDir::Asc)];
|
||||
config.aggregates = HashMap::from([(
|
||||
"value".to_string(),
|
||||
Aggregate::SingleAggregate("sum".to_string()),
|
||||
)]);
|
||||
config.group_rollup_mode = GroupRollupMode::Flat;
|
||||
let sql = builder
|
||||
.table_make_view("source_table", "dest_view", &config)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
sql.contains("sum(\"value\") ASC"),
|
||||
"expected direct aggregate in ORDER BY: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
!sql.contains("ROLLUP"),
|
||||
"should not contain ROLLUP: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
!sql.contains("__WINDOW_"),
|
||||
"should not contain WINDOW clauses: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_make_view_flat_group_by_with_split_by_and_sort() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.columns = vec![Some("value".to_string())];
|
||||
config.group_by = vec!["category".to_string()];
|
||||
config.split_by = vec!["quarter".to_string()];
|
||||
config.sort = vec![Sort("value".to_string(), SortDir::Desc)];
|
||||
config.aggregates = HashMap::from([(
|
||||
"value".to_string(),
|
||||
Aggregate::SingleAggregate("sum".to_string()),
|
||||
)]);
|
||||
config.group_rollup_mode = GroupRollupMode::Flat;
|
||||
let sql = builder
|
||||
.table_make_view("source_table", "dest_view", &config)
|
||||
.unwrap();
|
||||
|
||||
assert!(sql.contains("PIVOT"), "expected PIVOT: {}", sql);
|
||||
assert!(
|
||||
!sql.contains("ROLLUP"),
|
||||
"should not contain ROLLUP: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
!sql.contains("__GROUPING_ID__"),
|
||||
"should not contain __GROUPING_ID__: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
sql.contains("__SORT_0__"),
|
||||
"expected __SORT_0__ for flat+pivoted+sort: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
sql.contains("__SORT_0__ DESC"),
|
||||
"expected __SORT_0__ DESC in ORDER BY: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
!sql.contains("sum(\"value\") DESC"),
|
||||
"should not have raw aggregate in ORDER BY: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_view_get_data_flat_no_grouping_id() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.group_by = vec!["category".to_string()];
|
||||
config.group_rollup_mode = GroupRollupMode::Flat;
|
||||
let viewport = ViewPort {
|
||||
start_row: Some(0),
|
||||
end_row: Some(100),
|
||||
start_col: Some(0),
|
||||
end_col: None,
|
||||
..ViewPort::default()
|
||||
};
|
||||
|
||||
let mut schema = IndexMap::new();
|
||||
schema.insert("value".to_string(), ColumnType::Float);
|
||||
let sql = builder
|
||||
.view_get_data("my_view", &config, &viewport, &schema)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
!sql.contains("__GROUPING_ID__"),
|
||||
"flat mode should not select __GROUPING_ID__: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
sql.contains("__ROW_PATH_0__"),
|
||||
"flat mode should still select __ROW_PATH_0__: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_make_view_total() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.columns = vec![Some("value".to_string())];
|
||||
config.group_rollup_mode = GroupRollupMode::Total;
|
||||
config.aggregates = HashMap::from([(
|
||||
"value".to_string(),
|
||||
Aggregate::SingleAggregate("sum".to_string()),
|
||||
)]);
|
||||
let sql = builder
|
||||
.table_make_view("source_table", "dest_view", &config)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
sql.contains("sum(\"value\")"),
|
||||
"expected aggregate function: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
!sql.contains("GROUP BY"),
|
||||
"should not contain GROUP BY: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
!sql.contains("ORDER BY"),
|
||||
"should not contain ORDER BY: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_make_view_total_with_split_by() {
|
||||
let builder = GenericSQLVirtualServerModel::new(GenericSQLVirtualServerModelArgs::default());
|
||||
let mut config = ViewConfig::default();
|
||||
config.columns = vec![Some("value".to_string())];
|
||||
config.split_by = vec!["quarter".to_string()];
|
||||
config.group_rollup_mode = GroupRollupMode::Total;
|
||||
config.aggregates = HashMap::from([(
|
||||
"value".to_string(),
|
||||
Aggregate::SingleAggregate("sum".to_string()),
|
||||
)]);
|
||||
let sql = builder
|
||||
.table_make_view("source_table", "dest_view", &config)
|
||||
.unwrap();
|
||||
|
||||
assert!(sql.contains("PIVOT"), "expected PIVOT: {}", sql);
|
||||
assert!(
|
||||
!sql.contains("GROUP BY"),
|
||||
"should not contain GROUP BY: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
!sql.contains("ROW_NUMBER"),
|
||||
"should not contain ROW_NUMBER: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use super::data::VirtualDataSlice;
|
||||
use super::features::Features;
|
||||
use crate::config::{ViewConfig, ViewConfigUpdate};
|
||||
use crate::proto::{ColumnType, HostedTable, TableMakePortReq, ViewPort};
|
||||
|
||||
#[cfg(feature = "sendable")]
|
||||
pub type VirtualServerFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
|
||||
|
||||
/// A boxed future that conditionally implements `Send` based on the target
|
||||
/// architecture.
|
||||
///
|
||||
/// This only compiles on wasm, except for `rust-analyzer` and `metadata`
|
||||
/// generation, so this type exists to tryck the compiler
|
||||
#[cfg(not(feature = "sendable"))]
|
||||
pub type VirtualServerFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||||
|
||||
/// Handler trait for implementing virtual server backends.
|
||||
///
|
||||
/// This trait defines the interface that must be implemented to provide
|
||||
/// a custom data source for the Perspective virtual server. Implementors
|
||||
/// handle table and view operations, translating them to their underlying
|
||||
/// data store.
|
||||
pub trait VirtualServerHandler {
|
||||
// Required
|
||||
|
||||
/// The error type returned by handler methods.
|
||||
#[cfg(not(feature = "sendable"))]
|
||||
type Error: std::error::Error + Send + Sync + 'static;
|
||||
|
||||
#[cfg(feature = "sendable")]
|
||||
type Error: std::error::Error + 'static;
|
||||
|
||||
/// Returns a list of all tables hosted by this handler.
|
||||
fn get_hosted_tables(&self) -> VirtualServerFuture<'_, Result<Vec<HostedTable>, Self::Error>>;
|
||||
|
||||
/// Returns the schema (column names and types) for a table.
|
||||
fn table_schema(
|
||||
&self,
|
||||
table_id: &str,
|
||||
) -> VirtualServerFuture<'_, Result<IndexMap<String, ColumnType>, Self::Error>>;
|
||||
|
||||
/// Returns the number of rows in a table.
|
||||
fn table_size(&self, table_id: &str) -> VirtualServerFuture<'_, Result<u32, Self::Error>>;
|
||||
|
||||
/// Creates a new view on a table with the given configuration.
|
||||
///
|
||||
/// The handler may modify the configuration to reflect any adjustments
|
||||
/// made during view creation.
|
||||
fn table_make_view(
|
||||
&mut self,
|
||||
view_id: &str,
|
||||
view_id: &str,
|
||||
config: &mut ViewConfigUpdate,
|
||||
) -> VirtualServerFuture<'_, Result<String, Self::Error>>;
|
||||
|
||||
/// Deletes a view and releases its resources.
|
||||
fn view_delete(&self, view_id: &str) -> VirtualServerFuture<'_, Result<(), Self::Error>>;
|
||||
|
||||
/// Retrieves data from a view within the specified viewport.
|
||||
fn view_get_data(
|
||||
&self,
|
||||
view_id: &str,
|
||||
config: &ViewConfig,
|
||||
schema: &IndexMap<String, ColumnType>,
|
||||
viewport: &ViewPort,
|
||||
) -> VirtualServerFuture<'_, Result<VirtualDataSlice, Self::Error>>;
|
||||
|
||||
// Optional
|
||||
|
||||
/// Return the column count of a `Table`
|
||||
fn table_column_size(
|
||||
&self,
|
||||
table_id: &str,
|
||||
) -> VirtualServerFuture<'_, Result<u32, Self::Error>> {
|
||||
let fut = self.table_schema(table_id);
|
||||
Box::pin(async move { Ok(fut.await?.len() as u32) })
|
||||
}
|
||||
|
||||
/// Returns the number of rows in a `View`.
|
||||
fn view_size(&self, view_id: &str) -> VirtualServerFuture<'_, Result<u32, Self::Error>> {
|
||||
Box::pin(self.table_size(view_id))
|
||||
}
|
||||
|
||||
/// Return the column count of a `View`
|
||||
fn view_column_size(
|
||||
&self,
|
||||
view_id: &str,
|
||||
config: &ViewConfig,
|
||||
) -> VirtualServerFuture<'_, Result<u32, Self::Error>> {
|
||||
let fut = self.view_schema(view_id, config);
|
||||
Box::pin(async move { Ok(fut.await?.len() as u32) })
|
||||
}
|
||||
|
||||
/// Returns the schema of a view after applying its configuration.
|
||||
fn view_schema(
|
||||
&self,
|
||||
view_id: &str,
|
||||
_config: &ViewConfig,
|
||||
) -> VirtualServerFuture<'_, Result<IndexMap<String, ColumnType>, Self::Error>> {
|
||||
Box::pin(self.table_schema(view_id))
|
||||
}
|
||||
|
||||
/// Validates an expression against a table and returns its result type.
|
||||
///
|
||||
/// Default implementation returns `Float` for all expressions.
|
||||
fn table_validate_expression(
|
||||
&self,
|
||||
_table_id: &str,
|
||||
_expression: &str,
|
||||
) -> VirtualServerFuture<'_, Result<ColumnType, Self::Error>> {
|
||||
Box::pin(async { Ok(ColumnType::Float) })
|
||||
}
|
||||
|
||||
/// Returns the features supported by this handler.
|
||||
///
|
||||
/// Default implementation returns default features.
|
||||
fn get_features(&self) -> VirtualServerFuture<'_, Result<Features<'_>, Self::Error>> {
|
||||
Box::pin(async { Ok(Features::default()) })
|
||||
}
|
||||
|
||||
/// Creates a new input port on a table.
|
||||
///
|
||||
/// Default implementation returns port ID 0.
|
||||
fn table_make_port(
|
||||
&self,
|
||||
_req: &TableMakePortReq,
|
||||
) -> VirtualServerFuture<'_, Result<u32, Self::Error>> {
|
||||
Box::pin(async { Ok(0) })
|
||||
}
|
||||
|
||||
/// Returns the min and max values of a column in a view.
|
||||
///
|
||||
/// Default implementation panics with "not implemented".
|
||||
fn view_get_min_max(
|
||||
&self,
|
||||
_view_id: &str,
|
||||
_column_name: &str,
|
||||
_config: &crate::config::ViewConfig,
|
||||
) -> VirtualServerFuture<'_, Result<(crate::config::Scalar, crate::config::Scalar), Self::Error>>
|
||||
{
|
||||
Box::pin(async { unimplemented!("view_get_min_max not implemented") })
|
||||
}
|
||||
|
||||
// Unused
|
||||
|
||||
/// Creates a new table with the given data.
|
||||
///
|
||||
/// Default implementation panics with "not implemented".
|
||||
fn make_table(
|
||||
&mut self,
|
||||
_table_id: &str,
|
||||
_data: &crate::proto::MakeTableData,
|
||||
) -> VirtualServerFuture<'_, Result<(), Self::Error>> {
|
||||
Box::pin(async { unimplemented!("make_table not implemented") })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
//! Virtual server implementation for Perspective.
|
||||
//!
|
||||
//! This module provides a virtual server that can process Perspective protocol
|
||||
//! messages and delegate operations to a custom backend handler.
|
||||
|
||||
mod data;
|
||||
mod error;
|
||||
mod features;
|
||||
mod generic_sql_model;
|
||||
mod handler;
|
||||
mod server;
|
||||
|
||||
pub use data::{RowPathStyle, SetVirtualDataColumn, VirtualDataCell, VirtualDataSlice};
|
||||
pub use error::{ResultExt, VirtualServerError};
|
||||
pub use features::{AggSpec, Features};
|
||||
pub use generic_sql_model::{
|
||||
GenericSQLError, GenericSQLResult, GenericSQLVirtualServerModel,
|
||||
GenericSQLVirtualServerModelArgs,
|
||||
};
|
||||
pub use handler::{VirtualServerFuture, VirtualServerHandler};
|
||||
pub use server::VirtualServer;
|
||||
@@ -0,0 +1,451 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use prost::Message as ProstMessage;
|
||||
use prost::bytes::{Bytes, BytesMut};
|
||||
|
||||
use super::data::RowPathStyle;
|
||||
use super::error::VirtualServerError;
|
||||
use super::handler::VirtualServerHandler;
|
||||
use crate::config::{ViewConfig, ViewConfigUpdate};
|
||||
use crate::proto::response::ClientResp;
|
||||
use crate::proto::table_validate_expr_resp::ExprValidationError;
|
||||
use crate::proto::{
|
||||
ColumnType, GetFeaturesResp, GetHostedTablesResp, MakeTableResp, Request, Response,
|
||||
ServerError, TableMakePortResp, TableMakeViewResp, TableOnDeleteResp, TableRemoveDeleteResp,
|
||||
TableSchemaResp, TableSizeResp, TableValidateExprResp, ViewColumnPathsResp, ViewDeleteResp,
|
||||
ViewDimensionsResp, ViewExpressionSchemaResp, ViewGetConfigResp, ViewGetMinMaxResp,
|
||||
ViewOnDeleteResp, ViewOnUpdateResp, ViewRemoveDeleteResp, ViewRemoveOnUpdateResp,
|
||||
ViewSchemaResp, ViewToArrowResp, ViewToColumnsStringResp, ViewToCsvResp,
|
||||
ViewToNdjsonStringResp, ViewToRowsStringResp,
|
||||
};
|
||||
|
||||
macro_rules! respond {
|
||||
($msg:ident, $name:ident { $($rest:tt)* }) => {{
|
||||
let mut resp = BytesMut::new();
|
||||
let resp2 = ClientResp::$name($name {
|
||||
$($rest)*
|
||||
});
|
||||
|
||||
Response {
|
||||
msg_id: $msg.msg_id,
|
||||
entity_id: $msg.entity_id,
|
||||
client_resp: Some(resp2),
|
||||
}.encode(&mut resp).map_err(VirtualServerError::EncodeError)?;
|
||||
|
||||
resp.freeze()
|
||||
}};
|
||||
}
|
||||
|
||||
/// A virtual server that processes Perspective protocol messages.
|
||||
///
|
||||
/// `VirtualServer` acts as a bridge between the Perspective protocol and a
|
||||
/// custom data backend. It handles protocol decoding/encoding and delegates
|
||||
/// actual data operations to the provided [`VirtualServerHandler`].
|
||||
pub struct VirtualServer<T: VirtualServerHandler> {
|
||||
handler: T,
|
||||
view_to_table: IndexMap<String, String>,
|
||||
view_configs: IndexMap<String, ViewConfig>,
|
||||
view_schemas: IndexMap<String, IndexMap<String, ColumnType>>,
|
||||
}
|
||||
|
||||
impl<T: VirtualServerHandler> VirtualServer<T> {
|
||||
/// Creates a new virtual server with the given handler.
|
||||
pub fn new(handler: T) -> Self {
|
||||
Self {
|
||||
handler,
|
||||
view_configs: IndexMap::default(),
|
||||
view_to_table: IndexMap::default(),
|
||||
view_schemas: IndexMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes a Perspective protocol request and returns the response.
|
||||
///
|
||||
/// Decodes the incoming protobuf message, dispatches to the appropriate
|
||||
/// handler method, and encodes the response.
|
||||
pub async fn handle_request(
|
||||
&mut self,
|
||||
bytes: Bytes,
|
||||
) -> Result<Bytes, VirtualServerError<T::Error>> {
|
||||
let msg = Request::decode(bytes).map_err(VirtualServerError::DecodeError)?;
|
||||
tracing::debug!(
|
||||
"Handling request: entity_id={}, req={:?}",
|
||||
msg.entity_id,
|
||||
msg.client_req
|
||||
);
|
||||
|
||||
match self.internal_handle_request(msg.clone()).await {
|
||||
Ok(resp) => Ok(resp),
|
||||
Err(err) => {
|
||||
tracing::error!("{}", err);
|
||||
Ok(respond!(msg, ServerError {
|
||||
message: err.to_string(),
|
||||
status_code: 0
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_view_schema(
|
||||
&mut self,
|
||||
entity_id: &str,
|
||||
to_psp_format: bool,
|
||||
) -> Result<IndexMap<String, ColumnType>, VirtualServerError<T::Error>> {
|
||||
if !self.view_schemas.contains_key(entity_id) {
|
||||
self.view_schemas.insert(
|
||||
entity_id.to_string(),
|
||||
self.handler
|
||||
.view_schema(entity_id, self.view_configs.get(entity_id).unwrap())
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
if to_psp_format {
|
||||
Ok(self
|
||||
.view_schemas
|
||||
.get(entity_id)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.split("_").collect::<Vec<_>>().last().unwrap().to_string(),
|
||||
*v,
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
} else {
|
||||
Ok(self.view_schemas.get(entity_id).cloned().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
async fn internal_handle_request(
|
||||
&mut self,
|
||||
msg: Request,
|
||||
) -> Result<Bytes, VirtualServerError<T::Error>> {
|
||||
use crate::proto::request::ClientReq::*;
|
||||
let resp = match msg.client_req.unwrap() {
|
||||
GetFeaturesReq(_) => {
|
||||
let features = self.handler.get_features().await?;
|
||||
respond!(msg, GetFeaturesResp { ..features.into() })
|
||||
},
|
||||
GetHostedTablesReq(_) => {
|
||||
respond!(msg, GetHostedTablesResp {
|
||||
table_infos: self.handler.get_hosted_tables().await?
|
||||
})
|
||||
},
|
||||
TableSchemaReq(_) => {
|
||||
respond!(msg, TableSchemaResp {
|
||||
schema: Some(crate::proto::Schema {
|
||||
schema: self
|
||||
.handler
|
||||
.table_schema(msg.entity_id.as_str())
|
||||
.await?
|
||||
.iter()
|
||||
.map(|x| crate::proto::schema::KeyTypePair {
|
||||
name: x.0.to_string(),
|
||||
r#type: *x.1 as i32,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
})
|
||||
},
|
||||
TableMakePortReq(req) => {
|
||||
respond!(msg, TableMakePortResp {
|
||||
port_id: self.handler.table_make_port(&req).await?
|
||||
})
|
||||
},
|
||||
TableMakeViewReq(req) => {
|
||||
self.view_to_table
|
||||
.insert(req.view_id.clone(), msg.entity_id.clone());
|
||||
|
||||
let mut config: ViewConfigUpdate = req.config.clone().unwrap_or_default().into();
|
||||
let bytes = respond!(msg, TableMakeViewResp {
|
||||
view_id: self
|
||||
.handler
|
||||
.table_make_view(msg.entity_id.as_str(), req.view_id.as_str(), &mut config)
|
||||
.await?
|
||||
});
|
||||
|
||||
self.view_configs.insert(req.view_id.clone(), config.into());
|
||||
bytes
|
||||
},
|
||||
TableSizeReq(_) => {
|
||||
respond!(msg, TableSizeResp {
|
||||
size: self.handler.table_size(msg.entity_id.as_str()).await?
|
||||
})
|
||||
},
|
||||
TableValidateExprReq(req) => {
|
||||
let mut expression_schema = HashMap::<String, i32>::default();
|
||||
let mut expression_alias = HashMap::<String, String>::default();
|
||||
let mut errors = HashMap::<String, ExprValidationError>::default();
|
||||
for (name, ex) in req.column_to_expr.iter() {
|
||||
let _ = expression_alias.insert(name.clone(), ex.clone());
|
||||
match self
|
||||
.handler
|
||||
.table_validate_expression(&msg.entity_id, ex.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(dtype) => {
|
||||
let _ = expression_schema.insert(name.clone(), dtype as i32);
|
||||
},
|
||||
Err(e) => {
|
||||
let _ = errors.insert(name.clone(), ExprValidationError {
|
||||
error_message: format!("{}", e),
|
||||
line: 0,
|
||||
column: 0,
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
respond!(msg, TableValidateExprResp {
|
||||
expression_schema,
|
||||
errors,
|
||||
expression_alias,
|
||||
})
|
||||
},
|
||||
ViewSchemaReq(_) => {
|
||||
respond!(msg, ViewSchemaResp {
|
||||
schema: self
|
||||
.get_cached_view_schema(&msg.entity_id, true)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(x, y)| (x.to_string(), y as i32))
|
||||
.collect()
|
||||
})
|
||||
},
|
||||
ViewDimensionsReq(_) => {
|
||||
let view_id = &msg.entity_id;
|
||||
let table_id = self
|
||||
.view_to_table
|
||||
.get(view_id)
|
||||
.ok_or_else(|| VirtualServerError::UnknownViewId(view_id.to_string()))?;
|
||||
|
||||
let num_table_rows = self.handler.table_size(table_id).await?;
|
||||
let num_table_columns = self.handler.table_column_size(table_id).await? as u32;
|
||||
let config = self.view_configs.get(view_id).unwrap();
|
||||
let num_view_columns = self.handler.view_column_size(view_id, config).await? as u32;
|
||||
let num_view_rows = self.handler.view_size(view_id).await?;
|
||||
let resp = ViewDimensionsResp {
|
||||
num_table_columns,
|
||||
num_table_rows,
|
||||
num_view_columns,
|
||||
num_view_rows,
|
||||
};
|
||||
|
||||
respond!(msg, ViewDimensionsResp { ..resp })
|
||||
},
|
||||
ViewGetConfigReq(_) => {
|
||||
respond!(msg, ViewGetConfigResp {
|
||||
config: Some(
|
||||
ViewConfigUpdate::from(
|
||||
self.view_configs.get(&msg.entity_id).unwrap().clone()
|
||||
)
|
||||
.into()
|
||||
)
|
||||
})
|
||||
},
|
||||
ViewExpressionSchemaReq(_) => {
|
||||
let mut schema = HashMap::<String, i32>::default();
|
||||
let table_id = self.view_to_table.get(&msg.entity_id);
|
||||
for (name, ex) in self
|
||||
.view_configs
|
||||
.get(&msg.entity_id)
|
||||
.unwrap()
|
||||
.expressions
|
||||
.iter()
|
||||
{
|
||||
match self
|
||||
.handler
|
||||
.table_validate_expression(table_id.unwrap(), ex.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(dtype) => {
|
||||
let _ = schema.insert(name.clone(), dtype as i32);
|
||||
},
|
||||
Err(_e) => {
|
||||
// TODO: handle error
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let resp = ViewExpressionSchemaResp { schema };
|
||||
respond!(msg, ViewExpressionSchemaResp { ..resp })
|
||||
},
|
||||
ViewColumnPathsReq(_) => {
|
||||
respond!(msg, ViewColumnPathsResp {
|
||||
paths: self
|
||||
.handler
|
||||
.view_schema(
|
||||
msg.entity_id.as_str(),
|
||||
self.view_configs.get(&msg.entity_id).unwrap()
|
||||
)
|
||||
.await?
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect()
|
||||
})
|
||||
},
|
||||
ViewToArrowReq(view_to_arrow_req) => {
|
||||
let viewport = view_to_arrow_req.viewport.unwrap();
|
||||
let schema = self.get_cached_view_schema(&msg.entity_id, false).await?;
|
||||
let config = self.view_configs.get(&msg.entity_id).unwrap();
|
||||
let mut cols = self
|
||||
.handler
|
||||
.view_get_data(msg.entity_id.as_str(), config, &schema, &viewport)
|
||||
.await?;
|
||||
|
||||
let arrow = cols
|
||||
.render_to_arrow_ipc()
|
||||
.map_err(|e| VirtualServerError::Other(e.to_string()))?;
|
||||
|
||||
respond!(msg, ViewToArrowResp { arrow })
|
||||
},
|
||||
ViewToCsvReq(view_to_csv_req) => {
|
||||
let viewport = view_to_csv_req.viewport.unwrap();
|
||||
let schema = self.get_cached_view_schema(&msg.entity_id, false).await?;
|
||||
let config = self.view_configs.get(&msg.entity_id).unwrap();
|
||||
let mut cols = self
|
||||
.handler
|
||||
.view_get_data(msg.entity_id.as_str(), config, &schema, &viewport)
|
||||
.await?;
|
||||
|
||||
let rows = cols.render_to_rows(RowPathStyle::PerLevel);
|
||||
let mut csv = String::new();
|
||||
if let Some(first_row) = rows.first() {
|
||||
let headers: Vec<&str> = first_row.keys().map(|k| k.as_str()).collect();
|
||||
csv.push_str(&headers.join(","));
|
||||
csv.push('\n');
|
||||
}
|
||||
|
||||
for row in &rows {
|
||||
let values: Vec<String> = row
|
||||
.values()
|
||||
.map(|cell| serde_json::to_string(cell).unwrap_or_default())
|
||||
.collect();
|
||||
csv.push_str(&values.join(","));
|
||||
csv.push('\n');
|
||||
}
|
||||
|
||||
respond!(msg, ViewToCsvResp { csv })
|
||||
},
|
||||
ViewToNdjsonStringReq(view_to_ndjson_req) => {
|
||||
let viewport = view_to_ndjson_req.viewport.unwrap();
|
||||
let schema = self.get_cached_view_schema(&msg.entity_id, false).await?;
|
||||
let config = self.view_configs.get(&msg.entity_id).unwrap();
|
||||
let mut cols = self
|
||||
.handler
|
||||
.view_get_data(msg.entity_id.as_str(), config, &schema, &viewport)
|
||||
.await?;
|
||||
|
||||
let rows = cols.render_to_rows(RowPathStyle::PerLevel);
|
||||
let ndjson_string = rows
|
||||
.iter()
|
||||
.map(serde_json::to_string)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| VirtualServerError::InvalidJSON(std::sync::Arc::new(e)))?
|
||||
.join("\n");
|
||||
|
||||
respond!(msg, ViewToNdjsonStringResp { ndjson_string })
|
||||
},
|
||||
ViewToRowsStringReq(view_to_rows_string_req) => {
|
||||
let viewport = view_to_rows_string_req.viewport.unwrap();
|
||||
let schema = self.get_cached_view_schema(&msg.entity_id, false).await?;
|
||||
let config = self.view_configs.get(&msg.entity_id).unwrap();
|
||||
let mut cols = self
|
||||
.handler
|
||||
.view_get_data(msg.entity_id.as_str(), config, &schema, &viewport)
|
||||
.await?;
|
||||
|
||||
let rows = cols.render_to_rows(RowPathStyle::Sidecar);
|
||||
let json_string = serde_json::to_string(&rows)
|
||||
.map_err(|e| VirtualServerError::InvalidJSON(std::sync::Arc::new(e)))?;
|
||||
|
||||
respond!(msg, ViewToRowsStringResp { json_string })
|
||||
},
|
||||
ViewToColumnsStringReq(view_to_columns_string_req) => {
|
||||
let viewport = view_to_columns_string_req.viewport.unwrap();
|
||||
let schema = self.get_cached_view_schema(&msg.entity_id, false).await?;
|
||||
let config = self.view_configs.get(&msg.entity_id).unwrap();
|
||||
let mut cols = self
|
||||
.handler
|
||||
.view_get_data(msg.entity_id.as_str(), config, &schema, &viewport)
|
||||
.await?;
|
||||
|
||||
let json_string = cols
|
||||
.render_to_columns_json(RowPathStyle::Sidecar)
|
||||
.map_err(|e| VirtualServerError::Other(e.to_string()))?;
|
||||
|
||||
respond!(msg, ViewToColumnsStringResp { json_string })
|
||||
},
|
||||
ViewDeleteReq(_) => {
|
||||
self.handler.view_delete(msg.entity_id.as_str()).await?;
|
||||
self.view_to_table.shift_remove(&msg.entity_id);
|
||||
self.view_configs.shift_remove(&msg.entity_id);
|
||||
respond!(msg, ViewDeleteResp {})
|
||||
},
|
||||
MakeTableReq(req) => {
|
||||
self.handler
|
||||
.make_table(&msg.entity_id, req.data.as_ref().unwrap())
|
||||
.await?;
|
||||
respond!(msg, MakeTableResp {})
|
||||
},
|
||||
ViewGetMinMaxReq(req) => {
|
||||
let config = self.view_configs.get(&msg.entity_id).unwrap();
|
||||
let (min, max) = self
|
||||
.handler
|
||||
.view_get_min_max(&msg.entity_id, &req.column_name, config)
|
||||
.await?;
|
||||
respond!(msg, ViewGetMinMaxResp {
|
||||
min: Some(min.into()),
|
||||
max: Some(max.into()),
|
||||
})
|
||||
},
|
||||
|
||||
// Stub implementations for callback/update requests that VirtualServer doesn't support
|
||||
TableOnDeleteReq(_) => {
|
||||
respond!(msg, TableOnDeleteResp {})
|
||||
},
|
||||
ViewOnUpdateReq(_) => {
|
||||
respond!(msg, ViewOnUpdateResp {
|
||||
delta: None,
|
||||
port_id: 0
|
||||
})
|
||||
},
|
||||
ViewOnDeleteReq(_) => {
|
||||
respond!(msg, ViewOnDeleteResp {})
|
||||
},
|
||||
ViewRemoveOnUpdateReq(_) => {
|
||||
respond!(msg, ViewRemoveOnUpdateResp {})
|
||||
},
|
||||
TableRemoveDeleteReq(_) => {
|
||||
respond!(msg, TableRemoveDeleteResp {})
|
||||
},
|
||||
ViewRemoveDeleteReq(_) => {
|
||||
respond!(msg, ViewRemoveDeleteResp {})
|
||||
},
|
||||
x => {
|
||||
// Return an error response instead of empty bytes
|
||||
return Err(VirtualServerError::Other(format!(
|
||||
"Unhandled request: {:?}",
|
||||
x
|
||||
)));
|
||||
},
|
||||
};
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user