signalk-questdb 2.3.1 → 2.4.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/.release-please-manifest.json +1 -1
- package/CLAUDE.md +2 -2
- package/README.md +92 -0
- package/dist/archive.d.ts +7 -1
- package/dist/archive.js +7 -1
- package/dist/archive.js.map +1 -1
- package/dist/container-env.js +14 -0
- package/dist/container-env.js.map +1 -1
- package/dist/drained.d.ts +2 -0
- package/dist/drained.js +22 -0
- package/dist/drained.js.map +1 -0
- package/dist/history-v2.js +227 -33
- package/dist/history-v2.js.map +1 -1
- package/dist/ilp-writer.d.ts +2 -0
- package/dist/ilp-writer.js +48 -13
- package/dist/ilp-writer.js.map +1 -1
- package/dist/image-refresh.d.ts +35 -0
- package/dist/image-refresh.js +118 -0
- package/dist/image-refresh.js.map +1 -0
- package/dist/index.js +464 -136
- package/dist/index.js.map +1 -1
- package/dist/query-client.d.ts +12 -1
- package/dist/query-client.js +37 -3
- package/dist/query-client.js.map +1 -1
- package/dist/questdb-ready.d.ts +21 -0
- package/dist/questdb-ready.js +35 -0
- package/dist/questdb-ready.js.map +1 -0
- package/dist/questdb-watchdog.d.ts +73 -0
- package/dist/questdb-watchdog.js +308 -0
- package/dist/questdb-watchdog.js.map +1 -0
- package/dist/watchdog-checks.d.ts +50 -0
- package/dist/watchdog-checks.js +279 -0
- package/dist/watchdog-checks.js.map +1 -0
- package/package.json +3 -3
- package/public/assets/AppPanel-C293yEHl.js +1 -0
- package/public/assets/{PluginConfigurationPanel-DR-y5e3d.js → PluginConfigurationPanel-7qfy8fIR.js} +1 -1
- package/public/assets/_virtual_mf-localSharedImportMap___mfe_internal__signalk_questdb__mf_owner__1-BH9mY6vj.js +1 -0
- package/public/assets/_virtual_mf___mfe_internal__signalk_questdb__mf_owner__97224418320974__loadShare__shared-H1lSL0nN.js +1 -0
- package/public/assets/{_virtual_mf___mfe_internal__signalk_questdb__mf_owner__196689844091135__prebuild__react__prebuild__-eQjVI-Oj.js → _virtual_mf___mfe_internal__signalk_questdb__mf_owner__97224418320974__prebuild__react__prebuild__-CTXDWPi7.js} +1 -1
- package/public/assets/{dist-C5M3fhBw.js → dist-BhVCbinB.js} +1 -1
- package/public/assets/helpers-CvnX61Ju.js +1 -0
- package/public/assets/hostInit-Ds8KD_fS.js +1 -0
- package/public/assets/{index-eAjx9ZuX.js → index-QJC86Uvi.js} +2 -2
- package/public/assets/{pendingShares-PvyVL1Z8.js → pendingShares-DemvETkM.js} +2 -2
- package/public/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/public/assets/{utils-v9GZlbdk.js → utils-D62kKhzq.js} +1 -1
- package/public/remoteEntry.js +2 -2
- package/public/assets/AppPanel-Fvhb8xTK.js +0 -1
- package/public/assets/_virtual_mf-localSharedImportMap___mfe_internal__signalk_questdb__mf_owner__1-BUtmcsA-.js +0 -1
- package/public/assets/_virtual_mf___mfe_internal__signalk_questdb__mf_owner__196689844091135__loadShare__shared-BRI06Lxu.js +0 -8
- package/public/assets/helpers-oIxDNnZJ.js +0 -1
- package/public/assets/hostInit-B3lIJd9C.js +0 -1
package/CLAUDE.md
CHANGED
|
@@ -33,9 +33,9 @@ Both build outputs are shipped in the npm package; `prepublishOnly` rebuilds the
|
|
|
33
33
|
- Subscribes to the Signal K **streambundle** for all paths and writes filtered/throttled deltas to QuestDB via **ILP over raw TCP** (`src/ilp-writer.ts`) — no QuestDB client library.
|
|
34
34
|
- Three tables (`signalk`, `signalk_str`, `signalk_position`) defined and queried in `src/query-client.ts`. All use WAL mode + daily partitioning + dedup. The archives `signalk_archive` and `signalk_position_archive` are defined there too, with their base tables' columns and dedup keys and monthly partitions.
|
|
35
35
|
- Reads come from two registered providers:
|
|
36
|
-
- **v2** (`src/history-v2.ts`) — `app.registerHistoryApiProvider()`, REST under `/signalk/v2/api/history/`. Aggregates map directly to QuestDB SQL except `sma`/`ema` (computed client-side).
|
|
36
|
+
- **v2** (`src/history-v2.ts`) — `app.registerHistoryApiProvider()`, REST under `/signalk/v2/api/history/`. Aggregates map directly to QuestDB SQL except `sma`/`ema` (computed client-side). Where the archive covers the range, each table is sampled on its own and the buckets merged in the plugin.
|
|
37
37
|
- **v1** (`src/history-v1.ts`) — `app.registerHistoryProvider()`, WebSocket playback with chunked reads.
|
|
38
|
-
- Retention (`src/retention.ts`) drops old daily partitions on a timer. Before a partition of `signalk_position` or `signalk` goes, the own vessel's rows the config keeps are copied, thinned, into `signalk_position_archive` / `signalk_archive` (`src/archive.ts`), and the drop waits for that copy to be applied. Reads add an archive table only for a range it covers (`ArchiveSpans`), so a recent window stays on the base table alone
|
|
38
|
+
- Retention (`src/retention.ts`) drops old daily partitions on a timer. Before a partition of `signalk_position` or `signalk` goes, the own vessel's rows the config keeps are copied, thinned, into `signalk_position_archive` / `signalk_archive` (`src/archive.ts`), and the drop waits for that copy to be applied. Reads add an archive table only for a range it covers (`ArchiveSpans`), so a recent window stays on the base table alone. v1 snapshots read it as one more per-table `LATEST ON` branch, and v2's sampled and raw reads of the own vessel read each table on its own and merge the buckets or rows in the plugin. Their union (`readFrom` in `src/archive.ts`) sorts every row in range, so only reads that need one order across both tables, such as `middle_index`, or that a short window bounds still use it.
|
|
39
39
|
- GPX import (`src/gpx-import.ts`, `src/gpx-import-session.ts`) streams a sliced upload through saxes into `signalk_position_archive` under source `import.gpx`, thinned by stamping points on interval starts and letting dedup keep one per interval. The caller names the tracks to import: an export mixes the own track with AIS targets and other boats' passages, and nothing in GPX tells them apart.
|
|
40
40
|
- Startup restore (`src/restore.ts`) optionally replays each vessel's last recorded position back into the live model after a server restart.
|
|
41
41
|
- Console proxy (`src/console-proxy.ts`) reverse-proxies QuestDB's own web console so the embeddable webapp (`public/index.html`) can host it in the admin UI.
|
package/README.md
CHANGED
|
@@ -132,6 +132,51 @@ rootless Podman.
|
|
|
132
132
|
| Container network | `sk-network` | Shared Podman/Docker network QuestDB joins in managed mode, so the companion signalk-grafana reaches it by container DNS; host-port publication is separate ("Bind to 0.0.0.0") |
|
|
133
133
|
| Bind to 0.0.0.0 | `false` | Publish QuestDB's ports on all interfaces (see Connectivity) |
|
|
134
134
|
|
|
135
|
+
### QuestDB updates
|
|
136
|
+
|
|
137
|
+
The panel's **update check** compares the running QuestDB with its latest stable
|
|
138
|
+
release. **Update** pulls that release, recreates the container on it and saves
|
|
139
|
+
it as the image version.
|
|
140
|
+
|
|
141
|
+
After the plugin itself was updated, and at its very first start, the plugin
|
|
142
|
+
also checks once on its own, in the background. It does so only in managed mode,
|
|
143
|
+
and only where the image version is a tag that moves, such as the default
|
|
144
|
+
`latest`. If QuestDB runs an older release than the latest stable one, the
|
|
145
|
+
plugin pulls the tag again and recreates the container on it. The image version
|
|
146
|
+
stays `latest`. Without this check, QuestDB on `latest` would stay on whatever
|
|
147
|
+
release the first install pulled, because signalk-container pulls an image only
|
|
148
|
+
when it is missing. An image version that names one release, such as `9.4.3`,
|
|
149
|
+
stays as it is, and so does one that **Update** saved while the check was
|
|
150
|
+
waiting. QuestDB is down while it starts again, as with **Update**. Progress
|
|
151
|
+
goes to the debug log, and failures go to the server log. A recreate that fails
|
|
152
|
+
half way and leaves QuestDB stopped or without a container is followed by a
|
|
153
|
+
start, as the plugin's own start would do. Only if that fails too does the
|
|
154
|
+
status line say so.
|
|
155
|
+
|
|
156
|
+
With [signalk-internet](https://github.com/meri-imperiumi/signalk-internet)
|
|
157
|
+
enabled, the check waits for a free link. On a metered, captive or offline link
|
|
158
|
+
it asks again from time to time, for as long as the plugin runs. Without
|
|
159
|
+
signalk-internet, the link counts as free. Telling the two apart needs
|
|
160
|
+
signalk-container 1.38.0 or later; with an older one, the check does not run. A
|
|
161
|
+
failure to read the running or the latest version is also tried again later.
|
|
162
|
+
|
|
163
|
+
The check is done once QuestDB runs the latest release, or once its container
|
|
164
|
+
runs the image the pull brought, even if QuestDB still reports an older release
|
|
165
|
+
afterwards. It then waits for the next plugin update. The next start checks
|
|
166
|
+
again wherever the container keeps its old image, or where the new one cannot
|
|
167
|
+
be told from the old:
|
|
168
|
+
|
|
169
|
+
- the plugin stopped first;
|
|
170
|
+
- the download failed, or something else failed before the recreate;
|
|
171
|
+
- an InfluxDB import, a GPX import or an export was running;
|
|
172
|
+
- the string table was being rebuilt;
|
|
173
|
+
- the container was stopped, which the check leaves as it is;
|
|
174
|
+
- the container was wedged;
|
|
175
|
+
- the tag on Docker Hub did not name the latest release yet.
|
|
176
|
+
|
|
177
|
+
A tag that never reaches the latest release, such as `9` while the latest is
|
|
178
|
+
`10.1.0`, therefore costs a pull and a look at GitHub at every start.
|
|
179
|
+
|
|
135
180
|
## Recording
|
|
136
181
|
|
|
137
182
|
### Schema
|
|
@@ -804,6 +849,53 @@ shown on the status card as **WAL suspended**. Two causes need two remedies:
|
|
|
804
849
|
shows those numbers and offers **Skip**; if the stuck segment is the newest
|
|
805
850
|
one, skipping drops the entire pending backlog, and the panel says so.
|
|
806
851
|
|
|
852
|
+
### Out of memory or not answering
|
|
853
|
+
|
|
854
|
+
QuestDB caps the native memory it tracks below the container's memory limit
|
|
855
|
+
and fails any allocation past the cap with `global RSS memory limit exceeded`.
|
|
856
|
+
When something holds memory at the cap, queries fail, the WAL apply job then
|
|
857
|
+
suspends the tables, and in the end QuestDB stops accepting HTTP connections. The JVM stays up throughout, so the
|
|
858
|
+
container's restart policy never acts.
|
|
859
|
+
|
|
860
|
+
In managed mode the plugin checks for this every minute while it runs, and
|
|
861
|
+
restarts the QuestDB container when:
|
|
862
|
+
|
|
863
|
+
- QuestDB recently reported its global memory limit, and over consecutive
|
|
864
|
+
checks its native memory is still held at that limit, it fails even the
|
|
865
|
+
health check with it, or it has stopped answering. A single large query
|
|
866
|
+
frees what it held when it ends, so it does not count, nor does a client that
|
|
867
|
+
keeps asking for one.
|
|
868
|
+
- the container is running but QuestDB has answered nothing for several
|
|
869
|
+
minutes, over consecutive checks. A QuestDB busy with a long statement still
|
|
870
|
+
answers it in the end, and no check counts while a parquet export streams
|
|
871
|
+
from it.
|
|
872
|
+
|
|
873
|
+
Before the restart the plugin writes QuestDB's message and its last error lines
|
|
874
|
+
to the server log, shows the reason on the status card, and raises the Signal K
|
|
875
|
+
notification `notifications.signalk-questdb.watchdog`. After the restart it
|
|
876
|
+
logs the outcome. Once QuestDB answers again the notification returns to normal;
|
|
877
|
+
when it answered right after the restart, the notification keeps the time and
|
|
878
|
+
the reason. Restarts are rate limited, spaced apart and capped per few hours of
|
|
879
|
+
the plugin running; past that the plugin logs that the restart is held back and
|
|
880
|
+
keeps the alert up. A restart that fails, because stopping or starting the
|
|
881
|
+
container failed, counts against none of these limits: the plugin tries again a
|
|
882
|
+
few minutes later, since the container may be left stopped. It reports the
|
|
883
|
+
first failure; while the retries keep failing, they go to the debug log only.
|
|
884
|
+
|
|
885
|
+
The plugin leaves a container alone if someone else stopped it, and starts one
|
|
886
|
+
that its own restart left stopped. One exception: a container stopped by hand
|
|
887
|
+
while a stop of the plugin's is still pending, or right after a restart of the
|
|
888
|
+
plugin's before the plugin has seen it running again, may be started again too.
|
|
889
|
+
While a stop of the plugin's hangs, it sends no further stop. An external
|
|
890
|
+
QuestDB is never restarted. A
|
|
891
|
+
QuestDB that is already stuck when the plugin starts, as after a Signal K
|
|
892
|
+
restart while it was stuck, is not covered: restarting the plugin stops the
|
|
893
|
+
container and starts it afresh.
|
|
894
|
+
|
|
895
|
+
The managed container runs with QuestDB's query caches off: history reads put
|
|
896
|
+
their time range into the SQL, so the caches never got a hit, and on older
|
|
897
|
+
QuestDB releases they held memory.
|
|
898
|
+
|
|
807
899
|
### Performance (Pi and other low-power hosts)
|
|
808
900
|
|
|
809
901
|
- **Default sampling rate** of 2000 ms limits each path to one write per two
|
package/dist/archive.d.ts
CHANGED
|
@@ -18,7 +18,13 @@ export declare const NO_ARCHIVE: ArchiveReader;
|
|
|
18
18
|
* The merge has to declare `ts` its designated timestamp again, or SAMPLE BY
|
|
19
19
|
* and LATEST ON reject it, and that declaration is only true because of the
|
|
20
20
|
* ORDER BY. A time filter on the outer query still reaches both tables as an
|
|
21
|
-
* interval scan (verified with EXPLAIN on QuestDB 9.4 and 10.0)
|
|
21
|
+
* interval scan (verified with EXPLAIN on QuestDB 9.4 and 10.0), but the
|
|
22
|
+
* ORDER BY then sorts every row in range, so its memory grows with the
|
|
23
|
+
* range. And SAMPLE BY … FROM … TO … FILL(NULL) over it adds a bucket past
|
|
24
|
+
* TO that repeats the last one (QuestDB 9.4). The v2 provider's sampled and
|
|
25
|
+
* raw reads therefore read each table on its own and merge the rows
|
|
26
|
+
* themselves; this union is for reads that need one order across both
|
|
27
|
+
* tables, or that a short window bounds.
|
|
22
28
|
*/
|
|
23
29
|
export declare function readFrom(table: ArchivedTable, withArchive: boolean): string;
|
|
24
30
|
/**
|
package/dist/archive.js
CHANGED
|
@@ -30,7 +30,13 @@ export const NO_ARCHIVE = { covers: () => false };
|
|
|
30
30
|
* The merge has to declare `ts` its designated timestamp again, or SAMPLE BY
|
|
31
31
|
* and LATEST ON reject it, and that declaration is only true because of the
|
|
32
32
|
* ORDER BY. A time filter on the outer query still reaches both tables as an
|
|
33
|
-
* interval scan (verified with EXPLAIN on QuestDB 9.4 and 10.0)
|
|
33
|
+
* interval scan (verified with EXPLAIN on QuestDB 9.4 and 10.0), but the
|
|
34
|
+
* ORDER BY then sorts every row in range, so its memory grows with the
|
|
35
|
+
* range. And SAMPLE BY … FROM … TO … FILL(NULL) over it adds a bucket past
|
|
36
|
+
* TO that repeats the last one (QuestDB 9.4). The v2 provider's sampled and
|
|
37
|
+
* raw reads therefore read each table on its own and merge the rows
|
|
38
|
+
* themselves; this union is for reads that need one order across both
|
|
39
|
+
* tables, or that a short window bounds.
|
|
34
40
|
*/
|
|
35
41
|
export function readFrom(table, withArchive) {
|
|
36
42
|
if (!withArchive)
|
package/dist/archive.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive.js","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,EAAE;AACF,2EAA2E;AAC3E,2EAA2E;AAC3E,6EAA6E;AAC7E,yEAAyE;AACzE,oDAAoD;AACpD,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,+EAA+E;AAC/E,0EAA0E;AAC1E,6CAA6C;AAG7C,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,OAAO,EAAE,iBAAiB;IAC1B,gBAAgB,EAAE,0BAA0B;CACpC,CAAC;AAIX,MAAM,OAAO,GAAkC;IAC7C,OAAO,EAAE,kCAAkC;IAC3C,gBAAgB,EAAE,+BAA+B;CAClD,CAAC;AAOF,MAAM,CAAC,MAAM,UAAU,GAAkB,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC;AAEjE
|
|
1
|
+
{"version":3,"file":"archive.js","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"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"}
|
package/dist/container-env.js
CHANGED
|
@@ -134,6 +134,20 @@ export function buildContainerEnv(config) {
|
|
|
134
134
|
// apply memory-pressure backoff that precedes a suspension, so raise this
|
|
135
135
|
// back to INFO when diagnosing a database that has stopped recording.
|
|
136
136
|
QDB_LOG_W_STDOUT_LEVEL: "ERROR",
|
|
137
|
+
// The HTTP endpoint keeps up to 128 compiled queries (32 blocks of 4) for
|
|
138
|
+
// reuse by identical SQL text, and pg-wire keeps another such cache. The
|
|
139
|
+
// plugin's history reads put their time range into the SQL as literals,
|
|
140
|
+
// and so do Grafana's macros, so neither cache gets a hit — they only
|
|
141
|
+
// fill. On QuestDB before 10.0.0 every cached parallel GROUP BY / SAMPLE
|
|
142
|
+
// BY also keeps its native buffers at their peak size (fixed upstream in
|
|
143
|
+
// questdb#7267): about 7 MiB each over a month of data, so a full cache
|
|
144
|
+
// holds more than the 691 MiB the 768m cap leaves QuestDB, and it then
|
|
145
|
+
// fails every allocation with "global RSS memory limit exceeded". Seen
|
|
146
|
+
// twice on 9.4.3 within a day: 20 distinct reads added 150 MiB that only
|
|
147
|
+
// flush_query_cache() or a restart gave back. Without the caches, each
|
|
148
|
+
// read frees its buffers when it ends.
|
|
149
|
+
QDB_HTTP_QUERY_CACHE_ENABLED: "false",
|
|
150
|
+
QDB_PG_SELECT_CACHE_ENABLED: "false",
|
|
137
151
|
QDB_CAIRO_O3_COLUMN_MEMORY_SIZE: "262144",
|
|
138
152
|
...(config.compression && config.compression !== "none"
|
|
139
153
|
? {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"container-env.js","sourceRoot":"","sources":["../src/container-env.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"container-env.js","sourceRoot":"","sources":["../src/container-env.ts"],"names":[],"mappings":"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"}
|
package/dist/drained.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// After a write the response refused, waits until it takes more; false once
|
|
2
|
+
// the client is gone, or once `signal` (the export's deadline) aborts. A
|
|
3
|
+
// response whose client disconnected never drains, nor does one whose client
|
|
4
|
+
// stays connected but stops reading, and a stream loop waiting for it would
|
|
5
|
+
// hold its QuestDB export open for good, and with it the watchdog's export
|
|
6
|
+
// count (questdbExportsRunning in index.ts).
|
|
7
|
+
export function drained(res, signal) {
|
|
8
|
+
if (res.destroyed || signal?.aborted)
|
|
9
|
+
return Promise.resolve(false);
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
const settle = () => {
|
|
12
|
+
res.off("drain", settle);
|
|
13
|
+
res.off("close", settle);
|
|
14
|
+
signal?.removeEventListener("abort", settle);
|
|
15
|
+
resolve(!res.destroyed && !signal?.aborted);
|
|
16
|
+
};
|
|
17
|
+
res.once("drain", settle);
|
|
18
|
+
res.once("close", settle);
|
|
19
|
+
signal?.addEventListener("abort", settle, { once: true });
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=drained.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drained.js","sourceRoot":"","sources":["../src/drained.ts"],"names":[],"mappings":"AAEA,4EAA4E;AAC5E,yEAAyE;AACzE,6EAA6E;AAC7E,4EAA4E;AAC5E,2EAA2E;AAC3E,6CAA6C;AAC7C,MAAM,UAAU,OAAO,CACrB,GAAmB,EACnB,MAAoB;IAEpB,IAAI,GAAG,CAAC,SAAS,IAAI,MAAM,EAAE,OAAO;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACpE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,MAAM,GAAG,GAAG,EAAE;YAClB,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YACzB,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YACzB,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC7C,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC9C,CAAC,CAAC;QACF,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1B,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1B,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/history-v2.js
CHANGED
|
@@ -295,6 +295,8 @@ function normalizeContext(context, selfContext) {
|
|
|
295
295
|
// well-behaved clients budget ~2000 points per request; a million covers any
|
|
296
296
|
// sane chart while keeping the worst case bounded.
|
|
297
297
|
const MAX_SAMPLE_BUCKETS = 1_000_000;
|
|
298
|
+
// The rows a read without a resolution returns at most.
|
|
299
|
+
const RAW_LIMIT = 10_000;
|
|
298
300
|
// Upper bound on columns ONE path may expand into under sourcePolicy=all.
|
|
299
301
|
// The bucket cap above only applies to requests that named a resolution, so
|
|
300
302
|
// these are the only things bounding an unresolved expansion.
|
|
@@ -328,6 +330,128 @@ function sampleBy(range, resolution) {
|
|
|
328
330
|
const to = validateTimestamp(range.to);
|
|
329
331
|
return `SAMPLE BY ${effectiveResolution(resolution)}s FROM '${from}' TO '${to}' FILL(NULL)`;
|
|
330
332
|
}
|
|
333
|
+
const numbers = (values) => values.filter((v) => typeof v === "number");
|
|
334
|
+
const total = (values) => numbers(values).reduce((a, b) => a + b, 0);
|
|
335
|
+
/** The least or greatest number, or null where there is none, as in SQL. */
|
|
336
|
+
function extreme(values, pick) {
|
|
337
|
+
const found = numbers(values);
|
|
338
|
+
return found.length === 0 ? null : Math[pick](...found);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The row whose time (part `at`) is earliest, or latest. QuestDB writes
|
|
342
|
+
* every timestamp in one fixed-width ISO form, so the strings compare in
|
|
343
|
+
* time order.
|
|
344
|
+
*/
|
|
345
|
+
function byTime(rows, at, latest) {
|
|
346
|
+
return rows.reduce((best, row) => {
|
|
347
|
+
const [a, b] = [String(row[at]), String(best[at])];
|
|
348
|
+
return (latest ? a > b : a < b) ? row : best;
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/** A numeric bucket's `method` value, as aggregateToSql() computes it. */
|
|
352
|
+
function valueColumns(method) {
|
|
353
|
+
const sql = `${aggregateToSql(method)} as agg_value`;
|
|
354
|
+
switch (method) {
|
|
355
|
+
case "min":
|
|
356
|
+
case "max":
|
|
357
|
+
return {
|
|
358
|
+
sql,
|
|
359
|
+
parts: [`${method}(value)`],
|
|
360
|
+
width: 1,
|
|
361
|
+
merge: (rows) => [
|
|
362
|
+
extreme(rows.map((r) => r[0]), method),
|
|
363
|
+
],
|
|
364
|
+
};
|
|
365
|
+
case "first":
|
|
366
|
+
case "last":
|
|
367
|
+
return {
|
|
368
|
+
sql,
|
|
369
|
+
parts: [`${method}(value)`, method === "first" ? "min(ts)" : "max(ts)"],
|
|
370
|
+
width: 1,
|
|
371
|
+
merge: (rows) => [byTime(rows, 1, method === "last")[0]],
|
|
372
|
+
};
|
|
373
|
+
case "mid":
|
|
374
|
+
return {
|
|
375
|
+
sql,
|
|
376
|
+
parts: ["min(value)", "max(value)"],
|
|
377
|
+
width: 1,
|
|
378
|
+
merge: (rows) => {
|
|
379
|
+
const low = extreme(rows.map((r) => r[0]), "min");
|
|
380
|
+
const high = extreme(rows.map((r) => r[1]), "max");
|
|
381
|
+
return [low === null || high === null ? null : (low + high) / 2];
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
default: {
|
|
385
|
+
// A mean of the two means would weigh a bucket's few archived rows
|
|
386
|
+
// as much as its many recorded ones.
|
|
387
|
+
return {
|
|
388
|
+
sql,
|
|
389
|
+
parts: ["sum(value)", "count(value)"],
|
|
390
|
+
width: 1,
|
|
391
|
+
merge: (rows) => {
|
|
392
|
+
const count = total(rows.map((r) => r[1]));
|
|
393
|
+
return [count === 0 ? null : total(rows.map((r) => r[0])) / count];
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/** ANGULAR_BUCKET_SQL's mean angle and floor, summed across the tables. */
|
|
400
|
+
const ANGULAR_COLUMNS = {
|
|
401
|
+
sql: ANGULAR_BUCKET_SQL,
|
|
402
|
+
parts: ["sum(sin(value))", "sum(cos(value))", "count(value)", "min(value)"],
|
|
403
|
+
width: 2,
|
|
404
|
+
merge: (rows) => {
|
|
405
|
+
const column = (i) => rows.map((r) => r[i]);
|
|
406
|
+
const count = total(column(2));
|
|
407
|
+
if (count === 0)
|
|
408
|
+
return [null, null];
|
|
409
|
+
const [sin, cos] = [total(column(0)) / count, total(column(1)) / count];
|
|
410
|
+
return [
|
|
411
|
+
Math.hypot(sin, cos) < NO_DIRECTION ? null : Math.atan2(sin, cos),
|
|
412
|
+
extreme(column(3), "min"),
|
|
413
|
+
];
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
/** HELD_SQL's last sample and its time, from the table that took it last. */
|
|
417
|
+
const HELD_COLUMNS = {
|
|
418
|
+
sql: HELD_SQL,
|
|
419
|
+
parts: ["last(value)", "last(ts)"],
|
|
420
|
+
width: 2,
|
|
421
|
+
merge: (rows) => byTime(rows, 1, true),
|
|
422
|
+
};
|
|
423
|
+
/** A position bucket's `pick` fix, as one lat/lon pair from one table. */
|
|
424
|
+
function positionColumns(pick) {
|
|
425
|
+
return {
|
|
426
|
+
sql: `${pick}(lat) as lat, ${pick}(lon) as lon`,
|
|
427
|
+
parts: [
|
|
428
|
+
`${pick}(lat)`,
|
|
429
|
+
`${pick}(lon)`,
|
|
430
|
+
pick === "first" ? "min(ts)" : "max(ts)",
|
|
431
|
+
],
|
|
432
|
+
width: 2,
|
|
433
|
+
merge: (rows) => byTime(rows, 2, pick === "last").slice(0, 2),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* The rows of two reads ordered by time (column 0), paired where both have
|
|
438
|
+
* the same time and in time order throughout. Yielded rather than collected,
|
|
439
|
+
* so a merge holds the two reads and its result, not a third copy.
|
|
440
|
+
*/
|
|
441
|
+
function* pairByTime(own, kept) {
|
|
442
|
+
let i = 0;
|
|
443
|
+
let j = 0;
|
|
444
|
+
while (i < own.length || j < kept.length) {
|
|
445
|
+
const a = i < own.length ? String(own[i][0]) : null;
|
|
446
|
+
const b = j < kept.length ? String(kept[j][0]) : null;
|
|
447
|
+
if (a !== null && (b === null || a < b))
|
|
448
|
+
yield [own[i++], null];
|
|
449
|
+
else if (a === null || b < a)
|
|
450
|
+
yield [null, kept[j++]];
|
|
451
|
+
else
|
|
452
|
+
yield [own[i++], kept[j++]];
|
|
453
|
+
}
|
|
454
|
+
}
|
|
331
455
|
export function createHistoryProviderV2(queryClient, selfContext,
|
|
332
456
|
// Opt-in, default off. Expansion multiplies one requested path into one
|
|
333
457
|
// query and one column PER recording source, so a request that was cheap
|
|
@@ -357,11 +481,71 @@ archive = NO_ARCHIVE) {
|
|
|
357
481
|
const since = new Date(Date.parse(from) - holdMs).toISOString();
|
|
358
482
|
return `ts >= '${since}' AND ts < '${from}' AND context = '${safeContext}'`;
|
|
359
483
|
}
|
|
360
|
-
/**
|
|
361
|
-
|
|
484
|
+
/**
|
|
485
|
+
* `base` sampled over the range in `groups`' columns, one row a bucket.
|
|
486
|
+
*
|
|
487
|
+
* With the archive, each table is sampled on its own and the two grids
|
|
488
|
+
* merged here. QuestDB samples their union only after sorting every row in
|
|
489
|
+
* range by time: the tracks plugin's history import asks once for
|
|
490
|
+
* everything since 1970, and that sort of 5.5 M own positions went past
|
|
491
|
+
* QuestDB's memory limit. Each table alone is sampled in the order it is
|
|
492
|
+
* stored, and the merge holds the two grids and the merged one.
|
|
493
|
+
*/
|
|
494
|
+
async function readSampled(base, withArchive, where, range, resolution, groups) {
|
|
495
|
+
const grid = sampleBy(range, resolution);
|
|
496
|
+
if (!withArchive) {
|
|
497
|
+
const columns = groups.map((group) => group.sql).join(", ");
|
|
498
|
+
const result = await queryClient.exec(`SELECT ts, ${columns} FROM ${base} WHERE ${where} ${grid} ORDER BY ts`);
|
|
499
|
+
return result.dataset;
|
|
500
|
+
}
|
|
501
|
+
// Aliased, since two groups may select the same aggregate, and led by
|
|
502
|
+
// the bucket's row count, which FILL(NULL) leaves null where it is 0.
|
|
503
|
+
const parts = ["count()", ...groups.flatMap((group) => group.parts)]
|
|
504
|
+
.map((part, i) => `${part} p${i}`)
|
|
505
|
+
.join(", ");
|
|
506
|
+
const read = async (table) => (await queryClient.exec(`SELECT ts, ${parts} FROM ${table} WHERE ${where} ${grid} ORDER BY ts`)).dataset;
|
|
507
|
+
const own = await read(base);
|
|
508
|
+
const kept = await read(ARCHIVE_TABLES[base]);
|
|
509
|
+
return Array.from(pairByTime(own, kept), (pair) => {
|
|
510
|
+
const filled = pair.filter((row) => row !== null && Number(row[1]) > 0);
|
|
511
|
+
const row = [(pair[0] ?? pair[1])[0]];
|
|
512
|
+
let at = 2;
|
|
513
|
+
for (const group of groups) {
|
|
514
|
+
row.push(...(filled.length === 0
|
|
515
|
+
? Array(group.width).fill(null)
|
|
516
|
+
: group.merge(filled.map((r) => r.slice(at, at + group.parts.length)))));
|
|
517
|
+
at += group.parts.length;
|
|
518
|
+
}
|
|
519
|
+
return row;
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* The first rows of `base` in the range, in time order, under the LIMIT
|
|
524
|
+
* every raw read has: with the archive, from each table on its own and
|
|
525
|
+
* merged here, for the reason readSampled() gives.
|
|
526
|
+
*/
|
|
527
|
+
async function readRaw(base, withArchive, columns, where) {
|
|
528
|
+
const read = async (table) => (await queryClient.exec(`SELECT ts, ${columns} FROM ${table} WHERE ${where} ORDER BY ts LIMIT ${RAW_LIMIT}`)).dataset;
|
|
529
|
+
const own = await read(base);
|
|
530
|
+
if (!withArchive)
|
|
531
|
+
return own;
|
|
532
|
+
const kept = await read(ARCHIVE_TABLES[base]);
|
|
533
|
+
const rows = [];
|
|
534
|
+
for (const pair of pairByTime(own, kept)) {
|
|
535
|
+
rows.push(...pair.filter((row) => row !== null));
|
|
536
|
+
if (rows.length >= RAW_LIMIT)
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
return rows.slice(0, RAW_LIMIT);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* What the numeric seed is read from: the window `seedRangeWhere` names,
|
|
543
|
+
* with the archive only for the own vessel, the only one it holds.
|
|
544
|
+
*/
|
|
545
|
+
function seedSource(range, own) {
|
|
362
546
|
const from = validateTimestamp(range.from);
|
|
363
547
|
const since = new Date(Date.parse(from) - holdMs).toISOString();
|
|
364
|
-
return readFrom("signalk", archive.covers("signalk", since, from));
|
|
548
|
+
return readFrom("signalk", own && archive.covers("signalk", since, from));
|
|
365
549
|
}
|
|
366
550
|
/** The numeric table's last `[value, ts]` before the range, or null. */
|
|
367
551
|
async function numericSeed(seedWhere, source) {
|
|
@@ -413,7 +597,7 @@ archive = NO_ARCHIVE) {
|
|
|
413
597
|
* leaves the path as a single unexpanded column rather than failing the
|
|
414
598
|
* whole request.
|
|
415
599
|
*/
|
|
416
|
-
async function distinctSources(path, range, safeContext) {
|
|
600
|
+
async function distinctSources(path, range, safeContext, withArchive) {
|
|
417
601
|
const safePath = validateIdentifier(path);
|
|
418
602
|
const rangeWhere = buildRangeWhere(range, safeContext);
|
|
419
603
|
const position = path === "navigation.position";
|
|
@@ -422,7 +606,7 @@ archive = NO_ARCHIVE) {
|
|
|
422
606
|
const tables = [
|
|
423
607
|
`SELECT DISTINCT source FROM ${table} WHERE ${rangeWhere}${pathWhere}`,
|
|
424
608
|
];
|
|
425
|
-
if (
|
|
609
|
+
if (withArchive) {
|
|
426
610
|
tables.push(`SELECT DISTINCT source FROM ${ARCHIVE_TABLES[table]} WHERE ${rangeWhere}${pathWhere}`);
|
|
427
611
|
}
|
|
428
612
|
if (!position) {
|
|
@@ -531,7 +715,7 @@ archive = NO_ARCHIVE) {
|
|
|
531
715
|
// what an empty bucket after it holds (see holdThroughEmptyBuckets).
|
|
532
716
|
return sampled
|
|
533
717
|
? `SELECT ts, ${pick}(value_str) as value_str, ${kindOf(pick)} as value_kind, last(value_str) as held_str, ${kindOf("last")} as held_kind, last(ts) as held_ts FROM signalk_str WHERE ${clause} ${sampleBy(range, resolution)} ORDER BY ts`
|
|
534
|
-
: `SELECT ts, value_str, ${kind} as value_kind FROM signalk_str WHERE ${clause} ORDER BY ts LIMIT
|
|
718
|
+
: `SELECT ts, value_str, ${kind} as value_kind FROM signalk_str WHERE ${clause} ORDER BY ts LIMIT ${RAW_LIMIT}`;
|
|
535
719
|
};
|
|
536
720
|
// A read racing ensureTables()'s migration — or an external QuestDB the
|
|
537
721
|
// plugin does not own — must degrade, not fail the request. Two columns
|
|
@@ -607,6 +791,16 @@ archive = NO_ARCHIVE) {
|
|
|
607
791
|
const requestedContext = query.context ?? "vessels.self";
|
|
608
792
|
const storedContext = normalizeContext(requestedContext, selfContext);
|
|
609
793
|
const safeContext = validateIdentifier(storedContext);
|
|
794
|
+
// Whether each table's reads include its archive. The archive holds only
|
|
795
|
+
// the own vessel (archive.ts), so another vessel's reads leave it out.
|
|
796
|
+
// Decided once, so the bucket budget counts the reads that then run even
|
|
797
|
+
// when a refresh of the archive's spans lands in between.
|
|
798
|
+
const archived = {
|
|
799
|
+
signalk: storedContext === "self" &&
|
|
800
|
+
archive.covers("signalk", range.from, range.to),
|
|
801
|
+
signalk_position: storedContext === "self" &&
|
|
802
|
+
archive.covers("signalk_position", range.from, range.to),
|
|
803
|
+
};
|
|
610
804
|
// Expand each spec into the COLUMNS it will produce. Under
|
|
611
805
|
// sourcePolicy=all a path with no explicit sourceRef becomes one column
|
|
612
806
|
// per distinct source that actually recorded it in range; everything else
|
|
@@ -631,7 +825,7 @@ archive = NO_ARCHIVE) {
|
|
|
631
825
|
`${MAX_EXPANDED_COLUMNS} columns — request fewer paths, or name ` +
|
|
632
826
|
`the sources explicitly with paths=<path>|<sourceRef>`);
|
|
633
827
|
}
|
|
634
|
-
const sources = await distinctSources(spec.path, range, safeContext);
|
|
828
|
+
const sources = await distinctSources(spec.path, range, safeContext, archived[spec.path === "navigation.position" ? "signalk_position" : "signalk"]);
|
|
635
829
|
if (sources.length === 0) {
|
|
636
830
|
// Nothing recorded in range, or a database with no `source` column:
|
|
637
831
|
// keep the unexpanded column so the caller still gets the series
|
|
@@ -691,10 +885,15 @@ archive = NO_ARCHIVE) {
|
|
|
691
885
|
// navigation.position is served by its own table and never falls back;
|
|
692
886
|
// a moving average has no text to fall back to.
|
|
693
887
|
const fallbackCapableSpecs = columns.filter((spec) => spec.path !== "navigation.position" && !isMovingAverage(spec.aggregate)).length;
|
|
888
|
+
// A column the archive covers costs one more: readSampled() samples the
|
|
889
|
+
// base table and the archive each on the whole grid. middle_index reads
|
|
890
|
+
// their union in one query.
|
|
891
|
+
const archivedSpecs = columns.filter((spec) => spec.aggregate !== "middle_index" &&
|
|
892
|
+
archived[spec.path === "navigation.position" ? "signalk_position" : "signalk"]).length;
|
|
694
893
|
if (sampledSpecs > 0 && query.resolution && query.resolution > 0) {
|
|
695
894
|
const rangeSec = (Date.parse(range.to) - Date.parse(range.from)) / 1000;
|
|
696
895
|
const bucketsPerSeries = Math.ceil(rangeSec / effectiveResolution(query.resolution));
|
|
697
|
-
const worstCaseQueries = sampledSpecs + fallbackCapableSpecs;
|
|
896
|
+
const worstCaseQueries = sampledSpecs + fallbackCapableSpecs + archivedSpecs;
|
|
698
897
|
const buckets = bucketsPerSeries * worstCaseQueries;
|
|
699
898
|
if (buckets > MAX_SAMPLE_BUCKETS) {
|
|
700
899
|
throw new Error(`resolution ${query.resolution}s over this range produces up to ` +
|
|
@@ -723,7 +922,11 @@ archive = NO_ARCHIVE) {
|
|
|
723
922
|
const sourceWhere = buildSourceWhere(spec);
|
|
724
923
|
const isPosition = spec.path === "navigation.position";
|
|
725
924
|
const base = isPosition ? "signalk_position" : "signalk";
|
|
726
|
-
|
|
925
|
+
// A middle row needs both tables' rows in one order, so middle_index
|
|
926
|
+
// reads their union, as the numeric seed does over its short window.
|
|
927
|
+
// Over a long range the archive covers that still sorts every row in
|
|
928
|
+
// it. Every other read goes through readSampled() or readRaw().
|
|
929
|
+
const union = () => readFrom(base, archived[base]);
|
|
727
930
|
if (isPosition) {
|
|
728
931
|
const where = buildRangeWhere(range, safeContext) + sourceWhere;
|
|
729
932
|
// Position is a co-recorded lat/lon pair. first, last and
|
|
@@ -736,21 +939,20 @@ archive = NO_ARCHIVE) {
|
|
|
736
939
|
: "first";
|
|
737
940
|
if (posAgg !== spec.aggregate)
|
|
738
941
|
valuesList[specIndex].method = "first";
|
|
739
|
-
let
|
|
942
|
+
let dataset;
|
|
740
943
|
if (posAgg === "middle_index") {
|
|
741
|
-
|
|
944
|
+
dataset = (await queryClient.exec(middleRowSql(union(), "lat, lon", where, range, query.resolution))).dataset;
|
|
742
945
|
}
|
|
743
946
|
else if (query.resolution && query.resolution > 0) {
|
|
744
|
-
|
|
947
|
+
dataset = await readSampled(base, archived[base], where, range, query.resolution, [positionColumns(posAgg)]);
|
|
745
948
|
}
|
|
746
949
|
else {
|
|
747
|
-
|
|
950
|
+
dataset = await readRaw(base, archived[base], "lat, lon", where);
|
|
748
951
|
}
|
|
749
|
-
const result = await queryClient.exec(sql);
|
|
750
952
|
// A position goes out as a `[longitude, latitude]` pair — GeoJSON
|
|
751
953
|
// order, as the History API's OpenAPI schema defines and the other
|
|
752
954
|
// providers emit — not as the data model's `{latitude, longitude}`.
|
|
753
|
-
const rows =
|
|
955
|
+
const rows = dataset.map((row) => [
|
|
754
956
|
row[0],
|
|
755
957
|
row[1] !== null && row[2] !== null ? [row[2], row[1]] : null,
|
|
756
958
|
]);
|
|
@@ -759,7 +961,7 @@ archive = NO_ARCHIVE) {
|
|
|
759
961
|
}
|
|
760
962
|
const where = `${buildRangeWhere(range, safeContext)} AND path = '${safePath}'${sourceWhere}`;
|
|
761
963
|
const seedWhere = `${seedRangeWhere(range, safeContext)} AND path = '${safePath}'${sourceWhere}`;
|
|
762
|
-
const seedFrom = seedSource(range);
|
|
964
|
+
const seedFrom = seedSource(range, storedContext === "self");
|
|
763
965
|
if (isMovingAverage(spec.aggregate)) {
|
|
764
966
|
// The window runs over the series at the requested resolution — one
|
|
765
967
|
// averaged bucket per sample, the grid every other column is on — so
|
|
@@ -771,17 +973,13 @@ archive = NO_ARCHIVE) {
|
|
|
771
973
|
// gap ages values out instead of stretching the window across it.
|
|
772
974
|
const window = movingAverageWindow(spec);
|
|
773
975
|
const angular = isAngular(unitsOf(spec.path, requestedContext));
|
|
774
|
-
const bucket = angular ?
|
|
976
|
+
const bucket = angular ? ANGULAR_COLUMNS : valueColumns("average");
|
|
775
977
|
const sampled = !!query.resolution && query.resolution > 0;
|
|
776
|
-
const sql = sampled
|
|
777
|
-
? `SELECT ts, ${bucket}, ${HELD_SQL} FROM ${table} WHERE ${where} ${sampleBy(range, query.resolution)} ORDER BY ts`
|
|
778
|
-
: `SELECT ts, value FROM ${table} WHERE ${where} ORDER BY ts LIMIT 10000`;
|
|
779
|
-
const result = await queryClient.exec(sql);
|
|
780
978
|
// Held before averaging: an unchanged value is still a sample of
|
|
781
979
|
// the window, not a hole in it.
|
|
782
980
|
const dataset = sampled
|
|
783
|
-
? await holdNumericBuckets(
|
|
784
|
-
:
|
|
981
|
+
? await holdNumericBuckets(await readSampled(base, archived[base], where, range, query.resolution, [bucket, HELD_COLUMNS]), seedWhere, seedFrom)
|
|
982
|
+
: await readRaw(base, archived[base], "value", where);
|
|
785
983
|
// An angular series keeps one convention throughout: [0, 2π) unless
|
|
786
984
|
// a sample (or a bucket's lowest sample) was negative.
|
|
787
985
|
const floors = dataset.map((r) => angular && sampled
|
|
@@ -823,7 +1021,7 @@ archive = NO_ARCHIVE) {
|
|
|
823
1021
|
columnData.set(specIndex, strResult.rows);
|
|
824
1022
|
};
|
|
825
1023
|
if (spec.aggregate === "middle_index") {
|
|
826
|
-
const result = await queryClient.exec(middleRowSql(
|
|
1024
|
+
const result = await queryClient.exec(middleRowSql(union(), "value", where, range, query.resolution));
|
|
827
1025
|
const rows = result.dataset.map((r) => [
|
|
828
1026
|
r[0],
|
|
829
1027
|
r[1],
|
|
@@ -840,17 +1038,13 @@ archive = NO_ARCHIVE) {
|
|
|
840
1038
|
// the caller's choice, and first, last and middle_index are samples.
|
|
841
1039
|
const angularMean = spec.aggregate === "average" &&
|
|
842
1040
|
isAngular(unitsOf(spec.path, requestedContext));
|
|
843
|
-
const
|
|
844
|
-
?
|
|
845
|
-
:
|
|
1041
|
+
const value = angularMean
|
|
1042
|
+
? ANGULAR_COLUMNS
|
|
1043
|
+
: valueColumns(spec.aggregate);
|
|
846
1044
|
const sampled = !!query.resolution && query.resolution > 0;
|
|
847
|
-
const sql = sampled
|
|
848
|
-
? `SELECT ts, ${aggExpr}, ${HELD_SQL} FROM ${table} WHERE ${where} ${sampleBy(range, query.resolution)} ORDER BY ts`
|
|
849
|
-
: `SELECT ts, value FROM ${table} WHERE ${where} ORDER BY ts LIMIT 10000`;
|
|
850
|
-
const result = await queryClient.exec(sql);
|
|
851
1045
|
const dataset = sampled
|
|
852
|
-
? await holdNumericBuckets(
|
|
853
|
-
:
|
|
1046
|
+
? await holdNumericBuckets(await readSampled(base, archived[base], where, range, query.resolution, [value, HELD_COLUMNS]), seedWhere, seedFrom)
|
|
1047
|
+
: await readRaw(base, archived[base], "value", where);
|
|
854
1048
|
const rows = dataset.map((row) => [
|
|
855
1049
|
row[0],
|
|
856
1050
|
angularMean && row.length > 2 && row[1] !== null
|