chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
export const STD_DATE = new Date();
|
||||
|
||||
export const int_float_string_data = [
|
||||
{ int: 1, float: 2.25, string: "a", datetime: STD_DATE },
|
||||
{ int: 2, float: 3.5, string: "b", datetime: STD_DATE },
|
||||
{ int: 3, float: 4.75, string: "c", datetime: STD_DATE },
|
||||
{ int: 4, float: 5.25, string: "d", datetime: STD_DATE },
|
||||
];
|
||||
|
||||
export const pivoted_output = [
|
||||
{
|
||||
__ROW_PATH__: [],
|
||||
int: 10,
|
||||
float: 15.75,
|
||||
string: 4,
|
||||
datetime: 4,
|
||||
__INDEX__: [3, 2, 1, 0],
|
||||
},
|
||||
{
|
||||
__ROW_PATH__: [1],
|
||||
int: 1,
|
||||
float: 2.25,
|
||||
string: 1,
|
||||
datetime: 1,
|
||||
__INDEX__: [0],
|
||||
},
|
||||
{
|
||||
__ROW_PATH__: [2],
|
||||
int: 2,
|
||||
float: 3.5,
|
||||
string: 1,
|
||||
datetime: 1,
|
||||
__INDEX__: [1],
|
||||
},
|
||||
{
|
||||
__ROW_PATH__: [3],
|
||||
int: 3,
|
||||
float: 4.75,
|
||||
string: 1,
|
||||
datetime: 1,
|
||||
__INDEX__: [2],
|
||||
},
|
||||
{
|
||||
__ROW_PATH__: [4],
|
||||
int: 4,
|
||||
float: 5.25,
|
||||
string: 1,
|
||||
datetime: 1,
|
||||
__INDEX__: [3],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,344 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { test, expect } from "@perspective-dev/test";
|
||||
import perspective from "../perspective_client";
|
||||
import { int_float_string_data } from "./_shared";
|
||||
|
||||
((perspective) => {
|
||||
test.describe("data slice", function () {
|
||||
test("should filter out invalid start rows", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view();
|
||||
let json = await view.to_json({
|
||||
start_row: 5,
|
||||
});
|
||||
expect(json).toEqual([]);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should filter out invalid start columns", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view();
|
||||
let json = await view.to_json({
|
||||
start_col: 5,
|
||||
});
|
||||
expect(json).toEqual([]);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should filter out invalid start rows & columns", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view();
|
||||
let json = await view.to_json({
|
||||
start_row: 5,
|
||||
start_col: 5,
|
||||
});
|
||||
expect(json).toEqual([]);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should filter out invalid start rows based on view", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
filter: [["float", ">", 3.5]],
|
||||
});
|
||||
|
||||
// valid on view() but not this filtered view
|
||||
let json = await view.to_json({
|
||||
start_row: 3,
|
||||
});
|
||||
|
||||
expect(json).toEqual([]);
|
||||
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should filter out invalid start columns based on view", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
columns: ["float", "int"],
|
||||
});
|
||||
|
||||
let json = await view.to_json({
|
||||
start_col: 2,
|
||||
});
|
||||
|
||||
expect(json).toEqual([]);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should filter out invalid start rows & columns based on view", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
columns: ["float", "int"],
|
||||
filter: [["float", ">", 3.5]],
|
||||
});
|
||||
let json = await view.to_json({
|
||||
start_row: 5,
|
||||
start_col: 5,
|
||||
});
|
||||
expect(json).toEqual([]);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should respect start/end rows", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view();
|
||||
let json = await view.to_json({
|
||||
start_row: 2,
|
||||
end_row: 3,
|
||||
});
|
||||
let comparator = int_float_string_data[2];
|
||||
comparator.datetime = +comparator.datetime;
|
||||
expect(json[0]).toEqual(comparator);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should respect end rows when larger than data size", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view();
|
||||
let json = await view.to_json({
|
||||
start_row: 2,
|
||||
end_row: 6,
|
||||
});
|
||||
expect(json).toEqual(
|
||||
int_float_string_data.slice(2).map((x) => {
|
||||
x.datetime = +x.datetime;
|
||||
return x;
|
||||
}),
|
||||
);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should respect start/end columns", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view();
|
||||
let json = await view.to_columns({
|
||||
start_col: 2,
|
||||
end_col: 3,
|
||||
});
|
||||
let comparator = {
|
||||
string: int_float_string_data.map((d) => d.string),
|
||||
};
|
||||
expect(json).toEqual(comparator);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should floor float start rows", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view();
|
||||
let json = await view.to_json({
|
||||
start_row: 1.5,
|
||||
});
|
||||
expect(json).toEqual(
|
||||
int_float_string_data.slice(1).map((x) => {
|
||||
x.datetime = +x.datetime;
|
||||
return x;
|
||||
}),
|
||||
);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should ceil float end rows", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view();
|
||||
let json = await view.to_json({
|
||||
end_row: 1.5,
|
||||
});
|
||||
expect(json).toEqual(
|
||||
// deep copy
|
||||
JSON.parse(JSON.stringify(int_float_string_data))
|
||||
.slice(0, 2)
|
||||
.map((x) => {
|
||||
x.datetime = +new Date(x.datetime);
|
||||
return x;
|
||||
}),
|
||||
);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should floor/ceil float start/end rows", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view();
|
||||
let json = await view.to_json({
|
||||
start_row: 2.9,
|
||||
end_row: 2.4,
|
||||
});
|
||||
let comparator = int_float_string_data[2];
|
||||
comparator.datetime = +comparator.datetime;
|
||||
expect(json[0]).toEqual(comparator);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should ceil float end rows when larger than data size", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view();
|
||||
let json = await view.to_json({
|
||||
start_row: 2,
|
||||
end_row: 5.5,
|
||||
});
|
||||
expect(json).toEqual(
|
||||
int_float_string_data.slice(2).map((x) => {
|
||||
x.datetime = +x.datetime;
|
||||
return x;
|
||||
}),
|
||||
);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should floor/ceil float start/end columns", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view();
|
||||
let json = await view.to_columns({
|
||||
start_col: 2.6,
|
||||
end_col: 2.4,
|
||||
});
|
||||
let comparator = {
|
||||
string: int_float_string_data.map((d) => d.string),
|
||||
};
|
||||
expect(json).toEqual(comparator);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("one-sided views should have row paths", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
group_by: ["int"],
|
||||
});
|
||||
let json = await view.to_json();
|
||||
for (let d of json) {
|
||||
expect(d.__ROW_PATH__).toBeDefined();
|
||||
}
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("one-sided views with start_col > 0 should have row paths", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
group_by: ["int"],
|
||||
});
|
||||
let json = await view.to_json({ start_col: 1 });
|
||||
for (let d of json) {
|
||||
expect(d.__ROW_PATH__).toBeDefined();
|
||||
}
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("one-sided column-only views should not have row paths", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
split_by: ["int"],
|
||||
});
|
||||
let json = await view.to_json();
|
||||
for (let d of json) {
|
||||
expect(d.__ROW_PATH__).toBeUndefined();
|
||||
}
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("column-only views should not have header rows", async function () {
|
||||
let table = await perspective.table([
|
||||
{ x: 1, y: "a" },
|
||||
{ x: 2, y: "b" },
|
||||
]);
|
||||
let view = await table.view({
|
||||
split_by: ["x"],
|
||||
});
|
||||
let json = await view.to_json();
|
||||
expect(json).toEqual([
|
||||
{ "1|x": 1, "1|y": "a", "2|x": null, "2|y": null },
|
||||
{ "1|x": null, "1|y": null, "2|x": 2, "2|y": "b" },
|
||||
]);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("column-only views should return correct windows of data", async function () {
|
||||
let table = await perspective.table([
|
||||
{ x: 1, y: "a" },
|
||||
{ x: 2, y: "b" },
|
||||
]);
|
||||
let view = await table.view({
|
||||
split_by: ["x"],
|
||||
});
|
||||
let json = await view.to_json({
|
||||
start_row: 1,
|
||||
});
|
||||
expect(json).toEqual([
|
||||
{ "1|x": null, "1|y": null, "2|x": 2, "2|y": "b" },
|
||||
]);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("two-sided views should have row paths", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
group_by: ["int"],
|
||||
split_by: ["string"],
|
||||
});
|
||||
let json = await view.to_json();
|
||||
for (let d of json) {
|
||||
expect(d.__ROW_PATH__).toBeDefined();
|
||||
}
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("two-sided views with start_col > 0 should have row paths", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
group_by: ["int"],
|
||||
split_by: ["string"],
|
||||
});
|
||||
let json = await view.to_json({ start_col: 1 });
|
||||
for (let d of json) {
|
||||
expect(d.__ROW_PATH__).toBeDefined();
|
||||
}
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("two-sided sorted views with start_col > 0 should have row paths", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
group_by: ["int"],
|
||||
split_by: ["string"],
|
||||
sort: [["string", "desc"]],
|
||||
});
|
||||
let json = await view.to_json({ start_col: 1 });
|
||||
for (let d of json) {
|
||||
expect(d.__ROW_PATH__).toBeDefined();
|
||||
}
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
});
|
||||
})(perspective);
|
||||
@@ -0,0 +1,135 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { test, expect } from "@perspective-dev/test";
|
||||
import perspective from "../perspective_client";
|
||||
import { STD_DATE, int_float_string_data } from "./_shared";
|
||||
|
||||
((perspective) => {
|
||||
test.describe("leaves_only flag", function () {
|
||||
test("only emits leaves when leaves_only is set", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
group_by: ["int"],
|
||||
group_rollup_mode: "flat",
|
||||
});
|
||||
let json = await view.to_json();
|
||||
expect(json).toEqual([
|
||||
{
|
||||
__ROW_PATH__: [1],
|
||||
datetime: 1,
|
||||
float: 2.25,
|
||||
int: 1,
|
||||
string: 1,
|
||||
},
|
||||
{
|
||||
__ROW_PATH__: [2],
|
||||
datetime: 1,
|
||||
float: 3.5,
|
||||
int: 2,
|
||||
string: 1,
|
||||
},
|
||||
{
|
||||
__ROW_PATH__: [3],
|
||||
datetime: 1,
|
||||
float: 4.75,
|
||||
int: 3,
|
||||
string: 1,
|
||||
},
|
||||
{
|
||||
__ROW_PATH__: [4],
|
||||
datetime: 1,
|
||||
float: 5.25,
|
||||
int: 4,
|
||||
string: 1,
|
||||
},
|
||||
]);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("num_rows returns correct leaf count", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
group_by: ["int"],
|
||||
group_rollup_mode: "flat",
|
||||
});
|
||||
let num_rows = await view.num_rows();
|
||||
expect(num_rows).toEqual(4);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("viewport pagination returns exact page sizes", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
group_by: ["int"],
|
||||
group_rollup_mode: "flat",
|
||||
});
|
||||
let json = await view.to_json({
|
||||
start_row: 0,
|
||||
end_row: 2,
|
||||
});
|
||||
expect(json.length).toEqual(2);
|
||||
expect(json[0].__ROW_PATH__).toEqual([1]);
|
||||
expect(json[1].__ROW_PATH__).toEqual([2]);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("to_columns works with leaves_only", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
group_by: ["int"],
|
||||
group_rollup_mode: "flat",
|
||||
});
|
||||
let cols = await view.to_columns();
|
||||
expect(cols["__ROW_PATH__"]).toEqual([[1], [2], [3], [4]]);
|
||||
expect(cols["int"]).toEqual([1, 2, 3, 4]);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("leaves_only with multi-level group_by", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
group_by: ["string", "int"],
|
||||
group_rollup_mode: "flat",
|
||||
});
|
||||
let num_rows = await view.num_rows();
|
||||
expect(num_rows).toEqual(4);
|
||||
let json = await view.to_json();
|
||||
for (let row of json) {
|
||||
expect(row.__ROW_PATH__.length).toEqual(2);
|
||||
}
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("leaves_only updates after table.update()", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
group_by: ["int"],
|
||||
group_rollup_mode: "flat",
|
||||
});
|
||||
let num_rows = await view.num_rows();
|
||||
expect(num_rows).toEqual(4);
|
||||
table.update([
|
||||
{ int: 5, float: 6.0, string: "e", datetime: STD_DATE },
|
||||
]);
|
||||
let num_rows2 = await view.num_rows();
|
||||
expect(num_rows2).toEqual(5);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
});
|
||||
})(perspective);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { test, expect } from "@perspective-dev/test";
|
||||
import perspective from "../perspective_client";
|
||||
|
||||
test.describe("to_columns_string", () => {
|
||||
test("should return a string", async () => {
|
||||
const table = await perspective.table([{ x: 1 }]);
|
||||
const view = await table.view();
|
||||
const result = await view.to_columns_string();
|
||||
expect(result).toEqual('{"x":[1]}');
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { test, expect } from "@perspective-dev/test";
|
||||
import perspective from "../perspective_client";
|
||||
|
||||
test.describe("to_csv", () => {
|
||||
test("0-sided view returns CSV string", async () => {
|
||||
const table = await perspective.table([
|
||||
{ x: 1, y: "a" },
|
||||
{ x: 2, y: "b" },
|
||||
]);
|
||||
const view = await table.view();
|
||||
const csv = await view.to_csv();
|
||||
expect(csv).toEqual('"x","y"\n1,"a"\n2,"b"\n');
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("respects start_row/end_row viewport", async () => {
|
||||
const table = await perspective.table([
|
||||
{ x: 1, y: "a" },
|
||||
{ x: 2, y: "b" },
|
||||
{ x: 3, y: "c" },
|
||||
]);
|
||||
const view = await table.view();
|
||||
const csv = await view.to_csv({ start_row: 1, end_row: 2 });
|
||||
expect(csv).toEqual('"x","y"\n2,"b"\n');
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("1-sided view emits row paths", async () => {
|
||||
const table = await perspective.table([
|
||||
{ x: 1, y: "a" },
|
||||
{ x: 2, y: "a" },
|
||||
{ x: 3, y: "b" },
|
||||
]);
|
||||
const view = await table.view({ group_by: ["y"] });
|
||||
const csv = await view.to_csv();
|
||||
expect(csv).toEqual(
|
||||
'"y (Group by 1)","x","y"\n,6,3\n"a",3,2\n"b",3,1\n',
|
||||
);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("escapes quotes and commas in string values", async () => {
|
||||
const table = await perspective.table([
|
||||
{ x: 'he said "hi"', y: "a,b" },
|
||||
]);
|
||||
const view = await table.view();
|
||||
const csv = await view.to_csv();
|
||||
expect(csv).toEqual('"x","y"\n"he said ""hi""","a,b"\n');
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
// https://github.com/perspective-dev/perspective/issues/3152
|
||||
test("escapes utf8 in string values", async () => {
|
||||
const table = await perspective.table([
|
||||
{
|
||||
年月: "2023/01/01",
|
||||
轄區分局: "第四分局",
|
||||
路口名稱: "南屯區",
|
||||
路口名稱split: "五權西路與環中路口",
|
||||
A1: 0.0,
|
||||
A2: 3.0,
|
||||
A3: 130.0,
|
||||
總件數: 18.0,
|
||||
死亡人數: 0.0,
|
||||
受傷人數: null,
|
||||
主要肇因: "未注意車前狀態",
|
||||
},
|
||||
{
|
||||
年月: "2023/01/01",
|
||||
轄區分局: "第六分局",
|
||||
路口名稱: "西屯區",
|
||||
路口名稱split: "中清聯絡道與環中路口",
|
||||
A1: 0.0,
|
||||
A2: 2.0,
|
||||
A3: 100.0,
|
||||
總件數: 15.0,
|
||||
死亡人數: 0.0,
|
||||
受傷人數: null,
|
||||
主要肇因: "未注意車前狀態",
|
||||
},
|
||||
]);
|
||||
|
||||
const view = await table.view();
|
||||
const csv = await view.to_csv();
|
||||
expect(csv).toEqual(
|
||||
`"年月","轄區分局","路口名稱","路口名稱split","A1","A2","A3","總件數","死亡人數","主要肇因","受傷人數"\n2023-01-01,"第四分局","南屯區","五權西路與環中路口",0,3,130,18,0,"未注意車前狀態",\n2023-01-01,"第六分局","西屯區","中清聯絡道與環中路口",0,2,100,15,0,"未注意車前狀態",\n`,
|
||||
);
|
||||
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { test, expect } from "@perspective-dev/test";
|
||||
import perspective from "../perspective_client";
|
||||
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
import * as fs from "node:fs";
|
||||
|
||||
const superstore_uncompressed = fs.readFileSync(
|
||||
require.resolve("superstore-arrow/superstore.arrow"),
|
||||
).buffer;
|
||||
|
||||
const superstore_lz4 = fs.readFileSync(
|
||||
require.resolve("superstore-arrow/superstore.lz4.arrow"),
|
||||
).buffer;
|
||||
|
||||
test.describe("to_format regressions", function () {
|
||||
test("start_col is respected", async () => {
|
||||
let table = await perspective.table(superstore_uncompressed.slice());
|
||||
let view = await table.view({
|
||||
group_by: ["State"],
|
||||
split_by: ["Sub-Category"],
|
||||
// sort: [["Customer Name", "desc"]],
|
||||
group_rollup_mode: "rollup",
|
||||
columns: ["Sales", "Quantity", "Discount", "Profit"],
|
||||
});
|
||||
|
||||
const result1 = await view.to_columns({ start_col: 4, end_row: 1 });
|
||||
const result2 = await view.to_columns({ start_col: 5, end_row: 1 });
|
||||
|
||||
expect(result1).not.toEqual(result2);
|
||||
});
|
||||
|
||||
test("start_col is respected with sort", async () => {
|
||||
let table = await perspective.table(superstore_uncompressed.slice());
|
||||
let view = await table.view({
|
||||
group_by: ["State"],
|
||||
split_by: ["Sub-Category"],
|
||||
sort: [["Customer Name", "desc"]],
|
||||
group_rollup_mode: "rollup",
|
||||
columns: ["Sales", "Quantity", "Discount", "Profit"],
|
||||
});
|
||||
|
||||
const result1 = await view.to_columns({ start_col: 4, end_row: 1 });
|
||||
const result2 = await view.to_columns({ start_col: 5, end_row: 1 });
|
||||
|
||||
expect(result1).not.toEqual(result2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,292 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { test, expect } from "@perspective-dev/test";
|
||||
import perspective from "../perspective_client";
|
||||
|
||||
const data = {
|
||||
w: [
|
||||
1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, -1.5, -3.5, -1.5, -4.5, -9.5,
|
||||
-5.5, -8.5, -7.5,
|
||||
],
|
||||
x: [1, 2, 3, 4, 4, 3, 2, 1, 3, 4, 2, 1, 4, 3, 1, 2],
|
||||
y: [
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
],
|
||||
z: [
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
],
|
||||
};
|
||||
|
||||
test.describe("to_format viewport", function () {
|
||||
test.describe("0 sided", function () {
|
||||
test("start_col 0 is the first col", async function () {
|
||||
var table = await perspective.table(data);
|
||||
var view = await table.view({});
|
||||
const cols = await view.to_columns({ start_col: 0, end_col: 1 });
|
||||
expect(cols).toEqual({ w: data.w });
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("start_col 2 is the second col", async function () {
|
||||
var table = await perspective.table(data);
|
||||
var view = await table.view({});
|
||||
const cols = await view.to_columns({ start_col: 1, end_col: 2 });
|
||||
expect(cols).toEqual({ x: data.x });
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("start_col 0, end_col 2 is the first two columns", async function () {
|
||||
var table = await perspective.table(data);
|
||||
var view = await table.view({});
|
||||
const cols = await view.to_columns({ start_col: 0, end_col: 2 });
|
||||
expect(cols).toEqual({ w: data.w, x: data.x });
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("1 sided", function () {
|
||||
test("start_col 0 is the first col", async function () {
|
||||
var table = await perspective.table(data);
|
||||
var view = await table.view({
|
||||
group_by: ["y"],
|
||||
});
|
||||
const cols = await view.to_columns({ start_col: 0, end_col: 1 });
|
||||
expect(cols).toEqual({
|
||||
__ROW_PATH__: [[], ["a"], ["b"], ["c"], ["d"]],
|
||||
w: [-2, -4, 0, 1, 1],
|
||||
});
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("start_col 2 is the second col", async function () {
|
||||
var table = await perspective.table(data);
|
||||
var view = await table.view({
|
||||
group_by: ["y"],
|
||||
});
|
||||
const cols = await view.to_columns({ start_col: 1, end_col: 2 });
|
||||
expect(cols).toEqual({
|
||||
__ROW_PATH__: [[], ["a"], ["b"], ["c"], ["d"]],
|
||||
x: [40, 12, 12, 8, 8],
|
||||
});
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("start_col 0, end_col 2 is the first two columns", async function () {
|
||||
var table = await perspective.table(data);
|
||||
var view = await table.view({
|
||||
group_by: ["y"],
|
||||
});
|
||||
const cols = await view.to_columns({ start_col: 0, end_col: 2 });
|
||||
expect(cols).toEqual({
|
||||
__ROW_PATH__: [[], ["a"], ["b"], ["c"], ["d"]],
|
||||
w: [-2, -4, 0, 1, 1],
|
||||
x: [40, 12, 12, 8, 8],
|
||||
});
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("2 sided", function () {
|
||||
test("start_col 0 is the first col", async function () {
|
||||
var table = await perspective.table(data);
|
||||
var view = await table.view({
|
||||
group_by: ["y"],
|
||||
split_by: ["z"],
|
||||
});
|
||||
const cols = await view.to_columns({ start_col: 0, end_col: 1 });
|
||||
expect(cols).toEqual({
|
||||
__ROW_PATH__: [[], ["a"], ["b"], ["c"], ["d"]],
|
||||
"false|w": [-9, -9.5, 3.5, -8.5, 5.5],
|
||||
});
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("start_col 2 is the second col", async function () {
|
||||
var table = await perspective.table(data);
|
||||
var view = await table.view({
|
||||
group_by: ["y"],
|
||||
split_by: ["z"],
|
||||
});
|
||||
const cols = await view.to_columns({ start_col: 1, end_col: 2 });
|
||||
expect(cols).toEqual({
|
||||
__ROW_PATH__: [[], ["a"], ["b"], ["c"], ["d"]],
|
||||
"false|x": [20, 4, 8, 1, 7],
|
||||
});
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("start_col 0, end_col 2 is the first two columns", async function () {
|
||||
var table = await perspective.table(data);
|
||||
var view = await table.view({
|
||||
group_by: ["y"],
|
||||
split_by: ["z"],
|
||||
});
|
||||
const cols = await view.to_columns({ start_col: 0, end_col: 2 });
|
||||
expect(cols).toEqual({
|
||||
__ROW_PATH__: [[], ["a"], ["b"], ["c"], ["d"]],
|
||||
"false|w": [-9, -9.5, 3.5, -8.5, 5.5],
|
||||
"false|x": [20, 4, 8, 1, 7],
|
||||
});
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("column only", function () {
|
||||
test("start_col 0 is the first col", async function () {
|
||||
var table = await perspective.table(data);
|
||||
var view = await table.view({
|
||||
split_by: ["z"],
|
||||
});
|
||||
const cols = await view.to_columns({ start_col: 0, end_col: 1 });
|
||||
expect(cols).toEqual({
|
||||
"false|w": [
|
||||
null,
|
||||
2.5,
|
||||
null,
|
||||
4.5,
|
||||
null,
|
||||
6.5,
|
||||
null,
|
||||
8.5,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
-9.5,
|
||||
-5.5,
|
||||
-8.5,
|
||||
-7.5,
|
||||
],
|
||||
});
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("start_col 2 is the second col", async function () {
|
||||
var table = await perspective.table(data);
|
||||
var view = await table.view({
|
||||
split_by: ["z"],
|
||||
});
|
||||
const cols = await view.to_columns({ start_col: 1, end_col: 2 });
|
||||
expect(cols).toEqual({
|
||||
"false|x": [
|
||||
null,
|
||||
2,
|
||||
null,
|
||||
4,
|
||||
null,
|
||||
3,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
4,
|
||||
3,
|
||||
1,
|
||||
2,
|
||||
],
|
||||
});
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("start_col 0, end_col 2 is the first two columns", async function () {
|
||||
var table = await perspective.table(data);
|
||||
var view = await table.view({
|
||||
split_by: ["z"],
|
||||
});
|
||||
const cols = await view.to_columns({ start_col: 0, end_col: 2 });
|
||||
expect(cols).toEqual({
|
||||
"false|w": [
|
||||
null,
|
||||
2.5,
|
||||
null,
|
||||
4.5,
|
||||
null,
|
||||
6.5,
|
||||
null,
|
||||
8.5,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
-9.5,
|
||||
-5.5,
|
||||
-8.5,
|
||||
-7.5,
|
||||
],
|
||||
"false|x": [
|
||||
null,
|
||||
2,
|
||||
null,
|
||||
4,
|
||||
null,
|
||||
3,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
4,
|
||||
3,
|
||||
1,
|
||||
2,
|
||||
],
|
||||
});
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { test, expect } from "@perspective-dev/test";
|
||||
import perspective from "../perspective_client";
|
||||
import { int_float_string_data } from "./_shared";
|
||||
|
||||
((perspective) => {
|
||||
test.describe("to_json", function () {
|
||||
test("should emit same number of column names as number of pivots", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
group_by: ["int"],
|
||||
split_by: ["float", "string"],
|
||||
sort: [["int", "asc"]],
|
||||
});
|
||||
let json = await view.to_json();
|
||||
// Get the first emitted column name that is not __ROW_PATH__
|
||||
let name = Object.keys(json[0])[1];
|
||||
// make sure that number of separators = num of split by
|
||||
expect((name.match(/\|/g) || []).length).toEqual(2);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should return dates in native form by default", async function () {
|
||||
let table = await perspective.table([
|
||||
{ datetime: new Date("2016-06-13") },
|
||||
{ datetime: new Date("2016-06-14") },
|
||||
]);
|
||||
let view = await table.view();
|
||||
let json = await view.to_json();
|
||||
expect(json).toEqual([
|
||||
{ datetime: 1465776000000 },
|
||||
{ datetime: 1465862400000 },
|
||||
]);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test.skip("OG - should return dates in readable format on passing string in options", async function () {
|
||||
let table = await perspective.table([
|
||||
{ datetime: new Date("2016-06-13") },
|
||||
{ datetime: new Date("2016-06-14") },
|
||||
]);
|
||||
let view = await table.view();
|
||||
let json = await view.to_json({ formatted: true });
|
||||
expect(json).toEqual([
|
||||
{ datetime: "2016-06-13" },
|
||||
{ datetime: "2016-06-14" },
|
||||
]);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should return dates in readable format on passing string in options", async function () {
|
||||
let table = await perspective.table({
|
||||
date: "date",
|
||||
});
|
||||
await table.update([
|
||||
{ date: new Date("2016-06-13") },
|
||||
{ date: new Date("2016-06-14") },
|
||||
]);
|
||||
let view = await table.view();
|
||||
let json = await view.to_json({ formatted: true });
|
||||
expect(json).toEqual([
|
||||
{ date: "2016-06-13" },
|
||||
{ date: "2016-06-14" },
|
||||
]);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should return datetimes in readable format on passing string in options", async function () {
|
||||
let table = await perspective.table([
|
||||
{ datetime: new Date(2016, 0, 1, 0, 30) },
|
||||
{ datetime: new Date(2016, 5, 15, 19, 20) },
|
||||
]);
|
||||
let view = await table.view();
|
||||
let json = await view.to_json({ formatted: true });
|
||||
expect(json).toEqual([
|
||||
{ datetime: "2016-01-01 00:30:00.000" },
|
||||
{ datetime: "2016-06-15 19:20:00.000" },
|
||||
]);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
});
|
||||
})(perspective);
|
||||
@@ -0,0 +1,63 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { test, expect } from "@perspective-dev/test";
|
||||
import perspective from "../perspective_client";
|
||||
import { STD_DATE, int_float_string_data } from "./_shared";
|
||||
|
||||
((perspective) => {
|
||||
test.describe("to_ndjson", function () {
|
||||
test("should work with context 0", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({});
|
||||
let json = await view.to_ndjson();
|
||||
expect(json)
|
||||
.toEqual(`{"int":1,"float":2.25,"string":"a","datetime":${+STD_DATE}}
|
||||
{"int":2,"float":3.5,"string":"b","datetime":${+STD_DATE}}
|
||||
{"int":3,"float":4.75,"string":"c","datetime":${+STD_DATE}}
|
||||
{"int":4,"float":5.25,"string":"d","datetime":${+STD_DATE}}`);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should work with context 1", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({ group_by: ["string"] });
|
||||
let json = await view.to_ndjson();
|
||||
expect(json)
|
||||
.toEqual(`{"__ROW_PATH__":[],"int":10,"float":15.75,"string":4,"datetime":4}
|
||||
{"__ROW_PATH__":["a"],"int":1,"float":2.25,"string":1,"datetime":1}
|
||||
{"__ROW_PATH__":["b"],"int":2,"float":3.5,"string":1,"datetime":1}
|
||||
{"__ROW_PATH__":["c"],"int":3,"float":4.75,"string":1,"datetime":1}
|
||||
{"__ROW_PATH__":["d"],"int":4,"float":5.25,"string":1,"datetime":1}`);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
|
||||
test("should work with context 2", async function () {
|
||||
let table = await perspective.table(int_float_string_data);
|
||||
let view = await table.view({
|
||||
group_by: ["string"],
|
||||
split_by: ["int"],
|
||||
});
|
||||
let json = await view.to_ndjson();
|
||||
expect(json)
|
||||
.toEqual(`{"__ROW_PATH__":[],"1|int":1,"1|float":2.25,"1|string":1,"1|datetime":1,"2|int":2,"2|float":3.5,"2|string":1,"2|datetime":1,"3|int":3,"3|float":4.75,"3|string":1,"3|datetime":1,"4|int":4,"4|float":5.25,"4|string":1,"4|datetime":1}
|
||||
{"__ROW_PATH__":["a"],"1|int":1,"1|float":2.25,"1|string":1,"1|datetime":1,"2|int":null,"2|float":null,"2|string":null,"2|datetime":null,"3|int":null,"3|float":null,"3|string":null,"3|datetime":null,"4|int":null,"4|float":null,"4|string":null,"4|datetime":null}
|
||||
{"__ROW_PATH__":["b"],"1|int":null,"1|float":null,"1|string":null,"1|datetime":null,"2|int":2,"2|float":3.5,"2|string":1,"2|datetime":1,"3|int":null,"3|float":null,"3|string":null,"3|datetime":null,"4|int":null,"4|float":null,"4|string":null,"4|datetime":null}
|
||||
{"__ROW_PATH__":["c"],"1|int":null,"1|float":null,"1|string":null,"1|datetime":null,"2|int":null,"2|float":null,"2|string":null,"2|datetime":null,"3|int":3,"3|float":4.75,"3|string":1,"3|datetime":1,"4|int":null,"4|float":null,"4|string":null,"4|datetime":null}
|
||||
{"__ROW_PATH__":["d"],"1|int":null,"1|float":null,"1|string":null,"1|datetime":null,"2|int":null,"2|float":null,"2|string":null,"2|datetime":null,"3|int":null,"3|float":null,"3|string":null,"3|datetime":null,"4|int":4,"4|float":5.25,"4|string":1,"4|datetime":1}`);
|
||||
view.delete();
|
||||
table.delete();
|
||||
});
|
||||
});
|
||||
})(perspective);
|
||||
@@ -0,0 +1,825 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { test, expect } from "@perspective-dev/test";
|
||||
import perspective from "../perspective_client";
|
||||
|
||||
test.describe("with_typed_arrays()", () => {
|
||||
test("awaits promise returned by async callback before releasing the batch", async () => {
|
||||
// The callback returns a Promise that only resolves after a
|
||||
// microtask tick; the zero-copy views must remain valid for the
|
||||
// full awaited duration. We copy *after* the tick to prove the
|
||||
// backing WASM memory is still readable.
|
||||
const table = await perspective.table({
|
||||
x: ["a", "b", "a", "c"],
|
||||
});
|
||||
const view = await table.view();
|
||||
let resolved: string[] | null = null;
|
||||
await view.with_typed_arrays(
|
||||
{},
|
||||
async (
|
||||
_n: string[],
|
||||
vals: ArrayLike<number>[],
|
||||
_valids: any[],
|
||||
dicts: (string[] | null)[],
|
||||
) => {
|
||||
const keys = vals[0] as Int32Array;
|
||||
const dict = dicts[0]!;
|
||||
// Yield twice to prove the Rust side is actually awaiting
|
||||
// the returned promise rather than firing a sync call and
|
||||
// dropping the batch immediately.
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
resolved = Array.from(keys).map((k) => dict[k]);
|
||||
},
|
||||
);
|
||||
expect(resolved).toEqual(["a", "b", "a", "c"]);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("rejected promise from async callback surfaces as a rejection", async () => {
|
||||
const table = await perspective.table({ x: [1, 2, 3] });
|
||||
const view = await table.view();
|
||||
let caught: unknown = null;
|
||||
try {
|
||||
await view.with_typed_arrays({}, async () => {
|
||||
throw new Error("callback boom");
|
||||
});
|
||||
} catch (e) {
|
||||
caught = e;
|
||||
}
|
||||
expect(String(caught)).toContain("callback boom");
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("returns all columns with names, values, validities, dictionaries", async () => {
|
||||
const table = await perspective.table({
|
||||
a: [1, 2, 3],
|
||||
b: [10.0, 20.0, 30.0],
|
||||
});
|
||||
|
||||
const view = await table.view();
|
||||
let names: string[] = [];
|
||||
let valuesMap: Record<string, ArrayLike<number>> = {};
|
||||
await view.with_typed_arrays(
|
||||
{},
|
||||
(
|
||||
n: string[],
|
||||
vals: ArrayLike<number>[],
|
||||
_valids: (Uint8Array | null)[],
|
||||
_dicts: (string[] | null)[],
|
||||
) => {
|
||||
names = Array.from(n);
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
valuesMap[names[i]] = vals[i];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
expect(names).toContain("a");
|
||||
expect(names).toContain("b");
|
||||
expect(Array.from(valuesMap["b"])).toEqual([10.0, 20.0, 30.0]);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("returns Float64Array for float column by default", async () => {
|
||||
const table = await perspective.table({ x: [1.5, 2.5, 3.5] });
|
||||
const view = await table.view();
|
||||
let result: Float64Array | null = null;
|
||||
await view.with_typed_arrays(
|
||||
{},
|
||||
(_n: string[], vals: ArrayLike<number>[]) => {
|
||||
result = new Float64Array(vals[0] as Float64Array);
|
||||
},
|
||||
);
|
||||
|
||||
expect(Array.from(result!)).toEqual([1.5, 2.5, 3.5]);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("returns Float32Array when float32 option is set", async () => {
|
||||
const table = await perspective.table({ x: [1.5, 2.5, 3.5] });
|
||||
const view = await table.view();
|
||||
let isFloat32 = false;
|
||||
await view.with_typed_arrays(
|
||||
{ float32: true },
|
||||
(_n: string[], vals: any[]) => {
|
||||
isFloat32 = vals[0] instanceof Float32Array;
|
||||
},
|
||||
);
|
||||
|
||||
expect(isFloat32).toBe(true);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("returns Int32Array for integer column", async () => {
|
||||
const table = await perspective.table({ x: "integer" });
|
||||
table.update({ x: [10, 20, 30] });
|
||||
const view = await table.view();
|
||||
let result: Int32Array | null = null;
|
||||
await view.with_typed_arrays(
|
||||
{},
|
||||
(_n: string[], vals: ArrayLike<number>[]) => {
|
||||
result = new Int32Array(vals[0] as Int32Array);
|
||||
},
|
||||
);
|
||||
|
||||
expect(Array.from(result!)).toEqual([10, 20, 30]);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("passes validity bitmap for columns with nulls", async () => {
|
||||
const table = await perspective.table({ x: [1.5, null, 3.5, null] });
|
||||
const view = await table.view();
|
||||
let validity: Uint8Array | null = null;
|
||||
await view.with_typed_arrays(
|
||||
{},
|
||||
(_n: string[], _vals: any[], valids: (Uint8Array | null)[]) => {
|
||||
validity = valids[0] ? new Uint8Array(valids[0]) : null;
|
||||
},
|
||||
);
|
||||
|
||||
// Bits 0 and 2 set => 0b0101 = 5
|
||||
expect(validity![0]).toEqual(0b0101);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("validity is null when no nulls present", async () => {
|
||||
const table = await perspective.table({ x: [1.0, 2.0, 3.0] });
|
||||
const view = await table.view();
|
||||
let validityWasNull = false;
|
||||
await view.with_typed_arrays(
|
||||
{},
|
||||
(_n: string[], _vals: any[], valids: (Uint8Array | null)[]) => {
|
||||
validityWasNull = valids[0] === null;
|
||||
},
|
||||
);
|
||||
|
||||
expect(validityWasNull).toBe(true);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("returns dictionary keys and values for string column", async () => {
|
||||
const table = await perspective.table({
|
||||
x: ["apple", "banana", "apple", "cherry"],
|
||||
});
|
||||
const view = await table.view();
|
||||
let keys: Int32Array | null = null;
|
||||
let dict: string[] | null = null;
|
||||
await view.with_typed_arrays(
|
||||
{},
|
||||
(
|
||||
_n: string[],
|
||||
vals: ArrayLike<number>[],
|
||||
_valids: any[],
|
||||
dicts: (string[] | null)[],
|
||||
) => {
|
||||
keys = new Int32Array(vals[0] as Int32Array);
|
||||
dict = dicts[0] ? Array.from(dicts[0]) : null;
|
||||
},
|
||||
);
|
||||
|
||||
expect(keys!.length).toEqual(4);
|
||||
const resolved = Array.from(keys!).map((k) => dict![k]);
|
||||
expect(resolved).toEqual(["apple", "banana", "apple", "cherry"]);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("dictionary is null for non-string columns", async () => {
|
||||
const table = await perspective.table({ x: [1.0, 2.0, 3.0] });
|
||||
const view = await table.view();
|
||||
let dictWasNull = false;
|
||||
await view.with_typed_arrays(
|
||||
{},
|
||||
(
|
||||
_n: string[],
|
||||
_vals: any[],
|
||||
_valids: any[],
|
||||
dicts: (string[] | null)[],
|
||||
) => {
|
||||
dictWasNull = dicts[0] === null;
|
||||
},
|
||||
);
|
||||
|
||||
expect(dictWasNull).toBe(true);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("supports row windowing via start_row/end_row", async () => {
|
||||
const table = await perspective.table({ x: [10, 20, 30, 40, 50] });
|
||||
const view = await table.view();
|
||||
let result: Int32Array | null = null;
|
||||
await view.with_typed_arrays(
|
||||
{ start_row: 1, end_row: 3 },
|
||||
(_n: string[], vals: ArrayLike<number>[]) => {
|
||||
result = new Int32Array(vals[0] as Int32Array);
|
||||
},
|
||||
);
|
||||
|
||||
expect(Array.from(result!)).toEqual([20, 30]);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("returns Float64Array of millis for datetime column", async () => {
|
||||
const d1 = new Date("2020-01-01T00:00:00Z");
|
||||
const d2 = new Date("2021-06-15T12:30:00Z");
|
||||
const table = await perspective.table({
|
||||
x: [d1.getTime(), d2.getTime()],
|
||||
});
|
||||
const view = await table.view();
|
||||
let result: Float64Array | null = null;
|
||||
await view.with_typed_arrays(
|
||||
{},
|
||||
(_n: string[], vals: ArrayLike<number>[]) => {
|
||||
result = new Float64Array(vals[0] as Float64Array);
|
||||
},
|
||||
);
|
||||
|
||||
expect(Array.from(result!)).toEqual([d1.getTime(), d2.getTime()]);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("returns Float64Array of millis for date column", async () => {
|
||||
const table = await perspective.table({ x: "date" });
|
||||
table.update({ x: ["2020-01-01", "2021-06-15"] });
|
||||
const view = await table.view();
|
||||
let result: Float64Array | null = null;
|
||||
await view.with_typed_arrays(
|
||||
{},
|
||||
(_n: string[], vals: ArrayLike<number>[]) => {
|
||||
result = new Float64Array(vals[0] as Float64Array);
|
||||
},
|
||||
);
|
||||
|
||||
const expected = [
|
||||
new Date("2020-01-01").getTime(),
|
||||
new Date("2021-06-15").getTime(),
|
||||
];
|
||||
expect(Array.from(result!)).toEqual(expected);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test.describe("float32 option", () => {
|
||||
test("Float64 column contents narrowed to Float32Array", async () => {
|
||||
const table = await perspective.table({ x: [1.5, 2.5, 3.5, 4.5] });
|
||||
const view = await table.view();
|
||||
let result: Float32Array | null = null;
|
||||
await view.with_typed_arrays(
|
||||
{ float32: true },
|
||||
(_n: string[], vals: any[]) => {
|
||||
result = new Float32Array(vals[0]);
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBeInstanceOf(Float32Array);
|
||||
// Values exactly representable in float32
|
||||
expect(Array.from(result!)).toEqual([1.5, 2.5, 3.5, 4.5]);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("Int32 column is NOT affected by float32 option", async () => {
|
||||
const table = await perspective.table({ x: "integer" });
|
||||
table.update({ x: [10, 20, 30] });
|
||||
const view = await table.view();
|
||||
let isInt32 = false;
|
||||
let result: Int32Array | null = null;
|
||||
await view.with_typed_arrays(
|
||||
{ float32: true },
|
||||
(_n: string[], vals: any[]) => {
|
||||
isInt32 = vals[0] instanceof Int32Array;
|
||||
result = new Int32Array(vals[0]);
|
||||
},
|
||||
);
|
||||
|
||||
expect(isInt32).toBe(true);
|
||||
expect(Array.from(result!)).toEqual([10, 20, 30]);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("Date column narrowed to Float32Array of millis", async () => {
|
||||
const table = await perspective.table({ x: "date" });
|
||||
table.update({ x: ["2020-01-01", "2021-06-15"] });
|
||||
const view = await table.view();
|
||||
let result: Float32Array | null = null;
|
||||
await view.with_typed_arrays(
|
||||
{ float32: true },
|
||||
(_n: string[], vals: any[]) => {
|
||||
result = new Float32Array(vals[0]);
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBeInstanceOf(Float32Array);
|
||||
// Float32 precision loss is expected for large millis values
|
||||
const expected = [
|
||||
new Date("2020-01-01").getTime(),
|
||||
new Date("2021-06-15").getTime(),
|
||||
];
|
||||
// Narrow to f32 then back to compare (lossy conversion)
|
||||
const expectedF32 = Array.from(new Float32Array(expected));
|
||||
expect(Array.from(result!)).toEqual(expectedF32);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("Datetime column narrowed to Float32Array of millis", async () => {
|
||||
const d1 = new Date("2020-01-01T00:00:00Z");
|
||||
const d2 = new Date("2021-06-15T12:30:00Z");
|
||||
const table = await perspective.table({
|
||||
x: [d1.getTime(), d2.getTime()],
|
||||
});
|
||||
const view = await table.view();
|
||||
let result: Float32Array | null = null;
|
||||
await view.with_typed_arrays(
|
||||
{ float32: true },
|
||||
(_n: string[], vals: any[]) => {
|
||||
result = new Float32Array(vals[0]);
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBeInstanceOf(Float32Array);
|
||||
const expectedF32 = Array.from(
|
||||
new Float32Array([d1.getTime(), d2.getTime()]),
|
||||
);
|
||||
expect(Array.from(result!)).toEqual(expectedF32);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("Dictionary string column returns Int32Array keys (unaffected by float32)", async () => {
|
||||
const table = await perspective.table({
|
||||
x: ["a", "b", "a", "c"],
|
||||
});
|
||||
const view = await table.view();
|
||||
let isInt32 = false;
|
||||
let dict: string[] | null = null;
|
||||
await view.with_typed_arrays(
|
||||
{ float32: true },
|
||||
(
|
||||
_n: string[],
|
||||
vals: any[],
|
||||
_valids: any[],
|
||||
dicts: (string[] | null)[],
|
||||
) => {
|
||||
isInt32 = vals[0] instanceof Int32Array;
|
||||
dict = dicts[0] ? Array.from(dicts[0]) : null;
|
||||
},
|
||||
);
|
||||
|
||||
expect(isInt32).toBe(true);
|
||||
expect(dict).not.toBeNull();
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("float32 omitted defaults to Float64Array", async () => {
|
||||
const table = await perspective.table({ x: [1.5, 2.5, 3.5] });
|
||||
const view = await table.view();
|
||||
let isFloat64 = false;
|
||||
await view.with_typed_arrays({}, (_n: string[], vals: any[]) => {
|
||||
isFloat64 = vals[0] instanceof Float64Array;
|
||||
});
|
||||
|
||||
expect(isFloat64).toBe(true);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("float32: false is equivalent to omitting it", async () => {
|
||||
const table = await perspective.table({ x: [1.5, 2.5, 3.5] });
|
||||
const view = await table.view();
|
||||
let isFloat64 = false;
|
||||
await view.with_typed_arrays(
|
||||
{ float32: false },
|
||||
(_n: string[], vals: any[]) => {
|
||||
isFloat64 = vals[0] instanceof Float64Array;
|
||||
},
|
||||
);
|
||||
|
||||
expect(isFloat64).toBe(true);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("float32 combines with row windowing", async () => {
|
||||
const table = await perspective.table({
|
||||
x: [1.5, 2.5, 3.5, 4.5, 5.5],
|
||||
});
|
||||
const view = await table.view();
|
||||
let result: Float32Array | null = null;
|
||||
await view.with_typed_arrays(
|
||||
{ float32: true, start_row: 1, end_row: 4 },
|
||||
(_n: string[], vals: any[]) => {
|
||||
result = new Float32Array(vals[0]);
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBeInstanceOf(Float32Array);
|
||||
expect(Array.from(result!)).toEqual([2.5, 3.5, 4.5]);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("float32 on multi-column view: floats narrowed, ints unchanged", async () => {
|
||||
const table = await perspective.table({
|
||||
a: "integer",
|
||||
b: "float",
|
||||
});
|
||||
table.update({ a: [1, 2, 3], b: [1.5, 2.5, 3.5] });
|
||||
const view = await table.view();
|
||||
const typeMap: Record<string, string> = {};
|
||||
await view.with_typed_arrays(
|
||||
{ float32: true },
|
||||
(n: string[], vals: any[]) => {
|
||||
for (let i = 0; i < n.length; i++) {
|
||||
typeMap[n[i]] = vals[i].constructor.name;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
expect(typeMap["a"]).toEqual("Int32Array");
|
||||
expect(typeMap["b"]).toEqual("Float32Array");
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("group_by", () => {
|
||||
test("emits __ROW_PATH_0__ column for single group_by", async () => {
|
||||
const table = await perspective.table({
|
||||
category: ["a", "a", "b", "b"],
|
||||
value: [1, 2, 3, 4],
|
||||
});
|
||||
const view = await table.view({
|
||||
group_by: ["category"],
|
||||
aggregates: { value: "sum" },
|
||||
});
|
||||
|
||||
let names: string[] = [];
|
||||
await view.with_typed_arrays({}, (n: string[]) => {
|
||||
names = Array.from(n);
|
||||
});
|
||||
|
||||
expect(names).toContain("__ROW_PATH_0__");
|
||||
// Should NOT contain the legacy "category (Group by 1)" naming
|
||||
expect(names).not.toContain("category (Group by 1)");
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("__ROW_PATH_0__ is a Dictionary column with expected values", async () => {
|
||||
const table = await perspective.table({
|
||||
category: ["a", "a", "b", "b", "c"],
|
||||
value: [1, 2, 3, 4, 5],
|
||||
});
|
||||
const view = await table.view({
|
||||
group_by: ["category"],
|
||||
aggregates: { value: "sum" },
|
||||
});
|
||||
|
||||
let keys: Int32Array | null = null;
|
||||
let dict: string[] | null = null;
|
||||
await view.with_typed_arrays(
|
||||
{},
|
||||
(
|
||||
n: string[],
|
||||
vals: any[],
|
||||
_valids: any[],
|
||||
dicts: (string[] | null)[],
|
||||
) => {
|
||||
const idx = n.indexOf("__ROW_PATH_0__");
|
||||
expect(idx).toBeGreaterThanOrEqual(0);
|
||||
keys = new Int32Array(vals[idx]);
|
||||
dict = dicts[idx] ? Array.from(dicts[idx]) : null;
|
||||
},
|
||||
);
|
||||
|
||||
expect(dict).not.toBeNull();
|
||||
// Resolve keys to strings. First row is the total (empty/null),
|
||||
// remaining rows are the groups.
|
||||
const resolved = Array.from(keys!).map((k) =>
|
||||
k >= 0 ? dict![k] : null,
|
||||
);
|
||||
expect(resolved.slice(1).sort()).toEqual(["a", "b", "c"]);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("emits __ROW_PATH_0__ and __ROW_PATH_1__ for two-level group_by", async () => {
|
||||
const table = await perspective.table({
|
||||
region: ["US", "US", "EU", "EU"],
|
||||
country: ["x", "y", "a", "b"],
|
||||
value: [1, 2, 3, 4],
|
||||
});
|
||||
const view = await table.view({
|
||||
group_by: ["region", "country"],
|
||||
aggregates: { value: "sum" },
|
||||
});
|
||||
|
||||
let names: string[] = [];
|
||||
await view.with_typed_arrays({}, (n: string[]) => {
|
||||
names = Array.from(n);
|
||||
});
|
||||
|
||||
expect(names).toContain("__ROW_PATH_0__");
|
||||
expect(names).toContain("__ROW_PATH_1__");
|
||||
expect(names).not.toContain("region (Group by 1)");
|
||||
expect(names).not.toContain("country (Group by 2)");
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("aggregate value column is present alongside row path", async () => {
|
||||
const table = await perspective.table({
|
||||
category: ["a", "a", "b"],
|
||||
value: [10, 20, 30],
|
||||
});
|
||||
const view = await table.view({
|
||||
group_by: ["category"],
|
||||
aggregates: { value: "sum" },
|
||||
});
|
||||
|
||||
const columns: Record<string, any> = {};
|
||||
await view.with_typed_arrays(
|
||||
{},
|
||||
(
|
||||
n: string[],
|
||||
vals: any[],
|
||||
_valids: any[],
|
||||
dicts: (string[] | null)[],
|
||||
) => {
|
||||
for (let i = 0; i < n.length; i++) {
|
||||
columns[n[i]] = { vals: vals[i], dict: dicts[i] };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
expect(columns["__ROW_PATH_0__"]).toBeDefined();
|
||||
expect(columns["value"]).toBeDefined();
|
||||
// Value column should be numeric (not Dictionary)
|
||||
expect(columns["value"].dict).toBeNull();
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("regular to_arrow still uses legacy naming by default", async () => {
|
||||
// Sanity check: `to_arrow` (not with_typed_arrays) should
|
||||
// still use legacy "colname (Group by N)" naming for backwards
|
||||
// compatibility. `with_typed_arrays` forces the new naming.
|
||||
const table = await perspective.table({
|
||||
category: ["a", "b"],
|
||||
value: [1, 2],
|
||||
});
|
||||
const view = await table.view({
|
||||
group_by: ["category"],
|
||||
aggregates: { value: "sum" },
|
||||
});
|
||||
|
||||
const arrow = await view.to_arrow();
|
||||
// The Arrow IPC bytes should contain the legacy name.
|
||||
const bytes = new Uint8Array(arrow);
|
||||
const text = new TextDecoder().decode(bytes);
|
||||
expect(text.includes("(Group by 1)")).toBe(true);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("group_rollup_mode: flat", () => {
|
||||
test("emits one row per distinct group (no total/aggregate rows)", async () => {
|
||||
// In flat mode, only leaf rows are emitted — no intermediate
|
||||
// rollup totals. 5 input rows with 3 distinct categories yields
|
||||
// 3 output rows (one per group).
|
||||
const table = await perspective.table({
|
||||
category: ["a", "a", "b", "b", "c"],
|
||||
value: [1, 2, 3, 4, 5],
|
||||
});
|
||||
const view = await table.view({
|
||||
group_by: ["category"],
|
||||
group_rollup_mode: "flat",
|
||||
aggregates: { value: "sum" },
|
||||
});
|
||||
|
||||
let rowCount = 0;
|
||||
await view.with_typed_arrays({}, (_n: string[], vals: any[]) => {
|
||||
rowCount = vals[0].length;
|
||||
});
|
||||
|
||||
expect(rowCount).toEqual(3);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("__ROW_PATH_0__ contains one entry per distinct group", async () => {
|
||||
const table = await perspective.table({
|
||||
category: ["a", "b", "a", "c"],
|
||||
value: [10, 20, 30, 40],
|
||||
});
|
||||
const view = await table.view({
|
||||
group_by: ["category"],
|
||||
group_rollup_mode: "flat",
|
||||
aggregates: { value: "sum" },
|
||||
});
|
||||
|
||||
let paths: (string | null)[] = [];
|
||||
await view.with_typed_arrays(
|
||||
{},
|
||||
(
|
||||
n: string[],
|
||||
vals: any[],
|
||||
_valids: any[],
|
||||
dicts: (string[] | null)[],
|
||||
) => {
|
||||
const idx = n.indexOf("__ROW_PATH_0__");
|
||||
const keys = vals[idx] as Int32Array;
|
||||
const dict = dicts[idx]!;
|
||||
paths = Array.from(keys).map((k) =>
|
||||
k >= 0 ? dict[k] : null,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
expect(paths.slice().sort()).toEqual(["a", "b", "c"]);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("aggregate column contains per-group sums in flat mode", async () => {
|
||||
// Sum aggregate per group: a=10+20=30, b=30.
|
||||
const table = await perspective.table({
|
||||
category: ["a", "a", "b"],
|
||||
value: [10, 20, 30],
|
||||
});
|
||||
const view = await table.view({
|
||||
group_by: ["category"],
|
||||
group_rollup_mode: "flat",
|
||||
aggregates: { value: "sum" },
|
||||
});
|
||||
|
||||
const rowMap: Record<string, number> = {};
|
||||
await view.with_typed_arrays(
|
||||
{},
|
||||
(
|
||||
n: string[],
|
||||
vals: any[],
|
||||
_valids: any[],
|
||||
dicts: (string[] | null)[],
|
||||
) => {
|
||||
const pathIdx = n.indexOf("__ROW_PATH_0__");
|
||||
const valueIdx = n.indexOf("value");
|
||||
const keys = vals[pathIdx] as Int32Array;
|
||||
const dict = dicts[pathIdx]!;
|
||||
const values = vals[valueIdx] as ArrayLike<number>;
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
rowMap[dict[keys[i]]] = values[i];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
expect(rowMap).toEqual({ a: 30, b: 30 });
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("flat mode with two-level group_by emits both __ROW_PATH_N__ columns", async () => {
|
||||
const table = await perspective.table({
|
||||
region: ["US", "US", "EU"],
|
||||
country: ["x", "y", "a"],
|
||||
value: [1, 2, 3],
|
||||
});
|
||||
const view = await table.view({
|
||||
group_by: ["region", "country"],
|
||||
group_rollup_mode: "flat",
|
||||
aggregates: { value: "sum" },
|
||||
});
|
||||
|
||||
let names: string[] = [];
|
||||
const paths: Record<string, (string | null)[]> = {};
|
||||
await view.with_typed_arrays(
|
||||
{},
|
||||
(
|
||||
n: string[],
|
||||
vals: any[],
|
||||
_valids: any[],
|
||||
dicts: (string[] | null)[],
|
||||
) => {
|
||||
names = Array.from(n);
|
||||
for (const key of ["__ROW_PATH_0__", "__ROW_PATH_1__"]) {
|
||||
const idx = n.indexOf(key);
|
||||
if (idx >= 0) {
|
||||
const keys = vals[idx] as Int32Array;
|
||||
const dict = dicts[idx]!;
|
||||
paths[key] = Array.from(keys).map((k) =>
|
||||
k >= 0 ? dict[k] : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
expect(names).toContain("__ROW_PATH_0__");
|
||||
expect(names).toContain("__ROW_PATH_1__");
|
||||
// 3 distinct (region, country) combinations — one row each.
|
||||
expect(paths["__ROW_PATH_0__"].length).toEqual(3);
|
||||
expect(paths["__ROW_PATH_1__"].length).toEqual(3);
|
||||
// Ensure (region, country) pairs are preserved.
|
||||
const pairs = paths["__ROW_PATH_0__"].map(
|
||||
(r, i) => `${r}|${paths["__ROW_PATH_1__"][i]}`,
|
||||
);
|
||||
expect(pairs.slice().sort()).toEqual(["EU|a", "US|x", "US|y"]);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("flat mode: all __ROW_PATH_N__ values are non-null (no aggregate rows)", async () => {
|
||||
// In rollup mode, aggregate/total rows have null path segments.
|
||||
// In flat mode, every row is a leaf so all paths are fully
|
||||
// populated.
|
||||
const table = await perspective.table({
|
||||
region: ["US", "EU"],
|
||||
country: ["x", "y"],
|
||||
value: [1, 2],
|
||||
});
|
||||
const view = await table.view({
|
||||
group_by: ["region", "country"],
|
||||
group_rollup_mode: "flat",
|
||||
aggregates: { value: "sum" },
|
||||
});
|
||||
|
||||
let hasNullPath = false;
|
||||
await view.with_typed_arrays(
|
||||
{},
|
||||
(
|
||||
n: string[],
|
||||
vals: any[],
|
||||
_valids: any[],
|
||||
dicts: (string[] | null)[],
|
||||
) => {
|
||||
for (let i = 0; i < n.length; i++) {
|
||||
if (!n[i].startsWith("__ROW_PATH_")) continue;
|
||||
const keys = vals[i] as Int32Array;
|
||||
const dict = dicts[i];
|
||||
for (let j = 0; j < keys.length; j++) {
|
||||
if (keys[j] < 0 || !dict || !dict[keys[j]]) {
|
||||
hasNullPath = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
expect(hasNullPath).toBe(false);
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
|
||||
test("flat mode preserves value types (Int64 aggregate not coerced)", async () => {
|
||||
// Flat-mode sum over integer input still flows through as an
|
||||
// Int64 column (from the C++ engine's aggregate type), which
|
||||
// with_typed_arrays converts to Float64Array.
|
||||
const table = await perspective.table({
|
||||
category: ["a", "b", "c"],
|
||||
value: [10, 20, 30],
|
||||
});
|
||||
const view = await table.view({
|
||||
group_by: ["category"],
|
||||
group_rollup_mode: "flat",
|
||||
aggregates: { value: "sum" },
|
||||
});
|
||||
|
||||
let valueType = "";
|
||||
await view.with_typed_arrays({}, (n: string[], vals: any[]) => {
|
||||
const idx = n.indexOf("value");
|
||||
valueType = vals[idx].constructor.name;
|
||||
});
|
||||
|
||||
expect(valueType).toEqual("Float64Array");
|
||||
await view.delete();
|
||||
await table.delete();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user