@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.
- package/README.md +1 -1
- package/dist/mosaic-core.js +12960 -21458
- package/dist/mosaic-core.min.js +7 -16
- package/dist/types/Coordinator.d.ts +169 -0
- package/dist/types/MosaicClient.d.ts +94 -0
- package/dist/types/Param.d.ts +47 -0
- package/dist/types/QueryConsolidator.d.ts +9 -0
- package/dist/types/QueryManager.d.ts +64 -0
- package/dist/types/Selection.d.ts +224 -0
- package/dist/types/SelectionClause.d.ts +105 -0
- package/dist/types/connectors/rest.d.ts +17 -0
- package/dist/types/connectors/socket.d.ts +18 -0
- package/dist/types/connectors/wasm.d.ts +16 -0
- package/dist/types/index.d.ts +25 -0
- package/dist/types/preagg/PreAggregator.d.ts +178 -0
- package/dist/types/preagg/preagg-columns.d.ts +14 -0
- package/dist/types/preagg/sufficient-statistics.d.ts +13 -0
- package/dist/types/util/AsyncDispatch.d.ts +100 -0
- package/dist/types/util/cache.d.ts +13 -0
- package/dist/types/util/decode-ipc.d.ts +7 -0
- package/dist/types/util/distinct.d.ts +2 -0
- package/dist/types/util/field-info.d.ts +13 -0
- package/dist/types/util/hash.d.ts +1 -0
- package/dist/types/util/is-arrow-table.d.ts +8 -0
- package/dist/types/util/js-type.d.ts +1 -0
- package/dist/types/util/priority-queue.d.ts +37 -0
- package/dist/types/util/query-result.d.ts +44 -0
- package/dist/types/util/selection-types.d.ts +114 -0
- package/dist/types/util/synchronizer.d.ts +29 -0
- package/dist/types/util/throttle.d.ts +11 -0
- package/dist/types/util/to-data-columns.d.ts +29 -0
- package/dist/types/util/void-logger.d.ts +7 -0
- package/jsconfig.json +11 -0
- package/package.json +10 -8
- package/src/Coordinator.js +66 -41
- package/src/MosaicClient.js +14 -4
- package/src/QueryConsolidator.js +32 -39
- package/src/QueryManager.js +85 -48
- package/src/Selection.js +49 -15
- package/src/SelectionClause.js +19 -22
- package/src/connectors/rest.js +6 -4
- package/src/connectors/socket.js +7 -4
- package/src/connectors/wasm.js +20 -4
- package/src/index.js +16 -8
- package/src/preagg/PreAggregator.js +407 -0
- package/src/preagg/preagg-columns.js +103 -0
- package/src/preagg/sufficient-statistics.js +439 -0
- package/src/util/decode-ipc.js +11 -0
- package/src/util/field-info.js +19 -16
- package/src/util/hash.js +1 -1
- package/src/util/is-arrow-table.js +10 -0
- package/src/util/priority-queue.js +75 -76
- package/src/util/query-result.js +44 -2
- package/src/util/selection-types.ts +3 -3
- package/src/util/throttle.js +21 -9
- package/src/util/to-data-columns.js +4 -15
- package/src/util/void-logger.js +6 -5
- package/tsconfig.json +11 -0
- package/src/DataCubeIndexer.js +0 -320
- package/src/util/convert-arrow.js +0 -145
- package/src/util/index-columns.js +0 -540
package/src/QueryConsolidator.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DescribeQuery, isAggregateExpression, isColumnRef, isDescribeQuery, isSelectQuery, Query } from '@uwdata/mosaic-sql';
|
|
2
2
|
import { QueryResult } from './util/query-result.js';
|
|
3
3
|
|
|
4
4
|
function wait(callback) {
|
|
@@ -13,10 +13,9 @@ function wait(callback) {
|
|
|
13
13
|
* Create a consolidator to combine structurally compatible queries.
|
|
14
14
|
* @param {*} enqueue Query manager enqueue method
|
|
15
15
|
* @param {*} cache Client-side query cache (sql -> data)
|
|
16
|
-
* @param {*} record Query recorder function
|
|
17
16
|
* @returns A consolidator object
|
|
18
17
|
*/
|
|
19
|
-
export function consolidator(enqueue, cache
|
|
18
|
+
export function consolidator(enqueue, cache) {
|
|
20
19
|
let pending = [];
|
|
21
20
|
let id = 0;
|
|
22
21
|
|
|
@@ -28,7 +27,7 @@ export function consolidator(enqueue, cache, record) {
|
|
|
28
27
|
|
|
29
28
|
// build and issue consolidated queries
|
|
30
29
|
for (const group of groups) {
|
|
31
|
-
consolidate(group, enqueue
|
|
30
|
+
consolidate(group, enqueue);
|
|
32
31
|
processResults(group, cache);
|
|
33
32
|
}
|
|
34
33
|
}
|
|
@@ -75,18 +74,16 @@ function entryGroups(entries, cache) {
|
|
|
75
74
|
* Queries with matching keys are conosolidation-compatible.
|
|
76
75
|
* If a query is found in the cache, it is exempted from consolidation,
|
|
77
76
|
* which is indicated by returning the precise query SQL as the key.
|
|
78
|
-
* @param {
|
|
77
|
+
* @param {Query | DescribeQuery} query The input query.
|
|
79
78
|
* @param {*} cache The query cache (sql -> data).
|
|
80
79
|
* @returns a key string
|
|
81
80
|
*/
|
|
82
81
|
function consolidationKey(query, cache) {
|
|
83
82
|
const sql = `${query}`;
|
|
84
|
-
if (query
|
|
83
|
+
if (isSelectQuery(query) && !cache.get(sql)) {
|
|
85
84
|
if (
|
|
86
|
-
|
|
87
|
-
query.
|
|
88
|
-
// @ts-ignore
|
|
89
|
-
query.qualify().length || query.having().length
|
|
85
|
+
query._orderby.length || query._where.length ||
|
|
86
|
+
query._qualify.length || query._having.length
|
|
90
87
|
) {
|
|
91
88
|
// do not try to analyze if query includes clauses
|
|
92
89
|
// that may refer to *derived* columns we can't resolve
|
|
@@ -94,19 +91,21 @@ function consolidationKey(query, cache) {
|
|
|
94
91
|
}
|
|
95
92
|
|
|
96
93
|
// create a derived query stripped of selections
|
|
97
|
-
const q = query.clone()
|
|
94
|
+
const q = query.clone().setSelect('*');
|
|
98
95
|
|
|
99
96
|
// check group by criteria for compatibility
|
|
100
97
|
// queries may refer to *derived* columns as group by criteria
|
|
101
98
|
// we resolve these against the true grouping expressions
|
|
102
|
-
const groupby = query.
|
|
103
|
-
// @ts-ignore
|
|
99
|
+
const groupby = query._groupby;
|
|
104
100
|
if (groupby.length) {
|
|
105
|
-
const map = {}; // expression map (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
const map = {}; // expression map (alias -> expr)
|
|
102
|
+
query._select.forEach(({ alias, expr }) => map[alias] = expr);
|
|
103
|
+
q.setGroupby(groupby.map(e => (isColumnRef(e) && map[e.column]) || e));
|
|
104
|
+
}
|
|
105
|
+
else if (query._select.some(e => isAggregateExpression(e.expr))) {
|
|
106
|
+
// if query is an ungrouped aggregate, add an explicit groupby to
|
|
107
|
+
// prevent improper consolidation with non-aggregate queries
|
|
108
|
+
q.setGroupby('ALL');
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
// key is just the transformed query as SQL
|
|
@@ -121,17 +120,15 @@ function consolidationKey(query, cache) {
|
|
|
121
120
|
* Issue queries, consolidating where possible.
|
|
122
121
|
* @param {*} group Array of bundled query entries
|
|
123
122
|
* @param {*} enqueue Add entry to query queue
|
|
124
|
-
* @param {*} record Query recorder function
|
|
125
123
|
*/
|
|
126
|
-
function consolidate(group, enqueue
|
|
124
|
+
function consolidate(group, enqueue) {
|
|
127
125
|
if (shouldConsolidate(group)) {
|
|
128
126
|
// issue a single consolidated query
|
|
129
127
|
enqueue({
|
|
130
128
|
request: {
|
|
131
129
|
type: 'arrow',
|
|
132
130
|
cache: false,
|
|
133
|
-
|
|
134
|
-
query: (group.query = consolidatedQuery(group, record))
|
|
131
|
+
query: (group.query = consolidatedQuery(group))
|
|
135
132
|
},
|
|
136
133
|
result: (group.result = new QueryResult())
|
|
137
134
|
});
|
|
@@ -164,10 +161,9 @@ function shouldConsolidate(group) {
|
|
|
164
161
|
/**
|
|
165
162
|
* Create a consolidated query for a group.
|
|
166
163
|
* @param {*} group Array of bundled query entries
|
|
167
|
-
* @param {*} record Query recorder function
|
|
168
164
|
* @returns A consolidated Query instance
|
|
169
165
|
*/
|
|
170
|
-
function consolidatedQuery(group
|
|
166
|
+
function consolidatedQuery(group) {
|
|
171
167
|
const maps = group.maps = [];
|
|
172
168
|
const fields = new Map;
|
|
173
169
|
|
|
@@ -176,30 +172,29 @@ function consolidatedQuery(group, record) {
|
|
|
176
172
|
const { query } = item.entry.request;
|
|
177
173
|
const fieldMap = [];
|
|
178
174
|
maps.push(fieldMap);
|
|
179
|
-
for (const {
|
|
175
|
+
for (const { alias, expr } of query._select) {
|
|
180
176
|
const e = `${expr}`;
|
|
181
177
|
if (!fields.has(e)) {
|
|
182
178
|
fields.set(e, [`col${fields.size}`, expr]);
|
|
183
179
|
}
|
|
184
180
|
const [name] = fields.get(e);
|
|
185
|
-
fieldMap.push([name,
|
|
181
|
+
fieldMap.push([name, alias]);
|
|
186
182
|
}
|
|
187
|
-
record(`${query}`);
|
|
188
183
|
}
|
|
189
184
|
|
|
190
185
|
// use a cloned query as a starting point
|
|
191
186
|
const query = group[0].entry.request.query.clone();
|
|
192
187
|
|
|
193
188
|
// update group by statement as needed
|
|
194
|
-
const groupby = query.
|
|
189
|
+
const groupby = query._groupby;
|
|
195
190
|
if (groupby.length) {
|
|
196
191
|
const map = {};
|
|
197
192
|
group.maps[0].forEach(([name, as]) => map[as] = name);
|
|
198
|
-
query
|
|
193
|
+
query.setGroupby(groupby.map(e => (isColumnRef(e) && map[e.column]) || e));
|
|
199
194
|
}
|
|
200
195
|
|
|
201
196
|
// update select statement and return
|
|
202
|
-
return query
|
|
197
|
+
return query.setSelect(Array.from(fields.values()));
|
|
203
198
|
}
|
|
204
199
|
|
|
205
200
|
/**
|
|
@@ -244,22 +239,20 @@ async function processResults(group, cache) {
|
|
|
244
239
|
|
|
245
240
|
/**
|
|
246
241
|
* Project a consolidated result to a client result
|
|
247
|
-
* @param {
|
|
248
|
-
*
|
|
242
|
+
* @param {import('@uwdata/flechette').Table} data
|
|
243
|
+
* Consolidated query result, as an Arrow Table
|
|
244
|
+
* @param {[string, string][]} map Column name map as [source, target] pairs
|
|
249
245
|
* @returns the projected Apache Arrow table
|
|
250
246
|
*/
|
|
251
247
|
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);
|
|
248
|
+
return data.select(map.map(x => x[0]), map.map(x => x[1]));
|
|
257
249
|
}
|
|
258
250
|
|
|
259
251
|
/**
|
|
260
252
|
* Filter a consolidated describe query result to a client result
|
|
261
|
-
* @param {
|
|
262
|
-
*
|
|
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
|
|
263
256
|
* @returns the filtered table data
|
|
264
257
|
*/
|
|
265
258
|
function filterResult(data, map) {
|
package/src/QueryManager.js
CHANGED
|
@@ -1,56 +1,85 @@
|
|
|
1
1
|
import { consolidator } from './QueryConsolidator.js';
|
|
2
2
|
import { lruCache, voidCache } from './util/cache.js';
|
|
3
|
-
import {
|
|
4
|
-
import { QueryResult } from './util/query-result.js';
|
|
3
|
+
import { PriorityQueue } from './util/priority-queue.js';
|
|
4
|
+
import { QueryResult, QueryState } from './util/query-result.js';
|
|
5
|
+
import { voidLogger } from './util/void-logger.js';
|
|
5
6
|
|
|
6
|
-
export const Priority = { High: 0, Normal: 1, Low: 2 };
|
|
7
|
+
export const Priority = Object.freeze({ High: 0, Normal: 1, Low: 2 });
|
|
7
8
|
|
|
8
9
|
export class QueryManager {
|
|
9
|
-
constructor(
|
|
10
|
-
|
|
10
|
+
constructor(
|
|
11
|
+
maxConcurrentRequests = 32
|
|
12
|
+
) {
|
|
13
|
+
this.queue = new PriorityQueue(3);
|
|
11
14
|
this.db = null;
|
|
12
15
|
this.clientCache = null;
|
|
13
|
-
this._logger =
|
|
16
|
+
this._logger = voidLogger();
|
|
14
17
|
this._logQueries = false;
|
|
15
|
-
this.recorders = [];
|
|
16
|
-
this.pending = null;
|
|
17
18
|
this._consolidate = null;
|
|
19
|
+
/**
|
|
20
|
+
* Requests pending with the query manager.
|
|
21
|
+
*
|
|
22
|
+
* @type {QueryResult[]}
|
|
23
|
+
*/
|
|
24
|
+
this.pendingResults = [];
|
|
25
|
+
this.maxConcurrentRequests = maxConcurrentRequests;
|
|
26
|
+
this.pendingExec = false;
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
next() {
|
|
21
|
-
if (this.
|
|
30
|
+
if (this.queue.isEmpty() || this.pendingResults.length > this.maxConcurrentRequests || this.pendingExec) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
const { request, result } = this.queue.next();
|
|
23
|
-
|
|
24
|
-
this.
|
|
35
|
+
|
|
36
|
+
this.pendingResults.push(result);
|
|
37
|
+
if (request.type === 'exec') this.pendingExec = true;
|
|
38
|
+
|
|
39
|
+
this.submit(request, result).finally(() => {
|
|
40
|
+
// return from the queue all requests that are ready
|
|
41
|
+
while (this.pendingResults.length && this.pendingResults[0].state !== QueryState.pending) {
|
|
42
|
+
const result = this.pendingResults.shift();
|
|
43
|
+
if (result.state === QueryState.ready) {
|
|
44
|
+
result.fulfill();
|
|
45
|
+
} else if (result.state === QueryState.done) {
|
|
46
|
+
this._logger.warn('Found resolved query in pending results.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (request.type === 'exec') this.pendingExec = false;
|
|
50
|
+
this.next();
|
|
51
|
+
});
|
|
25
52
|
}
|
|
26
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Add an entry to the query queue with a priority.
|
|
56
|
+
* @param {object} entry The entry to add.
|
|
57
|
+
* @param {*} [entry.request] The query request.
|
|
58
|
+
* @param {QueryResult} [entry.result] The query result.
|
|
59
|
+
* @param {number} priority The query priority, defaults to `Priority.Normal`.
|
|
60
|
+
*/
|
|
27
61
|
enqueue(entry, priority = Priority.Normal) {
|
|
28
62
|
this.queue.insert(entry, priority);
|
|
29
63
|
this.next();
|
|
30
64
|
}
|
|
31
65
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Submit the query to the connector.
|
|
68
|
+
* @param {*} request The request.
|
|
69
|
+
* @param {QueryResult} result The query result.
|
|
70
|
+
*/
|
|
38
71
|
async submit(request, result) {
|
|
39
72
|
try {
|
|
40
|
-
const { query, type, cache = false,
|
|
73
|
+
const { query, type, cache = false, options } = request;
|
|
41
74
|
const sql = query ? `${query}` : null;
|
|
42
75
|
|
|
43
|
-
// update recorders
|
|
44
|
-
if (record) {
|
|
45
|
-
this.recordQuery(sql);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
76
|
// check query cache
|
|
49
77
|
if (cache) {
|
|
50
78
|
const cached = this.clientCache.get(sql);
|
|
51
79
|
if (cached) {
|
|
80
|
+
const data = await cached;
|
|
52
81
|
this._logger.debug('Cache');
|
|
53
|
-
result.
|
|
82
|
+
result.ready(data);
|
|
54
83
|
return;
|
|
55
84
|
}
|
|
56
85
|
}
|
|
@@ -60,10 +89,16 @@ export class QueryManager {
|
|
|
60
89
|
if (this._logQueries) {
|
|
61
90
|
this._logger.debug('Query', { type, sql, ...options });
|
|
62
91
|
}
|
|
63
|
-
|
|
92
|
+
|
|
93
|
+
const promise = this.db.query({ type, sql, ...options });
|
|
94
|
+
if (cache) this.clientCache.set(sql, promise);
|
|
95
|
+
|
|
96
|
+
const data = await promise;
|
|
97
|
+
|
|
64
98
|
if (cache) this.clientCache.set(sql, data);
|
|
99
|
+
|
|
65
100
|
this._logger.debug(`Request: ${(performance.now() - t0).toFixed(1)}`);
|
|
66
|
-
result.
|
|
101
|
+
result.ready(type === 'exec' ? null : data);
|
|
67
102
|
} catch (err) {
|
|
68
103
|
result.reject(err);
|
|
69
104
|
}
|
|
@@ -89,12 +124,18 @@ export class QueryManager {
|
|
|
89
124
|
|
|
90
125
|
consolidate(flag) {
|
|
91
126
|
if (flag && !this._consolidate) {
|
|
92
|
-
this._consolidate = consolidator(this.enqueue.bind(this), this.clientCache
|
|
127
|
+
this._consolidate = consolidator(this.enqueue.bind(this), this.clientCache);
|
|
93
128
|
} else if (!flag && this._consolidate) {
|
|
94
129
|
this._consolidate = null;
|
|
95
130
|
}
|
|
96
131
|
}
|
|
97
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Request a query result.
|
|
135
|
+
* @param {*} request The request.
|
|
136
|
+
* @param {number} priority The query priority, defaults to `Priority.Normal`.
|
|
137
|
+
* @returns {QueryResult} A query result promise.
|
|
138
|
+
*/
|
|
98
139
|
request(request, priority = Priority.Normal) {
|
|
99
140
|
const result = new QueryResult();
|
|
100
141
|
const entry = { request, result };
|
|
@@ -109,7 +150,19 @@ export class QueryManager {
|
|
|
109
150
|
cancel(requests) {
|
|
110
151
|
const set = new Set(requests);
|
|
111
152
|
if (set.size) {
|
|
112
|
-
this.queue.remove(({ result }) =>
|
|
153
|
+
this.queue.remove(({ result }) => {
|
|
154
|
+
if (set.has(result)) {
|
|
155
|
+
result.reject('Canceled');
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
for (const result of this.pendingResults) {
|
|
162
|
+
if (set.has(result)) {
|
|
163
|
+
result.reject('Canceled');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
113
166
|
}
|
|
114
167
|
}
|
|
115
168
|
|
|
@@ -118,26 +171,10 @@ export class QueryManager {
|
|
|
118
171
|
result.reject('Cleared');
|
|
119
172
|
return true;
|
|
120
173
|
});
|
|
121
|
-
}
|
|
122
174
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
state.push(query);
|
|
128
|
-
},
|
|
129
|
-
reset() {
|
|
130
|
-
state = [];
|
|
131
|
-
},
|
|
132
|
-
snapshot() {
|
|
133
|
-
return state.slice();
|
|
134
|
-
},
|
|
135
|
-
stop() {
|
|
136
|
-
this.recorders = this.recorders.filter(x => x !== recorder);
|
|
137
|
-
return state;
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
this.recorders.push(recorder);
|
|
141
|
-
return recorder;
|
|
175
|
+
for (const result of this.pendingResults) {
|
|
176
|
+
result.reject('Cleared');
|
|
177
|
+
}
|
|
178
|
+
this.pendingResults = [];
|
|
142
179
|
}
|
|
143
180
|
}
|
package/src/Selection.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { or } from '@uwdata/mosaic-sql';
|
|
1
|
+
import { literal, or } from '@uwdata/mosaic-sql';
|
|
2
2
|
import { Param } from './Param.js';
|
|
3
|
+
import { MosaicClient } from './MosaicClient.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Test if a value is a Selection instance.
|
|
@@ -10,6 +11,13 @@ export function isSelection(x) {
|
|
|
10
11
|
return x instanceof Selection;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
function create(options, include) {
|
|
15
|
+
return new Selection(
|
|
16
|
+
new SelectionResolver(options),
|
|
17
|
+
include ? [include].flat() : include
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
/**
|
|
14
22
|
* Represents a dynamic set of query filter predicates.
|
|
15
23
|
*/
|
|
@@ -25,10 +33,13 @@ export class Selection extends Param {
|
|
|
25
33
|
* @param {boolean} [options.empty=false] Boolean flag indicating if a lack
|
|
26
34
|
* of clauses should correspond to an empty selection with no records. This
|
|
27
35
|
* setting determines the default selection state.
|
|
36
|
+
* @param {Selection|Selection[]} [options.include] Upstream selections whose
|
|
37
|
+
* clauses should be included as part of the new selection. Any clauses
|
|
38
|
+
* published to upstream selections will be relayed to the new selection.
|
|
28
39
|
* @returns {Selection} The new Selection instance.
|
|
29
40
|
*/
|
|
30
|
-
static intersect({ cross = false, empty = false } = {}) {
|
|
31
|
-
return
|
|
41
|
+
static intersect({ cross = false, empty = false, include = [] } = {}) {
|
|
42
|
+
return create({ cross, empty }, include);
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
/**
|
|
@@ -41,10 +52,13 @@ export class Selection extends Param {
|
|
|
41
52
|
* @param {boolean} [options.empty=false] Boolean flag indicating if a lack
|
|
42
53
|
* of clauses should correspond to an empty selection with no records. This
|
|
43
54
|
* setting determines the default selection state.
|
|
55
|
+
* @param {Selection|Selection[]} [options.include] Upstream selections whose
|
|
56
|
+
* clauses should be included as part of the new selection. Any clauses
|
|
57
|
+
* published to upstream selections will be relayed to the new selection.
|
|
44
58
|
* @returns {Selection} The new Selection instance.
|
|
45
59
|
*/
|
|
46
|
-
static union({ cross = false, empty = false } = {}) {
|
|
47
|
-
return
|
|
60
|
+
static union({ cross = false, empty = false, include = [] } = {}) {
|
|
61
|
+
return create({ cross, empty, union: true }, include);
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
/**
|
|
@@ -57,10 +71,13 @@ export class Selection extends Param {
|
|
|
57
71
|
* @param {boolean} [options.empty=false] Boolean flag indicating if a lack
|
|
58
72
|
* of clauses should correspond to an empty selection with no records. This
|
|
59
73
|
* setting determines the default selection state.
|
|
74
|
+
* @param {Selection|Selection[]} [options.include] Upstream selections whose
|
|
75
|
+
* clauses should be included as part of the new selection. Any clauses
|
|
76
|
+
* published to upstream selections will be relayed to the new selection.
|
|
60
77
|
* @returns {Selection} The new Selection instance.
|
|
61
78
|
*/
|
|
62
|
-
static single({ cross = false, empty = false } = {}) {
|
|
63
|
-
return
|
|
79
|
+
static single({ cross = false, empty = false, include = [] } = {}) {
|
|
80
|
+
return create({ cross, empty, single: true }, include);
|
|
64
81
|
}
|
|
65
82
|
|
|
66
83
|
/**
|
|
@@ -70,21 +87,34 @@ export class Selection extends Param {
|
|
|
70
87
|
* @param {boolean} [options.empty=false] Boolean flag indicating if a lack
|
|
71
88
|
* of clauses should correspond to an empty selection with no records. This
|
|
72
89
|
* setting determines the default selection state.
|
|
90
|
+
* @param {Selection|Selection[]} [options.include] Upstream selections whose
|
|
91
|
+
* clauses should be included as part of the new selection. Any clauses
|
|
92
|
+
* published to upstream selections will be relayed to the new selection.
|
|
73
93
|
* @returns {Selection} The new Selection instance.
|
|
74
94
|
*/
|
|
75
|
-
static crossfilter({ empty = false } = {}) {
|
|
76
|
-
return
|
|
95
|
+
static crossfilter({ empty = false, include = [] } = {}) {
|
|
96
|
+
return create({ cross: true, empty }, include);
|
|
77
97
|
}
|
|
78
98
|
|
|
79
99
|
/**
|
|
80
100
|
* Create a new Selection instance.
|
|
81
|
-
* @param {SelectionResolver} resolver The selection resolution
|
|
101
|
+
* @param {SelectionResolver} [resolver] The selection resolution
|
|
82
102
|
* strategy to apply.
|
|
103
|
+
* @param {Selection[]} [include] Upstream selections whose clauses
|
|
104
|
+
* should be included as part of this selection. Any clauses published
|
|
105
|
+
* to these upstream selections will be relayed to this selection.
|
|
83
106
|
*/
|
|
84
|
-
constructor(resolver = new SelectionResolver()) {
|
|
107
|
+
constructor(resolver = new SelectionResolver(), include = []) {
|
|
85
108
|
super([]);
|
|
86
109
|
this._resolved = this._value;
|
|
87
110
|
this._resolver = resolver;
|
|
111
|
+
/** @type {Set<Selection>} */
|
|
112
|
+
this._relay = new Set;
|
|
113
|
+
if (Array.isArray(include)) {
|
|
114
|
+
for (const sel of include) {
|
|
115
|
+
sel._relay.add(this);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
88
118
|
}
|
|
89
119
|
|
|
90
120
|
/**
|
|
@@ -161,6 +191,7 @@ export class Selection extends Param {
|
|
|
161
191
|
*/
|
|
162
192
|
activate(clause) {
|
|
163
193
|
this.emit('activate', clause);
|
|
194
|
+
this._relay.forEach(sel => sel.activate(clause));
|
|
164
195
|
}
|
|
165
196
|
|
|
166
197
|
/**
|
|
@@ -173,6 +204,7 @@ export class Selection extends Param {
|
|
|
173
204
|
// this ensures consistent clause state across unemitted event values
|
|
174
205
|
this._resolved = this._resolver.resolve(this._resolved, clause, true);
|
|
175
206
|
this._resolved.active = clause;
|
|
207
|
+
this._relay.forEach(sel => sel.update(clause));
|
|
176
208
|
return super.update(this._resolved);
|
|
177
209
|
}
|
|
178
210
|
|
|
@@ -284,9 +316,11 @@ export class SelectionResolver {
|
|
|
284
316
|
|
|
285
317
|
/**
|
|
286
318
|
* Return a selection query predicate for the given client.
|
|
287
|
-
* @param {
|
|
288
|
-
*
|
|
289
|
-
* @param {
|
|
319
|
+
* @param {import('./util/selection-types.js').SelectionClause[]} clauseList
|
|
320
|
+
* An array of selection clauses.
|
|
321
|
+
* @param {import('./util/selection-types.js').SelectionClause} active
|
|
322
|
+
* The current active selection clause.
|
|
323
|
+
* @param {MosaicClient} client The client whose data may be filtered.
|
|
290
324
|
* @returns {*} The query predicate for filtering client data,
|
|
291
325
|
* based on the current state of this selection.
|
|
292
326
|
*/
|
|
@@ -294,7 +328,7 @@ export class SelectionResolver {
|
|
|
294
328
|
const { empty, union } = this;
|
|
295
329
|
|
|
296
330
|
if (empty && !clauseList.length) {
|
|
297
|
-
return [
|
|
331
|
+
return [literal(false)];
|
|
298
332
|
}
|
|
299
333
|
|
|
300
334
|
// do nothing if cross-filtering and client is currently active
|
package/src/SelectionClause.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
SQLExpression, and, contains, isBetween, isNotDistinct, literal,
|
|
3
|
-
or, prefix, regexp_matches, suffix
|
|
4
|
-
} from '@uwdata/mosaic-sql';
|
|
1
|
+
import { ExprNode, and, contains, isBetween, isIn, isNotDistinct, literal, or, prefix, regexp_matches, suffix } from '@uwdata/mosaic-sql';
|
|
5
2
|
import { MosaicClient } from './MosaicClient.js';
|
|
6
3
|
|
|
7
4
|
/**
|
|
@@ -10,12 +7,11 @@ import { MosaicClient } from './MosaicClient.js';
|
|
|
10
7
|
* @typedef {import('./util/selection-types.js').Extent} Extent
|
|
11
8
|
* @typedef {import('./util/selection-types.js').MatchMethod} MatchMethod
|
|
12
9
|
* @typedef {import('./util/selection-types.js').BinMethod} BinMethod
|
|
13
|
-
* @typedef {SQLExpression | string} Field
|
|
14
10
|
*/
|
|
15
11
|
|
|
16
12
|
/**
|
|
17
13
|
* Generate a selection clause for a single selected point value.
|
|
18
|
-
* @param {
|
|
14
|
+
* @param {import('@uwdata/mosaic-sql').ExprValue} field The table column or expression to select.
|
|
19
15
|
* @param {*} value The selected value.
|
|
20
16
|
* @param {object} options Additional clause properties.
|
|
21
17
|
* @param {*} options.source The source component generating this clause.
|
|
@@ -28,7 +24,7 @@ export function clausePoint(field, value, {
|
|
|
28
24
|
source,
|
|
29
25
|
clients = source ? new Set([source]) : undefined
|
|
30
26
|
}) {
|
|
31
|
-
/** @type {
|
|
27
|
+
/** @type {ExprNode | null} */
|
|
32
28
|
const predicate = value !== undefined
|
|
33
29
|
? isNotDistinct(field, literal(value))
|
|
34
30
|
: null;
|
|
@@ -43,9 +39,9 @@ export function clausePoint(field, value, {
|
|
|
43
39
|
|
|
44
40
|
/**
|
|
45
41
|
* Generate a selection clause for multiple selected point values.
|
|
46
|
-
* @param {
|
|
47
|
-
* @param {any[][]} value The selected values, as an array of
|
|
48
|
-
*
|
|
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
|
|
44
|
+
* arrays. Each subarray contains values for each *fields* entry.
|
|
49
45
|
* @param {object} options Additional clause properties.
|
|
50
46
|
* @param {*} options.source The source component generating this clause.
|
|
51
47
|
* @param {Set<MosaicClient>} [options.clients] The Mosaic clients associated
|
|
@@ -57,14 +53,15 @@ export function clausePoints(fields, value, {
|
|
|
57
53
|
source,
|
|
58
54
|
clients = source ? new Set([source]) : undefined
|
|
59
55
|
}) {
|
|
60
|
-
/** @type {
|
|
56
|
+
/** @type {ExprNode | null} */
|
|
61
57
|
let predicate = null;
|
|
62
58
|
if (value) {
|
|
63
|
-
const clauses = value.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
const clauses = value.length && fields.length === 1
|
|
60
|
+
? [isIn(fields[0], value.map(v => literal(v[0])))]
|
|
61
|
+
: value.map(v => and(v.map((_, i) => isNotDistinct(fields[i], literal(_)))));
|
|
62
|
+
predicate = value.length === 0 ? literal(false)
|
|
63
|
+
: clauses.length > 1 ? or(clauses)
|
|
64
|
+
: clauses[0];
|
|
68
65
|
}
|
|
69
66
|
return {
|
|
70
67
|
meta: { type: 'point' },
|
|
@@ -77,7 +74,7 @@ export function clausePoints(fields, value, {
|
|
|
77
74
|
|
|
78
75
|
/**
|
|
79
76
|
* Generate a selection clause for a selected 1D interval.
|
|
80
|
-
* @param {
|
|
77
|
+
* @param {import('@uwdata/mosaic-sql').ExprValue} field The table column or expression to select.
|
|
81
78
|
* @param {Extent} value The selected interval as a [lo, hi] array.
|
|
82
79
|
* @param {object} options Additional clause properties.
|
|
83
80
|
* @param {*} options.source The source component generating this clause.
|
|
@@ -96,7 +93,7 @@ export function clauseInterval(field, value, {
|
|
|
96
93
|
scale,
|
|
97
94
|
pixelSize = 1
|
|
98
95
|
}) {
|
|
99
|
-
/** @type {
|
|
96
|
+
/** @type {ExprNode | null} */
|
|
100
97
|
const predicate = value != null ? isBetween(field, value) : null;
|
|
101
98
|
/** @type {import('./util/selection-types.js').IntervalMetadata} */
|
|
102
99
|
const meta = { type: 'interval', scales: scale && [scale], bin, pixelSize };
|
|
@@ -105,7 +102,7 @@ export function clauseInterval(field, value, {
|
|
|
105
102
|
|
|
106
103
|
/**
|
|
107
104
|
* Generate a selection clause for multiple selected intervals.
|
|
108
|
-
* @param {
|
|
105
|
+
* @param {import('@uwdata/mosaic-sql').ExprValue[]} fields The table columns or expressions to select.
|
|
109
106
|
* @param {Extent[]} value The selected intervals, as an array of extents.
|
|
110
107
|
* @param {object} options Additional clause properties.
|
|
111
108
|
* @param {*} options.source The source component generating this clause.
|
|
@@ -125,7 +122,7 @@ export function clauseIntervals(fields, value, {
|
|
|
125
122
|
scales = [],
|
|
126
123
|
pixelSize = 1
|
|
127
124
|
}) {
|
|
128
|
-
/** @type {
|
|
125
|
+
/** @type {ExprNode | null} */
|
|
129
126
|
const predicate = value != null
|
|
130
127
|
? and(fields.map((f, i) => isBetween(f, value[i])))
|
|
131
128
|
: null;
|
|
@@ -138,7 +135,7 @@ const MATCH_METHODS = { contains, prefix, suffix, regexp: regexp_matches };
|
|
|
138
135
|
|
|
139
136
|
/**
|
|
140
137
|
* Generate a selection clause for text search matching.
|
|
141
|
-
* @param {
|
|
138
|
+
* @param {import('@uwdata/mosaic-sql').ExprValue} field The table column or expression to select.
|
|
142
139
|
* @param {string} value The selected text search query string.
|
|
143
140
|
* @param {object} options Additional clause properties.
|
|
144
141
|
* @param {*} options.source The source component generating this clause.
|
|
@@ -153,7 +150,7 @@ export function clauseMatch(field, value, {
|
|
|
153
150
|
source, clients = undefined, method = 'contains'
|
|
154
151
|
}) {
|
|
155
152
|
let fn = MATCH_METHODS[method];
|
|
156
|
-
/** @type {
|
|
153
|
+
/** @type {ExprNode | null} */
|
|
157
154
|
const predicate = value ? fn(field, literal(value)) : null;
|
|
158
155
|
/** @type {import('./util/selection-types.js').MatchMetadata} */
|
|
159
156
|
const meta = { type: 'match', method };
|
package/src/connectors/rest.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
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
|
|
9
|
-
* @param {string} query.sql A SQL query string.
|
|
8
|
+
* @param {'exec' | 'arrow' | 'json' | 'create-bundle' | 'load-bundle'} [query.type] The query type.
|
|
9
|
+
* @param {string} [query.sql] A SQL query string.
|
|
10
|
+
* @param {string[]} [query.queries] The queries used to create a bundle.
|
|
11
|
+
* @param {string} [query.name] The name of a bundle to create or load.
|
|
10
12
|
* @returns the query result
|
|
11
13
|
*/
|
|
12
14
|
async query(query) {
|
|
@@ -20,7 +22,7 @@ export function restConnector(uri = 'http://localhost:3000/') {
|
|
|
20
22
|
});
|
|
21
23
|
|
|
22
24
|
return query.type === 'exec' ? req
|
|
23
|
-
: query.type === 'arrow' ?
|
|
25
|
+
: query.type === 'arrow' ? decodeIPC(await (await req).arrayBuffer())
|
|
24
26
|
: (await req).json();
|
|
25
27
|
}
|
|
26
28
|
};
|