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