chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:25:07 +08:00
commit a26e856398
1681 changed files with 296950 additions and 0 deletions
@@ -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();
});
});
});