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/README.md
CHANGED
|
@@ -80,6 +80,7 @@ holds.
|
|
|
80
80
|
| Rows from a file too large to hold | `QvdDataFrame.iterate(path, {chunkSize})` | Everything, but holds two chunks — a 96 MB heap against 512 MB |
|
|
81
81
|
| A few columns of a large file | `QvdColumnTable.fromQvd(path)` | Everything, but stops before building rows — 141 MiB against 385 MiB |
|
|
82
82
|
| Only the schema: names, row count, types | `QvdDataFrame.readMetadata(path)` | The header alone. Constant cost, whatever the file's size |
|
|
83
|
+
| To know whether a read will fit first | `QvdDataFrame.checkRead(path, options)` | The header alone. Answers rather than reads; a read it approves is not refused for memory |
|
|
83
84
|
|
|
84
85
|
```javascript
|
|
85
86
|
import {QvdDataFrame, QvdColumnTable} from 'qvdjs';
|
|
@@ -87,6 +88,9 @@ import {QvdDataFrame, QvdColumnTable} from 'qvdjs';
|
|
|
87
88
|
// What is in this file? Costs the same whether it is 20 KB or 20 GB.
|
|
88
89
|
const {columns, rowCount} = await QvdDataFrame.readMetadata('sales.qvd');
|
|
89
90
|
|
|
91
|
+
// Will reading it fit? Also the header alone, and every suggestion it gives has been checked.
|
|
92
|
+
const {fits, suggestions} = await QvdDataFrame.checkRead('sales.qvd');
|
|
93
|
+
|
|
90
94
|
// Sum one column without ever building a row.
|
|
91
95
|
const table = await QvdColumnTable.fromQvd('sales.qvd');
|
|
92
96
|
let total = 0;
|
package/dist/index.cjs
CHANGED
|
@@ -453,7 +453,9 @@ var init_optionTypes = __esm({
|
|
|
453
453
|
function requireRowCount(value, name, filePath) {
|
|
454
454
|
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
455
455
|
throw new exports.QvdValidationError(`${name} must be a non-negative integer`, {
|
|
456
|
+
reason: "option",
|
|
456
457
|
option: name,
|
|
458
|
+
value,
|
|
457
459
|
provided: value,
|
|
458
460
|
type: typeof value,
|
|
459
461
|
file: filePath
|
|
@@ -470,6 +472,9 @@ function normaliseWindow(window, filePath) {
|
|
|
470
472
|
}
|
|
471
473
|
if (typeof window !== "object" || Array.isArray(window)) {
|
|
472
474
|
throw new exports.QvdValidationError("The row window must be a number, null, or an {offset, limit} object", {
|
|
475
|
+
reason: "option",
|
|
476
|
+
option: "limit",
|
|
477
|
+
value: window,
|
|
473
478
|
provided: window,
|
|
474
479
|
type: typeof window,
|
|
475
480
|
file: filePath
|
|
@@ -480,6 +485,9 @@ function normaliseWindow(window, filePath) {
|
|
|
480
485
|
const maxRowsGiven = maxRows !== void 0 && maxRows !== null;
|
|
481
486
|
if (limitGiven && maxRowsGiven) {
|
|
482
487
|
throw new exports.QvdValidationError("maxRows and limit are two names for the same option; pass one of them, not both", {
|
|
488
|
+
reason: "option",
|
|
489
|
+
option: "limit",
|
|
490
|
+
value: limit,
|
|
483
491
|
maxRows,
|
|
484
492
|
limit,
|
|
485
493
|
file: filePath
|
|
@@ -490,6 +498,19 @@ function normaliseWindow(window, filePath) {
|
|
|
490
498
|
limit: limitGiven ? requireRowCount(limit, "limit", filePath) : maxRowsGiven ? requireRowCount(maxRows, "maxRows", filePath) : null
|
|
491
499
|
};
|
|
492
500
|
}
|
|
501
|
+
function requireChunkSize(chunkSize, filePath) {
|
|
502
|
+
if (typeof chunkSize !== "number" || !Number.isInteger(chunkSize) || chunkSize <= 0) {
|
|
503
|
+
throw new exports.QvdValidationError("chunkSize must be a positive integer", {
|
|
504
|
+
reason: "option",
|
|
505
|
+
option: "chunkSize",
|
|
506
|
+
value: chunkSize,
|
|
507
|
+
provided: chunkSize,
|
|
508
|
+
type: typeof chunkSize,
|
|
509
|
+
file: filePath
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return chunkSize;
|
|
513
|
+
}
|
|
493
514
|
function resolveWindow(window, totalRows) {
|
|
494
515
|
const rows = Number.isSafeInteger(totalRows) && totalRows > 0 ? totalRows : 0;
|
|
495
516
|
const offset = Math.min(window.offset, rows);
|
|
@@ -504,6 +525,9 @@ function selectFields(fields, requested, filePath) {
|
|
|
504
525
|
}
|
|
505
526
|
if (!Array.isArray(requested)) {
|
|
506
527
|
throw new exports.QvdValidationError("fields must be an array of field names", {
|
|
528
|
+
reason: "option",
|
|
529
|
+
option: "fields",
|
|
530
|
+
value: requested,
|
|
507
531
|
provided: requested,
|
|
508
532
|
type: typeof requested,
|
|
509
533
|
file: filePath
|
|
@@ -512,6 +536,9 @@ function selectFields(fields, requested, filePath) {
|
|
|
512
536
|
const available = fields.map((field) => field["FieldName"]);
|
|
513
537
|
if (requested.length === 0) {
|
|
514
538
|
throw new exports.QvdValidationError("fields must name at least one field", {
|
|
539
|
+
reason: "option",
|
|
540
|
+
option: "fields",
|
|
541
|
+
value: requested,
|
|
515
542
|
availableColumns: available,
|
|
516
543
|
file: filePath
|
|
517
544
|
});
|
|
@@ -520,6 +547,9 @@ function selectFields(fields, requested, filePath) {
|
|
|
520
547
|
return requested.map((name) => {
|
|
521
548
|
if (typeof name !== "string") {
|
|
522
549
|
throw new exports.QvdValidationError("Field names must be strings", {
|
|
550
|
+
reason: "option",
|
|
551
|
+
option: "fields",
|
|
552
|
+
value: name,
|
|
523
553
|
provided: name,
|
|
524
554
|
type: typeof name,
|
|
525
555
|
availableColumns: available,
|
|
@@ -528,6 +558,9 @@ function selectFields(fields, requested, filePath) {
|
|
|
528
558
|
}
|
|
529
559
|
if (seen.has(name)) {
|
|
530
560
|
throw new exports.QvdValidationError(`Field '${name}' is listed twice`, {
|
|
561
|
+
reason: "option",
|
|
562
|
+
option: "fields",
|
|
563
|
+
value: name,
|
|
531
564
|
column: name,
|
|
532
565
|
fields: requested,
|
|
533
566
|
file: filePath
|
|
@@ -537,6 +570,9 @@ function selectFields(fields, requested, filePath) {
|
|
|
537
570
|
const index = available.indexOf(name);
|
|
538
571
|
if (index === -1) {
|
|
539
572
|
throw new exports.QvdValidationError(`Column '${name}' does not exist`, {
|
|
573
|
+
reason: "option",
|
|
574
|
+
option: "fields",
|
|
575
|
+
value: name,
|
|
540
576
|
column: name,
|
|
541
577
|
availableColumns: available,
|
|
542
578
|
file: filePath
|
|
@@ -551,7 +587,9 @@ function normaliseDuals(value, filePath) {
|
|
|
551
587
|
}
|
|
552
588
|
if (!DUAL_MODES.includes(value)) {
|
|
553
589
|
throw new exports.QvdValidationError(`duals must be one of ${DUAL_MODES.map((mode) => `'${mode}'`).join(", ")}`, {
|
|
590
|
+
reason: "option",
|
|
554
591
|
option: "duals",
|
|
592
|
+
value,
|
|
555
593
|
provided: value,
|
|
556
594
|
file: filePath
|
|
557
595
|
});
|
|
@@ -590,6 +628,7 @@ var init_readOptions = __esm({
|
|
|
590
628
|
init_optionTypes();
|
|
591
629
|
__name(requireRowCount, "requireRowCount");
|
|
592
630
|
__name(normaliseWindow, "normaliseWindow");
|
|
631
|
+
__name(requireChunkSize, "requireChunkSize");
|
|
593
632
|
__name(resolveWindow, "resolveWindow");
|
|
594
633
|
__name(selectFields, "selectFields");
|
|
595
634
|
DUAL_MODES = Object.freeze(["number", "text", "both"]);
|
|
@@ -2218,22 +2257,56 @@ function recommendedChunkFor(budget, symbolTableSize, windowRows, totalRows, col
|
|
|
2218
2257
|
}
|
|
2219
2258
|
return low;
|
|
2220
2259
|
}
|
|
2221
|
-
function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.8, columnCount = 0, materialisesRows = true, live = null, bytesHeld =
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2260
|
+
function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.8, columnCount = 0, materialisesRows = true, live = null, bytesHeld = null, readBytes = null) {
|
|
2261
|
+
const answer = checkMemory({
|
|
2262
|
+
symbolTableSize,
|
|
2263
|
+
maxRows,
|
|
2264
|
+
totalRows,
|
|
2265
|
+
safetyFactor,
|
|
2266
|
+
columnCount,
|
|
2267
|
+
materialisesRows,
|
|
2268
|
+
live,
|
|
2269
|
+
bytesHeld,
|
|
2270
|
+
readBytes
|
|
2271
|
+
});
|
|
2272
|
+
if (answer.fits) {
|
|
2226
2273
|
return;
|
|
2227
2274
|
}
|
|
2228
|
-
const
|
|
2275
|
+
const { message, context } = answer.refusal;
|
|
2276
|
+
throw new exports.QvdValidationError(message, {
|
|
2277
|
+
file: filePath,
|
|
2278
|
+
...context,
|
|
2279
|
+
reason: "memory",
|
|
2280
|
+
check: answerOf(answer)
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
function checkMemory({
|
|
2284
|
+
symbolTableSize,
|
|
2285
|
+
maxRows,
|
|
2286
|
+
totalRows,
|
|
2287
|
+
safetyFactor = 0.8,
|
|
2288
|
+
columnCount = 0,
|
|
2289
|
+
materialisesRows = true,
|
|
2290
|
+
live = null,
|
|
2291
|
+
bytesHeld = null,
|
|
2292
|
+
readBytes = null,
|
|
2293
|
+
measured = null
|
|
2294
|
+
}) {
|
|
2295
|
+
if (typeof safetyFactor !== "number" || safetyFactor < 0 || safetyFactor > 1) {
|
|
2296
|
+
throw new exports.QvdValidationError("safetyFactor must be a number between 0.0 and 1.0", {
|
|
2297
|
+
safetyFactor,
|
|
2298
|
+
reason: "option",
|
|
2299
|
+
option: "memorySafetyFactor",
|
|
2300
|
+
value: safetyFactor
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
const budget = measured ?? getMemoryBudget();
|
|
2229
2304
|
const rowsToLoad = maxRows === null || maxRows >= totalRows ? totalRows : maxRows;
|
|
2230
2305
|
const rowsLive = live === null ? null : live.rows;
|
|
2231
2306
|
const liveRowsPerChunk = live === null ? 1 : live.perChunk;
|
|
2232
2307
|
const liveRows = rowsLive === null ? rowsToLoad : Math.min(rowsLive, rowsToLoad);
|
|
2233
2308
|
const heapMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows, columnCount, materialisesRows, rowsLive);
|
|
2234
|
-
const held
|
|
2235
|
-
const heldForRows = typeof bytesHeld === "number" ? () => bytesHeld : bytesHeld.forRows;
|
|
2236
|
-
const heldForChunk = typeof bytesHeld === "number" ? () => bytesHeld : bytesHeld.forChunk;
|
|
2309
|
+
const { held, forRows: heldForRows, forChunk: heldForChunk } = bytesHeld ?? noBytesHeld;
|
|
2237
2310
|
const externalMemory = estimateExternalMemory(liveRows, columnCount) + held;
|
|
2238
2311
|
const bounded = budget.candidates.map((candidate) => {
|
|
2239
2312
|
const heapOnly = candidate.source === "V8 heap limit";
|
|
@@ -2245,11 +2318,26 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
2245
2318
|
bounds: heapOnly ? "the V8 heap" : "the whole process"
|
|
2246
2319
|
};
|
|
2247
2320
|
});
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2321
|
+
if (safetyFactor === 0) {
|
|
2322
|
+
const lowest = budget.candidates.reduce((least, candidate) => candidate.bytes < least.bytes ? candidate : least);
|
|
2323
|
+
return {
|
|
2324
|
+
fits: true,
|
|
2325
|
+
estimate: { heapBytes: heapMemory, externalBytes: externalMemory, readBytes },
|
|
2326
|
+
// The ceilings are still there and still named - what is missing is any measurement against them.
|
|
2327
|
+
// An earlier version reported `bound: 'none'` here, a third value in a two-value vocabulary that a
|
|
2328
|
+
// caller switching on the documented two would fall straight through.
|
|
2329
|
+
budget: {
|
|
2330
|
+
...budgetOf(budget, { ...lowest, heapOnly: lowest.source === "V8 heap limit" }, 0),
|
|
2331
|
+
allowedBytes: Infinity
|
|
2332
|
+
},
|
|
2333
|
+
exact: symbolTableSize === 0,
|
|
2334
|
+
suggestions: []
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
const tightest = bounded.reduce(
|
|
2338
|
+
(worst, candidate) => candidate.needs / candidate.allowed > worst.needs / worst.allowed ? candidate : worst
|
|
2252
2339
|
);
|
|
2340
|
+
const binding = tightest.needs > tightest.allowed ? tightest : null;
|
|
2253
2341
|
const heapLimit = getHeapLimit();
|
|
2254
2342
|
const availableMemory = binding ? binding.bytes : budget.bytes;
|
|
2255
2343
|
const estimatedMemory = binding ? binding.needs : heapMemory;
|
|
@@ -2306,31 +2394,75 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
2306
2394
|
} else {
|
|
2307
2395
|
advice = `Try holding fewer rows using the ${knob} parameter (recommended: ${formatCount(recommendedValue)} rows or less), or raise the heap with --max-old-space-size.`;
|
|
2308
2396
|
}
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
{
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
heapLimitMB,
|
|
2318
|
-
reportedHeapLimitMB,
|
|
2319
|
-
availableRamMB,
|
|
2320
|
-
limitingFactor,
|
|
2321
|
-
limitingScope,
|
|
2322
|
-
memoryBudget: budget.candidates,
|
|
2323
|
-
memoryObserved: budget.observed,
|
|
2324
|
-
columnCount,
|
|
2325
|
-
totalRows,
|
|
2326
|
-
maxRows,
|
|
2327
|
-
recommendedMaxRows,
|
|
2328
|
-
// Only present when a chunk size is what overflowed, so a caller cannot mistake one
|
|
2329
|
-
// recommendation for the other.
|
|
2330
|
-
...chunked ? { rowsLive, recommendedChunkSize: recommendedChunk } : {}
|
|
2397
|
+
const suggestions = [];
|
|
2398
|
+
if (!nothingFits) {
|
|
2399
|
+
suggestions.push({ option: knob, value: recommendedValue });
|
|
2400
|
+
}
|
|
2401
|
+
if (!containerBound) {
|
|
2402
|
+
let needed = Math.ceil(estimatedMemory / safetyFactor / (1024 * 1024));
|
|
2403
|
+
while (Math.max(needed * 1024 * 1024, MINIMUM_BUDGET_BYTES) * safetyFactor < estimatedMemory) {
|
|
2404
|
+
needed += 1;
|
|
2331
2405
|
}
|
|
2332
|
-
|
|
2406
|
+
suggestions.push({ nodeOption: "--max-old-space-size", value: needed });
|
|
2407
|
+
}
|
|
2408
|
+
return {
|
|
2409
|
+
fits: false,
|
|
2410
|
+
reason: "memory",
|
|
2411
|
+
estimate: { heapBytes: heapMemory, externalBytes: externalMemory, readBytes },
|
|
2412
|
+
budget: budgetOf(budget, tightest, safetyFactor),
|
|
2413
|
+
// The symbol term is six times the bytes on disk, an overhead measured across files rather than
|
|
2414
|
+
// derived, so any read with symbols in it is an estimate and says so. Only a read that decodes
|
|
2415
|
+
// nothing can be exact.
|
|
2416
|
+
exact: symbolTableSize === 0,
|
|
2417
|
+
suggestions,
|
|
2418
|
+
refusal: {
|
|
2419
|
+
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,
|
|
2420
|
+
context: {
|
|
2421
|
+
symbolTableSize,
|
|
2422
|
+
symbolTableSizeMB: sizeMB,
|
|
2423
|
+
estimatedMemoryMB: estimatedMB,
|
|
2424
|
+
availableMemoryMB: availableMB,
|
|
2425
|
+
heapLimitMB,
|
|
2426
|
+
reportedHeapLimitMB,
|
|
2427
|
+
availableRamMB,
|
|
2428
|
+
limitingFactor,
|
|
2429
|
+
limitingScope,
|
|
2430
|
+
memoryBudget: budget.candidates,
|
|
2431
|
+
memoryObserved: budget.observed,
|
|
2432
|
+
columnCount,
|
|
2433
|
+
totalRows,
|
|
2434
|
+
maxRows,
|
|
2435
|
+
recommendedMaxRows,
|
|
2436
|
+
// Only present when a chunk size is what overflowed, so a caller cannot mistake one
|
|
2437
|
+
// recommendation for the other.
|
|
2438
|
+
...chunked ? { rowsLive, recommendedChunkSize: recommendedChunk } : {}
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
};
|
|
2333
2442
|
}
|
|
2443
|
+
return {
|
|
2444
|
+
fits: true,
|
|
2445
|
+
estimate: { heapBytes: heapMemory, externalBytes: externalMemory, readBytes },
|
|
2446
|
+
budget: budgetOf(budget, tightest, safetyFactor),
|
|
2447
|
+
exact: symbolTableSize === 0,
|
|
2448
|
+
suggestions: []
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
function answerOf(answer) {
|
|
2452
|
+
const { refusal, ...rest } = answer;
|
|
2453
|
+
return rest;
|
|
2454
|
+
}
|
|
2455
|
+
function budgetOf(budget, tightest, safetyFactor) {
|
|
2456
|
+
const processLimit = budget.candidates.find((candidate) => candidate.source !== "V8 heap limit");
|
|
2457
|
+
return {
|
|
2458
|
+
heapBytes: usableOldSpaceLimit(),
|
|
2459
|
+
processBytes: processLimit ? processLimit.bytes : null,
|
|
2460
|
+
bound: tightest.heapOnly ? "heap" : "process",
|
|
2461
|
+
safetyFactor,
|
|
2462
|
+
allowedBytes: tightest.allowed ?? tightest.bytes * safetyFactor,
|
|
2463
|
+
candidates: budget.candidates,
|
|
2464
|
+
observed: budget.observed
|
|
2465
|
+
};
|
|
2334
2466
|
}
|
|
2335
2467
|
function formatCount(value) {
|
|
2336
2468
|
return value.toLocaleString("en-US");
|
|
@@ -2352,7 +2484,7 @@ function warnLargeSymbolTable(symbolTableSize, maxRows, totalRows, columnCount =
|
|
|
2352
2484
|
`\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.`
|
|
2353
2485
|
);
|
|
2354
2486
|
}
|
|
2355
|
-
var HEAP_LIMIT_OVERSTATEMENT_BYTES, MINIMUM_BUDGET_BYTES, BASE_BYTES, ROW_BASE_BYTES, PER_CELL_BYTES;
|
|
2487
|
+
var HEAP_LIMIT_OVERSTATEMENT_BYTES, MINIMUM_BUDGET_BYTES, BASE_BYTES, ROW_BASE_BYTES, PER_CELL_BYTES, noBytesHeld;
|
|
2356
2488
|
var init_memoryUtils = __esm({
|
|
2357
2489
|
"src/util/memoryUtils.js"() {
|
|
2358
2490
|
init_QvdErrors();
|
|
@@ -2370,7 +2502,11 @@ var init_memoryUtils = __esm({
|
|
|
2370
2502
|
__name(estimateMemoryUsage, "estimateMemoryUsage");
|
|
2371
2503
|
__name(recommendedRowsFor, "recommendedRowsFor");
|
|
2372
2504
|
__name(recommendedChunkFor, "recommendedChunkFor");
|
|
2505
|
+
noBytesHeld = Object.freeze({ held: 0, forRows: /* @__PURE__ */ __name(() => 0, "forRows"), forChunk: /* @__PURE__ */ __name(() => 0, "forChunk") });
|
|
2373
2506
|
__name(validateMemoryAvailability, "validateMemoryAvailability");
|
|
2507
|
+
__name(checkMemory, "checkMemory");
|
|
2508
|
+
__name(answerOf, "answerOf");
|
|
2509
|
+
__name(budgetOf, "budgetOf");
|
|
2374
2510
|
__name(formatCount, "formatCount");
|
|
2375
2511
|
__name(warnLargeSymbolTable, "warnLargeSymbolTable");
|
|
2376
2512
|
}
|
|
@@ -3455,6 +3591,16 @@ async function parseHeaderXml(text, file, stage) {
|
|
|
3455
3591
|
}
|
|
3456
3592
|
return parsed;
|
|
3457
3593
|
}
|
|
3594
|
+
function symbolBytesOf(selected, symbolTableLength) {
|
|
3595
|
+
const areaBytes = selected.map((field) => headerInteger(field["Length"]));
|
|
3596
|
+
return areaBytes.every((bytes) => Number.isSafeInteger(bytes) && bytes >= 0) ? Math.min(
|
|
3597
|
+
symbolTableLength,
|
|
3598
|
+
areaBytes.reduce((sum, bytes) => sum + bytes, 0)
|
|
3599
|
+
) : symbolTableLength;
|
|
3600
|
+
}
|
|
3601
|
+
function readPasses(analysisAhead) {
|
|
3602
|
+
return analysisAhead ? 2 : 1;
|
|
3603
|
+
}
|
|
3458
3604
|
var MAX_HEADER_SIZE, READ_CHUNK_SIZE, ANALYSIS_SLICE_ROWS, SLICE_BYTES, COUNT_SYMBOLS_PAST; exports.QvdFileReader = void 0;
|
|
3459
3605
|
var init_QvdFileReader = __esm({
|
|
3460
3606
|
"src/QvdFileReader.js"() {
|
|
@@ -3477,6 +3623,8 @@ var init_QvdFileReader = __esm({
|
|
|
3477
3623
|
COUNT_SYMBOLS_PAST = 65536;
|
|
3478
3624
|
__name(chunksFrom, "chunksFrom");
|
|
3479
3625
|
__name(parseHeaderXml, "parseHeaderXml");
|
|
3626
|
+
__name(symbolBytesOf, "symbolBytesOf");
|
|
3627
|
+
__name(readPasses, "readPasses");
|
|
3480
3628
|
exports.QvdFileReader = class {
|
|
3481
3629
|
static {
|
|
3482
3630
|
__name(this, "QvdFileReader");
|
|
@@ -3818,6 +3966,15 @@ var init_QvdFileReader = __esm({
|
|
|
3818
3966
|
const indexTableOffset = symbolTableOffset + symbolTableLength;
|
|
3819
3967
|
const recordSize = headerInteger(headerObj["QvdTableHeader"]["RecordByteSize"]);
|
|
3820
3968
|
const totalRows = headerInteger(headerObj["QvdTableHeader"]["NoOfRecords"]);
|
|
3969
|
+
const { size: fileSize } = await handle.stat().catch(failed);
|
|
3970
|
+
this._fileSize = fileSize;
|
|
3971
|
+
this._headerMatchesFile = false;
|
|
3972
|
+
const headerNumbersUsable = [symbolTableLength, recordSize, totalRows].every(
|
|
3973
|
+
(value) => Number.isSafeInteger(value) && value >= 0
|
|
3974
|
+
);
|
|
3975
|
+
if (headerNumbersUsable) {
|
|
3976
|
+
this._headerMatchesFile = headerEndIndex + symbolTableLength + totalRows * recordSize <= fileSize;
|
|
3977
|
+
}
|
|
3821
3978
|
if (headerOnly) {
|
|
3822
3979
|
this._headerBuffer = headerBuffer.subarray(0, headerEndIndex);
|
|
3823
3980
|
this._emitProgress("read", 1, 1);
|
|
@@ -3826,20 +3983,7 @@ var init_QvdFileReader = __esm({
|
|
|
3826
3983
|
this._headerBuffer = headerBuffer.subarray(0, headerEndIndex);
|
|
3827
3984
|
const selected = selectFields(headerFields, this._requestedFields, this._path);
|
|
3828
3985
|
const columnCount = selected.length;
|
|
3829
|
-
const
|
|
3830
|
-
const symbolBytes = areaBytes.every((bytes) => Number.isSafeInteger(bytes) && bytes >= 0) ? Math.min(
|
|
3831
|
-
symbolTableLength,
|
|
3832
|
-
areaBytes.reduce((sum, bytes) => sum + bytes, 0)
|
|
3833
|
-
) : symbolTableLength;
|
|
3834
|
-
const headerNumbersUsable = [symbolTableLength, recordSize, totalRows].every(
|
|
3835
|
-
(value) => Number.isSafeInteger(value) && value >= 0
|
|
3836
|
-
);
|
|
3837
|
-
const { size: fileSize } = await handle.stat().catch(failed);
|
|
3838
|
-
this._fileSize = fileSize;
|
|
3839
|
-
this._headerMatchesFile = false;
|
|
3840
|
-
if (headerNumbersUsable) {
|
|
3841
|
-
this._headerMatchesFile = headerEndIndex + symbolTableLength + totalRows * recordSize <= fileSize;
|
|
3842
|
-
}
|
|
3986
|
+
const symbolBytes = symbolBytesOf(selected, symbolTableLength);
|
|
3843
3987
|
const resolved = headerNumbersUsable ? resolveWindow(window, totalRows) : { offset: 0, limit: 0 };
|
|
3844
3988
|
const windowRows = resolved.limit;
|
|
3845
3989
|
if (headerNumbersUsable && this._headerMatchesFile) {
|
|
@@ -3858,7 +4002,14 @@ var init_QvdFileReader = __esm({
|
|
|
3858
4002
|
recordSize,
|
|
3859
4003
|
liveRows,
|
|
3860
4004
|
this._analysisWouldRun(window, resolved, totalRows, symbolTableLength)
|
|
3861
|
-
)
|
|
4005
|
+
),
|
|
4006
|
+
// What it reads, which is not what it holds - the records go through one buffer and are not
|
|
4007
|
+
// kept. Carried so that a refusal's `check` says everything the pre-flight would have said.
|
|
4008
|
+
//
|
|
4009
|
+
// Twice over where the symbol-usage pass is still ahead of it: that pass reads the window's
|
|
4010
|
+
// records to find which symbols the rows use, and the decode then reads them again. Counted
|
|
4011
|
+
// once, the figure understated the I/O of exactly the reads that do the most of it.
|
|
4012
|
+
symbolBytes + readPasses(this._analysisWouldRun(window, resolved, totalRows, symbolTableLength)) * windowRows * recordSize
|
|
3862
4013
|
);
|
|
3863
4014
|
}
|
|
3864
4015
|
if (window.offset === 0 && window.limit === null) {
|
|
@@ -3927,8 +4078,9 @@ var init_QvdFileReader = __esm({
|
|
|
3927
4078
|
*
|
|
3928
4079
|
* The areas are counted whole, although the read lets each range go once its fields are parsed, because
|
|
3929
4080
|
* two ranges are both live when a field of one is parsed between two fields of the other - the order the
|
|
3930
|
-
* caller asked for the fields decides it, so the sum is what holds in every order. The slice is
|
|
3931
|
-
* `_forEachSlice` will allocate
|
|
4081
|
+
* caller asked for the fields decides it, so the sum is what holds in every order. The slice is the
|
|
4082
|
+
* buffer `_forEachSlice` will allocate, sized by `_sliceRowsFor` so that the charge and the allocation
|
|
4083
|
+
* are one expression rather than two that agree today.
|
|
3932
4084
|
*
|
|
3933
4085
|
* @param {number} symbolBytes Bytes of symbols the read will read.
|
|
3934
4086
|
* @param {number} rows Records it will read.
|
|
@@ -3937,8 +4089,24 @@ var init_QvdFileReader = __esm({
|
|
|
3937
4089
|
* @private
|
|
3938
4090
|
*/
|
|
3939
4091
|
_bytesHeldBy(symbolBytes, rows, recordSize) {
|
|
3940
|
-
const
|
|
3941
|
-
return symbolBytes +
|
|
4092
|
+
const usable = Number.isSafeInteger(rows) && Number.isSafeInteger(recordSize) && rows > 0 && recordSize > 0;
|
|
4093
|
+
return symbolBytes + (usable ? this._sliceRowsFor(rows, recordSize) * recordSize : 0);
|
|
4094
|
+
}
|
|
4095
|
+
/**
|
|
4096
|
+
* Records the one buffer holds while a read of `rowCount` records goes through it.
|
|
4097
|
+
*
|
|
4098
|
+
* A slice is `sliceBytes` of records, rounded down to a whole record, or every record the read has left
|
|
4099
|
+
* when that is fewer - and at least one, since a read of a record wider than `sliceBytes` still has to
|
|
4100
|
+
* hold that record. The single definition: `_forEachSlice` allocates from it and the memory guard is
|
|
4101
|
+
* charged from it, so a change to how a read slices cannot leave the guard pricing the old rule.
|
|
4102
|
+
*
|
|
4103
|
+
* @param {number} rowCount Records the read will read.
|
|
4104
|
+
* @param {number} recordSize Bytes per record.
|
|
4105
|
+
* @return {number} Records in one slice.
|
|
4106
|
+
* @private
|
|
4107
|
+
*/
|
|
4108
|
+
_sliceRowsFor(rowCount, recordSize) {
|
|
4109
|
+
return Math.max(1, Math.min(rowCount, Math.floor(this._sliceBytes / Math.max(1, recordSize))));
|
|
3942
4110
|
}
|
|
3943
4111
|
/**
|
|
3944
4112
|
* What a read holds in bytes of the file, for the memory guard: what it holds now, and what a read
|
|
@@ -4154,7 +4322,7 @@ var init_QvdFileReader = __esm({
|
|
|
4154
4322
|
return;
|
|
4155
4323
|
}
|
|
4156
4324
|
assert4__default.default(this._indexTableOffset !== null, "The QVD file header has not been parsed before its records were read.");
|
|
4157
|
-
const sliceRows =
|
|
4325
|
+
const sliceRows = this._sliceRowsFor(rowCount, recordSize);
|
|
4158
4326
|
const slice = Buffer.alloc(sliceRows * recordSize);
|
|
4159
4327
|
const requiredBytes = this._indexTableOffset + (firstRow + rowCount) * recordSize;
|
|
4160
4328
|
for (let done = 0; done < rowCount; done += sliceRows) {
|
|
@@ -4407,7 +4575,10 @@ var init_QvdFileReader = __esm({
|
|
|
4407
4575
|
fields.length,
|
|
4408
4576
|
this._materialisesRows,
|
|
4409
4577
|
liveRows,
|
|
4410
|
-
this._bytesHeld(symbolBytes, rowsToLoad, recordSize, liveRows, false)
|
|
4578
|
+
this._bytesHeld(symbolBytes, rowsToLoad, recordSize, liveRows, false),
|
|
4579
|
+
// `symbolsToKeep` is non-null exactly when the symbol-usage pass has run, and a pass that has
|
|
4580
|
+
// run has read the window's records once already - so the read's total is two passes over them.
|
|
4581
|
+
symbolBytes + readPasses(symbolsToKeep !== null) * rowsToLoad * recordSize
|
|
4411
4582
|
);
|
|
4412
4583
|
}
|
|
4413
4584
|
warnLargeSymbolTable(symbolBytes, rowsToLoad, totalRows, fields.length, this._materialisesRows);
|
|
@@ -4552,6 +4723,95 @@ var init_QvdFileReader = __esm({
|
|
|
4552
4723
|
};
|
|
4553
4724
|
});
|
|
4554
4725
|
}
|
|
4726
|
+
/**
|
|
4727
|
+
* What a read of this file would cost, and whether it fits, without reading it.
|
|
4728
|
+
*
|
|
4729
|
+
* Reads the header and the file's size and nothing else, at the constant cost of `loadMetadata()`,
|
|
4730
|
+
* then asks the same question a read asks before it allocates anything - through the same function,
|
|
4731
|
+
* from the same numbers. That is the whole point: an answer computed a second way would be a second
|
|
4732
|
+
* opinion, and a read this approves would still be refused.
|
|
4733
|
+
*
|
|
4734
|
+
* @param {number|null|{offset?: number, limit?: number|null, maxRows?: number|null}} [rawWindow]
|
|
4735
|
+
* The rows the read would cover, spelled any of the ways a read accepts.
|
|
4736
|
+
* @param {{chunkSize?: number|null}} [options] `chunkSize` when the read would be an `iterate()`,
|
|
4737
|
+
* which holds two chunks of rows rather than the window.
|
|
4738
|
+
* @return {Promise<any>} The answer - see `checkMemory`.
|
|
4739
|
+
*/
|
|
4740
|
+
async checkRead(rawWindow, { chunkSize = null } = {}) {
|
|
4741
|
+
const window = normaliseWindow(rawWindow, this._path);
|
|
4742
|
+
return await this._closingAfter(async () => {
|
|
4743
|
+
await this._readData({ offset: 0, limit: null }, true);
|
|
4744
|
+
this._emitProgress("header", 0, 1);
|
|
4745
|
+
await this._parseHeader();
|
|
4746
|
+
this._emitProgress("header", 1, 1);
|
|
4747
|
+
this._throwIfAborted();
|
|
4748
|
+
assert4__default.default(
|
|
4749
|
+
this._header && this._selectedFields && this._allFields && this._symbolTableOffset !== null,
|
|
4750
|
+
"The QVD file header has not been parsed."
|
|
4751
|
+
);
|
|
4752
|
+
const header = this._header["QvdTableHeader"];
|
|
4753
|
+
const totalRows = headerInteger(header["NoOfRecords"]);
|
|
4754
|
+
const recordSize = headerInteger(header["RecordByteSize"]);
|
|
4755
|
+
const symbolTableLength = headerInteger(header["Offset"]);
|
|
4756
|
+
const selected = this._selectedFields;
|
|
4757
|
+
validateRecordSize(recordSize, this._path, "checkRead");
|
|
4758
|
+
validateRecordCount(totalRows, this._path, "checkRead");
|
|
4759
|
+
if (!this._headerMatchesFile) {
|
|
4760
|
+
throw new exports.QvdCorruptedError("The file is shorter than its header claims.", {
|
|
4761
|
+
file: this._path,
|
|
4762
|
+
fileSize: this._fileSize,
|
|
4763
|
+
requiredBytes: this._symbolTableOffset + symbolTableLength + totalRows * recordSize,
|
|
4764
|
+
stage: "checkRead"
|
|
4765
|
+
});
|
|
4766
|
+
}
|
|
4767
|
+
const tableLength = this._symbolTableLength();
|
|
4768
|
+
for (const field of this._allFields) {
|
|
4769
|
+
validateFieldMetadata(field, tableLength, this._path);
|
|
4770
|
+
validateFieldBitMetadata(field, recordSize, this._path);
|
|
4771
|
+
}
|
|
4772
|
+
validateSymbolAreas(this._allFields, this._path);
|
|
4773
|
+
const resolved = resolveWindow(window, totalRows);
|
|
4774
|
+
const windowRows = resolved.limit;
|
|
4775
|
+
const liveRows = chunkSize === null ? null : { rows: chunkSize * 2, perChunk: 2 };
|
|
4776
|
+
const analysisAhead = this._analysisWouldRun(window, resolved, totalRows, symbolTableLength);
|
|
4777
|
+
const measured = getMemoryBudget();
|
|
4778
|
+
const ask = /* @__PURE__ */ __name((fields, rows) => {
|
|
4779
|
+
const bytes = symbolBytesOf(fields, symbolTableLength);
|
|
4780
|
+
return checkMemory({
|
|
4781
|
+
measured,
|
|
4782
|
+
symbolTableSize: bytes,
|
|
4783
|
+
maxRows: rows,
|
|
4784
|
+
totalRows,
|
|
4785
|
+
safetyFactor: this._memorySafetyFactor,
|
|
4786
|
+
columnCount: fields.length,
|
|
4787
|
+
materialisesRows: this._materialisesRows,
|
|
4788
|
+
live: liveRows,
|
|
4789
|
+
bytesHeld: this._bytesHeld(bytes, rows, recordSize, liveRows, analysisAhead),
|
|
4790
|
+
// What it reads from the file, which is not what it holds: the symbol areas, and every record
|
|
4791
|
+
// the window covers, read a slice at a time and not kept - twice over where the symbol-usage
|
|
4792
|
+
// pass will run, since it reads them before the decode reads them again.
|
|
4793
|
+
readBytes: bytes + readPasses(analysisAhead) * windowRows * recordSize
|
|
4794
|
+
});
|
|
4795
|
+
}, "ask");
|
|
4796
|
+
const answer = ask(selected, windowRows);
|
|
4797
|
+
if (!answer.fits && selected.length > 1) {
|
|
4798
|
+
const bySize = [...selected].sort(
|
|
4799
|
+
(a, b) => headerInteger(a["Length"]) - headerInteger(b["Length"])
|
|
4800
|
+
);
|
|
4801
|
+
for (let take = selected.length - 1; take >= 1; take -= 1) {
|
|
4802
|
+
const fewer = bySize.slice(0, take);
|
|
4803
|
+
if (ask(fewer, windowRows).fits) {
|
|
4804
|
+
answer.suggestions.push({
|
|
4805
|
+
option: "fields",
|
|
4806
|
+
value: fewer.map((field) => field["FieldName"])
|
|
4807
|
+
});
|
|
4808
|
+
break;
|
|
4809
|
+
}
|
|
4810
|
+
}
|
|
4811
|
+
}
|
|
4812
|
+
return answer;
|
|
4813
|
+
});
|
|
4814
|
+
}
|
|
4555
4815
|
/**
|
|
4556
4816
|
* Loads the QVD file into memory and parses it.
|
|
4557
4817
|
*
|
|
@@ -4641,13 +4901,7 @@ var init_QvdFileReader = __esm({
|
|
|
4641
4901
|
* @return {AsyncGenerator<QvdDataFrame>} The chunks, in order.
|
|
4642
4902
|
*/
|
|
4643
4903
|
async *iterateRows(window, chunkSize) {
|
|
4644
|
-
|
|
4645
|
-
throw new exports.QvdValidationError("chunkSize must be a positive integer", {
|
|
4646
|
-
provided: chunkSize,
|
|
4647
|
-
type: typeof chunkSize,
|
|
4648
|
-
file: this._path
|
|
4649
|
-
});
|
|
4650
|
-
}
|
|
4904
|
+
requireChunkSize(chunkSize, this._path);
|
|
4651
4905
|
const liveRows = { rows: chunkSize * 2, perChunk: 2 };
|
|
4652
4906
|
const rows = normaliseWindow(window, this._path);
|
|
4653
4907
|
this._startRead();
|
|
@@ -5562,6 +5816,68 @@ var init_QvdDataFrame = __esm({
|
|
|
5562
5816
|
const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
|
|
5563
5817
|
return await new QvdFileReader2(path5, metadataOptionsFrom(options)).loadMetadata();
|
|
5564
5818
|
}
|
|
5819
|
+
/**
|
|
5820
|
+
* Answers what a read would cost, and whether it fits, without doing it.
|
|
5821
|
+
*
|
|
5822
|
+
* Takes the options `fromQvd()` takes, plus `as` and `chunkSize` to say which read is being asked
|
|
5823
|
+
* about. Reads the header and the file's size and nothing else, at a cost that does not grow with
|
|
5824
|
+
* the file.
|
|
5825
|
+
*
|
|
5826
|
+
* The answer comes from the same function a read consults before it allocates anything, from the
|
|
5827
|
+
* same numbers, so **a read this approves is not refused later for memory** - and every suggestion
|
|
5828
|
+
* it carries has been read back through the check, so following one gives a read that fits.
|
|
5829
|
+
*
|
|
5830
|
+
* It answers about resources, so it answers only for a header it can trust. A header whose numbers
|
|
5831
|
+
* are not usable, or that claims more than the file holds, is refused as a `QvdCorruptedError` rather
|
|
5832
|
+
* than answered: sizing a read from numbers the file contradicts produced a memory verdict about a
|
|
5833
|
+
* file whose real problem was structural, and it was wrong in both directions - approving a read the
|
|
5834
|
+
* library then refused, and refusing another with advice that was refused too.
|
|
5835
|
+
*
|
|
5836
|
+
* That covers everything a reader can tell from the header: a field area past the end of the symbol
|
|
5837
|
+
* table, two fields claiming one area, a `Bias` that is neither 0 nor -2, a `BitWidth` past 31. Damage
|
|
5838
|
+
* that is not in the header - a value or an index the file has spoiled - is still found only by
|
|
5839
|
+
* reading, and still refused as a `QvdCorruptedError` after this has said the read fits.
|
|
5840
|
+
*
|
|
5841
|
+
* ```js
|
|
5842
|
+
* const answer = await QvdDataFrame.checkRead('huge.qvd', {as: 'columns', fields: ['Amount']});
|
|
5843
|
+
*
|
|
5844
|
+
* if (!answer.fits) {
|
|
5845
|
+
* console.log(answer.reason); // 'memory'
|
|
5846
|
+
* console.log(answer.suggestions); // [{option: 'limit', value: 1250000}, ...]
|
|
5847
|
+
* }
|
|
5848
|
+
* ```
|
|
5849
|
+
*
|
|
5850
|
+
* @param {string} path The QVD file.
|
|
5851
|
+
* @param {object} [options] What `fromQvd()` takes, plus the two below.
|
|
5852
|
+
* @param {'rows'|'columns'} [options.as='rows'] Which read is being asked about: `rows` builds row
|
|
5853
|
+
* arrays and `columns` does not, which is most of what a read costs.
|
|
5854
|
+
* @param {number|null} [options.chunkSize=null] The chunk an `iterate()` would use, which holds two
|
|
5855
|
+
* chunks of rows rather than the whole window.
|
|
5856
|
+
* @return {Promise<any>} The answer: `fits`, `reason` when it does not, `estimate`, `budget`,
|
|
5857
|
+
* `exact` and `suggestions`.
|
|
5858
|
+
* @throws {QvdValidationError} If an option's value is not valid, with `context.reason` of `option`.
|
|
5859
|
+
* @throws {QvdCorruptedError} If the header cannot be read, its numbers are not usable, or it claims
|
|
5860
|
+
* more than the file holds. The read refuses such a file too, though it may name the fault
|
|
5861
|
+
* differently - it gets there by planning the index table, where this gets there from the size.
|
|
5862
|
+
*/
|
|
5863
|
+
static async checkRead(path5, options = {}) {
|
|
5864
|
+
const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
|
|
5865
|
+
const { as = "rows", chunkSize = null } = options;
|
|
5866
|
+
if (as !== "rows" && as !== "columns") {
|
|
5867
|
+
throw new exports.QvdValidationError("as must be 'rows' or 'columns'", {
|
|
5868
|
+
provided: as,
|
|
5869
|
+
reason: "option",
|
|
5870
|
+
option: "as",
|
|
5871
|
+
value: as,
|
|
5872
|
+
file: path5
|
|
5873
|
+
});
|
|
5874
|
+
}
|
|
5875
|
+
if (chunkSize !== null) {
|
|
5876
|
+
requireChunkSize(chunkSize, path5);
|
|
5877
|
+
}
|
|
5878
|
+
const reader = new QvdFileReader2(path5, { ...readerOptionsFrom(options), materialisesRows: as === "rows" });
|
|
5879
|
+
return await reader.checkRead(windowFrom(options), { chunkSize });
|
|
5880
|
+
}
|
|
5565
5881
|
/**
|
|
5566
5882
|
* Constructs a data frame from a dictionary.
|
|
5567
5883
|
*
|