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
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);
@@ -0,0 +1,825 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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("with_typed_arrays()", () => {
test("awaits promise returned by async callback before releasing the batch", async () => {
// The callback returns a Promise that only resolves after a
// microtask tick; the zero-copy views must remain valid for the
// full awaited duration. We copy *after* the tick to prove the
// backing WASM memory is still readable.
const table = await perspective.table({
x: ["a", "b", "a", "c"],
});
const view = await table.view();
let resolved: string[] | null = null;
await view.with_typed_arrays(
{},
async (
_n: string[],
vals: ArrayLike<number>[],
_valids: any[],
dicts: (string[] | null)[],
) => {
const keys = vals[0] as Int32Array;
const dict = dicts[0]!;
// Yield twice to prove the Rust side is actually awaiting
// the returned promise rather than firing a sync call and
// dropping the batch immediately.
await new Promise((r) => setTimeout(r, 0));
await new Promise((r) => setTimeout(r, 0));
resolved = Array.from(keys).map((k) => dict[k]);
},
);
expect(resolved).toEqual(["a", "b", "a", "c"]);
await view.delete();
await table.delete();
});
test("rejected promise from async callback surfaces as a rejection", async () => {
const table = await perspective.table({ x: [1, 2, 3] });
const view = await table.view();
let caught: unknown = null;
try {
await view.with_typed_arrays({}, async () => {
throw new Error("callback boom");
});
} catch (e) {
caught = e;
}
expect(String(caught)).toContain("callback boom");
await view.delete();
await table.delete();
});
test("returns all columns with names, values, validities, dictionaries", async () => {
const table = await perspective.table({
a: [1, 2, 3],
b: [10.0, 20.0, 30.0],
});
const view = await table.view();
let names: string[] = [];
let valuesMap: Record<string, ArrayLike<number>> = {};
await view.with_typed_arrays(
{},
(
n: string[],
vals: ArrayLike<number>[],
_valids: (Uint8Array | null)[],
_dicts: (string[] | null)[],
) => {
names = Array.from(n);
for (let i = 0; i < names.length; i++) {
valuesMap[names[i]] = vals[i];
}
},
);
expect(names).toContain("a");
expect(names).toContain("b");
expect(Array.from(valuesMap["b"])).toEqual([10.0, 20.0, 30.0]);
await view.delete();
await table.delete();
});
test("returns Float64Array for float column by default", async () => {
const table = await perspective.table({ x: [1.5, 2.5, 3.5] });
const view = await table.view();
let result: Float64Array | null = null;
await view.with_typed_arrays(
{},
(_n: string[], vals: ArrayLike<number>[]) => {
result = new Float64Array(vals[0] as Float64Array);
},
);
expect(Array.from(result!)).toEqual([1.5, 2.5, 3.5]);
await view.delete();
await table.delete();
});
test("returns Float32Array when float32 option is set", async () => {
const table = await perspective.table({ x: [1.5, 2.5, 3.5] });
const view = await table.view();
let isFloat32 = false;
await view.with_typed_arrays(
{ float32: true },
(_n: string[], vals: any[]) => {
isFloat32 = vals[0] instanceof Float32Array;
},
);
expect(isFloat32).toBe(true);
await view.delete();
await table.delete();
});
test("returns Int32Array for integer column", async () => {
const table = await perspective.table({ x: "integer" });
table.update({ x: [10, 20, 30] });
const view = await table.view();
let result: Int32Array | null = null;
await view.with_typed_arrays(
{},
(_n: string[], vals: ArrayLike<number>[]) => {
result = new Int32Array(vals[0] as Int32Array);
},
);
expect(Array.from(result!)).toEqual([10, 20, 30]);
await view.delete();
await table.delete();
});
test("passes validity bitmap for columns with nulls", async () => {
const table = await perspective.table({ x: [1.5, null, 3.5, null] });
const view = await table.view();
let validity: Uint8Array | null = null;
await view.with_typed_arrays(
{},
(_n: string[], _vals: any[], valids: (Uint8Array | null)[]) => {
validity = valids[0] ? new Uint8Array(valids[0]) : null;
},
);
// Bits 0 and 2 set => 0b0101 = 5
expect(validity![0]).toEqual(0b0101);
await view.delete();
await table.delete();
});
test("validity is null when no nulls present", async () => {
const table = await perspective.table({ x: [1.0, 2.0, 3.0] });
const view = await table.view();
let validityWasNull = false;
await view.with_typed_arrays(
{},
(_n: string[], _vals: any[], valids: (Uint8Array | null)[]) => {
validityWasNull = valids[0] === null;
},
);
expect(validityWasNull).toBe(true);
await view.delete();
await table.delete();
});
test("returns dictionary keys and values for string column", async () => {
const table = await perspective.table({
x: ["apple", "banana", "apple", "cherry"],
});
const view = await table.view();
let keys: Int32Array | null = null;
let dict: string[] | null = null;
await view.with_typed_arrays(
{},
(
_n: string[],
vals: ArrayLike<number>[],
_valids: any[],
dicts: (string[] | null)[],
) => {
keys = new Int32Array(vals[0] as Int32Array);
dict = dicts[0] ? Array.from(dicts[0]) : null;
},
);
expect(keys!.length).toEqual(4);
const resolved = Array.from(keys!).map((k) => dict![k]);
expect(resolved).toEqual(["apple", "banana", "apple", "cherry"]);
await view.delete();
await table.delete();
});
test("dictionary is null for non-string columns", async () => {
const table = await perspective.table({ x: [1.0, 2.0, 3.0] });
const view = await table.view();
let dictWasNull = false;
await view.with_typed_arrays(
{},
(
_n: string[],
_vals: any[],
_valids: any[],
dicts: (string[] | null)[],
) => {
dictWasNull = dicts[0] === null;
},
);
expect(dictWasNull).toBe(true);
await view.delete();
await table.delete();
});
test("supports row windowing via start_row/end_row", async () => {
const table = await perspective.table({ x: [10, 20, 30, 40, 50] });
const view = await table.view();
let result: Int32Array | null = null;
await view.with_typed_arrays(
{ start_row: 1, end_row: 3 },
(_n: string[], vals: ArrayLike<number>[]) => {
result = new Int32Array(vals[0] as Int32Array);
},
);
expect(Array.from(result!)).toEqual([20, 30]);
await view.delete();
await table.delete();
});
test("returns Float64Array of millis for datetime column", async () => {
const d1 = new Date("2020-01-01T00:00:00Z");
const d2 = new Date("2021-06-15T12:30:00Z");
const table = await perspective.table({
x: [d1.getTime(), d2.getTime()],
});
const view = await table.view();
let result: Float64Array | null = null;
await view.with_typed_arrays(
{},
(_n: string[], vals: ArrayLike<number>[]) => {
result = new Float64Array(vals[0] as Float64Array);
},
);
expect(Array.from(result!)).toEqual([d1.getTime(), d2.getTime()]);
await view.delete();
await table.delete();
});
test("returns Float64Array of millis for date column", async () => {
const table = await perspective.table({ x: "date" });
table.update({ x: ["2020-01-01", "2021-06-15"] });
const view = await table.view();
let result: Float64Array | null = null;
await view.with_typed_arrays(
{},
(_n: string[], vals: ArrayLike<number>[]) => {
result = new Float64Array(vals[0] as Float64Array);
},
);
const expected = [
new Date("2020-01-01").getTime(),
new Date("2021-06-15").getTime(),
];
expect(Array.from(result!)).toEqual(expected);
await view.delete();
await table.delete();
});
test.describe("float32 option", () => {
test("Float64 column contents narrowed to Float32Array", async () => {
const table = await perspective.table({ x: [1.5, 2.5, 3.5, 4.5] });
const view = await table.view();
let result: Float32Array | null = null;
await view.with_typed_arrays(
{ float32: true },
(_n: string[], vals: any[]) => {
result = new Float32Array(vals[0]);
},
);
expect(result).toBeInstanceOf(Float32Array);
// Values exactly representable in float32
expect(Array.from(result!)).toEqual([1.5, 2.5, 3.5, 4.5]);
await view.delete();
await table.delete();
});
test("Int32 column is NOT affected by float32 option", async () => {
const table = await perspective.table({ x: "integer" });
table.update({ x: [10, 20, 30] });
const view = await table.view();
let isInt32 = false;
let result: Int32Array | null = null;
await view.with_typed_arrays(
{ float32: true },
(_n: string[], vals: any[]) => {
isInt32 = vals[0] instanceof Int32Array;
result = new Int32Array(vals[0]);
},
);
expect(isInt32).toBe(true);
expect(Array.from(result!)).toEqual([10, 20, 30]);
await view.delete();
await table.delete();
});
test("Date column narrowed to Float32Array of millis", async () => {
const table = await perspective.table({ x: "date" });
table.update({ x: ["2020-01-01", "2021-06-15"] });
const view = await table.view();
let result: Float32Array | null = null;
await view.with_typed_arrays(
{ float32: true },
(_n: string[], vals: any[]) => {
result = new Float32Array(vals[0]);
},
);
expect(result).toBeInstanceOf(Float32Array);
// Float32 precision loss is expected for large millis values
const expected = [
new Date("2020-01-01").getTime(),
new Date("2021-06-15").getTime(),
];
// Narrow to f32 then back to compare (lossy conversion)
const expectedF32 = Array.from(new Float32Array(expected));
expect(Array.from(result!)).toEqual(expectedF32);
await view.delete();
await table.delete();
});
test("Datetime column narrowed to Float32Array of millis", async () => {
const d1 = new Date("2020-01-01T00:00:00Z");
const d2 = new Date("2021-06-15T12:30:00Z");
const table = await perspective.table({
x: [d1.getTime(), d2.getTime()],
});
const view = await table.view();
let result: Float32Array | null = null;
await view.with_typed_arrays(
{ float32: true },
(_n: string[], vals: any[]) => {
result = new Float32Array(vals[0]);
},
);
expect(result).toBeInstanceOf(Float32Array);
const expectedF32 = Array.from(
new Float32Array([d1.getTime(), d2.getTime()]),
);
expect(Array.from(result!)).toEqual(expectedF32);
await view.delete();
await table.delete();
});
test("Dictionary string column returns Int32Array keys (unaffected by float32)", async () => {
const table = await perspective.table({
x: ["a", "b", "a", "c"],
});
const view = await table.view();
let isInt32 = false;
let dict: string[] | null = null;
await view.with_typed_arrays(
{ float32: true },
(
_n: string[],
vals: any[],
_valids: any[],
dicts: (string[] | null)[],
) => {
isInt32 = vals[0] instanceof Int32Array;
dict = dicts[0] ? Array.from(dicts[0]) : null;
},
);
expect(isInt32).toBe(true);
expect(dict).not.toBeNull();
await view.delete();
await table.delete();
});
test("float32 omitted defaults to Float64Array", async () => {
const table = await perspective.table({ x: [1.5, 2.5, 3.5] });
const view = await table.view();
let isFloat64 = false;
await view.with_typed_arrays({}, (_n: string[], vals: any[]) => {
isFloat64 = vals[0] instanceof Float64Array;
});
expect(isFloat64).toBe(true);
await view.delete();
await table.delete();
});
test("float32: false is equivalent to omitting it", async () => {
const table = await perspective.table({ x: [1.5, 2.5, 3.5] });
const view = await table.view();
let isFloat64 = false;
await view.with_typed_arrays(
{ float32: false },
(_n: string[], vals: any[]) => {
isFloat64 = vals[0] instanceof Float64Array;
},
);
expect(isFloat64).toBe(true);
await view.delete();
await table.delete();
});
test("float32 combines with row windowing", async () => {
const table = await perspective.table({
x: [1.5, 2.5, 3.5, 4.5, 5.5],
});
const view = await table.view();
let result: Float32Array | null = null;
await view.with_typed_arrays(
{ float32: true, start_row: 1, end_row: 4 },
(_n: string[], vals: any[]) => {
result = new Float32Array(vals[0]);
},
);
expect(result).toBeInstanceOf(Float32Array);
expect(Array.from(result!)).toEqual([2.5, 3.5, 4.5]);
await view.delete();
await table.delete();
});
test("float32 on multi-column view: floats narrowed, ints unchanged", async () => {
const table = await perspective.table({
a: "integer",
b: "float",
});
table.update({ a: [1, 2, 3], b: [1.5, 2.5, 3.5] });
const view = await table.view();
const typeMap: Record<string, string> = {};
await view.with_typed_arrays(
{ float32: true },
(n: string[], vals: any[]) => {
for (let i = 0; i < n.length; i++) {
typeMap[n[i]] = vals[i].constructor.name;
}
},
);
expect(typeMap["a"]).toEqual("Int32Array");
expect(typeMap["b"]).toEqual("Float32Array");
await view.delete();
await table.delete();
});
});
test.describe("group_by", () => {
test("emits __ROW_PATH_0__ column for single group_by", async () => {
const table = await perspective.table({
category: ["a", "a", "b", "b"],
value: [1, 2, 3, 4],
});
const view = await table.view({
group_by: ["category"],
aggregates: { value: "sum" },
});
let names: string[] = [];
await view.with_typed_arrays({}, (n: string[]) => {
names = Array.from(n);
});
expect(names).toContain("__ROW_PATH_0__");
// Should NOT contain the legacy "category (Group by 1)" naming
expect(names).not.toContain("category (Group by 1)");
await view.delete();
await table.delete();
});
test("__ROW_PATH_0__ is a Dictionary column with expected values", async () => {
const table = await perspective.table({
category: ["a", "a", "b", "b", "c"],
value: [1, 2, 3, 4, 5],
});
const view = await table.view({
group_by: ["category"],
aggregates: { value: "sum" },
});
let keys: Int32Array | null = null;
let dict: string[] | null = null;
await view.with_typed_arrays(
{},
(
n: string[],
vals: any[],
_valids: any[],
dicts: (string[] | null)[],
) => {
const idx = n.indexOf("__ROW_PATH_0__");
expect(idx).toBeGreaterThanOrEqual(0);
keys = new Int32Array(vals[idx]);
dict = dicts[idx] ? Array.from(dicts[idx]) : null;
},
);
expect(dict).not.toBeNull();
// Resolve keys to strings. First row is the total (empty/null),
// remaining rows are the groups.
const resolved = Array.from(keys!).map((k) =>
k >= 0 ? dict![k] : null,
);
expect(resolved.slice(1).sort()).toEqual(["a", "b", "c"]);
await view.delete();
await table.delete();
});
test("emits __ROW_PATH_0__ and __ROW_PATH_1__ for two-level group_by", async () => {
const table = await perspective.table({
region: ["US", "US", "EU", "EU"],
country: ["x", "y", "a", "b"],
value: [1, 2, 3, 4],
});
const view = await table.view({
group_by: ["region", "country"],
aggregates: { value: "sum" },
});
let names: string[] = [];
await view.with_typed_arrays({}, (n: string[]) => {
names = Array.from(n);
});
expect(names).toContain("__ROW_PATH_0__");
expect(names).toContain("__ROW_PATH_1__");
expect(names).not.toContain("region (Group by 1)");
expect(names).not.toContain("country (Group by 2)");
await view.delete();
await table.delete();
});
test("aggregate value column is present alongside row path", async () => {
const table = await perspective.table({
category: ["a", "a", "b"],
value: [10, 20, 30],
});
const view = await table.view({
group_by: ["category"],
aggregates: { value: "sum" },
});
const columns: Record<string, any> = {};
await view.with_typed_arrays(
{},
(
n: string[],
vals: any[],
_valids: any[],
dicts: (string[] | null)[],
) => {
for (let i = 0; i < n.length; i++) {
columns[n[i]] = { vals: vals[i], dict: dicts[i] };
}
},
);
expect(columns["__ROW_PATH_0__"]).toBeDefined();
expect(columns["value"]).toBeDefined();
// Value column should be numeric (not Dictionary)
expect(columns["value"].dict).toBeNull();
await view.delete();
await table.delete();
});
test("regular to_arrow still uses legacy naming by default", async () => {
// Sanity check: `to_arrow` (not with_typed_arrays) should
// still use legacy "colname (Group by N)" naming for backwards
// compatibility. `with_typed_arrays` forces the new naming.
const table = await perspective.table({
category: ["a", "b"],
value: [1, 2],
});
const view = await table.view({
group_by: ["category"],
aggregates: { value: "sum" },
});
const arrow = await view.to_arrow();
// The Arrow IPC bytes should contain the legacy name.
const bytes = new Uint8Array(arrow);
const text = new TextDecoder().decode(bytes);
expect(text.includes("(Group by 1)")).toBe(true);
await view.delete();
await table.delete();
});
});
test.describe("group_rollup_mode: flat", () => {
test("emits one row per distinct group (no total/aggregate rows)", async () => {
// In flat mode, only leaf rows are emitted — no intermediate
// rollup totals. 5 input rows with 3 distinct categories yields
// 3 output rows (one per group).
const table = await perspective.table({
category: ["a", "a", "b", "b", "c"],
value: [1, 2, 3, 4, 5],
});
const view = await table.view({
group_by: ["category"],
group_rollup_mode: "flat",
aggregates: { value: "sum" },
});
let rowCount = 0;
await view.with_typed_arrays({}, (_n: string[], vals: any[]) => {
rowCount = vals[0].length;
});
expect(rowCount).toEqual(3);
await view.delete();
await table.delete();
});
test("__ROW_PATH_0__ contains one entry per distinct group", async () => {
const table = await perspective.table({
category: ["a", "b", "a", "c"],
value: [10, 20, 30, 40],
});
const view = await table.view({
group_by: ["category"],
group_rollup_mode: "flat",
aggregates: { value: "sum" },
});
let paths: (string | null)[] = [];
await view.with_typed_arrays(
{},
(
n: string[],
vals: any[],
_valids: any[],
dicts: (string[] | null)[],
) => {
const idx = n.indexOf("__ROW_PATH_0__");
const keys = vals[idx] as Int32Array;
const dict = dicts[idx]!;
paths = Array.from(keys).map((k) =>
k >= 0 ? dict[k] : null,
);
},
);
expect(paths.slice().sort()).toEqual(["a", "b", "c"]);
await view.delete();
await table.delete();
});
test("aggregate column contains per-group sums in flat mode", async () => {
// Sum aggregate per group: a=10+20=30, b=30.
const table = await perspective.table({
category: ["a", "a", "b"],
value: [10, 20, 30],
});
const view = await table.view({
group_by: ["category"],
group_rollup_mode: "flat",
aggregates: { value: "sum" },
});
const rowMap: Record<string, number> = {};
await view.with_typed_arrays(
{},
(
n: string[],
vals: any[],
_valids: any[],
dicts: (string[] | null)[],
) => {
const pathIdx = n.indexOf("__ROW_PATH_0__");
const valueIdx = n.indexOf("value");
const keys = vals[pathIdx] as Int32Array;
const dict = dicts[pathIdx]!;
const values = vals[valueIdx] as ArrayLike<number>;
for (let i = 0; i < keys.length; i++) {
rowMap[dict[keys[i]]] = values[i];
}
},
);
expect(rowMap).toEqual({ a: 30, b: 30 });
await view.delete();
await table.delete();
});
test("flat mode with two-level group_by emits both __ROW_PATH_N__ columns", async () => {
const table = await perspective.table({
region: ["US", "US", "EU"],
country: ["x", "y", "a"],
value: [1, 2, 3],
});
const view = await table.view({
group_by: ["region", "country"],
group_rollup_mode: "flat",
aggregates: { value: "sum" },
});
let names: string[] = [];
const paths: Record<string, (string | null)[]> = {};
await view.with_typed_arrays(
{},
(
n: string[],
vals: any[],
_valids: any[],
dicts: (string[] | null)[],
) => {
names = Array.from(n);
for (const key of ["__ROW_PATH_0__", "__ROW_PATH_1__"]) {
const idx = n.indexOf(key);
if (idx >= 0) {
const keys = vals[idx] as Int32Array;
const dict = dicts[idx]!;
paths[key] = Array.from(keys).map((k) =>
k >= 0 ? dict[k] : null,
);
}
}
},
);
expect(names).toContain("__ROW_PATH_0__");
expect(names).toContain("__ROW_PATH_1__");
// 3 distinct (region, country) combinations — one row each.
expect(paths["__ROW_PATH_0__"].length).toEqual(3);
expect(paths["__ROW_PATH_1__"].length).toEqual(3);
// Ensure (region, country) pairs are preserved.
const pairs = paths["__ROW_PATH_0__"].map(
(r, i) => `${r}|${paths["__ROW_PATH_1__"][i]}`,
);
expect(pairs.slice().sort()).toEqual(["EU|a", "US|x", "US|y"]);
await view.delete();
await table.delete();
});
test("flat mode: all __ROW_PATH_N__ values are non-null (no aggregate rows)", async () => {
// In rollup mode, aggregate/total rows have null path segments.
// In flat mode, every row is a leaf so all paths are fully
// populated.
const table = await perspective.table({
region: ["US", "EU"],
country: ["x", "y"],
value: [1, 2],
});
const view = await table.view({
group_by: ["region", "country"],
group_rollup_mode: "flat",
aggregates: { value: "sum" },
});
let hasNullPath = false;
await view.with_typed_arrays(
{},
(
n: string[],
vals: any[],
_valids: any[],
dicts: (string[] | null)[],
) => {
for (let i = 0; i < n.length; i++) {
if (!n[i].startsWith("__ROW_PATH_")) continue;
const keys = vals[i] as Int32Array;
const dict = dicts[i];
for (let j = 0; j < keys.length; j++) {
if (keys[j] < 0 || !dict || !dict[keys[j]]) {
hasNullPath = true;
return;
}
}
}
},
);
expect(hasNullPath).toBe(false);
await view.delete();
await table.delete();
});
test("flat mode preserves value types (Int64 aggregate not coerced)", async () => {
// Flat-mode sum over integer input still flows through as an
// Int64 column (from the C++ engine's aggregate type), which
// with_typed_arrays converts to Float64Array.
const table = await perspective.table({
category: ["a", "b", "c"],
value: [10, 20, 30],
});
const view = await table.view({
group_by: ["category"],
group_rollup_mode: "flat",
aggregates: { value: "sum" },
});
let valueType = "";
await view.with_typed_arrays({}, (n: string[], vals: any[]) => {
const idx = n.indexOf("value");
valueType = vals[idx].constructor.name;
});
expect(valueType).toEqual("Float64Array");
await view.delete();
await table.delete();
});
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,179 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 = [
{ x: 1, y: "a", z: true },
{ x: 2, y: "b", z: false },
{ x: 3, y: "c", z: true },
{
x: 4,
y: "abcdefghijklmnopqrstuvwxyz",
z: false,
},
];
((perspective) => {
test.describe("View config", function () {
test("Non-interned filter strings do not create corrupted view configs", async function () {
const table = await perspective.table(data);
const view = await table.view({
filter: [["y", "==", "abcdefghijklmnopqrstuvwxyz"]],
});
const config = await view.get_config();
expect(config.filter).toEqual([
["y", "==", "abcdefghijklmnopqrstuvwxyz"],
]);
view.delete();
table.delete();
});
test("Non-default aggregates are provided", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
columns: ["x", "z"],
aggregates: { x: "count", z: "sum" },
});
const config = await view.get_config();
expect(config).toEqual({
aggregates: { x: "count", z: "sum" },
columns: ["x", "z"],
expressions: {},
filter: [],
group_by: ["y"],
sort: [],
split_by: [],
group_rollup_mode: "rollup",
});
view.delete();
table.delete();
});
test("Compound aggregates are provided", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
columns: ["x"],
aggregates: { x: ["weighted mean", ["y"]] },
});
const config = await view.get_config();
expect(config).toEqual({
aggregates: { x: ["weighted mean", ["y"]] },
columns: ["x"],
expressions: {},
filter: [],
group_by: ["y"],
sort: [],
split_by: [],
group_rollup_mode: "rollup",
});
view.delete();
table.delete();
});
test("Expression aggregates are provided", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
columns: ["x", "z", "new"],
expressions: { new: `"x" + 1` },
aggregates: { x: ["weighted mean", ["y"]] },
});
const config = await view.get_config();
expect(config).toEqual({
aggregates: {
new: "sum",
z: "count",
x: ["weighted mean", ["y"]],
},
columns: ["x", "z", "new"],
expressions: { new: `"x" + 1` },
filter: [],
group_by: ["y"],
sort: [],
split_by: [],
group_rollup_mode: "rollup",
});
view.delete();
table.delete();
});
// The `aggregates` field was omitted entirely in `3.0.0` until `3.9.0`
// (due to a bug introduced by an inebriated office drone). Prior to
// this, columns were sleectively omitted from the result if they were
// _non-default_ (to shorten JSON encoding). For example if a `"flaot"`
// column was anything other than `"sum"`.
//
// In revisiting this broken feature, I've updated the behavior to
// reflect a less certain view of what is "default", and output all
// columns which need aggregates. These tests preserve the `2.x`
// behavior.
test.skip("Default aggregates are omitted", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
columns: ["x", "z"],
aggregates: { x: "sum", z: "count" },
});
const config = await view.get_config();
expect(config).toEqual({
aggregates: {},
columns: ["x", "z"],
expressions: {},
filter: [],
group_by: ["y"],
sort: [],
split_by: [],
group_rollup_mode: "rollup",
});
view.delete();
table.delete();
});
test.skip("Mixed aggregates are provided and omitted", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
columns: ["x", "z"],
aggregates: { x: "sum", z: "mean" },
});
const config = await view.get_config();
expect(config).toEqual({
aggregates: { z: "mean" },
columns: ["x", "z"],
expressions: {},
filter: [],
group_by: ["y"],
sort: [],
split_by: [],
group_rollup_mode: "rollup",
});
view.delete();
table.delete();
});
});
})(perspective);
+114
View File
@@ -0,0 +1,114 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 } from "@perspective-dev/test";
test.describe("worker types", () => {
test("Worker", async ({ page }) => {
await page.goto(
"http://localhost:6598/node_modules/@perspective-dev/client/test/html/worker.html",
);
await page.evaluate(async () => {
while (!window["__TEST_PERSPECTIVE_READY__"]) {
await new Promise((x) => setTimeout(x, 10));
}
});
const s = await page.evaluate(async (x) => {
const t = await window.table;
return await t.size();
});
test.expect(s).toEqual(99);
});
test("SharedWorker", async ({ page }) => {
await page.goto(
"http://localhost:6598/node_modules/@perspective-dev/client/test/html/shared_worker.html",
);
await page.evaluate(async () => {
while (!window["__TEST_PERSPECTIVE_READY__"]) {
await new Promise((x) => setTimeout(x, 10));
}
});
const s = await page.evaluate(async (x) => {
const t = await window.table;
return await t.size();
});
test.expect(s).toEqual(99);
});
// Not supported https://github.com/microsoft/playwright/issues/30981
test.skip("ServiceWorker", async ({ page }) => {
await page.goto(
"/node_modules/@perspective-dev/client/test/html/service_worker.html",
);
await page.evaluate(async () => {
while (!window["__TEST_PERSPECTIVE_READY__"]) {
await new Promise((x) => setTimeout(x, 10));
}
});
const s = await page.evaluate(async (x) => {
const t = await window.table;
return await t.size();
});
test.expect(s).toEqual(99);
});
test("No SharedWorker or ServiceWorker (embedded)", async ({ page }) => {
await page.goto(
"http://localhost:6598/node_modules/@perspective-dev/client/test/html/test.html",
);
const s = await page.evaluate(async () => {
window.SharedWorker = undefined;
window.ServiceWorker = undefined;
const perspective = await import(
"http://localhost:6598/node_modules/@perspective-dev/client/dist/esm/perspective.js"
);
const wasm = fetch(
"http://localhost:6598/node_modules/@perspective-dev/client/dist/wasm/perspective-js.wasm",
);
const wasm2 = fetch(
"http://localhost:6598/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(
"http://localhost:6598/node_modules/@perspective-dev/client/dist/cdn/perspective-server.worker.js",
),
);
let resp = await fetch(
"http://localhost:6598/node_modules/@perspective-dev/test/assets/superstore.csv",
);
let csv = await resp.text();
const t = await worker.table(csv);
return await t.size();
});
test.expect(s).toEqual(99);
});
});