@uwdata/mosaic-core 0.9.0 → 0.11.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
@@ -10,6 +10,13 @@ export function isSelection(x) {
10
10
  return x instanceof Selection;
11
11
  }
12
12
 
13
+ function create(options, include) {
14
+ return new Selection(
15
+ new SelectionResolver(options),
16
+ include ? [include].flat() : include
17
+ );
18
+ }
19
+
13
20
  /**
14
21
  * Represents a dynamic set of query filter predicates.
15
22
  */
@@ -22,10 +29,16 @@ export class Selection extends Param {
22
29
  * @param {boolean} [options.cross=false] Boolean flag indicating
23
30
  * cross-filtered resolution. If true, selection clauses will not
24
31
  * be applied to the clients they are associated with.
32
+ * @param {boolean} [options.empty=false] Boolean flag indicating if a lack
33
+ * of clauses should correspond to an empty selection with no records. This
34
+ * setting determines the default selection state.
35
+ * @param {Selection|Selection[]} [options.include] Upstream selections whose
36
+ * clauses should be included as part of the new selection. Any clauses
37
+ * published to upstream selections will be relayed to the new selection.
25
38
  * @returns {Selection} The new Selection instance.
26
39
  */
27
- static intersect({ cross = false } = {}) {
28
- return new Selection(new SelectionResolver({ cross }));
40
+ static intersect({ cross = false, empty = false, include = [] } = {}) {
41
+ return create({ cross, empty }, include);
29
42
  }
30
43
 
31
44
  /**
@@ -35,10 +48,16 @@ export class Selection extends Param {
35
48
  * @param {boolean} [options.cross=false] Boolean flag indicating
36
49
  * cross-filtered resolution. If true, selection clauses will not
37
50
  * be applied to the clients they are associated with.
51
+ * @param {boolean} [options.empty=false] Boolean flag indicating if a lack
52
+ * of clauses should correspond to an empty selection with no records. This
53
+ * setting determines the default selection state.
54
+ * @param {Selection|Selection[]} [options.include] Upstream selections whose
55
+ * clauses should be included as part of the new selection. Any clauses
56
+ * published to upstream selections will be relayed to the new selection.
38
57
  * @returns {Selection} The new Selection instance.
39
58
  */
40
- static union({ cross = false } = {}) {
41
- return new Selection(new SelectionResolver({ cross, union: true }));
59
+ static union({ cross = false, empty = false, include = [] } = {}) {
60
+ return create({ cross, empty, union: true }, include);
42
61
  }
43
62
 
44
63
  /**
@@ -48,30 +67,53 @@ export class Selection extends Param {
48
67
  * @param {boolean} [options.cross=false] Boolean flag indicating
49
68
  * cross-filtered resolution. If true, selection clauses will not
50
69
  * be applied to the clients they are associated with.
70
+ * @param {boolean} [options.empty=false] Boolean flag indicating if a lack
71
+ * of clauses should correspond to an empty selection with no records. This
72
+ * setting determines the default selection state.
73
+ * @param {Selection|Selection[]} [options.include] Upstream selections whose
74
+ * clauses should be included as part of the new selection. Any clauses
75
+ * published to upstream selections will be relayed to the new selection.
51
76
  * @returns {Selection} The new Selection instance.
52
77
  */
53
- static single({ cross = false } = {}) {
54
- return new Selection(new SelectionResolver({ cross, single: true }));
78
+ static single({ cross = false, empty = false, include = [] } = {}) {
79
+ return create({ cross, empty, single: true }, include);
55
80
  }
56
81
 
57
82
  /**
58
83
  * Create a new Selection instance with a
59
84
  * cross-filtered intersect resolution strategy.
85
+ * @param {object} [options] The selection options.
86
+ * @param {boolean} [options.empty=false] Boolean flag indicating if a lack
87
+ * of clauses should correspond to an empty selection with no records. This
88
+ * setting determines the default selection state.
89
+ * @param {Selection|Selection[]} [options.include] Upstream selections whose
90
+ * clauses should be included as part of the new selection. Any clauses
91
+ * published to upstream selections will be relayed to the new selection.
60
92
  * @returns {Selection} The new Selection instance.
61
93
  */
62
- static crossfilter() {
63
- return new Selection(new SelectionResolver({ cross: true }));
94
+ static crossfilter({ empty = false, include = [] } = {}) {
95
+ return create({ cross: true, empty }, include);
64
96
  }
65
97
 
66
98
  /**
67
99
  * Create a new Selection instance.
68
- * @param {SelectionResolver} resolver The selection resolution
100
+ * @param {SelectionResolver} [resolver] The selection resolution
69
101
  * strategy to apply.
102
+ * @param {Selection[]} [include] Upstream selections whose clauses
103
+ * should be included as part of this selection. Any clauses published
104
+ * to these upstream selections will be relayed to this selection.
70
105
  */
71
- constructor(resolver = new SelectionResolver()) {
106
+ constructor(resolver = new SelectionResolver(), include = []) {
72
107
  super([]);
73
108
  this._resolved = this._value;
74
109
  this._resolver = resolver;
110
+ /** @type {Set<Selection>} */
111
+ this._relay = new Set;
112
+ if (Array.isArray(include)) {
113
+ for (const sel of include) {
114
+ sel._relay.add(this);
115
+ }
116
+ }
75
117
  }
76
118
 
77
119
  /**
@@ -148,6 +190,7 @@ export class Selection extends Param {
148
190
  */
149
191
  activate(clause) {
150
192
  this.emit('activate', clause);
193
+ this._relay.forEach(sel => sel.activate(clause));
151
194
  }
152
195
 
153
196
  /**
@@ -160,6 +203,7 @@ export class Selection extends Param {
160
203
  // this ensures consistent clause state across unemitted event values
161
204
  this._resolved = this._resolver.resolve(this._resolved, clause, true);
162
205
  this._resolved.active = clause;
206
+ this._relay.forEach(sel => sel.update(clause));
163
207
  return super.update(this._resolved);
164
208
  }
165
209
 
@@ -232,11 +276,15 @@ export class SelectionResolver {
232
276
  * If false, an intersection strategy is used.
233
277
  * @param {boolean} [options.cross=false] Boolean flag to indicate cross-filtering.
234
278
  * @param {boolean} [options.single=false] Boolean flag to indicate single clauses only.
279
+ * @param {boolean} [options.empty=false] Boolean flag indicating if a lack
280
+ * of clauses should correspond to an empty selection with no records. This
281
+ * setting determines the default selection state.
235
282
  */
236
- constructor({ union, cross, single } = {}) {
283
+ constructor({ union, cross, single, empty } = {}) {
237
284
  this.union = !!union;
238
285
  this.cross = !!cross;
239
286
  this.single = !!single;
287
+ this.empty = !!empty;
240
288
  }
241
289
 
242
290
  /**
@@ -274,7 +322,11 @@ export class SelectionResolver {
274
322
  * based on the current state of this selection.
275
323
  */
276
324
  predicate(clauseList, active, client) {
277
- const { union } = this;
325
+ const { empty, union } = this;
326
+
327
+ if (empty && !clauseList.length) {
328
+ return ['FALSE'];
329
+ }
278
330
 
279
331
  // do nothing if cross-filtering and client is currently active
280
332
  if (this.skip(client, active)) return undefined;
@@ -24,7 +24,10 @@ import { MosaicClient } from './MosaicClient.js';
24
24
  * cross-filtering contexts.
25
25
  * @returns {SelectionClause} The generated selection clause.
26
26
  */
27
- export function point(field, value, { source, clients = undefined }) {
27
+ export function clausePoint(field, value, {
28
+ source,
29
+ clients = source ? new Set([source]) : undefined
30
+ }) {
28
31
  /** @type {SQLExpression | null} */
29
32
  const predicate = value !== undefined
30
33
  ? isNotDistinct(field, literal(value))
@@ -50,7 +53,10 @@ export function point(field, value, { source, clients = undefined }) {
50
53
  * cross-filtering contexts.
51
54
  * @returns {SelectionClause} The generated selection clause.
52
55
  */
53
- export function points(fields, value, { source, clients = undefined }) {
56
+ export function clausePoints(fields, value, {
57
+ source,
58
+ clients = source ? new Set([source]) : undefined
59
+ }) {
54
60
  /** @type {SQLExpression | null} */
55
61
  let predicate = null;
56
62
  if (value) {
@@ -83,13 +89,17 @@ export function points(fields, value, { source, clients = undefined }) {
83
89
  * @param {number} [options.pixelSize=1] The interactive pixel size.
84
90
  * @returns {SelectionClause} The generated selection clause.
85
91
  */
86
- export function interval(field, value, {
87
- source, clients, bin, scale, pixelSize = 1
92
+ export function clauseInterval(field, value, {
93
+ source,
94
+ clients = source ? new Set([source]) : undefined,
95
+ bin,
96
+ scale,
97
+ pixelSize = 1
88
98
  }) {
89
99
  /** @type {SQLExpression | null} */
90
100
  const predicate = value != null ? isBetween(field, value) : null;
91
101
  /** @type {import('./util/selection-types.js').IntervalMetadata} */
92
- const meta = { type: 'interval', scales: [scale], bin, pixelSize };
102
+ const meta = { type: 'interval', scales: scale && [scale], bin, pixelSize };
93
103
  return { meta, source, clients, value, predicate };
94
104
  }
95
105
 
@@ -108,8 +118,12 @@ export function interval(field, value, {
108
118
  * @param {number} [options.pixelSize=1] The interactive pixel size.
109
119
  * @returns {SelectionClause} The generated selection clause.
110
120
  */
111
- export function intervals(fields, value, {
112
- source, clients, bin, scales = [], pixelSize = 1
121
+ export function clauseIntervals(fields, value, {
122
+ source,
123
+ clients = source ? new Set([source]) : undefined,
124
+ bin,
125
+ scales = [],
126
+ pixelSize = 1
113
127
  }) {
114
128
  /** @type {SQLExpression | null} */
115
129
  const predicate = value != null
@@ -135,7 +149,7 @@ const MATCH_METHODS = { contains, prefix, suffix, regexp: regexp_matches };
135
149
  * text matching method to use. Defaults to `'contains'`.
136
150
  * @returns {SelectionClause} The generated selection clause.
137
151
  */
138
- export function match(field, value, {
152
+ export function clauseMatch(field, value, {
139
153
  source, clients = undefined, method = 'contains'
140
154
  }) {
141
155
  let fn = MATCH_METHODS[method];
@@ -1,11 +1,11 @@
1
- import { tableFromIPC } from 'apache-arrow';
1
+ import { decodeIPC } from '../util/decode-ipc.js';
2
2
 
3
3
  export function restConnector(uri = 'http://localhost:3000/') {
4
4
  return {
5
5
  /**
6
6
  * Query the DuckDB server.
7
7
  * @param {object} query
8
- * @param {'exec' | 'arrow' | 'json'} [query.type] The query type: 'exec', 'arrow', or 'json'.
8
+ * @param {'exec' | 'arrow' | 'json' | 'create-bundle' | 'load-bundle'} [query.type] The query type.
9
9
  * @param {string} query.sql A SQL query string.
10
10
  * @returns the query result
11
11
  */
@@ -20,7 +20,7 @@ export function restConnector(uri = 'http://localhost:3000/') {
20
20
  });
21
21
 
22
22
  return query.type === 'exec' ? req
23
- : query.type === 'arrow' ? tableFromIPC(req)
23
+ : query.type === 'arrow' ? decodeIPC(await (await req).arrayBuffer())
24
24
  : (await req).json();
25
25
  }
26
26
  };
@@ -1,4 +1,4 @@
1
- import { tableFromIPC } from 'apache-arrow';
1
+ import { decodeIPC } from '../util/decode-ipc.js';
2
2
 
3
3
  export function socketConnector(uri = 'ws://localhost:3000/') {
4
4
  const queue = [];
@@ -47,7 +47,7 @@ export function socketConnector(uri = 'ws://localhost:3000/') {
47
47
  } else if (query.type === 'exec') {
48
48
  resolve();
49
49
  } else if (query.type === 'arrow') {
50
- resolve(tableFromIPC(data.arrayBuffer()));
50
+ resolve(decodeIPC(data));
51
51
  } else {
52
52
  throw new Error(`Unexpected socket data: ${data}`);
53
53
  }
@@ -59,6 +59,7 @@ export function socketConnector(uri = 'ws://localhost:3000/') {
59
59
 
60
60
  function init() {
61
61
  ws = new WebSocket(uri);
62
+ ws.binaryType = 'arraybuffer';
62
63
  for (const type in events) {
63
64
  ws.addEventListener(type, events[type]);
64
65
  }
@@ -84,7 +85,7 @@ export function socketConnector(uri = 'ws://localhost:3000/') {
84
85
  /**
85
86
  * Query the DuckDB server.
86
87
  * @param {object} query
87
- * @param {'exec' | 'arrow' | 'json'} [query.type] The query type: 'exec', 'arrow', or 'json'.
88
+ * @param {'exec' | 'arrow' | 'json' | 'create-bundle' | 'load-bundle'} [query.type] The query type.
88
89
  * @param {string} query.sql A SQL query string.
89
90
  * @returns the query result
90
91
  */
@@ -1,4 +1,20 @@
1
1
  import * as duckdb from '@duckdb/duckdb-wasm';
2
+ import { decodeIPC } from '../util/decode-ipc.js';
3
+
4
+ // bypass duckdb-wasm query method to get Arrow IPC bytes directly
5
+ // https://github.com/duckdb/duckdb-wasm/issues/267#issuecomment-2252749509
6
+ function getArrowIPC(con, query) {
7
+ return new Promise((resolve, reject) => {
8
+ con.useUnsafe(async (bindings, conn) => {
9
+ try {
10
+ const buffer = await bindings.runQuery(conn, query);
11
+ resolve(buffer);
12
+ } catch (error) {
13
+ reject(error);
14
+ }
15
+ });
16
+ });
17
+ }
2
18
 
3
19
  export function wasmConnector(options = {}) {
4
20
  const { duckdb, connection, ...opts } = options;
@@ -45,17 +61,17 @@ export function wasmConnector(options = {}) {
45
61
  /**
46
62
  * Query the DuckDB-WASM instance.
47
63
  * @param {object} query
48
- * @param {'exec' | 'arrow' | 'json'} [query.type] The query type: 'exec', 'arrow', or 'json'.
64
+ * @param {'exec' | 'arrow' | 'json' | 'create-bundle' | 'load-bundle'} [query.type] The query type.
49
65
  * @param {string} query.sql A SQL query string.
50
66
  * @returns the query result
51
67
  */
52
68
  query: async query => {
53
69
  const { type, sql } = query;
54
70
  const con = await getConnection();
55
- const result = await con.query(sql);
71
+ const result = await getArrowIPC(con, sql);
56
72
  return type === 'exec' ? undefined
57
- : type === 'arrow' ? result
58
- : result.toArray();
73
+ : type === 'arrow' ? decodeIPC(result)
74
+ : decodeIPC(result).toArray();
59
75
  }
60
76
  };
61
77
  }
package/src/index.js CHANGED
@@ -3,18 +3,22 @@ export { Coordinator, coordinator } from './Coordinator.js';
3
3
  export { Selection, isSelection } from './Selection.js';
4
4
  export { Param, isParam } from './Param.js';
5
5
  export { Priority } from './QueryManager.js';
6
- export { point, points, interval, intervals, match } from './SelectionClause.js';
7
6
 
8
7
  export { restConnector } from './connectors/rest.js';
9
8
  export { socketConnector } from './connectors/socket.js';
10
9
  export { wasmConnector } from './connectors/wasm.js';
11
10
 
12
11
  export {
13
- isArrowTable,
14
- convertArrowArrayType,
15
- convertArrowValue,
16
- convertArrowColumn
17
- } from './util/convert-arrow.js'
12
+ clauseInterval,
13
+ clauseIntervals,
14
+ clausePoint,
15
+ clausePoints,
16
+ clauseMatch
17
+ } from './SelectionClause.js';
18
+
19
+ export { decodeIPC } from './util/decode-ipc.js';
18
20
  export { distinct } from './util/distinct.js';
21
+ export { isArrowTable } from './util/is-arrow-table.js';
19
22
  export { synchronizer } from './util/synchronizer.js';
20
23
  export { throttle } from './util/throttle.js';
24
+ export { toDataColumns } from './util/to-data-columns.js';
@@ -1,8 +1,7 @@
1
1
  /**
2
- * Event dispatcher supporting asynchronous updates.
3
- * If an event handler callback returns a Promise, this dispatcher will
4
- * wait for all such Promises to settle before dispatching future events
5
- * of the same type.
2
+ * Event dispatcher supporting asynchronous updates. If an event handler
3
+ * callback returns a Promise, the dispatcher waits for all such Promises
4
+ * to settle before dispatching future events of the same type.
6
5
  */
7
6
  export class AsyncDispatch {
8
7
 
@@ -63,7 +62,7 @@ export class AsyncDispatch {
63
62
  * queue of unemitted event values prior to enqueueing a new value.
64
63
  * This default implementation simply returns null, indicating that
65
64
  * any other unemitted event values should be dropped (that is, all
66
- * queued events are filtered)
65
+ * queued events are filtered).
67
66
  * @param {string} type The event type.
68
67
  * @param {*} value The new event value that will be enqueued.
69
68
  * @returns {(value: *) => boolean|null} A dispatch queue filter
@@ -83,6 +82,17 @@ export class AsyncDispatch {
83
82
  entry?.queue.clear();
84
83
  }
85
84
 
85
+ /**
86
+ * Returns a promise that resolves when any pending updates complete for
87
+ * the event of the given type currently being processed. The Promise will
88
+ * resolve immediately if the queue for the given event type is empty.
89
+ * @param {string} type The event type to wait for.
90
+ * @returns {Promise} A pending event promise.
91
+ */
92
+ async pending(type) {
93
+ await this._callbacks.get(type)?.pending;
94
+ }
95
+
86
96
  /**
87
97
  * Emit an event value to listeners for the given event type.
88
98
  * If a previous emit has not yet resolved, the event value
@@ -0,0 +1,11 @@
1
+ import { tableFromIPC } from '@uwdata/flechette';
2
+
3
+ /**
4
+ * Decode Arrow IPC bytes to a table instance, with an option to map date and
5
+ * timestamp values to JS Date objects.
6
+ * @param {ArrayBuffer | Uint8Array} data Arrow IPC bytes.
7
+ * @returns {import('@uwdata/flechette').Table} A table instance.
8
+ */
9
+ export function decodeIPC(data) {
10
+ return tableFromIPC(data, { useDate: true });
11
+ }
@@ -1,6 +1,5 @@
1
1
  import { Query, asRelation, count, isNull, max, min, sql } from '@uwdata/mosaic-sql';
2
2
  import { jsType } from './js-type.js';
3
- import { convertArrowValue } from './convert-arrow.js';
4
3
 
5
4
  export const Count = 'count';
6
5
  export const Nulls = 'nulls';
@@ -52,20 +51,13 @@ async function getFieldInfo(mc, { table, column, stats }) {
52
51
  if (!(stats?.length || stats?.size)) return info;
53
52
 
54
53
  // query for summary stats
55
- const result = await mc.query(
54
+ const [result] = await mc.query(
56
55
  summarize(table, column, stats),
57
56
  { persist: true }
58
57
  );
59
58
 
60
- // extract summary stats, copy to field info
61
- for (let i = 0; i < result.numCols; ++i) {
62
- const { name } = result.schema.fields[i];
63
- const child = result.getChildAt(i);
64
- const convert = convertArrowValue(child.type);
65
- info[name] = convert(child.get(0));
66
- }
67
-
68
- return info;
59
+ // extract summary stats, copy to field info, and return
60
+ return Object.assign(info, result);
69
61
  }
70
62
 
71
63
  async function getTableInfo(mc, table) {