@uwdata/mosaic-core 0.7.1 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uwdata/mosaic-core",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Scalable and extensible linked data views.",
5
5
  "keywords": [
6
6
  "mosaic",
@@ -23,14 +23,17 @@
23
23
  "scripts": {
24
24
  "prebuild": "rimraf dist && mkdir dist",
25
25
  "build": "node ../../esbuild.js mosaic-core",
26
- "lint": "eslint src test --ext .js",
26
+ "lint": "eslint src test",
27
27
  "test": "mocha 'test/**/*-test.js'",
28
28
  "prepublishOnly": "npm run test && npm run lint && npm run build"
29
29
  },
30
30
  "dependencies": {
31
- "@duckdb/duckdb-wasm": "^1.28.1-dev109.0",
32
- "@uwdata/mosaic-sql": "^0.7.0",
33
- "apache-arrow": "^15.0.0"
31
+ "@duckdb/duckdb-wasm": "^1.28.1-dev194.0",
32
+ "@uwdata/mosaic-sql": "^0.8.0",
33
+ "apache-arrow": "^15.0.2"
34
34
  },
35
- "gitHead": "7e6f3ea9b3011ea2c9201c1aa16e8e5664621a4c"
35
+ "devDependencies": {
36
+ "@uwdata/mosaic-duckdb": "^0.8.0"
37
+ },
38
+ "gitHead": "a24b4c9f7dfa1c38c6af96ec17e075326c1af9b0"
36
39
  }
@@ -8,8 +8,7 @@ let _instance;
8
8
 
9
9
  /**
10
10
  * Set or retrieve the coordinator instance.
11
- *
12
- * @param {Coordinator} instance the coordinator instance to set
11
+ * @param {Coordinator} [instance] the coordinator instance to set
13
12
  * @returns {Coordinator} the coordinator instance
14
13
  */
