Files
perspective-dev--perspective/rust/perspective-js/test/js/expressions/functionality.spec.js
T
2026-07-13 12:25:07 +08:00

3375 lines
125 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 expressions_common from "./common.js";
import { test, expect } from "@perspective-dev/test";
import perspective from "../perspective_client";
/**
* Tests the functionality of `View`-based expressions, specifically that
* existing column/view semantics (pivots, aggregates, columns, sorts, filters)
* continue to be functional on expressions.
*/
((perspective) => {
test.describe("Expression structures", function () {
test.describe("if statements", function () {
test("if without else should be invalid", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const validate = await table.validate_expressions([
'if ("w" > 1) 5;',
'if ("w" > 1) 5',
]);
expect(
validate.expression_schema['if ("w" > 1) 5;'],
).toBeUndefined();
expect(
validate.expression_schema['if ("w" > 1) 5'],
).toBeUndefined();
expect(validate.errors).toEqual({
'if ("w" > 1) 5': {
column: 0,
error_message:
"Type Error - inputs do not resolve to a valid expression.",
line: 0,
},
'if ("w" > 1) 5;': {
column: 0,
error_message:
"Type Error - inputs do not resolve to a valid expression.",
line: 0,
},
});
table.delete();
});
test.skip("if else with multiple return types should be invalid", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: {
"if (\"w\" == 1.5) 5; else 'abc'":
"if (\"w\" == 1.5) 5; else 'abc'",
},
});
const results = await view.to_columns();
// TODO: we should prevent this polymorphic return from being
// allowed, but the machinery around how if/else returns is
// opaque to me at the moment.
expect(results["if (\"w\" == 1.5) 5; else 'abc'"]).toEqual([
"",
"abc",
"abc",
"abc",
]);
view.delete();
table.delete();
});
test("functional if", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
'if ("w" > 2, 5, 10);',
'if ("w" == 1.5, 5, 10)',
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(results['if ("w" > 2, 5, 10);']).toEqual([10, 5, 5, 5]);
expect(results['if ("w" == 1.5, 5, 10)']).toEqual([
5, 10, 10, 10,
]);
view.delete();
table.delete();
});
test("functional if string", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
"if (\"y\" == 'a', 5, 10);",
"if (\"y\" != 'a', 5, 10)",
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(results["if (\"y\" == 'a', 5, 10);"]).toEqual([
5, 10, 10, 10,
]);
expect(results["if (\"y\" != 'a', 5, 10)"]).toEqual([
10, 5, 5, 5,
]);
view.delete();
table.delete();
});
test.skip("functional if bool", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
'if ("z" == true, 5, 10);',
'if ("z" != true, 5, 10)',
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(results['if ("z" == 1, 5, 10);']).toEqual([
5, 10, 5, 10,
]);
expect(results['if ("z" != true, 5, 10)']).toEqual([
10, 5, 10, 5,
]);
view.delete();
table.delete();
});
test("functional if date", async function () {
const table = await perspective.table({
a: "date",
b: "date",
});
const view = await table.view({
expressions: [
'if ("a" == "b", 5, 10);',
'if ("a" < "b", 5, 10)',
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
await table.update({
a: [
new Date(2020, 3, 1),
new Date(2012, 0, 30),
new Date(2018, 9, 31),
new Date(2020, 1, 29),
],
b: [
new Date(2020, 3, 1),
new Date(2012, 1, 30),
new Date(2018, 10, 31),
new Date(2020, 1, 29),
],
});
const results = await view.to_columns();
expect(results['if ("a" == "b", 5, 10);']).toEqual([
5, 10, 10, 5,
]);
expect(results['if ("a" < "b", 5, 10)']).toEqual([
10, 5, 5, 10,
]);
view.delete();
table.delete();
});
test("functional if datetime", async function () {
const table = await perspective.table({
a: "datetime",
b: "datetime",
});
const view = await table.view({
expressions: [
'if ("a" >= "b", 5, 10);',
'if ("a" < "b", 5, 10)',
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
await table.update({
a: [
new Date(2020, 3, 1, 12, 30, 45, 850),
new Date(2012, 0, 30, 19, 25),
new Date(2018, 9, 31, 3, 13),
new Date(2020, 1, 29, 23, 59, 59),
],
b: [
new Date(2020, 3, 1, 12, 30, 45, 850),
new Date(2012, 1, 30),
new Date(2018, 10, 31),
new Date(2020, 1, 29),
],
});
const results = await view.to_columns();
expect(results['if ("a" >= "b", 5, 10);']).toEqual([
5, 10, 10, 5,
]);
expect(results['if ("a" < "b", 5, 10)']).toEqual([
10, 5, 5, 10,
]);
view.delete();
table.delete();
});
test("ternary if", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: {
'"w" > 2 ? 5 : 10;': '"w" > 2 ? 5 : 10;',
'"w" == 1.5 ? 5 : 10': '"w" == 1.5 ? 5 : 10',
},
});
const results = await view.to_columns();
expect(results['"w" > 2 ? 5 : 10;']).toEqual([10, 5, 5, 5]);
expect(results['"w" == 1.5 ? 5 : 10']).toEqual([5, 10, 10, 10]);
view.delete();
table.delete();
});
test("ternary if string", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
"\"y\" == 'a' ? 5 : 10;",
"\"y\" != 'a' ? 5 : 10",
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(results["\"y\" == 'a' ? 5 : 10;"]).toEqual([
5, 10, 10, 10,
]);
expect(results["\"y\" != 'a' ? 5 : 10"]).toEqual([10, 5, 5, 5]);
view.delete();
table.delete();
});
test.skip("ternary if bool", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
'"z" == true ? 5 : 10;',
'"z" != true ? 5 : 10',
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(results['"z" == 1 ? 5 : 10;']).toEqual([5, 10, 5, 10]);
expect(results['"z" != true ? 5 : 10']).toEqual([10, 5, 10, 5]);
view.delete();
table.delete();
});
test("ternary if date", async function () {
const table = await perspective.table({
a: "date",
b: "date",
});
const view = await table.view({
expressions: {
'"a" == "b" ? 5 : 10;': '"a" == "b" ? 5 : 10;',
'"a" < "b" ? 5 : 10': '"a" < "b" ? 5 : 10',
},
});
await table.update({
a: [
new Date(2020, 3, 1),
new Date(2012, 0, 30),
new Date(2018, 9, 31),
new Date(2020, 1, 29),
],
b: [
new Date(2020, 3, 1),
new Date(2012, 1, 30),
new Date(2018, 10, 31),
new Date(2020, 1, 29),
],
});
const results = await view.to_columns();
expect(results['"a" == "b" ? 5 : 10;']).toEqual([5, 10, 10, 5]);
expect(results['"a" < "b" ? 5 : 10']).toEqual([10, 5, 5, 10]);
view.delete();
table.delete();
});
test("ternary if datetime", async function () {
const table = await perspective.table({
a: "datetime",
b: "datetime",
});
const view = await table.view({
expressions: {
'"a" >= "b" ? 5 : 10;': '"a" >= "b" ? 5 : 10;',
'"a" < "b" ? 5 : 10': '"a" < "b" ? 5 : 10',
},
});
await table.update({
a: [
new Date(2020, 3, 1, 12, 30, 45, 850),
new Date(2012, 0, 30, 19, 25),
new Date(2018, 9, 31, 3, 13),
new Date(2020, 1, 29, 23, 59, 59),
],
b: [
new Date(2020, 3, 1, 12, 30, 45, 850),
new Date(2012, 1, 30),
new Date(2018, 10, 31),
new Date(2020, 1, 29),
],
});
const results = await view.to_columns();
expect(results['"a" >= "b" ? 5 : 10;']).toEqual([5, 10, 10, 5]);
expect(results['"a" < "b" ? 5 : 10']).toEqual([10, 5, 5, 10]);
view.delete();
table.delete();
});
test("if else", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
'if ("w" > 2) 5; else 10',
'if ("w" > 2) 5; else 10;',
'if ("w" > 2) { \n5; } else { \n10;}',
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(results['if ("w" > 2) 5; else 10']).toEqual([
10, 5, 5, 5,
]);
expect(results['if ("w" > 2) 5; else 10;']).toEqual([
10, 5, 5, 5,
]);
expect(results['if ("w" > 2) { \n5; } else { \n10;}']).toEqual([
10, 5, 5, 5,
]);
view.delete();
table.delete();
});
test("if else if", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
'if ("w" == 3.5) 15; else if ("w" > 2) 5; else 10',
'if ("w" == 3.5) 15; else if ("w" > 2) 5; else 10;',
'if ("w" == 3.5) { \n15; } else if ("w" > 2) { \n5; } else { \n10;}',
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results['if ("w" == 3.5) 15; else if ("w" > 2) 5; else 10'],
).toEqual([10, 5, 15, 5]);
expect(
results[
'if ("w" == 3.5) 15; else if ("w" > 2) 5; else 10;'
],
).toEqual([10, 5, 15, 5]);
expect(
results[
'if ("w" == 3.5) { \n15; } else if ("w" > 2) { \n5; } else { \n10;}'
],
).toEqual([10, 5, 15, 5]);
view.delete();
table.delete();
});
});
test.describe("Switch", function () {
test("Switch on multiple columns", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
`switch { case "w" > 2: sqrt(144); case "y" == 'a': sqrt(121); default: 0; }`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results[
`switch { case "w" > 2: sqrt(144); case "y" == 'a': sqrt(121); default: 0; }`
],
).toEqual([11, 12, 12, 12]);
await view.delete();
await table.delete();
});
test("Switch on multiple columns with resulting expression", async function () {
const table = await perspective.table({
w: "float",
x: "float",
y: "string",
z: "boolean",
});
await table.update(expressions_common.int_float_data);
const expr = `switch { case "w" > 2: (sqrt(144) * "w"); case "y" == 'a': (sqrt(121) * "w"); default: "x"; }`;
const view = await table.view({
expressions: { [expr]: expr },
});
const results = await view.to_columns();
const schema = await view.schema();
expect(schema[expr]).toEqual("float");
expect(results[expr]).toEqual([16.5, 30, 42, 54]);
await view.delete();
await table.delete();
});
});
test.describe("While loop", function () {
test("Scalar while", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
// 2 ^ 10 = 1024
const view = await table.view({
expressions: [
`var x := 1; var y := 0; while (y < 10) { x := x * 2; y += 1; }; x`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results[
`var x := 1; var y := 0; while (y < 10) { x := x * 2; y += 1; }; x`
],
).toEqual([1024, 1024, 1024, 1024]);
await view.delete();
await table.delete();
});
test("Scalar while 2", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
// 2 * 2 = 4, 4 * 4 = 16, 16 * 16 = 256
const view = await table.view({
expressions: [
`var x := 2; var y := 0; while (y < 3) { x := x * x; y += 1; }; x`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results[
`var x := 2; var y := 0; while (y < 3) { x := x * x; y += 1; }; x`
],
).toEqual([256, 256, 256, 256]);
await view.delete();
await table.delete();
});
test("While without semicolon should not validate", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const schema = await table.validate_expressions({
[`expr`]: `var x := 2; var y := 0; while (y < 3) { x := x * x; y += 1; } x`,
});
expect(schema.expression_schema["expr"]).toBeUndefined();
expect(schema.errors["expr"]).toEqual({
column: 62,
line: 0,
error_message:
"Invalid syntax 'x' possible missing operator or context",
});
await table.delete();
});
test("Column while", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
`var x := 0; var y := 0; while (y < 10) { x := "w" * 2; y += 1; }; x`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results[
`var x := 0; var y := 0; while (y < 10) { x := "w" * 2; y += 1; }; x`
],
).toEqual([3, 5, 7, 9]);
await view.delete();
await table.delete();
});
test.skip("Column while with break", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
`var x := 0; var y := 0; while (y < 10) { if (y % 2 == 0) { break; }; x := "w" * 2; y += 1; }; x`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results[
`var x := 0; var y := 0; while (y < 10) { if (y % 2 == 0) { break; }; x := "w" * 2; y += 1; }; x`
],
).toEqual([16.5, 30, 42, 54]);
await view.delete();
await table.delete();
});
});
test.describe("For loop", function () {
test("For loop", async () => {
const expr = `var x := 0; for (var i := 0; i < 3; i += 1) { x += 1; }; x`;
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: { expression: expr },
});
const results = await view.to_columns();
expect(results["expression"]).toEqual([3, 3, 3, 3]);
await view.delete();
await table.delete();
});
test("Scalar for", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
`for (var x := 0; x < 10; x += 1) { var y := x + 1; y}`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results[
`for (var x := 0; x < 10; x += 1) { var y := x + 1; y}`
],
).toEqual([10, 10, 10, 10]);
await view.delete();
await table.delete();
});
test("Column for", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
`for (var x := 0; x < 10; x += 1) { var y := "w" + 1; y}`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results[
`for (var x := 0; x < 10; x += 1) { var y := "w" + 1; y}`
],
).toEqual([2.5, 3.5, 4.5, 5.5]);
await view.delete();
await table.delete();
});
test.skip("for loop only valid on numbers", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
// `for (var x := today(); x < 10; x += 1) { var y := "w" + 1; y}`
const expressions = [
`for (var x := 'abc'; x < 10; x += 1) { var y := "w" + 1; y}`,
];
const validate = await table.validate_expressions(expressions);
for (const expr of expressions) {
expect(validate.expression_schema[expr]).toBeUndefined();
}
await table.delete();
});
});
test.describe.skip("Repeat loop", function () {
test("Scalar repeat", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
`var x := 1; var y := 1; repeat x := x + (y * 2) until (x > 10)`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results[
`var x := 1; var y := 1; repeat x := x + (y * 2) until (x > 10)`
],
).toEqual([11, 11, 11, 11]);
await view.delete();
await table.delete();
});
test("Column repeat", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
`var x := 1; repeat x := x + ("x" * 2) until (x > 10)`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results[
`var x := 1; repeat x := x + ("x" * 2) until (x > 10)`
],
).toEqual([16.5, 30, 42, 54]);
await view.delete();
await table.delete();
});
});
});
test.describe("Local variables", function () {
test("Declare numeric", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: { 1: "1", [`100.0000001`]: "100.0000001" },
});
const results = await view.to_columns();
expect(results["1"]).toEqual([1, 1, 1, 1]);
expect(results["100.0000001"]).toEqual([
100.0000001, 100.0000001, 100.0000001, 100.0000001,
]);
await view.delete();
await table.delete();
});
test("Declare string", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
"'hello'",
"'very long string that is very long and has many characters'",
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(results["'hello'"]).toEqual([
"hello",
"hello",
"hello",
"hello",
]);
expect(
results[
"'very long string that is very long and has many characters'"
],
).toEqual([
"very long string that is very long and has many characters",
"very long string that is very long and has many characters",
"very long string that is very long and has many characters",
"very long string that is very long and has many characters",
]);
await view.delete();
await table.delete();
});
test("Clear() and declare string", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
"'hello'",
"'very long string that is very long and has many characters'",
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(results["'hello'"]).toEqual([
"hello",
"hello",
"hello",
"hello",
]);
expect(
results[
"'very long string that is very long and has many characters'"
],
).toEqual([
"very long string that is very long and has many characters",
"very long string that is very long and has many characters",
"very long string that is very long and has many characters",
"very long string that is very long and has many characters",
]);
await view.delete();
await table.clear();
await table.update([expressions_common.int_float_data[0]]);
const view2 = await table.view({
expressions: [
"'hello'",
"'another very long string with many characters'",
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results2 = await view2.to_columns();
expect(results2["'hello'"]).toEqual(["hello"]);
expect(
results2["'another very long string with many characters'"],
).toEqual(["another very long string with many characters"]);
await view2.delete();
await table.delete();
});
test("Declare numeric var", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
`var x := 10; var y := -100.00005; var z := x + y; abs(z)`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results[
`var x := 10; var y := -100.00005; var z := x + y; abs(z)`
],
).toEqual([90.00005, 90.00005, 90.00005, 90.00005]);
await view.delete();
await table.delete();
});
test("Declare numeric var 0 and 1", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const expressions = ["1", "0", "var x := 1; x", "var x := 0; x"];
const view = await table.view({
expressions,
});
const schema = await view.expression_schema();
for (const expr of expressions) {
expect(schema[expr]).toEqual("float");
}
const results = await view.to_columns();
expect(results["1"]).toEqual(Array(4).fill(1));
expect(results["0"]).toEqual(Array(4).fill(0));
expect(results["var x := 1; x"]).toEqual(Array(4).fill(1));
expect(results["var x := 0; x"]).toEqual(Array(4).fill(0));
await view.delete();
await table.delete();
});
test("Declare string var", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
`var x := 'long literal string here'; var y := 'long literal string here'; x == y ? concat('strings: ', x, ', ', y) : 'nope'`,
`var x := 'hello'; var y := upper(x); lower(y);`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results[
`var x := 'long literal string here'; var y := 'long literal string here'; x == y ? concat('strings: ', x, ', ', y) : 'nope'`
],
).toEqual([
"strings: long literal string here, long literal string here",
"strings: long literal string here, long literal string here",
"strings: long literal string here, long literal string here",
"strings: long literal string here, long literal string here",
]);
expect(
results[`var x := 'hello'; var y := upper(x); lower(y);`],
).toEqual(["hello", "hello", "hello", "hello"]);
await view.delete();
await table.delete();
});
test("Declare string var one long one short", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
`var x := 'abcdefghijklmnop'; var y := '123'; concat(x, y)`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results[
`var x := 'abcdefghijklmnop'; var y := '123'; concat(x, y)`
],
).toEqual([
"abcdefghijklmnop123",
"abcdefghijklmnop123",
"abcdefghijklmnop123",
"abcdefghijklmnop123",
]);
await view.delete();
await table.delete();
});
test("Declare string var short", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: {
"var x := 'abc'; var y := '123'; concat(x, y)":
"var x := 'abc'; var y := '123'; concat(x, y)",
},
});
const results = await view.to_columns();
expect(
results[`var x := 'abc'; var y := '123'; concat(x, y)`],
).toEqual(["abc123", "abc123", "abc123", "abc123"]);
await view.delete();
await table.delete();
});
test("Clear() and Declare string var", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
"var x := 'Eabcdefghijklmn'; var y := '0123456789'; var z := 'ABCD'; var zz := 'EFG'; concat(x, y, z, zz)",
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results[
"var x := 'Eabcdefghijklmn'; var y := '0123456789'; var z := 'ABCD'; var zz := 'EFG'; concat(x, y, z, zz)"
],
).toEqual([
"Eabcdefghijklmn0123456789ABCDEFG",
"Eabcdefghijklmn0123456789ABCDEFG",
"Eabcdefghijklmn0123456789ABCDEFG",
"Eabcdefghijklmn0123456789ABCDEFG",
]);
await view.delete();
await table.clear();
await table.update([expressions_common.int_float_data[0]]);
const view2 = await table.view({
expressions: [
"var x := 'Eabcdefghijklmn'; var y := '0123456789'; var z := 'ABCD'; var zz := 'EFG'; concat(x, y, z, zz)",
"var x := 'abcdefghijklmn'; var y := 'ABCDEFG123456789'; x",
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results2 = await view2.to_columns();
expect(
results2[
"var x := 'Eabcdefghijklmn'; var y := '0123456789'; var z := 'ABCD'; var zz := 'EFG'; concat(x, y, z, zz)"
],
).toEqual(["Eabcdefghijklmn0123456789ABCDEFG"]);
expect(
results2[
"var x := 'abcdefghijklmn'; var y := 'ABCDEFG123456789'; x"
],
).toEqual(["abcdefghijklmn"]);
await view2.delete();
await table.delete();
});
test("Declare date var", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
`var x := today(); var y := today(); x == y ? 1 : 0;`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results[`var x := today(); var y := today(); x == y ? 1 : 0;`],
).toEqual([1, 1, 1, 1]);
await view.delete();
await table.delete();
});
test("Declare datetime var", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const now = new Date().getTime();
const view = await table.view({
expressions: { "now()": "now()" },
});
const results = await view.to_columns();
for (const x of results["now()"]) {
expect(now - 1000 < x < now + 1000).toBe(true);
}
await view.delete();
await table.delete();
});
test("Declare boolean var", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
`var x := true; var y := false; x and y ? 1 : 0`,
`var x := true; var y := false; x or y`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results[`var x := true; var y := false; x and y ? 1 : 0`],
).toEqual([0, 0, 0, 0]);
expect(results["var x := true; var y := false; x or y"]).toEqual([
true,
true,
true,
true,
]);
await view.delete();
await table.delete();
});
test("Declare column as var", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: {
'var x := "w"; var y := 1.5; x > 2.5 ? x : y':
'var x := "w"; var y := 1.5; x > 2.5 ? x : y',
},
});
const results = await view.to_columns();
expect(
results[`var x := "w"; var y := 1.5; x > 2.5 ? x : y`],
).toEqual([1.5, 1.5, 3.5, 4.5]);
await view.delete();
await table.delete();
});
test("Assign one type to var of another", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
`var x := 'w'; var y := 1.5; x := y; x`,
`var x := 'w'; var y := 1.5; y := x; y`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(results[`var x := 'w'; var y := 1.5; x := y; x`]).toEqual([
1.5, 1.5, 1.5, 1.5,
]);
expect(results[`var x := 'w'; var y := 1.5; y := x; y`]).toEqual([
"w",
"w",
"w",
"w",
]);
await view.delete();
await table.delete();
});
test("Assign to declared numeric var", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
`var x := 100; var y := 200.5; x := y; x`,
`var x := 10; var y := pow(x, 3); y := x; y`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(results[`var x := 100; var y := 200.5; x := y; x`]).toEqual([
200.5, 200.5, 200.5, 200.5,
]);
expect(
results[`var x := 10; var y := pow(x, 3); y := x; y`],
).toEqual([10, 10, 10, 10]);
await view.delete();
await table.delete();
});
test("Assign to declared string var", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
`var x := 'long literal string here'; var y := lower('ANOTHER VERY LONG STRING HERE'); x := y`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(
results[
`var x := 'long literal string here'; var y := lower('ANOTHER VERY LONG STRING HERE'); x := y`
],
).toEqual([
"another very long string here",
"another very long string here",
"another very long string here",
"another very long string here",
]);
await view.delete();
await table.delete();
});
test("Assign to declared date var", async function () {
const table = await perspective.table({
a: "date",
});
const view = await table.view({
expressions: [
`var x := today(); var y := today(); var z := "a"; (x > z) and (y > z) ? 1 : 0`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const tomorrow = new Date();
tomorrow.setDate(new Date().getDate() + 1);
await table.update({
a: [
new Date(2020, 3, 1),
new Date(2012, 0, 30),
new Date(2018, 9, 31),
tomorrow,
],
});
const results = await view.to_columns();
expect(
results[
`var x := today(); var y := today(); var z := "a"; (x > z) and (y > z) ? 1 : 0`
],
).toEqual([1, 1, 1, 0]);
await view.delete();
await table.delete();
});
test("Assign to declared datetime var", async function () {
const table = await perspective.table({
a: "datetime",
});
const view = await table.view({
expressions: [
`var x := now(); var y := now(); var z := "a"; (x > z) and (y > z) ? 1 : 0`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const tomorrow = new Date();
tomorrow.setDate(new Date().getDate() + 1);
await table.update({
a: [
new Date(2020, 3, 1, 12, 30, 45, 850),
new Date(2012, 0, 30, 19, 25),
new Date(2018, 9, 31, 3, 13),
tomorrow,
],
});
const results = await view.to_columns();
expect(
results[
`var x := now(); var y := now(); var z := "a"; (x > z) and (y > z) ? 1 : 0`
],
).toEqual([1, 1, 1, 0]);
await view.delete();
await table.delete();
});
test("Assign to declared column as var", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
// test type transitions as assignments happen - from str to int,
// from float to str, etc.
const view = await table.view({
expressions: [
`var x := "w"; var y := x; x := "y"; x;`,
`var x := "z"; var y := x; x := "w"; x;`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const results = await view.to_columns();
expect(results[`var x := "w"; var y := x; x := "y"; x;`]).toEqual([
"a",
"b",
"c",
"d",
]);
expect(results[`var x := "z"; var y := x; x := "w"; x;`]).toEqual([
1.5, 2.5, 3.5, 4.5,
]);
await view.delete();
await table.delete();
});
});
test.describe("Functionality", function () {
test("Should be able to create an expression column in `view()`", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: { '"w" + "x"': '"w" + "x"' },
});
const result = await view.to_columns();
expect(result['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
view.delete();
table.delete();
});
test("Should only parse first comment as an alias", async () => {
const table = await perspective.table({ a: [1, 2, 3] });
const view = await table.view({
expressions: [
"var x := 1 + 2;\n// another comment\nx + 3 + 4 # comment",
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const schema = await view.expression_schema();
expect(schema).toEqual({
"var x := 1 + 2;\n// another comment\nx + 3 + 4 # comment":
"float",
});
const result = await view.to_columns();
expect(
result[
"var x := 1 + 2;\n// another comment\nx + 3 + 4 # comment"
],
).toEqual(Array(3).fill(10));
await view.delete();
await table.delete();
});
test("Should be able to alias a real column in `view()`", async function () {
const table = await perspective.table(
expressions_common.all_types_arrow.slice(),
);
const columns = await table.columns();
const view = await table.view({
expressions: columns.map((col) => `"${col}"`),
});
const schema = await view.schema();
const expression_schema = await view.expression_schema();
const result = await view.to_columns();
for (const col of columns) {
expect(expression_schema[`"${col}"`]).toEqual(schema[col]);
expect(result[`"${col}"`]).toEqual(result[col]);
}
view.delete();
table.delete();
});
test("Should be able to create an expression column with scalars in `view()`", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: {
"1 + 2 + 3 + 4 + 5 + 6": "1 + 2 + 3 + 4 + 5 + 6",
},
});
const result = await view.to_columns();
expect(result["1 + 2 + 3 + 4 + 5 + 6"]).toEqual([21, 21, 21, 21]);
view.delete();
table.delete();
});
test("Should be able to create a string expression column with scalars in `view()`", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
group_by: ["'abc'"], // checks that the strings are interned
expressions: { "'abc'": "'abc'" },
});
const result = await view.to_columns();
expect(result["__ROW_PATH__"]).toEqual([[], ["abc"]]);
expect(result["'abc'"]).toEqual([4, 4]);
view.delete();
table.delete();
});
test("Should be able to create a boolean expression column with scalars in `view()`", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
"0 and 1",
"0 or 1",
"true and true",
"false or true",
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
expect(await view.schema()).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
"0 and 1": "boolean",
"0 or 1": "boolean",
"true and true": "boolean",
"false or true": "boolean",
});
const result = await view.to_columns();
expect(result["0 and 1"]).toEqual([false, false, false, false]);
expect(result["0 or 1"]).toEqual([true, true, true, true]);
expect(result["true and true"]).toEqual([true, true, true, true]);
expect(result["false or true"]).toEqual([true, true, true, true]);
view.delete();
table.delete();
});
test("Boolean scalars should not compare to numeric scalars", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
"0 and 1",
"0 or 1",
"true and true",
"false or true",
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
expect(await view.schema()).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
"0 and 1": "boolean",
"0 or 1": "boolean",
"true and true": "boolean",
"false or true": "boolean",
});
const result = await view.to_columns();
expect(result["0 and 1"]).toEqual([false, false, false, false]);
expect(result["0 or 1"]).toEqual([true, true, true, true]);
expect(result["true and true"]).toEqual([true, true, true, true]);
expect(result["false or true"]).toEqual([true, true, true, true]);
view.delete();
table.delete();
});
test("Should be able to create an expression column with scalars and columns `view()`", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
columns: ['("w" + "x") ^ 2'],
expressions: { '("w" + "x") ^ 2': '("w" + "x") ^ 2' },
});
const result = await view.to_columns();
expect(result['("w" + "x") ^ 2']).toEqual(
[2.5, 4.5, 6.5, 8.5].map((x) => Math.pow(x, 2)),
);
view.delete();
table.delete();
});
test("Should be able to create a boolean expression column with scalars and columns in `view()`", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [
'"x" and 1',
'"x" or 1',
'"x" and true',
'"x" and false',
'"x" or true',
'"x" or false',
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
expect(await view.schema()).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
'"x" and 1': "boolean",
'"x" or 1': "boolean",
'"x" and true': "boolean",
'"x" and false': "boolean",
'"x" or true': "boolean",
'"x" or false': "boolean",
});
const result = await view.to_columns();
expect(result['"x" and 1']).toEqual([true, true, true, true]);
expect(result['"x" or 1']).toEqual([true, true, true, true]);
expect(result['"x" and true']).toEqual([true, true, true, true]);
expect(result['"x" and false']).toEqual([
false,
false,
false,
false,
]);
expect(result['"x" or true']).toEqual([true, true, true, true]);
expect(result['"x" or false']).toEqual([true, true, true, true]);
view.delete();
table.delete();
});
test("Should be able to create an expression column in `view()` from schema, and updates propagate", async function () {
const table = await perspective.table({
w: "float",
x: "integer",
y: "string",
z: "boolean",
});
const view = await table.view({
expressions: { '("w" + "x") * 10': '("w" + "x") * 10' },
});
const result = await view.to_columns();
expect(result).toEqual({
'("w" + "x") * 10': [],
w: [],
x: [],
y: [],
z: [],
});
await table.update(expressions_common.int_float_data);
const new_result = await view.to_columns();
expect(new_result['("w" + "x") * 10']).toEqual([25, 45, 65, 85]);
await table.update(expressions_common.int_float_data);
const new_result2 = await view.to_columns();
expect(new_result2['("w" + "x") * 10']).toEqual([
25, 45, 65, 85, 25, 45, 65, 85,
]);
view.delete();
table.delete();
});
test("Should be able to create an expression column using an alias.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: { "new column": '"w" + "x"' },
});
const result = await view.to_columns();
expect(result["new column"]).toEqual([2.5, 4.5, 6.5, 8.5]);
await view.delete();
await table.delete();
});
test("Alias should be trimmed.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: { "new column": '"w" + "x"' },
});
const result = await view.to_columns();
expect(result["new column"]).toEqual([2.5, 4.5, 6.5, 8.5]);
const view2 = await table.view({
expressions: { "new column": '"w" - "x"' },
});
const result2 = await view2.to_columns();
expect(result2["new column"]).toEqual([0.5, 0.5, 0.5, 0.5]);
await view2.delete();
await view.delete();
await table.delete();
});
test("Should be able to create an expression column using a very long alias.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: {
"new column with lots of different strings and names and lots of good characters":
'"w" + "x"',
},
});
const result = await view.to_columns();
expect(
result[
"new column with lots of different strings and names and lots of good characters"
],
).toEqual([2.5, 4.5, 6.5, 8.5]);
await view.delete();
await table.delete();
});
test("Comments after the alias are not picked up.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: {
alias: 'var x := "w" + "x"\n// a comment\n# another comment\n/* another comment here */\n',
},
});
const result = await view.to_columns();
expect(result["alias"]).toEqual([2.5, 4.5, 6.5, 8.5]);
await view.delete();
await table.delete();
});
test("Duplicate alias within the same view should resolve to the last expression.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: {
"new column": '"w" + "x"',
"new column 1": '"w" - "x"',
"new column": "100 * 10",
"new column 1": '"w" + "x"',
"new column": '"w" - "x"',
},
});
expect(await view.expression_schema()).toEqual({
"new column": "float",
"new column 1": "float",
});
const result = await view.to_columns();
expect(result["new column"]).toEqual([0.5, 0.5, 0.5, 0.5]);
expect(result["new column 1"]).toEqual([2.5, 4.5, 6.5, 8.5]);
await view.delete();
await table.delete();
});
test("Duplicate alias within the same view should resolve to the last expression, different types.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: {
"new column": '"w" + "x"',
"new column 1": '"w" - "x"',
"new column": "100",
"new column": "upper('abc')",
"new column 1": '"w" + "x"',
},
});
expect(await view.expression_schema()).toEqual({
"new column": "string",
"new column 1": "float",
});
const result = await view.to_columns();
expect(result["new column"]).toEqual(Array(4).fill("ABC"));
expect(result["new column 1"]).toEqual([2.5, 4.5, 6.5, 8.5]);
await view.delete();
await table.delete();
});
test("Duplicate alias across new views will not overwrite results in old view.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: { "new column": '"w" + "x"' },
});
let result = await view.to_columns();
expect(result["new column"]).toEqual([2.5, 4.5, 6.5, 8.5]);
const view2 = await table.view({
expressions: { "new column": "100 * 10" },
});
expect(await view.expression_schema()).toEqual(
await view2.expression_schema(),
);
result = await view.to_columns();
const result2 = await view2.to_columns();
expect(result["new column"]).toEqual([2.5, 4.5, 6.5, 8.5]);
expect(result2["new column"]).toEqual([1000, 1000, 1000, 1000]);
const view3 = await table.view({
expressions: { "new column": "100 * 100" },
group_by: ["y"],
});
const result3 = await view3.to_columns();
expect(result3["new column"]).toEqual([
40000, 10000, 10000, 10000, 10000,
]);
await view3.delete();
await view2.delete();
await view.delete();
await table.delete();
});
test("Should not be able to overwrite table column with an expression", async function () {
expect.assertions(1);
const table = await perspective.table(
expressions_common.int_float_data,
);
let emit;
let emitReject;
let result = new Promise((success, reject) => {
emit = success;
emitReject = reject;
});
table
.view({
expressions: { w: "upper('abc')" },
})
.catch((e) => {
expect(e.message.split("\n")[0]).toMatch(
`Abort(): Value Error - expression \"w\" cannot overwrite an existing column.`,
);
table.delete();
emit();
})
.then(() => {
emitReject("Promise should have thrown an error");
});
await result;
});
test("Should be able to overwrite expression column with one that returns a different type", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: { "new column": '"w" + "x"' },
});
let result = await view.to_columns();
expect(result["new column"]).toEqual([2.5, 4.5, 6.5, 8.5]);
const view2 = await table.view({
expressions: { "new column": " upper('abc')" },
});
const result2 = await view2.to_columns();
expect(result2["new column"]).toEqual(Array(4).fill("ABC"));
view2.delete();
view.delete();
table.delete();
});
test("A new view should not reference expression columns it did not create.", async function () {
expect.assertions(2);
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: { '"w" + "x"': '"w" + "x"' },
});
const result = await view.to_columns();
expect(result['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
let emit;
let result2 = new Promise((x) => {
emit = x;
});
table
.view({
columns: ['"w" + "x"', "x"],
})
.catch((e) => {
expect(e.message).toMatch(
`Abort(): Invalid column '"w" + "x"' found in View columns.\n`,
);
view.delete();
table.delete();
emit();
});
await result2;
});
test("A view should be able to shadow real columns with an expression column", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: {
'"w"': '"w"',
'"x"': '"x"',
'"y"': '"y"',
'"z"': '"z"',
},
});
expect(await view.expression_schema()).toEqual({
'"w"': "float",
'"x"': "integer",
'"y"': "string",
'"z"': "boolean",
});
expect(await view.to_json()).toEqual([
{
w: 1.5,
x: 1,
y: "a",
z: true,
'"w"': 1.5,
'"x"': 1,
'"y"': "a",
'"z"': true,
},
{
w: 2.5,
x: 2,
y: "b",
z: false,
'"w"': 2.5,
'"x"': 2,
'"y"': "b",
'"z"': false,
},
{
w: 3.5,
x: 3,
y: "c",
z: true,
'"w"': 3.5,
'"x"': 3,
'"y"': "c",
'"z"': true,
},
{
w: 4.5,
x: 4,
y: "d",
z: false,
'"w"': 4.5,
'"x"': 4,
'"y"': "d",
'"z"': false,
},
]);
view.delete();
table.delete();
});
test("Should be able to create multiple expression columns in `view()`", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: {
'"w" + "x"': '"w" + "x"',
'upper("y")': 'upper("y")',
},
});
expect(await view.expression_schema()).toEqual({
'"w" + "x"': "float",
'upper("y")': "string",
});
const result = await view.to_columns();
expect(result['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
expect(result['upper("y")']).toEqual(["A", "B", "C", "D"]);
view.delete();
table.delete();
});
test("Should be able to create multiple expression columns with unique names in multiple `view()`s", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: {
'"w" + "x"': '"w" + "x"',
'"w" - "x"': '"w" - "x"',
'"w" * "x"': '"w" * "x"',
},
});
const view2 = await table.view({
expressions: {
'"w" / "x"': '"w" / "x"',
'"x" * "w"': '"x" * "w"',
},
});
const schema = await view.schema();
const schema2 = await view2.schema();
expect(schema).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
'"w" + "x"': "float",
'"w" - "x"': "float",
'"w" * "x"': "float",
});
expect(schema2).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
'"w" / "x"': "float",
'"x" * "w"': "float",
});
const result = await view.to_columns();
const result2 = await view2.to_columns();
expect(result).toEqual({
w: [1.5, 2.5, 3.5, 4.5],
x: [1, 2, 3, 4],
y: ["a", "b", "c", "d"],
z: [true, false, true, false],
'"w" + "x"': [2.5, 4.5, 6.5, 8.5],
'"w" - "x"': [0.5, 0.5, 0.5, 0.5],
'"w" * "x"': [1.5, 5, 10.5, 18],
});
expect(result2).toEqual({
w: [1.5, 2.5, 3.5, 4.5],
x: [1, 2, 3, 4],
y: ["a", "b", "c", "d"],
z: [true, false, true, false],
'"w" / "x"': [1.5, 1.25, 1.1666666666666667, 1.125],
'"x" * "w"': [1.5, 5, 10.5, 18],
});
view2.delete();
view.delete();
table.delete();
});
test("Should be able to create multiple expression columns in multiple `view()`s, and arbitarily delete views.", async function () {
const table = await perspective.table(
expressions_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 schema = await view.schema();
expect(schema).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
'"w" + "x"': "float",
});
const result = await view.to_columns();
expect(result).toEqual({
w: [1.5, 2.5, 3.5, 4.5],
x: [1, 2, 3, 4],
y: ["a", "b", "c", "d"],
z: [true, false, true, false],
'"w" + "x"': [2.5, 4.5, 6.5, 8.5],
});
view.delete();
// force a process()
expect(await table.size()).toEqual(4);
const schema2 = await view2.schema();
expect(schema2).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
'"w" - "x"': "float",
});
const result2 = await view2.to_columns();
expect(result2).toEqual({
w: [1.5, 2.5, 3.5, 4.5],
x: [1, 2, 3, 4],
y: ["a", "b", "c", "d"],
z: [true, false, true, false],
'"w" - "x"': [0.5, 0.5, 0.5, 0.5],
});
view2.delete();
table.delete();
});
test("Should be able to create multiple duplicate expression columns in multiple `view()`s, and delete preceding views without affecting later columns.", async function () {
const table = await perspective.table(
expressions_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 schema = await view.schema();
expect(schema).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
'"w" + "x"': "float",
});
const result = await view.to_columns();
expect(result).toEqual({
w: [1.5, 2.5, 3.5, 4.5],
x: [1, 2, 3, 4],
y: ["a", "b", "c", "d"],
z: [true, false, true, false],
'"w" + "x"': [2.5, 4.5, 6.5, 8.5],
});
view.delete();
const schema2 = await view2.schema();
// force a process()
expect(await table.size()).toEqual(4);
expect(schema2).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
'"w" + "x"': "float",
'"w" - "x"': "float",
});
const result2 = await view2.to_columns();
expect(result2).toEqual({
w: [1.5, 2.5, 3.5, 4.5],
x: [1, 2, 3, 4],
y: ["a", "b", "c", "d"],
z: [true, false, true, false],
'"w" + "x"': [2.5, 4.5, 6.5, 8.5],
'"w" - "x"': [0.5, 0.5, 0.5, 0.5],
});
view2.delete();
table.delete();
});
test("Multiple views inheriting the same expression columns with the same names should not conflict", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: { '"w" + "x"': '"w" + "x"' },
});
expect(await view.schema()).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
'"w" + "x"': "float",
});
const view2 = await table.view({
expressions: {
'"w" + "x"': '"w" + "x"',
'"w" - "x"': '"w" - "x"',
},
});
expect(await view2.schema()).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
'"w" + "x"': "float",
'"w" - "x"': "float",
});
const view3 = await table.view({
expressions: {
'"w" + "x"': '"w" + "x"',
'"w" - "x"': '"w" - "x"',
'("w" - "x") + "x"': '("w" - "x") + "x"',
},
});
expect(await view3.schema()).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
'"w" + "x"': "float",
'"w" - "x"': "float",
'("w" - "x") + "x"': "float",
});
const result = await view3.to_columns();
expect(result['("w" - "x") + "x"']).toEqual(result["w"]);
view3.delete();
view2.delete();
view.delete();
table.delete();
});
test("A view should be able to create an expression column with the same name as another deleted view's expression columns.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: { '"w" + "x"': '"w" + "x"' },
});
const result = await view.to_columns();
expect(result['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
view.delete();
const view2 = await table.view({
expressions: { '"w" - "x"': '"w" - "x"' },
});
const result2 = await view2.to_columns();
expect(result2['"w" - "x"']).toEqual([0.5, 0.5, 0.5, 0.5]);
view2.delete();
table.delete();
});
test("A view without expression columns should not serialize expression columns from other views.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: { '"w" + "x"': '"w" + "x"' },
});
const result = await view.to_columns();
expect(result['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
view.delete();
const view2 = await table.view();
const result2 = await view2.to_json();
expect(result2).toEqual(expressions_common.int_float_data);
view2.delete();
table.delete();
});
test("When expression columns are repeated between views, column indices should grow linearly.", async function () {
let expressions = [
'"w" + "x"',
'"w" - "x"',
'"w" * "x"',
'"w" / "x"',
];
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: [expressions[0]].reduce(
(x, y) => Object.assign(x, { [y]: y }),
{},
),
});
const view2 = await table.view({
expressions: [expressions[0], expressions[1]].reduce(
(x, y) => Object.assign(x, { [y]: y }),
{},
),
});
const view3 = await table.view({
expressions: [
expressions[0],
expressions[1],
expressions[2],
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const view4 = await table.view({ expressions: expressions });
const schema = await view.schema();
const schema2 = await view2.schema();
const schema3 = await view3.schema();
const schema4 = await view4.schema();
expect(schema).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
'"w" + "x"': "float",
});
expect(schema2).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
'"w" + "x"': "float",
'"w" - "x"': "float",
});
expect(schema3).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
'"w" + "x"': "float",
'"w" - "x"': "float",
'"w" * "x"': "float",
});
expect(schema4).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
'"w" + "x"': "float",
'"w" - "x"': "float",
'"w" * "x"': "float",
'"w" / "x"': "float",
});
view4.delete();
view3.delete();
view2.delete();
view.delete();
table.delete();
});
test("Should be able to create duplicate expressions in multiple views, and updates should propagate to both", async function () {
const table = await perspective.table(
expressions_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 schema = await view.schema();
const schema2 = await view2.schema();
expect(schema).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
'"w" + "x"': "float",
});
expect(schema2).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
'"w" + "x"': "float",
});
const result = await view.to_columns();
const result2 = await view2.to_columns();
expect(result).toEqual({
w: [1.5, 2.5, 3.5, 4.5],
x: [1, 2, 3, 4],
y: ["a", "b", "c", "d"],
z: [true, false, true, false],
'"w" + "x"': [2.5, 4.5, 6.5, 8.5],
});
expect(result2).toEqual({
w: [1.5, 2.5, 3.5, 4.5],
x: [1, 2, 3, 4],
y: ["a", "b", "c", "d"],
z: [true, false, true, false],
'"w" + "x"': [2.5, 4.5, 6.5, 8.5],
});
await table.update({
w: [5, 6, 7, 8],
x: [1, 2, 3, 4],
});
expect(await view.to_columns()).toEqual({
w: [1.5, 2.5, 3.5, 4.5, 5, 6, 7, 8],
x: [1, 2, 3, 4, 1, 2, 3, 4],
y: ["a", "b", "c", "d", null, null, null, null],
z: [true, false, true, false, null, null, null, null],
'"w" + "x"': [2.5, 4.5, 6.5, 8.5, 6, 8, 10, 12],
});
expect(await view2.to_columns()).toEqual({
w: [1.5, 2.5, 3.5, 4.5, 5, 6, 7, 8],
x: [1, 2, 3, 4, 1, 2, 3, 4],
y: ["a", "b", "c", "d", null, null, null, null],
z: [true, false, true, false, null, null, null, null],
'"w" + "x"': [2.5, 4.5, 6.5, 8.5, 6, 8, 10, 12],
});
view2.delete();
view.delete();
table.delete();
});
test("A new view should not inherit expression columns if not created.", async function () {
expect.assertions(2);
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: { '"w" + "x"': '"w" + "x"' },
});
const result = await view.to_columns();
expect(result['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
try {
await table.view({
columns: ['"w" + "x"', "x"],
});
} catch (e) {
expect(e.message.split("\n")[0]).toEqual(
`Abort(): Invalid column '"w" + "x"' found in View columns.`,
);
}
view.delete();
table.delete();
});
test("The view's underlying table should not have a mutated schema.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
expressions: { '"w" + "x"': '"w" + "x"' },
});
const result = await view.to_columns();
expect(result['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
expect(await table.schema()).toEqual({
w: "float",
x: "integer",
y: "string",
z: "boolean",
});
view.delete();
table.delete();
});
test("Should be able to show an expression column.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
columns: ['"w" + "x"'],
expressions: { '"w" + "x"': '"w" + "x"' },
});
const result = await view.to_columns();
expect(result['"w" + "x"']).toEqual([2.5, 4.5, 6.5, 8.5]);
view.delete();
table.delete();
});
test("Should be able to hide an expression column.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
columns: ["x"],
expressions: { '"w" + "x"': '"w" + "x"' },
});
expect(await view.schema()).toEqual({
x: "integer",
});
expect(await view.expression_schema()).toEqual({
'"w" + "x"': "float",
});
const result = await view.to_columns();
expect(result['"w" + "x"']).toEqual(undefined);
expect(result["x"]).toEqual([1, 2, 3, 4]);
view.delete();
table.delete();
});
test("Should be able to group by on a non-expression column and get correct results for the expression column.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
group_by: ["w"],
expressions: { '"w" + "x"': '"w" + "x"' },
});
const result = await view.to_columns();
expect(result).toEqual({
__ROW_PATH__: [[], [1.5], [2.5], [3.5], [4.5]],
'"w" + "x"': [22, 2.5, 4.5, 6.5, 8.5],
w: [12, 1.5, 2.5, 3.5, 4.5],
x: [10, 1, 2, 3, 4],
y: [4, 1, 1, 1, 1],
z: [4, 1, 1, 1, 1],
});
view.delete();
table.delete();
});
test("Should be able to group by on an expression column.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
group_by: ['"w" + "x"'],
expressions: { '"w" + "x"': '"w" + "x"' },
});
const result = await view.to_columns();
expect(result).toEqual({
__ROW_PATH__: [[], [2.5], [4.5], [6.5], [8.5]],
'"w" + "x"': [22, 2.5, 4.5, 6.5, 8.5],
w: [12, 1.5, 2.5, 3.5, 4.5],
x: [10, 1, 2, 3, 4],
y: [4, 1, 1, 1, 1],
z: [4, 1, 1, 1, 1],
});
view.delete();
table.delete();
});
test("Row-pivoted expression columns return correct column_paths()", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
// default order
let view = await table.view({
group_by: ["y"],
expressions: { column: '"w" + "x"' },
});
let paths = await view.column_paths();
expect(paths).toEqual(["w", "x", "y", "z", "column"]);
await view.delete();
const expected_paths = [
["x", "column"],
["column"],
["x", "column", "y"],
];
for (const expected of expected_paths) {
const output = expected.slice();
view = await table.view({
group_by: ["y"],
expressions: { column: '"w" + "x"' },
columns: expected,
});
paths = await view.column_paths();
expect(paths).toEqual(output);
view.delete();
}
for (const expected of expected_paths) {
const output = expected.slice();
view = await table.view({
group_by: ["column"],
expressions: { column: '"w" + "x"' },
columns: expected,
});
paths = await view.column_paths();
expect(paths).toEqual(output);
view.delete();
}
table.delete();
});
test("Row-pivoted numeric expression columns return correct column_paths()", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const config = {
group_by: ["y"],
expressions: { 1234: "1234" },
aggregates: {
x: "sum",
y: "count",
1234: "sum",
},
};
// default order
let view = await table.view(config);
let paths = await view.column_paths();
expect(paths).toEqual(["w", "x", "y", "z", "1234"]);
await view.delete();
const expected_paths = [
["x", "1234"],
["1234"],
["x", "1234", "y"],
];
for (const expected of expected_paths) {
const output = expected.slice();
view = await table.view({
...config,
columns: expected,
});
paths = await view.column_paths();
expect(paths).toEqual(output);
view.delete();
}
for (const expected of expected_paths) {
const output = expected.slice();
view = await table.view({
...config,
columns: expected,
});
paths = await view.column_paths();
expect(paths).toEqual(output);
view.delete();
}
table.delete();
});
test("Should be able to split by on an expression column.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
split_by: ['"w" + "x"'],
expressions: { '"w" + "x"': '"w" + "x"' },
});
const result = await view.to_columns();
expect(result).toEqual({
"2.5|w": [1.5, null, null, null],
"2.5|x": [1, null, null, null],
"2.5|y": ["a", null, null, null],
"2.5|z": [true, null, null, null],
'2.5|"w" + "x"': [2.5, null, null, null],
"4.5|w": [null, 2.5, null, null],
"4.5|x": [null, 2, null, null],
"4.5|y": [null, "b", null, null],
"4.5|z": [null, false, null, null],
'4.5|"w" + "x"': [null, 4.5, null, null],
"6.5|w": [null, null, 3.5, null],
"6.5|x": [null, null, 3, null],
"6.5|y": [null, null, "c", null],
"6.5|z": [null, null, true, null],
'6.5|"w" + "x"': [null, null, 6.5, null],
"8.5|w": [null, null, null, 4.5],
"8.5|x": [null, null, null, 4],
"8.5|y": [null, null, null, "d"],
"8.5|z": [null, null, null, false],
'8.5|"w" + "x"': [null, null, null, 8.5],
});
view.delete();
table.delete();
});
test("Should be able to row + split by on an expression column.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
group_by: ['"w" + "x"'],
split_by: ['"w" + "x"'],
expressions: { '"w" + "x"': '"w" + "x"' },
});
const result = await view.to_columns();
expect(result).toEqual({
__ROW_PATH__: [[], [2.5], [4.5], [6.5], [8.5]],
"2.5|w": [1.5, 1.5, null, null, null],
"2.5|x": [1, 1, null, null, null],
"2.5|y": [1, 1, null, null, null],
"2.5|z": [1, 1, null, null, null],
'2.5|"w" + "x"': [2.5, 2.5, null, null, null],
"4.5|w": [2.5, null, 2.5, null, null],
"4.5|x": [2, null, 2, null, null],
"4.5|y": [1, null, 1, null, null],
"4.5|z": [1, null, 1, null, null],
'4.5|"w" + "x"': [4.5, null, 4.5, null, null],
"6.5|w": [3.5, null, null, 3.5, null],
"6.5|x": [3, null, null, 3, null],
"6.5|y": [1, null, null, 1, null],
"6.5|z": [1, null, null, 1, null],
'6.5|"w" + "x"': [6.5, null, null, 6.5, null],
"8.5|w": [4.5, null, null, null, 4.5],
"8.5|x": [4, null, null, null, 4],
"8.5|y": [1, null, null, null, 1],
"8.5|z": [1, null, null, null, 1],
'8.5|"w" + "x"': [8.5, null, null, null, 8.5],
});
view.delete();
table.delete();
});
test("Should be able to aggregate a numeric expression column.", async function () {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: [100, 200, 300, 400],
z: [1.5, 2.5, 3.5, 4.5],
});
const view = await table.view({
group_by: ['"x" + "z"'],
aggregates: {
'"x" + "z"': "median",
x: "median",
},
expressions: { '"x" + "z"': '"x" + "z"' },
});
const result = await view.to_columns();
expect(result).toEqual({
__ROW_PATH__: [[], [2.5], [4.5], [6.5], [8.5]],
'"x" + "z"': [5.5, 2.5, 4.5, 6.5, 8.5],
x: [3, 1, 2, 3, 4],
y: [1000, 100, 200, 300, 400],
z: [12, 1.5, 2.5, 3.5, 4.5],
});
view.delete();
table.delete();
});
test("Should be able to weighted mean aggregate a numeric expression column.", async function () {
const table = await perspective.table({
x: [2, 2, 4, 4],
y: [100, 200, 300, 400],
z: [1.5, 2.5, 3.5, 4.5],
});
const view = await table.view({
group_by: ["y"],
columns: ['"x" + "z"'],
aggregates: {
'"x" + "z"': ["weighted mean", ["y"]],
},
expressions: { '"x" + "z"': '"x" + "z"' },
});
const result = await view.to_columns();
expect(result).toEqual({
__ROW_PATH__: [[], [100], [200], [300], [400]],
'"x" + "z"': [6.9, 3.5, 4.5, 7.5, 8.5],
});
view.delete();
table.delete();
});
test("Should be able to aggregate a numeric expression column that aliases a real column.", async function () {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: [100, 200, 300, 400],
z: [1.5, 2.5, 3.5, 4.5],
});
const view = await table.view({
group_by: ['"x"'],
aggregates: {
'"x"': "median",
x: "median",
},
expressions: { '"x"': '"x"' },
});
const result = await view.to_columns();
expect(result).toEqual({
__ROW_PATH__: [[], [1], [2], [3], [4]],
'"x"': [3, 1, 2, 3, 4],
x: [3, 1, 2, 3, 4],
y: [1000, 100, 200, 300, 400],
z: [12, 1.5, 2.5, 3.5, 4.5],
});
view.delete();
table.delete();
});
test("Should be able to aggregate a string expression column.", async function () {
const table = await perspective.table({
x: ["a", "a", "c", "a"],
y: ["w", "w", "y", "w"],
});
const view = await table.view({
group_by: ['upper("x")'],
aggregates: {
'upper("x")': "distinct count",
},
expressions: { 'upper("x")': 'upper("x")' },
});
const result = await view.to_columns();
expect(result).toEqual({
__ROW_PATH__: [[], ["A"], ["C"]],
'upper("x")': [2, 1, 1],
x: [4, 3, 1],
y: [4, 3, 1],
});
view.delete();
table.delete();
});
test("Should be able to aggregate a date expression column.", async function () {
const table = await perspective.table({
x: [
new Date(2019, 0, 15),
new Date(2019, 0, 30),
new Date(2019, 1, 15),
],
});
const view = await table.view({
group_by: [`bucket("x", 'M')`],
aggregates: {
"bucket(\"x\", 'M')": "distinct count",
},
expressions: { "bucket(\"x\", 'M')": "bucket(\"x\", 'M')" },
});
const result = await view.to_columns();
expect(result).toEqual({
__ROW_PATH__: [[], [1546300800000], [1548979200000]],
"bucket(\"x\", 'M')": [2, 1, 1],
x: [3, 2, 1],
});
view.delete();
table.delete();
});
test("Should be able to weighted mean on an expression column.", async function () {
const table = await perspective.table({
x: [1, 2, 3, 4],
y: [100, 200, 300, 400],
z: [1.5, 2.5, 3.5, 4.5],
});
const view = await table.view({
group_by: ['"x" + "z"'],
aggregates: {
x: ["weighted mean", ['"x" + "z"']],
'"x" + "z"': ["weighted mean", ["y"]],
},
expressions: { '"x" + "z"': '"x" + "z"' },
});
const result = await view.to_columns();
expect(result).toEqual({
__ROW_PATH__: [[], [2.5], [4.5], [6.5], [8.5]],
'"x" + "z"': [6.5, 2.5, 4.5, 6.5, 8.5],
x: [2.9545454545454546, 1, 2, 3, 4],
y: [1000, 100, 200, 300, 400],
z: [12, 1.5, 2.5, 3.5, 4.5],
});
view.delete();
table.delete();
});
test("Should be able to filter on an expression column.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
filter: [['"w" + "x"', ">", 6.5]],
expressions: { '"w" + "x"': '"w" + "x"' },
});
const result = await view.to_columns();
expect(result).toEqual({
'"w" + "x"': [8.5],
w: [4.5],
x: [4],
y: ["d"],
z: [false],
});
view.delete();
table.delete();
});
test("Should be able to sort on an expression column.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
sort: [['"w" + "x"', "desc"]],
expressions: { '"w" + "x"': '"w" + "x"' },
});
const result = await view.to_columns();
expect(result).toEqual({
'"w" + "x"': [8.5, 6.5, 4.5, 2.5],
w: [4.5, 3.5, 2.5, 1.5],
x: [4, 3, 2, 1],
y: ["d", "c", "b", "a"],
z: [false, true, false, true],
});
view.delete();
table.delete();
});
test("Should be able to sort on a hidden expression column.", async function () {
const table = await perspective.table(
expressions_common.int_float_data,
);
const view = await table.view({
columns: ["w"],
sort: [['"w" + "x"', "desc"]],
expressions: { '"w" + "x"': '"w" + "x"' },
});
const result = await view.to_columns();
expect(result).toEqual({
w: [4.5, 3.5, 2.5, 1.5],
});
view.delete();
table.delete();
});
test.describe("Validation using table expression schema", function () {
test("Should show correct expression column types in table expression schema.", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [new Date(), new Date(), new Date(), new Date()],
c: ["a", "b", "c", "d"],
});
const expressions = [
'"a" ^ 2',
"sqrt(144)",
"0 and 1",
"0 or 1",
'-"a"',
'upper("c")',
`bucket("b", 'M')`,
`bucket("b", 's')`,
];
const results = await table.validate_expressions(expressions);
expect(results.expression_schema).toEqual({
'"a" ^ 2': "float",
"sqrt(144)": "float",
'-"a"': "integer",
"0 and 1": "boolean",
"0 or 1": "boolean",
'upper("c")': "string",
"bucket(\"b\", 'M')": "date",
"bucket(\"b\", 's')": "datetime",
});
table.delete();
});
test("Should skip expressions that try to overwrite table columns", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [new Date(), new Date(), new Date(), new Date()],
c: ["a", "b", "c", "d"],
d: [1.5, 2.5, 3.5, 4.5],
});
const expressions = {
abc: " 123 + 345", //valid
a: " sqrt(144)", // invalid
b: " upper('abc')", // invalid
c: " concat('a', 'b')", // invalid
d: " today()", // invalid
};
const results = await table.validate_expressions(expressions);
expect(results.expression_schema).toEqual({
abc: "float",
});
table.delete();
});
test("Should not skip expressions that try to overwrite expressions with different types", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [new Date(), new Date(), new Date(), new Date()],
c: ["a", "b", "c", "d"],
d: [1.5, 2.5, 3.5, 4.5],
});
const view = await table.view({
expressions: {
abc: " 123 + 345",
def: " lower('ABC')",
},
});
const results = await table.validate_expressions({
abc: " upper('abc')",
def: " 1 + 2",
});
expect(results.expression_schema).toEqual({
abc: "string",
def: "float",
});
view.delete();
table.delete();
});
test("Should skip invalid columns due to type error, with alias", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [new Date(), new Date(), new Date(), new Date()],
c: ["a", "b", "c", "d"],
d: [1.5, 2.5, 3.5, 4.5],
});
const expressions = {
abc: '"a" + "d"', // valid
def: '"c" + "b"', // invalid
'"c"': '"c"', // valid
"new column": "bucket(\"b\", 'M')", // valid
"bucket(\"b\", 'abcde')": "bucket(\"b\", 'abcde')", // invalid
'concat("c", "a")': 'concat("c", "a")', // invalid
"more columns": "concat(\"c\", ' ', \"c\", 'abc')", // valid
'upper("c")': 'upper("c")', // valid
'lower("a")': 'lower("a")', // invalid,
'min("a", "c")': 'min("a", "c")', // invalid,
'max(100, "a")': 'max(100, "a")', // valid
};
const results = await table.validate_expressions(expressions);
expect(results.expression_schema).toEqual({
abc: "float",
'"c"': "string",
"new column": "date",
'upper("c")': "string",
'max(100, "a")': "float",
"more columns": "string",
});
for (const alias of Object.keys(expressions)) {
expect(results.expression_alias[alias]).toEqual(
expressions[alias],
);
}
table.delete();
});
test("Should skip invalid columns due to type error", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [new Date(), new Date(), new Date(), new Date()],
c: ["a", "b", "c", "d"],
d: [1.5, 2.5, 3.5, 4.5],
});
const expressions = [
'"a" + "d"', // valid
'"c" + "b"', // invalid
'"c"', // valid
"bucket(\"b\", 'M')", // valid
"bucket(\"b\", 'abcde')", // invalid
'concat("c", "a")', // invalid
"concat(\"c\", ' ', \"c\", 'abc')", // valid
'upper("c")', // valid
'lower("a")', // invalid,
'min("a", "c")', // invalid,
'max(100, "a")', // valid
];
const results = await table.validate_expressions(expressions);
expect(results.expression_schema).toEqual({
'"a" + "d"': "float",
'"c"': "string",
"bucket(\"b\", 'M')": "date",
'upper("c")': "string",
'max(100, "a")': "float",
"concat(\"c\", ' ', \"c\", 'abc')": "string",
});
for (const expr of expressions) {
expect(results.expression_alias[expr]).toEqual(expr);
}
table.delete();
});
test("Should skip a invalid column due to invalid column name", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [new Date(), new Date(), new Date(), new Date()],
c: ["a", "b", "c", "d"],
d: [1.5, 2.5, 3.5, 4.5],
});
const expressions = [
'"a" + "d"', // valid
'"abxbasd" + "sdfadsf"', // invalid
'"c"', // valid
"bucket(\"b\", 'M')", // valid
"bucket(\"basdsa\", 'Y')", // invalid
'upper("c")', // valid
'lower("sdfadsj")', // invalid
];
const results = await table.validate_expressions(expressions);
expect(results.expression_schema).toEqual({
'"a" + "d"': "float",
'"c"': "string",
"bucket(\"b\", 'M')": "date",
'upper("c")': "string",
});
for (const expr of expressions) {
expect(results.expression_alias[expr]).toEqual(expr);
}
table.delete();
});
test("Should skip a invalid column due to parse error", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [new Date(), new Date(), new Date(), new Date()],
c: ["a", "b", "c", "d"],
d: [1.5, 2.5, 3.5, 4.5],
});
const expressions = [
'"a" + "d"', // valid
"{", // invalid
"if (\"c\" == 'a') 123; else 0;", // valid
"if (", // invalid
];
const results = await table.validate_expressions(expressions);
expect(results.expression_schema).toEqual({
'"a" + "d"': "float",
"if (\"c\" == 'a') 123; else 0;": "float",
});
table.delete();
});
});
test.describe("View expression schema", function () {
test("Column types in view schema on 0-sided view.", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [new Date(), new Date(), new Date(), new Date()],
c: ["a", "b", "c", "d"],
});
const view = await table.view({
expressions: [
'"a" ^ 2',
"sqrt(144)",
"0 and 1",
"0 or 1",
'-"a"',
'upper("c")',
`bucket("b", 'M')`,
`bucket("b", 's')`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const schema = await view.schema();
expect(schema).toEqual({
a: "integer",
b: "datetime",
c: "string",
'"a" ^ 2': "float",
"sqrt(144)": "float",
'-"a"': "integer",
"0 and 1": "boolean",
"0 or 1": "boolean",
'upper("c")': "string",
"bucket(\"b\", 'M')": "date",
"bucket(\"b\", 's')": "datetime",
});
view.delete();
table.delete();
});
test("Column types in view expression schema on 0-sided view.", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [new Date(), new Date(), new Date(), new Date()],
c: ["a", "b", "c", "d"],
});
const view = await table.view({
expressions: [
'"a" ^ 2',
"sqrt(144)",
"0 and 1",
"0 or 1",
'-"a"',
'upper("c")',
`bucket("b", 'M')`,
`bucket("b", 's')`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const schema = await view.expression_schema();
expect(schema).toEqual({
'"a" ^ 2': "float",
"sqrt(144)": "float",
'-"a"': "integer",
"0 and 1": "boolean",
"0 or 1": "boolean",
'upper("c")': "string",
"bucket(\"b\", 'M')": "date",
"bucket(\"b\", 's')": "datetime",
});
view.delete();
table.delete();
});
test("Should show ALL columns in view expression schema regardless of custom columns.", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [new Date(), new Date(), new Date(), new Date()],
c: ["a", "b", "c", "d"],
});
const view = await table.view({
columns: ["a"],
expressions: [
'"a" ^ 2',
"sqrt(144)",
"0 and 1",
"0 or 1",
'-"a"',
'upper("c")',
`bucket("b", 'M')`,
`bucket("b", 's')`,
`bucket("b",'M')`,
`bucket("b",'s')`,
`bucket("b", 'M')`,
`bucket("b", 's')`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const schema = await view.expression_schema();
expect(schema).toEqual({
'"a" ^ 2': "float",
"sqrt(144)": "float",
'-"a"': "integer",
"0 and 1": "boolean",
"0 or 1": "boolean",
'upper("c")': "string",
"bucket(\"b\", 'M')": "date",
"bucket(\"b\", 's')": "datetime",
"bucket(\"b\",'M')": "date",
"bucket(\"b\",'s')": "datetime",
"bucket(\"b\", 'M')": "date",
"bucket(\"b\", 's')": "datetime",
});
view.delete();
table.delete();
});
test("Aggregated types in view expression schema on 1-sided view.", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [new Date(), new Date(), new Date(), new Date()],
c: ["a", "b", "c", "d"],
});
const view = await table.view({
group_by: ["c"],
expressions: [
'"a" ^ 2',
"sqrt(144)",
"0 and 1",
"0 or 1",
'-"a"',
'upper("c")',
`bucket("b", 'M')`,
`bucket("b", 's')`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const schema = await view.expression_schema();
expect(schema).toEqual({
'"a" ^ 2': "float",
"sqrt(144)": "float",
'-"a"': "integer",
"0 and 1": "integer",
"0 or 1": "integer",
'upper("c")': "integer",
"bucket(\"b\", 'M')": "integer",
"bucket(\"b\", 's')": "integer",
});
view.delete();
table.delete();
});
test("Aggregated types in view expression schema on 1-sided view with custom aggregates.", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [new Date(), new Date(), new Date(), new Date()],
c: ["a", "b", "c", "d"],
});
const view = await table.view({
group_by: ["c"],
aggregates: {
"0 and 1": "any",
"0 or 1": "any",
"bucket(\"b\", 's')": "last",
},
expressions: [
'"a" ^ 2',
"sqrt(144)",
"0 and 1",
"0 or 1",
'-"a"',
'upper("c")',
`bucket("b", 'M')`,
`bucket("b", 's')`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const schema = await view.expression_schema();
expect(schema).toEqual({
'"a" ^ 2': "float",
"sqrt(144)": "float",
'-"a"': "integer",
"0 and 1": "boolean",
"0 or 1": "boolean",
'upper("c")': "integer",
"bucket(\"b\", 'M')": "integer",
"bucket(\"b\", 's')": "datetime",
});
view.delete();
table.delete();
});
test("Aggregated types in view expression schema on 2-sided view.", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [new Date(), new Date(), new Date(), new Date()],
c: ["a", "b", "c", "d"],
});
const view = await table.view({
group_by: ["c"],
split_by: ["a"],
expressions: [
'"a" ^ 2',
"sqrt(144)",
"0 and 1",
"0 or 1",
'-"a"',
'upper("c")',
`bucket("b", 'M')`,
`bucket("b", 's')`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const schema = await view.expression_schema();
expect(schema).toEqual({
'"a" ^ 2': "float",
"sqrt(144)": "float",
'-"a"': "integer",
"0 and 1": "integer", // aggregated bool = count
"0 or 1": "integer",
'upper("c")': "integer",
"bucket(\"b\", 'M')": "integer",
"bucket(\"b\", 's')": "integer",
});
view.delete();
table.delete();
});
test("Aggregated types in view expression schema on 2-sided column-only view.", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [new Date(), new Date(), new Date(), new Date()],
c: ["a", "b", "c", "d"],
});
const view = await table.view({
group_by: ["c"],
split_by: ["a"],
aggregates: {
"0 and 1": "any",
"0 or 1": "any",
"bucket(\"b\", 's')": "last",
},
expressions: [
'"a" ^ 2',
"sqrt(144)",
"0 and 1",
"0 or 1",
'-"a"',
'upper("c")',
`bucket("b", 'M')`,
`bucket("b", 's')`,
].reduce((x, y) => Object.assign(x, { [y]: y }), {}),
});
const schema = await view.expression_schema();
expect(schema).toEqual({
'"a" ^ 2': "float",
"sqrt(144)": "float",
'-"a"': "integer",
"0 and 1": "boolean",
"0 or 1": "boolean",
'upper("c")': "integer",
"bucket(\"b\", 'M')": "integer",
"bucket(\"b\", 's')": "datetime",
});
view.delete();
table.delete();
});
test("Vlookup should access the correct row", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: ["a", "b", "c", "d"],
});
const expr = "vlookup('b', integer(0))";
const view = await table.view({
expressions: [expr].reduce(
(x, y) => Object.assign(x, { [y]: y }),
{},
),
});
const result = await view.to_columns();
expect(result).toEqual({
a: [1, 2, 3, 4],
b: ["a", "b", "c", "d"],
[expr]: ["a", "a", "a", "a"],
});
view.delete();
table.delete();
});
test("Col should behave the same as direct column access", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: ["a", "b", "c", "d"],
});
const view = await table.view({
expressions: { "col('b')": "col('b')" },
});
const result = await view.to_columns();
expect(result).toEqual({
a: [1, 2, 3, 4],
b: ["a", "b", "c", "d"],
"col('b')": ["a", "b", "c", "d"],
});
view.delete();
table.delete();
});
test("index() should return the correct row number", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
});
const view = await table.view({
expressions: { "index()": "index()" },
});
const result = await view.to_columns();
expect(result).toEqual({
a: [1, 2, 3, 4],
"index()": [0, 1, 2, 3],
});
view.delete();
table.delete();
});
test("index() should work with custom a index", async function () {
const table = await perspective.table(
{
a: [1, 2, 3, 4],
},
{ index: "a" },
);
const view = await table.view({
expressions: { "index()": "index()" },
});
const result = await view.to_columns();
expect(result).toEqual({
a: [1, 2, 3, 4],
"index()": [1, 2, 3, 4],
});
view.delete();
table.delete();
});
test("index() should work with an update", async function () {
const table = await perspective.table(
{
a: [1, 2, 3, 4],
},
{ index: "a" },
);
const view = await table.view({
expressions: { "index()": "index()" },
});
let sentinel = false;
view.on_update(() => {
sentinel = true;
});
await table.update({ a: [5, 6] });
const result = await view.to_columns();
expect(sentinel).toBeTruthy();
expect(result).toEqual({
a: [1, 2, 3, 4, 5, 6],
"index()": [1, 2, 3, 4, 5, 6],
});
view.delete();
table.delete();
});
test("Validation works with both input formats", async function () {
const table = await perspective.table({
a: [1, 2, 3, 4],
b: [1, 2, 3, 4],
});
const str_exprs = {
x: '"a"',
y: '"b" * 0.5',
c: "'abcdefg'",
d: "true and false",
e: 'float("a") > 2 ? null : 1',
f: "today()",
g: "now()",
h: "length(123)",
};
const dict_exprs = {
x: '"a"',
y: '"b" * 0.5',
c: "'abcdefg'",
d: "true and false",
e: 'float("a") > 2 ? null : 1',
f: "today()",
g: "now()",
h: "length(123)",
};
let vstrs = await table.validate_expressions(str_exprs);
let vdicts = await table.validate_expressions(dict_exprs);
expect(vstrs.errors).toEqual(vdicts.errors);
expect(vstrs.expression_schema).toEqual(
vdicts.expression_schema,
);
for (let [key, val] of Object.entries(vstrs.expression_alias)) {
let dict_val = vdicts.expression_alias[key];
// let parsed_val = `${dict_val}`;
expect(val).toEqual(dict_val);
}
});
});
});
})(perspective);