signalk-questdb 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ilp-writer.d.ts +1 -0
- package/dist/ilp-writer.js +17 -1
- package/dist/ilp-writer.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/migration.js +40 -45
- package/dist/migration.js.map +1 -1
- package/dist/wal-monitor.js +1 -1
- package/dist/wal-monitor.js.map +1 -1
- package/package.json +2 -2
- package/dist/test/config.test.d.ts +0 -1
- package/dist/test/config.test.js +0 -161
- package/dist/test/config.test.js.map +0 -1
- package/dist/test/configpanel-responses.test.d.ts +0 -1
- package/dist/test/configpanel-responses.test.js +0 -197
- package/dist/test/configpanel-responses.test.js.map +0 -1
- package/dist/test/console-proxy.test.d.ts +0 -1
- package/dist/test/console-proxy.test.js +0 -664
- package/dist/test/console-proxy.test.js.map +0 -1
- package/dist/test/container-env.test.d.ts +0 -1
- package/dist/test/container-env.test.js +0 -70
- package/dist/test/container-env.test.js.map +0 -1
- package/dist/test/delta-routing.test.d.ts +0 -1
- package/dist/test/delta-routing.test.js +0 -258
- package/dist/test/delta-routing.test.js.map +0 -1
- package/dist/test/full-export-range.test.d.ts +0 -1
- package/dist/test/full-export-range.test.js +0 -78
- package/dist/test/full-export-range.test.js.map +0 -1
- package/dist/test/history-v1.test.d.ts +0 -1
- package/dist/test/history-v1.test.js +0 -676
- package/dist/test/history-v1.test.js.map +0 -1
- package/dist/test/history-v2.test.d.ts +0 -1
- package/dist/test/history-v2.test.js +0 -1001
- package/dist/test/history-v2.test.js.map +0 -1
- package/dist/test/host-limits.test.d.ts +0 -1
- package/dist/test/host-limits.test.js +0 -81
- package/dist/test/host-limits.test.js.map +0 -1
- package/dist/test/ilp-writer.test.d.ts +0 -1
- package/dist/test/ilp-writer.test.js +0 -419
- package/dist/test/ilp-writer.test.js.map +0 -1
- package/dist/test/influx-detect.test.d.ts +0 -1
- package/dist/test/influx-detect.test.js +0 -287
- package/dist/test/influx-detect.test.js.map +0 -1
- package/dist/test/migration.test.d.ts +0 -1
- package/dist/test/migration.test.js +0 -851
- package/dist/test/migration.test.js.map +0 -1
- package/dist/test/package-e2e.test.d.ts +0 -1
- package/dist/test/package-e2e.test.js +0 -306
- package/dist/test/package-e2e.test.js.map +0 -1
- package/dist/test/path-matcher.test.d.ts +0 -1
- package/dist/test/path-matcher.test.js +0 -224
- package/dist/test/path-matcher.test.js.map +0 -1
- package/dist/test/query-client.test.d.ts +0 -1
- package/dist/test/query-client.test.js +0 -299
- package/dist/test/query-client.test.js.map +0 -1
- package/dist/test/questdb-endpoint.test.d.ts +0 -1
- package/dist/test/questdb-endpoint.test.js +0 -297
- package/dist/test/questdb-endpoint.test.js.map +0 -1
- package/dist/test/restore.test.d.ts +0 -1
- package/dist/test/restore.test.js +0 -543
- package/dist/test/restore.test.js.map +0 -1
- package/dist/test/time-range.test.d.ts +0 -1
- package/dist/test/time-range.test.js +0 -53
- package/dist/test/time-range.test.js.map +0 -1
- package/dist/test/wal-monitor.test.d.ts +0 -1
- package/dist/test/wal-monitor.test.js +0 -405
- package/dist/test/wal-monitor.test.js.map +0 -1
|
@@ -1,851 +0,0 @@
|
|
|
1
|
-
import { test, describe } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import { coerceValue, listBuckets, mergePositionRows, parseAnnotatedCsv, rfc3339ToNanos, toSignalKPath, MigrationRun, runMigration, } from "../migration.js";
|
|
4
|
-
/** Minimal stand-in for the ILP writer, recording what would be written. */
|
|
5
|
-
class FakeWriter {
|
|
6
|
-
numbers = [];
|
|
7
|
-
strings = [];
|
|
8
|
-
positions = [];
|
|
9
|
-
/** Mirrors the real writer: every enqueued line counts until drained. */
|
|
10
|
-
get pendingLines() {
|
|
11
|
-
return this.numbers.length + this.strings.length + this.positions.length;
|
|
12
|
-
}
|
|
13
|
-
writeAtNanos(path, context, value, ts, source) {
|
|
14
|
-
this.numbers.push({ path, context, value, ts, source });
|
|
15
|
-
}
|
|
16
|
-
writeStringAtNanos(path, context, value, ts, kind, source) {
|
|
17
|
-
this.strings.push({ path, context, value, ts, kind, source });
|
|
18
|
-
}
|
|
19
|
-
writePositionAtNanos(context, position, ts, source) {
|
|
20
|
-
this.positions.push({
|
|
21
|
-
context,
|
|
22
|
-
lat: position.latitude,
|
|
23
|
-
lon: position.longitude,
|
|
24
|
-
ts,
|
|
25
|
-
source,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
describe("timestamp precision", () => {
|
|
30
|
-
// The tables dedup on (ts, path, context, source). Truncating to
|
|
31
|
-
// milliseconds would make sub-millisecond points collide and upsert.
|
|
32
|
-
test("sub-millisecond digits survive the conversion", () => {
|
|
33
|
-
const a = rfc3339ToNanos("2024-03-01T12:00:00.000200Z");
|
|
34
|
-
const b = rfc3339ToNanos("2024-03-01T12:00:00.000400Z");
|
|
35
|
-
assert.notStrictEqual(a, b);
|
|
36
|
-
assert.strictEqual(b - a, 200000n);
|
|
37
|
-
});
|
|
38
|
-
test("nanosecond precision is preserved exactly", () => {
|
|
39
|
-
const ns = rfc3339ToNanos("2024-03-01T12:00:00.123456789Z");
|
|
40
|
-
assert.strictEqual(ns % 1000000000n, 123456789n);
|
|
41
|
-
});
|
|
42
|
-
test("a plain second-resolution instant still parses", () => {
|
|
43
|
-
const ns = rfc3339ToNanos("2024-03-01T12:00:00Z");
|
|
44
|
-
assert.strictEqual(ns, BigInt(Date.parse("2024-03-01T12:00:00Z")) * 1000000n);
|
|
45
|
-
});
|
|
46
|
-
test("garbage yields null rather than a bogus instant", () => {
|
|
47
|
-
assert.strictEqual(rfc3339ToNanos("not-a-time"), null);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
describe("annotated CSV parsing", () => {
|
|
51
|
-
test("reads Flux records, ignoring annotations and the gutter column", () => {
|
|
52
|
-
const csv = [
|
|
53
|
-
"#datatype,string,long,dateTime:RFC3339,double,string",
|
|
54
|
-
"#group,false,false,false,false,true",
|
|
55
|
-
"#default,_result,,,,",
|
|
56
|
-
",result,table,_time,_value,_field",
|
|
57
|
-
",,0,2024-03-01T12:00:00Z,3.5,value",
|
|
58
|
-
",,0,2024-03-01T12:00:01Z,3.6,value",
|
|
59
|
-
].join("\n");
|
|
60
|
-
const rows = parseAnnotatedCsv(csv);
|
|
61
|
-
assert.strictEqual(rows.length, 2);
|
|
62
|
-
assert.strictEqual(rows[0].values["_value"], "3.5");
|
|
63
|
-
assert.strictEqual(rows[1].values["_time"], "2024-03-01T12:00:01Z");
|
|
64
|
-
// The unnamed gutter column must not become a key.
|
|
65
|
-
assert.ok(!("" in rows[0].values));
|
|
66
|
-
});
|
|
67
|
-
test("a second table with a different header is read correctly", () => {
|
|
68
|
-
const csv = [
|
|
69
|
-
"#datatype,string,long,dateTime:RFC3339,double",
|
|
70
|
-
",result,table,_time,_value",
|
|
71
|
-
",,0,2024-03-01T12:00:00Z,1.0",
|
|
72
|
-
"",
|
|
73
|
-
"#datatype,string,long,dateTime:RFC3339,string",
|
|
74
|
-
",result,table,_time,_value",
|
|
75
|
-
",,1,2024-03-01T12:00:00Z,hello",
|
|
76
|
-
].join("\n");
|
|
77
|
-
const rows = parseAnnotatedCsv(csv);
|
|
78
|
-
assert.strictEqual(rows.length, 2);
|
|
79
|
-
assert.strictEqual(rows[1].values["_value"], "hello");
|
|
80
|
-
});
|
|
81
|
-
// Verified against a live 2.9.1: a string value holding a newline comes back
|
|
82
|
-
// as a quoted cell spanning two physical lines.
|
|
83
|
-
test("a quoted value containing a newline stays one record", () => {
|
|
84
|
-
const csv = [
|
|
85
|
-
"#datatype,string,long,dateTime:RFC3339,string,string",
|
|
86
|
-
",result,table,_time,_value,_field",
|
|
87
|
-
',,0,2024-03-01T12:00:00Z,"line1',
|
|
88
|
-
'line2",value',
|
|
89
|
-
].join("\n");
|
|
90
|
-
const rows = parseAnnotatedCsv(csv);
|
|
91
|
-
assert.strictEqual(rows.length, 1);
|
|
92
|
-
assert.strictEqual(rows[0].values["_value"], "line1\nline2");
|
|
93
|
-
assert.strictEqual(rows[0].values["_field"], "value");
|
|
94
|
-
});
|
|
95
|
-
// The \r is stripped per PHYSICAL line before joining. A CRLF file whose
|
|
96
|
-
// quoted value spans two lines would otherwise keep the first line's \r
|
|
97
|
-
// inside the joined value — a stray carriage return in the imported string.
|
|
98
|
-
test("a quoted value spanning two CRLF lines keeps no stray carriage return", () => {
|
|
99
|
-
const csv = [
|
|
100
|
-
"#datatype,string,long,dateTime:RFC3339,string,string",
|
|
101
|
-
",result,table,_time,_value,_field",
|
|
102
|
-
',,0,2024-03-01T12:00:00Z,"line1',
|
|
103
|
-
'line2",value',
|
|
104
|
-
].join("\r\n");
|
|
105
|
-
const rows = parseAnnotatedCsv(csv);
|
|
106
|
-
assert.strictEqual(rows.length, 1);
|
|
107
|
-
assert.strictEqual(rows[0].values["_value"], "line1\nline2");
|
|
108
|
-
assert.ok(!rows[0].values["_value"].includes("\r"), "a stray carriage return survived the join");
|
|
109
|
-
});
|
|
110
|
-
test("an escaped double quote does not unbalance the record", () => {
|
|
111
|
-
const csv = [",result,table,_value", ',,0,"say ""hi"" now"'].join("\n");
|
|
112
|
-
const rows = parseAnnotatedCsv(csv);
|
|
113
|
-
assert.strictEqual(rows.length, 1);
|
|
114
|
-
assert.strictEqual(rows[0].values["_value"], 'say "hi" now');
|
|
115
|
-
});
|
|
116
|
-
// A genuine STRING value of "3.5" is byte-identical to the number 3.5 in the
|
|
117
|
-
// CSV body — verified against a live 2.9.1. Only the #datatype annotation
|
|
118
|
-
// tells them apart, so it has to reach coerceValue or the string lands in
|
|
119
|
-
// the numeric table.
|
|
120
|
-
test("the #datatype of each column is carried alongside the values", () => {
|
|
121
|
-
const csv = [
|
|
122
|
-
"#datatype,string,long,dateTime:RFC3339,string,string",
|
|
123
|
-
",result,table,_time,_value,_field",
|
|
124
|
-
",,0,2024-03-01T12:00:00Z,3.5,value",
|
|
125
|
-
].join("\n");
|
|
126
|
-
const rows = parseAnnotatedCsv(csv);
|
|
127
|
-
assert.strictEqual(rows[0].types["_value"], "string");
|
|
128
|
-
assert.strictEqual(coerceValue(rows[0].values["_value"], rows[0].types["_value"]), "3.5");
|
|
129
|
-
});
|
|
130
|
-
test("a double column still coerces to a number", () => {
|
|
131
|
-
const csv = [
|
|
132
|
-
"#datatype,string,long,dateTime:RFC3339,double,string",
|
|
133
|
-
",result,table,_time,_value,_field",
|
|
134
|
-
",,0,2024-03-01T12:00:00Z,3.5,value",
|
|
135
|
-
].join("\n");
|
|
136
|
-
const rows = parseAnnotatedCsv(csv);
|
|
137
|
-
assert.strictEqual(coerceValue(rows[0].values["_value"], rows[0].types["_value"]), 3.5);
|
|
138
|
-
});
|
|
139
|
-
test("quoted cells containing commas stay intact", () => {
|
|
140
|
-
const csv = [",result,table,_value", ',,0,"a,b"'].join("\n");
|
|
141
|
-
const rows = parseAnnotatedCsv(csv);
|
|
142
|
-
assert.strictEqual(rows[0].values["_value"], "a,b");
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
describe("value and path mapping", () => {
|
|
146
|
-
test("the conventional `value` field maps to the measurement as the path", () => {
|
|
147
|
-
assert.strictEqual(toSignalKPath("navigation.speedOverGround", "value"), "navigation.speedOverGround");
|
|
148
|
-
});
|
|
149
|
-
test("a named field is appended so two fields cannot collide", () => {
|
|
150
|
-
assert.strictEqual(toSignalKPath("environment.wind", "speedApparent"), "environment.wind.speedApparent");
|
|
151
|
-
});
|
|
152
|
-
test("numeric text becomes a number, other text stays a string", () => {
|
|
153
|
-
assert.strictEqual(coerceValue("3.5"), 3.5);
|
|
154
|
-
assert.strictEqual(coerceValue("hello"), "hello");
|
|
155
|
-
assert.strictEqual(coerceValue("true"), true);
|
|
156
|
-
assert.strictEqual(coerceValue("false"), false);
|
|
157
|
-
});
|
|
158
|
-
// A STRING column holding "true" is the word, not a boolean. Detecting the
|
|
159
|
-
// literal before honouring the declared type recorded it as a boolean and
|
|
160
|
-
// lost the distinction value_kind exists to preserve.
|
|
161
|
-
test("a declared string type wins over boolean detection", () => {
|
|
162
|
-
assert.strictEqual(coerceValue("true", "string"), "true");
|
|
163
|
-
assert.strictEqual(coerceValue("false", "string"), "false");
|
|
164
|
-
// With no declared type the literal still reads as a boolean.
|
|
165
|
-
assert.strictEqual(coerceValue("true"), true);
|
|
166
|
-
assert.strictEqual(coerceValue("true", "boolean"), true);
|
|
167
|
-
});
|
|
168
|
-
test("an empty value is null rather than 0", () => {
|
|
169
|
-
// Number("") is 0 — importing a gap as a real zero reading would be a
|
|
170
|
-
// silent data corruption.
|
|
171
|
-
assert.strictEqual(coerceValue(""), null);
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
describe("position reassembly", () => {
|
|
175
|
-
test("lat/lon fields at one instant become a single position", () => {
|
|
176
|
-
const ts = 1700000000000000000n;
|
|
177
|
-
const rows = [
|
|
178
|
-
{ tsNanos: ts, field: "latitude", value: 52.1 },
|
|
179
|
-
{ tsNanos: ts, field: "longitude", value: 4.3 },
|
|
180
|
-
];
|
|
181
|
-
const merged = mergePositionRows("navigation.position", rows);
|
|
182
|
-
assert.strictEqual(merged.rows.length, 1);
|
|
183
|
-
assert.strictEqual(merged.dropped, 0);
|
|
184
|
-
assert.deepStrictEqual(merged.rows[0].value, {
|
|
185
|
-
latitude: 52.1,
|
|
186
|
-
longitude: 4.3,
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
test("a lat with no matching lon is dropped, not written as a number", () => {
|
|
190
|
-
const rows = [{ tsNanos: 1n, field: "latitude", value: 52.1 }];
|
|
191
|
-
const merged = mergePositionRows("navigation.position", rows);
|
|
192
|
-
assert.deepStrictEqual(merged.rows, []);
|
|
193
|
-
// Reported, not silently vanished — it lands in the run's skipped total.
|
|
194
|
-
assert.strictEqual(merged.dropped, 1);
|
|
195
|
-
});
|
|
196
|
-
test("non-position measurements pass through untouched", () => {
|
|
197
|
-
const rows = [{ tsNanos: 1n, field: "value", value: 3.5 }];
|
|
198
|
-
assert.deepStrictEqual(mergePositionRows("navigation.speedOverGround", rows), {
|
|
199
|
-
rows,
|
|
200
|
-
dropped: 0,
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
describe("bucket listing", () => {
|
|
205
|
-
const page = (names, next) => new Response(JSON.stringify({
|
|
206
|
-
buckets: names.map((name, i) => ({ id: `id${i}`, name })),
|
|
207
|
-
links: next ? { self: "/x", next } : { self: "/x" },
|
|
208
|
-
}), { status: 200 });
|
|
209
|
-
// 100 is the API maximum per page. A live 2.9.1 holding 106 buckets returned
|
|
210
|
-
// only 98 from a single request — eight silently missing, and a bucket the
|
|
211
|
-
// operator cannot see is one they cannot import from.
|
|
212
|
-
test("follows links.next until every page is collected", async () => {
|
|
213
|
-
const urls = [];
|
|
214
|
-
const fakeFetch = (async (url) => {
|
|
215
|
-
const u = String(url);
|
|
216
|
-
urls.push(u);
|
|
217
|
-
if (urls.length === 1)
|
|
218
|
-
return page(["a", "b"], "/api/v2/buckets?offset=2");
|
|
219
|
-
return page(["c"]);
|
|
220
|
-
});
|
|
221
|
-
const buckets = await listBuckets({ url: "http://x", type: "influxdb2", auth: { token: "t" } }, fakeFetch);
|
|
222
|
-
assert.deepStrictEqual(buckets.map((b) => b.name), ["a", "b", "c"]);
|
|
223
|
-
assert.strictEqual(urls.length, 2);
|
|
224
|
-
});
|
|
225
|
-
// `links.next` is server-supplied. One that points at the page just fetched
|
|
226
|
-
// would otherwise loop until the page cap.
|
|
227
|
-
test("stops when links.next repeats the current page", async () => {
|
|
228
|
-
let calls = 0;
|
|
229
|
-
const fakeFetch = (async (url) => {
|
|
230
|
-
calls++;
|
|
231
|
-
// Always name the very URL being requested.
|
|
232
|
-
return page(["a"], String(url));
|
|
233
|
-
});
|
|
234
|
-
const buckets = await listBuckets({ url: "http://x", type: "influxdb2" }, fakeFetch);
|
|
235
|
-
assert.strictEqual(calls, 1, `looped ${calls} times`);
|
|
236
|
-
assert.deepStrictEqual(buckets.map((b) => b.name), ["a"]);
|
|
237
|
-
});
|
|
238
|
-
test("stops at the page cap when next never ends", async () => {
|
|
239
|
-
let calls = 0;
|
|
240
|
-
const fakeFetch = (async () => {
|
|
241
|
-
calls++;
|
|
242
|
-
// A distinct link every time, so only the cap can stop it.
|
|
243
|
-
return page([`b${calls}`], `/api/v2/buckets?offset=${calls}`);
|
|
244
|
-
});
|
|
245
|
-
await listBuckets({ url: "http://x", type: "influxdb2" }, fakeFetch);
|
|
246
|
-
assert.strictEqual(calls, 100, `expected the 100-page cap, got ${calls}`);
|
|
247
|
-
});
|
|
248
|
-
// validateInfluxUrl deliberately preserves a sub-path for reverse-proxied
|
|
249
|
-
// instances. Resolving an absolute path against the base would drop it and
|
|
250
|
-
// probe the wrong host root.
|
|
251
|
-
test("keeps a configured path prefix on the first page", async () => {
|
|
252
|
-
const urls = [];
|
|
253
|
-
const fakeFetch = (async (url) => {
|
|
254
|
-
urls.push(String(url));
|
|
255
|
-
return page(["a"]);
|
|
256
|
-
});
|
|
257
|
-
await listBuckets({ url: "http://host:8086/influx", type: "influxdb2" }, fakeFetch);
|
|
258
|
-
assert.strictEqual(urls[0], "http://host:8086/influx/api/v2/buckets?limit=100");
|
|
259
|
-
});
|
|
260
|
-
test("system buckets are hidden", async () => {
|
|
261
|
-
const fakeFetch = (async () => page(["_monitoring", "_tasks", "boatdata"]));
|
|
262
|
-
const buckets = await listBuckets({ url: "http://x", type: "influxdb2" }, fakeFetch);
|
|
263
|
-
assert.deepStrictEqual(buckets.map((b) => b.name), ["boatdata"]);
|
|
264
|
-
});
|
|
265
|
-
// 1.x answers a rejected query with HTTP 200 and an `error` member, so
|
|
266
|
-
// without the check an auth failure reads as "no databases".
|
|
267
|
-
test("a 1.x error member is surfaced, not read as an empty list", async () => {
|
|
268
|
-
const fakeFetch = (async () => new Response(JSON.stringify({ results: [{ error: "authorization failed" }] }), { status: 200 }));
|
|
269
|
-
await assert.rejects(() => listBuckets({ url: "http://x", type: "influxdb1" }, fakeFetch), /authorization failed/);
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
describe("import run", () => {
|
|
273
|
-
const fluxCsv = (rows) => [
|
|
274
|
-
"#datatype,string,long,dateTime:RFC3339,double,string",
|
|
275
|
-
",result,table,_time,_value,_field",
|
|
276
|
-
...rows.map(([t, v, f]) => `,,0,${t},${v},${f}`),
|
|
277
|
-
].join("\n");
|
|
278
|
-
test("numeric rows land in the numeric table with their original timestamps", async () => {
|
|
279
|
-
const fakeFetch = (async () => new Response(fluxCsv([["2024-03-01T12:00:00.000500Z", "3.5", "value"]]), {
|
|
280
|
-
status: 200,
|
|
281
|
-
}));
|
|
282
|
-
const writer = new FakeWriter();
|
|
283
|
-
const run = new MigrationRun("t1", "http://x", "b");
|
|
284
|
-
await runMigration({
|
|
285
|
-
url: "http://x",
|
|
286
|
-
type: "influxdb2",
|
|
287
|
-
bucket: "b",
|
|
288
|
-
from: "2024-03-01T00:00:00Z",
|
|
289
|
-
to: "2024-03-02T00:00:00Z",
|
|
290
|
-
context: "vessels.self",
|
|
291
|
-
measurements: ["navigation.speedOverGround"],
|
|
292
|
-
}, writer, run, { fetchImpl: fakeFetch });
|
|
293
|
-
assert.strictEqual(run.state, "done");
|
|
294
|
-
assert.strictEqual(writer.numbers.length, 1);
|
|
295
|
-
assert.strictEqual(writer.numbers[0].path, "navigation.speedOverGround");
|
|
296
|
-
assert.strictEqual(writer.numbers[0].value, 3.5);
|
|
297
|
-
assert.strictEqual(writer.numbers[0].source, "influxdb-import");
|
|
298
|
-
// The 500µs must survive; a Date round-trip would have flattened it.
|
|
299
|
-
assert.strictEqual(writer.numbers[0].ts, rfc3339ToNanos("2024-03-01T12:00:00.000500Z"));
|
|
300
|
-
assert.strictEqual(run.progress.written, 1);
|
|
301
|
-
});
|
|
302
|
-
test("string and boolean rows go to the string table, booleans tagged", async () => {
|
|
303
|
-
const fakeFetch = (async () => new Response(fluxCsv([
|
|
304
|
-
["2024-03-01T12:00:00Z", "docked", "value"],
|
|
305
|
-
["2024-03-01T12:00:01Z", "true", "value"],
|
|
306
|
-
]), { status: 200 }));
|
|
307
|
-
const writer = new FakeWriter();
|
|
308
|
-
const run = new MigrationRun("t2", "http://x", "b");
|
|
309
|
-
await runMigration({
|
|
310
|
-
url: "http://x",
|
|
311
|
-
type: "influxdb2",
|
|
312
|
-
bucket: "b",
|
|
313
|
-
from: "2024-03-01T00:00:00Z",
|
|
314
|
-
to: "2024-03-02T00:00:00Z",
|
|
315
|
-
context: "vessels.self",
|
|
316
|
-
measurements: ["navigation.state"],
|
|
317
|
-
}, writer, run, { fetchImpl: fakeFetch });
|
|
318
|
-
assert.strictEqual(writer.strings.length, 2);
|
|
319
|
-
assert.strictEqual(writer.strings[0].value, "docked");
|
|
320
|
-
assert.strictEqual(writer.strings[0].kind, undefined);
|
|
321
|
-
assert.strictEqual(writer.strings[1].value, "true");
|
|
322
|
-
assert.strictEqual(writer.strings[1].kind, "boolean");
|
|
323
|
-
});
|
|
324
|
-
test("a lat/lon pair imports as one position tagged with the source", async () => {
|
|
325
|
-
const csv = [
|
|
326
|
-
"#datatype,string,long,dateTime:RFC3339,double,string",
|
|
327
|
-
",result,table,_time,_value,_field",
|
|
328
|
-
",,0,2024-03-01T12:00:00Z,52.1,latitude",
|
|
329
|
-
",,0,2024-03-01T12:00:00Z,4.3,longitude",
|
|
330
|
-
].join("\n");
|
|
331
|
-
const fakeFetch = (async () => new Response(csv, { status: 200 }));
|
|
332
|
-
const writer = new FakeWriter();
|
|
333
|
-
const run = new MigrationRun("tpos", "http://x", "b");
|
|
334
|
-
await runMigration({
|
|
335
|
-
url: "http://x",
|
|
336
|
-
type: "influxdb2",
|
|
337
|
-
bucket: "b",
|
|
338
|
-
from: "2024-03-01T00:00:00Z",
|
|
339
|
-
to: "2024-03-02T00:00:00Z",
|
|
340
|
-
context: "vessels.self",
|
|
341
|
-
measurements: ["navigation.position"],
|
|
342
|
-
}, writer, run, { fetchImpl: fakeFetch });
|
|
343
|
-
assert.strictEqual(writer.positions.length, 1);
|
|
344
|
-
assert.strictEqual(writer.positions[0].lat, 52.1);
|
|
345
|
-
assert.strictEqual(writer.positions[0].source, "influxdb-import");
|
|
346
|
-
// The pair became ONE position, not two numeric paths.
|
|
347
|
-
assert.strictEqual(writer.numbers.length, 0);
|
|
348
|
-
});
|
|
349
|
-
// A half-pair never becomes a row, so it has to be counted explicitly or
|
|
350
|
-
// read - written - skipped stops adding up.
|
|
351
|
-
test("a dropped half-pair position is counted as skipped", async () => {
|
|
352
|
-
const csv = [
|
|
353
|
-
"#datatype,string,long,dateTime:RFC3339,double,string",
|
|
354
|
-
",result,table,_time,_value,_field",
|
|
355
|
-
",,0,2024-03-01T12:00:00Z,52.1,latitude",
|
|
356
|
-
].join("\n");
|
|
357
|
-
const fakeFetch = (async () => new Response(csv, { status: 200 }));
|
|
358
|
-
const writer = new FakeWriter();
|
|
359
|
-
const run = new MigrationRun("thalf", "http://x", "b");
|
|
360
|
-
await runMigration({
|
|
361
|
-
url: "http://x",
|
|
362
|
-
type: "influxdb2",
|
|
363
|
-
bucket: "b",
|
|
364
|
-
from: "2024-03-01T00:00:00Z",
|
|
365
|
-
to: "2024-03-02T00:00:00Z",
|
|
366
|
-
context: "vessels.self",
|
|
367
|
-
measurements: ["navigation.position"],
|
|
368
|
-
}, writer, run, { fetchImpl: fakeFetch });
|
|
369
|
-
assert.strictEqual(writer.positions.length, 0);
|
|
370
|
-
assert.strictEqual(run.progress.skipped, 1);
|
|
371
|
-
assert.strictEqual(run.progress.read, 1);
|
|
372
|
-
});
|
|
373
|
-
// Reading must pause while the writer is backed up, or the ILP buffer cap
|
|
374
|
-
// discards the OLDEST queued lines — losing the history being imported.
|
|
375
|
-
test("reading waits for the writer to drain past the resume mark", async () => {
|
|
376
|
-
const writer = new FakeWriter();
|
|
377
|
-
// Start above the high-water mark so the loop must wait.
|
|
378
|
-
for (let i = 0; i < 25_000; i++) {
|
|
379
|
-
writer.numbers.push({
|
|
380
|
-
path: "p",
|
|
381
|
-
context: "c",
|
|
382
|
-
value: 1,
|
|
383
|
-
ts: 1n,
|
|
384
|
-
source: "s",
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
let sleeps = 0;
|
|
388
|
-
const sleep = async () => {
|
|
389
|
-
sleeps++;
|
|
390
|
-
// Drain a chunk per tick; the loop must keep waiting until it is under
|
|
391
|
-
// the RESUME mark, not merely under the HIGH mark.
|
|
392
|
-
writer.numbers.splice(0, 6_000);
|
|
393
|
-
};
|
|
394
|
-
const fakeFetch = (async () => new Response([
|
|
395
|
-
"#datatype,string,long,dateTime:RFC3339,double,string",
|
|
396
|
-
",result,table,_time,_value,_field",
|
|
397
|
-
].join("\n"), { status: 200 }));
|
|
398
|
-
const run = new MigrationRun("tbp", "http://x", "b");
|
|
399
|
-
await runMigration({
|
|
400
|
-
url: "http://x",
|
|
401
|
-
type: "influxdb2",
|
|
402
|
-
bucket: "b",
|
|
403
|
-
from: "2024-03-01T00:00:00Z",
|
|
404
|
-
to: "2024-03-02T00:00:00Z",
|
|
405
|
-
context: "vessels.self",
|
|
406
|
-
measurements: ["a"],
|
|
407
|
-
}, writer, run, { fetchImpl: fakeFetch, sleep });
|
|
408
|
-
assert.ok(sleeps > 0, "should have waited at least once");
|
|
409
|
-
// 25k -> under 5k at 6k per tick takes more than the single tick that
|
|
410
|
-
// would have sufficed to merely fall under the 20k high-water mark.
|
|
411
|
-
assert.ok(sleeps >= 4, `expected to drain to the resume mark, only slept ${sleeps}x`);
|
|
412
|
-
});
|
|
413
|
-
// A bare `SELECT *` returns TAG columns next to the fields, and every
|
|
414
|
-
// non-time column is read as a field — so a point tagged source=n2k would
|
|
415
|
-
// import a bogus `<measurement>.source = "n2k"` path. Signal K's InfluxDB
|
|
416
|
-
// writers tag their points, so this is the normal case.
|
|
417
|
-
test("the 1.x query asks for fields only, not tags", async () => {
|
|
418
|
-
let seenQuery = "";
|
|
419
|
-
const fakeFetch = (async (url) => {
|
|
420
|
-
seenQuery = decodeURIComponent(String(url));
|
|
421
|
-
return new Response(JSON.stringify({ results: [{ series: [] }] }), {
|
|
422
|
-
status: 200,
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
const writer = new FakeWriter();
|
|
426
|
-
const run = new MigrationRun("tql", "http://x", "signalk");
|
|
427
|
-
await runMigration({
|
|
428
|
-
url: "http://x",
|
|
429
|
-
type: "influxdb1",
|
|
430
|
-
bucket: "signalk",
|
|
431
|
-
from: "2024-03-01T00:00:00Z",
|
|
432
|
-
to: "2024-03-02T00:00:00Z",
|
|
433
|
-
context: "vessels.self",
|
|
434
|
-
measurements: ["navigation.speedOverGround"],
|
|
435
|
-
}, writer, run, { fetchImpl: fakeFetch });
|
|
436
|
-
assert.match(seenQuery, /SELECT \*::field FROM/);
|
|
437
|
-
});
|
|
438
|
-
// The own vessel is stored as the literal "self" by the live recorder and
|
|
439
|
-
// both history providers normalise to it. An import that wrote
|
|
440
|
-
// `vessels.<uuid>` instead would file rows in a context no query looks at:
|
|
441
|
-
// present in the table, invisible through the API.
|
|
442
|
-
test("rows are written under exactly the context they are given", async () => {
|
|
443
|
-
const csv = [
|
|
444
|
-
"#datatype,string,long,dateTime:RFC3339,double,string",
|
|
445
|
-
",result,table,_time,_value,_field",
|
|
446
|
-
",,0,2024-03-01T12:00:00Z,3.5,value",
|
|
447
|
-
].join("\n");
|
|
448
|
-
const fakeFetch = (async () => new Response(csv, { status: 200 }));
|
|
449
|
-
const writer = new FakeWriter();
|
|
450
|
-
const run = new MigrationRun("tctx", "http://x", "b");
|
|
451
|
-
await runMigration({
|
|
452
|
-
url: "http://x",
|
|
453
|
-
type: "influxdb2",
|
|
454
|
-
bucket: "b",
|
|
455
|
-
from: "2024-03-01T00:00:00Z",
|
|
456
|
-
to: "2024-03-02T00:00:00Z",
|
|
457
|
-
context: "self",
|
|
458
|
-
measurements: ["navigation.speedOverGround"],
|
|
459
|
-
}, writer, run, { fetchImpl: fakeFetch });
|
|
460
|
-
assert.strictEqual(writer.numbers[0].context, "self");
|
|
461
|
-
});
|
|
462
|
-
// The numeric assertion above would still pass if only the string path
|
|
463
|
-
// regressed, so the string table is checked on its own context.
|
|
464
|
-
test("string rows are written under the given context too", async () => {
|
|
465
|
-
const csv = [
|
|
466
|
-
"#datatype,string,long,dateTime:RFC3339,string,string",
|
|
467
|
-
",result,table,_time,_value,_field",
|
|
468
|
-
",,0,2024-03-01T12:00:00Z,docked,value",
|
|
469
|
-
].join("\n");
|
|
470
|
-
const fakeFetch = (async () => new Response(csv, { status: 200 }));
|
|
471
|
-
const writer = new FakeWriter();
|
|
472
|
-
const run = new MigrationRun("tctxstr", "http://x", "b");
|
|
473
|
-
await runMigration({
|
|
474
|
-
url: "http://x",
|
|
475
|
-
type: "influxdb2",
|
|
476
|
-
bucket: "b",
|
|
477
|
-
from: "2024-03-01T00:00:00Z",
|
|
478
|
-
to: "2024-03-02T00:00:00Z",
|
|
479
|
-
context: "self",
|
|
480
|
-
measurements: ["navigation.state"],
|
|
481
|
-
}, writer, run, { fetchImpl: fakeFetch });
|
|
482
|
-
assert.strictEqual(writer.strings.length, 1);
|
|
483
|
-
assert.strictEqual(writer.strings[0].context, "self");
|
|
484
|
-
});
|
|
485
|
-
// `pendingLines` only falls when QuestDB actually accepts data, so a
|
|
486
|
-
// QuestDB that is down or wedged left the drain wait spinning forever: the
|
|
487
|
-
// run sat at "running" with frozen counters and nothing logged, and the
|
|
488
|
-
// only way out was cancelling by hand.
|
|
489
|
-
test("a writer that never drains fails the run instead of hanging", async () => {
|
|
490
|
-
const stuck = {
|
|
491
|
-
pendingLines: 999_999,
|
|
492
|
-
writeAtNanos() { },
|
|
493
|
-
writeStringAtNanos() { },
|
|
494
|
-
writePositionAtNanos() { },
|
|
495
|
-
};
|
|
496
|
-
let slept = 0;
|
|
497
|
-
const run = new MigrationRun("tdrain", "http://x", "b");
|
|
498
|
-
await runMigration({
|
|
499
|
-
url: "http://x",
|
|
500
|
-
type: "influxdb2",
|
|
501
|
-
bucket: "b",
|
|
502
|
-
from: "2024-03-01T00:00:00Z",
|
|
503
|
-
to: "2024-03-02T00:00:00Z",
|
|
504
|
-
context: "vessels.self",
|
|
505
|
-
measurements: ["a"],
|
|
506
|
-
}, stuck, run, {
|
|
507
|
-
fetchImpl: (async () => new Response("", { status: 200 })),
|
|
508
|
-
// Virtual clock: the loop counts elapsed ms itself, so an
|
|
509
|
-
// instant sleep still reaches the timeout deterministically.
|
|
510
|
-
sleep: async () => {
|
|
511
|
-
slept++;
|
|
512
|
-
},
|
|
513
|
-
});
|
|
514
|
-
assert.strictEqual(run.state, "failed");
|
|
515
|
-
assert.match(run.error ?? "", /not accepting writes/);
|
|
516
|
-
// Bounded, not spinning: 5 min at 200ms is 1500 iterations.
|
|
517
|
-
assert.ok(slept <= 1600, `slept ${slept} times`);
|
|
518
|
-
});
|
|
519
|
-
// A dense window can carry far more rows than the writer's cap, so waiting
|
|
520
|
-
// only at window boundaries would let the buffer sail past MAX_BUFFER_LINES
|
|
521
|
-
// mid-window — dropping its oldest lines.
|
|
522
|
-
test("backpressure is applied inside a window, not only between windows", async () => {
|
|
523
|
-
// Deliberately BELOW the high-water mark: the window-boundary check must
|
|
524
|
-
// pass straight through, so any wait that happens is proof the check
|
|
525
|
-
// inside the row loop fired. Starting above it made this test vacuous —
|
|
526
|
-
// the boundary check alone satisfied both assertions and the test passed
|
|
527
|
-
// with the intra-window block deleted.
|
|
528
|
-
let pending = 0;
|
|
529
|
-
const writer = {
|
|
530
|
-
writtenCount: 0,
|
|
531
|
-
get pendingLines() {
|
|
532
|
-
return pending;
|
|
533
|
-
},
|
|
534
|
-
writeAtNanos() {
|
|
535
|
-
pending++;
|
|
536
|
-
this.writtenCount++;
|
|
537
|
-
},
|
|
538
|
-
writeStringAtNanos() { },
|
|
539
|
-
writePositionAtNanos() { },
|
|
540
|
-
};
|
|
541
|
-
let drainedAfterRows = 0;
|
|
542
|
-
const csv = [
|
|
543
|
-
"#datatype,string,long,dateTime:RFC3339,double,string",
|
|
544
|
-
",result,table,_time,_value,_field",
|
|
545
|
-
...Array.from({ length: 25_000 }, (_, i) => `,,0,2024-03-01T12:00:00.${String(i).padStart(6, "0")}Z,${i},value`),
|
|
546
|
-
].join("\n");
|
|
547
|
-
const run = new MigrationRun("tintra", "http://x", "b");
|
|
548
|
-
await runMigration({
|
|
549
|
-
url: "http://x",
|
|
550
|
-
type: "influxdb2",
|
|
551
|
-
bucket: "b",
|
|
552
|
-
from: "2024-03-01T00:00:00Z",
|
|
553
|
-
to: "2024-03-02T00:00:00Z",
|
|
554
|
-
context: "vessels.self",
|
|
555
|
-
measurements: ["a"],
|
|
556
|
-
}, writer, run, {
|
|
557
|
-
fetchImpl: (async () => new Response(csv, { status: 200 })),
|
|
558
|
-
// Draining is what a healthy writer does; count that it was needed.
|
|
559
|
-
sleep: async () => {
|
|
560
|
-
// Only a drain that happens once rows have been written can have
|
|
561
|
-
// come from the intra-window check.
|
|
562
|
-
if (writer.writtenCount > 0)
|
|
563
|
-
drainedAfterRows++;
|
|
564
|
-
pending = 0;
|
|
565
|
-
},
|
|
566
|
-
});
|
|
567
|
-
assert.strictEqual(run.state, "done");
|
|
568
|
-
assert.ok(drainedAfterRows > 0, "expected a drain triggered from INSIDE the row loop");
|
|
569
|
-
// Every row still landed rather than being dropped by the buffer cap.
|
|
570
|
-
assert.strictEqual(run.progress.written, 25_000);
|
|
571
|
-
});
|
|
572
|
-
// InfluxQL returns the field's ACTUAL JSON type — a string field as a JSON
|
|
573
|
-
// string, a float as a number, a boolean as a boolean (verified against
|
|
574
|
-
// 1.8.10). Re-parsing strings through coerceValue destroyed that: a genuine
|
|
575
|
-
// string "3.5" landed in the NUMERIC table and a string "true" was recorded
|
|
576
|
-
// as a boolean, both indistinguishable from the real thing.
|
|
577
|
-
test("1.x preserves the JSON field type instead of re-parsing it", async () => {
|
|
578
|
-
const body = {
|
|
579
|
-
results: [
|
|
580
|
-
{
|
|
581
|
-
series: [
|
|
582
|
-
{
|
|
583
|
-
name: "m",
|
|
584
|
-
columns: [
|
|
585
|
-
"time",
|
|
586
|
-
"strnum",
|
|
587
|
-
"strbool",
|
|
588
|
-
"realnum",
|
|
589
|
-
"realbool",
|
|
590
|
-
"empty",
|
|
591
|
-
],
|
|
592
|
-
values: [[1709294400000000000, "3.5", "true", 3.5, true, ""]],
|
|
593
|
-
},
|
|
594
|
-
],
|
|
595
|
-
},
|
|
596
|
-
],
|
|
597
|
-
};
|
|
598
|
-
const fakeFetch = (async () => new Response(JSON.stringify(body), {
|
|
599
|
-
status: 200,
|
|
600
|
-
}));
|
|
601
|
-
const writer = new FakeWriter();
|
|
602
|
-
const run = new MigrationRun("t1xtype", "http://x", "db");
|
|
603
|
-
await runMigration({
|
|
604
|
-
url: "http://x",
|
|
605
|
-
type: "influxdb1",
|
|
606
|
-
bucket: "db",
|
|
607
|
-
from: "2024-03-01T00:00:00Z",
|
|
608
|
-
to: "2024-03-02T00:00:00Z",
|
|
609
|
-
context: "vessels.self",
|
|
610
|
-
measurements: ["m"],
|
|
611
|
-
}, writer, run, { fetchImpl: fakeFetch });
|
|
612
|
-
// Only the genuine number reaches the numeric table.
|
|
613
|
-
assert.deepStrictEqual(writer.numbers.map((n) => n.path), ["m.realnum"]);
|
|
614
|
-
// An empty 1.x string field is a gap, not an empty reading: it must not
|
|
615
|
-
// be recorded at all.
|
|
616
|
-
assert.ok(!writer.strings.some((w) => w.path === "m.empty"), "an empty string field should be skipped, not recorded");
|
|
617
|
-
const strings = Object.fromEntries(writer.strings.map((w) => [w.path, { value: w.value, kind: w.kind }]));
|
|
618
|
-
// A string field holding "3.5" stays a string.
|
|
619
|
-
assert.deepStrictEqual(strings["m.strnum"], {
|
|
620
|
-
value: "3.5",
|
|
621
|
-
kind: undefined,
|
|
622
|
-
});
|
|
623
|
-
// A string field holding "true" is the word, not a boolean.
|
|
624
|
-
assert.deepStrictEqual(strings["m.strbool"], {
|
|
625
|
-
value: "true",
|
|
626
|
-
kind: undefined,
|
|
627
|
-
});
|
|
628
|
-
// A real boolean is still tagged as one.
|
|
629
|
-
assert.deepStrictEqual(strings["m.realbool"], {
|
|
630
|
-
value: "true",
|
|
631
|
-
kind: "boolean",
|
|
632
|
-
});
|
|
633
|
-
});
|
|
634
|
-
// The last window must not read past the requested end. An unclamped
|
|
635
|
-
// `start + windowMs` would query beyond `to` and import history the
|
|
636
|
-
// operator did not ask for.
|
|
637
|
-
test("the final window is clamped to the requested end", async () => {
|
|
638
|
-
const stops = [];
|
|
639
|
-
const fakeFetch = (async (_url, init) => {
|
|
640
|
-
const m = /stop: ([^)]+)\)/.exec(String(init?.body ?? ""));
|
|
641
|
-
if (m)
|
|
642
|
-
stops.push(m[1]);
|
|
643
|
-
return new Response("", { status: 200 });
|
|
644
|
-
});
|
|
645
|
-
const writer = new FakeWriter();
|
|
646
|
-
const run = new MigrationRun("tclamp", "http://x", "b");
|
|
647
|
-
await runMigration({
|
|
648
|
-
url: "http://x",
|
|
649
|
-
type: "influxdb2",
|
|
650
|
-
bucket: "b",
|
|
651
|
-
// A 6h range with the default 24h window: one window, clamped.
|
|
652
|
-
from: "2024-03-01T00:00:00Z",
|
|
653
|
-
to: "2024-03-01T06:00:00Z",
|
|
654
|
-
context: "vessels.self",
|
|
655
|
-
measurements: ["a"],
|
|
656
|
-
}, writer, run, { fetchImpl: fakeFetch });
|
|
657
|
-
assert.strictEqual(stops.length, 1);
|
|
658
|
-
assert.strictEqual(stops[0], "2024-03-01T06:00:00.000Z");
|
|
659
|
-
});
|
|
660
|
-
// A value no table can hold must be COUNTED, not quietly ignored — the
|
|
661
|
-
// module's contract is that read = written + skipped.
|
|
662
|
-
test("an unmappable value increments the skipped counter", async () => {
|
|
663
|
-
const csv = [
|
|
664
|
-
"#datatype,string,long,dateTime:RFC3339,string,string",
|
|
665
|
-
",result,table,_time,_value,_field",
|
|
666
|
-
// An empty value coerces to null, which no table can hold.
|
|
667
|
-
",,0,2024-03-01T12:00:00Z,,value",
|
|
668
|
-
].join("\n");
|
|
669
|
-
const fakeFetch = (async () => new Response(csv, { status: 200 }));
|
|
670
|
-
const writer = new FakeWriter();
|
|
671
|
-
const run = new MigrationRun("tskip", "http://x", "b");
|
|
672
|
-
await runMigration({
|
|
673
|
-
url: "http://x",
|
|
674
|
-
type: "influxdb2",
|
|
675
|
-
bucket: "b",
|
|
676
|
-
from: "2024-03-01T00:00:00Z",
|
|
677
|
-
to: "2024-03-02T00:00:00Z",
|
|
678
|
-
context: "vessels.self",
|
|
679
|
-
measurements: ["a"],
|
|
680
|
-
}, writer, run, { fetchImpl: fakeFetch });
|
|
681
|
-
assert.strictEqual(run.progress.written, 0);
|
|
682
|
-
assert.strictEqual(run.progress.skipped, 1);
|
|
683
|
-
assert.strictEqual(run.progress.read, run.progress.written + run.progress.skipped);
|
|
684
|
-
});
|
|
685
|
-
// Cancellation is checked per ROW, not only per window. Plugin stop relies
|
|
686
|
-
// on it: a run that kept going would write to a disconnected writer.
|
|
687
|
-
test("cancelling mid-window stops within a few rows", async () => {
|
|
688
|
-
const csv = [
|
|
689
|
-
"#datatype,string,long,dateTime:RFC3339,double,string",
|
|
690
|
-
",result,table,_time,_value,_field",
|
|
691
|
-
...Array.from({ length: 3000 }, (_, i) => `,,0,2024-03-01T12:00:00.${String(i).padStart(6, "0")}Z,${i},value`),
|
|
692
|
-
].join("\n");
|
|
693
|
-
const fakeFetch = (async () => new Response(csv, { status: 200 }));
|
|
694
|
-
const run = new MigrationRun("tcancelrow", "http://x", "b");
|
|
695
|
-
const writer = new (class extends FakeWriter {
|
|
696
|
-
writeAtNanos(path, context, value, ts, source) {
|
|
697
|
-
super.writeAtNanos(path, context, value, ts, source);
|
|
698
|
-
if (this.numbers.length === 5)
|
|
699
|
-
run.cancel();
|
|
700
|
-
}
|
|
701
|
-
})();
|
|
702
|
-
await runMigration({
|
|
703
|
-
url: "http://x",
|
|
704
|
-
type: "influxdb2",
|
|
705
|
-
bucket: "b",
|
|
706
|
-
from: "2024-03-01T00:00:00Z",
|
|
707
|
-
to: "2024-03-02T00:00:00Z",
|
|
708
|
-
context: "vessels.self",
|
|
709
|
-
measurements: ["a"],
|
|
710
|
-
}, writer, run, { fetchImpl: fakeFetch });
|
|
711
|
-
assert.strictEqual(run.state, "cancelled");
|
|
712
|
-
// Stopped promptly rather than draining all 3000 rows.
|
|
713
|
-
assert.ok(writer.numbers.length < 20, `wrote ${writer.numbers.length} rows after cancel`);
|
|
714
|
-
});
|
|
715
|
-
// The ILP writer drops its OLDEST buffered lines when the cap is reached
|
|
716
|
-
// (disconnected, or QuestDB not keeping up). Those rows counted as written
|
|
717
|
-
// but never reached the database, so the run must not report clean success.
|
|
718
|
-
test("rows dropped by the writer fail the run rather than reporting success", async () => {
|
|
719
|
-
const csv = [
|
|
720
|
-
"#datatype,string,long,dateTime:RFC3339,double,string",
|
|
721
|
-
",result,table,_time,_value,_field",
|
|
722
|
-
",,0,2024-03-01T12:00:00Z,1,value",
|
|
723
|
-
].join("\n");
|
|
724
|
-
const writer = Object.assign(new FakeWriter(), {
|
|
725
|
-
// Monotonic counter, as the real writer exposes it.
|
|
726
|
-
droppedLineCount: 0,
|
|
727
|
-
});
|
|
728
|
-
const run = new MigrationRun("tdrop", "http://x", "b");
|
|
729
|
-
await runMigration({
|
|
730
|
-
url: "http://x",
|
|
731
|
-
type: "influxdb2",
|
|
732
|
-
bucket: "b",
|
|
733
|
-
from: "2024-03-01T00:00:00Z",
|
|
734
|
-
to: "2024-03-02T00:00:00Z",
|
|
735
|
-
context: "vessels.self",
|
|
736
|
-
measurements: ["a"],
|
|
737
|
-
}, writer, run, {
|
|
738
|
-
fetchImpl: (async () => {
|
|
739
|
-
// Simulate the buffer overflowing during the run.
|
|
740
|
-
writer.droppedLineCount += 42;
|
|
741
|
-
return new Response(csv, { status: 200 });
|
|
742
|
-
}),
|
|
743
|
-
});
|
|
744
|
-
assert.strictEqual(run.state, "failed");
|
|
745
|
-
assert.match(run.error ?? "", /42 buffered rows were dropped/);
|
|
746
|
-
});
|
|
747
|
-
test("a run with no writer drops still reports success", async () => {
|
|
748
|
-
const csv = [
|
|
749
|
-
"#datatype,string,long,dateTime:RFC3339,double,string",
|
|
750
|
-
",result,table,_time,_value,_field",
|
|
751
|
-
",,0,2024-03-01T12:00:00Z,1,value",
|
|
752
|
-
].join("\n");
|
|
753
|
-
// A NON-zero starting count: only the delta across this run counts, since
|
|
754
|
-
// the counter is shared with the live recorder.
|
|
755
|
-
const writer = Object.assign(new FakeWriter(), { droppedLineCount: 17 });
|
|
756
|
-
const run = new MigrationRun("tnodrop", "http://x", "b");
|
|
757
|
-
await runMigration({
|
|
758
|
-
url: "http://x",
|
|
759
|
-
type: "influxdb2",
|
|
760
|
-
bucket: "b",
|
|
761
|
-
from: "2024-03-01T00:00:00Z",
|
|
762
|
-
to: "2024-03-02T00:00:00Z",
|
|
763
|
-
context: "vessels.self",
|
|
764
|
-
measurements: ["a"],
|
|
765
|
-
}, writer, run, {
|
|
766
|
-
fetchImpl: (async () => new Response(csv, { status: 200 })),
|
|
767
|
-
});
|
|
768
|
-
assert.strictEqual(run.state, "done");
|
|
769
|
-
});
|
|
770
|
-
test("cancelling stops the run and reports it as cancelled", async () => {
|
|
771
|
-
const fakeFetch = (async () => new Response(fluxCsv([["2024-03-01T12:00:00Z", "1", "value"]]), {
|
|
772
|
-
status: 200,
|
|
773
|
-
}));
|
|
774
|
-
const writer = new FakeWriter();
|
|
775
|
-
const run = new MigrationRun("t3", "http://x", "b");
|
|
776
|
-
run.cancel();
|
|
777
|
-
await runMigration({
|
|
778
|
-
url: "http://x",
|
|
779
|
-
type: "influxdb2",
|
|
780
|
-
bucket: "b",
|
|
781
|
-
from: "2024-03-01T00:00:00Z",
|
|
782
|
-
to: "2024-03-02T00:00:00Z",
|
|
783
|
-
context: "vessels.self",
|
|
784
|
-
measurements: ["a", "b"],
|
|
785
|
-
}, writer, run, { fetchImpl: fakeFetch });
|
|
786
|
-
assert.strictEqual(run.state, "cancelled");
|
|
787
|
-
assert.strictEqual(writer.numbers.length, 0);
|
|
788
|
-
});
|
|
789
|
-
test("a failing read marks the run failed and keeps the reason", async () => {
|
|
790
|
-
const fakeFetch = (async () => new Response("boom", { status: 500 }));
|
|
791
|
-
const writer = new FakeWriter();
|
|
792
|
-
const run = new MigrationRun("t4", "http://x", "b");
|
|
793
|
-
await runMigration({
|
|
794
|
-
url: "http://x",
|
|
795
|
-
type: "influxdb2",
|
|
796
|
-
bucket: "b",
|
|
797
|
-
from: "2024-03-01T00:00:00Z",
|
|
798
|
-
to: "2024-03-02T00:00:00Z",
|
|
799
|
-
context: "vessels.self",
|
|
800
|
-
measurements: ["a"],
|
|
801
|
-
}, writer, run, { fetchImpl: fakeFetch });
|
|
802
|
-
assert.strictEqual(run.state, "failed");
|
|
803
|
-
assert.match(run.error ?? "", /500/);
|
|
804
|
-
});
|
|
805
|
-
// `start += windowMs` with a non-positive window never advances.
|
|
806
|
-
test("a non-positive window does not spin forever", async () => {
|
|
807
|
-
let calls = 0;
|
|
808
|
-
const fakeFetch = (async () => {
|
|
809
|
-
calls++;
|
|
810
|
-
return new Response([
|
|
811
|
-
"#datatype,string,long,dateTime:RFC3339,double,string",
|
|
812
|
-
",result,table,_time,_value,_field",
|
|
813
|
-
].join("\n"), { status: 200 });
|
|
814
|
-
});
|
|
815
|
-
const writer = new FakeWriter();
|
|
816
|
-
const run = new MigrationRun("t6", "http://x", "b");
|
|
817
|
-
await runMigration({
|
|
818
|
-
url: "http://x",
|
|
819
|
-
type: "influxdb2",
|
|
820
|
-
bucket: "b",
|
|
821
|
-
from: "2024-03-01T00:00:00Z",
|
|
822
|
-
to: "2024-03-02T00:00:00Z",
|
|
823
|
-
context: "vessels.self",
|
|
824
|
-
measurements: ["a"],
|
|
825
|
-
windowMs: 0,
|
|
826
|
-
}, writer, run, { fetchImpl: fakeFetch });
|
|
827
|
-
assert.strictEqual(run.state, "done");
|
|
828
|
-
// One day at the default 24h window is a single read.
|
|
829
|
-
assert.strictEqual(calls, 1);
|
|
830
|
-
});
|
|
831
|
-
test("an inverted range is rejected before any read", async () => {
|
|
832
|
-
const writer = new FakeWriter();
|
|
833
|
-
const run = new MigrationRun("t5", "http://x", "b");
|
|
834
|
-
await runMigration({
|
|
835
|
-
url: "http://x",
|
|
836
|
-
type: "influxdb2",
|
|
837
|
-
bucket: "b",
|
|
838
|
-
from: "2024-03-02T00:00:00Z",
|
|
839
|
-
to: "2024-03-01T00:00:00Z",
|
|
840
|
-
context: "vessels.self",
|
|
841
|
-
measurements: ["a"],
|
|
842
|
-
}, writer, run, {
|
|
843
|
-
fetchImpl: (async () => {
|
|
844
|
-
throw new Error("should not be called");
|
|
845
|
-
}),
|
|
846
|
-
});
|
|
847
|
-
assert.strictEqual(run.state, "failed");
|
|
848
|
-
assert.match(run.error ?? "", /after/);
|
|
849
|
-
});
|
|
850
|
-
});
|
|
851
|
-
//# sourceMappingURL=migration.test.js.map
|