chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:25:07 +08:00
commit a26e856398
1681 changed files with 296950 additions and 0 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
{"ENTITY_TYPE":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]}
@@ -0,0 +1 @@
{"ENTITY_TYPE":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]}
@@ -0,0 +1 @@
{"ENTITY_TYPE":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]}
@@ -0,0 +1 @@
{"ENTITY_TYPE":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"XXX","XXX",null,"XXX","XXX","YYY","XXX","YYY","ZZZ","ZZZ","YYY","YYY","ZZZ","ZZZ",null,"YYY",null,null,"XXX","YYY",null,"ZZZ","XXX","ZZZ","WWW","WWW","YYY",null,"WWW","ZZZ","YYY","YYY","XXX","XXX","ZZZ","ZZZ","ZZZ","XXX",null,null,"YYY","XXX",null,"XXX","WWW","YYY",null,null,"ZZZ",null,"YYY",null,null,"ZZZ",null,"YYY","XXX","XXX","YYY","YYY",null,"XXX","YYY","YYY",null,"YYY","YYY",null,"XXX","YYY","XXX","ZZZ",null,"XXX",null,null,"YYY","XXX","XXX","XXX","XXX","WWW","XXX","ZZZ","XXX","XXX","ZZZ","ZZZ",null,"XXX","YYY",null,"YYY","ZZZ","YYY",null,null,null,"YYY","ZZZ","XXX",null,"WWW","WWW"]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
<!doctype html>
<html>
<head>
<script type="module">
import perspective from "/node_modules/@perspective-dev/client/dist/esm/perspective.js";
let resp = await fetch("/node_modules/@perspective-dev/test/assets/superstore.csv");
let csv = await resp.text();
const wasm = fetch("/node_modules/@perspective-dev/client/dist/wasm/perspective-js.wasm");
const wasm2 = fetch("/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm");
perspective.init_client(wasm);
perspective.init_server(wasm2);
const registration = await navigator.serviceWorker.register("/node_modules/@perspective-dev/client/dist/cdn/perspective-server.worker.js", {
scope: "/node_modules/@perspective-dev/client/dist/cdn/",
});
const worker = await perspective.worker(registration.active);
window.table = worker.table(csv, { index: "Row ID" });
window.__TEST_WORKER__ = worker;
window.__TEST_PERSPECTIVE_READY__ = true;
</script>
</head>
</html>
@@ -0,0 +1,18 @@
<!doctype html>
<html>
<head>
<script type="module">
import perspective from "/node_modules/@perspective-dev/client/dist/esm/perspective.js";
let resp = await fetch("/node_modules/@perspective-dev/test/assets/superstore.csv");
let csv = await resp.text();
const wasm = fetch("/node_modules/@perspective-dev/client/dist/wasm/perspective-js.wasm");
const wasm2 = fetch("/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm");
perspective.init_client(wasm);
perspective.init_server(wasm2);
const worker = await perspective.worker(new SharedWorker("/node_modules/@perspective-dev/client/dist/cdn/perspective-server.worker.js"));
window.table = worker.table(csv, { index: "Row ID" });
window.__TEST_WORKER__ = worker;
window.__TEST_PERSPECTIVE_READY__ = true;
</script>
</head>
</html>
+4
View File
@@ -0,0 +1,4 @@
<html>
<head> </head>
<body></body>
</html>
+18
View File
@@ -0,0 +1,18 @@
<!doctype html>
<html>
<head>
<script type="module">
import perspective from "/node_modules/@perspective-dev/client/dist/esm/perspective.js";
let resp = await fetch("/node_modules/@perspective-dev/test/assets/superstore.csv");
let csv = await resp.text();
const wasm = fetch("/node_modules/@perspective-dev/client/dist/wasm/perspective-js.wasm");
const wasm2 = fetch("/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm");
perspective.init_client(wasm);
perspective.init_server(wasm2);
const worker = await perspective.worker(new Worker("/node_modules/@perspective-dev/client/dist/cdn/perspective-server.worker.js"));
window.table = worker.table(csv, { index: "Row ID" });
window.__TEST_WORKER__ = worker;
window.__TEST_PERSPECTIVE_READY__ = true;
</script>
</head>
</html>
File diff suppressed because it is too large Load Diff
+209
View File
@@ -0,0 +1,209 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client.ts";
import type * as psp_types from "@perspective-dev/client";
((perspective) => {
test.describe("Clear", function () {
test("removes the rows from the table", async function () {
const table = await perspective.table([{ x: 1 }]);
const view = await table.view();
let json = await view.to_json();
expect(json).toHaveLength(1);
table.clear();
json = await view.to_json();
expect(json).toHaveLength(0);
view.delete();
table.delete();
});
test("to_columns output is empty", async function () {
const table = await perspective.table([{ x: 1 }]);
const view = await table.view();
let result = await view.to_columns();
expect(result).toEqual({
x: [1],
});
table.clear();
result = await view.to_columns();
expect(result).toEqual({ x: [] });
view.delete();
table.delete();
});
});
test.describe("Replace", function () {
test("replaces the rows in the table with the input data", async function () {
const table = await perspective.table([
{ x: 1, y: 2 },
{ x: 3, y: 4 },
]);
const view = await table.view();
let json = await view.to_json();
expect(json).toHaveLength(2);
expect(json).toEqual([
{ x: 1, y: 2 },
{ x: 3, y: 4 },
]);
await table.replace([{ x: 5, y: 6 }]);
json = await view.to_json();
expect(json).toHaveLength(1);
expect(json).toEqual([{ x: 5, y: 6 }]);
view.delete();
table.delete();
});
test("Replaces CSV Table with high precision datetimes", async function () {
const a = '"start"\n2024-08-14T14:06:07.826Z';
const b = '"start"\n2024-08-14T14:06:09.876667543Z';
const table = await perspective.table(a);
const view = await table.view();
const csv1 = await view.to_csv();
expect(csv1).toEqual('"start"\n2024-08-14 14:06:07.826\n');
await table.replace(b);
const csv2 = await view.to_csv();
expect(csv2).toEqual('"start"\n2024-08-14 14:06:09.876\n');
view.delete();
table.delete();
});
test("replaces the rows in the table with the input data and fires an on_update", async function () {
const table = await perspective.table([
{ x: 1, y: 2 },
{ x: 3, y: 4 },
]);
const view = await table.view();
let emit: ((x: unknown) => void) | undefined;
const callback = async function (updated: psp_types.OnUpdateArgs) {
expect(updated.port_id).toEqual(0);
const json = await view.to_json();
expect(json).toHaveLength(1);
expect(json).toEqual([{ x: 5, y: 6 }]);
view.delete();
table.delete();
emit!(undefined);
};
let result = new Promise((x) => {
emit = x;
});
view.on_update(callback);
let json = await view.to_json();
expect(json).toHaveLength(2);
expect(json).toEqual([
{ x: 1, y: 2 },
{ x: 3, y: 4 },
]);
await table.replace([{ x: 5, y: 6 }]);
await result;
});
test("replaces the rows in the table with the input data and fires an on_update with the correct delta", async function () {
const table = await perspective.table([
{ x: 1, y: 2 },
{ x: 3, y: 4 },
]);
const view = await table.view();
let emit: ((x: unknown) => void) | undefined;
const callback = async function (updated: psp_types.OnUpdateArgs) {
expect(updated.port_id).toEqual(0);
const table2 = await perspective.table(updated.delta!);
const view2 = await table2.view();
const json = await view.to_json();
expect(json).toHaveLength(1);
expect(json).toEqual([{ x: 5, y: 6 }]);
const json2 = await view2.to_json();
expect(json2).toEqual(json);
view2.delete();
table2.delete();
view.delete();
table.delete();
emit!(undefined);
};
let result = new Promise((x) => {
emit = x;
});
view.on_update(callback, { mode: "row" });
let json = await view.to_json();
expect(json).toHaveLength(2);
expect(json).toEqual([
{ x: 1, y: 2 },
{ x: 3, y: 4 },
]);
await table.replace([{ x: 5, y: 6 }]);
await result;
});
test("replace the rows in the table atomically", async function () {
const table = await perspective.table([
{ x: 1, y: 2 },
{ x: 3, y: 4 },
]);
const view = await table.view();
setTimeout(() => table.replace([{ x: 5, y: 6 }]));
let json = await view.to_json();
expect(json).toHaveLength(2);
expect(json).toEqual([
{ x: 1, y: 2 },
{ x: 3, y: 4 },
]);
await new Promise((x, y) => setTimeout(x));
json = await view.to_json();
expect(json).toHaveLength(1);
expect(json).toEqual([{ x: 5, y: 6 }]);
view.delete();
table.delete();
});
test("Preserves sort order with 2-sided pivot", async function () {
const input = [
{ x: 1, y: 7, z: "a" },
{ x: 1, y: 6, z: "b" },
{ x: 2, y: 5, z: "a" },
{ x: 2, y: 4, z: "b" },
{ x: 3, y: 3, z: "a" },
{ x: 3, y: 2, z: "b" },
];
const table = await perspective.table(input);
const view = await table.view({
group_by: ["z"],
split_by: ["x"],
sort: [["y", "asc"]],
columns: ["y"],
});
setTimeout(() => table.replace(input));
let json = await view.to_json();
await new Promise((x, y) => setTimeout(x));
let json2 = await view.to_json();
expect(json).toEqual(json2);
view.delete();
table.delete();
});
});
})(perspective);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,191 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import * as arrow from "apache-arrow";
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
import * as fs from "node:fs";
import * as url from "node:url";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url)).slice(0, -1);
test.describe("Arrow", function () {
test.describe("Date columns", function () {
// https://github.com/perspective-dev/perspective/issues/2894
// https://github.com/jdangerx/repro-perspective-float-filter/tree/dates
test("Date columns are preserved through Arrow in and out", async function () {
const tableData = arrow.tableFromArrays({
date: arrow.vectorFromArray([20089], new arrow.Date_()),
});
const table = await perspective.table(arrow.tableToIPC(tableData));
const view = await table.view();
const json = await view.to_json();
const d = new Date(json[0].date);
expect(json[0].date).toEqual(1735689600000);
// This doesn't test anything except my math
expect(d.getUTCFullYear()).toEqual(2025);
expect(d.getUTCDate()).toEqual(1);
expect(d.getUTCMonth()).toEqual(0);
expect(d.getUTCHours()).toEqual(0);
expect(d.getUTCMinutes()).toEqual(0);
expect(d.getUTCSeconds()).toEqual(0);
expect(d.getTimezoneOffset()).toEqual(0);
await view.delete();
await table.delete();
});
test("Date columns are preserved through Arrow in and out, in a negative timezone", async function () {
process.env.TZ = `America/New_York`;
const tableData = arrow.tableFromArrays({
date: arrow.vectorFromArray([20089], new arrow.Date_()),
});
const table = await perspective.table(arrow.tableToIPC(tableData));
const view = await table.view();
const json = await view.to_json();
const d = new Date(json[0].date);
expect(json[0].date).toEqual(1735689600000);
expect(d.getUTCFullYear()).toEqual(2025);
expect(d.getUTCDate()).toEqual(1);
expect(d.getUTCMonth()).toEqual(0);
expect(d.getUTCHours()).toEqual(0);
expect(d.getUTCMinutes()).toEqual(0);
expect(d.getUTCSeconds()).toEqual(0);
// NY now ...
expect(d.getTimezoneOffset()).toEqual(300);
await view.delete();
await table.delete();
process.env.TZ = `UTC`;
});
});
test.describe("regressions", () => {
// https://github.com/perspective-dev/perspective/issues/3169
test("null values are preserved across multi-batch Arrow IPC streams", async function () {
function row(
identifier: string,
value: number | null,
date: Date | null,
) {
return arrow.tableFromArrays({
Identifier: arrow.vectorFromArray(
[identifier],
new arrow.Utf8(),
),
Value: arrow.vectorFromArray([value], new arrow.Float64()),
Date: arrow.vectorFromArray([date], new arrow.DateDay()),
});
}
const t1 = row("A", null, null);
const t2 = row("B", 5, null);
const t3 = row("C", null, new Date(Date.UTC(2025, 5, 15)));
const multiBatchTable = new arrow.Table([
...t1.batches,
...t2.batches,
...t3.batches,
]);
expect(multiBatchTable.batches.length).toEqual(3);
const ipc = arrow.tableToIPC(multiBatchTable, "stream");
const table = await perspective.table(ipc.buffer as ArrayBuffer);
const view = await table.view();
const json = await view.to_json();
await view.delete();
await table.delete();
expect(json).toStrictEqual([
{ Identifier: "A", Value: null, Date: null },
{ Identifier: "B", Value: 5, Date: null },
{ Identifier: "C", Value: null, Date: 1749945600000 },
]);
});
test("null equality works correctly in updates", async function () {
async function write_to_json(
buffer: ArrayBuffer,
filename: string,
) {
const table = await perspective.table(buffer);
const view = await table.view({
columns: ["ENTITY_TYPE"],
});
const json = await view.to_columns_string();
fs.writeFileSync(filename, json);
await view.delete();
await table.delete();
}
const file = JSON.parse(
fs.readFileSync(
`${__dirname}/../../arrow/untitled.json`,
"utf8",
),
);
const table = await perspective.table(file, {
name: "arrow_null_test",
});
const view = await table.view({ group_by: ["ENTITY_TYPE"] });
for (let i = 2; i < 6; i++) {
const file = JSON.parse(
fs.readFileSync(
`${__dirname}/../../arrow/untitled${i}.json`,
"utf8",
),
);
await table.update(file);
}
const cols = await view.to_columns({ end_row: 4 });
expect(cols).toStrictEqual({
ENTITY_TYPE: [2158, 985, 168, 311],
__ROW_PATH__: [[], [null], [""], ["AAAA"]],
});
});
test("Loads a time32 (millisecond) column without overflow", async function () {
const expected = Array.from({ length: 64 }, (_, i) => i);
const tableData = arrow.tableFromArrays({
t: arrow.vectorFromArray(expected, new arrow.TimeMillisecond()),
});
const table = await perspective.table(arrow.tableToIPC(tableData));
const view = await table.view();
expect(await table.size()).toEqual(64);
expect(await view.to_columns()).toEqual({ t: expected });
await view.delete();
await table.delete();
});
});
test.describe("Malformed input", function () {
test("Rejects an Arrow with a row count that exceeds 32 bits", async function () {
const bytes = new Uint8Array(
fs.readFileSync(`${__dirname}/../../arrow/bad_row_count.arrow`),
);
await expect(perspective.table(bytes)).rejects.toThrow(
/row count exceeds maximum supported size/,
);
});
});
});
@@ -0,0 +1,66 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
const CSV = "x,y,z\n1,2,3\n4,5,6";
test.describe("CSV Constructors", function () {
test("Handles String format", async function () {
const table = await perspective.table(CSV);
const v = await table.view();
const json = await v.to_json();
expect(json).toEqual([
{ x: 1, y: 2, z: 3 },
{ x: 4, y: 5, z: 6 },
]);
});
test("Handles String format with explicit format option", async function () {
const table = await perspective.table(CSV, {
format: "csv",
});
const v = await table.view();
const json = await v.to_json();
expect(json).toEqual([
{ x: 1, y: 2, z: 3 },
{ x: 4, y: 5, z: 6 },
]);
});
test("Handles ArrayBuffer format", async function () {
var enc = new TextEncoder();
const table = await perspective.table(enc.encode(CSV).buffer, {
format: "csv",
});
const v = await table.view();
const json = await v.to_json();
expect(json).toEqual([
{ x: 1, y: 2, z: 3 },
{ x: 4, y: 5, z: 6 },
]);
});
test("Handles UInt8Arry format", async function () {
var enc = new TextEncoder();
const table = await perspective.table(enc.encode(CSV), {
format: "csv",
});
const v = await table.view();
const json = await v.to_json();
expect(json).toEqual([
{ x: 1, y: 2, z: 3 },
{ x: 4, y: 5, z: 6 },
]);
});
});
@@ -0,0 +1,111 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
const JAN_1_2024_UTC = 1704067200000;
test.describe("Date timezone invariance", function () {
let SAVED_TZ: string | undefined;
test.beforeEach(() => {
SAVED_TZ = process.env.TZ;
process.env.TZ = "Europe/Amsterdam";
});
test.afterEach(() => {
process.env.TZ = SAVED_TZ;
});
test("date strings serialize to UTC midnight epoch ms", async function () {
const table = await perspective.table({ d: "date" });
await table.update({ d: ["2024-01-01"] });
const view = await table.view();
const cols = (await view.to_columns()) as { d: number[] };
expect(cols.d).toEqual([JAN_1_2024_UTC]);
await view.delete();
await table.delete();
});
test("date epoch ms input round-trips exactly", async function () {
const table = await perspective.table({ d: "date" });
await table.update({ d: [JAN_1_2024_UTC] });
const view = await table.view();
const cols = (await view.to_columns()) as { d: number[] };
expect(cols.d).toEqual([JAN_1_2024_UTC]);
await view.delete();
await table.delete();
});
test("formatted output prints the stored calendar day", async function () {
const table = await perspective.table({ d: "date" });
await table.update({ d: ["2024-01-01"] });
const view = await table.view();
expect(await view.to_columns_string({ formatted: true })).toEqual(
'{"d":["2024-01-01"]}',
);
expect(await view.to_csv()).toEqual('"d"\n2024-01-01\n');
await view.delete();
await table.delete();
});
test("day_bucket stays on the UTC calendar day at the day boundary", async function () {
const table = await perspective.table({ t: "datetime" });
// 23:59 UTC is already the next day in Europe/Amsterdam
await table.update({ t: ["2020-01-31 23:59:00"] });
const view = await table.view({
expressions: { bucket: `bucket("t", 'D')` },
});
const cols = (await view.to_columns()) as { bucket: number[] };
// 2020-01-31T00:00:00Z
expect(cols.bucket).toEqual([1580428800000]);
await view.delete();
await table.delete();
});
test("day_of_week and hour_of_day compute in UTC", async function () {
const table = await perspective.table({ t: "datetime" });
await table.update({ t: ["2020-01-31 23:59:00"] });
const view = await table.view({
expressions: {
dow: `day_of_week("t")`,
hod: `hour_of_day("t")`,
},
});
const cols = (await view.to_columns()) as {
dow: string[];
hod: number[];
};
// Friday 23:00 UTC, not Saturday 00:59 Amsterdam
expect(cols.dow).toEqual(["6 Friday"]);
expect(cols.hod).toEqual([23]);
await view.delete();
await table.delete();
});
test("JSON date output agrees with Arrow date32 day arithmetic", async function () {
const table = await perspective.table({ d: "date" });
await table.update({ d: ["2024-01-01"] });
const view = await table.view();
const cols = (await view.to_columns()) as { d: number[] };
expect(cols.d[0] % 86400000).toEqual(0);
expect(cols.d[0] / 86400000).toEqual(19723); // days since epoch
await view.delete();
await table.delete();
});
});
@@ -0,0 +1,90 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
const TEST_JSON = { x: [1, 4], y: [2, 5], z: [3, 6] };
test.describe("JSON", function () {
test.describe("Integer columns", function () {
test("Integer columns can be updated with all JSON numeric types and cousins", async function () {
const table = await perspective.table({ x: "integer" });
await table.update([{ x: 0 }]);
await table.update([{ x: 1 }]);
await table.update([{ x: undefined }]);
await table.update([{ x: null }]);
await table.update([{ x: 7.23 }]);
await table.update([{ x: -6 }]);
await table.update([{ x: Infinity }]);
await table.update([{ x: "100.1" }]);
await table.update([{ x: "11" }]);
const v = await table.view();
const json = await v.to_json();
expect(json).toEqual([
{ x: 0 },
{ x: 1 },
{ x: null },
{ x: null },
{ x: 7 },
{ x: -6 },
{ x: null },
{ x: 100 },
{ x: 11 },
]);
});
});
test("Handles String format", async function () {
const table = await perspective.table(JSON.stringify(TEST_JSON), {
format: "columns",
});
const v = await table.view();
const json = await v.to_json();
expect(json).toEqual([
{ x: 1, y: 2, z: 3 },
{ x: 4, y: 5, z: 6 },
]);
});
test("Handles UInt8Array format", async function () {
const enc = new TextEncoder();
const table = await perspective.table(
enc.encode(JSON.stringify(TEST_JSON)),
{
format: "columns",
},
);
const v = await table.view();
const json = await v.to_json();
expect(json).toEqual([
{ x: 1, y: 2, z: 3 },
{ x: 4, y: 5, z: 6 },
]);
});
test("Handles ArrayBuffer format", async function () {
const enc = new TextEncoder();
const table = await perspective.table(
enc.encode(JSON.stringify(TEST_JSON)).buffer,
{
format: "columns",
},
);
const v = await table.view();
const json = await v.to_json();
expect(json).toEqual([
{ x: 1, y: 2, z: 3 },
{ x: 4, y: 5, z: 6 },
]);
});
});
+121
View File
@@ -0,0 +1,121 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
function it_old_behavior(name, capture) {
test(name, async function () {
let done;
let result = new Promise((x) => {
done = x;
});
await capture(done);
await result;
});
}
function mock_fn() {
let count = 0;
let fun = () => {
count += 1;
};
fun.count = () => count;
return fun;
}
((perspective) => {
test.describe("Delete", function () {
test("calls all delete callbacks registered on table", async function () {
const table = await perspective.table([{ x: 1 }]);
const cb1 = mock_fn();
const cb2 = mock_fn();
table.on_delete(cb1);
table.on_delete(cb2);
await table.delete();
expect(cb1.count()).toEqual(1);
expect(cb2.count()).toEqual(1);
});
test("remove_delete unregisters table delete callbacks", async function () {
const table = await perspective.table([{ x: 1 }]);
const cb1 = mock_fn();
const cb2 = mock_fn();
const sub1 = await table.on_delete(cb1);
table.on_delete(cb2);
await table.remove_delete(sub1);
await table.delete();
expect(cb1.count()).toEqual(0);
expect(cb2.count()).toEqual(1);
});
test("calls all delete callbacks registered on view", async function () {
const table = await perspective.table([{ x: 1 }]);
const view = await table.view();
const cb1 = mock_fn();
const cb2 = mock_fn();
view.on_delete(cb1);
view.on_delete(cb2);
await view.delete();
expect(cb1.count()).toEqual(1);
expect(cb2.count()).toEqual(1);
await table.delete();
});
test("remove_delete unregisters view delete callbacks", async function () {
const table = await perspective.table([{ x: 1 }]);
const view = await table.view();
const cb1 = mock_fn();
const cb2 = mock_fn();
const sub1 = await view.on_delete(cb1);
view.on_delete(cb2);
view.remove_delete(sub1);
await view.delete();
expect(cb1.count()).toEqual(0);
expect(cb2.count()).toEqual(1);
await table.delete();
});
it_old_behavior(
"properly removes a failed delete callback on a table",
async function (done) {
const table = await perspective.table([{ x: 1 }]);
// when a callback throws, it should delete that callback
table.on_delete(() => {
throw new Error("something went wrong!");
});
table.delete();
done();
},
);
it_old_behavior(
"properly removes a failed delete callback on a view",
async function (done) {
const table = await perspective.table([{ x: 1 }]);
const view = await table.view();
// when a callback throws, it should delete that callback
view.on_delete(() => {
throw new Error("something went wrong!");
});
view.delete();
table.delete();
done();
},
);
});
})(perspective);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import { describeDuckDB } from "./setup.js";
describeDuckDB("client", (getClient) => {
test("get_hosted_table_names()", async function () {
const client = getClient();
const tables = await client.get_hosted_table_names();
expect(tables).toEqual(["memory.superstore"]);
});
});
@@ -0,0 +1,286 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import { describeDuckDB } from "./setup.js";
describeDuckDB("combined operations", (getClient) => {
test("group_by + filter + sort", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
group_by: ["Category"],
filter: [["Region", "==", "West"]],
sort: [["Sales", "desc"]],
aggregates: { Sales: "sum" },
});
const json = await view.to_json();
expect(json).toEqual([
{ __ROW_PATH__: [], Sales: 725457.8245000006 },
{
__ROW_PATH__: ["Furniture"],
Sales: 252612.7435000003,
},
{
__ROW_PATH__: ["Technology"],
Sales: 251991.83199999997,
},
{
__ROW_PATH__: ["Office Supplies"],
Sales: 220853.24900000007,
},
]);
await view.delete();
});
test("split_by + group_by + filter", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
group_by: ["Category"],
split_by: ["Region"],
filter: [["Quantity", ">", 3]],
aggregates: { Sales: "sum" },
});
const paths = await view.column_paths();
expect(paths).toEqual([
"Central_Sales",
"East_Sales",
"South_Sales",
"West_Sales",
]);
const numRows = await view.num_rows();
expect(numRows).toBe(4);
const json = await view.to_json();
expect(json).toEqual([
{
__ROW_PATH__: [],
"Central|Sales": 332883.0567999998,
"East|Sales": 455143.735,
"South|Sales": 274208.7699999999,
"West|Sales": 470561.28350000136,
},
{
__ROW_PATH__: ["Furniture"],
"Central|Sales": 111457.73279999988,
"East|Sales": 140376.95899999997,
"South|Sales": 80859.618,
"West|Sales": 165219.5734999998,
},
{
__ROW_PATH__: ["Office Supplies"],
"Central|Sales": 103937.78599999992,
"East|Sales": 135823.893,
"South|Sales": 84393.3579999999,
"West|Sales": 140206.93099999975,
},
{
__ROW_PATH__: ["Technology"],
"Central|Sales": 117487.53800000002,
"East|Sales": 178942.883,
"South|Sales": 108955.79400000005,
"West|Sales": 165134.77900000007,
},
]);
await view.delete();
});
test("split_by only", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
split_by: ["Region"],
filter: [["Quantity", ">", 3]],
});
const paths = await view.column_paths();
expect(paths).toEqual([
"Central_Sales",
"East_Sales",
"South_Sales",
"West_Sales",
]);
const numRows = await view.num_rows();
expect(numRows).toBe(4284);
const json = await view.to_json({ start_row: 0, end_row: 1 });
expect(json).toEqual([
{
"Central|Sales": null,
"East|Sales": null,
"South|Sales": 957.5775,
"West|Sales": null,
},
]);
await view.delete();
});
test("split_by only + sort", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
split_by: ["Region"],
sort: [["Sales", "desc"]],
filter: [["Quantity", ">", 3]],
});
const paths = await view.column_paths();
expect(paths).toEqual([
"Central_Sales",
"East_Sales",
"South_Sales",
"West_Sales",
]);
const numRows = await view.num_rows();
expect(numRows).toBe(4284);
const json = await view.to_json({ start_row: 0, end_row: 1 });
expect(json).toEqual([
{
"Central|Sales": null,
"East|Sales": null,
"South|Sales": 22638.48,
"West|Sales": null,
},
]);
await view.delete();
});
test("flat + multi group_by + sort", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
group_by: ["Region", "Category"],
sort: [["Sales", "desc"]],
aggregates: { Sales: "sum" },
group_rollup_mode: "flat",
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{
__ROW_PATH__: ["West", "Furniture"],
Sales: 252612.7435000003,
},
{
__ROW_PATH__: ["West", "Technology"],
Sales: 251991.83199999997,
},
{
__ROW_PATH__: ["West", "Office Supplies"],
Sales: 220853.24900000007,
},
{
__ROW_PATH__: ["East", "Technology"],
Sales: 264973.9810000003,
},
{
__ROW_PATH__: ["East", "Furniture"],
Sales: 208291.20400000009,
},
]);
await view.delete();
});
test("flat + multi group_by + split_by + sort", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
group_by: ["Region", "Category"],
split_by: ["Ship Mode"],
sort: [["Sales", "desc"]],
aggregates: { Sales: "sum" },
group_rollup_mode: "flat",
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{
__ROW_PATH__: ["West", "Furniture"],
"First Class|Sales": 40018.829499999985,
"Same Day|Sales": 14527.978000000001,
"Second Class|Sales": 54155.6555,
"Standard Class|Sales": 143910.28049999996,
},
{
__ROW_PATH__: ["West", "Technology"],
"First Class|Sales": 61107.98900000001,
"Same Day|Sales": 19218.053999999993,
"Second Class|Sales": 38610.979999999996,
"Standard Class|Sales": 133054.809,
},
{
__ROW_PATH__: ["West", "Office Supplies"],
"First Class|Sales": 28635.06999999996,
"Same Day|Sales": 9857.678000000002,
"Second Class|Sales": 52572.79199999999,
"Standard Class|Sales": 129787.70900000003,
},
{
__ROW_PATH__: ["East", "Technology"],
"First Class|Sales": 47693.312999999995,
"Same Day|Sales": 21349.464999999997,
"Second Class|Sales": 29304.490000000005,
"Standard Class|Sales": 166626.71300000005,
},
{
__ROW_PATH__: ["East", "Furniture"],
"First Class|Sales": 29410.643999999997,
"Same Day|Sales": 12852.570999999996,
"Second Class|Sales": 44035.937000000005,
"Standard Class|Sales": 121992.05199999997,
},
]);
await view.delete();
});
test("expressions + group_by + sort", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["profitmargin"],
group_by: ["Region"],
expressions: { profitmargin: '"Profit" / "Sales" * 100' },
sort: [["profitmargin", "desc"]],
aggregates: { profitmargin: "avg" },
});
const json = await view.to_json();
expect(json).toEqual([
{
__ROW_PATH__: [],
profitmargin: 12.031392972104467,
},
{
__ROW_PATH__: ["West"],
profitmargin: 21.948661793784012,
},
{
__ROW_PATH__: ["East"],
profitmargin: 16.722695960406636,
},
{
__ROW_PATH__: ["South"],
profitmargin: 16.35190329218107,
},
{
__ROW_PATH__: ["Central"],
profitmargin: -10.407293926323575,
},
]);
await view.delete();
});
});
@@ -0,0 +1,100 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import { describeDuckDB } from "./setup.js";
describeDuckDB("data types", (getClient) => {
test("integer columns", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Quantity"],
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{ Quantity: 2 },
{ Quantity: 3 },
{ Quantity: 2 },
{ Quantity: 5 },
{ Quantity: 2 },
]);
await view.delete();
});
test("float columns", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Profit"],
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{ Sales: 261.96, Profit: 41.9136 },
{ Sales: 731.94, Profit: 219.582 },
{ Sales: 14.62, Profit: 6.8714 },
{ Sales: 957.5775, Profit: -383.031 },
{ Sales: 22.368, Profit: 2.5164 },
]);
await view.delete();
});
test("string columns", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Region", "State", "City"],
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{
Region: "South",
State: "Kentucky",
City: "Henderson",
},
{
Region: "South",
State: "Kentucky",
City: "Henderson",
},
{
Region: "West",
State: "California",
City: "Los Angeles",
},
{
Region: "South",
State: "Florida",
City: "Fort Lauderdale",
},
{
Region: "South",
State: "Florida",
City: "Fort Lauderdale",
},
]);
await view.delete();
});
test("date columns", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Order Date"],
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{ "Order Date": 1478563200000 },
{ "Order Date": 1478563200000 },
{ "Order Date": 1465689600000 },
{ "Order Date": 1444521600000 },
{ "Order Date": 1444521600000 },
]);
await view.delete();
});
});
@@ -0,0 +1,89 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import { describeDuckDB } from "./setup.js";
describeDuckDB("expressions", (getClient) => {
test("simple expression", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "doublesales"],
expressions: { doublesales: '"Sales" * 2' },
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{ Sales: 261.96, doublesales: 523.92 },
{ Sales: 731.94, doublesales: 1463.88 },
{ Sales: 14.62, doublesales: 29.24 },
{ Sales: 957.5775, doublesales: 1915.155 },
{ Sales: 22.368, doublesales: 44.736 },
]);
await view.delete();
});
test("expression with multiple columns", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Profit", "margin"],
expressions: { margin: '"Profit" / "Sales"' },
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{
Sales: 261.96,
Profit: 41.9136,
margin: 0.16000000000000003,
},
{ Sales: 731.94, Profit: 219.582, margin: 0.3 },
{
Sales: 14.62,
Profit: 6.8714,
margin: 0.47000000000000003,
},
{ Sales: 957.5775, Profit: -383.031, margin: -0.4 },
{ Sales: 22.368, Profit: 2.5164, margin: 0.1125 },
]);
await view.delete();
});
test("expression with group_by", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["total"],
group_by: ["Region"],
expressions: { total: '"Sales" + "Profit"' },
aggregates: { total: "sum" },
});
const json = await view.to_json();
expect(json).toEqual([
{ __ROW_PATH__: [], total: 2583597.882000014 },
{
__ROW_PATH__: ["Central"],
total: 540946.2532999996,
},
{ __ROW_PATH__: ["East"], total: 770304.0199999991 },
{
__ROW_PATH__: ["South"],
total: 438471.33530000027,
},
{ __ROW_PATH__: ["West"], total: 833876.2733999988 },
]);
await view.delete();
});
});
@@ -0,0 +1,184 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import { describeDuckDB } from "./setup.js";
describeDuckDB("filter", (getClient) => {
test("filter with equals", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Region"],
filter: [["Region", "==", "West"]],
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{ Sales: 14.62, Region: "West" },
{ Sales: 48.86, Region: "West" },
{ Sales: 7.28, Region: "West" },
{ Sales: 907.152, Region: "West" },
{ Sales: 18.504, Region: "West" },
]);
await view.delete();
});
test("filter with not equals", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Region"],
filter: [["Region", "!=", "West"]],
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{ Sales: 261.96, Region: "South" },
{ Sales: 731.94, Region: "South" },
{ Sales: 957.5775, Region: "South" },
{ Sales: 22.368, Region: "South" },
{ Sales: 15.552, Region: "South" },
]);
await view.delete();
});
test("filter with greater than", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Quantity"],
filter: [["Quantity", ">", 5]],
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{ Sales: 48.86, Quantity: 7 },
{ Sales: 907.152, Quantity: 6 },
{ Sales: 1706.184, Quantity: 9 },
{ Sales: 665.88, Quantity: 6 },
{ Sales: 19.46, Quantity: 7 },
]);
await view.delete();
});
test("filter with less than", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Quantity"],
filter: [["Quantity", "<", 3]],
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{ Sales: 261.96, Quantity: 2 },
{ Sales: 14.62, Quantity: 2 },
{ Sales: 22.368, Quantity: 2 },
{ Sales: 55.5, Quantity: 2 },
{ Sales: 8.56, Quantity: 2 },
]);
await view.delete();
});
test("filter with greater than or equal", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Quantity"],
filter: [["Quantity", ">=", 10]],
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{ Sales: 40.096, Quantity: 14 },
{ Sales: 43.12, Quantity: 14 },
{ Sales: 384.45, Quantity: 11 },
{ Sales: 3347.37, Quantity: 13 },
{ Sales: 100.24, Quantity: 10 },
]);
await view.delete();
});
test("filter with less than or equal", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Quantity"],
filter: [["Quantity", "<=", 2]],
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{ Sales: 261.96, Quantity: 2 },
{ Sales: 14.62, Quantity: 2 },
{ Sales: 22.368, Quantity: 2 },
{ Sales: 55.5, Quantity: 2 },
{ Sales: 8.56, Quantity: 2 },
]);
await view.delete();
});
test("filter with LIKE", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "State"],
filter: [["State", "LIKE", "Cal%"]],
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{ Sales: 14.62, State: "California" },
{ Sales: 48.86, State: "California" },
{ Sales: 7.28, State: "California" },
{ Sales: 907.152, State: "California" },
{ Sales: 18.504, State: "California" },
]);
await view.delete();
});
test("multiple filters", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Region", "Quantity"],
filter: [
["Region", "==", "West"],
["Quantity", ">", 3],
],
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{ Sales: 48.86, Region: "West", Quantity: 7 },
{ Sales: 7.28, Region: "West", Quantity: 4 },
{ Sales: 907.152, Region: "West", Quantity: 6 },
{ Sales: 114.9, Region: "West", Quantity: 5 },
{ Sales: 1706.184, Region: "West", Quantity: 9 },
]);
await view.delete();
});
test("filter with group_by", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
group_by: ["Category"],
filter: [["Region", "==", "West"]],
aggregates: { Sales: "sum" },
});
const numRows = await view.num_rows();
expect(numRows).toBe(4);
const json = await view.to_json();
expect(json).toEqual([
{ __ROW_PATH__: [], Sales: 725457.8245000006 },
{
__ROW_PATH__: ["Furniture"],
Sales: 252612.7435000003,
},
{
__ROW_PATH__: ["Office Supplies"],
Sales: 220853.24900000007,
},
{
__ROW_PATH__: ["Technology"],
Sales: 251991.83199999997,
},
]);
await view.delete();
});
});
@@ -0,0 +1,194 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import { describeDuckDB } from "./setup.js";
describeDuckDB("group_by", (getClient) => {
test("single group_by", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
group_by: ["Region"],
aggregates: { Sales: "sum" },
});
const numRows = await view.num_rows();
expect(numRows).toBe(5);
const json = await view.to_json();
expect(json).toEqual([
{ __ROW_PATH__: [], Sales: 2297200.860299955 },
{
__ROW_PATH__: ["Central"],
Sales: 501239.8908000005,
},
{ __ROW_PATH__: ["East"], Sales: 678781.2399999979 },
{
__ROW_PATH__: ["South"],
Sales: 391721.9050000003,
},
{ __ROW_PATH__: ["West"], Sales: 725457.8245000006 },
]);
await view.delete();
});
test("multi-level group_by", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
group_by: ["Region", "Category"],
aggregates: { Sales: "sum" },
});
const json = await view.to_json();
expect(json).toEqual([
{ __ROW_PATH__: [], Sales: 2297200.860299955 },
{
__ROW_PATH__: ["Central"],
Sales: 501239.8908000005,
},
{
__ROW_PATH__: ["Central", "Furniture"],
Sales: 163797.16380000004,
},
{
__ROW_PATH__: ["Central", "Office Supplies"],
Sales: 167026.41500000027,
},
{
__ROW_PATH__: ["Central", "Technology"],
Sales: 170416.3119999999,
},
{ __ROW_PATH__: ["East"], Sales: 678781.2399999979 },
{
__ROW_PATH__: ["East", "Furniture"],
Sales: 208291.20400000009,
},
{
__ROW_PATH__: ["East", "Office Supplies"],
Sales: 205516.0549999999,
},
{
__ROW_PATH__: ["East", "Technology"],
Sales: 264973.9810000003,
},
{
__ROW_PATH__: ["South"],
Sales: 391721.9050000003,
},
{
__ROW_PATH__: ["South", "Furniture"],
Sales: 117298.6840000001,
},
{
__ROW_PATH__: ["South", "Office Supplies"],
Sales: 125651.31299999992,
},
{
__ROW_PATH__: ["South", "Technology"],
Sales: 148771.9079999999,
},
{ __ROW_PATH__: ["West"], Sales: 725457.8245000006 },
{
__ROW_PATH__: ["West", "Furniture"],
Sales: 252612.7435000003,
},
{
__ROW_PATH__: ["West", "Office Supplies"],
Sales: 220853.24900000007,
},
{
__ROW_PATH__: ["West", "Technology"],
Sales: 251991.83199999997,
},
]);
await view.delete();
});
test("group_by with count aggregate", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
group_by: ["Region"],
aggregates: { Sales: "count" },
});
const json = await view.to_json();
expect(json).toEqual([
{ __ROW_PATH__: [], Sales: 9994 },
{ __ROW_PATH__: ["Central"], Sales: 2323 },
{ __ROW_PATH__: ["East"], Sales: 2848 },
{ __ROW_PATH__: ["South"], Sales: 1620 },
{ __ROW_PATH__: ["West"], Sales: 3203 },
]);
await view.delete();
});
test("group_by with avg aggregate", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
group_by: ["Category"],
aggregates: { Sales: "avg" },
});
const json = await view.to_json();
expect(json).toEqual([
{ __ROW_PATH__: [], Sales: 229.8580008304938 },
{
__ROW_PATH__: ["Furniture"],
Sales: 349.83488698727007,
},
{
__ROW_PATH__: ["Office Supplies"],
Sales: 119.32410089611732,
},
{
__ROW_PATH__: ["Technology"],
Sales: 452.70927612344155,
},
]);
await view.delete();
});
test("group_by with min aggregate", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Quantity"],
group_by: ["Region"],
aggregates: { Quantity: "min" },
});
const json = await view.to_json();
expect(json).toEqual([
{ __ROW_PATH__: [], Quantity: 1 },
{ __ROW_PATH__: ["Central"], Quantity: 1 },
{ __ROW_PATH__: ["East"], Quantity: 1 },
{ __ROW_PATH__: ["South"], Quantity: 1 },
{ __ROW_PATH__: ["West"], Quantity: 1 },
]);
await view.delete();
});
test("group_by with max aggregate", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Quantity"],
group_by: ["Region"],
aggregates: { Quantity: "max" },
});
const json = await view.to_json();
expect(json).toEqual([
{ __ROW_PATH__: [], Quantity: 14 },
{ __ROW_PATH__: ["Central"], Quantity: 14 },
{ __ROW_PATH__: ["East"], Quantity: 14 },
{ __ROW_PATH__: ["South"], Quantity: 14 },
{ __ROW_PATH__: ["West"], Quantity: 14 },
]);
await view.delete();
});
});
@@ -0,0 +1,256 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import { describeDuckDB } from "./setup.js";
describeDuckDB("group_rollup_mode", (getClient) => {
test("flat mode with group_by", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
group_by: ["Region"],
aggregates: { Sales: "sum" },
group_rollup_mode: "flat",
});
const numRows = await view.num_rows();
expect(numRows).toBe(4);
const json = await view.to_json();
expect(json).toEqual([
{
__ROW_PATH__: ["Central"],
Sales: 501239.8908000005,
},
{ __ROW_PATH__: ["East"], Sales: 678781.2399999979 },
{
__ROW_PATH__: ["South"],
Sales: 391721.9050000003,
},
{ __ROW_PATH__: ["West"], Sales: 725457.8245000006 },
]);
await view.delete();
});
test("flat mode with multi-level group_by", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
group_by: ["Region", "Category"],
aggregates: { Sales: "sum" },
group_rollup_mode: "flat",
});
const numRows = await view.num_rows();
expect(numRows).toBe(12);
const json = await view.to_json();
expect(json).toEqual([
{
__ROW_PATH__: ["Central", "Furniture"],
Sales: 163797.16380000004,
},
{
__ROW_PATH__: ["Central", "Office Supplies"],
Sales: 167026.41500000027,
},
{
__ROW_PATH__: ["Central", "Technology"],
Sales: 170416.3119999999,
},
{
__ROW_PATH__: ["East", "Furniture"],
Sales: 208291.20400000009,
},
{
__ROW_PATH__: ["East", "Office Supplies"],
Sales: 205516.0549999999,
},
{
__ROW_PATH__: ["East", "Technology"],
Sales: 264973.9810000003,
},
{
__ROW_PATH__: ["South", "Furniture"],
Sales: 117298.6840000001,
},
{
__ROW_PATH__: ["South", "Office Supplies"],
Sales: 125651.31299999992,
},
{
__ROW_PATH__: ["South", "Technology"],
Sales: 148771.9079999999,
},
{
__ROW_PATH__: ["West", "Furniture"],
Sales: 252612.7435000003,
},
{
__ROW_PATH__: ["West", "Office Supplies"],
Sales: 220853.24900000007,
},
{
__ROW_PATH__: ["West", "Technology"],
Sales: 251991.83199999997,
},
]);
await view.delete();
});
test("flat mode with group_by and split_by", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
group_by: ["Category"],
split_by: ["Region"],
aggregates: { Sales: "sum" },
group_rollup_mode: "flat",
});
const numRows = await view.num_rows();
expect(numRows).toBe(3);
const json = await view.to_json();
expect(json).toEqual([
{
__ROW_PATH__: ["Furniture"],
"Central|Sales": 163797.16380000004,
"East|Sales": 208291.20400000009,
"South|Sales": 117298.6840000001,
"West|Sales": 252612.7435000003,
},
{
__ROW_PATH__: ["Office Supplies"],
"Central|Sales": 167026.41500000027,
"East|Sales": 205516.0549999999,
"South|Sales": 125651.31299999992,
"West|Sales": 220853.24900000007,
},
{
__ROW_PATH__: ["Technology"],
"Central|Sales": 170416.3119999999,
"East|Sales": 264973.9810000003,
"South|Sales": 148771.9079999999,
"West|Sales": 251991.83199999997,
},
]);
await view.delete();
});
test("flat mode with group_by and split_by and sort", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
group_by: ["Category"],
split_by: ["Region"],
sort: [["Sales", "desc"]],
aggregates: { Sales: "sum" },
group_rollup_mode: "flat",
});
const numRows = await view.num_rows();
expect(numRows).toBe(3);
const json = await view.to_json();
expect(json).toEqual([
{
__ROW_PATH__: ["Technology"],
"Central|Sales": 170416.3119999999,
"East|Sales": 264973.9810000003,
"South|Sales": 148771.9079999999,
"West|Sales": 251991.83199999997,
},
{
__ROW_PATH__: ["Furniture"],
"Central|Sales": 163797.16380000004,
"East|Sales": 208291.20400000009,
"South|Sales": 117298.6840000001,
"West|Sales": 252612.7435000003,
},
{
__ROW_PATH__: ["Office Supplies"],
"Central|Sales": 167026.41500000027,
"East|Sales": 205516.0549999999,
"South|Sales": 125651.31299999992,
"West|Sales": 220853.24900000007,
},
]);
await view.delete();
});
test("flat mode with group_by and sort", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
group_by: ["Region"],
sort: [["Sales", "desc"]],
aggregates: { Sales: "sum" },
group_rollup_mode: "flat",
});
const json = await view.to_json();
expect(json).toEqual([
{ __ROW_PATH__: ["West"], Sales: 725457.8245000006 },
{ __ROW_PATH__: ["East"], Sales: 678781.2399999979 },
{
__ROW_PATH__: ["Central"],
Sales: 501239.8908000005,
},
{
__ROW_PATH__: ["South"],
Sales: 391721.9050000003,
},
]);
await view.delete();
});
test("total mode", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
aggregates: { Sales: "sum" },
group_rollup_mode: "total",
});
const numRows = await view.num_rows();
expect(numRows).toBe(1);
const json = await view.to_json();
expect(json).toEqual([{ Sales: 2297200.860299955 }]);
await view.delete();
});
test("total mode with multiple columns", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Quantity"],
aggregates: { Sales: "sum", Quantity: "sum" },
group_rollup_mode: "total",
});
const json = await view.to_json();
expect(json).toEqual([{ Sales: 2297200.860299955, Quantity: 37873 }]);
await view.delete();
});
test("total mode with split_by", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
split_by: ["Region"],
aggregates: { Sales: "sum" },
group_rollup_mode: "total",
});
const numRows = await view.num_rows();
expect(numRows).toBe(1);
const json = await view.to_json();
expect(json).toEqual([
{
"Central|Sales": 501239.8908000005,
"East|Sales": 678781.2399999979,
"South|Sales": 391721.9050000003,
"West|Sales": 725457.8245000006,
},
]);
await view.delete();
});
});
@@ -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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import { describeDuckDB } from "./setup.js";
describeDuckDB("min_max", (getClient) => {
test("get_min_max() on integer column", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({ columns: ["Quantity"] });
const result = await view.get_min_max("Quantity");
expect(result[0]).toBe(1);
expect(result[1]).toBe(14);
await view.delete();
});
test("get_min_max() on float column", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({ columns: ["Sales"] });
const result = await view.get_min_max("Sales");
expect(result[0]).toBe(0.444);
expect(result[1]).toBe(22638.48);
await view.delete();
});
test("get_min_max() on string column", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({ columns: ["Category"] });
const result = await view.get_min_max("Category");
expect(result[0]).toBe("Furniture");
expect(result[1]).toBe("Technology");
await view.delete();
});
test("get_min_max() with group_by", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
group_by: ["Region"],
aggregates: { Sales: "sum" },
});
const result = await view.get_min_max("Sales");
expect(result[0]).toBeGreaterThan(0);
expect(result[1]).toBeGreaterThan(0);
expect(result[1]).toBeGreaterThanOrEqual(result[0]);
await view.delete();
});
test("get_min_max() with filter", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Quantity"],
filter: [["Quantity", ">", 10]],
});
const result = await view.get_min_max("Quantity");
expect(result[0]).toBeGreaterThanOrEqual(11);
expect(result[1]).toBe(14);
await view.delete();
});
});
@@ -0,0 +1,90 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import * as fs from "fs";
import * as path from "path";
import { createRequire } from "module";
import * as duckdb from "@duckdb/duckdb-wasm";
import { test } from "@perspective-dev/test";
import {
default as perspective,
createMessageHandler,
wasmModule,
} from "@perspective-dev/client";
import { DuckDBHandler } from "@perspective-dev/client/src/ts/virtual_servers/duckdb.ts";
const require = createRequire(import.meta.url);
const DUCKDB_DIST = path.dirname(require.resolve("@duckdb/duckdb-wasm"));
const Worker = require("web-worker");
async function initializeDuckDB() {
const bundle = await duckdb.selectBundle({
mvp: {
mainModule: path.resolve(DUCKDB_DIST, "./duckdb-mvp.wasm"),
mainWorker: path.resolve(
DUCKDB_DIST,
"./duckdb-node-mvp.worker.cjs",
),
},
eh: {
mainModule: path.resolve(DUCKDB_DIST, "./duckdb-eh.wasm"),
mainWorker: path.resolve(
DUCKDB_DIST,
"./duckdb-node-eh.worker.cjs",
),
},
});
const logger = new duckdb.ConsoleLogger();
const worker = new Worker(bundle.mainWorker);
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
const c = await db.connect();
await c.query(`
SET default_null_order=NULLS_FIRST_ON_ASC_LAST_ON_DESC;
`);
return c;
}
async function loadSuperstoreData(db) {
const arrowPath = path.resolve(
import.meta.dirname,
"../../../node_modules/superstore-arrow/superstore.lz4.arrow",
);
const arrayBuffer = fs.readFileSync(arrowPath);
await db.insertArrowFromIPCStream(new Uint8Array(arrayBuffer), {
name: "superstore",
create: true,
});
}
export function describeDuckDB(name, fn) {
test.describe("DuckDB Virtual Server " + name, function () {
let db;
let client;
test.beforeAll(async () => {
db = await initializeDuckDB();
const server = createMessageHandler(
new DuckDBHandler(db, wasmModule),
);
client = await perspective.worker(server);
await loadSuperstoreData(db);
});
fn(() => client);
});
}
@@ -0,0 +1,95 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import { describeDuckDB } from "./setup.js";
describeDuckDB("sort", (getClient) => {
test("sort ascending", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Quantity"],
sort: [["Sales", "asc"]],
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{ Sales: 0.444, Quantity: 1 },
{ Sales: 0.556, Quantity: 1 },
{ Sales: 0.836, Quantity: 1 },
{ Sales: 0.852, Quantity: 1 },
{ Sales: 0.876, Quantity: 1 },
]);
await view.delete();
});
test("sort descending", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Quantity"],
sort: [["Sales", "desc"]],
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{ Sales: 22638.48, Quantity: 6 },
{ Sales: 17499.95, Quantity: 5 },
{ Sales: 13999.96, Quantity: 4 },
{ Sales: 11199.968, Quantity: 4 },
{ Sales: 10499.97, Quantity: 3 },
]);
await view.delete();
});
test("sort with group_by", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
group_by: ["Region"],
sort: [["Sales", "desc"]],
aggregates: { Sales: "sum" },
});
const json = await view.to_json();
expect(json).toEqual([
{ __ROW_PATH__: [], Sales: 2297200.860299955 },
{ __ROW_PATH__: ["West"], Sales: 725457.8245000006 },
{ __ROW_PATH__: ["East"], Sales: 678781.2399999979 },
{
__ROW_PATH__: ["Central"],
Sales: 501239.8908000005,
},
{
__ROW_PATH__: ["South"],
Sales: 391721.9050000003,
},
]);
await view.delete();
});
test("multi-column sort", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Region", "Sales", "Quantity"],
sort: [
["Region", "asc"],
["Sales", "desc"],
],
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{ Region: "Central", Sales: 17499.95, Quantity: 5 },
{ Region: "Central", Sales: 9892.74, Quantity: 13 },
{ Region: "Central", Sales: 9449.95, Quantity: 5 },
{ Region: "Central", Sales: 8159.952, Quantity: 8 },
{ Region: "Central", Sales: 5443.96, Quantity: 4 },
]);
await view.delete();
});
});
@@ -0,0 +1,124 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import { describeDuckDB } from "./setup.js";
describeDuckDB("split_by", (getClient) => {
test("single split_by", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
split_by: ["Region"],
group_by: ["Category"],
aggregates: { Sales: "sum" },
});
const columns = await view.column_paths();
expect(columns).toEqual([
"Central_Sales",
"East_Sales",
"South_Sales",
"West_Sales",
]);
const json = await view.to_json();
expect(json).toEqual([
{
__ROW_PATH__: [],
"Central|Sales": 501239.8908000005,
"East|Sales": 678781.2399999979,
"South|Sales": 391721.9050000003,
"West|Sales": 725457.8245000006,
},
{
__ROW_PATH__: ["Furniture"],
"Central|Sales": 163797.16380000004,
"East|Sales": 208291.20400000009,
"South|Sales": 117298.6840000001,
"West|Sales": 252612.7435000003,
},
{
__ROW_PATH__: ["Office Supplies"],
"Central|Sales": 167026.41500000027,
"East|Sales": 205516.0549999999,
"South|Sales": 125651.31299999992,
"West|Sales": 220853.24900000007,
},
{
__ROW_PATH__: ["Technology"],
"Central|Sales": 170416.3119999999,
"East|Sales": 264973.9810000003,
"South|Sales": 148771.9079999999,
"West|Sales": 251991.83199999997,
},
]);
await view.delete();
});
// https://github.com/perspective-dev/perspective/issues/3148
test("split_by with group_by and count aggregate", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Row ID"],
split_by: ["Region"],
group_by: ["Category"],
aggregates: { "Row ID": "count" },
});
const json = await view.to_json({ start_row: 0, end_row: 4 });
expect(json).toEqual([
{
__ROW_PATH__: [],
"Central|Row ID": 2323,
"East|Row ID": 2848,
"South|Row ID": 1620,
"West|Row ID": 3203,
},
{
__ROW_PATH__: ["Furniture"],
"Central|Row ID": 481,
"East|Row ID": 601,
"South|Row ID": 332,
"West|Row ID": 707,
},
{
__ROW_PATH__: ["Office Supplies"],
"Central|Row ID": 1422,
"East|Row ID": 1712,
"South|Row ID": 995,
"West|Row ID": 1897,
},
{
__ROW_PATH__: ["Technology"],
"Central|Row ID": 420,
"East|Row ID": 535,
"South|Row ID": 293,
"West|Row ID": 599,
},
]);
await view.delete();
});
test.skip("split_by without group_by", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales"],
split_by: ["Category"],
});
const paths = await view.column_paths();
expect(paths.some((c) => c.includes("Furniture"))).toBe(true);
expect(paths.some((c) => c.includes("Office Supplies"))).toBe(true);
expect(paths.some((c) => c.includes("Technology"))).toBe(true);
await view.delete();
});
});
@@ -0,0 +1,78 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import { describeDuckDB } from "./setup.js";
describeDuckDB("table", (getClient) => {
test("schema()", async function () {
const table = await getClient().open_table("memory.superstore");
const schema = await table.schema();
expect(schema).toEqual({
"Product Name": "string",
"Ship Date": "date",
City: "string",
"Row ID": "integer",
"Customer Name": "string",
Quantity: "integer",
Discount: "float",
"Sub-Category": "string",
Segment: "string",
Category: "string",
"Order Date": "date",
"Order ID": "string",
Sales: "float",
State: "string",
"Postal Code": "float",
Country: "string",
"Customer ID": "string",
"Ship Mode": "string",
Region: "string",
Profit: "float",
"Product ID": "string",
});
});
test("columns()", async function () {
const table = await getClient().open_table("memory.superstore");
const columns = await table.columns();
expect(columns).toEqual([
"Row ID",
"Order ID",
"Order Date",
"Ship Date",
"Ship Mode",
"Customer ID",
"Customer Name",
"Segment",
"Country",
"City",
"State",
"Postal Code",
"Region",
"Product ID",
"Category",
"Sub-Category",
"Product Name",
"Sales",
"Quantity",
"Discount",
"Profit",
]);
});
test("size()", async function () {
const table = await getClient().open_table("memory.superstore");
const size = await table.size();
expect(size).toBe(9994);
});
});
@@ -0,0 +1,91 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import { describeDuckDB } from "./setup.js";
describeDuckDB("view", (getClient) => {
test("num_rows()", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({ columns: ["Sales", "Profit"] });
const numRows = await view.num_rows();
expect(numRows).toBe(9994);
await view.delete();
});
test("num_columns()", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Profit", "State"],
});
const numColumns = await view.num_columns();
expect(numColumns).toBe(3);
await view.delete();
});
test("schema()", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Profit", "State"],
});
const schema = await view.schema();
expect(schema).toEqual({
Sales: "float",
Profit: "float",
State: "string",
});
await view.delete();
});
test("to_json()", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Quantity"],
});
const json = await view.to_json({ start_row: 0, end_row: 5 });
expect(json).toEqual([
{ Sales: 261.96, Quantity: 2 },
{ Sales: 731.94, Quantity: 3 },
{ Sales: 14.62, Quantity: 2 },
{ Sales: 957.5775, Quantity: 5 },
{ Sales: 22.368, Quantity: 2 },
]);
await view.delete();
});
test("to_columns()", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Quantity"],
});
const columns = await view.to_columns({
start_row: 0,
end_row: 5,
});
expect(columns).toEqual({
Sales: [261.96, 731.94, 14.62, 957.5775, 22.368],
Quantity: [2, 3, 2, 5, 2],
});
await view.delete();
});
test("column_paths()", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Profit", "State"],
});
const paths = await view.column_paths();
expect(paths).toEqual(["Sales", "Profit", "State"]);
await view.delete();
});
});
@@ -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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import { describeDuckDB } from "./setup.js";
describeDuckDB("viewport", (getClient) => {
test("start_row and end_row", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Profit"],
});
const json = await view.to_json({ start_row: 10, end_row: 15 });
expect(json).toEqual([
{ Sales: 1706.184, Profit: 85.3092 },
{ Sales: 911.424, Profit: 68.3568 },
{ Sales: 15.552, Profit: 5.4432 },
{ Sales: 407.976, Profit: 132.5922 },
{ Sales: 68.81, Profit: -123.858 },
]);
await view.delete();
});
test("start_col and end_col", async function () {
const table = await getClient().open_table("memory.superstore");
const view = await table.view({
columns: ["Sales", "Profit", "Quantity", "Discount"],
});
const json = await view.to_json({
start_row: 0,
end_row: 5,
start_col: 1,
end_col: 3,
});
expect(json).toEqual([
{ Profit: 41.9136, Quantity: 2 },
{ Profit: 219.582, Quantity: 3 },
{ Profit: 6.8714, Quantity: 2 },
{ Profit: -383.031, Quantity: 5 },
{ Profit: 2.5164, Quantity: 2 },
]);
await view.delete();
});
});
@@ -0,0 +1,163 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import * as arrows from "../test_arrows";
export const data = [
{ x: 1, y: "a", z: true },
{ x: 2, y: "b", z: false },
{ x: 3, y: "c", z: true },
{ x: 4, y: "d", z: false },
];
export const int_float_data = [
{ w: 1.5, x: 1, y: "a", z: true },
{ w: 2.5, x: 2, y: "b", z: false },
{ w: 3.5, x: 3, y: "c", z: true },
{ w: 4.5, x: 4, y: "d", z: false },
];
export const int_float_subtract_data = [
{ u: 2.5, v: 2, w: 1.5, x: 1, y: "a", z: true },
{ u: 3.5, v: 3, w: 2.5, x: 2, y: "b", z: false },
{ u: 4.5, v: 4, w: 3.5, x: 3, y: "c", z: true },
{ u: 5.5, v: 5, w: 4.5, x: 4, y: "d", z: false },
];
export const comparison_data = [
{ u: 0, v: 1.5, w: 1.5, x: 1, x2: 1, y: "a", z: true },
{ u: 1, v: 2.55, w: 2.5, x: 2, x2: 10, y: "b", z: false },
{ u: 0, v: 3.5, w: 3.5, x: 3, x2: 3, y: "c", z: true },
{ u: 1, v: 4.55, w: 4.5, x: 4, x2: 10, y: "d", z: false },
];
export const cols = [
"i8",
"ui8",
"i16",
"ui16",
"i32",
"ui32",
"i64",
"ui64",
"f32",
"f64",
];
export const arrow = arrows.numbers_arrow;
export const all_types_arrow = arrows.all_types_arrow;
export const all_types_multi_arrow = arrows.all_types_multi_arrow;
export const days_of_week = [
"1 Sunday",
"2 Monday",
"3 Tuesday",
"4 Wednesday",
"5 Thursday",
"6 Friday",
"7 Saturday",
];
export const months_of_year = [
"01 January",
"02 February",
"03 March",
"04 April",
"05 May",
"06 June",
"07 July",
"08 August",
"09 September",
"10 October",
"11 November",
"12 December",
];
export const second_bucket = function (val, multiplicity) {
if (multiplicity === undefined) {
multiplicity = 1;
}
const mult = 1000 * multiplicity;
return new Date(Math.floor(new Date(val).getTime() / mult) * mult);
};
export const minute_bucket = function (val, multiplicity) {
if (multiplicity === undefined) {
multiplicity = 1;
}
let date = new Date(val);
date.setSeconds(0);
date.setMilliseconds(0);
date.setMinutes(
Math.floor(date.getMinutes() / multiplicity) * multiplicity,
);
return date;
};
export const hour_bucket = function (val, multiplicity) {
if (multiplicity === undefined) {
multiplicity = 1;
}
let date = new Date(val);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
date.setHours(Math.floor(date.getHours() / multiplicity) * multiplicity);
return date;
};
export const day_bucket = function (val) {
let date = new Date(val);
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
return date;
};
export const week_bucket = function (val) {
let date = new Date(val);
let day = date.getDay();
let diff = date.getDate() - day + (day == 0 ? -6 : 1);
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setDate(diff);
return date;
};
export const month_bucket = function (val, multiplicity) {
if (multiplicity === undefined) {
multiplicity = 1;
}
let date = new Date(val);
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setDate(1);
date.setMonth(Math.floor(date.getMonth() / multiplicity) * multiplicity);
return date;
};
export const year_bucket = function (val, multiplicity) {
if (multiplicity === undefined) {
multiplicity = 1;
}
let date = new Date(val);
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setDate(1);
date.setMonth(0);
date.setFullYear(
Math.floor(date.getFullYear() / multiplicity) * multiplicity,
);
return date;
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,591 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
const match_delta = async function (perspective, delta, expected) {
let table = await perspective.table(delta);
let view = await table.view();
let json = await view.to_json();
expect(json).toEqual(expected);
await view.delete();
await table.delete();
};
function it_old_behavior(name, capture) {
test(name, async function () {
let done;
let result = new Promise((x) => {
done = x;
});
await capture(done);
await result;
});
}
/**
* Tests the correctness of updates on Tables with expression columns created
* through `View` and deltas created through `on_update`.
*/
((perspective) => {
test.describe("Expression column update deltas", function () {
test.describe("0-sided expression column deltas", function () {
it_old_behavior(
"Returns appended rows for normal and expression columns",
async function (done) {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: ["A", "B", "C", "D"],
});
const view = await table.view({
expressions: {
'lower("y")': 'lower("y")',
'-"x"': '-"x"',
},
});
view.on_update(
async function (updated) {
const expected = [
{
x: 1,
y: "HELLO",
'lower("y")': "hello",
'-"x"': -1,
},
{
x: 3,
y: "WORLD",
'lower("y")': "world",
'-"x"': -3,
},
];
await match_delta(
perspective,
updated.delta,
expected,
);
await view.delete();
await table.delete();
done();
},
{ mode: "row" },
);
await table.update({ x: [1, 3], y: ["HELLO", "WORLD"] });
},
);
it_old_behavior(
"Returns appended rows for normal and expression columns from schema",
async function (done) {
const table = await perspective.table({
x: "integer",
y: "string",
});
const view = await table.view({
expressions: { 'upper("y")': 'upper("y")' },
});
view.on_update(
async function (updated) {
const expected = [
{ x: 1, y: "a", 'upper("y")': "A" },
{ x: 2, y: "b", 'upper("y")': "B" },
{ x: 3, y: "c", 'upper("y")': "C" },
{ x: 4, y: "d", 'upper("y")': "D" },
];
await match_delta(
perspective,
updated.delta,
expected,
);
await view.delete();
await table.delete();
done();
},
{ mode: "row" },
);
await table.update({
x: [1, 2, 3, 4],
y: ["a", "b", "c", "d"],
});
},
);
it_old_behavior(
"Returns partially updated rows for normal and expression columns",
async function (done) {
const table = await perspective.table(
{
x: [1, 2, 3, 4],
y: ["A", "B", "C", "D"],
},
{ index: "x" },
);
const view = await table.view({
expressions: { 'lower("y")': 'lower("y")' },
});
view.on_update(
async function (updated) {
const full = await view.to_columns();
const expected = [
{ x: 1, y: "HELLO", 'lower("y")': "hello" },
{ x: 3, y: "WORLD", 'lower("y")': "world" },
];
expect(full).toEqual({
x: [1, 2, 3, 4],
y: ["HELLO", "B", "WORLD", "D"],
'lower("y")': ["hello", "b", "world", "d"],
});
await match_delta(
perspective,
updated.delta,
expected,
);
await view.delete();
await table.delete();
done();
},
{ mode: "row" },
);
await table.update({ x: [1, 3], y: ["HELLO", "WORLD"] });
},
);
it_old_behavior(
"Returns appended rows with missing columns for normal and expression columns",
async function (done) {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: ["A", "B", "C", "D"],
});
const view = await table.view({
expressions: { 'lower("y")': 'lower("y")' },
});
view.on_update(
async function (updated) {
const full = await view.to_columns();
const expected = [
{ x: 1, y: null, 'lower("y")': null },
{ x: 3, y: null, 'lower("y")': null },
];
expect(full).toEqual({
x: [1, 2, 3, 4, 1, 3],
y: ["A", "B", "C", "D", null, null],
'lower("y")': ["a", "b", "c", "d", null, null],
});
await match_delta(
perspective,
updated.delta,
expected,
);
await view.delete();
await table.delete();
done();
},
{ mode: "row" },
);
await table.update({ x: [1, 3] });
},
);
});
test.describe("1-sided expression column deltas", function () {
it_old_behavior(
"Returns appended rows for normal and expression columns, 1-sided",
async function (done) {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: ["A", "B", "C", "D"],
});
const view = await table.view({
group_by: ['lower("y")'],
expressions: { 'lower("y")': 'lower("y")' },
});
view.on_update(
async function (updated) {
const expected = [
{ x: 14, y: 6, 'lower("y")': 6 },
{ x: 1, y: 1, 'lower("y")': 1 },
{ x: 3, y: 1, 'lower("y")': 1 },
];
await match_delta(
perspective,
updated.delta,
expected,
);
await view.delete();
await table.delete();
done();
},
{ mode: "row" },
);
await table.update({ x: [1, 3], y: ["HELLO", "WORLD"] });
},
);
it_old_behavior(
"Returns appended rows for normal and expression columns, 1-sided hidden sorted",
async function (done) {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: ["A", "B", "C", "D"],
});
const view = await table.view({
group_by: ['lower("y")'],
expressions: { 'lower("y")': 'lower("y")' },
sort: [['lower("y")', "desc"]],
columns: ["x"],
});
view.on_update(
async function (updated) {
const expected = [{ x: 14 }, { x: 3 }, { x: 1 }];
await match_delta(
perspective,
updated.delta,
expected,
);
await view.delete();
await table.delete();
done();
},
{ mode: "row" },
);
await table.update({ x: [1, 3], y: ["HELLO", "WORLD"] });
},
);
});
test.describe("2-sided expression column deltas", function () {
it_old_behavior(
"Returns appended rows for normal and expression columns, 2-sided",
async function (done) {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: ["A", "B", "C", "D"],
});
const view = await table.view({
aggregates: {
'lower("y")': "last",
},
group_by: ['lower("y")'],
split_by: ["y"],
expressions: { 'lower("y")': 'lower("y")' },
});
view.on_update(
async function (updated) {
const expected = [
{
'A|lower("y")': "a",
"A|x": 1,
"A|y": 1,
'B|lower("y")': "b",
"B|x": 2,
"B|y": 1,
'C|lower("y")': "c",
"C|x": 3,
"C|y": 1,
'D|lower("y")': "d",
"D|x": 4,
"D|y": 1,
'HELLO|lower("y")': "hello",
"HELLO|x": 1,
"HELLO|y": 1,
'WORLD|lower("y")': "world",
"WORLD|x": 3,
"WORLD|y": 1,
},
{
'A|lower("y")': null,
"A|x": null,
"A|y": null,
'B|lower("y")': null,
"B|x": null,
"B|y": null,
'C|lower("y")': null,
"C|x": null,
"C|y": null,
'D|lower("y")': null,
"D|x": null,
"D|y": null,
'HELLO|lower("y")': "hello",
"HELLO|x": 1,
"HELLO|y": 1,
'WORLD|lower("y")': null,
"WORLD|x": null,
"WORLD|y": null,
},
{
'A|lower("y")': null,
"A|x": null,
"A|y": null,
'B|lower("y")': null,
"B|x": null,
"B|y": null,
'C|lower("y")': null,
"C|x": null,
"C|y": null,
'D|lower("y")': null,
"D|x": null,
"D|y": null,
'HELLO|lower("y")': null,
"HELLO|x": null,
"HELLO|y": null,
'WORLD|lower("y")': "world",
"WORLD|x": 3,
"WORLD|y": 1,
},
];
await match_delta(
perspective,
updated.delta,
expected,
);
await view.delete();
await table.delete();
done();
},
{ mode: "row" },
);
await table.update({ x: [1, 3], y: ["HELLO", "WORLD"] });
},
);
});
test.describe("0-sided expression column deltas with multiple views", function () {
it_old_behavior(
"`on_update` on a view with expression column should contain expression delta when columns are appended",
async function (done) {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: ["A", "B", "C", "D"],
});
const view = await table.view({
expressions: { 'lower("y")': 'lower("y")' },
});
const pre_update = await view.to_columns();
expect(pre_update).toEqual({
x: [1, 2, 3, 4],
y: ["A", "B", "C", "D"],
'lower("y")': ["a", "b", "c", "d"],
});
view.on_update(
async function (updated) {
const expected = [
{ x: 1, y: null, 'lower("y")': null },
{ x: 3, y: null, 'lower("y")': null },
];
const full = await view.to_columns();
expect(full).toEqual({
x: [1, 2, 3, 4, 1, 3],
y: ["A", "B", "C", "D", null, null],
'lower("y")': ["a", "b", "c", "d", null, null],
});
await match_delta(
perspective,
updated.delta,
expected,
);
await view.delete();
await table.delete();
done();
},
{ mode: "row" },
);
await table.update({ x: [1, 3] });
},
);
it_old_behavior(
"`on_update` on a view with expression column should contain expression delta when columns are updated",
async function (done) {
const table = await perspective.table(
{
x: [1, 2, 3, 4],
y: ["A", "B", "C", "D"],
},
{ index: "x" },
);
const view = await table.view({
expressions: { 'lower("y")': 'lower("y")' },
});
const pre_update = await view.to_columns();
expect(pre_update).toEqual({
x: [1, 2, 3, 4],
y: ["A", "B", "C", "D"],
'lower("y")': ["a", "b", "c", "d"],
});
view.on_update(
async function (updated) {
const expected = [
{ x: 1, y: "ABCD", 'lower("y")': "abcd" },
{ x: 3, y: null, 'lower("y")': null },
];
const full = await view.to_columns();
expect(full).toEqual({
x: [1, 2, 3, 4],
y: ["ABCD", "B", null, "D"],
'lower("y")': ["abcd", "b", null, "d"],
});
await match_delta(
perspective,
updated.delta,
expected,
);
await view.delete();
await table.delete();
done();
},
{ mode: "row" },
);
await table.update({ x: [1, 3], y: ["ABCD", null] });
},
);
it_old_behavior(
"`on_update` on different views with different expression columns should only be notified of their columns",
async function (done) {
const table = await perspective.table(
{
x: [1, 2, 3, 4],
y: ["A", "B", "C", "D"],
},
{
index: "x",
},
);
const view = await table.view({
expressions: { 'lower("y")': 'lower("y")' },
});
const view2 = await table.view({
expressions: { '-"x"': '-"x"' },
});
view.on_update(
async function (updated) {
const expected = [
{ x: 1, y: "HELLO", 'lower("y")': "hello" },
{ x: 3, y: "WORLD", 'lower("y")': "world" },
];
const full = await view.to_columns();
expect(full).toEqual({
x: [1, 2, 3, 4],
y: ["HELLO", "B", "WORLD", "D"],
'lower("y")': ["hello", "b", "world", "d"],
});
await match_delta(
perspective,
updated.delta,
expected,
);
},
{ mode: "row" },
);
view2.on_update(
async function (updated) {
const expected = [
{ x: 1, y: "HELLO", '-"x"': -1 },
{ x: 3, y: "WORLD", '-"x"': -3 },
];
const full = await view2.to_columns();
expect(full).toEqual({
x: [1, 2, 3, 4],
y: ["HELLO", "B", "WORLD", "D"],
'-"x"': [-1, -2, -3, -4],
});
await match_delta(
perspective,
updated.delta,
expected,
);
await view.delete();
await view2.delete();
await table.delete();
done();
},
{ mode: "row" },
);
await table.update({ x: [1, 3], y: ["HELLO", "WORLD"] });
},
);
it_old_behavior(
"`on_update` on view without expression column should not be notified of expression column",
async function (done) {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: ["A", "B", "C", "D"],
});
const view = await table.view();
const view2 = await table.view({
expressions: { 'lower("y")': 'lower("y")' },
});
expect(await view2.to_columns()).toEqual({
x: [1, 2, 3, 4],
y: ["A", "B", "C", "D"],
'lower("y")': ["a", "b", "c", "d"],
});
view.on_update(
async function (updated) {
const expected = [
{ x: 1, y: "abc" },
{ x: 3, y: "def" },
];
await match_delta(
perspective,
updated.delta,
expected,
);
await view2.delete();
await view.delete();
await table.delete();
done();
},
{ mode: "row" },
);
await table.update({ x: [1, 3], y: ["abc", "def"] });
},
);
});
});
})(perspective);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,385 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
const jsc = require("jsverify");
const replicate = (n, g) => jsc.tuple(new Array(n).fill(g));
const array_equals = function (a, b) {
return JSON.stringify(a) === JSON.stringify(b);
};
const generator = function (length = 100, has_zero = true) {
const min = has_zero ? 0 : 1;
return jsc.record({
a: replicate(length, jsc.number(min, 1000000)),
b: replicate(length, jsc.number(min, 1000000)),
c: replicate(length, jsc.string),
d: replicate(length, jsc.datetime),
e: replicate(length, jsc.bool),
});
};
/**
* Uses invariant testing to assert base correctness of computed columns.
*/
((perspective) => {
test.describe("Invariant testing", function () {
test.describe("Inverse expressions should be invariant", function () {
jsc.property("(x - y) + x == y", generator(), async (data) => {
const table = await perspective.table(data);
const view = await table.view({
expressions: { '("a" - "b") + "b"': '("a" - "b") + "b"' },
});
const result = await view.to_columns();
const expected = array_equals(
result['("a" - "b") + "b"'],
data["a"],
);
view.delete();
table.delete();
return expected;
});
jsc.property("(x + y) - x - y == 0", generator(), async (data) => {
const table = await perspective.table(data);
const view = await table.view({
expressions: {
'("a" + "b") - "a" - "b"': '("a" + "b") - "a" - "b"',
},
});
const result = await view.to_columns();
const expected = array_equals(
result['("a" + "b") - "a" - "b"'],
Array(data["a"].length).fill(0),
);
view.delete();
table.delete();
return expected;
});
jsc.property(
"(x + y) - (x + y) == 0",
generator(),
async (data) => {
const table = await perspective.table(data);
const view = await table.view({
expressions: {
'("a" + "b") - ("a" + "b")':
'("a" + "b") - ("a" + "b")',
},
});
const result = await view.to_columns();
const expected = array_equals(
result['("a" + "b") - ("a" + "b")'],
Array(data["a"].length).fill(0),
);
view.delete();
table.delete();
return expected;
},
);
jsc.property("(x - x) == 0", generator(), async (data) => {
const table = await perspective.table(data);
const view = await table.view({
expressions: { '"a" - "a"': '"a" - "a"' },
});
const result = await view.to_columns();
const expected = array_equals(
result['"a" - "a"'],
Array(data["a"].length).fill(0),
);
view.delete();
table.delete();
return expected;
});
jsc.property("(x / x) == 1", generator(), async (data) => {
const table = await perspective.table(data);
const view = await table.view({
expressions: { '"a" / "a"': '"a" / "a"' },
});
const result = await view.to_columns();
const expected = array_equals(
result['"a" / "a"'],
Array(data["a"].length).fill(1),
);
view.delete();
table.delete();
return expected;
});
jsc.property("(x + x) - x - x == 0", generator(), async (data) => {
const table = await perspective.table(data);
const view = await table.view({
expressions: {
'("a" + "a") - "a" - "a"': '("a" + "a") - "a" - "a"',
},
});
const result = await view.to_columns();
const expected = array_equals(
result['("a" + "a") - "a" - "a"'],
Array(data["a"].length).fill(0),
);
view.delete();
table.delete();
return expected;
});
jsc.property(
"sqrt(x ^ 2) == x",
generator(100, false),
async (data) => {
const table = await perspective.table(data);
const view = await table.view({
expressions: {
'sqrt(pow("a", 2))': 'sqrt(pow("a", 2))',
},
});
const result = await view.to_columns();
const expected = array_equals(
result['sqrt(pow("a", 2))'],
data["a"],
);
view.delete();
table.delete();
return expected;
},
);
// jsc.property(
// "x ^ 2 == (x * x)",
// generator(100, false),
// async (data) => {
// const table = await perspective.table(data);
// const view = await table.view({
// expressions: ['pow("a", 2)', '"a" * "a"'].reduce((x, y) => Object.assign(x, {[y]: y}), {}),
// });
// const result = await view.to_columns();
// const expected = array_equals(
// result['pow("a", 2)'],
// result['"a" * "a"']
// );
// view.delete();
// table.delete();
// return expected;
// }
// );
jsc.property(
"x % x == 100",
generator(100, false),
async (data) => {
const table = await perspective.table(data);
const view = await table.view({
expressions: {
'percent_of("a", "a")': 'percent_of("a", "a")',
},
});
const result = await view.to_columns();
const expected = array_equals(
result['percent_of("a", "a")'],
Array(data["a"].length).fill(100),
);
view.delete();
table.delete();
return expected;
},
);
jsc.property(
"abs(x) ? x > 0 == x",
generator(100, false),
async (data) => {
const table = await perspective.table(data);
const view = await table.view({
expressions: { 'abs("a")': 'abs("a")' },
});
const result = await view.to_columns();
const expected = array_equals(
result['abs("a")'],
data["a"],
);
view.delete();
table.delete();
return expected;
},
);
});
test.describe("Comparison operations should be pure with the same inputs.", function () {
jsc.property(
"== should always be true with the same input column",
generator(),
async (data) => {
const table = await perspective.table(data);
const view = await table.view({
expressions: { '"a" == "a"': '"a" == "a"' },
});
const result = await view.to_columns();
const expected = array_equals(
result['"a" == "a"'],
Array(data["a"].length).fill(true),
);
view.delete();
table.delete();
return expected;
},
);
jsc.property(
"> should always be false with the same input column",
generator(),
async (data) => {
const table = await perspective.table(data);
const view = await table.view({
expressions: { '"a" > "a"': '"a" > "a"' },
});
const result = await view.to_columns();
const expected = array_equals(
result['"a" > "a"'],
Array(data["a"].length).fill(false),
);
view.delete();
table.delete();
return expected;
},
);
jsc.property(
"< should always be false with the same input column",
generator(),
async (data) => {
const table = await perspective.table(data);
const view = await table.view({
expressions: { '"a" < "a"': '"a" < "a"' },
});
const result = await view.to_columns();
const expected = array_equals(
result['"a" < "a"'],
Array(data["a"].length).fill(false),
);
view.delete();
table.delete();
return expected;
},
);
jsc.property(
"String == should always be true with the same input column",
generator(),
async (data) => {
const table = await perspective.table({
a: "float",
b: "float",
c: "string",
d: "datetime",
e: "boolean",
});
await table.update(data);
const view = await table.view({
expressions: { '"c" == "c"': '"c" == "c"' },
});
const result = await view.to_columns();
const expected = array_equals(
result['"c" == "c"'],
Array(data["c"].length).fill(true),
);
view.delete();
table.delete();
return expected;
},
);
jsc.property(
"Datetime == should always be true with the same input column",
generator(),
async (data) => {
const table = await perspective.table({
a: "float",
b: "float",
c: "string",
d: "datetime",
e: "boolean",
});
await table.update(data);
const view = await table.view({
expressions: { '"d" == "d"': '"d" == "d"' },
});
const result = await view.to_columns();
const expected = array_equals(
result['"d" == "d"'],
Array(data["d"].length).fill(true),
);
view.delete();
table.delete();
return expected;
},
);
jsc.property(
"Date == should always be true with the same input column",
generator(),
async (data) => {
const table = await perspective.table({
a: "float",
b: "float",
c: "string",
d: "date",
e: "boolean",
});
await table.update(data);
const view = await table.view({
expressions: { '"d" == "d"': '"d" == "d"' },
});
const result = await view.to_columns();
const expected = array_equals(
result['"d" == "d"'],
Array(data["d"].length).fill(true),
);
view.delete();
table.delete();
return expected;
},
);
});
});
})(perspective);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,71 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
import * as common from "./common.js";
/**
* Tests the correctness of each datetime computation function in various
* environments and parameters - different types, nulls, undefined, etc.
*/
((perspective) => {
test.describe("bucket() function", function () {
test.describe("parses arguments separated by arbitrary whitespace", function () {
for (const [title, expression] of [
["space before", `bucket( "a",'Y')`],
["space after", `bucket("a",'Y' )`],
["space between", `bucket("a", 'Y')`],
["two spaces before", `bucket( "a",'Y')`],
["two spaces after", `bucket("a",'Y' )`],
["two spaces between", `bucket("a", 'Y')`],
["space before between and after", `bucket( "a", 'Y' )`],
[
"two spaces before between and after",
`bucket( "a", 'Y' )`,
],
]) {
test(title, async function () {
const table = await perspective.table({
a: "datetime",
});
const view = await table.view({
expressions: [expression].reduce(
(x, y) => Object.assign(x, { [y]: y }),
{},
),
});
await table.update({
a: [
new Date(2020, 0, 12),
new Date(2020, 0, 15),
new Date(2021, 11, 17),
new Date(2019, 2, 18),
new Date(2019, 2, 29),
],
});
let result = await view.to_columns();
expect(
result[expression].map((x) => (x ? new Date(x) : null)),
).toEqual(result.a.map((x) => common.year_bucket(x)));
view.delete();
table.delete();
});
}
});
});
})(perspective);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,143 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
// Concrete use cases from issue #1527 ("Better string functions in
// expressions"). The original issue body sketched these in a hypothetical
// dialect; the tests below port them to the dialect Perspective actually
// implements. Differences from the issue's pseudocode:
//
// - `find(str, substr) -> int` does not exist. The closest function is
// `indexof(col, regex, out_vec) -> bool`, which performs a *regex* search,
// writes [start, end] of the first capturing group into `out_vec`, and
// requires the regex to have at least one capturing group (else it
// returns STATUS_CLEAR). The tests therefore wrap the literal char in a
// capturing group: `' '` -> `'( )'`, `','` -> `'(,)'`, `$` -> `'([$])'`.
// - `null()` is not a function call; `null` is a literal.
// - `strlen(s)` -> `length(s)`.
// - `substring(s, start, count)` takes a *count*, not an end index, and
// returns null if `start + count > length(s)`.
// - String literals pass through ExprTK's `cleanup_escapes`, which drops
// unrecognized escape characters (`\s` -> `s`, `\.` -> `.`).
((perspective) => {
test.describe("Issue 1527 use cases", function () {
test("contains literal substring", async function () {
const table = await perspective.table({
a: ["abcdef", "xyz", "abXabY", null, "abc"],
});
const view = await table.view({
expressions: {
has_ab: "contains(\"a\", 'ab')",
},
});
const result = await view.to_columns();
const schema = await view.expression_schema();
expect(schema["has_ab"]).toEqual("boolean");
expect(result["has_ab"]).toEqual([true, false, true, null, true]);
view.delete();
table.delete();
});
// Parse "USD $1000"-style strings into Currency (string) and Value
// (float) columns, tolerant of malformed rows.
test("split currency/value string column", async function () {
const table = await perspective.table({
"Bad Column": [
"USD $1000",
"EUR $250",
"malformed",
null,
"GBP $42",
],
});
const view = await table.view({
expressions: {
Currency: `var v[2];
if (indexof("Bad Column", '( )', v)) { substring("Bad Column", 0, v[0]) } else { null }`,
Value: `var v[2];
if (indexof("Bad Column", '([$])', v)) { float(substring("Bad Column", v[0] + 1)) } else { null }`,
},
});
const result = await view.to_columns();
const schema = await view.expression_schema();
expect(schema["Currency"]).toEqual("string");
expect(schema["Value"]).toEqual("float");
expect(result["Currency"]).toEqual([
"USD",
"EUR",
null,
null,
"GBP",
]);
expect(result["Value"]).toEqual([1000, 250, null, null, 42]);
view.delete();
table.delete();
});
// Parse "(123, 456)"-style strings into Longitude and Latitude
// float columns.
test("split longitude/latitude string column", async function () {
const table = await perspective.table({
Coords: [
"(123, 456)",
"(1.5, -2.25)",
"broken",
null,
"(0, 0)",
],
});
const view = await table.view({
expressions: {
Longitude: `var v[2];
if (indexof("Coords", '(,)', v)) { float(substring("Coords", 1, v[0] - 1)) } else { null }`,
Latitude: `var v[2];
if (indexof("Coords", '(,)', v)) { float(substring("Coords", v[0] + 1, length("Coords") - v[0] - 2)) } else { null }`,
},
});
const result = await view.to_columns();
const schema = await view.expression_schema();
expect(schema["Longitude"]).toEqual("float");
expect(schema["Latitude"]).toEqual("float");
expect(result["Longitude"]).toEqual([123, 1.5, null, null, 0]);
expect(result["Latitude"]).toEqual([456, -2.25, null, null, 0]);
view.delete();
table.delete();
});
// Normalize spelling variants by stripping dots and whitespace.
test("replace_all regex strips dots/whitespace", async function () {
const table = await perspective.table({
State: ["NC", "N.C.", "N. C.", "N .C.", "VA"],
});
const view = await table.view({
expressions: {
Normalized: `replace_all("State", '[. ]', '')`,
},
});
const result = await view.to_columns();
expect(result["Normalized"]).toEqual([
"NC",
"NC",
"NC",
"NC",
"VA",
]);
view.delete();
table.delete();
});
});
})(perspective);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,154 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
import * as common from "./common.js";
/**
* Tests the correctness of user-defined vectors inside expressions.
*
* @param {*} perspective
*/
((perspective) => {
test.describe("Vectors", () => {
test("Create vector and return value", async () => {
const table = await perspective.table(common.int_float_data);
const view = await table.view({
expressions: { a: `var vec[3] := {1, "w", 2}; vec[1]` },
});
expect(await view.expression_schema()).toEqual({
a: "float",
});
const result = await view.to_columns();
expect(result["a"]).toEqual(result["w"]);
await view.delete();
await table.delete();
});
test("Return from empty vector should be 0", async () => {
const table = await perspective.table(common.int_float_data);
const view = await table.view({
expressions: { a: `var vec[3]; vec[1]` },
});
expect(await view.expression_schema()).toEqual({
a: "float",
});
const result = await view.to_columns();
expect(result["a"]).toEqual(Array(4).fill(0));
await view.delete();
await table.delete();
});
test("Dynamic return types from vector", async () => {
const table = await perspective.table(common.int_float_data);
const view = await table.view({
expressions: {
[`a`]: `var vec[3] := {'abc', 123, today()}; vec[0]`,
[`b`]: `var vec[3] := {'abc', 123, today()}; vec[1]`,
[`c`]: `var vec[3] := {'abc', 123, date(2020, 5, 23)}; vec[2]`,
[`d`]: `var vec[3] := {'abc', 123, is_null(null)}; vec[2]`,
},
});
expect(await view.expression_schema()).toEqual({
a: "string",
b: "float",
c: "date",
d: "boolean",
});
const result = await view.to_columns();
expect(result["a"]).toEqual(Array(4).fill("abc"));
expect(result["b"]).toEqual(Array(4).fill(123));
expect(result["c"]).toEqual(
Array(4).fill(new Date(2020, 4, 23).getTime()),
);
expect(result["d"]).toEqual(Array(4).fill(true));
await view.delete();
await table.delete();
});
test("Use vector items as inputs", async () => {
const table = await perspective.table(common.int_float_data);
const view = await table.view({
expressions: {
[`a`]: `var vec[2] := {"w", "x"}; vec[0] * vec[1]`,
},
});
expect(await view.expression_schema()).toEqual({
a: "float",
});
const result = await view.to_columns();
expect(result["a"]).toEqual(
result["w"].map((item, idx) => item * result["x"][idx]),
);
await view.delete();
await table.delete();
});
test("Custom function takes vector item input", async () => {
const table = await perspective.table(common.int_float_data);
const view = await table.view({
expressions: {
[`a`]: `var vec[2] := {"w", "x"}; max(vec[0], vec[1])`,
},
});
expect(await view.expression_schema()).toEqual({
a: "float",
});
const result = await view.to_columns();
expect(result["a"]).toEqual(
result["w"].map((item, idx) =>
Math.max(item, result["x"][idx]),
),
);
await view.delete();
await table.delete();
});
test("`norm3` with a vector shorter than 3 is rejected", async () => {
const table = await perspective.table(common.int_float_data);
await expect(
table.view({
expressions: { a: `var v[2] := {1, 2}; norm3(v)` },
}),
).rejects.toThrow();
await table.delete();
});
test("`diff3` with vectors shorter than 3 is rejected", async () => {
const table = await perspective.table(common.int_float_data);
await expect(
table.view({
expressions: {
a: `var x[2] := {1, 2}; var y[2] := {3, 4}; var o[2]; diff3(x, y, o)`,
},
}),
).rejects.toThrow();
await table.delete();
});
test("`norm3` with a 3-element vector still computes", async () => {
const table = await perspective.table(common.int_float_data);
const view = await table.view({
expressions: { a: `var v[3] := {3, 4, 0}; norm3(v)` },
});
const result = await view.to_columns();
expect(result["a"]).toEqual(Array(4).fill(5)); // sqrt(9+16+0)
await view.delete();
await table.delete();
});
});
})(perspective);
+993
View File
@@ -0,0 +1,993 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
import * as arrows from "./test_arrows.js";
var yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
var now = new Date();
var data = [
{ w: now, x: 1, y: "a", z: true },
{ w: now, x: 2, y: "b", z: false },
{ w: now, x: 3, y: "c", z: true },
{ w: yesterday, x: 4, y: "d", z: false },
];
var rdata = [
{ w: +now, x: 1, y: "a", z: true },
{ w: +now, x: 2, y: "b", z: false },
{ w: +now, x: 3, y: "c", z: true },
{ w: +yesterday, x: 4, y: "d", z: false },
];
// starting from 09/01/2018 to 12/01/2018
var date_range_data = [
{ w: new Date(1535778060000), x: 1, y: "a", z: true }, // Sat Sep 01 2018 01:01:00 GMT-0400
{ w: new Date(1538370060000), x: 2, y: "b", z: false }, // Mon Oct 01 2018 01:01:00 GMT-0400
{ w: new Date(1541048460000), x: 3, y: "c", z: true }, // Thu Nov 01 2018 01:01:00 GMT-0400
{ w: new Date(1543644060000), x: 4, y: "d", z: false }, // Sat Dec 01 2018 01:01:00 GMT-0500
];
var r_date_range_data = [
{ w: +new Date(1535778060000), x: 1, y: "a", z: true },
{ w: +new Date(1538370060000), x: 2, y: "b", z: false },
{ w: +new Date(1541048460000), x: 3, y: "c", z: true },
{ w: +new Date(1543644060000), x: 4, y: "d", z: false },
];
const datetime_data = [
{ x: new Date(2019, 1, 28, 23, 59, 59) }, // 2019/02/28 23:59:59 GMT-0500
{ x: new Date(2020, 1, 29, 0, 0, 1) }, // 2020/02/29 00:00:01 GMT-0500
{ x: new Date(2020, 2, 8, 1, 59, 59) }, // 2020/03/8 01:59:59 GMT-0400
{ x: new Date(2020, 2, 8, 2, 0, 1) }, // 2020/03/8 02:00:01 GMT-0500
{ x: new Date(2020, 9, 1, 15, 11, 55) }, // 2020/10/01 15:30:55 GMT-0400
{ x: new Date(2020, 10, 1, 19, 29, 55) }, // 2020/11/01 19:30:55 GMT-0400
{ x: new Date(2020, 11, 31, 7, 42, 55) }, // 2020/12/31 07:30:55 GMT-0500
];
const datetime_data_local = [
{ x: new Date(2019, 1, 28, 23, 59, 59).toLocaleString() }, // 2019/02/28 23:59:59 GMT-0500
{ x: new Date(2020, 1, 29, 0, 0, 1).toLocaleString() }, // 2020/02/29 00:00:01 GMT-0500
{ x: new Date(2020, 2, 8, 1, 59, 59).toLocaleString() }, // 2020/03/8 01:59:59 GMT-0400
{ x: new Date(2020, 2, 8, 2, 0, 1).toLocaleString() }, // 2020/03/8 02:00:01 GMT-0500
{ x: new Date(2020, 9, 1, 15, 11, 55).toLocaleString() }, // 2020/10/01 15:30:55 GMT-0400
{ x: new Date(2020, 10, 1, 19, 29, 55).toLocaleString() }, // 2020/11/01 19:30:55 GMT-0400
{ x: new Date(2020, 11, 31, 7, 42, 55).toLocaleString() }, // 2020/12/31 07:30:55 GMT-0500
];
((perspective) => {
test.describe("Filters", function () {
test.describe("GT & LT", function () {
test("filters on long strings", async function () {
var table = await perspective.table([
{ x: 1, y: "123456789012a", z: true },
{ x: 2, y: "123456789012a", z: false },
{ x: 3, y: "123456789012b", z: true },
{ x: 4, y: "123456789012b", z: false },
]);
var view = await table.view({
filter: [["y", "contains", "123456789012a"]],
});
let json = await view.to_json();
expect(json.length).toEqual(2);
view.delete();
table.delete();
});
test("x > 2", async function () {
var table = await perspective.table(data);
var view = await table.view({
filter: [["x", ">", 2.0]],
});
let json = await view.to_json();
expect(json).toEqual(rdata.slice(2));
view.delete();
table.delete();
});
test("x < 3", async function () {
var table = await perspective.table(data);
var view = await table.view({
filter: [["x", "<", 3.0]],
});
let json = await view.to_json();
expect(json).toEqual(rdata.slice(0, 2));
view.delete();
table.delete();
});
test("x > 4", async function () {
var table = await perspective.table(data);
var view = await table.view({
filter: [["x", ">", 4]],
});
let json = await view.to_json();
expect(json).toEqual([]);
view.delete();
table.delete();
});
test("x < 0", async function () {
var table = await perspective.table(data);
var view = await table.view({
filter: [["x", ">", 4]],
});
let json = await view.to_json();
expect(json).toEqual([]);
view.delete();
table.delete();
});
test("w > datetime as string", async function () {
var table = await perspective.table(date_range_data);
var view = await table.view({
filter: [["w", ">", "10/01/2018"]],
});
let json = await view.to_json();
expect(json).toEqual(r_date_range_data.slice(1, 4));
view.delete();
table.delete();
});
test("w < datetime as string", async function () {
var table = await perspective.table(date_range_data);
var view = await table.view({
filter: [["w", "<", "10/01/2018"]],
});
let json = await view.to_json();
expect(json).toEqual([r_date_range_data[0]]);
view.delete();
table.delete();
});
test.describe("filtering on date column", function () {
const schema = {
w: "date",
};
const date_results = [
{ w: +new Date(1535760000000) }, // Fri Aug 31 2018 20:00:00 GMT-0400
{ w: +new Date(1538352000000) }, // Sun Sep 30 2018 20:00:00 GMT-0400
{ w: +new Date(1541030400000) }, // Wed Oct 31 2018 20:00:00 GMT-0400
{ w: +new Date(1543622400000) }, // Fri Nov 30 2018 19:00:00 GMT-0500
];
test("w > date as string", async function () {
var table = await perspective.table(schema);
await table.update(date_results);
var view = await table.view({
filter: [["w", ">", "10/02/2018"]],
});
let json = await view.to_json();
expect(json).toEqual(date_results.slice(2, 4));
view.delete();
table.delete();
});
test("w > date as number", async function () {
var table = await perspective.table(schema);
await table.update(date_results);
var view = await table.view({
filter: [["w", ">", 1538352000000]],
});
let json = await view.to_json();
expect(json).toEqual(date_results.slice(2, 4));
view.delete();
table.delete();
});
test("w < date as string", async function () {
var table = await perspective.table(schema);
await table.update(date_results);
var view = await table.view({
filter: [["w", "<", "10/02/2018"]],
});
let json = await view.to_json();
expect(json).toEqual(date_results.slice(0, 2));
view.delete();
table.delete();
});
});
});
test.describe("EQ", function () {
test("x == 1", async function () {
var table = await perspective.table(data);
var view = await table.view({
filter: [["x", "==", 1]],
});
let json = await view.to_json();
expect(json).toEqual(rdata.slice(0, 1));
view.delete();
table.delete();
});
test("empty, pivoted", async function () {
var table = await perspective.table(data);
var view = await table.view({
group_by: ["y"],
filter: [["x", "==", 100]],
});
let json = await view.to_json();
expect(json).toEqual([
// empty sum
{ __ROW_PATH__: [], w: 0, x: null, y: 0, z: 0 },
]);
view.delete();
table.delete();
});
test("x == 1, rolling updates", async function () {
var table = await perspective.table(data);
var view = await table.view({
columns: ["x"],
filter: [["x", "==", 1]],
});
let json = await view.to_json();
expect(json).toEqual([{ x: 1 }]);
for (let i = 0; i < 5; i++) {
await table.update([{ x: 1 }]);
}
expect(await view.to_columns()).toEqual({
x: [1, 1, 1, 1, 1, 1],
});
await table.update([{ x: 2 }]);
expect(await view.to_columns()).toEqual({
x: [1, 1, 1, 1, 1, 1],
});
view.delete();
table.delete();
});
test.skip("x == 1 pivoted, rolling updates", async function () {
var table = await perspective.table(
{
a: [1, 2, 3, 4],
b: ["a", "b", "c", "d"],
c: ["A", "B", "C", "D"],
},
{
index: "c",
},
);
var view = await table.view({
group_by: ["a"],
columns: ["b", "c"],
});
let out = await view.to_columns();
expect(out["__ROW_PATH__"]).toEqual([[], [1], [2], [3], [4]]);
for (let i = 0; i < 5; i++) {
await table.update([{ x: 1 }]);
}
expect(await view.to_columns()).toEqual({
x: [1, 1, 1, 1, 1, 1],
});
await table.update([{ x: 2 }]);
expect(await view.to_columns()).toEqual({
x: [1, 1, 1, 1, 1, 1],
});
view.delete();
table.delete();
});
test("x == 5", async function () {
var table = await perspective.table(data);
var view = await table.view({
filter: [["x", "==", 5]],
});
let json = await view.to_json();
expect(json).toEqual([]);
view.delete();
table.delete();
});
test("y == 'a'", async function () {
var table = await perspective.table(data);
var view = await table.view({
filter: [["y", "==", "a"]],
});
let json = await view.to_json();
expect(json).toEqual(rdata.slice(0, 1));
view.delete();
table.delete();
});
test("y == 'e'", async function () {
var table = await perspective.table(data);
var view = await table.view({
filter: [["y", "==", "e"]],
});
let json = await view.to_json();
expect(json).toEqual([]);
view.delete();
table.delete();
});
test("y == 'abcdefghijklmnopqrstuvwxyz' (interned)", async function () {
const data = [
{ x: 1, y: "a", z: true },
{ x: 2, y: "b", z: false },
{ x: 3, y: "c", z: true },
{
x: 4,
y: "abcdefghijklmnopqrstuvwxyz",
z: false,
},
];
const table = await perspective.table(data);
const view = await table.view({
filter: [["y", "==", "abcdefghijklmnopqrstuvwxyz"]],
});
const json = await view.to_json();
expect(json).toEqual([data[3]]);
view.delete();
table.delete();
});
test("z == true", async function () {
var table = await perspective.table(data);
var view = await table.view({
filter: [["z", "==", true]],
});
let json = await view.to_json();
expect(json).toEqual([rdata[0], rdata[2]]);
view.delete();
table.delete();
});
test("z == false", async function () {
var table = await perspective.table(data);
var view = await table.view({
filter: [["z", "==", false]],
});
let json = await view.to_json();
expect(json).toEqual([rdata[1], rdata[3]]);
view.delete();
table.delete();
});
test("w == yesterday", async function () {
var table = await perspective.table(data);
var view = await table.view({
filter: [["w", "==", yesterday]],
});
let json = await view.to_json();
expect(json).toEqual([rdata[3]]);
view.delete();
table.delete();
});
test("w != yesterday", async function () {
var table = await perspective.table(data);
var view = await table.view({
filter: [["w", "!=", yesterday]],
});
let json = await view.to_json();
expect(json).toEqual(rdata.slice(0, 3));
view.delete();
table.delete();
});
test("w == datetime as Date() object", async function () {
const table = await perspective.table({ x: "datetime" });
await table.update(datetime_data);
expect(await table.schema()).toEqual({
x: "datetime",
});
const view = await table.view({
filter: [["x", "==", datetime_data[0]["x"]]],
});
expect(await view.num_rows()).toBe(1);
let data = await view.to_json();
data = data.map((d) => {
d.x = new Date(d.x);
return d;
});
expect(data).toEqual(datetime_data.slice(0, 1));
await view.delete();
await table.delete();
});
test("w == datetime as Number", async function () {
const table = await perspective.table({ x: "datetime" });
await table.update(datetime_data);
expect(await table.schema()).toEqual({
x: "datetime",
});
const view = await table.view({
filter: [["x", "==", +datetime_data[0]["x"]]],
});
expect(await view.num_rows()).toBe(1);
let data = await view.to_json();
data = data.map((d) => {
d.x = new Date(d.x);
return d;
});
expect(data).toEqual(datetime_data.slice(0, 1));
await view.delete();
await table.delete();
});
test("w == datetime as US locale string", async function () {
const table = await perspective.table({ x: "datetime" });
await table.update(datetime_data);
expect(await table.schema()).toEqual({
x: "datetime",
});
const view = await table.view({
filter: [["x", "==", datetime_data_local[0]["x"]]],
});
expect(await view.num_rows()).toBe(1);
let data = await view.to_json();
data = data.map((d) => {
d.x = new Date(d.x);
return d;
});
expect(data).toEqual(datetime_data.slice(0, 1));
await view.delete();
await table.delete();
});
});
test.describe("in", function () {
test("y in ['a', 'b']", async function () {
var table = await perspective.table(data);
var view = await table.view({
filter: [["y", "in", ["a", "b"]]],
});
let json = await view.to_json();
expect(json).toEqual(rdata.slice(0, 2));
view.delete();
table.delete();
});
});
test.describe("not in", function () {
test("y not in ['d']", async function () {
var table = await perspective.table(data);
var view = await table.view({
filter: [["y", "not in", ["d"]]],
});
let json = await view.to_json();
expect(json).toEqual(rdata.slice(0, 3));
view.delete();
table.delete();
});
});
test.describe("contains", function () {
test("y contains 'a'", async function () {
var table = await perspective.table(data);
var view = await table.view({
filter: [["y", "contains", "a"]],
});
let json = await view.to_json();
expect(rdata.slice(0, 1)).toEqual(json);
view.delete();
table.delete();
});
});
test.describe("Arrow types", function () {
// https://github.com/perspective-dev/perspective/issues/2881
test("Arrow float32 filters", async function () {
const table = await perspective.table(
arrows.float32_arrow.slice(),
);
const view = await table.view({ filter: [["score", "<", 93]] });
const result = await view.to_columns();
expect(result).toEqual({
id: [1, 2],
name: ["Alice", "Bob"],
score: [92.5, 87.30000305175781],
});
const cfg = await view.get_config();
expect(cfg).toEqual({
aggregates: {},
columns: ["id", "name", "score"],
expressions: {},
filter: [["score", "<", 93]],
group_by: [],
sort: [],
split_by: [],
group_rollup_mode: "rollup",
});
view.delete();
table.delete();
});
});
test.describe("multiple", function () {
test("x > 1 & x < 4", async function () {
var table = await perspective.table(data);
var view = await table.view({
filter: [
["x", ">", 1],
["x", "<", 4],
],
});
let json = await view.to_json();
expect(json).toEqual(rdata.slice(1, 3));
view.delete();
table.delete();
});
test("y contains 'a' OR y contains 'b'", async function () {
var table = await perspective.table(data);
// when `filter_op` is provided, perspective returns data differently. In this case, returned data should satisfy either/or of the filter conditions.
var view = await table.view({
filter_op: "or",
filter: [
["y", "contains", "a"],
["y", "contains", "b"],
],
});
let json = await view.to_json();
expect(json).toEqual(rdata.slice(0, 2));
view.delete();
table.delete();
});
});
test.describe("long strings", function () {
test("", async function () {
const data = [
{
field: 100,
index: "1",
title: "s15",
ts: "2024-11-30T06:50:10.214Z",
},
{
field: 100,
index: "2",
title: "sys_c_c 15min",
ts: "2024-11-30T07:06:00.763Z",
},
{
field: 100,
index: "3",
title: "s15",
ts: "2024-12-01T06:50:15.596Z",
},
{
field: 100,
index: "4",
title: "sys_c_c 15min",
ts: "2024-12-01T07:06:04.909Z",
},
{
field: 100,
index: "5",
title: "s15",
ts: "2024-12-02T06:50:24.712Z",
},
{
field: 100,
index: "6",
title: "sys_c_c 15min",
ts: "2024-12-02T07:06:15.339Z",
},
{
field: 100,
index: "7",
title: "s15",
ts: "2024-12-03T06:50:22.144Z",
},
{
field: 100,
index: "8",
title: "sys_c_c 15min",
ts: "2024-12-03T07:06:20.146Z",
},
];
const config = {
group_by: ["ts"],
split_by: [],
columns: ["field"],
filter: [
["title", "in", ["sys_c_c 15min"]],
["ts", ">=", "2024-12-01T00:00:00.000Z"],
["ts", "<=", "2024-12-02T23:59:59.999Z"],
],
expressions: {},
aggregates: {},
};
const table = await perspective.table(
{
title: "string",
field: "float",
ts: "datetime",
index: "string",
},
{ index: "index" },
);
let x, y;
let xx = new Promise((xxx) => {
x = xxx;
});
let yy = new Promise((yyy) => {
y = yyy;
});
await table.view(config).then((view) => {
view.on_update(() => view.to_json().then(x));
});
await table.view(config).then((view) => {
view.on_update(() => view.to_json().then(y));
});
await table.update(data);
const [xxx, yyy] = await Promise.all([xx, yy]);
expect(xxx).toEqual(yyy);
});
});
test.describe("is null", function () {
test("returns the correct null cells for string column", async function () {
const table = await perspective.table([
{ x: 1, y: null },
{ x: 2, y: null },
{ x: 3, y: "x" },
{ x: 4, y: "x" },
{ x: 1, y: "y" },
{ x: 2, y: "x" },
{ x: 3, y: "y" },
]);
const view = await table.view({
filter: [["y", "is null"]],
});
const answer = [
{ x: 1, y: null },
{ x: 2, y: null },
];
const result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test("returns the correct null cells for integer column", async function () {
const table = await perspective.table([
{ x: 1, y: null },
{ x: 2, y: null },
{ x: 3, y: 1 },
{ x: 4, y: 2 },
{ x: 1, y: 3 },
{ x: 2, y: 4 },
{ x: 3, y: 5 },
]);
const view = await table.view({
filter: [["y", "is null"]],
});
const answer = [
{ x: 1, y: null },
{ x: 2, y: null },
];
const result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test("returns the correct null cells for datetime column", async function () {
const table = await perspective.table([
{ x: 1, y: null },
{ x: 2, y: null },
{ x: 3, y: "1/1/2019" },
{ x: 4, y: "1/1/2019" },
{ x: 1, y: "1/1/2019" },
{ x: 2, y: "1/1/2019" },
{ x: 3, y: "1/1/2019" },
]);
const view = await table.view({
filter: [["y", "is null"]],
});
const answer = [
{ x: 1, y: null },
{ x: 2, y: null },
];
const result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
});
test.describe("nulls", function () {
test("x > 2", async function () {
var table = await perspective.table([
{ x: 3, y: 1 },
{ x: 2, y: 1 },
{ x: null, y: 1 },
{ x: null, y: 1 },
{ x: 4, y: 2 },
{ x: null, y: 2 },
]);
var view = await table.view({
filter: [["x", ">", 2]],
});
var answer = [
{ x: 3, y: 1 },
{ x: 4, y: 2 },
];
let result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test("x < 3", async function () {
var table = await perspective.table([
{ x: 3, y: 1 },
{ x: 2, y: 1 },
{ x: null, y: 1 },
{ x: null, y: 1 },
{ x: 4, y: 2 },
{ x: null, y: 2 },
]);
var view = await table.view({
filter: [["x", "<", 3]],
});
var answer = [{ x: 2, y: 1 }];
let result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test("x > 2.5", async function () {
var table = await perspective.table({
x: "float",
y: "integer",
});
await table.update([
{ x: 3.5, y: 1 },
{ x: 2.5, y: 1 },
{ x: null, y: 1 },
{ x: null, y: 1 },
{ x: 4.5, y: 2 },
{ x: null, y: 2 },
]);
var view = await table.view({
filter: [["x", ">", 2.5]],
});
var answer = [
{ x: 3.5, y: 1 },
{ x: 4.5, y: 2 },
];
let result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test.skip("x > null should be an invalid filter", async function () {
var table = await perspective.table({
x: "float",
y: "integer",
});
const dataSet = [
{ x: 3.5, y: 1 },
{ x: 2.5, y: 1 },
{ x: null, y: 1 },
{ x: null, y: 1 },
{ x: 4.5, y: 2 },
{ x: null, y: 2 },
];
await table.update(dataSet);
var view = await table.view({
filter: [["x", ">", null]],
});
var answer = dataSet;
let result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test("x == 'a'", async function () {
var table = await perspective.table([
{ x: "b", y: 1 },
{ x: "a", y: 1 },
{ x: null, y: 1 },
{ x: null, y: 1 },
{ x: "a", y: 2 },
{ x: "b", y: 2 },
{ x: null, y: 2 },
]);
var view = await table.view({
filter: [["x", "==", "a"]],
});
var answer = [
{ x: "a", y: 1 },
{ x: "a", y: 2 },
];
let result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test("x != 'a'", async function () {
var table = await perspective.table([
{ x: "b", y: 1 },
{ x: "a", y: 1 },
{ x: null, y: 1 },
{ x: null, y: 1 },
{ x: "a", y: 2 },
{ x: "b", y: 2 },
{ x: null, y: 2 },
]);
var view = await table.view({
filter: [["x", "!=", "a"]],
});
var answer = [
{ x: "b", y: 1 },
{ x: null, y: 1 },
{ x: null, y: 1 },
{ x: "b", y: 2 },
{ x: null, y: 2 },
];
let result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test("x == 'b'", async function () {
var table = await perspective.table([
{ x: "b", y: 1 },
{ x: "a", y: 1 },
{ x: null, y: 1 },
{ x: null, y: 1 },
{ x: "a", y: 2 },
{ x: "b", y: 2 },
{ x: null, y: 2 },
]);
var view = await table.view({
filter: [["x", "==", "b"]],
});
var answer = [
{ x: "b", y: 1 },
{ x: "b", y: 2 },
];
let result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
});
// TODO `is_valid_filter` is not currently used and is soon to be replaced by `validate()`
test.describe.skip("is_valid_filter", function () {
test("x == 2", async function () {
let table = await perspective.table(data);
let isValid = await table.is_valid_filter(["x", "==", 2]);
expect(isValid).toBeTruthy();
table.delete();
});
test("x < null", async function () {
let table = await perspective.table(data);
let isValid = await table.is_valid_filter(["x", "<", null]);
expect(isValid).toBeFalsy();
table.delete();
});
test("x > undefined", async function () {
let table = await perspective.table(data);
let isValid = await table.is_valid_filter([
"x",
">",
undefined,
]);
expect(isValid).toBeFalsy();
table.delete();
});
test('x == ""', async function () {
let table = await perspective.table(data);
let isValid = await table.is_valid_filter(["x", "==", ""]);
expect(isValid).toBeTruthy();
table.delete();
});
test("valid date", async function () {
const schema = {
x: "string",
y: "date",
};
let table = await perspective.table(schema);
let isValid = await table.is_valid_filter([
"y",
"==",
"01-01-1970",
]);
expect(isValid).toBeTruthy();
table.delete();
});
test("invalid date", async function () {
const schema = {
x: "string",
y: "date",
};
let table = await perspective.table(schema);
let isValid = await table.is_valid_filter(["y", "<", "1234"]);
expect(isValid).toBeFalsy();
table.delete();
});
test("valid datetime", async function () {
const schema = {
x: "string",
y: "datetime",
};
let table = await perspective.table(schema);
let isValid = await table.is_valid_filter([
"y",
"==",
"2019-11-02 11:11:11.111",
]);
expect(isValid).toBeTruthy();
table.delete();
});
test("invalid datetime", async function () {
const schema = {
x: "string",
y: "datetime",
};
let table = await perspective.table(schema);
let isValid = await table.is_valid_filter([
"y",
">",
"2019-11-02 11:11:11:111",
]);
expect(isValid).toBeFalsy();
table.delete();
});
test("ignores schema check if column is not in schema", async function () {
let table = await perspective.table(data);
let isValid = await table.is_valid_filter([
"not a valid column",
"==",
2,
]);
expect(isValid).toBeTruthy();
table.delete();
});
});
});
})(perspective);
@@ -0,0 +1,197 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
const data = {
w: [
1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, -1.5, -3.5, -1.5, -4.5, -9.5,
-5.5, -8.5, -7.5,
],
x: [1, 2, 3, 4, 4, 3, 2, 1, 3, 4, 2, 1, 4, 3, 1, 2],
y: [
"a",
"b",
"c",
"d",
"a",
"b",
"c",
"d",
"a",
"b",
"c",
"d",
"a",
"b",
"c",
"d",
],
z: [
true,
false,
true,
false,
true,
false,
true,
false,
true,
true,
true,
true,
false,
false,
false,
false,
],
};
test.describe("get_min_max", function () {
test.describe("0 sided", function () {
test("float column", async function () {
var table = await perspective.table(data);
var view = await table.view({});
const range = await view.get_min_max("w");
expect(range).toEqual([-9.5, 8.5]);
view.delete();
table.delete();
});
test("int column", async function () {
var table = await perspective.table(data);
var view = await table.view({});
const range = await view.get_min_max("x");
expect(range).toEqual([1, 4]);
view.delete();
table.delete();
});
test("string column", async function () {
var table = await perspective.table(data);
var view = await table.view({});
const range = await view.get_min_max("y");
expect(range).toEqual(["a", "d"]);
view.delete();
table.delete();
});
});
test.describe("1 sided", function () {
test("float col", async function () {
var table = await perspective.table(data);
var view = await table.view({
group_by: ["y"],
});
const cols = await view.get_min_max("w");
expect(cols).toEqual([-4, 1]);
view.delete();
table.delete();
});
test("int col", async function () {
var table = await perspective.table(data);
var view = await table.view({
group_by: ["y"],
});
const cols = await view.get_min_max("x");
expect(cols).toEqual([8, 12]);
view.delete();
table.delete();
});
test("string col", async function () {
var table = await perspective.table(data);
var view = await table.view({
group_by: ["y"],
});
const cols = await view.get_min_max("y");
expect(cols).toEqual([4, 4]);
view.delete();
table.delete();
});
});
test.describe("2 sided", function () {
test("float col", async function () {
var table = await perspective.table(data);
var view = await table.view({
group_by: ["y"],
split_by: ["z"],
});
const cols = await view.get_min_max("w");
expect(cols).toEqual([-9.5, 9.5]);
view.delete();
table.delete();
});
test("int column", async function () {
var table = await perspective.table(data);
var view = await table.view({
group_by: ["y"],
split_by: ["z"],
});
const cols = await view.get_min_max("x");
expect(cols).toEqual([1, 8]);
view.delete();
table.delete();
});
test("string column", async function () {
var table = await perspective.table(data);
var view = await table.view({
group_by: ["y"],
split_by: ["z"],
});
const cols = await view.get_min_max("y");
expect(cols).toEqual([1, 3]);
view.delete();
table.delete();
});
});
test.describe("column only", function () {
test("float col", async function () {
var table = await perspective.table(data);
var view = await table.view({
split_by: ["z"],
});
const cols = await view.get_min_max("w");
expect(cols).toEqual([-9.5, 8.5]);
view.delete();
table.delete();
});
test("int col", async function () {
var table = await perspective.table(data);
var view = await table.view({
split_by: ["z"],
});
const cols = await view.get_min_max("x");
expect(cols).toEqual([1, 4]);
view.delete();
table.delete();
});
test("string col", async function () {
var table = await perspective.table(data);
var view = await table.view({
split_by: ["z"],
});
const cols = await view.get_min_max("y");
expect(cols).toEqual(["a", "d"]);
view.delete();
table.delete();
});
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,624 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
const data = {
w: [1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5],
x: [1, 2, 3, 4, 4, 3, 2, 1],
y: ["a", "b", "c", "d", "a", "b", "c", "d"],
z: [true, false, true, false, true, false, true, false],
};
((perspective) => {
test.describe("group_rollup_mode", function () {
test.describe("flat", function () {
test("only emits leaves", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
});
const json = await view.to_json();
expect(json).toStrictEqual([
{ __ROW_PATH__: ["a"], w: 7, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["b"], w: 9, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["c"], w: 11, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["d"], w: 13, x: 5, y: 2, z: 2 },
]);
view.delete();
table.delete();
});
test("num_rows returns leaf count", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
});
const num_rows = await view.num_rows();
expect(num_rows).toEqual(4);
view.delete();
table.delete();
});
test("to_columns works", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
});
const cols = await view.to_columns();
expect(cols).toStrictEqual({
__ROW_PATH__: [["a"], ["b"], ["c"], ["d"]],
w: [7, 9, 11, 13],
x: [5, 5, 5, 5],
y: [2, 2, 2, 2],
z: [2, 2, 2, 2],
});
view.delete();
table.delete();
});
test("sort asc", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
sort: [["w", "asc"]],
});
const cols = await view.to_columns();
expect(cols).toStrictEqual({
__ROW_PATH__: [["a"], ["b"], ["c"], ["d"]],
w: [7, 9, 11, 13],
x: [5, 5, 5, 5],
y: [2, 2, 2, 2],
z: [2, 2, 2, 2],
});
view.delete();
table.delete();
});
test("sort desc", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
sort: [["w", "desc"]],
});
const cols = await view.to_columns();
expect(cols).toStrictEqual({
__ROW_PATH__: [["d"], ["c"], ["b"], ["a"]],
w: [13, 11, 9, 7],
x: [5, 5, 5, 5],
y: [2, 2, 2, 2],
z: [2, 2, 2, 2],
});
view.delete();
table.delete();
});
test("sort with hidden column", async function () {
const table = await perspective.table(data);
const view = await table.view({
columns: ["y"],
group_by: ["y"],
group_rollup_mode: "flat",
sort: [["x", "desc"]],
});
const cols = await view.to_columns();
expect(cols).toStrictEqual({
__ROW_PATH__: [["a"], ["b"], ["c"], ["d"]],
y: [2, 2, 2, 2],
});
view.delete();
table.delete();
});
test("multi-level group_by", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y", "z"],
group_rollup_mode: "flat",
});
const json = await view.to_json();
expect(json).toStrictEqual([
{
__ROW_PATH__: ["a", true],
w: 7,
x: 5,
y: 2,
z: 2,
},
{
__ROW_PATH__: ["b", false],
w: 9,
x: 5,
y: 2,
z: 2,
},
{
__ROW_PATH__: ["c", true],
w: 11,
x: 5,
y: 2,
z: 2,
},
{
__ROW_PATH__: ["d", false],
w: 13,
x: 5,
y: 2,
z: 2,
},
]);
view.delete();
table.delete();
});
test("multi-level group_by with sort matches expanded tree order", async function () {
const table = await perspective.table(data);
const flat_view = await table.view({
group_by: ["y", "z"],
group_rollup_mode: "flat",
sort: [["w", "desc"]],
});
const flat_cols = await flat_view.to_columns();
expect(flat_cols).toStrictEqual({
__ROW_PATH__: [
["d", false],
["c", true],
["b", false],
["a", true],
],
w: [13, 11, 9, 7],
x: [5, 5, 5, 5],
y: [2, 2, 2, 2],
z: [2, 2, 2, 2],
});
flat_view.delete();
table.delete();
});
test("with split_by", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
split_by: ["z"],
group_rollup_mode: "flat",
});
const json = await view.to_json();
expect(json).toStrictEqual([
{
__ROW_PATH__: ["a"],
"false|w": null,
"false|x": null,
"false|y": null,
"false|z": null,
"true|w": 7,
"true|x": 5,
"true|y": 2,
"true|z": 2,
},
{
__ROW_PATH__: ["b"],
"false|w": 9,
"false|x": 5,
"false|y": 2,
"false|z": 2,
"true|w": null,
"true|x": null,
"true|y": null,
"true|z": null,
},
{
__ROW_PATH__: ["c"],
"false|w": null,
"false|x": null,
"false|y": null,
"false|z": null,
"true|w": 11,
"true|x": 5,
"true|y": 2,
"true|z": 2,
},
{
__ROW_PATH__: ["d"],
"false|w": 13,
"false|x": 5,
"false|y": 2,
"false|z": 2,
"true|w": null,
"true|x": null,
"true|y": null,
"true|z": null,
},
]);
view.delete();
table.delete();
});
test("split_by only", async function () {
const table = await perspective.table(data);
const view = await table.view({
split_by: ["z"],
group_rollup_mode: "flat",
});
const json = await view.to_json();
expect(json).toStrictEqual([
{
"false|w": null,
"false|x": null,
"false|y": null,
"false|z": null,
"true|w": 1.5,
"true|x": 1,
"true|y": "a",
"true|z": true,
},
{
"false|w": 2.5,
"false|x": 2,
"false|y": "b",
"false|z": false,
"true|w": null,
"true|x": null,
"true|y": null,
"true|z": null,
},
{
"false|w": null,
"false|x": null,
"false|y": null,
"false|z": null,
"true|w": 3.5,
"true|x": 3,
"true|y": "c",
"true|z": true,
},
{
"false|w": 4.5,
"false|x": 4,
"false|y": "d",
"false|z": false,
"true|w": null,
"true|x": null,
"true|y": null,
"true|z": null,
},
{
"false|w": null,
"false|x": null,
"false|y": null,
"false|z": null,
"true|w": 5.5,
"true|x": 4,
"true|y": "a",
"true|z": true,
},
{
"false|w": 6.5,
"false|x": 3,
"false|y": "b",
"false|z": false,
"true|w": null,
"true|x": null,
"true|y": null,
"true|z": null,
},
{
"false|w": null,
"false|x": null,
"false|y": null,
"false|z": null,
"true|w": 7.5,
"true|x": 2,
"true|y": "c",
"true|z": true,
},
{
"false|w": 8.5,
"false|x": 1,
"false|y": "d",
"false|z": false,
"true|w": null,
"true|x": null,
"true|y": null,
"true|z": null,
},
]);
view.delete();
table.delete();
});
test("with split_by and sort", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
split_by: ["z"],
group_rollup_mode: "flat",
sort: [["w", "desc"]],
});
const json = await view.to_json();
expect(json).toStrictEqual([
{
__ROW_PATH__: ["d"],
"false|w": 13,
"false|x": 5,
"false|y": 2,
"false|z": 2,
"true|w": null,
"true|x": null,
"true|y": null,
"true|z": null,
},
{
__ROW_PATH__: ["c"],
"false|w": null,
"false|x": null,
"false|y": null,
"false|z": null,
"true|w": 11,
"true|x": 5,
"true|y": 2,
"true|z": 2,
},
{
__ROW_PATH__: ["b"],
"false|w": 9,
"false|x": 5,
"false|y": 2,
"false|z": 2,
"true|w": null,
"true|x": null,
"true|y": null,
"true|z": null,
},
{
__ROW_PATH__: ["a"],
"false|w": null,
"false|x": null,
"false|y": null,
"false|z": null,
"true|w": 7,
"true|x": 5,
"true|y": 2,
"true|z": 2,
},
]);
view.delete();
table.delete();
});
test("updates after table.update()", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
});
const before = await view.to_json();
expect(before).toStrictEqual([
{ __ROW_PATH__: ["a"], w: 7, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["b"], w: 9, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["c"], w: 11, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["d"], w: 13, x: 5, y: 2, z: 2 },
]);
table.update([{ w: 9.5, x: 5, y: "e", z: true }]);
const after = await view.to_json();
expect(after).toStrictEqual([
{ __ROW_PATH__: ["a"], w: 7, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["b"], w: 9, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["c"], w: 11, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["d"], w: 13, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["e"], w: 9.5, x: 5, y: 1, z: 1 },
]);
view.delete();
table.delete();
});
test("updates preserve sort order", async function () {
const table = await perspective.table(data);
const flat_view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
sort: [["w", "desc"]],
});
table.update([{ w: 100, x: 5, y: "e", z: true }]);
const flat_cols = await flat_view.to_columns();
expect(flat_cols).toStrictEqual({
__ROW_PATH__: [["e"], ["d"], ["c"], ["b"], ["a"]],
w: [100, 13, 11, 9, 7],
x: [5, 5, 5, 5, 5],
y: [1, 2, 2, 2, 2],
z: [1, 2, 2, 2, 2],
});
flat_view.delete();
table.delete();
});
test("viewport pagination", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
});
const json = await view.to_json({
start_row: 0,
end_row: 2,
});
expect(json).toStrictEqual([
{ __ROW_PATH__: ["a"], w: 7, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["b"], w: 9, x: 5, y: 2, z: 2 },
]);
view.delete();
table.delete();
});
test("viewport pagination with sort", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
sort: [["w", "desc"]],
});
const json = await view.to_json({
start_row: 0,
end_row: 2,
});
expect(json).toStrictEqual([
{ __ROW_PATH__: ["d"], w: 13, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["c"], w: 11, x: 5, y: 2, z: 2 },
]);
view.delete();
table.delete();
});
});
test.describe("total", function () {
test.skip("returns only grand total with group_by", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "total",
});
const json = await view.to_json();
expect(json).toStrictEqual([
{ __ROW_PATH__: [], w: 40, x: 20, y: 8, z: 4 },
]);
view.delete();
table.delete();
});
test("returns only grand total schema", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_rollup_mode: "total",
});
const json = await view.schema();
expect(json).toStrictEqual({
w: "float",
x: "integer",
y: "integer",
z: "integer",
});
view.delete();
table.delete();
});
test("returns only grand total", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_rollup_mode: "total",
});
const json = await view.to_json();
expect(json).toStrictEqual([
{ __ROW_PATH__: [], w: 40, x: 20, y: 8, z: 8 },
]);
view.delete();
table.delete();
});
test("num_rows returns 1 without group_by", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_rollup_mode: "total",
});
const num_rows = await view.num_rows();
expect(num_rows).toEqual(1);
view.delete();
table.delete();
});
test("to_columns works", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_rollup_mode: "total",
});
const cols = await view.to_columns();
expect(cols).toStrictEqual({
w: [40],
x: [20],
y: [8],
z: [8],
});
view.delete();
table.delete();
});
test("with split_by", async function () {
const table = await perspective.table(data);
const view = await table.view({
split_by: ["z"],
group_rollup_mode: "total",
});
const json = await view.to_json();
expect(json).toStrictEqual([
{
"false|w": 22,
"false|x": 10,
"false|y": 4,
"false|z": 4,
"true|w": 18,
"true|x": 10,
"true|y": 4,
"true|z": 4,
},
]);
view.delete();
table.delete();
});
test("with split_by schema", async function () {
const table = await perspective.table(data);
const view = await table.view({
split_by: ["z"],
group_rollup_mode: "total",
});
const schema = await view.schema();
expect(schema).toStrictEqual({
w: "float",
x: "integer",
y: "integer",
z: "integer",
});
view.delete();
table.delete();
});
test("updates after table.update()", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_rollup_mode: "total",
});
const before = await view.to_json();
expect(before).toStrictEqual([
{ __ROW_PATH__: [], w: 40, x: 20, y: 8, z: 8 },
]);
table.update([{ w: 10, x: 5, y: "e", z: true }]);
const after = await view.to_json();
expect(after).toStrictEqual([
{ __ROW_PATH__: [], w: 50, x: 25, y: 9, z: 9 },
]);
view.delete();
table.delete();
});
});
});
})(perspective);
@@ -0,0 +1,59 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
import { test_arrow } from "./test_arrows";
test("Can get hosted table name from worker", async () => {
const testTableName1 = Math.random().toString();
const _table = await perspective.table(test_arrow, {
name: testTableName1,
});
const names = await perspective.get_hosted_table_names();
expect(names).toContain(testTableName1);
const testTableName2 = Math.random().toString();
const _table2 = await perspective.table(test_arrow, {
name: testTableName2,
});
const names2 = await perspective.get_hosted_table_names();
expect(names2).toContain(testTableName1);
expect(names2).toContain(testTableName2);
await _table.delete();
await _table2.delete();
});
test("Can subscribe to hosted table changes from worker", async () => {
let count = 0;
const id = perspective.on_hosted_tables_update(() => {
count += 1;
});
const testTableName1 = Math.random().toString();
const _table = await perspective.table(test_arrow, {
name: testTableName1,
});
expect(count).toEqual(1);
const testTableName2 = Math.random().toString();
const _table2 = await perspective.table(test_arrow, {
name: testTableName2,
});
expect(count).toEqual(2);
await _table.delete();
expect(count).toEqual(3);
await _table2.delete();
expect(count).toEqual(4);
});
@@ -0,0 +1,60 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
import { test_null_arrow } from "./test_arrows.js";
const arrow_psp_internal_schema = [
9, 10, 1, 2, 3, 4, 11, 19, 19, 12, 12, 12, 2,
];
((perspective) => {
test.describe("Internal API", function () {
test.skip("is actually using the correct runtime", async function () {
// Get the internal module;
if (perspective.sync_module) {
perspective = await perspective.sync_module();
}
expect(perspective.__module__.wasmJSMethod).toEqual("native-wasm");
});
test.skip("Arrow schema types are mapped correctly", async function () {
// This only works for non parallel
if (perspective.sync_module) {
perspective = await perspective.sync_module();
}
var table = await perspective.table(test_null_arrow.slice());
let schema, stypes;
let types = [];
try {
schema = table._Table.get_schema();
stypes = schema.types();
for (let i = 0; i < stypes.size(); i++) {
types.push(stypes.get(i).value);
}
expect(types).toEqual(arrow_psp_internal_schema);
} finally {
if (schema) {
schema.delete();
}
if (stypes) {
stypes.delete();
}
}
table.delete();
});
});
})(perspective);
@@ -0,0 +1,276 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client.ts";
test.describe("Inner joins", function () {
test("inner joins two tables on a shared key", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
{ id: 3, x: 30 },
]);
const right = await perspective.table([
{ id: 1, y: "a" },
{ id: 2, y: "b" },
{ id: 4, y: "d" },
]);
const joined = await perspective.join(left, right, "id");
const view = await joined.view();
const json = await view.to_json();
expect(json).toHaveLength(2);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("joined table has correct schema", async function () {
const left = await perspective.table({ id: "integer", x: "float" });
const right = await perspective.table({
id: "integer",
y: "string",
});
const joined = await perspective.join(left, right, "id");
const schema = await joined.schema();
expect(schema).toEqual({
id: "integer",
x: "float",
y: "string",
});
await joined.delete();
await right.delete();
await left.delete();
});
test("joined table reacts to left table updates", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
]);
const right = await perspective.table([
{ id: 1, y: "a" },
{ id: 2, y: "b" },
]);
const joined = await perspective.join(left, right, "id");
const view = await joined.view();
let json = await view.to_json();
expect(json).toHaveLength(2);
await left.update([{ id: 1, x: 99 }]);
json = await view.to_json();
expect(json).toEqual([
{ id: 1, x: 10, y: "a" },
{ id: 2, x: 20, y: "b" },
{ id: 1, x: 99, y: "a" },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("joined table reacts to right table updates", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
]);
const right = await perspective.table([
{ id: 1, y: "a" },
{ id: 2, y: "b" },
]);
const joined = await perspective.join(left, right, "id");
const view = await joined.view();
await right.update([{ id: 1, y: "c" }]);
const json = await view.to_json();
// id=3 only exists in right, so inner join should not include it
expect(json).toHaveLength(3);
expect(json).toEqual([
{ id: 1, x: 10, y: "a" },
{ id: 1, x: 10, y: "c" },
{ id: 2, x: 20, y: "b" },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("joined table reacts to new matching rows", async function () {
const left = await perspective.table([{ id: 1, x: 10 }]);
const right = await perspective.table([{ id: 2, y: "b" }]);
const joined = await perspective.join(left, right, "id");
const view = await joined.view();
let json = await view.to_json();
expect(json).toHaveLength(0);
// Add matching row to right
await right.update([{ id: 1, y: "a" }]);
json = await view.to_json();
expect(json).toHaveLength(1);
expect(json).toEqual([{ id: 1, x: 10, y: "a" }]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("joined table supports views with group_by", async function () {
const left = await perspective.table([
{ id: 1, category: "A", x: 10 },
{ id: 2, category: "A", x: 20 },
{ id: 3, category: "B", x: 30 },
]);
const right = await perspective.table([
{ id: 1, y: 100 },
{ id: 2, y: 200 },
{ id: 3, y: 300 },
]);
const joined = await perspective.join(left, right, "id");
const view = await joined.view({
group_by: ["category"],
columns: ["x", "y"],
});
const json = await view.to_columns();
expect(json["x"]).toEqual([60, 30, 30]);
expect(json["y"]).toEqual([600, 300, 300]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("inner joins two tables by name strings", async function () {
const left = await perspective.table(
[
{ id: 1, x: 10 },
{ id: 2, x: 20 },
{ id: 3, x: 30 },
],
{ name: "left_named" },
);
const right = await perspective.table(
[
{ id: 1, y: "a" },
{ id: 2, y: "b" },
{ id: 4, y: "d" },
],
{ name: "right_named" },
);
const joined = await perspective.join(
"left_named",
"right_named",
"id",
);
const view = await joined.view();
const json = await view.to_json();
expect(json).toHaveLength(2);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("inner joins with mixed Table and string args", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
]);
const right = await perspective.table(
[
{ id: 1, y: "a" },
{ id: 2, y: "b" },
],
{ name: "right_mixed" },
);
const joined = await perspective.join(left, "right_mixed", "id");
const view = await joined.view();
const json = await view.to_json();
expect(json).toHaveLength(2);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("rejects column name conflicts", async function () {
const left = await perspective.table([{ id: 1, value: 10 }]);
const right = await perspective.table([{ id: 1, value: 20 }]);
let error;
try {
await perspective.join(left, right, "id");
} catch (e) {
error = e;
}
expect(error).toBeDefined();
await right.delete();
await left.delete();
});
test("rejects updates on joined table", async function () {
const left = await perspective.table([{ id: 1, x: 10 }]);
const right = await perspective.table([{ id: 1, y: "a" }]);
const joined = await perspective.join(left, right, "id");
let error;
try {
await joined.update([{ id: 1, x: 99, y: "z" }]);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
await joined.delete();
await right.delete();
await left.delete();
});
});
@@ -0,0 +1,248 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client.ts";
((perspective) => {
test.describe("Inner joins, indexed tables", function () {
test("inner joins two tables on a shared key", async function () {
const left = await perspective.table(
[
{ id: 1, x: 10 },
{ id: 2, x: 20 },
{ id: 3, x: 30 },
],
{ index: "id" },
);
const right = await perspective.table(
[
{ id: 1, y: "a" },
{ id: 2, y: "b" },
{ id: 4, y: "d" },
],
{ index: "id" },
);
const joined = await perspective.join(left, right, "id");
const view = await joined.view();
const json = await view.to_json();
expect(json).toHaveLength(2);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("joined table has correct schema", async function () {
const left = await perspective.table(
{ id: "integer", x: "float" },
{ index: "id" },
);
const right = await perspective.table(
{ id: "integer", y: "string" },
{ index: "id" },
);
const joined = await perspective.join(left, right, "id");
const schema = await joined.schema();
expect(schema).toEqual({
id: "integer",
x: "float",
y: "string",
});
await joined.delete();
await right.delete();
await left.delete();
});
test("joined table reacts to left table updates", async function () {
const left = await perspective.table(
[
{ id: 1, x: 10 },
{ id: 2, x: 20 },
],
{ index: "id" },
);
const right = await perspective.table(
[
{ id: 1, y: "a" },
{ id: 2, y: "b" },
],
{ index: "id" },
);
const joined = await perspective.join(left, right, "id");
const view = await joined.view();
let json = await view.to_json();
expect(json).toHaveLength(2);
await left.update([{ id: 1, x: 99 }]);
json = await view.to_json();
expect(json).toEqual([
{ id: 1, x: 99, y: "a" },
{ id: 2, x: 20, y: "b" },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("joined table reacts to right table updates", async function () {
const left = await perspective.table(
[
{ id: 1, x: 10 },
{ id: 2, x: 20 },
],
{ index: "id" },
);
const right = await perspective.table(
[
{ id: 1, y: "a" },
{ id: 2, y: "b" },
],
{ index: "id" },
);
const joined = await perspective.join(left, right, "id");
const view = await joined.view();
await right.update([{ id: 3, y: "c" }]);
const json = await view.to_json();
// id=3 only exists in right, so inner join should not include it
expect(json).toHaveLength(2);
expect(json).toEqual([
{ id: 1, x: 10, y: "a" },
{ id: 2, x: 20, y: "b" },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("joined table reacts to new matching rows", async function () {
const left = await perspective.table([{ id: 1, x: 10 }], {
index: "id",
});
const right = await perspective.table([{ id: 2, y: "b" }], {
index: "id",
});
const joined = await perspective.join(left, right, "id");
const view = await joined.view();
let json = await view.to_json();
expect(json).toHaveLength(0);
// Add matching row to right
await right.update([{ id: 1, y: "a" }]);
json = await view.to_json();
expect(json).toHaveLength(1);
expect(json).toEqual([{ id: 1, x: 10, y: "a" }]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("joined table supports views with group_by", async function () {
const left = await perspective.table(
[
{ id: 1, category: "A", x: 10 },
{ id: 2, category: "A", x: 20 },
{ id: 3, category: "B", x: 30 },
],
{ index: "id" },
);
const right = await perspective.table(
[
{ id: 1, y: 100 },
{ id: 2, y: 200 },
{ id: 3, y: 300 },
],
{ index: "id" },
);
const joined = await perspective.join(left, right, "id");
const view = await joined.view({
group_by: ["category"],
columns: ["x", "y"],
});
const json = await view.to_columns();
expect(json["x"]).toEqual([60, 30, 30]);
expect(json["y"]).toEqual([600, 300, 300]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("rejects column name conflicts", async function () {
const left = await perspective.table([{ id: 1, value: 10 }]);
const right = await perspective.table([{ id: 1, value: 20 }]);
let error;
try {
await perspective.join(left, right, "id");
} catch (e) {
error = e;
}
expect(error).toBeDefined();
await right.delete();
await left.delete();
});
test("rejects updates on joined table", async function () {
const left = await perspective.table([{ id: 1, x: 10 }], {
index: "id",
});
const right = await perspective.table([{ id: 1, y: "a" }], {
index: "id",
});
const joined = await perspective.join(left, right, "id");
let error;
try {
await joined.update([{ id: 1, x: 99, y: "z" }]);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
await joined.delete();
await right.delete();
await left.delete();
});
});
})(perspective);
@@ -0,0 +1,183 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client.ts";
((perspective) => {
test.describe("Left joins", function () {
test("left joins two tables on a shared key", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
{ id: 3, x: 30 },
]);
const right = await perspective.table([
{ id: 1, y: "a" },
{ id: 2, y: "b" },
{ id: 4, y: "d" },
]);
const joined = await perspective.join(left, right, "id", {
join_type: "left",
});
const view = await joined.view();
const json = await view.to_json();
// Left join: all left rows, matched right rows, id=3 has null y
expect(json).toHaveLength(3);
expect(json).toEqual([
{ id: 1, x: 10, y: "a" },
{ id: 2, x: 20, y: "b" },
{ id: 3, x: 30, y: null },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("left join includes unmatched left rows with nulls", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
]);
const right = await perspective.table([{ id: 1, y: "a" }]);
const joined = await perspective.join(left, right, "id", {
join_type: "left",
});
const view = await joined.view();
const json = await view.to_json();
expect(json).toHaveLength(2);
expect(json).toEqual([
{ id: 1, x: 10, y: "a" },
{ id: 2, x: 20, y: null },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("left join does not include unmatched right rows", async function () {
const left = await perspective.table([{ id: 1, x: 10 }]);
const right = await perspective.table([
{ id: 1, y: "a" },
{ id: 2, y: "b" },
]);
const joined = await perspective.join(left, right, "id", {
join_type: "left",
});
const view = await joined.view();
const json = await view.to_json();
expect(json).toHaveLength(1);
expect(json).toEqual([{ id: 1, x: 10, y: "a" }]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("left join reacts to right table updates", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
]);
const right = await perspective.table([{ id: 1, y: "a" }]);
const joined = await perspective.join(left, right, "id", {
join_type: "left",
});
const view = await joined.view();
// Add matching row for id=2
await right.update([{ id: 2, y: "b" }]);
const json = await view.to_json();
// Now both rows match
expect(json).toHaveLength(2);
expect(json).toEqual([
{ id: 1, x: 10, y: "a" },
{ id: 2, x: 20, y: "b" },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("left join with no matching rows", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
]);
const right = await perspective.table([
{ id: 3, y: "c" },
{ id: 4, y: "d" },
]);
const joined = await perspective.join(left, right, "id", {
join_type: "left",
});
const view = await joined.view();
const json = await view.to_json();
// All left rows present with null right columns
expect(json).toHaveLength(2);
expect(json).toEqual([
{ id: 1, x: 10, y: null },
{ id: 2, x: 20, y: null },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("left join has correct schema", async function () {
const left = await perspective.table({ id: "integer", x: "float" });
const right = await perspective.table({
id: "integer",
y: "string",
});
const joined = await perspective.join(left, right, "id", {
join_type: "left",
});
const schema = await joined.schema();
expect(schema).toEqual({
id: "integer",
x: "float",
y: "string",
});
await joined.delete();
await right.delete();
await left.delete();
});
});
})(perspective);
@@ -0,0 +1,208 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client.ts";
((perspective) => {
test.describe("Outer joins", function () {
test("outer joins two tables on a shared key", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
{ id: 3, x: 30 },
]);
const right = await perspective.table([
{ id: 1, y: "a" },
{ id: 2, y: "b" },
{ id: 4, y: "d" },
]);
const joined = await perspective.join(left, right, "id", {
join_type: "outer",
});
const view = await joined.view();
const json = await view.to_json();
// Outer join: all left + matched right + unmatched right
expect(json).toHaveLength(4);
expect(json).toEqual([
{ id: 1, x: 10, y: "a" },
{ id: 2, x: 20, y: "b" },
{ id: 3, x: 30, y: null },
{ id: 4, x: null, y: "d" },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("outer join includes all rows when no keys match", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
]);
const right = await perspective.table([
{ id: 3, y: "c" },
{ id: 4, y: "d" },
]);
const joined = await perspective.join(left, right, "id", {
join_type: "outer",
});
const view = await joined.view();
const json = await view.to_json();
expect(json).toHaveLength(4);
expect(json).toEqual([
{ id: 1, x: 10, y: null },
{ id: 2, x: 20, y: null },
{ id: 3, x: null, y: "c" },
{ id: 4, x: null, y: "d" },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("outer join with all keys matching is same as inner", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
]);
const right = await perspective.table([
{ id: 1, y: "a" },
{ id: 2, y: "b" },
]);
const joined = await perspective.join(left, right, "id", {
join_type: "outer",
});
const view = await joined.view();
const json = await view.to_json();
expect(json).toHaveLength(2);
expect(json).toEqual([
{ id: 1, x: 10, y: "a" },
{ id: 2, x: 20, y: "b" },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("outer join reacts to left table updates", async function () {
const left = await perspective.table([{ id: 1, x: 10 }]);
const right = await perspective.table([
{ id: 1, y: "a" },
{ id: 2, y: "b" },
]);
const joined = await perspective.join(left, right, "id", {
join_type: "outer",
});
const view = await joined.view();
let json = await view.to_json();
expect(json).toHaveLength(2);
expect(json).toEqual([
{ id: 1, x: 10, y: "a" },
{ id: 2, x: null, y: "b" },
]);
// Add matching row for id=2
await left.update([{ id: 2, x: 20 }]);
json = await view.to_json();
// Both left rows now match right rows
expect(json).toHaveLength(2);
expect(json).toEqual([
{ id: 1, x: 10, y: "a" },
{ id: 2, x: 20, y: "b" },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("outer join reacts to right table updates", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
]);
const right = await perspective.table([{ id: 1, y: "a" }]);
const joined = await perspective.join(left, right, "id", {
join_type: "outer",
});
const view = await joined.view();
let json = await view.to_json();
expect(json).toHaveLength(2);
expect(json).toEqual([
{ id: 1, x: 10, y: "a" },
{ id: 2, x: 20, y: null },
]);
await right.update([{ id: 2, y: "b" }]);
json = await view.to_json();
// Now both match, but non-indexed tables append
expect(json).toHaveLength(2);
expect(json).toEqual([
{ id: 1, x: 10, y: "a" },
{ id: 2, x: 20, y: "b" },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("outer join has correct schema", async function () {
const left = await perspective.table({ id: "integer", x: "float" });
const right = await perspective.table({
id: "integer",
y: "string",
});
const joined = await perspective.join(left, right, "id", {
join_type: "outer",
});
const schema = await joined.schema();
expect(schema).toEqual({
id: "integer",
x: "float",
y: "string",
});
await joined.delete();
await right.delete();
await left.delete();
});
});
})(perspective);
@@ -0,0 +1,235 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client.ts";
test.describe("right_on option", function () {
test("joins on differently-named columns", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
{ id: 3, x: 30 },
]);
const right = await perspective.table([
{ user_id: 1, y: "a" },
{ user_id: 2, y: "b" },
{ user_id: 4, y: "d" },
]);
const joined = await perspective.join(left, right, "id", {
right_on: "user_id",
});
const view = await joined.view();
const json = await view.to_json();
expect(json).toHaveLength(2);
expect(json).toEqual([
{ id: 1, x: 10, y: "a" },
{ id: 2, x: 20, y: "b" },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("output schema uses left key column name", async function () {
const left = await perspective.table({ id: "integer", x: "float" });
const right = await perspective.table({
user_id: "integer",
y: "string",
});
const joined = await perspective.join(left, right, "id", {
right_on: "user_id",
});
const schema = await joined.schema();
expect(schema).toEqual({
id: "integer",
x: "float",
y: "string",
});
await joined.delete();
await right.delete();
await left.delete();
});
test("errors on type mismatch between on and right_on", async function () {
const left = await perspective.table({ id: "integer", x: "float" });
const right = await perspective.table({
user_id: "string",
y: "float",
});
let error;
try {
await perspective.join(left, right, "id", {
right_on: "user_id",
});
} catch (e) {
error = e;
}
expect(error).toBeDefined();
await right.delete();
await left.delete();
});
test("errors when right_on column not found", async function () {
const left = await perspective.table({ id: "integer", x: "float" });
const right = await perspective.table({
user_id: "integer",
y: "string",
});
let error;
try {
await perspective.join(left, right, "id", {
right_on: "nonexistent",
});
} catch (e) {
error = e;
}
expect(error).toBeDefined();
await right.delete();
await left.delete();
});
test("right_on same as on behaves identically to omitting it", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
]);
const right = await perspective.table([
{ id: 1, y: "a" },
{ id: 2, y: "b" },
]);
const joined = await perspective.join(left, right, "id", {
right_on: "id",
});
const view = await joined.view();
const json = await view.to_json();
expect(json).toEqual([
{ id: 1, x: 10, y: "a" },
{ id: 2, x: 20, y: "b" },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("reacts to right table updates with right_on", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
]);
const right = await perspective.table([
{ user_id: 1, y: "a" },
{ user_id: 2, y: "b" },
]);
const joined = await perspective.join(left, right, "id", {
right_on: "user_id",
});
const view = await joined.view();
await right.update([{ user_id: 1, y: "c" }]);
const json = await view.to_json();
expect(json).toHaveLength(3);
expect(json).toEqual([
{ id: 1, x: 10, y: "a" },
{ id: 1, x: 10, y: "c" },
{ id: 2, x: 20, y: "b" },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("left join with right_on", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
{ id: 3, x: 30 },
]);
const right = await perspective.table([
{ user_id: 1, y: "a" },
{ user_id: 2, y: "b" },
]);
const joined = await perspective.join(left, right, "id", {
join_type: "left",
right_on: "user_id",
});
const view = await joined.view();
const json = await view.to_json();
expect(json).toHaveLength(3);
expect(json).toEqual([
{ id: 1, x: 10, y: "a" },
{ id: 2, x: 20, y: "b" },
{ id: 3, x: 30, y: null },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
test("outer join with right_on", async function () {
const left = await perspective.table([
{ id: 1, x: 10 },
{ id: 2, x: 20 },
]);
const right = await perspective.table([
{ user_id: 2, y: "b" },
{ user_id: 3, y: "c" },
]);
const joined = await perspective.join(left, right, "id", {
join_type: "outer",
right_on: "user_id",
});
const view = await joined.view();
const json = await view.to_json();
expect(json).toHaveLength(3);
expect(json).toEqual([
{ id: 1, x: 10, y: null },
{ id: 2, x: 20, y: "b" },
{ id: 3, x: null, y: "c" },
]);
await view.delete();
await joined.delete();
await right.delete();
await left.delete();
});
});
+317
View File
@@ -0,0 +1,317 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
import * as fs from "node:fs";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const arr = fs.readFileSync(
require.resolve("superstore-arrow/superstore.lz4.arrow"),
).buffer;
/**
* Run a function in a loop, comparing before-and-after wasm heap for leaks.
* Emscripten allocates in pages, and Perspective is hard coded to start at
* 16777216b, so rather than check `end - start`, we just test that any
* allocation ever occurs.
* @param {*} test
* @param {*} num_iterations
*/
async function leak_test(test, num_iterations = 10_000) {
// warmup
await test();
await test();
// TODO Playwright uses the same host instance so this may have grown by
// the time the suite runs. Could fix with a nod eagent (and test in other
// browsers).
const start = (await perspective.system_info()).heap_size;
const start_used = (await perspective.system_info()).used_size;
for (var i = 0; i < num_iterations; i++) {
await test();
}
const final_used = (await perspective.system_info()).used_size;
expect((await perspective.system_info()).heap_size).toEqual(start);
expect(Number(final_used) / Number(start_used)).toBeGreaterThanOrEqual(0.8);
expect(Number(final_used) / Number(start_used)).toBeLessThanOrEqual(1.5);
}
/**
* Given columns a (int), b (float), c (string) and d (datetime),
* generate expressions that use all columns and scalar values.
*/
function generate_expressions() {
const expressions = {
"concat('abcd', \"c\", 'efg')": "concat('abcd', \"c\", 'efg')",
};
for (const op of ["+", "-", "*", "/", "^", "%"]) {
expressions[`("a" ${op} "b") + ${Math.floor(Math.random() * 100)}`] =
`("a" ${op} "b") + ${Math.floor(Math.random() * 100)}`;
}
for (const fn of ["sqrt", "log10", "deg2rad"]) {
expressions[`${fn}("b")`] = `${fn}("b")`;
}
for (const fn of ["upper", "lower", "length"]) {
expressions[`${fn}("c")`] = `${fn}("c")`;
}
for (const unit of ["m", "D"]) {
expressions[`bucket("d", '${unit}')`] = `bucket("d", '${unit}')`;
}
const rand =
Object.keys(expressions)[
Math.floor(Math.random() * Object.keys(expressions).length)
];
return { [rand]: expressions[rand] };
}
test.describe("leaks", function () {
test.describe("view", function () {
test.describe("1-sided", function () {
test("to_json does not leak", async () => {
test.setTimeout(60000);
const table = await perspective.table(arr.slice());
const view = await table.view({ group_by: ["State"] });
await leak_test(async function () {
let json = await view.to_json();
expect(json.length).toEqual(50);
});
await view.delete();
await table.delete();
});
test.skip("OG - to_columns_string does not leak", async () => {
const table = await perspective.table(arr.slice());
const view = await table.view({ group_by: ["State"] });
await leak_test(async function () {
let json = await view.to_columns_string();
expect(json.length).toEqual(6722);
});
await view.delete();
await table.delete();
});
// The length of this string changed due to
// some of the trailing fractional digits in the floats.
test("to_columns_string does not leak", async () => {
const table = await perspective.table(arr.slice());
const view = await table.view({ group_by: ["State"] });
await leak_test(async function () {
let json = await view.to_columns_string();
expect(json.length).toEqual(6669);
});
await view.delete();
await table.delete();
});
});
});
test.describe("table", function () {
test("update does not leak", async () => {
const table = await perspective.table(
{ x: "integer", y: "string" },
{ index: "x" },
);
let count = 0;
const view = await table.view();
view.on_update(function () {
count += 1;
});
await table.update([{ x: 1, y: "TestTestTest" }]);
await leak_test(async function () {
table.update([{ x: 1, y: "TestTestTest" }]);
expect(await table.size()).toEqual(1);
});
// await table.update([{ x: 1, y: "TestTestTest" }]);
expect(count).toBeGreaterThan(0);
await view.delete();
await table.delete();
});
test.skip("csv loading does not leak", async () => {
const table = await perspective.table(arr.slice());
const view = await table.view();
const csv = await view.to_csv({ end_row: 10 });
await view.delete();
await table.delete();
await leak_test(async function () {
const table = await perspective.table(csv);
expect(await table.size()).toEqual(10);
await table.delete();
});
});
});
test.describe("expression columns", function () {
test("0 sided does not leak", async () => {
test.setTimeout(60000);
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [1.5, 2.5, 3.5, 4.5],
c: ["a", "b", "c", "d"],
d: [new Date(), new Date(), new Date(), new Date()],
});
const expressions = generate_expressions();
await leak_test(async () => {
const view = await table.view({
expressions,
});
const expression_schema = await view.expression_schema();
expect(Object.keys(expression_schema).length).toEqual(
Object.keys(expressions).length,
);
await view.delete();
});
await table.delete();
});
/**
* Because the expression vocab and the regex cache is per-table and
* not per-view, we should be able to leak test the table creation
* and view creation.
*/
test.skip("0 sided regex does not leak", async () => {
const expressions = {
"match(\"a\", '.{1}')": "match(\"a\", '.{1}')",
"match_all(\"a\", '[a-z]{1}')": "match_all(\"a\", '[a-z]{1}')",
"search(\"a\", '.')": "search(\"a\", '.')",
};
await leak_test(async () => {
const table = await perspective.table({
a: "abcdefghijklmnopqrstuvwxyz".split(""),
});
const view = await table.view({
expressions,
});
const expression_schema = await view.expression_schema();
expect(Object.keys(expression_schema).length).toEqual(
Object.keys(expressions).length,
);
await view.delete();
await table.delete();
});
});
test.skip("0 sided string does not leak", async () => {
const table = await perspective.table({
a: "abcdefghijklmnopqrstuvwxyz".split(""),
});
const expressions = {
"var x := 'abcdefghijklmnopqrstuvwxyz'; concat(\"a\", x, 'abc')":
"var x := 'abcdefghijklmnopqrstuvwxyz'; concat(\"a\", x, 'abc')",
"var x := 'abcdefghijklmnopqrstuvwxyz'; var y := 'defhijklmnopqrst'; concat(\"a\", x, 'abc', y)":
"var x := 'abcdefghijklmnopqrstuvwxyz'; var y := 'defhijklmnopqrst'; concat(\"a\", x, 'abc', y)",
};
await leak_test(async () => {
const view = await table.view({
expressions,
});
const expression_schema = await view.expression_schema();
expect(Object.keys(expression_schema).length).toEqual(
Object.keys(expressions).length,
);
await view.delete();
});
await table.delete();
});
test("1 sided does not leak", async () => {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [1.5, 2.5, 3.5, 4.5],
c: ["a", "b", "c", "d"],
d: [new Date(), new Date(), new Date(), new Date()],
});
const columns = ["a", "b", "c", "d"];
const expressions = generate_expressions();
await leak_test(async () => {
const view = await table.view({
group_by: [
columns[Math.floor(Math.random() * columns.length)],
],
expressions,
});
const expression_schema = await view.expression_schema();
expect(Object.keys(expression_schema).length).toEqual(
Object.keys(expressions).length,
);
await view.delete();
}, 3000);
await table.delete();
});
test("2 sided does not leak", async () => {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [1.5, 2.5, 3.5, 4.5],
c: ["a", "b", "c", "d"],
d: [new Date(), new Date(), new Date(), new Date()],
});
const columns = ["a", "b", "c", "d"];
const expressions = generate_expressions();
await leak_test(async () => {
const view = await table.view({
group_by: [
columns[Math.floor(Math.random() * columns.length)],
],
split_by: [
columns[Math.floor(Math.random() * columns.length)],
],
expressions,
});
const expression_schema = await view.expression_schema();
expect(Object.keys(expression_schema).length).toEqual(
Object.keys(expressions).length,
);
await view.delete();
}, 3000);
await table.delete();
});
});
});
+102
View File
@@ -0,0 +1,102 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
import moment from "moment";
import * as arrows from "./test_arrows.js";
((perspective) => {
async function match_delta(
perspective,
delta,
expected,
formatter = "to_json",
) {
const table = await perspective.table(delta);
const view = await table.view();
const result = await view[formatter]();
expect(result).toEqual(expected);
await view.delete();
await table.delete();
}
test.describe("Limits", () => {
test("Limiting rows should not limit the index space", async ({
page,
}) => {
const table = await perspective.table(
{
x: "integer",
group: "integer",
},
{ limit: 10 },
);
const data = [];
for (let i = 0; i < 15; i++) {
data.push({ x: i, group: 0 });
}
await table.update(data);
const lastByIdx = await (
await table.view({
group_by: ["group"],
aggregates: { x: "last by index" },
})
).to_json();
expect(lastByIdx[0]["x"]).toEqual(14);
const view = await table.view();
let viewData = await view.to_json();
expect(viewData.length).toBe(10);
expect(viewData).toEqual(data.slice(-10));
});
test("Append in sequence", async () => {
const table = await perspective.table(
{
x: "integer",
y: "string",
z: "boolean",
},
{ limit: 2 },
);
const v1 = await table.view();
const v2 = await table.view();
await table.update({
x: [0],
y: ["0"],
});
await v1.to_columns();
await table.update({
x: [1, 2],
y: ["1", "2"],
});
let result = await v1.to_columns();
let result2 = await v2.to_columns();
expect(result["x"]).toEqual([1, 2]);
expect(result2["y"]).toEqual(["1", "2"]);
await v2.delete();
await v1.delete();
await table.delete();
});
});
})(perspective);
+54
View File
@@ -0,0 +1,54 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
import * as fs from "node:fs";
const superstore_uncompressed = fs.readFileSync(
require.resolve("superstore-arrow/superstore.arrow"),
).buffer;
const superstore_lz4 = fs.readFileSync(
require.resolve("superstore-arrow/superstore.lz4.arrow"),
).buffer;
test.describe("Arrow IPC", function () {
test.describe("to_arrow()", function () {
test("to_arrow() compressed is serializable roundtrip", async () => {
let table = await perspective.table(
superstore_uncompressed.slice(),
);
let view = await table.view();
let arr = await view.to_arrow({ compression: "lz4" });
expect(arr).toEqual(superstore_lz4);
expect(arr.byteLength).toBeLessThan(
superstore_uncompressed.byteLength,
);
view.delete();
table.delete();
table = await perspective.table(arr);
view = await table.view();
let arr2 = await view.to_arrow({ compression: null });
expect(arr2.byteLength).toBeGreaterThan(arr.byteLength);
view.delete();
table.delete();
});
});
});
@@ -0,0 +1,66 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
((perspective) => {
test.describe("Multiple Server instances", function () {
test("Construct Table from remote View", async function ({ page }) {
await page.goto("/rust/perspective-js/test/html/test.html");
const [json0, json1] = await page.evaluate(async () => {
let perspective = await import(
"http://localhost:6598/node_modules/@perspective-dev/client/dist/esm/perspective.inline.js"
);
const worker0 = await perspective.worker();
const worker1 = await perspective.worker();
const table0 = await worker0.table("x,y\n1,2\n3,4");
const view0 = await table0.view();
const table1 = await worker1.table(view0);
const view1 = await table1.view();
const json0 = await view0.to_json();
const json1 = await view1.to_json();
return [json0, json1];
});
expect(json0).toEqual(json1);
});
test("View on_update transfers to new table", async function ({
page,
}) {
await page.goto("/rust/perspective-js/test/html/test.html");
const [json0, json1] = await page.evaluate(async () => {
let perspective = await import(
"http://localhost:6598/node_modules/@perspective-dev/client/dist/esm/perspective.inline.js"
);
const worker0 = await perspective.worker();
const worker1 = await perspective.worker();
const table0 = await worker0.table("x,y\n1,2\n3,4");
const view0 = await table0.view();
const table1 = await worker1.table(view0);
const view1 = await table1.view();
const { promise, resolve } = Promise.withResolvers();
await view1.on_update(() => resolve());
await table0.update([{ x: 6 }]);
await promise;
const json0 = await view0.to_json();
const json1 = await view1.to_json();
return [json0, json1];
});
expect(json0).toEqual(json1);
});
});
})(perspective);
@@ -0,0 +1,122 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
import _ from "lodash";
var arrow_result = [
{
f32: 1.5,
f64: 1.5,
i64: 1,
i32: 1,
i16: 1,
i8: 1,
bool: true,
char: "a",
dict: "a",
datetime: +new Date("2018-01-25"),
},
{
f32: 2.5,
f64: 2.5,
i64: 2,
i32: 2,
i16: 2,
i8: 2,
bool: false,
char: "b",
dict: "b",
datetime: +new Date("2018-01-26"),
},
{
f32: 3.5,
f64: 3.5,
i64: 3,
i32: 3,
i16: 3,
i8: 3,
bool: true,
char: "c",
dict: "c",
datetime: +new Date("2018-01-27"),
},
{
f32: 4.5,
f64: 4.5,
i64: 4,
i32: 4,
i16: 4,
i8: 4,
bool: false,
char: "d",
dict: "d",
datetime: +new Date("2018-01-28"),
},
{
f32: 5.5,
f64: 5.5,
i64: 5,
i32: 5,
i16: 5,
i8: 5,
bool: true,
char: "d",
dict: "d",
datetime: +new Date("2018-01-29"),
},
];
((perspective) => {
test.describe("Multiple Perspectives", function () {
test("Constructs table using data generated by to_arrow()", async function () {
let table = await perspective.table(arrow_result);
let view = await table.view();
let result = await view.to_arrow();
let table2 = await perspective.table(result);
let view2 = await table2.view();
let result2 = await view2.to_json();
expect(result2).toEqual(arrow_result);
view.delete();
view2.delete();
table.delete();
table2.delete();
});
test("Constructs table using data in random column order generated by to_arrow()", async function () {
let table = await perspective.table(arrow_result);
let columns = _.shuffle(await table.columns());
let view = await table.view({
columns: columns,
});
let result = await view.to_arrow();
let table2 = await perspective.table(result);
let columns2 = _.shuffle(await table2.columns());
let view2 = await table2.view({
columns: columns2,
});
let result2 = await view2.to_json();
expect(result2).toEqual(arrow_result);
view.delete();
view2.delete();
table.delete();
table2.delete();
});
});
})(perspective);
@@ -0,0 +1,136 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
const DISK_ROOT = path.join(os.tmpdir(), `perspective-${process.pid}`);
function disk_file_count() {
const out = [];
const walk = (dir) => {
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, ent.name);
if (ent.isDirectory()) walk(p);
else out.push(p);
}
};
try {
walk(DISK_ROOT);
} catch (e) {
/* root not created yet */
}
return out.length;
}
const data = {
x: [1, 2, 3, 4],
y: ["a", "b", "c", "d"],
z: [1.5, 2.5, 3.5, 4.5],
};
test.describe("page_to_disk", function () {
test("produces results identical to an in-memory table", async function () {
const mem = await perspective.table(data);
const disk = await perspective.table(data, { page_to_disk: true });
const vm = await mem.view();
const vd = await disk.view();
expect(await vd.to_columns()).toEqual(await vm.to_columns());
expect(await disk.schema()).toEqual(await mem.schema());
expect(await disk.size()).toEqual(await mem.size());
await vm.delete();
await vd.delete();
await mem.delete();
await disk.delete();
});
test("a small page_to_disk table stays in-heap (no files spilled)", async function () {
const before = disk_file_count();
const disk = await perspective.table(data, { page_to_disk: true });
const view = await disk.view();
await view.to_columns();
expect(disk_file_count()).toEqual(before);
await view.delete();
await disk.delete();
});
test("supports update + aggregates", async function () {
const disk = await perspective.table(data, { page_to_disk: true });
await disk.update({ x: [5, 6], y: ["e", "f"], z: [5.5, 6.5] });
const view = await disk.view({
group_by: ["y"],
aggregates: { z: "sum" },
columns: ["z"],
});
const cols = await view.to_columns();
expect(cols.z[0]).toEqual(1.5 + 2.5 + 3.5 + 4.5 + 5.5 + 6.5);
await view.delete();
await disk.delete();
});
test("supports expression columns", async function () {
const disk = await perspective.table(data, { page_to_disk: true });
const view = await disk.view({
columns: ["w"],
expressions: { w: `"x" + "z"` },
});
const cols = await view.to_columns();
expect(cols.w).toEqual([2.5, 4.5, 6.5, 8.5]);
await view.delete();
await disk.delete();
});
// Forces actual eviction (table > 1gb resident) so the `node:fs` bridge
// round-trips: columns are flushed to disk on eviction and re-read on access.
test("evicts to disk over budget and restores correctly", async function () {
const ROWS = 100_000;
const COLS = 256;
const UPDATES = 5; // 25M rows total
const chunk = {};
for (let c = 0; c < COLS; c++) {
const col = new Array(ROWS);
for (let i = 0; i < ROWS; i++) col[i] = c * 1000 + (i % 1000) + 0.5;
chunk["c" + c] = col;
}
const before = disk_file_count();
const table = await perspective.table(chunk, { page_to_disk: true });
for (let u = 1; u < UPDATES; u++) await table.update(chunk);
expect(await table.size()).toEqual(ROWS * UPDATES);
// Eviction must have spilled column buffers to disk.
expect(disk_file_count()).toBeGreaterThan(before);
// Reading after eviction restores evicted columns from disk. Verify the
// head of every column matches the source (a broken restore would read
// zeros). `c[c][i] === c*1000 + i + 0.5` for i < 1000.
const view = await table.view();
const head = await view.to_columns({ start_row: 0, end_row: 4 });
for (let c = 0; c < COLS; c++) {
expect(head["c" + c]).toEqual([
c * 1000 + 0.5,
c * 1000 + 1.5,
c * 1000 + 2.5,
c * 1000 + 3.5,
]);
}
await view.delete();
await table.delete();
expect(disk_file_count()).toEqual(before);
});
});
@@ -0,0 +1,19 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import perspective from "@perspective-dev/client";
// // TO run the test suite against a different server:
// const client = await perspective.websocket("ws://localhost:8080/subscribe");
// export default client;
export default perspective;
@@ -0,0 +1,336 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
((perspective) => {
test.describe("Pivotting with nulls", function () {
test.describe("last aggregate", function () {
test("preserves null when it is the last element in a leaf", async function () {
const DATA = {
a: ["a", "a", "a", "b", "b", "b", "c", "c", "c"],
b: [1, 2, null, 3, 4, 5, null, null, null],
};
var table = await perspective.table(DATA);
var view = await table.view({
group_by: ["a"],
columns: ["b"],
aggregates: { b: "last" },
});
var answer = [
{ __ROW_PATH__: [], b: null },
{ __ROW_PATH__: ["a"], b: null },
{ __ROW_PATH__: ["b"], b: 5 },
{ __ROW_PATH__: ["c"], b: null },
];
let result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test("preserves null when it is the last element in a leaf under 2 levels", async function () {
const DATA = {
a: [
"a",
"a",
"a",
"b",
"b",
"b",
"c",
"c",
"c",
"a",
"a",
"a",
"b",
"b",
"b",
"c",
"c",
"c",
],
b: [
1,
2,
null,
3,
4,
5,
null,
null,
null,
1,
2,
null,
null,
null,
null,
3,
4,
5,
],
c: [
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"y",
"y",
"y",
"y",
"y",
"y",
"y",
"y",
"y",
],
};
var table = await perspective.table(DATA);
var view = await table.view({
group_by: ["c", "a"],
columns: ["b"],
aggregates: { b: "last" },
});
var answer = [
{ __ROW_PATH__: [], b: 5 },
{ __ROW_PATH__: ["x"], b: null },
{ __ROW_PATH__: ["x", "a"], b: null },
{ __ROW_PATH__: ["x", "b"], b: 5 },
{ __ROW_PATH__: ["x", "c"], b: null },
{ __ROW_PATH__: ["y"], b: 5 },
{ __ROW_PATH__: ["y", "a"], b: null },
{ __ROW_PATH__: ["y", "b"], b: null },
{ __ROW_PATH__: ["y", "c"], b: 5 },
];
let result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test("preserves null when it is the last element in a leaf under 2 levels when grand total is null", async function () {
const DATA = {
a: [
"a",
"a",
"a",
"b",
"b",
"b",
"c",
"c",
"c",
"a",
"a",
"a",
"b",
"b",
"b",
"c",
"c",
"c",
],
b: [
1,
2,
null,
null,
null,
null,
3,
4,
5,
1,
2,
null,
3,
4,
5,
null,
null,
null,
],
c: [
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"y",
"y",
"y",
"y",
"y",
"y",
"y",
"y",
"y",
],
};
var table = await perspective.table(DATA);
var view = await table.view({
group_by: ["c", "a"],
columns: ["b"],
aggregates: { b: "last" },
});
var answer = [
{ __ROW_PATH__: [], b: null },
{ __ROW_PATH__: ["x"], b: 5 },
{ __ROW_PATH__: ["x", "a"], b: null },
{ __ROW_PATH__: ["x", "b"], b: null },
{ __ROW_PATH__: ["x", "c"], b: 5 },
{ __ROW_PATH__: ["y"], b: null },
{ __ROW_PATH__: ["y", "a"], b: null },
{ __ROW_PATH__: ["y", "b"], b: 5 },
{ __ROW_PATH__: ["y", "c"], b: null },
];
let result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
});
test("shows one pivot for the nulls on initial load", async function () {
const dataWithNulls = [
{ name: "Homer", value: 1 },
{ name: null, value: 1 },
{ name: null, value: 1 },
{ name: "Krusty", value: 1 },
];
var table = await perspective.table(dataWithNulls);
var view = await table.view({
group_by: ["name"],
aggregates: { name: "distinct count" },
});
const answer = [
{ __ROW_PATH__: [], name: 3, value: 4 },
{ __ROW_PATH__: [null], name: 1, value: 2 },
{ __ROW_PATH__: ["Homer"], name: 1, value: 1 },
{ __ROW_PATH__: ["Krusty"], name: 1, value: 1 },
];
let results = await view.to_json();
expect(results).toEqual(answer);
view.delete();
table.delete();
});
test("shows one pivot for the nulls after updating with a null", async function () {
const dataWithNull1 = [
{ name: "Homer", value: 1 },
{ name: null, value: 1 },
];
const dataWithNull2 = [
{ name: null, value: 1 },
{ name: "Krusty", value: 1 },
];
var table = await perspective.table(dataWithNull1);
await table.update(dataWithNull2);
var view = await table.view({
group_by: ["name"],
aggregates: { name: "distinct count" },
});
const answer = [
{ __ROW_PATH__: [], name: 3, value: 4 },
{ __ROW_PATH__: [null], name: 1, value: 2 },
{ __ROW_PATH__: ["Homer"], name: 1, value: 1 },
{ __ROW_PATH__: ["Krusty"], name: 1, value: 1 },
];
let results = await view.to_json();
expect(results).toEqual(answer);
view.delete();
table.delete();
});
test("aggregates that return NaN render correctly", async function () {
const dataWithNull1 = [
{ name: "Homer", value: 3 },
{ name: "Homer", value: 1 },
{ name: "Marge", value: null },
{ name: "Marge", value: null },
];
var table = await perspective.table(dataWithNull1);
var view = await table.view({
group_by: ["name"],
aggregates: { value: "avg" },
});
const answer = [
{ __ROW_PATH__: [], name: 4, value: 2 },
{ __ROW_PATH__: ["Homer"], name: 2, value: 2 },
{ __ROW_PATH__: ["Marge"], name: 2, value: null },
];
let results = await view.to_json();
expect(results).toEqual(answer);
view.delete();
table.delete();
});
test("aggregates nulls correctly", async function () {
const data = [
{ x: "AAAAAAAAAAAAAA" },
{ x: "AAAAAAAAAAAAAA" },
{ x: "AAAAAAAAAAAAAA" },
{ x: null },
{ x: null },
{ x: "BBBBBBBBBBBBBB" },
{ x: "BBBBBBBBBBBBBB" },
{ x: "BBBBBBBBBBBBBB" },
];
const tbl = await perspective.table(data);
const view = await tbl.view({ group_by: ["x"] });
const result = await view.to_json();
expect(result).toEqual([
{
__ROW_PATH__: [],
x: 8,
},
{
__ROW_PATH__: [null],
x: 2,
},
{
__ROW_PATH__: ["AAAAAAAAAAAAAA"],
x: 3,
},
{
__ROW_PATH__: ["BBBBBBBBBBBBBB"],
x: 3,
},
]);
});
});
})(perspective);
+839
View File
@@ -0,0 +1,839 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
import _ from "lodash";
const data = {
w: [1.5, 2.5, 3.5, 4.5],
x: [1, 2, 3, 4],
y: ["a", "b", "c", "d"],
z: [true, false, true, false],
};
const get_random_int = function (min, max) {
// liberally copied from stack
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
};
function it_old_behavior(name, capture) {
test(name, async function () {
let done;
let result = new Promise((x) => {
done = x;
});
await capture(done);
await result;
});
}
((perspective) => {
test.describe("ports", function () {
test("Should create port IDs in incremental order", async function () {
const table = await perspective.table(data);
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
table.delete();
});
test("Should create port IDs in incremental order and allow updates on each port", async function () {
const table = await perspective.table(data);
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
for (const port_id of port_ids) {
await table.update(
{
w: [1.5],
x: [port_id],
y: ["d"],
z: [true],
},
{ port_id },
);
}
const view = await table.view();
const output = await view.to_columns();
expect(await table.size()).toEqual(14);
const expected = {
w: [
1.5, 2.5, 3.5, 4.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5,
1.5, 1.5,
],
x: [1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
y: [
"a",
"b",
"c",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
],
z: [
true,
false,
true,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
],
};
expect(output).toEqual(expected);
view.delete();
table.delete();
});
test("Should create port IDs in incremental order and allow updates on each port, indexed", async function () {
const table = await perspective.table(data, { index: "w" });
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
for (const port_id of port_ids) {
await table.update(
{
w: [1.5],
x: [1],
y: ["a"],
z: [true],
},
{ port_id },
);
}
expect(await table.size()).toEqual(4);
const view = await table.view();
const output = await view.to_columns();
expect(output).toEqual(data);
view.delete();
table.delete();
});
test("Should create port IDs in incremental order and allow random arbitrary updates on each port, indexed", async function () {
const table = await perspective.table(data, { index: "w" });
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
for (const port_id of port_ids) {
await table.update(
{
w: [1.5],
x: [1],
y: ["a"],
z: [true],
},
{ port_id },
);
}
expect(await table.size()).toEqual(4);
const view = await table.view();
const output = await view.to_columns();
expect(output).toEqual(data);
// arbitarily update from a random port, force append with new pkey
const port = get_random_int(1, 9);
const update_data = {
w: [5.5],
x: [5],
y: ["e"],
z: [true],
};
await table.update(update_data, { port_id: 0 });
const output2 = await view.to_columns();
expect(output2).toEqual({
w: [1.5, 2.5, 3.5, 4.5, 5.5],
x: [1, 2, 3, 4, 5],
y: ["a", "b", "c", "d", "e"],
z: [true, false, true, false, true],
});
// and do it again but this time with null as pkey
let port2 = get_random_int(1, 9);
while (port2 === port) {
port2 = get_random_int(1, 9);
}
const update_data2 = {
w: [null],
x: [6],
y: ["f"],
z: [true],
};
await table.update(update_data2, { port_id: port2 });
const output3 = await view.to_columns();
expect(output3).toEqual({
w: [null, 1.5, 2.5, 3.5, 4.5, 5.5],
x: [6, 1, 2, 3, 4, 5],
y: ["f", "a", "b", "c", "d", "e"],
z: [true, true, false, true, false, true],
});
view.delete();
table.delete();
});
test.describe("View notifications from different ports", function () {
test("All views should be notified by appends on all ports", async function () {
const table = await perspective.table(data);
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const view = await table.view();
const view2 = await table.view({
group_by: ["w"],
});
const view3 = await table.view({
group_by: ["w"],
split_by: ["x"],
});
for (const port_id of port_ids) {
await table.update(
{
w: [1.5],
x: [port_id],
y: ["d"],
z: [true],
},
{ port_id },
);
}
const output = await view.to_columns();
expect(await table.size()).toEqual(14);
const expected = {
w: [
1.5, 2.5, 3.5, 4.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5,
1.5, 1.5, 1.5,
],
x: [1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
y: [
"a",
"b",
"c",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
],
z: [
true,
false,
true,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
],
};
expect(output).toEqual(expected);
const output2 = await view2.to_columns();
expect(output2).toEqual({
__ROW_PATH__: [[], [1.5], [2.5], [3.5], [4.5]],
w: [27, 16.5, 2.5, 3.5, 4.5],
x: [65, 56, 2, 3, 4],
y: [14, 11, 1, 1, 1],
z: [14, 11, 1, 1, 1],
});
const output3 = await view3.to_columns();
expect(output3).toEqual({
__ROW_PATH__: [[], [1.5], [2.5], [3.5], [4.5]],
"1|w": [3, 3, null, null, null],
"1|x": [2, 2, null, null, null],
"1|y": [2, 2, null, null, null],
"1|z": [2, 2, null, null, null],
"2|w": [4, 1.5, 2.5, null, null],
"2|x": [4, 2, 2, null, null],
"2|y": [2, 1, 1, null, null],
"2|z": [2, 1, 1, null, null],
"3|w": [5, 1.5, null, 3.5, null],
"3|x": [6, 3, null, 3, null],
"3|y": [2, 1, null, 1, null],
"3|z": [2, 1, null, 1, null],
"4|w": [6, 1.5, null, null, 4.5],
"4|x": [8, 4, null, null, 4],
"4|y": [2, 1, null, null, 1],
"4|z": [2, 1, null, null, 1],
"5|w": [1.5, 1.5, null, null, null],
"5|x": [5, 5, null, null, null],
"5|y": [1, 1, null, null, null],
"5|z": [1, 1, null, null, null],
"6|w": [1.5, 1.5, null, null, null],
"6|x": [6, 6, null, null, null],
"6|y": [1, 1, null, null, null],
"6|z": [1, 1, null, null, null],
"7|w": [1.5, 1.5, null, null, null],
"7|x": [7, 7, null, null, null],
"7|y": [1, 1, null, null, null],
"7|z": [1, 1, null, null, null],
"8|w": [1.5, 1.5, null, null, null],
"8|x": [8, 8, null, null, null],
"8|y": [1, 1, null, null, null],
"8|z": [1, 1, null, null, null],
"9|w": [1.5, 1.5, null, null, null],
"9|x": [9, 9, null, null, null],
"9|y": [1, 1, null, null, null],
"9|z": [1, 1, null, null, null],
"10|w": [1.5, 1.5, null, null, null],
"10|x": [10, 10, null, null, null],
"10|y": [1, 1, null, null, null],
"10|z": [1, 1, null, null, null],
});
view3.delete();
view2.delete();
view.delete();
table.delete();
});
test("All views should be notified by partial updates on all ports", async function () {
const table = await perspective.table(data, { index: "w" });
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const view = await table.view();
const view2 = await table.view({
group_by: ["w"],
});
const view3 = await table.view({
group_by: ["w"],
split_by: ["x"],
});
for (const port_id of port_ids) {
await table.update(
{
w: [1.5],
x: [port_id],
y: ["d"],
z: [false],
},
{ port_id },
);
}
const output = await view.to_columns();
expect(await table.size()).toEqual(4);
const expected = {
w: [1.5, 2.5, 3.5, 4.5],
x: [10, 2, 3, 4],
y: ["d", "b", "c", "d"],
z: [false, false, true, false],
};
expect(output).toEqual(expected);
const output2 = await view2.to_columns();
expect(output2).toEqual({
__ROW_PATH__: [[], [1.5], [2.5], [3.5], [4.5]],
w: [12, 1.5, 2.5, 3.5, 4.5],
x: [19, 10, 2, 3, 4],
y: [4, 1, 1, 1, 1],
z: [4, 1, 1, 1, 1],
});
const output3 = await view3.to_columns();
expect(output3).toEqual({
__ROW_PATH__: [[], [1.5], [2.5], [3.5], [4.5]],
"2|w": [2.5, null, 2.5, null, null],
"2|x": [2, null, 2, null, null],
"2|y": [1, null, 1, null, null],
"2|z": [1, null, 1, null, null],
"3|w": [3.5, null, null, 3.5, null],
"3|x": [3, null, null, 3, null],
"3|y": [1, null, null, 1, null],
"3|z": [1, null, null, 1, null],
"4|w": [4.5, null, null, null, 4.5],
"4|x": [4, null, null, null, 4],
"4|y": [1, null, null, null, 1],
"4|z": [1, null, null, null, 1],
"10|w": [1.5, 1.5, null, null, null],
"10|x": [10, 10, null, null, null],
"10|y": [1, 1, null, null, null],
"10|z": [1, 1, null, null, null],
});
view3.delete();
view2.delete();
view.delete();
table.delete();
});
});
test.describe("on_update notifications from different ports", function () {
it_old_behavior(
"All calls to on_update should contain the port ID",
async function (done) {
const table = await perspective.table(data);
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const view = await table.view();
// because no updates were called in port 0, start at 1
let last_port_id = 1;
view.on_update(function (updated) {
expect(updated.port_id).toEqual(last_port_id);
if (last_port_id == 10) {
view.delete();
table.delete();
done();
} else {
last_port_id++;
}
});
for (const port_id of port_ids) {
await table.update(
{
w: [1.5],
x: [1],
y: ["a"],
z: [true],
},
{ port_id },
);
}
},
);
it_old_behavior(
"Only the port that was updated should be notified in on_update",
async function (done) {
const table = await perspective.table(data);
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const view = await table.view();
const port_id = get_random_int(1, 9);
view.on_update(function (updated) {
expect(updated.port_id).toEqual(port_id);
view.delete();
table.delete();
done();
});
await table.update(
{
w: [1.5],
x: [1],
y: ["a"],
z: [true],
},
{ port_id },
);
},
);
it_old_behavior(
"All ports should be notified in creation order, regardless of what order the update is called.",
async function (done) {
const table = await perspective.table(data);
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const view = await table.view();
let last_port_id = 0;
let num_updates = 0;
view.on_update(function (updated) {
expect(updated.port_id).toEqual(last_port_id);
if (num_updates === 10) {
view.delete();
table.delete();
done();
} else {
num_updates++;
last_port_id++;
}
});
const update_order = _.shuffle([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
]);
for (const port_id of update_order) {
table.update(
{
w: [1.5],
x: [1],
y: ["a"],
z: [true],
},
{ port_id },
);
}
},
);
});
test.describe("deltas", function () {
it_old_behavior(
"Deltas should be unique to each port",
async function (done) {
const table = await perspective.table(data);
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const view = await table.view();
let update_count = 0;
view.on_update(
async function (updated) {
const _t = await perspective.table(updated.delta);
const _v = await _t.view();
const result = await _v.to_columns();
if (updated.port_id == first_port) {
expect(result).toEqual({
w: [100],
x: [first_port],
y: ["first port"],
z: [false],
});
} else if (updated.port_id == second_port) {
expect(result).toEqual({
w: [200],
x: [second_port],
y: ["second port"],
z: [true],
});
}
update_count++;
if (update_count === 2) {
_v.delete();
_t.delete();
view.delete();
table.delete();
done();
}
},
{ mode: "row" },
);
const first_port = get_random_int(0, 10);
let second_port = get_random_int(0, 10);
while (second_port === first_port) {
second_port = get_random_int(0, 10);
}
await table.update(
{
w: [100],
x: [first_port],
y: ["first port"],
z: [false],
},
{ port_id: first_port },
);
await table.update(
{
w: [200],
x: [second_port],
y: ["second port"],
z: [true],
},
{ port_id: second_port },
);
},
);
});
test.describe("Cross-table port operations", function () {
it_old_behavior(
"Should allow a client-server round trip without loops",
async function (done) {
const client_table = await perspective.table(data);
const server_table = await perspective.table(data);
const client_port = await client_table.make_port();
// "get rid" of a random number of ports by creating them
for (let i = 0; i < get_random_int(5, 20); i++) {
await server_table.make_port();
}
const server_port = await server_table.make_port();
expect(client_port).toBeLessThan(server_port);
const client_view = await client_table.view();
let CLIENT_TO_SERVER_UPDATES = 0;
client_view.on_update(
async (updated) => {
if (updated.port_id === client_port) {
await server_table.update(updated.delta, {
port_id: server_port,
});
CLIENT_TO_SERVER_UPDATES++;
} else {
// Should not pass forward that update, and break
// the chain.
expect(CLIENT_TO_SERVER_UPDATES).toEqual(1);
}
},
{ mode: "row" },
);
const server_view = await server_table.view();
server_view.on_update(
async (updated) => {
if (updated.port_id !== server_port) {
await client_table.update(updated.delta);
} else {
// update is trapped in this state - assert correct
// state and exit the test.
expect(await client_table.size()).toEqual(8);
expect(await server_table.size()).toEqual(8);
client_view.delete();
server_view.delete();
client_table.delete();
server_table.delete();
done();
}
},
{ mode: "row" },
);
// this should go to the server and round trip back to the
// client, but stop when it gets to the client again.
await client_table.update(data, { port_id: client_port });
},
);
it_old_behavior(
"Should allow multi client-server round trips without loops",
async function (done) {
const client_table = await perspective.table(data, {
index: "w",
});
const client_table2 = await perspective.table(data, {
index: "w",
});
const server_table = await perspective.table(data, {
index: "w",
});
const client_port = await client_table.make_port();
const client_port2 = await client_table2.make_port();
// "get rid" of a random number of ports by creating them
for (let i = 0; i < get_random_int(5, 20); i++) {
await server_table.make_port();
}
const server_port = await server_table.make_port();
const server_port2 = await server_table.make_port();
expect(client_port).toBeLessThan(server_port);
expect(client_port2).toBeLessThan(server_port);
expect(client_port).toBeLessThan(server_port2);
expect(client_port2).toBeLessThan(server_port2);
// port numbers can be equal between clients
expect(client_port2).toEqual(client_port);
const client_view = await client_table.view();
const client_view2 = await client_table2.view();
client_view.on_update(
async (updated) => {
// client1 and server handlers are as minimal as can be
if (updated.port_id === client_port) {
await server_table.update(updated.delta, {
port_id: server_port,
});
}
},
{ mode: "row" },
);
client_view2.on_update(
async (updated) => {
// client2 only reads updates from other clients
const results = await client_view2.to_columns();
expect(results).toEqual({
w: [1.5, 2.5, 3.5, 4.5],
x: [1, 2, 300, 4],
y: ["a", "b", "ccc", "d"],
z: [true, false, true, false],
});
expect(await client_view.to_columns()).toEqual(
results,
);
expect(await server_view.to_columns()).toEqual(
results,
);
client_view.delete();
client_view2.delete();
server_view.delete();
client_table.delete();
client_table2.delete();
server_table.delete();
done();
},
{ mode: "row" },
);
const server_view = await server_table.view();
// Simulate multiple connections to the server
server_view.on_update(
async (updated) => {
if (updated.port_id !== server_port) {
await client_table.update(updated.delta);
}
},
{ mode: "row" },
);
server_view.on_update(
async (updated) => {
if (updated.port_id !== server_port2) {
await client_table2.update(updated.delta);
}
},
{ mode: "row" },
);
// this should go to the server and round trip back to the
// client, but stop when it gets to the client again. It should
// reflect on the client2 table though.
await client_table.update(
[
{
w: 3.5,
x: 300,
y: "ccc",
},
],
{ port_id: client_port },
);
},
);
});
});
})(perspective);
@@ -0,0 +1,102 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import { make_client, PerspectiveServer } from "@perspective-dev/client";
test("Proxy session tunnels requests through client", async () => {
const { client, server } = connectClientToServer();
const { proxyClient } = connectProxyClient(client);
// Verify that main client + proxy client observe the same server
const clientTables1 = await client.get_hosted_table_names();
const proxyTables1 = await proxyClient.get_hosted_table_names();
expect(proxyTables1).toStrictEqual([]);
expect(proxyTables1).toStrictEqual(clientTables1);
const name = "abc-" + Math.random();
const _table = await client.table({ abc: [123] }, { name });
const clientTables2 = await client.get_hosted_table_names();
const proxyTables2 = await proxyClient.get_hosted_table_names();
expect(proxyTables2).toStrictEqual([name]);
expect(proxyTables2).toStrictEqual(clientTables2);
});
test("Proxy session tunnels on_update callbacks through client", async () => {
// test.setTimeout(2000);
const { client } = connectClientToServer();
const { proxyClient } = connectProxyClient(client);
const name = "abc-" + Math.random();
const clientTable = await client.table({ abc: [123] }, { name });
// Add an on_update callback to the proxy client's view of the table
const proxyTable = await proxyClient.open_table(name);
const proxyView = await proxyTable.view();
let resolveUpdate;
const onUpdateResp = new Promise((r) => (resolveUpdate = r));
await proxyView.on_update(
(x) => {
resolveUpdate(x);
},
{ mode: "row" },
);
// Enact table update through client's table handle, and assert that proxy
// client's on_update callback is called
await clientTable.update({ abc: [999] });
const expectUpdate = expect.poll(
async () => {
const data = await onUpdateResp;
// TODO: construct table out of onUpdateResp, assert contents?
console.log("onUpdateResp, with data", data);
return data;
},
{
message: "Ensure proxy view updates with table",
timeout: 10000,
},
);
expect(await proxyView.to_columns()).toStrictEqual({ abc: [123, 999] });
await expectUpdate.toHaveProperty("delta");
await expectUpdate.toHaveProperty("port_id");
});
function connectClientToServer() {
const server = new PerspectiveServer();
const session = server.make_session(async (msg) => {
await client.handle_response(msg);
});
const client = make_client(async (msg) => {
session.handle_request(msg);
});
return {
client,
server,
};
}
function connectProxyClient(client) {
const sess = client.new_proxy_session((res) => {
proxyClient.handle_response(res);
});
const proxyClient = make_client(async (msg) => {
// Have to copy msg with slice() because the memory backing it will be
// deallocated before handle_request() is scheduled and executes
await sess.handle_request(msg.slice());
});
return {
proxyClient,
};
}
+250
View File
@@ -0,0 +1,250 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "@perspective-dev/client";
let server;
let port;
test.describe("WebSocketManager", function () {
test.beforeEach(() => {
server = new perspective.WebSocketServer({ port: 0 });
port = server._server.address().port;
});
test.afterEach(() => {
server.close();
});
test("sends initial data client on subscribe", async () => {
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const client = await perspective.websocket(`ws://localhost:${port}`);
const client_table = await client.open_table("test");
const client_view = await client_table.view();
const client_data = await client_view.to_json();
expect(client_data).toEqual(data);
await client.terminate();
// // TODO Can't await delete here because `terminate()` doesn't
// // guarantee server-side cleanup happens immediately. So this test
// // leaks a small `Table` sometimes, as do several others in this
// // suite.
await new Promise((x) => setTimeout(x, 10));
await table.delete();
});
test("Throws an exception when the server is detached", async () => {
const data = [{ x: 1 }];
const _table = await perspective.table(data, { name: "test2" });
const client = await perspective.websocket(`ws://localhost:${port}`);
const client_table = await client.open_table("test2");
const client_view = await client_table.view();
await server.close();
const client_data = client_view.to_json();
await expect(client_data).rejects.toThrow();
});
test("passes back errors from server", async () => {
expect.assertions(2);
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const client = await perspective.websocket(`ws://localhost:${port}`);
const client_table = await client.open_table("test");
client_table.view({ columns: ["z"] }).catch((error) => {
expect(error.message).toContain(
"Abort(): Invalid column 'z' found in View columns.\n",
);
});
const client_view = await client_table.view();
const client_data = await client_view.to_json();
expect(client_data).toEqual(data);
await client.terminate();
await new Promise((x) => setTimeout(x, 10));
await table.delete();
});
test("sends initial data multiple client on subscribe", async () => {
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const client_1 = await perspective.websocket(`ws://localhost:${port}`);
const client_2 = await perspective.websocket(`ws://localhost:${port}`);
const client_1_table = await client_1.open_table("test");
const client_2_table = await client_2.open_table("test");
const client_1_view = await client_1_table.view();
const client_2_view = await client_2_table.view();
const client_1_data = await client_1_view.to_json();
const client_2_data = await client_2_view.to_json();
await client_1.terminate();
await client_2.terminate();
expect(client_1_data).toEqual(data);
expect(client_2_data).toEqual(data);
await new Promise((x) => setTimeout(x, 10));
await table.delete();
});
test("passes back errors with multiple client on subscribe", async () => {
expect.assertions(3);
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const client_1 = await perspective.websocket(`ws://localhost:${port}`);
const client_2 = await perspective.websocket(`ws://localhost:${port}`);
const client_1_table = await client_1.open_table("test");
const client_2_table = await client_2.open_table("test");
client_1_table.view({ columns: ["z"] }).catch((error) => {
expect(error.message).toContain(
"Abort(): Invalid column 'z' found in View columns.\n",
);
});
const client_1_view = await client_1_table.view();
const client_2_view = await client_2_table.view();
const client_1_data = await client_1_view.to_json();
const client_2_data = await client_2_view.to_json();
await client_1.terminate();
await client_2.terminate();
expect(client_1_data).toEqual(data);
expect(client_2_data).toEqual(data);
await new Promise((x) => setTimeout(x, 10));
await table.delete();
});
test("sends updates to client on subscribe", async () => {
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const client = await perspective.websocket(`ws://localhost:${port}`);
const client_table = await client.open_table("test");
const client_view = await client_table.view();
let done;
let result = new Promise((x) => {
done = x;
});
const on_update = () => {
client_view.to_json().then(async (updated_data) => {
await client_view.delete();
await table.delete();
expect(updated_data).toEqual([{ x: 1 }, { x: 2 }]);
await client.terminate();
setTimeout(done);
});
};
client_view.on_update(on_update);
client_view.to_json().then((client_data) => {
expect(client_data).toEqual(data);
return table.update([{ x: 2 }]);
});
await result;
});
test("Calls `update` and sends arraybuffers using `binary_length`", async () => {
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const view = await table.view();
const arrow = await view.to_arrow();
const client = await perspective.websocket(`ws://localhost:${port}`);
const client_table = await client.open_table("test");
await client_table.update(arrow);
const client_view = await client_table.view();
const client_data = await client_view.to_json();
expect(client_data).toEqual([{ x: 1 }, { x: 1 }]);
await client_view.delete();
await client.terminate();
await view.delete();
await table.delete();
});
test("Calls `update` and sends `Uint8Array` using `binary_length` multiple times", async () => {
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const view = await table.view();
const arrow = await view.to_arrow();
const client = await perspective.websocket(`ws://localhost:${port}`);
const client_table = await client.open_table("test");
await client_table.update(arrow);
await client_table.update(arrow);
await client_table.update(arrow);
const client_view = await client_table.view();
const client_data = await client_view.to_json();
expect(client_data).toEqual([{ x: 1 }, { x: 1 }, { x: 1 }, { x: 1 }]);
await client_view.delete();
await view.delete();
await table.delete();
});
test("Calls `update` and sends `Uint8Array` using `on_update`", async () => {
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const view = await table.view();
const arrow = await view.to_arrow();
let update_port;
let done;
let result = new Promise((x) => {
done = x;
});
const updater = async (updated) => {
expect(updated.port_id).toEqual(update_port);
expect(updated.delta instanceof Uint8Array).toEqual(true);
expect(updated.delta.byteLength).toBeGreaterThan(0);
await view.delete();
await table.delete();
await client.terminate();
done();
};
view.on_update(updater, { mode: "row" });
const client = await perspective.websocket(`ws://localhost:${port}`);
const client_table = await client.open_table("test");
for (let i = 0; i < 5; i++) {
// take up some ports on the remote table
await client_table.make_port();
}
update_port = await client_table.make_port();
await client_table.update(arrow, { port_id: update_port });
await result;
});
// TODO ping loop is not yet implemented
test.skip("disables ping loop on disconnect", async () => {
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const client = await perspective.websocket(`ws://localhost:${port}`);
const _client_table = await client.open_table("test");
expect(await _client_table.size()).toEqual(1);
expect(client._ping_loop).toBeDefined();
await server.close();
expect(client._ping_loop).toBeUndefined();
await table.delete();
});
test("Can get hosted table names", async () => {
const data = [{ x: 1 }];
const host_table_name = "test";
const table = await perspective.table(data, { name: host_table_name });
const client = await perspective.websocket(`ws://localhost:${port}`);
const table_names = await client.get_hosted_table_names();
expect(table_names).toContain(host_table_name);
await client.terminate();
await table.delete();
});
});
+490
View File
@@ -0,0 +1,490 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
const _query = async (should_cache, table, config = {}, callback) => {
let cached_view;
if (should_cache) {
cached_view = await table.view(config);
}
const data_getter = async () => {
if (should_cache) {
return await cached_view.to_columns();
} else {
const view = await table.view(config);
const data = await view.to_columns();
await view.delete();
return data;
}
};
const v = await callback(data_getter);
if (cached_view) {
cached_view.delete();
}
return v;
};
const SCHEMA = {
str: "string",
int: "integer",
float: "float",
};
test.describe("Removes", () => {
for (const cond of [true, false]) {
const query = _query.bind(undefined, cond);
for (const idx of ["str", "int"]) {
const string_pkey = idx === "str";
test.describe(`View cached? ${cond}, index: ${idx}`, () => {
test("Output consistent with filter", async () => {
const table = await perspective.table(SCHEMA, {
index: idx,
});
const data = {
str: [1, 2, 3, 4, 5, 6].map((x) => x.toString()),
int: [1, 2, 3, 4, 5, 6],
float: [1, 2, 3, 4, 5, 6].map((x) => x * 0.5),
};
await table.update(data);
await query(
table,
{
filter: [["str", "!=", "2"]],
},
async (getter) => {
const pkey = 3;
await table.remove([
string_pkey ? pkey.toString() : pkey,
]);
expect(await getter()).toEqual({
str: ["1", "4", "5", "6"],
int: [1, 4, 5, 6],
float: [0.5, 2, 2.5, 3],
});
},
);
await table.delete();
});
test("Output consistent with filter, empty string", async () => {
const table = await perspective.table(SCHEMA, {
index: idx,
});
const data = {
str: [1, 2, 3, 4, 5, 6].map((x) => x.toString()),
int: [1, 2, 3, 4, 5, 6],
float: [1, 2, 3, 4, 5, 6].map((x) => x * 0.5),
};
data.str.push("");
data.int.push(7);
data.float.push(3.5);
await table.update(data);
await query(
table,
{
filter: [["str", "!=", "2"]],
},
async (getter) => {
const pkey = 3;
await table.remove([
string_pkey ? pkey.toString() : pkey,
]);
if (string_pkey) {
expect(await getter()).toEqual({
str: ["", "1", "4", "5", "6"],
int: [7, 1, 4, 5, 6],
float: [3.5, 0.5, 2, 2.5, 3],
});
} else {
expect(await getter()).toEqual({
str: ["1", "4", "5", "6", ""],
int: [1, 4, 5, 6, 7],
float: [0.5, 2, 2.5, 3, 3.5],
});
}
},
);
await table.delete();
});
test("Output consistent with filter, null", async () => {
const table = await perspective.table(SCHEMA, {
index: idx,
});
const data = {
str: [1, 2, 3, 4, 5, 6].map((x) => x.toString()),
int: [1, 2, 3, 4, 5, 6],
float: [1, 2, 3, 4, 5, 6].map((x) => x * 0.5),
};
data.str.push(null);
data.int.push(7);
data.float.push(3.5);
await table.update(data);
await query(
table,
{
filter: [["str", "!=", "2"]],
},
async (getter) => {
const pkey = 3;
await table.remove([
string_pkey ? pkey.toString() : pkey,
]);
if (string_pkey) {
expect(await getter()).toEqual({
str: [null, "1", "4", "5", "6"],
int: [7, 1, 4, 5, 6],
float: [3.5, 0.5, 2, 2.5, 3],
});
} else {
expect(await getter()).toEqual({
str: ["1", "4", "5", "6", null],
int: [1, 4, 5, 6, 7],
float: [0.5, 2, 2.5, 3, 3.5],
});
}
},
);
await table.delete();
});
test("Output consistent with filter, remove before view construction, sorted", async () => {
const table = await perspective.table(SCHEMA, {
index: idx,
});
const data = {
str: [1, 2, 3, 4, 5, 6].map((x) => x.toString()),
int: [1, 2, 3, 4, 5, 6],
float: [1, 2, 3, 4, 5, 6].map((x) => x * 0.5),
};
await table.update(data);
await table.remove(
[1, 3, 5].map((x) => (string_pkey ? x.toString() : x)),
);
await query(
table,
{
sort: [["str", "desc"]],
},
async (getter) => {
expect(await getter()).toEqual({
str: ["6", "4", "2"],
int: [6, 4, 2],
float: [3, 2, 1],
});
await table.update({
str: ["7", "9", "8"],
int: [7, 9, 8],
float: [3.5, 4.5, 4],
});
expect(await getter()).toEqual({
str: ["9", "8", "7", "6", "4", "2"],
int: [9, 8, 7, 6, 4, 2],
float: [4.5, 4, 3.5, 3, 2, 1],
});
},
);
await table.delete();
});
test("Output consistent with filter, remove and add identical", async () => {
const table = await perspective.table(SCHEMA, {
index: idx,
});
const data = {
str: [1, 2, 3, 4, 5, 6].map((x) => x.toString()),
int: [1, 2, 3, 4, 5, 6],
float: [1, 2, 3, 4, 5, 6].map((x) => x * 0.5),
};
await table.update(data);
await query(
table,
{
filter: [["str", "!=", "2"]],
},
async (getter) => {
const pkey = 3;
await table.remove([
string_pkey ? pkey.toString() : pkey,
]);
expect(await getter()).toEqual({
str: ["1", "4", "5", "6"],
int: [1, 4, 5, 6],
float: [0.5, 2, 2.5, 3],
});
await table.update({
str: ["7", "9", "8"],
int: [7, 9, 8],
float: [3.5, 4.5, 4],
});
expect(await getter()).toEqual({
str: ["1", "4", "5", "6", "7", "8", "9"],
int: [1, 4, 5, 6, 7, 8, 9],
float: [0.5, 2, 2.5, 3, 3.5, 4, 4.5],
});
},
);
await table.delete();
});
test("Output consistent with filter, add null and remove other", async () => {
const table = await perspective.table(SCHEMA, {
index: idx,
});
const data = {
str: [1, 2, 3, 4, 5, 6].map((x) => x.toString()),
int: [1, 2, 3, 4, 5, 6],
float: [1, 2, 3, 4, 5, 6].map((x) => x * 0.5),
};
await table.update(data);
await query(table, {}, async (getter) => {
expect(await getter()).toEqual(data);
await table.update({
str: [string_pkey ? null : "7"],
int: [string_pkey ? 7 : null],
float: [3.5],
});
if (string_pkey) {
expect(await getter()).toEqual({
str: [null, 1, 2, 3, 4, 5, 6].map((x) =>
x ? x.toString() : x,
),
int: [7, 1, 2, 3, 4, 5, 6],
float: [7, 1, 2, 3, 4, 5, 6].map(
(x) => x * 0.5,
),
});
} else {
expect(await getter()).toEqual({
str: [7, 1, 2, 3, 4, 5, 6].map((x) =>
x.toString(),
),
int: [null, 1, 2, 3, 4, 5, 6],
float: [7, 1, 2, 3, 4, 5, 6].map(
(x) => x * 0.5,
),
});
}
await table.remove([string_pkey ? "3" : 3]);
if (string_pkey) {
expect(await getter()).toEqual({
str: [null, 1, 2, 4, 5, 6].map((x) =>
x ? x.toString() : x,
),
int: [7, 1, 2, 4, 5, 6],
float: [7, 1, 2, 4, 5, 6].map((x) => x * 0.5),
});
} else {
expect(await getter()).toEqual({
str: [7, 1, 2, 4, 5, 6].map((x) =>
x.toString(),
),
int: [null, 1, 2, 4, 5, 6],
float: [7, 1, 2, 4, 5, 6].map((x) => x * 0.5),
});
}
});
await table.delete();
});
test("Output consistent with filter, remove and add new dataset", async () => {
const table = await perspective.table(SCHEMA, {
index: idx,
});
const data = {
str: [1, 2, 3, 4, 5, 6].map((x) => x.toString()),
int: [1, 2, 3, 4, 5, 6],
float: [1, 2, 3, 4, 5, 6].map((x) => x * 0.5),
};
await table.update(data);
await query(
table,
{
filter: [["int", "!=", 3]],
},
async (getter) => {
await table.remove(
[1, 2, 3, 4, 5, 6].map((x) =>
string_pkey ? x.toString() : x,
),
);
expect(await getter()).toEqual({
float: [],
int: [],
str: [],
});
await table.update({
str: ["def", "abc", "deff"],
int: [1, 2, 4],
float: [100.5, 200.5, 300.5],
});
if (string_pkey) {
expect(await getter()).toEqual({
str: ["abc", "def", "deff"],
int: [2, 1, 4],
float: [200.5, 100.5, 300.5],
});
} else {
expect(await getter()).toEqual({
str: ["def", "abc", "deff"],
int: [1, 2, 4],
float: [100.5, 200.5, 300.5],
});
}
},
);
await table.delete();
});
test.skip("Output consistent with filter, remove and add null", async () => {
// FIXME
const table = await perspective.table(SCHEMA, {
index: idx,
});
const data = {
str: [1, 2, 3, 4, 5, 6].map((x) => x.toString()),
int: [1, 2, 3, 4, 5, 6],
float: [1, 2, 3, 4, 5, 6].map((x) => x * 0.5),
};
const splice_idx = Math.floor(Math.random() * 6);
data.str.splice(splice_idx, 0, string_pkey ? null : "7");
data.int.splice(splice_idx, 0, string_pkey ? 7 : null);
data.float.splice(splice_idx, 0, 3.5);
await table.update(data);
await query(
table,
{
filter: [["float", "==", 3.5]],
},
async (getter) => {
await table.remove([null]);
const v = await table.view();
console.log(
"str?",
string_pkey,
await v.to_columns(),
);
await v.delete();
expect(await getter()).toEqual({});
await table.update({
str: [string_pkey ? null : "7"],
int: [string_pkey ? 7 : null],
float: [3.5],
});
expect(await getter()).toEqual({
str: [string_pkey ? null : "7"],
int: [string_pkey ? 7 : null],
float: [3.5],
});
},
);
await table.delete();
});
test.skip("Output consistent with filter, update null", async () => {
const table = await perspective.table(SCHEMA, {
index: idx,
});
const data = {
str: [1, 2, 3, 4, 5, 6].map((x) => x.toString()),
int: [1, 2, 3, 4, 5, 6],
float: [1, 2, 3, 4, 5, 6].map((x) => x * 0.5),
};
const splice_idx = Math.floor(Math.random() * 6);
data.str.splice(splice_idx, 0, string_pkey ? null : "7");
data.int.splice(splice_idx, 0, string_pkey ? 7 : null);
data.float.splice(splice_idx, 0, 3.5);
await table.update(data);
await query(table, {}, async (getter) => {
const expected = {
str: data.str.slice(),
int: data.int.slice(),
float: data.float.slice(),
};
expect(await getter()).toEqual(expected);
await table.update([
{
str: string_pkey ? null : "7",
int: string_pkey ? 7 : null,
float: 4.5,
},
]);
expected.float[splice_idx] = 4.5;
expect(await getter()).toEqual(expected);
});
await table.delete();
});
});
}
}
});
+896
View File
@@ -0,0 +1,896 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
const data = {
w: [1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5],
x: [1, 2, 3, 4, 4, 3, 2, 1],
y: ["a", "b", "c", "d", "a", "b", "c", "d"],
z: [true, false, true, false, true, false, true, false],
};
const data2 = {
w: [3.5, 4.5, null, null, null, null, 1.5, 2.5],
x: [1, 2, 3, 4, 4, 3, 2, 1],
y: ["a", "b", "c", "d", "e", "f", "g", "h"],
};
const data3 = {
w: [3.5, 4.5, null, null, null, null, 1.5, 2.5],
x: [1, 2, 3, 4, 4, 3, 2, 1],
y: ["a", "a", "a", "a", "b", "b", "b", "b"],
};
((perspective) => {
test.describe("Sorts", function () {
test.describe("With nulls", () => {
test("asc", async function () {
var table = await perspective.table(data2);
var view = await table.view({
columns: ["x", "w"],
sort: [["w", "asc"]],
});
const json = await view.to_columns();
expect(json).toEqual({
w: [null, null, null, null, 1.5, 2.5, 3.5, 4.5],
x: [3, 4, 4, 3, 2, 1, 1, 2],
});
view.delete();
table.delete();
});
test("desc", async function () {
var table = await perspective.table(data2);
var view = await table.view({
columns: ["x", "w"],
sort: [["w", "desc"]],
});
const json = await view.to_columns();
expect(json).toEqual({
w: [4.5, 3.5, 2.5, 1.5, null, null, null, null],
x: [2, 1, 1, 2, 3, 4, 4, 3],
});
view.delete();
table.delete();
});
test("asc datetime", async function () {
var table = await perspective.table({
w: [
new Date(2020, 0, 1, 12, 30, 45),
new Date(2020, 0, 1),
null,
null,
null,
null,
new Date(2008, 0, 1, 12, 30, 45),
new Date(2020, 12, 1, 12, 30, 45),
],
x: [1, 2, 3, 4, 4, 3, 2, 1],
y: ["a", "b", "c", "d", "e", "f", "g", "h"],
});
var view = await table.view({
columns: ["x", "w"],
sort: [["w", "asc"]],
});
const json = await view.to_columns();
expect(json).toEqual({
w: [
null,
null,
null,
null,
new Date(2008, 0, 1, 12, 30, 45).getTime(),
new Date(2020, 0, 1).getTime(),
new Date(2020, 0, 1, 12, 30, 45).getTime(),
new Date(2020, 12, 1, 12, 30, 45).getTime(),
],
x: [3, 4, 4, 3, 2, 2, 1, 1],
});
view.delete();
table.delete();
});
test("desc datetime", async function () {
var table = await perspective.table({
w: [
new Date(2020, 0, 1, 12, 30, 45),
new Date(2020, 0, 1),
null,
null,
null,
null,
new Date(2008, 0, 1, 12, 30, 45),
new Date(2020, 12, 1, 12, 30, 45),
],
x: [1, 2, 3, 4, 4, 3, 2, 1],
y: ["a", "b", "c", "d", "e", "f", "g", "h"],
});
var view = await table.view({
columns: ["x", "w"],
sort: [["w", "desc"]],
});
const json = await view.to_columns();
expect(json).toEqual({
w: [
new Date(2020, 12, 1, 12, 30, 45).getTime(),
new Date(2020, 0, 1, 12, 30, 45).getTime(),
new Date(2020, 0, 1).getTime(),
new Date(2008, 0, 1, 12, 30, 45).getTime(),
null,
null,
null,
null,
],
x: [1, 1, 2, 2, 3, 4, 4, 3],
});
view.delete();
table.delete();
});
});
test.describe("With aggregates", function () {
test.describe("aggregates, in a sorted column with nulls", function () {
test("sum", async function () {
var table = await perspective.table(data2);
var view = await table.view({
columns: ["x", "w"],
group_by: ["y"],
aggregates: {
w: "sum",
x: "unique",
},
sort: [["w", "asc"]],
});
const json = await view.to_columns();
expect(json).toEqual({
__ROW_PATH__: [
[],
["c"],
["d"],
["e"],
["f"],
["g"],
["h"],
["a"],
["b"],
],
w: [12, 0, 0, 0, 0, 1.5, 2.5, 3.5, 4.5],
x: [null, 3, 4, 4, 3, 2, 1, 1, 2],
});
view.delete();
table.delete();
});
test("sum of floats", async function () {
var table = await perspective.table({
w: [3.25, 4.51, null, null, null, null, 1.57, 2.59],
x: [1, 2, 3, 4, 4, 3, 2, 1],
y: ["a", "b", "c", "d", "e", "f", "g", "h"],
});
var view = await table.view({
columns: ["x", "w"],
group_by: ["y"],
aggregates: {
w: "sum",
x: "unique",
},
sort: [["w", "asc"]],
});
const json = await view.to_columns();
expect(json).toEqual({
__ROW_PATH__: [
[],
["c"],
["d"],
["e"],
["f"],
["g"],
["h"],
["a"],
["b"],
],
w: [11.92, 0, 0, 0, 0, 1.57, 2.59, 3.25, 4.51],
x: [null, 3, 4, 4, 3, 2, 1, 1, 2],
});
view.delete();
table.delete();
});
test("unique", async function () {
var table = await perspective.table(data2);
var view = await table.view({
columns: ["x", "w"],
group_by: ["y"],
aggregates: {
w: "unique",
x: "unique",
},
sort: [["w", "asc"]],
});
const json = await view.to_columns();
expect(json).toEqual({
__ROW_PATH__: [
[],
["c"],
["d"],
["e"],
["f"],
["g"],
["h"],
["a"],
["b"],
],
w: [null, null, null, null, null, 1.5, 2.5, 3.5, 4.5],
x: [null, 3, 4, 4, 3, 2, 1, 1, 2],
});
view.delete();
table.delete();
});
test("avg", async function () {
var table = await perspective.table(data2);
var view = await table.view({
columns: ["x", "w"],
group_by: ["y"],
aggregates: {
w: "avg",
x: "unique",
},
sort: [["w", "asc"]],
});
const json = await view.to_columns();
expect(json).toEqual({
__ROW_PATH__: [
[],
["c"],
["d"],
["e"],
["f"],
["g"],
["h"],
["a"],
["b"],
],
w: [3, null, null, null, null, 1.5, 2.5, 3.5, 4.5],
x: [null, 3, 4, 4, 3, 2, 1, 1, 2],
});
// Broken result:
// {
// __ROW_PATH__: [[], ["a"], ["b"], ["c"], ["d"], ["e"], ["f"], ["g"], ["h"]],
// w: [3, 3.5, 4.5, null, null, null, null, 1.5, 2.5],
// x: [null, 1, 2, 3, 4, 4, 3, 2, 1]
// };
view.delete();
table.delete();
});
test.describe("Multiple hidden sort", () => {
test("sum", async function () {
var table = await perspective.table(data3);
var view = await table.view({
columns: ["x", "w"],
group_by: ["y"],
aggregates: {
w: "sum",
},
sort: [
["x", "desc"],
["w", "desc"],
],
});
const json = await view.to_columns();
expect(json).toEqual({
__ROW_PATH__: [[], ["a"], ["b"]],
w: [12, 8, 4],
x: [20, 10, 10],
});
view.delete();
table.delete();
});
test("sum of floats", async function () {
var table = await perspective.table({
w: [3.25, 4.51, null, null, null, null, 1.57, 2.59],
x: [1, 2, 3, 4, 4, 3, 2, 1],
y: ["a", "a", "a", "a", "b", "b", "b", "b"],
});
var view = await table.view({
columns: ["x", "w"],
group_by: ["y"],
aggregates: {
w: "sum",
},
sort: [
["x", "desc"],
["w", "desc"],
],
});
const json = await view.to_columns();
expect(json).toEqual({
__ROW_PATH__: [[], ["a"], ["b"]],
w: [11.92, 7.76, 4.16],
x: [20, 10, 10],
});
view.delete();
table.delete();
});
test("unique", async function () {
var table = await perspective.table(data3);
var view = await table.view({
columns: ["x", "w"],
group_by: ["y"],
aggregates: {
w: "unique",
},
sort: [
["x", "desc"],
["w", "desc"],
],
});
const json = await view.to_columns();
expect(json).toEqual({
__ROW_PATH__: [[], ["a"], ["b"]],
w: [null, null, null],
x: [20, 10, 10],
});
view.delete();
table.delete();
});
test("avg", async function () {
var table = await perspective.table(data3);
var view = await table.view({
columns: ["x", "w"],
group_by: ["y"],
aggregates: {
w: "avg",
},
sort: [
["x", "desc"],
["w", "desc"],
],
});
const json = await view.to_columns();
expect(json).toEqual({
__ROW_PATH__: [[], ["a"], ["b"]],
// 4 and 2 are the avg of the non-null rows
w: [3, 4, 2],
x: [20, 10, 10],
});
view.delete();
table.delete();
});
});
});
});
test.describe("On hidden columns", function () {
test("Column path should not emit hidden sorts", async function () {
var table = await perspective.table(data);
var view = await table.view({
columns: ["w", "y"],
sort: [["x", "desc"]],
});
const paths = await view.column_paths();
expect(paths).toEqual(["w", "y"]);
view.delete();
table.delete();
});
test("Column path should not emit hidden column sorts", async function () {
var table = await perspective.table(data);
var view = await table.view({
columns: ["w"],
group_by: ["y"],
split_by: ["z"],
sort: [["x", "col desc"]],
});
const paths = await view.column_paths();
expect(paths).toEqual(["false|w", "true|w"]);
view.delete();
table.delete();
});
test("Column path should not emit hidden regular and column sorts", async function () {
var table = await perspective.table(data);
var view = await table.view({
columns: ["w"],
group_by: ["y"],
split_by: ["z"],
sort: [
["x", "col desc"],
["y", "desc"],
],
});
const paths = await view.column_paths();
expect(paths).toEqual(["false|w", "true|w"]);
view.delete();
table.delete();
});
test("unpivoted", async function () {
var table = await perspective.table(data);
var view = await table.view({
columns: ["w", "y"],
sort: [["x", "desc"]],
});
var answer = [
{ w: 4.5, y: "d" },
{ w: 5.5, y: "a" },
{ w: 3.5, y: "c" },
{ w: 6.5, y: "b" },
{ w: 2.5, y: "b" },
{ w: 7.5, y: "c" },
{ w: 1.5, y: "a" },
{ w: 8.5, y: "d" },
];
let result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test("group by ['y']", async function () {
var table = await perspective.table(data);
var view = await table.view({
columns: ["w"],
group_by: ["y"],
sort: [["x", "desc"]],
});
var answer = [
{ __ROW_PATH__: [], w: 40 },
{ __ROW_PATH__: ["a"], w: 7 },
{ __ROW_PATH__: ["b"], w: 9 },
{ __ROW_PATH__: ["c"], w: 11 },
{ __ROW_PATH__: ["d"], w: 13 },
];
let result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test("group by and hidden sort ['y'] with aggregates specified", async function () {
const table = await perspective.table({
x: [1, 2, 3, 4, 5],
y: ["a", "b", "b", "b", "c"],
});
// Aggregate should be overriden if the sort column is hidden
// AND also in group by
const view = await table.view({
aggregates: {
x: "sum",
y: "count",
},
columns: ["x"],
group_by: ["y"],
sort: [["y", "desc"]],
});
const answer = [
{ __ROW_PATH__: [], x: 15 },
{ __ROW_PATH__: ["c"], x: 5 },
{ __ROW_PATH__: ["b"], x: 9 },
{ __ROW_PATH__: ["a"], x: 1 },
];
const result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test("group by and hidden sort ['y'] without aggregates specified", async function () {
const table = await perspective.table({
x: [1, 2, 3, 4, 5],
y: ["a", "b", "b", "b", "c"],
});
const view = await table.view({
columns: ["x"],
group_by: ["y"],
sort: [["y", "desc"]],
});
const answer = [
{ __ROW_PATH__: [], x: 15 },
{ __ROW_PATH__: ["c"], x: 5 },
{ __ROW_PATH__: ["b"], x: 9 },
{ __ROW_PATH__: ["a"], x: 1 },
];
const result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test("group by with multiple hidden sorts emits all visible columns in to_columns()", async function () {
// https://github.com/perspective-dev/perspective/issues/3195
const table = await perspective.table({
v1: [1, 2, 3, 4],
v2: [10, 20, 30, 40],
v3: [100, 200, 300, 400],
h1: [1.5, 2.5, 3.5, 4.5],
h2: [4.5, 3.5, 2.5, 1.5],
g: ["a", "a", "b", "b"],
});
const view = await table.view({
columns: ["v1", "v2", "v3"],
group_by: ["g"],
sort: [
["h1", "desc"],
["h2", "desc"],
],
});
const result = await view.to_columns();
expect(result).toEqual({
__ROW_PATH__: [[], ["b"], ["a"]],
v1: [10, 7, 3],
v2: [100, 70, 30],
v3: [1000, 700, 300],
});
view.delete();
table.delete();
});
test("group by with more hidden sorts than visible columns does not emit hidden columns in to_columns()", async function () {
// https://github.com/perspective-dev/perspective/issues/3195
const table = await perspective.table({
v1: [1, 2, 3, 4],
h1: [1.5, 2.5, 3.5, 4.5],
h2: [4.5, 3.5, 2.5, 1.5],
g: ["a", "a", "b", "b"],
});
const view = await table.view({
columns: ["v1"],
group_by: ["g"],
sort: [
["h1", "desc"],
["h2", "desc"],
],
});
const result = await view.to_columns();
expect(result).toEqual({
__ROW_PATH__: [[], ["b"], ["a"]],
v1: [10, 7, 3],
});
view.delete();
table.delete();
});
test("split by ['y']", async function () {
const table = await perspective.table(data);
const view = await table.view({
columns: ["w"],
split_by: ["y"],
sort: [["x", "desc"]],
});
const paths = await view.column_paths();
expect(paths).toEqual(["a|w", "b|w", "c|w", "d|w"]);
const answer = [
{ "a|w": null, "b|w": null, "c|w": null, "d|w": 4.5 },
{ "a|w": 5.5, "b|w": null, "c|w": null, "d|w": null },
{ "a|w": null, "b|w": null, "c|w": 3.5, "d|w": null },
{ "a|w": null, "b|w": 6.5, "c|w": null, "d|w": null },
{ "a|w": null, "b|w": 2.5, "c|w": null, "d|w": null },
{ "a|w": null, "b|w": null, "c|w": 7.5, "d|w": null },
{ "a|w": 1.5, "b|w": null, "c|w": null, "d|w": null },
{ "a|w": null, "b|w": null, "c|w": null, "d|w": 8.5 },
];
const result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test("split by ['y'], col desc sort", async function () {
const table = await perspective.table(data);
const view = await table.view({
columns: ["w"],
split_by: ["y"],
sort: [["x", "col desc"]],
});
const paths = await view.column_paths();
expect(paths).toEqual(["d|w", "c|w", "b|w", "a|w"]);
const answer = {
"d|w": [null, null, null, 4.5, null, null, null, 8.5],
"c|w": [null, null, 3.5, null, null, null, 7.5, null],
"b|w": [null, 2.5, null, null, null, 6.5, null, null],
"a|w": [1.5, null, null, null, 5.5, null, null, null],
};
const result = await view.to_columns();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test("split by and hidden sort ['y']", async function () {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: ["a", "a", "a", "b"],
});
const view = await table.view({
columns: ["x"],
split_by: ["y"],
sort: [["y", "desc"]],
});
const paths = await view.column_paths();
// regular non-col sort should not change order of column paths
expect(paths).toEqual(["a|x", "b|x"]);
const result = await view.to_columns();
expect(result).toEqual({
"a|x": [null, 1, 2, 3],
"b|x": [4, null, null, null],
});
view.delete();
table.delete();
});
test("split by and hidden col sort ['y']", async function () {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: ["a", "a", "a", "b"],
});
const view = await table.view({
columns: ["x"],
split_by: ["y"],
sort: [["y", "col desc"]],
});
const paths = await view.column_paths();
expect(paths).toEqual(["b|x", "a|x"]);
const result = await view.to_columns();
expect(result).toEqual({
"b|x": [null, null, null, 4],
"a|x": [1, 2, 3, null],
});
view.delete();
table.delete();
});
test("split by and hidden sort ['y'] with aggregates specified", async function () {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: ["a", "a", "a", "b"],
});
// Aggregate for hidden sort should be ignored in column-only,
// so just make sure we stick to that.
const view = await table.view({
columns: ["x"],
split_by: ["y"],
sort: [["y", "desc"]],
aggregates: {
y: "count",
},
});
const paths = await view.column_paths();
// regular non-col sort should not change order of column paths
expect(paths).toEqual(["a|x", "b|x"]);
const result = await view.to_columns();
expect(result).toEqual({
"a|x": [null, 1, 2, 3],
"b|x": [4, null, null, null],
});
view.delete();
table.delete();
});
test("split by and hidden col sort ['y'] with aggregates specified", async function () {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: ["a", "a", "a", "b"],
});
// Aggregate for hidden sort should be ignored in column-only,
// so just make sure we stick to that.
const view = await table.view({
columns: ["x"],
split_by: ["y"],
sort: [["y", "col desc"]],
aggregates: {
y: "count",
},
});
const paths = await view.column_paths();
expect(paths).toEqual(["b|x", "a|x"]);
const result = await view.to_columns();
expect(result).toEqual({
"b|x": [null, null, null, 4],
"a|x": [1, 2, 3, null],
});
view.delete();
table.delete();
});
test("split by ['y'] with overridden aggregates", async function () {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: ["a", "a", "a", "b"],
});
const view = await table.view({
columns: ["x"],
split_by: ["y"],
aggregates: { y: "count" },
sort: [["y", "col desc"]],
});
const paths = await view.column_paths();
// Col sort will override aggregate
expect(paths).toEqual(["b|x", "a|x"]);
let result = await view.to_columns();
expect(result).toEqual({
"b|x": [null, null, null, 4],
"a|x": [1, 2, 3, null],
});
view.delete();
table.delete();
});
test("split by ['y'] with extra aggregates", async function () {
var table = await perspective.table(data);
var view = await table.view({
columns: ["w"],
split_by: ["y"],
aggregates: { w: "sum", z: "last" },
sort: [["x", "desc"]],
});
var answer = [
{ "a|w": null, "b|w": null, "c|w": null, "d|w": 4.5 },
{ "a|w": 5.5, "b|w": null, "c|w": null, "d|w": null },
{ "a|w": null, "b|w": null, "c|w": 3.5, "d|w": null },
{ "a|w": null, "b|w": 6.5, "c|w": null, "d|w": null },
{ "a|w": null, "b|w": 2.5, "c|w": null, "d|w": null },
{ "a|w": null, "b|w": null, "c|w": 7.5, "d|w": null },
{ "a|w": 1.5, "b|w": null, "c|w": null, "d|w": null },
{ "a|w": null, "b|w": null, "c|w": null, "d|w": 8.5 },
];
let result = await view.to_json();
expect(result).toEqual(answer);
view.delete();
table.delete();
});
test("group by ['x'], split by ['y'], both hidden and asc sorted", async function () {
const table = await perspective.table({
x: ["a", "a", "b", "c"],
y: ["x", "x", "y", "x"],
z: [1, 2, 3, 4],
});
const view = await table.view({
columns: ["z"],
group_by: ["x"],
split_by: ["y"],
sort: [
["x", "asc"],
["y", "col asc"],
],
});
const paths = await view.column_paths();
expect(paths).toEqual(["x|z", "y|z"]);
const expected = {
__ROW_PATH__: [[], ["a"], ["b"], ["c"]],
"x|z": [7, 3, null, 4],
"y|z": [3, null, 3, null],
};
const result = await view.to_columns();
expect(result).toEqual(expected);
view.delete();
table.delete();
});
test("group by ['x'], split by ['y'], both hidden and desc sorted", async function () {
const table = await perspective.table({
x: ["a", "a", "b", "c"],
y: ["x", "x", "y", "x"],
z: [1, 2, 3, 4],
});
const view = await table.view({
columns: ["z"],
group_by: ["x"],
split_by: ["y"],
sort: [
["x", "desc"],
["y", "col desc"],
],
});
const paths = await view.column_paths();
expect(paths).toEqual(["y|z", "x|z"]);
const expected = {
__ROW_PATH__: [[], ["c"], ["b"], ["a"]],
"y|z": [3, null, 3, null],
"x|z": [7, 4, null, 3],
};
const result = await view.to_columns();
expect(result).toEqual(expected);
view.delete();
table.delete();
});
test("group by ['x'], split by ['y'], hidden sorted on group by", async function () {
const table = await perspective.table({
x: ["a", "a", "b", "c"],
y: ["x", "x", "y", "x"],
z: [1, 2, 3, 4],
});
const view = await table.view({
columns: ["z"],
group_by: ["y"],
split_by: ["x"],
sort: [["x", "desc"]],
});
const paths = await view.column_paths();
expect(paths).toEqual(["a|z", "b|z", "c|z"]);
const expected = {
__ROW_PATH__: [[], ["x"], ["y"]],
"a|z": [3, 3, null],
"b|z": [3, null, 3],
"c|z": [4, 4, null],
};
const result = await view.to_columns();
expect(result).toEqual(expected);
view.delete();
table.delete();
});
test("split by ['y'] has correct # of columns", async function () {
var table = await perspective.table(data);
var view = await table.view({
columns: ["w"],
split_by: ["y"],
sort: [["x", "desc"]],
});
let to_cols = await view.to_columns();
let result = await view.num_columns();
expect(result).toEqual(4);
view.delete();
table.delete();
});
});
});
})(perspective);
@@ -0,0 +1,22 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
test.describe("perspective.js module", function () {
test("does not access the WASM module until it is ready", async () => {
const perspective = await import("@perspective-dev/client");
const tbl = await perspective.table([{ x: 1 }]);
const size = await tbl.size();
expect(size).toEqual(1);
});
});
+127
View File
@@ -0,0 +1,127 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import * as fs from "node:fs";
import * as path from "node:path";
import * as url from "node:url";
// globalThis.require = createRequire(import.meta.url);
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Returns an `ArrayBuffer` containing the contents of a `.arrow` file
* located at `arrow_path`.
*
* Because `fs.readFileSync` shares its underlying buffer
* between calls to `readFileSync`, we need to get a slice
* of the `ArrayBuffer` specifically at its byte offset.
*
* See https://github.com/nodejs/node/issues/11132 for more details.
*
* @param arrow_path {String} a path to an arrow file.
* @returns {ArrayBuffer} an ArrayBuffer containing the arrow-serialized data.
*/
function load_arrow(arrow_path) {
const data = fs.readFileSync(arrow_path);
return data.buffer.slice(
data.byteOffset,
data.byteOffset + data.byteLength,
);
}
export const chunked_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "chunked.arrow"),
);
export const test_null_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "test_null.arrow"),
);
export const test_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "test.arrow"),
);
export const partial_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "partial.arrow"),
);
export const partial_missing_rows_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "partial_missing_rows.arrow"),
);
export const int_float_str_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "int_float_str.arrow"),
);
export const int_float_str_update_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "int_float_str_update.arrow"),
);
export const int_float_str_file_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "int_float_str_file.arrow"),
);
export const float32_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "float32.arrow"),
);
export const date32_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "date32.arrow"),
);
export const date64_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "date64.arrow"),
);
export const dict_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "dict.arrow"),
);
export const dict_update_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "dict_update.arrow"),
);
export const numbers_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "number_types.arrow"),
);
export const all_types_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "all_types_small.arrow"),
);
// uint8-64 x2, int8-64 x2, date, datetime, bool, string
export const all_types_multi_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "all_types_small_multi.arrow"),
);
export const lists_arrow = load_arrow(
path.join(__dirname, "..", "arrow", "lists.arrow"),
);
// module.exports = {
// chunked_arrow,
// test_null_arrow,
// test_arrow,
// partial_arrow,
// partial_missing_rows_arrow,
// int_float_str_arrow,
// int_float_str_update_arrow,
// int_float_str_file_arrow,
// date32_arrow,
// date64_arrow,
// dict_arrow,
// dict_update_arrow,
// numbers_arrow,
// all_types_arrow,
// all_types_multi_arrow,
// };
@@ -0,0 +1,63 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
export const STD_DATE = new Date();
export const int_float_string_data = [
{ int: 1, float: 2.25, string: "a", datetime: STD_DATE },
{ int: 2, float: 3.5, string: "b", datetime: STD_DATE },
{ int: 3, float: 4.75, string: "c", datetime: STD_DATE },
{ int: 4, float: 5.25, string: "d", datetime: STD_DATE },
];
export const pivoted_output = [
{
__ROW_PATH__: [],
int: 10,
float: 15.75,
string: 4,
datetime: 4,
__INDEX__: [3, 2, 1, 0],
},
{
__ROW_PATH__: [1],
int: 1,
float: 2.25,
string: 1,
datetime: 1,
__INDEX__: [0],
},
{
__ROW_PATH__: [2],
int: 2,
float: 3.5,
string: 1,
datetime: 1,
__INDEX__: [1],
},
{
__ROW_PATH__: [3],
int: 3,
float: 4.75,
string: 1,
datetime: 1,
__INDEX__: [2],
},
{
__ROW_PATH__: [4],
int: 4,
float: 5.25,
string: 1,
datetime: 1,
__INDEX__: [3],
},
];
@@ -0,0 +1,344 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
import { int_float_string_data } from "./_shared";
((perspective) => {
test.describe("data slice", function () {
test("should filter out invalid start rows", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view();
let json = await view.to_json({
start_row: 5,
});
expect(json).toEqual([]);
view.delete();
table.delete();
});
test("should filter out invalid start columns", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view();
let json = await view.to_json({
start_col: 5,
});
expect(json).toEqual([]);
view.delete();
table.delete();
});
test("should filter out invalid start rows & columns", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view();
let json = await view.to_json({
start_row: 5,
start_col: 5,
});
expect(json).toEqual([]);
view.delete();
table.delete();
});
test("should filter out invalid start rows based on view", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
filter: [["float", ">", 3.5]],
});
// valid on view() but not this filtered view
let json = await view.to_json({
start_row: 3,
});
expect(json).toEqual([]);
view.delete();
table.delete();
});
test("should filter out invalid start columns based on view", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
columns: ["float", "int"],
});
let json = await view.to_json({
start_col: 2,
});
expect(json).toEqual([]);
view.delete();
table.delete();
});
test("should filter out invalid start rows & columns based on view", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
columns: ["float", "int"],
filter: [["float", ">", 3.5]],
});
let json = await view.to_json({
start_row: 5,
start_col: 5,
});
expect(json).toEqual([]);
view.delete();
table.delete();
});
test("should respect start/end rows", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view();
let json = await view.to_json({
start_row: 2,
end_row: 3,
});
let comparator = int_float_string_data[2];
comparator.datetime = +comparator.datetime;
expect(json[0]).toEqual(comparator);
view.delete();
table.delete();
});
test("should respect end rows when larger than data size", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view();
let json = await view.to_json({
start_row: 2,
end_row: 6,
});
expect(json).toEqual(
int_float_string_data.slice(2).map((x) => {
x.datetime = +x.datetime;
return x;
}),
);
view.delete();
table.delete();
});
test("should respect start/end columns", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view();
let json = await view.to_columns({
start_col: 2,
end_col: 3,
});
let comparator = {
string: int_float_string_data.map((d) => d.string),
};
expect(json).toEqual(comparator);
view.delete();
table.delete();
});
test("should floor float start rows", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view();
let json = await view.to_json({
start_row: 1.5,
});
expect(json).toEqual(
int_float_string_data.slice(1).map((x) => {
x.datetime = +x.datetime;
return x;
}),
);
view.delete();
table.delete();
});
test("should ceil float end rows", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view();
let json = await view.to_json({
end_row: 1.5,
});
expect(json).toEqual(
// deep copy
JSON.parse(JSON.stringify(int_float_string_data))
.slice(0, 2)
.map((x) => {
x.datetime = +new Date(x.datetime);
return x;
}),
);
view.delete();
table.delete();
});
test("should floor/ceil float start/end rows", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view();
let json = await view.to_json({
start_row: 2.9,
end_row: 2.4,
});
let comparator = int_float_string_data[2];
comparator.datetime = +comparator.datetime;
expect(json[0]).toEqual(comparator);
view.delete();
table.delete();
});
test("should ceil float end rows when larger than data size", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view();
let json = await view.to_json({
start_row: 2,
end_row: 5.5,
});
expect(json).toEqual(
int_float_string_data.slice(2).map((x) => {
x.datetime = +x.datetime;
return x;
}),
);
view.delete();
table.delete();
});
test("should floor/ceil float start/end columns", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view();
let json = await view.to_columns({
start_col: 2.6,
end_col: 2.4,
});
let comparator = {
string: int_float_string_data.map((d) => d.string),
};
expect(json).toEqual(comparator);
view.delete();
table.delete();
});
test("one-sided views should have row paths", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
group_by: ["int"],
});
let json = await view.to_json();
for (let d of json) {
expect(d.__ROW_PATH__).toBeDefined();
}
view.delete();
table.delete();
});
test("one-sided views with start_col > 0 should have row paths", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
group_by: ["int"],
});
let json = await view.to_json({ start_col: 1 });
for (let d of json) {
expect(d.__ROW_PATH__).toBeDefined();
}
view.delete();
table.delete();
});
test("one-sided column-only views should not have row paths", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
split_by: ["int"],
});
let json = await view.to_json();
for (let d of json) {
expect(d.__ROW_PATH__).toBeUndefined();
}
view.delete();
table.delete();
});
test("column-only views should not have header rows", async function () {
let table = await perspective.table([
{ x: 1, y: "a" },
{ x: 2, y: "b" },
]);
let view = await table.view({
split_by: ["x"],
});
let json = await view.to_json();
expect(json).toEqual([
{ "1|x": 1, "1|y": "a", "2|x": null, "2|y": null },
{ "1|x": null, "1|y": null, "2|x": 2, "2|y": "b" },
]);
view.delete();
table.delete();
});
test("column-only views should return correct windows of data", async function () {
let table = await perspective.table([
{ x: 1, y: "a" },
{ x: 2, y: "b" },
]);
let view = await table.view({
split_by: ["x"],
});
let json = await view.to_json({
start_row: 1,
});
expect(json).toEqual([
{ "1|x": null, "1|y": null, "2|x": 2, "2|y": "b" },
]);
view.delete();
table.delete();
});
test("two-sided views should have row paths", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
group_by: ["int"],
split_by: ["string"],
});
let json = await view.to_json();
for (let d of json) {
expect(d.__ROW_PATH__).toBeDefined();
}
view.delete();
table.delete();
});
test("two-sided views with start_col > 0 should have row paths", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
group_by: ["int"],
split_by: ["string"],
});
let json = await view.to_json({ start_col: 1 });
for (let d of json) {
expect(d.__ROW_PATH__).toBeDefined();
}
view.delete();
table.delete();
});
test("two-sided sorted views with start_col > 0 should have row paths", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
group_by: ["int"],
split_by: ["string"],
sort: [["string", "desc"]],
});
let json = await view.to_json({ start_col: 1 });
for (let d of json) {
expect(d.__ROW_PATH__).toBeDefined();
}
view.delete();
table.delete();
});
});
})(perspective);
@@ -0,0 +1,135 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
import { STD_DATE, int_float_string_data } from "./_shared";
((perspective) => {
test.describe("leaves_only flag", function () {
test("only emits leaves when leaves_only is set", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
group_by: ["int"],
group_rollup_mode: "flat",
});
let json = await view.to_json();
expect(json).toEqual([
{
__ROW_PATH__: [1],
datetime: 1,
float: 2.25,
int: 1,
string: 1,
},
{
__ROW_PATH__: [2],
datetime: 1,
float: 3.5,
int: 2,
string: 1,
},
{
__ROW_PATH__: [3],
datetime: 1,
float: 4.75,
int: 3,
string: 1,
},
{
__ROW_PATH__: [4],
datetime: 1,
float: 5.25,
int: 4,
string: 1,
},
]);
view.delete();
table.delete();
});
test("num_rows returns correct leaf count", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
group_by: ["int"],
group_rollup_mode: "flat",
});
let num_rows = await view.num_rows();
expect(num_rows).toEqual(4);
view.delete();
table.delete();
});
test("viewport pagination returns exact page sizes", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
group_by: ["int"],
group_rollup_mode: "flat",
});
let json = await view.to_json({
start_row: 0,
end_row: 2,
});
expect(json.length).toEqual(2);
expect(json[0].__ROW_PATH__).toEqual([1]);
expect(json[1].__ROW_PATH__).toEqual([2]);
view.delete();
table.delete();
});
test("to_columns works with leaves_only", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
group_by: ["int"],
group_rollup_mode: "flat",
});
let cols = await view.to_columns();
expect(cols["__ROW_PATH__"]).toEqual([[1], [2], [3], [4]]);
expect(cols["int"]).toEqual([1, 2, 3, 4]);
view.delete();
table.delete();
});
test("leaves_only with multi-level group_by", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
group_by: ["string", "int"],
group_rollup_mode: "flat",
});
let num_rows = await view.num_rows();
expect(num_rows).toEqual(4);
let json = await view.to_json();
for (let row of json) {
expect(row.__ROW_PATH__.length).toEqual(2);
}
view.delete();
table.delete();
});
test("leaves_only updates after table.update()", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
group_by: ["int"],
group_rollup_mode: "flat",
});
let num_rows = await view.num_rows();
expect(num_rows).toEqual(4);
table.update([
{ int: 5, float: 6.0, string: "e", datetime: STD_DATE },
]);
let num_rows2 = await view.num_rows();
expect(num_rows2).toEqual(5);
view.delete();
table.delete();
});
});
})(perspective);
File diff suppressed because it is too large Load Diff
@@ -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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
test.describe("to_columns_string", () => {
test("should return a string", async () => {
const table = await perspective.table([{ x: 1 }]);
const view = await table.view();
const result = await view.to_columns_string();
expect(result).toEqual('{"x":[1]}');
view.delete();
table.delete();
});
});
@@ -0,0 +1,108 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
test.describe("to_csv", () => {
test("0-sided view returns CSV string", async () => {
const table = await perspective.table([
{ x: 1, y: "a" },
{ x: 2, y: "b" },
]);
const view = await table.view();
const csv = await view.to_csv();
expect(csv).toEqual('"x","y"\n1,"a"\n2,"b"\n');
view.delete();
table.delete();
});
test("respects start_row/end_row viewport", async () => {
const table = await perspective.table([
{ x: 1, y: "a" },
{ x: 2, y: "b" },
{ x: 3, y: "c" },
]);
const view = await table.view();
const csv = await view.to_csv({ start_row: 1, end_row: 2 });
expect(csv).toEqual('"x","y"\n2,"b"\n');
view.delete();
table.delete();
});
test("1-sided view emits row paths", async () => {
const table = await perspective.table([
{ x: 1, y: "a" },
{ x: 2, y: "a" },
{ x: 3, y: "b" },
]);
const view = await table.view({ group_by: ["y"] });
const csv = await view.to_csv();
expect(csv).toEqual(
'"y (Group by 1)","x","y"\n,6,3\n"a",3,2\n"b",3,1\n',
);
view.delete();
table.delete();
});
test("escapes quotes and commas in string values", async () => {
const table = await perspective.table([
{ x: 'he said "hi"', y: "a,b" },
]);
const view = await table.view();
const csv = await view.to_csv();
expect(csv).toEqual('"x","y"\n"he said ""hi""","a,b"\n');
view.delete();
table.delete();
});
// https://github.com/perspective-dev/perspective/issues/3152
test("escapes utf8 in string values", async () => {
const table = await perspective.table([
{
: "2023/01/01",
: "第四分局",
: "南屯區",
split: "五權西路與環中路口",
A1: 0.0,
A2: 3.0,
A3: 130.0,
總件數: 18.0,
死亡人數: 0.0,
受傷人數: null,
: "未注意車前狀態",
},
{
: "2023/01/01",
: "第六分局",
: "西屯區",
split: "中清聯絡道與環中路口",
A1: 0.0,
A2: 2.0,
A3: 100.0,
總件數: 15.0,
死亡人數: 0.0,
受傷人數: null,
: "未注意車前狀態",
},
]);
const view = await table.view();
const csv = await view.to_csv();
expect(csv).toEqual(
`"年月","轄區分局","路口名稱","路口名稱split","A1","A2","A3","總件數","死亡人數","主要肇因","受傷人數"\n2023-01-01,"第四分局","南屯區","五權西路與環中路口",0,3,130,18,0,"未注意車前狀態",\n2023-01-01,"第六分局","西屯區","中清聯絡道與環中路口",0,2,100,15,0,"未注意車前狀態",\n`,
);
view.delete();
table.delete();
});
});
@@ -0,0 +1,62 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
import * as fs from "node:fs";
const superstore_uncompressed = fs.readFileSync(
require.resolve("superstore-arrow/superstore.arrow"),
).buffer;
const superstore_lz4 = fs.readFileSync(
require.resolve("superstore-arrow/superstore.lz4.arrow"),
).buffer;
test.describe("to_format regressions", function () {
test("start_col is respected", async () => {
let table = await perspective.table(superstore_uncompressed.slice());
let view = await table.view({
group_by: ["State"],
split_by: ["Sub-Category"],
// sort: [["Customer Name", "desc"]],
group_rollup_mode: "rollup",
columns: ["Sales", "Quantity", "Discount", "Profit"],
});
const result1 = await view.to_columns({ start_col: 4, end_row: 1 });
const result2 = await view.to_columns({ start_col: 5, end_row: 1 });
expect(result1).not.toEqual(result2);
});
test("start_col is respected with sort", async () => {
let table = await perspective.table(superstore_uncompressed.slice());
let view = await table.view({
group_by: ["State"],
split_by: ["Sub-Category"],
sort: [["Customer Name", "desc"]],
group_rollup_mode: "rollup",
columns: ["Sales", "Quantity", "Discount", "Profit"],
});
const result1 = await view.to_columns({ start_col: 4, end_row: 1 });
const result2 = await view.to_columns({ start_col: 5, end_row: 1 });
expect(result1).not.toEqual(result2);
});
});
@@ -0,0 +1,292 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
const data = {
w: [
1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, -1.5, -3.5, -1.5, -4.5, -9.5,
-5.5, -8.5, -7.5,
],
x: [1, 2, 3, 4, 4, 3, 2, 1, 3, 4, 2, 1, 4, 3, 1, 2],
y: [
"a",
"b",
"c",
"d",
"a",
"b",
"c",
"d",
"a",
"b",
"c",
"d",
"a",
"b",
"c",
"d",
],
z: [
true,
false,
true,
false,
true,
false,
true,
false,
true,
true,
true,
true,
false,
false,
false,
false,
],
};
test.describe("to_format viewport", function () {
test.describe("0 sided", function () {
test("start_col 0 is the first col", async function () {
var table = await perspective.table(data);
var view = await table.view({});
const cols = await view.to_columns({ start_col: 0, end_col: 1 });
expect(cols).toEqual({ w: data.w });
view.delete();
table.delete();
});
test("start_col 2 is the second col", async function () {
var table = await perspective.table(data);
var view = await table.view({});
const cols = await view.to_columns({ start_col: 1, end_col: 2 });
expect(cols).toEqual({ x: data.x });
view.delete();
table.delete();
});
test("start_col 0, end_col 2 is the first two columns", async function () {
var table = await perspective.table(data);
var view = await table.view({});
const cols = await view.to_columns({ start_col: 0, end_col: 2 });
expect(cols).toEqual({ w: data.w, x: data.x });
view.delete();
table.delete();
});
});
test.describe("1 sided", function () {
test("start_col 0 is the first col", async function () {
var table = await perspective.table(data);
var view = await table.view({
group_by: ["y"],
});
const cols = await view.to_columns({ start_col: 0, end_col: 1 });
expect(cols).toEqual({
__ROW_PATH__: [[], ["a"], ["b"], ["c"], ["d"]],
w: [-2, -4, 0, 1, 1],
});
view.delete();
table.delete();
});
test("start_col 2 is the second col", async function () {
var table = await perspective.table(data);
var view = await table.view({
group_by: ["y"],
});
const cols = await view.to_columns({ start_col: 1, end_col: 2 });
expect(cols).toEqual({
__ROW_PATH__: [[], ["a"], ["b"], ["c"], ["d"]],
x: [40, 12, 12, 8, 8],
});
view.delete();
table.delete();
});
test("start_col 0, end_col 2 is the first two columns", async function () {
var table = await perspective.table(data);
var view = await table.view({
group_by: ["y"],
});
const cols = await view.to_columns({ start_col: 0, end_col: 2 });
expect(cols).toEqual({
__ROW_PATH__: [[], ["a"], ["b"], ["c"], ["d"]],
w: [-2, -4, 0, 1, 1],
x: [40, 12, 12, 8, 8],
});
view.delete();
table.delete();
});
});
test.describe("2 sided", function () {
test("start_col 0 is the first col", async function () {
var table = await perspective.table(data);
var view = await table.view({
group_by: ["y"],
split_by: ["z"],
});
const cols = await view.to_columns({ start_col: 0, end_col: 1 });
expect(cols).toEqual({
__ROW_PATH__: [[], ["a"], ["b"], ["c"], ["d"]],
"false|w": [-9, -9.5, 3.5, -8.5, 5.5],
});
view.delete();
table.delete();
});
test("start_col 2 is the second col", async function () {
var table = await perspective.table(data);
var view = await table.view({
group_by: ["y"],
split_by: ["z"],
});
const cols = await view.to_columns({ start_col: 1, end_col: 2 });
expect(cols).toEqual({
__ROW_PATH__: [[], ["a"], ["b"], ["c"], ["d"]],
"false|x": [20, 4, 8, 1, 7],
});
view.delete();
table.delete();
});
test("start_col 0, end_col 2 is the first two columns", async function () {
var table = await perspective.table(data);
var view = await table.view({
group_by: ["y"],
split_by: ["z"],
});
const cols = await view.to_columns({ start_col: 0, end_col: 2 });
expect(cols).toEqual({
__ROW_PATH__: [[], ["a"], ["b"], ["c"], ["d"]],
"false|w": [-9, -9.5, 3.5, -8.5, 5.5],
"false|x": [20, 4, 8, 1, 7],
});
view.delete();
table.delete();
});
});
test.describe("column only", function () {
test("start_col 0 is the first col", async function () {
var table = await perspective.table(data);
var view = await table.view({
split_by: ["z"],
});
const cols = await view.to_columns({ start_col: 0, end_col: 1 });
expect(cols).toEqual({
"false|w": [
null,
2.5,
null,
4.5,
null,
6.5,
null,
8.5,
null,
null,
null,
null,
-9.5,
-5.5,
-8.5,
-7.5,
],
});
view.delete();
table.delete();
});
test("start_col 2 is the second col", async function () {
var table = await perspective.table(data);
var view = await table.view({
split_by: ["z"],
});
const cols = await view.to_columns({ start_col: 1, end_col: 2 });
expect(cols).toEqual({
"false|x": [
null,
2,
null,
4,
null,
3,
null,
1,
null,
null,
null,
null,
4,
3,
1,
2,
],
});
view.delete();
table.delete();
});
test("start_col 0, end_col 2 is the first two columns", async function () {
var table = await perspective.table(data);
var view = await table.view({
split_by: ["z"],
});
const cols = await view.to_columns({ start_col: 0, end_col: 2 });
expect(cols).toEqual({
"false|w": [
null,
2.5,
null,
4.5,
null,
6.5,
null,
8.5,
null,
null,
null,
null,
-9.5,
-5.5,
-8.5,
-7.5,
],
"false|x": [
null,
2,
null,
4,
null,
3,
null,
1,
null,
null,
null,
null,
4,
3,
1,
2,
],
});
view.delete();
table.delete();
});
});
});
@@ -0,0 +1,98 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
import { int_float_string_data } from "./_shared";
((perspective) => {
test.describe("to_json", function () {
test("should emit same number of column names as number of pivots", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
group_by: ["int"],
split_by: ["float", "string"],
sort: [["int", "asc"]],
});
let json = await view.to_json();
// Get the first emitted column name that is not __ROW_PATH__
let name = Object.keys(json[0])[1];
// make sure that number of separators = num of split by
expect((name.match(/\|/g) || []).length).toEqual(2);
view.delete();
table.delete();
});
test("should return dates in native form by default", async function () {
let table = await perspective.table([
{ datetime: new Date("2016-06-13") },
{ datetime: new Date("2016-06-14") },
]);
let view = await table.view();
let json = await view.to_json();
expect(json).toEqual([
{ datetime: 1465776000000 },
{ datetime: 1465862400000 },
]);
view.delete();
table.delete();
});
test.skip("OG - should return dates in readable format on passing string in options", async function () {
let table = await perspective.table([
{ datetime: new Date("2016-06-13") },
{ datetime: new Date("2016-06-14") },
]);
let view = await table.view();
let json = await view.to_json({ formatted: true });
expect(json).toEqual([
{ datetime: "2016-06-13" },
{ datetime: "2016-06-14" },
]);
view.delete();
table.delete();
});
test("should return dates in readable format on passing string in options", async function () {
let table = await perspective.table({
date: "date",
});
await table.update([
{ date: new Date("2016-06-13") },
{ date: new Date("2016-06-14") },
]);
let view = await table.view();
let json = await view.to_json({ formatted: true });
expect(json).toEqual([
{ date: "2016-06-13" },
{ date: "2016-06-14" },
]);
view.delete();
table.delete();
});
test("should return datetimes in readable format on passing string in options", async function () {
let table = await perspective.table([
{ datetime: new Date(2016, 0, 1, 0, 30) },
{ datetime: new Date(2016, 5, 15, 19, 20) },
]);
let view = await table.view();
let json = await view.to_json({ formatted: true });
expect(json).toEqual([
{ datetime: "2016-01-01 00:30:00.000" },
{ datetime: "2016-06-15 19:20:00.000" },
]);
view.delete();
table.delete();
});
});
})(perspective);
@@ -0,0 +1,63 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
import { STD_DATE, int_float_string_data } from "./_shared";
((perspective) => {
test.describe("to_ndjson", function () {
test("should work with context 0", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({});
let json = await view.to_ndjson();
expect(json)
.toEqual(`{"int":1,"float":2.25,"string":"a","datetime":${+STD_DATE}}
{"int":2,"float":3.5,"string":"b","datetime":${+STD_DATE}}
{"int":3,"float":4.75,"string":"c","datetime":${+STD_DATE}}
{"int":4,"float":5.25,"string":"d","datetime":${+STD_DATE}}`);
view.delete();
table.delete();
});
test("should work with context 1", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({ group_by: ["string"] });
let json = await view.to_ndjson();
expect(json)
.toEqual(`{"__ROW_PATH__":[],"int":10,"float":15.75,"string":4,"datetime":4}
{"__ROW_PATH__":["a"],"int":1,"float":2.25,"string":1,"datetime":1}
{"__ROW_PATH__":["b"],"int":2,"float":3.5,"string":1,"datetime":1}
{"__ROW_PATH__":["c"],"int":3,"float":4.75,"string":1,"datetime":1}
{"__ROW_PATH__":["d"],"int":4,"float":5.25,"string":1,"datetime":1}`);
view.delete();
table.delete();
});
test("should work with context 2", async function () {
let table = await perspective.table(int_float_string_data);
let view = await table.view({
group_by: ["string"],
split_by: ["int"],
});
let json = await view.to_ndjson();
expect(json)
.toEqual(`{"__ROW_PATH__":[],"1|int":1,"1|float":2.25,"1|string":1,"1|datetime":1,"2|int":2,"2|float":3.5,"2|string":1,"2|datetime":1,"3|int":3,"3|float":4.75,"3|string":1,"3|datetime":1,"4|int":4,"4|float":5.25,"4|string":1,"4|datetime":1}
{"__ROW_PATH__":["a"],"1|int":1,"1|float":2.25,"1|string":1,"1|datetime":1,"2|int":null,"2|float":null,"2|string":null,"2|datetime":null,"3|int":null,"3|float":null,"3|string":null,"3|datetime":null,"4|int":null,"4|float":null,"4|string":null,"4|datetime":null}
{"__ROW_PATH__":["b"],"1|int":null,"1|float":null,"1|string":null,"1|datetime":null,"2|int":2,"2|float":3.5,"2|string":1,"2|datetime":1,"3|int":null,"3|float":null,"3|string":null,"3|datetime":null,"4|int":null,"4|float":null,"4|string":null,"4|datetime":null}
{"__ROW_PATH__":["c"],"1|int":null,"1|float":null,"1|string":null,"1|datetime":null,"2|int":null,"2|float":null,"2|string":null,"2|datetime":null,"3|int":3,"3|float":4.75,"3|string":1,"3|datetime":1,"4|int":null,"4|float":null,"4|string":null,"4|datetime":null}
{"__ROW_PATH__":["d"],"1|int":null,"1|float":null,"1|string":null,"1|datetime":null,"2|int":null,"2|float":null,"2|string":null,"2|datetime":null,"3|int":null,"3|float":null,"3|string":null,"3|datetime":null,"4|int":4,"4|float":5.25,"4|string":1,"4|datetime":1}`);
view.delete();
table.delete();
});
});
})(perspective);

Some files were not shown because too many files have changed in this diff Show More