@uwdata/mosaic-core 0.12.2 → 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.
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.
@@ -21,9 +21,16 @@ export function restConnector(uri = 'http://localhost:3000/') {
21
21
  body: JSON.stringify(query)
22
22
  });
23
23
 
24
+
25
+ const res = await req;
26
+
27
+ if (!res.ok) {
28
+ throw new Error(`Query failed with HTTP status ${res.status}: ${await res.text()}`);
29
+ }
30
+
24
31
  return query.type === 'exec' ? req
25
- : query.type === 'arrow' ? decodeIPC(await (await req).arrayBuffer())
26
- : (await req).json();
32
+ : query.type === 'arrow' ? decodeIPC(await res.arrayBuffer())
33
+ : res.json();
27
34
  }
28
35
  };
29
36
  }
@@ -0,0 +1,3 @@
1
+ export * from './index.js';
2
+ export * from './types.js';
3
+ export * from './util/selection-types.js';
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { MosaicClient } from './MosaicClient.js';
2
+ export { makeClient } from './make-client.js';
2
3
  export { Coordinator, coordinator } from './Coordinator.js';
3
4
  export { Selection, isSelection } from './Selection.js';
4
5
  export { Param, isParam } from './Param.js';
@@ -24,16 +25,4 @@ export { throttle } from './util/throttle.js';
24
25
  export { toDataColumns } from './util/to-data-columns.js';
25
26
  export { queryFieldInfo } from './util/field-info.js';
26
27
  export { jsType } from './util/js-type.js';
27
-
28
- /**
29
- * @typedef {import('./util/selection-types.js').ClauseMetadata} ClauseMetadata
30
- * @typedef {import('./util/selection-types.js').PointMetadata} PointMetadata
31
- * @typedef {import('./util/selection-types.js').MatchMethod} MatchMethod
32
- * @typedef {import('./util/selection-types.js').MatchMetadata} MatchMetadata
33
- * @typedef {import('./util/selection-types.js').ScaleType} ScaleType
34
- * @typedef {import('./util/selection-types.js').Extent} Extent
35
- * @typedef {import('./util/selection-types.js').Scale} Scale
36
- * @typedef {import('./util/selection-types.js').BinMethod} BinMethod
37
- * @typedef {import('./util/selection-types.js').IntervalMetadata} IntervalMetadata
38
- * @typedef {import('./util/selection-types.js').SelectionClause} SelectionClause
39
- */
28
+ export { isActivatable } from './util/is-activatable.js';
@@ -0,0 +1,64 @@
1
+ import { MosaicClient } from "./MosaicClient.js";
2
+ import {
3
+ coordinator as defaultCoordinator,
4
+ } from "./Coordinator.js";
5
+
6
+ /**
7
+ * @typedef {Object} MakeClientOptions
8
+ * @property {import('./Coordinator.js').Coordinator} [coordinator] - Mosaic coordinator. Default to the global coordinator.
9
+ * @property {import('./Selection.js').Selection|null} [selection] - A selection whose predicates will be fed into the query function to produce the SQL query.
10
+ * @property {function(): Promise<void>} [prepare] - An async function to prepare the client before running queries.
11
+ * @property {function(any): any} query - A function that returns a query from a list of selection predicates.
12
+ * @property {function(any): void} [queryResult] - Called by the coordinator to return a query result.
13
+ * @property {function(): void} [queryPending] - Called by the coordinator to report a query execution error.
14
+ * @property {function(any): void} [queryError] - Called by the coordinator to inform the client that a query is pending.
15
+ */
16
+
17
+ /** Make a new client with the given options, and connect the client to the provided coordinator.
18
+ * @param {MakeClientOptions} options - The options for making the client
19
+ * @returns {MosaicClient & { destroy: () => void }} - The result object with methods to request an update or destroy the client.
20
+ */
21
+ export function makeClient(options) {
22
+ const coordinator = options.coordinator ?? defaultCoordinator();
23
+ const client = new ProxyClient({ ...options, coordinator });
24
+ coordinator.connect(client);
25
+ return client;
26
+ }
27
+
28
+ /** An internal class used to implement the makeClient API */
29
+ class ProxyClient extends MosaicClient {
30
+ /** @param {MakeClientOptions} options */
31
+ constructor(options) {
32
+ super(options.selection);
33
+
34
+ /** @type {MakeClientOptions} */
35
+ this._options = { ...options };
36
+ }
37
+
38
+ async prepare() {
39
+ await this._options.prepare?.();
40
+ }
41
+
42
+ query(filter) {
43
+ return this._options.query(filter);
44
+ }
45
+
46
+ queryResult(data) {
47
+ this._options.queryResult?.(data);
48
+ return this;
49
+ }
50
+
51
+ queryPending() {
52
+ this._options.queryPending?.();
53
+ return this;
54
+ }
55
+
56
+ queryError(error) {
57
+ this._options.queryError?.(error);
58
+ return this;
59
+ }
60
+
61
+ destroy() {
62
+ this._options.coordinator.disconnect(this);
63
+ }
64
+ }
@@ -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, ExprNode, isNotNull, 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,10 +14,13 @@ 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':
20
21
  return avgExpr(preagg, node);
