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

3532 lines
116 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 * as _ from "lodash";
import * as arrows from "./test_arrows.js";
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
function it_old_behavior(name, capture, opts = { skip: false }) {
let test_fn = test;
if (opts.skip) {
test_fn = test.skip;
}
test_fn(name, async function () {
let done;
let result = new Promise((x) => {
done = x;
});
await capture(done);
await result;
});
}
var 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 },
];
var col_data = {
x: [1, 2, 3, 4],
y: ["a", "b", "c", "d"],
z: [true, false, true, false],
};
var meta = {
x: "integer",
y: "string",
z: "boolean",
};
var data_2 = [
{ x: 3, y: "c", z: false },
{ x: 4, y: "d", z: true },
{ x: 5, y: "g", z: false },
{ x: 6, y: "h", z: true },
];
const 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"),
},
];
const arrow_partial_result = [
{
f32: 1.5,
f64: 1.5,
i64: 1,
i32: 1,
i16: 1,
i8: 1,
bool: false,
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: true,
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: false,
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: true,
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: false,
char: "d",
dict: "d",
datetime: +new Date("2018-01-29"),
},
];
const arrow_partial_missing_result = [
{
f32: 1.5,
f64: 1.5,
i64: 1,
i32: 1,
i16: 1,
i8: 1,
bool: false,
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: false,
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: false,
char: "d",
dict: "d",
datetime: +new Date("2018-01-29"),
},
];
const arrow_indexed_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: 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"),
},
];
async function match_delta(perspective, delta, expected) {
let table = await perspective.table(delta);
let view = await table.view();
let json = await view.to_json();
expect(json).toEqual(expected);
view.delete();
table.delete();
}
((perspective) => {
test.describe("Removes", function () {
test("should not remove without explicit index", async function () {
const table = await perspective.table(meta);
await table.update(data);
const view = await table.view();
let threw = false;
try {
// In 3.0, this throws an exception
await table.remove([0, 1]);
} catch (e) {
threw = true;
}
expect(threw).toBeTruthy();
const result = await view.to_json();
expect(await view.num_rows()).toEqual(4);
expect(result.length).toEqual(4);
expect(result).toEqual(data);
expect(await table.size()).toEqual(4);
view.delete();
table.delete();
});
test("after an `update()`", async function () {
const table = await perspective.table(meta, { index: "x" });
await table.update(data);
const view = await table.view();
await table.remove([1, 2]);
const result = await view.to_json();
expect(await view.num_rows()).toEqual(2);
expect(result.length).toEqual(2);
expect(result).toEqual(data.slice(2, 4));
expect(await table.size()).toEqual(2);
view.delete();
table.delete();
});
test("after a regular data load", async function () {
const table = await perspective.table(data, { index: "x" });
const view = await table.view();
await table.remove([1, 2]);
const result = await view.to_json();
expect(await view.num_rows()).toEqual(2);
expect(result.length).toEqual(2);
expect(result).toEqual(data.slice(2, 4));
expect(await table.size()).toEqual(2);
view.delete();
table.delete();
});
test("after an `update()`, string pkey", async function () {
const table = await perspective.table(meta, { index: "y" });
await table.update(data);
const view = await table.view();
await table.remove(["a", "b"]);
const result = await view.to_json();
expect(await view.num_rows()).toEqual(2);
expect(result.length).toEqual(2);
expect(result).toEqual(data.slice(2, 4));
expect(await table.size()).toEqual(2);
view.delete();
table.delete();
});
test("after a regular data load, string pkey", async function () {
const table = await perspective.table(data, { index: "y" });
const view = await table.view();
await table.remove(["a", "b"]);
const result = await view.to_json();
expect(await view.num_rows()).toEqual(2);
expect(result.length).toEqual(2);
expect(result).toEqual(data.slice(2, 4));
expect(await table.size()).toEqual(2);
view.delete();
table.delete();
});
test("after an update, date pkey", async function () {
const datetimes = [
new Date(2020, 0, 15),
new Date(2020, 1, 15),
new Date(2020, 2, 15),
new Date(2020, 3, 15),
];
const table = await perspective.table(
{
x: "integer",
y: "date",
z: "float",
},
{ index: "y" },
);
await table.update({
x: [1, 2, 3, 4],
y: datetimes,
z: [1.5, 2.5, 3.5, 4.5],
});
const view = await table.view();
await table.remove(datetimes.slice(0, 2));
const result = await view.to_columns();
expect(await view.num_rows()).toEqual(2);
expect(result).toEqual({
x: [3, 4],
y: [1584230400000, 1586908800000],
z: [3.5, 4.5],
});
// expect(await table.size()).toEqual(2);
view.delete();
table.delete();
});
test("after an update, datetime pkey", async function () {
const datetimes = [
new Date(2020, 0, 15),
new Date(2020, 1, 15),
new Date(2020, 2, 15),
new Date(2020, 3, 15),
];
const table = await perspective.table(
{
x: "integer",
y: "datetime",
z: "float",
},
{ index: "y" },
);
await table.update({
x: [1, 2, 3, 4],
y: datetimes,
z: [1.5, 2.5, 3.5, 4.5],
});
const view = await table.view();
await table.remove(datetimes.slice(0, 2));
const result = await view.to_columns();
expect(await view.num_rows()).toEqual(2);
expect(result).toEqual({
x: [3, 4],
y: [1584230400000, 1586908800000],
z: [3.5, 4.5],
});
expect(await table.size()).toEqual(2);
view.delete();
table.delete();
});
test("after a regular data load, datetime pkey", async function () {
const datetimes = [
new Date(2020, 0, 15),
new Date(2020, 1, 15),
new Date(2020, 2, 15),
new Date(2020, 3, 15),
];
const table = await perspective.table(
{
x: [1, 2, 3, 4],
y: datetimes,
z: [1.5, 2.5, 3.5, 4.5],
},
{ index: "y" },
);
const view = await table.view();
await table.remove(datetimes.slice(0, 2));
const result = await view.to_columns();
expect(await view.num_rows()).toEqual(2);
expect(result).toEqual({
x: [3, 4],
y: [1584230400000, 1586908800000],
z: [3.5, 4.5],
});
expect(await table.size()).toEqual(2);
view.delete();
table.delete();
});
test("multiple single element removes", async function () {
const table = await perspective.table(meta, { index: "x" });
for (let i = 0; i < 100; i++) {
await table.update([{ x: i, y: "test", z: false }]);
}
for (let i = 1; i < 100; i++) {
await table.remove([i]);
}
const view = await table.view();
const result = await view.to_json();
expect(result).toEqual([{ x: 0, y: "test", z: false }]);
expect(result.length).toEqual(1);
expect(await table.size()).toEqual(1);
view.delete();
table.delete();
});
test("remove while pivoted, last and count", async function () {
const table = await perspective.table(
{
x: ["a"],
y: ["A"],
idx: [1],
},
{ index: "idx" },
);
const view = await table.view({
aggregates: {
x: "last",
y: "count",
},
group_by: ["y"],
});
expect(await view.to_columns()).toEqual({
__ROW_PATH__: [[], ["A"]],
x: ["a", "a"],
y: [1, 1],
idx: [1, 1],
});
await table.update({
x: ["b"],
y: ["B"],
idx: [2],
});
expect(await view.to_columns()).toEqual({
__ROW_PATH__: [[], ["A"], ["B"]],
x: ["b", "a", "b"],
y: [2, 1, 1],
idx: [3, 1, 2],
});
await table.remove([1]);
expect(await view.to_columns()).toEqual({
__ROW_PATH__: [[], ["B"]],
x: ["b", "b"],
y: [1, 1],
idx: [2, 2],
});
await table.remove([2]);
expect(await view.to_columns()).toEqual({
__ROW_PATH__: [[]],
x: [""],
y: [0],
idx: [0],
});
await view.delete();
await table.delete();
});
test("Removes out of order", async function () {
const table = await perspective.table(
{
x: "string",
y: "integer",
},
{ index: "x" },
);
await table.update({
x: ["b", "1", "a", "c"],
y: [3, 1, 2, 4],
});
const view = await table.view();
expect(await view.to_columns()).toEqual({
x: ["1", "a", "b", "c"],
y: [1, 2, 3, 4],
});
await table.remove(["a", "c", "1", "b"]);
// num_rows should always reflect latest - we did not call_process
// in num_rows and I think the viewer covered that up because
// it would immediately serialize and thus flush the queue.
expect(await view.num_rows()).toEqual(0);
expect(await view.to_json()).toEqual([]);
await view.delete();
await table.delete();
});
test("No-op on pkeys not in the set", async function () {
const table = await perspective.table(
{
x: "string",
y: "integer",
},
{ index: "x" },
);
await table.update({
x: ["b", "1", "a", "c"],
y: [3, 1, 2, 4],
});
const view = await table.view();
expect(await view.to_columns()).toEqual({
x: ["1", "a", "b", "c"],
y: [1, 2, 3, 4],
});
await table.remove(["z", "ff", "2312", "b"]);
expect(await view.to_columns()).toEqual({
x: ["1", "a", "c"],
y: [1, 2, 4],
});
await view.delete();
await table.delete();
});
test("conflation order is consistent", async function () {
const table = await perspective.table(
{
x: "string",
y: "integer",
},
{ index: "x" },
);
await table.update({
x: ["b", "1", "a", "c"],
y: [3, 1, 2, 4],
});
await table.remove(["1", "c"]);
// removes applied after update
const view = await table.view();
expect(await view.to_columns()).toEqual({
x: ["a", "b"],
y: [2, 3],
});
await table.update({
x: ["b"],
y: [103],
});
await table.remove(["b"]);
expect(await view.to_columns()).toEqual({
x: ["a"],
y: [2],
});
await table.update({
x: ["b"],
y: [100],
});
await table.remove(["a"]);
expect(await view.to_columns()).toEqual({
x: ["b"],
y: [100],
});
// remove applied after update
for (let i = 0; i < 100; i++) {
await table.update({
x: ["c", "a"],
y: [i + 1, i + 2],
});
await table.remove(["c", "a"]);
}
expect(await view.to_columns()).toEqual({
x: ["b"],
y: [100],
});
await view.delete();
await table.delete();
});
test("correct size", async function () {
const table = await perspective.table(
{
x: "integer",
y: "integer",
},
{ index: "x" },
);
for (let i = 0; i < 100; i++) {
await table.update([{ x: i, y: i }]);
}
expect(await table.size()).toEqual(100);
for (let i = 0; i < 100; i++) {
await table.remove([i]);
await table.update([{ x: i, y: i }]);
}
expect(await table.size()).toEqual(100);
for (let i = 0; i < 100; i++) {
await table.update([{ x: i, y: i }]);
}
expect(await table.size()).toEqual(100);
for (let i = 0; i < 100; i++) {
await table.remove([i]);
}
expect(await table.size()).toEqual(0);
await table.delete();
});
});
test.describe("Schema", function () {
test("updates with columns not in schema", async function () {
var table = await perspective.table({ x: "integer", y: "string" });
await table.update(data);
var view = await table.view();
let result = await view.to_json();
expect(result).toEqual([
{ x: 1, y: "a" },
{ x: 2, y: "b" },
{ x: 3, y: "c" },
{ x: 4, y: "d" },
]);
view.delete();
table.delete();
});
test("coerces to string", async function () {
var table = await perspective.table({
x: "string",
y: "string",
z: "string",
});
await table.update(data);
var view = await table.view();
let result = await view.to_json();
expect(result).toEqual([
{ x: "1", y: "a", z: "true" },
{ x: "2", y: "b", z: "false" },
{ x: "3", y: "c", z: "true" },
{ x: "4", y: "d", z: "false" },
]);
view.delete();
table.delete();
});
});
test.describe("Updates", function () {
test("Meta constructor then `update()`", async function () {
var table = await perspective.table(meta);
await table.update(data);
var view = await table.view();
let result = await view.to_json();
expect(result).toEqual(data);
view.delete();
table.delete();
});
test("Meta constructor then column oriented `update()`", async function () {
var table = await perspective.table(meta);
await table.update(col_data);
var view = await table.view();
let result = await view.to_json();
expect(result).toEqual(data);
view.delete();
table.delete();
});
test("Column oriented `update()` with columns in different order to schema", async function () {
var table = await perspective.table(meta);
var reordered_col_data = {
y: ["a", "b", "c", "d"],
z: [true, false, true, false],
x: [1, 2, 3, 4],
};
await table.update(reordered_col_data);
var view = await table.view();
let result = await view.to_json();
expect(result).toEqual(data);
view.delete();
table.delete();
});
test("Column data constructor then column oriented `update()`", async function () {
var colUpdate = {
x: [3, 4, 5],
y: ["h", "i", "j"],
z: [false, true, false],
};
var expected = [
{ x: 1, y: "a", z: true },
{ x: 2, y: "b", z: false },
{ x: 3, y: "h", z: false },
{ x: 4, y: "i", z: true },
{ x: 5, y: "j", z: false },
];
var table = await perspective.table(col_data, { index: "x" });
await table.update(colUpdate);
var view = await table.view();
let result = await view.to_json();
expect(result).toEqual(expected);
view.delete();
table.delete();
});
test("`update()` should increment `table.size()` without a view created", async function () {
const table = await perspective.table(data);
expect(await table.size()).toEqual(4);
await table.update(data);
expect(await table.size()).toEqual(8);
await table.update(data);
expect(await table.size()).toEqual(12);
table.delete();
});
test.skip("`update()` unbound to table", async function () {
var table = await perspective.table(meta);
var updater = await table.update;
console.log(updater);
updater(data);
let view = await table.view();
let result = await view.to_json();
expect(result).toEqual(data);
view.delete();
table.delete();
});
test("`update([])` does not error", async function () {
let table = await perspective.table(meta);
let view = await table.view();
await table.update([]);
let result = await view.to_json();
expect(result).toEqual([]);
view.delete();
table.delete();
});
test("Multiple `update()`s", async function () {
var table = await perspective.table(meta);
await table.update(data);
await table.update(data);
var view = await table.view();
let result = await view.to_json();
expect(result).toEqual(data.concat(data));
view.delete();
table.delete();
});
test("`update()` called after `view()`", async function () {
var table = await perspective.table(meta);
var view = await table.view();
await table.update(data);
let result = await view.to_json();
expect(result).toEqual(data);
view.delete();
table.delete();
});
});
test.describe("Arrow Updates", function () {
test("arrow contructor then arrow `update()`", async function () {
const arrow = arrows.test_arrow;
var table = await perspective.table(arrow.slice());
await table.update(arrow.slice());
var view = await table.view();
let result = await view.to_json();
expect(result).toEqual(arrow_result.concat(arrow_result));
view.delete();
table.delete();
});
test("non-arrow constructor then arrow `update()`", async function () {
let table = await perspective.table(arrow_result);
let view = await table.view();
let generated_arrow = await view.to_arrow();
await table.update(generated_arrow);
let result = await view.to_json();
expect(result).toEqual(arrow_result.concat(arrow_result));
view.delete();
table.delete();
});
test("arrow dict contructor then arrow dict `update()`", async function () {
var table = await perspective.table(arrows.dict_arrow.slice());
await table.update(arrows.dict_update_arrow.slice());
var view = await table.view();
let result = await view.to_columns();
expect(result).toEqual({
a: [
"abc",
"def",
"def",
null,
"abc",
null,
"update1",
"update2",
],
b: [
"klm",
"hij",
null,
"hij",
"klm",
"update3",
null,
"update4",
],
});
view.delete();
table.delete();
});
test("non-arrow constructor then arrow dict `update()`", async function () {
let table = await perspective.table({
a: ["a", "b", "c"],
b: ["d", "e", "f"],
});
let view = await table.view();
await table.update(arrows.dict_update_arrow.slice());
let result = await view.to_columns();
expect(result).toEqual({
a: ["a", "b", "c", null, "update1", "update2"],
b: ["d", "e", "f", "update3", null, "update4"],
});
view.delete();
table.delete();
});
test("arrow dict contructor then arrow dict `update()` subset of columns", async function () {
var table = await perspective.table(arrows.dict_arrow.slice());
await table.update(arrows.dict_update_arrow.slice());
var view = await table.view({
columns: ["a"],
});
let result = await view.to_columns();
expect(result).toEqual({
a: [
"abc",
"def",
"def",
null,
"abc",
null,
"update1",
"update2",
],
});
view.delete();
table.delete();
});
test("non-arrow constructor then arrow dict `update()`, subset of columns", async function () {
let table = await perspective.table({
a: ["a", "b", "c"],
b: ["d", "e", "f"],
});
let view = await table.view({
columns: ["a"],
});
await table.update(arrows.dict_update_arrow.slice());
let result = await view.to_columns();
expect(result).toEqual({
a: ["a", "b", "c", null, "update1", "update2"],
});
view.delete();
table.delete();
});
test("arrow partial `update()` a single column", async function () {
let table = await perspective.table(arrows.test_arrow.slice(), {
index: "i64",
});
await table.update(arrows.partial_arrow.slice());
const view = await table.view();
const result = await view.to_json();
expect(result).toEqual(arrow_partial_result);
view.delete();
table.delete();
});
test("arrow partial `update()` a single column with missing rows", async function () {
let table = await perspective.table(arrows.test_arrow.slice(), {
index: "i64",
});
await table.update(arrows.partial_missing_rows_arrow.slice());
const view = await table.view();
const result = await view.to_json();
expect(result).toEqual(arrow_partial_missing_result);
view.delete();
table.delete();
});
test("schema constructor, then arrow `update()`", async function () {
const table = await perspective.table({
a: "integer",
b: "float",
c: "string",
});
await table.update(arrows.int_float_str_arrow.slice());
const view = await table.view();
const size = await table.size();
expect(size).toEqual(4);
const result = await view.to_columns();
expect(result).toEqual({
a: [1, 2, 3, 4],
b: [1.5, 2.5, 3.5, 4.5],
c: ["a", "b", "c", "d"],
});
view.delete();
table.delete();
});
test("schema constructor, then arrow dictionary `update()`", async function () {
const table = await perspective.table({
a: "string",
b: "string",
});
await table.update(arrows.dict_arrow.slice());
const view = await table.view();
const size = await table.size();
expect(size).toEqual(5);
const result = await view.to_columns();
expect(result).toEqual({
a: ["abc", "def", "def", null, "abc"],
b: ["klm", "hij", null, "hij", "klm"],
});
view.delete();
table.delete();
});
test.skip("schema constructor, then indexed arrow dictionary `update()`", async function () {
const table = await perspective.table(
{
a: "string",
b: "string",
},
{ index: "a" },
);
await table.update(arrows.dict_arrow.slice());
const view = await table.view();
const size = await table.size();
expect(size).toEqual(3);
const result = await view.to_columns();
expect(result).toEqual({
a: [null, "abc", "def"],
b: ["hij", "klm", "hij"],
});
view.delete();
table.delete();
});
test.skip("schema constructor, then indexed arrow dictionary `update()` with more columns than in schema", async function () {
const table = await perspective.table(
{
a: "string",
},
{ index: "a" },
);
await table.update(arrows.dict_arrow.slice());
const view = await table.view();
const size = await table.size();
console.log(await view.to_columns());
expect(size).toEqual(3);
const result = await view.to_columns();
expect(result).toEqual({
a: [null, "abc", "def"],
});
view.delete();
table.delete();
});
test.skip("schema constructor, then indexed arrow dictionary `update()` with less columns than in schema", async function () {
const table = await perspective.table(
{
a: "string",
b: "string",
x: "integer",
},
{ index: "a" },
);
await table.update(arrows.dict_arrow.slice());
const view = await table.view();
const size = await table.size();
console.log(await view.to_columns());
expect(size).toEqual(3);
const result = await view.to_columns();
expect(result).toEqual({
a: [null, "abc", "def"],
b: ["hij", "klm", "hij"],
x: [null, null, null],
});
view.delete();
table.delete();
});
test("schema constructor, then indexed arrow `update()`", async function () {
const table = await perspective.table(
{
a: "integer",
b: "float",
c: "string",
},
{ index: "a" },
);
const view = await table.view();
await table.update(arrows.int_float_str_arrow.slice());
await table.update(arrows.int_float_str_update_arrow.slice());
expect(await table.size()).toBe(4);
const result = await view.to_columns();
expect(result).toEqual({
a: [1, 2, 3, 4],
b: [100.5, 2.5, 3.5, 400.5],
c: ["x", "b", "c", "y"],
});
view.delete();
table.delete();
});
test("schema constructor, then arrow update() with more columns than in the Table", async function () {
const table = await perspective.table({
a: "integer",
});
const view = await table.view();
await table.update(arrows.int_float_str_arrow.slice());
expect(await table.size()).toBe(4);
const result = await view.to_columns();
expect(result).toEqual({
a: [1, 2, 3, 4],
});
view.delete();
table.delete();
});
test.skip("Table.get_num_views", async function () {
const table = await perspective.table({
a: "integer",
});
const view1 = await table.view();
expect(await table.get_num_views()).toBe(1);
const view2 = await table.view();
const view3 = await table.view();
const view4 = await table.view();
const view5 = await table.view();
expect(await table.get_num_views()).toBe(5);
await view1.delete();
await view2.delete();
await view3.delete();
await view4.delete();
await view5.delete();
expect(await table.get_num_views()).toBe(0);
table.delete();
});
test("schema constructor indexed, then arrow update() with more columns than in the Table", async function () {
const table = await perspective.table(
{
a: "integer",
b: "float",
},
{
index: "a",
},
);
const view = await table.view();
await table.update(arrows.int_float_str_arrow.slice());
await table.update(arrows.int_float_str_update_arrow.slice());
expect(await table.size()).toBe(4);
const result = await view.to_columns();
expect(result).toEqual({
a: [1, 2, 3, 4],
b: [100.5, 2.5, 3.5, 400.5],
});
view.delete();
table.delete();
});
test("schema constructor, then arrow update() with less columns than in the Table", async function () {
const table = await perspective.table({
a: "integer",
x: "float",
y: "string",
});
const view = await table.view();
await table.update(arrows.int_float_str_arrow.slice());
expect(await table.size()).toBe(4);
const result = await view.to_columns();
expect(result).toEqual({
a: [1, 2, 3, 4],
x: [null, null, null, null],
y: [null, null, null, null],
});
view.delete();
table.delete();
});
test("schema constructor indexed, then arrow update() with less columns than in the Table", async function () {
const table = await perspective.table(
{
a: "integer",
x: "float",
y: "string",
},
{
index: "a",
},
);
const view = await table.view();
await table.update(arrows.int_float_str_arrow.slice());
await table.update(arrows.int_float_str_update_arrow.slice());
expect(await table.size()).toBe(4);
const result = await view.to_columns();
expect(result).toEqual({
a: [1, 2, 3, 4],
x: [null, null, null, null],
y: [null, null, null, null],
});
view.delete();
table.delete();
});
test("schema constructor, then arrow stream and arrow file `update()`", async function () {
const table = await perspective.table({
a: "integer",
b: "float",
c: "string",
});
await table.update(arrows.int_float_str_arrow.slice());
await table.update(arrows.int_float_str_file_arrow.slice());
const view = await table.view();
const result = await view.to_columns();
expect(result).toEqual({
a: [1, 2, 3, 4, 1, 2, 3, 4],
b: [1.5, 2.5, 3.5, 4.5, 1.5, 2.5, 3.5, 4.5],
c: ["a", "b", "c", "d", "a", "b", "c", "d"],
});
view.delete();
table.delete();
});
});
test.describe("Notifications", function () {
it_old_behavior("`on_update()`", async function (done) {
var table = await perspective.table(meta);
var view = await table.view();
view.on_update(
async function (updated) {
await match_delta(perspective, updated.delta, data);
view.delete();
table.delete();
done();
},
{ mode: "row" },
);
await table.update(data);
});
it_old_behavior(
"`on_update` before and after `update()`",
async function (done) {
var table = await perspective.table(meta);
var view = await table.view();
await table.update(data);
var ran = false;
view.on_update(
async function (updated) {
if (!ran) {
await match_delta(perspective, updated.delta, data);
ran = true;
view.delete();
table.delete();
done();
}
},
{ mode: "row" },
);
await table.update(data);
},
);
it_old_behavior(
"`on_update(await table.update) !`",
async function (done) {
var table1 = await perspective.table(meta);
var table2 = await perspective.table(meta);
var view1 = await table1.view();
var view2 = await table2.view();
view1.on_update(
async function (updated) {
await table2.update(updated.delta);
let result = await view2.to_json();
expect(result).toEqual(data);
view1.delete();
view2.delete();
table1.delete();
table2.delete();
done();
},
{ mode: "row" },
);
await table1.update(data);
},
);
it_old_behavior(
"`on_update( table.update)` before and after `update()`",
async function (done) {
var table1 = await perspective.table(meta);
var table2 = await perspective.table(meta);
var view1 = await table1.view();
var view2 = await table2.view();
await table1.update(data);
await table2.update(data);
view1.on_update(
async function (updated) {
await table2.update(updated.delta);
let result = await view2.to_json();
expect(result).toEqual(data.concat(data));
view1.delete();
view2.delete();
table1.delete();
table2.delete();
done();
},
{ mode: "row" },
);
await table1.update(data);
},
);
it_old_behavior(
"OG - properly removes a failed update callback on a table",
async function (done) {
const table = await perspective.table({ x: "integer" });
const view = await table.view();
let size = await table.size();
let counter = 0;
// when a callback throws, it should delete that callback
view.on_update(() => {
counter++;
throw new Error("something went wrong!");
});
view.on_update(async () => {
// failed callback gets removed; non-failing callback gets called
let sz = await table.size();
expect(counter).toEqual(1);
expect(sz).toEqual(size++);
});
await table.update([{ x: 1 }]);
await table.update([{ x: 2 }]);
await table.update([{ x: 3 }]);
const view2 = await table.view(); // create a new view to make sure we process every update transacation.
const final_size = await table.size();
expect(final_size).toEqual(3);
view2.delete();
view.delete();
table.delete();
done();
},
{ skip: true },
);
it_old_behavior(
"properly removes a failed update callback on a table",
async function (done) {
const table = await perspective.table({ x: "integer" });
const view = await table.view();
let size = await table.size();
view.on_update(async () => {
// failed callback gets removed; non-failing callback gets called
let sz = await table.size();
expect(sz).toEqual(++size);
});
await table.update([{ x: 1 }]);
await table.size();
await table.update([{ x: 2 }]);
await table.size();
await table.update([{ x: 3 }]);
await table.size();
const final_size = await table.size();
expect(final_size).toEqual(3);
// view2.delete();
view.delete();
table.delete();
done();
},
);
it_old_behavior(
"`on_update()` that calls operations on the table should not recurse",
async function (done) {
var table = await perspective.table(meta);
var view = await table.view();
view.on_update(async function (updated) {
expect(updated.port_id).toEqual(0);
const json = await view.to_json();
// Not checking for correctness, literally just to assert
// that the `process()` call triggered by `to_json` will not
// infinitely recurse.
expect(json).toEqual(await view.to_json());
view.delete();
table.delete();
done();
});
await table.update(data);
},
);
it_old_behavior(
"`on_update()` should be triggered in sequence",
async function (done) {
var table = await perspective.table(meta);
var view = await table.view();
let order = [];
const finish = function () {
if (order.length === 3) {
expect(order).toEqual([0, 1, 2]);
view.delete();
table.delete();
done();
}
};
for (let i = 0; i < 3; i++) {
view.on_update(() => {
order.push(i);
finish();
});
}
await table.update(data);
},
);
it_old_behavior(
"`on_update()` should be triggered in sequence across multiple views",
async function (done) {
var table = await perspective.table(meta);
const views = [
await table.view(),
await table.view(),
await table.view(),
];
let order = [];
const finish = function () {
if (order.length === 3) {
expect(order).toEqual([0, 1, 2]);
for (const view of views) {
view.delete();
}
table.delete();
done();
}
};
for (let i = 0; i < views.length; i++) {
views[i].on_update(() => {
order.push(i);
finish();
});
}
await table.update(data);
},
);
});
test.describe("Limit", function () {
test("{limit: 2} with table of size 4", async function () {
var table = await perspective.table(data, { limit: 2 });
var view = await table.view();
let result = await view.to_json();
expect(result).toEqual(data.slice(2));
view.delete();
table.delete();
});
test("{limit: 5} with 2 updates of size 4", async function () {
var table = await perspective.table(data, { limit: 5 });
await table.update(data);
var view = await table.view();
let result = await view.to_json();
let expected = data.concat(data).slice(-5);
expect(result).toEqual(expected);
view.delete();
table.delete();
});
test("{limit: 1} with arrow update", async function () {
const arrow = arrows.test_arrow.slice();
var table = await perspective.table(arrow, { limit: 1 });
await table.update(arrow.slice());
var view = await table.view();
let result = await view.to_json();
expect(result).toEqual([arrow_result[arrow_result.length - 1]]);
view.delete();
table.delete();
});
});
test.describe("Indexed", function () {
test("{index: 'x'} (int)", async function () {
var table = await perspective.table(data, { index: "x" });
var view = await table.view();
await table.update(data);
let result = await view.to_json();
expect(result).toEqual(data);
view.delete();
table.delete();
});
test("{index: 'y'} (string)", async function () {
var table = await perspective.table(data, { index: "y" });
var view = await table.view();
await table.update(data);
let result = await view.to_json();
expect(result).toEqual(data);
view.delete();
table.delete();
});
test("{index: 'x'} (int) with null and 0", async function () {
const data = {
x: [0, 1, null, 2, 3],
y: ["a", "b", "c", "d", "e"],
};
const table = await perspective.table(data, { index: "x" });
const view = await table.view();
const result = await view.to_columns();
expect(result).toEqual({
x: [null, 0, 1, 2, 3], // null before 0
y: ["c", "a", "b", "d", "e"],
});
view.delete();
table.delete();
});
test("{index: 'y'} (str) with null and empty string", async function () {
const data = {
x: [0, 1, 2, 3, 4],
y: ["", "a", "b", "c", null],
};
const table = await perspective.table(data, { index: "y" });
const view = await table.view();
const result = await view.to_columns();
expect(result).toEqual({
x: [4, 0, 1, 2, 3],
y: [null, "", "a", "b", "c"], // null before empties
});
view.delete();
table.delete();
});
test("{index: 'x'} (int) with null and 0, update", async function () {
const data = {
x: [0, 1, null, 2, 3],
y: ["a", "b", "c", "d", "e"],
};
const table = await perspective.table(data, { index: "x" });
await table.update({
x: [null, 0],
y: ["x", "y"],
});
const view = await table.view();
const result = await view.to_columns();
expect(result).toEqual({
x: [null, 0, 1, 2, 3], // null before 0
y: ["x", "y", "b", "d", "e"],
});
view.delete();
table.delete();
});
test("{index: 'y'} (str) with null and empty string, update", async function () {
const data = {
x: [0, 1, 2, 3, 4],
y: ["", "a", "b", "c", null],
};
const table = await perspective.table(data, { index: "y" });
await table.update({
x: [5, 6],
y: ["", null],
});
const view = await table.view();
const result = await view.to_columns();
expect(result).toEqual({
x: [6, 5, 1, 2, 3],
y: [null, "", "a", "b", "c"], // null before empties
});
view.delete();
table.delete();
});
test("{index: 'x'} (date) with null", async function () {
const data = {
x: ["10/30/2016", "11/1/2016", null, "1/1/2000"],
y: [1, 2, 3, 4],
};
const table = await perspective.table(
{
x: "date",
y: "integer",
},
{ index: "x" },
);
await table.update(data);
const view = await table.view();
const result = await view.to_columns();
expect(result).toEqual({
x: [
null,
new Date("2000-1-1").getTime(),
new Date("2016-10-30").getTime(),
new Date("2016-11-1").getTime(),
],
y: [3, 4, 1, 2],
});
view.delete();
table.delete();
});
test("{index: 'y'} (datetime) with datetime and null", async function () {
const data = {
x: [
"2016-11-01 11:00:00",
"2016-11-01 11:10:00",
null,
"2016-11-01 11:20:00",
],
y: [1, 2, 3, 4],
};
const table = await perspective.table(
{
x: "datetime",
y: "integer",
},
{ index: "x" },
);
await table.update(data);
const view = await table.view();
const result = await view.to_columns();
expect(result).toEqual({
x: [
null,
new Date("2016-11-1 11:00:00").getTime(),
new Date("2016-11-1 11:10:00").getTime(),
new Date("2016-11-1 11:20:00").getTime(),
],
y: [3, 1, 2, 4],
});
view.delete();
table.delete();
});
test("Arrow with {index: 'i64'} (int)", async function () {
var table = await perspective.table(arrows.test_arrow.slice(), {
index: "i64",
});
var view = await table.view();
let result = await view.to_json();
expect(result).toEqual(arrow_result);
view.delete();
table.delete();
});
test("Arrow with {index: 'char'} (char)", async function () {
var table = await perspective.table(arrows.test_arrow.slice(), {
index: "char",
});
var view = await table.view();
let result = await view.to_json();
expect(result).toEqual(arrow_indexed_result);
view.delete();
table.delete();
});
test("Arrow with {index: 'dict'} (dict)", async function () {
var table = await perspective.table(arrows.test_arrow.slice(), {
index: "dict",
});
var view = await table.view();
let result = await view.to_json();
expect(result).toEqual(arrow_indexed_result);
view.delete();
table.delete();
});
test("multiple updates on int {index: 'x'}", async function () {
var table = await perspective.table(data, { index: "x" });
var view = await table.view();
await table.update(data);
await table.update(data);
await table.update(data);
let result = await view.to_json();
expect(result).toEqual(data);
view.delete();
table.delete();
});
test("multiple updates on str {index: 'y'}", async function () {
var table = await perspective.table(data, { index: "y" });
var view = await table.view();
await table.update(data);
await table.update(data);
await table.update(data);
let result = await view.to_json();
expect(result).toEqual(data);
view.delete();
table.delete();
});
test("multiple updates on str {index: 'y'} with new, old, null pkey", async function () {
var table = await perspective.table(data, { index: "y" });
var view = await table.view();
await table.update([{ x: 1, y: "a", z: true }]);
await table.update([{ x: 100, y: null, z: true }]);
await table.update([{ x: 123, y: "abc", z: true }]);
let result = await view.to_json();
expect(result).toEqual([
{ x: 100, y: null, z: true },
{ x: 1, y: "a", z: true },
{ x: 123, y: "abc", z: true },
{ x: 2, y: "b", z: false },
{ x: 3, y: "c", z: true },
{ x: 4, y: "d", z: false },
]);
view.delete();
table.delete();
});
test.skip("multiple updates on str {index: 'y'} with new, old, null in dataset", async function () {
// FIXME: this is an engine bug
const table = await perspective.table(
{
x: [1, 2, 3],
y: ["a", null, "c"],
},
{ index: "y" },
);
const view = await table.view();
await table.update([{ x: 12345, y: "a" }]);
await table.update([{ x: 100, y: null }]);
await table.update([{ x: 123, y: "abc" }]);
const result = await view.to_json();
expect(result).toEqual([
{ x: 100, y: null },
{ x: 12345, y: "a" },
{ x: 123, y: "abc" },
{ x: 3, y: "c" },
]);
await view.delete();
await table.delete();
});
test("{index: 'x'} with overlap", async function () {
var table = await perspective.table(data, { index: "x" });
var view = await table.view();
await table.update(data);
await table.update(data_2);
let result = await view.to_json();
expect(result).toEqual(data.slice(0, 2).concat(data_2));
view.delete();
table.delete();
});
it_old_behavior("update and index (int)", async function (done) {
var table = await perspective.table(meta, { index: "x" });
var view = await table.view();
await table.update(data);
view.on_update(
async function (updated) {
await match_delta(perspective, updated.delta, data_2);
let json = await view.to_json();
expect(json).toEqual(data.slice(0, 2).concat(data_2));
view.delete();
table.delete();
done();
},
{ mode: "row" },
);
await table.update(data_2);
});
it_old_behavior("update and index (string)", async function (done) {
var table = await perspective.table(meta, { index: "y" });
var view = await table.view();
await table.update(data);
view.on_update(
async function (updated) {
await match_delta(perspective, updated.delta, data_2);
let json = await view.to_json();
expect(json).toEqual(data.slice(0, 2).concat(data_2));
view.delete();
table.delete();
done();
},
{ mode: "row" },
);
await table.update(data_2);
});
test("update with depth expansion", async function () {
var table = await perspective.table(meta, { index: "y" });
var view = await table.view({
group_by: ["z", "y"],
columns: [],
});
await table.update(data);
let result = await view.to_json();
let expected = [
{ __ROW_PATH__: [] },
{ __ROW_PATH__: [false] },
{ __ROW_PATH__: [false, "b"] },
{ __ROW_PATH__: [false, "d"] },
{ __ROW_PATH__: [true] },
{ __ROW_PATH__: [true, "a"] },
{ __ROW_PATH__: [true, "c"] },
];
expect(result).toEqual(expected);
view.delete();
table.delete();
});
it_old_behavior("partial update", async function (done) {
var partial = [
{ x: 5, y: "a" },
{ y: "b", z: true },
];
var expected = [
{ x: 5, y: "a", z: true },
{ x: 2, y: "b", z: true },
{ x: 3, y: "c", z: true },
{ x: 4, y: "d", z: false },
];
var table = await perspective.table(meta, { index: "y" });
var view = await table.view();
await table.update(data);
view.on_update(
async function (updated) {
await match_delta(
perspective,
updated.delta,
expected.slice(0, 2),
);
let json = await view.to_json();
expect(json).toEqual(expected);
view.delete();
table.delete();
done();
},
{ mode: "row" },
);
await table.update(partial);
});
it_old_behavior(
"OG - partial column oriented update",
async function (done) {
var partial = {
x: [5, undefined],
y: ["a", "b"],
z: [undefined, true],
};
var expected = [
{ x: 5, y: "a", z: true },
{ x: 2, y: "b", z: true },
{ x: 3, y: "c", z: true },
{ x: 4, y: "d", z: false },
];
var table = await perspective.table(meta, { index: "y" });
var view = await table.view();
await table.update(col_data);
view.on_update(
async function (updated) {
await match_delta(
perspective,
updated.delta,
expected.slice(0, 2),
);
let json = await view.to_json();
expect(json).toEqual(expected);
view.delete();
table.delete();
done();
},
{ mode: "row" },
);
await table.update(partial);
},
{ skip: true },
);
it_old_behavior(
"partial column oriented update",
async function (done) {
var batches = [
{
x: [5],
y: ["a"],
},
{
y: ["b"],
z: [true],
},
];
var expected = [
{ x: 5, y: "a", z: true },
{ x: 2, y: "b", z: true },
{ x: 3, y: "c", z: true },
{ x: 4, y: "d", z: false },
];
var table = await perspective.table(meta, { index: "y" });
var view = await table.view();
await table.update(col_data);
view.on_update(
async function (updated) {
await match_delta(
perspective,
updated.delta,
expected.slice(0, 2),
);
let json = await view.to_json();
expect(json).toEqual(expected);
view.delete();
table.delete();
done();
},
{ mode: "row" },
);
for (const batch of batches) {
table.update(batch);
}
},
);
it_old_behavior(
"Partial column oriented update with new pkey",
async function (done) {
const data = {
x: [0, 1, null, 2, 3],
y: ["a", "b", "c", "d", "e"],
};
const table = await perspective.table(data, { index: "x" });
const view = await table.view();
const result = await view.to_columns();
expect(result).toEqual({
x: [null, 0, 1, 2, 3], // null before 0
y: ["c", "a", "b", "d", "e"],
});
const expected = {
x: [null, 0, 1, 2, 3, 4],
y: ["c", "a", "b", "d", "e", "f"],
};
view.on_update(
async function (updated) {
await match_delta(perspective, updated.delta, [
{
x: 4,
y: "f",
},
]);
const result2 = await view.to_columns();
expect(result2).toEqual(expected);
view.delete();
table.delete();
done();
},
{ mode: "row" },
);
await table.update({
x: [4],
y: ["f"],
});
},
);
it_old_behavior(
"Partial column oriented update with new pkey, missing columns",
async function (done) {
const data = {
x: [0, 1, null, 2, 3],
y: ["a", "b", "c", "d", "e"],
z: [true, false, true, false, true],
};
const table = await perspective.table(data, { index: "x" });
const view = await table.view();
const result = await view.to_columns();
expect(result).toEqual({
x: [null, 0, 1, 2, 3], // null before 0
y: ["c", "a", "b", "d", "e"],
z: [true, true, false, false, true],
});
const expected = {
x: [null, 0, 1, 2, 3, 4],
y: ["c", "a", "b", "d", "e", "f"],
z: [true, true, false, false, true, null],
};
view.on_update(
async function (updated) {
await match_delta(perspective, updated.delta, [
{
x: 4,
y: "f",
z: null,
},
]);
const result2 = await view.to_columns();
expect(result2).toEqual(expected);
view.delete();
table.delete();
done();
},
{ mode: "row" },
);
await table.update({
x: [4],
y: ["f"],
});
},
);
it_old_behavior(
"partial column oriented update with entire columns missing",
async function (done) {
var partial = {
y: ["a", "b"],
z: [false, true],
};
var expected = [
{ x: 1, y: "a", z: false },
{ x: 2, y: "b", z: true },
{ x: 3, y: "c", z: true },
{ x: 4, y: "d", z: false },
];
var table = await perspective.table(meta, { index: "y" });
var view = await table.view();
await table.update(col_data);
view.on_update(
async function (updated) {
await match_delta(
perspective,
updated.delta,
expected.slice(0, 2),
);
let json = await view.to_json();
expect(json).toEqual(expected);
view.delete();
table.delete();
done();
},
{ mode: "row" },
);
await table.update(partial);
},
);
});
test.describe("null handling", function () {
test("recalculates sum aggregates when a null unsets a value", async function () {
var table = await perspective.table(
[
{ x: 1, y: 1 },
{ x: 2, y: 1 },
],
{ index: "x" },
);
await table.update([{ x: 2, y: null }]);
var view = await table.view({
group_by: ["x"],
columns: ["y"],
});
let json = await view.to_json();
expect(json).toEqual([
{ __ROW_PATH__: [], y: 1 },
{ __ROW_PATH__: [1], y: 1 },
{ __ROW_PATH__: [2], y: 0 },
]);
view.delete();
table.delete();
});
test("can be removed entirely", async function () {
var table = await perspective.table([{ x: 1, y: 1 }], {
index: "x",
});
await table.update([{ x: 1, y: null }]);
await table.update([{ x: 1, y: 1 }]);
var view = await table.view();
let json = await view.to_json();
expect(json).toEqual([{ x: 1, y: 1 }]);
view.delete();
table.delete();
});
test("partial update with null unsets value", async function () {
var partial = [{ x: null, y: "a", z: false }];
var expected = [
{ x: null, y: "a", z: false },
{ x: 2, y: "b", z: false },
{ x: 3, y: "c", z: true },
{ x: 4, y: "d", z: false },
];
var table = await perspective.table(meta, { index: "y" });
var view = await table.view();
await table.update(data);
await table.update(partial);
const json = await view.to_json();
expect(json).toEqual(expected);
view.delete();
table.delete();
});
test("update by adding rows (new pkeys) with partials/nulls", async function () {
var update = [{ x: null, y: "e", z: null }];
var expected = [
{ x: 1, y: "a", z: true },
{ x: 2, y: "b", z: false },
{ x: 3, y: "c", z: true },
{ x: 4, y: "d", z: false },
{ x: null, y: "e", z: null },
];
var table = await perspective.table(meta, { index: "y" });
var view = await table.view();
await table.update(data);
await table.update(update);
const json = await view.to_json();
expect(json).toEqual(expected);
view.delete();
table.delete();
});
test("partial column oriented update with null unsets value", async function () {
var partial = {
x: [null],
y: ["a"],
};
var expected = [
{ x: null, y: "a", z: true },
{ x: 2, y: "b", z: false },
{ x: 3, y: "c", z: true },
{ x: 4, y: "d", z: false },
];
var table = await perspective.table(meta, { index: "y" });
var view = await table.view();
await table.update(col_data);
await table.update(partial);
const json = await view.to_json();
expect(json).toEqual(expected);
view.delete();
table.delete();
});
});
test.describe("Viewport", function () {
test.skip("`height`", async function () {
var table = await perspective.table(data);
var view = await table.view({
viewport: {
height: 2,
},
});
let result = await view.to_json();
expect(result).toEqual(data.slice(0, 2));
view.delete();
table.delete();
});
test.skip("`top`", async function () {
var table = await perspective.table(data);
var view = await table.view({
viewport: {
top: 2,
},
});
let result = await view.to_json();
expect(result).toEqual(data.slice(2));
view.delete();
table.delete();
});
test.skip("`width`", async function () {
var table = await perspective.table(data);
var view = await table.view({
viewport: {
width: 2,
},
});
var result2 = _.map(data, (x) => _.pick(x, "x", "y"));
let result = await view.to_json();
expect(result).toEqual(result2);
view.delete();
table.delete();
});
test.skip("`left`", async function () {
var table = await perspective.table(data);
var view = await table.view({
viewport: {
left: 1,
},
});
var result = _.map(data, function (x) {
return _.pick(x, "y", "z");
});
let result2 = await view.to_json();
expect(result).toEqual(result2);
view.delete();
table.delete();
});
test.skip("All", async function () {
var table = await perspective.table(data);
var view = await table.view({
viewport: {
top: 1,
left: 1,
width: 1,
height: 2,
},
});
var result = _.map(data, function (x) {
return _.pick(x, "y");
});
let result2 = await view.to_json();
expect(result2).toEqual(result.slice(1, 3));
view.delete();
table.delete();
});
});
test.describe("implicit index", function () {
test("should apply single partial update on unindexed table using row id from '__INDEX__'", async function () {
let table = await perspective.table(data);
await table.update([
{
__INDEX__: 2,
y: "new_string",
},
]);
let view = await table.view();
let result = await view.to_json();
let expected = JSON.parse(JSON.stringify(data));
expected[2]["y"] = "new_string";
expect(result).toEqual(expected);
view.delete();
table.delete();
});
test("should partial update on unindexed table, column dataset", async function () {
let table = await perspective.table(data);
await table.update({
__INDEX__: [2],
y: ["new_string"],
});
let view = await table.view();
let result = await view.to_json();
// does not unset any values
expect(result).toEqual([
{ x: 1, y: "a", z: true },
{ x: 2, y: "b", z: false },
{ x: 3, y: "new_string", z: true },
{ x: 4, y: "d", z: false },
]);
view.delete();
table.delete();
});
test("should partial update and unset on unindexed table", async function () {
let table = await perspective.table(data);
await table.update([
{
__INDEX__: 2,
y: "new_string",
z: null,
},
]);
let view = await table.view();
let result = await view.to_json();
// does not unset any values
expect(result).toEqual([
{ x: 1, y: "a", z: true },
{ x: 2, y: "b", z: false },
{ x: 3, y: "new_string", z: null },
{ x: 4, y: "d", z: false },
]);
view.delete();
table.delete();
});
// ORIGINAL VERSION OF TEST BELOW
test.skip("OG - should partial update and unset on unindexed table, column dataset", async function () {
let table = await perspective.table(data);
await table.update({
__INDEX__: [0, 2],
y: [undefined, "new_string"],
z: [null, undefined],
});
let view = await table.view();
let result = await view.to_json();
// does not unset any values
expect(result).toEqual([
{ x: 1, y: "a", z: null },
{ x: 2, y: "b", z: false },
{ x: 3, y: "new_string", z: true },
{ x: 4, y: "d", z: false },
]);
view.delete();
table.delete();
});
// Split the updates to avoid handling `undefined` values.
test("should partial update and unset on unindexed table, column dataset", async function () {
let table = await perspective.table(data);
await table.update({
__INDEX__: [0],
z: [null],
});
await table.update({
__INDEX__: [2],
y: ["new_string"],
});
let view = await table.view();
let result = await view.to_json();
// does not unset any values
expect(result).toEqual([
{ x: 1, y: "a", z: null },
{ x: 2, y: "b", z: false },
{ x: 3, y: "new_string", z: true },
{ x: 4, y: "d", z: false },
]);
view.delete();
table.delete();
});
test("should apply single multi-column partial update on unindexed table using row id from '__INDEX__'", async function () {
let table = await perspective.table(data);
await table.update([
{
__INDEX__: 2,
y: "new_string",
x: 100,
},
]);
let view = await table.view();
let result = await view.to_json();
let expected = JSON.parse(JSON.stringify(data));
expected[2]["x"] = 100;
expected[2]["y"] = "new_string";
expect(result).toEqual(expected);
view.delete();
table.delete();
});
test("should apply updates using '__INDEX__' on a table with explicit index set", async function () {
let table = await perspective.table(data, { index: "x" });
await table.update([
{
__INDEX__: 2,
y: "new_string",
},
]);
let view = await table.view();
let result = await view.to_json();
let expected = JSON.parse(JSON.stringify(data));
expected[1]["y"] = "new_string";
expect(result).toEqual(expected);
view.delete();
table.delete();
});
test("should apply multiple sequential updates using '__INDEX__' on a table with explicit index set", async function () {
let table = await perspective.table(data, { index: "x" });
await table.update([
{
__INDEX__: 2,
y: "new_string",
},
{
__INDEX__: 3,
y: "new_string",
},
]);
let view = await table.view();
let result = await view.to_json();
let expected = JSON.parse(JSON.stringify(data));
expected[1]["y"] = "new_string";
expected[2]["y"] = "new_string";
expect(result).toEqual(expected);
view.delete();
table.delete();
});
test("should apply mulitple nonsequential updates using '__INDEX__' on a table with explicit index set", async function () {
let table = await perspective.table(data, { index: "x" });
await table.update([
{
__INDEX__: 2,
y: "new_string",
},
{
__INDEX__: 4,
y: "new_string",
},
]);
let view = await table.view();
let result = await view.to_json();
let expected = JSON.parse(JSON.stringify(data));
expected[1]["y"] = "new_string";
expected[3]["y"] = "new_string";
expect(result).toEqual(expected);
view.delete();
table.delete();
});
test("should apply multiple sequential partial updates on unindexed table using '__INDEX__'", async function () {
let table = await perspective.table(data);
await table.update([
{
__INDEX__: 0,
y: "new_string1",
},
{
__INDEX__: 1,
y: "new_string2",
},
{
__INDEX__: 2,
y: "new_string3",
},
]);
let view = await table.view();
let result = await view.to_json();
let expected = JSON.parse(JSON.stringify(data));
expected[0]["y"] = "new_string1";
expected[1]["y"] = "new_string2";
expected[2]["y"] = "new_string3";
expect(result).toEqual(expected);
view.delete();
table.delete();
});
test("should correctly apply multiple out-of-sequence partial updates on unindexed table using '__INDEX__'", async function () {
let table = await perspective.table(data);
await table.update([
{
__INDEX__: 0,
y: "new_string1",
},
{
__INDEX__: 2,
y: "new_string3",
},
{
__INDEX__: 3,
y: "new_string4",
},
{
__INDEX__: 1,
y: "new_string2",
},
]);
let view = await table.view();
let result = await view.to_json();
let expected = JSON.parse(JSON.stringify(data));
expected[0]["y"] = "new_string1";
expected[1]["y"] = "new_string2";
expected[2]["y"] = "new_string3";
expected[3]["y"] = "new_string4";
expect(result).toEqual(expected);
view.delete();
table.delete();
});
test("should stack multiple partial updates on unindexed table using the same '__INDEX__'", async function () {
let table = await perspective.table(data);
await table.update([
{
__INDEX__: 0,
y: "new_string1",
},
{
__INDEX__: 0,
y: "new_string2",
},
{
__INDEX__: 0,
y: "new_string3",
},
]);
let view = await table.view();
let result = await view.to_json();
let expected = JSON.parse(JSON.stringify(data));
expected[0]["y"] = "new_string3";
expect(result).toEqual(expected);
view.delete();
table.delete();
});
test("updates without '__INDEX' should append", async function () {
let table = await perspective.table(data);
await table.update([
{
__INDEX__: 0,
y: "new_string",
},
]);
await table.update([
{
y: "new_string",
},
]);
let view = await table.view();
let result = await view.to_json();
let expected = JSON.parse(JSON.stringify(data));
expected[0]["y"] = "new_string";
expected.push({ x: null, y: "new_string", z: null });
expect(result).toEqual(expected);
view.delete();
table.delete();
});
test("should partial update on 1-sided views using implicit '__INDEX__'", async function () {
let table = await perspective.table(data);
let view = await table.view({
group_by: ["x"],
});
await table.update([
{
__INDEX__: 0,
x: 100,
},
]);
let result = await view.to_json();
// update should be applied properly
expect(result).toEqual([
{ __ROW_PATH__: [], x: 109, y: 4, z: 4 },
{ __ROW_PATH__: [2], x: 2, y: 1, z: 1 },
{ __ROW_PATH__: [3], x: 3, y: 1, z: 1 },
{ __ROW_PATH__: [4], x: 4, y: 1, z: 1 },
{ __ROW_PATH__: [100], x: 100, y: 1, z: 1 },
]);
// check that un-pivoted view reflects data correctly
let view2 = await table.view();
let result2 = await view2.to_json();
let expected = JSON.parse(JSON.stringify(data));
expected[0]["x"] = 100;
expect(result2).toEqual(expected);
view.delete();
view2.delete();
table.delete();
});
test("should partial update on 2-sided views using implicit '__INDEX__'", async function () {
let table = await perspective.table(data);
let view = await table.view({
group_by: ["x"],
split_by: ["y"],
});
await table.update([
{
__INDEX__: 0,
x: 100,
},
]);
let result = await view.to_json();
// update should be applied properly
expect(result).toEqual([
{
__ROW_PATH__: [],
"a|x": 100,
"a|y": 1,
"a|z": 1,
"b|x": 2,
"b|y": 1,
"b|z": 1,
"c|x": 3,
"c|y": 1,
"c|z": 1,
"d|x": 4,
"d|y": 1,
"d|z": 1,
},
{
__ROW_PATH__: [2],
"a|x": null,
"a|y": null,
"a|z": null,
"b|x": 2,
"b|y": 1,
"b|z": 1,
"c|x": null,
"c|y": null,
"c|z": null,
"d|x": null,
"d|y": null,
"d|z": null,
},
{
__ROW_PATH__: [3],
"a|x": null,
"a|y": null,
"a|z": null,
"b|x": null,
"b|y": null,
"b|z": null,
"c|x": 3,
"c|y": 1,
"c|z": 1,
"d|x": null,
"d|y": null,
"d|z": null,
},
{
__ROW_PATH__: [4],
"a|x": null,
"a|y": null,
"a|z": null,
"b|x": null,
"b|y": null,
"b|z": null,
"c|x": null,
"c|y": null,
"c|z": null,
"d|x": 4,
"d|y": 1,
"d|z": 1,
},
{
__ROW_PATH__: [100],
"a|x": 100,
"a|y": 1,
"a|z": 1,
"b|x": null,
"b|y": null,
"b|z": null,
"c|x": null,
"c|y": null,
"c|z": null,
"d|x": null,
"d|y": null,
"d|z": null,
},
]);
// check that un-pivoted view reflects data correctly
let view2 = await table.view();
let result2 = await view2.to_json();
let expected = JSON.parse(JSON.stringify(data));
expected[0]["x"] = 100;
expect(result2).toEqual(expected);
view.delete();
view2.delete();
table.delete();
});
});
test.describe("Remove update", function () {
it_old_behavior("Should remove a single update", async function (done) {
let count = 0;
const cb1 = () => {
count += 1;
};
const cb2 = () => {
expect(count).toEqual(0);
setTimeout(() => {
view.delete();
table.delete();
done();
}, 0);
};
const table = await perspective.table(meta);
const view = await table.view();
const sub1 = view.on_update(cb1);
view.on_update(cb2);
view.remove_update(await sub1);
await table.update(data);
});
it_old_behavior(
"Should remove multiple updates",
async function (done) {
let count1 = 0;
const cb1 = () => {
count1 += 1;
};
let count2 = 0;
const cb2 = () => {
count2 += 1;
};
const cb3 = function () {
// cb2 should have been called
expect(count1).toEqual(0);
expect(count2).toEqual(0);
setTimeout(() => {
view.delete();
table.delete();
done();
}, 0);
};
const table = await perspective.table(meta);
const view = await table.view();
const sub1 = view.on_update(cb1);
const sub2 = view.on_update(cb2);
view.on_update(cb3);
view.remove_update(await sub1);
view.remove_update(await sub2);
await table.update(data);
},
);
});
test.describe("Filtered update and remove", () => {
let table, flat_view, filtered_view;
test.beforeEach(async () => {
table = await perspective.table(
{
x: "integer",
y: "string",
},
{ index: "x" },
);
flat_view = await table.view();
filtered_view = await table.view({ filter: [["y", "==", "a"]] });
});
test.afterEach(async () => {
await filtered_view.delete();
await flat_view.delete();
filtered_view = null;
flat_view = null;
});
test.afterAll(async () => {
if (filtered_view) await filtered_view.delete();
if (flat_view) await flat_view.delete();
await table.delete();
});
test("View output should always be consistent with filter", async () => {
let i = 0,
op = 0,
update_idx = 1,
remove_idx = 0;
for (i; i < 90; i++) {
if (op > 2) op = 0;
switch (op) {
case 0:
{
// insert
await table.update({
x: [i],
y: [i % 2 ? "a" : "b"],
});
}
break;
case 1:
{
// partial update
await table.update({
x: [update_idx],
y: [update_idx % 2 ? "b" : "a"],
});
update_idx++;
}
break;
case 2:
{
// remove
await table.remove([remove_idx]);
remove_idx++;
}
break;
default:
break;
}
const flat = await flat_view.to_json();
expect(await filtered_view.to_json()).toEqual(
flat.filter((row) => row.y === "a"),
);
op++;
}
});
test("View output should always be consistent with filter, recreating views", async () => {
let i = 0,
op = 0,
update_idx = 1,
remove_idx = 0;
for (i; i < 90; i++) {
if (op > 3) op = 0;
switch (op) {
case 0:
{
// insert
await table.update({
x: [i],
y: [i % 2 ? "a" : "b"],
});
}
break;
case 1:
{
// partial update
await table.update({
x: [update_idx],
y: [update_idx % 2 ? "b" : "a"],
});
update_idx++;
}
break;
case 2:
{
// remove
await table.remove([remove_idx]);
remove_idx++;
}
break;
case 3: {
// recreate views
await flat_view.delete();
flat_view = await table.view();
await filtered_view.delete();
filtered_view = await table.view({
filter: [["y", "==", "a"]],
});
}
default:
break;
}
const flat = await flat_view.to_json();
expect(await filtered_view.to_json()).toEqual(
flat.filter((row) => row.y === "a"),
);
op++;
}
});
test("View output should always be consistent with filter, deterministic", async () => {
filtered_view = await table.view({ filter: [["y", "==", "b"]] });
for (let i = 0; i < 10; i++) {
// append new rows
await table.update([{ x: i, y: "a" }]);
expect(await table.size()).toEqual(i + 1);
// make sure appended
const result = await flat_view.to_json();
expect(result[result.length - 1]).toEqual({ x: i, y: "a" });
// filtering
expect(await filtered_view.to_json()).toEqual(
result.filter((row) => row.y === "b"),
);
}
await filtered_view.delete();
// new view with filter
filtered_view = await table.view({ filter: [["y", "==", "a"]] });
let flat = await flat_view.to_json();
let filtered = await filtered_view.to_json();
expect(flat.length).toEqual(filtered.length);
expect(filtered).toEqual(flat);
for (let i = 0; i < 10; i++) {
// partial update rows
if (i % 2 == 0) {
await table.update([{ x: i, y: "b" }]);
flat = await flat_view.to_json();
filtered = await filtered_view.to_json();
expect(flat.length).toBeGreaterThan(filtered.length);
expect(flat.filter((row) => row.x === i)[0]).toEqual({
x: i,
y: "b",
});
expect(filtered).toEqual(
flat.filter((row) => row.y === "a"),
);
// partial updates not appends
expect(await table.size()).toEqual(10);
}
}
// Remove "b" rows
flat = await flat_view.to_json();
await table.remove(
flat.filter((row) => row.y === "b").map((row) => row.x),
);
expect(await flat_view.to_json()).toEqual([
{ x: 1, y: "a" },
{ x: 3, y: "a" },
{ x: 5, y: "a" },
{ x: 7, y: "a" },
{ x: 9, y: "a" },
]);
});
test("Output consistent with filter, out of order", async () => {
const tbl = await perspective.table(
{
index: "string",
x: "string",
},
{ index: "index" },
);
const view = await tbl.view();
await tbl.update({
index: ["a", "d", "b"],
x: ["abc", "def", "acc"],
});
await tbl.remove(["b"]);
const view2 = await tbl.view({ filter: [["x", "==", "abc"]] });
await view.delete();
expect(await view2.to_json()).toEqual([{ index: "a", x: "abc" }]);
console.log(await view2.to_json());
await view2.delete();
await tbl.delete();
});
test("Output consistent with filter, string small", async () => {
const tbl = await perspective.table(
{
index: "string",
x: "string",
},
{ index: "index" },
);
const view = await tbl.view();
await tbl.update({ index: ["a", "c"], x: ["abc", "def"] });
await tbl.remove(["c"]);
const view2 = await tbl.view({ filter: [["x", "==", "abc"]] });
await view.delete();
expect(await view2.to_json()).toEqual([{ index: "a", x: "abc" }]);
console.log(await view2.to_json());
await view2.delete();
await tbl.delete();
});
test("Output consistent with filter, ordered", async () => {
const tbl = await perspective.table(
{
index: "string",
x: "string",
},
{ index: "index" },
);
const view = await tbl.view();
await tbl.update({
index: ["a", "b", "c"],
x: ["abc", "def", "acc"],
});
await tbl.remove(["b"]);
const view2 = await tbl.view({ filter: [["x", "==", "abc"]] });
await view.delete();
expect(await view2.to_json()).toEqual([{ index: "a", x: "abc" }]);
console.log(await view2.to_json());
await view2.delete();
await tbl.delete();
});
test("Output consistent with filter, numeric index out of order", async () => {
const tbl = await perspective.table(
{
index: "float",
x: "string",
},
{ index: "index" },
);
const view = await tbl.view();
await tbl.update({ index: [50, 1, 2], x: ["abc", "def", "acc"] });
await tbl.remove([1]);
const view2 = await tbl.view({ filter: [["x", "==", "acc"]] });
await view.delete();
expect(await view2.to_json()).toEqual([{ index: 2, x: "acc" }]);
console.log(await view2.to_json());
await view2.delete();
await tbl.delete();
});
test("Output consistent with filter, numeric index ordered", async () => {
const tbl = await perspective.table(
{
index: "float",
x: "string",
},
{ index: "index" },
);
const view = await tbl.view();
await tbl.update({ index: [1, 2, 3], x: ["abc", "def", "acc"] });
await tbl.remove([3]);
const view2 = await tbl.view({ filter: [["x", "==", "def"]] });
await view.delete();
expect(await view2.to_json()).toEqual([{ index: 2, x: "def" }]);
console.log(await view2.to_json());
await view2.delete();
await tbl.delete();
});
test("Output consistent with filter, date index out of order", async () => {
const tbl = await perspective.table(
{
index: "date",
x: "string",
},
{ index: "index" },
);
const view = await tbl.view();
await tbl.update({
index: [
new Date(2021, 3, 15),
new Date(2009, 1, 13),
new Date(2015, 5, 10),
],
x: ["abc", "def", "acc"],
});
await tbl.remove([1]);
const view2 = await tbl.view({ filter: [["x", "==", "acc"]] });
await view.delete();
expect(await view2.to_json()).toEqual([
{ index: new Date(2015, 5, 10).getTime(), x: "acc" },
]);
console.log(await view2.to_json());
await view2.delete();
await tbl.delete();
});
test("Output consistent with filter, date index ordered", async () => {
const tbl = await perspective.table(
{
index: "date",
x: "string",
},
{ index: "index" },
);
const view = await tbl.view();
await tbl.update({
index: [
new Date(2009, 1, 13),
new Date(2015, 5, 10),
new Date(2021, 3, 15),
],
x: ["abc", "def", "acc"],
});
await tbl.remove([3]);
const view2 = await tbl.view({ filter: [["x", "==", "def"]] });
await view.delete();
expect(await view2.to_json()).toEqual([
{ index: new Date(2015, 5, 10).getTime(), x: "def" },
]);
console.log(await view2.to_json());
await view2.delete();
await tbl.delete();
});
test("Output consistent with filter, datetime index out of order", async () => {
const tbl = await perspective.table(
{
index: "datetime",
x: "string",
},
{ index: "index" },
);
const view = await tbl.view();
await tbl.update({
index: [
new Date(2021, 3, 15, 5, 10, 3),
new Date(2009, 1, 13, 0, 0, 1),
new Date(2015, 5, 10, 15, 10, 2),
],
x: ["abc", "def", "acc"],
});
await tbl.remove([1]);
const view2 = await tbl.view({ filter: [["x", "==", "acc"]] });
await view.delete();
expect(await view2.to_json()).toEqual([
{ index: new Date(2015, 5, 10, 15, 10, 2).getTime(), x: "acc" },
]);
console.log(await view2.to_json());
await view2.delete();
await tbl.delete();
});
test("Output consistent with filter, datetime index ordered", async () => {
const tbl = await perspective.table(
{
index: "datetime",
x: "string",
},
{ index: "index" },
);
const view = await tbl.view();
await tbl.update({
index: [
new Date(2009, 1, 13, 0, 0, 1),
new Date(2015, 5, 10, 15, 10, 2),
new Date(2021, 3, 15, 5, 10, 3),
],
x: ["abc", "def", "acc"],
});
await tbl.remove([3]);
const view2 = await tbl.view({ filter: [["x", "==", "def"]] });
await view.delete();
expect(await view2.to_json()).toEqual([
{ index: new Date(2015, 5, 10, 15, 10, 2).getTime(), x: "def" },
]);
console.log(await view2.to_json());
await view2.delete();
await tbl.delete();
});
test("Output consistent with filter, out of order updates", async () => {
const tbl = await perspective.table(
{
index: "string",
x: "string",
},
{ index: "index" },
);
const view = await tbl.view();
await tbl.update({
index: ["a", "d", "b"],
x: ["abc", "def", "acc"],
});
await tbl.remove(["b"]);
const view2 = await tbl.view({ filter: [["x", "==", "abc"]] });
await view.delete();
expect(await view2.to_json()).toEqual([{ index: "a", x: "abc" }]);
await tbl.update([{ index: "b", x: "abc" }]);
expect(await view2.to_json()).toEqual([
{ index: "a", x: "abc" },
{ index: "b", x: "abc" },
]);
await tbl.remove(["a"]);
expect(await view2.to_json()).toEqual([{ index: "b", x: "abc" }]);
await tbl.update([
{ index: "a", x: "abc" },
{ index: null, x: "abc" },
]);
expect(await view2.to_json()).toEqual([
{ index: null, x: "abc" },
{ index: "a", x: "abc" },
{ index: "b", x: "abc" },
]);
const indices = "abcdefghijklmnopqrstuvwxyz".split("");
for (const idx of indices) {
await tbl.update([{ index: idx, x: "abc" }]);
}
let result = await view2.to_json();
expect(result[0]).toEqual({ index: null, x: "abc" });
result = result.slice(1);
for (let i = 0; i < result.length; i++) {
expect(result[i]).toEqual({ index: indices[i], x: "abc" });
}
await view2.delete();
await tbl.delete();
});
test("Output consistent with filter, out of order updates random", async () => {
const tbl = await perspective.table(
{
index: "string",
x: "string",
},
{ index: "index" },
);
const view = await tbl.view();
await tbl.update({
index: ["a", "d", "b"],
x: ["abc", "def", "acc"],
});
await tbl.remove(["b"]);
const view2 = await tbl.view({ filter: [["x", "==", "abc"]] });
await view.delete();
expect(await view2.to_json()).toEqual([{ index: "a", x: "abc" }]);
await tbl.update([{ index: "b", x: "abc" }]);
expect(await view2.to_json()).toEqual([
{ index: "a", x: "abc" },
{ index: "b", x: "abc" },
]);
await tbl.remove(["a"]);
expect(await view2.to_json()).toEqual([{ index: "b", x: "abc" }]);
await tbl.update([
{ index: "a", x: "abc" },
{ index: null, x: "abc" },
]);
expect(await view2.to_json()).toEqual([
{ index: null, x: "abc" },
{ index: "a", x: "abc" },
{ index: "b", x: "abc" },
]);
// randomize
let indices = "abcdefghijklmnopqrstuvwxyz"
.split("")
.map((v) => {
return { value: v, key: Math.random() };
})
.sort((a, b) => a.key - b.key)
.map((v) => v.value);
// console.log(indices);
for (const idx of indices) {
await tbl.update([{ index: idx, x: "abc" }]);
}
let result = await view2.to_json();
expect(result[0]).toEqual({ index: null, x: "abc" });
result = result.slice(1);
indices = indices.sort();
for (let i = 0; i < result.length; i++) {
expect(result[i]).toEqual({ index: indices[i], x: "abc" });
}
await view2.delete();
await tbl.delete();
});
test("Delete a table with pending updates", async () => {
const tbl = await perspective.table(
{
index: "string",
x: "string",
},
{ index: "index" },
);
for (let i = 0; i < 3; i++) {
tbl.update({
index: ["a", "d", "b"],
x: ["abc", "def", "acc"],
});
}
await tbl.delete();
});
});
// Regression coverage for the `t_ftrav::step_end` fast-path leak: a
// pass containing only `update_row` calls (every pkey already exists)
// used to take the fast path because `m_step_inserts` and
// `m_step_deletes` are both zero — leaving `m_updated` flags stamped
// on `m_index` and dropping the staged replacements from
// `m_new_elems`. The next pass that *did* trigger a rebuild (any
// `add_row` or `delete_row`) skipped the flagged entries with no
// counterpart to reinstate them, and the rows silently disappeared
// from the sorted view.
test.describe("Update under sorted view", function () {
const d1 = +new Date("2024-01-01T00:00:00Z");
const d2 = +new Date("2024-01-02T00:00:00Z");
const d3 = +new Date("2024-01-03T00:00:00Z");
const d4 = +new Date("2024-01-04T00:00:00Z");
const d5 = +new Date("2024-01-05T00:00:00Z");
async function seed(perspective) {
const table = await perspective.table(
{ id: "integer", ts: "datetime", payload: "string" },
{ index: "id" },
);
await table.update({
id: [1, 2, 3, 4],
ts: [d1, d2, d3, d4],
payload: ["a", "b", "c", "d"],
});
return table;
}
test("baseline: update existing rows then append a new row", async function () {
const table = await seed(perspective);
const view = await table.view({ sort: [["ts", "asc"]] });
await table.update({ id: [2, 3], payload: ["bb", "cc"] });
await table.update({ id: [5], ts: [d5], payload: ["e"] });
const cols = await view.to_columns();
expect(await view.num_rows()).toEqual(5);
expect(cols.id).toEqual([1, 2, 3, 4, 5]);
expect(cols.payload).toEqual(["a", "bb", "cc", "d", "e"]);
expect(cols.ts).toEqual([d1, d2, d3, d4, d5]);
await view.delete();
await table.delete();
});
test("update-only pass is correct on its own", async function () {
const table = await seed(perspective);
const view = await table.view({ sort: [["ts", "asc"]] });
await table.update({ id: [2, 3], payload: ["bb", "cc"] });
const cols = await view.to_columns();
expect(await view.num_rows()).toEqual(4);
expect(cols.id).toEqual([1, 2, 3, 4]);
expect(cols.payload).toEqual(["a", "bb", "cc", "d"]);
await view.delete();
await table.delete();
});
test("multiple update-only passes before an insert", async function () {
const table = await seed(perspective);
const view = await table.view({ sort: [["ts", "asc"]] });
await table.update({ id: [2], payload: ["b1"] });
await table.update({ id: [3], payload: ["c1"] });
await table.update({ id: [2], payload: ["b2"] });
await table.update({ id: [5], ts: [d5], payload: ["e"] });
const cols = await view.to_columns();
expect(await view.num_rows()).toEqual(5);
expect(cols.id).toEqual([1, 2, 3, 4, 5]);
expect(cols.payload).toEqual(["a", "b2", "c1", "d", "e"]);
await view.delete();
await table.delete();
});
test("update that changes the sort key, then append", async function () {
const table = await seed(perspective);
const view = await table.view({ sort: [["ts", "asc"]] });
const d_late = +new Date("2024-01-10T00:00:00Z");
await table.update({ id: [2], ts: [d_late] });
await table.update({ id: [5], ts: [d5], payload: ["e"] });
const cols = await view.to_columns();
expect(await view.num_rows()).toEqual(5);
expect(cols.id).toEqual([1, 3, 4, 5, 2]);
expect(cols.ts).toEqual([d1, d3, d4, d5, d_late]);
await view.delete();
await table.delete();
});
test("update-only pass followed by a delete", async function () {
const table = await seed(perspective);
const view = await table.view({ sort: [["ts", "asc"]] });
await table.update({ id: [2, 3], payload: ["bb", "cc"] });
await table.remove([4]);
const cols = await view.to_columns();
expect(await view.num_rows()).toEqual(3);
expect(cols.id).toEqual([1, 2, 3]);
expect(cols.payload).toEqual(["a", "bb", "cc"]);
await view.delete();
await table.delete();
});
test("no sort: update-only pass + append is unaffected", async function () {
const table = await seed(perspective);
const view = await table.view();
await table.update({ id: [2, 3], payload: ["bb", "cc"] });
await table.update({ id: [5], ts: [d5], payload: ["e"] });
expect(await view.num_rows()).toEqual(5);
await view.delete();
await table.delete();
});
});
})(perspective);