15
14
  export function coordinator(instance) {
@@ -42,7 +41,15 @@ export class Coordinator {
42
41
  return this._logger;
43
42
  }
44
43
 
45
- configure({ cache = true, consolidate = true, indexes = true }) {
44
+ /**
45
+ * Set configuration options for this coordinator.
46
+ * @param {object} [options] Configration options.
47
+ * @param {boolean} [options.cache=true] Boolean flag to enable/disable query caching.
48
+ * @param {boolean} [options.consolidate=true] Boolean flag to enable/disable query consolidation.
49
+ * @param {boolean|object} [options.indexes=true] Boolean flag to enable/disable
50
+ * automatic data cube indexes or an index options object.
51
+ */
52
+ configure({ cache = true, consolidate = true, indexes = true } = {}) {
46
53
  this.manager.cache(cache);
47
54
  this.manager.consolidate(consolidate);
48
55
  this.indexes = indexes;
@@ -116,7 +123,6 @@ export class Coordinator {
116
123
 
117
124
  /**
118
125
  * Connect a client to the coordinator.
119
- *
120
126
  * @param {import('./MosaicClient.js').MosaicClient} client the client to disconnect
121
127
  */
122
128
  async connect(client) {
@@ -54,7 +54,8 @@ export class DataCubeIndexer {
54
54
 
55
55
  active = active || this.selection.active;
56
56
  const { source } = active;
57
- if (source && source === this.activeView?.source) return true; // we're good!
57
+ // exit early if indexes already set up for active view
58
+ if (source && source === this.activeView?.source) return true;
58
59
 
59
60
  this.clear();
60
61
  if (!source) return false; // nothing to work with
@@ -75,7 +76,7 @@ export class DataCubeIndexer {
75
76
 
76
77
  // build index construction query
77
78
  const query = client.query(sel.predicate(client))
78
- .select({ ...activeView.columns, ...index.count })
79
+ .select({ ...activeView.columns, ...index.aux })
79
80
  .groupby(Object.keys(activeView.columns));
80
81
 
81
82
  // ensure active view columns are selected by subqueries
@@ -95,6 +96,9 @@ export class DataCubeIndexer {
95
96
  const result = mc.exec(create(table, sql, { temp }));
96
97
  indices.set(client, { table, result, order, ...index });
97
98
  }
99
+
100
+ // index creation successful
101
+ return true;
98
102
  }
99
103
 
100
104
  async update() {
@@ -179,23 +183,40 @@ function getIndexColumns(client) {
179
183
 
180
184
  const aggr = [];
181
185
  const dims = [];
182
- let count;
186
+ const aux = {}; // auxiliary columns needed by aggregates
187
+ let auxAs;
183
188
 
184
- for (const { as, expr: { aggregate } } of q.select()) {
185
- switch (aggregate?.toUpperCase?.()) {
189
+ for (const entry of q.select()) {
190
+ const { as, expr: { aggregate, args } } = entry;
191
+ const op = aggregate?.toUpperCase?.();
192
+ switch (op) {
186
193
  case 'COUNT':
187
194
  case 'SUM':
188
195
  aggr.push({ [as]: sql`SUM("${as}")::DOUBLE` });
189
196
  break;
190
197
  case 'AVG':
191
- count = '_count_';
192
- aggr.push({ [as]: sql`(SUM("${as}" * ${count}) / SUM(${count}))::DOUBLE` });
198
+ aux[auxAs = '__count__'] = sql`COUNT(*)`;
199
+ aggr.push({ [as]: sql`(SUM("${as}" * ${auxAs}) / SUM(${auxAs}))::DOUBLE` });
193
200
  break;
194
- case 'MAX':
195
- aggr.push({ [as]: sql`MAX("${as}")` });
201
+ case 'ARG_MAX':
202
+ aux[auxAs = `__max_${as}__`] = sql`MAX(${args[1]})`;
203
+ aggr.push({ [as]: sql`ARG_MAX("${as}", ${auxAs})` });
204
+ break;
205
+ case 'ARG_MIN':
206
+ aux[auxAs = `__min_${as}__`] = sql`MIN(${args[1]})`;
207
+ aggr.push({ [as]: sql`ARG_MIN("${as}", ${auxAs})` });
196
208
  break;
209
+
210
+ // aggregates that commute directly
211
+ case 'MAX':
197
212
  case 'MIN':
198
- aggr.push({ [as]: sql`MIN("${as}")` });
213
+ case 'BIT_AND':
214
+ case 'BIT_OR':
215
+ case 'BIT_XOR':
216
+ case 'BOOL_AND':
217
+ case 'BOOL_OR':
218
+ case 'PRODUCT':
219
+ aggr.push({ [as]: sql`${op}("${as}")` });
199
220
  break;
200
221
  default:
201
222
  if (g.has(as)) dims.push(as);
@@ -203,12 +224,7 @@ function getIndexColumns(client) {
203
224
  }
204
225
  }
205
226
 
206
- return {
207
- aggr,
208
- dims,
209
- count: count ? { [count]: sql`COUNT(*)` } : {},
210
- from
211
- };
227
+ return { aggr, dims, aux, from };
212
228
  }
213
229
 
214
230
  function getBaseTable(query) {
@@ -46,9 +46,15 @@ export class FilterGroup {
46
46
  return this;
47
47
  }
48
48
 
49
+ /**
50
+ * Internal method to process a selection update.
51
+ * The return value is passed as a selection callback value.
52
+ * @returns {Promise} A Promise that resolves when the update completes.
53
+ */
49
54
  update() {
50
55
  const { mc, indexer, clients, selection } = this;
51
- return indexer?.index(clients)
56
+ const hasIndex = indexer?.index(clients);
57
+ return hasIndex
52
58
  ? indexer.update()
53
59
  : defaultUpdate(mc, clients, selection);
54
60
  }
@@ -48,6 +48,7 @@ export class MosaicClient {
48
48
 
49
49
  /**
50
50
  * Return an array of fields queried by this client.
51
+ * @returns {object[]|null} The fields to retrieve info for.
51
52
  */
52
53
  fields() {
53
54
  return null;
@@ -55,21 +56,25 @@ export class MosaicClient {
55
56
 
56
57
  /**
57
58
  * Called by the coordinator to set the field info for this client.
59
+ * @param {*} info The field info result.
58
60
  * @returns {this}
59
61
  */
60
- fieldInfo() {
62
+ fieldInfo(info) { // eslint-disable-line no-unused-vars
61
63
  return this;
62
64
  }
63
65
 
64
66
  /**
65
67
  * Return a query specifying the data needed by this client.
68
+ * @param {*} [filter] The filtering criteria to apply in the query.
69
+ * @returns {*} The client query
66
70
  */
67
- query() {
71
+ query(filter) { // eslint-disable-line no-unused-vars
68
72
  return null;
69
73
  }
70
74
 
71
75
  /**
72
76
  * Called by the coordinator to inform the client that a query is pending.
77
+ * @returns {this}
73
78
  */
74
79
  queryPending() {
75
80
  return this;
@@ -77,16 +82,17 @@ export class MosaicClient {
77
82
 
78
83
  /**
79
84
  * Called by the coordinator to return a query result.
80
- *
81
- * @param {*} data the query result
85
+ * @param {*} data The query result.
82
86
  * @returns {this}
83
87
  */
84
- queryResult() {
88
+ queryResult(data) { // eslint-disable-line no-unused-vars
85
89
  return this;
86
90
  }
87
91
 
88
92
  /**
89
93
  * Called by the coordinator to report a query execution error.
94
+ * @param {*} error
95
+ * @returns {this}
90
96
  */
91
97
  queryError(error) {
92
98
  console.error(error);
@@ -116,6 +122,8 @@ export class MosaicClient {
116
122
  /**
117
123
  * Requests a client update.
118
124
  * For example to (re-)render an interface component.
125
+ *
126
+ * @returns {this | Promise<any>}
119
127
  */
120
128
  update() {
121
129
  return this;
package/src/Param.js CHANGED
@@ -4,7 +4,7 @@ import { distinct } from './util/distinct.js';
4
4
  /**
5
5
  * Test if a value is a Param instance.
6
6
  * @param {*} x The value to test.
7
- * @returns {boolean} True if the input is a Param, false otherwise.
7
+ * @returns {x is Param} True if the input is a Param, false otherwise.
8
8
  */
9
9
  export function isParam(x) {
10
10
  return x instanceof Param;
@@ -43,7 +43,9 @@ export class Param extends AsyncDispatch {
43
43
  static array(values) {
44
44
  if (values.some(v => isParam(v))) {
45
45
  const p = new Param();
46
- const update = () => p.update(values.map(v => isParam(v) ? v.value : v));
46
+ const update = () => {
47
+ p.update(values.map(v => isParam(v) ? v.value : v));
48
+ };
47
49
  update();
48
50
  values.forEach(v => isParam(v) ? v.addEventListener('value', update) : 0);
49
51
  return p;
@@ -5,6 +5,7 @@ function wait(callback) {
5
5
  const method = typeof requestAnimationFrame !== 'undefined'
6
6
  ? requestAnimationFrame
7
7
  : typeof setImmediate !== 'undefined' ? setImmediate : setTimeout;
8
+ // @ts-ignore
8
9
  return method(callback);
9
10
  }
10
11
 
@@ -82,7 +83,9 @@ function consolidationKey(query, cache) {
82
83
  const sql = `${query}`;
83
84
  if (query instanceof Query && !cache.get(sql)) {
84
85
  if (
86
+ // @ts-ignore
85
87
  query.orderby().length || query.where().length ||
88
+ // @ts-ignore
86
89
  query.qualify().length || query.having().length
87
90
  ) {
88
91
  // do not try to analyze if query includes clauses
@@ -97,9 +100,12 @@ function consolidationKey(query, cache) {
97
100
  // queries may refer to *derived* columns as group by criteria
98
101
  // we resolve these against the true grouping expressions
99
102
  const groupby = query.groupby();
103
+ // @ts-ignore
100
104
  if (groupby.length) {
101
105
  const map = {}; // expression map (as -> expr)
106
+ // @ts-ignore
102
107
  query.select().forEach(({ as, expr }) => map[as] = expr);
108
+ // @ts-ignore
103
109
  q.$groupby(groupby.map(e => (e instanceof Ref && map[e.column]) || e));
104
110
  }
105
111
 
package/src/Selection.js CHANGED
@@ -4,7 +4,7 @@ import { Param } from './Param.js';
4
4
  /**
5
5
  * Test if a value is a Selection instance.
6
6
  * @param {*} x The value to test.
7
- * @returns {boolean} True if the input is a Selection, false otherwise.
7
+ * @returns {x is Selection} True if the input is a Selection, false otherwise.
8
8
  */
9
9
  export function isSelection(x) {
10
10
  return x instanceof Selection;
@@ -76,7 +76,7 @@ export class Selection extends Param {
76
76
 
77
77
  /**
78
78
  * Create a cloned copy of this Selection instance.
79
- * @returns {this} A clone of this selection.
79
+ * @returns {Selection} A clone of this selection.
80
80
  */
81
81
  clone() {
82
82
  const s = new Selection(this._resolver);
@@ -88,7 +88,7 @@ export class Selection extends Param {
88
88
  * Create a clone of this Selection with clauses corresponding
89
89
  * to the provided source removed.
90
90
  * @param {*} source The clause source to remove.
91
- * @returns {this} A cloned and updated Selection.
91
+ * @returns {Selection} A cloned and updated Selection.
92
92
  */
93
93
  remove(source) {
94
94
  const s = this.clone();
@@ -168,9 +168,9 @@ export class Selection extends Param {
168
168
  * Upon value-typed updates, returns a dispatch queue filter function.
169
169
  * The return value depends on the selection resolution strategy.
170
170
  * @param {string} type The event type.
171
- * @param {*} value The input event value.
172
- * @returns {*} For value-typed events, returns a dispatch queue filter
173
- * function. Otherwise returns null.
171
+ * @param {*} value The new event value that will be enqueued.
172
+ * @returns {(value: *) => boolean|null} For value-typed events,
173
+ * returns a dispatch queue filter function. Otherwise returns null.
174
174
  */
175
175
  emitQueueFilter(type, value) {
176
176
  return type === 'value'
@@ -285,5 +285,6 @@ export class SelectionResolver {
285
285
  const source = value.active?.source;
286
286
  return clauses => clauses.active?.source !== source;
287
287
  }
288
+ return null;
288
289
  }
289
290
  }
@@ -22,7 +22,7 @@ export function wasmConnector(options = {}) {
22
22
  /**
23
23
  * Get the backing DuckDB-WASM instance.
24
24
  * Will lazily initialize DuckDB-WASM if not already loaded.
25
- * @returns {duckdb.AsyncDuckDB} The DuckDB-WASM instance.
25
+ * @returns {Promise<duckdb.AsyncDuckDB>} The DuckDB-WASM instance.
26
26
  */
27
27
  async function getDuckDB() {
28
28
  if (!db) await load();
@@ -32,7 +32,7 @@ export function wasmConnector(options = {}) {
32
32
  /**
33
33
  * Get the backing DuckDB-WASM connection.
34
34
  * Will lazily initialize DuckDB-WASM if not already loaded.
35
- * @returns {duckdb.AsyncDuckDBConnection} The DuckDB-WASM connection.
35
+ * @returns {Promise<duckdb.AsyncDuckDBConnection>} The DuckDB-WASM connection.
36
36
  */
37
37
  async function getConnection() {
38
38
  if (!con) await load();
@@ -16,7 +16,7 @@ export class AsyncDispatch {
16
16
  /**
17
17
  * Add an event listener callback for the provided event type.
18
18
  * @param {string} type The event type.
19
- * @param {(value: *) => Promise?} callback The event handler
19
+ * @param {(value: *) => void | Promise} callback The event handler
20
20
  * callback function to add. If the callback has already been
21
21
  * added for the event type, this method has no effect.
22
22
  */
@@ -35,7 +35,7 @@ export class AsyncDispatch {
35
35
  /**
36
36
  * Remove an event listener callback for the provided event type.
37
37
  * @param {string} type The event type.
38
- * @param {(value: *) => Promise?} callback The event handler
38
+ * @param {(value: *) => void | Promise} callback The event handler
39
39
  * callback function to remove.
40
40
  */
41
41
  removeEventListener(type, callback) {
@@ -64,11 +64,12 @@ export class AsyncDispatch {
64
64
  * This default implementation simply returns null, indicating that
65
65
  * any other unemitted event values should be dropped (that is, all
66
66
  * queued events are filtered)
67
+ * @param {string} type The event type.
67
68
  * @param {*} value The new event value that will be enqueued.
68
69
  * @returns {(value: *) => boolean|null} A dispatch queue filter
69
70
  * function, or null if all unemitted event values should be filtered.
70
71
  */
71
- emitQueueFilter() {
72
+ emitQueueFilter(type, value) { // eslint-disable-line no-unused-vars
72
73
  // removes all pending items
73
74
  return null;
74
75
  }
@@ -94,20 +95,22 @@ export class AsyncDispatch {
94
95
  emit(type, value) {
95
96
  const entry = this._callbacks.get(type) || {};
96
97
  if (entry.pending) {
98
+ // an earlier emit is still processing
99
+ // enqueue the current update, possibly filtering other pending updates
97
100
  entry.queue.enqueue(value, this.emitQueueFilter(type, value));
98
101
  } else {
99
102
  const event = this.willEmit(type, value);
100
103
  const { callbacks, queue } = entry;
101
104
  if (callbacks?.size) {
102
- const promise = Promise
103
- .allSettled(Array.from(callbacks, callback => callback(event)))
104
- .then(() => {
105
- entry.pending = null;
106
- if (!queue.isEmpty()) {
107
- this.emit(type, queue.dequeue());
108
- }
109
- });
110
- entry.pending = promise;
105
+ // broadcast update to callbacks, which may return promises
106
+ // wait until promises resolve, then process pending updates
107
+ const callbackValues = Array.from(callbacks, cb => cb(event));
108
+ entry.pending = Promise.allSettled(callbackValues).then(() => {
109
+ entry.pending = null;
110
+ if (!queue.isEmpty()) {
111
+ this.emit(type, queue.dequeue());
112
+ }
113
+ });
111
114
  }
112
115
  }
113
116
  }
@@ -1,34 +1,25 @@
1
- // arrow type ids
2
- const INTEGER = 2;
3
- const FLOAT = 3;
4
- const DECIMAL = 7;
5
- const TIMESTAMP = 10;
1
+ import { DataType } from 'apache-arrow';
6
2
 
7
3
  /**
8
4
  * Test if a value is an Apache Arrow table.
9
5
  * As sometimes multiple Arrow versions may be used simultaneously,
10
6
  * we use a "duck typing" approach and check for a getChild function.
11
7
  * @param {*} values The value to test
12
- * @returns true if the value duck types as Apache Arrow data
8
+ * @returns {values is import('apache-arrow').Table} true if the value duck types as Apache Arrow data
13
9
  */
14
10
  export function isArrowTable(values) {
15
11
  return typeof values?.getChild === 'function';
16
12
  }
17
13
 
18
14
  /**
19
- * Return a JavaScript array type for an Apache Arrow column type.
20
- * @param {*} type an Apache Arrow column type
21
- * @returns a JavaScript array constructor
22
- */
15
+ * Return a JavaScript array type for an Apache Arrow column type.
16
+ * @param {DataType} type an Apache Arrow column type
17
+ * @returns a JavaScript array constructor
18
+ */
23
19
  export function convertArrowArrayType(type) {
24
- switch (type.typeId) {
25
- case INTEGER:
26
- case FLOAT:
27
- case DECIMAL:
28
- return Float64Array;
29
- default:
30
- return Array;
31
- }
20
+ return DataType.isInt(type) || DataType.isFloat(type) || DataType.isDecimal(type)
21
+ ? Float64Array
22
+ : Array;
32
23
  }
33
24
 
34
25
  /**
@@ -37,24 +28,22 @@ export function convertArrowArrayType(type) {
37
28
  * Large integers (BigInt) are converted to Float64 numbers.
38
29
  * Fixed-point decimal values are convert to Float64 numbers.
39
30
  * Otherwise, the default Arrow values are used.
40
- * @param {*} type an Apache Arrow column type
31
+ * @param {DataType} type an Apache Arrow column type
41
32
  * @returns a value conversion function
42
33
  */
43
34
  export function convertArrowValue(type) {
44
- const { typeId } = type;
45
-
46
35
  // map timestamp numbers to date objects
47
- if (typeId === TIMESTAMP) {
36
+ if (DataType.isTimestamp(type)) {
48
37
  return v => v == null ? v : new Date(v);
49
38
  }
50
39
 
51
40
  // map bigint to number
52
- if (typeId === INTEGER && type.bitWidth >= 64) {
41
+ if (DataType.isInt(type) && type.bitWidth >= 64) {
53
42
  return v => v == null ? v : Number(v);
54
43
  }
55
44
 
56
45
  // map decimal to number
57
- if (typeId === DECIMAL) {
46
+ if (DataType.isDecimal(type)) {
58
47
  const scale = 1 / Math.pow(10, type.scale);
59
48
  return v => v == null ? v : decimalToNumber(v, scale);
60
49
  }
@@ -74,10 +63,9 @@ export function convertArrowValue(type) {
74
63
  */
75
64
  export function convertArrowColumn(column) {
76
65
  const { type } = column;
77
- const { typeId } = type;
78
66
 
79
67
  // map timestamp numbers to date objects
80
- if (typeId === TIMESTAMP) {
68
+ if (DataType.isTimestamp(type)) {
81
69
  const size = column.length;
82
70
  const array = new Array(size);
83
71
  for (let row = 0; row < size; ++row) {
@@ -88,7 +76,7 @@ export function convertArrowColumn(column) {
88
76
  }
89
77
 
90
78
  // map bigint to number
91
- if (typeId === INTEGER && type.bitWidth >= 64) {
79
+ if (DataType.isInt(type) && type.bitWidth >= 64) {
92
80
  const size = column.length;
93
81
  const array = new Float64Array(size);
94
82
  for (let row = 0; row < size; ++row) {
@@ -99,7 +87,7 @@ export function convertArrowColumn(column) {
99
87
  }
100
88
 
101
89
  // map decimal to number
102
- if (typeId === DECIMAL) {
90
+ if (DataType.isDecimal(type)) {
103
91
  const scale = 1 / Math.pow(10, type.scale);
104
92
  const size = column.length;
105
93
  const array = new Float64Array(size);
@@ -124,10 +112,10 @@ const BASE32 = Array.from(
124
112
  /**
125
113
  * Convert a fixed point decimal value to a double precision number.
126
114
  * Note: if the value is sufficiently large the conversion may be lossy!
127
- * @param {Uint32Array} v a fixed decimal value
115
+ * @param {Uint32Array & { signed: boolean }} v a fixed decimal value
128
116
  * @param {number} scale a scale factor, corresponding to the
129
117
  * number of fractional decimal digits in the fixed point value
130
- * @returns the resulting number
118
+ * @returns {number} the resulting number
131
119
  */
132
120
  function decimalToNumber(v, scale) {
133
121
  const n = v.length;
@@ -24,6 +24,7 @@ export function jsType(type) {
24
24
  return 'boolean';
25
25
  case 'VARCHAR':
26
26
  case 'UUID':
27
+ case 'JSON':
27
28
  return 'string';
28
29
  case 'ARRAY':
29
30
  case 'LIST':
@@ -2,7 +2,8 @@ export function queryResult() {
2
2
  let resolve;
3
3
  let reject;
4
4
  const p = new Promise((r, e) => { resolve = r; reject = e; });
5
- p.fulfill = value => (resolve(value), p);
6
- p.reject = err => (reject(err), p);
7
- return p;
5
+ return Object.assign(p, {
6
+ fulfill: value => (resolve(value), p),
7
+ reject: err => (reject(err), p)
8
+ });
8
9
  }