@fjader/svelte 0.2.3 → 0.2.5
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 +3 -1
- package/dist/animation/EntranceGate.d.ts +52 -0
- package/dist/animation/EntranceGate.js +93 -0
- package/dist/animation/TransitionClock.d.ts +61 -30
- package/dist/animation/TransitionClock.js +67 -29
- package/dist/animation/transition-log.d.ts +41 -0
- package/dist/animation/transition-log.js +57 -0
- package/dist/components/axes/AxisBottom.svelte +27 -8
- package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
- package/dist/components/axes/AxisCenter.svelte +4 -2
- package/dist/components/axes/AxisLeft.svelte +81 -10
- package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
- package/dist/components/axes/BandAxisTicks.svelte +4 -3
- package/dist/components/axes/axis-title.d.ts +30 -0
- package/dist/components/axes/axis-title.js +77 -0
- package/dist/components/charts/ChartCell.svelte +33 -6
- package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
- package/dist/components/charts/ChartContainer.svelte +55 -5
- package/dist/components/charts/ChartContainer.svelte.d.ts +14 -1
- package/dist/components/charts/ChartHover.svelte.d.ts +14 -0
- package/dist/components/charts/ChartHover.svelte.js +22 -0
- package/dist/components/charts/ChartVisibility.svelte.d.ts +43 -0
- package/dist/components/charts/ChartVisibility.svelte.js +67 -0
- package/dist/components/guides/Grid.svelte +12 -1
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +7 -1
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
- package/dist/components/marks/AreaMarkX.svelte +18 -4
- package/dist/components/marks/AreaMarkY.svelte +18 -4
- package/dist/components/marks/BarMarkX.svelte +20 -4
- package/dist/components/marks/BarMarkY.svelte +20 -4
- package/dist/components/marks/DotsMark.svelte +8 -3
- package/dist/components/marks/LineMark.svelte +13 -6
- package/dist/components/marks/ParallelCoordinatesMark.svelte +11 -2
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +4 -0
- package/dist/components/marks/RectMark.svelte +8 -3
- package/dist/components/marks/RectXMark.svelte +9 -3
- package/dist/components/marks/RectYMark.svelte +9 -3
- package/dist/components/marks/RuleMarkX.svelte +31 -25
- package/dist/components/marks/RuleMarkY.svelte +31 -25
- package/dist/components/marks/TextMark.svelte +52 -14
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +22 -1
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +579 -66
- package/dist/components/marks/lifecycle/domain-queries.d.ts +13 -2
- package/dist/components/marks/lifecycle/domain-queries.js +43 -11
- package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
- package/dist/components/marks/lifecycle/domain-registration.js +17 -20
- package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +112 -0
- package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
- package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
- package/dist/components/marks/lifecycle/other-split.js +112 -0
- package/dist/components/marks/lifecycle/query-builder.d.ts +53 -3
- package/dist/components/marks/lifecycle/query-builder.js +46 -23
- package/dist/components/marks/utils/MarkColor.svelte.d.ts +3 -1
- package/dist/components/marks/utils/MarkColor.svelte.js +33 -3
- package/dist/components/marks/utils/MarkSeries.svelte.d.ts +14 -1
- package/dist/components/marks/utils/MarkSeries.svelte.js +25 -1
- package/dist/components/marks/utils/MarkSize.svelte.js +4 -0
- package/dist/components/marks/utils/reactive-config.d.ts +1 -1
- package/dist/components/marks/utils/reactive-config.js +9 -0
- package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
- package/dist/components/marks/utils/tooltip-helpers.js +2 -1
- package/dist/components/rendering/Areas.svelte +57 -19
- package/dist/components/rendering/Areas.svelte.d.ts +12 -0
- package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
- package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
- package/dist/components/rendering/Dots.svelte +23 -3
- package/dist/components/rendering/Dots.svelte.d.ts +6 -0
- package/dist/components/rendering/GridLine.svelte +13 -1
- package/dist/components/rendering/GridLine.svelte.d.ts +5 -0
- package/dist/components/rendering/Lines.svelte +48 -13
- package/dist/components/rendering/Lines.svelte.d.ts +12 -0
- package/dist/components/rendering/ParallelLines.svelte +5 -5
- package/dist/components/rendering/Rects.svelte +164 -30
- package/dist/components/rendering/Rects.svelte.d.ts +23 -2
- package/dist/components/rendering/RuleX.svelte +1 -1
- package/dist/components/rendering/RuleY.svelte +1 -1
- package/dist/components/rendering/WebGPUCanvas.svelte +4 -2
- package/dist/components/rendering/activity-reads.d.ts +47 -12
- package/dist/components/rendering/activity-reads.js +61 -19
- package/dist/components/rendering/uniform-sizes.d.ts +1 -1
- package/dist/components/rendering/uniform-sizes.js +1 -1
- package/dist/config/ChartConfig.svelte.d.ts +10 -1
- package/dist/config/ChartConfig.svelte.js +17 -0
- package/dist/gpu/GPUCoordinator.svelte.d.ts +31 -1
- package/dist/gpu/GPUCoordinator.svelte.js +164 -7
- package/dist/gpu/MorphedChannels.d.ts +11 -28
- package/dist/gpu/MorphedChannels.js +72 -8
- package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
- package/dist/gpu/ScaleTemplateSystem.js +83 -6
- package/dist/index.d.ts +19 -4
- package/dist/index.js +22 -4
- package/dist/interactors/Interval1D.svelte +6 -1
- package/dist/interactors/Interval2D.svelte +6 -1
- package/dist/interactors/ParallelAxisSource.svelte +10 -1
- package/dist/interactors/Toggle.svelte +32 -3
- package/dist/interactors/Tooltip.svelte +138 -41
- package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
- package/dist/interactors/TooltipManager.svelte.js +46 -12
- package/dist/interactors/other-tooltip.d.ts +16 -0
- package/dist/interactors/other-tooltip.js +22 -0
- package/dist/query-engine/CrossfilterSelection.svelte.d.ts +4 -2
- package/dist/query-engine/CrossfilterSelection.svelte.js +2 -0
- package/dist/query-engine/FieldActivity.svelte.d.ts +49 -1
- package/dist/query-engine/FieldActivity.svelte.js +175 -5
- package/dist/query-engine/FilterGroup.svelte.d.ts +23 -1
- package/dist/query-engine/FilterGroup.svelte.js +43 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +31 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.js +62 -15
- package/dist/query-engine/MarkHandle.svelte.d.ts +28 -1
- package/dist/query-engine/MarkHandle.svelte.js +33 -1
- package/dist/query-engine/QueryEngine.d.ts +225 -5
- package/dist/query-engine/QueryEngine.js +697 -47
- package/dist/query-engine/categorical-identity.d.ts +41 -0
- package/dist/query-engine/categorical-identity.js +145 -65
- package/dist/query-engine/debug/query-log.d.ts +39 -0
- package/dist/query-engine/debug/query-log.js +90 -0
- package/dist/query-engine/duckdb.d.ts +55 -8
- package/dist/query-engine/duckdb.js +82 -30
- package/dist/query-engine/index.d.ts +7 -4
- package/dist/query-engine/index.js +3 -3
- package/dist/query-engine/preagg/BuildQueue.d.ts +33 -13
- package/dist/query-engine/preagg/BuildQueue.js +51 -15
- package/dist/query-engine/preagg/Preagg.d.ts +46 -3
- package/dist/query-engine/preagg/Preagg.js +159 -31
- package/dist/query-engine/preagg/plan.d.ts +33 -1
- package/dist/query-engine/preagg/plan.js +158 -14
- package/dist/query-engine/sliced-lane.d.ts +108 -0
- package/dist/query-engine/sliced-lane.js +299 -0
- package/dist/query-engine/source-routing.d.ts +106 -0
- package/dist/query-engine/source-routing.js +189 -0
- package/dist/query-engine/toggle-predicate.d.ts +21 -0
- package/dist/query-engine/toggle-predicate.js +29 -1
- package/dist/scales/ActivityTable.d.ts +5 -1
- package/dist/scales/ActivityTable.js +12 -3
- package/dist/scales/BandScaleState.svelte.d.ts +22 -9
- package/dist/scales/BandScaleState.svelte.js +39 -21
- package/dist/scales/ChartScales.svelte.d.ts +3 -0
- package/dist/scales/ChartScales.svelte.js +26 -8
- package/dist/scales/ColorScaleState.svelte.d.ts +18 -2
- package/dist/scales/ColorScaleState.svelte.js +138 -26
- package/dist/scales/LinearScaleState.svelte.d.ts +7 -10
- package/dist/scales/LinearScaleState.svelte.js +21 -39
- package/dist/scales/ScaleGeneration.svelte.d.ts +48 -0
- package/dist/scales/ScaleGeneration.svelte.js +42 -0
- package/dist/scales/ScaleRegistry.svelte.js +3 -1
- package/dist/scales/SlotTransition.d.ts +15 -1
- package/dist/scales/SlotTransition.js +33 -4
- package/dist/scales/TimeScaleState.svelte.d.ts +7 -3
- package/dist/scales/TimeScaleState.svelte.js +7 -7
- package/dist/scales/color-utils.d.ts +0 -3
- package/dist/scales/color-utils.js +0 -3
- package/dist/scales/entered.d.ts +8 -0
- package/dist/scales/entered.js +10 -0
- package/dist/spec/LazyQueryEngine.js +2 -19
- package/dist/spec/MarkRegistry.d.ts +3 -1
- package/dist/spec/MarkRegistry.js +13 -1
- package/dist/spec/PlotFromSpec.svelte +1 -0
- package/dist/spec/export.js +11 -2
- package/dist/spec/types.d.ts +43 -13
- package/dist/sql/bin.d.ts +25 -0
- package/dist/sql/bin.js +108 -5
- package/dist/theme.css +5 -4
- package/dist/transforms/TransformCompiler.d.ts +47 -2
- package/dist/transforms/TransformCompiler.js +70 -1
- package/dist/transforms/index.js +176 -42
- package/dist/types.d.ts +177 -2
- package/package.json +4 -3
|
@@ -51,23 +51,46 @@ export class Preagg {
|
|
|
51
51
|
constructor(host, buildMaxWaitMs) {
|
|
52
52
|
this.#host = host;
|
|
53
53
|
this.#builds = new BuildQueue({
|
|
54
|
-
exec: (sql) => host.exec(sql),
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
exec: (sql, signal) => host.exec(sql, signal),
|
|
55
|
+
// Builds wait for interaction only on a shared connection. On a lane
|
|
56
|
+
// they run at once, and interaction still comes first on its own:
|
|
57
|
+
// each slice of a build is a poll, a message at the back of the
|
|
58
|
+
// worker's queue, so mark queries already waiting there are served
|
|
59
|
+
// before the next slice. One query waits at most a slice (~5 ms)
|
|
60
|
+
// behind a build, not the whole build: measured on 2M rows, an
|
|
61
|
+
// update's first queries 3-9 ms during a build, 52-57 ms on one
|
|
62
|
+
// connection. A burst of queries still stretches: the build takes a
|
|
63
|
+
// slice between two of them, and its polls need main-thread turns
|
|
64
|
+
// while results decode (+~65 ms on that burst, still under the one
|
|
65
|
+
// connection's 195 ms median); the build is the one that waits.
|
|
66
|
+
busy: () => !host.sliced() && host.busy(),
|
|
67
|
+
unwanted: (item) => this.#unwanted(item),
|
|
68
|
+
started: (item) => this.#emitFor('started', item),
|
|
69
|
+
dropped: (item, reason) => {
|
|
70
|
+
// Before the bookkeeping below: the detail reads it.
|
|
71
|
+
if (this.#observing()) {
|
|
72
|
+
this.#emitFor('dropped', item, { reason, detail: this.#dropDetail(item, reason) });
|
|
73
|
+
}
|
|
74
|
+
const { markId, sourceId, build } = item;
|
|
58
75
|
const own = this.#marks.get(markId);
|
|
59
76
|
if (own?.building.get(sourceId) === build)
|
|
60
77
|
own.building.delete(sourceId);
|
|
61
78
|
},
|
|
62
|
-
built: (
|
|
79
|
+
built: (item) => {
|
|
80
|
+
this.#emitFor('built', item);
|
|
81
|
+
const { markId, sourceId, build } = item;
|
|
63
82
|
const own = this.#marks.get(markId);
|
|
64
83
|
if (own?.building.get(sourceId) !== build)
|
|
65
84
|
return;
|
|
66
85
|
own.building.delete(sourceId);
|
|
67
86
|
own.tables.set(sourceId, build);
|
|
68
87
|
},
|
|
69
|
-
failed: (
|
|
70
|
-
|
|
88
|
+
failed: (item, e) => {
|
|
89
|
+
this.#emitFor('failed', item, { error: e });
|
|
90
|
+
const { markId, sourceId, build } = item;
|
|
91
|
+
if (!(e instanceof DOMException && e.name === 'AbortError')) {
|
|
92
|
+
console.warn(`[QueryEngine] preagg failed for ${markId}:`, e);
|
|
93
|
+
}
|
|
71
94
|
const own = this.#marks.get(markId);
|
|
72
95
|
if (own?.building.get(sourceId) === build)
|
|
73
96
|
own.building.delete(sourceId);
|
|
@@ -82,6 +105,8 @@ export class Preagg {
|
|
|
82
105
|
* tables — hash-named tables are reused via CREATE TABLE IF NOT EXISTS. */
|
|
83
106
|
dropMark(markId) {
|
|
84
107
|
this.#marks.delete(markId);
|
|
108
|
+
// Its running build is no longer wanted: cancelled, it rolls back.
|
|
109
|
+
this.#builds.abortRunning((item) => item.markId === markId);
|
|
85
110
|
}
|
|
86
111
|
/**
|
|
87
112
|
* Pre-aggregate marks for a filter group before a source is used.
|
|
@@ -110,6 +135,10 @@ export class Preagg {
|
|
|
110
135
|
for (const [markId, reg] of this.#host.marks()) {
|
|
111
136
|
if (reg.filterGroup !== filterGroup)
|
|
112
137
|
continue;
|
|
138
|
+
// A mark off screen is not re-queried, so a table for it would only
|
|
139
|
+
// take the connection from the ones that are.
|
|
140
|
+
if (!reg.handle.active)
|
|
141
|
+
continue;
|
|
113
142
|
// A mark the source never filters gets no table for it, and keeps
|
|
114
143
|
// the tables it holds for other sources.
|
|
115
144
|
const route = reg.descriptor.route ?? NO_ROUTE;
|
|
@@ -132,10 +161,25 @@ export class Preagg {
|
|
|
132
161
|
own.tables.delete(activeSourceId);
|
|
133
162
|
own.building.delete(activeSourceId);
|
|
134
163
|
const plan = planPreagg(reg.descriptor, dims, this.#host.identity);
|
|
135
|
-
if (!plan)
|
|
164
|
+
if (!plan) {
|
|
165
|
+
if (this.#observing()) {
|
|
166
|
+
this.#emit({ kind: 'skipped', markId, sourceId: activeSourceId, reason: 'unplannable' });
|
|
167
|
+
}
|
|
136
168
|
continue;
|
|
137
|
-
|
|
169
|
+
}
|
|
170
|
+
const size = this.#size(reg, plan.activeKeys);
|
|
171
|
+
if (size.groups != null && size.groups > size.cap) {
|
|
172
|
+
if (this.#observing()) {
|
|
173
|
+
this.#emit({
|
|
174
|
+
kind: 'skipped',
|
|
175
|
+
markId,
|
|
176
|
+
sourceId: activeSourceId,
|
|
177
|
+
reason: 'too large',
|
|
178
|
+
detail: { ...size, markKeys: size.markKeys, activeKeys: plan.activeKeys }
|
|
179
|
+
});
|
|
180
|
+
}
|
|
138
181
|
continue;
|
|
182
|
+
}
|
|
139
183
|
// Bake every other clause into the preagg table. Sorted by source ID,
|
|
140
184
|
// so the same filters always give the same SQL, and so the same table.
|
|
141
185
|
const bakedClauses = selection.bakedClauses(route, activeSourceId);
|
|
@@ -167,9 +211,19 @@ export class Preagg {
|
|
|
167
211
|
// Most recently planned last, for the cap on unused tables.
|
|
168
212
|
this.#tables.delete(preaggTable);
|
|
169
213
|
this.#tables.add(preaggTable);
|
|
214
|
+
if (this.#observing()) {
|
|
215
|
+
const event = { markId, sourceId: activeSourceId, table: preaggTable };
|
|
216
|
+
this.#emit({ kind: 'planned', ...event, detail: { groups: size.groups, cap: size.cap } });
|
|
217
|
+
this.#emit({ kind: 'queued', ...event, batch: this.#builds.batch });
|
|
218
|
+
}
|
|
170
219
|
promises.push(this.#builds.push({ markId, sourceId: activeSourceId, build, createSQL }));
|
|
171
220
|
}
|
|
172
|
-
|
|
221
|
+
// From a microtask, not now: activation runs in the pointer-enter handler,
|
|
222
|
+
// which first marks the chart hovered, and the effect that writes the
|
|
223
|
+
// hovered marks' tooltip tables (what the first hover waits for) flushes
|
|
224
|
+
// in an earlier microtask. That write is then in flight, `busy()` holds
|
|
225
|
+
// the build back, and the tooltip does not wait behind it.
|
|
226
|
+
queueMicrotask(() => this.#builds.pump());
|
|
173
227
|
this.#evictUnused();
|
|
174
228
|
await Promise.all(promises);
|
|
175
229
|
}
|
|
@@ -233,6 +287,11 @@ export class Preagg {
|
|
|
233
287
|
* A data reload: drop every queued build, every mark's tables and the tables
|
|
234
288
|
* themselves (CREATE TABLE IF NOT EXISTS would otherwise keep serving
|
|
235
289
|
* pre-reload aggregates), and the row counts.
|
|
290
|
+
*
|
|
291
|
+
* The running build is cancelled too (`clear`). On a lane it runs beside the
|
|
292
|
+
* reload, reading the old rows through its snapshot; should it finish anyway
|
|
293
|
+
* (the cancel lost the race), `built` finds its mark gone from `#marks` and
|
|
294
|
+
* does not register it, and the DROP below follows it on the same lane.
|
|
236
295
|
*/
|
|
237
296
|
reset() {
|
|
238
297
|
this.#builds.clear();
|
|
@@ -246,8 +305,8 @@ export class Preagg {
|
|
|
246
305
|
/**
|
|
247
306
|
* The engine is going: settle waiting activations and drop this session's
|
|
248
307
|
* tables. Resolves once the drops have run; call it while the engine can
|
|
249
|
-
* still exec. A build already running is
|
|
250
|
-
* connection, so its table is dropped too.
|
|
308
|
+
* still exec. A build already running is cancelled, and is ahead of the
|
|
309
|
+
* drops on its connection either way, so its table is dropped too.
|
|
251
310
|
*/
|
|
252
311
|
async destroy() {
|
|
253
312
|
this.#destroyed = true;
|
|
@@ -298,23 +357,93 @@ export class Preagg {
|
|
|
298
357
|
return own;
|
|
299
358
|
}
|
|
300
359
|
/**
|
|
301
|
-
*
|
|
302
|
-
* against the mark's current registration, which a
|
|
303
|
-
* against the view now (a zoom in or a long pan while it
|
|
304
|
-
* table settling would only rebuild), and against the
|
|
305
|
-
* baked into the table may have changed.
|
|
360
|
+
* Why a queued build is no longer wanted when it would be sent, or null if
|
|
361
|
+
* it still is: checked against the mark's current registration, which a
|
|
362
|
+
* re-setup replaces, against the view now (a zoom in or a long pan while it
|
|
363
|
+
* waited means a table settling would only rebuild), and against the
|
|
364
|
+
* filters now: a clause baked into the table may have changed. The reason
|
|
365
|
+
* goes into the drop's build record.
|
|
306
366
|
*/
|
|
307
|
-
#
|
|
367
|
+
#unwanted({ markId, sourceId, build }) {
|
|
308
368
|
const reg = this.#host.mark(markId);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
369
|
+
if (!reg)
|
|
370
|
+
return 'unregistered';
|
|
371
|
+
// Gone off screen while the build waited.
|
|
372
|
+
if (!reg.handle.active)
|
|
373
|
+
return 'inactive';
|
|
374
|
+
if (this.#marks.get(markId)?.building.get(sourceId) !== build)
|
|
375
|
+
return 'superseded';
|
|
376
|
+
if (reg.descriptor.parts !== build.basis.parts)
|
|
377
|
+
return 'parts changed';
|
|
378
|
+
if (!this.#binsFitCurrentView(build))
|
|
379
|
+
return 'bins do not fit the view';
|
|
380
|
+
if (!bakedClausesHold(reg.descriptor.route ?? NO_ROUTE, reg.filterGroup.selection, build)) {
|
|
381
|
+
return 'baked clauses changed';
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
// --- Build records (observeBuilds) ---
|
|
386
|
+
#observing() {
|
|
387
|
+
return !!this.#host.observingBuilds?.();
|
|
388
|
+
}
|
|
389
|
+
#emit(event) {
|
|
390
|
+
this.#host.onBuild?.({ table: null, ...event, time: performance.now() });
|
|
391
|
+
}
|
|
392
|
+
#emitFor(kind, item, extra = {}) {
|
|
393
|
+
if (!this.#observing())
|
|
394
|
+
return;
|
|
395
|
+
this.#emit({
|
|
396
|
+
kind,
|
|
397
|
+
markId: item.markId,
|
|
398
|
+
sourceId: item.sourceId,
|
|
399
|
+
table: item.build.table,
|
|
400
|
+
batch: item.batch,
|
|
401
|
+
...extra
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* The values a failed check compared, for a drop's record. For the bin
|
|
406
|
+
* check, each bin dim's built window against the view it was checked
|
|
407
|
+
* against: the domain read now, through the source's view callback.
|
|
408
|
+
*/
|
|
409
|
+
#dropDetail(item, reason) {
|
|
410
|
+
const { build } = item;
|
|
411
|
+
if (reason === 'bins do not fit the view') {
|
|
412
|
+
const out = this.#viewScratch;
|
|
413
|
+
out.fill(NaN);
|
|
414
|
+
build.basis.view?.(out);
|
|
415
|
+
const dims = [];
|
|
416
|
+
let bin = 0;
|
|
417
|
+
for (const meta of build.activeMeta) {
|
|
418
|
+
if (meta.kind !== 'bin')
|
|
419
|
+
continue;
|
|
420
|
+
const i = 3 * bin++;
|
|
421
|
+
const width = meta.bins / meta.scaleFactor;
|
|
422
|
+
dims.push({
|
|
423
|
+
built: {
|
|
424
|
+
d0: meta.d0,
|
|
425
|
+
d1: meta.d0 + width,
|
|
426
|
+
bins: meta.bins,
|
|
427
|
+
scaleFactor: meta.scaleFactor
|
|
428
|
+
},
|
|
429
|
+
view: { v0: out[i], v1: out[i + 1], pixelExtent: out[i + 2] },
|
|
430
|
+
fits: binFitsView(meta, out[i], out[i + 1], out[i + 2]),
|
|
431
|
+
fineEnough: binsFineEnough(meta.scaleFactor, out[i + 1] - out[i], out[i + 2])
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return { dims };
|
|
435
|
+
}
|
|
436
|
+
if (reason === 'parts changed')
|
|
437
|
+
return { sameParts: false };
|
|
438
|
+
if (reason === 'baked clauses changed')
|
|
439
|
+
return { baked: build.basis.bakedClauses.length };
|
|
440
|
+
return undefined;
|
|
314
441
|
}
|
|
315
442
|
/**
|
|
316
|
-
* The size guard:
|
|
317
|
-
* its base table's rows (a fixed
|
|
443
|
+
* The size guard's numbers: the groups a planned table would hold, and the
|
|
444
|
+
* cap it must not exceed, a quarter of its base table's rows (a fixed
|
|
445
|
+
* 100,000 until the row count has been read). activate() skips a table
|
|
446
|
+
* over the cap.
|
|
318
447
|
*
|
|
319
448
|
* Measured on 1M rows (DuckDB-WASM, one thread): a toggle click on a table a
|
|
320
449
|
* quarter the size of the base table is still ~3.7× faster than on the base
|
|
@@ -323,15 +452,14 @@ export class Preagg {
|
|
|
323
452
|
* when the connection is idle, so their cost shows only as the wait of a
|
|
324
453
|
* click that lands mid-build, and as memory; past a quarter that outweighs
|
|
325
454
|
* the gain. The groups are estimated by estimateGroups; an unknown estimate
|
|
326
|
-
* builds.
|
|
455
|
+
* (null) builds.
|
|
327
456
|
*/
|
|
328
|
-
#
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
return false;
|
|
457
|
+
#size(reg, activeKeys) {
|
|
458
|
+
const keys = markKeys(reg.descriptor, reg.handle, this.#host.identity);
|
|
459
|
+
const groups = estimateGroups(keys, activeKeys);
|
|
332
460
|
const rows = this.#rowCounts.get(reg.descriptor.baseTable);
|
|
333
461
|
const cap = typeof rows === 'number' ? rows / 4 : 100_000;
|
|
334
|
-
return groups
|
|
462
|
+
return { groups, cap, markKeys: keys };
|
|
335
463
|
}
|
|
336
464
|
/** Read a base table's row count once, for the size guard. */
|
|
337
465
|
#countRows(table) {
|
|
@@ -70,6 +70,11 @@ export interface PreaggCreation {
|
|
|
70
70
|
select: Record<string, ExprNode>;
|
|
71
71
|
groupby: ExprNode[];
|
|
72
72
|
orderby: ExprNode[];
|
|
73
|
+
/**
|
|
74
|
+
* The mark's own row filters (`filter()`), applied to the source rows inside the
|
|
75
|
+
* build: the table groups them away, so the read could not apply them.
|
|
76
|
+
*/
|
|
77
|
+
where: ExprNode[];
|
|
73
78
|
/**
|
|
74
79
|
* Fields grouped by value inside and mapped to their ids on the grouped
|
|
75
80
|
* result: `raw` is the inner column, `alias` the id column, `dim` the dim
|
|
@@ -80,7 +85,22 @@ export interface PreaggCreation {
|
|
|
80
85
|
alias: string;
|
|
81
86
|
dim: string;
|
|
82
87
|
}>;
|
|
88
|
+
/**
|
|
89
|
+
* The mark's padding rows, held in the table so its reads need no padding
|
|
90
|
+
* (see TransformCompiler.padRows), or null. `rows` is their SELECT over
|
|
91
|
+
* every channel; the table takes its group channels (`dims`), 0 for every
|
|
92
|
+
* partial (`partials`: count and sum only, for which 0 adds nothing), and
|
|
93
|
+
* `__pad` true. Source rows whose bin is NULL are left out (in `where`):
|
|
94
|
+
* those bins are never drawn, and padding a base read drops them too.
|
|
95
|
+
*/
|
|
96
|
+
pad: {
|
|
97
|
+
rows: string;
|
|
98
|
+
dims: string[];
|
|
99
|
+
partials: string[];
|
|
100
|
+
} | null;
|
|
83
101
|
}
|
|
102
|
+
/** The column marking a preagg table's padding rows; real rows hold false. */
|
|
103
|
+
export declare const PAD_COLUMN = "__pad";
|
|
84
104
|
/**
|
|
85
105
|
* How each active dim appears in a preagg table. A `bin` dim is a pixel-bin
|
|
86
106
|
* index over a window (`buildBinExpr`): bins 0 .. bins - 1 from d0, with -1
|
|
@@ -107,6 +127,12 @@ export type ActiveMeta = {
|
|
|
107
127
|
export interface GroupKey {
|
|
108
128
|
column: string | null;
|
|
109
129
|
groups: number | null;
|
|
130
|
+
/**
|
|
131
|
+
* For a mark's key over several columns: the mark's own bins per binned
|
|
132
|
+
* source column (from its query parts, see binCounts), so a bin dim on one
|
|
133
|
+
* of them merges into the key instead of multiplying it.
|
|
134
|
+
*/
|
|
135
|
+
columns?: ReadonlyMap<string, number>;
|
|
110
136
|
}
|
|
111
137
|
export interface PreaggState {
|
|
112
138
|
table: string;
|
|
@@ -165,7 +191,13 @@ export declare function markKeys(descriptor: QueryDescriptor, handle: Pick<MarkH
|
|
|
165
191
|
* mark with x and fill on the same field has that field's groups, not their
|
|
166
192
|
* square, and a bin dim on a column the mark already groups by adds its bins
|
|
167
193
|
* to that key's (each pixel bin falls in about one of the mark's bins)
|
|
168
|
-
* instead of multiplying.
|
|
194
|
+
* instead of multiplying. For a mark key over several columns (a histogram
|
|
195
|
+
* coloured by a category) the same holds per value of the other columns: a
|
|
196
|
+
* bin dim on a column the mark bins into `d` bins makes each group of the
|
|
197
|
+
* rest hold about `d + bins` groups instead of `d`, so the key's groups grow
|
|
198
|
+
* by `1 + bins / d`. Only one bin dim merges into a mark key, on purpose: a
|
|
199
|
+
* 2D brush over two binned columns of the same mark multiplies the second,
|
|
200
|
+
* which overestimates (the guard then skips rather than builds too much).
|
|
169
201
|
*/
|
|
170
202
|
export declare function estimateGroups(markKeys: GroupKey[], activeKeys: GroupKey[]): number | null;
|
|
171
203
|
/**
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
* the query. Copyright (c) 2023-2025, UW Interactive Data Lab. BSD 3-Clause
|
|
12
12
|
* License; see NOTICE.
|
|
13
13
|
*/
|
|
14
|
-
import { isAggregateExpression, isNode, collectColumns, column, and, isBetween, literal, Query, sql as sqlTag } from '@uwdata/mosaic-sql';
|
|
15
|
-
import { togglePredicate } from '../toggle-predicate';
|
|
14
|
+
import { isAggregateExpression, isNode, collectColumns, column, and, isBetween, literal, Query, collectAggregates, sql as sqlTag } from '@uwdata/mosaic-sql';
|
|
15
|
+
import { BITMAP_MIN_IDS, idBitmapPredicate, togglePredicate } from '../toggle-predicate';
|
|
16
16
|
import { rewriteForPreagg, buildBinExpr, binsFineEnough, BIN_WINDOW_MARGIN } from '../sufficient-statistics';
|
|
17
17
|
import { assembleQuery, castNumeric, castReal } from '../../components/marks/lifecycle/query-builder';
|
|
18
|
+
/** The column marking a preagg table's padding rows; real rows hold false. */
|
|
19
|
+
export const PAD_COLUMN = '__pad';
|
|
18
20
|
/**
|
|
19
21
|
* The preagg table a mark would build for a source's active dims, or null
|
|
20
22
|
* when the mark cannot be pre-aggregated (no aggregates, or an aggregate that
|
|
@@ -37,6 +39,11 @@ export function planPreagg(descriptor, dims, identity) {
|
|
|
37
39
|
const { parts, table, baseTable, schema } = descriptor;
|
|
38
40
|
if (!parts.hasAggregates && !parts.hasWindowAggregates)
|
|
39
41
|
return null;
|
|
42
|
+
// A having() on a preagg table would count its groups, not the source rows, and
|
|
43
|
+
// return the wrong rows without an error. The built-in transforms add none; see
|
|
44
|
+
// TransformCompiler.hasHaving for why, and for what supporting it would take.
|
|
45
|
+
if (descriptor.transformCompiler.hasHaving())
|
|
46
|
+
return null;
|
|
40
47
|
// Analyze selectDef for preagg compatibility
|
|
41
48
|
const analysis = rewriteForPreagg(parts.selectDef, parts.passthroughChannels);
|
|
42
49
|
if (!analysis)
|
|
@@ -99,6 +106,7 @@ export function planPreagg(descriptor, dims, identity) {
|
|
|
99
106
|
activeMeta.push({ kind: 'value', field, column: alias, byId: false });
|
|
100
107
|
activeKeys.push({ column: null, groups: null });
|
|
101
108
|
});
|
|
109
|
+
const pad = analysis.windowChannels ? null : padPlan(descriptor, preaggCols);
|
|
102
110
|
// CAST-wrap dimension preaggCols (bake CAST into the preagg table).
|
|
103
111
|
// Partial-aggregate and active columns stay raw for recombination.
|
|
104
112
|
for (const [ch, expr] of Object.entries(preaggCols)) {
|
|
@@ -115,7 +123,9 @@ export function planPreagg(descriptor, dims, identity) {
|
|
|
115
123
|
groupby: [...preaggGroupBy, ...activeGroupBy],
|
|
116
124
|
// Sorted by bin columns for fast range scans
|
|
117
125
|
orderby: binOrder,
|
|
118
|
-
|
|
126
|
+
where: [...descriptor.transformCompiler.rowFilters(), ...(pad?.where ?? [])],
|
|
127
|
+
joins,
|
|
128
|
+
pad: pad?.pad ?? null
|
|
119
129
|
},
|
|
120
130
|
outputSelect,
|
|
121
131
|
activeMeta,
|
|
@@ -123,6 +133,46 @@ export function planPreagg(descriptor, dims, identity) {
|
|
|
123
133
|
activeKeys
|
|
124
134
|
};
|
|
125
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* The padding rows a mark's table can hold, or null when its reads must pad
|
|
138
|
+
* themselves: the mark pads nothing, a transform would see the rows differently
|
|
139
|
+
* (TransformCompiler.padRows), or a measure is not a count or a sum. A mean of
|
|
140
|
+
* an empty bin would read 0 / 0 from a padding row, where padding gives 0.
|
|
141
|
+
*/
|
|
142
|
+
function padPlan(descriptor, preaggCols) {
|
|
143
|
+
const { parts, transformCompiler } = descriptor;
|
|
144
|
+
for (const expr of Object.values(parts.selectDef)) {
|
|
145
|
+
if (!isNode(expr) || !isAggregateExpression(expr))
|
|
146
|
+
continue;
|
|
147
|
+
for (const agg of collectAggregates(expr)) {
|
|
148
|
+
const name = agg.name.toLowerCase();
|
|
149
|
+
if (agg.isDistinct || (name !== 'count' && name !== 'sum'))
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const rows = transformCompiler.padRows(Object.keys(parts.selectDef), {
|
|
154
|
+
groupKeys: parts.groupKeys,
|
|
155
|
+
bins: parts.bins ?? null
|
|
156
|
+
});
|
|
157
|
+
if (!rows)
|
|
158
|
+
return null;
|
|
159
|
+
const dims = [];
|
|
160
|
+
const partials = [];
|
|
161
|
+
for (const [col, expr] of Object.entries(preaggCols)) {
|
|
162
|
+
if (col.startsWith('__ps'))
|
|
163
|
+
partials.push(col);
|
|
164
|
+
else if (col.startsWith('__active') || col.startsWith('__raw'))
|
|
165
|
+
continue;
|
|
166
|
+
else if (!isAggregateExpression(expr))
|
|
167
|
+
dims.push(col);
|
|
168
|
+
}
|
|
169
|
+
// A filter on the source column, not on the bin: on the bin it would be
|
|
170
|
+
// pushed below the GROUP BY and compute every row's bin twice (measured:
|
|
171
|
+
// half again the build time), where a NULL test on the column is free.
|
|
172
|
+
const sources = new Set(dims.flatMap((col) => parts.bins?.[col]?.source ?? []));
|
|
173
|
+
const where = [...sources].map((source) => sqlTag([`(${source}) IS NOT NULL`]));
|
|
174
|
+
return { pad: { rows, dims, partials }, where };
|
|
175
|
+
}
|
|
126
176
|
/** The mark's channel that is `field`'s id channel (see idFieldOf), or null. */
|
|
127
177
|
function idChannelFor(selectDef, field, identity) {
|
|
128
178
|
for (const [channel, expr] of Object.entries(selectDef)) {
|
|
@@ -174,6 +224,14 @@ export function markKeys(descriptor, handle, identity) {
|
|
|
174
224
|
else
|
|
175
225
|
fields.add(field);
|
|
176
226
|
}
|
|
227
|
+
// A bin() channel resolves to SQL text that collectColumns cannot see into,
|
|
228
|
+
// so its column comes from the plan: the mark groups by it too.
|
|
229
|
+
const bins = binCounts(parts);
|
|
230
|
+
if (bins) {
|
|
231
|
+
allIds = false;
|
|
232
|
+
for (const name of bins.keys())
|
|
233
|
+
columns.add(name);
|
|
234
|
+
}
|
|
177
235
|
if (allIds) {
|
|
178
236
|
return [...fields].map((field) => ({
|
|
179
237
|
column: null,
|
|
@@ -181,7 +239,35 @@ export function markKeys(descriptor, handle, identity) {
|
|
|
181
239
|
}));
|
|
182
240
|
}
|
|
183
241
|
const rows = handle.params.limit == null ? handle.arrowTable?.numRows : undefined;
|
|
184
|
-
|
|
242
|
+
if (columns.size === 1)
|
|
243
|
+
return [{ column: [...columns][0], groups: rows ?? 1 }];
|
|
244
|
+
return [{ column: null, groups: rows ?? 1, columns: bins }];
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The mark's own bins per source column, from its query parts: each `bin()`
|
|
248
|
+
* channel records its binned source and how many bins it has (`parts.bins`,
|
|
249
|
+
* what `dense()` pads by). Read from the plan, never from the data. Only
|
|
250
|
+
* plain columns count; an expression's bins, or a mark without bin metadata,
|
|
251
|
+
* give no entry, and the estimate then multiplies as before.
|
|
252
|
+
*/
|
|
253
|
+
function binCounts(parts) {
|
|
254
|
+
if (!parts.bins)
|
|
255
|
+
return undefined;
|
|
256
|
+
const counts = new Map();
|
|
257
|
+
for (const padding of Object.values(parts.bins)) {
|
|
258
|
+
const name = plainColumn(padding.source);
|
|
259
|
+
if (name == null || !(padding.count > 0))
|
|
260
|
+
continue;
|
|
261
|
+
counts.set(name, Math.max(counts.get(name) ?? 0, padding.count));
|
|
262
|
+
}
|
|
263
|
+
return counts.size > 0 ? counts : undefined;
|
|
264
|
+
}
|
|
265
|
+
/** The column a bin's source SQL names, when it is a plain (optionally quoted) column. */
|
|
266
|
+
function plainColumn(source) {
|
|
267
|
+
const quoted = /^"((?:[^"]|"")+)"$/.exec(source);
|
|
268
|
+
if (quoted)
|
|
269
|
+
return quoted[1].replaceAll('""', '"');
|
|
270
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(source) ? source : null;
|
|
185
271
|
}
|
|
186
272
|
/**
|
|
187
273
|
* The groups a planned table would hold: the mark's own keys (markKeys) and
|
|
@@ -190,7 +276,13 @@ export function markKeys(descriptor, handle, identity) {
|
|
|
190
276
|
* mark with x and fill on the same field has that field's groups, not their
|
|
191
277
|
* square, and a bin dim on a column the mark already groups by adds its bins
|
|
192
278
|
* to that key's (each pixel bin falls in about one of the mark's bins)
|
|
193
|
-
* instead of multiplying.
|
|
279
|
+
* instead of multiplying. For a mark key over several columns (a histogram
|
|
280
|
+
* coloured by a category) the same holds per value of the other columns: a
|
|
281
|
+
* bin dim on a column the mark bins into `d` bins makes each group of the
|
|
282
|
+
* rest hold about `d + bins` groups instead of `d`, so the key's groups grow
|
|
283
|
+
* by `1 + bins / d`. Only one bin dim merges into a mark key, on purpose: a
|
|
284
|
+
* 2D brush over two binned columns of the same mark multiplies the second,
|
|
285
|
+
* which overestimates (the guard then skips rather than builds too much).
|
|
194
286
|
*/
|
|
195
287
|
export function estimateGroups(markKeys, activeKeys) {
|
|
196
288
|
if (activeKeys.some((key) => key.groups == null))
|
|
@@ -198,6 +290,15 @@ export function estimateGroups(markKeys, activeKeys) {
|
|
|
198
290
|
const merged = new Set();
|
|
199
291
|
let groups = 1;
|
|
200
292
|
for (const key of markKeys) {
|
|
293
|
+
if (key.column == null && key.columns) {
|
|
294
|
+
const within = activeKeys.find((active) => active.column != null && key.columns.has(active.column) && !merged.has(active));
|
|
295
|
+
if (within) {
|
|
296
|
+
merged.add(within);
|
|
297
|
+
const d = Math.max(1, key.columns.get(within.column));
|
|
298
|
+
groups *= Math.ceil(key.groups * (1 + within.groups / d));
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
201
302
|
const same = key.column == null
|
|
202
303
|
? undefined
|
|
203
304
|
: activeKeys.find((active) => active.column === key.column && !merged.has(active));
|
|
@@ -280,8 +381,9 @@ export function preaggQuerySQL(route, descriptor, selection, preagg, params, ide
|
|
|
280
381
|
}
|
|
281
382
|
if (valueExprs && valueMeta) {
|
|
282
383
|
// A toggle's clause holds tuples in its field order. Id columns
|
|
283
|
-
// filter by id; a value with no id (
|
|
284
|
-
// be expressed, so this query goes to the base
|
|
384
|
+
// filter by id (the NULL category has one); a value with no id (not in
|
|
385
|
+
// the dim yet) cannot be expressed, so this query goes to the base
|
|
386
|
+
// table instead.
|
|
285
387
|
const tuples = [];
|
|
286
388
|
for (const tuple of clause.value) {
|
|
287
389
|
const mapped = [];
|
|
@@ -294,7 +396,17 @@ export function preaggQuerySQL(route, descriptor, selection, preagg, params, ide
|
|
|
294
396
|
}
|
|
295
397
|
tuples.push(mapped);
|
|
296
398
|
}
|
|
297
|
-
|
|
399
|
+
// One field by id, and many of them (a click on a large Other): a
|
|
400
|
+
// constant-size bitmap rather than a long IN list, which DuckDB spends
|
|
401
|
+
// most of its time parsing. Measured on a preagg table: 1 ms against
|
|
402
|
+
// 20 ms for IN of 9 000 ids.
|
|
403
|
+
const only = valueMeta.length === 1 ? valueMeta[0] : null;
|
|
404
|
+
if (only?.byId && tuples.length > BITMAP_MIN_IDS) {
|
|
405
|
+
filters.push(idBitmapPredicate(valueExprs[0], tuples.map((t) => t[0]), identity.identity(only.field).values.length));
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
filters.push(togglePredicate(valueExprs, tuples));
|
|
409
|
+
}
|
|
298
410
|
}
|
|
299
411
|
}
|
|
300
412
|
// --- Build selectDef: CAST recombined aggregates; dimensions are already
|
|
@@ -315,33 +427,65 @@ export function preaggQuerySQL(route, descriptor, selection, preagg, params, ide
|
|
|
315
427
|
castSelect[ch] = castReal(expr);
|
|
316
428
|
}
|
|
317
429
|
}
|
|
430
|
+
// A table holding its padding: those rows pass whatever the brush says, and
|
|
431
|
+
// the read's GROUP BY folds them into the real rows of the same group.
|
|
432
|
+
if (preagg.creation.pad && filters.length > 0) {
|
|
433
|
+
filters.splice(0, filters.length, sqlTag `(${column(PAD_COLUMN)} OR ${and(filters)})`);
|
|
434
|
+
}
|
|
318
435
|
const preaggParts = {
|
|
319
436
|
selectDef: castSelect,
|
|
320
437
|
nonAggregateGroupBy: preaggGroupBy,
|
|
321
438
|
hasAggregates: true,
|
|
322
439
|
groupKeys: preaggGroupKeys,
|
|
440
|
+
// The table keeps the bin channels as columns of the same bins, so the
|
|
441
|
+
// read pads exactly as the base read does, or is padded already.
|
|
442
|
+
bins: parts.bins,
|
|
443
|
+
padded: !!preagg.creation.pad,
|
|
323
444
|
hasWindowAggregates: false,
|
|
324
445
|
hasSQLTransforms: parts.hasSQLTransforms,
|
|
325
446
|
orderChannels: parts.orderChannels,
|
|
326
447
|
aliasMap: parts.aliasMap,
|
|
327
|
-
passthroughChannels: parts.passthroughChannels
|
|
448
|
+
passthroughChannels: parts.passthroughChannels,
|
|
449
|
+
// nulls="drop", read through this select: each dropped channel is a group
|
|
450
|
+
// key, so the table keeps NULL as its own group, and dropping that group
|
|
451
|
+
// here removes exactly what the base scan would have left out.
|
|
452
|
+
nullDrops: parts.nullDrops
|
|
328
453
|
};
|
|
329
454
|
return assembleQuery({
|
|
330
455
|
table: preagg.table,
|
|
331
456
|
parts: preaggParts,
|
|
332
457
|
filter: filters,
|
|
333
458
|
transformCompiler,
|
|
459
|
+
// The table was built with them.
|
|
460
|
+
skipRowFilters: true,
|
|
334
461
|
limit: params.limit,
|
|
335
462
|
window: windowChannels ? { channels: windowChannels, cteName: '__preagg_cte' } : null
|
|
336
463
|
}).toString();
|
|
337
464
|
}
|
|
338
465
|
/** A preagg table's SELECT. Build and validity check share it, so equal SQL means an equal table. */
|
|
339
466
|
export function preaggSelectSQL(creation, filters) {
|
|
467
|
+
const pad = creation.pad;
|
|
468
|
+
if (!pad)
|
|
469
|
+
return realRowsSQL(creation, filters, true);
|
|
470
|
+
// By name: the padding rows name only what they set, and the brush and
|
|
471
|
+
// value columns they leave out come back NULL. Ordered once, outside.
|
|
472
|
+
const quote = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
473
|
+
const padRows = `SELECT ${pad.dims.map((col) => `p.${quote(col)}`).join(', ')}` +
|
|
474
|
+
pad.partials.map((col) => `, 0 AS ${quote(col)}`).join('') +
|
|
475
|
+
`, true AS ${quote(PAD_COLUMN)} FROM (${pad.rows}) AS p`;
|
|
476
|
+
const order = creation.orderby.length ? ` ORDER BY ${creation.orderby.join(', ')}` : '';
|
|
477
|
+
return (`SELECT * FROM (SELECT *, false AS ${quote(PAD_COLUMN)} FROM (${realRowsSQL(creation, filters, false)}) AS r` +
|
|
478
|
+
` UNION ALL BY NAME ${padRows})${order}`);
|
|
479
|
+
}
|
|
480
|
+
/** The table's real rows: the mark's groups over the source rows. */
|
|
481
|
+
function realRowsSQL(creation, filters, ordered) {
|
|
340
482
|
const query = Query.from(creation.table).select(creation.select).groupby(creation.groupby);
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
483
|
+
const where = [...creation.where, ...filters];
|
|
484
|
+
if (where.length > 0)
|
|
485
|
+
query.where(and(where));
|
|
486
|
+
if (creation.joins.length === 0) {
|
|
487
|
+
return (ordered ? query.orderby(creation.orderby) : query).toString();
|
|
488
|
+
}
|
|
345
489
|
// Map fields grouped by value to their ids on the grouped result: a join
|
|
346
490
|
// over the table's groups, not over every source row. The filters stay in
|
|
347
491
|
// the inner query, where the source's columns are in scope.
|
|
@@ -353,7 +497,7 @@ export function preaggSelectSQL(creation, filters) {
|
|
|
353
497
|
const joins = creation.joins
|
|
354
498
|
.map((j, k) => `LEFT JOIN ${quote(j.dim)} AS d${k} ON p.${quote(j.raw)} = d${k}.value`)
|
|
355
499
|
.join(' ');
|
|
356
|
-
const order = creation.orderby.length ? ` ORDER BY ${creation.orderby.join(', ')}` : '';
|
|
500
|
+
const order = ordered && creation.orderby.length ? ` ORDER BY ${creation.orderby.join(', ')}` : '';
|
|
357
501
|
return `SELECT p.* EXCLUDE (${raws}), ${ids} FROM (${query}) AS p ${joins}${order}`;
|
|
358
502
|
}
|
|
359
503
|
/**
|