@perspective-dev/client 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdn/perspective-server.worker.js +1 -1
- package/dist/cdn/perspective-server.worker.js.map +3 -3
- package/dist/cdn/perspective.js +2 -2
- package/dist/cdn/perspective.js.map +4 -4
- package/dist/esm/perspective.browser.d.ts +5 -1
- package/dist/esm/perspective.inline.js +2 -2
- package/dist/esm/perspective.inline.js.map +4 -4
- package/dist/esm/perspective.js +2 -2
- package/dist/esm/perspective.js.map +4 -4
- package/dist/esm/perspective.node.d.ts +6 -1
- package/dist/esm/perspective.node.js +722 -353
- package/dist/esm/perspective.node.js.map +4 -4
- package/dist/esm/ts-rs/ColumnType.d.ts +4 -0
- package/dist/esm/ts-rs/ViewConfig.d.ts +18 -0
- package/dist/esm/ts-rs/ViewWindow.d.ts +9 -9
- package/dist/esm/virtual_server.d.ts +47 -0
- package/dist/esm/virtual_servers/duckdb.d.ts +39 -0
- package/dist/esm/virtual_servers/duckdb.js +12 -0
- package/dist/esm/virtual_servers/duckdb.js.map +7 -0
- package/dist/esm/wasm/browser.d.ts +1 -1
- package/dist/wasm/perspective-js.d.ts +52 -13
- package/dist/wasm/perspective-js.js +680 -399
- package/dist/wasm/perspective-js.wasm +0 -0
- package/dist/wasm/perspective-js.wasm.d.ts +20 -8
- package/package.json +4 -1
- package/src/rust/client.rs +14 -5
- package/src/rust/lib.rs +11 -1
- package/src/rust/table.rs +3 -2
- package/src/rust/table_data.rs +19 -14
- package/src/rust/utils/browser.rs +0 -4
- package/src/rust/utils/console_logger.rs +3 -2
- package/src/rust/utils/errors.rs +10 -28
- package/src/rust/utils/futures.rs +3 -3
- package/src/rust/utils/json.rs +4 -4
- package/src/rust/virtual_server.rs +746 -0
- package/src/ts/perspective-server.worker.ts +33 -23
- package/src/ts/perspective.browser.ts +17 -2
- package/src/ts/perspective.node.ts +46 -11
- package/src/ts/ts-rs/ColumnType.ts +6 -0
- package/src/ts/ts-rs/ViewConfig.ts +8 -0
- package/src/ts/ts-rs/ViewWindow.ts +3 -3
- package/src/ts/virtual_server.ts +126 -0
- package/src/ts/virtual_servers/duckdb.ts +511 -0
- package/src/ts/wasm/browser.ts +17 -9
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
VirtualDataSlice,
|
|
15
|
+
VirtualServerHandler,
|
|
16
|
+
} from "@perspective-dev/client";
|
|
17
|
+
import type { ColumnType } from "@perspective-dev/client/dist/esm/ts-rs/ColumnType.d.ts";
|
|
18
|
+
import type { ViewConfig } from "@perspective-dev/client/dist/esm/ts-rs/ViewConfig.d.ts";
|
|
19
|
+
import type { ViewWindow } from "@perspective-dev/client/dist/esm/ts-rs/ViewWindow.d.ts";
|
|
20
|
+
import type * as duckdb from "@duckdb/duckdb-wasm";
|
|
21
|
+
|
|
22
|
+
const NUMBER_AGGS = [
|
|
23
|
+
"sum",
|
|
24
|
+
"count",
|
|
25
|
+
"any_value",
|
|
26
|
+
"arbitrary",
|
|
27
|
+
"array_agg",
|
|
28
|
+
"avg",
|
|
29
|
+
"bit_and",
|
|
30
|
+
"bit_or",
|
|
31
|
+
"bit_xor",
|
|
32
|
+
"bitstring_agg",
|
|
33
|
+
"bool_and",
|
|
34
|
+
"bool_or",
|
|
35
|
+
"countif",
|
|
36
|
+
"favg",
|
|
37
|
+
"fsum",
|
|
38
|
+
"geomean",
|
|
39
|
+
"kahan_sum",
|
|
40
|
+
"last",
|
|
41
|
+
"max",
|
|
42
|
+
"min",
|
|
43
|
+
"product",
|
|
44
|
+
"string_agg",
|
|
45
|
+
"sumkahan",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const STRING_AGGS = [
|
|
49
|
+
"count",
|
|
50
|
+
"any_value",
|
|
51
|
+
"arbitrary",
|
|
52
|
+
"first",
|
|
53
|
+
"countif",
|
|
54
|
+
"last",
|
|
55
|
+
"string_agg",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const FILTER_OPS = [
|
|
59
|
+
"==",
|
|
60
|
+
"!=",
|
|
61
|
+
"LIKE",
|
|
62
|
+
"IS DISTINCT FROM",
|
|
63
|
+
"IS NOT DISTINCT FROM",
|
|
64
|
+
">=",
|
|
65
|
+
"<=",
|
|
66
|
+
">",
|
|
67
|
+
"<",
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
function duckdbTypeToPsp(name: string): ColumnType {
|
|
71
|
+
if (name === "VARCHAR") return "string";
|
|
72
|
+
if (name === "DOUBLE" || name === "BIGINT" || name === "HUGEINT")
|
|
73
|
+
return "float";
|
|
74
|
+
if (name.startsWith("Decimal")) return "float";
|
|
75
|
+
if (name.startsWith("Int")) return "integer";
|
|
76
|
+
if (name === "INTEGER") return "integer";
|
|
77
|
+
if (name === "Utf8") return "string";
|
|
78
|
+
if (name === "Date32<DAY>") return "date";
|
|
79
|
+
if (name === "Float64") return "float";
|
|
80
|
+
if (name === "DATE") return "date";
|
|
81
|
+
if (name === "BOOLEAN") return "boolean";
|
|
82
|
+
if (name === "TIMESTAMP") return "datetime";
|
|
83
|
+
throw new Error(`Unknown type '${name}'`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function convertDecimalToNumber(value: any, dtypeString: string) {
|
|
87
|
+
if (
|
|
88
|
+
value === null ||
|
|
89
|
+
value === undefined ||
|
|
90
|
+
!(value instanceof Uint32Array || value instanceof Int32Array)
|
|
91
|
+
) {
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let bigIntValue = BigInt(0);
|
|
96
|
+
for (let i = 0; i < value.length; i++) {
|
|
97
|
+
bigIntValue |= BigInt(value[i]) << BigInt(i * 32);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const maxInt128 = BigInt(2) ** BigInt(127);
|
|
101
|
+
if (bigIntValue >= maxInt128) {
|
|
102
|
+
bigIntValue -= BigInt(2) ** BigInt(128);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const scaleMatch = dtypeString.match(/Decimal\[\d+e(\d+)\]/);
|
|
106
|
+
const scale = scaleMatch ? parseInt(scaleMatch[1]) : 0;
|
|
107
|
+
|
|
108
|
+
if (scale > 0) {
|
|
109
|
+
return Number(bigIntValue) / Math.pow(10, scale);
|
|
110
|
+
} else {
|
|
111
|
+
return Number(bigIntValue);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function runQuery(
|
|
116
|
+
db: duckdb.AsyncDuckDBConnection,
|
|
117
|
+
query: string,
|
|
118
|
+
options: { columns: true },
|
|
119
|
+
): Promise<{
|
|
120
|
+
rows: any[];
|
|
121
|
+
columns: string[];
|
|
122
|
+
dtypes: string[];
|
|
123
|
+
}>;
|
|
124
|
+
|
|
125
|
+
async function runQuery(
|
|
126
|
+
db: duckdb.AsyncDuckDBConnection,
|
|
127
|
+
query: string,
|
|
128
|
+
options?: { columns: boolean },
|
|
129
|
+
): Promise<any[]>;
|
|
130
|
+
|
|
131
|
+
async function runQuery(
|
|
132
|
+
db: duckdb.AsyncDuckDBConnection,
|
|
133
|
+
query: string,
|
|
134
|
+
options: { columns?: boolean } = {},
|
|
135
|
+
) {
|
|
136
|
+
query = query.replace(/\s+/g, " ").trim();
|
|
137
|
+
// console.log("Query:", query);
|
|
138
|
+
try {
|
|
139
|
+
const result = await db.query(query);
|
|
140
|
+
if (options.columns) {
|
|
141
|
+
return {
|
|
142
|
+
rows: result.toArray(),
|
|
143
|
+
columns: result.schema.fields.map((f) => f.name),
|
|
144
|
+
dtypes: result.schema.fields.map((f) => f.type.toString()),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result.toArray();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error("Query error:", error);
|
|
151
|
+
console.error("Query:", query);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export class DuckDBHandler implements VirtualServerHandler {
|
|
157
|
+
private db: duckdb.AsyncDuckDBConnection;
|
|
158
|
+
|
|
159
|
+
constructor(db: duckdb.AsyncDuckDBConnection) {
|
|
160
|
+
this.db = db;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
getFeatures() {
|
|
164
|
+
return {
|
|
165
|
+
group_by: true,
|
|
166
|
+
split_by: true,
|
|
167
|
+
sort: true,
|
|
168
|
+
expressions: true,
|
|
169
|
+
filter_ops: {
|
|
170
|
+
integer: FILTER_OPS,
|
|
171
|
+
float: FILTER_OPS,
|
|
172
|
+
string: FILTER_OPS,
|
|
173
|
+
boolean: FILTER_OPS,
|
|
174
|
+
date: FILTER_OPS,
|
|
175
|
+
datetime: FILTER_OPS,
|
|
176
|
+
},
|
|
177
|
+
aggregates: {
|
|
178
|
+
integer: NUMBER_AGGS,
|
|
179
|
+
float: NUMBER_AGGS,
|
|
180
|
+
string: STRING_AGGS,
|
|
181
|
+
boolean: STRING_AGGS,
|
|
182
|
+
date: STRING_AGGS,
|
|
183
|
+
datetime: STRING_AGGS,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async getHostedTables() {
|
|
189
|
+
const results = await runQuery(this.db, "SHOW ALL TABLES");
|
|
190
|
+
return results.map((row) => row.toJSON().name);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async tableSchema(tableId: string) {
|
|
194
|
+
const query = `DESCRIBE ${tableId}`;
|
|
195
|
+
const results = await runQuery(this.db, query);
|
|
196
|
+
const schema = {} as Record<string, ColumnType>;
|
|
197
|
+
for (const result of results) {
|
|
198
|
+
const res = result.toJSON();
|
|
199
|
+
const colName = res.column_name;
|
|
200
|
+
if (!colName.startsWith("__") || !colName.endsWith("__")) {
|
|
201
|
+
const cleanName = colName.split("_").slice(-1)[0] as string;
|
|
202
|
+
schema[cleanName] = duckdbTypeToPsp(res.column_type);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return schema;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async viewColumnSize(viewId: string, config: ViewConfig) {
|
|
210
|
+
const query = `SELECT COUNT(*) FROM (DESCRIBE ${viewId})`;
|
|
211
|
+
const results = await runQuery(this.db, query);
|
|
212
|
+
const gs = config.group_by?.length || 0;
|
|
213
|
+
const count = Number(Object.values(results[0].toJSON())[0]);
|
|
214
|
+
return (
|
|
215
|
+
count -
|
|
216
|
+
(gs === 0 ? 0 : gs + (config.split_by?.length === 0 ? 1 : 0))
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async tableSize(tableId: string) {
|
|
221
|
+
const query = `SELECT COUNT(*) FROM ${tableId}`;
|
|
222
|
+
const results = await runQuery(this.db, query);
|
|
223
|
+
return Number(results[0].toJSON()["count_star()"]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// async viewSchema(viewId: string, config: ViewConfig) {
|
|
227
|
+
// return this.tableSchema(viewId);
|
|
228
|
+
// }
|
|
229
|
+
|
|
230
|
+
// async viewSize(viewId: string) {
|
|
231
|
+
// return this.tableSize(viewId);
|
|
232
|
+
// }
|
|
233
|
+
|
|
234
|
+
async tableMakeView(tableId: string, viewId: string, config: ViewConfig) {
|
|
235
|
+
const columns = config.columns || [];
|
|
236
|
+
const group_by = config.group_by || [];
|
|
237
|
+
const split_by = config.split_by || [];
|
|
238
|
+
const aggregates = config.aggregates || {};
|
|
239
|
+
const sort = config.sort || [];
|
|
240
|
+
const expressions = config.expressions || {};
|
|
241
|
+
const filter = config.filter || [];
|
|
242
|
+
|
|
243
|
+
const colName = (col: string) => {
|
|
244
|
+
const expr = expressions[col];
|
|
245
|
+
return expr || `"${col}"`;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const getAggregate = (col: string) => aggregates[col] || null;
|
|
249
|
+
|
|
250
|
+
const generateSelectClauses = () => {
|
|
251
|
+
const clauses = [];
|
|
252
|
+
if (group_by.length > 0) {
|
|
253
|
+
for (const col of columns) {
|
|
254
|
+
if (col !== null) {
|
|
255
|
+
// TODO texodus
|
|
256
|
+
const agg = getAggregate(col) || "any_value";
|
|
257
|
+
clauses.push(`${agg}(${colName(col)}) as "${col}"`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (split_by.length === 0) {
|
|
262
|
+
for (let idx = 0; idx < group_by.length; idx++) {
|
|
263
|
+
clauses.push(
|
|
264
|
+
`${colName(group_by[idx])} as __ROW_PATH_${idx}__`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const groups = group_by.map(colName).join(", ");
|
|
269
|
+
clauses.push(`GROUPING_ID(${groups}) AS __GROUPING_ID__`);
|
|
270
|
+
}
|
|
271
|
+
} else if (columns.length > 0) {
|
|
272
|
+
for (const col of columns) {
|
|
273
|
+
if (col !== null) {
|
|
274
|
+
// TODO texodus
|
|
275
|
+
clauses.push(
|
|
276
|
+
`${colName(col)} as "${col.replace(/"/g, '""')}"`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return clauses;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const orderByClauses = [];
|
|
286
|
+
const windowClauses = [];
|
|
287
|
+
const whereClauses = [];
|
|
288
|
+
|
|
289
|
+
if (group_by.length > 0) {
|
|
290
|
+
for (let gidx = 0; gidx < group_by.length; gidx++) {
|
|
291
|
+
const groups = group_by
|
|
292
|
+
.slice(0, gidx + 1)
|
|
293
|
+
.map(colName)
|
|
294
|
+
.join(", ");
|
|
295
|
+
if (split_by.length === 0) {
|
|
296
|
+
orderByClauses.push(`GROUPING_ID(${groups}) DESC`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const [sort_col, sort_dir] of sort) {
|
|
300
|
+
if (sort_dir !== "none") {
|
|
301
|
+
const agg = getAggregate(sort_col) || "any_value";
|
|
302
|
+
if (gidx >= group_by.length - 1) {
|
|
303
|
+
orderByClauses.push(
|
|
304
|
+
`${agg}(${colName(sort_col)}) ${sort_dir}`,
|
|
305
|
+
);
|
|
306
|
+
} else {
|
|
307
|
+
orderByClauses.push(
|
|
308
|
+
`first(${agg}(${colName(sort_col)})) OVER __WINDOW_${gidx}__ ${sort_dir}`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
orderByClauses.push(`__ROW_PATH_${gidx}__ ASC`);
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
for (const [sort_col, sort_dir] of sort) {
|
|
318
|
+
if (sort_dir) {
|
|
319
|
+
orderByClauses.push(`${colName(sort_col)} ${sort_dir}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (sort.length > 0 && group_by.length > 1) {
|
|
325
|
+
for (let gidx = 0; gidx < group_by.length - 1; gidx++) {
|
|
326
|
+
const partition = Array.from(
|
|
327
|
+
{ length: gidx + 1 },
|
|
328
|
+
(_, i) => `__ROW_PATH_${i}__`,
|
|
329
|
+
).join(", ");
|
|
330
|
+
const sub_groups = group_by
|
|
331
|
+
.slice(0, gidx + 1)
|
|
332
|
+
.map(colName)
|
|
333
|
+
.join(", ");
|
|
334
|
+
const groups = group_by.map(colName).join(", ");
|
|
335
|
+
windowClauses.push(
|
|
336
|
+
`__WINDOW_${gidx}__ AS (PARTITION BY GROUPING_ID(${sub_groups}), ${partition} ORDER BY ${groups})`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
for (const [name, op, value] of filter) {
|
|
342
|
+
if (value !== null && value !== undefined) {
|
|
343
|
+
const term_lit =
|
|
344
|
+
typeof value === "string" ? `'${value}'` : String(value);
|
|
345
|
+
whereClauses.push(`${colName(name)} ${op} ${term_lit}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let query;
|
|
350
|
+
if (split_by.length > 0) {
|
|
351
|
+
query = `SELECT * FROM ${tableId}`;
|
|
352
|
+
} else {
|
|
353
|
+
const selectClauses = generateSelectClauses();
|
|
354
|
+
query = `SELECT ${selectClauses.join(", ")} FROM ${tableId}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (whereClauses.length > 0) {
|
|
358
|
+
query = `${query} WHERE ${whereClauses.join(" AND ")}`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (split_by.length > 0) {
|
|
362
|
+
const groups = group_by.map(colName).join(", ");
|
|
363
|
+
const group_aliases = group_by
|
|
364
|
+
.map((x, i) => `${colName(x)} AS __ROW_PATH_${i}__`)
|
|
365
|
+
.join(", ");
|
|
366
|
+
const pivotOn = split_by.map((c) => `"${c}"`).join(", ");
|
|
367
|
+
const pivotUsing = generateSelectClauses().join(", ");
|
|
368
|
+
|
|
369
|
+
query = `
|
|
370
|
+
SELECT * EXCLUDE (${groups}), ${group_aliases} FROM (
|
|
371
|
+
PIVOT (${query})
|
|
372
|
+
ON ${pivotOn}
|
|
373
|
+
USING ${pivotUsing}
|
|
374
|
+
GROUP BY ${groups}
|
|
375
|
+
)
|
|
376
|
+
`;
|
|
377
|
+
} else if (group_by.length > 0) {
|
|
378
|
+
const groups = group_by.map(colName).join(", ");
|
|
379
|
+
query = `${query} GROUP BY ROLLUP(${groups})`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (windowClauses.length > 0) {
|
|
383
|
+
query = `${query} WINDOW ${windowClauses.join(", ")}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (orderByClauses.length > 0) {
|
|
387
|
+
query = `${query} ORDER BY ${orderByClauses.join(", ")}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
query = `CREATE TABLE ${viewId} AS (${query})`;
|
|
391
|
+
await runQuery(this.db, query);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async tableValidateExpression(tableId: string, expression: string) {
|
|
395
|
+
const query = `DESCRIBE (select ${expression} from ${tableId})`;
|
|
396
|
+
const results = await runQuery(this.db, query);
|
|
397
|
+
return duckdbTypeToPsp(results[0].toJSON()["column_type"]);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async viewDelete(viewId: string) {
|
|
401
|
+
const query = `DROP TABLE IF EXISTS ${viewId}`;
|
|
402
|
+
await runQuery(this.db, query);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async viewGetData(
|
|
406
|
+
viewId: string,
|
|
407
|
+
config: ViewConfig,
|
|
408
|
+
viewport: ViewWindow,
|
|
409
|
+
dataSlice: VirtualDataSlice,
|
|
410
|
+
) {
|
|
411
|
+
const group_by = config.group_by || [];
|
|
412
|
+
const split_by = config.split_by || [];
|
|
413
|
+
const start_col = viewport.start_col;
|
|
414
|
+
const end_col = viewport.end_col;
|
|
415
|
+
const start_row = viewport.start_row || 0;
|
|
416
|
+
const end_row = viewport.end_row;
|
|
417
|
+
|
|
418
|
+
let limit = "";
|
|
419
|
+
if (end_row !== null && end_row !== undefined) {
|
|
420
|
+
limit = `LIMIT ${end_row - start_row} OFFSET ${start_row}`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const schemaQuery = `DESCRIBE ${viewId}`;
|
|
424
|
+
const schemaResults = await runQuery(this.db, schemaQuery);
|
|
425
|
+
const columnTypes = new Map();
|
|
426
|
+
for (const result of schemaResults) {
|
|
427
|
+
const res = result.toJSON();
|
|
428
|
+
columnTypes.set(res.column_name, res.column_type);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const dataColumns = Array.from(columnTypes.entries())
|
|
432
|
+
.filter(([colName]) => !colName.startsWith("__"))
|
|
433
|
+
.slice(start_col, end_col);
|
|
434
|
+
|
|
435
|
+
const groupByColsList = [];
|
|
436
|
+
if (group_by.length > 0) {
|
|
437
|
+
if (split_by.length === 0) {
|
|
438
|
+
groupByColsList.push("__GROUPING_ID__");
|
|
439
|
+
}
|
|
440
|
+
for (let idx = 0; idx < group_by.length; idx++) {
|
|
441
|
+
groupByColsList.push(`__ROW_PATH_${idx}__`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const allColumns = [
|
|
446
|
+
...groupByColsList.map((col) => `"${col}"`),
|
|
447
|
+
...dataColumns.map(([colName]) => `"${colName}"`),
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
const query = `
|
|
451
|
+
SELECT ${allColumns.join(", ")}
|
|
452
|
+
FROM ${viewId} ${limit}
|
|
453
|
+
`;
|
|
454
|
+
|
|
455
|
+
const { rows, columns, dtypes } = await runQuery(this.db, query, {
|
|
456
|
+
columns: true,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
for (let cidx = 0; cidx < columns.length; cidx++) {
|
|
460
|
+
const col = columns[cidx];
|
|
461
|
+
|
|
462
|
+
if (cidx === 0 && group_by.length > 0 && split_by.length === 0) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
let group_by_index = null;
|
|
467
|
+
let max_grouping_id = null;
|
|
468
|
+
const row_path_match = col.match(/__ROW_PATH_(\d+)__/);
|
|
469
|
+
if (row_path_match) {
|
|
470
|
+
group_by_index = parseInt(row_path_match[1]);
|
|
471
|
+
max_grouping_id = 2 ** (group_by.length - group_by_index) - 1;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const dtype = duckdbTypeToPsp(dtypes[cidx]);
|
|
475
|
+
const isDecimal = dtypes[cidx].startsWith("Decimal");
|
|
476
|
+
const colName =
|
|
477
|
+
group_by_index !== null
|
|
478
|
+
? "__ROW_PATH__"
|
|
479
|
+
: col.replace(/_/g, "|");
|
|
480
|
+
|
|
481
|
+
for (let ridx = 0; ridx < rows.length; ridx++) {
|
|
482
|
+
const row = rows[ridx];
|
|
483
|
+
const rowArray = row.toArray();
|
|
484
|
+
const shouldSet =
|
|
485
|
+
split_by.length > 0 ||
|
|
486
|
+
max_grouping_id === null ||
|
|
487
|
+
rowArray[0] < max_grouping_id;
|
|
488
|
+
|
|
489
|
+
if (shouldSet) {
|
|
490
|
+
let value = rowArray[cidx];
|
|
491
|
+
|
|
492
|
+
if (isDecimal) {
|
|
493
|
+
value = convertDecimalToNumber(value, dtypes[cidx]);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (typeof value === "bigint") {
|
|
497
|
+
value = Number(value);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
dataSlice.setCol(
|
|
501
|
+
dtype,
|
|
502
|
+
colName,
|
|
503
|
+
ridx,
|
|
504
|
+
value,
|
|
505
|
+
group_by_index,
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
package/src/ts/wasm/browser.ts
CHANGED
|
@@ -32,7 +32,10 @@ function invert_promise<T>(): [
|
|
|
32
32
|
];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
async function _init(
|
|
35
|
+
async function _init(
|
|
36
|
+
ws: MessagePort | Worker,
|
|
37
|
+
wasm: WebAssembly.Module | undefined,
|
|
38
|
+
) {
|
|
36
39
|
const [sender, receiver] = invert_promise();
|
|
37
40
|
ws.addEventListener("message", function listener(resp) {
|
|
38
41
|
ws.removeEventListener("message", listener);
|
|
@@ -47,7 +50,12 @@ async function _init(ws: MessagePort | Worker, wasm: WebAssembly.Module) {
|
|
|
47
50
|
ws.onmessageerror = console.error;
|
|
48
51
|
ws.postMessage(
|
|
49
52
|
{ cmd: "init", args: [wasm] },
|
|
50
|
-
{
|
|
53
|
+
{
|
|
54
|
+
transfer:
|
|
55
|
+
wasm === undefined || wasm instanceof WebAssembly.Module
|
|
56
|
+
? []
|
|
57
|
+
: [wasm],
|
|
58
|
+
},
|
|
51
59
|
);
|
|
52
60
|
|
|
53
61
|
await receiver;
|
|
@@ -61,11 +69,13 @@ async function _init(ws: MessagePort | Worker, wasm: WebAssembly.Module) {
|
|
|
61
69
|
*/
|
|
62
70
|
export async function worker(
|
|
63
71
|
module: Promise<typeof psp>,
|
|
64
|
-
server_wasm: Promise<WebAssembly.Module
|
|
65
|
-
perspective_wasm_worker: Promise<
|
|
72
|
+
server_wasm: Promise<WebAssembly.Module> | undefined,
|
|
73
|
+
perspective_wasm_worker: Promise<
|
|
74
|
+
SharedWorker | ServiceWorker | Worker | MessagePort
|
|
75
|
+
>,
|
|
66
76
|
) {
|
|
67
77
|
let [wasm, webworker]: [
|
|
68
|
-
WebAssembly.Module,
|
|
78
|
+
WebAssembly.Module | undefined,
|
|
69
79
|
SharedWorker | ServiceWorker | Worker | MessagePort,
|
|
70
80
|
] = await Promise.all([server_wasm, perspective_wasm_worker]);
|
|
71
81
|
|
|
@@ -77,10 +87,8 @@ export async function worker(
|
|
|
77
87
|
) {
|
|
78
88
|
port = webworker.port;
|
|
79
89
|
} else {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
webworker.postMessage(null, [messageChannel.port2]);
|
|
83
|
-
port = messageChannel.port1;
|
|
90
|
+
// Assume `MessagePort`
|
|
91
|
+
port = webworker as MessagePort;
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
const client = new Client(
|