Skip to content

Commit 78363c0

Browse files
committed
Outer and Left joins
Signed-off-by: Andrew Stein <steinlink@gmail.com>
1 parent dc2fcd9 commit 78363c0

24 files changed

Lines changed: 1836 additions & 521 deletions

File tree

rust/metadata/main.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ use std::fs;
3232

3333
use perspective_client::config::*;
3434
use perspective_client::{
35-
ColumnWindow, DeleteOptions, OnUpdateData, OnUpdateOptions, SystemInfo, TableInitOptions,
36-
UpdateOptions, ViewWindow,
35+
ColumnWindow, DeleteOptions, JoinOptions, OnUpdateData, OnUpdateOptions, SystemInfo,
36+
TableInitOptions, UpdateOptions, ViewWindow,
3737
};
3838
use perspective_viewer::config::ViewerConfigUpdate;
3939
use ts_rs::TS;
@@ -71,6 +71,7 @@ pub fn generate_type_bindings_js() -> Result<(), Box<dyn Error>> {
7171
ViewConfigUpdate::export_all_to(&path)?;
7272
OnUpdateData::export_all_to(&path)?;
7373
OnUpdateOptions::export_all_to(&path)?;
74+
JoinOptions::export_all_to(&path)?;
7475
UpdateOptions::export_all_to(&path)?;
7576
DeleteOptions::export_all_to(&path)?;
7677
ViewWindow::export_all_to(&path)?;

rust/perspective-client/build.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ fn prost_build() -> Result<()> {
5757
.field_attribute("ViewOnUpdateResp.delta", "#[ts(as = \"Vec::<u8>\")]")
5858
.field_attribute("ViewOnUpdateResp.delta", "#[serde(with = \"serde_bytes\")]")
5959
.type_attribute("ColumnType", "#[derive(ts_rs::TS)]")
60+
.type_attribute(
61+
"JoinType",
62+
"#[derive(serde::Deserialize, ts_rs::TS)] #[serde(rename_all = \"snake_case\")]",
63+
)
6064
.field_attribute("ViewToArrowResp.arrow", "#[serde(skip)]")
6165
.field_attribute("from_arrow", "#[serde(skip)]")
6266
.type_attribute(".", "#[derive(serde::Serialize)]")

rust/perspective-client/perspective.proto

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,11 +337,19 @@ message MakeTableReq {
337337
}
338338
message MakeTableResp {}
339339

340-
// `Client::join` — create a read-only table from an INNER JOIN of two tables.
340+
enum JoinType {
341+
INNER = 0;
342+
LEFT = 1;
343+
OUTER = 2;
344+
}
345+
346+
// `Client::join` — create a read-only table from a JOIN of two tables.
341347
message MakeJoinTableReq {
342348
string left_table_id = 1;
343349
string right_table_id = 2;
344350
string on_column = 3;
351+
JoinType join_type = 4;
352+
string right_on_column = 5;
345353
}
346354
message MakeJoinTableResp {}
347355

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

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@ use crate::proto::request::ClientReq;
2626
use crate::proto::response::ClientResp;
2727
use crate::proto::{
2828
ColumnType, GetFeaturesReq, GetFeaturesResp, GetHostedTablesReq, GetHostedTablesResp,
29-
HostedTable, MakeJoinTableReq, MakeTableReq, RemoveHostedTablesUpdateReq, Request, Response,
30-
ServerError, ServerSystemInfoReq,
29+
HostedTable, JoinType, MakeJoinTableReq, MakeTableReq, RemoveHostedTablesUpdateReq, Request,
30+
Response, ServerError, ServerSystemInfoReq,
3131
};
32-
use crate::table::{Table, TableInitOptions, TableOptions};
32+
use crate::table::{JoinOptions, Table, TableInitOptions, TableOptions};
3333
use crate::table_data::{TableData, UpdateData};
34+
use crate::table_ref::TableRef;
3435
use crate::utils::*;
3536
use crate::view::{OnUpdateData, ViewWindow};
3637
use crate::{OnUpdateMode, OnUpdateOptions, asyncfn, clone};
@@ -589,32 +590,36 @@ impl Client {
589590
}
590591
}
591592

592-
/// Create a new read-only [`Table`] by performing an INNER JOIN on two
593-
/// source tables. The resulting table is reactive: when either source
594-
/// table is updated, the join is automatically recomputed.
593+
/// Create a new read-only [`Table`] by performing a JOIN on two source
594+
/// tables. The resulting table is reactive: when either source table is
595+
/// updated, the join is automatically recomputed.
595596
///
596597
/// # Arguments
597598
///
598-
/// * `left` - The left source table.
599-
/// * `right` - The right source table.
599+
/// * `left` - The left source table (as a [`Table`] or name string).
600+
/// * `right` - The right source table (as a [`Table`] or name string).
600601
/// * `on` - The column name to join on. Must exist in both tables with the
601602
/// same type.
602-
/// * `name` - Optional name for the resulting table.
603+
/// * `options` - Join configuration (join type, table name).
603604
pub async fn join(
604605
&self,
605-
left: &Table,
606-
right: &Table,
606+
left: TableRef,
607+
right: TableRef,
607608
on: &str,
608-
name: Option<String>,
609+
options: JoinOptions,
609610
) -> ClientResult<Table> {
610-
let entity_id = name.unwrap_or_else(randid);
611+
let entity_id = options.name.unwrap_or_else(randid);
612+
let join_type: JoinType = options.join_type.unwrap_or_default();
613+
let right_on_column = options.right_on.unwrap_or_default();
611614
let msg = Request {
612615
msg_id: self.gen_id(),
613616
entity_id: entity_id.clone(),
614617
client_req: Some(ClientReq::MakeJoinTableReq(MakeJoinTableReq {
615-
left_table_id: left.get_name().to_owned(),
616-
right_table_id: right.get_name().to_owned(),
618+
left_table_id: left.table_name().to_owned(),
619+
right_table_id: right.table_name().to_owned(),
617620
on_column: on.to_owned(),
621+
join_type: join_type.into(),
622+
right_on_column,
618623
})),
619624
};
620625

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ mod client;
3838
mod session;
3939
mod table;
4040
mod table_data;
41+
mod table_ref;
4142
mod view;
4243
pub mod virtual_server;
4344

@@ -51,11 +52,14 @@ pub mod utils;
5152

5253
pub use crate::client::{Client, ClientHandler, Features, ReconnectCallback, SystemInfo};
5354
use crate::proto::HostedTable;
55+
pub use crate::proto::JoinType;
5456
pub use crate::session::{ProxySession, Session};
5557
pub use crate::table::{
56-
DeleteOptions, ExprValidationResult, Table, TableInitOptions, TableReadFormat, UpdateOptions,
58+
DeleteOptions, ExprValidationResult, JoinOptions, Table, TableInitOptions, TableReadFormat,
59+
UpdateOptions,
5760
};
5861
pub use crate::table_data::{TableData, UpdateData};
62+
pub use crate::table_ref::TableRef;
5963
pub use crate::view::{
6064
ColumnWindow, OnUpdateData, OnUpdateMode, OnUpdateOptions, View, ViewWindow,
6165
};

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,22 @@ impl From<TableInitOptions> for TableOptions {
137137
}
138138
}
139139

140+
/// Options for [`Client::join`].
141+
#[derive(Clone, Debug, Default, Serialize, Deserialize, TS)]
142+
pub struct JoinOptions {
143+
#[serde(default)]
144+
#[ts(optional)]
145+
pub join_type: Option<crate::proto::JoinType>,
146+
147+
#[serde(default)]
148+
#[ts(optional)]
149+
pub name: Option<String>,
150+
151+
#[serde(default)]
152+
#[ts(optional)]
153+
pub right_on: Option<String>,
154+
}
155+
140156
/// Options for [`Table::delete`].
141157
#[derive(Clone, Debug, Default, Deserialize, TS)]
142158
pub struct DeleteOptions {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
8+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
10+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12+
13+
use crate::Table;
14+
15+
/// A reference to a table, either by handle or by name.
16+
#[derive(Clone)]
17+
pub enum TableRef {
18+
Table(Table),
19+
Name(String),
20+
}
21+
22+
impl TableRef {
23+
pub fn table_name(&self) -> &str {
24+
match self {
25+
TableRef::Table(table) => table.get_name(),
26+
TableRef::Name(name) => name,
27+
}
28+
}
29+
}
30+
31+
impl From<&Table> for TableRef {
32+
fn from(table: &Table) -> Self {
33+
TableRef::Table(table.clone())
34+
}
35+
}
36+
37+
impl From<Table> for TableRef {
38+
fn from(table: Table) -> Self {
39+
TableRef::Table(table)
40+
}
41+
}
42+
43+
impl From<String> for TableRef {
44+
fn from(name: String) -> Self {
45+
TableRef::Name(name)
46+
}
47+
}
48+
49+
impl From<&str> for TableRef {
50+
fn from(name: &str) -> Self {
51+
TableRef::Name(name.to_owned())
52+
}
53+
}

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

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use js_sys::{Function, Uint8Array};
2020
#[cfg(doc)]
2121
use perspective_client::SystemInfo;
2222
use perspective_client::{
23-
ClientError, ReconnectCallback, Session, TableData, TableInitOptions, asyncfn,
23+
ClientError, ReconnectCallback, Session, TableData, TableInitOptions, TableRef, asyncfn,
2424
};
2525
use wasm_bindgen::prelude::*;
2626
use wasm_bindgen_derive::TryFromJsValue;
@@ -35,6 +35,35 @@ extern "C" {
3535
#[derive(Clone)]
3636
#[wasm_bindgen(typescript_type = "TableInitOptions")]
3737
pub type JsTableInitOptions;
38+
39+
#[derive(Clone)]
40+
#[wasm_bindgen(typescript_type = "JoinOptions")]
41+
pub type JsJoinOptions;
42+
}
43+
44+
async fn js_to_table_ref(val: &JsValue) -> ApiResult<TableRef> {
45+
if let Some(name) = val.as_string() {
46+
Ok(TableRef::from(name))
47+
} else {
48+
let get_name = js_sys::Reflect::get(val, &wasm_bindgen::intern("get_name").into())
49+
.map_err(|_| apierror!(TableRefError))?
50+
.dyn_into::<js_sys::Function>()
51+
.map_err(|_| apierror!(TableRefError))?;
52+
53+
let promise = get_name
54+
.call0(val)
55+
.map_err(|_| apierror!(TableRefError))?
56+
.dyn_into::<js_sys::Promise>()
57+
.map_err(|_| apierror!(TableRefError))?;
58+
59+
let name = wasm_bindgen_futures::JsFuture::from(promise)
60+
.await
61+
.map_err(|_| apierror!(TableRefError))?
62+
.as_string()
63+
.ok_or_else(|| apierror!(TableRefError))?;
64+
65+
Ok(TableRef::from(name))
66+
}
3867
}
3968

4069
#[wasm_bindgen]
@@ -388,26 +417,38 @@ impl Client {
388417
///
389418
/// # Arguments
390419
///
391-
/// - `left` - The left source table.
392-
/// - `right` - The right source table.
420+
/// - `left` - The left source table (a [`Table`] instance or a table name
421+
/// string).
422+
/// - `right` - The right source table (a [`Table`] instance or a table name
423+
/// string).
393424
/// - `on` - The column name to join on. Must exist in both tables with the
394425
/// same type.
395-
/// - `name` - Optional name for the resulting table.
426+
/// - `options` - Optional join configuration: `{ join_type?: "inner" |
427+
/// "left" | "outer", name?: string }`.
396428
///
397429
/// # JavaScript Examples
398430
///
399431
/// ```javascript
400-
/// const joined = await client.join(orders_table, products_table, "Product ID");
432+
/// const joined = await client.join(orders_table, products_table, "Product ID", { join_type: "left" });
433+
/// const joined = await client.join("orders", "products", "Product ID", { join_type: "left" });
401434
/// ```
402435
#[wasm_bindgen]
403436
pub async fn join(
404437
&self,
405-
left: &Table,
406-
right: &Table,
438+
left: JsValue,
439+
right: JsValue,
407440
on: &str,
408-
name: Option<String>,
441+
options: Option<JsJoinOptions>,
409442
) -> ApiResult<Table> {
410-
Ok(Table(self.client.join(&left.0, &right.0, on, name).await?))
443+
let options = options
444+
.into_serde_ext::<Option<perspective_client::JoinOptions>>()?
445+
.unwrap_or_default();
446+
447+
let left_ref = js_to_table_ref(&left).await?;
448+
let right_ref = js_to_table_ref(&right).await?;
449+
Ok(Table(
450+
self.client.join(left_ref, right_ref, on, options).await?,
451+
))
411452
}
412453

413454
/// Terminates this [`Client`], cleaning up any [`crate::View`] handles the

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,15 @@ export type * from "../../src/ts/ts-rs/SystemInfo.d.ts";
5959
export type * from "../../src/ts/ts-rs/SortDir.d.ts";
6060
export type * from "../../src/ts/ts-rs/Filter.d.ts";
6161
export type * from "../../src/ts/ts-rs/ViewConfig.d.ts";
62+
export type * from "../../src/ts/ts-rs/JoinOptions.ts";
63+
export type * from "../../src/ts/ts-rs/JoinType.ts";
6264
6365
import type {ColumnWindow} from "../../src/ts/ts-rs/ColumnWindow.d.ts";
6466
import type {ColumnType} from "../../src/ts/ts-rs/ColumnType.d.ts";
6567
import type {ViewWindow} from "../../src/ts/ts-rs/ViewWindow.d.ts";
6668
import type {TableInitOptions} from "../../src/ts/ts-rs/TableInitOptions.d.ts";
69+
import type {JoinOptions} from "../../src/ts/ts-rs/JoinOptions.ts";
70+
import type {JoinType} from "../../src/ts/ts-rs/JoinType.ts";
6771
import type {ViewConfigUpdate} from "../../src/ts/ts-rs/ViewConfigUpdate.d.ts";
6872
import type * as on_update_args from "../../src/ts/ts-rs/ViewOnUpdateResp.d.ts";
6973
import type {OnUpdateOptions} from "../../src/ts/ts-rs/OnUpdateOptions.d.ts";

rust/perspective-js/src/rust/utils/errors.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ pub enum ApiErrorType {
9797
#[error("Invalid `expressions` {}", format_valid_exprs(.0))]
9898
InvalidViewerConfigExpressionsError(Rc<ExprValidationResult>),
9999

100+
#[error("Expected a Table or string table name")]
101+
TableRefError,
102+
100103
#[error("No `Table` attached")]
101104
NoTableError,
102105

@@ -134,6 +137,7 @@ impl ApiError {
134137
ApiErrorType::ProstError(_) => "[ProstError]",
135138
ApiErrorType::InvalidViewerConfigError(..) => "[InvalidViewerConfigError]",
136139
ApiErrorType::InvalidViewerConfigExpressionsError(_) => "[InvalidViewerConfigError]",
140+
ApiErrorType::TableRefError => "[TableRefError]",
137141
ApiErrorType::NoTableError => "[NoTableError]",
138142
ApiErrorType::SerdeWasmBindgenError(_) => "[SerdeWasmBindgenError]",
139143
ApiErrorType::Utf8Error(_) => "[FromUtf8Error]",

0 commit comments

Comments
 (0)