@perspective-dev/client 4.1.1 → 4.3.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.
Files changed (40) hide show
  1. package/dist/cdn/perspective.js +2 -2
  2. package/dist/cdn/perspective.js.map +4 -4
  3. package/dist/esm/perspective.browser.d.ts +4 -0
  4. package/dist/esm/perspective.inline.js +2 -2
  5. package/dist/esm/perspective.inline.js.map +4 -4
  6. package/dist/esm/perspective.js +2 -2
  7. package/dist/esm/perspective.js.map +4 -4
  8. package/dist/esm/perspective.node.d.ts +9 -0
  9. package/dist/esm/perspective.node.js +1629 -1291
  10. package/dist/esm/perspective.node.js.map +3 -3
  11. package/dist/esm/ts-rs/GroupRollupMode.d.ts +1 -0
  12. package/dist/esm/ts-rs/ViewConfig.d.ts +2 -0
  13. package/dist/esm/ts-rs/ViewConfigUpdate.d.ts +2 -0
  14. package/dist/esm/ts-rs/ViewWindow.d.ts +0 -1
  15. package/dist/esm/virtual_server.d.ts +1 -8
  16. package/dist/esm/virtual_servers/clickhouse.js +2 -0
  17. package/dist/esm/virtual_servers/clickhouse.js.map +7 -0
  18. package/dist/esm/virtual_servers/duckdb.d.ts +22 -7
  19. package/dist/esm/virtual_servers/duckdb.js +1 -11
  20. package/dist/esm/virtual_servers/duckdb.js.map +3 -3
  21. package/dist/wasm/perspective-js.d.ts +723 -647
  22. package/dist/wasm/perspective-js.js +2115 -1841
  23. package/dist/wasm/perspective-js.wasm +0 -0
  24. package/dist/wasm/perspective-js.wasm.d.ts +61 -49
  25. package/package.json +2 -1
  26. package/src/rust/generic_sql_model.rs +189 -0
  27. package/src/rust/lib.rs +2 -2
  28. package/src/rust/utils/console_logger.rs +4 -4
  29. package/src/rust/utils/futures.rs +1 -6
  30. package/src/rust/virtual_server.rs +50 -46
  31. package/src/ts/perspective.browser.ts +15 -2
  32. package/src/ts/perspective.node.ts +21 -1
  33. package/src/ts/ts-rs/GroupRollupMode.ts +3 -0
  34. package/src/ts/ts-rs/ViewConfig.ts +2 -1
  35. package/src/ts/ts-rs/ViewConfigUpdate.ts +2 -1
  36. package/src/ts/ts-rs/ViewWindow.ts +1 -1
  37. package/src/ts/virtual_server.ts +4 -14
  38. package/src/ts/virtual_servers/clickhouse.ts +363 -0
  39. package/src/ts/virtual_servers/duckdb.ts +138 -300
  40. package/tsconfig.json +1 -0
@@ -10,10 +10,18 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- import type {
14
- VirtualDataSlice,
15
- VirtualServerHandler,
16
- } from "@perspective-dev/client";
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";
17
25
  import type { ColumnType } from "@perspective-dev/client/dist/esm/ts-rs/ColumnType.d.ts";
18
26
  import type { ViewConfig } from "@perspective-dev/client/dist/esm/ts-rs/ViewConfig.d.ts";
19
27
  import type { ViewWindow } from "@perspective-dev/client/dist/esm/ts-rs/ViewWindow.d.ts";
@@ -68,32 +76,51 @@ const FILTER_OPS = [
68
76
  ];
69
77
 
