qvdjs 2.0.5 → 2.0.6

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/dist/index.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  var fs2 = require('fs');
4
4
  var path2 = require('path');
5
- var assert3 = require('assert');
5
+ var assert4 = require('assert');
6
6
  var crypto2 = require('crypto');
7
7
  var promises = require('timers/promises');
8
8
  var xml = require('xml2js');
@@ -14,7 +14,7 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
14
14
 
15
15
  var fs2__default = /*#__PURE__*/_interopDefault(fs2);
16
16
  var path2__default = /*#__PURE__*/_interopDefault(path2);
17
- var assert3__default = /*#__PURE__*/_interopDefault(assert3);
17
+ var assert4__default = /*#__PURE__*/_interopDefault(assert4);
18
18
  var crypto2__default = /*#__PURE__*/_interopDefault(crypto2);
19
19
  var xml__default = /*#__PURE__*/_interopDefault(xml);
20
20
  var os__default = /*#__PURE__*/_interopDefault(os);
@@ -971,7 +971,7 @@ function changedAfterCheck(checked, change) {
971
971
  });
972
972
  }
973
973
  async function openChecked(checked, purpose, failed, { nofollow = NOFOLLOW } = {}) {
974
- assert3__default.default(purpose === "read" || checked.stats !== null, "A rewrite in place is of a file that exists.");
974
+ assert4__default.default(purpose === "read" || checked.stats !== null, "A rewrite in place is of a file that exists.");
975
975
  const noFollow = checked.onDisk ? nofollow : 0;
976
976
  const flags = (purpose === "rewrite" ? O_WRONLY : O_RDONLY) | noFollow;
977
977
  let handle;
@@ -1547,9 +1547,9 @@ var init_QvdFileWriter = __esm({
1547
1547
  * Writes the data to the QVD file.
1548
1548
  */
1549
1549
  async _writeData() {
1550
- assert3__default.default(this._header, "The QVD file header has not been parsed.");
1551
- assert3__default.default(this._symbolBuffer, "The QVD file symbol table has not been parsed.");
1552
- assert3__default.default(this._indexBuffer, "The QVD file index table has not been parsed.");
1550
+ assert4__default.default(this._header, "The QVD file header has not been parsed.");
1551
+ assert4__default.default(this._symbolBuffer, "The QVD file symbol table has not been parsed.");
1552
+ assert4__default.default(this._indexBuffer, "The QVD file index table has not been parsed.");
1553
1553
  this._emitProgress("write", 0, 1);
1554
1554
  const headerBuffer = Buffer.concat([Buffer.from(this._header, "utf-8"), Buffer.from([0])]);
1555
1555
  const failed = rethrowAsIoError(this._path, "write");
@@ -1997,7 +1997,7 @@ var init_QvdFileWriter = __esm({
1997
1997
  const key = keys[slot];
1998
1998
  offset = typeof key === "number" ? writeSymbol(columnBuffer, offset, kinds[slot], key, texts[slot]) : writeSymbol(columnBuffer, offset, kinds[slot], null, key);
1999
1999
  }
2000
- assert3__default.default(offset === byteLength, "A column was encoded into a different number of bytes than it was sized for.");
2000
+ assert4__default.default(offset === byteLength, "A column was encoded into a different number of bytes than it was sized for.");
2001
2001
  columnBuffers.push(columnBuffer);
2002
2002
  this._symbolTableMetadata?.push([symbolsOffset, byteLength, containsNull[column]]);
2003
2003
  this._symbolCounts?.push(keys.length);
@@ -2036,9 +2036,9 @@ var init_QvdFileWriter = __esm({
2036
2036
  * @private
2037
2037
  */
2038
2038
  _buildIndexTable() {
2039
- assert3__default.default(this._symbolCounts, "The QVD file symbol table has not been built.");
2040
- assert3__default.default(this._symbolTableMetadata, "The QVD file symbol table metadata has not been built.");
2041
- assert3__default.default(this._symbolIndexByValue, "The QVD file symbol index has not been built.");
2039
+ assert4__default.default(this._symbolCounts, "The QVD file symbol table has not been built.");
2040
+ assert4__default.default(this._symbolTableMetadata, "The QVD file symbol table metadata has not been built.");
2041
+ assert4__default.default(this._symbolIndexByValue, "The QVD file symbol index has not been built.");
2042
2042
  this._indexTableMetadata = [];
2043
2043
  const columns = this._df.columns;
2044
2044
  const data = this._df.data;
@@ -2218,7 +2218,7 @@ function recommendedChunkFor(budget, symbolTableSize, windowRows, totalRows, col
2218
2218
  }
2219
2219
  return low;
2220
2220
  }
2221
- function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.8, columnCount = 0, materialisesRows = true, live = null) {
2221
+ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.8, columnCount = 0, materialisesRows = true, live = null, bytesHeld = 0) {
2222
2222
  if (typeof safetyFactor !== "number" || safetyFactor < 0 || safetyFactor > 1) {
2223
2223
  throw new exports.QvdValidationError("safetyFactor must be a number between 0.0 and 1.0", { safetyFactor });
2224
2224
  }
@@ -2231,7 +2231,10 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
2231
2231
  const liveRowsPerChunk = live === null ? 1 : live.perChunk;
2232
2232
  const liveRows = rowsLive === null ? rowsToLoad : Math.min(rowsLive, rowsToLoad);
2233
2233
  const heapMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows, columnCount, materialisesRows, rowsLive);
2234
- const externalMemory = estimateExternalMemory(liveRows, columnCount);
2234
+ const held = typeof bytesHeld === "number" ? bytesHeld : bytesHeld.held;
2235
+ const heldForRows = typeof bytesHeld === "number" ? () => bytesHeld : bytesHeld.forRows;
2236
+ const heldForChunk = typeof bytesHeld === "number" ? () => bytesHeld : bytesHeld.forChunk;
2237
+ const externalMemory = estimateExternalMemory(liveRows, columnCount) + held;
2235
2238
  const bounded = budget.candidates.map((candidate) => {
2236
2239
  const heapOnly = candidate.source === "V8 heap limit";
2237
2240
  return {
@@ -2253,14 +2256,19 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
2253
2256
  const maxAllowedMemory = binding ? binding.allowed : budget.bytes * safetyFactor;
2254
2257
  if (binding) {
2255
2258
  const includeExternal = !binding.heapOnly;
2256
- const recommendedMaxRows = recommendedRowsFor(
2257
- maxAllowedMemory,
2259
+ const rowsHeldBudget = /* @__PURE__ */ __name((rows) => maxAllowedMemory - (includeExternal ? heldForRows(rows) : 0), "rowsHeldBudget");
2260
+ const fitting = /* @__PURE__ */ __name((rows) => recommendedRowsFor(
2261
+ rowsHeldBudget(rows),
2258
2262
  symbolTableSize,
2259
2263
  totalRows,
2260
2264
  columnCount,
2261
2265
  materialisesRows,
2262
2266
  includeExternal
2263
- );
2267
+ ), "fitting");
2268
+ const firstGuess = fitting(liveRows);
2269
+ const over = fitting(firstGuess);
2270
+ const under = fitting(over);
2271
+ const recommendedMaxRows = Math.max(firstGuess, under);
2264
2272
  const sizeMB = Math.round(symbolTableSize / 1024 / 1024);
2265
2273
  const estimatedMB = Math.round(estimatedMemory / 1024 / 1024);
2266
2274
  const availableMB = Math.round(maxAllowedMemory / 1024 / 1024);
@@ -2273,15 +2281,20 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
2273
2281
  const observedBreakdown = budget.observed.map((entry) => `${entry.source} ${Math.round(entry.bytes / 1024 / 1024)}MB`).join(", ");
2274
2282
  const containerBound = binding.source === "container memory limit";
2275
2283
  const chunked = rowsLive !== null;
2276
- const recommendedChunk = chunked ? recommendedChunkFor(
2277
- maxAllowedMemory,
2284
+ const chunkHeldBudget = /* @__PURE__ */ __name((rows) => maxAllowedMemory - (includeExternal ? heldForChunk(rows) : 0), "chunkHeldBudget");
2285
+ const chunkFitting = /* @__PURE__ */ __name((rows) => recommendedChunkFor(
2286
+ chunkHeldBudget(rows),
2278
2287
  symbolTableSize,
2279
2288
  maxRows,
2280
2289
  totalRows,
2281
2290
  columnCount,
2282
2291
  liveRowsPerChunk,
2283
2292
  includeExternal
2284
- ) : 0;
2293
+ ), "chunkFitting");
2294
+ const callersChunk = chunked ? Math.max(1, Math.floor(rowsLive / Math.max(1, liveRowsPerChunk))) : 0;
2295
+ const firstChunk = chunked ? chunkFitting(callersChunk) : 0;
2296
+ const overChunk = chunked ? chunkFitting(firstChunk) : 0;
2297
+ const recommendedChunk = chunked ? Math.max(firstChunk, chunkFitting(overChunk)) : 0;
2285
2298
  const knob = chunked ? "chunkSize" : "limit";
2286
2299
  const recommendedValue = chunked ? recommendedChunk : recommendedMaxRows;
2287
2300
  const nothingFits = recommendedValue === 0;
@@ -2476,6 +2489,7 @@ function validateSymbolTableSize(symbolTableLength, filePath, totalRows) {
2476
2489
  function validateFieldMetadata(field, symbolBufferLength, filePath) {
2477
2490
  const symbolsOffset = headerInteger(field["Offset"]);
2478
2491
  const symbolsLength = headerInteger(field["Length"]);
2492
+ const symbolCount = headerInteger(field["NoOfSymbols"]);
2479
2493
  if (isNaN(symbolsOffset) || !Number.isSafeInteger(symbolsOffset) || symbolsOffset < 0) {
2480
2494
  throw new exports.QvdCorruptedError("Invalid symbol offset", {
2481
2495
  field: field["FieldName"],
@@ -2492,6 +2506,14 @@ function validateFieldMetadata(field, symbolBufferLength, filePath) {
2492
2506
  stage: "parseSymbolTable"
2493
2507
  });
2494
2508
  }
2509
+ if (isNaN(symbolCount) || !Number.isSafeInteger(symbolCount) || symbolCount < 0) {
2510
+ throw new exports.QvdCorruptedError("Invalid symbol count", {
2511
+ field: field["FieldName"],
2512
+ noOfSymbols: symbolCount,
2513
+ file: filePath,
2514
+ stage: "parseSymbolTable"
2515
+ });
2516
+ }
2495
2517
  if (symbolsOffset + symbolsLength > symbolBufferLength) {
2496
2518
  throw new exports.QvdCorruptedError("Symbol data extends beyond buffer", {
2497
2519
  field: field["FieldName"],
@@ -2747,12 +2769,25 @@ var init_validationUtils = __esm({
2747
2769
  __name(validateFieldBitMetadata, "validateFieldBitMetadata");
2748
2770
  }
2749
2771
  });
2750
-
2751
- // src/util/symbolParser.js
2752
- function textEnd(symbolBuffer, from, kind, fieldName, filePath) {
2753
- const bufferLength = symbolBuffer.length;
2754
- const found = symbolBuffer.indexOf(0, from);
2755
- if ((found === -1 ? bufferLength : found) - from > MAX_TEXT_BYTES) {
2772
+ function nulFinder(area, { reach, rebaseAfter }) {
2773
+ assert4__default.default(reach - rebaseAfter > MAX_TEXT_BYTES, "A text search view must hold the longest text a symbol may have.");
2774
+ if (area.length <= reach) {
2775
+ return (from) => area.indexOf(0, from);
2776
+ }
2777
+ let base = 0;
2778
+ let view = area.subarray(0, reach);
2779
+ return (from) => {
2780
+ if (from - base > rebaseAfter) {
2781
+ base = from;
2782
+ view = area.subarray(base, Math.min(area.length, base + reach));
2783
+ }
2784
+ const found = view.indexOf(0, from - base);
2785
+ return found === -1 ? -1 : base + found;
2786
+ };
2787
+ }
2788
+ function textEnd(findNul, areaEnd, from, kind, fieldName, filePath, base) {
2789
+ const found = findNul(from);
2790
+ if ((found === -1 ? areaEnd : found) - from > MAX_TEXT_BYTES) {
2756
2791
  throw new exports.QvdCorruptedError(`${kind} exceeds maximum length`, {
2757
2792
  field: fieldName,
2758
2793
  maxLength: MAX_TEXT_BYTES,
@@ -2763,58 +2798,59 @@ function textEnd(symbolBuffer, from, kind, fieldName, filePath) {
2763
2798
  if (found === -1) {
2764
2799
  throw new exports.QvdCorruptedError(`${kind} not null-terminated`, {
2765
2800
  field: fieldName,
2766
- pointer: bufferLength,
2767
- bufferSize: bufferLength,
2801
+ pointer: base + from,
2802
+ areaEnd: base + areaEnd,
2768
2803
  file: filePath,
2769
2804
  stage: "parseSymbolTable"
2770
2805
  });
2771
2806
  }
2772
2807
  return found;
2773
2808
  }
2774
- function overflow(message, pointer, bufferLength, fieldName, filePath) {
2809
+ function overflow(message, pointer, areaEnd, fieldName, filePath, base) {
2775
2810
  throw new exports.QvdCorruptedError(message, {
2776
2811
  field: fieldName,
2777
- pointer,
2778
- bufferSize: bufferLength,
2812
+ pointer: base + pointer,
2813
+ areaEnd: base + areaEnd,
2779
2814
  file: filePath,
2780
2815
  stage: "parseSymbolTable"
2781
2816
  });
2782
2817
  }
2783
- function parseFieldSymbols(symbolBuffer, start, end, keep, fieldName, filePath) {
2784
- const bufferLength = symbolBuffer.length;
2818
+ function parseFieldSymbols(symbolBuffer, start, end, symbolCount, keep, fieldName, filePath, search = TEXT_SEARCH, base = 0) {
2819
+ const area = symbolBuffer.subarray(0, end);
2820
+ const findNul = nulFinder(area, search);
2785
2821
  const numbers = [];
2786
2822
  const texts = [];
2787
2823
  let pointer = start;
2788
2824
  while (pointer < end) {
2789
- const typeByte = symbolBuffer[pointer++];
2825
+ const typeByte = area[pointer++];
2790
2826
  const decode = keep === null || keep.has(numbers.length);
2791
2827
  let number = null;
2792
2828
  let text = null;
2793
2829
  switch (typeByte) {
2794
2830
  case 1: {
2831
+ if (pointer + 4 > end) {
2832
+ overflow("Buffer overflow reading integer symbol", pointer, end, fieldName, filePath, base);
2833
+ }
2795
2834
  if (decode) {
2796
- if (pointer + 4 > bufferLength) {
2797
- overflow("Buffer overflow reading integer symbol", pointer, bufferLength, fieldName, filePath);
2798
- }
2799
- number = symbolBuffer.readInt32LE(pointer);
2835
+ number = area.readInt32LE(pointer);
2800
2836
  }
2801
2837
  pointer += 4;
2802
2838
  break;
2803
2839
  }
2804
2840
  case 2: {
2841
+ if (pointer + 8 > end) {
2842
+ overflow("Buffer overflow reading double symbol", pointer, end, fieldName, filePath, base);
2843
+ }
2805
2844
  if (decode) {
2806
- if (pointer + 8 > bufferLength) {
2807
- overflow("Buffer overflow reading double symbol", pointer, bufferLength, fieldName, filePath);
2808
- }
2809
- number = symbolBuffer.readDoubleLE(pointer);
2845
+ number = area.readDoubleLE(pointer);
2810
2846
  }
2811
2847
  pointer += 8;
2812
2848
  break;
2813
2849
  }
2814
2850
  case 4: {
2815
- const terminator = textEnd(symbolBuffer, pointer, "String symbol", fieldName, filePath);
2851
+ const terminator = textEnd(findNul, end, pointer, "String symbol", fieldName, filePath, base);
2816
2852
  if (decode) {
2817
- text = symbolBuffer.toString("utf8", pointer, terminator);
2853
+ text = area.toString("utf8", pointer, terminator);
2818
2854
  }
2819
2855
  pointer = terminator + 1;
2820
2856
  break;
@@ -2822,14 +2858,22 @@ function parseFieldSymbols(symbolBuffer, start, end, keep, fieldName, filePath)
2822
2858
  case 5:
2823
2859
  case 6: {
2824
2860
  const numberBytes = typeByte === 5 ? 4 : 8;
2825
- if (pointer + numberBytes > bufferLength) {
2826
- const read = !decode ? "dual symbol" : typeByte === 5 ? "dual integer symbol" : "dual double symbol";
2827
- overflow(`Buffer overflow reading ${read}`, pointer, bufferLength, fieldName, filePath);
2861
+ if (pointer + numberBytes > end) {
2862
+ const read = typeByte === 5 ? "dual integer symbol" : "dual double symbol";
2863
+ overflow(`Buffer overflow reading ${read}`, pointer, end, fieldName, filePath, base);
2828
2864
  }
2829
- const terminator = textEnd(symbolBuffer, pointer + numberBytes, "Dual string symbol", fieldName, filePath);
2865
+ const terminator = textEnd(
2866
+ findNul,
2867
+ end,
2868
+ pointer + numberBytes,
2869
+ "Dual string symbol",
2870
+ fieldName,
2871
+ filePath,
2872
+ base
2873
+ );
2830
2874
  if (decode) {
2831
- number = typeByte === 5 ? symbolBuffer.readInt32LE(pointer) : symbolBuffer.readDoubleLE(pointer);
2832
- text = symbolBuffer.toString("utf8", pointer + numberBytes, terminator);
2875
+ number = typeByte === 5 ? area.readInt32LE(pointer) : area.readDoubleLE(pointer);
2876
+ text = area.toString("utf8", pointer + numberBytes, terminator);
2833
2877
  }
2834
2878
  pointer = terminator + 1;
2835
2879
  break;
@@ -2837,7 +2881,7 @@ function parseFieldSymbols(symbolBuffer, start, end, keep, fieldName, filePath)
2837
2881
  default: {
2838
2882
  throw new exports.QvdParseError("Unknown symbol type byte", {
2839
2883
  typeByte: typeByte.toString(16),
2840
- offset: pointer - 1,
2884
+ offset: base + pointer - 1,
2841
2885
  file: filePath,
2842
2886
  stage: "parseSymbolTable"
2843
2887
  });
@@ -2846,16 +2890,28 @@ function parseFieldSymbols(symbolBuffer, start, end, keep, fieldName, filePath)
2846
2890
  numbers.push(number);
2847
2891
  texts.push(text);
2848
2892
  }
2893
+ assert4__default.default(pointer === end, `The symbols of ${fieldName} were walked to byte ${pointer} of an area ending at ${end}.`);
2894
+ if (numbers.length !== symbolCount) {
2895
+ throw new exports.QvdCorruptedError("Symbol count mismatch", {
2896
+ field: fieldName,
2897
+ symbolCount: numbers.length,
2898
+ noOfSymbols: symbolCount,
2899
+ file: filePath,
2900
+ stage: "parseSymbolTable"
2901
+ });
2902
+ }
2849
2903
  return { numbers, texts };
2850
2904
  }
2851
- function countFieldSymbols(symbolBuffer, start, end, fieldName, filePath) {
2852
- return parseFieldSymbols(symbolBuffer, start, end, DECODE_NOTHING, fieldName, filePath).numbers.length;
2905
+ function countFieldSymbols(symbolBuffer, start, end, symbolCount, fieldName, filePath, base = 0) {
2906
+ return parseFieldSymbols(symbolBuffer, start, end, symbolCount, DECODE_NOTHING, fieldName, filePath, void 0, base).numbers.length;
2853
2907
  }
2854
- var MAX_TEXT_BYTES, DECODE_NOTHING;
2908
+ var MAX_TEXT_BYTES, TEXT_SEARCH, DECODE_NOTHING;
2855
2909
  var init_symbolParser = __esm({
2856
2910
  "src/util/symbolParser.js"() {
2857
2911
  init_QvdErrors();
2858
2912
  MAX_TEXT_BYTES = 1048576;
2913
+ TEXT_SEARCH = Object.freeze({ reach: 2 ** 31 - 1, rebaseAfter: 2 ** 30 });
2914
+ __name(nulFinder, "nulFinder");
2859
2915
  __name(textEnd, "textEnd");
2860
2916
  __name(overflow, "overflow");
2861
2917
  __name(parseFieldSymbols, "parseFieldSymbols");
@@ -3399,7 +3455,7 @@ async function parseHeaderXml(text, file, stage) {
3399
3455
  }
3400
3456
  return parsed;
3401
3457
  }
3402
- var MAX_HEADER_SIZE, READ_CHUNK_SIZE, ANALYSIS_SLICE_ROWS, COUNT_SYMBOLS_PAST; exports.QvdFileReader = void 0;
3458
+ var MAX_HEADER_SIZE, READ_CHUNK_SIZE, ANALYSIS_SLICE_ROWS, SLICE_BYTES, COUNT_SYMBOLS_PAST; exports.QvdFileReader = void 0;
3403
3459
  var init_QvdFileReader = __esm({
3404
3460
  "src/QvdFileReader.js"() {
3405
3461
  init_QvdDataFrame();
@@ -3417,6 +3473,7 @@ var init_QvdFileReader = __esm({
3417
3473
  MAX_HEADER_SIZE = 16 * 1024 * 1024;
3418
3474
  READ_CHUNK_SIZE = 512 * 1024 * 1024;
3419
3475
  ANALYSIS_SLICE_ROWS = 65536;
3476
+ SLICE_BYTES = 16 * 1024 * 1024;
3420
3477
  COUNT_SYMBOLS_PAST = 65536;
3421
3478
  __name(chunksFrom, "chunksFrom");
3422
3479
  __name(parseHeaderXml, "parseHeaderXml");
@@ -3448,6 +3505,10 @@ var init_QvdFileReader = __esm({
3448
3505
  * above which a lazy load switches to the two-pass filtering path. The default of 50MB is
3449
3506
  * the point where the extra analysis pass pays for itself; lower it to use filtering on
3450
3507
  * smaller files, raise it to keep the simpler single-pass read for longer.
3508
+ * @param {number} [options.sliceBytes=16777216] The most bytes of records a read holds at a time. An
3509
+ * option rather than a constant for the reason `symbolFilteringThreshold` is one: so that a test can
3510
+ * cross the boundaries between slices in a small file. There is no other reason to change it, and it
3511
+ * is not one of the options a read through `QvdDataFrame` or `QvdColumnTable` passes on.
3451
3512
  * @param {Array<string>|null} [options.fields] Field names to read, in the order they should
3452
3513
  * appear. Null reads every field, in file order. An unknown or repeated name is refused.
3453
3514
  * @param {'number'|'text'|'both'} [options.duals='number'] What a dual symbol - a number with the
@@ -3472,6 +3533,7 @@ var init_QvdFileReader = __esm({
3472
3533
  allowedDir,
3473
3534
  memorySafetyFactor = 0.8,
3474
3535
  symbolFilteringThreshold = 50 * 1024 * 1024,
3536
+ sliceBytes = SLICE_BYTES,
3475
3537
  materialisesRows = true,
3476
3538
  fields = null,
3477
3539
  duals,
@@ -3487,6 +3549,14 @@ var init_QvdFileReader = __esm({
3487
3549
  this._coerceNumericStrings = normaliseCoerceNumericStrings(coerceNumericStrings, this._path);
3488
3550
  this._memorySafetyFactor = memorySafetyFactor;
3489
3551
  this._symbolFilteringThreshold = symbolFilteringThreshold;
3552
+ if (!Number.isSafeInteger(sliceBytes) || sliceBytes <= 0) {
3553
+ throw new exports.QvdValidationError("sliceBytes must be a positive integer", {
3554
+ provided: sliceBytes,
3555
+ type: typeof sliceBytes,
3556
+ file: this._path
3557
+ });
3558
+ }
3559
+ this._sliceBytes = sliceBytes;
3490
3560
  if (onProgress !== void 0 && typeof onProgress !== "function") {
3491
3561
  throw new exports.QvdValidationError("onProgress must be a function", {
3492
3562
  provided: onProgress,
@@ -3504,7 +3574,11 @@ var init_QvdFileReader = __esm({
3504
3574
  this._requestedFields = fields === void 0 ? null : fields;
3505
3575
  this._onProgress = onProgress;
3506
3576
  this._signal = signal;
3507
- this._buffer = null;
3577
+ this._headerBuffer = null;
3578
+ this._handle = null;
3579
+ this._failed = null;
3580
+ this._reading = false;
3581
+ this._symbolAreas = null;
3508
3582
  this._headerOffset = null;
3509
3583
  this._symbolTableOffset = null;
3510
3584
  this._indexTableOffset = null;
@@ -3515,7 +3589,6 @@ var init_QvdFileReader = __esm({
3515
3589
  this._symbolTable = null;
3516
3590
  this._indexColumns = null;
3517
3591
  this._rowsDecoded = 0;
3518
- this._bufferFirstRow = 0;
3519
3592
  this._fileSize = null;
3520
3593
  this._headerMatchesFile = false;
3521
3594
  }
@@ -3554,54 +3627,137 @@ var init_QvdFileReader = __esm({
3554
3627
  }
3555
3628
  }
3556
3629
  /**
3557
- * Reads the binary data of the QVD file.
3558
- *
3559
- * A windowed read - anything with `offset`, `limit` or `maxRows` - reads only the bytes it
3560
- * needs, rather than the file. Measured on `chicago_taxi_rides_2016_01.qvd`, 1,705,805 rows
3561
- * over 20 fields: the last thousand rows take 19 ms against 636 ms for the whole file.
3562
- *
3563
- * The saving is in the index table and the rows, not in the symbol table, which is read in
3564
- * full whatever the window because a stored index in any row can address any symbol. So the
3565
- * gain scales with how much of the file is rows: on a file whose bytes are mostly distinct
3566
- * values there is very little to save, which is what `symbolFilteringThreshold` and the
3567
- * two-pass path exist for.
3568
- *
3569
- * Algorithm for a windowed read:
3570
- * 1. Read the file a chunk at a time until the XML header delimiter is found
3571
- * 2. Parse header to determine symbol table and index table locations
3572
- * 3. Calculate bytes needed: header + full symbol table + partial index table
3573
- * 4. Read only those calculated bytes, by position
3574
- * 5. Rest of parsing proceeds normally with limited data
3575
- *
3576
- * WHY THIS APPROACH:
3577
- * - Symbol table must be fully loaded (contains all unique values)
3578
- * - Index table can be partially loaded (only rows we need)
3579
- * - Reading chunks to find the header is efficient for unknown header sizes
3580
- * - Direct byte-range reading for remaining data is fastest
3630
+ * Opens the file and reads its header, and for a read of rows checks that the read can be made.
3631
+ *
3632
+ * Nothing past the header is read here. The selected fields' symbols and the records are read by position
3633
+ * as they are parsed - see `_symbolAreaOf` and `_forEachSlice` - so no read holds the file in one buffer
3634
+ * (#122). A read of rows therefore leaves the file open, and whatever started the read closes it with
3635
+ * `_closeFile` once the last record it needs is decoded; a header-only read closes it here.
3636
+ *
3637
+ * A windowed read - anything with `offset`, `limit` or `maxRows` - reads the header, the symbols of the
3638
+ * fields it selects, and the window's records, and no byte between. Measured on
3639
+ * `chicago_taxi_rides_2016_01.qvd`, 1,705,805 rows over 20 fields: the last thousand rows take 19 ms
3640
+ * against 636 ms for the whole file. A selected field's area is read in full whatever the window,
3641
+ * because a stored index in any row can address any of that field's symbols; the areas of the fields
3642
+ * `fields` leaves out are not read at all. So a window's gain scales with how much of the file is
3643
+ * rows: on a file whose bytes are mostly distinct values of the fields it reads there is very little
3644
+ * to save, which is what `symbolFilteringThreshold` and the two-pass path exist for.
3581
3645
  *
3582
3646
  * All of it goes through one handle, opened once, on the file the containment check approved. See
3583
3647
  * `chunksFrom` and `openChecked` for why a read no longer opens the path more than once.
3584
3648
  *
3585
- * A window with a non-zero `offset` reads two ranges rather than one: the header and symbol
3586
- * table from the front of the file, and the window's records from wherever they sit. The bytes
3587
- * between are never read, which is what makes `{offset: 1_700_000, limit: 100}` on the taxi
3588
- * fixture a 0.4MB read rather than a 38MB one.
3589
- *
3590
3649
  * @param {QvdRowWindow} window The rows to read.
3591
- * @param {boolean} [headerOnly=false] Stop once the XML header has been read, leaving the
3592
- * symbol and index tables on disk. This is the metadata-only path: the header is a few
3593
- * kilobytes whatever the file's size, so reading a schema costs the same for a 40MB file as
3594
- * for a 40GB one.
3650
+ * @param {boolean} [headerOnly=false] Stop once the XML header has been read, and close the file. This
3651
+ * is the metadata-only path: the header is a few kilobytes whatever the file's size, so reading a
3652
+ * schema costs the same for a 40MB file as for a 40GB one.
3595
3653
  * @param {{rows: number, perChunk: number}|null} [liveRows=null] Rows held at one instant when
3596
3654
  * that is fewer than the window covers - see `_prepare`.
3597
3655
  * @private
3598
3656
  */
3599
3657
  async _readData(window = { offset: 0, limit: null }, headerOnly = false, liveRows = null) {
3658
+ assert4__default.default(this._reading, "A read opens the QVD file only once it has started, through _startRead.");
3659
+ this._symbolTable = null;
3660
+ this._indexColumns = null;
3661
+ this._rowsDecoded = 0;
3600
3662
  this._throwIfAborted();
3601
3663
  this._emitProgress("read", 0, 1);
3602
3664
  const failed = rethrowAsIoError(this._path, "read");
3603
3665
  const handle = await openChecked(checkPath(this._path, this._allowedDir), "read", failed);
3604
- await closeAfter(handle, failed, () => this._readFrom(handle, window, headerOnly, liveRows, failed));
3666
+ if (headerOnly) {
3667
+ await closeAfter(handle, failed, () => this._readFrom(handle, window, true, liveRows, failed));
3668
+ return;
3669
+ }
3670
+ this._handle = handle;
3671
+ this._failed = failed;
3672
+ try {
3673
+ await this._readFrom(handle, window, false, liveRows, failed);
3674
+ } catch (error) {
3675
+ await this._closeFile(true);
3676
+ throw error;
3677
+ }
3678
+ }
3679
+ /**
3680
+ * Starts a read on this reader, refusing it while another is under way.
3681
+ *
3682
+ * A read of rows holds the file, and what it has read of it, on the reader until it ends. A second read
3683
+ * started meanwhile - `load()` while an iteration is suspended, say - would take over that state and
3684
+ * leave the first read's file open. Reads one after another are fine. Called before a read takes charge
3685
+ * of closing the file, so that refusing the second read cannot close the first one's.
3686
+ *
3687
+ * The first thing every read does, and synchronous: the flag is set before the read's first `await`, so
3688
+ * two reads started together - `Promise.all([reader.load(), reader.load()])` - cannot both pass. The check
3689
+ * used to be of the handle, which is set only once the file has opened, and both did: the second read's
3690
+ * handle replaced the first's, which was never closed, and the first read to finish closed the file the
3691
+ * other was still reading.
3692
+ *
3693
+ * @throws {QvdValidationError} If a read is under way.
3694
+ * @private
3695
+ */
3696
+ _startRead() {
3697
+ if (this._reading) {
3698
+ throw new exports.QvdValidationError("The reader is already reading this file: finish that read first", {
3699
+ file: this._path
3700
+ });
3701
+ }
3702
+ this._reading = true;
3703
+ }
3704
+ /**
3705
+ * Ends the read `_startRead` began: closes its file, if it still holds one, and lets the next read start.
3706
+ *
3707
+ * @param {boolean} failing Whether the read is already throwing - see `_closeFile`.
3708
+ * @private
3709
+ */
3710
+ async _endRead(failing) {
3711
+ try {
3712
+ await this._closeFile(failing);
3713
+ } finally {
3714
+ this._reading = false;
3715
+ }
3716
+ }
3717
+ /**
3718
+ * Closes the file a read of rows opened, and drops what it had read of it.
3719
+ *
3720
+ * The rule `closeAfter` follows: a close that fails is reported only when the read succeeded, so it can
3721
+ * never replace the error that says what went wrong. After a successful read it is the only news.
3722
+ *
3723
+ * @param {boolean} failing Whether the read is already throwing.
3724
+ * @private
3725
+ */
3726
+ async _closeFile(failing) {
3727
+ const handle = this._handle;
3728
+ const failed = this._failed;
3729
+ this._handle = null;
3730
+ this._failed = null;
3731
+ this._symbolAreas = null;
3732
+ if (handle === null || failed === null) {
3733
+ return;
3734
+ }
3735
+ if (failing) {
3736
+ await handle.close().catch(() => {
3737
+ });
3738
+ return;
3739
+ }
3740
+ await handle.close().catch(failed);
3741
+ }
3742
+ /**
3743
+ * Runs a read, the only one under way on this reader, and closes its file when it ends, however it ends.
3744
+ *
3745
+ * @template T
3746
+ * @param {() => Promise<T>} read The read, from opening the file to its last record.
3747
+ * @return {Promise<T>} What it returned.
3748
+ * @private
3749
+ */
3750
+ async _closingAfter(read) {
3751
+ this._startRead();
3752
+ let result;
3753
+ try {
3754
+ result = await read();
3755
+ } catch (error) {
3756
+ await this._endRead(true);
3757
+ throw error;
3758
+ }
3759
+ await this._endRead(false);
3760
+ return result;
3605
3761
  }
3606
3762
  /**
3607
3763
  * Reads what `_readData` was asked for, through the handle it opened.
@@ -3663,50 +3819,57 @@ var init_QvdFileReader = __esm({
3663
3819
  const recordSize = headerInteger(headerObj["QvdTableHeader"]["RecordByteSize"]);
3664
3820
  const totalRows = headerInteger(headerObj["QvdTableHeader"]["NoOfRecords"]);
3665
3821
  if (headerOnly) {
3666
- this._buffer = headerBuffer.subarray(0, headerEndIndex);
3822
+ this._headerBuffer = headerBuffer.subarray(0, headerEndIndex);
3667
3823
  this._emitProgress("read", 1, 1);
3668
3824
  return;
3669
3825
  }
3670
- const columnCount = selectFields(headerFields, this._requestedFields, this._path).length;
3826
+ this._headerBuffer = headerBuffer.subarray(0, headerEndIndex);
3827
+ const selected = selectFields(headerFields, this._requestedFields, this._path);
3828
+ const columnCount = selected.length;
3829
+ const areaBytes = selected.map((field) => headerInteger(field["Length"]));
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;
3671
3834
  const headerNumbersUsable = [symbolTableLength, recordSize, totalRows].every(
3672
3835
  (value) => Number.isSafeInteger(value) && value >= 0
3673
3836
  );
3837
+ const { size: fileSize } = await handle.stat().catch(failed);
3838
+ this._fileSize = fileSize;
3839
+ this._headerMatchesFile = false;
3674
3840
  if (headerNumbersUsable) {
3675
- const { size: fileSize2 } = await handle.stat().catch(failed);
3676
- this._fileSize = fileSize2;
3677
- this._headerMatchesFile = headerEndIndex + symbolTableLength + totalRows * recordSize <= fileSize2;
3841
+ this._headerMatchesFile = headerEndIndex + symbolTableLength + totalRows * recordSize <= fileSize;
3678
3842
  }
3679
3843
  const resolved = headerNumbersUsable ? resolveWindow(window, totalRows) : { offset: 0, limit: 0 };
3680
3844
  const windowRows = resolved.limit;
3681
3845
  if (headerNumbersUsable && this._headerMatchesFile) {
3682
3846
  validateMemoryAvailability(
3683
- symbolTableLength,
3847
+ symbolBytes,
3684
3848
  windowRows,
3685
3849
  totalRows,
3686
3850
  this._path,
3687
3851
  this._memorySafetyFactor,
3688
3852
  columnCount,
3689
3853
  this._materialisesRows,
3690
- liveRows
3854
+ liveRows,
3855
+ this._bytesHeld(
3856
+ symbolBytes,
3857
+ windowRows,
3858
+ recordSize,
3859
+ liveRows,
3860
+ this._analysisWouldRun(window, resolved, totalRows, symbolTableLength)
3861
+ )
3691
3862
  );
3692
3863
  }
3693
3864
  if (window.offset === 0 && window.limit === null) {
3694
- this._buffer = await handle.readFile().catch(failed);
3695
- this._fileSize = this._buffer.length;
3696
- this._bufferFirstRow = 0;
3697
3865
  this._emitProgress("read", 1, 1);
3698
3866
  return;
3699
3867
  }
3700
3868
  const rowsToLoad = windowRows;
3701
- validateSymbolTableSizeEarly(symbolTableLength, this._path);
3869
+ validateSymbolTableSizeEarly(symbolBytes, this._path);
3702
3870
  validateRecordSize(recordSize, this._path, "readData");
3703
3871
  validateRecordCount(totalRows, this._path, "readData");
3704
- const skippedIndexBytes = resolved.offset * recordSize;
3705
- const indexTableBytesToRead = rowsToLoad * recordSize;
3706
- const totalBytesToRead = indexTableOffset + indexTableBytesToRead;
3707
- const fileBytesRequired = indexTableOffset + skippedIndexBytes + indexTableBytesToRead;
3708
- const { size: fileSize } = await handle.stat().catch(failed);
3709
- this._fileSize = fileSize;
3872
+ const fileBytesRequired = indexTableOffset + (resolved.offset + rowsToLoad) * recordSize;
3710
3873
  if (fileBytesRequired > fileSize) {
3711
3874
  throw new exports.QvdCorruptedError("The file is shorter than its header claims.", {
3712
3875
  file: this._path,
@@ -3715,47 +3878,35 @@ var init_QvdFileReader = __esm({
3715
3878
  stage: "readData"
3716
3879
  });
3717
3880
  }
3718
- this._buffer = Buffer.alloc(totalBytesToRead);
3719
- await this._readRange(handle, 0, indexTableOffset, 0, fileSize, totalBytesToRead);
3720
- if (indexTableBytesToRead > 0) {
3721
- await this._readRange(
3722
- handle,
3723
- indexTableOffset,
3724
- indexTableBytesToRead,
3725
- indexTableOffset + skippedIndexBytes,
3726
- fileSize,
3727
- fileBytesRequired
3728
- );
3729
- }
3730
- this._bufferFirstRow = resolved.offset;
3731
3881
  this._emitProgress("read", 1, 1);
3732
3882
  }
3733
3883
  /**
3734
- * Reads one byte range of the file into the buffer.
3884
+ * Reads one byte range of the open file into a buffer.
3735
3885
  *
3736
3886
  * Read in bounded chunks, checking bytesRead each time. A single fs.read call with a length of
3737
3887
  * 2^31 or more does not throw - it trips a C++ assertion and aborts the whole process, which no
3738
3888
  * try/catch can intercept.
3739
3889
  *
3740
- * @param {import('fs/promises').FileHandle} fd The open file.
3741
- * @param {number} bufferOffset Where in the buffer to write.
3890
+ * @param {Buffer} target The buffer to read into.
3891
+ * @param {number} targetOffset Where in it to write.
3742
3892
  * @param {number} byteCount How many bytes to read.
3743
3893
  * @param {number} filePosition Where in the file to read from.
3744
- * @param {number} fileSize The file's size, for the error.
3745
- * @param {number} requiredBytes Bytes the whole read needs, for the error.
3894
+ * @param {number} requiredBytes How far into the file the read has to reach, for the error.
3895
+ * @throws {QvdCorruptedError} If the file ends before the range does.
3746
3896
  * @private
3747
3897
  */
3748
- async _readRange(fd, bufferOffset, byteCount, filePosition, fileSize, requiredBytes) {
3749
- assert3__default.default(this._buffer, "The read buffer has not been allocated.");
3750
- const failed = rethrowAsIoError(this._path, "read");
3898
+ async _readAt(target, targetOffset, byteCount, filePosition, requiredBytes) {
3899
+ assert4__default.default(this._handle && this._failed, "The QVD file is not open.");
3900
+ const handle = this._handle;
3901
+ const failed = this._failed;
3751
3902
  let done = 0;
3752
3903
  while (done < byteCount) {
3753
3904
  const length = Math.min(READ_CHUNK_SIZE, byteCount - done);
3754
- const { bytesRead } = await fd.read(this._buffer, bufferOffset + done, length, filePosition + done).catch(failed);
3905
+ const { bytesRead } = await handle.read(target, targetOffset + done, length, filePosition + done).catch(failed);
3755
3906
  if (bytesRead === 0) {
3756
3907
  throw new exports.QvdCorruptedError("Unexpected end of file while reading QVD data.", {
3757
3908
  file: this._path,
3758
- fileSize,
3909
+ fileSize: this._fileSize,
3759
3910
  // Two numbers, because they stopped being the same one when a window began reading two
3760
3911
  // ranges: `bytesRead` is how much of this range arrived, `filePosition` is where in the
3761
3912
  // file it gave up. Reporting the position under the name of the count made a windowed
@@ -3770,12 +3921,262 @@ var init_QvdFileReader = __esm({
3770
3921
  done += bytesRead;
3771
3922
  }
3772
3923
  }
3924
+ /**
3925
+ * Bytes of the file a read holds outside the heap while it works, beside the codes the guard counts for
3926
+ * itself: the symbol areas it reads, and the one buffer its records come through.
3927
+ *
3928
+ * The areas are counted whole, although the read lets each range go once its fields are parsed, because
3929
+ * 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 what
3931
+ * `_forEachSlice` will allocate: `sliceBytes` unless the records are fewer.
3932
+ *
3933
+ * @param {number} symbolBytes Bytes of symbols the read will read.
3934
+ * @param {number} rows Records it will read.
3935
+ * @param {number} recordSize Bytes per record.
3936
+ * @return {number} Bytes.
3937
+ * @private
3938
+ */
3939
+ _bytesHeldBy(symbolBytes, rows, recordSize) {
3940
+ const records = Number.isSafeInteger(rows) && Number.isSafeInteger(recordSize) ? rows * recordSize : 0;
3941
+ return symbolBytes + Math.min(this._sliceBytes, Math.max(0, records));
3942
+ }
3943
+ /**
3944
+ * What a read holds in bytes of the file, for the memory guard: what it holds now, and what a read
3945
+ * following either piece of advice a refusal can carry would hold instead.
3946
+ *
3947
+ * The two knobs are not the same knob, which is why there are two functions rather than one. A smaller
3948
+ * `limit` is a smaller window, so every record the read touches is one of fewer - the symbol-usage pass
3949
+ * included, since it reads the window. A smaller `chunkSize` leaves the window exactly where it is and
3950
+ * only changes how much of it is decoded at a time, so a read with that pass still ahead of it holds the
3951
+ * window's slice however small the chunk. Priced with `forRows`, such a chunk was charged for the records
3952
+ * of one chunk and then held sixteen megabytes more than that.
3953
+ *
3954
+ * @param {number} symbolBytes Bytes of symbols the read will read.
3955
+ * @param {number} windowRows Rows the read covers.
3956
+ * @param {number} recordSize Bytes per record.
3957
+ * @param {{rows: number, perChunk: number}|null} liveRows Rows held at one instant - see `_prepare`.
3958
+ * @param {boolean} analysisAhead Whether the symbol-usage pass has still to run.
3959
+ * @return {{held: number, forRows: (rows: number) => number, forChunk: (rows: number) => number}} What
3960
+ * this read holds, what a read of so many rows would hold, and what one reading so many rows a chunk
3961
+ * would hold.
3962
+ * @private
3963
+ */
3964
+ _bytesHeld(symbolBytes, windowRows, recordSize, liveRows, analysisAhead) {
3965
+ return {
3966
+ held: this._bytesHeldBy(symbolBytes, this._recordsAtOnce(windowRows, liveRows, analysisAhead), recordSize),
3967
+ // A window of so many rows reads so many records at a time, and the pass that reads it ahead of the
3968
+ // decode reads the same rows, so the buffer is sized from the rows either way.
3969
+ forRows: /* @__PURE__ */ __name((rows) => this._bytesHeldBy(symbolBytes, rows, recordSize), "forRows"),
3970
+ // A chunk of so many rows, over this read's window - which is what `chunkSize` changes and what it
3971
+ // leaves alone. `_recordsAtOnce` is what answers that, given a chunk size as the rows held at once.
3972
+ forChunk: /* @__PURE__ */ __name((rows) => this._bytesHeldBy(symbolBytes, this._recordsAtOnce(windowRows, { rows, perChunk: 1 }, analysisAhead), recordSize), "forChunk")
3973
+ };
3974
+ }
3975
+ /**
3976
+ * Whether a read takes the symbol-usage pass, which reads the window's records before the symbol table
3977
+ * is parsed and so before the first chunk is built.
3978
+ *
3979
+ * The one statement of the condition. `_prepare` asks it to decide, and `_readData` asks it before the
3980
+ * header has been parsed, to know what to charge the memory guard: a read with the pass ahead of it
3981
+ * holds a whole slice of records, and a read without it holds only what it reads at a time. Said in two
3982
+ * places, the two would drift and a read would be charged for one path and take the other - which fails
3983
+ * open for a chunked read, and that is the direction the guard exists to prevent.
3984
+ *
3985
+ * Any window that does not cover the whole file is a candidate, which includes one bounded by its offset
3986
+ * rather than by its limit. Covering every row rules it out, however the window was spelled -
3987
+ * `{offset: 0, limit: n}` over all n rows, or an `iterate` of them. Such a read needs every symbol any
3988
+ * row uses, which is what `estimateMemoryUsage` assumes for a full read as well, so the pass has nothing
3989
+ * to filter. It is not free: since the records are read as they are decoded rather than held in one
3990
+ * buffer, the pass reads the window's records and the decode then reads them again. A window that really
3991
+ * is a window pays that for the symbols it saves parsing; a window that is a full read in disguise paid
3992
+ * it for nothing.
3993
+ *
3994
+ * The threshold is an option rather than a constant so this path can be exercised with a small fixture:
3995
+ * it is the most intricate code in the reader, and the only files large enough to reach the 50MB default
3996
+ * are ones no repository should be carrying around. It measures the whole table rather than the areas a
3997
+ * read selects, because what the pass saves is parsing work across the table.
3998
+ *
3999
+ * @param {QvdRowWindow} window The window as the caller spelled it.
4000
+ * @param {{offset: number, limit: number}} resolved Where it lands in this file.
4001
+ * @param {number} totalRows Rows the file declares.
4002
+ * @param {number} symbolTableLength The symbol table's declared length.
4003
+ * @return {boolean} Whether the pass will run.
4004
+ * @private
4005
+ */
4006
+ _analysisWouldRun(window, resolved, totalRows, symbolTableLength) {
4007
+ return resolved.limit < totalRows && (window.limit !== null || window.offset > 0) && symbolTableLength > this._symbolFilteringThreshold;
4008
+ }
4009
+ /**
4010
+ * Records a read holds at one time, which is what its record buffer is sized from.
4011
+ *
4012
+ * A slice holds `sliceBytes` of records, or every record the read has left to read when that is fewer -
4013
+ * so what it costs depends on how many a read asks for at a time, not on how many it covers. An
4014
+ * iteration asks for a chunk: `iterate({limit: 20_000_000, chunkSize: 1000})` reads a thousand records at
4015
+ * a time however many its window covers, and charging it a full slice would refuse it for 16 MiB it never
4016
+ * allocates. The symbol-usage pass is the exception, because it reads the whole window in slices of its
4017
+ * own before the first chunk is built, so a read that still has that pass ahead of it is charged for it.
4018
+ *
4019
+ * @param {number} windowRows Rows the read covers.
4020
+ * @param {{rows: number, perChunk: number}|null} liveRows Rows held at one instant - see `_prepare`.
4021
+ * @param {boolean} analysisAhead Whether the symbol-usage pass has still to run.
4022
+ * @return {number} Records read at one time.
4023
+ * @private
4024
+ */
4025
+ _recordsAtOnce(windowRows, liveRows, analysisAhead) {
4026
+ const chunkRows = liveRows === null ? windowRows : Math.max(1, Math.floor(liveRows.rows / Math.max(1, liveRows.perChunk)));
4027
+ return analysisAhead ? Math.max(windowRows, chunkRows) : chunkRows;
4028
+ }
4029
+ /**
4030
+ * The symbol table's length, as much of it as the file holds: what the header declares, cut short where
4031
+ * the file ends. Known before a byte of the table is read, so everything that can refuse the table is
4032
+ * checked on this, before the table is allocated.
4033
+ *
4034
+ * A file that ends inside its symbol table is measured to where it ends, and the fields whose areas it cut
4035
+ * short are refused as `Symbol data extends beyond buffer` when their metadata is checked - what a
4036
+ * whole-file read has always said of such a file. A window has refused it already, before reading anything.
4037
+ *
4038
+ * @return {number} Bytes.
4039
+ * @private
4040
+ */
4041
+ _symbolTableLength() {
4042
+ assert4__default.default(
4043
+ this._symbolTableOffset !== null && this._indexTableOffset !== null && this._fileSize !== null,
4044
+ "The QVD file header has not been parsed before its symbol table was measured."
4045
+ );
4046
+ const declared = this._indexTableOffset - this._symbolTableOffset;
4047
+ return Math.max(0, Math.min(declared, this._fileSize - this._symbolTableOffset));
4048
+ }
4049
+ /**
4050
+ * Where each selected field's symbols are, as ranges of the symbol table this read will read.
4051
+ *
4052
+ * A field's `Offset` and `Length` say exactly where its symbols are, so a read of some of a file's fields
4053
+ * has no reason to read the areas of the rest (#122). Qlik writes the areas one after another in field
4054
+ * order, so ranges that touch are merged: a read of every field is one range, and so is a read of fields
4055
+ * that happen to be neighbours. A read of one field of twenty reads that field's area alone.
4056
+ *
4057
+ * Built once per read, from the fields the read selected, and each field's metadata is checked as it is
4058
+ * added - a range is arithmetic on `Offset` and `Length`, and those have to be inside the table first.
4059
+ * `_parseSymbolTable` checks every field of the file, selected or not, before it parses any.
4060
+ *
4061
+ * @return {{ranges: Array<{start: number, end: number, fields: number, buffer: Buffer|null}>,
4062
+ * byField: Map<any, {range: {start: number, end: number, fields: number, buffer: Buffer|null},
4063
+ * start: number, end: number}>}} The ranges, and where in its range each field's area sits.
4064
+ * @private
4065
+ */
4066
+ _symbolAreaPlan() {
4067
+ if (this._symbolAreas !== null) {
4068
+ return this._symbolAreas;
4069
+ }
4070
+ assert4__default.default(this._selectedFields, "The QVD file fields have not been resolved before their symbols were read.");
4071
+ const tableLength = this._symbolTableLength();
4072
+ const areas = this._selectedFields.map((field) => {
4073
+ validateFieldMetadata(field, tableLength, this._path);
4074
+ const start = headerInteger(field["Offset"]);
4075
+ return { field, start, end: start + headerInteger(field["Length"]) };
4076
+ });
4077
+ const ranges = [];
4078
+ const byField = /* @__PURE__ */ new Map();
4079
+ for (const area of [...areas].sort((a, b) => a.start - b.start)) {
4080
+ const last = ranges.at(-1);
4081
+ const range = last !== void 0 && area.start <= last.end ? last : { start: area.start, end: area.end, fields: 0, buffer: null };
4082
+ if (range !== last) {
4083
+ ranges.push(range);
4084
+ }
4085
+ range.end = Math.max(range.end, area.end);
4086
+ range.fields += 1;
4087
+ byField.set(area.field, { range, start: area.start, end: area.end });
4088
+ }
4089
+ this._symbolAreas = { ranges, byField };
4090
+ return this._symbolAreas;
4091
+ }
4092
+ /**
4093
+ * One field's symbols, as bytes: the range that holds them, read from the open file the first time a field
4094
+ * of that range needs it.
4095
+ *
4096
+ * @param {any} field The field, one this read selected.
4097
+ * @return {Promise<{buffer: Buffer, start: number, end: number, base: number}>} Its area, as a range of
4098
+ * `buffer`, with where that buffer starts in the symbol table - what an error adds back to say where a
4099
+ * damaged symbol is in the file, rather than where it is in the bytes this read happened to read.
4100
+ * @throws {QvdValidationError} If the range is larger than half the heap.
4101
+ * @private
4102
+ */
4103
+ async _symbolAreaOf(field) {
4104
+ assert4__default.default(
4105
+ this._header && this._symbolTableOffset !== null,
4106
+ "The QVD file header has not been parsed before its symbols were read."
4107
+ );
4108
+ const area = this._symbolAreaPlan().byField.get(field);
4109
+ assert4__default.default(area, "A field this read did not select has no symbol area.");
4110
+ const { range } = area;
4111
+ if (range.buffer === null) {
4112
+ const length = range.end - range.start;
4113
+ validateSymbolTableSize(length, this._path, headerInteger(this._header["QvdTableHeader"]["NoOfRecords"]));
4114
+ const buffer = Buffer.alloc(length);
4115
+ const from = this._symbolTableOffset + range.start;
4116
+ await this._readAt(buffer, 0, length, from, from + length);
4117
+ range.buffer = buffer;
4118
+ }
4119
+ return { buffer: range.buffer, start: area.start - range.start, end: area.end - range.start, base: range.start };
4120
+ }
4121
+ /**
4122
+ * Lets go of a field's symbols once they are parsed, and of the bytes of its range once every field in it
4123
+ * has been.
4124
+ *
4125
+ * Every text is copied out of the bytes as it is decoded, so what the read keeps is the values. A read of
4126
+ * one field of a file whose other fields are large therefore holds that field's bytes and no others.
4127
+ *
4128
+ * @param {any} field The field whose symbols are parsed.
4129
+ * @private
4130
+ */
4131
+ _releaseSymbolArea(field) {
4132
+ const area = this._symbolAreaPlan().byField.get(field);
4133
+ assert4__default.default(area, "A field this read did not select has no symbol area.");
4134
+ area.range.fields -= 1;
4135
+ if (area.range.fields === 0) {
4136
+ area.range.buffer = null;
4137
+ }
4138
+ }
4139
+ /**
4140
+ * Reads records from the open file a slice at a time, and hands each slice to `visit`.
4141
+ *
4142
+ * One buffer of at most `sliceBytes` holds a slice, and is reused for the next one, so a read of any
4143
+ * number of records holds that much of them and no more. Cancellation is checked before each slice.
4144
+ *
4145
+ * @param {number} firstRow The file row of the first record.
4146
+ * @param {number} rowCount How many records.
4147
+ * @param {number} recordSize Bytes per record.
4148
+ * @param {(slice: Buffer, done: number, count: number) => void|Promise<void>} visit Called with each
4149
+ * slice's records, how many records came before it, and how many it holds.
4150
+ * @private
4151
+ */
4152
+ async _forEachSlice(firstRow, rowCount, recordSize, visit) {
4153
+ if (rowCount === 0) {
4154
+ return;
4155
+ }
4156
+ assert4__default.default(this._indexTableOffset !== null, "The QVD file header has not been parsed before its records were read.");
4157
+ const sliceRows = Math.max(1, Math.min(rowCount, Math.floor(this._sliceBytes / Math.max(1, recordSize))));
4158
+ const slice = Buffer.alloc(sliceRows * recordSize);
4159
+ const requiredBytes = this._indexTableOffset + (firstRow + rowCount) * recordSize;
4160
+ for (let done = 0; done < rowCount; done += sliceRows) {
4161
+ this._throwIfAborted();
4162
+ const count = Math.min(sliceRows, rowCount - done);
4163
+ const records = slice.subarray(0, count * recordSize);
4164
+ await this._readAt(
4165
+ records,
4166
+ 0,
4167
+ records.length,
4168
+ this._indexTableOffset + (firstRow + done) * recordSize,
4169
+ requiredBytes
4170
+ );
4171
+ await visit(records, done, count);
4172
+ }
4173
+ }
3773
4174
  /**
3774
4175
  * Parses the XML header of the QVD file. This method is part of the parsing process
3775
4176
  * and should not be called directly.
3776
4177
  */
3777
4178
  async _parseHeader() {
3778
- if (!this._buffer) {
4179
+ if (!this._headerBuffer) {
3779
4180
  throw new exports.QvdCorruptedError(
3780
4181
  "The QVD file has not been loaded in the proper order or has not been loaded at all.",
3781
4182
  {
@@ -3786,7 +4187,7 @@ var init_QvdFileReader = __esm({
3786
4187
  }
3787
4188
  const HEADER_DELIMITER = "\r\n\0";
3788
4189
  const headerBeginIndex = 0;
3789
- const headerDelimiterIndex = this._buffer.indexOf(HEADER_DELIMITER, headerBeginIndex);
4190
+ const headerDelimiterIndex = this._headerBuffer.indexOf(HEADER_DELIMITER, headerBeginIndex);
3790
4191
  if (headerDelimiterIndex === -1) {
3791
4192
  throw new exports.QvdCorruptedError(
3792
4193
  "The XML header section does not exist or is not properly delimited from the binary data.",
@@ -3797,7 +4198,7 @@ var init_QvdFileReader = __esm({
3797
4198
  );
3798
4199
  }
3799
4200
  const headerEndIndex = headerDelimiterIndex + HEADER_DELIMITER.length;
3800
- const headerBuffer = this._buffer.subarray(headerBeginIndex, headerEndIndex);
4201
+ const headerBuffer = this._headerBuffer.subarray(headerBeginIndex, headerEndIndex);
3801
4202
  this._fieldBitMetadataValidated = false;
3802
4203
  this._header = await parseHeaderXml(headerBuffer.toString(), this._path, "parseHeader");
3803
4204
  const fieldList = validateHeaderStructure(this._header, this._path, "parseHeader");
@@ -3819,12 +4220,12 @@ var init_QvdFileReader = __esm({
3819
4220
  * @param {QvdRowWindow} window The rows of interest, as file row indices.
3820
4221
  * @param {string} stage Stage name for any error raised here.
3821
4222
  * @return {{fields: Array<any>, recordSize: number, totalRows: number, rowsToLoad: number,
3822
- * indexBuffer: Buffer, firstRow: number}} The record geometry. `indexBuffer` starts at the
3823
- * window's first record, file row `firstRow`, so the decoder always counts from zero.
4223
+ * firstRow: number}} The record geometry: the window's `rowsToLoad` records start at file row
4224
+ * `firstRow`, and `_forEachSlice` reads them.
3824
4225
  * @private
3825
4226
  */
3826
4227
  _planIndexTable(window, stage) {
3827
- if (!this._buffer || !this._header || !this._indexTableOffset || !this._selectedFields || !this._allFields) {
4228
+ if (!this._handle || !this._header || !this._indexTableOffset || !this._selectedFields || !this._allFields) {
3828
4229
  throw new exports.QvdCorruptedError(
3829
4230
  "The QVD file has not been loaded in the proper order or has not been loaded at all.",
3830
4231
  {
@@ -3839,22 +4240,18 @@ var init_QvdFileReader = __esm({
3839
4240
  const totalRows = headerInteger(this._header["QvdTableHeader"]["NoOfRecords"]);
3840
4241
  const indexTableLength = headerInteger(this._header["QvdTableHeader"]["Length"]);
3841
4242
  const { offset: firstRow, limit: rowsToLoad } = resolveWindow(window, totalRows);
4243
+ assert4__default.default(this._fileSize !== null, "The QVD file has not been measured before its records were planned.");
3842
4244
  validateIndexTableMetadata(
3843
4245
  recordSize,
3844
4246
  totalRows,
3845
4247
  indexTableLength,
3846
4248
  this._indexTableOffset,
3847
- this._buffer.length,
4249
+ this._fileSize,
3848
4250
  rowsToLoad,
3849
4251
  this._path,
3850
4252
  this._fileSize,
3851
4253
  firstRow,
3852
- this._bufferFirstRow
3853
- );
3854
- const bufferRecordStart = (firstRow - this._bufferFirstRow) * recordSize;
3855
- const indexBuffer = this._buffer.subarray(
3856
- this._indexTableOffset + bufferRecordStart,
3857
- this._indexTableOffset + bufferRecordStart + rowsToLoad * recordSize
4254
+ 0
3858
4255
  );
3859
4256
  if (!this._fieldBitMetadataValidated) {
3860
4257
  for (const field of allFields) {
@@ -3863,11 +4260,12 @@ var init_QvdFileReader = __esm({
3863
4260
  validateBitFields(allFields, this._path);
3864
4261
  this._fieldBitMetadataValidated = true;
3865
4262
  }
3866
- assert3__default.default(
3867
- rowsToLoad === 0 || recordSize === 0 || Math.floor(indexBuffer.length / recordSize) >= rowsToLoad,
3868
- `The index table holds ${Math.floor(indexBuffer.length / (recordSize || 1))} whole records but ${rowsToLoad} were validated as present.`
4263
+ const windowEnd = this._indexTableOffset + (firstRow + rowsToLoad) * recordSize;
4264
+ assert4__default.default(
4265
+ rowsToLoad === 0 || recordSize === 0 || windowEnd <= this._fileSize,
4266
+ `The window's records end at byte ${windowEnd} of a file of ${this._fileSize}, but ${rowsToLoad} were validated as present.`
3869
4267
  );
3870
- return { fields, recordSize, totalRows, rowsToLoad, indexBuffer, firstRow };
4268
+ return { fields, recordSize, totalRows, rowsToLoad, firstRow };
3871
4269
  }
3872
4270
  /**
3873
4271
  * Analyzes the index table to determine which symbols are actually needed.
@@ -3883,48 +4281,58 @@ var init_QvdFileReader = __esm({
3883
4281
  * @private
3884
4282
  */
3885
4283
  async _analyzeIndexTableSymbolUsage(window) {
3886
- const { fields, recordSize, rowsToLoad, indexBuffer } = this._planIndexTable(window, "analyzeIndexTableSymbolUsage");
4284
+ const { fields, recordSize, rowsToLoad, firstRow } = this._planIndexTable(window, "analyzeIndexTableSymbolUsage");
3887
4285
  const symbolUsage = [];
3888
4286
  const sliceRows = Math.min(rowsToLoad, ANALYSIS_SLICE_ROWS);
3889
4287
  const column = new Int32Array(sliceRows);
3890
- fields.forEach((field, position) => {
3891
- this._throwIfAborted();
4288
+ const state = fields.map((field) => {
3892
4289
  const needed = /* @__PURE__ */ new Set();
3893
- symbolUsage[position] = needed;
3894
- const bitOffset = headerInteger(field["BitOffset"]);
3895
- const bitWidth = headerInteger(field["BitWidth"]);
3896
- const bias = headerInteger(field["Bias"]);
4290
+ symbolUsage.push(needed);
3897
4291
  const length = headerInteger(field["Length"]);
3898
- let indexLimit = Number.isSafeInteger(length) && length >= 0 ? Math.ceil(length / 2) : Infinity;
3899
- let counted = false;
3900
- for (let first = 0; first < rowsToLoad; first += sliceRows) {
3901
- const count = Math.min(sliceRows, rowsToLoad - first);
3902
- decodeIndexColumn(
3903
- first === 0 ? indexBuffer : indexBuffer.subarray(first * recordSize),
3904
- recordSize,
3905
- count,
3906
- bitOffset,
3907
- bitWidth,
3908
- bias,
3909
- column
3910
- );
3911
- for (let row = 0; row < count; row++) {
3912
- if (column[row] >= 0 && column[row] < indexLimit) {
3913
- needed.add(column[row]);
4292
+ return {
4293
+ field,
4294
+ needed,
4295
+ bitOffset: headerInteger(field["BitOffset"]),
4296
+ bitWidth: headerInteger(field["BitWidth"]),
4297
+ bias: headerInteger(field["Bias"]),
4298
+ indexLimit: Number.isSafeInteger(length) && length >= 0 ? Math.ceil(length / 2) : Infinity,
4299
+ counted: false
4300
+ };
4301
+ });
4302
+ await this._forEachSlice(firstRow, rowsToLoad, recordSize, async (records, done, recordCount) => {
4303
+ for (let first = 0; first < recordCount; first += sliceRows) {
4304
+ const count = Math.min(sliceRows, recordCount - first);
4305
+ for (const field of state) {
4306
+ decodeIndexColumn(
4307
+ first === 0 ? records : records.subarray(first * recordSize),
4308
+ recordSize,
4309
+ count,
4310
+ field.bitOffset,
4311
+ field.bitWidth,
4312
+ field.bias,
4313
+ column
4314
+ );
4315
+ for (let row = 0; row < count; row++) {
4316
+ if (column[row] >= 0 && column[row] < field.indexLimit) {
4317
+ field.needed.add(column[row]);
4318
+ }
3914
4319
  }
3915
- }
3916
- if (!counted && needed.size > COUNT_SYMBOLS_PAST) {
3917
- counted = true;
3918
- indexLimit = Math.min(indexLimit, this._countFieldSymbols(field));
3919
- for (const index of needed) {
3920
- if (index >= indexLimit) {
3921
- needed.delete(index);
4320
+ if (!field.counted && field.needed.size > COUNT_SYMBOLS_PAST) {
4321
+ field.counted = true;
4322
+ field.indexLimit = Math.min(field.indexLimit, await this._countFieldSymbols(field.field));
4323
+ for (const index of field.needed) {
4324
+ if (index >= field.indexLimit) {
4325
+ field.needed.delete(index);
4326
+ }
3922
4327
  }
3923
4328
  }
3924
4329
  }
3925
4330
  }
3926
- this._emitProgress("symbol-analysis", position + 1, fields.length);
4331
+ this._emitProgress("symbol-analysis", done + recordCount, rowsToLoad);
3927
4332
  });
4333
+ if (rowsToLoad === 0) {
4334
+ this._emitProgress("symbol-analysis", 0, 0);
4335
+ }
3928
4336
  return symbolUsage;
3929
4337
  }
3930
4338
  /**
@@ -3932,28 +4340,27 @@ var init_QvdFileReader = __esm({
3932
4340
  *
3933
4341
  * The count is `countFieldSymbols`, the parse itself told to decode nothing, so it is the count
3934
4342
  * `_parseSymbolTable` will produce and `_parseIndexTable` will check against. The field's area is
3935
- * validated first, as `_parseSymbolTable` would, so a damaged `Offset` or `Length` is reported the
3936
- * same way wherever it is met.
4343
+ * validated before it is read, by `_symbolAreaPlan`, so a damaged `Offset`, `Length` or `NoOfSymbols` is
4344
+ * reported the same way wherever it is met. Its bytes are kept for the parse that follows. The walk checks the count against `NoOfSymbols` as the
4345
+ * parse does, so a field whose count is wrong is refused here, before the pass keeps anything on the
4346
+ * strength of it.
3937
4347
  *
3938
4348
  * @param {any} field The field's header.
3939
- * @return {number} Its symbols.
3940
- * @throws {QvdCorruptedError} If the area is not inside the symbol table, or a symbol runs past it.
4349
+ * @return {Promise<number>} Its symbols.
4350
+ * @throws {QvdCorruptedError} If the area is not inside the symbol table, a symbol runs past it, or it
4351
+ * holds a different number of symbols from its `NoOfSymbols`.
3941
4352
  * @private
3942
4353
  */
3943
- _countFieldSymbols(field) {
3944
- assert3__default.default(
3945
- this._buffer && this._symbolTableOffset && this._indexTableOffset,
3946
- "The QVD file has not been read before its symbols were counted."
3947
- );
3948
- const symbolBuffer = this._buffer.subarray(this._symbolTableOffset, this._indexTableOffset);
3949
- validateFieldMetadata(field, symbolBuffer.length, this._path);
3950
- const offset = headerInteger(field["Offset"]);
4354
+ async _countFieldSymbols(field) {
4355
+ const area = await this._symbolAreaOf(field);
3951
4356
  return countFieldSymbols(
3952
- symbolBuffer,
3953
- offset,
3954
- offset + headerInteger(field["Length"]),
4357
+ area.buffer,
4358
+ area.start,
4359
+ area.end,
4360
+ headerInteger(field["NoOfSymbols"]),
3955
4361
  field["FieldName"],
3956
- this._path
4362
+ this._path,
4363
+ area.base
3957
4364
  );
3958
4365
  }
3959
4366
  /**
@@ -3973,7 +4380,7 @@ var init_QvdFileReader = __esm({
3973
4380
  * that is fewer than the window covers - see `_prepare`.
3974
4381
  */
3975
4382
  async _parseSymbolTable(symbolsToKeep = null, rowsToLoad = 0, liveRows = null) {
3976
- if (!this._buffer || !this._header || !this._symbolTableOffset || !this._indexTableOffset || !this._selectedFields || !this._allFields) {
4383
+ if (!this._handle || !this._header || !this._symbolTableOffset || !this._indexTableOffset || !this._selectedFields || !this._allFields) {
3977
4384
  throw new exports.QvdCorruptedError(
3978
4385
  "The QVD file has not been loaded in the proper order or has not been loaded at all.",
3979
4386
  {
@@ -3984,44 +4391,55 @@ var init_QvdFileReader = __esm({
3984
4391
  }
3985
4392
  const allFields = this._allFields;
3986
4393
  const fields = this._selectedFields;
3987
- const symbolBuffer = this._buffer.subarray(this._symbolTableOffset, this._indexTableOffset);
3988
- const symbolTableSize = symbolBuffer.length;
4394
+ const symbolTableSize = this._symbolTableLength();
4395
+ const plan = this._symbolAreaPlan();
4396
+ const symbolBytes = plan.ranges.reduce((sum, range) => sum + (range.end - range.start), 0);
3989
4397
  const totalRows = headerInteger(this._header["QvdTableHeader"]["NoOfRecords"]);
3990
- validateSymbolTableSize(symbolTableSize, this._path, totalRows);
4398
+ const recordSize = headerInteger(this._header["QvdTableHeader"]["RecordByteSize"]);
4399
+ validateSymbolTableSize(symbolBytes, this._path, totalRows);
3991
4400
  if (this._headerMatchesFile) {
3992
4401
  validateMemoryAvailability(
3993
- symbolTableSize,
4402
+ symbolBytes,
3994
4403
  rowsToLoad,
3995
4404
  totalRows,
3996
4405
  this._path,
3997
4406
  this._memorySafetyFactor,
3998
4407
  fields.length,
3999
4408
  this._materialisesRows,
4000
- liveRows
4409
+ liveRows,
4410
+ this._bytesHeld(symbolBytes, rowsToLoad, recordSize, liveRows, false)
4001
4411
  );
4002
4412
  }
4003
- warnLargeSymbolTable(symbolTableSize, rowsToLoad, totalRows, fields.length, this._materialisesRows);
4413
+ warnLargeSymbolTable(symbolBytes, rowsToLoad, totalRows, fields.length, this._materialisesRows);
4004
4414
  for (const field of allFields) {
4005
- validateFieldMetadata(field, symbolBuffer.length, this._path);
4415
+ validateFieldMetadata(field, symbolTableSize, this._path);
4006
4416
  }
4007
4417
  validateSymbolAreas(allFields, this._path);
4008
- this._symbolTable = fields.map((field, position) => {
4418
+ const symbolTable = [];
4419
+ for (const [position, field] of fields.entries()) {
4009
4420
  this._throwIfAborted();
4010
- const symbolsOffset = headerInteger(field["Offset"]);
4011
- const symbolsLength = headerInteger(field["Length"]);
4012
- const symbols = parseFieldSymbols(
4013
- symbolBuffer,
4014
- symbolsOffset,
4015
- symbolsOffset + symbolsLength,
4016
- // By position, matching how `_analyzeIndexTableSymbolUsage` built it. Both walk
4017
- // `this._selectedFields`, so position is the one key that cannot collide.
4018
- symbolsToKeep ? symbolsToKeep[position] : null,
4019
- field["FieldName"],
4020
- this._path
4421
+ const area = await this._symbolAreaOf(field);
4422
+ symbolTable.push(
4423
+ parseFieldSymbols(
4424
+ area.buffer,
4425
+ area.start,
4426
+ area.end,
4427
+ // Checked against the symbols the area holds, which is the one check that sees a terminator
4428
+ // damaged in the middle of it (#124).
4429
+ headerInteger(field["NoOfSymbols"]),
4430
+ // By position, matching how `_analyzeIndexTableSymbolUsage` built it. Both walk
4431
+ // `this._selectedFields`, so position is the one key that cannot collide.
4432
+ symbolsToKeep ? symbolsToKeep[position] : null,
4433
+ field["FieldName"],
4434
+ this._path,
4435
+ void 0,
4436
+ area.base
4437
+ )
4021
4438
  );
4439
+ this._releaseSymbolArea(field);
4022
4440
  this._emitProgress("symbol-table", position + 1, fields.length);
4023
- return symbols;
4024
- });
4441
+ }
4442
+ this._symbolTable = symbolTable;
4025
4443
  }
4026
4444
  /**
4027
4445
  * Parses the bit stuffed index table of the QVD file. This method is part of the parsing process
@@ -4048,27 +4466,46 @@ var init_QvdFileReader = __esm({
4048
4466
  * straight to the caller. Rows outside the window are not decoded, so they are not checked.
4049
4467
  *
4050
4468
  * @param {QvdRowWindow} window The rows to decode.
4469
+ * @param {number} [progressBase=0] Rows decoded before this call, so that progress over a chunked
4470
+ * iteration counts the whole window rather than restarting at every chunk - what `_buildRows` takes
4471
+ * for the same reason.
4472
+ * @param {number|null} [progressTotal=null] Rows the whole window covers, or null for this call's own.
4473
+ * @param {Array<Int32Array>|null} [into=null] Arrays to decode into, one per selected field and at least
4474
+ * `limit` long, for a caller that decodes chunk after chunk and keeps none of them. Null allocates.
4051
4475
  * @throws {QvdCorruptedError} If an index in the window addresses neither a symbol nor NULL.
4052
4476
  */
4053
- async _parseIndexTable(window) {
4054
- const { fields, recordSize, rowsToLoad, indexBuffer, firstRow } = this._planIndexTable(window, "parseIndexTable");
4055
- assert3__default.default(this._symbolTable, "The QVD file symbol table has not been parsed.");
4477
+ async _parseIndexTable(window, progressBase = 0, progressTotal = null, into = null) {
4478
+ const { fields, recordSize, rowsToLoad, firstRow } = this._planIndexTable(window, "parseIndexTable");
4479
+ const decodedBefore = progressBase;
4480
+ const decodedTotal = progressTotal === null ? rowsToLoad : progressTotal;
4481
+ assert4__default.default(this._symbolTable, "The QVD file symbol table has not been parsed.");
4056
4482
  const symbolTable = this._symbolTable;
4057
- const columns = fields.map((field, position) => {
4058
- this._throwIfAborted();
4059
- const column = decodeIndexColumn(
4060
- indexBuffer,
4061
- recordSize,
4062
- rowsToLoad,
4063
- headerInteger(field["BitOffset"]),
4064
- headerInteger(field["BitWidth"]),
4065
- headerInteger(field["Bias"]),
4066
- new Int32Array(rowsToLoad),
4067
- { symbolCount: symbolTable[position].numbers.length, field: field["FieldName"], file: this._path, firstRow }
4068
- );
4069
- this._emitProgress("index-table", position + 1, fields.length);
4070
- return column;
4483
+ const decoders = fields.map((field, position) => ({
4484
+ bitOffset: headerInteger(field["BitOffset"]),
4485
+ bitWidth: headerInteger(field["BitWidth"]),
4486
+ bias: headerInteger(field["Bias"]),
4487
+ symbolCount: symbolTable[position].numbers.length,
4488
+ name: field["FieldName"]
4489
+ }));
4490
+ const columns = into === null ? fields.map(() => new Int32Array(rowsToLoad)) : into.map((codes) => codes.subarray(0, rowsToLoad));
4491
+ await this._forEachSlice(firstRow, rowsToLoad, recordSize, (records, done, count) => {
4492
+ decoders.forEach((decoder, position) => {
4493
+ decodeIndexColumn(
4494
+ records,
4495
+ recordSize,
4496
+ count,
4497
+ decoder.bitOffset,
4498
+ decoder.bitWidth,
4499
+ decoder.bias,
4500
+ columns[position].subarray(done, done + count),
4501
+ { symbolCount: decoder.symbolCount, field: decoder.name, file: this._path, firstRow: firstRow + done }
4502
+ );
4503
+ });
4504
+ this._emitProgress("index-table", decodedBefore + done + count, decodedTotal);
4071
4505
  });
4506
+ if (rowsToLoad === 0) {
4507
+ this._emitProgress("index-table", decodedBefore, decodedTotal);
4508
+ }
4072
4509
  this._indexColumns = columns;
4073
4510
  this._rowsDecoded = rowsToLoad;
4074
4511
  }
@@ -4087,31 +4524,33 @@ var init_QvdFileReader = __esm({
4087
4524
  * @return {Promise<import('./QvdDataFrame.js').QvdFileMetadata>} The file's schema and header.
4088
4525
  */
4089
4526
  async loadMetadata() {
4090
- await this._readData({ offset: 0, limit: null }, true);
4091
- this._emitProgress("header", 0, 1);
4092
- await this._parseHeader();
4093
- this._emitProgress("header", 1, 1);
4094
- this._throwIfAborted();
4095
- assert3__default.default(this._header && this._allFields, "The QVD file header has not been parsed.");
4096
- const header = this._header["QvdTableHeader"];
4097
- const columns = this._allFields.map((field) => field["FieldName"]);
4098
- const rowCount = headerInteger(header["NoOfRecords"]);
4099
- validateRecordCount(rowCount, this._path, "readMetadata");
4100
- const shape = new exports.QvdDataFrame([], columns, header, {
4101
- symbolTableBytes: headerInteger(header["Offset"]),
4102
- totalRows: rowCount,
4103
- rowsLoaded: 0,
4104
- symbolFiltering: false,
4105
- symbolsKept: null
4527
+ return await this._closingAfter(async () => {
4528
+ await this._readData({ offset: 0, limit: null }, true);
4529
+ this._emitProgress("header", 0, 1);
4530
+ await this._parseHeader();
4531
+ this._emitProgress("header", 1, 1);
4532
+ this._throwIfAborted();
4533
+ assert4__default.default(this._header && this._allFields, "The QVD file header has not been parsed.");
4534
+ const header = this._header["QvdTableHeader"];
4535
+ const columns = this._allFields.map((field) => field["FieldName"]);
4536
+ const rowCount = headerInteger(header["NoOfRecords"]);
4537
+ validateRecordCount(rowCount, this._path, "readMetadata");
4538
+ const shape = new exports.QvdDataFrame([], columns, header, {
4539
+ symbolTableBytes: headerInteger(header["Offset"]),
4540
+ totalRows: rowCount,
4541
+ rowsLoaded: 0,
4542
+ symbolFiltering: false,
4543
+ symbolsKept: null
4544
+ });
4545
+ return {
4546
+ columns,
4547
+ rowCount,
4548
+ columnCount: columns.length,
4549
+ fields: columns.map((name) => shape.getFieldMetadata(name)),
4550
+ fileMetadata: shape.fileMetadata,
4551
+ metadata: header
4552
+ };
4106
4553
  });
4107
- return {
4108
- columns,
4109
- rowCount,
4110
- columnCount: columns.length,
4111
- fields: columns.map((name) => shape.getFieldMetadata(name)),
4112
- fileMetadata: shape.fileMetadata,
4113
- metadata: header
4114
- };
4115
4554
  }
4116
4555
  /**
4117
4556
  * Loads the QVD file into memory and parses it.
@@ -4126,19 +4565,21 @@ var init_QvdFileReader = __esm({
4126
4565
  */
4127
4566
  async load(window = null) {
4128
4567
  const rows = normaliseWindow(window, this._path);
4129
- const prepared = await this._prepare(rows);
4130
- await this._parseIndexTable({ offset: prepared.offset, limit: prepared.rowsAvailable });
4131
- const data = this._buildRows(prepared.resolvedByField, 0, prepared.rowsAvailable);
4132
- return new exports.QvdDataFrame(
4133
- data,
4134
- prepared.columns,
4135
- prepared.metadata,
4136
- {
4137
- ...prepared.loadStats,
4138
- rowsLoaded: data.length
4139
- },
4140
- prepared.storedSymbols
4141
- );
4568
+ return await this._closingAfter(async () => {
4569
+ const prepared = await this._prepare(rows);
4570
+ await this._parseIndexTable({ offset: prepared.offset, limit: prepared.rowsAvailable });
4571
+ const data = this._buildRows(prepared.resolvedByField, 0, prepared.rowsAvailable);
4572
+ return new exports.QvdDataFrame(
4573
+ data,
4574
+ prepared.columns,
4575
+ prepared.metadata,
4576
+ {
4577
+ ...prepared.loadStats,
4578
+ rowsLoaded: data.length
4579
+ },
4580
+ prepared.storedSymbols
4581
+ );
4582
+ });
4142
4583
  }
4143
4584
  /**
4144
4585
  * Reads the file as columns, without ever materialising rows.
@@ -4157,19 +4598,21 @@ var init_QvdFileReader = __esm({
4157
4598
  */
4158
4599
  async loadColumnar(window = null) {
4159
4600
  const rows = normaliseWindow(window, this._path);
4160
- const prepared = await this._prepare(rows, null, true);
4161
- await this._parseIndexTable({ offset: prepared.offset, limit: prepared.rowsAvailable });
4162
- const { QvdColumnTable: QvdColumnTable2 } = await Promise.resolve().then(() => (init_QvdColumnTable(), QvdColumnTable_exports));
4163
- assert3__default.default(this._indexColumns, "The QVD file index table has not been parsed.");
4164
- return new QvdColumnTable2({
4165
- columns: prepared.columns,
4166
- codesByField: this._indexColumns,
4167
- symbolsByField: prepared.resolvedByField,
4168
- halvesByField: prepared.halvesByField,
4169
- rowCount: this._rowsDecoded,
4170
- metadata: prepared.metadata,
4171
- storedSymbols: prepared.storedSymbols,
4172
- loadStats: { ...prepared.loadStats, rowsLoaded: this._rowsDecoded }
4601
+ return await this._closingAfter(async () => {
4602
+ const prepared = await this._prepare(rows, null, true);
4603
+ await this._parseIndexTable({ offset: prepared.offset, limit: prepared.rowsAvailable });
4604
+ const { QvdColumnTable: QvdColumnTable2 } = await Promise.resolve().then(() => (init_QvdColumnTable(), QvdColumnTable_exports));
4605
+ assert4__default.default(this._indexColumns, "The QVD file index table has not been parsed.");
4606
+ return new QvdColumnTable2({
4607
+ columns: prepared.columns,
4608
+ codesByField: this._indexColumns,
4609
+ symbolsByField: prepared.resolvedByField,
4610
+ halvesByField: prepared.halvesByField,
4611
+ rowCount: this._rowsDecoded,
4612
+ metadata: prepared.metadata,
4613
+ storedSymbols: prepared.storedSymbols,
4614
+ loadStats: { ...prepared.loadStats, rowsLoaded: this._rowsDecoded }
4615
+ });
4173
4616
  });
4174
4617
  }
4175
4618
  /**
@@ -4207,28 +4650,38 @@ var init_QvdFileReader = __esm({
4207
4650
  }
4208
4651
  const liveRows = { rows: chunkSize * 2, perChunk: 2 };
4209
4652
  const rows = normaliseWindow(window, this._path);
4210
- const prepared = await this._prepare(rows, liveRows);
4211
- if (prepared.rowsAvailable === 0) {
4212
- this._planIndexTable({ offset: prepared.offset, limit: 0 }, "parseIndexTable");
4213
- return;
4214
- }
4215
- for (let done = 0; done < prepared.rowsAvailable; done += chunkSize) {
4216
- this._throwIfAborted();
4217
- const count = Math.min(chunkSize, prepared.rowsAvailable - done);
4218
- const offset = prepared.offset + done;
4219
- await this._parseIndexTable({ offset, limit: count });
4220
- const data = this._buildRows(prepared.resolvedByField, done, prepared.rowsAvailable);
4221
- yield new exports.QvdDataFrame(
4222
- data,
4223
- prepared.columns,
4224
- prepared.metadata,
4225
- {
4226
- ...prepared.loadStats,
4227
- offset,
4228
- rowsLoaded: data.length
4229
- },
4230
- prepared.storedSymbols
4231
- );
4653
+ this._startRead();
4654
+ let failing = false;
4655
+ try {
4656
+ const prepared = await this._prepare(rows, liveRows);
4657
+ if (prepared.rowsAvailable === 0) {
4658
+ this._planIndexTable({ offset: prepared.offset, limit: 0 }, "parseIndexTable");
4659
+ return;
4660
+ }
4661
+ const codes = prepared.columns.map(() => new Int32Array(Math.min(chunkSize, prepared.rowsAvailable)));
4662
+ for (let done = 0; done < prepared.rowsAvailable; done += chunkSize) {
4663
+ this._throwIfAborted();
4664
+ const count = Math.min(chunkSize, prepared.rowsAvailable - done);
4665
+ const offset = prepared.offset + done;
4666
+ await this._parseIndexTable({ offset, limit: count }, done, prepared.rowsAvailable, codes);
4667
+ const data = this._buildRows(prepared.resolvedByField, done, prepared.rowsAvailable);
4668
+ yield new exports.QvdDataFrame(
4669
+ data,
4670
+ prepared.columns,
4671
+ prepared.metadata,
4672
+ {
4673
+ ...prepared.loadStats,
4674
+ offset,
4675
+ rowsLoaded: data.length
4676
+ },
4677
+ prepared.storedSymbols
4678
+ );
4679
+ }
4680
+ } catch (error) {
4681
+ failing = true;
4682
+ throw error;
4683
+ } finally {
4684
+ await this._endRead(failing);
4232
4685
  }
4233
4686
  }
4234
4687
  /**
@@ -4262,23 +4715,21 @@ var init_QvdFileReader = __esm({
4262
4715
  await this._parseHeader();
4263
4716
  this._emitProgress("header", 1, 1);
4264
4717
  this._throwIfAborted();
4265
- assert3__default.default(this._header, "The QVD file header has not been parsed.");
4718
+ assert4__default.default(this._header, "The QVD file header has not been parsed.");
4266
4719
  const totalRows = headerInteger(this._header["QvdTableHeader"]["NoOfRecords"]);
4267
4720
  const symbolTableLength = headerInteger(this._header["QvdTableHeader"]["Offset"]);
4268
4721
  const resolved = resolveWindow(window, totalRows);
4269
4722
  const rowsAvailable = resolved.limit;
4270
4723
  let symbolsToKeep = null;
4271
4724
  let symbolsKept = null;
4272
- if (window.limit !== null || window.offset > 0) {
4273
- if (symbolTableLength > this._symbolFilteringThreshold) {
4274
- symbolsToKeep = await this._analyzeIndexTableSymbolUsage({ offset: resolved.offset, limit: rowsAvailable });
4275
- symbolsKept = symbolsToKeep.reduce((sum, set) => sum + set.size, 0);
4276
- }
4725
+ if (this._analysisWouldRun(window, resolved, totalRows, symbolTableLength)) {
4726
+ symbolsToKeep = await this._analyzeIndexTableSymbolUsage({ offset: resolved.offset, limit: rowsAvailable });
4727
+ symbolsKept = symbolsToKeep.reduce((sum, set) => sum + set.size, 0);
4277
4728
  }
4278
4729
  await this._parseSymbolTable(symbolsToKeep, rowsAvailable, liveRows);
4279
- assert3__default.default(this._symbolTable, "The QVD file symbol table has not been parsed.");
4730
+ assert4__default.default(this._symbolTable, "The QVD file symbol table has not been parsed.");
4280
4731
  this._throwIfAborted();
4281
- assert3__default.default(this._selectedFields, "The QVD file fields have not been resolved.");
4732
+ assert4__default.default(this._selectedFields, "The QVD file fields have not been resolved.");
4282
4733
  const resolvedByField = [];
4283
4734
  const halvesByField = [];
4284
4735
  const entries = [];
@@ -4339,7 +4790,7 @@ var init_QvdFileReader = __esm({
4339
4790
  * @private
4340
4791
  */
4341
4792
  _buildRows(resolvedByField, progressBase, progressTotal) {
4342
- assert3__default.default(this._indexColumns, "The QVD file index table has not been parsed.");
4793
+ assert4__default.default(this._indexColumns, "The QVD file index table has not been parsed.");
4343
4794
  const indexColumns = this._indexColumns;
4344
4795
  const fieldCount = indexColumns.length;
4345
4796
  const rowCount = this._rowsDecoded;
@@ -5028,12 +5479,16 @@ var init_QvdDataFrame = __esm({
5028
5479
  /**
5029
5480
  * Reads a QVD file in chunks, as an async generator of data frames.
5030
5481
  *
5031
- * The file is opened, read and parsed once; only the index decode and the row building happen
5032
- * per chunk, so what this bounds is row materialisation - the part that actually dominates a
5033
- * large read's heap. It is **not** constant-memory reading of an arbitrarily large file: the
5034
- * symbol table is parsed in full whatever the chunk size, because a stored index in the last
5035
- * chunk can address the first symbol. On a high-cardinality file that table is the bulk of the
5036
- * cost, and `readMetadata` is the only read that avoids it.
5482
+ * The file is opened once and its symbol table parsed once. Each chunk's records are read from the
5483
+ * file when that chunk is built, so what this holds is the symbol table and two chunks of rows,
5484
+ * whatever the size of the file: a 20 GB QVD iterates in the memory its symbol table needs. That
5485
+ * table is still parsed in full whatever the chunk size, because a stored index in the last chunk
5486
+ * can address the first symbol. On a high-cardinality file that table is the bulk of the cost, and
5487
+ * `readMetadata` is the only read that avoids it.
5488
+ *
5489
+ * The file stays open until the iteration ends. Running it to the end closes it, and so do
5490
+ * `break` or a throw inside `for await` and a call to `return()` on the iterator; an iterator
5491
+ * abandoned part-way without any of those holds the file until it is garbage-collected.
5037
5492
  *
5038
5493
  * ```js
5039
5494
  * for await (const chunk of QvdDataFrame.iterate('big.qvd', {chunkSize: 50_000})) {