chore: import upstream snapshot with attribution
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user