qvdjs 2.0.6 → 2.2.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 +13 -3
- package/dist/index.cjs +1028 -133
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1029 -134
- 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"]);
|
|
@@ -2146,13 +2185,13 @@ function estimateRowMemory(rows, columnCount) {
|
|
|
2146
2185
|
}
|
|
2147
2186
|
return BASE_BYTES + rows * (ROW_BASE_BYTES + PER_CELL_BYTES * columnCount);
|
|
2148
2187
|
}
|
|
2149
|
-
function estimateMemoryUsage(symbolTableSize, maxRows, totalRows, columnCount = 0, materialisesRows = true, rowsLive = null) {
|
|
2188
|
+
function estimateMemoryUsage(symbolTableSize, maxRows, totalRows, columnCount = 0, materialisesRows = true, rowsLive = null, wholeSymbols = false) {
|
|
2150
2189
|
const FULL_PARSE_OVERHEAD = 6;
|
|
2151
2190
|
const MINIMAL_OVERHEAD = 0.01;
|
|
2152
2191
|
const rowsToLoad = maxRows === null || maxRows >= totalRows ? totalRows : maxRows;
|
|
2153
2192
|
const liveRows = rowsLive === null ? rowsToLoad : Math.min(rowsLive, rowsToLoad);
|
|
2154
2193
|
const rowMemory = materialisesRows ? estimateRowMemory(liveRows, columnCount) : BASE_BYTES;
|
|
2155
|
-
if (maxRows === null || maxRows >= totalRows) {
|
|
2194
|
+
if (maxRows === null || maxRows >= totalRows || wholeSymbols) {
|
|
2156
2195
|
return symbolTableSize * FULL_PARSE_OVERHEAD + rowMemory;
|
|
2157
2196
|
}
|
|
2158
2197
|
const rowPercentage = maxRows / totalRows;
|
|
@@ -2161,8 +2200,8 @@ function estimateMemoryUsage(symbolTableSize, maxRows, totalRows, columnCount =
|
|
|
2161
2200
|
const skippedSymbolsMemory = symbolTableSize * (1 - symbolPercentage) * MINIMAL_OVERHEAD;
|
|
2162
2201
|
return keptSymbolsMemory + skippedSymbolsMemory + rowMemory;
|
|
2163
2202
|
}
|
|
2164
|
-
function recommendedRowsFor(budget, symbolTableSize, totalRows, columnCount, materialisesRows = true, includeExternal = false) {
|
|
2165
|
-
const costOf = /* @__PURE__ */ __name((rows) => estimateMemoryUsage(symbolTableSize, rows, totalRows, columnCount, materialisesRows) + (includeExternal ? estimateExternalMemory(Math.min(rows, totalRows), columnCount) : 0), "costOf");
|
|
2203
|
+
function recommendedRowsFor(budget, symbolTableSize, totalRows, columnCount, materialisesRows = true, includeExternal = false, wholeSymbols = false) {
|
|
2204
|
+
const costOf = /* @__PURE__ */ __name((rows) => estimateMemoryUsage(symbolTableSize, rows, totalRows, columnCount, materialisesRows, null, wholeSymbols) + (includeExternal ? estimateExternalMemory(Math.min(rows, totalRows), columnCount) : 0), "costOf");
|
|
2166
2205
|
if (costOf(totalRows) <= budget) {
|
|
2167
2206
|
return totalRows;
|
|
2168
2207
|
}
|
|
@@ -2181,11 +2220,19 @@ function recommendedRowsFor(budget, symbolTableSize, totalRows, columnCount, mat
|
|
|
2181
2220
|
}
|
|
2182
2221
|
return low;
|
|
2183
2222
|
}
|
|
2184
|
-
function recommendedChunkFor(budget, symbolTableSize, windowRows, totalRows, columnCount, liveRowsPerChunk = 1, includeExternal = false) {
|
|
2223
|
+
function recommendedChunkFor(budget, symbolTableSize, windowRows, totalRows, columnCount, liveRowsPerChunk = 1, includeExternal = false, wholeSymbols = false) {
|
|
2185
2224
|
const covered = windowRows === null || windowRows >= totalRows ? totalRows : windowRows;
|
|
2186
2225
|
const fits = /* @__PURE__ */ __name((chunk) => {
|
|
2187
2226
|
const live = Math.min(chunk * liveRowsPerChunk, covered);
|
|
2188
|
-
const cost = estimateMemoryUsage(
|
|
2227
|
+
const cost = estimateMemoryUsage(
|
|
2228
|
+
symbolTableSize,
|
|
2229
|
+
windowRows,
|
|
2230
|
+
totalRows,
|
|
2231
|
+
columnCount,
|
|
2232
|
+
true,
|
|
2233
|
+
chunk * liveRowsPerChunk,
|
|
2234
|
+
wholeSymbols
|
|
2235
|
+
) + (includeExternal ? estimateExternalMemory(live, columnCount) : 0);
|
|
2189
2236
|
return cost <= budget;
|
|
2190
2237
|
}, "fits");
|
|
2191
2238
|
if (fits(covered)) {
|
|
@@ -2206,22 +2253,66 @@ function recommendedChunkFor(budget, symbolTableSize, windowRows, totalRows, col
|
|
|
2206
2253
|
}
|
|
2207
2254
|
return low;
|
|
2208
2255
|
}
|
|
2209
|
-
function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.8, columnCount = 0, materialisesRows = true, live = null, bytesHeld =
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2256
|
+
function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.8, columnCount = 0, materialisesRows = true, live = null, bytesHeld = null, readBytes = null, wholeSymbols = false) {
|
|
2257
|
+
const answer = checkMemory({
|
|
2258
|
+
wholeSymbols,
|
|
2259
|
+
symbolTableSize,
|
|
2260
|
+
maxRows,
|
|
2261
|
+
totalRows,
|
|
2262
|
+
safetyFactor,
|
|
2263
|
+
columnCount,
|
|
2264
|
+
materialisesRows,
|
|
2265
|
+
live,
|
|
2266
|
+
bytesHeld,
|
|
2267
|
+
readBytes
|
|
2268
|
+
});
|
|
2269
|
+
if (answer.fits) {
|
|
2214
2270
|
return;
|
|
2215
2271
|
}
|
|
2216
|
-
const
|
|
2272
|
+
const { message, context } = answer.refusal;
|
|
2273
|
+
throw new QvdValidationError(message, {
|
|
2274
|
+
file: filePath,
|
|
2275
|
+
...context,
|
|
2276
|
+
reason: "memory",
|
|
2277
|
+
check: answerOf(answer)
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
function checkMemory({
|
|
2281
|
+
symbolTableSize,
|
|
2282
|
+
maxRows,
|
|
2283
|
+
totalRows,
|
|
2284
|
+
safetyFactor = 0.8,
|
|
2285
|
+
columnCount = 0,
|
|
2286
|
+
materialisesRows = true,
|
|
2287
|
+
live = null,
|
|
2288
|
+
bytesHeld = null,
|
|
2289
|
+
readBytes = null,
|
|
2290
|
+
measured = null,
|
|
2291
|
+
wholeSymbols = false
|
|
2292
|
+
}) {
|
|
2293
|
+
if (typeof safetyFactor !== "number" || safetyFactor < 0 || safetyFactor > 1) {
|
|
2294
|
+
throw new QvdValidationError("safetyFactor must be a number between 0.0 and 1.0", {
|
|
2295
|
+
safetyFactor,
|
|
2296
|
+
reason: "option",
|
|
2297
|
+
option: "memorySafetyFactor",
|
|
2298
|
+
value: safetyFactor
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
const budget = measured ?? getMemoryBudget();
|
|
2217
2302
|
const rowsToLoad = maxRows === null || maxRows >= totalRows ? totalRows : maxRows;
|
|
2218
2303
|
const rowsLive = live === null ? null : live.rows;
|
|
2219
2304
|
const liveRowsPerChunk = live === null ? 1 : live.perChunk;
|
|
2220
2305
|
const liveRows = rowsLive === null ? rowsToLoad : Math.min(rowsLive, rowsToLoad);
|
|
2221
|
-
const heapMemory = estimateMemoryUsage(
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2306
|
+
const heapMemory = estimateMemoryUsage(
|
|
2307
|
+
symbolTableSize,
|
|
2308
|
+
maxRows,
|
|
2309
|
+
totalRows,
|
|
2310
|
+
columnCount,
|
|
2311
|
+
materialisesRows,
|
|
2312
|
+
rowsLive,
|
|
2313
|
+
wholeSymbols
|
|
2314
|
+
);
|
|
2315
|
+
const { held, forRows: heldForRows, forChunk: heldForChunk } = bytesHeld ?? noBytesHeld;
|
|
2225
2316
|
const externalMemory = estimateExternalMemory(liveRows, columnCount) + held;
|
|
2226
2317
|
const bounded = budget.candidates.map((candidate) => {
|
|
2227
2318
|
const heapOnly = candidate.source === "V8 heap limit";
|
|
@@ -2233,11 +2324,26 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
2233
2324
|
bounds: heapOnly ? "the V8 heap" : "the whole process"
|
|
2234
2325
|
};
|
|
2235
2326
|
});
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2327
|
+
if (safetyFactor === 0) {
|
|
2328
|
+
const lowest = budget.candidates.reduce((least, candidate) => candidate.bytes < least.bytes ? candidate : least);
|
|
2329
|
+
return {
|
|
2330
|
+
fits: true,
|
|
2331
|
+
estimate: { heapBytes: heapMemory, externalBytes: externalMemory, readBytes },
|
|
2332
|
+
// The ceilings are still there and still named - what is missing is any measurement against them.
|
|
2333
|
+
// An earlier version reported `bound: 'none'` here, a third value in a two-value vocabulary that a
|
|
2334
|
+
// caller switching on the documented two would fall straight through.
|
|
2335
|
+
budget: {
|
|
2336
|
+
...budgetOf(budget, { ...lowest, heapOnly: lowest.source === "V8 heap limit" }, 0),
|
|
2337
|
+
allowedBytes: Infinity
|
|
2338
|
+
},
|
|
2339
|
+
exact: symbolTableSize === 0,
|
|
2340
|
+
suggestions: []
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
const tightest = bounded.reduce(
|
|
2344
|
+
(worst, candidate) => candidate.needs / candidate.allowed > worst.needs / worst.allowed ? candidate : worst
|
|
2240
2345
|
);
|
|
2346
|
+
const binding = tightest.needs > tightest.allowed ? tightest : null;
|
|
2241
2347
|
const heapLimit = getHeapLimit();
|
|
2242
2348
|
const availableMemory = binding ? binding.bytes : budget.bytes;
|
|
2243
2349
|
const estimatedMemory = binding ? binding.needs : heapMemory;
|
|
@@ -2251,7 +2357,8 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
2251
2357
|
totalRows,
|
|
2252
2358
|
columnCount,
|
|
2253
2359
|
materialisesRows,
|
|
2254
|
-
includeExternal
|
|
2360
|
+
includeExternal,
|
|
2361
|
+
wholeSymbols
|
|
2255
2362
|
), "fitting");
|
|
2256
2363
|
const firstGuess = fitting(liveRows);
|
|
2257
2364
|
const over = fitting(firstGuess);
|
|
@@ -2277,7 +2384,8 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
2277
2384
|
totalRows,
|
|
2278
2385
|
columnCount,
|
|
2279
2386
|
liveRowsPerChunk,
|
|
2280
|
-
includeExternal
|
|
2387
|
+
includeExternal,
|
|
2388
|
+
wholeSymbols
|
|
2281
2389
|
), "chunkFitting");
|
|
2282
2390
|
const callersChunk = chunked ? Math.max(1, Math.floor(rowsLive / Math.max(1, liveRowsPerChunk))) : 0;
|
|
2283
2391
|
const firstChunk = chunked ? chunkFitting(callersChunk) : 0;
|
|
@@ -2294,42 +2402,94 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
2294
2402
|
} else {
|
|
2295
2403
|
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
2404
|
}
|
|
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 } : {}
|
|
2405
|
+
const suggestions = [];
|
|
2406
|
+
if (!nothingFits) {
|
|
2407
|
+
suggestions.push({ option: knob, value: recommendedValue });
|
|
2408
|
+
}
|
|
2409
|
+
if (!containerBound) {
|
|
2410
|
+
let needed = Math.ceil(estimatedMemory / safetyFactor / (1024 * 1024));
|
|
2411
|
+
while (Math.max(needed * 1024 * 1024, MINIMUM_BUDGET_BYTES) * safetyFactor < estimatedMemory) {
|
|
2412
|
+
needed += 1;
|
|
2319
2413
|
}
|
|
2320
|
-
|
|
2414
|
+
suggestions.push({ nodeOption: "--max-old-space-size", value: needed });
|
|
2415
|
+
}
|
|
2416
|
+
return {
|
|
2417
|
+
fits: false,
|
|
2418
|
+
reason: "memory",
|
|
2419
|
+
estimate: { heapBytes: heapMemory, externalBytes: externalMemory, readBytes },
|
|
2420
|
+
budget: budgetOf(budget, tightest, safetyFactor),
|
|
2421
|
+
// The symbol term is six times the bytes on disk, an overhead measured across files rather than
|
|
2422
|
+
// derived, so any read with symbols in it is an estimate and says so. Only a read that decodes
|
|
2423
|
+
// nothing can be exact.
|
|
2424
|
+
exact: symbolTableSize === 0,
|
|
2425
|
+
suggestions,
|
|
2426
|
+
refusal: {
|
|
2427
|
+
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,
|
|
2428
|
+
context: {
|
|
2429
|
+
symbolTableSize,
|
|
2430
|
+
symbolTableSizeMB: sizeMB,
|
|
2431
|
+
estimatedMemoryMB: estimatedMB,
|
|
2432
|
+
availableMemoryMB: availableMB,
|
|
2433
|
+
heapLimitMB,
|
|
2434
|
+
reportedHeapLimitMB,
|
|
2435
|
+
availableRamMB,
|
|
2436
|
+
limitingFactor,
|
|
2437
|
+
limitingScope,
|
|
2438
|
+
memoryBudget: budget.candidates,
|
|
2439
|
+
memoryObserved: budget.observed,
|
|
2440
|
+
columnCount,
|
|
2441
|
+
totalRows,
|
|
2442
|
+
maxRows,
|
|
2443
|
+
recommendedMaxRows,
|
|
2444
|
+
// Only present when a chunk size is what overflowed, so a caller cannot mistake one
|
|
2445
|
+
// recommendation for the other.
|
|
2446
|
+
...chunked ? { rowsLive, recommendedChunkSize: recommendedChunk } : {}
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
};
|
|
2321
2450
|
}
|
|
2451
|
+
return {
|
|
2452
|
+
fits: true,
|
|
2453
|
+
estimate: { heapBytes: heapMemory, externalBytes: externalMemory, readBytes },
|
|
2454
|
+
budget: budgetOf(budget, tightest, safetyFactor),
|
|
2455
|
+
exact: symbolTableSize === 0,
|
|
2456
|
+
suggestions: []
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
function answerOf(answer) {
|
|
2460
|
+
const { refusal, ...rest } = answer;
|
|
2461
|
+
return rest;
|
|
2462
|
+
}
|
|
2463
|
+
function budgetOf(budget, tightest, safetyFactor) {
|
|
2464
|
+
const processLimit = budget.candidates.find((candidate) => candidate.source !== "V8 heap limit");
|
|
2465
|
+
return {
|
|
2466
|
+
heapBytes: usableOldSpaceLimit(),
|
|
2467
|
+
processBytes: processLimit ? processLimit.bytes : null,
|
|
2468
|
+
bound: tightest.heapOnly ? "heap" : "process",
|
|
2469
|
+
safetyFactor,
|
|
2470
|
+
allowedBytes: tightest.allowed ?? tightest.bytes * safetyFactor,
|
|
2471
|
+
candidates: budget.candidates,
|
|
2472
|
+
observed: budget.observed
|
|
2473
|
+
};
|
|
2322
2474
|
}
|
|
2323
2475
|
function formatCount(value) {
|
|
2324
2476
|
return value.toLocaleString("en-US");
|
|
2325
2477
|
}
|
|
2326
|
-
function warnLargeSymbolTable(symbolTableSize, maxRows, totalRows, columnCount = 0, materialisesRows = true) {
|
|
2478
|
+
function warnLargeSymbolTable(symbolTableSize, maxRows, totalRows, columnCount = 0, materialisesRows = true, wholeSymbols = false) {
|
|
2327
2479
|
const LARGE_SYMBOL_TABLE_WARNING = usableOldSpaceLimit() * 0.125;
|
|
2328
2480
|
if (symbolTableSize <= LARGE_SYMBOL_TABLE_WARNING) {
|
|
2329
2481
|
return;
|
|
2330
2482
|
}
|
|
2331
2483
|
const rowsToLoad = maxRows === null || maxRows >= totalRows ? totalRows : maxRows;
|
|
2332
|
-
const estimatedMemory = estimateMemoryUsage(
|
|
2484
|
+
const estimatedMemory = estimateMemoryUsage(
|
|
2485
|
+
symbolTableSize,
|
|
2486
|
+
maxRows,
|
|
2487
|
+
totalRows,
|
|
2488
|
+
columnCount,
|
|
2489
|
+
materialisesRows,
|
|
2490
|
+
null,
|
|
2491
|
+
wholeSymbols
|
|
2492
|
+
);
|
|
2333
2493
|
if (estimatedMemory <= LARGE_SYMBOL_TABLE_WARNING) {
|
|
2334
2494
|
return;
|
|
2335
2495
|
}
|
|
@@ -2340,7 +2500,7 @@ function warnLargeSymbolTable(symbolTableSize, maxRows, totalRows, columnCount =
|
|
|
2340
2500
|
`\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
2501
|
);
|
|
2342
2502
|
}
|
|
2343
|
-
var HEAP_LIMIT_OVERSTATEMENT_BYTES, MINIMUM_BUDGET_BYTES, BASE_BYTES, ROW_BASE_BYTES, PER_CELL_BYTES;
|
|
2503
|
+
var HEAP_LIMIT_OVERSTATEMENT_BYTES, MINIMUM_BUDGET_BYTES, BASE_BYTES, ROW_BASE_BYTES, PER_CELL_BYTES, noBytesHeld;
|
|
2344
2504
|
var init_memoryUtils = __esm({
|
|
2345
2505
|
"src/util/memoryUtils.js"() {
|
|
2346
2506
|
init_QvdErrors();
|
|
@@ -2358,7 +2518,11 @@ var init_memoryUtils = __esm({
|
|
|
2358
2518
|
__name(estimateMemoryUsage, "estimateMemoryUsage");
|
|
2359
2519
|
__name(recommendedRowsFor, "recommendedRowsFor");
|
|
2360
2520
|
__name(recommendedChunkFor, "recommendedChunkFor");
|
|
2521
|
+
noBytesHeld = Object.freeze({ held: 0, forRows: /* @__PURE__ */ __name(() => 0, "forRows"), forChunk: /* @__PURE__ */ __name(() => 0, "forChunk") });
|
|
2361
2522
|
__name(validateMemoryAvailability, "validateMemoryAvailability");
|
|
2523
|
+
__name(checkMemory, "checkMemory");
|
|
2524
|
+
__name(answerOf, "answerOf");
|
|
2525
|
+
__name(budgetOf, "budgetOf");
|
|
2362
2526
|
__name(formatCount, "formatCount");
|
|
2363
2527
|
__name(warnLargeSymbolTable, "warnLargeSymbolTable");
|
|
2364
2528
|
}
|
|
@@ -3443,6 +3607,36 @@ async function parseHeaderXml(text, file, stage) {
|
|
|
3443
3607
|
}
|
|
3444
3608
|
return parsed;
|
|
3445
3609
|
}
|
|
3610
|
+
function symbolBytesOf(selected, symbolTableLength) {
|
|
3611
|
+
const areaBytes = selected.map((field) => headerInteger(field["Length"]));
|
|
3612
|
+
return areaBytes.every((bytes) => Number.isSafeInteger(bytes) && bytes >= 0) ? Math.min(
|
|
3613
|
+
symbolTableLength,
|
|
3614
|
+
areaBytes.reduce((sum, bytes) => sum + bytes, 0)
|
|
3615
|
+
) : symbolTableLength;
|
|
3616
|
+
}
|
|
3617
|
+
function readPasses(analysisAhead) {
|
|
3618
|
+
return analysisAhead ? 2 : 1;
|
|
3619
|
+
}
|
|
3620
|
+
function validateWatchers(onProgress, signal, path5) {
|
|
3621
|
+
if (onProgress !== void 0 && typeof onProgress !== "function") {
|
|
3622
|
+
throw new QvdValidationError("onProgress must be a function", {
|
|
3623
|
+
provided: onProgress,
|
|
3624
|
+
type: typeof onProgress,
|
|
3625
|
+
reason: "option",
|
|
3626
|
+
option: "onProgress",
|
|
3627
|
+
file: path5
|
|
3628
|
+
});
|
|
3629
|
+
}
|
|
3630
|
+
if (signal !== void 0 && (typeof signal !== "object" || signal === null || typeof signal.aborted !== "boolean")) {
|
|
3631
|
+
throw new QvdValidationError("signal must be an AbortSignal", {
|
|
3632
|
+
provided: signal,
|
|
3633
|
+
type: typeof signal,
|
|
3634
|
+
reason: "option",
|
|
3635
|
+
option: "signal",
|
|
3636
|
+
file: path5
|
|
3637
|
+
});
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3446
3640
|
var MAX_HEADER_SIZE, READ_CHUNK_SIZE, ANALYSIS_SLICE_ROWS, SLICE_BYTES, COUNT_SYMBOLS_PAST, QvdFileReader;
|
|
3447
3641
|
var init_QvdFileReader = __esm({
|
|
3448
3642
|
"src/QvdFileReader.js"() {
|
|
@@ -3465,6 +3659,9 @@ var init_QvdFileReader = __esm({
|
|
|
3465
3659
|
COUNT_SYMBOLS_PAST = 65536;
|
|
3466
3660
|
__name(chunksFrom, "chunksFrom");
|
|
3467
3661
|
__name(parseHeaderXml, "parseHeaderXml");
|
|
3662
|
+
__name(symbolBytesOf, "symbolBytesOf");
|
|
3663
|
+
__name(readPasses, "readPasses");
|
|
3664
|
+
__name(validateWatchers, "validateWatchers");
|
|
3468
3665
|
QvdFileReader = class {
|
|
3469
3666
|
static {
|
|
3470
3667
|
__name(this, "QvdFileReader");
|
|
@@ -3545,20 +3742,7 @@ var init_QvdFileReader = __esm({
|
|
|
3545
3742
|
});
|
|
3546
3743
|
}
|
|
3547
3744
|
this._sliceBytes = sliceBytes;
|
|
3548
|
-
|
|
3549
|
-
throw new QvdValidationError("onProgress must be a function", {
|
|
3550
|
-
provided: onProgress,
|
|
3551
|
-
type: typeof onProgress,
|
|
3552
|
-
file: this._path
|
|
3553
|
-
});
|
|
3554
|
-
}
|
|
3555
|
-
if (signal !== void 0 && (typeof signal !== "object" || signal === null || typeof signal.aborted !== "boolean")) {
|
|
3556
|
-
throw new QvdValidationError("signal must be an AbortSignal", {
|
|
3557
|
-
provided: signal,
|
|
3558
|
-
type: typeof signal,
|
|
3559
|
-
file: this._path
|
|
3560
|
-
});
|
|
3561
|
-
}
|
|
3745
|
+
validateWatchers(onProgress, signal, this._path);
|
|
3562
3746
|
this._requestedFields = fields === void 0 ? null : fields;
|
|
3563
3747
|
this._onProgress = onProgress;
|
|
3564
3748
|
this._signal = signal;
|
|
@@ -3567,6 +3751,8 @@ var init_QvdFileReader = __esm({
|
|
|
3567
3751
|
this._failed = null;
|
|
3568
3752
|
this._reading = false;
|
|
3569
3753
|
this._symbolAreas = null;
|
|
3754
|
+
this._symbolCache = null;
|
|
3755
|
+
this._cachedFor = null;
|
|
3570
3756
|
this._headerOffset = null;
|
|
3571
3757
|
this._symbolTableOffset = null;
|
|
3572
3758
|
this._indexTableOffset = null;
|
|
@@ -3578,6 +3764,7 @@ var init_QvdFileReader = __esm({
|
|
|
3578
3764
|
this._indexColumns = null;
|
|
3579
3765
|
this._rowsDecoded = 0;
|
|
3580
3766
|
this._fileSize = null;
|
|
3767
|
+
this._fileIdentity = null;
|
|
3581
3768
|
this._headerMatchesFile = false;
|
|
3582
3769
|
}
|
|
3583
3770
|
/**
|
|
@@ -3806,6 +3993,16 @@ var init_QvdFileReader = __esm({
|
|
|
3806
3993
|
const indexTableOffset = symbolTableOffset + symbolTableLength;
|
|
3807
3994
|
const recordSize = headerInteger(headerObj["QvdTableHeader"]["RecordByteSize"]);
|
|
3808
3995
|
const totalRows = headerInteger(headerObj["QvdTableHeader"]["NoOfRecords"]);
|
|
3996
|
+
const { size: fileSize, ino, dev, mtimeMs } = await handle.stat().catch(failed);
|
|
3997
|
+
this._fileSize = fileSize;
|
|
3998
|
+
this._fileIdentity = `${dev}:${ino}:${mtimeMs}:${fileSize}`;
|
|
3999
|
+
this._headerMatchesFile = false;
|
|
4000
|
+
const headerNumbersUsable = [symbolTableLength, recordSize, totalRows].every(
|
|
4001
|
+
(value) => Number.isSafeInteger(value) && value >= 0
|
|
4002
|
+
);
|
|
4003
|
+
if (headerNumbersUsable) {
|
|
4004
|
+
this._headerMatchesFile = headerEndIndex + symbolTableLength + totalRows * recordSize <= fileSize;
|
|
4005
|
+
}
|
|
3809
4006
|
if (headerOnly) {
|
|
3810
4007
|
this._headerBuffer = headerBuffer.subarray(0, headerEndIndex);
|
|
3811
4008
|
this._emitProgress("read", 1, 1);
|
|
@@ -3814,20 +4011,8 @@ var init_QvdFileReader = __esm({
|
|
|
3814
4011
|
this._headerBuffer = headerBuffer.subarray(0, headerEndIndex);
|
|
3815
4012
|
const selected = selectFields(headerFields, this._requestedFields, this._path);
|
|
3816
4013
|
const columnCount = selected.length;
|
|
3817
|
-
const
|
|
3818
|
-
const
|
|
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
|
-
}
|
|
4014
|
+
const symbolBytes = symbolBytesOf(this._fieldsHeldAfter(selected, headerFields), symbolTableLength);
|
|
4015
|
+
const readSymbolBytes = symbolBytesOf(this._fieldsReadBy(selected), symbolTableLength);
|
|
3831
4016
|
const resolved = headerNumbersUsable ? resolveWindow(window, totalRows) : { offset: 0, limit: 0 };
|
|
3832
4017
|
const windowRows = resolved.limit;
|
|
3833
4018
|
if (headerNumbersUsable && this._headerMatchesFile) {
|
|
@@ -3841,12 +4026,23 @@ var init_QvdFileReader = __esm({
|
|
|
3841
4026
|
this._materialisesRows,
|
|
3842
4027
|
liveRows,
|
|
3843
4028
|
this._bytesHeld(
|
|
3844
|
-
|
|
4029
|
+
readSymbolBytes,
|
|
3845
4030
|
windowRows,
|
|
3846
4031
|
recordSize,
|
|
3847
4032
|
liveRows,
|
|
3848
|
-
this.
|
|
3849
|
-
)
|
|
4033
|
+
this._analysisAhead(window, resolved, totalRows, symbolTableLength)
|
|
4034
|
+
),
|
|
4035
|
+
// What it reads, which is not what it holds - the records go through one buffer and are not
|
|
4036
|
+
// kept. Carried so that a refusal's `check` says everything the pre-flight would have said.
|
|
4037
|
+
//
|
|
4038
|
+
// Twice over where the symbol-usage pass is still ahead of it: that pass reads the window's
|
|
4039
|
+
// records to find which symbols the rows use, and the decode then reads them again. Counted
|
|
4040
|
+
// once, the figure understated the I/O of exactly the reads that do the most of it.
|
|
4041
|
+
readSymbolBytes + readPasses(this._analysisAhead(window, resolved, totalRows, symbolTableLength)) * windowRows * recordSize,
|
|
4042
|
+
// A paging read keeps whole columns, so the estimate must not discount its symbols as a window's
|
|
4043
|
+
// sample of them - see `estimateMemoryUsage`. Under-charging is the direction that ends in a
|
|
4044
|
+
// heap-limit abort rather than an error.
|
|
4045
|
+
this._symbolCache !== null
|
|
3850
4046
|
);
|
|
3851
4047
|
}
|
|
3852
4048
|
if (window.offset === 0 && window.limit === null) {
|
|
@@ -3915,8 +4111,9 @@ var init_QvdFileReader = __esm({
|
|
|
3915
4111
|
*
|
|
3916
4112
|
* The areas are counted whole, although the read lets each range go once its fields are parsed, because
|
|
3917
4113
|
* 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
|
|
4114
|
+
* caller asked for the fields decides it, so the sum is what holds in every order. The slice is the
|
|
4115
|
+
* buffer `_forEachSlice` will allocate, sized by `_sliceRowsFor` so that the charge and the allocation
|
|
4116
|
+
* are one expression rather than two that agree today.
|
|
3920
4117
|
*
|
|
3921
4118
|
* @param {number} symbolBytes Bytes of symbols the read will read.
|
|
3922
4119
|
* @param {number} rows Records it will read.
|
|
@@ -3925,8 +4122,24 @@ var init_QvdFileReader = __esm({
|
|
|
3925
4122
|
* @private
|
|
3926
4123
|
*/
|
|
3927
4124
|
_bytesHeldBy(symbolBytes, rows, recordSize) {
|
|
3928
|
-
const
|
|
3929
|
-
return symbolBytes +
|
|
4125
|
+
const usable = Number.isSafeInteger(rows) && Number.isSafeInteger(recordSize) && rows > 0 && recordSize > 0;
|
|
4126
|
+
return symbolBytes + (usable ? this._sliceRowsFor(rows, recordSize) * recordSize : 0);
|
|
4127
|
+
}
|
|
4128
|
+
/**
|
|
4129
|
+
* Records the one buffer holds while a read of `rowCount` records goes through it.
|
|
4130
|
+
*
|
|
4131
|
+
* A slice is `sliceBytes` of records, rounded down to a whole record, or every record the read has left
|
|
4132
|
+
* when that is fewer - and at least one, since a read of a record wider than `sliceBytes` still has to
|
|
4133
|
+
* hold that record. The single definition: `_forEachSlice` allocates from it and the memory guard is
|
|
4134
|
+
* charged from it, so a change to how a read slices cannot leave the guard pricing the old rule.
|
|
4135
|
+
*
|
|
4136
|
+
* @param {number} rowCount Records the read will read.
|
|
4137
|
+
* @param {number} recordSize Bytes per record.
|
|
4138
|
+
* @return {number} Records in one slice.
|
|
4139
|
+
* @private
|
|
4140
|
+
*/
|
|
4141
|
+
_sliceRowsFor(rowCount, recordSize) {
|
|
4142
|
+
return Math.max(1, Math.min(rowCount, Math.floor(this._sliceBytes / Math.max(1, recordSize))));
|
|
3930
4143
|
}
|
|
3931
4144
|
/**
|
|
3932
4145
|
* What a read holds in bytes of the file, for the memory guard: what it holds now, and what a read
|
|
@@ -4014,6 +4227,191 @@ var init_QvdFileReader = __esm({
|
|
|
4014
4227
|
const chunkRows = liveRows === null ? windowRows : Math.max(1, Math.floor(liveRows.rows / Math.max(1, liveRows.perChunk)));
|
|
4015
4228
|
return analysisAhead ? Math.max(windowRows, chunkRows) : chunkRows;
|
|
4016
4229
|
}
|
|
4230
|
+
/**
|
|
4231
|
+
* The fields this reader will be holding the symbols of once this read has finished.
|
|
4232
|
+
*
|
|
4233
|
+
* The ones it selects, and - while paging - the ones it decoded for an earlier page and kept. That
|
|
4234
|
+
* union is what the memory checks have to be sized by, because it is what is live: a reader four
|
|
4235
|
+
* pages into a wide file holds four columns' values whether or not this page asks about them, and a
|
|
4236
|
+
* check sized by this page alone would approve a fifth column that does not fit beside them.
|
|
4237
|
+
*
|
|
4238
|
+
* The same set for every check, so the ceilings, the guard and the pre-flight cannot disagree about
|
|
4239
|
+
* what a paging read costs. Without a cache it is just the selection, which is what every one-shot
|
|
4240
|
+
* read has always been sized by.
|
|
4241
|
+
*
|
|
4242
|
+
* @param {Array<any>} selected The fields this read selects.
|
|
4243
|
+
* @param {Array<any>} all Every field in the header, to find a cached one by name.
|
|
4244
|
+
* @return {Array<any>} The fields whose symbols will be live.
|
|
4245
|
+
* @private
|
|
4246
|
+
*/
|
|
4247
|
+
_fieldsHeldAfter(selected, all) {
|
|
4248
|
+
if (this._symbolCache === null || this._symbolCache.size === 0) {
|
|
4249
|
+
return selected;
|
|
4250
|
+
}
|
|
4251
|
+
const names = new Set(selected.map((field) => field["FieldName"]));
|
|
4252
|
+
const cached = all.filter(
|
|
4253
|
+
(field) => !names.has(field["FieldName"]) && this._symbolCache !== null && this._symbolCache.has(field["FieldName"])
|
|
4254
|
+
);
|
|
4255
|
+
return [...selected, ...cached];
|
|
4256
|
+
}
|
|
4257
|
+
/**
|
|
4258
|
+
* The fields whose symbol areas this read will actually read.
|
|
4259
|
+
*
|
|
4260
|
+
* The selection, less anything already decoded and kept. `_fieldsHeldAfter` answers what the read will
|
|
4261
|
+
* be *holding*, which is the right figure for the heap; this is the right one for the bytes it buffers
|
|
4262
|
+
* while parsing and for the I/O it reports, because a cached column's area is left out of the plan
|
|
4263
|
+
* entirely and never read.
|
|
4264
|
+
*
|
|
4265
|
+
* Sized by the wrong one of the two, a warm page was charged external bytes for buffers it never
|
|
4266
|
+
* allocates - and external bytes bind against a container limit, so a page that fits could be refused -
|
|
4267
|
+
* and `estimate.readBytes` claimed I/O it does not perform: on four columns with three cached it
|
|
4268
|
+
* reported 3,155,600 bytes for a read of 788,930.
|
|
4269
|
+
*
|
|
4270
|
+
* @param {Array<any>} selected The fields this read selects.
|
|
4271
|
+
* @return {Array<any>} The fields whose areas will be read.
|
|
4272
|
+
* @private
|
|
4273
|
+
*/
|
|
4274
|
+
_fieldsReadBy(selected) {
|
|
4275
|
+
if (this._symbolCache === null || this._symbolCache.size === 0) {
|
|
4276
|
+
return selected;
|
|
4277
|
+
}
|
|
4278
|
+
return selected.filter(
|
|
4279
|
+
(field) => this._symbolCache !== null && !this._symbolCache.has(field["FieldName"])
|
|
4280
|
+
);
|
|
4281
|
+
}
|
|
4282
|
+
/**
|
|
4283
|
+
* Keeps what this reader decodes, so that a later read of the same file does not decode it again.
|
|
4284
|
+
*
|
|
4285
|
+
* For a caller reading one file many times over - a `QvdFile` and its pages - and off by default,
|
|
4286
|
+
* because every other entry point is one read and would only be holding values nobody will ask for
|
|
4287
|
+
* again. Decoding the symbols is 72% of a page of a hundred rows from a 300,000-row file; the rest is
|
|
4288
|
+
* the open, the header, the records and the rows.
|
|
4289
|
+
*
|
|
4290
|
+
* It turns the two-pass symbol path off with it. That path decodes only the symbols a window's rows
|
|
4291
|
+
* use, which is right for one read and wrong for a cache: a later page asking for a row that uses a
|
|
4292
|
+
* skipped symbol would read `undefined` where the value is. So a cached field is always a whole
|
|
4293
|
+
* field, walked and checked against its `NoOfSymbols` like any other.
|
|
4294
|
+
*
|
|
4295
|
+
* @return {void}
|
|
4296
|
+
*/
|
|
4297
|
+
beginPaging() {
|
|
4298
|
+
this._symbolCache = /* @__PURE__ */ new Map();
|
|
4299
|
+
}
|
|
4300
|
+
/**
|
|
4301
|
+
* Reads with the fields the caller names for this read alone, rather than the reader's own.
|
|
4302
|
+
*
|
|
4303
|
+
* A `QvdFile` is opened once and its pages may each name a projection, so the selection cannot be
|
|
4304
|
+
* fixed at construction as it is for every other entry point.
|
|
4305
|
+
*
|
|
4306
|
+
* It holds until the next call replaces it rather than being cleared by the read, so **every caller
|
|
4307
|
+
* sets it before every read**, passing the file's own fields where the page named none. A caller that
|
|
4308
|
+
* relied on it being empty would instead get the projection of whatever ran last: that is what made a
|
|
4309
|
+
* `check()` naming no fields answer for the previous page's columns.
|
|
4310
|
+
*
|
|
4311
|
+
* @param {Array<string>|null|undefined} fields The fields, or undefined to use the reader's own.
|
|
4312
|
+
* @return {void}
|
|
4313
|
+
*/
|
|
4314
|
+
selectForNextRead(fields) {
|
|
4315
|
+
if (fields !== void 0) {
|
|
4316
|
+
this._requestedFields = fields;
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
/**
|
|
4320
|
+
* Watches the next read with the caller's `onProgress` and `signal`, rather than the reader's own.
|
|
4321
|
+
*
|
|
4322
|
+
* Both belong to one call, and a reader is told them when it is built - so a `QvdFile` page that named
|
|
4323
|
+
* either used to get a reader of its own. That made passing a progress callback change what the read
|
|
4324
|
+
* did rather than only observing it: a fresh reader is not paging, so it took the two-pass symbol
|
|
4325
|
+
* path, reported a different `loadStats.symbolFiltering`, and cached nothing. An observer must not
|
|
4326
|
+
* change what it observes, and a caller must not have to choose between cancelling a page and paging
|
|
4327
|
+
* cheaply.
|
|
4328
|
+
*
|
|
4329
|
+
* Like `selectForNextRead`, it holds until the next call replaces it rather than being cleared by the
|
|
4330
|
+
* read, so a caller that sets it for one page and not the next is still watched on the next - pass the
|
|
4331
|
+
* file's own watchers explicitly, as `QvdFile._page` does, rather than leaving them out.
|
|
4332
|
+
*
|
|
4333
|
+
* @param {{onProgress?: Function, signal?: AbortSignal}} [watchers] What this read is watched with.
|
|
4334
|
+
* @return {void}
|
|
4335
|
+
*/
|
|
4336
|
+
observeNextRead({ onProgress, signal } = {}) {
|
|
4337
|
+
validateWatchers(onProgress, signal, this._path);
|
|
4338
|
+
this._onProgress = onProgress;
|
|
4339
|
+
this._signal = signal;
|
|
4340
|
+
}
|
|
4341
|
+
/**
|
|
4342
|
+
* Whether the symbol-usage pass runs for this read, cache and all.
|
|
4343
|
+
*
|
|
4344
|
+
* `_analysisWouldRun` answers whether the window wants the pass; a paging read never takes it, because
|
|
4345
|
+
* a column decoded in part cannot be kept. Asked in one place because it was asked in two and they
|
|
4346
|
+
* disagreed: the pass was gated on the cache while the memory charge and `estimate.readBytes` were
|
|
4347
|
+
* not, so every page of a file above the threshold was charged a slice of records it never buffered
|
|
4348
|
+
* and reported twice the bytes it read.
|
|
4349
|
+
*
|
|
4350
|
+
* @param {QvdRowWindow} window The window as the caller spelled it.
|
|
4351
|
+
* @param {{offset: number, limit: number}} resolved Where it lands in this file.
|
|
4352
|
+
* @param {number} totalRows Rows the file declares.
|
|
4353
|
+
* @param {number} symbolTableLength The symbol table's declared length.
|
|
4354
|
+
* @return {boolean} Whether the pass will run.
|
|
4355
|
+
* @private
|
|
4356
|
+
*/
|
|
4357
|
+
_analysisAhead(window, resolved, totalRows, symbolTableLength) {
|
|
4358
|
+
return this._symbolCache === null && this._analysisWouldRun(window, resolved, totalRows, symbolTableLength);
|
|
4359
|
+
}
|
|
4360
|
+
/**
|
|
4361
|
+
* Empties the cache when the header in front of us is not the one it was decoded from.
|
|
4362
|
+
*
|
|
4363
|
+
* The fingerprint is what a rewrite moves: the file's identity on disk - device, inode, modification
|
|
4364
|
+
* time and size - and then the header numbers, down to each cached field's own offset, length and
|
|
4365
|
+
* symbol count.
|
|
4366
|
+
*
|
|
4367
|
+
* The header numbers alone were not enough, and the gap is not exotic. `QvdFileWriter` carries
|
|
4368
|
+
* `CreateUtcTime` over from the metadata it is handed, so reading a QVD, changing one text to another
|
|
4369
|
+
* of the same byte length and writing it back leaves `CreateUtcTime`, `NoOfRecords`, `Offset` and
|
|
4370
|
+
* every field's `Offset`, `Length` and `NoOfSymbols` exactly as they were - a different file the
|
|
4371
|
+
* fingerprint could not tell from the first. The filesystem sees it either way: an atomic write
|
|
4372
|
+
* renames a new file into place, which changes the inode, and an in-place one moves `mtimeMs`.
|
|
4373
|
+
*
|
|
4374
|
+
* @return {void}
|
|
4375
|
+
* @private
|
|
4376
|
+
*/
|
|
4377
|
+
_forgetCacheIfFileChanged() {
|
|
4378
|
+
if (this._symbolCache === null || this._symbolCache.size === 0) {
|
|
4379
|
+
return;
|
|
4380
|
+
}
|
|
4381
|
+
if (this._cachedFor !== this._fileFingerprint()) {
|
|
4382
|
+
this._symbolCache = /* @__PURE__ */ new Map();
|
|
4383
|
+
this._cachedFor = null;
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
/**
|
|
4387
|
+
* What identifies the file this reader's cache was decoded from.
|
|
4388
|
+
*
|
|
4389
|
+
* @return {string} The fingerprint.
|
|
4390
|
+
* @private
|
|
4391
|
+
*/
|
|
4392
|
+
_fileFingerprint() {
|
|
4393
|
+
assert4(this._header && this._allFields, "The QVD file header has not been parsed.");
|
|
4394
|
+
const header = this._header["QvdTableHeader"];
|
|
4395
|
+
return [
|
|
4396
|
+
// First, because it is the only part that moves when a rewrite preserves the header's numbers.
|
|
4397
|
+
this._fileIdentity,
|
|
4398
|
+
header["CreateUtcTime"],
|
|
4399
|
+
header["NoOfRecords"],
|
|
4400
|
+
header["Offset"],
|
|
4401
|
+
...this._allFields.map(
|
|
4402
|
+
(field) => `${field["FieldName"]}:${field["Offset"]}:${field["Length"]}:${field["NoOfSymbols"]}`
|
|
4403
|
+
)
|
|
4404
|
+
].join("|");
|
|
4405
|
+
}
|
|
4406
|
+
/**
|
|
4407
|
+
* Drops everything this reader has decoded, so that nothing outlives the caller that wanted it.
|
|
4408
|
+
*
|
|
4409
|
+
* @return {void}
|
|
4410
|
+
*/
|
|
4411
|
+
endPaging() {
|
|
4412
|
+
this._symbolCache = null;
|
|
4413
|
+
this._cachedFor = null;
|
|
4414
|
+
}
|
|
4017
4415
|
/**
|
|
4018
4416
|
* The symbol table's length, as much of it as the file holds: what the header declares, cut short where
|
|
4019
4417
|
* the file ends. Known before a byte of the table is read, so everything that can refuse the table is
|
|
@@ -4042,7 +4440,13 @@ var init_QvdFileReader = __esm({
|
|
|
4042
4440
|
* order, so ranges that touch are merged: a read of every field is one range, and so is a read of fields
|
|
4043
4441
|
* that happen to be neighbours. A read of one field of twenty reads that field's area alone.
|
|
4044
4442
|
*
|
|
4045
|
-
*
|
|
4443
|
+
* A field whose symbols this reader already holds is left out, because its bytes are not wanted: the
|
|
4444
|
+
* ranges are what gets read, and including a cached field's span had a page read every byte of every
|
|
4445
|
+
* column it named, cached or not. Measured on four columns of 20,000 distinct texts, a page naming all
|
|
4446
|
+
* four with three of them cached read all four columns' bytes - 1,155,600 of them, where 288,930 were
|
|
4447
|
+
* needed. The decode was saved and the I/O was not, which on the files #122 is about is the whole cost.
|
|
4448
|
+
*
|
|
4449
|
+
* Built once per read, from the fields the read must read, and each field's metadata is checked as it is
|
|
4046
4450
|
* added - a range is arithmetic on `Offset` and `Length`, and those have to be inside the table first.
|
|
4047
4451
|
* `_parseSymbolTable` checks every field of the file, selected or not, before it parses any.
|
|
4048
4452
|
*
|
|
@@ -4057,7 +4461,10 @@ var init_QvdFileReader = __esm({
|
|
|
4057
4461
|
}
|
|
4058
4462
|
assert4(this._selectedFields, "The QVD file fields have not been resolved before their symbols were read.");
|
|
4059
4463
|
const tableLength = this._symbolTableLength();
|
|
4060
|
-
const
|
|
4464
|
+
const toRead = this._selectedFields.filter(
|
|
4465
|
+
(field) => this._symbolCache === null || !this._symbolCache.has(field["FieldName"])
|
|
4466
|
+
);
|
|
4467
|
+
const areas = toRead.map((field) => {
|
|
4061
4468
|
validateFieldMetadata(field, tableLength, this._path);
|
|
4062
4469
|
const start = headerInteger(field["Offset"]);
|
|
4063
4470
|
return { field, start, end: start + headerInteger(field["Length"]) };
|
|
@@ -4142,7 +4549,7 @@ var init_QvdFileReader = __esm({
|
|
|
4142
4549
|
return;
|
|
4143
4550
|
}
|
|
4144
4551
|
assert4(this._indexTableOffset !== null, "The QVD file header has not been parsed before its records were read.");
|
|
4145
|
-
const sliceRows =
|
|
4552
|
+
const sliceRows = this._sliceRowsFor(rowCount, recordSize);
|
|
4146
4553
|
const slice = Buffer.alloc(sliceRows * recordSize);
|
|
4147
4554
|
const requiredBytes = this._indexTableOffset + (firstRow + rowCount) * recordSize;
|
|
4148
4555
|
for (let done = 0; done < rowCount; done += sliceRows) {
|
|
@@ -4379,9 +4786,11 @@ var init_QvdFileReader = __esm({
|
|
|
4379
4786
|
}
|
|
4380
4787
|
const allFields = this._allFields;
|
|
4381
4788
|
const fields = this._selectedFields;
|
|
4789
|
+
this._forgetCacheIfFileChanged();
|
|
4382
4790
|
const symbolTableSize = this._symbolTableLength();
|
|
4383
4791
|
const plan = this._symbolAreaPlan();
|
|
4384
|
-
const
|
|
4792
|
+
const readSymbolBytes = plan.ranges.reduce((sum, range) => sum + (range.end - range.start), 0);
|
|
4793
|
+
const symbolBytes = readSymbolBytes + symbolBytesOf(this._fieldsHeldAfter(fields, allFields).slice(fields.length), symbolTableSize);
|
|
4385
4794
|
const totalRows = headerInteger(this._header["QvdTableHeader"]["NoOfRecords"]);
|
|
4386
4795
|
const recordSize = headerInteger(this._header["QvdTableHeader"]["RecordByteSize"]);
|
|
4387
4796
|
validateSymbolTableSize(symbolBytes, this._path, totalRows);
|
|
@@ -4395,10 +4804,21 @@ var init_QvdFileReader = __esm({
|
|
|
4395
4804
|
fields.length,
|
|
4396
4805
|
this._materialisesRows,
|
|
4397
4806
|
liveRows,
|
|
4398
|
-
this._bytesHeld(
|
|
4807
|
+
this._bytesHeld(readSymbolBytes, rowsToLoad, recordSize, liveRows, false),
|
|
4808
|
+
// `symbolsToKeep` is non-null exactly when the symbol-usage pass has run, and a pass that has
|
|
4809
|
+
// run has read the window's records once already - so the read's total is two passes over them.
|
|
4810
|
+
readSymbolBytes + readPasses(symbolsToKeep !== null) * rowsToLoad * recordSize,
|
|
4811
|
+
this._symbolCache !== null
|
|
4399
4812
|
);
|
|
4400
4813
|
}
|
|
4401
|
-
warnLargeSymbolTable(
|
|
4814
|
+
warnLargeSymbolTable(
|
|
4815
|
+
symbolBytes,
|
|
4816
|
+
rowsToLoad,
|
|
4817
|
+
totalRows,
|
|
4818
|
+
fields.length,
|
|
4819
|
+
this._materialisesRows,
|
|
4820
|
+
this._symbolCache !== null
|
|
4821
|
+
);
|
|
4402
4822
|
for (const field of allFields) {
|
|
4403
4823
|
validateFieldMetadata(field, symbolTableSize, this._path);
|
|
4404
4824
|
}
|
|
@@ -4406,24 +4826,36 @@ var init_QvdFileReader = __esm({
|
|
|
4406
4826
|
const symbolTable = [];
|
|
4407
4827
|
for (const [position, field] of fields.entries()) {
|
|
4408
4828
|
this._throwIfAborted();
|
|
4829
|
+
const cached = this._symbolCache?.get(field["FieldName"]);
|
|
4830
|
+
if (cached) {
|
|
4831
|
+
symbolTable.push(cached);
|
|
4832
|
+
this._emitProgress("symbol-table", position + 1, fields.length);
|
|
4833
|
+
continue;
|
|
4834
|
+
}
|
|
4409
4835
|
const area = await this._symbolAreaOf(field);
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
)
|
|
4836
|
+
const parsed = parseFieldSymbols(
|
|
4837
|
+
area.buffer,
|
|
4838
|
+
area.start,
|
|
4839
|
+
area.end,
|
|
4840
|
+
// Checked against the symbols the area holds, which is the one check that sees a terminator
|
|
4841
|
+
// damaged in the middle of it (#124). A cached field was checked when it was decoded, which is
|
|
4842
|
+
// why the cache may only hold a field a full walk produced.
|
|
4843
|
+
headerInteger(field["NoOfSymbols"]),
|
|
4844
|
+
// By position, matching how `_analyzeIndexTableSymbolUsage` built it. Both walk
|
|
4845
|
+
// `this._selectedFields`, so position is the one key that cannot collide.
|
|
4846
|
+
symbolsToKeep ? symbolsToKeep[position] : null,
|
|
4847
|
+
field["FieldName"],
|
|
4848
|
+
this._path,
|
|
4849
|
+
void 0,
|
|
4850
|
+
area.base
|
|
4426
4851
|
);
|
|
4852
|
+
symbolTable.push(parsed);
|
|
4853
|
+
if (this._symbolCache && symbolsToKeep === null) {
|
|
4854
|
+
if (this._symbolCache.size === 0) {
|
|
4855
|
+
this._cachedFor = this._fileFingerprint();
|
|
4856
|
+
}
|
|
4857
|
+
this._symbolCache.set(field["FieldName"], parsed);
|
|
4858
|
+
}
|
|
4427
4859
|
this._releaseSymbolArea(field);
|
|
4428
4860
|
this._emitProgress("symbol-table", position + 1, fields.length);
|
|
4429
4861
|
}
|
|
@@ -4518,28 +4950,173 @@ var init_QvdFileReader = __esm({
|
|
|
4518
4950
|
await this._parseHeader();
|
|
4519
4951
|
this._emitProgress("header", 1, 1);
|
|
4520
4952
|
this._throwIfAborted();
|
|
4521
|
-
|
|
4522
|
-
const header = this._header["QvdTableHeader"];
|
|
4523
|
-
const columns = this._allFields.map((field) => field["FieldName"]);
|
|
4524
|
-
const rowCount = headerInteger(header["NoOfRecords"]);
|
|
4525
|
-
validateRecordCount(rowCount, this._path, "readMetadata");
|
|
4526
|
-
const shape = new QvdDataFrame([], columns, header, {
|
|
4527
|
-
symbolTableBytes: headerInteger(header["Offset"]),
|
|
4528
|
-
totalRows: rowCount,
|
|
4529
|
-
rowsLoaded: 0,
|
|
4530
|
-
symbolFiltering: false,
|
|
4531
|
-
symbolsKept: null
|
|
4532
|
-
});
|
|
4533
|
-
return {
|
|
4534
|
-
columns,
|
|
4535
|
-
rowCount,
|
|
4536
|
-
columnCount: columns.length,
|
|
4537
|
-
fields: columns.map((name) => shape.getFieldMetadata(name)),
|
|
4538
|
-
fileMetadata: shape.fileMetadata,
|
|
4539
|
-
metadata: header
|
|
4540
|
-
};
|
|
4953
|
+
return this.describeParsed();
|
|
4541
4954
|
});
|
|
4542
4955
|
}
|
|
4956
|
+
/**
|
|
4957
|
+
* The schema and header of the file this reader has parsed, as `readMetadata()` reports them.
|
|
4958
|
+
*
|
|
4959
|
+
* Built from the parsed header and nothing else, so a caller holding a header - a `QvdFile` - can have
|
|
4960
|
+
* it without reading the file a second time.
|
|
4961
|
+
*
|
|
4962
|
+
* @return {any} The metadata.
|
|
4963
|
+
*/
|
|
4964
|
+
describeParsed() {
|
|
4965
|
+
assert4(this._header && this._allFields, "The QVD file header has not been parsed.");
|
|
4966
|
+
const header = this._header["QvdTableHeader"];
|
|
4967
|
+
const columns = this._allFields.map((field) => field["FieldName"]);
|
|
4968
|
+
const rowCount = headerInteger(header["NoOfRecords"]);
|
|
4969
|
+
validateRecordCount(rowCount, this._path, "readMetadata");
|
|
4970
|
+
const shape = new QvdDataFrame([], columns, header, {
|
|
4971
|
+
symbolTableBytes: headerInteger(header["Offset"]),
|
|
4972
|
+
totalRows: rowCount,
|
|
4973
|
+
rowsLoaded: 0,
|
|
4974
|
+
symbolFiltering: false,
|
|
4975
|
+
symbolsKept: null
|
|
4976
|
+
});
|
|
4977
|
+
return {
|
|
4978
|
+
columns,
|
|
4979
|
+
rowCount,
|
|
4980
|
+
columnCount: columns.length,
|
|
4981
|
+
fields: columns.map((name) => shape.getFieldMetadata(name)),
|
|
4982
|
+
fileMetadata: shape.fileMetadata,
|
|
4983
|
+
metadata: header
|
|
4984
|
+
};
|
|
4985
|
+
}
|
|
4986
|
+
/**
|
|
4987
|
+
* What a read of this file would cost, and whether it fits, without reading it.
|
|
4988
|
+
*
|
|
4989
|
+
* Reads the header and the file's size and nothing else, at the constant cost of `loadMetadata()`,
|
|
4990
|
+
* then asks the same question a read asks before it allocates anything - through the same function,
|
|
4991
|
+
* from the same numbers. That is the whole point: an answer computed a second way would be a second
|
|
4992
|
+
* opinion, and a read this approves would still be refused.
|
|
4993
|
+
*
|
|
4994
|
+
* @param {number|null|{offset?: number, limit?: number|null, maxRows?: number|null}} [rawWindow]
|
|
4995
|
+
* The rows the read would cover, spelled any of the ways a read accepts.
|
|
4996
|
+
* @param {{chunkSize?: number|null}} [options] `chunkSize` when the read would be an `iterate()`,
|
|
4997
|
+
* which holds two chunks of rows rather than the window.
|
|
4998
|
+
* @return {Promise<any>} The answer - see `checkMemory`.
|
|
4999
|
+
*/
|
|
5000
|
+
async checkRead(rawWindow, { chunkSize = null } = {}) {
|
|
5001
|
+
const window = normaliseWindow(rawWindow, this._path);
|
|
5002
|
+
return await this._closingAfter(async () => {
|
|
5003
|
+
await this._parseHeaderChecked();
|
|
5004
|
+
return this.checkParsed(window, { chunkSize });
|
|
5005
|
+
});
|
|
5006
|
+
}
|
|
5007
|
+
/**
|
|
5008
|
+
* Reads this file's header, and nothing else, leaving it parsed on the reader.
|
|
5009
|
+
*
|
|
5010
|
+
* What `checkRead` and `QvdFile` both start with: the second asks many questions of one header, so the
|
|
5011
|
+
* read that produces it is separate from the questions. Every check a read makes before it trusts the
|
|
5012
|
+
* header's numbers is made here, so that nothing downstream has to wonder whether they hold.
|
|
5013
|
+
*
|
|
5014
|
+
* @return {Promise<void>} When the header is parsed and checked.
|
|
5015
|
+
*/
|
|
5016
|
+
async parseHeaderOnly() {
|
|
5017
|
+
return await this._closingAfter(async () => await this._parseHeaderChecked());
|
|
5018
|
+
}
|
|
5019
|
+
/**
|
|
5020
|
+
* `parseHeaderOnly`'s body, for a caller already inside a read session - `checkRead` is one.
|
|
5021
|
+
*
|
|
5022
|
+
* @return {Promise<void>} When the header is parsed and checked.
|
|
5023
|
+
* @private
|
|
5024
|
+
*/
|
|
5025
|
+
async _parseHeaderChecked() {
|
|
5026
|
+
await this._readData({ offset: 0, limit: null }, true);
|
|
5027
|
+
this._emitProgress("header", 0, 1);
|
|
5028
|
+
await this._parseHeader();
|
|
5029
|
+
this._emitProgress("header", 1, 1);
|
|
5030
|
+
this._throwIfAborted();
|
|
5031
|
+
assert4(
|
|
5032
|
+
this._header && this._selectedFields && this._allFields && this._symbolTableOffset !== null,
|
|
5033
|
+
"The QVD file header has not been parsed."
|
|
5034
|
+
);
|
|
5035
|
+
const header = this._header["QvdTableHeader"];
|
|
5036
|
+
const totalRows = headerInteger(header["NoOfRecords"]);
|
|
5037
|
+
const recordSize = headerInteger(header["RecordByteSize"]);
|
|
5038
|
+
const symbolTableLength = headerInteger(header["Offset"]);
|
|
5039
|
+
validateRecordSize(recordSize, this._path, "checkRead");
|
|
5040
|
+
validateRecordCount(totalRows, this._path, "checkRead");
|
|
5041
|
+
if (!this._headerMatchesFile) {
|
|
5042
|
+
throw new QvdCorruptedError("The file is shorter than its header claims.", {
|
|
5043
|
+
file: this._path,
|
|
5044
|
+
fileSize: this._fileSize,
|
|
5045
|
+
requiredBytes: this._symbolTableOffset + symbolTableLength + totalRows * recordSize,
|
|
5046
|
+
stage: "checkRead"
|
|
5047
|
+
});
|
|
5048
|
+
}
|
|
5049
|
+
const tableLength = this._symbolTableLength();
|
|
5050
|
+
for (const field of this._allFields) {
|
|
5051
|
+
validateFieldMetadata(field, tableLength, this._path);
|
|
5052
|
+
validateFieldBitMetadata(field, recordSize, this._path);
|
|
5053
|
+
}
|
|
5054
|
+
validateSymbolAreas(this._allFields, this._path);
|
|
5055
|
+
}
|
|
5056
|
+
/**
|
|
5057
|
+
* What a read of the parsed header's file would cost, with no I/O at all.
|
|
5058
|
+
*
|
|
5059
|
+
* Separate from `checkRead` because a `QvdFile` asks this of one header many times - once per page a
|
|
5060
|
+
* viewer scrolls to - and the header is already in hand. `parseHeaderOnly` has to have run.
|
|
5061
|
+
*
|
|
5062
|
+
* @param {QvdRowWindow} window The rows the read would cover, normalised.
|
|
5063
|
+
* @param {{chunkSize?: number|null, fields?: Array<string>|null, materialisesRows?: boolean}} [options]
|
|
5064
|
+
* `chunkSize` for an `iterate()`; `fields` and `materialisesRows` to ask about a read other than the
|
|
5065
|
+
* one this reader was built for, which is what a `QvdFile` does per call.
|
|
5066
|
+
* @return {any} The answer - see `checkMemory`.
|
|
5067
|
+
*/
|
|
5068
|
+
checkParsed(window, { chunkSize = null, fields = void 0, materialisesRows = void 0 } = {}) {
|
|
5069
|
+
assert4(this._header && this._allFields, "The QVD file header has not been parsed.");
|
|
5070
|
+
const header = this._header["QvdTableHeader"];
|
|
5071
|
+
const totalRows = headerInteger(header["NoOfRecords"]);
|
|
5072
|
+
const recordSize = headerInteger(header["RecordByteSize"]);
|
|
5073
|
+
const symbolTableLength = headerInteger(header["Offset"]);
|
|
5074
|
+
const builds = materialisesRows === void 0 ? this._materialisesRows : materialisesRows;
|
|
5075
|
+
const selected = selectFields(this._allFields, fields === void 0 ? this._requestedFields : fields, this._path);
|
|
5076
|
+
const resolved = resolveWindow(window, totalRows);
|
|
5077
|
+
const windowRows = resolved.limit;
|
|
5078
|
+
const liveRows = chunkSize === null ? null : { rows: chunkSize * 2, perChunk: 2 };
|
|
5079
|
+
const analysisAhead = this._analysisAhead(window, resolved, totalRows, symbolTableLength);
|
|
5080
|
+
const measured = getMemoryBudget();
|
|
5081
|
+
const ask = /* @__PURE__ */ __name((asked, rows) => {
|
|
5082
|
+
const bytes = symbolBytesOf(this._fieldsHeldAfter(asked, this._allFields), symbolTableLength);
|
|
5083
|
+
const read = symbolBytesOf(this._fieldsReadBy(asked), symbolTableLength);
|
|
5084
|
+
return checkMemory({
|
|
5085
|
+
measured,
|
|
5086
|
+
symbolTableSize: bytes,
|
|
5087
|
+
maxRows: rows,
|
|
5088
|
+
totalRows,
|
|
5089
|
+
safetyFactor: this._memorySafetyFactor,
|
|
5090
|
+
columnCount: asked.length,
|
|
5091
|
+
materialisesRows: builds,
|
|
5092
|
+
live: liveRows,
|
|
5093
|
+
// A paging read keeps whole columns, so it is charged for whole columns - see `estimateMemoryUsage`.
|
|
5094
|
+
wholeSymbols: this._symbolCache !== null,
|
|
5095
|
+
bytesHeld: this._bytesHeld(read, rows, recordSize, liveRows, analysisAhead),
|
|
5096
|
+
// What it reads from the file, which is not what it holds: the symbol areas it has still to read,
|
|
5097
|
+
// and every record the window covers, read a slice at a time and not kept - twice over where the
|
|
5098
|
+
// symbol-usage pass will run, since it reads them before the decode reads them again.
|
|
5099
|
+
readBytes: read + readPasses(analysisAhead) * windowRows * recordSize
|
|
5100
|
+
});
|
|
5101
|
+
}, "ask");
|
|
5102
|
+
const answer = ask(selected, windowRows);
|
|
5103
|
+
if (!answer.fits && selected.length > 1) {
|
|
5104
|
+
const bySize = [...selected].sort(
|
|
5105
|
+
(a, b) => headerInteger(a["Length"]) - headerInteger(b["Length"])
|
|
5106
|
+
);
|
|
5107
|
+
for (let take = selected.length - 1; take >= 1; take -= 1) {
|
|
5108
|
+
const fewer = bySize.slice(0, take);
|
|
5109
|
+
if (ask(fewer, windowRows).fits) {
|
|
5110
|
+
answer.suggestions.push({
|
|
5111
|
+
option: "fields",
|
|
5112
|
+
value: fewer.map((field) => field["FieldName"])
|
|
5113
|
+
});
|
|
5114
|
+
break;
|
|
5115
|
+
}
|
|
5116
|
+
}
|
|
5117
|
+
}
|
|
5118
|
+
return answer;
|
|
5119
|
+
}
|
|
4543
5120
|
/**
|
|
4544
5121
|
* Loads the QVD file into memory and parses it.
|
|
4545
5122
|
*
|
|
@@ -4629,13 +5206,7 @@ var init_QvdFileReader = __esm({
|
|
|
4629
5206
|
* @return {AsyncGenerator<QvdDataFrame>} The chunks, in order.
|
|
4630
5207
|
*/
|
|
4631
5208
|
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
|
-
}
|
|
5209
|
+
requireChunkSize(chunkSize, this._path);
|
|
4639
5210
|
const liveRows = { rows: chunkSize * 2, perChunk: 2 };
|
|
4640
5211
|
const rows = normaliseWindow(window, this._path);
|
|
4641
5212
|
this._startRead();
|
|
@@ -4710,7 +5281,7 @@ var init_QvdFileReader = __esm({
|
|
|
4710
5281
|
const rowsAvailable = resolved.limit;
|
|
4711
5282
|
let symbolsToKeep = null;
|
|
4712
5283
|
let symbolsKept = null;
|
|
4713
|
-
if (this.
|
|
5284
|
+
if (this._analysisAhead(window, resolved, totalRows, symbolTableLength)) {
|
|
4714
5285
|
symbolsToKeep = await this._analyzeIndexTableSymbolUsage({ offset: resolved.offset, limit: rowsAvailable });
|
|
4715
5286
|
symbolsKept = symbolsToKeep.reduce((sum, set) => sum + set.size, 0);
|
|
4716
5287
|
}
|
|
@@ -4802,6 +5373,224 @@ var init_QvdFileReader = __esm({
|
|
|
4802
5373
|
}
|
|
4803
5374
|
});
|
|
4804
5375
|
|
|
5376
|
+
// src/QvdFile.js
|
|
5377
|
+
var QvdFile_exports = {};
|
|
5378
|
+
__export(QvdFile_exports, {
|
|
5379
|
+
QvdFile: () => QvdFile
|
|
5380
|
+
});
|
|
5381
|
+
var QvdFile;
|
|
5382
|
+
var init_QvdFile = __esm({
|
|
5383
|
+
"src/QvdFile.js"() {
|
|
5384
|
+
init_QvdErrors();
|
|
5385
|
+
init_readOptions();
|
|
5386
|
+
QvdFile = class {
|
|
5387
|
+
static {
|
|
5388
|
+
__name(this, "QvdFile");
|
|
5389
|
+
}
|
|
5390
|
+
/**
|
|
5391
|
+
* Not called directly - `QvdDataFrame.open()` is the way in, because a `QvdFile` is only ever a file
|
|
5392
|
+
* whose header has been read, and a constructor cannot wait for that.
|
|
5393
|
+
*
|
|
5394
|
+
* @param {any} reader The reader holding the parsed header.
|
|
5395
|
+
* @param {any} metadata What `readMetadata()` returns for this file.
|
|
5396
|
+
* @param {any} options The options the file was opened with.
|
|
5397
|
+
* @private
|
|
5398
|
+
*/
|
|
5399
|
+
constructor(reader, metadata, options) {
|
|
5400
|
+
this._reader = reader;
|
|
5401
|
+
this._metadata = metadata;
|
|
5402
|
+
this._options = options;
|
|
5403
|
+
this._closed = false;
|
|
5404
|
+
this._tail = Promise.resolve();
|
|
5405
|
+
this._readers = { rows: null, columns: null };
|
|
5406
|
+
}
|
|
5407
|
+
/**
|
|
5408
|
+
* The file's header and schema, as `QvdDataFrame.readMetadata()` returns them.
|
|
5409
|
+
*
|
|
5410
|
+
* Read when the file was opened, so this costs nothing and cannot fail.
|
|
5411
|
+
*
|
|
5412
|
+
* @return {any} The metadata.
|
|
5413
|
+
*/
|
|
5414
|
+
get metadata() {
|
|
5415
|
+
return this._metadata;
|
|
5416
|
+
}
|
|
5417
|
+
/**
|
|
5418
|
+
* Whether `close()` has been called.
|
|
5419
|
+
*
|
|
5420
|
+
* @return {boolean} True once it has.
|
|
5421
|
+
*/
|
|
5422
|
+
get closed() {
|
|
5423
|
+
return this._closed;
|
|
5424
|
+
}
|
|
5425
|
+
/**
|
|
5426
|
+
* What a read of this file would cost, and whether it fits - with no I/O at all.
|
|
5427
|
+
*
|
|
5428
|
+
* The same answer `QvdDataFrame.checkRead()` gives, from the header this file already holds, so a
|
|
5429
|
+
* viewer can size a page before asking for it without touching the disk.
|
|
5430
|
+
*
|
|
5431
|
+
* @param {{offset?: number, limit?: number|null, maxRows?: number|null, fields?: Array<string>|null,
|
|
5432
|
+
* as?: 'rows'|'columns', chunkSize?: number|null}} [options] The read being asked about - the same
|
|
5433
|
+
* bag `rows()` takes, plus `as` and `chunkSize` to say which shape of read it is.
|
|
5434
|
+
* @return {any} The answer - `fits`, `reason`, `estimate`, `budget`, `exact`, `suggestions`.
|
|
5435
|
+
* @throws {QvdValidationError} If the file is closed, or an option's value is not valid.
|
|
5436
|
+
*/
|
|
5437
|
+
check(options = {}) {
|
|
5438
|
+
this._refuseWhenClosed("check");
|
|
5439
|
+
const { as = "rows", chunkSize = null } = options;
|
|
5440
|
+
if (as !== "rows" && as !== "columns") {
|
|
5441
|
+
throw new QvdValidationError("as must be 'rows' or 'columns'", {
|
|
5442
|
+
provided: as,
|
|
5443
|
+
reason: "option",
|
|
5444
|
+
option: "as",
|
|
5445
|
+
value: as,
|
|
5446
|
+
file: this._options.path
|
|
5447
|
+
});
|
|
5448
|
+
}
|
|
5449
|
+
if (chunkSize !== null) {
|
|
5450
|
+
requireChunkSize(chunkSize, this._options.path);
|
|
5451
|
+
}
|
|
5452
|
+
const reader = this._readers[as] ?? this._reader;
|
|
5453
|
+
return reader.checkParsed(normaliseWindow(windowFrom(options), this._options.path), {
|
|
5454
|
+
chunkSize,
|
|
5455
|
+
// Resolved here rather than left to the reader, exactly as `_page` resolves it. A warm reader is
|
|
5456
|
+
// still holding the last page's selection, and `checkParsed` falls back to it - so a `check()`
|
|
5457
|
+
// naming no fields answered for whatever the previous page happened to name. On a four-column
|
|
5458
|
+
// file after a page naming one of them, it reported 27,490 bytes for a read that costs 108,160:
|
|
5459
|
+
// understating, which is the direction that approves a read the read then refuses.
|
|
5460
|
+
fields: options.fields === void 0 ? this._options.fields ?? null : options.fields,
|
|
5461
|
+
materialisesRows: as === "rows"
|
|
5462
|
+
});
|
|
5463
|
+
}
|
|
5464
|
+
/**
|
|
5465
|
+
* Reads a page of rows.
|
|
5466
|
+
*
|
|
5467
|
+
* @param {{offset?: number, limit?: number|null, maxRows?: number|null, fields?: Array<string>|null,
|
|
5468
|
+
* onProgress?: Function, signal?: AbortSignal}} [options] The page, and how to read it. One bag,
|
|
5469
|
+
* as every other entry point takes: `offset` and `limit` say which rows, `fields` names a projection
|
|
5470
|
+
* for this page alone, and anything left out falls back to what the file was opened with.
|
|
5471
|
+
* @return {Promise<any>} The page, as a `QvdDataFrame`.
|
|
5472
|
+
* @throws {QvdValidationError} If the file is closed.
|
|
5473
|
+
*/
|
|
5474
|
+
async rows(options = {}) {
|
|
5475
|
+
this._refuseWhenClosed("rows");
|
|
5476
|
+
return await this._serialised(async () => await this._page(options, true, (reader, window) => reader.load(window)));
|
|
5477
|
+
}
|
|
5478
|
+
/**
|
|
5479
|
+
* Reads a page as columns, building no rows.
|
|
5480
|
+
*
|
|
5481
|
+
* @param {{offset?: number, limit?: number|null, maxRows?: number|null, fields?: Array<string>|null,
|
|
5482
|
+
* onProgress?: Function, signal?: AbortSignal}} [options] The page, as `rows()` takes it.
|
|
5483
|
+
* @return {Promise<any>} The page, as a `QvdColumnTable`.
|
|
5484
|
+
* @throws {QvdValidationError} If the file is closed.
|
|
5485
|
+
*/
|
|
5486
|
+
async columns(options = {}) {
|
|
5487
|
+
this._refuseWhenClosed("columns");
|
|
5488
|
+
return await this._serialised(
|
|
5489
|
+
async () => await this._page(options, false, (reader, window) => reader.loadColumnar(window))
|
|
5490
|
+
);
|
|
5491
|
+
}
|
|
5492
|
+
/**
|
|
5493
|
+
* Closes the file.
|
|
5494
|
+
*
|
|
5495
|
+
* Every call after it is refused with `reason: 'closed'`. Calling it twice is not an error: a
|
|
5496
|
+
* `finally` that closes and an `await using` that closes are both right, and both may run.
|
|
5497
|
+
*
|
|
5498
|
+
* @return {Promise<void>} When the pages already in flight have finished.
|
|
5499
|
+
*/
|
|
5500
|
+
async close() {
|
|
5501
|
+
if (this._closed) {
|
|
5502
|
+
return;
|
|
5503
|
+
}
|
|
5504
|
+
this._closed = true;
|
|
5505
|
+
await this._tail;
|
|
5506
|
+
for (const reader of Object.values(this._readers)) {
|
|
5507
|
+
reader?.endPaging();
|
|
5508
|
+
}
|
|
5509
|
+
this._readers = { rows: null, columns: null };
|
|
5510
|
+
this._reader.endPaging();
|
|
5511
|
+
this._reader = null;
|
|
5512
|
+
}
|
|
5513
|
+
/**
|
|
5514
|
+
* `await using` support, where the runtime has it.
|
|
5515
|
+
*
|
|
5516
|
+
* @return {Promise<void>} When closed.
|
|
5517
|
+
*/
|
|
5518
|
+
async [Symbol.asyncDispose]() {
|
|
5519
|
+
await this.close();
|
|
5520
|
+
}
|
|
5521
|
+
/**
|
|
5522
|
+
* Refuses a call on a closed file, in the vocabulary the rest of the API uses.
|
|
5523
|
+
*
|
|
5524
|
+
* @param {string} call The method the caller reached for, for the error.
|
|
5525
|
+
* @private
|
|
5526
|
+
*/
|
|
5527
|
+
_refuseWhenClosed(call) {
|
|
5528
|
+
if (this._closed) {
|
|
5529
|
+
throw new QvdValidationError("The file is closed: open it again to read from it", {
|
|
5530
|
+
reason: "closed",
|
|
5531
|
+
call,
|
|
5532
|
+
file: this._options.path
|
|
5533
|
+
});
|
|
5534
|
+
}
|
|
5535
|
+
}
|
|
5536
|
+
/**
|
|
5537
|
+
* Runs `work` after everything asked for before it, and before everything asked for after.
|
|
5538
|
+
*
|
|
5539
|
+
* @param {() => Promise<any>} work The page to read.
|
|
5540
|
+
* @return {Promise<any>} Its result.
|
|
5541
|
+
* @private
|
|
5542
|
+
*/
|
|
5543
|
+
async _serialised(work) {
|
|
5544
|
+
const run = this._tail.then(work, work);
|
|
5545
|
+
this._tail = run.then(
|
|
5546
|
+
() => void 0,
|
|
5547
|
+
() => void 0
|
|
5548
|
+
);
|
|
5549
|
+
return await run;
|
|
5550
|
+
}
|
|
5551
|
+
/**
|
|
5552
|
+
* Reads one page, through the reader that keeps what the pages before it decoded.
|
|
5553
|
+
*
|
|
5554
|
+
* One reader for every page rather than one per page, which is what makes the symbol cache possible:
|
|
5555
|
+
* decoding the symbols is 72% of a page of a hundred rows from a 300,000-row file, and a reader built
|
|
5556
|
+
* fresh each time did all of it again. Two readers, because a columnar page builds no rows and a row
|
|
5557
|
+
* page does, and `materialisesRows` is fixed when a reader is constructed - so each shape keeps its
|
|
5558
|
+
* own, and its own cache.
|
|
5559
|
+
*
|
|
5560
|
+
* Options that belong to one call rather than to the file - the fields this page alone wants, and the
|
|
5561
|
+
* `onProgress` and `signal` watching it - are told to that shared reader for the next read and no
|
|
5562
|
+
* further. A page naming one of them used to build a reader of its own instead, which quietly turned
|
|
5563
|
+
* the cache off and the two-pass symbol path on: watching a page changed what the page did.
|
|
5564
|
+
*
|
|
5565
|
+
* @param {any} options What the call passed.
|
|
5566
|
+
* @param {boolean} builds Whether the page materialises rows.
|
|
5567
|
+
* @param {(reader: any, window: any) => Promise<any>} read The read to make.
|
|
5568
|
+
* @return {Promise<any>} The page.
|
|
5569
|
+
* @private
|
|
5570
|
+
*/
|
|
5571
|
+
async _page(options, builds, read) {
|
|
5572
|
+
const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
|
|
5573
|
+
const kept = builds ? "rows" : "columns";
|
|
5574
|
+
if (!this._readers[kept]) {
|
|
5575
|
+
const reader2 = new QvdFileReader2(this._options.path, {
|
|
5576
|
+
...readerOptionsFrom(this._options),
|
|
5577
|
+
materialisesRows: builds
|
|
5578
|
+
});
|
|
5579
|
+
reader2.beginPaging();
|
|
5580
|
+
this._readers[kept] = reader2;
|
|
5581
|
+
}
|
|
5582
|
+
const reader = this._readers[kept];
|
|
5583
|
+
reader.selectForNextRead(options.fields === void 0 ? this._options.fields ?? null : options.fields);
|
|
5584
|
+
reader.observeNextRead({
|
|
5585
|
+
onProgress: options.onProgress ?? this._options.onProgress,
|
|
5586
|
+
signal: options.signal ?? this._options.signal
|
|
5587
|
+
});
|
|
5588
|
+
return await read(reader, windowFrom(options));
|
|
5589
|
+
}
|
|
5590
|
+
};
|
|
5591
|
+
}
|
|
5592
|
+
});
|
|
5593
|
+
|
|
4805
5594
|
// src/QvdDataFrame.js
|
|
4806
5595
|
function defaultFieldHeader(fieldName) {
|
|
4807
5596
|
return {
|
|
@@ -5550,6 +6339,111 @@ var init_QvdDataFrame = __esm({
|
|
|
5550
6339
|
const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
|
|
5551
6340
|
return await new QvdFileReader2(path5, metadataOptionsFrom(options)).loadMetadata();
|
|
5552
6341
|
}
|
|
6342
|
+
/**
|
|
6343
|
+
* Answers what a read would cost, and whether it fits, without doing it.
|
|
6344
|
+
*
|
|
6345
|
+
* Takes the options `fromQvd()` takes, plus `as` and `chunkSize` to say which read is being asked
|
|
6346
|
+
* about. Reads the header and the file's size and nothing else, at a cost that does not grow with
|
|
6347
|
+
* the file.
|
|
6348
|
+
*
|
|
6349
|
+
* The answer comes from the same function a read consults before it allocates anything, from the
|
|
6350
|
+
* same numbers, so **a read this approves is not refused later for memory** - and every suggestion
|
|
6351
|
+
* it carries has been read back through the check, so following one gives a read that fits.
|
|
6352
|
+
*
|
|
6353
|
+
* It answers about resources, so it answers only for a header it can trust. A header whose numbers
|
|
6354
|
+
* are not usable, or that claims more than the file holds, is refused as a `QvdCorruptedError` rather
|
|
6355
|
+
* than answered: sizing a read from numbers the file contradicts produced a memory verdict about a
|
|
6356
|
+
* file whose real problem was structural, and it was wrong in both directions - approving a read the
|
|
6357
|
+
* library then refused, and refusing another with advice that was refused too.
|
|
6358
|
+
*
|
|
6359
|
+
* That covers everything a reader can tell from the header: a field area past the end of the symbol
|
|
6360
|
+
* table, two fields claiming one area, a `Bias` that is neither 0 nor -2, a `BitWidth` past 31. Damage
|
|
6361
|
+
* that is not in the header - a value or an index the file has spoiled - is still found only by
|
|
6362
|
+
* reading, and still refused as a `QvdCorruptedError` after this has said the read fits.
|
|
6363
|
+
*
|
|
6364
|
+
* ```js
|
|
6365
|
+
* const answer = await QvdDataFrame.checkRead('huge.qvd', {as: 'columns', fields: ['Amount']});
|
|
6366
|
+
*
|
|
6367
|
+
* if (!answer.fits) {
|
|
6368
|
+
* console.log(answer.reason); // 'memory'
|
|
6369
|
+
* console.log(answer.suggestions); // [{option: 'limit', value: 1250000}, ...]
|
|
6370
|
+
* }
|
|
6371
|
+
* ```
|
|
6372
|
+
*
|
|
6373
|
+
* @param {string} path The QVD file.
|
|
6374
|
+
* @param {object} [options] What `fromQvd()` takes, plus the two below.
|
|
6375
|
+
* @param {'rows'|'columns'} [options.as='rows'] Which read is being asked about: `rows` builds row
|
|
6376
|
+
* arrays and `columns` does not, which is most of what a read costs.
|
|
6377
|
+
* @param {number|null} [options.chunkSize=null] The chunk an `iterate()` would use, which holds two
|
|
6378
|
+
* chunks of rows rather than the whole window.
|
|
6379
|
+
* @return {Promise<any>} The answer: `fits`, `reason` when it does not, `estimate`, `budget`,
|
|
6380
|
+
* `exact` and `suggestions`.
|
|
6381
|
+
* @throws {QvdValidationError} If an option's value is not valid, with `context.reason` of `option`.
|
|
6382
|
+
* @throws {QvdCorruptedError} If the header cannot be read, its numbers are not usable, or it claims
|
|
6383
|
+
* more than the file holds. The read refuses such a file too, though it may name the fault
|
|
6384
|
+
* differently - it gets there by planning the index table, where this gets there from the size.
|
|
6385
|
+
*/
|
|
6386
|
+
static async checkRead(path5, options = {}) {
|
|
6387
|
+
const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
|
|
6388
|
+
const { as = "rows", chunkSize = null } = options;
|
|
6389
|
+
if (as !== "rows" && as !== "columns") {
|
|
6390
|
+
throw new QvdValidationError("as must be 'rows' or 'columns'", {
|
|
6391
|
+
provided: as,
|
|
6392
|
+
reason: "option",
|
|
6393
|
+
option: "as",
|
|
6394
|
+
value: as,
|
|
6395
|
+
file: path5
|
|
6396
|
+
});
|
|
6397
|
+
}
|
|
6398
|
+
if (chunkSize !== null) {
|
|
6399
|
+
requireChunkSize(chunkSize, path5);
|
|
6400
|
+
}
|
|
6401
|
+
const reader = new QvdFileReader2(path5, { ...readerOptionsFrom(options), materialisesRows: as === "rows" });
|
|
6402
|
+
return await reader.checkRead(windowFrom(options), { chunkSize });
|
|
6403
|
+
}
|
|
6404
|
+
/**
|
|
6405
|
+
* Opens a QVD file for paging, reading its header and nothing else.
|
|
6406
|
+
*
|
|
6407
|
+
* Every other entry point is one read from start to finish. A viewer showing a hundred rows at a time
|
|
6408
|
+
* pays the header again on every page, and `iterate()` goes forwards only - it cannot jump to row five
|
|
6409
|
+
* million and it cannot go back. This holds the header so that `check()` costs nothing and a page can
|
|
6410
|
+
* be asked for by position.
|
|
6411
|
+
*
|
|
6412
|
+
* ```js
|
|
6413
|
+
* const qvd = await QvdDataFrame.open('sales.qvd', {allowedDir: '/data'});
|
|
6414
|
+
*
|
|
6415
|
+
* qvd.metadata; // read once, when it opened
|
|
6416
|
+
* const answer = qvd.check({offset: 0, limit: 100}); // no I/O at all
|
|
6417
|
+
* const page = await qvd.rows({offset: 5_000_000, limit: 100});
|
|
6418
|
+
* const cols = await qvd.columns({offset: 0, limit: 100, fields: ['Amount']});
|
|
6419
|
+
*
|
|
6420
|
+
* await qvd.close();
|
|
6421
|
+
* ```
|
|
6422
|
+
*
|
|
6423
|
+
* The header is read once, and so is each column: a column decoded for one page is kept for the pages
|
|
6424
|
+
* after it, so the first page costs about what a single read costs and the ones after it are cheap.
|
|
6425
|
+
* What a file has decoded is charged to the memory check, so a page is refused rather than the process
|
|
6426
|
+
* aborting, and `close()` releases it - close a file you have finished with.
|
|
6427
|
+
*
|
|
6428
|
+
* Each page still opens the file, and a first touch still decodes a whole column rather than only as
|
|
6429
|
+
* far as the page needs.
|
|
6430
|
+
*
|
|
6431
|
+
* @param {string} path The QVD file.
|
|
6432
|
+
* @param {object} [options] What `fromQvd()` takes - `allowedDir`, `fields`, `duals`,
|
|
6433
|
+
* `coerceNumericStrings`, `memorySafetyFactor` - describing the file and how its values read. A
|
|
6434
|
+
* window means nothing here: pages carry their own.
|
|
6435
|
+
* @return {Promise<import('./QvdFile.js').QvdFile>} The open file.
|
|
6436
|
+
* @throws {QvdValidationError} If an option's value is not valid, with `context.reason` of `option`.
|
|
6437
|
+
* @throws {QvdCorruptedError} If the header cannot be read, or describes a file this is not.
|
|
6438
|
+
*/
|
|
6439
|
+
static async open(path5, options = {}) {
|
|
6440
|
+
const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
|
|
6441
|
+
const { QvdFile: QvdFile2 } = await Promise.resolve().then(() => (init_QvdFile(), QvdFile_exports));
|
|
6442
|
+
const reader = new QvdFileReader2(path5, readerOptionsFrom(options));
|
|
6443
|
+
reader.beginPaging();
|
|
6444
|
+
await reader.parseHeaderOnly();
|
|
6445
|
+
return new QvdFile2(reader, reader.describeParsed(), { ...options, path: path5 });
|
|
6446
|
+
}
|
|
5553
6447
|
/**
|
|
5554
6448
|
* Constructs a data frame from a dictionary.
|
|
5555
6449
|
*
|
|
@@ -5854,10 +6748,11 @@ __name(dateToQlikSerial, "dateToQlikSerial");
|
|
|
5854
6748
|
// src/index.js
|
|
5855
6749
|
init_QvdDataFrame();
|
|
5856
6750
|
init_QvdColumnTable();
|
|
6751
|
+
init_QvdFile();
|
|
5857
6752
|
init_QvdFileReader();
|
|
5858
6753
|
init_QvdFileWriter();
|
|
5859
6754
|
init_QvdErrors();
|
|
5860
6755
|
|
|
5861
|
-
export { QvdColumn, QvdColumnTable, QvdCorruptedError, QvdDataFrame, QvdDual, QvdError, QvdFileReader, QvdFileWriter, QvdIOError, QvdParseError, QvdSecurityError, QvdSymbol, QvdValidationError, dateToQlikSerial, qlikSerialToDate };
|
|
6756
|
+
export { QvdColumn, QvdColumnTable, QvdCorruptedError, QvdDataFrame, QvdDual, QvdError, QvdFile, QvdFileReader, QvdFileWriter, QvdIOError, QvdParseError, QvdSecurityError, QvdSymbol, QvdValidationError, dateToQlikSerial, qlikSerialToDate };
|
|
5862
6757
|
//# sourceMappingURL=index.js.map
|
|
5863
6758
|
//# sourceMappingURL=index.js.map
|