22
+ case 'geomean':
23
+ return geomeanExpr(preagg, node);
21
24
  case 'arg_max':
22
25
  return argmaxExpr(preagg, node);
23
26
  case 'arg_min':
@@ -126,11 +129,24 @@ function addStat(preagg, expr, node) {
126
129
  */
127
130
  function countExpr(preagg, node) {
128
131
  const name = addStat(preagg, count(node.args[0]), node);
129
- return { expr: sum(name), name };
132
+ return { expr: coalesce(sum(name), 0), name };
130
133
  }
131
134
 
132
135
  /**
133
- * 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.
134
150
  * @param {Record<string, ExprNode>} preagg A map of columns (such as
135
151
  * sufficient statistics) to pre-aggregate.
136
152
  * @param {AggregateNode} node The originating aggregate function call.
@@ -155,6 +171,24 @@ function avgExpr(preagg, node) {
155
171
  return div(sum(mul(as, name)), expr);
156
172
  }
157
173
 
174
+ /**
175
+ * Generate an expression for calculating geometric means over data dimensions.
176
+ * This method uses log-based computations to ensure numerical stability. The
177
+ * geomean calculation uses two sufficient statistics: the sum of log values
178
+ * and the count of non-null values. As a side effect, this method adds columns
179
+ * for these statistics to the input *preagg* object.
180
+ * @param {Record<string, ExprNode>} preagg A map of columns (such as
181
+ * sufficient statistics) to pre-aggregate.
182
+ * @param {AggregateNode} node The originating aggregate function call.
183
+ * @returns {ExprNode} An aggregate expression over pre-aggregated dimensions.
184
+ */
185
+ function geomeanExpr(preagg, node) {
186
+ const x = node.args[0];
187
+ const expr = addStat(preagg, sum(ln(x)), node);
188
+ const { expr: n } = countExpr(preagg, node);
189
+ return exp(div(sum(expr), n));
190
+ }
191
+
158
192
  /**
159
193
  * Generate an expression for calculating argmax over data dimensions.
160
194
  * As a side effect, this method adds a column to the input *preagg* object
package/src/types.ts CHANGED
@@ -1,4 +1,10 @@
1
- import type { ExprNode } from '@uwdata/mosaic-sql';
1
+ import type { DescribeQuery, ExprNode, Query } from '@uwdata/mosaic-sql';
2
+
3
+ /** Query type accepted by a coordinator. */
4
+ export type QueryType =
5
+ | string
6
+ | Query
7
+ | DescribeQuery;
2
8
 
3
9
  /** String indicating a JavaScript data type. */
4
10
  export type JSType =
@@ -46,3 +52,13 @@ export interface ColumnDescription {
46
52
  column_type: string,
47
53
  null: 'YES' | 'NO'
48
54
  }
55
+
56
+ /**
57
+ * Interface for components that perform selection activation.
58
+ */
59
+ export interface Activatable {
60
+ /**
61
+ * Activate the selection that this component publishes to.
62
+ */
63
+ activate(): void;
64
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Test if a value implements the Activatable interface.
3
+ * @param {*} value The value to test.
4
+ * @returns {value is import('../types.js').Activatable}
5
+ */
6
+ export function isActivatable(value) {
7
+ return typeof value?.activate === 'function' && value.activate.length === 0;
8
+ }
@@ -5,10 +5,12 @@ const NIL = {};
5
5
  * a Promise. Upon repeated invocation, the callback will not be invoked
6
6
  * until a prior Promise resolves. If multiple invocations occurs while
7
7
  * waiting, only the most recent invocation will be pending.
8
- * @param {(event: *) => Promise} callback The callback function.
8
+ * @template E, T
9
+ * @param {(event: E) => Promise<T>} callback The callback function.
9
10
  * @param {boolean} [debounce=true] Flag indicating if invocations
10
11
  * should also be debounced within the current animation frame.
11
- * @returns A new function that throttles access to the callback.
12
+ * @returns {(event: E) => void} A new function that throttles
13
+ * access to the callback.
12
14
  */
13
15
  export function throttle(callback, debounce = false) {
14
16
  let curr;
@@ -0,0 +1,3 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ export default defineConfig({});