qvdjs 2.0.6 → 2.1.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/README.md +4 -0
- package/dist/index.cjs +381 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +381 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -441,7 +441,9 @@ var init_optionTypes = __esm({
|
|
|
441
441
|
function requireRowCount(value, name, filePath) {
|
|
442
442
|
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
443
443
|
throw new QvdValidationError(`${name} must be a non-negative integer`, {
|
|
444
|
+
reason: "option",
|
|
444
445
|
option: name,
|
|
446
|
+
value,
|
|
445
447
|
provided: value,
|
|
446
448
|
type: typeof value,
|
|
447
449
|
file: filePath
|
|
@@ -458,6 +460,9 @@ function normaliseWindow(window, filePath) {
|
|
|
458
460
|
}
|
|
459
461
|
if (typeof window !== "object" || Array.isArray(window)) {
|
|
460
462
|
throw new QvdValidationError("The row window must be a number, null, or an {offset, limit} object", {
|
|
463
|
+
reason: "option",
|
|
464
|
+
option: "limit",
|
|
465
|
+
value: window,
|
|
461
466
|
provided: window,
|
|
462
467
|
type: typeof window,
|
|
463
468
|
file: filePath
|
|
@@ -468,6 +473,9 @@ function normaliseWindow(window, filePath) {
|
|
|
468
473
|
const maxRowsGiven = maxRows !== void 0 && maxRows !== null;
|
|
469
474
|
if (limitGiven && maxRowsGiven) {
|
|
470
475
|
throw new QvdValidationError("maxRows and limit are two names for the same option; pass one of them, not both", {
|
|
476
|
+
reason: "option",
|
|
477
|
+
option: "limit",
|
|
478
|
+
value: limit,
|
|
471
479
|
maxRows,
|
|
472
480
|
limit,
|
|
473
481
|
file: filePath
|
|
@@ -478,6 +486,19 @@ function normaliseWindow(window, filePath) {
|
|
|
478
486
|
limit: limitGiven ? requireRowCount(limit, "limit", filePath) : maxRowsGiven ? requireRowCount(maxRows, "maxRows", filePath) : null
|
|
479
487
|
};
|
|
480
488
|
}
|
|
489
|
+
function requireChunkSize(chunkSize, filePath) {
|
|
490
|
+
if (typeof chunkSize !== "number" || !Number.isInteger(chunkSize) || chunkSize <= 0) {
|
|
491
|
+
throw new QvdValidationError("chunkSize must be a positive integer", {
|
|
492
|
+
reason: "option",
|
|
493
|
+
option: "chunkSize",
|
|
494
|
+
value: chunkSize,
|
|
495
|
+
provided: chunkSize,
|
|
496
|
+
type: typeof chunkSize,
|
|
497
|
+
file: filePath
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
return chunkSize;
|
|
501
|
+
}
|
|
481
502
|
function resolveWindow(window, totalRows) {
|
|
482
503
|
const rows = Number.isSafeInteger(totalRows) && totalRows > 0 ? totalRows : 0;
|
|
483
504
|
const offset = Math.min(window.offset, rows);
|
|
@@ -492,6 +513,9 @@ function selectFields(fields, requested, filePath) {
|
|
|
492
513
|
}
|
|
493
514
|
if (!Array.isArray(requested)) {
|
|
494
515
|
throw new QvdValidationError("fields must be an array of field names", {
|
|
516
|
+
reason: "option",
|
|
517
|
+
option: "fields",
|
|
518
|
+
value: requested,
|
|
495
519
|
provided: requested,
|
|
496
520
|
type: typeof requested,
|
|
497
521
|
file: filePath
|
|
@@ -500,6 +524,9 @@ function selectFields(fields, requested, filePath) {
|
|
|
500
524
|
const available = fields.map((field) => field["FieldName"]);
|
|
501
525
|
if (requested.length === 0) {
|
|
502
526
|
throw new QvdValidationError("fields must name at least one field", {
|
|
527
|
+
reason: "option",
|
|
528
|
+
option: "fields",
|
|
529
|
+
value: requested,
|
|
503
530
|
availableColumns: available,
|
|
504
531
|
file: filePath
|
|
505
532
|
});
|
|
@@ -508,6 +535,9 @@ function selectFields(fields, requested, filePath) {
|
|
|
508
535
|
return requested.map((name) => {
|
|
509
536
|
if (typeof name !== "string") {
|
|
510
537
|
throw new QvdValidationError("Field names must be strings", {
|
|
538
|
+
reason: "option",
|
|
539
|
+
option: "fields",
|
|
540
|
+
value: name,
|
|
511
541
|
provided: name,
|
|
512
542
|
type: typeof name,
|
|
513
543
|
availableColumns: available,
|
|
@@ -516,6 +546,9 @@ function selectFields(fields, requested, filePath) {
|
|
|
516
546
|
}
|
|
517
547
|
if (seen.has(name)) {
|
|
518
548
|
throw new QvdValidationError(`Field '${name}' is listed twice`, {
|
|
549
|
+
reason: "option",
|
|
550
|
+
option: "fields",
|
|
551
|
+
value: name,
|
|
519
552
|
column: name,
|
|
520
553
|
fields: requested,
|
|
521
554
|
file: filePath
|
|
@@ -525,6 +558,9 @@ function selectFields(fields, requested, filePath) {
|
|
|
525
558
|
const index = available.indexOf(name);
|
|
526
559
|
if (index === -1) {
|
|
527
560
|
throw new QvdValidationError(`Column '${name}' does not exist`, {
|
|
561
|
+
reason: "option",
|
|
562
|
+
option: "fields",
|
|
563
|
+
value: name,
|
|
528
564
|
column: name,
|
|
529
565
|
availableColumns: available,
|
|
530
566
|
file: filePath
|
|
@@ -539,7 +575,9 @@ function normaliseDuals(value, filePath) {
|
|
|
539
575
|
}
|
|
540
576
|
if (!DUAL_MODES.includes(value)) {
|
|
541
577
|
throw new QvdValidationError(`duals must be one of ${DUAL_MODES.map((mode) => `'${mode}'`).join(", ")}`, {
|
|
578
|
+
reason: "option",
|
|
542
579
|
option: "duals",
|
|
580
|
+
value,
|
|
543
581
|
provided: value,
|
|
544
582
|
file: filePath
|
|
545
583
|
});
|
|
@@ -578,6 +616,7 @@ var init_readOptions = __esm({
|
|
|
578
616
|
init_optionTypes();
|
|
579
617
|
__name(requireRowCount, "requireRowCount");
|
|
580
618
|
__name(normaliseWindow, "normaliseWindow");
|
|
619
|
+
__name(requireChunkSize, "requireChunkSize");
|
|
581
620
|
__name(resolveWindow, "resolveWindow");
|
|
582
621
|
__name(selectFields, "selectFields");
|
|
583
622
|
DUAL_MODES = Object.freeze(["number", "text", "both"]);
|
|
@@ -2206,22 +2245,56 @@ function recommendedChunkFor(budget, symbolTableSize, windowRows, totalRows, col
|
|
|
2206
2245
|
}
|
|
2207
2246
|
return low;
|
|
2208
2247
|
}
|
|
2209
|
-
function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.8, columnCount = 0, materialisesRows = true, live = null, bytesHeld =
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2248
|
+
function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.8, columnCount = 0, materialisesRows = true, live = null, bytesHeld = null, readBytes = null) {
|
|
2249
|
+
const answer = checkMemory({
|
|
2250
|
+
symbolTableSize,
|
|
2251
|
+
maxRows,
|
|
2252
|
+
totalRows,
|
|
2253
|
+
safetyFactor,
|
|
2254
|
+
columnCount,
|
|
2255
|
+
materialisesRows,
|
|
2256
|
+
live,
|
|
2257
|
+
bytesHeld,
|
|
2258
|
+
readBytes
|
|
2259
|
+
});
|
|
2260
|
+
if (answer.fits) {
|
|
2214
2261
|
return;
|
|
2215
2262
|
}
|
|
2216
|
-
const
|
|
2263
|
+
const { message, context } = answer.refusal;
|
|
2264
|
+
throw new QvdValidationError(message, {
|
|
2265
|
+
file: filePath,
|
|
2266
|
+
...context,
|
|
2267
|
+
reason: "memory",
|
|
2268
|
+
check: answerOf(answer)
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
function checkMemory({
|
|
2272
|
+
symbolTableSize,
|
|
2273
|
+
maxRows,
|
|
2274
|
+
totalRows,
|
|
2275
|
+
safetyFactor = 0.8,
|
|
2276
|
+
columnCount = 0,
|
|
2277
|
+
materialisesRows = true,
|
|
2278
|
+
live = null,
|
|
2279
|
+
bytesHeld = null,
|
|
2280
|
+
readBytes = null,
|
|
2281
|
+
measured = null
|
|
2282
|
+
}) {
|
|
2283
|
+
if (typeof safetyFactor !== "number" || safetyFactor < 0 || safetyFactor > 1) {
|
|
2284
|
+
throw new QvdValidationError("safetyFactor must be a number between 0.0 and 1.0", {
|
|
2285
|
+
safetyFactor,
|
|
2286
|
+
reason: "option",
|
|
2287
|
+
option: "memorySafetyFactor",
|
|
2288
|
+
value: safetyFactor
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
const budget = measured ?? getMemoryBudget();
|
|
2217
2292
|
const rowsToLoad = maxRows === null || maxRows >= totalRows ? totalRows : maxRows;
|
|
2218
2293
|
const rowsLive = live === null ? null : live.rows;
|
|
2219
2294
|
const liveRowsPerChunk = live === null ? 1 : live.perChunk;
|
|
2220
2295
|
const liveRows = rowsLive === null ? rowsToLoad : Math.min(rowsLive, rowsToLoad);
|
|
2221
2296
|
const heapMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows, columnCount, materialisesRows, rowsLive);
|
|
2222
|
-
const held
|
|
2223
|
-
const heldForRows = typeof bytesHeld === "number" ? () => bytesHeld : bytesHeld.forRows;
|
|
2224
|
-
const heldForChunk = typeof bytesHeld === "number" ? () => bytesHeld : bytesHeld.forChunk;
|
|
2297
|
+
const { held, forRows: heldForRows, forChunk: heldForChunk } = bytesHeld ?? noBytesHeld;
|
|
2225
2298
|
const externalMemory = estimateExternalMemory(liveRows, columnCount) + held;
|
|
2226
2299
|
const bounded = budget.candidates.map((candidate) => {
|
|
2227
2300
|
const heapOnly = candidate.source === "V8 heap limit";
|
|
@@ -2233,11 +2306,26 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
2233
2306
|
bounds: heapOnly ? "the V8 heap" : "the whole process"
|
|
2234
2307
|
};
|
|
2235
2308
|
});
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2309
|
+
if (safetyFactor === 0) {
|
|
2310
|
+
const lowest = budget.candidates.reduce((least, candidate) => candidate.bytes < least.bytes ? candidate : least);
|
|
2311
|
+
return {
|
|
2312
|
+
fits: true,
|
|
2313
|
+
estimate: { heapBytes: heapMemory, externalBytes: externalMemory, readBytes },
|
|
2314
|
+
// The ceilings are still there and still named - what is missing is any measurement against them.
|
|
2315
|
+
// An earlier version reported `bound: 'none'` here, a third value in a two-value vocabulary that a
|
|
2316
|
+
// caller switching on the documented two would fall straight through.
|
|
2317
|
+
budget: {
|
|
2318
|
+
...budgetOf(budget, { ...lowest, heapOnly: lowest.source === "V8 heap limit" }, 0),
|
|
2319
|
+
allowedBytes: Infinity
|
|
2320
|
+
},
|
|
2321
|
+
exact: symbolTableSize === 0,
|
|
2322
|
+
suggestions: []
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
const tightest = bounded.reduce(
|
|
2326
|
+
(worst, candidate) => candidate.needs / candidate.allowed > worst.needs / worst.allowed ? candidate : worst
|
|
2240
2327
|
);
|
|
2328
|
+
const binding = tightest.needs > tightest.allowed ? tightest : null;
|
|
2241
2329
|
const heapLimit = getHeapLimit();
|
|
2242
2330
|
const availableMemory = binding ? binding.bytes : budget.bytes;
|
|
2243
2331
|
const estimatedMemory = binding ? binding.needs : heapMemory;
|
|
@@ -2294,31 +2382,75 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
2294
2382
|
} else {
|
|
2295
2383
|
advice = `Try holding fewer rows using the ${knob} parameter (recommended: ${formatCount(recommendedValue)} rows or less), or raise the heap with --max-old-space-size.`;
|
|
2296
2384
|
}
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
{
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
heapLimitMB,
|
|
2306
|
-
reportedHeapLimitMB,
|
|
2307
|
-
availableRamMB,
|
|
2308
|
-
limitingFactor,
|
|
2309
|
-
limitingScope,
|
|
2310
|
-
memoryBudget: budget.candidates,
|
|
2311
|
-
memoryObserved: budget.observed,
|
|
2312
|
-
columnCount,
|
|
2313
|
-
totalRows,
|
|
2314
|
-
maxRows,
|
|
2315
|
-
recommendedMaxRows,
|
|
2316
|
-
// Only present when a chunk size is what overflowed, so a caller cannot mistake one
|
|
2317
|
-
// recommendation for the other.
|
|
2318
|
-
...chunked ? { rowsLive, recommendedChunkSize: recommendedChunk } : {}
|
|
2385
|
+
const suggestions = [];
|
|
2386
|
+
if (!nothingFits) {
|
|
2387
|
+
suggestions.push({ option: knob, value: recommendedValue });
|
|
2388
|
+
}
|
|
2389
|
+
if (!containerBound) {
|
|
2390
|
+
let needed = Math.ceil(estimatedMemory / safetyFactor / (1024 * 1024));
|
|
2391
|
+
while (Math.max(needed * 1024 * 1024, MINIMUM_BUDGET_BYTES) * safetyFactor < estimatedMemory) {
|
|
2392
|
+
needed += 1;
|
|
2319
2393
|
}
|
|
2320
|
-
|
|
2394
|
+
suggestions.push({ nodeOption: "--max-old-space-size", value: needed });
|
|
2395
|
+
}
|
|
2396
|
+
return {
|
|
2397
|
+
fits: false,
|
|
2398
|
+
reason: "memory",
|
|
2399
|
+
estimate: { heapBytes: heapMemory, externalBytes: externalMemory, readBytes },
|
|
2400
|
+
budget: budgetOf(budget, tightest, safetyFactor),
|
|
2401
|
+
// The symbol term is six times the bytes on disk, an overhead measured across files rather than
|
|
2402
|
+
// derived, so any read with symbols in it is an estimate and says so. Only a read that decodes
|
|
2403
|
+
// nothing can be exact.
|
|
2404
|
+
exact: symbolTableSize === 0,
|
|
2405
|
+
suggestions,
|
|
2406
|
+
refusal: {
|
|
2407
|
+
message: `Insufficient memory to load file safely. Symbol table: ${sizeMB}MB, Estimated memory needed: ${estimatedMB}MB, Available: ${availableMB}MB (limited by ${limitingFactor}, which bounds ${limitingScope}; considered: ${budgetBreakdown}; observed but not used: ${observedBreakdown}). ` + advice,
|
|
2408
|
+
context: {
|
|
2409
|
+
symbolTableSize,
|
|
2410
|
+
symbolTableSizeMB: sizeMB,
|
|
2411
|
+
estimatedMemoryMB: estimatedMB,
|
|
2412
|
+
availableMemoryMB: availableMB,
|
|
2413
|
+
heapLimitMB,
|
|
2414
|
+
reportedHeapLimitMB,
|
|
2415
|
+
availableRamMB,
|
|
2416
|
+
limitingFactor,
|
|
2417
|
+
limitingScope,
|
|
2418
|
+
memoryBudget: budget.candidates,
|
|
2419
|
+
memoryObserved: budget.observed,
|
|
2420
|
+
columnCount,
|
|
2421
|
+
totalRows,
|
|
2422
|
+
maxRows,
|
|
2423
|
+
recommendedMaxRows,
|
|
2424
|
+
// Only present when a chunk size is what overflowed, so a caller cannot mistake one
|
|
2425
|
+
// recommendation for the other.
|
|
2426
|
+
...chunked ? { rowsLive, recommendedChunkSize: recommendedChunk } : {}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
};
|
|
2321
2430
|
}
|
|
2431
|
+
return {
|
|
2432
|
+
fits: true,
|
|
2433
|
+
estimate: { heapBytes: heapMemory, externalBytes: externalMemory, readBytes },
|
|
2434
|
+
budget: budgetOf(budget, tightest, safetyFactor),
|
|
2435
|
+
exact: symbolTableSize === 0,
|
|
2436
|
+
suggestions: []
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
function answerOf(answer) {
|
|
2440
|
+
const { refusal, ...rest } = answer;
|
|
2441
|
+
return rest;
|
|
2442
|
+
}
|
|
2443
|
+
function budgetOf(budget, tightest, safetyFactor) {
|
|
2444
|
+
const processLimit = budget.candidates.find((candidate) => candidate.source !== "V8 heap limit");
|
|
2445
|
+
return {
|
|
2446
|
+
heapBytes: usableOldSpaceLimit(),
|
|
2447
|
+
processBytes: processLimit ? processLimit.bytes : null,
|
|
2448
|
+
bound: tightest.heapOnly ? "heap" : "process",
|
|
2449
|
+
safetyFactor,
|
|
2450
|
+
allowedBytes: tightest.allowed ?? tightest.bytes * safetyFactor,
|
|
2451
|
+
candidates: budget.candidates,
|
|
2452
|
+
observed: budget.observed
|
|
2453
|
+
};
|
|
2322
2454
|
}
|
|
2323
2455
|
function formatCount(value) {
|
|
2324
2456
|
return value.toLocaleString("en-US");
|
|
@@ -2340,7 +2472,7 @@ function warnLargeSymbolTable(symbolTableSize, maxRows, totalRows, columnCount =
|
|
|
2340
2472
|
`\u26A0\uFE0F Large symbol table detected (${sizeMB}MB > ${warnMB}MB threshold). This read materialises ${formatCount(rowsToLoad)} of ${formatCount(totalRows)} rows and will use ~${estimatedMB}MB RAM. Reading fewer rows - with limit, maxRows, or a narrower offset window - lowers the row cost, though the symbol table is read in full either way.`
|
|
2341
2473
|
);
|
|
2342
2474
|
}
|
|
2343
|
-
var HEAP_LIMIT_OVERSTATEMENT_BYTES, MINIMUM_BUDGET_BYTES, BASE_BYTES, ROW_BASE_BYTES, PER_CELL_BYTES;
|
|
2475
|
+
var HEAP_LIMIT_OVERSTATEMENT_BYTES, MINIMUM_BUDGET_BYTES, BASE_BYTES, ROW_BASE_BYTES, PER_CELL_BYTES, noBytesHeld;
|
|
2344
2476
|
var init_memoryUtils = __esm({
|
|
2345
2477
|
"src/util/memoryUtils.js"() {
|
|
2346
2478
|
init_QvdErrors();
|
|
@@ -2358,7 +2490,11 @@ var init_memoryUtils = __esm({
|
|
|
2358
2490
|
__name(estimateMemoryUsage, "estimateMemoryUsage");
|
|
2359
2491
|
__name(recommendedRowsFor, "recommendedRowsFor");
|
|
2360
2492
|
__name(recommendedChunkFor, "recommendedChunkFor");
|
|
2493
|
+
noBytesHeld = Object.freeze({ held: 0, forRows: /* @__PURE__ */ __name(() => 0, "forRows"), forChunk: /* @__PURE__ */ __name(() => 0, "forChunk") });
|
|
2361
2494
|
__name(validateMemoryAvailability, "validateMemoryAvailability");
|
|
2495
|
+
__name(checkMemory, "checkMemory");
|
|
2496
|
+
__name(answerOf, "answerOf");
|
|
2497
|
+
__name(budgetOf, "budgetOf");
|
|
2362
2498
|
__name(formatCount, "formatCount");
|
|
2363
2499
|
__name(warnLargeSymbolTable, "warnLargeSymbolTable");
|
|
2364
2500
|
}
|
|
@@ -3443,6 +3579,16 @@ async function parseHeaderXml(text, file, stage) {
|
|
|
3443
3579
|
}
|
|
3444
3580
|
return parsed;
|
|
3445
3581
|
}
|
|
3582
|
+
function symbolBytesOf(selected, symbolTableLength) {
|
|
3583
|
+
const areaBytes = selected.map((field) => headerInteger(field["Length"]));
|
|
3584
|
+
return areaBytes.every((bytes) => Number.isSafeInteger(bytes) && bytes >= 0) ? Math.min(
|
|
3585
|
+
symbolTableLength,
|
|
3586
|
+
areaBytes.reduce((sum, bytes) => sum + bytes, 0)
|
|
3587
|
+
) : symbolTableLength;
|
|
3588
|
+
}
|
|
3589
|
+
function readPasses(analysisAhead) {
|
|
3590
|
+
return analysisAhead ? 2 : 1;
|
|
3591
|
+
}
|
|
3446
3592
|
var MAX_HEADER_SIZE, READ_CHUNK_SIZE, ANALYSIS_SLICE_ROWS, SLICE_BYTES, COUNT_SYMBOLS_PAST, QvdFileReader;
|
|
3447
3593
|
var init_QvdFileReader = __esm({
|
|
3448
3594
|
"src/QvdFileReader.js"() {
|
|
@@ -3465,6 +3611,8 @@ var init_QvdFileReader = __esm({
|
|
|
3465
3611
|
COUNT_SYMBOLS_PAST = 65536;
|
|
3466
3612
|
__name(chunksFrom, "chunksFrom");
|
|
3467
3613
|
__name(parseHeaderXml, "parseHeaderXml");
|
|
3614
|
+
__name(symbolBytesOf, "symbolBytesOf");
|
|
3615
|
+
__name(readPasses, "readPasses");
|
|
3468
3616
|
QvdFileReader = class {
|
|
3469
3617
|
static {
|
|
3470
3618
|
__name(this, "QvdFileReader");
|
|
@@ -3806,6 +3954,15 @@ var init_QvdFileReader = __esm({
|
|
|
3806
3954
|
const indexTableOffset = symbolTableOffset + symbolTableLength;
|
|
3807
3955
|
const recordSize = headerInteger(headerObj["QvdTableHeader"]["RecordByteSize"]);
|
|
3808
3956
|
const totalRows = headerInteger(headerObj["QvdTableHeader"]["NoOfRecords"]);
|
|
3957
|
+
const { size: fileSize } = await handle.stat().catch(failed);
|
|
3958
|
+
this._fileSize = fileSize;
|
|
3959
|
+
this._headerMatchesFile = false;
|
|
3960
|
+
const headerNumbersUsable = [symbolTableLength, recordSize, totalRows].every(
|
|
3961
|
+
(value) => Number.isSafeInteger(value) && value >= 0
|
|
3962
|
+
);
|
|
3963
|
+
if (headerNumbersUsable) {
|
|
3964
|
+
this._headerMatchesFile = headerEndIndex + symbolTableLength + totalRows * recordSize <= fileSize;
|
|
3965
|
+
}
|
|
3809
3966
|
if (headerOnly) {
|
|
3810
3967
|
this._headerBuffer = headerBuffer.subarray(0, headerEndIndex);
|
|
3811
3968
|
this._emitProgress("read", 1, 1);
|
|
@@ -3814,20 +3971,7 @@ var init_QvdFileReader = __esm({
|
|
|
3814
3971
|
this._headerBuffer = headerBuffer.subarray(0, headerEndIndex);
|
|
3815
3972
|
const selected = selectFields(headerFields, this._requestedFields, this._path);
|
|
3816
3973
|
const columnCount = selected.length;
|
|
3817
|
-
const
|
|
3818
|
-
const symbolBytes = areaBytes.every((bytes) => Number.isSafeInteger(bytes) && bytes >= 0) ? Math.min(
|
|
3819
|
-
symbolTableLength,
|
|
3820
|
-
areaBytes.reduce((sum, bytes) => sum + bytes, 0)
|
|
3821
|
-
) : symbolTableLength;
|
|
3822
|
-
const headerNumbersUsable = [symbolTableLength, recordSize, totalRows].every(
|
|
3823
|
-
(value) => Number.isSafeInteger(value) && value >= 0
|
|
3824
|
-
);
|
|
3825
|
-
const { size: fileSize } = await handle.stat().catch(failed);
|
|
3826
|
-
this._fileSize = fileSize;
|
|
3827
|
-
this._headerMatchesFile = false;
|
|
3828
|
-
if (headerNumbersUsable) {
|
|
3829
|
-
this._headerMatchesFile = headerEndIndex + symbolTableLength + totalRows * recordSize <= fileSize;
|
|
3830
|
-
}
|
|
3974
|
+
const symbolBytes = symbolBytesOf(selected, symbolTableLength);
|
|
3831
3975
|
const resolved = headerNumbersUsable ? resolveWindow(window, totalRows) : { offset: 0, limit: 0 };
|
|
3832
3976
|
const windowRows = resolved.limit;
|
|
3833
3977
|
if (headerNumbersUsable && this._headerMatchesFile) {
|
|
@@ -3846,7 +3990,14 @@ var init_QvdFileReader = __esm({
|
|
|
3846
3990
|
recordSize,
|
|
3847
3991
|
liveRows,
|
|
3848
3992
|
this._analysisWouldRun(window, resolved, totalRows, symbolTableLength)
|
|
3849
|
-
)
|
|
3993
|
+
),
|
|
3994
|
+
// What it reads, which is not what it holds - the records go through one buffer and are not
|
|
3995
|
+
// kept. Carried so that a refusal's `check` says everything the pre-flight would have said.
|
|
3996
|
+
//
|
|
3997
|
+
// Twice over where the symbol-usage pass is still ahead of it: that pass reads the window's
|
|
3998
|
+
// records to find which symbols the rows use, and the decode then reads them again. Counted
|
|
3999
|
+
// once, the figure understated the I/O of exactly the reads that do the most of it.
|
|
4000
|
+
symbolBytes + readPasses(this._analysisWouldRun(window, resolved, totalRows, symbolTableLength)) * windowRows * recordSize
|
|
3850
4001
|
);
|
|
3851
4002
|
}
|
|
3852
4003
|
if (window.offset === 0 && window.limit === null) {
|
|
@@ -3915,8 +4066,9 @@ var init_QvdFileReader = __esm({
|
|
|
3915
4066
|
*
|
|
3916
4067
|
* The areas are counted whole, although the read lets each range go once its fields are parsed, because
|
|
3917
4068
|
* two ranges are both live when a field of one is parsed between two fields of the other - the order the
|
|
3918
|
-
* caller asked for the fields decides it, so the sum is what holds in every order. The slice is
|
|
3919
|
-
* `_forEachSlice` will allocate
|
|
4069
|
+
* caller asked for the fields decides it, so the sum is what holds in every order. The slice is the
|
|
4070
|
+
* buffer `_forEachSlice` will allocate, sized by `_sliceRowsFor` so that the charge and the allocation
|
|
4071
|
+
* are one expression rather than two that agree today.
|
|
3920
4072
|
*
|
|
3921
4073
|
* @param {number} symbolBytes Bytes of symbols the read will read.
|
|
3922
4074
|
* @param {number} rows Records it will read.
|
|
@@ -3925,8 +4077,24 @@ var init_QvdFileReader = __esm({
|
|
|
3925
4077
|
* @private
|
|
3926
4078
|
*/
|
|
3927
4079
|
_bytesHeldBy(symbolBytes, rows, recordSize) {
|
|
3928
|
-
const
|
|
3929
|
-
return symbolBytes +
|
|
4080
|
+
const usable = Number.isSafeInteger(rows) && Number.isSafeInteger(recordSize) && rows > 0 && recordSize > 0;
|
|
4081
|
+
return symbolBytes + (usable ? this._sliceRowsFor(rows, recordSize) * recordSize : 0);
|
|
4082
|
+
}
|
|
4083
|
+
/**
|
|
4084
|
+
* Records the one buffer holds while a read of `rowCount` records goes through it.
|
|
4085
|
+
*
|
|
4086
|
+
* A slice is `sliceBytes` of records, rounded down to a whole record, or every record the read has left
|
|
4087
|
+
* when that is fewer - and at least one, since a read of a record wider than `sliceBytes` still has to
|
|
4088
|
+
* hold that record. The single definition: `_forEachSlice` allocates from it and the memory guard is
|
|
4089
|
+
* charged from it, so a change to how a read slices cannot leave the guard pricing the old rule.
|
|
4090
|
+
*
|
|
4091
|
+
* @param {number} rowCount Records the read will read.
|
|
4092
|
+
* @param {number} recordSize Bytes per record.
|
|
4093
|
+
* @return {number} Records in one slice.
|
|
4094
|
+
* @private
|
|
4095
|
+
*/
|
|
4096
|
+
_sliceRowsFor(rowCount, recordSize) {
|
|
4097
|
+
return Math.max(1, Math.min(rowCount, Math.floor(this._sliceBytes / Math.max(1, recordSize))));
|
|
3930
4098
|
}
|
|
3931
4099
|
/**
|
|
3932
4100
|
* What a read holds in bytes of the file, for the memory guard: what it holds now, and what a read
|
|
@@ -4142,7 +4310,7 @@ var init_QvdFileReader = __esm({
|
|
|
4142
4310
|
return;
|
|
4143
4311
|
}
|
|
4144
4312
|
assert4(this._indexTableOffset !== null, "The QVD file header has not been parsed before its records were read.");
|
|
4145
|
-
const sliceRows =
|
|
4313
|
+
const sliceRows = this._sliceRowsFor(rowCount, recordSize);
|
|
4146
4314
|
const slice = Buffer.alloc(sliceRows * recordSize);
|
|
4147
4315
|
const requiredBytes = this._indexTableOffset + (firstRow + rowCount) * recordSize;
|
|
4148
4316
|
for (let done = 0; done < rowCount; done += sliceRows) {
|
|
@@ -4395,7 +4563,10 @@ var init_QvdFileReader = __esm({
|
|
|
4395
4563
|
fields.length,
|
|
4396
4564
|
this._materialisesRows,
|
|
4397
4565
|
liveRows,
|
|
4398
|
-
this._bytesHeld(symbolBytes, rowsToLoad, recordSize, liveRows, false)
|
|
4566
|
+
this._bytesHeld(symbolBytes, rowsToLoad, recordSize, liveRows, false),
|
|
4567
|
+
// `symbolsToKeep` is non-null exactly when the symbol-usage pass has run, and a pass that has
|
|
4568
|
+
// run has read the window's records once already - so the read's total is two passes over them.
|
|
4569
|
+
symbolBytes + readPasses(symbolsToKeep !== null) * rowsToLoad * recordSize
|
|
4399
4570
|
);
|
|
4400
4571
|
}
|
|
4401
4572
|
warnLargeSymbolTable(symbolBytes, rowsToLoad, totalRows, fields.length, this._materialisesRows);
|
|
@@ -4540,6 +4711,95 @@ var init_QvdFileReader = __esm({
|
|
|
4540
4711
|
};
|
|
4541
4712
|
});
|
|
4542
4713
|
}
|
|
4714
|
+
/**
|
|
4715
|
+
* What a read of this file would cost, and whether it fits, without reading it.
|
|
4716
|
+
*
|
|
4717
|
+
* Reads the header and the file's size and nothing else, at the constant cost of `loadMetadata()`,
|
|
4718
|
+
* then asks the same question a read asks before it allocates anything - through the same function,
|
|
4719
|
+
* from the same numbers. That is the whole point: an answer computed a second way would be a second
|
|
4720
|
+
* opinion, and a read this approves would still be refused.
|
|
4721
|
+
*
|
|
4722
|
+
* @param {number|null|{offset?: number, limit?: number|null, maxRows?: number|null}} [rawWindow]
|
|
4723
|
+
* The rows the read would cover, spelled any of the ways a read accepts.
|
|
4724
|
+
* @param {{chunkSize?: number|null}} [options] `chunkSize` when the read would be an `iterate()`,
|
|
4725
|
+
* which holds two chunks of rows rather than the window.
|
|
4726
|
+
* @return {Promise<any>} The answer - see `checkMemory`.
|
|
4727
|
+
*/
|
|
4728
|
+
async checkRead(rawWindow, { chunkSize = null } = {}) {
|
|
4729
|
+
const window = normaliseWindow(rawWindow, this._path);
|
|
4730
|
+
return await this._closingAfter(async () => {
|
|
4731
|
+
await this._readData({ offset: 0, limit: null }, true);
|
|
4732
|
+
this._emitProgress("header", 0, 1);
|
|
4733
|
+
await this._parseHeader();
|
|
4734
|
+
this._emitProgress("header", 1, 1);
|
|
4735
|
+
this._throwIfAborted();
|
|
4736
|
+
assert4(
|
|
4737
|
+
this._header && this._selectedFields && this._allFields && this._symbolTableOffset !== null,
|
|
4738
|
+
"The QVD file header has not been parsed."
|
|
4739
|
+
);
|
|
4740
|
+
const header = this._header["QvdTableHeader"];
|
|
4741
|
+
const totalRows = headerInteger(header["NoOfRecords"]);
|
|
4742
|
+
const recordSize = headerInteger(header["RecordByteSize"]);
|
|
4743
|
+
const symbolTableLength = headerInteger(header["Offset"]);
|
|
4744
|
+
const selected = this._selectedFields;
|
|
4745
|
+
validateRecordSize(recordSize, this._path, "checkRead");
|
|
4746
|
+
validateRecordCount(totalRows, this._path, "checkRead");
|
|
4747
|
+
if (!this._headerMatchesFile) {
|
|
4748
|
+
throw new QvdCorruptedError("The file is shorter than its header claims.", {
|
|
4749
|
+
file: this._path,
|
|
4750
|
+
fileSize: this._fileSize,
|
|
4751
|
+
requiredBytes: this._symbolTableOffset + symbolTableLength + totalRows * recordSize,
|
|
4752
|
+
stage: "checkRead"
|
|
4753
|
+
});
|
|
4754
|
+
}
|
|
4755
|
+
const tableLength = this._symbolTableLength();
|
|
4756
|
+
for (const field of this._allFields) {
|
|
4757
|
+
validateFieldMetadata(field, tableLength, this._path);
|
|
4758
|
+
validateFieldBitMetadata(field, recordSize, this._path);
|
|
4759
|
+
}
|
|
4760
|
+
validateSymbolAreas(this._allFields, this._path);
|
|
4761
|
+
const resolved = resolveWindow(window, totalRows);
|
|
4762
|
+
const windowRows = resolved.limit;
|
|
4763
|
+
const liveRows = chunkSize === null ? null : { rows: chunkSize * 2, perChunk: 2 };
|
|
4764
|
+
const analysisAhead = this._analysisWouldRun(window, resolved, totalRows, symbolTableLength);
|
|
4765
|
+
const measured = getMemoryBudget();
|
|
4766
|
+
const ask = /* @__PURE__ */ __name((fields, rows) => {
|
|
4767
|
+
const bytes = symbolBytesOf(fields, symbolTableLength);
|
|
4768
|
+
return checkMemory({
|
|
4769
|
+
measured,
|
|
4770
|
+
symbolTableSize: bytes,
|
|
4771
|
+
maxRows: rows,
|
|
4772
|
+
totalRows,
|
|
4773
|
+
safetyFactor: this._memorySafetyFactor,
|
|
4774
|
+
columnCount: fields.length,
|
|
4775
|
+
materialisesRows: this._materialisesRows,
|
|
4776
|
+
live: liveRows,
|
|
4777
|
+
bytesHeld: this._bytesHeld(bytes, rows, recordSize, liveRows, analysisAhead),
|
|
4778
|
+
// What it reads from the file, which is not what it holds: the symbol areas, and every record
|
|
4779
|
+
// the window covers, read a slice at a time and not kept - twice over where the symbol-usage
|
|
4780
|
+
// pass will run, since it reads them before the decode reads them again.
|
|
4781
|
+
readBytes: bytes + readPasses(analysisAhead) * windowRows * recordSize
|
|
4782
|
+
});
|
|
4783
|
+
}, "ask");
|
|
4784
|
+
const answer = ask(selected, windowRows);
|
|
4785
|
+
if (!answer.fits && selected.length > 1) {
|
|
4786
|
+
const bySize = [...selected].sort(
|
|
4787
|
+
(a, b) => headerInteger(a["Length"]) - headerInteger(b["Length"])
|
|
4788
|
+
);
|
|
4789
|
+
for (let take = selected.length - 1; take >= 1; take -= 1) {
|
|
4790
|
+
const fewer = bySize.slice(0, take);
|
|
4791
|
+
if (ask(fewer, windowRows).fits) {
|
|
4792
|
+
answer.suggestions.push({
|
|
4793
|
+
option: "fields",
|
|
4794
|
+
value: fewer.map((field) => field["FieldName"])
|
|
4795
|
+
});
|
|
4796
|
+
break;
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
return answer;
|
|
4801
|
+
});
|
|
4802
|
+
}
|
|
4543
4803
|
/**
|
|
4544
4804
|
* Loads the QVD file into memory and parses it.
|
|
4545
4805
|
*
|
|
@@ -4629,13 +4889,7 @@ var init_QvdFileReader = __esm({
|
|
|
4629
4889
|
* @return {AsyncGenerator<QvdDataFrame>} The chunks, in order.
|
|
4630
4890
|
*/
|
|
4631
4891
|
async *iterateRows(window, chunkSize) {
|
|
4632
|
-
|
|
4633
|
-
throw new QvdValidationError("chunkSize must be a positive integer", {
|
|
4634
|
-
provided: chunkSize,
|
|
4635
|
-
type: typeof chunkSize,
|
|
4636
|
-
file: this._path
|
|
4637
|
-
});
|
|
4638
|
-
}
|
|
4892
|
+
requireChunkSize(chunkSize, this._path);
|
|
4639
4893
|
const liveRows = { rows: chunkSize * 2, perChunk: 2 };
|
|
4640
4894
|
const rows = normaliseWindow(window, this._path);
|
|
4641
4895
|
this._startRead();
|
|
@@ -5550,6 +5804,68 @@ var init_QvdDataFrame = __esm({
|
|
|
5550
5804
|
const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
|
|
5551
5805
|
return await new QvdFileReader2(path5, metadataOptionsFrom(options)).loadMetadata();
|
|
5552
5806
|
}
|
|
5807
|
+
/**
|
|
5808
|
+
* Answers what a read would cost, and whether it fits, without doing it.
|
|
5809
|
+
*
|
|
5810
|
+
* Takes the options `fromQvd()` takes, plus `as` and `chunkSize` to say which read is being asked
|
|
5811
|
+
* about. Reads the header and the file's size and nothing else, at a cost that does not grow with
|
|
5812
|
+
* the file.
|
|
5813
|
+
*
|
|
5814
|
+
* The answer comes from the same function a read consults before it allocates anything, from the
|
|
5815
|
+
* same numbers, so **a read this approves is not refused later for memory** - and every suggestion
|
|
5816
|
+
* it carries has been read back through the check, so following one gives a read that fits.
|
|
5817
|
+
*
|
|
5818
|
+
* It answers about resources, so it answers only for a header it can trust. A header whose numbers
|
|
5819
|
+
* are not usable, or that claims more than the file holds, is refused as a `QvdCorruptedError` rather
|
|
5820
|
+
* than answered: sizing a read from numbers the file contradicts produced a memory verdict about a
|
|
5821
|
+
* file whose real problem was structural, and it was wrong in both directions - approving a read the
|
|
5822
|
+
* library then refused, and refusing another with advice that was refused too.
|
|
5823
|
+
*
|
|
5824
|
+
* That covers everything a reader can tell from the header: a field area past the end of the symbol
|
|
5825
|
+
* table, two fields claiming one area, a `Bias` that is neither 0 nor -2, a `BitWidth` past 31. Damage
|
|
5826
|
+
* that is not in the header - a value or an index the file has spoiled - is still found only by
|
|
5827
|
+
* reading, and still refused as a `QvdCorruptedError` after this has said the read fits.
|
|
5828
|
+
*
|
|
5829
|
+
* ```js
|
|
5830
|
+
* const answer = await QvdDataFrame.checkRead('huge.qvd', {as: 'columns', fields: ['Amount']});
|
|
5831
|
+
*
|
|
5832
|
+
* if (!answer.fits) {
|
|
5833
|
+
* console.log(answer.reason); // 'memory'
|
|
5834
|
+
* console.log(answer.suggestions); // [{option: 'limit', value: 1250000}, ...]
|
|
5835
|
+
* }
|
|
5836
|
+
* ```
|
|
5837
|
+
*
|
|
5838
|
+
* @param {string} path The QVD file.
|
|
5839
|
+
* @param {object} [options] What `fromQvd()` takes, plus the two below.
|
|
5840
|
+
* @param {'rows'|'columns'} [options.as='rows'] Which read is being asked about: `rows` builds row
|
|
5841
|
+
* arrays and `columns` does not, which is most of what a read costs.
|
|
5842
|
+
* @param {number|null} [options.chunkSize=null] The chunk an `iterate()` would use, which holds two
|
|
5843
|
+
* chunks of rows rather than the whole window.
|
|
5844
|
+
* @return {Promise<any>} The answer: `fits`, `reason` when it does not, `estimate`, `budget`,
|
|
5845
|
+
* `exact` and `suggestions`.
|
|
5846
|
+
* @throws {QvdValidationError} If an option's value is not valid, with `context.reason` of `option`.
|
|
5847
|
+
* @throws {QvdCorruptedError} If the header cannot be read, its numbers are not usable, or it claims
|
|
5848
|
+
* more than the file holds. The read refuses such a file too, though it may name the fault
|
|
5849
|
+
* differently - it gets there by planning the index table, where this gets there from the size.
|
|
5850
|
+
*/
|
|
5851
|
+
static async checkRead(path5, options = {}) {
|
|
5852
|
+
const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
|
|
5853
|
+
const { as = "rows", chunkSize = null } = options;
|
|
5854
|
+
if (as !== "rows" && as !== "columns") {
|
|
5855
|
+
throw new QvdValidationError("as must be 'rows' or 'columns'", {
|
|
5856
|
+
provided: as,
|
|
5857
|
+
reason: "option",
|
|
5858
|
+
option: "as",
|
|
5859
|
+
value: as,
|
|
5860
|
+
file: path5
|
|
5861
|
+
});
|
|
5862
|
+
}
|
|
5863
|
+
if (chunkSize !== null) {
|
|
5864
|
+
requireChunkSize(chunkSize, path5);
|
|
5865
|
+
}
|
|
5866
|
+
const reader = new QvdFileReader2(path5, { ...readerOptionsFrom(options), materialisesRows: as === "rows" });
|
|
5867
|
+
return await reader.checkRead(windowFrom(options), { chunkSize });
|
|
5868
|
+
}
|
|
5553
5869
|
/**
|
|
5554
5870
|
* Constructs a data frame from a dictionary.
|
|
5555
5871
|
*
|