@fjader/svelte 0.2.4 → 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 +1 -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 +4 -2
- package/dist/components/axes/AxisCenter.svelte +4 -2
- package/dist/components/axes/AxisLeft.svelte +4 -2
- package/dist/components/axes/BandAxisTicks.svelte +3 -2
- package/dist/components/charts/ChartCell.svelte +32 -5
- package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
- package/dist/components/charts/ChartContainer.svelte +45 -3
- package/dist/components/charts/ChartContainer.svelte.d.ts +7 -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/marks/AreaMarkX.svelte +16 -4
- package/dist/components/marks/AreaMarkY.svelte +16 -4
- package/dist/components/marks/BarMarkX.svelte +7 -3
- package/dist/components/marks/BarMarkY.svelte +7 -3
- package/dist/components/marks/DotsMark.svelte +6 -3
- package/dist/components/marks/LineMark.svelte +11 -8
- package/dist/components/marks/ParallelCoordinatesMark.svelte +7 -2
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +2 -0
- package/dist/components/marks/RectMark.svelte +6 -3
- package/dist/components/marks/RectXMark.svelte +7 -3
- package/dist/components/marks/RectYMark.svelte +7 -3
- package/dist/components/marks/RuleMarkX.svelte +30 -26
- package/dist/components/marks/RuleMarkY.svelte +30 -26
- package/dist/components/marks/TextMark.svelte +44 -5
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +16 -0
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +398 -54
- package/dist/components/marks/lifecycle/domain-queries.d.ts +11 -2
- package/dist/components/marks/lifecycle/domain-queries.js +33 -6
- package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
- package/dist/components/marks/lifecycle/domain-registration.js +15 -14
- package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +9 -1
- package/dist/components/marks/lifecycle/other-rows.svelte.js +2 -2
- package/dist/components/marks/lifecycle/query-builder.d.ts +27 -2
- package/dist/components/marks/lifecycle/query-builder.js +23 -20
- 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 +3 -0
- 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 +1 -1
- 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 +51 -11
- package/dist/components/rendering/Rects.svelte.d.ts +16 -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 +41 -24
- package/dist/components/rendering/activity-reads.js +54 -31
- 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 +7 -0
- 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 -2
- package/dist/gpu/MorphedChannels.js +33 -11
- package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
- package/dist/gpu/ScaleTemplateSystem.js +58 -2
- package/dist/index.d.ts +17 -4
- package/dist/index.js +19 -4
- package/dist/interactors/TooltipManager.svelte.js +14 -5
- 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 +2 -3
- package/dist/query-engine/FieldActivity.svelte.js +1 -2
- 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 +671 -41
- package/dist/query-engine/categorical-identity.d.ts +34 -8
- package/dist/query-engine/categorical-identity.js +138 -73
- 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 +28 -1
- package/dist/query-engine/preagg/plan.js +128 -8
- 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/scales/BandScaleState.svelte.d.ts +9 -1
- package/dist/scales/BandScaleState.svelte.js +16 -5
- 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 +41 -13
- package/dist/sql/bin.d.ts +25 -0
- package/dist/sql/bin.js +108 -5
- package/dist/transforms/TransformCompiler.d.ts +10 -0
- package/dist/transforms/TransformCompiler.js +26 -0
- package/dist/transforms/index.js +144 -24
- package/dist/types.d.ts +136 -2
- package/package.json +2 -2
|
@@ -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) {
|
|
@@ -85,7 +85,22 @@ export interface PreaggCreation {
|
|
|
85
85
|
alias: string;
|
|
86
86
|
dim: string;
|
|
87
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;
|
|
88
101
|
}
|
|
102
|
+
/** The column marking a preagg table's padding rows; real rows hold false. */
|
|
103
|
+
export declare const PAD_COLUMN = "__pad";
|
|
89
104
|
/**
|
|
90
105
|
* How each active dim appears in a preagg table. A `bin` dim is a pixel-bin
|
|
91
106
|
* index over a window (`buildBinExpr`): bins 0 .. bins - 1 from d0, with -1
|
|
@@ -112,6 +127,12 @@ export type ActiveMeta = {
|
|
|
112
127
|
export interface GroupKey {
|
|
113
128
|
column: string | null;
|
|
114
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>;
|
|
115
136
|
}
|
|
116
137
|
export interface PreaggState {
|
|
117
138
|
table: string;
|
|
@@ -170,7 +191,13 @@ export declare function markKeys(descriptor: QueryDescriptor, handle: Pick<MarkH
|
|
|
170
191
|
* mark with x and fill on the same field has that field's groups, not their
|
|
171
192
|
* square, and a bin dim on a column the mark already groups by adds its bins
|
|
172
193
|
* to that key's (each pixel bin falls in about one of the mark's bins)
|
|
173
|
-
* 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).
|
|
174
201
|
*/
|
|
175
202
|
export declare function estimateGroups(markKeys: GroupKey[], activeKeys: GroupKey[]): number | null;
|
|
176
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';
|
|
14
|
+
import { isAggregateExpression, isNode, collectColumns, column, and, isBetween, literal, Query, collectAggregates, sql as sqlTag } from '@uwdata/mosaic-sql';
|
|
15
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
|
|
@@ -104,6 +106,7 @@ export function planPreagg(descriptor, dims, identity) {
|
|
|
104
106
|
activeMeta.push({ kind: 'value', field, column: alias, byId: false });
|
|
105
107
|
activeKeys.push({ column: null, groups: null });
|
|
106
108
|
});
|
|
109
|
+
const pad = analysis.windowChannels ? null : padPlan(descriptor, preaggCols);
|
|
107
110
|
// CAST-wrap dimension preaggCols (bake CAST into the preagg table).
|
|
108
111
|
// Partial-aggregate and active columns stay raw for recombination.
|
|
109
112
|
for (const [ch, expr] of Object.entries(preaggCols)) {
|
|
@@ -120,8 +123,9 @@ export function planPreagg(descriptor, dims, identity) {
|
|
|
120
123
|
groupby: [...preaggGroupBy, ...activeGroupBy],
|
|
121
124
|
// Sorted by bin columns for fast range scans
|
|
122
125
|
orderby: binOrder,
|
|
123
|
-
where: descriptor.transformCompiler.rowFilters(),
|
|
124
|
-
joins
|
|
126
|
+
where: [...descriptor.transformCompiler.rowFilters(), ...(pad?.where ?? [])],
|
|
127
|
+
joins,
|
|
128
|
+
pad: pad?.pad ?? null
|
|
125
129
|
},
|
|
126
130
|
outputSelect,
|
|
127
131
|
activeMeta,
|
|
@@ -129,6 +133,46 @@ export function planPreagg(descriptor, dims, identity) {
|
|
|
129
133
|
activeKeys
|
|
130
134
|
};
|
|
131
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
|
+
}
|
|
132
176
|
/** The mark's channel that is `field`'s id channel (see idFieldOf), or null. */
|
|
133
177
|
function idChannelFor(selectDef, field, identity) {
|
|
134
178
|
for (const [channel, expr] of Object.entries(selectDef)) {
|
|
@@ -180,6 +224,14 @@ export function markKeys(descriptor, handle, identity) {
|
|
|
180
224
|
else
|
|
181
225
|
fields.add(field);
|
|
182
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
|
+
}
|
|
183
235
|
if (allIds) {
|
|
184
236
|
return [...fields].map((field) => ({
|
|
185
237
|
column: null,
|
|
@@ -187,7 +239,35 @@ export function markKeys(descriptor, handle, identity) {
|
|
|
187
239
|
}));
|
|
188
240
|
}
|
|
189
241
|
const rows = handle.params.limit == null ? handle.arrowTable?.numRows : undefined;
|
|
190
|
-
|
|
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;
|
|
191
271
|
}
|
|
192
272
|
/**
|
|
193
273
|
* The groups a planned table would hold: the mark's own keys (markKeys) and
|
|
@@ -196,7 +276,13 @@ export function markKeys(descriptor, handle, identity) {
|
|
|
196
276
|
* mark with x and fill on the same field has that field's groups, not their
|
|
197
277
|
* square, and a bin dim on a column the mark already groups by adds its bins
|
|
198
278
|
* to that key's (each pixel bin falls in about one of the mark's bins)
|
|
199
|
-
* 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).
|
|
200
286
|
*/
|
|
201
287
|
export function estimateGroups(markKeys, activeKeys) {
|
|
202
288
|
if (activeKeys.some((key) => key.groups == null))
|
|
@@ -204,6 +290,15 @@ export function estimateGroups(markKeys, activeKeys) {
|
|
|
204
290
|
const merged = new Set();
|
|
205
291
|
let groups = 1;
|
|
206
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
|
+
}
|
|
207
302
|
const same = key.column == null
|
|
208
303
|
? undefined
|
|
209
304
|
: activeKeys.find((active) => active.column === key.column && !merged.has(active));
|
|
@@ -332,11 +427,20 @@ export function preaggQuerySQL(route, descriptor, selection, preagg, params, ide
|
|
|
332
427
|
castSelect[ch] = castReal(expr);
|
|
333
428
|
}
|
|
334
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
|
+
}
|
|
335
435
|
const preaggParts = {
|
|
336
436
|
selectDef: castSelect,
|
|
337
437
|
nonAggregateGroupBy: preaggGroupBy,
|
|
338
438
|
hasAggregates: true,
|
|
339
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,
|
|
340
444
|
hasWindowAggregates: false,
|
|
341
445
|
hasSQLTransforms: parts.hasSQLTransforms,
|
|
342
446
|
orderChannels: parts.orderChannels,
|
|
@@ -360,12 +464,28 @@ export function preaggQuerySQL(route, descriptor, selection, preagg, params, ide
|
|
|
360
464
|
}
|
|
361
465
|
/** A preagg table's SELECT. Build and validity check share it, so equal SQL means an equal table. */
|
|
362
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) {
|
|
363
482
|
const query = Query.from(creation.table).select(creation.select).groupby(creation.groupby);
|
|
364
483
|
const where = [...creation.where, ...filters];
|
|
365
484
|
if (where.length > 0)
|
|
366
485
|
query.where(and(where));
|
|
367
|
-
if (creation.joins.length === 0)
|
|
368
|
-
return query.orderby(creation.orderby).toString();
|
|
486
|
+
if (creation.joins.length === 0) {
|
|
487
|
+
return (ordered ? query.orderby(creation.orderby) : query).toString();
|
|
488
|
+
}
|
|
369
489
|
// Map fields grouped by value to their ids on the grouped result: a join
|
|
370
490
|
// over the table's groups, not over every source row. The filters stay in
|
|
371
491
|
// the inner query, where the source's columns are in scope.
|
|
@@ -377,7 +497,7 @@ export function preaggSelectSQL(creation, filters) {
|
|
|
377
497
|
const joins = creation.joins
|
|
378
498
|
.map((j, k) => `LEFT JOIN ${quote(j.dim)} AS d${k} ON p.${quote(j.raw)} = d${k}.value`)
|
|
379
499
|
.join(' ');
|
|
380
|
-
const order = creation.orderby.length ? ` ORDER BY ${creation.orderby.join(', ')}` : '';
|
|
500
|
+
const order = ordered && creation.orderby.length ? ` ORDER BY ${creation.orderby.join(', ')}` : '';
|
|
381
501
|
return `SELECT p.* EXCLUDE (${raws}), ${ids} FROM (${query}) AS p ${joins}${order}`;
|
|
382
502
|
}
|
|
383
503
|
/**
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A sliced lane: one DuckDB-WASM connection whose statements run through the
|
|
3
|
+
* pending-query API, so the worker executes them in slices and serves other
|
|
4
|
+
* connections' messages in between. A slow read on a lane no longer holds up a
|
|
5
|
+
* quick one on another connection.
|
|
6
|
+
*
|
|
7
|
+
* ONE STATEMENT AT A TIME, enforced here. A connection holds one pending query,
|
|
8
|
+
* and a second one, or a `runQuery`, on the same connection silently resets the
|
|
9
|
+
* first (measured in Phase 0). So the lane queues: first in, first out.
|
|
10
|
+
*
|
|
11
|
+
* ABORT (an AbortSignal per statement) has three cases:
|
|
12
|
+
* - queued: dropped, rejected with AbortError, never sent;
|
|
13
|
+
* - running, before the result header: `cancelPendingQuery`, rejected with
|
|
14
|
+
* AbortError. Only an abort we asked for becomes AbortError; any other error
|
|
15
|
+
* passes through unchanged;
|
|
16
|
+
* - after the header, fetching chunks: the query has finished executing, so
|
|
17
|
+
* there is nothing to cancel. Fetching stops and the statement rejects; the
|
|
18
|
+
* next `startPendingQuery` resets the leftover result.
|
|
19
|
+
* A cancel that loses the race (the query finished as it arrived, so the next
|
|
20
|
+
* poll returns the header) still rejects with AbortError: the caller sees one
|
|
21
|
+
* outcome for one abort.
|
|
22
|
+
*
|
|
23
|
+
* SLICE LENGTH is the database's `queryPollingInterval`, set when it is opened
|
|
24
|
+
* (`initDuckDB` uses 5 ms). At the default, ~100 ms, a quick statement on
|
|
25
|
+
* another connection can wait ~50 ms for a slice to end. It cannot be read
|
|
26
|
+
* back. When the opener knows it (`connectionFrom`'s `pollingIntervalMs`),
|
|
27
|
+
* the lane trusts that; otherwise it measures, and warns once per page when
|
|
28
|
+
* the MEDIAN slice over its first sliced statements runs long. A median, not
|
|
29
|
+
* one reading: each poll waits for a main-thread turn, and during load a busy
|
|
30
|
+
* main thread stretches round trips past the threshold with short slices.
|
|
31
|
+
*/
|
|
32
|
+
/** The DuckDB-WASM bindings a lane drives; `AsyncDuckDB` has them. */
|
|
33
|
+
export interface PendingQueryBindings {
|
|
34
|
+
startPendingQuery(conn: number, text: string, allowStreamResult?: boolean): Promise<Uint8Array | null>;
|
|
35
|
+
pollPendingQuery(conn: number): Promise<Uint8Array | null>;
|
|
36
|
+
cancelPendingQuery(conn: number): Promise<boolean>;
|
|
37
|
+
fetchQueryResults(conn: number): Promise<Uint8Array | null>;
|
|
38
|
+
}
|
|
39
|
+
/** A sliced query's result, and what it cost in the worker. */
|
|
40
|
+
export interface LaneResult {
|
|
41
|
+
/** The Arrow IPC stream, `[header, ...record batches]`. */
|
|
42
|
+
ipc: Uint8Array[];
|
|
43
|
+
/** Slices it took past the first: 0 or 1, a quick read. */
|
|
44
|
+
polls: number;
|
|
45
|
+
/** From start to its result header: execution, without the chunked fetch. */
|
|
46
|
+
execMs: number;
|
|
47
|
+
}
|
|
48
|
+
export interface SlicedLane {
|
|
49
|
+
/**
|
|
50
|
+
* Run a query sliced. `ipc` is the Arrow IPC stream as the worker returned
|
|
51
|
+
* it, `[header, ...record batches]`, for flechette's `tableFromIPC` (each
|
|
52
|
+
* buffer holds whole messages). `polls`: how many slices it took past the
|
|
53
|
+
* first, the cost signal (0 or 1: a quick read).
|
|
54
|
+
*/
|
|
55
|
+
query(sql: string, signal?: AbortSignal): Promise<LaneResult>;
|
|
56
|
+
/**
|
|
57
|
+
* Run a statement sliced, discarding any result. The result is left
|
|
58
|
+
* pending on the connection, unfetched, until the next statement resets
|
|
59
|
+
* it (as after an abort past the header): the connection is NOT idle
|
|
60
|
+
* between statements, so never send a `runQuery` on it from outside.
|
|
61
|
+
*/
|
|
62
|
+
exec(sql: string, signal?: AbortSignal): Promise<{
|
|
63
|
+
polls: number;
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* Reject what is queued (AbortError), cancel what runs, then close the
|
|
67
|
+
* connection. Statements sent after it reject with AbortError.
|
|
68
|
+
*/
|
|
69
|
+
close(): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Asked before each call that executes (start, poll): a promise that
|
|
73
|
+
* resolves when this lane may run its next slice, or null to run it now. The
|
|
74
|
+
* engine decides, across all its lanes (see QueryEngine #sliceGrant): freely
|
|
75
|
+
* while its main connection is quiet, a share of the worker while it works.
|
|
76
|
+
*/
|
|
77
|
+
export type LaneHold = () => Promise<void> | null;
|
|
78
|
+
export declare class PendingQueryLane implements SlicedLane {
|
|
79
|
+
#private;
|
|
80
|
+
constructor(bindings: PendingQueryBindings, conn: number, closeConnection: () => Promise<void>,
|
|
81
|
+
/** The database's polling interval, when the opener knows it. */
|
|
82
|
+
options?: {
|
|
83
|
+
pollingIntervalMs?: number;
|
|
84
|
+
hold?: LaneHold;
|
|
85
|
+
});
|
|
86
|
+
query(sql: string, signal?: AbortSignal): Promise<LaneResult>;
|
|
87
|
+
exec(sql: string, signal?: AbortSignal): Promise<{
|
|
88
|
+
polls: number;
|
|
89
|
+
}>;
|
|
90
|
+
close(): Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
/** For tests: the once-per-page slice warning, rearmed. */
|
|
93
|
+
export declare function _resetSliceWarning(): void;
|
|
94
|
+
/**
|
|
95
|
+
* One lane, opened on first use. `get()` answers in the same tick once the
|
|
96
|
+
* lane's state is known (open, or none to be had), and returns a promise only
|
|
97
|
+
* while it opens: a statement sent while there is no lane goes out with no
|
|
98
|
+
* extra microtask, so nothing sent after it in the same tick can overtake it.
|
|
99
|
+
*/
|
|
100
|
+
export declare class LaneSlot {
|
|
101
|
+
#private;
|
|
102
|
+
/** `open` returns null when no lane can be had (no capability). */
|
|
103
|
+
constructor(open: () => Promise<SlicedLane> | null, onFail: (e: unknown) => void);
|
|
104
|
+
get isOpen(): boolean;
|
|
105
|
+
get(): SlicedLane | null | Promise<SlicedLane | null>;
|
|
106
|
+
/** Close the lane if it was opened (or is opening); none can open after. */
|
|
107
|
+
close(): Promise<void>;
|
|
108
|
+
}
|