Files
2026-07-13 12:25:07 +08:00

1471 lines
53 KiB
JavaScript

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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";
const data = [
{ x: 1, y: 2 },
{ x: 2, y: 4 },
{ x: 3, y: 6 },
{ x: 4, y: 8 },
];
const pivot_data = [
{ int: 1, float: 2.25 },
{ int: 2, float: 3.5 },
{ int: 3, float: 4.75 },
{ int: 4, float: 5.25 },
];
/**
* Tests the correctness of updates on Tables with expression columns created
* through `View`, including partial updates, appends, and removes.
*/
((perspective) => {
test.describe("Expression updates", function () {
test("Scalar-only expressions should respond to appends", async function () {
const table = await perspective.table({
x: [1.5, 2.5, 3.5, 4.5],
y: ["A", "B", "C", "D"],
});
const view = await table.view({
expressions: [
"10 + 20",
"lower('ABC')",
"concat('hello', ' ', 'world', ', ', 'here is a long, long, long string with lots of characters')",
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const before = await view.to_columns();
expect(before["10 + 20"]).toEqual([30, 30, 30, 30]);
expect(before["lower('ABC')"]).toEqual([
"abc",
"abc",
"abc",
"abc",
]);
expect(
before[
"concat('hello', ' ', 'world', ', ', 'here is a long, long, long string with lots of characters')"
],
).toEqual([
"hello world, here is a long, long, long string with lots of characters",
"hello world, here is a long, long, long string with lots of characters",
"hello world, here is a long, long, long string with lots of characters",
"hello world, here is a long, long, long string with lots of characters",
]);
await table.update({ x: [5, 6, 7] });
const after = await view.to_columns();
expect(after["10 + 20"]).toEqual([30, 30, 30, 30, 30, 30, 30]);
expect(after["lower('ABC')"]).toEqual([
"abc",
"abc",
"abc",
"abc",
"abc",
"abc",
"abc",
]);
expect(
after[
"concat('hello', ' ', 'world', ', ', 'here is a long, long, long string with lots of characters')"
],
).toEqual([
"hello world, here is a long, long, long string with lots of characters",
"hello world, here is a long, long, long string with lots of characters",
"hello world, here is a long, long, long string with lots of characters",
"hello world, here is a long, long, long string with lots of characters",
"hello world, here is a long, long, long string with lots of characters",
"hello world, here is a long, long, long string with lots of characters",
"hello world, here is a long, long, long string with lots of characters",
]);
view.delete();
table.delete();
});
test("Conditional expressions should respond to appends", async function () {
const table = await perspective.table({
x: [1.5, 2.5, 3.5, 4.5],
y: ["A", "B", "C", "D"],
});
const view = await table.view({
// conditional here must be float, as int-float comparisons
// don't work as of yet.
expressions: [
'if ("x" > 4) 10; else 100',
`"y" == 'A' ? true : false`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const before = await view.to_columns();
expect(before['if ("x" > 4) 10; else 100']).toEqual([
100, 100, 100, 10,
]);
expect(before[`"y" == 'A' ? true : false`]).toEqual([
true,
false,
false,
false,
]);
await table.update({ x: [5, 6, 7], y: ["A", "A", "B"] });
const after = await view.to_columns();
expect(after['if ("x" > 4) 10; else 100']).toEqual([
100, 100, 100, 10, 10, 10, 10,
]);
expect(after[`"y" == 'A' ? true : false`]).toEqual([
true,
false,
false,
false,
true,
true,
false,
]);
view.delete();
table.delete();
});
test("Scalar-only expressions should respond to partial updates", async function () {
const table = await perspective.table(
{
x: [1.5, 2.5, 3.5, 4.5],
y: ["A", "B", "C", "D"],
},
{ index: "y" },
);
const view = await table.view({
expressions: [
"10 + 20",
"lower('ABC')",
"concat('hello', ' ', 'world', ', ', 'here is a long, long, long string with lots of characters')",
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const before = await view.to_columns();
expect(before["10 + 20"]).toEqual([30, 30, 30, 30]);
expect(before["lower('ABC')"]).toEqual([
"abc",
"abc",
"abc",
"abc",
]);
expect(
before[
"concat('hello', ' ', 'world', ', ', 'here is a long, long, long string with lots of characters')"
],
).toEqual([
"hello world, here is a long, long, long string with lots of characters",
"hello world, here is a long, long, long string with lots of characters",
"hello world, here is a long, long, long string with lots of characters",
"hello world, here is a long, long, long string with lots of characters",
]);
await table.update({ x: [5, 6, 7], y: ["A", "B", "D"] });
const after = await view.to_columns();
expect(after["10 + 20"]).toEqual([30, 30, 30, 30]);
expect(after["lower('ABC')"]).toEqual(["abc", "abc", "abc", "abc"]);
expect(
after[
"concat('hello', ' ', 'world', ', ', 'here is a long, long, long string with lots of characters')"
],
).toEqual([
"hello world, here is a long, long, long string with lots of characters",
"hello world, here is a long, long, long string with lots of characters",
"hello world, here is a long, long, long string with lots of characters",
"hello world, here is a long, long, long string with lots of characters",
]);
view.delete();
table.delete();
});
test("Expressions should respond to multiple streaming updates", async function () {
const table = await perspective.table({
x: [1.5, 2.5, 3.5, 4.5],
y: ["A", "B", "C", "D"],
});
const view = await table.view({
expressions: {
123: "123",
c0: '10 + "x"',
c1: 'lower("y")',
c2: `concat("y", ' ', 'abcd')`,
},
});
const before = await view.to_columns();
expect(before["123"]).toEqual(Array(4).fill(123));
expect(before["c0"]).toEqual([11.5, 12.5, 13.5, 14.5]);
expect(before["c1"]).toEqual(["a", "b", "c", "d"]);
expect(before["c2"]).toEqual([
"A abcd",
"B abcd",
"C abcd",
"D abcd",
]);
for (let i = 0; i < 5; i++) {
await table.update({
x: [1.5, 2.5, 3.5, 4.5],
y: ["A", "B", "C", "D"],
});
}
const after = await view.to_columns();
expect(await view.num_rows()).toEqual(24);
console.log(after);
expect(after["123"]).toEqual(Array(24).fill(123));
expect(after["c0"]).toEqual([
11.5, 12.5, 13.5, 14.5, 11.5, 12.5, 13.5, 14.5, 11.5, 12.5,
13.5, 14.5, 11.5, 12.5, 13.5, 14.5, 11.5, 12.5, 13.5, 14.5,
11.5, 12.5, 13.5, 14.5,
]);
expect(after["c1"]).toEqual([
"a",
"b",
"c",
"d",
"a",
"b",
"c",
"d",
"a",
"b",
"c",
"d",
"a",
"b",
"c",
"d",
"a",
"b",
"c",
"d",
"a",
"b",
"c",
"d",
]);
expect(after["c2"]).toEqual([
"A abcd",
"B abcd",
"C abcd",
"D abcd",
"A abcd",
"B abcd",
"C abcd",
"D abcd",
"A abcd",
"B abcd",
"C abcd",
"D abcd",
"A abcd",
"B abcd",
"C abcd",
"D abcd",
"A abcd",
"B abcd",
"C abcd",
"D abcd",
"A abcd",
"B abcd",
"C abcd",
"D abcd",
]);
view.delete();
table.delete();
});
test("Conditional expressions should respond to partial updates", async function () {
const table = await perspective.table(
{
x: [1.5, 2.5, 3.5, 4.5],
y: ["A", "B", "C", "D"],
z: ["a", "b", "c", "d"],
},
{ index: "y" },
);
const view = await table.view({
expressions: [
'if ("x" > 4) 10; else 100',
`"z" == 'a' ? true : false`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const before = await view.to_columns();
expect(before['if ("x" > 4) 10; else 100']).toEqual([
100, 100, 100, 10,
]);
expect(before[`"z" == 'a' ? true : false`]).toEqual([
true,
false,
false,
false,
]);
await table.update({
x: [5, 6, 7],
y: ["A", "C", "D"],
z: ["a", "a", "a"],
});
const after = await view.to_columns();
expect(after['if ("x" > 4) 10; else 100']).toEqual([
10, 100, 10, 10,
]);
expect(after[`"z" == 'a' ? true : false`]).toEqual([
true,
false,
true,
true,
]);
view.delete();
table.delete();
});
test("Appends should notify expression column", async function () {
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 before = await view.to_columns();
expect(before['lower("y")']).toEqual(["a", "b", "c", "d"]);
await table.update({ y: ["HELLO", "WORLD"] });
const after = await view.to_columns();
expect(after['lower("y")']).toEqual([
"a",
"b",
"c",
"d",
"hello",
"world",
]);
view.delete();
table.delete();
});
test("Multiple appends should notify expression column", async function () {
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 before = await view.to_columns();
expect(before['lower("y")']).toEqual(["a", "b", "c", "d"]);
await table.update({ y: ["HELLO", "WORLD"] });
const result = await view.to_columns();
expect(result['lower("y")']).toEqual([
"a",
"b",
"c",
"d",
"hello",
"world",
]);
await table.update({ x: [12, 34], y: ["XYZ", "ABCdEFg"] });
const result2 = await view.to_columns();
expect(result2["x"]).toEqual([1, 2, 3, 4, null, null, 12, 34]);
expect(result2['lower("y")']).toEqual([
"a",
"b",
"c",
"d",
"hello",
"world",
"xyz",
"abcdefg",
]);
view.delete();
table.delete();
});
test("Appends should notify expression column with multiple columns", async function () {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: [1.5, 2.5, 3.5, 4.5],
z: [2, 4, 6, 8],
});
const view = await table.view({
expressions: {
'"x" + ("y" + 5.5) / "z"': '"x" + ("y" + 5.5) / "z"',
},
});
const before = await view.to_columns();
expect(before['"x" + ("y" + 5.5) / "z"']).toEqual([
4.5, 4, 4.5, 5.25,
]);
await table.update({
x: [5, 6, 7, 8],
y: [5.5, 6.5, 7.5, 8.5],
z: [10, 10, 10, 10],
});
const after = await view.to_columns();
expect(after['"x" + ("y" + 5.5) / "z"']).toEqual([
4.5, 4, 4.5, 5.25, 6.1, 7.2, 8.3, 9.4,
]);
view.delete();
table.delete();
});
test("Partial updates should notify expression column", async function () {
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 before = await view.to_columns();
expect(before['lower("y")']).toEqual(["a", "b", "c", "d"]);
await table.update({ x: [1, 3], y: ["HELLO", "WORLD"] });
const after = await view.to_columns();
expect(after['lower("y")']).toEqual(["hello", "b", "world", "d"]);
view.delete();
table.delete();
});
test("Dependent column appends with missing columns should notify expression columns", async function () {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: [1.5, 2.5, 3.5, 4.5],
z: ["a", "b", "c", "d"],
});
const view = await table.view({
expressions: { 'upper("z")': 'upper("z")' },
});
const before = await view.to_columns();
expect(before['upper("z")']).toEqual(["A", "B", "C", "D"]);
// `z` is missing in the update, so it should render as null
await table.update({ x: [2, 4], y: [10.5, 12.5] });
const after = await view.to_columns();
expect(after).toEqual({
x: [1, 2, 3, 4, 2, 4],
y: [1.5, 2.5, 3.5, 4.5, 10.5, 12.5],
z: ["a", "b", "c", "d", null, null],
'upper("z")': ["A", "B", "C", "D", null, null],
});
view.delete();
table.delete();
});
test("Dependent column update appends should notify expression columns, pivoted arity 1", async function () {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: ["A", "B", "C", "C"],
});
const view = await table.view({
group_by: ['lower("y")'],
expressions: { 'lower("y")': 'lower("y")' },
});
const before = await view.to_columns();
expect(before).toEqual({
__ROW_PATH__: [[], ["a"], ["b"], ["c"]],
'lower("y")': [4, 1, 1, 2],
x: [10, 1, 2, 7],
y: [4, 1, 1, 2],
});
await table.update({ y: ["HELLO", "WORLD"] });
const after = await view.to_columns();
expect(after).toEqual({
__ROW_PATH__: [[], ["a"], ["b"], ["c"], ["hello"], ["world"]],
'lower("y")': [6, 1, 1, 2, 1, 1],
x: [10, 1, 2, 7, 0, 0],
y: [6, 1, 1, 2, 1, 1],
});
view.delete();
table.delete();
});
test("Dependent column appends should notify expression columns, arity 2", async function () {
const table = await perspective.table(common.int_float_data);
const view = await table.view({
expressions: { '"w" + "x"': '"w" + "x"' },
});
const before = await view.to_columns();
expect(before['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
await table.update({ x: [2, 4], w: [10.5, 12.5] });
const after = await view.to_columns();
expect(after['"w" + "x"']).toEqual([
2.5, 4.5, 6.5, 8.5, 12.5, 16.5,
]);
view.delete();
table.delete();
});
test("Dependent column updates on all column updates should notify expression columns, arity 2", async function () {
const table = await perspective.table(common.int_float_data, {
index: "x",
});
const view = await table.view({
expressions: {
'"w" + "x"': '"w" + "x"',
'upper("y")': 'upper("y")',
},
});
let before = await view.to_columns();
expect(before['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
expect(before['upper("y")']).toEqual(["A", "B", "C", "D"]);
await table.update({ x: [2, 4], w: [10.5, 12.5] });
const after = await view.to_columns();
expect(after['"w" + "x"']).toEqual([2.5, 12.5, 6.5, 16.5]);
await table.update({ x: [1, 3], y: ["hello", "world"] });
const after2 = await view.to_columns();
expect(after2['upper("y")']).toEqual(["HELLO", "B", "WORLD", "D"]);
view.delete();
table.delete();
});
test("Dependent column appends should notify expression columns on different views, arity 2", async function () {
const table = await perspective.table(common.int_float_data);
const view = await table.view({
expressions: { '"w" + "x"': '"w" + "x"' },
});
const view2 = await table.view({
expressions: { '"w" - "x"': '"w" - "x"' },
});
const before = await view.to_columns();
const before2 = await view2.to_columns();
expect(before['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
expect(before2['"w" - "x"']).toEqual([0.5, 0.5, 0.5, 0.5]);
await table.update({ x: [2, 4], w: [10.5, 12.5] });
const after = await view.to_columns();
const after2 = await view2.to_columns();
expect(after['"w" + "x"']).toEqual([
2.5, 4.5, 6.5, 8.5, 12.5, 16.5,
]);
expect(after2['"w" - "x"']).toEqual([0.5, 0.5, 0.5, 0.5, 8.5, 8.5]);
view2.delete();
view.delete();
table.delete();
});
test("Dependent column appends should notify equivalent expression columns on different views, arity 2", async function () {
const table = await perspective.table(common.int_float_data);
const view = await table.view({
expressions: { '"w" + "x"': '"w" + "x"' },
});
const view2 = await table.view({
expressions: {
'"w" + "x"': '"w" + "x"',
'"w" - "x"': '"w" - "x"',
},
});
const before = await view.to_columns();
const before2 = await view2.to_columns();
expect(before['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
expect(before2['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
expect(before2['"w" - "x"']).toEqual([0.5, 0.5, 0.5, 0.5]);
await table.update({ x: [2, 4], w: [10.5, 12.5] });
const after = await view.to_columns();
const after2 = await view2.to_columns();
expect(after['"w" + "x"']).toEqual([
2.5, 4.5, 6.5, 8.5, 12.5, 16.5,
]);
expect(after2['"w" + "x"']).toEqual([
2.5, 4.5, 6.5, 8.5, 12.5, 16.5,
]);
expect(after2['"w" - "x"']).toEqual([0.5, 0.5, 0.5, 0.5, 8.5, 8.5]);
view2.delete();
view.delete();
table.delete();
});
test("Dependent column updates should notify expression columns on different views, arity 2.", async function () {
const table = await perspective.table(common.int_float_data, {
index: "x",
});
const view = await table.view({
expressions: { '"w" + "x"': '"w" + "x"' },
});
const view2 = await table.view({
expressions: { '"w" - "x"': '"w" - "x"' },
});
const before = await view.to_columns();
const before2 = await view2.to_columns();
expect(before['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
expect(before2['"w" - "x"']).toEqual([0.5, 0.5, 0.5, 0.5]);
await table.update({ x: [2, 4], w: [10.5, 12.5] });
const after = await view.to_columns();
const after2 = await view2.to_columns();
expect(after['"w" + "x"']).toEqual([2.5, 12.5, 6.5, 16.5]);
expect(after2['"w" - "x"']).toEqual([0.5, 8.5, 0.5, 8.5]);
view2.delete();
view.delete();
table.delete();
});
test("Dependent column updates should notify equivalent expression columns on different views, arity 2.", async function () {
const table = await perspective.table(common.int_float_data, {
index: "x",
});
const view = await table.view({
expressions: { '"w" + "x"': '"w" + "x"' },
});
const view2 = await table.view({
expressions: {
'"w" + "x"': '"w" + "x"',
'"w" - "x"': '"w" - "x"',
},
});
const before = await view.to_columns();
const before2 = await view2.to_columns();
expect(before['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
expect(before2['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
expect(before2['"w" - "x"']).toEqual([0.5, 0.5, 0.5, 0.5]);
await table.update({ x: [2, 4], w: [10.5, 12.5] });
const after = await view.to_columns();
const after2 = await view2.to_columns();
expect(after['"w" + "x"']).toEqual([2.5, 12.5, 6.5, 16.5]);
expect(after2['"w" + "x"']).toEqual([2.5, 12.5, 6.5, 16.5]);
expect(after2['"w" - "x"']).toEqual([0.5, 8.5, 0.5, 8.5]);
view2.delete();
view.delete();
table.delete();
});
test("Dependent column update with `null` should notify expression columns on different views, arity 2.", async function () {
const table = await perspective.table(common.int_float_data, {
index: "x",
});
const view = await table.view({
expressions: { '"w" + "x"': '"w" + "x"' },
});
const view2 = await table.view({
expressions: { '"w" - "x"': '"w" - "x"' },
});
const before = await view.to_columns();
const before2 = await view2.to_columns();
expect(before['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
expect(before2['"w" - "x"']).toEqual([0.5, 0.5, 0.5, 0.5]);
await table.update({ x: [2, 4], w: [null, 12.5] });
const after = await view.to_columns();
const after2 = await view2.to_columns();
expect(after['"w" + "x"']).toEqual([2.5, null, 6.5, 16.5]);
expect(after2['"w" - "x"']).toEqual([0.5, null, 0.5, 8.5]);
view2.delete();
view.delete();
table.delete();
});
test("Dependent column update with `null` should notify equivalent expression columns on different views, arity 2.", async function () {
const table = await perspective.table(common.int_float_data, {
index: "x",
});
const view = await table.view({
expressions: { '"w" + "x"': '"w" + "x"' },
});
const view2 = await table.view({
expressions: {
'"w" + "x"': '"w" + "x"',
'"w" - "x"': '"w" - "x"',
},
});
const before = await view.to_columns();
const before2 = await view2.to_columns();
expect(before['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
expect(before2['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
expect(before2['"w" - "x"']).toEqual([0.5, 0.5, 0.5, 0.5]);
await table.update({ x: [2, 4], w: [null, 12.5] });
const after = await view.to_columns();
const after2 = await view2.to_columns();
expect(after['"w" + "x"']).toEqual([2.5, null, 6.5, 16.5]);
expect(after2['"w" + "x"']).toEqual([2.5, null, 6.5, 16.5]);
expect(after2['"w" - "x"']).toEqual([0.5, null, 0.5, 8.5]);
view2.delete();
view.delete();
table.delete();
});
test("Updating with `null` should clear the output expression column.", async function () {
const table = await perspective.table(
{
w: [1.5, 2.5, 3.5, 4.5],
x: [1, 2, 3, 4],
y: [5, 6, 7, 8],
},
{ index: "x" },
);
const view = await table.view({
expressions: { '"w" + "y"': '"w" + "y"' },
});
let before = await view.to_columns();
expect(before['"w" + "y"']).toEqual([6.5, 8.5, 10.5, 12.5]);
await table.update({ x: [2, 4], w: [null, 12.5] });
const after = await view.to_columns();
expect(after['"w" + "y"']).toEqual([6.5, null, 10.5, 20.5]);
await table.update({ x: [2, 3], w: [20.5, null] });
const after2 = await view.to_columns();
expect(after2['"w" + "y"']).toEqual([6.5, 26.5, null, 20.5]);
view.delete();
table.delete();
});
test.skip("OG - Updating with `undefined` should not clear the output expression column.", async function () {
const table = await perspective.table(
{
w: [1.5, 2.5, 3.5, 4.5],
x: [1, 2, 3, 4],
y: [5, 6, 7, 8],
},
{ index: "x" },
);
const view = await table.view({
expressions: { '"w" + "y"': '"w" + "y"' },
});
let before = await view.to_columns();
expect(before['"w" + "y"']).toEqual([6.5, 8.5, 10.5, 12.5]);
await table.update({ x: [2, 4], w: [undefined, 12.5] });
const after = await view.to_columns();
expect(after['"w" + "y"']).toEqual([6.5, 8.5, 10.5, 20.5]);
await table.update({ x: [2, 3], w: [20.5, undefined] });
const after2 = await view.to_columns();
expect(after2['"w" + "y"']).toEqual([6.5, 26.5, 10.5, 20.5]);
view.delete();
table.delete();
});
test("Updating with `undefined` should not clear the output expression column.", async function () {
const table = await perspective.table(
{
w: [1.5, 2.5, 3.5, 4.5],
x: [1, 2, 3, 4],
y: [5, 6, 7, 8],
},
{ index: "x" },
);
const view = await table.view({
expressions: { '"w" + "y"': '"w" + "y"' },
});
let before = await view.to_columns();
expect(before['"w" + "y"']).toEqual([6.5, 8.5, 10.5, 12.5]);
await table.update({ x: [2] });
await table.update({ x: [4], w: [12.5] });
const after = await view.to_columns();
expect(after['"w" + "y"']).toEqual([6.5, 8.5, 10.5, 20.5]);
await table.update({ x: [2], w: [20.5] });
await table.update({ x: [3] });
const after2 = await view.to_columns();
expect(after2['"w" + "y"']).toEqual([6.5, 26.5, 10.5, 20.5]);
view.delete();
table.delete();
});
test("Updates on non-dependent columns should not change expression columns.", async function () {
var meta = {
w: "float",
x: "float",
y: "string",
z: "boolean",
};
const table = await perspective.table(meta, { index: "y" });
const view = await table.view({
columns: ["y", '"w" / "x"'],
expressions: { '"w" / "x"': '"w" / "x"' },
});
await table.update(common.int_float_data);
let delta_upd = [
{ y: "a", z: false },
{ y: "b", z: true },
{ y: "c", z: false },
{ y: "d", z: true },
];
await table.update(delta_upd);
let result = await view.to_json();
let expected = [
{ y: "a", '"w" / "x"': 1.5 },
{ y: "b", '"w" / "x"': 1.25 },
{ y: "c", '"w" / "x"': 1.1666666666666667 },
{ y: "d", '"w" / "x"': 1.125 },
];
expect(result).toEqual(expected);
view.delete();
table.delete();
});
test("Should recompute after partial update using `__INDEX__`", async function () {
const table = await perspective.table({
x: "integer",
y: "integer",
});
const view = await table.view({
expressions: { '"x" * "y"': '"x" * "y"' },
});
await table.update(data);
await table.update([
{ __INDEX__: 0, x: 10 },
{ __INDEX__: 2, x: 10 },
]);
const json = await view.to_json();
expect(json).toEqual([
{ x: 10, y: 2, '"x" * "y"': 20 },
{ x: 2, y: 4, '"x" * "y"': 8 },
{ x: 10, y: 6, '"x" * "y"': 60 },
{ x: 4, y: 8, '"x" * "y"': 32 },
]);
view.delete();
table.delete();
});
test("Partial update without a new value shouldn't change computed output", async function () {
const table = await perspective.table(data);
const view = await table.view({
expressions: { '"x" * "y"': '"x" * "y"' },
});
const json = await view.to_json();
expect(json).toEqual([
{ x: 1, y: 2, '"x" * "y"': 2 },
{ x: 2, y: 4, '"x" * "y"': 8 },
{ x: 3, y: 6, '"x" * "y"': 18 },
{ x: 4, y: 8, '"x" * "y"': 32 },
]);
await table.update([{ __INDEX__: 0, x: 1 }]);
const json2 = await view.to_json();
expect(json2).toEqual(json);
view.delete();
table.delete();
});
test("partial update on single computed source column", async function () {
const table = await perspective.table(data);
const view = await table.view({
expressions: { '"x" * "y"': '"x" * "y"' },
});
await table.update([{ __INDEX__: 0, x: 10 }]);
const json = await view.to_json();
expect(json).toEqual([
{ x: 10, y: 2, '"x" * "y"': 20 },
{ x: 2, y: 4, '"x" * "y"': 8 },
{ x: 3, y: 6, '"x" * "y"': 18 },
{ x: 4, y: 8, '"x" * "y"': 32 },
]);
view.delete();
table.delete();
});
test("partial update on non-contiguous computed source columns", async function () {
const table = await perspective.table(data);
const view = await table.view({
expressions: { '"x" * "y"': '"x" * "y"' },
});
await table.update([
{ __INDEX__: 0, x: 1, y: 10 },
{ __INDEX__: 2, x: 3, y: 20 },
]);
let json = await view.to_json();
expect(json).toEqual([
{ x: 1, y: 10, '"x" * "y"': 10 },
{ x: 2, y: 4, '"x" * "y"': 8 },
{ x: 3, y: 20, '"x" * "y"': 60 },
{ x: 4, y: 8, '"x" * "y"': 32 },
]);
view.delete();
table.delete();
});
test("partial update on non-contiguous computed source columns, indexed table", async function () {
const table = await perspective.table(data, { index: "x" });
const view = await table.view({
expressions: { '"x" * "y"': '"x" * "y"' },
});
await table.update([
{ x: 1, y: 10 },
{ x: 3, y: 20 },
]);
let json = await view.to_json();
expect(json).toEqual([
{ x: 1, y: 10, '"x" * "y"': 10 },
{ x: 2, y: 4, '"x" * "y"': 8 },
{ x: 3, y: 20, '"x" * "y"': 60 },
{ x: 4, y: 8, '"x" * "y"': 32 },
]);
view.delete();
table.delete();
});
test("multiple updates change validity", async function () {
const table = await perspective.table(
{
a: "float",
b: "float",
c: "string",
index: "integer",
},
{ index: "index" },
);
await table.update({
a: [null, null, 1.5, null],
b: [null, 2.5, null, null],
c: ["a", "b", "b", "a"],
index: [1, 2, 3, 4],
});
const view = await table.view({
expressions: { computed: '"a" + "b"' },
});
let result = await view.to_columns();
expect(result["computed"]).toEqual([null, null, null, null]);
await table.update({
index: [4],
a: [100],
});
result = await view.to_columns();
expect(result["computed"]).toEqual([null, null, null, null]);
await table.update({
index: [4],
b: [100],
});
result = await view.to_columns();
expect(result["computed"]).toEqual([null, null, null, 200]);
await table.update({
index: [3],
b: [100],
});
result = await view.to_columns();
expect(result["computed"]).toEqual([null, null, 101.5, 200]);
await view.delete();
await table.delete();
});
test("multiple partial update on single computed source column", async function () {
const table = await perspective.table(data);
const view = await table.view({
expressions: { '"x" * "y"': '"x" * "y"' },
});
await table.update([
{ __INDEX__: 0, x: 10 },
{ __INDEX__: 2, x: 10 },
]);
await table.update([
{ __INDEX__: 0, x: 20 },
{ __INDEX__: 2, x: 20 },
]);
await table.update([
{ __INDEX__: 0, x: 30 },
{ __INDEX__: 2, x: 30 },
]);
let json = await view.to_json();
expect(json).toEqual([
{ x: 30, y: 2, '"x" * "y"': 60 },
{ x: 2, y: 4, '"x" * "y"': 8 },
{ x: 30, y: 6, '"x" * "y"': 180 },
{ x: 4, y: 8, '"x" * "y"': 32 },
]);
view.delete();
table.delete();
});
test("multiple expression columns with updates on source columns", async function () {
const table = await perspective.table(data);
const view = await table.view({
expressions: { '"x" * "y"': '"x" * "y"' },
});
const view2 = await table.view({
expressions: { '"x" + "y"': '"x" + "y"' },
});
await table.update([
{ __INDEX__: 0, x: 5 },
{ __INDEX__: 2, x: 10 },
]);
let result = await view.to_columns();
let result2 = await view2.to_columns();
expect(result).toEqual({
x: [5, 2, 10, 4],
y: [2, 4, 6, 8],
'"x" * "y"': [10, 8, 60, 32],
});
expect(result2).toEqual({
x: [5, 2, 10, 4],
y: [2, 4, 6, 8],
'"x" + "y"': [7, 6, 16, 12],
});
view2.delete();
view.delete();
table.delete();
});
test("propagate updates to all expression columns", async function () {
const table = await perspective.table(data, { index: "x" });
const view = await table.view({
expressions: { '"x" * "y"': '"x" * "y"' },
});
const view2 = await table.view({
expressions: { '"x" + "y"': '"x" + "y"' },
});
const view3 = await table.view({
expressions: { '"x" - "y"': '"x" - "y"' },
});
await table.update({ x: [1, 2, 3, 4], y: [1, 2, 3, 4] });
let result = await view.to_columns();
let result2 = await view2.to_columns();
let result3 = await view3.to_columns();
expect(result).toEqual({
x: [1, 2, 3, 4],
y: [1, 2, 3, 4],
'"x" * "y"': [1, 4, 9, 16],
});
expect(result2).toEqual({
x: [1, 2, 3, 4],
y: [1, 2, 3, 4],
'"x" + "y"': [2, 4, 6, 8],
});
expect(result3).toEqual({
x: [1, 2, 3, 4],
y: [1, 2, 3, 4],
'"x" - "y"': [0, 0, 0, 0],
});
view3.delete();
view2.delete();
view.delete();
table.delete();
});
test("propagate appends to all expression columns", async function () {
const table = await perspective.table(data);
const view = await table.view({
expressions: { '"x" * "y"': '"x" * "y"' },
});
const view2 = await table.view({
expressions: { '"x" + "y"': '"x" + "y"' },
});
const view3 = await table.view({
expressions: { '"x" - "y"': '"x" - "y"' },
});
await table.update({ x: [1, 2, 3, 4], y: [1, 2, 3, 4] });
let result = await view.to_columns();
let result2 = await view2.to_columns();
let result3 = await view3.to_columns();
expect(result).toEqual({
x: [1, 2, 3, 4, 1, 2, 3, 4],
y: [2, 4, 6, 8, 1, 2, 3, 4],
'"x" * "y"': [2, 8, 18, 32, 1, 4, 9, 16],
});
expect(result2).toEqual({
x: [1, 2, 3, 4, 1, 2, 3, 4],
y: [2, 4, 6, 8, 1, 2, 3, 4],
'"x" + "y"': [3, 6, 9, 12, 2, 4, 6, 8],
});
expect(result3).toEqual({
x: [1, 2, 3, 4, 1, 2, 3, 4],
y: [2, 4, 6, 8, 1, 2, 3, 4],
'"x" - "y"': [-1, -2, -3, -4, 0, 0, 0, 0],
});
view3.delete();
view2.delete();
view.delete();
table.delete();
});
});
test.describe("Computed updates with group by", function () {
test("should update on dependent columns, add", async function () {
const table = await perspective.table(pivot_data);
const view = await table.view({
columns: ['"int" + "float"', "int"],
aggregates: {
'"int" + "float"': "sum",
},
group_by: ['"int" + "float"'],
expressions: { '"int" + "float"': '"int" + "float"' },
});
await table.update({ int: [4], __INDEX__: [0] });
let results = await view.to_columns({
index: true,
});
expect(results).toEqual({
__ROW_PATH__: [[], [5.5], [6.25], [7.75], [9.25]],
int: [13, 2, 4, 3, 4],
'"int" + "float"': [28.75, 5.5, 6.25, 7.75, 9.25],
__INDEX__: [[0, 3, 2, 1], [1], [0], [2], [3]],
});
view.delete();
table.delete();
});
test("should update on dependent columns, subtract", async function () {
const table = await perspective.table(pivot_data);
const view = await table.view({
columns: ['"int" - "float"', "int"],
group_by: ['"int" - "float"'],
expressions: { '"int" - "float"': '"int" - "float"' },
});
await table.update([{ int: 4, __INDEX__: 0 }]);
let json = await view.to_json({
index: true,
});
expect(json).toEqual([
{
__ROW_PATH__: [],
int: 13,
'"int" - "float"': -2.75,
__INDEX__: [0, 3, 1, 2],
},
{
__ROW_PATH__: [-1.75],
int: 3,
'"int" - "float"': -1.75,
__INDEX__: [2],
},
{
__ROW_PATH__: [-1.5],
int: 2,
'"int" - "float"': -1.5,
__INDEX__: [1],
},
{
__ROW_PATH__: [-1.25],
int: 4,
'"int" - "float"': -1.25,
__INDEX__: [3],
},
{
__ROW_PATH__: [1.75],
int: 4,
'"int" - "float"': 1.75,
__INDEX__: [0],
},
]);
view.delete();
table.delete();
});
test("should update on dependent columns, multiply", async function () {
const table = await perspective.table(pivot_data);
const view = await table.view({
columns: ['"int" * "float"', "int"],
group_by: ['"int" * "float"'],
expressions: { '"int" * "float"': '"int" * "float"' },
});
await table.update([{ int: 4, __INDEX__: 0 }]);
let json = await view.to_json({
index: true,
});
expect(json).toEqual([
{
__ROW_PATH__: [],
int: 13,
'"int" * "float"': 51.25,
__INDEX__: [0, 3, 2, 1],
},
{
__ROW_PATH__: [7],
int: 2,
'"int" * "float"': 7,
__INDEX__: [1],
},
{
__ROW_PATH__: [9],
int: 4,
'"int" * "float"': 9,
__INDEX__: [0],
},
{
__ROW_PATH__: [14.25],
int: 3,
'"int" * "float"': 14.25,
__INDEX__: [2],
},
{
__ROW_PATH__: [21],
int: 4,
'"int" * "float"': 21,
__INDEX__: [3],
},
]);
view.delete();
table.delete();
});
test("should update on dependent columns, divide", async function () {
const table = await perspective.table(pivot_data);
const view = await table.view({
columns: ['"int" / "float"', "int"],
group_by: ['"int" / "float"'],
expressions: { '"int" / "float"': '"int" / "float"' },
});
await table.update([{ int: 4, __INDEX__: 0 }]);
let json = await view.to_json({
index: true,
});
expect(json).toEqual([
{
__ROW_PATH__: [],
int: 13,
'"int" / "float"': 3.742690058479532,
__INDEX__: [0, 3, 2, 1],
},
{
__ROW_PATH__: [0.5714285714285714],
int: 2,
'"int" / "float"': 0.5714285714285714,
__INDEX__: [1],
},
{
__ROW_PATH__: [0.631578947368421],
int: 3,
'"int" / "float"': 0.631578947368421,
__INDEX__: [2],
},
{
__ROW_PATH__: [0.7619047619047619],
int: 4,
'"int" / "float"': 0.7619047619047619,
__INDEX__: [3],
},
{
__ROW_PATH__: [1.7777777777777777],
int: 4,
'"int" / "float"': 1.7777777777777777,
__INDEX__: [0],
},
]);
view.delete();
table.delete();
});
});
test.describe("Partial update with null", function () {
test("Update over null should recalculate", async function () {
const table = await perspective.table(pivot_data, { index: "int" });
const view = await table.view({
columns: ['"int" + "float"', "int", "float"],
expressions: { '"int" + "float"': '"int" + "float"' },
});
await table.update([{ int: 2, float: 3.5 }]);
let result = await view.to_columns();
expect(result).toEqual({
'"int" + "float"': [3.25, 5.5, 7.75, 9.25],
int: [1, 2, 3, 4],
float: [2.25, 3.5, 4.75, 5.25],
});
view.delete();
table.delete();
});
test("Update with null should unset", async function () {
const table = await perspective.table(pivot_data, { index: "int" });
const view = await table.view({
columns: ['"int" + "float"', "int", "float"],
expressions: { '"int" + "float"': '"int" + "float"' },
});
await table.update([{ int: 2, float: null }]);
let result = await view.to_columns();
expect(result).toEqual({
'"int" + "float"': [3.25, null, 7.75, 9.25],
int: [1, 2, 3, 4],
float: [2.25, null, 4.75, 5.25],
});
view.delete();
table.delete();
});
test("Undefined should be a no-op on expression columns", async function () {
const table = await perspective.table(
[
{ int: 1, float: 2.25, string: "a", datetime: new Date() },
{ int: 2, float: 3.5, string: "b", datetime: new Date() },
{ int: 3, float: 4.75, string: "c", datetime: new Date() },
{ int: 4, float: 5.25, string: "d", datetime: new Date() },
],
{ index: "int" },
);
const view = await table.view({
columns: ['"int" + "float"', "int", "float"],
expressions: { '"int" + "float"': '"int" + "float"' },
});
await table.update([{ int: 2, float: undefined }]);
let result = await view.to_columns();
expect(result).toEqual({
'"int" + "float"': [3.25, 5.5, 7.75, 9.25],
int: [1, 2, 3, 4],
float: [2.25, 3.5, 4.75, 5.25],
});
view.delete();
table.delete();
});
});
test.describe("Removing expression columns", () => {
test("Removes should remove rows in expression columns", async () => {
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 before = await view.to_columns();
expect(before['lower("y")']).toEqual(["a", "b", "c", "d"]);
await table.update({ y: ["HELLO", "WORLD"] });
const result = await view.to_columns();
expect(result["x"]).toEqual([null, 1, 2, 3, 4]);
expect(result['lower("y")']).toEqual(["world", "a", "b", "c", "d"]);
await table.remove([4, null, 1]);
const result2 = await view.to_columns();
expect(result2["x"]).toEqual([2, 3]);
expect(result2['lower("y")']).toEqual(["b", "c"]);
view.delete();
table.delete();
});
});
})(perspective);