// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ 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);