sliftutils 1.4.47 → 1.4.48
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/index.d.ts
CHANGED
|
@@ -1141,7 +1141,13 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseFormat" {
|
|
|
1141
1141
|
export declare const EMPTY_BUFFER: Buffer;
|
|
1142
1142
|
export declare const ABSENT: unique symbol;
|
|
1143
1143
|
export declare const TARGET_FILE_BYTES: number;
|
|
1144
|
-
export
|
|
1144
|
+
export interface BuiltFile {
|
|
1145
|
+
buffer: Buffer;
|
|
1146
|
+
minKey: string;
|
|
1147
|
+
maxKey: string;
|
|
1148
|
+
rowCount: number;
|
|
1149
|
+
}
|
|
1150
|
+
export declare function buildFileBuffer(rows: Record<string, unknown>[], times: number[], targetBytes?: number): BuiltFile[];
|
|
1145
1151
|
export type BaseBulkDatabaseReader = {
|
|
1146
1152
|
rowCount: number;
|
|
1147
1153
|
totalBytes: number;
|
package/package.json
CHANGED
|
@@ -929,9 +929,9 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
929
929
|
const timestamp = nextFileTime();
|
|
930
930
|
const now = Date.now();
|
|
931
931
|
const times = rows.map(() => now);
|
|
932
|
-
for (const
|
|
932
|
+
for (const built of buildFileBuffer(rows, times)) {
|
|
933
933
|
const name = newFileName(timestamp);
|
|
934
|
-
await storage.set(name, encodeCompressedBlocks(buffer));
|
|
934
|
+
await storage.set(name, encodeCompressedBlocks(built.buffer));
|
|
935
935
|
}
|
|
936
936
|
this.resetReader();
|
|
937
937
|
void this.maybeMerge();
|
|
@@ -1270,10 +1270,19 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
1270
1270
|
// Write all outputs BEFORE deleting any input, so a throw mid-write just leaves duplicates.
|
|
1271
1271
|
const newNames: string[] = [];
|
|
1272
1272
|
if (rows.length) {
|
|
1273
|
-
|
|
1273
|
+
const built = buildFileBuffer(rows, times);
|
|
1274
|
+
// When the result is big enough to split across several files, log each sub-write (its key
|
|
1275
|
+
// range + start/finish), since each storage.set can be large/slow and we want to see progress.
|
|
1276
|
+
const split = built.length > 1;
|
|
1277
|
+
if (split) console.log(`${blue(this.name)} merge: output split into ${built.length} files (> ${fmtBytes(TARGET_FILE_BYTES)} each)`);
|
|
1278
|
+
for (let i = 0; i < built.length; i++) {
|
|
1279
|
+
const part = built[i];
|
|
1274
1280
|
const name = newFileName(timestamp);
|
|
1275
|
-
|
|
1281
|
+
const subStart = Date.now();
|
|
1282
|
+
if (split) console.log(` [${i + 1}/${built.length}] writing ${formatNumber(part.rowCount)} rows [${part.minKey} .. ${part.maxKey}] → ${name} at ${new Date(subStart).toISOString()}`);
|
|
1283
|
+
await storage.set(name, encodeCompressedBlocks(part.buffer));
|
|
1276
1284
|
newNames.push(name);
|
|
1285
|
+
if (split) console.log(` [${i + 1}/${built.length}] wrote ${name} (${fmtBytes((await storage.getInfo(name).catch(() => undefined))?.size ?? 0)}) in ${formatTime(Date.now() - subStart)}`);
|
|
1277
1286
|
}
|
|
1278
1287
|
}
|
|
1279
1288
|
// Carry surviving tombstones forward only if older files exist outside this merge that they still
|
|
@@ -4,7 +4,13 @@ export declare const KEY_COLUMN = "key";
|
|
|
4
4
|
export declare const EMPTY_BUFFER: Buffer;
|
|
5
5
|
export declare const ABSENT: unique symbol;
|
|
6
6
|
export declare const TARGET_FILE_BYTES: number;
|
|
7
|
-
export
|
|
7
|
+
export interface BuiltFile {
|
|
8
|
+
buffer: Buffer;
|
|
9
|
+
minKey: string;
|
|
10
|
+
maxKey: string;
|
|
11
|
+
rowCount: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function buildFileBuffer(rows: Record<string, unknown>[], times: number[], targetBytes?: number): BuiltFile[];
|
|
8
14
|
export type BaseBulkDatabaseReader = {
|
|
9
15
|
rowCount: number;
|
|
10
16
|
totalBytes: number;
|
|
@@ -226,14 +226,25 @@ function buildOneFile(rows: Record<string, unknown>[], times: number[]): Buffer
|
|
|
226
226
|
return Buffer.concat([lengthPrefix, headerBuf, ...blobs]);
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
//
|
|
229
|
+
// One complete, independent file: the encoded buffer plus its key range + row count (the caller logs the
|
|
230
|
+
// range when a merge splits across several files).
|
|
231
|
+
export interface BuiltFile { buffer: Buffer; minKey: string; maxKey: string; rowCount: number; }
|
|
232
|
+
|
|
233
|
+
// Returns one complete, independent file per chunk of rows. Rows are first sorted by key, then
|
|
230
234
|
// partitioned into key-contiguous chunks of ~targetBytes each — so every returned file is key-sorted
|
|
231
235
|
// (tight minKey/maxKey for the read-skip + merge planner) and stays near the target size, and no
|
|
232
236
|
// single column blob / Buffer.concat approaches the ~2GB limit. The chunks have disjoint key ranges,
|
|
233
|
-
// so the caller just writes each as its own file. A normal-sized write returns a single
|
|
237
|
+
// so the caller just writes each as its own file. A normal-sized write returns a single file.
|
|
234
238
|
// `times[i]` is row i's write-time, stored per row so reads resolve a key to its latest value by time.
|
|
235
|
-
export function buildFileBuffer(rows: Record<string, unknown>[], times: number[], targetBytes = TARGET_FILE_BYTES):
|
|
236
|
-
|
|
239
|
+
export function buildFileBuffer(rows: Record<string, unknown>[], times: number[], targetBytes = TARGET_FILE_BYTES): BuiltFile[] {
|
|
240
|
+
// A chunk is already key-sorted, so its first/last row are its min/max key.
|
|
241
|
+
const make = (rs: Record<string, unknown>[], ts: number[]): BuiltFile => ({
|
|
242
|
+
buffer: buildOneFile(rs, ts),
|
|
243
|
+
minKey: rs.length ? (rs[0][KEY_COLUMN] as string) : "",
|
|
244
|
+
maxKey: rs.length ? (rs[rs.length - 1][KEY_COLUMN] as string) : "",
|
|
245
|
+
rowCount: rs.length,
|
|
246
|
+
});
|
|
247
|
+
if (rows.length === 0) return [make([], [])];
|
|
237
248
|
// Sort rows + their times together by key so each output file is key-contiguous.
|
|
238
249
|
const order = rows.map((_, i) => i).sort((a, b) => {
|
|
239
250
|
const ka = rows[a][KEY_COLUMN] as string;
|
|
@@ -242,19 +253,19 @@ export function buildFileBuffer(rows: Record<string, unknown>[], times: number[]
|
|
|
242
253
|
});
|
|
243
254
|
const sortedRows = order.map(i => rows[i]);
|
|
244
255
|
const sortedTimes = order.map(i => times[i]);
|
|
245
|
-
const result:
|
|
256
|
+
const result: BuiltFile[] = [];
|
|
246
257
|
let chunkStart = 0;
|
|
247
258
|
let chunkBytes = 0;
|
|
248
259
|
for (let i = 0; i < sortedRows.length; i++) {
|
|
249
260
|
const rowBytes = estimateRowBytes(sortedRows[i]);
|
|
250
261
|
if (i > chunkStart && chunkBytes + rowBytes > targetBytes) {
|
|
251
|
-
result.push(
|
|
262
|
+
result.push(make(sortedRows.slice(chunkStart, i), sortedTimes.slice(chunkStart, i)));
|
|
252
263
|
chunkStart = i;
|
|
253
264
|
chunkBytes = 0;
|
|
254
265
|
}
|
|
255
266
|
chunkBytes += rowBytes;
|
|
256
267
|
}
|
|
257
|
-
result.push(
|
|
268
|
+
result.push(make(sortedRows.slice(chunkStart), sortedTimes.slice(chunkStart)));
|
|
258
269
|
return result;
|
|
259
270
|
}
|
|
260
271
|
|