@uwdata/mosaic-core 0.10.0 → 0.12.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.
Files changed (61) hide show
  1. package/README.md +1 -1
  2. package/dist/mosaic-core.js +12960 -21458
  3. package/dist/mosaic-core.min.js +7 -16
  4. package/dist/types/Coordinator.d.ts +169 -0
  5. package/dist/types/MosaicClient.d.ts +94 -0
  6. package/dist/types/Param.d.ts +47 -0
  7. package/dist/types/QueryConsolidator.d.ts +9 -0
  8. package/dist/types/QueryManager.d.ts +64 -0
  9. package/dist/types/Selection.d.ts +224 -0
  10. package/dist/types/SelectionClause.d.ts +105 -0
  11. package/dist/types/connectors/rest.d.ts +17 -0
  12. package/dist/types/connectors/socket.d.ts +18 -0
  13. package/dist/types/connectors/wasm.d.ts +16 -0
  14. package/dist/types/index.d.ts +25 -0
  15. package/dist/types/preagg/PreAggregator.d.ts +178 -0
  16. package/dist/types/preagg/preagg-columns.d.ts +14 -0
  17. package/dist/types/preagg/sufficient-statistics.d.ts +13 -0
  18. package/dist/types/util/AsyncDispatch.d.ts +100 -0
  19. package/dist/types/util/cache.d.ts +13 -0
  20. package/dist/types/util/decode-ipc.d.ts +7 -0
  21. package/dist/types/util/distinct.d.ts +2 -0
  22. package/dist/types/util/field-info.d.ts +13 -0
  23. package/dist/types/util/hash.d.ts +1 -0
  24. package/dist/types/util/is-arrow-table.d.ts +8 -0
  25. package/dist/types/util/js-type.d.ts +1 -0
  26. package/dist/types/util/priority-queue.d.ts +37 -0
  27. package/dist/types/util/query-result.d.ts +44 -0
  28. package/dist/types/util/selection-types.d.ts +114 -0
  29. package/dist/types/util/synchronizer.d.ts +29 -0
  30. package/dist/types/util/throttle.d.ts +11 -0
  31. package/dist/types/util/to-data-columns.d.ts +29 -0
  32. package/dist/types/util/void-logger.d.ts +7 -0
  33. package/jsconfig.json +11 -0
  34. package/package.json +10 -8
  35. package/src/Coordinator.js +66 -41
  36. package/src/MosaicClient.js +14 -4
  37. package/src/QueryConsolidator.js +32 -39
  38. package/src/QueryManager.js +85 -48
  39. package/src/Selection.js +49 -15
  40. package/src/SelectionClause.js +19 -22
  41. package/src/connectors/rest.js +6 -4
  42. package/src/connectors/socket.js +7 -4
  43. package/src/connectors/wasm.js +20 -4
  44. package/src/index.js +16 -8
  45. package/src/preagg/PreAggregator.js +407 -0
  46. package/src/preagg/preagg-columns.js +103 -0
  47. package/src/preagg/sufficient-statistics.js +439 -0
  48. package/src/util/decode-ipc.js +11 -0
  49. package/src/util/field-info.js +19 -16
  50. package/src/util/hash.js +1 -1
  51. package/src/util/is-arrow-table.js +10 -0
  52. package/src/util/priority-queue.js +75 -76
  53. package/src/util/query-result.js +44 -2
  54. package/src/util/selection-types.ts +3 -3
  55. package/src/util/throttle.js +21 -9
  56. package/src/util/to-data-columns.js +4 -15
  57. package/src/util/void-logger.js +6 -5
  58. package/tsconfig.json +11 -0
  59. package/src/DataCubeIndexer.js +0 -320
  60. package/src/util/convert-arrow.js +0 -145
  61. package/src/util/index-columns.js +0 -540
