@perspective-dev/client 4.1.0 → 4.2.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.
@@ -0,0 +1,362 @@
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
+ /**
14
+ * An implementation of a Perspective Virtual Server for DuckDB.
15
+ *
16
+ * This import is optional, and so must be imported manually from either
17
+ * `@perspective-dev/client/dist/esm/virtual_servers/duckdb.js` or
18
+ * `@perspective-dev/client/src/ts/virtual_servers/duckdb.ts`, it is not
19
+ * exported from the package root `@perspective-dev/client`
20
+ *
21
+ * @module
22
+ */
23
+
24
+ import type * as perspective from "@perspective-dev/client";
25
+ import type { ColumnType } from "@perspective-dev/client/dist/esm/ts-rs/ColumnType.d.ts";
26
+ import type { ViewConfig } from "@perspective-dev/client/dist/esm/ts-rs/ViewConfig.d.ts";
27
+ import type { ViewWindow } from "@perspective-dev/client/dist/esm/ts-rs/ViewWindow.d.ts";
28
+ import type * as clickhouse from "@clickhouse/client-web";
29
+
30
+ const NUMBER_AGGS = [
31
+ "sum",
32
+ "count",
33
+ "any_value",
34
+ "arbitrary",
35
+ "array_agg",
36
+ "avg",
37
+ "bit_and",
38
+ "bit_or",
39
+ "bit_xor",
40
+ "bitstring_agg",
41
+ "bool_and",
42
+ "bool_or",
43
+ "countif",
44
+ "favg",
45
+ "fsum",
46
+ "geomean",
47
+ "kahan_sum",
48
+ "last",
49
+ "max",
50
+ "min",
51
+ "product",
52
+ "string_agg",
53
+ "sumkahan",
54
+ ];
55
+
56
+ const STRING_AGGS = [
57
+ "count",
58
+ "any_value",
59
+ "arbitrary",
60
+ "first",
61
+ "countif",
62
+ "last",
63
+ "string_agg",
64
+ ];
65
+
66
+ const FILTER_OPS = [
67
+ "==",
68
+ "!=",
69
+ "LIKE",
70
+ "IS DISTINCT FROM",
71
+ "IS NOT DISTINCT FROM",
72
+ ">=",
73
+ "<=",
74
+ ">",
75
+ "<",
76
+ ];
77
+
78
+ function duckdbTypeToPsp(name: string): ColumnType {
79
+ if (name.startsWith("Nullable")) {
80
+ name = name.match(/Nullable\((.+?)\)/)![1];
81
+ }
82
+
83
+ if (name.startsWith("Array")) {
84
+ return "string";
85
+ }
86
+
87
+ if (name === "Int64" || name === "UInt64" || name === "Float64") {
88
+ return "float";
89
+ }
90
+
91
+ if (name === "String") {
92
+ return "string";
93
+ }
94
+
95
+ if (name === "DateTime") {
96
+ return "datetime";
97
+ }
98
+
99
+ if (name === "Date") {
100
+ return "date";
101
+ }
102
+
103
+ throw new Error(`Unknown type '${name}'`);
104
+ }
105
+
106
+ function convertDecimalToNumber(value: any, dtypeString: string) {
107
+ if (!(value instanceof Uint32Array || value instanceof Int32Array)) {
108
+ return value;
109
+ }
110
+
111
+ let bigIntValue = BigInt(0);
112
+ for (let i = 0; i < value.length; i++) {
113
+ bigIntValue |= BigInt(value[i]) << BigInt(i * 32);
114
+ }
115
+
116
+ const scaleMatch = dtypeString.match(/Decimal\[\d+e(\d+)\]/);
117
+ if (scaleMatch) {
118
+ const scale = parseInt(scaleMatch[1]);
119
+ return Number(bigIntValue) / Math.pow(10, scale);
120
+ } else {
121
+ return Number(bigIntValue);
122
+ }
123
+ }
124
+
125
+ class Lock {
126
+ lockPromise: Promise<void>;
127
+ constructor() {
128
+ this.lockPromise = Promise.resolve();
129
+ }
130
+
131
+ acquire() {
132
+ let releaseLock: (value: void) => void;
133
+ const newLockPromise: Promise<void> = new Promise((resolve) => {
134
+ releaseLock = resolve;
135
+ });
136
+
137
+ const acquirePromise = this.lockPromise.then(() => releaseLock);
138
+ this.lockPromise = newLockPromise;
139
+ return acquirePromise;
140
+ }
141
+ }
142
+
143
+ const LOCK = new Lock();
144
+
145
+ async function runQuery(
146
+ db: clickhouse.ClickHouseClient,
147
+ query: string,
148
+ options: { columns?: true; execute?: boolean },
149
+ ): Promise<{
150
+ rows: any[];
151
+ columns: string[];
152
+ dtypes: string[];
153
+ }>;
154
+
155
+ async function runQuery(
156
+ db: clickhouse.ClickHouseClient,
157
+ query: string,
158
+ options?: { columns?: false; execute?: boolean },
159
+ ): Promise<any[]>;
160
+
161
+ async function runQuery(
162
+ db: clickhouse.ClickHouseClient,
163
+ query: string,
164
+ options: { columns?: boolean; execute?: boolean } = {},
165
+ ) {
166
+ query = query.replace(/\s+/g, " ").trim();
167
+ const release = await LOCK.acquire();
168
+ try {
169
+ const result = await db.query({ query });
170
+ if (!options.execute) {
171
+ const { data, meta } =
172
+ (await result.json()) as clickhouse.ResponseJSON<unknown>;
173
+
174
+ if (options.columns) {
175
+ return {
176
+ rows: data,
177
+ columns: meta!.map((f) => f.name),
178
+ dtypes: meta!.map((f) => f.type),
179
+ };
180
+ }
181
+
182
+ return data;
183
+ }
184
+ } catch (error) {
185
+ console.error("Query error:", error);
186
+ console.error("Query:", query);
187
+ throw error;
188
+ } finally {
189
+ release();
190
+ }
191
+ }
192
+
193
+ /**
194
+ * An implementation of Perspective's Virtual Server for `@duckdb/duckdb-wasm`.
195
+ */
196
+ export class ClickhouseHandler implements perspective.VirtualServerHandler {
197
+ private db: clickhouse.ClickHouseClient;
198
+ private sqlBuilder: perspective.GenericSQLVirtualServerModel;
199
+ constructor(db: clickhouse.ClickHouseClient, mod?: typeof perspective) {
200
+ if (!mod) {
201
+ if (customElements) {
202
+ const viewer_class: any =
203
+ customElements.get("perspective-viewer");
204
+ if (viewer_class) {
205
+ mod = viewer_class.__wasm_module__;
206
+ } else {
207
+ throw new Error("Missing perspective-client.wasm");
208
+ }
209
+ } else {
210
+ }
211
+ }
212
+
213
+ this.db = db;
214
+ this.sqlBuilder = new mod!.GenericSQLVirtualServerModel({
215
+ create_entity: "VIEW",
216
+ grouping_fn: "GROUPING",
217
+ });
218
+ }
219
+
220
+ getFeatures() {
221
+ return {
222
+ group_by: true,
223
+ split_by: false,
224
+ sort: true,
225
+ expressions: true,
226
+ filter_ops: {
227
+ integer: FILTER_OPS,
228
+ float: FILTER_OPS,
229
+ string: FILTER_OPS,
230
+ boolean: FILTER_OPS,
231
+ date: FILTER_OPS,
232
+ datetime: FILTER_OPS,
233
+ },
234
+ aggregates: {
235
+ integer: NUMBER_AGGS,
236
+ float: NUMBER_AGGS,
237
+ string: STRING_AGGS,
238
+ boolean: STRING_AGGS,
239
+ date: STRING_AGGS,
240
+ datetime: STRING_AGGS,
241
+ },
242
+ };
243
+ }
244
+
245
+ async getHostedTables() {
246
+ const query = "SHOW TABLES";
247
+ const results = await runQuery(this.db, query);
248
+ return results.map((row) => {
249
+ return `${row.name}`;
250
+ });
251
+ }
252
+
253
+ async tableSchema(tableId: string, config?: ViewConfig) {
254
+ const query = this.sqlBuilder.tableSchema(tableId);
255
+ const results = await runQuery(this.db, query);
256
+ const schema = {} as Record<string, ColumnType>;
257
+ for (const result of results) {
258
+ const colName = result.name;
259
+ if (!colName.startsWith("__")) {
260
+ schema[colName] = duckdbTypeToPsp(result.type) as ColumnType;
261
+ }
262
+ }
263
+
264
+ return schema;
265
+ }
266
+
267
+ async viewColumnSize(viewId: string, config: ViewConfig) {
268
+ const query = `SELECT COUNT() FROM system.columns WHERE table = '${viewId}'`;
269
+ const results = await runQuery(this.db, query);
270
+ const gs = config.group_by?.length || 0;
271
+ const count = Number(results[0]["COUNT()"]);
272
+ console.log(count);
273
+ return (
274
+ count -
275
+ (gs === 0 ? 0 : gs + (config.split_by?.length === 0 ? 1 : 0))
276
+ );
277
+ }
278
+
279
+ async tableSize(tableId: string) {
280
+ const query = this.sqlBuilder.tableSize(tableId);
281
+ const results = await runQuery(this.db, query);
282
+ return Number(results[0]["COUNT()"]);
283
+ }
284
+
285
+ async tableMakeView(tableId: string, viewId: string, config: ViewConfig) {
286
+ const query = this.sqlBuilder.tableMakeView(tableId, viewId, config);
287
+ await runQuery(this.db, query, { execute: true });
288
+ }
289
+
290
+ async tableValidateExpression(tableId: string, expression: string) {
291
+ const query = this.sqlBuilder.tableValidateExpression(
292
+ tableId,
293
+ expression,
294
+ );
295
+ const results = await runQuery(this.db, query);
296
+ return duckdbTypeToPsp(results[0]["type"]) as ColumnType;
297
+ }
298
+
299
+ async viewDelete(viewId: string) {
300
+ const query = this.sqlBuilder.viewDelete(viewId);
301
+ await runQuery(this.db, query, { execute: true });
302
+ }
303
+
304
+ async viewGetData(
305
+ viewId: string,
306
+ config: ViewConfig,
307
+ schema: Record<string, ColumnType>,
308
+ viewport: ViewWindow,
309
+ dataSlice: perspective.VirtualDataSlice,
310
+ ) {
311
+ const is_group_by = config.group_by?.length > 0;
312
+ const is_split_by = config.split_by?.length > 0;
313
+ const query = this.sqlBuilder.viewGetData(
314
+ viewId,
315
+ config,
316
+ viewport,
317
+ schema,
318
+ );
319
+
320
+ const { rows, columns, dtypes } = await runQuery(this.db, query, {
321
+ columns: true,
322
+ });
323
+
324
+ for (let cidx = 0; cidx < columns.length; cidx++) {
325
+ if (cidx === 0 && is_group_by && !is_split_by) {
326
+ // This is the grouping_id column, skip it
327
+ continue;
328
+ }
329
+
330
+ let col = columns[cidx];
331
+ if (is_split_by && !col.startsWith("__ROW_PATH_")) {
332
+ col = col.replaceAll("_", "|");
333
+ }
334
+
335
+ const dtype = duckdbTypeToPsp(dtypes[cidx]) as ColumnType;
336
+
337
+ const isDecimal = dtypes[cidx].startsWith("Decimal");
338
+ for (let ridx = 0; ridx < rows.length; ridx++) {
339
+ const row = rows[ridx];
340
+ const grouping_id = row["__GROUPING_ID__"];
341
+ let value = row[columns[cidx]];
342
+ if (isDecimal) {
343
+ value = convertDecimalToNumber(value, dtypes[cidx]);
344
+ }
345
+
346
+ if (typeof value === "bigint") {
347
+ value = Number(value);
348
+ }
349
+
350
+ if (dtype === "datetime" && typeof value === "string") {
351
+ value = +new Date(value);
352
+ }
353
+
354
+ if (dtype === "string" && typeof value !== "string") {
355
+ value = `${value}`;
356
+ }
357
+
358
+ dataSlice.setCol(dtype, col, ridx, value, grouping_id);
359
+ }
360
+ }
361
+ }
362
+ }