@uwdata/mosaic-core 0.13.0 → 0.14.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.
@@ -155,7 +155,7 @@ export class Coordinator {
155
155
  * Connect a client to the coordinator.
156
156
  * @param {MosaicClient} client The Mosaic client to connect.
157
157
  */
158
- connect(client: MosaicClient): Promise<void>;
158
+ connect(client: MosaicClient): void;
159
159
  initializeClient(client: any): Promise<any>;
160
160
  /**
161
161
  * Disconnect a client from the coordinator.
@@ -81,7 +81,8 @@ export class MosaicClient {
81
81
  /**
82
82
  * Request the coordinator to execute a query for this client.
83
83
  * If an explicit query is not provided, the client query method will
84
- * be called, filtered by the current filterBy selection.
84
+ * be called, filtered by the current filterBy selection. This method
85
+ * has no effect if the client is not registered with a coordinator.
85
86
  * @returns {Promise}
86
87
  */
87
88
  requestQuery(query: any): Promise<any>;
@@ -93,14 +94,15 @@ export class MosaicClient {
93
94
  */
94
95
  requestUpdate(): void;
95
96
  /**
96
- * Reset this client, initiating new field info, call the prepare method, and query requests.
97
+ * Reset this client, initiating new field info, call the prepare method,
98
+ * and query requests. This method has no effect if the client is not
99
+ * registered with a coordinator.
97
100
  * @returns {Promise}
98
101
  */
99
102
  initialize(): Promise<any>;
100
103
  /**
101
- * Requests a client update.
102
- * For example to (re-)render an interface component.
103
- *
104
+ * Requests a client update, for example to (re-)render an interface
105
+ * component.
104
106
  * @returns {this | Promise<any>}
105
107
  */
106
108
  update(): this | Promise<any>;
@@ -133,15 +133,25 @@ export class Selection extends Param {
133
133
  valueFor(source: any): any;
134
134
  /**
135
135
  * Emit an activate event with the given selection clause.
136
- * @param {*} clause The clause repesenting the potential activation.
136
+ * @param {SelectionClause} clause The clause repesenting the potential activation.
137
137
  */
138
- activate(clause: any): void;
138
+ activate(clause: SelectionClause): void;
139
139
  /**
140
140
  * Update the selection with a new selection clause.
141
- * @param {*} clause The selection clause to add.
141
+ * @param {SelectionClause} clause The selection clause to add.
142
142
  * @returns {this} This Selection instance.
143
143
  */
144
- update(clause: any): this;
144
+ update(clause: SelectionClause): this;
145
+ /**
146
+ * Reset the selection state by removing all provided clauses. If no clause
147
+ * array is provided as an argument, all current clauses are removed. The
148
+ * reset method (if defined) is invoked on all corresponding clause sources.
149
+ * The reset is relayed to downstream selections that include this selection.
150
+ * @param {SelectionClause[]} [clauses] The clauses to remove. If
151
+ * unspecified, all current clauses are removed.
152
+ * @returns {this} This selection instance.
153
+ */
154
+ reset(clauses?: SelectionClause[]): this;
145
155
  /**
146
156
  * Indicates if a selection clause should not be applied to a given client.
147
157
  * The return value depends on the selection resolution strategy.
@@ -221,4 +231,5 @@ export class SelectionResolver {
221
231
  queueFilter(value: any): (value: any) => boolean | null;
222
232
  }
223
233
  import { Param } from './Param.js';
234
+ import type { SelectionClause } from './util/selection-types.js';
224
235
  import { MosaicClient } from './MosaicClient.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uwdata/mosaic-core",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Scalable and extensible linked data views.",
5
5
  "keywords": [
6
6
  "mosaic",
@@ -32,10 +32,10 @@
32
32
  "dependencies": {
33
33
  "@duckdb/duckdb-wasm": "^1.29.0",
34
34
  "@uwdata/flechette": "^2.0.0",
35
- "@uwdata/mosaic-sql": "^0.13.0"
35
+ "@uwdata/mosaic-sql": "^0.14.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@uwdata/mosaic-duckdb": "^0.13.0"
38
+ "@uwdata/mosaic-duckdb": "^0.14.0"
39
39
  },
40
- "gitHead": "b5a0e03e200c0f04c46562a288f084ffc9f6ad55"
40
+ "gitHead": "a882aab60867e4e9d9738bc950aa9de32729a806"
41
41
  }
@@ -222,13 +222,17 @@ export class Coordinator {
222
222
  * Connect a client to the coordinator.
223
223
  * @param {MosaicClient} client The Mosaic client to connect.
224
224
  */
225
- async connect(client) {
225
+ connect(client) {
226
226
  const { clients } = this;
227
227
 
228
228
  if (clients.has(client)) {
229
229
  throw new Error('Client already connected.');
230
230
  }
231
- clients.add(client); // mark as connected
231
+
232
+ // add client to client set
233
+ clients.add(client);
234
+
235
+ // register coordinator on client instance
232
236
  client.coordinator = this;
233
237
 
234
238
  // initialize client lifecycle
@@ -123,12 +123,13 @@ export class MosaicClient {
123
123
  /**
124
124
  * Request the coordinator to execute a query for this client.
125
125
  * If an explicit query is not provided, the client query method will
126
- * be called, filtered by the current filterBy selection.
126
+ * be called, filtered by the current filterBy selection. This method
127
+ * has no effect if the client is not registered with a coordinator.
127
128
  * @returns {Promise}
128
129
  */
129
130
  requestQuery(query) {
130
131
  const q = query || this.query(this.filterBy?.predicate(this));
131
- return this._coordinator.requestQuery(this, q);
132
+ return this._coordinator?.requestQuery(this, q);
132
133
  }
133
134
 
134
135
  /**
@@ -142,17 +143,18 @@ export class MosaicClient {
142
143
  }
143
144
 
144
145
  /**
145
- * Reset this client, initiating new field info, call the prepare method, and query requests.
146
+ * Reset this client, initiating new field info, call the prepare method,
147
+ * and query requests. This method has no effect if the client is not
148
+ * registered with a coordinator.
146
149
  * @returns {Promise}
147
150
  */
148
151
  initialize() {
149
- return this._coordinator.initializeClient(this);
152
+ return this._coordinator?.initializeClient(this);
150
153
  }
151
154
 
152
155
  /**
153
- * Requests a client update.
154
- * For example to (re-)render an interface component.
155
- *
156
+ * Requests a client update, for example to (re-)render an interface
157
+ * component.
156
158
  * @returns {this | Promise<any>}
157
159
  */
158
160
  update() {
package/src/Selection.js CHANGED
@@ -1,3 +1,4 @@
1
+ /** @import {SelectionClause} from './util/selection-types.js' */
1
2
  import { literal, or } from '@uwdata/mosaic-sql';
2
3
  import { Param } from './Param.js';
3
4
  import { MosaicClient } from './MosaicClient.js';
@@ -187,7 +188,7 @@ export class Selection extends Param {
187
188
 
188
189
  /**
189
190
  * Emit an activate event with the given selection clause.
190
- * @param {*} clause The clause repesenting the potential activation.
191
+ * @param {SelectionClause} clause The clause repesenting the potential activation.
191
192
  */
192
193
  activate(clause) {
193
194
  this.emit('activate', clause);
@@ -196,7 +197,7 @@ export class Selection extends Param {
196
197
 
197
198
  /**
198
199
  * Update the selection with a new selection clause.
199
- * @param {*} clause The selection clause to add.
200
+ * @param {SelectionClause} clause The selection clause to add.
200
201
  * @returns {this} This Selection instance.
201
202
  */
202
203
  update(clause) {
@@ -208,6 +209,23 @@ export class Selection extends Param {
208
209
  return super.update(this._resolved);
209
210
  }
210
211
 
212
+ /**
213
+ * Reset the selection state by removing all provided clauses. If no clause
214
+ * array is provided as an argument, all current clauses are removed. The
215
+ * reset method (if defined) is invoked on all corresponding clause sources.
216
+ * The reset is relayed to downstream selections that include this selection.
217
+ * @param {SelectionClause[]} [clauses] The clauses to remove. If
218
+ * unspecified, all current clauses are removed.
219
+ * @returns {this} This selection instance.
220
+ */
221
+ reset(clauses) {
222
+ clauses ??= this._resolved;
223
+ clauses.forEach(c => c.source?.reset?.());
224
+ this._resolved = this._resolved.filter(c => clauses.includes(c));
225
+ this._relay.forEach(sel => sel.reset(clauses));
226
+ return super.update(this._resolved = []);
227
+ }
228
+
211
229
  /**
212
230
  * Upon value-typed updates, sets the current clause list to the
213
231
  * input value and returns the active clause value.
@@ -1,4 +1,4 @@
1
- import { Query, and, asNode, ceil, collectColumns, createTable, float64, floor, isBetween, int32, mul, round, scaleTransform, sub, isSelectQuery, ExprNode, SelectQuery } from '@uwdata/mosaic-sql';
1
+ import { Query, and, asNode, ceil, collectColumns, createTable, float64, floor, isBetween, int32, mul, round, scaleTransform, sub, isSelectQuery, ExprNode, SelectQuery, isAggregateExpression, ColumnNameRefNode } from '@uwdata/mosaic-sql';
2
2
  import { preaggColumns } from './preagg-columns.js';
3
3
  import { fnv_hash } from '../util/hash.js';
4
4
 
@@ -157,6 +157,10 @@ export class PreAggregator {
157
157
 
158
158
  // if cached active columns are unset, analyze the active clause
159
159
  if (!active) {
160
+ // if active clause predicate is null, we can't analyze it
161
+ // return null to backoff to standard client query
162
+ // non-null clauses may come later, so don't set active state
163
+ if (activeClause.predicate == null) return null;
160
164
  // generate active dimension columns to select over
161
165
  // will return an object with null source if it has unstable filters
162
166
  this.active = active = activeColumns(activeClause);
@@ -331,20 +335,45 @@ function preaggregateInfo(clientQuery, active, preaggCols, schema) {
331
335
 
332
336
  /**
333
337
  * Push column selections down to subqueries.
338
+ * @param {Query} query The (sub)query to push down to.
339
+ * @param {string[]} cols The column names to push down.
334
340
  */
335
341
  function subqueryPushdown(query, cols) {
336
342
  const memo = new Set;
337
343
  const pushdown = q => {
344
+ // it is possible to have duplicate subqueries
345
+ // so we memoize and exit early if already seen
338
346
  if (memo.has(q)) return;
339
347
  memo.add(q);
348
+
340
349
  if (isSelectQuery(q) && q._from.length) {
350
+ // select the pushed down columns
351
+ // note that the select method will deduplicate for us
341
352
  q.select(cols);
353
+ if (isAggregateQuery(q)) {
354
+ // if an aggregation query, we need to push to groupby as well
355
+ // we also deduplicate as the column may already be present
356
+ const set = new Set(
357
+ q._groupby.flatMap(x => x instanceof ColumnNameRefNode ? x.name : []
358
+ ));
359
+ q.groupby(cols.filter(c => !set.has(c)));
360
+ }
342
361
  }
343
362
  q.subqueries.forEach(pushdown);
344
363
  };
345
364
  pushdown(query);
346
365
  }
347
366
 
367
+ /**
368
+ * Test if a query performs aggregation.
369
+ * @param {SelectQuery} query
370
+ * @returns {boolean}
371
+ */
372
+ function isAggregateQuery(query) {
373
+ return query._groupby.length > 0
374
+ || query._select.some(node => isAggregateExpression(node));
375
+ }
376
+
348
377
  /**
349
378
  * Metadata and query generator for materialized views of pre-aggregated data.
350
379
  * This object provides the information needed to generate and query the
@@ -1,4 +1,4 @@
1
- import { AggregateNode, and, argmax, argmin, count, div, exp, ExprNode, isNotNull, ln, max, min, mul, pow, regrAvgX, regrAvgY, regrCount, sql, sqrt, sub, sum } from '@uwdata/mosaic-sql';
1
+ import { AggregateNode, and, argmax, argmin, coalesce, count, div, exp, ExprNode, isNotNull, ln, max, min, mul, pow, regrAvgX, regrAvgY, regrCount, sql, sqrt, sub, sum } from '@uwdata/mosaic-sql';
2
2
  import { fnv_hash } from '../util/hash.js';
3
3
 
4
4
  /**
@@ -14,6 +14,7 @@ import { fnv_hash } from '../util/hash.js';
14
14
  export function sufficientStatistics(node, preagg, avg) {
15
15
  switch (node.name) {
16
16
  case 'count':
17
+ return sumCountExpr(preagg, node);
17
18
  case 'sum':
18
19
  return sumExpr(preagg, node);
19
20
  case 'avg':
@@ -128,11 +129,24 @@ function addStat(preagg, expr, node) {
128
129
  */
129
130
  function countExpr(preagg, node) {
130
131
  const name = addStat(preagg, count(node.args[0]), node);
131
- return { expr: sum(name), name };
132
+ return { expr: coalesce(sum(name), 0), name };
132
133
  }
133
134
 
134
135
  /**
135
- * Generate an expression for calculating counts or sums over data dimensions.
136
+ * Generate an expression for calculating counts over data dimensions.
137
+ * The expression is a summation with an additional coalesce operation
138
+ * to map null sums to zero-valued counts.
139
+ * @param {Record<string, ExprNode>} preagg A map of columns (such as
140
+ * sufficient statistics) to pre-aggregate.
141
+ * @param {AggregateNode} node The originating aggregate function call.
142
+ * @returns {ExprNode} An aggregate expression over pre-aggregated dimensions.
143
+ */
144
+ function sumCountExpr(preagg, node) {
145
+ return coalesce(sumExpr(preagg, node), 0);
146
+ }
147
+
148
+ /**
149
+ * Generate an expression for calculating sums over data dimensions.
136
150
  * @param {Record<string, ExprNode>} preagg A map of columns (such as
137
151
  * sufficient statistics) to pre-aggregate.
138
152
  * @param {AggregateNode} node The originating aggregate function call.