@uwdata/mosaic-core 0.12.1 → 0.13.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.
@@ -1,3 +1,5 @@
1
+ import { Coordinator } from './Coordinator.js';
2
+ import { Selection } from './Selection.js';
1
3
  import { throttle } from './util/throttle.js';
2
4
 
3
5
  /**
@@ -11,9 +13,13 @@ export class MosaicClient {
11
13
  * the client when the selection updates.
12
14
  */
13
15
  constructor(filterSelection) {
16
+ /** @type {Selection} */
14
17
  this._filterBy = filterSelection;
15
18
  this._requestUpdate = throttle(() => this.requestQuery(), true);
19
+ /** @type {Coordinator} */
16
20
  this._coordinator = null;
21
+ /** @type {Promise<any>} */
22
+ this._pending = Promise.resolve();
17
23
  }
18
24
 
19
25
  /**
@@ -30,6 +36,13 @@ export class MosaicClient {
30
36
  this._coordinator = coordinator;
31
37
  }
32
38
 
39
+ /**
40
+ * Return a Promise that resolves once the client has updated.
41
+ */
42
+ get pending() {
43
+ return this._pending;
44
+ }
45
+
33
46
  /**
34
47
  * Return this client's filter selection.
35
48
  */
@@ -49,7 +62,8 @@ export class MosaicClient {
49
62
 
50
63
  /**
51
64
  * Return an array of fields queried by this client.
52
- * @returns {object[]|null} The fields to retrieve info for.
65
+ * @returns {import('./types.js').FieldInfoRequest[] | null}
66
+ * The fields to retrieve info for.
53
67
  */
54
68
  fields() {
55
69
  return null;
@@ -57,13 +71,19 @@ export class MosaicClient {
57
71
 
58
72
  /**
59
73
  * Called by the coordinator to set the field info for this client.
60
- * @param {*} info The field info result.
74
+ * @param {import('./types.js').FieldInfo[]} info The field info result.
61
75
  * @returns {this}
62
76
  */
63
77
  fieldInfo(info) { // eslint-disable-line no-unused-vars
64
78
  return this;
65
79
  }
66
80
 
81
+ /**
82
+ * Prepare the client before the query() method is called.
83
+ */
84
+ async prepare() {
85
+ }
86
+
67
87
  /**
68
88
  * Return a query specifying the data needed by this client.
69
89
  * @param {*} [filter] The filtering criteria to apply in the query.
@@ -122,7 +142,7 @@ export class MosaicClient {
122
142
  }
123
143
 
124
144
  /**
125
- * Reset this client, initiating new field info and query requests.
145
+ * Reset this client, initiating new field info, call the prepare method, and query requests.
126
146
  * @returns {Promise}
127
147
  */
128
148
  initialize() {
@@ -10,6 +10,7 @@ export class QueryManager {
10
10
  constructor(
11
11
  maxConcurrentRequests = 32
12
12
  ) {
13
+ /** @type {PriorityQueue} */
13
14
  this.queue = new PriorityQueue(3);
14
15
  this.db = null;
15
16
  this.clientCache = null;
@@ -18,11 +19,12 @@ export class QueryManager {
18
19
  this._consolidate = null;
19
20
  /**
20
21
  * Requests pending with the query manager.
21
- *
22
22
  * @type {QueryResult[]}
23
23
  */
24
24
  this.pendingResults = [];
25
+ /** @type {number} */
25
26
  this.maxConcurrentRequests = maxConcurrentRequests;
27
+ /** @type {boolean} */
26
28
  this.pendingExec = false;
27
29
  }
28
30
 
@@ -40,7 +40,7 @@ export function clausePoint(field, value, {
40
40
  /**
41
41
  * Generate a selection clause for multiple selected point values.
42
42
  * @param {import('@uwdata/mosaic-sql').ExprValue[]} fields The table columns or expressions to select.
43
- * @param {any[][] | undefined} value The selected values, as an array of
43
+ * @param {any[][] | null | undefined} value The selected values, as an array of
44
44
  * arrays. Each subarray contains values for each *fields* entry.
45
45
  * @param {object} options Additional clause properties.
46
46
  * @param {*} options.source The source component generating this clause.
@@ -75,7 +75,7 @@ export function clausePoints(fields, value, {
75
75
  /**
76
76
  * Generate a selection clause for a selected 1D interval.
77
77
  * @param {import('@uwdata/mosaic-sql').ExprValue} field The table column or expression to select.
78
- * @param {Extent} value The selected interval as a [lo, hi] array.
78
+ * @param {Extent | null | undefined} value The selected interval as a [lo, hi] array.
79
79
  * @param {object} options Additional clause properties.
80
80
  * @param {*} options.source The source component generating this clause.
81
81
  * @param {Set<MosaicClient>} [options.clients] The Mosaic clients associated
@@ -93,7 +93,6 @@ export function clauseInterval(field, value, {
93
93
  scale,
94
94
  pixelSize = 1
95
95
  }) {
96
- /** @type {ExprNode | null} */
97
96
  const predicate = value != null ? isBetween(field, value) : null;
98
97
  /** @type {import('./util/selection-types.js').IntervalMetadata} */
99
98
  const meta = { type: 'interval', scales: scale && [scale], bin, pixelSize };
@@ -103,7 +102,7 @@ export function clauseInterval(field, value, {
103
102
  /**
104
103
  * Generate a selection clause for multiple selected intervals.
105
104
  * @param {import('@uwdata/mosaic-sql').ExprValue[]} fields The table columns or expressions to select.
106
- * @param {Extent[]} value The selected intervals, as an array of extents.
105
+ * @param {Extent[] | null | undefined} value The selected intervals, as an array of extents.
107
106
  * @param {object} options Additional clause properties.
108
107
  * @param {*} options.source The source component generating this clause.
109
108
  * @param {Set<MosaicClient>} [options.clients] The Mosaic clients associated
@@ -122,7 +121,6 @@ export function clauseIntervals(fields, value, {
122
121
  scales = [],
123
122
  pixelSize = 1
124
123
  }) {
125
- /** @type {ExprNode | null} */
126
124
  const predicate = value != null
127
125
  ? and(fields.map((f, i) => isBetween(f, value[i])))
128
126
  : null;
@@ -136,7 +134,7 @@ const MATCH_METHODS = { contains, prefix, suffix, regexp: regexp_matches };
136
134
  /**
137
135
  * Generate a selection clause for text search matching.
138
136
  * @param {import('@uwdata/mosaic-sql').ExprValue} field The table column or expression to select.
139
- * @param {string} value The selected text search query string.
137
+ * @param {string | null | undefined} value The selected text search query string.
140
138
  * @param {object} options Additional clause properties.
141
139
  * @param {*} options.source The source component generating this clause.
142
140
  * @param {Set<MosaicClient>} [options.clients] The Mosaic clients associated
@@ -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';
@@ -22,16 +23,6 @@ export { isArrowTable } from './util/is-arrow-table.js';
22
23
  export { synchronizer } from './util/synchronizer.js';
23
24
  export { throttle } from './util/throttle.js';
24
25
  export { toDataColumns } from './util/to-data-columns.js';
25
-
26
- /**
27
- * @typedef {import('./util/selection-types.js').ClauseMetadata} ClauseMetadata
28
- * @typedef {import('./util/selection-types.js').PointMetadata} PointMetadata
29
- * @typedef {import('./util/selection-types.js').MatchMethod} MatchMethod
30
- * @typedef {import('./util/selection-types.js').MatchMetadata} MatchMetadata
31
- * @typedef {import('./util/selection-types.js').ScaleType} ScaleType
32
- * @typedef {import('./util/selection-types.js').Extent} Extent
33
- * @typedef {import('./util/selection-types.js').Scale} Scale
34
- * @typedef {import('./util/selection-types.js').BinMethod} BinMethod
35
- * @typedef {import('./util/selection-types.js').IntervalMetadata} IntervalMetadata
36
- * @typedef {import('./util/selection-types.js').SelectionClause} SelectionClause
37
- */
26
+ export { queryFieldInfo } from './util/field-info.js';
27
+ export { jsType } from './util/js-type.js';
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 { 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, 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
  /**
@@ -18,6 +18,8 @@ export function sufficientStatistics(node, preagg, avg) {
18
18
  return sumExpr(preagg, node);
19
19
  case 'avg':
20
20
  return avgExpr(preagg, node);
21
+ case 'geomean':
22
+ return geomeanExpr(preagg, node);
21
23
  case 'arg_max':
22
24
  return argmaxExpr(preagg, node);
23
25
  case 'arg_min':
@@ -155,6 +157,24 @@ function avgExpr(preagg, node) {
155
157
  return div(sum(mul(as, name)), expr);
156
158
  }
157
159
 
160
+ /**
161
+ * Generate an expression for calculating geometric means over data dimensions.
162
+ * This method uses log-based computations to ensure numerical stability. The
163
+ * geomean calculation uses two sufficient statistics: the sum of log values
164
+ * and the count of non-null values. As a side effect, this method adds columns
165
+ * for these statistics to the input *preagg* object.
166
+ * @param {Record<string, ExprNode>} preagg A map of columns (such as
167
+ * sufficient statistics) to pre-aggregate.
168
+ * @param {AggregateNode} node The originating aggregate function call.
169
+ * @returns {ExprNode} An aggregate expression over pre-aggregated dimensions.
170
+ */
171
+ function geomeanExpr(preagg, node) {
172
+ const x = node.args[0];
173
+ const expr = addStat(preagg, sum(ln(x)), node);
174
+ const { expr: n } = countExpr(preagg, node);
175
+ return exp(div(sum(expr), n));
176
+ }
177
+
158
178
  /**
159
179
  * Generate an expression for calculating argmax over data dimensions.
160
180
  * As a side effect, this method adds a column to the input *preagg* object
package/src/types.ts ADDED
@@ -0,0 +1,64 @@
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;
8
+
9
+ /** String indicating a JavaScript data type. */
10
+ export type JSType =
11
+ | 'number'
12
+ | 'date'
13
+ | 'boolean'
14
+ | 'string'
15
+ | 'array'
16
+ | 'object';
17
+
18
+ /** String indicating a requested summary statistic. */
19
+ export type Stat =
20
+ | 'count'
21
+ | 'nulls'
22
+ | 'max'
23
+ | 'min'
24
+ | 'distinct';
25
+
26
+ /** A reference to a database column or expression. */
27
+ export type FieldRef = string | ExprNode;
28
+
29
+ /**
30
+ * A request for metadata information about a database column.
31
+ */
32
+ export interface FieldInfoRequest {
33
+ table: string;
34
+ column: FieldRef;
35
+ stats?: Stat[];
36
+ }
37
+
38
+ /**
39
+ * A response with metadata information about a database column.
40
+ */
41
+ export interface FieldInfo extends Partial<Record<Stat, number>> {
42
+ table: string,
43
+ column: string,
44
+ sqlType: string,
45
+ type: JSType,
46
+ nullable: boolean
47
+ }
48
+
49
+ /** A result row from a DESCRIBE query. */
50
+ export interface ColumnDescription {
51
+ column_name: string,
52
+ column_type: string,
53
+ null: 'YES' | 'NO'
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
+ }
@@ -1,4 +1,4 @@
1
- import { AggregateNode, Query, asTableRef, count, isNull, max, min, sql } from '@uwdata/mosaic-sql';
1
+ import { AggregateNode, Query, asTableRef, count, isAggregateExpression, isNode, isNull, max, min, sql } from '@uwdata/mosaic-sql';
2
2
  import { jsType } from './js-type.js';
3
3
 
4
4
  export const Count = 'count';
@@ -9,7 +9,10 @@ export const Distinct = 'distinct';
9
9
  export const Stats = { Count, Nulls, Max, Min, Distinct };
10
10
 
11
11
  /**
12
- * @type {Record<string, (column: string) => AggregateNode>}
12
+ * @type {Record<
13
+ * import('../types.js').Stat,
14
+ * (column: import('../types.js').FieldRef) => AggregateNode
15
+ * >}
13
16
  */
14
17
  const statMap = {
15
18
  [Count]: count,
@@ -20,18 +23,26 @@ const statMap = {
20
23
  };
21
24
 
22
25
  /**
23
- *
24
- * @param {string} table
25
- * @param {string} column
26
- * @param {string[]|Set<string>} stats
27
- * @returns
26
+ * Get summary stats of the given column
27
+ * @param {import('../types.js').FieldInfoRequest} field
28
+ * @returns {Query}
28
29
  */
29
- function summarize(table, column, stats) {
30
+ function summarize({ table, column, stats }) {
30
31
  return Query
31
32
  .from(table)
32
- .select(Array.from(stats, s => ({[s]: statMap[s](column)})));
33
+ .select(Array.from(stats, s => ({ [s]: statMap[s](column) })));
33
34
  }
34
35
 
36
+ /**
37
+ * Queries information about fields of a table.
38
+ * If the `fields` array contains a single field with the column set to '*',
39
+ * the function will retrieve and return the table information using `getTableInfo`.
40
+ * Otherwise, it will query individual field information using `getFieldInfo`
41
+ * for each field in the `fields` array.
42
+ * @param {import('../Coordinator.js').Coordinator} mc A Mosaic coordinator.
43
+ * @param {import('../types.js').FieldInfoRequest[]} fields
44
+ * @returns {Promise<import('../types.js').FieldInfo[]>}
45
+ */
35
46
  export async function queryFieldInfo(mc, fields) {
36
47
  if (fields.length === 1 && fields[0].column === '*') {
37
48
  return getTableInfo(mc, fields[0].table);
@@ -42,13 +53,21 @@ export async function queryFieldInfo(mc, fields) {
42
53
  }
43
54
  }
44
55
 
56
+ /**
57
+ * Get information about a single field of a table.
58
+ * @param {import('../Coordinator.js').Coordinator} mc A Mosaic coordinator.
59
+ * @param {import('../types.js').FieldInfoRequest} field
60
+ * @returns {Promise<import('../types.js').FieldInfo>}
61
+ */
45
62
  async function getFieldInfo(mc, { table, column, stats }) {
46
63
  // generate and issue a query for field metadata info
47
64
  // use GROUP BY ALL to differentiate & consolidate aggregates
48
65
  const q = Query
49
66
  .from({ source: table })
50
67
  .select({ column })
51
- .groupby(column.aggregate ? sql`ALL` : []);
68
+ .groupby(isNode(column) && isAggregateExpression(column) ? sql`ALL` : []);
69
+
70
+ /** @type {import('../types.js').ColumnDescription[]} */
52
71
  const [desc] = Array.from(await mc.query(Query.describe(q)));
53
72
  const info = {
54
73
  table,
@@ -59,11 +78,11 @@ async function getFieldInfo(mc, { table, column, stats }) {
59
78
  };
60
79
 
61
80
  // no need for summary statistics
62
- if (!(stats?.length || stats?.size)) return info;
81
+ if (!stats?.length) return info;
63
82
 
64
83
  // query for summary stats
65
84
  const [result] = await mc.query(
66
- summarize(table, column, stats),
85
+ summarize({ table, column, stats }),
67
86
  { persist: true }
68
87
  );
69
88
 
@@ -71,9 +90,16 @@ async function getFieldInfo(mc, { table, column, stats }) {
71
90
  return Object.assign(info, result);
72
91
  }
73
92
 
93
+ /**
94
+ * Get information about the fields of a table.
95
+ * @param {import('../Coordinator.js').Coordinator} mc A Mosaic coordinator.
96
+ * @param {string} table The table name.
97
+ * @returns {Promise<import('../types.js').FieldInfo[]>}
98
+ */
74
99
  async function getTableInfo(mc, table) {
75
- const result = await mc.query(`DESCRIBE ${asTableRef(table)}`);
76
- return Array.from(result).map(desc => ({
100
+ /** @type {import('../types.js').ColumnDescription[]} */
101
+ const result = Array.from(await mc.query(`DESCRIBE ${asTableRef(table)}`));
102
+ return result.map(desc => ({
77
103
  table,
78
104
  column: desc.column_name,
79
105
  sqlType: desc.column_type,
@@ -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
+ }
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Maps a SQL data type to its corresponding JavaScript type.
3
+ * @param {string} type The name of a SQL data type
4
+ * @returns {import('../types.js').JSType} The corresponding JavaScript type name
5
+ * @throws {Error} Throws an error if the given SQL type name is unsupported or unrecognized.
6
+ */
1
7
  export function jsType(type) {
2
8
  switch (type) {
3
9
  case 'BIGINT':
@@ -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({});