sliftutils 1.4.48 → 1.4.49
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 +368 -70
- package/package.json +1 -1
- package/storage/BulkDatabase2/BulkDatabaseBase.d.ts +36 -70
- package/storage/BulkDatabase2/BulkDatabaseBase.ts +290 -1243
- package/storage/BulkDatabase2/BulkDatabaseFormat.d.ts +32 -0
- package/storage/BulkDatabase2/BulkDatabaseFormat.ts +224 -19
- package/storage/BulkDatabase2/BulkDatabaseMerge.d.ts +49 -0
- package/storage/BulkDatabase2/BulkDatabaseMerge.ts +398 -0
- package/storage/BulkDatabase2/BulkDatabaseReader.d.ts +71 -0
- package/storage/BulkDatabase2/BulkDatabaseReader.ts +273 -0
- package/storage/BulkDatabase2/LoadedIndex.d.ts +133 -0
- package/storage/BulkDatabase2/LoadedIndex.ts +402 -0
- package/storage/BulkDatabase2/WriteOverlay.d.ts +30 -0
- package/storage/BulkDatabase2/WriteOverlay.ts +52 -0
- package/storage/BulkDatabase2/blockCache.d.ts +1 -0
- package/storage/BulkDatabase2/blockCache.ts +6 -0
- package/storage/BulkDatabase2/streamLog.ts +78 -1
|
@@ -3,7 +3,35 @@
|
|
|
3
3
|
export declare const KEY_COLUMN = "key";
|
|
4
4
|
export declare const EMPTY_BUFFER: Buffer;
|
|
5
5
|
export declare const ABSENT: unique symbol;
|
|
6
|
+
export type RawCell = {
|
|
7
|
+
type: number;
|
|
8
|
+
bytes: Buffer;
|
|
9
|
+
};
|
|
10
|
+
export declare const TYPE_ABSENT_TAG = 14;
|
|
11
|
+
export type ColumnIndex = {
|
|
12
|
+
offsets: Uint32Array;
|
|
13
|
+
types: Uint8Array;
|
|
14
|
+
readValueBytes: (startRow: number, endRow: number) => Promise<Buffer>;
|
|
15
|
+
};
|
|
16
|
+
export declare function encodeValue(value: unknown): {
|
|
17
|
+
type: number;
|
|
18
|
+
bytes: Buffer;
|
|
19
|
+
};
|
|
6
20
|
export declare const TARGET_FILE_BYTES: number;
|
|
21
|
+
export type RawRow = {
|
|
22
|
+
key: string;
|
|
23
|
+
time: number;
|
|
24
|
+
cells: Map<string, RawCell>;
|
|
25
|
+
};
|
|
26
|
+
export declare function columnIndexByteLength(rowCount: number): number;
|
|
27
|
+
export declare function assemblePlannedFile(config: {
|
|
28
|
+
valueColumns: {
|
|
29
|
+
name: string;
|
|
30
|
+
blob: Buffer;
|
|
31
|
+
}[];
|
|
32
|
+
keys: string[];
|
|
33
|
+
times: number[];
|
|
34
|
+
}): Buffer;
|
|
7
35
|
export interface BuiltFile {
|
|
8
36
|
buffer: Buffer;
|
|
9
37
|
minKey: string;
|
|
@@ -11,6 +39,7 @@ export interface BuiltFile {
|
|
|
11
39
|
rowCount: number;
|
|
12
40
|
}
|
|
13
41
|
export declare function buildFileBuffer(rows: Record<string, unknown>[], times: number[], targetBytes?: number): BuiltFile[];
|
|
42
|
+
export declare function buildFileBufferRaw(rows: RawRow[], targetBytes?: number): BuiltFile[];
|
|
14
43
|
export type BaseBulkDatabaseReader = {
|
|
15
44
|
rowCount: number;
|
|
16
45
|
totalBytes: number;
|
|
@@ -30,6 +59,9 @@ export type BaseBulkDatabaseReader = {
|
|
|
30
59
|
value: unknown;
|
|
31
60
|
time: number;
|
|
32
61
|
}[]>;
|
|
62
|
+
getRawColumn: (column: string) => Promise<Map<string, RawCell>>;
|
|
63
|
+
getColumnIndex: (column: string) => Promise<ColumnIndex>;
|
|
64
|
+
rowOfKey: (key: string) => number | undefined;
|
|
33
65
|
getSingleField: (key: string, column: string) => Promise<{
|
|
34
66
|
value: unknown;
|
|
35
67
|
time: number;
|
|
@@ -43,6 +43,35 @@ export const EMPTY_BUFFER = Buffer.alloc(0) as Buffer;
|
|
|
43
43
|
// to an older reader for that column. Distinct from a stored undefined, which is a real clearing value.
|
|
44
44
|
export const ABSENT = Symbol("absent");
|
|
45
45
|
|
|
46
|
+
// One value exactly as it sits on disk: its type tag + raw bytes. A cell's encoding is position-
|
|
47
|
+
// independent (nothing about it depends on which row/file it lives in), so a merge copies the winning
|
|
48
|
+
// RawCell straight from an input column into the output — never decoding it to a JS value and
|
|
49
|
+
// re-encoding. That's far less memory (no object materialization) and much faster (a byte copy, not a
|
|
50
|
+
// JPEG/typed-array decode + re-encode).
|
|
51
|
+
export type RawCell = { type: number; bytes: Buffer };
|
|
52
|
+
|
|
53
|
+
// The type tag a cell carries when its row never set this column — read it from the column's `types`
|
|
54
|
+
// array to detect ABSENT without decoding. Re-exported so the merge planner (which works at the
|
|
55
|
+
// column-index level, no value materialization) can check it.
|
|
56
|
+
export const TYPE_ABSENT_TAG = TYPE_ABSENT;
|
|
57
|
+
|
|
58
|
+
// A column's index alone (offsets + types — small, ~5 bytes/row) plus a primitive to read a CONTIGUOUS
|
|
59
|
+
// row range's raw value bytes. Used by the planned merge: it loads the index across every (source,
|
|
60
|
+
// column) once (cheap), uses types to detect ABSENT and offsets to size each cell, plans the byte
|
|
61
|
+
// layout of every output file from those — then in the execute phase reads only the runs of bytes it
|
|
62
|
+
// actually needs, copying them straight into pre-laid-out output buffers. No cell value is ever
|
|
63
|
+
// materialized as a JS object.
|
|
64
|
+
export type ColumnIndex = {
|
|
65
|
+
// offsets[i]..offsets[i+1] is row i's value byte range within the column's data section.
|
|
66
|
+
offsets: Uint32Array;
|
|
67
|
+
// Per-row type tag; TYPE_ABSENT_TAG marks "this row never set this column" (fall-through).
|
|
68
|
+
types: Uint8Array;
|
|
69
|
+
// Read the contiguous bytes of value(s) for rows [startRow, endRow). Length is exactly
|
|
70
|
+
// offsets[endRow] - offsets[startRow] — the cells in that range concatenated. The caller composes
|
|
71
|
+
// larger reads from consecutive rows (one source-side getRange per run).
|
|
72
|
+
readValueBytes: (startRow: number, endRow: number) => Promise<Buffer>;
|
|
73
|
+
};
|
|
74
|
+
|
|
46
75
|
// Hidden per-row column holding each row's write-time (so reads can resolve a key to its latest value
|
|
47
76
|
// by actual time). NUL-prefixed so it can't collide with a user column; excluded from `columns`.
|
|
48
77
|
const TIME_COLUMN = String.fromCharCode(0) + "t";
|
|
@@ -62,7 +91,7 @@ type FileHeader = {
|
|
|
62
91
|
maxKey?: string;
|
|
63
92
|
};
|
|
64
93
|
|
|
65
|
-
function encodeValue(value: unknown): { type: number; bytes: Buffer } {
|
|
94
|
+
export function encodeValue(value: unknown): { type: number; bytes: Buffer } {
|
|
66
95
|
if (value === ABSENT) {
|
|
67
96
|
return { type: TYPE_ABSENT, bytes: EMPTY_BUFFER };
|
|
68
97
|
}
|
|
@@ -148,6 +177,26 @@ function encodeBulkData(data: unknown[]): Buffer {
|
|
|
148
177
|
return Buffer.concat([offsets, types, ...parts]);
|
|
149
178
|
}
|
|
150
179
|
|
|
180
|
+
// Like encodeBulkData but the values are already encoded (a merge supplies the winning cells' raw bytes
|
|
181
|
+
// straight from the input files). Lays out the same offsets / types / data section without touching the
|
|
182
|
+
// values themselves.
|
|
183
|
+
function encodeBulkDataRaw(cells: RawCell[]): Buffer {
|
|
184
|
+
const n = cells.length;
|
|
185
|
+
const offsets = Buffer.alloc(4 * (n + 1));
|
|
186
|
+
const types = Buffer.alloc(n);
|
|
187
|
+
const parts: Buffer[] = [];
|
|
188
|
+
let pos = 0;
|
|
189
|
+
for (let i = 0; i < n; i++) {
|
|
190
|
+
const { type, bytes } = cells[i];
|
|
191
|
+
offsets.writeUInt32LE(pos, 4 * i);
|
|
192
|
+
types[i] = type;
|
|
193
|
+
parts.push(bytes);
|
|
194
|
+
pos += bytes.length;
|
|
195
|
+
}
|
|
196
|
+
offsets.writeUInt32LE(pos, 4 * n);
|
|
197
|
+
return Buffer.concat([offsets, types, ...parts]);
|
|
198
|
+
}
|
|
199
|
+
|
|
151
200
|
function decodeBulkData(blob: Buffer, rowCount: number): unknown[] {
|
|
152
201
|
const indexSize = 4 * (rowCount + 1) + rowCount;
|
|
153
202
|
const values: unknown[] = [];
|
|
@@ -188,21 +237,10 @@ function estimateRowBytes(row: Record<string, unknown>): number {
|
|
|
188
237
|
return total;
|
|
189
238
|
}
|
|
190
239
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
for (const field of Object.keys(row)) {
|
|
196
|
-
if (columnSet.has(field)) continue;
|
|
197
|
-
columnSet.add(field);
|
|
198
|
-
columnNames.push(field);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
// A row that doesn't include a column stores ABSENT (fall-through), not undefined (a real value).
|
|
202
|
-
const blobs = columnNames.map(col => encodeBulkData(rows.map(row => col in row ? row[col] : ABSENT)));
|
|
203
|
-
// The hidden per-row time column.
|
|
204
|
-
columnNames.push(TIME_COLUMN);
|
|
205
|
-
blobs.push(encodeBulkData(times));
|
|
240
|
+
// Concatenates already-encoded column blobs into a complete file (4-byte header length + header JSON +
|
|
241
|
+
// blobs), computing the header's time + key bounds from the per-row times and keys. Shared by the
|
|
242
|
+
// object-based builder (buildOneFile) and the raw-splice builder (buildOneFileRaw).
|
|
243
|
+
function assembleFile(columnNames: string[], blobs: Buffer[], rowCount: number, times: number[], keys: string[]): Buffer {
|
|
206
244
|
let offset = 0;
|
|
207
245
|
const columns = columnNames.map((name, i) => {
|
|
208
246
|
const entry = { name, offset, length: blobs[i].length };
|
|
@@ -214,18 +252,92 @@ function buildOneFile(rows: Record<string, unknown>[], times: number[]): Buffer
|
|
|
214
252
|
for (const t of times) { if (t < minTime) minTime = t; if (t > maxTime) maxTime = t; }
|
|
215
253
|
let minKey: string | undefined;
|
|
216
254
|
let maxKey: string | undefined;
|
|
217
|
-
for (const
|
|
218
|
-
const key = row[KEY_COLUMN] as string;
|
|
255
|
+
for (const key of keys) {
|
|
219
256
|
if (minKey === undefined || key < minKey) minKey = key;
|
|
220
257
|
if (maxKey === undefined || key > maxKey) maxKey = key;
|
|
221
258
|
}
|
|
222
|
-
const header: FileHeader = { rowCount
|
|
259
|
+
const header: FileHeader = { rowCount, columns, minTime, maxTime, minKey, maxKey };
|
|
223
260
|
const headerBuf = Buffer.from(JSON.stringify(header), "utf8");
|
|
224
261
|
const lengthPrefix = Buffer.alloc(4);
|
|
225
262
|
lengthPrefix.writeUInt32LE(headerBuf.length, 0);
|
|
226
263
|
return Buffer.concat([lengthPrefix, headerBuf, ...blobs]);
|
|
227
264
|
}
|
|
228
265
|
|
|
266
|
+
function buildOneFile(rows: Record<string, unknown>[], times: number[]): Buffer {
|
|
267
|
+
const columnNames: string[] = [];
|
|
268
|
+
const columnSet = new Set<string>();
|
|
269
|
+
for (const row of rows) {
|
|
270
|
+
for (const field of Object.keys(row)) {
|
|
271
|
+
if (columnSet.has(field)) continue;
|
|
272
|
+
columnSet.add(field);
|
|
273
|
+
columnNames.push(field);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// A row that doesn't include a column stores ABSENT (fall-through), not undefined (a real value).
|
|
277
|
+
const blobs = columnNames.map(col => encodeBulkData(rows.map(row => col in row ? row[col] : ABSENT)));
|
|
278
|
+
// The hidden per-row time column.
|
|
279
|
+
columnNames.push(TIME_COLUMN);
|
|
280
|
+
blobs.push(encodeBulkData(times));
|
|
281
|
+
return assembleFile(columnNames, blobs, rows.length, times, rows.map(r => r[KEY_COLUMN] as string));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// A resolved output row for the raw-splice merge: its key, write-time, and the winning raw cell for each
|
|
285
|
+
// column it has (a column it lacks is written ABSENT — fall-through). The cell bytes are copied straight
|
|
286
|
+
// from the input files; no value is ever decoded.
|
|
287
|
+
export type RawRow = { key: string; time: number; cells: Map<string, RawCell> };
|
|
288
|
+
|
|
289
|
+
// Size of a column blob's INDEX section (offsets array + types array) for a column of N rows. The data
|
|
290
|
+
// section follows immediately after. The planned merge uses this to size output buffers exactly.
|
|
291
|
+
export function columnIndexByteLength(rowCount: number): number {
|
|
292
|
+
return 4 * (rowCount + 1) + rowCount;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Assembles a complete bulk file from a set of pre-built value column blobs plus the keys + times. Auto-
|
|
296
|
+
// adds the KEY_COLUMN (keys) and hidden TIME_COLUMN (times) — they're small enough to encode in one shot
|
|
297
|
+
// here. Used by the planned merge: it builds each value column's blob by-hand (offsets/types + raw bytes
|
|
298
|
+
// copied directly from inputs, no value materialization) and hands the result here for header + bounds.
|
|
299
|
+
export function assemblePlannedFile(config: {
|
|
300
|
+
valueColumns: { name: string; blob: Buffer }[];
|
|
301
|
+
keys: string[];
|
|
302
|
+
times: number[];
|
|
303
|
+
}): Buffer {
|
|
304
|
+
const columnNames = [KEY_COLUMN, ...config.valueColumns.map(c => c.name), TIME_COLUMN];
|
|
305
|
+
const blobs = [
|
|
306
|
+
encodeBulkData(config.keys),
|
|
307
|
+
...config.valueColumns.map(c => c.blob),
|
|
308
|
+
encodeBulkData(config.times),
|
|
309
|
+
];
|
|
310
|
+
return assembleFile(columnNames, blobs, config.keys.length, config.times, config.keys);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const ABSENT_CELL: RawCell = { type: TYPE_ABSENT, bytes: EMPTY_BUFFER };
|
|
314
|
+
|
|
315
|
+
function buildOneFileRaw(rows: RawRow[]): Buffer {
|
|
316
|
+
// Columns present in this chunk, in first-seen order (matches buildOneFile, which only emits columns
|
|
317
|
+
// some row actually has).
|
|
318
|
+
const valueColumns: string[] = [];
|
|
319
|
+
const seen = new Set<string>();
|
|
320
|
+
for (const row of rows) for (const col of row.cells.keys()) {
|
|
321
|
+
if (seen.has(col)) continue;
|
|
322
|
+
seen.add(col);
|
|
323
|
+
valueColumns.push(col);
|
|
324
|
+
}
|
|
325
|
+
const columnNames = [KEY_COLUMN, ...valueColumns, TIME_COLUMN];
|
|
326
|
+
const times = rows.map(r => r.time);
|
|
327
|
+
const blobs = [
|
|
328
|
+
encodeBulkData(rows.map(r => r.key)),
|
|
329
|
+
...valueColumns.map(col => encodeBulkDataRaw(rows.map(r => r.cells.get(col) ?? ABSENT_CELL))),
|
|
330
|
+
encodeBulkData(times),
|
|
331
|
+
];
|
|
332
|
+
return assembleFile(columnNames, blobs, rows.length, times, rows.map(r => r.key));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function estimateRawRowBytes(row: RawRow): number {
|
|
336
|
+
let total = row.key.length * 2 + PER_VALUE_OVERHEAD;
|
|
337
|
+
for (const cell of row.cells.values()) total += cell.bytes.length + PER_VALUE_OVERHEAD;
|
|
338
|
+
return total;
|
|
339
|
+
}
|
|
340
|
+
|
|
229
341
|
// One complete, independent file: the encoded buffer plus its key range + row count (the caller logs the
|
|
230
342
|
// range when a merge splits across several files).
|
|
231
343
|
export interface BuiltFile { buffer: Buffer; minKey: string; maxKey: string; rowCount: number; }
|
|
@@ -269,6 +381,34 @@ export function buildFileBuffer(rows: Record<string, unknown>[], times: number[]
|
|
|
269
381
|
return result;
|
|
270
382
|
}
|
|
271
383
|
|
|
384
|
+
// The raw-splice counterpart of buildFileBuffer, used by merges: the rows already carry their winning
|
|
385
|
+
// cells as raw on-disk bytes (no JS values), so this just sorts by key, chunks to ~targetBytes, and
|
|
386
|
+
// concatenates the bytes. Same output guarantees: key-contiguous, disjoint, ascending files.
|
|
387
|
+
export function buildFileBufferRaw(rows: RawRow[], targetBytes = TARGET_FILE_BYTES): BuiltFile[] {
|
|
388
|
+
const make = (rs: RawRow[]): BuiltFile => ({
|
|
389
|
+
buffer: buildOneFileRaw(rs),
|
|
390
|
+
minKey: rs.length ? rs[0].key : "",
|
|
391
|
+
maxKey: rs.length ? rs[rs.length - 1].key : "",
|
|
392
|
+
rowCount: rs.length,
|
|
393
|
+
});
|
|
394
|
+
if (rows.length === 0) return [make([])];
|
|
395
|
+
const sorted = rows.slice().sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
396
|
+
const result: BuiltFile[] = [];
|
|
397
|
+
let chunkStart = 0;
|
|
398
|
+
let chunkBytes = 0;
|
|
399
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
400
|
+
const rowBytes = estimateRawRowBytes(sorted[i]);
|
|
401
|
+
if (i > chunkStart && chunkBytes + rowBytes > targetBytes) {
|
|
402
|
+
result.push(make(sorted.slice(chunkStart, i)));
|
|
403
|
+
chunkStart = i;
|
|
404
|
+
chunkBytes = 0;
|
|
405
|
+
}
|
|
406
|
+
chunkBytes += rowBytes;
|
|
407
|
+
}
|
|
408
|
+
result.push(make(sorted.slice(chunkStart)));
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
|
|
272
412
|
export type BaseBulkDatabaseReader = {
|
|
273
413
|
rowCount: number;
|
|
274
414
|
totalBytes: number;
|
|
@@ -290,6 +430,18 @@ export type BaseBulkDatabaseReader = {
|
|
|
290
430
|
// Each key's value for the column plus the row's write-time. value may be ABSENT (the row never set
|
|
291
431
|
// this column — the join then falls through to an older reader for that key/column).
|
|
292
432
|
getColumn: (column: string) => Promise<{ key: string; value: unknown; time: number }[]>;
|
|
433
|
+
// Like getColumn but returns each cell's raw on-disk encoding (type tag + bytes) keyed by key, WITHOUT
|
|
434
|
+
// decoding the value — for merges, which splice the winning bytes straight into the output. ABSENT
|
|
435
|
+
// cells are omitted (a missing key means this reader didn't set this column, so the merge falls
|
|
436
|
+
// through to an older reader). Each cell's write-time is the reader's keyTimes value for that key.
|
|
437
|
+
getRawColumn: (column: string) => Promise<Map<string, RawCell>>;
|
|
438
|
+
// Returns the column's INDEX (offsets + types) plus a contiguous row-range byte reader. The planned
|
|
439
|
+
// merge loads this for every (source, column) once — small (~5B/row), no value bytes pulled — and
|
|
440
|
+
// uses types/offsets to plan the output's byte layout. Execute then reads only the needed runs.
|
|
441
|
+
getColumnIndex: (column: string) => Promise<ColumnIndex>;
|
|
442
|
+
// Maps a key to its row index in this reader (and undefined if absent). The planned merge uses this
|
|
443
|
+
// to look up a winning cell's source row without going through a column read.
|
|
444
|
+
rowOfKey: (key: string) => number | undefined;
|
|
293
445
|
// The value + write-time for (key, column), or ABSENT if this reader has no such cell.
|
|
294
446
|
getSingleField: (key: string, column: string) => Promise<{ value: unknown; time: number } | typeof ABSENT>;
|
|
295
447
|
};
|
|
@@ -363,6 +515,59 @@ export async function loadBulkDatabase(config: {
|
|
|
363
515
|
const values = await readWholeColumn(column);
|
|
364
516
|
return keys.map((key, i) => ({ key, value: values[i], time: times[i] }));
|
|
365
517
|
},
|
|
518
|
+
async getRawColumn(column) {
|
|
519
|
+
const map = new Map<string, RawCell>();
|
|
520
|
+
const col = colByName.get(column);
|
|
521
|
+
if (!col) return map; // file lacks this column → every cell ABSENT (fall through)
|
|
522
|
+
const blob = await config.getRange(dataBase + col.offset, dataBase + col.offset + col.length);
|
|
523
|
+
const indexSize = 4 * (rowCount + 1) + rowCount;
|
|
524
|
+
for (let i = 0; i < rowCount; i++) {
|
|
525
|
+
const type = blob[4 * (rowCount + 1) + i];
|
|
526
|
+
if (type === TYPE_ABSENT) continue; // omit ABSENT so the join falls through
|
|
527
|
+
const start = blob.readUInt32LE(4 * i);
|
|
528
|
+
const end = blob.readUInt32LE(4 * (i + 1));
|
|
529
|
+
map.set(keys[i], { type, bytes: blob.subarray(indexSize + start, indexSize + end) });
|
|
530
|
+
}
|
|
531
|
+
return map;
|
|
532
|
+
},
|
|
533
|
+
async getColumnIndex(column) {
|
|
534
|
+
const col = colByName.get(column);
|
|
535
|
+
if (!col) {
|
|
536
|
+
// File lacks this column — present an all-ABSENT index so the planner can treat it
|
|
537
|
+
// uniformly with files that have the column.
|
|
538
|
+
const offsets = new Uint32Array(rowCount + 1);
|
|
539
|
+
const types = new Uint8Array(rowCount).fill(TYPE_ABSENT);
|
|
540
|
+
return {
|
|
541
|
+
offsets,
|
|
542
|
+
types,
|
|
543
|
+
async readValueBytes() { return EMPTY_BUFFER; },
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
const colBase = dataBase + col.offset;
|
|
547
|
+
const indexSize = 4 * (rowCount + 1) + rowCount;
|
|
548
|
+
// One read pulls offsets + types (small — ~5 B/row). Block cache makes subsequent value reads
|
|
549
|
+
// of nearby rows cheap. Decoded into aligned typed arrays so the executor can do O(1) lookups.
|
|
550
|
+
const indexBuf = await config.getRange(colBase, colBase + indexSize);
|
|
551
|
+
const offsets = new Uint32Array(rowCount + 1);
|
|
552
|
+
for (let i = 0; i <= rowCount; i++) offsets[i] = indexBuf.readUInt32LE(4 * i);
|
|
553
|
+
const types = new Uint8Array(rowCount);
|
|
554
|
+
for (let i = 0; i < rowCount; i++) types[i] = indexBuf[4 * (rowCount + 1) + i];
|
|
555
|
+
const dataStart = colBase + indexSize;
|
|
556
|
+
return {
|
|
557
|
+
offsets,
|
|
558
|
+
types,
|
|
559
|
+
async readValueBytes(startRow, endRow) {
|
|
560
|
+
if (endRow <= startRow) return EMPTY_BUFFER;
|
|
561
|
+
const start = offsets[startRow];
|
|
562
|
+
const end = offsets[endRow];
|
|
563
|
+
if (end <= start) return EMPTY_BUFFER;
|
|
564
|
+
return config.getRange(dataStart + start, dataStart + end);
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
},
|
|
568
|
+
rowOfKey(key) {
|
|
569
|
+
return keyIndex.get(key);
|
|
570
|
+
},
|
|
366
571
|
async getSingleField(key, column) {
|
|
367
572
|
const row = keyIndex.get(key);
|
|
368
573
|
if (row === undefined) return ABSENT;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
import { BaseBulkDatabaseReader } from "./BulkDatabaseFormat";
|
|
4
|
+
type CopyRun = {
|
|
5
|
+
sourceIdx: number;
|
|
6
|
+
sourceStartRow: number;
|
|
7
|
+
sourceEndRow: number;
|
|
8
|
+
outputByteStart: number;
|
|
9
|
+
byteLength: number;
|
|
10
|
+
};
|
|
11
|
+
type PlannedOutputColumn = {
|
|
12
|
+
name: string;
|
|
13
|
+
offsets: Uint32Array;
|
|
14
|
+
types: Uint8Array;
|
|
15
|
+
dataLength: number;
|
|
16
|
+
runs: CopyRun[];
|
|
17
|
+
};
|
|
18
|
+
export type PlannedOutputFile = {
|
|
19
|
+
keys: string[];
|
|
20
|
+
times: number[];
|
|
21
|
+
minKey: string;
|
|
22
|
+
maxKey: string;
|
|
23
|
+
columns: PlannedOutputColumn[];
|
|
24
|
+
estimatedFileBytes: number;
|
|
25
|
+
sourceCounts: Map<number, number>;
|
|
26
|
+
};
|
|
27
|
+
export type PlannedMergeOutput = {
|
|
28
|
+
name: string;
|
|
29
|
+
minKey: string;
|
|
30
|
+
maxKey: string;
|
|
31
|
+
rowCount: number;
|
|
32
|
+
size: number;
|
|
33
|
+
sources: Map<string, number>;
|
|
34
|
+
};
|
|
35
|
+
export declare function runPlannedMerge(config: {
|
|
36
|
+
sources: BaseBulkDatabaseReader[];
|
|
37
|
+
sourceNames: string[];
|
|
38
|
+
collectionName: string;
|
|
39
|
+
targetFileBytes?: number;
|
|
40
|
+
targetBatchBytes?: number;
|
|
41
|
+
writeFile: (data: Buffer) => Promise<{
|
|
42
|
+
name: string;
|
|
43
|
+
size: number;
|
|
44
|
+
}>;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
outputs: PlannedMergeOutput[];
|
|
47
|
+
carriedDeletes: Map<string, number>;
|
|
48
|
+
}>;
|
|
49
|
+
export {};
|