70
78
  function duckdbTypeToPsp(name: string): ColumnType {
71
- if (name === "VARCHAR") return "string";
79
+ name = name.toLowerCase();
80
+ if (name === "varchar" || name == "utf8") {
81
+ return "string";
82
+ }
83
+
72
84
  if (
73
- name === "DOUBLE" ||
74
- name === "BIGINT" ||
75
- name === "HUGEINT" ||
76
- name.startsWith("Decimal")
77
- )
85
+ name === "double" ||
86
+ name === "bigint" ||
87
+ name === "hugeint" ||
88
+ name === "float64" ||
89
+ name.startsWith("decimal")
90
+ ) {
78
91
  return "float";
79
- if (name.startsWith("Decimal")) return "float";
80
- if (name.startsWith("Int")) return "integer";
81
- if (name === "INTEGER") return "integer";
82
- if (name === "Utf8") return "string";
83
- if (name === "Date32<DAY>") return "date";
84
- if (name === "Float64") return "float";
85
- if (name === "DATE") return "date";
86
- if (name === "BOOLEAN") return "boolean";
87
- if (name === "TIMESTAMP" || name.startsWith("Timestamp")) return "datetime";
88
- throw new Error(`Unknown type '${name}'`);
92
+ }
93
+
94
+ if (name.startsWith("int")) {
95
+ return "integer";
96
+ }
97
+
98
+ if (name.startsWith("date")) {
99
+ return "date";
100
+ }
101
+
102
+ if (name.startsWith("bool")) {
103
+ return "boolean";
104
+ }
105
+
106
+ if (name.startsWith("timestamp")) {
107
+ return "datetime";
108
+ }
109
+
110
+ if (name.startsWith("json")) {
111
+ return "string";
112
+ }
113
+
114
+ if (name.startsWith("struct")) {
115
+ return "string";
116
+ }
117
+
118
+ console.warn(`Unknown type '${name}'`);
119
+ return "string";
89
120
  }
90
121
 