@@ -1,85 +1,84 @@
1
- /**
2
- * Create a new priority queue instance.
3
- * @param {number} ranks An integer number of rank-order priority levels.
4
- * @returns A priority queue instance.
5
- */
6
- export function priorityQueue(ranks) {
7
- // one list for each integer priority level
8
- const queue = Array.from(
1
+ export class PriorityQueue {
2
+ /**
3
+ * Create a new priority queue instance.
4
+ * @param {number} ranks An integer number of rank-order priority levels.
5
+ */
6
+ constructor(ranks) {
7
+ // one list for each integer priority level
8
+ this.queue = Array.from(
9
9
  { length: ranks },
10
10
  () => ({ head: null, tail: null })
11
11
  );
12
+ }
12
13
 
13
- return {
14
- /**
15
- * Indicate if the queue is empty.
16
- * @returns [boolean] true if empty, false otherwise.
17
- */
18
- isEmpty() {
19
- return queue.every(list => !list.head);
20
- },
14
+ /**
15
+ * Indicate if the queue is empty.
16
+ * @returns {boolean} true if empty, false otherwise.
17
+ */
18
+ isEmpty() {
19
+ return this.queue.every(list => !list.head);
20
+ }
21
21
 
22
- /**
23
- * Insert an item into the queue with a given priority rank.
24
- * @param {*} item The item to add.
25
- * @param {number} rank The integer priority rank.
26
- * Priority ranks are integers starting at zero.
27
- * Lower ranks indicate higher priority.
28
- */
29
- insert(item, rank) {
30
- const list = queue[rank];
31
- if (!list) {
32
- throw new Error(`Invalid queue priority rank: ${rank}`);
33
- }
22
+ /**
23
+ * Insert an item into the queue with a given priority rank.
24
+ * @param {*} item The item to add.
25
+ * @param {number} rank The integer priority rank.
26
+ * Priority ranks are integers starting at zero.
27
+ * Lower ranks indicate higher priority.
28
+ */
29
+ insert(item, rank) {
30
+ const list = this.queue[rank];
31
+ if (!list) {
32
+ throw new Error(`Invalid queue priority rank: ${rank}`);
33
+ }
34
34
 
35
- const node = { item, next: null };
36
- if (list.head === null) {
37
- list.head = list.tail = node;
38
- } else {
39
- list.tail = (list.tail.next = node);
40
- }
41
- },
35
+ const node = { item, next: null };
36
+ if (list.head === null) {
37
+ list.head = list.tail = node;
38
+ } else {
39
+ list.tail = list.tail.next = node;
40
+ }
41
+ }
42
42
 
43
- /**
44
- * Remove a set of items from the queue, regardless of priority rank.
45
- * If a provided item is not in the queue it will be ignored.
46
- * @param {(item: *) => boolean} test A predicate function to test
47
- * if an item should be removed (true to drop, false to keep).
48
- */
49
- remove(test) {
50
- for (const list of queue) {
51
- let { head, tail } = list;
52
- for (let prev = null, curr = head; curr; prev = curr, curr = curr.next) {
53
- if (test(curr.item)) {
54
- if (curr === head) {
55
- head = curr.next;
56
- } else {
57
- prev.next = curr.next;
58
- }
59
- if (curr === tail) tail = prev || head;
60
- }
61
- }
62
- list.head = head;
63
- list.tail = tail;
64
- }
65
- },
43
+ /**
44
+ * Remove a set of items from the queue, regardless of priority rank.
45
+ * If a provided item is not in the queue it will be ignored.
46
+ * @param {(item: *) => boolean} test A predicate function to test
47
+ * if an item should be removed (true to drop, false to keep).
48
+ */
49
+ remove(test) {
50
+ for (const list of this.queue) {
51
+ let { head, tail } = list;
52
+ for (let prev = null, curr = head; curr; prev = curr, curr = curr.next) {
53
+ if (test(curr.item)) {
54
+ if (curr === head) {
55
+ head = curr.next;
56
+ } else {
57
+ prev.next = curr.next;
58
+ }
59
+ if (curr === tail) tail = prev || head;
60
+ }
61
+ }
62
+ list.head = head;
63
+ list.tail = tail;
64
+ }
65
+ }
66
66
 
67
- /**
68
- * Remove and return the next highest priority item.
69
- * @returns {*} The next item in the queue,
70
- * or undefined if this queue is empty.
71
- */
72
- next() {
73
- for (const list of queue) {
74
- const { head } = list;
75
- if (head !== null) {
76
- list.head = head.next;
77
- if (list.tail === head) {
78
- list.tail = null;
79
- }
80
- return head.item;
81
- }
82
- }
83
- }
84
- };
67
+ /**
68
+ * Remove and return the next highest priority item.
69
+ * @returns {*} The next item in the queue,
70
+ * or undefined if this queue is empty.
71
+ */
72
+ next() {
73
+ for (const list of this.queue) {
74
+ const { head } = list;
75
+ if (head !== null) {
76
+ list.head = head.next;
77
+ if (list.tail === head) {
78
+ list.tail = null;
79
+ }
80
+ return head.item;
81
+ }
82
+ }
83
+ }
85
84
  }
@@ -1,3 +1,10 @@
1
+ export const QueryState = Object.freeze({
2
+ pending: Symbol('pending'),
3
+ ready: Symbol('ready'),
4
+ error: Symbol('error'),
5
+ done: Symbol('done')
6
+ });
7
+
1
8
  /**
2
9
  * A query result Promise that can allows external callers
3
10
  * to resolve or reject the Promise.
@@ -15,15 +22,41 @@ export class QueryResult extends Promise {
15
22
  });
16
23
  this._resolve = resolve;
17
24
  this._reject = reject;
25
+ this._state = QueryState.pending;
26
+ this._value = undefined;
18
27
  }
19
28
 
20
29
  /**
21
- * Resolve the result Promise with the provided value.
30
+ * Resolve the result Promise with a prepared value or the provided value.
31
+ * This method will only succeed if either a value is provided or the promise is ready.
22
32
  * @param {*} value The result value.
23
33
  * @returns {this}
24
34
  */
25
35
  fulfill(value) {
26
- this._resolve(value);
36
+ if (this._value !== undefined) {
37
+ if (value !== undefined) {
38
+ throw Error('Promise is ready and fulfill has a provided value');
39
+ }
40
+ this._resolve(this._value);
41
+ } else if (value === undefined) {
42
+ throw Error('Promise is neither ready nor has provided value');
43
+ } else {
44
+ this._resolve(value);
45
+ }
46
+
47
+ this._state = QueryState.done;
48
+
49
+ return this;
50
+ }
51
+
52
+ /**
53
+ * Prepare to resolve with the provided value.
54
+ * @param {*} value The result value.
55
+ * @returns {this}
56
+ */
57
+ ready(value) {
58
+ this._state = QueryState.ready;
59
+ this._value = value;
27
60
  return this;
28
61
  }
29
62
 
@@ -33,9 +66,18 @@ export class QueryResult extends Promise {
33
66
  * @returns {this}
34
67
  */
35
68
  reject(error) {
69
+ this._state = QueryState.error;
36
70
  this._reject(error);
37
71
  return this;
38
72
  }
73
+
74
+ /**
75
+ * Returns the state of this query result.
76
+ * @returns {symbol}
77
+ */
78
+ get state() {
79
+ return this._state;
80
+ }
39
81
  }
40
82
 
41
83
  // necessary to make Promise subclass act like a Promise
@@ -1,4 +1,4 @@
1
- import { SQLExpression } from '@uwdata/mosaic-sql';
1
+ import { ExprNode } from '@uwdata/mosaic-sql';
2
2
  import { MosaicClient } from '../MosaicClient.js';
3
3
 
4
4
  /**
@@ -127,11 +127,11 @@ export interface SelectionClause {
127
127
  * The predicate should apply filtering criteria consistent with this
128
128
  * clause's *value* property.
129
129
  */
130
- predicate: SQLExpression | null;
130
+ predicate: ExprNode | null;
131
131
  /**
132
132
  * Optional clause metadata that varies based on the selection type.
133
133
  * The metadata can be used to optimize selection queries, for example
134
- * by creating pre-aggregated data cubes when applicable.
134
+ * by creating materialized views of pre-aggregated data when applicable.
135
135
  */
136
136
  meta?: ClauseMetadata;
137
137
  }
@@ -1,20 +1,32 @@
1
1
  const NIL = {};
2
2
 
3
+ /**
4
+ * Throttle invocations of a callback function. The callback must return
5
+ * a Promise. Upon repeated invocation, the callback will not be invoked
6
+ * until a prior Promise resolves. If multiple invocations occurs while
7
+ * waiting, only the most recent invocation will be pending.
8
+ * @param {(event: *) => Promise} callback The callback function.
9
+ * @param {boolean} [debounce=true] Flag indicating if invocations
10
+ * should also be debounced within the current animation frame.
11
+ * @returns A new function that throttles access to the callback.
12
+ */
3
13
  export function throttle(callback, debounce = false) {
4
14
  let curr;
5
15
  let next;
6
16
  let pending = NIL;
7
17
 
8
18
  function invoke(event) {
9
- curr = callback(event).then(() => {
10
- if (next) {
11
- const { value } = next;
12
- next = null;
13
- invoke(value);
14
- } else {
15
- curr = null;
16
- }
17
- });
19
+ curr = callback(event)
20
+ .catch(() => {})
21
+ .finally(() => {
22
+ if (next) {
23
+ const { value } = next;
24
+ next = null;
25
+ invoke(value);
26
+ } else {
27
+ curr = null;
28
+ }
29
+ });
18
30
  }
19
31
 
20
32
  function enqueue(event) {
@@ -1,4 +1,4 @@
1
- import { convertArrowColumn, isArrowTable } from './convert-arrow.js';
1
+ import { isArrowTable } from './is-arrow-table.js';
2
2
 
3
3
  /**
4
4
  * @typedef {Array | Int8Array | Uint8Array | Uint8ClampedArray
@@ -27,23 +27,12 @@ export function toDataColumns(data) {
27
27
 
28
28
  /**
29
29
  * Convert an Arrow table to a set of column arrays.
30
- * @param {import('apache-arrow').Table} data An Apache Arrow Table.
30
+ * @param {import('@uwdata/flechette').Table} data An Arrow Table.
31
31
  * @returns {DataColumns} An object with named column arrays.
32
32
  */
33
33
  function arrowToColumns(data) {
34
- const { numRows, numCols, schema: { fields } } = data;
35
- const columns = {};
36
-
37
- for (let col = 0; col < numCols; ++col) {
38
- const name = fields[col].name;
39
- if (columns[name]) {
40
- console.warn(`Redundant column name "${name}". Skipping...`);
41
- } else {
42
- columns[name] = convertArrowColumn(data.getChildAt(col));
43
- }
44
- }
45
-
46
- return { numRows, columns };
34
+ const { numRows } = data;
35
+ return { numRows, columns: data.toColumns() };
47
36
  }
48
37
 
49
38
  /**
@@ -1,9 +1,10 @@
1
+ /* eslint-disable no-unused-vars */
1
2
  export function voidLogger() {
2
3
  return {
3
- debug() {},
4
- info() {},
5
- log() {},
6
- warn() {},
7
- error() {}
4
+ debug(..._) {},
5
+ info(..._) {},
6
+ log(..._) {},
7
+ warn(..._) {},
8
+ error(..._) {}
8
9
  };
9
10
  }
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "include": ["src/**/*.js", "src/**/*.ts"],
3
+ "compilerOptions": {
4
+ "allowJs": true,
5
+ "declaration": true,
6
+ "emitDeclarationOnly": true,
7
+ "outDir": "dist/types",
8
+ "module": "node16",
9
+ "skipLibCheck": true
10
+ }
11
+ }
@@ -1,320 +0,0 @@
1
- import {
2
- Query, and, asColumn, create, isBetween, scaleTransform, sql
3
- } from '@uwdata/mosaic-sql';
4
- import { indexColumns } from './util/index-columns.js';
5
- import { fnv_hash } from './util/hash.js';
6
-
7
- const Skip = { skip: true, result: null };
8
-
9
- /**
10
- * Build and query optimized indices ("data cubes") for fast computation of
11
- * groupby aggregate queries over compatible client queries and selections.
12
- * A data cube contains pre-aggregated data for a Mosaic client, subdivided
13
- * by possible query values from an active selection clause. These cubes are
14
- * realized as as database tables that can be queried for rapid updates.
15
- * Compatible client queries must consist of only groupby dimensions and
16
- * supported aggregate functions. Compatible selections must contain an active
17
- * clause that exposes metadata for an interval or point value predicate.
18
- */
19
- export class DataCubeIndexer {
20
- /**
21
- * Create a new data cube index table manager.
22
- * @param {import('./Coordinator.js').Coordinator} coordinator A Mosaic coordinator.
23
- * @param {object} [options] Indexer options.
24
- * @param {boolean} [options.enabled=true] Flag to enable/disable indexer.
25
- * @param {boolean} [options.temp=true] Flag to indicate if generated data
26
- * cube index tables should be temporary tables.
27
- */
28
- constructor(coordinator, {
29
- enabled = true,
30
- temp = true
31
- } = {}) {
32
- /** @type {Map<import('./MosaicClient.js').MosaicClient, DataCubeInfo | Skip | null>} */
33
- this.indexes = new Map();
34
- this.active = null;
35
- this.temp = temp;
36
- this.mc = coordinator;
37
- this._enabled = enabled;
38
- }
39
-
40
- /**
41
- * Set the enabled state of this indexer. If false, any cached state is
42
- * cleared and subsequent index calls will return null until re-enabled.
43
- * @param {boolean} state The enabled state.
44
- */
45
- enabled(state) {
46
- if (state === undefined) {
47
- return this._enabled;
48
- } else if (this._enabled !== state) {
49
- if (!state) this.clear();
50
- this._enabled = state;
51
- }
52
- }
53
-
54
- /**
55
- * Clear the cache of data cube index table entries for the current active
56
- * selection clause. This method will also cancel any queued data cube table
57
- * creation queries that have not yet been submitted to the database. This
58
- * method does _not_ drop any existing data cube tables.
59
- */
60
- clear() {
61
- this.mc.cancel(Array.from(this.indexes.values(), info => info?.result));
62
- this.indexes.clear();
63
- this.active = null;
64
- }
65
-
66
- /**
67
- * Return data cube index table information for the active state of a
68
- * client-selection pair, or null if the client is not indexable. This
69
- * method has multiple possible side effects, including data cube table
70
- * generation and updating internal caches.
71
- * @param {import('./MosaicClient.js').MosaicClient} client A Mosaic client.
72
- * @param {import('./Selection.js').Selection} selection A Mosaic selection
73
- * to filter the client by.
74
- * @param {import('./util/selection-types.js').SelectionClause} activeClause
75
- * A representative active selection clause for which to (possibly) generate
76
- * data cube index tables.
77
- * @returns {DataCubeInfo | Skip | null} Data cube index table
78
- * information and query generator, or null if the client is not indexable.
79
- */
80
- index(client, selection, activeClause) {
81
- // if not enabled, do nothing
82
- if (!this._enabled) return null;
83
-
84
- const { indexes, mc, temp } = this;
85
- const { source } = activeClause;
86
-
87
- // if there is no clause source to track, do nothing
88
- if (!source) return null;
89
-
90
- // if we have cached active columns, check for updates or exit
91
- if (this.active) {
92
- // if the active clause source has changed, clear indexer state
93
- // this cancels outstanding requests and clears the index cache
94
- // a clear also sets this.active to null
95
- if (this.active.source !== source) this.clear();
96
- // if we've seen this source and it's not indexable, do nothing
97
- if (this.active?.source === null) return null;
98
- }
99
-
100
- // the current active columns cache value
101
- let { active } = this;
102
-
103
- // if cached active columns are unset, analyze the active clause
104
- if (!active) {
105
- // generate active data cube dimension columns to select over
106
- // will return an object with null source if not indexable
107
- this.active = active = activeColumns(activeClause);
108
- // if the active clause is not indexable, exit now
109
- if (active.source === null) return null;
110
- }
111
-
112
- // if we have cached data cube index table info, return that
113
- if (indexes.has(client)) {
114
- return indexes.get(client);
115
- }
116
-
117
- // get non-active data cube index table columns
118
- const indexCols = indexColumns(client);
119
-
120
- let info;
121
- if (!indexCols) {
122
- // if client is not indexable, record null index
123
- info = null;
124
- } else if (selection.skip(client, activeClause)) {
125
- // skip client if untouched by cross-filtering
126
- info = Skip;
127
- } else {
128
- // generate data cube index table
129
- const filter = selection.remove(source).predicate(client);
130
- info = dataCubeInfo(client.query(filter), active, indexCols);
131
- info.result = mc.exec(create(info.table, info.create, { temp }));
132
- info.result.catch(e => mc.logger().error(e));
133
- }
134
-
135
- indexes.set(client, info);
136
- return info;
137
- }
138
- }
139
-
140
- /**
141
- * Determines the active data cube dimension columns to select over. Returns
142
- * an object with the clause source, column definitions, and a predicate
143
- * generator function for the active dimensions of a data cube index table. If
144
- * the active clause is not indexable or is missing metadata, this method
145
- * returns an object with a null source property.
146
- * @param {import('./util/selection-types.js').SelectionClause} clause The
147
- * active selection clause to analyze.
148
- */
149
- function activeColumns(clause) {
150
- const { source, meta } = clause;
151
- const clausePred = clause.predicate;
152
- const clauseCols = clausePred?.columns;
153
- let predicate;
154
- let columns;
155
-
156
- if (!meta || !clauseCols) {
157
- return { source: null, columns, predicate };
158
- }
159
-
160
- // @ts-ignore
161
- const { type, scales, bin, pixelSize = 1 } = meta;
162
-
163
- if (type === 'point') {
164
- predicate = x => x;
165
- columns = Object.fromEntries(
166
- clauseCols.map(col => [`${col}`, asColumn(col)])
167
- );
168
- } else if (type === 'interval' && scales) {
169
- // determine pixel-level binning
170
- const bins = scales.map(s => binInterval(s, pixelSize, bin));
171
-
172
- if (bins.some(b => !b)) {
173
- // bail if a scale type is unsupported
174
- } else if (bins.length === 1) {
175
- // single interval selection
176
- predicate = p => p ? isBetween('active0', p.range.map(bins[0])) : [];
177
- // @ts-ignore
178
- columns = { active0: bins[0](clausePred.field) };
179
- } else {
180
- // multiple interval selection
181
- predicate = p => p
182
- ? and(p.children.map(
183
- ({ range }, i) => isBetween(`active${i}`, range.map(bins[i]))
184
- ))
185
- : [];
186
- columns = Object.fromEntries(
187
- // @ts-ignore
188
- clausePred.children.map((p, i) => [`active${i}`, bins[i](p.field)])
189
- );
190
- }
191
- }
192
-
193
- return { source: columns ? source : null, columns, predicate };
194
- }
195
-
196
- const BIN = { ceil: 'CEIL', round: 'ROUND' };
197
-
198
- /**
199
- * Returns a bin function generator to discretize a selection interval domain.
200
- * @param {import('./util/selection-types.js').Scale} scale A scale that maps
201
- * domain values to the output range (typically pixels).
202
- * @param {number} pixelSize The interactive pixel size. This value indicates
203
- * the bin step size and may be greater than an actual screen pixel.
204
- * @param {import('./util/selection-types.js').BinMethod} bin The binning
205
- * method to apply, one of `floor`, `ceil', or `round`.
206
- * @returns {(value: any) => import('@uwdata/mosaic-sql').SQLExpression}
207
- * A bin function generator.
208
- */
209
- function binInterval(scale, pixelSize, bin) {
210
- const { type, domain, range, apply, sqlApply } = scaleTransform(scale);
211
- if (!apply) return; // unsupported scale type
212
- const fn = BIN[`${bin}`.toLowerCase()] || 'FLOOR';
213
- const lo = apply(Math.min(...domain));
214
- const hi = apply(Math.max(...domain));
215
- const a = type === 'identity' ? 1 : Math.abs(range[1] - range[0]) / (hi - lo);
216
- const s = a / pixelSize === 1 ? '' : `${a / pixelSize}::DOUBLE * `;
217
- const d = lo === 0 ? '' : ` - ${lo}::DOUBLE`;
218
- return value => sql`${fn}(${s}(${sqlApply(value)}${d}))::INTEGER`;
219
- }
220
-
221
- /**
222
- * Generate data cube table query information.
223
- * @param {Query} clientQuery The original client query.
224
- * @param {*} active Active (selected) column definitions.
225
- * @param {*} indexCols Data cube index column definitions.
226
- * @returns {DataCubeInfo}
227
- */
228
- function dataCubeInfo(clientQuery, active, indexCols) {
229
- const { dims, aggr, aux } = indexCols;
230
- const { columns } = active;
231
-
232
- // build index table construction query
233
- const query = clientQuery
234
- .select({ ...columns, ...aux })
235
- .groupby(Object.keys(columns));
236
-
237
- // ensure active clause columns are selected by subqueries
238
- const [subq] = query.subqueries;
239
- if (subq) {
240
- const cols = Object.values(columns).flatMap(c => c.columns);
241
- subqueryPushdown(subq, cols);
242
- }
243
-
244
- // push orderby criteria to later cube queries
245
- const order = query.orderby();
246
- query.query.orderby = [];
247
-
248
- // generate creation query string and hash id
249
- const create = query.toString();
250
- const id = (fnv_hash(create) >>> 0).toString(16);
251
- const table = `cube_index_${id}`;
252
-
253
- // generate data cube select query
254
- const select = Query
255
- .select(dims, aggr)
256
- .from(table)
257
- .groupby(dims)
258
- .orderby(order);
259
-
260
- return new DataCubeInfo({ table, create, active, select });
261
- }
262
-
263
- /**
264
- * Push column selections down to subqueries.
265
- */
266
- function subqueryPushdown(query, cols) {
267
- const memo = new Set;
268
- const pushdown = q => {
269
- if (memo.has(q)) return;
270
- memo.add(q);
271
- if (q.select && q.from().length) {
272
- q.select(cols);
273
- }
274
- q.subqueries.forEach(pushdown);
275
- };
276
- pushdown(query);
277
- }
278
-
279
- /**
280
- * Metadata and query generator for a data cube index table. This
281
- * object provides the information needed to generate and query
282
- * a data cube index table for a client-selection pair relative to
283
- * a specific active clause and selection state.
284
- */
285
- export class DataCubeInfo {
286
- /**
287
- * Create a new DataCubeInfo instance.
288
- * @param {object} options
289
- */
290
- constructor({ table, create, active, select } = {}) {
291
- /** The name of the data cube index table. */
292
- this.table = table;
293
- /** The SQL query used to generate the data cube index table. */
294
- this.create = create;
295
- /** A result promise returned for the data cube creation query. */
296
- this.result = null;
297
- /**
298
- * Definitions and predicate function for the active columns,
299
- * which are dynamically filtered by the active clause.
300
- */
301
- this.active = active;
302
- /** Select query (sans where clause) for data cube tables. */
303
- this.select = select;
304
- /**
305
- * Boolean flag indicating a client that should be skipped.
306
- * This value is always false for completed data cube info.
307
- */
308
- this.skip = false;
309
- }
310
-
311
- /**
312
- * Generate a data cube index table query for the given predicate.
313
- * @param {import('@uwdata/mosaic-sql').SQLExpression} predicate The current
314
- * active clause predicate.
315
- * @returns {Query} A data cube index table query.
316
- */
317
- query(predicate) {
318
- return this.select.clone().where(this.active.predicate(predicate));
319
- }
320
- }