@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/dist/mosaic-core.js +11493 -19779
- package/dist/mosaic-core.min.js +6 -15
- package/package.json +8 -8
- package/src/Coordinator.js +225 -55
- package/src/DataCubeIndexer.js +304 -141
- package/src/MosaicClient.js +12 -3
- package/src/QueryConsolidator.js +15 -11
- package/src/QueryManager.js +13 -5
- package/src/Selection.js +64 -12
- package/src/SelectionClause.js +22 -8
- package/src/connectors/rest.js +3 -3
- package/src/connectors/socket.js +4 -3
- package/src/connectors/wasm.js +20 -4
- package/src/index.js +10 -6
- package/src/util/AsyncDispatch.js +15 -5
- package/src/util/decode-ipc.js +11 -0
- package/src/util/field-info.js +3 -11
- package/src/util/index-columns.js +79 -80
- package/src/util/is-arrow-table.js +10 -0
- package/src/util/priority-queue.js +75 -76
- package/src/util/query-result.js +41 -8
- package/src/util/throttle.js +11 -1
- package/src/util/to-data-columns.js +60 -0
- package/src/FilterGroup.js +0 -81
- package/src/util/convert-arrow.js +0 -145
package/src/DataCubeIndexer.js
CHANGED
|
@@ -1,194 +1,269 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
Query, and, asColumn, create, isBetween, scaleTransform, sql
|
|
3
|
+
} from '@uwdata/mosaic-sql';
|
|
3
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
|
+
* @typedef {object} DataCubeIndexerOptions
|
|
11
|
+
* @property {string} [schema] Database schema (namespace) in which to write
|
|
12
|
+
* data cube index tables (default 'mosaic').
|
|
13
|
+
* @property {boolean} [options.enabled=true] Flag to enable or disable the
|
|
14
|
+
* indexer. This setting can later be updated via the `enabled` method.
|
|
15
|
+
*/
|
|
4
16
|
|
|
5
17
|
/**
|
|
6
18
|
* Build and query optimized indices ("data cubes") for fast computation of
|
|
7
19
|
* groupby aggregate queries over compatible client queries and selections.
|
|
8
20
|
* A data cube contains pre-aggregated data for a Mosaic client, subdivided
|
|
9
|
-
* by possible query values from an active
|
|
10
|
-
* as
|
|
11
|
-
*
|
|
12
|
-
* must consist of only groupby dimensions and
|
|
13
|
-
* Compatible selections must contain an active
|
|
14
|
-
* for an interval or point value predicate.
|
|
21
|
+
* by possible query values from an active selection clause. These cubes are
|
|
22
|
+
* realized as as database tables that can be queried for rapid updates.
|
|
23
|
+
*
|
|
24
|
+
* Compatible client queries must consist of only groupby dimensions and
|
|
25
|
+
* supported aggregate functions. Compatible selections must contain an active
|
|
26
|
+
* clause that exposes metadata for an interval or point value predicate.
|
|
27
|
+
*
|
|
28
|
+
* Data cube index tables are written to a dedicated schema (namespace) that
|
|
29
|
+
* can be set using the *schema* constructor option. This schema acts as a
|
|
30
|
+
* persistent cache, and index tables may be used across sessions. The
|
|
31
|
+
* `dropIndexTables` method issues a query to remove *all* tables within
|
|
32
|
+
* this schema. This may be needed if the original tables have updated data,
|
|
33
|
+
* but should be used with care.
|
|
15
34
|
*/
|
|
16
35
|
export class DataCubeIndexer {
|
|
17
36
|
/**
|
|
18
|
-
*
|
|
19
|
-
* @param {import('./Coordinator.js').Coordinator}
|
|
20
|
-
* @param {
|
|
37
|
+
* Create a new data cube index table manager.
|
|
38
|
+
* @param {import('./Coordinator.js').Coordinator} coordinator A Mosaic coordinator.
|
|
39
|
+
* @param {DataCubeIndexerOptions} [options] Data cube indexer options.
|
|
21
40
|
*/
|
|
22
|
-
constructor(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
this.
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
reset() {
|
|
31
|
-
this.enabled = false;
|
|
32
|
-
this.clients = null;
|
|
33
|
-
this.indices = null;
|
|
41
|
+
constructor(coordinator, {
|
|
42
|
+
schema = 'mosaic',
|
|
43
|
+
enabled = true
|
|
44
|
+
} = {}) {
|
|
45
|
+
/** @type {Map<import('./MosaicClient.js').MosaicClient, DataCubeInfo | Skip | null>} */
|
|
46
|
+
this.indexes = new Map();
|
|
34
47
|
this.active = null;
|
|
48
|
+
this.mc = coordinator;
|
|
49
|
+
this._schema = schema;
|
|
50
|
+
this._enabled = enabled;
|
|
35
51
|
}
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Set the enabled state of this indexer. If false, any local state is
|
|
55
|
+
* cleared and subsequent index calls will return null until re-enabled.
|
|
56
|
+
* This method has no effect on any index tables already in the database.
|
|
57
|
+
* @param {boolean} [state] The enabled state to set.
|
|
58
|
+
*/
|
|
59
|
+
set enabled(state) {
|
|
60
|
+
if (this._enabled !== state) {
|
|
61
|
+
if (!state) this.clear();
|
|
62
|
+
this._enabled = state;
|
|
41
63
|
}
|
|
42
64
|
}
|
|
43
65
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Get the enabled state of this indexer.
|
|
68
|
+
* @returns {boolean} The current enabled state.
|
|
69
|
+
*/
|
|
70
|
+
get enabled() {
|
|
71
|
+
return this._enabled;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set the database schema used by this indexer. Upon changes, any local
|
|
76
|
+
* state is cleared. This method does _not_ drop any existing data cube
|
|
77
|
+
* tables, use `dropIndexTables` before changing the schema to also remove
|
|
78
|
+
* existing index tables in the database.
|
|
79
|
+
* @param {string} [schema] The schema name to set.
|
|
80
|
+
*/
|
|
81
|
+
set schema(schema) {
|
|
82
|
+
if (this._schema !== schema) {
|
|
52
83
|
this.clear();
|
|
84
|
+
this._schema = schema;
|
|
53
85
|
}
|
|
54
|
-
|
|
86
|
+
}
|
|
55
87
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Get the database schema used by this indexer.
|
|
90
|
+
* @returns {string} The current schema name.
|
|
91
|
+
*/
|
|
92
|
+
get schema() {
|
|
93
|
+
return this._schema;
|
|
94
|
+
}
|
|
60
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Issues a query through the coordinator to drop the current index table
|
|
98
|
+
* schema. *All* tables in the schema will be removed and local state is
|
|
99
|
+
* cleared. Call this method if the underlying base tables have been updated,
|
|
100
|
+
* causing derived index tables to become stale and inaccurate. Use this
|
|
101
|
+
* method with care! Once dropped, the schema will be repopulated by future
|
|
102
|
+
* data cube indexer requests.
|
|
103
|
+
* @returns A query result promise.
|
|
104
|
+
*/
|
|
105
|
+
dropIndexTables() {
|
|
61
106
|
this.clear();
|
|
62
|
-
|
|
63
|
-
const active = this.active = activeColumns(activeClause);
|
|
64
|
-
if (!active) return false; // active selection clause not compatible
|
|
65
|
-
|
|
66
|
-
const logger = this.mc.logger();
|
|
67
|
-
logger.warn('DATA CUBE INDEX CONSTRUCTION');
|
|
68
|
-
|
|
69
|
-
// create a selection with the active source removed
|
|
70
|
-
const sel = this.selection.remove(source);
|
|
71
|
-
|
|
72
|
-
// generate data cube indices
|
|
73
|
-
const indices = this.indices = new Map;
|
|
74
|
-
const { mc, temp } = this;
|
|
75
|
-
for (const client of clients) {
|
|
76
|
-
// determine if client should be skipped due to cross-filtering
|
|
77
|
-
if (sel.skip(client, activeClause)) {
|
|
78
|
-
indices.set(client, null);
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// generate column definitions for data cube and cube queries
|
|
83
|
-
const index = indexColumns(client);
|
|
84
|
-
|
|
85
|
-
// skip if client is not indexable
|
|
86
|
-
if (!index) {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// build index table construction query
|
|
91
|
-
const query = client.query(sel.predicate(client))
|
|
92
|
-
.select({ ...active.columns, ...index.aux })
|
|
93
|
-
.groupby(Object.keys(active.columns));
|
|
94
|
-
|
|
95
|
-
// ensure active view columns are selected by subqueries
|
|
96
|
-
const [subq] = query.subqueries;
|
|
97
|
-
if (subq) {
|
|
98
|
-
const cols = Object.values(active.columns).flatMap(c => c.columns);
|
|
99
|
-
subqueryPushdown(subq, cols);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// push orderby criteria to later cube queries
|
|
103
|
-
const order = query.orderby();
|
|
104
|
-
query.query.orderby = [];
|
|
105
|
-
|
|
106
|
-
const sql = query.toString();
|
|
107
|
-
const id = (fnv_hash(sql) >>> 0).toString(16);
|
|
108
|
-
const table = `cube_index_${id}`;
|
|
109
|
-
const result = mc.exec(create(table, sql, { temp }));
|
|
110
|
-
result.catch(e => logger.error(e));
|
|
111
|
-
indices.set(client, { table, result, order, ...index });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// index creation successful
|
|
115
|
-
return true;
|
|
107
|
+
return this.mc.exec(`DROP SCHEMA IF EXISTS "${this.schema}" CASCADE`);
|
|
116
108
|
}
|
|
117
109
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Clear the cache of data cube index table entries for the current active
|
|
112
|
+
* selection clause. This method does _not_ drop any existing data cube
|
|
113
|
+
* tables. Use `dropIndexTables` to remove existing index tables from the
|
|
114
|
+
* database.
|
|
115
|
+
*/
|
|
116
|
+
clear() {
|
|
117
|
+
this.indexes.clear();
|
|
118
|
+
this.active = null;
|
|
124
119
|
}
|
|
125
120
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Return data cube index table information for the active state of a
|
|
123
|
+
* client-selection pair, or null if the client is not indexable. This
|
|
124
|
+
* method has multiple possible side effects, including data cube table
|
|
125
|
+
* generation and updating internal caches.
|
|
126
|
+
* @param {import('./MosaicClient.js').MosaicClient} client A Mosaic client.
|
|
127
|
+
* @param {import('./Selection.js').Selection} selection A Mosaic selection
|
|
128
|
+
* to filter the client by.
|
|
129
|
+
* @param {import('./util/selection-types.js').SelectionClause} activeClause
|
|
130
|
+
* A representative active selection clause for which to (possibly) generate
|
|
131
|
+
* data cube index tables.
|
|
132
|
+
* @returns {DataCubeInfo | Skip | null} Data cube index table
|
|
133
|
+
* information and query generator, or null if the client is not indexable.
|
|
134
|
+
*/
|
|
135
|
+
index(client, selection, activeClause) {
|
|
136
|
+
// if not enabled, do nothing
|
|
137
|
+
if (!this.enabled) return null;
|
|
138
|
+
|
|
139
|
+
const { indexes, mc, schema } = this;
|
|
140
|
+
const { source } = activeClause;
|
|
141
|
+
|
|
142
|
+
// if there is no clause source to track, do nothing
|
|
143
|
+
if (!source) return null;
|
|
144
|
+
|
|
145
|
+
// if we have cached active columns, check for updates or exit
|
|
146
|
+
if (this.active) {
|
|
147
|
+
// if the active clause source has changed, clear indexer state
|
|
148
|
+
// this cancels outstanding requests and clears the index cache
|
|
149
|
+
// a clear also sets this.active to null
|
|
150
|
+
if (this.active.source !== source) this.clear();
|
|
151
|
+
// if we've seen this source and it's not indexable, do nothing
|
|
152
|
+
if (this.active?.source === null) return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// the current active columns cache value
|
|
156
|
+
let { active } = this;
|
|
157
|
+
|
|
158
|
+
// if cached active columns are unset, analyze the active clause
|
|
159
|
+
if (!active) {
|
|
160
|
+
// generate active data cube dimension columns to select over
|
|
161
|
+
// will return an object with null source if not indexable
|
|
162
|
+
this.active = active = activeColumns(activeClause);
|
|
163
|
+
// if the active clause is not indexable, exit now
|
|
164
|
+
if (active.source === null) return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// if we have cached data cube index table info, return that
|
|
168
|
+
if (indexes.has(client)) {
|
|
169
|
+
return indexes.get(client);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// get non-active data cube index table columns
|
|
173
|
+
const indexCols = indexColumns(client);
|
|
174
|
+
|
|
175
|
+
let info;
|
|
176
|
+
if (!indexCols) {
|
|
177
|
+
// if client is not indexable, record null index
|
|
178
|
+
info = null;
|
|
179
|
+
} else if (selection.skip(client, activeClause)) {
|
|
180
|
+
// skip client if untouched by cross-filtering
|
|
181
|
+
info = Skip;
|
|
182
|
+
} else {
|
|
183
|
+
// generate data cube index table
|
|
184
|
+
const filter = selection.remove(source).predicate(client);
|
|
185
|
+
info = dataCubeInfo(client.query(filter), active, indexCols, schema);
|
|
186
|
+
info.result = mc.exec([
|
|
187
|
+
`CREATE SCHEMA IF NOT EXISTS ${schema}`,
|
|
188
|
+
create(info.table, info.create, { temp: false })
|
|
189
|
+
]);
|
|
190
|
+
info.result.catch(e => mc.logger().error(e));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
indexes.set(client, info);
|
|
194
|
+
return info;
|
|
149
195
|
}
|
|
150
196
|
}
|
|
151
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Determines the active data cube dimension columns to select over. Returns
|
|
200
|
+
* an object with the clause source, column definitions, and a predicate
|
|
201
|
+
* generator function for the active dimensions of a data cube index table. If
|
|
202
|
+
* the active clause is not indexable or is missing metadata, this method
|
|
203
|
+
* returns an object with a null source property.
|
|
204
|
+
* @param {import('./util/selection-types.js').SelectionClause} clause The
|
|
205
|
+
* active selection clause to analyze.
|
|
206
|
+
*/
|
|
152
207
|
function activeColumns(clause) {
|
|
153
208
|
const { source, meta } = clause;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const { type, scales, bin, pixelSize = 1 } = meta;
|
|
209
|
+
const clausePred = clause.predicate;
|
|
210
|
+
const clauseCols = clausePred?.columns;
|
|
157
211
|
let predicate;
|
|
212
|
+
let columns;
|
|
158
213
|
|
|
159
|
-
if (
|
|
214
|
+
if (!meta || !clauseCols) {
|
|
215
|
+
return { source: null, columns, predicate };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// @ts-ignore
|
|
219
|
+
const { type, scales, bin, pixelSize = 1 } = meta;
|
|
220
|
+
|
|
221
|
+
if (type === 'point') {
|
|
222
|
+
predicate = x => x;
|
|
223
|
+
columns = Object.fromEntries(
|
|
224
|
+
clauseCols.map(col => [`${col}`, asColumn(col)])
|
|
225
|
+
);
|
|
226
|
+
} else if (type === 'interval' && scales) {
|
|
160
227
|
// determine pixel-level binning
|
|
161
228
|
const bins = scales.map(s => binInterval(s, pixelSize, bin));
|
|
162
229
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (bins.length === 1) {
|
|
230
|
+
if (bins.some(b => !b)) {
|
|
231
|
+
// bail if a scale type is unsupported
|
|
232
|
+
} else if (bins.length === 1) {
|
|
167
233
|
// single interval selection
|
|
168
234
|
predicate = p => p ? isBetween('active0', p.range.map(bins[0])) : [];
|
|
169
|
-
|
|
235
|
+
// @ts-ignore
|
|
236
|
+
columns = { active0: bins[0](clausePred.field) };
|
|
170
237
|
} else {
|
|
171
238
|
// multiple interval selection
|
|
172
239
|
predicate = p => p
|
|
173
|
-
? and(p.children.map(
|
|
240
|
+
? and(p.children.map(
|
|
241
|
+
({ range }, i) => isBetween(`active${i}`, range.map(bins[i]))
|
|
242
|
+
))
|
|
174
243
|
: [];
|
|
175
244
|
columns = Object.fromEntries(
|
|
176
|
-
|
|
245
|
+
// @ts-ignore
|
|
246
|
+
clausePred.children.map((p, i) => [`active${i}`, bins[i](p.field)])
|
|
177
247
|
);
|
|
178
248
|
}
|
|
179
|
-
} else if (type === 'point') {
|
|
180
|
-
predicate = x => x;
|
|
181
|
-
columns = Object.fromEntries(columns.map(col => [`${col}`, asColumn(col)]));
|
|
182
|
-
} else {
|
|
183
|
-
// unsupported selection type
|
|
184
|
-
return null;
|
|
185
249
|
}
|
|
186
250
|
|
|
187
|
-
return { source, columns, predicate };
|
|
251
|
+
return { source: columns ? source : null, columns, predicate };
|
|
188
252
|
}
|
|
189
253
|
|
|
190
254
|
const BIN = { ceil: 'CEIL', round: 'ROUND' };
|
|
191
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Returns a bin function generator to discretize a selection interval domain.
|
|
258
|
+
* @param {import('./util/selection-types.js').Scale} scale A scale that maps
|
|
259
|
+
* domain values to the output range (typically pixels).
|
|
260
|
+
* @param {number} pixelSize The interactive pixel size. This value indicates
|
|
261
|
+
* the bin step size and may be greater than an actual screen pixel.
|
|
262
|
+
* @param {import('./util/selection-types.js').BinMethod} bin The binning
|
|
263
|
+
* method to apply, one of `floor`, `ceil', or `round`.
|
|
264
|
+
* @returns {(value: any) => import('@uwdata/mosaic-sql').SQLExpression}
|
|
265
|
+
* A bin function generator.
|
|
266
|
+
*/
|
|
192
267
|
function binInterval(scale, pixelSize, bin) {
|
|
193
268
|
const { type, domain, range, apply, sqlApply } = scaleTransform(scale);
|
|
194
269
|
if (!apply) return; // unsupported scale type
|
|
@@ -201,6 +276,51 @@ function binInterval(scale, pixelSize, bin) {
|
|
|
201
276
|
return value => sql`${fn}(${s}(${sqlApply(value)}${d}))::INTEGER`;
|
|
202
277
|
}
|
|
203
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Generate data cube table query information.
|
|
281
|
+
* @param {Query} clientQuery The original client query.
|
|
282
|
+
* @param {*} active Active (selected) column definitions.
|
|
283
|
+
* @param {*} indexCols Data cube index column definitions.
|
|
284
|
+
* @returns {DataCubeInfo}
|
|
285
|
+
*/
|
|
286
|
+
function dataCubeInfo(clientQuery, active, indexCols, schema) {
|
|
287
|
+
const { dims, aggr, aux } = indexCols;
|
|
288
|
+
const { columns } = active;
|
|
289
|
+
|
|
290
|
+
// build index table construction query
|
|
291
|
+
const query = clientQuery
|
|
292
|
+
.select({ ...columns, ...aux })
|
|
293
|
+
.groupby(Object.keys(columns));
|
|
294
|
+
|
|
295
|
+
// ensure active clause columns are selected by subqueries
|
|
296
|
+
const [subq] = query.subqueries;
|
|
297
|
+
if (subq) {
|
|
298
|
+
const cols = Object.values(columns).flatMap(c => c.columns);
|
|
299
|
+
subqueryPushdown(subq, cols);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// push orderby criteria to later cube queries
|
|
303
|
+
const order = query.orderby();
|
|
304
|
+
query.query.orderby = [];
|
|
305
|
+
|
|
306
|
+
// generate creation query string and hash id
|
|
307
|
+
const create = query.toString();
|
|
308
|
+
const id = (fnv_hash(create) >>> 0).toString(16);
|
|
309
|
+
const table = `${schema}.cube_${id}`;
|
|
310
|
+
|
|
311
|
+
// generate data cube select query
|
|
312
|
+
const select = Query
|
|
313
|
+
.select(dims, aggr)
|
|
314
|
+
.from(table)
|
|
315
|
+
.groupby(dims)
|
|
316
|
+
.orderby(order);
|
|
317
|
+
|
|
318
|
+
return new DataCubeInfo({ id, table, create, active, select });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Push column selections down to subqueries.
|
|
323
|
+
*/
|
|
204
324
|
function subqueryPushdown(query, cols) {
|
|
205
325
|
const memo = new Set;
|
|
206
326
|
const pushdown = q => {
|
|
@@ -213,3 +333,46 @@ function subqueryPushdown(query, cols) {
|
|
|
213
333
|
};
|
|
214
334
|
pushdown(query);
|
|
215
335
|
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Metadata and query generator for a data cube index table. This
|
|
339
|
+
* object provides the information needed to generate and query
|
|
340
|
+
* a data cube index table for a client-selection pair relative to
|
|
341
|
+
* a specific active clause and selection state.
|
|
342
|
+
*/
|
|
343
|
+
export class DataCubeInfo {
|
|
344
|
+
/**
|
|
345
|
+
* Create a new DataCubeInfo instance.
|
|
346
|
+
* @param {object} options
|
|
347
|
+
*/
|
|
348
|
+
constructor({ table, create, active, select } = {}) {
|
|
349
|
+
/** The name of the data cube index table. */
|
|
350
|
+
this.table = table;
|
|
351
|
+
/** The SQL query used to generate the data cube index table. */
|
|
352
|
+
this.create = create;
|
|
353
|
+
/** A result promise returned for the data cube creation query. */
|
|
354
|
+
this.result = null;
|
|
355
|
+
/**
|
|
356
|
+
* Definitions and predicate function for the active columns,
|
|
357
|
+
* which are dynamically filtered by the active clause.
|
|
358
|
+
*/
|
|
359
|
+
this.active = active;
|
|
360
|
+
/** Select query (sans where clause) for data cube tables. */
|
|
361
|
+
this.select = select;
|
|
362
|
+
/**
|
|
363
|
+
* Boolean flag indicating a client that should be skipped.
|
|
364
|
+
* This value is always false for completed data cube info.
|
|
365
|
+
*/
|
|
366
|
+
this.skip = false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Generate a data cube index table query for the given predicate.
|
|
371
|
+
* @param {import('@uwdata/mosaic-sql').SQLExpression} predicate The current
|
|
372
|
+
* active clause predicate.
|
|
373
|
+
* @returns {Query} A data cube index table query.
|
|
374
|
+
*/
|
|
375
|
+
query(predicate) {
|
|
376
|
+
return this.select.clone().where(this.active.predicate(predicate));
|
|
377
|
+
}
|
|
378
|
+
}
|
package/src/MosaicClient.js
CHANGED
|
@@ -94,8 +94,8 @@ export class MosaicClient {
|
|
|
94
94
|
* @param {*} error
|
|
95
95
|
* @returns {this}
|
|
96
96
|
*/
|
|
97
|
-
queryError(error) {
|
|
98
|
-
|
|
97
|
+
queryError(error) { // eslint-disable-line no-unused-vars
|
|
98
|
+
// do nothing, the coordinator logs the error
|
|
99
99
|
return this;
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -103,6 +103,7 @@ export class MosaicClient {
|
|
|
103
103
|
* Request the coordinator to execute a query for this client.
|
|
104
104
|
* If an explicit query is not provided, the client query method will
|
|
105
105
|
* be called, filtered by the current filterBy selection.
|
|
106
|
+
* @returns {Promise}
|
|
106
107
|
*/
|
|
107
108
|
requestQuery(query) {
|
|
108
109
|
const q = query || this.query(this.filterBy?.predicate(this));
|
|
@@ -119,10 +120,18 @@ export class MosaicClient {
|
|
|
119
120
|
this._requestUpdate();
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Reset this client, initiating new field info and query requests.
|
|
125
|
+
* @returns {Promise}
|
|
126
|
+
*/
|
|
127
|
+
initialize() {
|
|
128
|
+
return this._coordinator.initializeClient(this);
|
|
129
|
+
}
|
|
130
|
+
|
|
122
131
|
/**
|
|
123
132
|
* Requests a client update.
|
|
124
133
|
* For example to (re-)render an interface component.
|
|
125
|
-
*
|
|
134
|
+
*
|
|
126
135
|
* @returns {this | Promise<any>}
|
|
127
136
|
*/
|
|
128
137
|
update() {
|
package/src/QueryConsolidator.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Query, Ref, isDescribeQuery } from '@uwdata/mosaic-sql';
|
|
2
|
-
import {
|
|
2
|
+
import { QueryResult } from './util/query-result.js';
|
|
3
3
|
|
|
4
4
|
function wait(callback) {
|
|
5
5
|
const method = typeof requestAnimationFrame !== 'undefined'
|
|
@@ -108,6 +108,12 @@ function consolidationKey(query, cache) {
|
|
|
108
108
|
// @ts-ignore
|
|
109
109
|
q.$groupby(groupby.map(e => (e instanceof Ref && map[e.column]) || e));
|
|
110
110
|
}
|
|
111
|
+
// @ts-ignore
|
|
112
|
+
else if (query.select().some(({ expr }) => expr.aggregate)) {
|
|
113
|
+
// if query is an ungrouped aggregate, add an explicit groupby to
|
|
114
|
+
// prevent improper consolidation with non-aggregate queries
|
|
115
|
+
q.$groupby('ALL');
|
|
116
|
+
}
|
|
111
117
|
|
|
112
118
|
// key is just the transformed query as SQL
|
|
113
119
|
return `${q}`;
|
|
@@ -133,7 +139,7 @@ function consolidate(group, enqueue, record) {
|
|
|
133
139
|
record: false,
|
|
134
140
|
query: (group.query = consolidatedQuery(group, record))
|
|
135
141
|
},
|
|
136
|
-
result: (group.result =
|
|
142
|
+
result: (group.result = new QueryResult())
|
|
137
143
|
});
|
|
138
144
|
} else {
|
|
139
145
|
// issue queries directly
|
|
@@ -244,22 +250,20 @@ async function processResults(group, cache) {
|
|
|
244
250
|
|
|
245
251
|
/**
|
|
246
252
|
* Project a consolidated result to a client result
|
|
247
|
-
* @param {
|
|
248
|
-
*
|
|
253
|
+
* @param {import('@uwdata/flechette').Table} data
|
|
254
|
+
* Consolidated query result, as an Arrow Table
|
|
255
|
+
* @param {[string, string][]} map Column name map as [source, target] pairs
|
|
249
256
|
* @returns the projected Apache Arrow table
|
|
250
257
|
*/
|
|
251
258
|
function projectResult(data, map) {
|
|
252
|
-
|
|
253
|
-
for (const [name, as] of map) {
|
|
254
|
-
cols[as] = data.getChild(name);
|
|
255
|
-
}
|
|
256
|
-
return new data.constructor(cols);
|
|
259
|
+
return data.select(map.map(x => x[0]), map.map(x => x[1]));
|
|
257
260
|
}
|
|
258
261
|
|
|
259
262
|
/**
|
|
260
263
|
* Filter a consolidated describe query result to a client result
|
|
261
|
-
* @param {
|
|
262
|
-
*
|
|
264
|
+
* @param {import('@uwdata/flechette').Table} data
|
|
265
|
+
* Consolidated query result, as an Arrow Table
|
|
266
|
+
* @param {[string, string][]} map Column name map as [source, target] pairs
|
|
263
267
|
* @returns the filtered table data
|
|
264
268
|
*/
|
|
265
269
|
function filterResult(data, map) {
|
package/src/QueryManager.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { consolidator } from './QueryConsolidator.js';
|
|
2
2
|
import { lruCache, voidCache } from './util/cache.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { PriorityQueue } from './util/priority-queue.js';
|
|
4
|
+
import { QueryResult } from './util/query-result.js';
|
|
5
5
|
|
|
6
6
|
export const Priority = { High: 0, Normal: 1, Low: 2 };
|
|
7
7
|
|
|
8
8
|
export class QueryManager {
|
|
9
9
|
constructor() {
|
|
10
|
-
this.queue =
|
|
10
|
+
this.queue = new PriorityQueue(3);
|
|
11
11
|
this.db = null;
|
|
12
12
|
this.clientCache = null;
|
|
13
13
|
this._logger = null;
|
|
@@ -96,7 +96,7 @@ export class QueryManager {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
request(request, priority = Priority.Normal) {
|
|
99
|
-
const result =
|
|
99
|
+
const result = new QueryResult();
|
|
100
100
|
const entry = { request, result };
|
|
101
101
|
if (this._consolidate) {
|
|
102
102
|
this._consolidate.add(entry, priority);
|
|
@@ -108,7 +108,15 @@ export class QueryManager {
|
|
|
108
108
|
|
|
109
109
|
cancel(requests) {
|
|
110
110
|
const set = new Set(requests);
|
|
111
|
-
|
|
111
|
+
if (set.size) {
|
|
112
|
+
this.queue.remove(({ result }) => {
|
|
113
|
+
if (set.has(result)) {
|
|
114
|
+
result.reject('Canceled');
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
clear() {
|