91
122
  function convertDecimalToNumber(value: any, dtypeString: string) {
92
- if (
93
- value === null ||
94
- value === undefined ||
95
- !(value instanceof Uint32Array || value instanceof Int32Array)
96
- ) {
123
+ if (!(value instanceof Uint32Array || value instanceof Int32Array)) {
97
124
  return value;
98
125
  }
99
126
 
@@ -102,15 +129,9 @@ function convertDecimalToNumber(value: any, dtypeString: string) {
102
129
  bigIntValue |= BigInt(value[i]) << BigInt(i * 32);
103
130
  }
104
131
 
105
- const maxInt128 = BigInt(2) ** BigInt(127);
106
- if (bigIntValue >= maxInt128) {
107
- bigIntValue -= BigInt(2) ** BigInt(128);
108
- }
109
-
110
132
  const scaleMatch = dtypeString.match(/Decimal\[\d+e(\d+)\]/);
111
- const scale = scaleMatch ? parseInt(scaleMatch[1]) : 0;
112
-
113
- if (scale > 0) {
133
+ if (scaleMatch) {
134
+ const scale = parseInt(scaleMatch[1]);
114
135
  return Number(bigIntValue) / Math.pow(10, scale);
115
136
  } else {
116
137
  return Number(bigIntValue);
@@ -130,7 +151,7 @@ async function runQuery(
130
151
  async function runQuery(
131
152
  db: duckdb.AsyncDuckDBConnection,
132
153
  query: string,
133
- options?: { columns: boolean },
154
+ options?: { columns: false },
134
155
  ): Promise<any[]>;
135
156
 
136
157
  async function runQuery(
@@ -139,7 +160,6 @@ async function runQuery(
139
160
  options: { columns?: boolean } = {},
140
161
  ) {
141
162
  query = query.replace(/\s+/g, " ").trim();
142
- // console.log("Query:", query);
143
163
  try {
144
164
  const result = await db.query(query);
145
165
  if (options.columns) {
@@ -158,11 +178,28 @@ async function runQuery(
158
178
  }
159
179
  }
160
180
 
161
- export class DuckDBHandler implements VirtualServerHandler {
181
+ /**
182
+ * An implementation of Perspective's Virtual Server for `@duckdb/duckdb-wasm`.
183
+ */
184
+ export class DuckDBHandler implements perspective.VirtualServerHandler {
162
185
  private db: duckdb.AsyncDuckDBConnection;
186
+ private sqlBuilder: perspective.GenericSQLVirtualServerModel;
187
+ constructor(db: duckdb.AsyncDuckDBConnection, mod?: typeof perspective) {
188
+ if (!mod) {
189
+ if (customElements) {
190
+ const viewer_class: any =
191
+ customElements.get("perspective-viewer");
192
+ if (viewer_class) {
193
+ mod = viewer_class.__wasm_module__;
194
+ } else {
195
+ throw new Error("Missing perspective-client.wasm");
196
+ }
197
+ } else {
198
+ }
199
+ }
163
200
 
164
- constructor(db: duckdb.AsyncDuckDBConnection) {
165
201
  this.db = db;
202
+ this.sqlBuilder = new mod!.GenericSQLVirtualServerModel();
166
203
  }
167
204
 
168
205
  getFeatures() {
@@ -171,6 +208,7 @@ export class DuckDBHandler implements VirtualServerHandler {
171
208
  split_by: true,
172
209
  sort: true,
173
210
  expressions: true,
211
+ group_rollup_mode: ["rollup", "flat", "total"],
174
212
  filter_ops: {
175
213
  integer: FILTER_OPS,
176
214
  float: FILTER_OPS,
@@ -191,20 +229,25 @@ export class DuckDBHandler implements VirtualServerHandler {
191
229
  }
192
230
 
193
231
  async getHostedTables() {
194
- const results = await runQuery(this.db, "SHOW ALL TABLES");
195
- return results.map((row) => row.toJSON().name);
232
+ const query = this.sqlBuilder.getHostedTables();
233
+ const results = await runQuery(this.db, query);
234
+ return results.map((row) => {
235
+ const json = row.toJSON();
236
+ return `${json.database || "memory"}.${json.name}`;
237
+ });
196
238
  }
197
239
 
198
- async tableSchema(tableId: string) {
199
- const query = `DESCRIBE ${tableId}`;
240
+ async tableSchema(tableId: string, config?: ViewConfig) {
241
+ const query = this.sqlBuilder.tableSchema(tableId);
200
242
  const results = await runQuery(this.db, query);
201
243
  const schema = {} as Record<string, ColumnType>;
202
244
  for (const result of results) {
203
245
  const res = result.toJSON();
204
246
  const colName = res.column_name;
205
- if (!colName.startsWith("__") || !colName.endsWith("__")) {
206
- const cleanName = colName.split("_").slice(-1)[0] as string;
207
- schema[cleanName] = duckdbTypeToPsp(res.column_type);
247
+ if (!colName.startsWith("__")) {
248
+ schema[colName] = duckdbTypeToPsp(
249
+ res.column_type,
250
+ ) as ColumnType;
208
251
  }
209
252
  }
210
253
 
@@ -212,304 +255,99 @@ export class DuckDBHandler implements VirtualServerHandler {
212
255
  }
213
256
 
214
257
  async viewColumnSize(viewId: string, config: ViewConfig) {
215
- const query = `SELECT COUNT(*) FROM (DESCRIBE ${viewId})`;
258
+ const query = this.sqlBuilder.viewColumnSize(viewId);
216
259
  const results = await runQuery(this.db, query);
217
- const gs = config.group_by?.length || 0;
218
260
  const count = Number(Object.values(results[0].toJSON())[0]);
219
- return (
220
- count -
221
- (gs === 0 ? 0 : gs + (config.split_by?.length === 0 ? 1 : 0))
222
- );
261
+ const gs = config.group_by?.length || 0;
262
+ const is_flat = config.group_rollup_mode === "flat";
263
+ return count - (gs === 0 ? 0 : is_flat ? gs : gs + 1);
223
264
  }
224
265
 
225
266
  async tableSize(tableId: string) {
226
- const query = `SELECT COUNT(*) FROM ${tableId}`;
267
+ const query = this.sqlBuilder.tableSize(tableId);
227
268
  const results = await runQuery(this.db, query);
228
269
  return Number(results[0].toJSON()["count_star()"]);
229
270
  }
230
271
 
231
- // async viewSchema(viewId: string, config: ViewConfig) {
232
- // return this.tableSchema(viewId);
233
- // }
234
-
235
- // async viewSize(viewId: string) {
236
- // return this.tableSize(viewId);
237
- // }
238
-
239
272
  async tableMakeView(tableId: string, viewId: string, config: ViewConfig) {
240
- const columns = config.columns || [];
241
- const group_by = config.group_by || [];
242
- const split_by = config.split_by || [];
243
- const aggregates = config.aggregates || {};
244
- const sort = config.sort || [];
245
- const expressions = config.expressions || {};
246
- const filter = config.filter || [];
247
-
248
- const colName = (col: string) => {
249
- const expr = expressions[col];
250
- return expr || `"${col}"`;
251
- };
252
-
253
- const getAggregate = (col: string) => aggregates[col] || null;
254
-
255
- const generateSelectClauses = () => {
256
- const clauses = [];
257
- if (group_by.length > 0) {
258
- for (const col of columns) {
259
- if (col !== null) {
260
- // TODO texodus
261
- const agg = getAggregate(col) || "any_value";
262
- clauses.push(`${agg}(${colName(col)}) as "${col}"`);
263
- }
264
- }
265
-
266
- if (split_by.length === 0) {
267
- for (let idx = 0; idx < group_by.length; idx++) {
268
- clauses.push(
269
- `${colName(group_by[idx])} as __ROW_PATH_${idx}__`,
270
- );
271
- }
272
-
273
- const groups = group_by.map(colName).join(", ");
274
- clauses.push(`GROUPING_ID(${groups}) AS __GROUPING_ID__`);
275
- }
276
- } else if (columns.length > 0) {
277
- for (const col of columns) {
278
- if (col !== null) {
279
- // TODO texodus
280
- clauses.push(
281
- `${colName(col)} as "${col.replace(/"/g, '""')}"`,
282
- );
283
- }
284
- }
285
- }
286
-
287
- return clauses;
288
- };
289
-
290
- const orderByClauses = [];
291
- const windowClauses = [];
292
- const whereClauses = [];
293
-
294
- if (group_by.length > 0) {
295
- for (let gidx = 0; gidx < group_by.length; gidx++) {
296
- const groups = group_by
297
- .slice(0, gidx + 1)
298
- .map(colName)
299
- .join(", ");
300
- if (split_by.length === 0) {
301
- orderByClauses.push(`GROUPING_ID(${groups}) DESC`);
302
- }
303
-
304
- for (const [sort_col, sort_dir] of sort) {
305
- if (sort_dir !== "none") {
306
- const agg = getAggregate(sort_col) || "any_value";
307
- if (gidx >= group_by.length - 1) {
308
- orderByClauses.push(
309
- `${agg}(${colName(sort_col)}) ${sort_dir}`,
310
- );
311
- } else {
312
- orderByClauses.push(
313
- `first(${agg}(${colName(sort_col)})) OVER __WINDOW_${gidx}__ ${sort_dir}`,
314
- );
315
- }
316
- }
317
- }
318
-
319
- orderByClauses.push(`__ROW_PATH_${gidx}__ ASC`);
320
- }
321
- } else {
322
- for (const [sort_col, sort_dir] of sort) {
323
- if (sort_dir) {
324
- orderByClauses.push(`${colName(sort_col)} ${sort_dir}`);
325
- }
326
- }
327
- }
328
-
329
- if (sort.length > 0 && group_by.length > 1) {
330
- for (let gidx = 0; gidx < group_by.length - 1; gidx++) {
331
- const partition = Array.from(
332
- { length: gidx + 1 },
333
- (_, i) => `__ROW_PATH_${i}__`,
334
- ).join(", ");
335
- const sub_groups = group_by
336
- .slice(0, gidx + 1)
337
- .map(colName)
338
- .join(", ");
339
- const groups = group_by.map(colName).join(", ");
340
- windowClauses.push(
341
- `__WINDOW_${gidx}__ AS (PARTITION BY GROUPING_ID(${sub_groups}), ${partition} ORDER BY ${groups})`,
342
- );
343
- }
344
- }
345
-
346
- for (const [name, op, value] of filter) {
347
- if (value !== null && value !== undefined) {
348
- const term_lit =
349
- typeof value === "string" ? `'${value}'` : String(value);
350
- whereClauses.push(`${colName(name)} ${op} ${term_lit}`);
351
- }
352
- }
353
-
354
- let query;
355
- if (split_by.length > 0) {
356
- query = `SELECT * FROM ${tableId}`;
357
- } else {
358
- const selectClauses = generateSelectClauses();
359
- query = `SELECT ${selectClauses.join(", ")} FROM ${tableId}`;
360
- }
361
-
362
- if (whereClauses.length > 0) {
363
- query = `${query} WHERE ${whereClauses.join(" AND ")}`;
364
- }
365
-
366
- if (split_by.length > 0) {
367
- const groups = group_by.map(colName).join(", ");
368
- const group_aliases = group_by
369
- .map((x, i) => `${colName(x)} AS __ROW_PATH_${i}__`)
370
- .join(", ");
371
- const pivotOn = split_by.map((c) => `"${c}"`).join(", ");
372
- const pivotUsing = generateSelectClauses().join(", ");
373
-
374
- query = `
375
- SELECT * EXCLUDE (${groups}), ${group_aliases} FROM (
376
- PIVOT (${query})
377
- ON ${pivotOn}
378
- USING ${pivotUsing}
379
- GROUP BY ${groups}
380
- )
381
- `;
382
- } else if (group_by.length > 0) {
383
- const groups = group_by.map(colName).join(", ");
384
- query = `${query} GROUP BY ROLLUP(${groups})`;
385
- }
386
-
387
- if (windowClauses.length > 0) {
388
- query = `${query} WINDOW ${windowClauses.join(", ")}`;
389
- }
390
-
391
- if (orderByClauses.length > 0) {
392
- query = `${query} ORDER BY ${orderByClauses.join(", ")}`;
393
- }
394
-
395
- query = `CREATE TABLE ${viewId} AS (${query})`;
273
+ const query = this.sqlBuilder.tableMakeView(tableId, viewId, config);
396
274
  await runQuery(this.db, query);
397
275
  }
398
276
 
399
277
  async tableValidateExpression(tableId: string, expression: string) {
400
- const query = `DESCRIBE (select ${expression} from ${tableId})`;
278
+ const query = this.sqlBuilder.tableValidateExpression(
279
+ tableId,
280
+ expression,
281
+ );
401
282
  const results = await runQuery(this.db, query);
402
- return duckdbTypeToPsp(results[0].toJSON()["column_type"]);
283
+ return duckdbTypeToPsp(
284
+ results[0].toJSON()["column_type"],
285
+ ) as ColumnType;
403
286
  }
404
287
 
405
288
  async viewDelete(viewId: string) {
406
- const query = `DROP TABLE IF EXISTS ${viewId}`;
289
+ const query = this.sqlBuilder.viewDelete(viewId);
407
290
  await runQuery(this.db, query);
408
291
  }
409
292
 
410
293
  async viewGetData(
411
294
  viewId: string,
412
295
  config: ViewConfig,
296
+ schema: Record<string, ColumnType>,
413
297
  viewport: ViewWindow,
414
- dataSlice: VirtualDataSlice,
298
+ dataSlice: perspective.VirtualDataSlice,
415
299
  ) {
416
- const group_by = config.group_by || [];
417
- const split_by = config.split_by || [];
418
- const start_col = viewport.start_col;
419
- const end_col = viewport.end_col;
420
- const start_row = viewport.start_row || 0;
421
- const end_row = viewport.end_row;
422
-
423
- let limit = "";
424
- if (end_row !== null && end_row !== undefined) {
425
- limit = `LIMIT ${end_row - start_row} OFFSET ${start_row}`;
426
- }
427
-
428
- const schemaQuery = `DESCRIBE ${viewId}`;
429
- const schemaResults = await runQuery(this.db, schemaQuery);
430
- const columnTypes = new Map();
431
- for (const result of schemaResults) {
432
- const res = result.toJSON();
433
- columnTypes.set(res.column_name, res.column_type);
434
- }
435
-
436
- const dataColumns = Array.from(columnTypes.entries())
437
- .filter(([colName]) => !colName.startsWith("__"))
438
- .slice(start_col, end_col);
439
-
440
- const groupByColsList = [];
441
- if (group_by.length > 0) {
442
- if (split_by.length === 0) {
443
- groupByColsList.push("__GROUPING_ID__");
444
- }
445
- for (let idx = 0; idx < group_by.length; idx++) {
446
- groupByColsList.push(`__ROW_PATH_${idx}__`);
447
- }
448
- }
449
-
450
- const allColumns = [
451
- ...groupByColsList.map((col) => `"${col}"`),
452
- ...dataColumns.map(([colName]) => `"${colName}"`),
453
- ];
454
-
455
- const query = `
456
- SELECT ${allColumns.join(", ")}
457
- FROM ${viewId} ${limit}
458
- `;
300
+ const is_group_by = config.group_by?.length > 0;
301
+ const is_split_by = config.split_by?.length > 0;
302
+ const is_flat = config.group_rollup_mode === "flat";
303
+ const has_grouping_id = is_group_by && !is_flat;
304
+ const query = this.sqlBuilder.viewGetData(
305
+ viewId,
306
+ config,
307
+ viewport,
308
+ schema,
309
+ );
459
310
 
460
311
  const { rows, columns, dtypes } = await runQuery(this.db, query, {
461
312
  columns: true,
462
313
  });
463
314
 
464
315
  for (let cidx = 0; cidx < columns.length; cidx++) {
465
- const col = columns[cidx];
466
-
467
- if (cidx === 0 && group_by.length > 0 && split_by.length === 0) {
316
+ if (cidx === 0 && has_grouping_id) {
317
+ // This is the grouping_id column, skip it
468
318
  continue;
469
319
  }
470
320
 
471
- let group_by_index = null;
472
- let max_grouping_id = null;
473
- const row_path_match = col.match(/__ROW_PATH_(\d+)__/);
474
- if (row_path_match) {
475
- group_by_index = parseInt(row_path_match[1]);
476
- max_grouping_id = 2 ** (group_by.length - group_by_index) - 1;
321
+ let col = columns[cidx];
322
+ if (is_split_by && !col.startsWith("__")) {
323
+ col = col.replaceAll("_", "|");
477
324
  }
478
325
 
479
- const dtype = duckdbTypeToPsp(dtypes[cidx]);
326
+ const dtype = duckdbTypeToPsp(dtypes[cidx]) as ColumnType;
480
327
  const isDecimal = dtypes[cidx].startsWith("Decimal");
481
- const colName =
482
- group_by_index !== null
483
- ? "__ROW_PATH__"
484
- : col.replace(/_/g, "|");
485
-
486
328
  for (let ridx = 0; ridx < rows.length; ridx++) {
487
- const row = rows[ridx];
488
- const rowArray = row.toArray();
489
- const shouldSet =
490
- split_by.length > 0 ||
491
- max_grouping_id === null ||
492
- rowArray[0] < max_grouping_id;
493
-
494
- if (shouldSet) {
495
- let value = rowArray[cidx];
496
-
497
- if (isDecimal) {
498
- value = convertDecimalToNumber(value, dtypes[cidx]);
499
- }
329
+ const rowArray = rows[ridx].toArray();
330
+ const grouping_id = has_grouping_id
331
+ ? Number(rowArray[0])
332
+ : undefined;
333
+ let value = rowArray[cidx];
334
+ if (isDecimal) {
335
+ value = convertDecimalToNumber(value, dtypes[cidx]);
336
+ }
500
337
 
501
- if (typeof value === "bigint") {
502
- value = Number(value);
503
- }
338
+ if (typeof value === "bigint") {
339
+ value = Number(value);
340
+ }
504
341
 
505
- dataSlice.setCol(
506
- dtype,
507
- colName,
508
- ridx,
509
- value,
510
- group_by_index,
511
- );
342
+ if (typeof value !== "string" && dtype === "string") {
343
+ try {
344
+ value = JSON.stringify(value);
345
+ } catch (e) {
346
+ value = `${value}`;
347
+ }
512
348
  }
349
+
350
+ dataSlice.setCol(dtype, col, ridx, value, grouping_id);
513
351
  }
514
352
  }
515
353
  }
package/tsconfig.json CHANGED
@@ -16,6 +16,7 @@
16
16
  "./src/ts/perspective.node.ts",
17
17
  "./src/ts/perspective.cdn.ts",
18
18
  "./src/ts/virtual_servers/duckdb.ts",
19
+ "./src/ts/virtual_servers/clickhouse.ts",
19
20
  "./test/js/*.ts"
20
21
  ]
21
22
  }