qvdjs 0.10.1 → 0.11.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 +286 -56
- package/dist/index.cjs +690 -210
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +690 -210
- package/dist/index.js.map +1 -1
- package/img/logo/qvdjs_logo-512.png +0 -0
- package/package.json +5 -2
package/dist/index.cjs
CHANGED
|
@@ -262,6 +262,128 @@ var init_QvdSymbol = __esm({
|
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
264
|
});
|
|
265
|
+
|
|
266
|
+
// src/util/readOptions.js
|
|
267
|
+
function requireRowCount(value, name, filePath) {
|
|
268
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
269
|
+
throw new exports.QvdValidationError(`${name} must be a non-negative integer`, {
|
|
270
|
+
option: name,
|
|
271
|
+
provided: value,
|
|
272
|
+
type: typeof value,
|
|
273
|
+
file: filePath
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return value;
|
|
277
|
+
}
|
|
278
|
+
function normaliseWindow(window, filePath) {
|
|
279
|
+
if (window === null || window === void 0) {
|
|
280
|
+
return { offset: 0, limit: null };
|
|
281
|
+
}
|
|
282
|
+
if (typeof window === "number") {
|
|
283
|
+
return { offset: 0, limit: requireRowCount(window, "maxRows", filePath) };
|
|
284
|
+
}
|
|
285
|
+
if (typeof window !== "object" || Array.isArray(window)) {
|
|
286
|
+
throw new exports.QvdValidationError("The row window must be a number, null, or an {offset, limit} object", {
|
|
287
|
+
provided: window,
|
|
288
|
+
type: typeof window,
|
|
289
|
+
file: filePath
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
const { offset, limit, maxRows } = window;
|
|
293
|
+
const limitGiven = limit !== void 0 && limit !== null;
|
|
294
|
+
const maxRowsGiven = maxRows !== void 0 && maxRows !== null;
|
|
295
|
+
if (limitGiven && maxRowsGiven) {
|
|
296
|
+
throw new exports.QvdValidationError("maxRows and limit are two names for the same option; pass one of them, not both", {
|
|
297
|
+
maxRows,
|
|
298
|
+
limit,
|
|
299
|
+
file: filePath
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
offset: offset === void 0 || offset === null ? 0 : requireRowCount(offset, "offset", filePath),
|
|
304
|
+
limit: limitGiven ? requireRowCount(limit, "limit", filePath) : maxRowsGiven ? requireRowCount(maxRows, "maxRows", filePath) : null
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function resolveWindow(window, totalRows) {
|
|
308
|
+
const rows = Number.isSafeInteger(totalRows) && totalRows > 0 ? totalRows : 0;
|
|
309
|
+
const offset = Math.min(window.offset, rows);
|
|
310
|
+
return {
|
|
311
|
+
offset,
|
|
312
|
+
limit: Math.max(0, Math.min(window.limit === null ? Infinity : window.limit, rows - offset))
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function selectFields(fields, requested, filePath) {
|
|
316
|
+
if (requested === null || requested === void 0) {
|
|
317
|
+
return fields;
|
|
318
|
+
}
|
|
319
|
+
if (!Array.isArray(requested)) {
|
|
320
|
+
throw new exports.QvdValidationError("fields must be an array of field names", {
|
|
321
|
+
provided: requested,
|
|
322
|
+
type: typeof requested,
|
|
323
|
+
file: filePath
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
const available = fields.map((field) => field["FieldName"]);
|
|
327
|
+
if (requested.length === 0) {
|
|
328
|
+
throw new exports.QvdValidationError("fields must name at least one field", {
|
|
329
|
+
availableColumns: available,
|
|
330
|
+
file: filePath
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
const seen = /* @__PURE__ */ new Set();
|
|
334
|
+
return requested.map((name) => {
|
|
335
|
+
if (typeof name !== "string") {
|
|
336
|
+
throw new exports.QvdValidationError("Field names must be strings", {
|
|
337
|
+
provided: name,
|
|
338
|
+
type: typeof name,
|
|
339
|
+
availableColumns: available,
|
|
340
|
+
file: filePath
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
if (seen.has(name)) {
|
|
344
|
+
throw new exports.QvdValidationError(`Field '${name}' is listed twice`, {
|
|
345
|
+
column: name,
|
|
346
|
+
fields: requested,
|
|
347
|
+
file: filePath
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
seen.add(name);
|
|
351
|
+
const index = available.indexOf(name);
|
|
352
|
+
if (index === -1) {
|
|
353
|
+
throw new exports.QvdValidationError(`Column '${name}' does not exist`, {
|
|
354
|
+
column: name,
|
|
355
|
+
availableColumns: available,
|
|
356
|
+
file: filePath
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return fields[index];
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
function readerOptionsFrom(options) {
|
|
363
|
+
return {
|
|
364
|
+
allowedDir: options.allowedDir,
|
|
365
|
+
memorySafetyFactor: options.memorySafetyFactor,
|
|
366
|
+
symbolFilteringThreshold: options.symbolFilteringThreshold,
|
|
367
|
+
fields: options.fields === void 0 ? null : options.fields,
|
|
368
|
+
onProgress: options.onProgress,
|
|
369
|
+
signal: options.signal
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function metadataOptionsFrom(options) {
|
|
373
|
+
return {
|
|
374
|
+
allowedDir: options.allowedDir,
|
|
375
|
+
onProgress: options.onProgress,
|
|
376
|
+
signal: options.signal
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function windowFrom(options) {
|
|
380
|
+
return { offset: options.offset, limit: options.limit, maxRows: options.maxRows };
|
|
381
|
+
}
|
|
382
|
+
var init_readOptions = __esm({
|
|
383
|
+
"src/util/readOptions.js"() {
|
|
384
|
+
init_QvdErrors();
|
|
385
|
+
}
|
|
386
|
+
});
|
|
265
387
|
function isWithinDirectoryLexically(resolvedBaseDir, resolvedPath) {
|
|
266
388
|
const isCaseInsensitiveFS = process.platform === "win32";
|
|
267
389
|
const base = isCaseInsensitiveFS ? resolvedBaseDir.toLowerCase() : resolvedBaseDir;
|
|
@@ -819,11 +941,12 @@ function estimateRowMemory(rows, columnCount) {
|
|
|
819
941
|
}
|
|
820
942
|
return BASE_BYTES + rows * (ROW_BASE_BYTES + PER_CELL_BYTES * columnCount);
|
|
821
943
|
}
|
|
822
|
-
function estimateMemoryUsage(symbolTableSize, maxRows, totalRows, columnCount = 0, materialisesRows = true) {
|
|
944
|
+
function estimateMemoryUsage(symbolTableSize, maxRows, totalRows, columnCount = 0, materialisesRows = true, rowsLive = null) {
|
|
823
945
|
const FULL_PARSE_OVERHEAD = 6;
|
|
824
946
|
const MINIMAL_OVERHEAD = 0.01;
|
|
825
947
|
const rowsToLoad = maxRows === null || maxRows >= totalRows ? totalRows : maxRows;
|
|
826
|
-
const
|
|
948
|
+
const liveRows = rowsLive === null ? rowsToLoad : Math.min(rowsLive, rowsToLoad);
|
|
949
|
+
const rowMemory = materialisesRows ? estimateRowMemory(liveRows, columnCount) : BASE_BYTES;
|
|
827
950
|
if (maxRows === null || maxRows >= totalRows) {
|
|
828
951
|
return symbolTableSize * FULL_PARSE_OVERHEAD + rowMemory;
|
|
829
952
|
}
|
|
@@ -833,18 +956,44 @@ function estimateMemoryUsage(symbolTableSize, maxRows, totalRows, columnCount =
|
|
|
833
956
|
const skippedSymbolsMemory = symbolTableSize * (1 - symbolPercentage) * MINIMAL_OVERHEAD;
|
|
834
957
|
return keptSymbolsMemory + skippedSymbolsMemory + rowMemory;
|
|
835
958
|
}
|
|
836
|
-
function recommendedRowsFor(budget, symbolTableSize, totalRows, columnCount, materialisesRows = true) {
|
|
837
|
-
|
|
959
|
+
function recommendedRowsFor(budget, symbolTableSize, totalRows, columnCount, materialisesRows = true, includeExternal = false) {
|
|
960
|
+
const costOf = (rows) => estimateMemoryUsage(symbolTableSize, rows, totalRows, columnCount, materialisesRows) + (includeExternal ? estimateExternalMemory(Math.min(rows, totalRows), columnCount) : 0);
|
|
961
|
+
if (costOf(totalRows) <= budget) {
|
|
838
962
|
return totalRows;
|
|
839
963
|
}
|
|
840
|
-
if (
|
|
964
|
+
if (costOf(0) > budget) {
|
|
841
965
|
return 0;
|
|
842
966
|
}
|
|
843
967
|
let low = 0;
|
|
844
968
|
let high = totalRows;
|
|
845
969
|
while (high - low > 1) {
|
|
846
970
|
const mid = Math.floor((low + high) / 2);
|
|
847
|
-
if (
|
|
971
|
+
if (costOf(mid) <= budget) {
|
|
972
|
+
low = mid;
|
|
973
|
+
} else {
|
|
974
|
+
high = mid;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return low;
|
|
978
|
+
}
|
|
979
|
+
function recommendedChunkFor(budget, symbolTableSize, windowRows, totalRows, columnCount, liveRowsPerChunk = 1, includeExternal = false) {
|
|
980
|
+
const covered = windowRows === null || windowRows >= totalRows ? totalRows : windowRows;
|
|
981
|
+
const fits = (chunk) => {
|
|
982
|
+
const live = Math.min(chunk * liveRowsPerChunk, covered);
|
|
983
|
+
const cost = estimateMemoryUsage(symbolTableSize, windowRows, totalRows, columnCount, true, chunk * liveRowsPerChunk) + (includeExternal ? estimateExternalMemory(live, columnCount) : 0);
|
|
984
|
+
return cost <= budget;
|
|
985
|
+
};
|
|
986
|
+
if (fits(covered)) {
|
|
987
|
+
return covered;
|
|
988
|
+
}
|
|
989
|
+
if (!fits(1)) {
|
|
990
|
+
return 0;
|
|
991
|
+
}
|
|
992
|
+
let low = 1;
|
|
993
|
+
let high = covered;
|
|
994
|
+
while (high - low > 1) {
|
|
995
|
+
const mid = Math.floor((low + high) / 2);
|
|
996
|
+
if (fits(mid)) {
|
|
848
997
|
low = mid;
|
|
849
998
|
} else {
|
|
850
999
|
high = mid;
|
|
@@ -852,7 +1001,7 @@ function recommendedRowsFor(budget, symbolTableSize, totalRows, columnCount, mat
|
|
|
852
1001
|
}
|
|
853
1002
|
return low;
|
|
854
1003
|
}
|
|
855
|
-
function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.8, columnCount = 0, materialisesRows = true) {
|
|
1004
|
+
function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.8, columnCount = 0, materialisesRows = true, live = null) {
|
|
856
1005
|
if (typeof safetyFactor !== "number" || safetyFactor < 0 || safetyFactor > 1) {
|
|
857
1006
|
throw new exports.QvdValidationError("safetyFactor must be a number between 0.0 and 1.0", { safetyFactor });
|
|
858
1007
|
}
|
|
@@ -861,12 +1010,16 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
861
1010
|
}
|
|
862
1011
|
const budget = getMemoryBudget();
|
|
863
1012
|
const rowsToLoad = maxRows === null || maxRows >= totalRows ? totalRows : maxRows;
|
|
864
|
-
const
|
|
865
|
-
const
|
|
1013
|
+
const rowsLive = live === null ? null : live.rows;
|
|
1014
|
+
const liveRowsPerChunk = live === null ? 1 : live.perChunk;
|
|
1015
|
+
const liveRows = rowsLive === null ? rowsToLoad : Math.min(rowsLive, rowsToLoad);
|
|
1016
|
+
const heapMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows, columnCount, materialisesRows, rowsLive);
|
|
1017
|
+
const externalMemory = estimateExternalMemory(liveRows, columnCount);
|
|
866
1018
|
const bounded = budget.candidates.map((candidate) => {
|
|
867
1019
|
const heapOnly = candidate.source === "V8 heap limit";
|
|
868
1020
|
return {
|
|
869
1021
|
...candidate,
|
|
1022
|
+
heapOnly,
|
|
870
1023
|
needs: heapOnly ? heapMemory : heapMemory + externalMemory,
|
|
871
1024
|
allowed: candidate.bytes * safetyFactor,
|
|
872
1025
|
bounds: heapOnly ? "the V8 heap" : "the whole process"
|
|
@@ -882,12 +1035,14 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
882
1035
|
const estimatedMemory = binding ? binding.needs : heapMemory;
|
|
883
1036
|
const maxAllowedMemory = binding ? binding.allowed : budget.bytes * safetyFactor;
|
|
884
1037
|
if (binding) {
|
|
1038
|
+
const includeExternal = !binding.heapOnly;
|
|
885
1039
|
const recommendedMaxRows = recommendedRowsFor(
|
|
886
1040
|
maxAllowedMemory,
|
|
887
1041
|
symbolTableSize,
|
|
888
1042
|
totalRows,
|
|
889
1043
|
columnCount,
|
|
890
|
-
materialisesRows
|
|
1044
|
+
materialisesRows,
|
|
1045
|
+
includeExternal
|
|
891
1046
|
);
|
|
892
1047
|
const sizeMB = Math.round(symbolTableSize / 1024 / 1024);
|
|
893
1048
|
const estimatedMB = Math.round(estimatedMemory / 1024 / 1024);
|
|
@@ -899,15 +1054,27 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
899
1054
|
const limitingScope = binding.bounds;
|
|
900
1055
|
const budgetBreakdown = budget.candidates.map((candidate) => `${candidate.source} ${Math.round(candidate.bytes / 1024 / 1024)}MB`).join(", ");
|
|
901
1056
|
const observedBreakdown = budget.observed.map((entry) => `${entry.source} ${Math.round(entry.bytes / 1024 / 1024)}MB`).join(", ");
|
|
902
|
-
const nothingFits = recommendedMaxRows === 0;
|
|
903
1057
|
const containerBound = binding.source === "container memory limit";
|
|
1058
|
+
const chunked = rowsLive !== null;
|
|
1059
|
+
const recommendedChunk = chunked ? recommendedChunkFor(
|
|
1060
|
+
maxAllowedMemory,
|
|
1061
|
+
symbolTableSize,
|
|
1062
|
+
maxRows,
|
|
1063
|
+
totalRows,
|
|
1064
|
+
columnCount,
|
|
1065
|
+
liveRowsPerChunk,
|
|
1066
|
+
includeExternal
|
|
1067
|
+
) : 0;
|
|
1068
|
+
const knob = chunked ? "chunkSize" : "limit";
|
|
1069
|
+
const recommendedValue = chunked ? recommendedChunk : recommendedMaxRows;
|
|
1070
|
+
const nothingFits = recommendedValue === 0;
|
|
904
1071
|
let advice;
|
|
905
1072
|
if (nothingFits) {
|
|
906
|
-
advice = `No row count fits this budget - the symbol table alone exceeds it, so
|
|
1073
|
+
advice = `No row count fits this budget - the symbol table alone exceeds it, so ${knob} cannot help. ` + (containerBound ? `Raise the container's memory limit.` : `Raise the heap with --max-old-space-size, or raise memorySafetyFactor.`);
|
|
907
1074
|
} else if (containerBound) {
|
|
908
|
-
advice = `The binding limit is the container's, so raising --max-old-space-size would let V8 grow past it and be killed by the OOM killer instead. Set it below the container limit, raise the limit, or
|
|
1075
|
+
advice = `The binding limit is the container's, so raising --max-old-space-size would let V8 grow past it and be killed by the OOM killer instead. Set it below the container limit, raise the limit, or hold fewer rows with ${knob} (recommended: ${recommendedValue.toLocaleString()} rows or less).`;
|
|
909
1076
|
} else {
|
|
910
|
-
advice = `Try
|
|
1077
|
+
advice = `Try holding fewer rows using the ${knob} parameter (recommended: ${recommendedValue.toLocaleString()} rows or less), or raise the heap with --max-old-space-size.`;
|
|
911
1078
|
}
|
|
912
1079
|
throw new exports.QvdValidationError(
|
|
913
1080
|
`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,
|
|
@@ -927,22 +1094,30 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
927
1094
|
columnCount,
|
|
928
1095
|
totalRows,
|
|
929
1096
|
maxRows,
|
|
930
|
-
recommendedMaxRows
|
|
1097
|
+
recommendedMaxRows,
|
|
1098
|
+
// Only present when a chunk size is what overflowed, so a caller cannot mistake one
|
|
1099
|
+
// recommendation for the other.
|
|
1100
|
+
...chunked ? { rowsLive, recommendedChunkSize: recommendedChunk } : {}
|
|
931
1101
|
}
|
|
932
1102
|
);
|
|
933
1103
|
}
|
|
934
1104
|
}
|
|
935
1105
|
function warnLargeSymbolTable(symbolTableSize, maxRows, totalRows, columnCount = 0, materialisesRows = true) {
|
|
936
1106
|
const LARGE_SYMBOL_TABLE_WARNING = usableOldSpaceLimit() * 0.125;
|
|
937
|
-
if (symbolTableSize
|
|
938
|
-
|
|
939
|
-
const estimatedMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows, columnCount, materialisesRows);
|
|
940
|
-
const estimatedMB = Math.round(estimatedMemory / 1024 / 1024);
|
|
941
|
-
const warnMB = Math.round(LARGE_SYMBOL_TABLE_WARNING / 1024 / 1024);
|
|
942
|
-
console.warn(
|
|
943
|
-
`\u26A0\uFE0F Large symbol table detected (${sizeMB}MB > ${warnMB}MB threshold). Loading all ${totalRows.toLocaleString()} rows will use ~${estimatedMB}MB RAM. Consider using the maxRows parameter for better performance and lower memory usage.`
|
|
944
|
-
);
|
|
1107
|
+
if (symbolTableSize <= LARGE_SYMBOL_TABLE_WARNING) {
|
|
1108
|
+
return;
|
|
945
1109
|
}
|
|
1110
|
+
const rowsToLoad = maxRows === null || maxRows >= totalRows ? totalRows : maxRows;
|
|
1111
|
+
const estimatedMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows, columnCount, materialisesRows);
|
|
1112
|
+
if (estimatedMemory <= LARGE_SYMBOL_TABLE_WARNING) {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
const sizeMB = Math.round(symbolTableSize / 1024 / 1024);
|
|
1116
|
+
const estimatedMB = Math.round(estimatedMemory / 1024 / 1024);
|
|
1117
|
+
const warnMB = Math.round(LARGE_SYMBOL_TABLE_WARNING / 1024 / 1024);
|
|
1118
|
+
console.warn(
|
|
1119
|
+
`\u26A0\uFE0F Large symbol table detected (${sizeMB}MB > ${warnMB}MB threshold). This read materialises ${rowsToLoad.toLocaleString()} of ${totalRows.toLocaleString()} 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.`
|
|
1120
|
+
);
|
|
946
1121
|
}
|
|
947
1122
|
var HEAP_LIMIT_OVERSTATEMENT_BYTES, MINIMUM_BUDGET_BYTES, BASE_BYTES, ROW_BASE_BYTES, PER_CELL_BYTES;
|
|
948
1123
|
var init_memoryUtils = __esm({
|
|
@@ -983,7 +1158,7 @@ function validateSymbolTableSizeEarly(symbolTableLength, filePath) {
|
|
|
983
1158
|
const maxMB = Math.round(MAX_SYMBOL_TABLE_SIZE / 1024 / 1024);
|
|
984
1159
|
const heapMB = Math.round(heapLimit / 1024 / 1024);
|
|
985
1160
|
throw new exports.QvdValidationError(
|
|
986
|
-
`Symbol table too large (${sizeMB}MB exceeds ${maxMB}MB limit for lazy loading). This QVD file contains extremely high-cardinality fields. Limit scales with heap size (current: ${heapMB}MB, limit: 12.5% = ${maxMB}MB). Consider: (1) loading the full file without maxRows, (2) increasing heap size with --max-old-space-size, or (3) aggregating high-cardinality fields.`,
|
|
1161
|
+
`Symbol table too large (${sizeMB}MB exceeds ${maxMB}MB limit for lazy loading). This QVD file contains extremely high-cardinality fields. Limit scales with heap size (current: ${heapMB}MB, limit: 12.5% = ${maxMB}MB). Consider: (1) loading the full file without a row window - maxRows, limit or offset - since the symbol table is read in full either way, (2) increasing heap size with --max-old-space-size, or (3) aggregating high-cardinality fields.`,
|
|
987
1162
|
{
|
|
988
1163
|
file: filePath,
|
|
989
1164
|
symbolTableSize: symbolTableLength,
|
|
@@ -1057,7 +1232,7 @@ function validateRecordCount(totalRows, filePath, stage = "parseIndexTable") {
|
|
|
1057
1232
|
});
|
|
1058
1233
|
}
|
|
1059
1234
|
}
|
|
1060
|
-
function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, indexTableOffset, bufferLength, rowsToLoad, filePath, fileSize = null) {
|
|
1235
|
+
function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, indexTableOffset, bufferLength, rowsToLoad, filePath, fileSize = null, windowFirstRow = 0, bufferFirstRow = 0) {
|
|
1061
1236
|
if (isNaN(recordSize) || !Number.isSafeInteger(recordSize) || recordSize < 0) {
|
|
1062
1237
|
throw new exports.QvdCorruptedError("Invalid record byte size", {
|
|
1063
1238
|
recordSize,
|
|
@@ -1119,23 +1294,28 @@ function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, ind
|
|
|
1119
1294
|
}
|
|
1120
1295
|
}
|
|
1121
1296
|
const requiredIndexBytes = rowsToLoad * recordSize;
|
|
1122
|
-
|
|
1297
|
+
const bufferRecordStart = (windowFirstRow - bufferFirstRow) * recordSize;
|
|
1298
|
+
if (indexTableOffset + bufferRecordStart + requiredIndexBytes > bufferLength) {
|
|
1123
1299
|
throw new exports.QvdCorruptedError("Index table truncated", {
|
|
1124
1300
|
indexTableOffset,
|
|
1125
1301
|
requiredBytes: requiredIndexBytes,
|
|
1126
|
-
availableBytes: Math.max(0, bufferLength - indexTableOffset),
|
|
1302
|
+
availableBytes: Math.max(0, bufferLength - indexTableOffset - bufferRecordStart),
|
|
1127
1303
|
rowsToLoad,
|
|
1304
|
+
windowFirstRow,
|
|
1305
|
+
bufferFirstRow,
|
|
1128
1306
|
recordSize,
|
|
1129
1307
|
bufferSize: bufferLength,
|
|
1130
1308
|
file: filePath,
|
|
1131
1309
|
stage: "parseIndexTable"
|
|
1132
1310
|
});
|
|
1133
1311
|
}
|
|
1134
|
-
|
|
1312
|
+
const requiredTableBytes = (windowFirstRow + rowsToLoad) * recordSize;
|
|
1313
|
+
if (indexTableLength < requiredTableBytes) {
|
|
1135
1314
|
throw new exports.QvdCorruptedError("Index table length smaller than required", {
|
|
1136
1315
|
indexTableLength,
|
|
1137
|
-
requiredBytes:
|
|
1316
|
+
requiredBytes: requiredTableBytes,
|
|
1138
1317
|
rowsToLoad,
|
|
1318
|
+
windowFirstRow,
|
|
1139
1319
|
recordSize,
|
|
1140
1320
|
file: filePath,
|
|
1141
1321
|
stage: "parseIndexTable"
|
|
@@ -1465,6 +1645,7 @@ exports.QvdColumn = void 0; exports.QvdColumnTable = void 0;
|
|
|
1465
1645
|
var init_QvdColumnTable = __esm({
|
|
1466
1646
|
"src/QvdColumnTable.js"() {
|
|
1467
1647
|
init_QvdErrors();
|
|
1648
|
+
init_readOptions();
|
|
1468
1649
|
exports.QvdColumn = class {
|
|
1469
1650
|
/**
|
|
1470
1651
|
* @param {string} name The field name.
|
|
@@ -1659,9 +1840,21 @@ var init_QvdColumnTable = __esm({
|
|
|
1659
1840
|
/**
|
|
1660
1841
|
* Reads a QVD file as columns.
|
|
1661
1842
|
*
|
|
1843
|
+
* Takes the same options as `QvdDataFrame.fromQvd`, with the same meanings - one option
|
|
1844
|
+
* vocabulary for both read paths, because they are two answers about the same file rather than
|
|
1845
|
+
* two features. `{offset, limit}` is how a caller pages through a file columnwise; there is no
|
|
1846
|
+
* columnar `iterate()` because there is nothing for it to bound - a columnar read materialises
|
|
1847
|
+
* no rows, which is the memory chunking exists to cap.
|
|
1848
|
+
*
|
|
1662
1849
|
* @param {string} path The path to the QVD file.
|
|
1663
1850
|
* @param {Object} [options] Loading options, with the same meanings they have on `fromQvd`.
|
|
1664
|
-
* @param {number|null} [options.maxRows]
|
|
1851
|
+
* @param {number|null} [options.maxRows] Rows to decode. The older name for `limit`.
|
|
1852
|
+
* @param {number|null} [options.limit] Rows to decode, counting from `offset`.
|
|
1853
|
+
* @param {number} [options.offset] File row to start at.
|
|
1854
|
+
* @param {Array<string>|null} [options.fields] Field names to read, in the order they should
|
|
1855
|
+
* appear. Unselected fields have their symbols skipped entirely.
|
|
1856
|
+
* @param {Function} [options.onProgress] Progress callback, `{stage, current, total, percent}`.
|
|
1857
|
+
* @param {AbortSignal} [options.signal] Cancels the read.
|
|
1665
1858
|
* @param {string} [options.allowedDir] Directory the path must resolve inside.
|
|
1666
1859
|
* @param {number} [options.memorySafetyFactor] Fraction of the memory budget a load may use.
|
|
1667
1860
|
* @param {number} [options.symbolFilteringThreshold] Symbol table size above which a limited
|
|
@@ -1671,15 +1864,13 @@ var init_QvdColumnTable = __esm({
|
|
|
1671
1864
|
static async fromQvd(path3, options = {}) {
|
|
1672
1865
|
const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
|
|
1673
1866
|
const reader = new QvdFileReader2(path3, {
|
|
1674
|
-
|
|
1675
|
-
memorySafetyFactor: options.memorySafetyFactor,
|
|
1676
|
-
symbolFilteringThreshold: options.symbolFilteringThreshold,
|
|
1867
|
+
...readerOptionsFrom(options),
|
|
1677
1868
|
// This read builds no rows, so the memory guard must not charge it for them. A columnar
|
|
1678
1869
|
// read of the 38MB taxi fixture completes in a 15MB heap; charged the row cost it was
|
|
1679
1870
|
// refused below a 2GB one.
|
|
1680
1871
|
materialisesRows: false
|
|
1681
1872
|
});
|
|
1682
|
-
return await reader.loadColumnar(options
|
|
1873
|
+
return await reader.loadColumnar(windowFrom(options));
|
|
1683
1874
|
}
|
|
1684
1875
|
/** @return {Array<string>} Field names, in file order. */
|
|
1685
1876
|
get columns() {
|
|
@@ -1727,7 +1918,7 @@ var QvdFileReader_exports = {};
|
|
|
1727
1918
|
__export(QvdFileReader_exports, {
|
|
1728
1919
|
QvdFileReader: () => exports.QvdFileReader
|
|
1729
1920
|
});
|
|
1730
|
-
var MAX_HEADER_SIZE, READ_CHUNK_SIZE; exports.QvdFileReader = void 0;
|
|
1921
|
+
var MAX_HEADER_SIZE, READ_CHUNK_SIZE, ANALYSIS_SLICE_ROWS; exports.QvdFileReader = void 0;
|
|
1731
1922
|
var init_QvdFileReader = __esm({
|
|
1732
1923
|
"src/QvdFileReader.js"() {
|
|
1733
1924
|
init_QvdDataFrame();
|
|
@@ -1737,8 +1928,10 @@ var init_QvdFileReader = __esm({
|
|
|
1737
1928
|
init_memoryUtils();
|
|
1738
1929
|
init_validationUtils();
|
|
1739
1930
|
init_symbolParser();
|
|
1931
|
+
init_readOptions();
|
|
1740
1932
|
MAX_HEADER_SIZE = 16 * 1024 * 1024;
|
|
1741
1933
|
READ_CHUNK_SIZE = 512 * 1024 * 1024;
|
|
1934
|
+
ANALYSIS_SLICE_ROWS = 65536;
|
|
1742
1935
|
exports.QvdFileReader = class {
|
|
1743
1936
|
/**
|
|
1744
1937
|
* Constructs a new QVD file parser.
|
|
@@ -1750,9 +1943,10 @@ var init_QvdFileReader = __esm({
|
|
|
1750
1943
|
* points outside it is rejected. Defaults to the current working directory. To permit
|
|
1751
1944
|
* an entire volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or
|
|
1752
1945
|
* empty value falls back to the working directory rather than removing the restriction.
|
|
1753
|
-
* @param {number} [options.memorySafetyFactor=0.
|
|
1754
|
-
* load may use. The budget is the
|
|
1755
|
-
*
|
|
1946
|
+
* @param {number} [options.memorySafetyFactor=0.8] Fraction (0.0-1.0) of the memory budget a
|
|
1947
|
+
* load may use. The budget is the smaller of the V8 heap limit and any container memory limit;
|
|
1948
|
+
* what the OS reports as available is recorded for diagnostics and deliberately not allowed to
|
|
1949
|
+
* bind - see `getMemoryBudget`. Default is 0.8. **Zero disables the memory
|
|
1756
1950
|
* check entirely**, which is the escape hatch for runtimes whose limits cannot be measured -
|
|
1757
1951
|
* Bun reports its current heap as its heap limit - and for callers who would rather manage
|
|
1758
1952
|
* memory themselves than trust the estimate.
|
|
@@ -1763,29 +1957,93 @@ var init_QvdFileReader = __esm({
|
|
|
1763
1957
|
* above which a lazy load switches to the two-pass filtering path. The default of 50MB is
|
|
1764
1958
|
* the point where the extra analysis pass pays for itself; lower it to use filtering on
|
|
1765
1959
|
* smaller files, raise it to keep the simpler single-pass read for longer.
|
|
1960
|
+
* @param {Array<string>|null} [options.fields] Field names to read, in the order they should
|
|
1961
|
+
* appear. Null reads every field, in file order. An unknown or repeated name is refused.
|
|
1962
|
+
* @param {Function} [options.onProgress] Called with `{stage, current, total, percent}` as the
|
|
1963
|
+
* read proceeds - the same shape `QvdFileWriter` emits.
|
|
1964
|
+
* @param {AbortSignal} [options.signal] Cancels the read. When it is aborted the read throws
|
|
1965
|
+
* `signal.reason`, exactly as `signal.throwIfAborted()` does.
|
|
1766
1966
|
*/
|
|
1767
1967
|
constructor(filePath, options = {}) {
|
|
1768
1968
|
const {
|
|
1769
1969
|
allowedDir,
|
|
1770
1970
|
memorySafetyFactor = 0.8,
|
|
1771
1971
|
symbolFilteringThreshold = 50 * 1024 * 1024,
|
|
1772
|
-
materialisesRows = true
|
|
1972
|
+
materialisesRows = true,
|
|
1973
|
+
fields = null,
|
|
1974
|
+
onProgress,
|
|
1975
|
+
signal
|
|
1773
1976
|
} = options;
|
|
1774
1977
|
this._materialisesRows = materialisesRows;
|
|
1775
1978
|
this._path = validatePath(filePath, allowedDir);
|
|
1776
1979
|
this._memorySafetyFactor = memorySafetyFactor;
|
|
1777
1980
|
this._symbolFilteringThreshold = symbolFilteringThreshold;
|
|
1981
|
+
if (onProgress !== void 0 && typeof onProgress !== "function") {
|
|
1982
|
+
throw new exports.QvdValidationError("onProgress must be a function", {
|
|
1983
|
+
provided: onProgress,
|
|
1984
|
+
type: typeof onProgress,
|
|
1985
|
+
file: this._path
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
if (signal !== void 0 && (typeof signal !== "object" || signal === null || typeof signal.aborted !== "boolean")) {
|
|
1989
|
+
throw new exports.QvdValidationError("signal must be an AbortSignal", {
|
|
1990
|
+
provided: signal,
|
|
1991
|
+
type: typeof signal,
|
|
1992
|
+
file: this._path
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
this._requestedFields = fields === void 0 ? null : fields;
|
|
1996
|
+
this._onProgress = onProgress;
|
|
1997
|
+
this._signal = signal;
|
|
1778
1998
|
this._buffer = null;
|
|
1779
1999
|
this._headerOffset = null;
|
|
1780
2000
|
this._symbolTableOffset = null;
|
|
1781
2001
|
this._indexTableOffset = null;
|
|
1782
2002
|
this._header = null;
|
|
2003
|
+
this._allFields = null;
|
|
2004
|
+
this._selectedFields = null;
|
|
2005
|
+
this._fieldBitMetadataValidated = false;
|
|
1783
2006
|
this._symbolTable = null;
|
|
1784
2007
|
this._indexColumns = null;
|
|
1785
2008
|
this._rowsDecoded = 0;
|
|
2009
|
+
this._bufferFirstRow = 0;
|
|
1786
2010
|
this._fileSize = null;
|
|
1787
2011
|
this._headerMatchesFile = false;
|
|
1788
2012
|
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Emits a progress event if a callback is registered.
|
|
2015
|
+
*
|
|
2016
|
+
* The same shape `QvdFileWriter._emitProgress` emits, deliberately: a caller who has written a
|
|
2017
|
+
* progress bar for a write should not have to write a second one for a read. The stage names
|
|
2018
|
+
* differ because the stages differ, but `symbol-table` and `index-table` mean the same thing on
|
|
2019
|
+
* both sides.
|
|
2020
|
+
*
|
|
2021
|
+
* @param {string} stage The current stage of the read.
|
|
2022
|
+
* @param {number} current The current progress value.
|
|
2023
|
+
* @param {number} total The total progress value.
|
|
2024
|
+
* @private
|
|
2025
|
+
*/
|
|
2026
|
+
_emitProgress(stage, current, total) {
|
|
2027
|
+
if (this._onProgress) {
|
|
2028
|
+
const percent = total > 0 ? Math.round(current / total * 100) : 100;
|
|
2029
|
+
this._onProgress({ stage, current, total, percent });
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Throws if the caller has cancelled the read.
|
|
2034
|
+
*
|
|
2035
|
+
* Throws `signal.reason` - a `DOMException` named `AbortError` unless the caller aborted with a
|
|
2036
|
+
* reason of their own. That is what `AbortSignal` means everywhere else in Node, and inventing
|
|
2037
|
+
* a `QvdAbortError` here would make this library's cancellation the one a caller has to special
|
|
2038
|
+
* case.
|
|
2039
|
+
*
|
|
2040
|
+
* @private
|
|
2041
|
+
*/
|
|
2042
|
+
_throwIfAborted() {
|
|
2043
|
+
if (this._signal) {
|
|
2044
|
+
this._signal.throwIfAborted();
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
1789
2047
|
/**
|
|
1790
2048
|
* Reads the binary data of the QVD file.
|
|
1791
2049
|
*
|
|
@@ -1810,14 +2068,23 @@ var init_QvdFileReader = __esm({
|
|
|
1810
2068
|
* - Streaming for header finding is efficient for unknown header sizes
|
|
1811
2069
|
* - Direct byte-range reading for remaining data is fastest
|
|
1812
2070
|
*
|
|
1813
|
-
*
|
|
2071
|
+
* A window with a non-zero `offset` reads two ranges rather than one: the header and symbol
|
|
2072
|
+
* table from the front of the file, and the window's records from wherever they sit. The bytes
|
|
2073
|
+
* between are never read, which is what makes `{offset: 1_700_000, limit: 100}` on the taxi
|
|
2074
|
+
* fixture a 0.4MB read rather than a 38MB one.
|
|
2075
|
+
*
|
|
2076
|
+
* @param {QvdRowWindow} window The rows to read.
|
|
1814
2077
|
* @param {boolean} [headerOnly=false] Stop once the XML header has been read, leaving the
|
|
1815
2078
|
* symbol and index tables on disk. This is the metadata-only path: the header is a few
|
|
1816
2079
|
* kilobytes whatever the file's size, so reading a schema costs the same for a 40MB file as
|
|
1817
2080
|
* for a 40GB one.
|
|
2081
|
+
* @param {{rows: number, perChunk: number}|null} [liveRows=null] Rows held at one instant when
|
|
2082
|
+
* that is fewer than the window covers - see `_prepare`.
|
|
1818
2083
|
* @private
|
|
1819
2084
|
*/
|
|
1820
|
-
async _readData(
|
|
2085
|
+
async _readData(window = { offset: 0, limit: null }, headerOnly = false, liveRows = null) {
|
|
2086
|
+
this._throwIfAborted();
|
|
2087
|
+
this._emitProgress("read", 0, 1);
|
|
1821
2088
|
const HEADER_DELIMITER = "\r\n\0";
|
|
1822
2089
|
const CHUNK_SIZE = 64 * 1024;
|
|
1823
2090
|
const stream = fs__default.default.createReadStream(this._path, {
|
|
@@ -1887,13 +2154,14 @@ var init_QvdFileReader = __esm({
|
|
|
1887
2154
|
const totalRows = parseInt(headerObj["QvdTableHeader"]["NoOfRecords"], 10);
|
|
1888
2155
|
if (headerOnly) {
|
|
1889
2156
|
this._buffer = headerBuffer.subarray(0, headerEndIndex);
|
|
2157
|
+
this._emitProgress("read", 1, 1);
|
|
1890
2158
|
return;
|
|
1891
2159
|
}
|
|
1892
2160
|
let headerFields = headerObj["QvdTableHeader"]?.["Fields"]?.["QvdFieldHeader"];
|
|
1893
2161
|
if (headerFields && !Array.isArray(headerFields)) {
|
|
1894
2162
|
headerFields = [headerFields];
|
|
1895
2163
|
}
|
|
1896
|
-
const columnCount = Array.isArray(headerFields) ? headerFields.length : 0;
|
|
2164
|
+
const columnCount = Array.isArray(headerFields) ? selectFields(headerFields, this._requestedFields, this._path).length : 0;
|
|
1897
2165
|
const headerNumbersUsable = [symbolTableLength, recordSize, totalRows].every(
|
|
1898
2166
|
(value) => Number.isSafeInteger(value) && value >= 0
|
|
1899
2167
|
);
|
|
@@ -1902,23 +2170,28 @@ var init_QvdFileReader = __esm({
|
|
|
1902
2170
|
this._fileSize = fileSize;
|
|
1903
2171
|
this._headerMatchesFile = headerEndIndex + symbolTableLength + totalRows * recordSize <= fileSize;
|
|
1904
2172
|
}
|
|
2173
|
+
const resolved = headerNumbersUsable ? resolveWindow(window, totalRows) : { offset: 0, limit: 0 };
|
|
2174
|
+
const windowRows = resolved.limit;
|
|
1905
2175
|
if (headerNumbersUsable && this._headerMatchesFile) {
|
|
1906
2176
|
validateMemoryAvailability(
|
|
1907
2177
|
symbolTableLength,
|
|
1908
|
-
|
|
2178
|
+
windowRows,
|
|
1909
2179
|
totalRows,
|
|
1910
2180
|
this._path,
|
|
1911
2181
|
this._memorySafetyFactor,
|
|
1912
2182
|
columnCount,
|
|
1913
|
-
this._materialisesRows
|
|
2183
|
+
this._materialisesRows,
|
|
2184
|
+
liveRows
|
|
1914
2185
|
);
|
|
1915
2186
|
}
|
|
1916
|
-
if (
|
|
2187
|
+
if (window.offset === 0 && window.limit === null) {
|
|
1917
2188
|
this._buffer = await fs__default.default.promises.readFile(this._path);
|
|
1918
2189
|
this._fileSize = this._buffer.length;
|
|
2190
|
+
this._bufferFirstRow = 0;
|
|
2191
|
+
this._emitProgress("read", 1, 1);
|
|
1919
2192
|
return;
|
|
1920
2193
|
}
|
|
1921
|
-
const rowsToLoad =
|
|
2194
|
+
const rowsToLoad = windowRows;
|
|
1922
2195
|
validateSymbolTableSizeEarly(symbolTableLength, this._path);
|
|
1923
2196
|
for (const [name, value] of [
|
|
1924
2197
|
["Offset", symbolTableLength],
|
|
@@ -1934,39 +2207,78 @@ var init_QvdFileReader = __esm({
|
|
|
1934
2207
|
});
|
|
1935
2208
|
}
|
|
1936
2209
|
}
|
|
2210
|
+
const skippedIndexBytes = resolved.offset * recordSize;
|
|
1937
2211
|
const indexTableBytesToRead = rowsToLoad * recordSize;
|
|
1938
2212
|
const totalBytesToRead = indexTableOffset + indexTableBytesToRead;
|
|
2213
|
+
const fileBytesRequired = indexTableOffset + skippedIndexBytes + indexTableBytesToRead;
|
|
1939
2214
|
const fd = await fs__default.default.promises.open(this._path, "r");
|
|
1940
2215
|
try {
|
|
1941
2216
|
const { size: fileSize } = await fd.stat();
|
|
1942
2217
|
this._fileSize = fileSize;
|
|
1943
|
-
if (
|
|
2218
|
+
if (fileBytesRequired > fileSize) {
|
|
1944
2219
|
throw new exports.QvdCorruptedError("The file is shorter than its header claims.", {
|
|
1945
2220
|
file: this._path,
|
|
1946
2221
|
fileSize,
|
|
1947
|
-
requiredBytes:
|
|
2222
|
+
requiredBytes: fileBytesRequired,
|
|
1948
2223
|
stage: "readData"
|
|
1949
2224
|
});
|
|
1950
2225
|
}
|
|
1951
2226
|
this._buffer = Buffer.alloc(totalBytesToRead);
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
stage: "readData"
|
|
1963
|
-
});
|
|
1964
|
-
}
|
|
1965
|
-
position += bytesRead;
|
|
2227
|
+
await this._readRange(fd, 0, indexTableOffset, 0, fileSize, totalBytesToRead);
|
|
2228
|
+
if (indexTableBytesToRead > 0) {
|
|
2229
|
+
await this._readRange(
|
|
2230
|
+
fd,
|
|
2231
|
+
indexTableOffset,
|
|
2232
|
+
indexTableBytesToRead,
|
|
2233
|
+
indexTableOffset + skippedIndexBytes,
|
|
2234
|
+
fileSize,
|
|
2235
|
+
fileBytesRequired
|
|
2236
|
+
);
|
|
1966
2237
|
}
|
|
2238
|
+
this._bufferFirstRow = resolved.offset;
|
|
1967
2239
|
} finally {
|
|
1968
2240
|
await fd.close();
|
|
1969
2241
|
}
|
|
2242
|
+
this._emitProgress("read", 1, 1);
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* Reads one byte range of the file into the buffer.
|
|
2246
|
+
*
|
|
2247
|
+
* Read in bounded chunks, checking bytesRead each time. A single fs.read call with a length of
|
|
2248
|
+
* 2^31 or more does not throw - it trips a C++ assertion and aborts the whole process, which no
|
|
2249
|
+
* try/catch can intercept.
|
|
2250
|
+
*
|
|
2251
|
+
* @param {import('fs/promises').FileHandle} fd The open file.
|
|
2252
|
+
* @param {number} bufferOffset Where in the buffer to write.
|
|
2253
|
+
* @param {number} byteCount How many bytes to read.
|
|
2254
|
+
* @param {number} filePosition Where in the file to read from.
|
|
2255
|
+
* @param {number} fileSize The file's size, for the error.
|
|
2256
|
+
* @param {number} requiredBytes Bytes the whole read needs, for the error.
|
|
2257
|
+
* @private
|
|
2258
|
+
*/
|
|
2259
|
+
async _readRange(fd, bufferOffset, byteCount, filePosition, fileSize, requiredBytes) {
|
|
2260
|
+
assert2__default.default(this._buffer, "The read buffer has not been allocated.");
|
|
2261
|
+
let done = 0;
|
|
2262
|
+
while (done < byteCount) {
|
|
2263
|
+
const length = Math.min(READ_CHUNK_SIZE, byteCount - done);
|
|
2264
|
+
const { bytesRead } = await fd.read(this._buffer, bufferOffset + done, length, filePosition + done);
|
|
2265
|
+
if (bytesRead === 0) {
|
|
2266
|
+
throw new exports.QvdCorruptedError("Unexpected end of file while reading QVD data.", {
|
|
2267
|
+
file: this._path,
|
|
2268
|
+
fileSize,
|
|
2269
|
+
// Two numbers, because they stopped being the same one when a window began reading two
|
|
2270
|
+
// ranges: `bytesRead` is how much of this range arrived, `filePosition` is where in the
|
|
2271
|
+
// file it gave up. Reporting the position under the name of the count made a windowed
|
|
2272
|
+
// read of a truncated file claim tens of megabytes had been read when a few hundred
|
|
2273
|
+
// bytes had.
|
|
2274
|
+
bytesRead: done,
|
|
2275
|
+
filePosition: filePosition + done,
|
|
2276
|
+
requiredBytes,
|
|
2277
|
+
stage: "readData"
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
done += bytesRead;
|
|
2281
|
+
}
|
|
1970
2282
|
}
|
|
1971
2283
|
/**
|
|
1972
2284
|
* Parses the XML header of the QVD file. This method is part of the parsing process
|
|
@@ -2026,6 +2338,8 @@ var init_QvdFileReader = __esm({
|
|
|
2026
2338
|
this._headerOffset = headerBeginIndex;
|
|
2027
2339
|
this._symbolTableOffset = headerEndIndex;
|
|
2028
2340
|
this._indexTableOffset = this._symbolTableOffset + parseInt(this._header["QvdTableHeader"]["Offset"], 10);
|
|
2341
|
+
this._allFields = fieldList;
|
|
2342
|
+
this._selectedFields = selectFields(this._allFields, this._requestedFields, this._path);
|
|
2029
2343
|
}
|
|
2030
2344
|
/**
|
|
2031
2345
|
* Establishes the geometry of the index table, and validates it.
|
|
@@ -2036,14 +2350,15 @@ var init_QvdFileReader = __esm({
|
|
|
2036
2350
|
* about keeping the sign in step with the other one: the two could drift, and #113 is what
|
|
2037
2351
|
* that looks like when they do. There is one copy now.
|
|
2038
2352
|
*
|
|
2039
|
-
* @param {
|
|
2353
|
+
* @param {QvdRowWindow} window The rows of interest, as file row indices.
|
|
2040
2354
|
* @param {string} stage Stage name for any error raised here.
|
|
2041
2355
|
* @return {{fields: Array<any>, recordSize: number, totalRows: number, rowsToLoad: number,
|
|
2042
|
-
* indexBuffer: Buffer}} The record geometry.
|
|
2356
|
+
* indexBuffer: Buffer}} The record geometry. `indexBuffer` starts at the window's first
|
|
2357
|
+
* record, so the decoder always counts from zero.
|
|
2043
2358
|
* @private
|
|
2044
2359
|
*/
|
|
2045
|
-
_planIndexTable(
|
|
2046
|
-
if (!this._buffer || !this._header || !this._indexTableOffset) {
|
|
2360
|
+
_planIndexTable(window, stage) {
|
|
2361
|
+
if (!this._buffer || !this._header || !this._indexTableOffset || !this._selectedFields || !this._allFields) {
|
|
2047
2362
|
throw new exports.QvdCorruptedError(
|
|
2048
2363
|
"The QVD file has not been loaded in the proper order or has not been loaded at all.",
|
|
2049
2364
|
{
|
|
@@ -2052,14 +2367,12 @@ var init_QvdFileReader = __esm({
|
|
|
2052
2367
|
}
|
|
2053
2368
|
);
|
|
2054
2369
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
fields = [fields];
|
|
2058
|
-
}
|
|
2370
|
+
const allFields = this._allFields;
|
|
2371
|
+
const fields = this._selectedFields;
|
|
2059
2372
|
const recordSize = parseInt(this._header["QvdTableHeader"]["RecordByteSize"], 10);
|
|
2060
2373
|
const totalRows = parseInt(this._header["QvdTableHeader"]["NoOfRecords"], 10);
|
|
2061
|
-
const rowsToLoad = rowLimit !== null ? Math.min(rowLimit, totalRows) : totalRows;
|
|
2062
2374
|
const indexTableLength = parseInt(this._header["QvdTableHeader"]["Length"], 10);
|
|
2375
|
+
const { offset: firstRow, limit: rowsToLoad } = resolveWindow(window, totalRows);
|
|
2063
2376
|
validateIndexTableMetadata(
|
|
2064
2377
|
recordSize,
|
|
2065
2378
|
totalRows,
|
|
@@ -2068,11 +2381,20 @@ var init_QvdFileReader = __esm({
|
|
|
2068
2381
|
this._buffer.length,
|
|
2069
2382
|
rowsToLoad,
|
|
2070
2383
|
this._path,
|
|
2071
|
-
this._fileSize
|
|
2384
|
+
this._fileSize,
|
|
2385
|
+
firstRow,
|
|
2386
|
+
this._bufferFirstRow
|
|
2387
|
+
);
|
|
2388
|
+
const bufferRecordStart = (firstRow - this._bufferFirstRow) * recordSize;
|
|
2389
|
+
const indexBuffer = this._buffer.subarray(
|
|
2390
|
+
this._indexTableOffset + bufferRecordStart,
|
|
2391
|
+
this._indexTableOffset + bufferRecordStart + rowsToLoad * recordSize
|
|
2072
2392
|
);
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2393
|
+
if (!this._fieldBitMetadataValidated) {
|
|
2394
|
+
for (const field of allFields) {
|
|
2395
|
+
validateFieldBitMetadata(field, recordSize, this._path);
|
|
2396
|
+
}
|
|
2397
|
+
this._fieldBitMetadataValidated = true;
|
|
2076
2398
|
}
|
|
2077
2399
|
assert2__default.default(
|
|
2078
2400
|
rowsToLoad === 0 || recordSize === 0 || Math.floor(indexBuffer.length / recordSize) >= rowsToLoad,
|
|
@@ -2084,31 +2406,45 @@ var init_QvdFileReader = __esm({
|
|
|
2084
2406
|
* Analyzes the index table to determine which symbols are actually needed.
|
|
2085
2407
|
* This is used for two-pass symbol filtering optimization.
|
|
2086
2408
|
*
|
|
2087
|
-
*
|
|
2088
|
-
*
|
|
2409
|
+
* Only the selected fields are analysed. An unselected field's symbols are never parsed, so
|
|
2410
|
+
* there is nothing for a usage set to filter and decoding its column would be a pass over the
|
|
2411
|
+
* whole window for an answer nobody reads.
|
|
2412
|
+
*
|
|
2413
|
+
* @param {QvdRowWindow} window The rows to analyse.
|
|
2414
|
+
* @return {Promise<Array<Set<number>>>} One set of needed symbol indices per selected field, in
|
|
2415
|
+
* the same order `_parseSymbolTable` walks them.
|
|
2089
2416
|
* @private
|
|
2090
2417
|
*/
|
|
2091
|
-
async _analyzeIndexTableSymbolUsage(
|
|
2092
|
-
const { fields, recordSize, rowsToLoad, indexBuffer } = this._planIndexTable(
|
|
2093
|
-
const symbolUsage =
|
|
2094
|
-
const
|
|
2095
|
-
|
|
2418
|
+
async _analyzeIndexTableSymbolUsage(window) {
|
|
2419
|
+
const { fields, recordSize, rowsToLoad, indexBuffer } = this._planIndexTable(window, "analyzeIndexTableSymbolUsage");
|
|
2420
|
+
const symbolUsage = [];
|
|
2421
|
+
const sliceRows = Math.min(rowsToLoad, ANALYSIS_SLICE_ROWS);
|
|
2422
|
+
const column = new Int32Array(sliceRows);
|
|
2423
|
+
fields.forEach((field, position) => {
|
|
2424
|
+
this._throwIfAborted();
|
|
2096
2425
|
const needed = /* @__PURE__ */ new Set();
|
|
2097
|
-
symbolUsage
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2426
|
+
symbolUsage[position] = needed;
|
|
2427
|
+
const bitOffset = parseInt(field["BitOffset"], 10);
|
|
2428
|
+
const bitWidth = parseInt(field["BitWidth"], 10);
|
|
2429
|
+
const bias = parseInt(field["Bias"], 10);
|
|
2430
|
+
for (let first = 0; first < rowsToLoad; first += sliceRows) {
|
|
2431
|
+
const count = Math.min(sliceRows, rowsToLoad - first);
|
|
2432
|
+
decodeIndexColumn(
|
|
2433
|
+
first === 0 ? indexBuffer : indexBuffer.subarray(first * recordSize),
|
|
2434
|
+
recordSize,
|
|
2435
|
+
count,
|
|
2436
|
+
bitOffset,
|
|
2437
|
+
bitWidth,
|
|
2438
|
+
bias,
|
|
2439
|
+
column
|
|
2440
|
+
);
|
|
2441
|
+
for (let row = 0; row < count; row++) {
|
|
2442
|
+
if (column[row] >= 0) {
|
|
2443
|
+
needed.add(column[row]);
|
|
2444
|
+
}
|
|
2110
2445
|
}
|
|
2111
2446
|
}
|
|
2447
|
+
this._emitProgress("symbol-analysis", position + 1, fields.length);
|
|
2112
2448
|
});
|
|
2113
2449
|
return symbolUsage;
|
|
2114
2450
|
}
|
|
@@ -2116,12 +2452,20 @@ var init_QvdFileReader = __esm({
|
|
|
2116
2452
|
* Parses the symbol table of the QVD file. This method is part of the parsing process
|
|
2117
2453
|
* and should not be called directly.
|
|
2118
2454
|
*
|
|
2119
|
-
*
|
|
2120
|
-
*
|
|
2121
|
-
*
|
|
2455
|
+
* A field the caller did not select is skipped whole. Its symbol area is neither scanned nor
|
|
2456
|
+
* parsed - the per-field `Offset` and `Length` say exactly where it is, so there is nothing to
|
|
2457
|
+
* walk past - and that is where field selection earns its keep. The index decode is cheap by
|
|
2458
|
+
* comparison; parsing symbols is not.
|
|
2459
|
+
*
|
|
2460
|
+
* @param {Array<Set<number>>|null} symbolsToKeep Optional set of symbol indices to keep per
|
|
2461
|
+
* selected field, indexed by position. If provided, only these symbols will be parsed
|
|
2462
|
+
* (two-pass filtering optimization).
|
|
2463
|
+
* @param {number} rowsToLoad Rows the read covers, for memory estimation.
|
|
2464
|
+
* @param {{rows: number, perChunk: number}|null} [liveRows=null] Rows held at one instant when
|
|
2465
|
+
* that is fewer than the window covers - see `_prepare`.
|
|
2122
2466
|
*/
|
|
2123
|
-
async _parseSymbolTable(symbolsToKeep = null,
|
|
2124
|
-
if (!this._buffer || !this._header || !this._symbolTableOffset || !this._indexTableOffset) {
|
|
2467
|
+
async _parseSymbolTable(symbolsToKeep = null, rowsToLoad = 0, liveRows = null) {
|
|
2468
|
+
if (!this._buffer || !this._header || !this._symbolTableOffset || !this._indexTableOffset || !this._selectedFields || !this._allFields) {
|
|
2125
2469
|
throw new exports.QvdCorruptedError(
|
|
2126
2470
|
"The QVD file has not been loaded in the proper order or has not been loaded at all.",
|
|
2127
2471
|
{
|
|
@@ -2130,7 +2474,8 @@ var init_QvdFileReader = __esm({
|
|
|
2130
2474
|
}
|
|
2131
2475
|
);
|
|
2132
2476
|
}
|
|
2133
|
-
|
|
2477
|
+
const allFields = this._allFields;
|
|
2478
|
+
const fields = this._selectedFields;
|
|
2134
2479
|
const symbolBuffer = this._buffer.subarray(this._symbolTableOffset, this._indexTableOffset);
|
|
2135
2480
|
const symbolTableSize = symbolBuffer.length;
|
|
2136
2481
|
const totalRows = parseInt(this._header["QvdTableHeader"]["NoOfRecords"], 10);
|
|
@@ -2138,32 +2483,25 @@ var init_QvdFileReader = __esm({
|
|
|
2138
2483
|
if (this._headerMatchesFile) {
|
|
2139
2484
|
validateMemoryAvailability(
|
|
2140
2485
|
symbolTableSize,
|
|
2141
|
-
|
|
2486
|
+
rowsToLoad,
|
|
2142
2487
|
totalRows,
|
|
2143
2488
|
this._path,
|
|
2144
2489
|
this._memorySafetyFactor,
|
|
2145
|
-
|
|
2146
|
-
this._materialisesRows
|
|
2490
|
+
fields.length,
|
|
2491
|
+
this._materialisesRows,
|
|
2492
|
+
liveRows
|
|
2147
2493
|
);
|
|
2148
2494
|
}
|
|
2149
|
-
warnLargeSymbolTable(
|
|
2150
|
-
|
|
2151
|
-
maxRows,
|
|
2152
|
-
totalRows,
|
|
2153
|
-
Array.isArray(fields) ? fields.length : 1,
|
|
2154
|
-
this._materialisesRows
|
|
2155
|
-
);
|
|
2156
|
-
if (!Array.isArray(fields)) {
|
|
2157
|
-
fields = [fields];
|
|
2158
|
-
}
|
|
2159
|
-
for (const field of fields) {
|
|
2495
|
+
warnLargeSymbolTable(symbolTableSize, rowsToLoad, totalRows, fields.length, this._materialisesRows);
|
|
2496
|
+
for (const field of allFields) {
|
|
2160
2497
|
validateFieldMetadata(field, symbolBuffer.length, this._path);
|
|
2161
2498
|
}
|
|
2162
|
-
this._symbolTable = fields.map((field) => {
|
|
2499
|
+
this._symbolTable = fields.map((field, position) => {
|
|
2500
|
+
this._throwIfAborted();
|
|
2163
2501
|
const symbolsOffset = parseInt(field["Offset"], 10);
|
|
2164
2502
|
const symbolsLength = parseInt(field["Length"], 10);
|
|
2165
2503
|
const fieldName = field["FieldName"];
|
|
2166
|
-
const neededSymbols = symbolsToKeep ? symbolsToKeep
|
|
2504
|
+
const neededSymbols = symbolsToKeep ? symbolsToKeep[position] : null;
|
|
2167
2505
|
const filteringEnabled = neededSymbols !== null;
|
|
2168
2506
|
const symbols = [];
|
|
2169
2507
|
let symbolIndex = 0;
|
|
@@ -2183,6 +2521,7 @@ var init_QvdFileReader = __esm({
|
|
|
2183
2521
|
pointer += bytesRead - 1;
|
|
2184
2522
|
symbolIndex++;
|
|
2185
2523
|
}
|
|
2524
|
+
this._emitProgress("symbol-table", position + 1, fields.length);
|
|
2186
2525
|
return symbols;
|
|
2187
2526
|
});
|
|
2188
2527
|
}
|
|
@@ -2201,13 +2540,18 @@ var init_QvdFileReader = __esm({
|
|
|
2201
2540
|
* same for every row, so they are hoisted out of the loop and the inner loop does arithmetic
|
|
2202
2541
|
* into a typed array and nothing else. Rows are assembled later, once, in `load()`.
|
|
2203
2542
|
*
|
|
2204
|
-
*
|
|
2543
|
+
* The window is what makes chunked iteration cheap: `decodeIndexColumn` walks records by
|
|
2544
|
+
* `base += recordSize`, so decoding rows k to k+n is a question of where the buffer slice starts
|
|
2545
|
+
* and how many iterations run. Nothing about the decoder changed to support it.
|
|
2546
|
+
*
|
|
2547
|
+
* @param {QvdRowWindow} window The rows to decode.
|
|
2205
2548
|
*/
|
|
2206
|
-
async _parseIndexTable(
|
|
2207
|
-
const { fields, recordSize, rowsToLoad, indexBuffer } = this._planIndexTable(
|
|
2549
|
+
async _parseIndexTable(window) {
|
|
2550
|
+
const { fields, recordSize, rowsToLoad, indexBuffer } = this._planIndexTable(window, "parseIndexTable");
|
|
2208
2551
|
this._rowsDecoded = rowsToLoad;
|
|
2209
|
-
this._indexColumns = fields.map(
|
|
2210
|
-
(
|
|
2552
|
+
this._indexColumns = fields.map((field, position) => {
|
|
2553
|
+
this._throwIfAborted();
|
|
2554
|
+
const column = decodeIndexColumn(
|
|
2211
2555
|
indexBuffer,
|
|
2212
2556
|
recordSize,
|
|
2213
2557
|
rowsToLoad,
|
|
@@ -2215,8 +2559,10 @@ var init_QvdFileReader = __esm({
|
|
|
2215
2559
|
parseInt(field["BitWidth"], 10),
|
|
2216
2560
|
parseInt(field["Bias"], 10),
|
|
2217
2561
|
new Int32Array(rowsToLoad)
|
|
2218
|
-
)
|
|
2219
|
-
|
|
2562
|
+
);
|
|
2563
|
+
this._emitProgress("index-table", position + 1, fields.length);
|
|
2564
|
+
return column;
|
|
2565
|
+
});
|
|
2220
2566
|
}
|
|
2221
2567
|
/**
|
|
2222
2568
|
* Reads the file's schema and header metadata, without touching the symbol or index tables.
|
|
@@ -2233,8 +2579,11 @@ var init_QvdFileReader = __esm({
|
|
|
2233
2579
|
* @return {Promise<import('./QvdDataFrame.js').QvdFileMetadata>} The file's schema and header.
|
|
2234
2580
|
*/
|
|
2235
2581
|
async loadMetadata() {
|
|
2236
|
-
await this._readData(null, true);
|
|
2582
|
+
await this._readData({ offset: 0, limit: null }, true);
|
|
2583
|
+
this._emitProgress("header", 0, 1);
|
|
2237
2584
|
await this._parseHeader();
|
|
2585
|
+
this._emitProgress("header", 1, 1);
|
|
2586
|
+
this._throwIfAborted();
|
|
2238
2587
|
assert2__default.default(this._header, "The QVD file header has not been parsed.");
|
|
2239
2588
|
const header = this._header["QvdTableHeader"];
|
|
2240
2589
|
let fields = header["Fields"]?.["QvdFieldHeader"] ?? [];
|
|
@@ -2269,114 +2618,200 @@ var init_QvdFileReader = __esm({
|
|
|
2269
2618
|
/**
|
|
2270
2619
|
* Loads the QVD file into memory and parses it.
|
|
2271
2620
|
*
|
|
2272
|
-
* @param {number|null
|
|
2273
|
-
*
|
|
2274
|
-
*
|
|
2621
|
+
* @param {number|null|{offset?: number, limit?: number|null, maxRows?: number|null}} [window]
|
|
2622
|
+
* The rows to load. A number or null means what it always meant - the first N rows, or all of
|
|
2623
|
+
* them - and `{offset, limit}` is the same thing said more precisely, so `5` and
|
|
2624
|
+
* `{offset: 0, limit: 5}` are one read. `maxRows` is accepted as a second name for `limit`.
|
|
2625
|
+
* @throws {QvdValidationError} If the window is not a non-negative integer, null, or a valid
|
|
2626
|
+
* `{offset, limit}` object.
|
|
2275
2627
|
* @return {Promise<QvdDataFrame>} The loaded QVD file.
|
|
2276
2628
|
*/
|
|
2277
|
-
async load(
|
|
2278
|
-
const
|
|
2629
|
+
async load(window = null) {
|
|
2630
|
+
const rows = normaliseWindow(window, this._path);
|
|
2631
|
+
const prepared = await this._prepare(rows);
|
|
2632
|
+
await this._parseIndexTable({ offset: prepared.offset, limit: prepared.rowsAvailable });
|
|
2633
|
+
const data = this._buildRows(prepared.resolvedByField, 0, prepared.rowsAvailable);
|
|
2634
|
+
return new exports.QvdDataFrame(data, prepared.columns, prepared.metadata, {
|
|
2635
|
+
...prepared.loadStats,
|
|
2636
|
+
rowsLoaded: data.length
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* Reads the file as columns, without ever materialising rows.
|
|
2641
|
+
*
|
|
2642
|
+
* Shares every step with `load()` up to the point where rows would be built - see `_prepare`.
|
|
2643
|
+
* What it keeps instead is what the decoder already produced: one `Int32Array` of stored
|
|
2644
|
+
* indices per field, and one resolved value per distinct symbol. On the 1.7M x 20 taxi
|
|
2645
|
+
* fixture that is 38.6 MiB against the 352.8 MiB `data` retains, because a column costs four
|
|
2646
|
+
* bytes per row rather than a boxed value per cell, and the symbols are a few thousand
|
|
2647
|
+
* entries shared across every row that uses them.
|
|
2648
|
+
*
|
|
2649
|
+
* @param {number|null|{offset?: number, limit?: number|null, maxRows?: number|null}} [window]
|
|
2650
|
+
* The rows to decode, in the same spellings `load()` accepts.
|
|
2651
|
+
* @return {Promise<import('./QvdColumnTable.js').QvdColumnTable>} The decoded columns.
|
|
2652
|
+
*/
|
|
2653
|
+
async loadColumnar(window = null) {
|
|
2654
|
+
const rows = normaliseWindow(window, this._path);
|
|
2655
|
+
const prepared = await this._prepare(rows);
|
|
2656
|
+
await this._parseIndexTable({ offset: prepared.offset, limit: prepared.rowsAvailable });
|
|
2657
|
+
const { QvdColumnTable: QvdColumnTable2 } = await Promise.resolve().then(() => (init_QvdColumnTable(), QvdColumnTable_exports));
|
|
2279
2658
|
assert2__default.default(this._indexColumns, "The QVD file index table has not been parsed.");
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
}
|
|
2289
|
-
data[row] = values;
|
|
2290
|
-
}
|
|
2291
|
-
loadStats.rowsLoaded = data.length;
|
|
2292
|
-
return new exports.QvdDataFrame(data, columns, metadata, loadStats);
|
|
2659
|
+
return new QvdColumnTable2({
|
|
2660
|
+
columns: prepared.columns,
|
|
2661
|
+
codesByField: this._indexColumns,
|
|
2662
|
+
symbolsByField: prepared.resolvedByField,
|
|
2663
|
+
rowCount: this._rowsDecoded,
|
|
2664
|
+
metadata: prepared.metadata,
|
|
2665
|
+
loadStats: { ...prepared.loadStats, rowsLoaded: this._rowsDecoded }
|
|
2666
|
+
});
|
|
2293
2667
|
}
|
|
2294
2668
|
/**
|
|
2295
|
-
*
|
|
2669
|
+
* Yields the window as data frames of at most `chunkSize` rows.
|
|
2296
2670
|
*
|
|
2297
|
-
*
|
|
2298
|
-
*
|
|
2299
|
-
*
|
|
2300
|
-
*
|
|
2301
|
-
*
|
|
2671
|
+
* The file is opened, read and parsed **once**; only the index decode and the row building
|
|
2672
|
+
* happen per chunk. That is the whole reason this exists as a method rather than as a loop of
|
|
2673
|
+
* `load({offset, limit})` calls at the call site: the symbol table has to be parsed in full
|
|
2674
|
+
* whatever the chunk size - a stored index in the last chunk can address the first symbol -
|
|
2675
|
+
* and re-parsing it per chunk is what makes the obvious implementation cost more than a plain
|
|
2676
|
+
* load rather than less. PyQvd's chunked read does re-read it, and the comment on #140 records
|
|
2677
|
+
* that as a limitation rather than a design.
|
|
2302
2678
|
*
|
|
2303
|
-
*
|
|
2304
|
-
*
|
|
2305
|
-
*
|
|
2306
|
-
*
|
|
2679
|
+
* What it bounds is row materialisation, which is what actually dominates a large read's heap.
|
|
2680
|
+
* Two chunks of rows are alive at a time, not one - `for await` keeps the yielded frame
|
|
2681
|
+
* reachable while this generator builds the next - which is why `liveRows` below is
|
|
2682
|
+
* `chunkSize * 2`, and why the heap it needs is twice what one chunk suggests.
|
|
2683
|
+
*
|
|
2684
|
+
* A window covering no rows yields nothing at all, rather than one empty frame - so
|
|
2685
|
+
* `for await` over an exhausted offset does nothing, which is what a paging loop wants.
|
|
2686
|
+
*
|
|
2687
|
+
* @param {number|null|{offset?: number, limit?: number|null, maxRows?: number|null}} window
|
|
2688
|
+
* The rows to cover, in the same spellings `load()` accepts.
|
|
2689
|
+
* @param {number} chunkSize Rows per frame. Must be a positive integer.
|
|
2690
|
+
* @return {AsyncGenerator<QvdDataFrame>} The chunks, in order.
|
|
2307
2691
|
*/
|
|
2308
|
-
async
|
|
2309
|
-
if (
|
|
2310
|
-
throw new exports.QvdValidationError("
|
|
2311
|
-
provided:
|
|
2312
|
-
type: typeof
|
|
2692
|
+
async *iterateRows(window, chunkSize) {
|
|
2693
|
+
if (typeof chunkSize !== "number" || !Number.isInteger(chunkSize) || chunkSize <= 0) {
|
|
2694
|
+
throw new exports.QvdValidationError("chunkSize must be a positive integer", {
|
|
2695
|
+
provided: chunkSize,
|
|
2696
|
+
type: typeof chunkSize,
|
|
2313
2697
|
file: this._path
|
|
2314
2698
|
});
|
|
2315
2699
|
}
|
|
2316
|
-
|
|
2700
|
+
const liveRows = { rows: chunkSize * 2, perChunk: 2 };
|
|
2701
|
+
const rows = normaliseWindow(window, this._path);
|
|
2702
|
+
const prepared = await this._prepare(rows, liveRows);
|
|
2703
|
+
for (let done = 0; done < prepared.rowsAvailable; done += chunkSize) {
|
|
2704
|
+
this._throwIfAborted();
|
|
2705
|
+
const count = Math.min(chunkSize, prepared.rowsAvailable - done);
|
|
2706
|
+
const offset = prepared.offset + done;
|
|
2707
|
+
await this._parseIndexTable({ offset, limit: count });
|
|
2708
|
+
const data = this._buildRows(prepared.resolvedByField, done, prepared.rowsAvailable);
|
|
2709
|
+
yield new exports.QvdDataFrame(data, prepared.columns, prepared.metadata, {
|
|
2710
|
+
...prepared.loadStats,
|
|
2711
|
+
offset,
|
|
2712
|
+
rowsLoaded: data.length
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
/**
|
|
2717
|
+
* Reads the file and resolves its symbols, stopping short of decoding any rows.
|
|
2718
|
+
*
|
|
2719
|
+
* Everything `load()`, `loadColumnar()` and `iterateRows()` have in common, which is everything
|
|
2720
|
+
* that depends on the file rather than on the window. Two read paths for one binary format is
|
|
2721
|
+
* the drift risk #113 is the standing example of - a stored index resolved one way here and
|
|
2722
|
+
* another way there returns plausible wrong values and throws nothing - so there is one path,
|
|
2723
|
+
* and the entry points differ only in what they do with what it returns and how many rows they
|
|
2724
|
+
* ask for at a time.
|
|
2725
|
+
*
|
|
2726
|
+
* @param {QvdRowWindow} window The rows the read covers.
|
|
2727
|
+
* @param {{rows: number, perChunk: number}|null} [liveRows] Rows held at one instant when that
|
|
2728
|
+
* is fewer than the window covers, and how many of them one row of the caller's chunk size
|
|
2729
|
+
* accounts for. Only `iterateRows` passes it; every other read holds what it covers.
|
|
2730
|
+
* @return {Promise<{columns: Array<string>, metadata: any, loadStats: any,
|
|
2731
|
+
* resolvedByField: Array<Array<any>>, rowsAvailable: number, offset: number}>} The parsed
|
|
2732
|
+
* file, with the window as it resolved against it.
|
|
2733
|
+
* @private
|
|
2734
|
+
*/
|
|
2735
|
+
async _prepare(window, liveRows = null) {
|
|
2736
|
+
this._throwIfAborted();
|
|
2737
|
+
await this._readData(window, false, liveRows);
|
|
2738
|
+
this._emitProgress("header", 0, 1);
|
|
2317
2739
|
await this._parseHeader();
|
|
2740
|
+
this._emitProgress("header", 1, 1);
|
|
2741
|
+
this._throwIfAborted();
|
|
2742
|
+
assert2__default.default(this._header, "The QVD file header has not been parsed.");
|
|
2743
|
+
const totalRows = parseInt(this._header["QvdTableHeader"]["NoOfRecords"], 10);
|
|
2744
|
+
const symbolTableLength = parseInt(this._header["QvdTableHeader"]["Offset"], 10);
|
|
2745
|
+
const resolved = resolveWindow(window, totalRows);
|
|
2746
|
+
const rowsAvailable = resolved.limit;
|
|
2318
2747
|
let symbolsToKeep = null;
|
|
2319
2748
|
let symbolsKept = null;
|
|
2320
|
-
if (
|
|
2321
|
-
const symbolTableLength = parseInt(this._header["QvdTableHeader"]["Offset"], 10);
|
|
2749
|
+
if (window.limit !== null || window.offset > 0) {
|
|
2322
2750
|
if (symbolTableLength > this._symbolFilteringThreshold) {
|
|
2323
|
-
symbolsToKeep = await this._analyzeIndexTableSymbolUsage(
|
|
2324
|
-
symbolsKept =
|
|
2751
|
+
symbolsToKeep = await this._analyzeIndexTableSymbolUsage({ offset: resolved.offset, limit: rowsAvailable });
|
|
2752
|
+
symbolsKept = symbolsToKeep.reduce((sum, set) => sum + set.size, 0);
|
|
2325
2753
|
}
|
|
2326
2754
|
}
|
|
2327
|
-
await this._parseSymbolTable(symbolsToKeep,
|
|
2328
|
-
await this._parseIndexTable(maxRows);
|
|
2329
|
-
assert2__default.default(this._header, "The QVD file header has not been parsed.");
|
|
2755
|
+
await this._parseSymbolTable(symbolsToKeep, rowsAvailable, liveRows);
|
|
2330
2756
|
assert2__default.default(this._symbolTable, "The QVD file symbol table has not been parsed.");
|
|
2331
|
-
|
|
2757
|
+
this._throwIfAborted();
|
|
2332
2758
|
const resolvedByField = this._symbolTable.map((symbols) => {
|
|
2333
|
-
const
|
|
2759
|
+
const resolved2 = new Array(symbols.length);
|
|
2334
2760
|
for (let index = 0; index < symbols.length; index++) {
|
|
2335
2761
|
const value = symbols[index]?.toPrimaryValue();
|
|
2336
|
-
|
|
2762
|
+
resolved2[index] = typeof value === "string" && value.trim() !== "" && !isNaN(Number(value)) ? Number(value) : value;
|
|
2337
2763
|
}
|
|
2338
|
-
return
|
|
2764
|
+
return resolved2;
|
|
2339
2765
|
});
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
fields = [fields];
|
|
2343
|
-
}
|
|
2344
|
-
const columns = fields.map((field) => field["FieldName"]);
|
|
2766
|
+
assert2__default.default(this._selectedFields, "The QVD file fields have not been resolved.");
|
|
2767
|
+
const columns = this._selectedFields.map((field) => field["FieldName"]);
|
|
2345
2768
|
const metadata = this._header["QvdTableHeader"];
|
|
2346
2769
|
const loadStats = {
|
|
2347
|
-
symbolTableBytes:
|
|
2348
|
-
totalRows
|
|
2349
|
-
rowsLoaded:
|
|
2770
|
+
symbolTableBytes: symbolTableLength,
|
|
2771
|
+
totalRows,
|
|
2772
|
+
rowsLoaded: 0,
|
|
2773
|
+
offset: resolved.offset,
|
|
2350
2774
|
symbolFiltering: symbolsToKeep !== null,
|
|
2351
2775
|
symbolsKept
|
|
2352
2776
|
};
|
|
2353
|
-
return { columns, metadata, loadStats, resolvedByField };
|
|
2777
|
+
return { columns, metadata, loadStats, resolvedByField, rowsAvailable, offset: resolved.offset };
|
|
2354
2778
|
}
|
|
2355
2779
|
/**
|
|
2356
|
-
*
|
|
2780
|
+
* Builds rows from the columns currently decoded.
|
|
2357
2781
|
*
|
|
2358
|
-
*
|
|
2359
|
-
*
|
|
2360
|
-
*
|
|
2361
|
-
*
|
|
2362
|
-
*
|
|
2363
|
-
* entries shared across every row that uses them.
|
|
2782
|
+
* `data` stays eager: of the four ways this library is used - a full read, a preview already
|
|
2783
|
+
* bounded by a limit, writing an array out, and reading metadata - not one is helped by
|
|
2784
|
+
* materialising a row only when it is touched, and a lazy accessor would cost a proxy, a cache
|
|
2785
|
+
* and mutation semantics to serve none of them. A caller who wants columns without paying for
|
|
2786
|
+
* rows uses `QvdColumnTable`, which stops before this loop.
|
|
2364
2787
|
*
|
|
2365
|
-
* @param {
|
|
2366
|
-
* @
|
|
2788
|
+
* @param {Array<Array<any>>} resolvedByField One resolved value per distinct symbol, per field.
|
|
2789
|
+
* @param {number} progressBase Rows already delivered before this call, so that progress over a
|
|
2790
|
+
* chunked iteration counts the whole window rather than restarting at every chunk.
|
|
2791
|
+
* @param {number} progressTotal Rows the whole window covers.
|
|
2792
|
+
* @return {Array<Array<any>>} The rows.
|
|
2793
|
+
* @private
|
|
2367
2794
|
*/
|
|
2368
|
-
|
|
2369
|
-
const { columns, metadata, loadStats, resolvedByField } = await this._decode(maxRows);
|
|
2370
|
-
const { QvdColumnTable: QvdColumnTable2 } = await Promise.resolve().then(() => (init_QvdColumnTable(), QvdColumnTable_exports));
|
|
2795
|
+
_buildRows(resolvedByField, progressBase, progressTotal) {
|
|
2371
2796
|
assert2__default.default(this._indexColumns, "The QVD file index table has not been parsed.");
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2797
|
+
const indexColumns = this._indexColumns;
|
|
2798
|
+
const fieldCount = indexColumns.length;
|
|
2799
|
+
const rowCount = this._rowsDecoded;
|
|
2800
|
+
const data = new Array(rowCount);
|
|
2801
|
+
const reportInterval = Math.max(1, Math.floor(progressTotal / 100));
|
|
2802
|
+
for (let row = 0; row < rowCount; row++) {
|
|
2803
|
+
const values = new Array(fieldCount);
|
|
2804
|
+
for (let field = 0; field < fieldCount; field++) {
|
|
2805
|
+
const symbolIndex = indexColumns[field][row];
|
|
2806
|
+
values[field] = symbolIndex < 0 ? null : resolvedByField[field][symbolIndex];
|
|
2807
|
+
}
|
|
2808
|
+
data[row] = values;
|
|
2809
|
+
if ((progressBase + row + 1) % reportInterval === 0 || row + 1 === rowCount) {
|
|
2810
|
+
this._throwIfAborted();
|
|
2811
|
+
this._emitProgress("rows", progressBase + row + 1, progressTotal);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
return data;
|
|
2380
2815
|
}
|
|
2381
2816
|
};
|
|
2382
2817
|
}
|
|
@@ -2387,6 +2822,7 @@ exports.QvdDataFrame = void 0;
|
|
|
2387
2822
|
var init_QvdDataFrame = __esm({
|
|
2388
2823
|
"src/QvdDataFrame.js"() {
|
|
2389
2824
|
init_QvdErrors();
|
|
2825
|
+
init_readOptions();
|
|
2390
2826
|
exports.QvdDataFrame = class _QvdDataFrame {
|
|
2391
2827
|
/**
|
|
2392
2828
|
* Represents the data frame stored inside a QVD file.
|
|
@@ -2429,12 +2865,15 @@ var init_QvdDataFrame = __esm({
|
|
|
2429
2865
|
/**
|
|
2430
2866
|
* Returns statistics about the read that produced this data frame.
|
|
2431
2867
|
*
|
|
2432
|
-
*
|
|
2433
|
-
*
|
|
2868
|
+
* Carried by every frame that came from a file - `fromQvd()`, and each chunk `iterate()` yields,
|
|
2869
|
+
* which is how a chunk reports its `offset`. `fromDict()`, `head()`, `tail()`, `rows()` and
|
|
2870
|
+
* `select()` describe no particular read and report null rather than a stale figure.
|
|
2434
2871
|
*
|
|
2435
2872
|
* The main use is confirming that a lazy load actually filtered the symbol table:
|
|
2436
2873
|
* `symbolFiltering` says whether the two-pass path ran, and `symbolsKept` how many symbols
|
|
2437
|
-
* survived it
|
|
2874
|
+
* survived it. Note that a bounded read does not filter on its own - the two-pass path engages
|
|
2875
|
+
* only above `symbolFilteringThreshold`, so on a file below it this reports false and every
|
|
2876
|
+
* symbol was parsed however few rows were asked for.
|
|
2438
2877
|
*
|
|
2439
2878
|
* @return {QvdLoadStats|null} Load statistics, or null if this frame did not come from a file.
|
|
2440
2879
|
*/
|
|
@@ -2811,6 +3250,18 @@ var init_QvdDataFrame = __esm({
|
|
|
2811
3250
|
* @param {Object} [options] Optional loading options.
|
|
2812
3251
|
* @param {number|null} [options.maxRows] The maximum number of rows to load. Must be a non-negative
|
|
2813
3252
|
* integer; if not specified or null, all rows are loaded. Anything else throws a QvdValidationError.
|
|
3253
|
+
* This is the older name for `limit`; the two are the same option and passing both throws.
|
|
3254
|
+
* @param {number|null} [options.limit] Rows to read, counting from `offset`. The same number as
|
|
3255
|
+
* `maxRows`, spelled so that it reads correctly beside an offset.
|
|
3256
|
+
* @param {number} [options.offset=0] File row to start at. An offset past the end of the file
|
|
3257
|
+
* returns no rows rather than throwing, so a paging loop terminates on its own.
|
|
3258
|
+
* @param {Array<string>|null} [options.fields] Field names to read, in the order they should
|
|
3259
|
+
* appear in the result. Unselected fields have their symbols skipped entirely rather than
|
|
3260
|
+
* parsed and discarded. An unknown or repeated name throws.
|
|
3261
|
+
* @param {Function} [options.onProgress] Called with `{stage, current, total, percent}` as the
|
|
3262
|
+
* read proceeds - the same shape `toQvd`'s callback receives.
|
|
3263
|
+
* @param {AbortSignal} [options.signal] Cancels the read. The rejection is `signal.reason`,
|
|
3264
|
+
* which is a `DOMException` named `AbortError` unless you aborted with a reason of your own.
|
|
2814
3265
|
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
|
|
2815
3266
|
* must be within this directory, with symlinks resolved first, so a link inside it that points
|
|
2816
3267
|
* outside it is rejected. Defaults to the current working directory. To permit an entire
|
|
@@ -2823,17 +3274,42 @@ var init_QvdDataFrame = __esm({
|
|
|
2823
3274
|
* **Zero disables the memory check entirely.**
|
|
2824
3275
|
* @param {number} [options.symbolFilteringThreshold=52428800] Symbol table size, in bytes, above which
|
|
2825
3276
|
* a lazy load switches to the two-pass filtering path. Defaults to 50MB.
|
|
2826
|
-
* @throws {QvdValidationError} If
|
|
3277
|
+
* @throws {QvdValidationError} If a window option is not a non-negative integer, if both
|
|
3278
|
+
* `maxRows` and `limit` are given, or if `fields` names a column the file does not have.
|
|
2827
3279
|
* @return {Promise<QvdDataFrame>} The data frame of the QVD file.
|
|
2828
3280
|
*/
|
|
2829
3281
|
static async fromQvd(path3, options = {}) {
|
|
2830
3282
|
const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
3283
|
+
return await new QvdFileReader2(path3, readerOptionsFrom(options)).load(windowFrom(options));
|
|
3284
|
+
}
|
|
3285
|
+
/**
|
|
3286
|
+
* Reads a QVD file in chunks, as an async generator of data frames.
|
|
3287
|
+
*
|
|
3288
|
+
* The file is opened, read and parsed once; only the index decode and the row building happen
|
|
3289
|
+
* per chunk, so what this bounds is row materialisation - the part that actually dominates a
|
|
3290
|
+
* large read's heap. It is **not** constant-memory reading of an arbitrarily large file: the
|
|
3291
|
+
* symbol table is parsed in full whatever the chunk size, because a stored index in the last
|
|
3292
|
+
* chunk can address the first symbol. On a high-cardinality file that table is the bulk of the
|
|
3293
|
+
* cost, and `readMetadata` is the only read that avoids it.
|
|
3294
|
+
*
|
|
3295
|
+
* ```js
|
|
3296
|
+
* for await (const chunk of QvdDataFrame.iterate('big.qvd', {chunkSize: 50_000})) {
|
|
3297
|
+
* process(chunk.data);
|
|
3298
|
+
* }
|
|
3299
|
+
* ```
|
|
3300
|
+
*
|
|
3301
|
+
* A window covering no rows yields nothing, so a loop over an exhausted offset simply does not
|
|
3302
|
+
* run its body.
|
|
3303
|
+
*
|
|
3304
|
+
* @param {string} path The path to the QVD file.
|
|
3305
|
+
* @param {Object} [options] The same options `fromQvd` takes, plus:
|
|
3306
|
+
* @param {number} [options.chunkSize=100000] Rows per frame. Must be a positive integer.
|
|
3307
|
+
* @return {AsyncGenerator<QvdDataFrame>} The chunks, in file order.
|
|
3308
|
+
*/
|
|
3309
|
+
static async *iterate(path3, options = {}) {
|
|
3310
|
+
const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
|
|
3311
|
+
const reader = new QvdFileReader2(path3, readerOptionsFrom(options));
|
|
3312
|
+
yield* reader.iterateRows(windowFrom(options), options.chunkSize === void 0 ? 1e5 : options.chunkSize);
|
|
2837
3313
|
}
|
|
2838
3314
|
/**
|
|
2839
3315
|
* Reads a QVD file's schema and header metadata, without reading its data.
|
|
@@ -2857,11 +3333,15 @@ var init_QvdDataFrame = __esm({
|
|
|
2857
3333
|
* @param {Object} [options] Optional reading options.
|
|
2858
3334
|
* @param {string} [options.allowedDir] Optional allowed directory path, applied exactly as it
|
|
2859
3335
|
* is for `fromQvd`.
|
|
3336
|
+
* @param {Function} [options.onProgress] Called with `{stage, current, total, percent}`, as on
|
|
3337
|
+
* the reads that return data. Only the `read` and `header` stages occur here; there are no
|
|
3338
|
+
* symbols to parse and no rows to build.
|
|
3339
|
+
* @param {AbortSignal} [options.signal] Cancels the read, rejecting with `signal.reason`.
|
|
2860
3340
|
* @return {Promise<QvdFileMetadata>} The file's schema and header metadata.
|
|
2861
3341
|
*/
|
|
2862
3342
|
static async readMetadata(path3, options = {}) {
|
|
2863
3343
|
const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
|
|
2864
|
-
return await new QvdFileReader2(path3,
|
|
3344
|
+
return await new QvdFileReader2(path3, metadataOptionsFrom(options)).loadMetadata();
|
|
2865
3345
|
}
|
|
2866
3346
|
/**
|
|
2867
3347
|
* Constructs a data frame from a dictionary.
|