qvdjs 0.9.4 → 0.10.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 +175 -17
- package/dist/index.cjs +869 -321
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +869 -321
- package/dist/index.js.map +1 -1
- package/package.json +15 -15
package/dist/index.cjs
CHANGED
|
@@ -4,7 +4,7 @@ var fs = require('fs');
|
|
|
4
4
|
var path = require('path');
|
|
5
5
|
var crypto = require('crypto');
|
|
6
6
|
var xml2 = require('xml2js');
|
|
7
|
-
var
|
|
7
|
+
var assert2 = require('assert');
|
|
8
8
|
var os = require('os');
|
|
9
9
|
var v8 = require('v8');
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
|
14
14
|
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
15
15
|
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
16
16
|
var xml2__default = /*#__PURE__*/_interopDefault(xml2);
|
|
17
|
-
var
|
|
17
|
+
var assert2__default = /*#__PURE__*/_interopDefault(assert2);
|
|
18
18
|
var os__default = /*#__PURE__*/_interopDefault(os);
|
|
19
19
|
var v8__default = /*#__PURE__*/_interopDefault(v8);
|
|
20
20
|
|
|
@@ -367,6 +367,55 @@ var init_validatePath = __esm({
|
|
|
367
367
|
}
|
|
368
368
|
});
|
|
369
369
|
|
|
370
|
+
// src/util/bitUtils.js
|
|
371
|
+
function fieldGeometry(bitOffset, bitWidth) {
|
|
372
|
+
const shift = bitOffset & 7;
|
|
373
|
+
return {
|
|
374
|
+
byteStart: bitOffset >>> 3,
|
|
375
|
+
shift,
|
|
376
|
+
byteCount: bitWidth === 0 ? 0 : shift + bitWidth + 7 >>> 3
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function decodeIndexColumn(buffer, recordSize, rowCount, bitOffset, bitWidth, bias, out) {
|
|
380
|
+
if (bitWidth === 0) {
|
|
381
|
+
out.fill(bias, 0, rowCount);
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
const { byteStart, shift, byteCount } = fieldGeometry(bitOffset, bitWidth);
|
|
385
|
+
const divisor = POW2[shift];
|
|
386
|
+
const modulus = POW2[bitWidth];
|
|
387
|
+
let base = byteStart;
|
|
388
|
+
for (let row = 0; row < rowCount; row++, base += recordSize) {
|
|
389
|
+
let acc = buffer[base];
|
|
390
|
+
if (byteCount > 1) acc += buffer[base + 1] * 256;
|
|
391
|
+
if (byteCount > 2) acc += buffer[base + 2] * 65536;
|
|
392
|
+
if (byteCount > 3) acc += buffer[base + 3] * 16777216;
|
|
393
|
+
if (byteCount > 4) acc += buffer[base + 4] * 4294967296;
|
|
394
|
+
out[row] = Math.floor(acc / divisor) % modulus + bias;
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
function writeBitField(buffer, recordBase, geometry, value) {
|
|
399
|
+
const { byteStart, shift, byteCount } = geometry;
|
|
400
|
+
if (byteCount === 0) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const base = recordBase + byteStart;
|
|
404
|
+
const shifted = value * POW2[shift];
|
|
405
|
+
buffer[base] |= shifted % 256;
|
|
406
|
+
if (byteCount > 1) buffer[base + 1] |= Math.floor(shifted / 256) % 256;
|
|
407
|
+
if (byteCount > 2) buffer[base + 2] |= Math.floor(shifted / 65536) % 256;
|
|
408
|
+
if (byteCount > 3) buffer[base + 3] |= Math.floor(shifted / 16777216) % 256;
|
|
409
|
+
if (byteCount > 4) buffer[base + 4] |= Math.floor(shifted / 4294967296) % 256;
|
|
410
|
+
}
|
|
411
|
+
var MAX_BIT_WIDTH, POW2;
|
|
412
|
+
var init_bitUtils = __esm({
|
|
413
|
+
"src/util/bitUtils.js"() {
|
|
414
|
+
MAX_BIT_WIDTH = 31;
|
|
415
|
+
POW2 = Array.from({ length: 41 }, (_, exponent) => 2 ** exponent);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
370
419
|
// src/QvdFileWriter.js
|
|
371
420
|
var QvdFileWriter_exports = {};
|
|
372
421
|
__export(QvdFileWriter_exports, {
|
|
@@ -376,7 +425,9 @@ exports.QvdFileWriter = void 0;
|
|
|
376
425
|
var init_QvdFileWriter = __esm({
|
|
377
426
|
"src/QvdFileWriter.js"() {
|
|
378
427
|
init_QvdSymbol();
|
|
428
|
+
init_QvdErrors();
|
|
379
429
|
init_validatePath();
|
|
430
|
+
init_bitUtils();
|
|
380
431
|
exports.QvdFileWriter = class _QvdFileWriter {
|
|
381
432
|
/**
|
|
382
433
|
* Constructs a new QVD file writer.
|
|
@@ -401,8 +452,9 @@ var init_QvdFileWriter = __esm({
|
|
|
401
452
|
this._symbolTable = null;
|
|
402
453
|
this._symbolTableMetadata = null;
|
|
403
454
|
this._indexBuffer = null;
|
|
404
|
-
this.
|
|
455
|
+
this._symbolIndexByValue = null;
|
|
405
456
|
this._indexTableMetadata = null;
|
|
457
|
+
this._recordCount = 0;
|
|
406
458
|
this._recordByteSize = null;
|
|
407
459
|
}
|
|
408
460
|
/**
|
|
@@ -427,9 +479,9 @@ var init_QvdFileWriter = __esm({
|
|
|
427
479
|
* Writes the data to the QVD file.
|
|
428
480
|
*/
|
|
429
481
|
async _writeData() {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
482
|
+
assert2__default.default(this._header, "The QVD file header has not been parsed.");
|
|
483
|
+
assert2__default.default(this._symbolBuffer, "The QVD file symbol table has not been parsed.");
|
|
484
|
+
assert2__default.default(this._indexBuffer, "The QVD file index table has not been parsed.");
|
|
433
485
|
this._emitProgress("write", 0, 1);
|
|
434
486
|
const headerBuffer = Buffer.concat([Buffer.from(this._header, "utf-8"), Buffer.from([0])]);
|
|
435
487
|
let fd;
|
|
@@ -524,7 +576,7 @@ var init_QvdFileWriter = __esm({
|
|
|
524
576
|
};
|
|
525
577
|
})
|
|
526
578
|
},
|
|
527
|
-
NoOfRecords: this.
|
|
579
|
+
NoOfRecords: this._recordCount,
|
|
528
580
|
RecordByteSize: this._recordByteSize,
|
|
529
581
|
Offset: this._symbolTableMetadata && this._symbolTableMetadata.length > 0 ? this._symbolTableMetadata[this._symbolTableMetadata.length - 1][0] + this._symbolTableMetadata[this._symbolTableMetadata.length - 1][1] : 0,
|
|
530
582
|
Length: this._indexBuffer?.length
|
|
@@ -563,128 +615,133 @@ var init_QvdFileWriter = __esm({
|
|
|
563
615
|
_buildSymbolTable() {
|
|
564
616
|
this._symbolTable = [];
|
|
565
617
|
this._symbolTableMetadata = [];
|
|
566
|
-
this.
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
this._df.columns.forEach((column, columnIndex) => {
|
|
572
|
-
const value = row[columnIndex];
|
|
573
|
-
uniqueValuesSets[columnIndex].add(value);
|
|
618
|
+
this._symbolIndexByValue = [];
|
|
619
|
+
if (this._df.columns.length === 0) {
|
|
620
|
+
throw new exports.QvdValidationError("A QVD file must have at least one field", {
|
|
621
|
+
file: this._path,
|
|
622
|
+
stage: "buildSymbolTable"
|
|
574
623
|
});
|
|
575
|
-
}
|
|
576
|
-
this._df.columns
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
|
|
624
|
+
}
|
|
625
|
+
const columns = this._df.columns;
|
|
626
|
+
const data = this._df.data;
|
|
627
|
+
const numColumns = columns.length;
|
|
628
|
+
const numRows = data.length;
|
|
629
|
+
this._emitProgress("symbol-table", 0, numColumns);
|
|
630
|
+
const indexByValue = columns.map(() => /* @__PURE__ */ new Map());
|
|
631
|
+
const containsNull = columns.map(() => false);
|
|
632
|
+
for (let row = 0; row < numRows; row++) {
|
|
633
|
+
const values = data[row];
|
|
634
|
+
for (let column = 0; column < numColumns; column++) {
|
|
635
|
+
const value = values?.[column];
|
|
636
|
+
if (value === null || value === void 0) {
|
|
637
|
+
containsNull[column] = true;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const map = indexByValue[column];
|
|
641
|
+
if (!map.has(value)) {
|
|
642
|
+
map.set(value, map.size);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const columnBuffers = [];
|
|
647
|
+
let symbolsOffset = 0;
|
|
648
|
+
for (let column = 0; column < numColumns; column++) {
|
|
649
|
+
const symbols = Array.from(indexByValue[column].keys(), (value) => _QvdFileWriter._convertRawToSymbol(value));
|
|
650
|
+
const columnBuffer = Buffer.concat(symbols.map((symbol) => symbol.toByteRepresentation()));
|
|
651
|
+
columnBuffers.push(columnBuffer);
|
|
652
|
+
this._symbolTableMetadata?.push([symbolsOffset, columnBuffer.length, containsNull[column]]);
|
|
586
653
|
this._symbolTable?.push(symbols);
|
|
587
|
-
this.
|
|
588
|
-
|
|
654
|
+
this._symbolIndexByValue.push(indexByValue[column]);
|
|
655
|
+
symbolsOffset += columnBuffer.length;
|
|
656
|
+
this._emitProgress("symbol-table", column + 1, numColumns);
|
|
657
|
+
}
|
|
658
|
+
this._symbolBuffer = Buffer.concat(columnBuffers);
|
|
589
659
|
}
|
|
590
660
|
/**
|
|
591
|
-
* Builds the index table of the QVD file.
|
|
661
|
+
* Builds the bit-packed index table of the QVD file.
|
|
592
662
|
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
663
|
+
* A record is `recordByteSize` bytes. Each column occupies `bitWidth` consecutive bits at
|
|
664
|
+
* `bitOffset`, least significant bit first, where bit `p` of a record is
|
|
665
|
+
* `(byte[p >> 3] >> (p & 7)) & 1`. `Bias` is -2 for a column containing NULLs: stored index 0
|
|
666
|
+
* means NULL, and real symbols start at 2. This is exactly what the reader's
|
|
667
|
+
* `decodeIndexColumn` undoes, and `writeBitField` is its inverse.
|
|
596
668
|
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
* -
|
|
604
|
-
*
|
|
605
|
-
* - Reduces file size significantly for columns with few unique values
|
|
669
|
+
* The previous implementation went the long way round. Per cell it converted the raw value to
|
|
670
|
+
* a QvdSymbol - a second time, the symbol table pass having already done it - to build a
|
|
671
|
+
* template-literal lookup key, then turned the resulting index into a string of bits via
|
|
672
|
+
* `toString(2).split('').map().reverse().concat().slice().reverse()`. Per row it kept an array
|
|
673
|
+
* of those strings, padded each with `padStart`, reversed and joined them, split the result
|
|
674
|
+
* with `/.{1,8}/g`, and allocated a Buffer; then `Buffer.concat` over one Buffer per row. On
|
|
675
|
+
* the 1.7M-row taxi fixture that is 34 million symbols, 34 million key strings, 34 million bit
|
|
676
|
+
* strings and 1.7 million Buffers, and it accounted for 98% of a write.
|
|
606
677
|
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
678
|
+
* Now: the widths are derived from the symbol counts, the offsets by prefix sum, one Buffer is
|
|
679
|
+
* allocated for the whole table, and each value is OR-ed into place as an integer.
|
|
609
680
|
*
|
|
610
681
|
* @private
|
|
611
682
|
*/
|
|
612
683
|
_buildIndexTable() {
|
|
613
|
-
this.
|
|
684
|
+
assert2__default.default(this._symbolTable, "The QVD file symbol table has not been built.");
|
|
685
|
+
assert2__default.default(this._symbolTableMetadata, "The QVD file symbol table metadata has not been built.");
|
|
686
|
+
assert2__default.default(this._symbolIndexByValue, "The QVD file symbol index has not been built.");
|
|
614
687
|
this._indexTableMetadata = [];
|
|
615
|
-
|
|
616
|
-
const
|
|
688
|
+
const columns = this._df.columns;
|
|
689
|
+
const data = this._df.data;
|
|
690
|
+
const numRows = data.length;
|
|
691
|
+
const numColumns = columns.length;
|
|
692
|
+
this._recordCount = numRows;
|
|
617
693
|
this._emitProgress("index-table", 0, numRows);
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const key = `${symbol.intValue}|${symbol.doubleValue}|${symbol.stringValue}`;
|
|
637
|
-
const symbolIndex = symbolIndexMaps?.[columnIndex].get(key);
|
|
638
|
-
return fieldContainsNull ? (symbolIndex ?? 0) + 2 : symbolIndex ?? 0;
|
|
694
|
+
const layout = [];
|
|
695
|
+
let totalBits = 0;
|
|
696
|
+
for (let column = 0; column < numColumns; column++) {
|
|
697
|
+
const fieldContainsNull = this._symbolTableMetadata[column][2];
|
|
698
|
+
const symbolCount = this._symbolTable[column].length;
|
|
699
|
+
const nullShift = fieldContainsNull ? 2 : 0;
|
|
700
|
+
const maxStoredIndex = symbolCount === 0 ? 0 : symbolCount - 1 + nullShift;
|
|
701
|
+
const bitWidth = maxStoredIndex === 0 ? 0 : 32 - Math.clz32(maxStoredIndex);
|
|
702
|
+
for (const index of this._symbolIndexByValue[column].values()) {
|
|
703
|
+
if (index + nullShift > maxStoredIndex) {
|
|
704
|
+
throw new exports.QvdValidationError("The symbol table and the index table are out of sync", {
|
|
705
|
+
field: columns[column],
|
|
706
|
+
storedIndex: index + nullShift,
|
|
707
|
+
maxStoredIndex,
|
|
708
|
+
symbolCount,
|
|
709
|
+
file: this._path,
|
|
710
|
+
stage: "buildIndexTable"
|
|
711
|
+
});
|
|
639
712
|
}
|
|
640
|
-
});
|
|
641
|
-
const stringIndices = indices.map((index) => {
|
|
642
|
-
const bits = _QvdFileWriter._convertInt32ToBits(index, 32);
|
|
643
|
-
let bitString = bits.join("");
|
|
644
|
-
bitString = bitString.replace(/^0+/, "") || "0";
|
|
645
|
-
return bitString;
|
|
646
|
-
});
|
|
647
|
-
this._indexTable?.push(stringIndices);
|
|
648
|
-
processedRows++;
|
|
649
|
-
if (processedRows % progressInterval === 0 || processedRows === numRows) {
|
|
650
|
-
this._emitProgress("index-table", processedRows, numRows);
|
|
651
713
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
`Stored index for column '${column}' needs ${bitString.length} bits, but BitWidth was derived as ${bitWidth} from ${symbolCount} symbols. The symbol table and the index table are out of sync.`
|
|
667
|
-
);
|
|
668
|
-
indices[columnIndex] = bitWidth === 0 ? "" : bitString.padStart(bitWidth, "0");
|
|
669
|
-
});
|
|
670
|
-
});
|
|
671
|
-
this._indexBuffer = Buffer.concat(
|
|
672
|
-
// @ts-ignore - Buffer array type compatibility
|
|
673
|
-
this._indexTable.map((indices) => {
|
|
674
|
-
indices.reverse();
|
|
675
|
-
const bits = indices.join("");
|
|
676
|
-
if (bits.length === 0) {
|
|
677
|
-
return Buffer.from(Uint8Array.from([0]));
|
|
714
|
+
layout.push({ geometry: fieldGeometry(totalBits, bitWidth), nullShift });
|
|
715
|
+
this._indexTableMetadata.push([totalBits, bitWidth, fieldContainsNull ? -2 : 0]);
|
|
716
|
+
totalBits += bitWidth;
|
|
717
|
+
}
|
|
718
|
+
const recordByteSize = Math.max(1, Math.ceil(totalBits / 8));
|
|
719
|
+
this._recordByteSize = recordByteSize;
|
|
720
|
+
this._indexBuffer = Buffer.alloc(numRows * recordByteSize);
|
|
721
|
+
const progressInterval = Math.max(1, Math.floor(numRows / 100));
|
|
722
|
+
for (let row = 0, recordBase = 0; row < numRows; row++, recordBase += recordByteSize) {
|
|
723
|
+
const values = data[row];
|
|
724
|
+
for (let column = 0; column < numColumns; column++) {
|
|
725
|
+
const value = values?.[column];
|
|
726
|
+
if (value === null || value === void 0) {
|
|
727
|
+
continue;
|
|
678
728
|
}
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
729
|
+
const index = this._symbolIndexByValue[column].get(value);
|
|
730
|
+
if (index === void 0) {
|
|
731
|
+
throw new exports.QvdValidationError("A value is missing from the symbol table", {
|
|
732
|
+
field: columns[column],
|
|
733
|
+
row,
|
|
734
|
+
file: this._path,
|
|
735
|
+
stage: "buildIndexTable"
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
writeBitField(this._indexBuffer, recordBase, layout[column].geometry, index + layout[column].nullShift);
|
|
739
|
+
}
|
|
740
|
+
if ((row + 1) % progressInterval === 0 || row + 1 === numRows) {
|
|
741
|
+
this._emitProgress("index-table", row + 1, numRows);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
this._symbolIndexByValue = null;
|
|
688
745
|
}
|
|
689
746
|
/**
|
|
690
747
|
* Converts a raw value/literal to a QVD symbol.
|
|
@@ -709,16 +766,6 @@ var init_QvdFileWriter = __esm({
|
|
|
709
766
|
return exports.QvdSymbol.fromStringValue(raw);
|
|
710
767
|
}
|
|
711
768
|
}
|
|
712
|
-
/**
|
|
713
|
-
* Converts an integer to a list of bits.
|
|
714
|
-
*
|
|
715
|
-
* @param {number} value The integer value to convert.
|
|
716
|
-
* @param {number} width The width of the bit list.
|
|
717
|
-
* @return {Array<number>} The list of bits.
|
|
718
|
-
*/
|
|
719
|
-
static _convertInt32ToBits(value, width) {
|
|
720
|
-
return value.toString(2).split("").map((bit) => parseInt(bit)).reverse().concat(new Array(width).fill(0)).slice(0, width).reverse();
|
|
721
|
-
}
|
|
722
769
|
/**
|
|
723
770
|
* Persists the data frame to a QVD file.
|
|
724
771
|
*/
|
|
@@ -731,18 +778,6 @@ var init_QvdFileWriter = __esm({
|
|
|
731
778
|
};
|
|
732
779
|
}
|
|
733
780
|
});
|
|
734
|
-
|
|
735
|
-
// src/util/bitUtils.js
|
|
736
|
-
function convertBitsToInt32(bits) {
|
|
737
|
-
if (bits.length === 0) {
|
|
738
|
-
return 0;
|
|
739
|
-
}
|
|
740
|
-
return bits.reduce((value, bit, index) => value += bit * Math.pow(2, index), 0);
|
|
741
|
-
}
|
|
742
|
-
var init_bitUtils = __esm({
|
|
743
|
-
"src/util/bitUtils.js"() {
|
|
744
|
-
}
|
|
745
|
-
});
|
|
746
781
|
function getHeapLimit() {
|
|
747
782
|
return v8__default.default.getHeapStatistics().heap_size_limit;
|
|
748
783
|
}
|
|
@@ -752,7 +787,7 @@ function heapLimitIsMeaningful() {
|
|
|
752
787
|
function getMemoryBudget() {
|
|
753
788
|
const candidates = [];
|
|
754
789
|
if (heapLimitIsMeaningful()) {
|
|
755
|
-
candidates.push({ source: "V8 heap limit", bytes:
|
|
790
|
+
candidates.push({ source: "V8 heap limit", bytes: usableOldSpaceLimit() });
|
|
756
791
|
}
|
|
757
792
|
const constrained = typeof process.constrainedMemory === "function" ? process.constrainedMemory() : 0;
|
|
758
793
|
if (constrained > 0 && constrained < os__default.default.totalmem()) {
|
|
@@ -768,19 +803,56 @@ function getMemoryBudget() {
|
|
|
768
803
|
const binding = candidates.reduce((lowest, candidate) => candidate.bytes < lowest.bytes ? candidate : lowest);
|
|
769
804
|
return { bytes: binding.bytes, limitedBy: binding.source, candidates, observed };
|
|
770
805
|
}
|
|
771
|
-
function
|
|
806
|
+
function usableOldSpaceLimit() {
|
|
807
|
+
const usable = getHeapLimit() - HEAP_LIMIT_OVERSTATEMENT_BYTES;
|
|
808
|
+
return usable > MINIMUM_BUDGET_BYTES ? usable : MINIMUM_BUDGET_BYTES;
|
|
809
|
+
}
|
|
810
|
+
function estimateExternalMemory(rows, columnCount) {
|
|
811
|
+
if (!columnCount || columnCount <= 0 || !rows || rows <= 0) {
|
|
812
|
+
return 0;
|
|
813
|
+
}
|
|
814
|
+
return rows * columnCount * Int32Array.BYTES_PER_ELEMENT;
|
|
815
|
+
}
|
|
816
|
+
function estimateRowMemory(rows, columnCount) {
|
|
817
|
+
if (!columnCount || columnCount <= 0) {
|
|
818
|
+
return 0;
|
|
819
|
+
}
|
|
820
|
+
return BASE_BYTES + rows * (ROW_BASE_BYTES + PER_CELL_BYTES * columnCount);
|
|
821
|
+
}
|
|
822
|
+
function estimateMemoryUsage(symbolTableSize, maxRows, totalRows, columnCount = 0, materialisesRows = true) {
|
|
772
823
|
const FULL_PARSE_OVERHEAD = 6;
|
|
773
824
|
const MINIMAL_OVERHEAD = 0.01;
|
|
825
|
+
const rowsToLoad = maxRows === null || maxRows >= totalRows ? totalRows : maxRows;
|
|
826
|
+
const rowMemory = materialisesRows ? estimateRowMemory(rowsToLoad, columnCount) : BASE_BYTES;
|
|
774
827
|
if (maxRows === null || maxRows >= totalRows) {
|
|
775
|
-
return symbolTableSize * FULL_PARSE_OVERHEAD;
|
|
828
|
+
return symbolTableSize * FULL_PARSE_OVERHEAD + rowMemory;
|
|
776
829
|
}
|
|
777
830
|
const rowPercentage = maxRows / totalRows;
|
|
778
831
|
const symbolPercentage = Math.sqrt(rowPercentage);
|
|
779
832
|
const keptSymbolsMemory = symbolTableSize * symbolPercentage * FULL_PARSE_OVERHEAD;
|
|
780
833
|
const skippedSymbolsMemory = symbolTableSize * (1 - symbolPercentage) * MINIMAL_OVERHEAD;
|
|
781
|
-
return keptSymbolsMemory + skippedSymbolsMemory;
|
|
834
|
+
return keptSymbolsMemory + skippedSymbolsMemory + rowMemory;
|
|
782
835
|
}
|
|
783
|
-
function
|
|
836
|
+
function recommendedRowsFor(budget, symbolTableSize, totalRows, columnCount, materialisesRows = true) {
|
|
837
|
+
if (estimateMemoryUsage(symbolTableSize, totalRows, totalRows, columnCount, materialisesRows) <= budget) {
|
|
838
|
+
return totalRows;
|
|
839
|
+
}
|
|
840
|
+
if (estimateMemoryUsage(symbolTableSize, 0, totalRows, columnCount, materialisesRows) > budget) {
|
|
841
|
+
return 0;
|
|
842
|
+
}
|
|
843
|
+
let low = 0;
|
|
844
|
+
let high = totalRows;
|
|
845
|
+
while (high - low > 1) {
|
|
846
|
+
const mid = Math.floor((low + high) / 2);
|
|
847
|
+
if (estimateMemoryUsage(symbolTableSize, mid, totalRows, columnCount, materialisesRows) <= budget) {
|
|
848
|
+
low = mid;
|
|
849
|
+
} else {
|
|
850
|
+
high = mid;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return low;
|
|
854
|
+
}
|
|
855
|
+
function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.8, columnCount = 0, materialisesRows = true) {
|
|
784
856
|
if (typeof safetyFactor !== "number" || safetyFactor < 0 || safetyFactor > 1) {
|
|
785
857
|
throw new exports.QvdValidationError("safetyFactor must be a number between 0.0 and 1.0", { safetyFactor });
|
|
786
858
|
}
|
|
@@ -788,25 +860,57 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
788
860
|
return;
|
|
789
861
|
}
|
|
790
862
|
const budget = getMemoryBudget();
|
|
863
|
+
const rowsToLoad = maxRows === null || maxRows >= totalRows ? totalRows : maxRows;
|
|
864
|
+
const heapMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows, columnCount, materialisesRows);
|
|
865
|
+
const externalMemory = estimateExternalMemory(rowsToLoad, columnCount);
|
|
866
|
+
const bounded = budget.candidates.map((candidate) => {
|
|
867
|
+
const heapOnly = candidate.source === "V8 heap limit";
|
|
868
|
+
return {
|
|
869
|
+
...candidate,
|
|
870
|
+
needs: heapOnly ? heapMemory : heapMemory + externalMemory,
|
|
871
|
+
allowed: candidate.bytes * safetyFactor,
|
|
872
|
+
bounds: heapOnly ? "the V8 heap" : "the whole process"
|
|
873
|
+
};
|
|
874
|
+
});
|
|
875
|
+
const exceeded = bounded.filter((candidate) => candidate.needs > candidate.allowed);
|
|
876
|
+
const binding = exceeded.reduce(
|
|
877
|
+
(worst, candidate) => candidate.needs / candidate.allowed > worst.needs / worst.allowed ? candidate : worst,
|
|
878
|
+
exceeded[0]
|
|
879
|
+
);
|
|
791
880
|
const heapLimit = getHeapLimit();
|
|
792
|
-
const availableMemory = budget.bytes;
|
|
793
|
-
const estimatedMemory =
|
|
794
|
-
const maxAllowedMemory = budget.bytes * safetyFactor;
|
|
795
|
-
if (
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
881
|
+
const availableMemory = binding ? binding.bytes : budget.bytes;
|
|
882
|
+
const estimatedMemory = binding ? binding.needs : heapMemory;
|
|
883
|
+
const maxAllowedMemory = binding ? binding.allowed : budget.bytes * safetyFactor;
|
|
884
|
+
if (binding) {
|
|
885
|
+
const recommendedMaxRows = recommendedRowsFor(
|
|
886
|
+
maxAllowedMemory,
|
|
887
|
+
symbolTableSize,
|
|
888
|
+
totalRows,
|
|
889
|
+
columnCount,
|
|
890
|
+
materialisesRows
|
|
891
|
+
);
|
|
799
892
|
const sizeMB = Math.round(symbolTableSize / 1024 / 1024);
|
|
800
893
|
const estimatedMB = Math.round(estimatedMemory / 1024 / 1024);
|
|
801
894
|
const availableMB = Math.round(maxAllowedMemory / 1024 / 1024);
|
|
802
|
-
const heapLimitMB = Math.round(
|
|
895
|
+
const heapLimitMB = Math.round(usableOldSpaceLimit() / 1024 / 1024);
|
|
896
|
+
const reportedHeapLimitMB = Math.round(heapLimit / 1024 / 1024);
|
|
803
897
|
const availableRamMB = Math.round(availableMemory / 1024 / 1024);
|
|
804
|
-
const limitingFactor =
|
|
898
|
+
const limitingFactor = binding.source;
|
|
899
|
+
const limitingScope = binding.bounds;
|
|
805
900
|
const budgetBreakdown = budget.candidates.map((candidate) => `${candidate.source} ${Math.round(candidate.bytes / 1024 / 1024)}MB`).join(", ");
|
|
806
901
|
const observedBreakdown = budget.observed.map((entry) => `${entry.source} ${Math.round(entry.bytes / 1024 / 1024)}MB`).join(", ");
|
|
807
|
-
const
|
|
902
|
+
const nothingFits = recommendedMaxRows === 0;
|
|
903
|
+
const containerBound = binding.source === "container memory limit";
|
|
904
|
+
let advice;
|
|
905
|
+
if (nothingFits) {
|
|
906
|
+
advice = `No row count fits this budget - the symbol table alone exceeds it, so maxRows cannot help. ` + (containerBound ? `Raise the container's memory limit.` : `Raise the heap with --max-old-space-size, or raise memorySafetyFactor.`);
|
|
907
|
+
} 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 load fewer rows with maxRows (recommended: ${recommendedMaxRows.toLocaleString()} rows or less).`;
|
|
909
|
+
} else {
|
|
910
|
+
advice = `Try loading fewer rows using the maxRows parameter (recommended: ${recommendedMaxRows.toLocaleString()} rows or less), or raise the heap with --max-old-space-size.`;
|
|
911
|
+
}
|
|
808
912
|
throw new exports.QvdValidationError(
|
|
809
|
-
`Insufficient memory to load file safely. Symbol table: ${sizeMB}MB, Estimated memory needed: ${estimatedMB}MB, Available: ${availableMB}MB (limited by ${limitingFactor}; considered: ${budgetBreakdown}; observed but not used: ${observedBreakdown}). ` + advice,
|
|
913
|
+
`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,
|
|
810
914
|
{
|
|
811
915
|
file: filePath,
|
|
812
916
|
symbolTableSize,
|
|
@@ -814,10 +918,13 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
814
918
|
estimatedMemoryMB: estimatedMB,
|
|
815
919
|
availableMemoryMB: availableMB,
|
|
816
920
|
heapLimitMB,
|
|
921
|
+
reportedHeapLimitMB,
|
|
817
922
|
availableRamMB,
|
|
818
923
|
limitingFactor,
|
|
924
|
+
limitingScope,
|
|
819
925
|
memoryBudget: budget.candidates,
|
|
820
926
|
memoryObserved: budget.observed,
|
|
927
|
+
columnCount,
|
|
821
928
|
totalRows,
|
|
822
929
|
maxRows,
|
|
823
930
|
recommendedMaxRows
|
|
@@ -825,12 +932,11 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
825
932
|
);
|
|
826
933
|
}
|
|
827
934
|
}
|
|
828
|
-
function warnLargeSymbolTable(symbolTableSize, maxRows, totalRows) {
|
|
829
|
-
const
|
|
830
|
-
const LARGE_SYMBOL_TABLE_WARNING = heapLimit * 0.125;
|
|
935
|
+
function warnLargeSymbolTable(symbolTableSize, maxRows, totalRows, columnCount = 0, materialisesRows = true) {
|
|
936
|
+
const LARGE_SYMBOL_TABLE_WARNING = usableOldSpaceLimit() * 0.125;
|
|
831
937
|
if (symbolTableSize > LARGE_SYMBOL_TABLE_WARNING && (maxRows === null || maxRows >= totalRows)) {
|
|
832
938
|
const sizeMB = Math.round(symbolTableSize / 1024 / 1024);
|
|
833
|
-
const estimatedMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows);
|
|
939
|
+
const estimatedMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows, columnCount, materialisesRows);
|
|
834
940
|
const estimatedMB = Math.round(estimatedMemory / 1024 / 1024);
|
|
835
941
|
const warnMB = Math.round(LARGE_SYMBOL_TABLE_WARNING / 1024 / 1024);
|
|
836
942
|
console.warn(
|
|
@@ -838,9 +944,15 @@ function warnLargeSymbolTable(symbolTableSize, maxRows, totalRows) {
|
|
|
838
944
|
);
|
|
839
945
|
}
|
|
840
946
|
}
|
|
947
|
+
var HEAP_LIMIT_OVERSTATEMENT_BYTES, MINIMUM_BUDGET_BYTES, BASE_BYTES, ROW_BASE_BYTES, PER_CELL_BYTES;
|
|
841
948
|
var init_memoryUtils = __esm({
|
|
842
949
|
"src/util/memoryUtils.js"() {
|
|
843
950
|
init_QvdErrors();
|
|
951
|
+
HEAP_LIMIT_OVERSTATEMENT_BYTES = 192 * 1024 * 1024;
|
|
952
|
+
MINIMUM_BUDGET_BYTES = 64 * 1024 * 1024;
|
|
953
|
+
BASE_BYTES = 16 * 1024 * 1024;
|
|
954
|
+
ROW_BASE_BYTES = 72;
|
|
955
|
+
PER_CELL_BYTES = 8;
|
|
844
956
|
}
|
|
845
957
|
});
|
|
846
958
|
|
|
@@ -918,6 +1030,15 @@ function validateFieldMetadata(field, symbolBufferLength, filePath) {
|
|
|
918
1030
|
});
|
|
919
1031
|
}
|
|
920
1032
|
}
|
|
1033
|
+
function validateRecordCount(totalRows, filePath, stage = "parseIndexTable") {
|
|
1034
|
+
if (isNaN(totalRows) || !Number.isSafeInteger(totalRows) || totalRows < 0) {
|
|
1035
|
+
throw new exports.QvdCorruptedError("Invalid number of records", {
|
|
1036
|
+
totalRows,
|
|
1037
|
+
file: filePath,
|
|
1038
|
+
stage
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
921
1042
|
function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, indexTableOffset, bufferLength, rowsToLoad, filePath, fileSize = null) {
|
|
922
1043
|
if (isNaN(recordSize) || !Number.isSafeInteger(recordSize) || recordSize < 0) {
|
|
923
1044
|
throw new exports.QvdCorruptedError("Invalid record byte size", {
|
|
@@ -926,13 +1047,7 @@ function validateIndexTableMetadata(recordSize, totalRows, indexTableLength, ind
|
|
|
926
1047
|
stage: "parseIndexTable"
|
|
927
1048
|
});
|
|
928
1049
|
}
|
|
929
|
-
|
|
930
|
-
throw new exports.QvdCorruptedError("Invalid number of records", {
|
|
931
|
-
totalRows,
|
|
932
|
-
file: filePath,
|
|
933
|
-
stage: "parseIndexTable"
|
|
934
|
-
});
|
|
935
|
-
}
|
|
1050
|
+
validateRecordCount(totalRows, filePath);
|
|
936
1051
|
if (recordSize === 0 && totalRows > 0) {
|
|
937
1052
|
throw new exports.QvdCorruptedError("Record byte size cannot be zero when records exist", {
|
|
938
1053
|
recordSize,
|
|
@@ -1028,6 +1143,24 @@ function validateFieldBitMetadata(field, recordSize, filePath) {
|
|
|
1028
1143
|
stage: "parseIndexTable"
|
|
1029
1144
|
});
|
|
1030
1145
|
}
|
|
1146
|
+
const bias = parseInt(field["Bias"], 10);
|
|
1147
|
+
if (isNaN(bias) || !Number.isSafeInteger(bias)) {
|
|
1148
|
+
throw new exports.QvdCorruptedError("Invalid bias", {
|
|
1149
|
+
field: field["FieldName"],
|
|
1150
|
+
bias: field["Bias"],
|
|
1151
|
+
file: filePath,
|
|
1152
|
+
stage: "parseIndexTable"
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
if (bitWidth > MAX_BIT_WIDTH) {
|
|
1156
|
+
throw new exports.QvdCorruptedError("Bit width exceeds maximum", {
|
|
1157
|
+
field: field["FieldName"],
|
|
1158
|
+
bitWidth,
|
|
1159
|
+
maxBitWidth: MAX_BIT_WIDTH,
|
|
1160
|
+
file: filePath,
|
|
1161
|
+
stage: "parseIndexTable"
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1031
1164
|
const recordSizeInBits = recordSize * 8;
|
|
1032
1165
|
if (bitOffset + bitWidth > recordSizeInBits) {
|
|
1033
1166
|
throw new exports.QvdCorruptedError("Bit field extends beyond record size", {
|
|
@@ -1044,6 +1177,7 @@ var init_validationUtils = __esm({
|
|
|
1044
1177
|
"src/util/validationUtils.js"() {
|
|
1045
1178
|
init_QvdErrors();
|
|
1046
1179
|
init_memoryUtils();
|
|
1180
|
+
init_bitUtils();
|
|
1047
1181
|
}
|
|
1048
1182
|
});
|
|
1049
1183
|
|
|
@@ -1303,6 +1437,273 @@ var init_symbolParser = __esm({
|
|
|
1303
1437
|
}
|
|
1304
1438
|
});
|
|
1305
1439
|
|
|
1440
|
+
// src/QvdColumnTable.js
|
|
1441
|
+
var QvdColumnTable_exports = {};
|
|
1442
|
+
__export(QvdColumnTable_exports, {
|
|
1443
|
+
QvdColumn: () => exports.QvdColumn,
|
|
1444
|
+
QvdColumnTable: () => exports.QvdColumnTable
|
|
1445
|
+
});
|
|
1446
|
+
exports.QvdColumn = void 0; exports.QvdColumnTable = void 0;
|
|
1447
|
+
var init_QvdColumnTable = __esm({
|
|
1448
|
+
"src/QvdColumnTable.js"() {
|
|
1449
|
+
init_QvdErrors();
|
|
1450
|
+
exports.QvdColumn = class {
|
|
1451
|
+
/**
|
|
1452
|
+
* @param {string} name The field name.
|
|
1453
|
+
* @param {Int32Array} codes One stored index per row, bias applied. Negative means NULL.
|
|
1454
|
+
* @param {Array<any>} symbols The field's distinct values, indexed by code.
|
|
1455
|
+
*/
|
|
1456
|
+
constructor(name, codes, symbols) {
|
|
1457
|
+
this._name = name;
|
|
1458
|
+
this._codes = codes;
|
|
1459
|
+
this._symbols = symbols;
|
|
1460
|
+
Object.freeze(this);
|
|
1461
|
+
}
|
|
1462
|
+
/** @return {string} The field name. */
|
|
1463
|
+
get name() {
|
|
1464
|
+
return this._name;
|
|
1465
|
+
}
|
|
1466
|
+
/** @return {number} Rows in the column. */
|
|
1467
|
+
get length() {
|
|
1468
|
+
return this._codes.length;
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* The stored index of each row. Negative means NULL.
|
|
1472
|
+
*
|
|
1473
|
+
* The table's own array, not a copy - it is the thing that makes this cheap, and copying it
|
|
1474
|
+
* per call would defeat the point. Treat it as read-only.
|
|
1475
|
+
*
|
|
1476
|
+
* @return {Int32Array} One code per row.
|
|
1477
|
+
*/
|
|
1478
|
+
get codes() {
|
|
1479
|
+
return this._codes;
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* The field's distinct values, indexed by the codes.
|
|
1483
|
+
*
|
|
1484
|
+
* One entry per distinct value, not per row: a few thousand entries for a column of millions.
|
|
1485
|
+
*
|
|
1486
|
+
* @return {ReadonlyArray<any>} The dictionary.
|
|
1487
|
+
*/
|
|
1488
|
+
get symbols() {
|
|
1489
|
+
return this._symbols;
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* The value of one row.
|
|
1493
|
+
*
|
|
1494
|
+
* @param {number} row The row index.
|
|
1495
|
+
* @return {any} The value, or null where the file stores NULL.
|
|
1496
|
+
* @throws {QvdValidationError} If the row is not an integer within the column.
|
|
1497
|
+
*/
|
|
1498
|
+
at(row) {
|
|
1499
|
+
if (!Number.isInteger(row) || row < 0 || row >= this._codes.length) {
|
|
1500
|
+
throw new exports.QvdValidationError("Row index out of bounds", {
|
|
1501
|
+
column: this._name,
|
|
1502
|
+
row,
|
|
1503
|
+
length: this._codes.length
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
const code = this._codes[row];
|
|
1507
|
+
return code < 0 ? null : this._symbols[code];
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Iterates the column's values without materialising it.
|
|
1511
|
+
*
|
|
1512
|
+
* @return {Iterator<any>} An iterator over the values, NULLs included as null.
|
|
1513
|
+
*/
|
|
1514
|
+
[Symbol.iterator]() {
|
|
1515
|
+
const codes = this._codes;
|
|
1516
|
+
const symbols = this._symbols;
|
|
1517
|
+
let row = 0;
|
|
1518
|
+
return {
|
|
1519
|
+
next() {
|
|
1520
|
+
if (row >= codes.length) {
|
|
1521
|
+
return { done: true, value: void 0 };
|
|
1522
|
+
}
|
|
1523
|
+
const code = codes[row++];
|
|
1524
|
+
return { done: false, value: code < 0 ? null : symbols[code] };
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* The column as a plain array of values, one per row.
|
|
1530
|
+
*
|
|
1531
|
+
* Lossless, and cell-for-cell what `QvdDataFrame.data[row][column]` would hold. The caller
|
|
1532
|
+
* owns the result; the table keeps no reference to it, so two calls return two arrays.
|
|
1533
|
+
*
|
|
1534
|
+
* @return {Array<any>} One value per row.
|
|
1535
|
+
*/
|
|
1536
|
+
toArray() {
|
|
1537
|
+
const out = new Array(this._codes.length);
|
|
1538
|
+
for (let row = 0; row < this._codes.length; row++) {
|
|
1539
|
+
const code = this._codes[row];
|
|
1540
|
+
out[row] = code < 0 ? null : this._symbols[code];
|
|
1541
|
+
}
|
|
1542
|
+
return out;
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* The dictionary as numbers, for scanning without materialising the column.
|
|
1546
|
+
*
|
|
1547
|
+
* One entry per *distinct* value, not per row - 17 KB for a column of 1.7 million rows with
|
|
1548
|
+
* 2,188 symbols - so this is the cheap conversion, where `toFloat64Array()` is the expensive
|
|
1549
|
+
* one. Non-numeric symbols become NaN, which is safe here in a way it is not per row: the
|
|
1550
|
+
* codes still distinguish NULL, and a caller that wants the blank back still has `symbols`.
|
|
1551
|
+
*
|
|
1552
|
+
* Scanning `codes` against this is the fastest way to read a column, because both sides are
|
|
1553
|
+
* contiguous typed arrays and the dictionary fits in cache:
|
|
1554
|
+
*
|
|
1555
|
+
* ```js
|
|
1556
|
+
* const codes = column.codes;
|
|
1557
|
+
* const values = column.numericSymbols();
|
|
1558
|
+
* let total = 0;
|
|
1559
|
+
* for (let row = 0; row < codes.length; row++) {
|
|
1560
|
+
* const code = codes[row];
|
|
1561
|
+
* if (code >= 0) {
|
|
1562
|
+
* const value = values[code];
|
|
1563
|
+
* if (!Number.isNaN(value)) total += value;
|
|
1564
|
+
* }
|
|
1565
|
+
* }
|
|
1566
|
+
* ```
|
|
1567
|
+
*
|
|
1568
|
+
* @return {Float64Array} One number per distinct symbol, NaN where the symbol is not a number.
|
|
1569
|
+
*/
|
|
1570
|
+
numericSymbols() {
|
|
1571
|
+
const out = new Float64Array(this._symbols.length);
|
|
1572
|
+
for (let index = 0; index < this._symbols.length; index++) {
|
|
1573
|
+
const value = this._symbols[index];
|
|
1574
|
+
out[index] = typeof value === "number" ? value : NaN;
|
|
1575
|
+
}
|
|
1576
|
+
return out;
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* The column as a `Float64Array`.
|
|
1580
|
+
*
|
|
1581
|
+
* Named for what it costs rather than offered as *the* representation, because it is lossy
|
|
1582
|
+
* and on real data it is lossy often: it cannot distinguish a blank from a number, and the
|
|
1583
|
+
* bundled taxi fixture has a column that is 43% blank. It therefore refuses by default
|
|
1584
|
+
* rather than quietly writing NaN over two fifths of a column.
|
|
1585
|
+
*
|
|
1586
|
+
* @param {Object} [options] Conversion options.
|
|
1587
|
+
* @param {'throw'|'nan'} [options.onNonNumeric='throw'] What to do with a value that is not a
|
|
1588
|
+
* number - including NULL. `'throw'` refuses and names the offending row; `'nan'` writes
|
|
1589
|
+
* NaN, which is the right choice only when the caller knows the column is numeric.
|
|
1590
|
+
* @return {Float64Array} One number per row.
|
|
1591
|
+
* @throws {QvdValidationError} If a value is not a number and `onNonNumeric` is `'throw'`.
|
|
1592
|
+
*/
|
|
1593
|
+
toFloat64Array(options = {}) {
|
|
1594
|
+
const { onNonNumeric = "throw" } = options;
|
|
1595
|
+
if (onNonNumeric !== "throw" && onNonNumeric !== "nan") {
|
|
1596
|
+
throw new exports.QvdValidationError('onNonNumeric must be "throw" or "nan"', {
|
|
1597
|
+
column: this._name,
|
|
1598
|
+
provided: onNonNumeric
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
const out = new Float64Array(this._codes.length);
|
|
1602
|
+
for (let row = 0; row < this._codes.length; row++) {
|
|
1603
|
+
const code = this._codes[row];
|
|
1604
|
+
const value = code < 0 ? null : this._symbols[code];
|
|
1605
|
+
if (typeof value === "number") {
|
|
1606
|
+
out[row] = value;
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
if (onNonNumeric === "throw") {
|
|
1610
|
+
throw new exports.QvdValidationError("Column holds a value that is not a number", {
|
|
1611
|
+
column: this._name,
|
|
1612
|
+
row,
|
|
1613
|
+
value,
|
|
1614
|
+
type: value === null ? "null" : typeof value,
|
|
1615
|
+
hint: 'Pass {onNonNumeric: "nan"} to write NaN instead, or use toArray() to keep the value.'
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
out[row] = NaN;
|
|
1619
|
+
}
|
|
1620
|
+
return out;
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
exports.QvdColumnTable = class {
|
|
1624
|
+
/**
|
|
1625
|
+
* @param {Object} decoded What the reader decoded.
|
|
1626
|
+
* @param {Array<string>} decoded.columns Field names, in file order.
|
|
1627
|
+
* @param {Array<Int32Array>} decoded.codesByField One code array per field.
|
|
1628
|
+
* @param {Array<Array<any>>} decoded.symbolsByField One dictionary per field.
|
|
1629
|
+
* @param {number} decoded.rowCount Rows decoded.
|
|
1630
|
+
* @param {any} decoded.metadata The raw QvdTableHeader.
|
|
1631
|
+
* @param {any} decoded.loadStats Statistics about the read.
|
|
1632
|
+
*/
|
|
1633
|
+
constructor({ columns, codesByField, symbolsByField, rowCount, metadata, loadStats }) {
|
|
1634
|
+
this._columns = columns;
|
|
1635
|
+
this._codesByField = codesByField;
|
|
1636
|
+
this._symbolsByField = symbolsByField;
|
|
1637
|
+
this._rowCount = rowCount;
|
|
1638
|
+
this._metadata = metadata;
|
|
1639
|
+
this._loadStats = loadStats;
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Reads a QVD file as columns.
|
|
1643
|
+
*
|
|
1644
|
+
* @param {string} path The path to the QVD file.
|
|
1645
|
+
* @param {Object} [options] Loading options, with the same meanings they have on `fromQvd`.
|
|
1646
|
+
* @param {number|null} [options.maxRows] Maximum rows to decode.
|
|
1647
|
+
* @param {string} [options.allowedDir] Directory the path must resolve inside.
|
|
1648
|
+
* @param {number} [options.memorySafetyFactor] Fraction of the memory budget a load may use.
|
|
1649
|
+
* @param {number} [options.symbolFilteringThreshold] Symbol table size above which a limited
|
|
1650
|
+
* read switches to two-pass filtering.
|
|
1651
|
+
* @return {Promise<QvdColumnTable>} The file, as columns.
|
|
1652
|
+
*/
|
|
1653
|
+
static async fromQvd(path3, options = {}) {
|
|
1654
|
+
const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
|
|
1655
|
+
const reader = new QvdFileReader2(path3, {
|
|
1656
|
+
allowedDir: options.allowedDir,
|
|
1657
|
+
memorySafetyFactor: options.memorySafetyFactor,
|
|
1658
|
+
symbolFilteringThreshold: options.symbolFilteringThreshold,
|
|
1659
|
+
// This read builds no rows, so the memory guard must not charge it for them. A columnar
|
|
1660
|
+
// read of the 38MB taxi fixture completes in a 15MB heap; charged the row cost it was
|
|
1661
|
+
// refused below a 2GB one.
|
|
1662
|
+
materialisesRows: false
|
|
1663
|
+
});
|
|
1664
|
+
return await reader.loadColumnar(options.maxRows !== void 0 ? options.maxRows : null);
|
|
1665
|
+
}
|
|
1666
|
+
/** @return {Array<string>} Field names, in file order. */
|
|
1667
|
+
get columns() {
|
|
1668
|
+
return this._columns;
|
|
1669
|
+
}
|
|
1670
|
+
/** @return {number} Rows decoded. */
|
|
1671
|
+
get rowCount() {
|
|
1672
|
+
return this._rowCount;
|
|
1673
|
+
}
|
|
1674
|
+
/** @return {Array<number>} `[rows, columns]`, as on a data frame. */
|
|
1675
|
+
get shape() {
|
|
1676
|
+
return [this._rowCount, this._columns.length];
|
|
1677
|
+
}
|
|
1678
|
+
/** @return {any} The raw `QvdTableHeader`. */
|
|
1679
|
+
get metadata() {
|
|
1680
|
+
return this._metadata;
|
|
1681
|
+
}
|
|
1682
|
+
/** @return {any} Statistics about the read. */
|
|
1683
|
+
get loadStats() {
|
|
1684
|
+
return this._loadStats;
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* One column.
|
|
1688
|
+
*
|
|
1689
|
+
* @param {string} name The field name.
|
|
1690
|
+
* @return {QvdColumn} The column.
|
|
1691
|
+
* @throws {QvdValidationError} If the field is not in this file.
|
|
1692
|
+
*/
|
|
1693
|
+
column(name) {
|
|
1694
|
+
const index = this._columns.indexOf(name);
|
|
1695
|
+
if (index === -1) {
|
|
1696
|
+
throw new exports.QvdValidationError(`Column '${name}' does not exist`, {
|
|
1697
|
+
column: name,
|
|
1698
|
+
availableColumns: this._columns
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
return new exports.QvdColumn(name, this._codesByField[index], this._symbolsByField[index]);
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1306
1707
|
// src/QvdFileReader.js
|
|
1307
1708
|
var QvdFileReader_exports = {};
|
|
1308
1709
|
__export(QvdFileReader_exports, {
|
|
@@ -1337,13 +1738,22 @@ var init_QvdFileReader = __esm({
|
|
|
1337
1738
|
* check entirely**, which is the escape hatch for runtimes whose limits cannot be measured -
|
|
1338
1739
|
* Bun reports its current heap as its heap limit - and for callers who would rather manage
|
|
1339
1740
|
* memory themselves than trust the estimate.
|
|
1741
|
+
* @param {boolean} [options.materialisesRows=true] Whether this read will build row arrays.
|
|
1742
|
+
* False for a columnar read, whose memory is TypedArray backing stores outside the V8 heap
|
|
1743
|
+
* and which must not be charged the row cost.
|
|
1340
1744
|
* @param {number} [options.symbolFilteringThreshold=52428800] Symbol table size, in bytes,
|
|
1341
1745
|
* above which a lazy load switches to the two-pass filtering path. The default of 50MB is
|
|
1342
1746
|
* the point where the extra analysis pass pays for itself; lower it to use filtering on
|
|
1343
1747
|
* smaller files, raise it to keep the simpler single-pass read for longer.
|
|
1344
1748
|
*/
|
|
1345
1749
|
constructor(filePath, options = {}) {
|
|
1346
|
-
const {
|
|
1750
|
+
const {
|
|
1751
|
+
allowedDir,
|
|
1752
|
+
memorySafetyFactor = 0.8,
|
|
1753
|
+
symbolFilteringThreshold = 50 * 1024 * 1024,
|
|
1754
|
+
materialisesRows = true
|
|
1755
|
+
} = options;
|
|
1756
|
+
this._materialisesRows = materialisesRows;
|
|
1347
1757
|
this._path = validatePath(filePath, allowedDir);
|
|
1348
1758
|
this._memorySafetyFactor = memorySafetyFactor;
|
|
1349
1759
|
this._symbolFilteringThreshold = symbolFilteringThreshold;
|
|
@@ -1353,8 +1763,10 @@ var init_QvdFileReader = __esm({
|
|
|
1353
1763
|
this._indexTableOffset = null;
|
|
1354
1764
|
this._header = null;
|
|
1355
1765
|
this._symbolTable = null;
|
|
1356
|
-
this.
|
|
1766
|
+
this._indexColumns = null;
|
|
1767
|
+
this._rowsDecoded = 0;
|
|
1357
1768
|
this._fileSize = null;
|
|
1769
|
+
this._headerMatchesFile = false;
|
|
1358
1770
|
}
|
|
1359
1771
|
/**
|
|
1360
1772
|
* Reads the binary data of the QVD file.
|
|
@@ -1381,14 +1793,13 @@ var init_QvdFileReader = __esm({
|
|
|
1381
1793
|
* - Direct byte-range reading for remaining data is fastest
|
|
1382
1794
|
*
|
|
1383
1795
|
* @param {number|null} maxRows The maximum number of rows to load. If null, all data is loaded.
|
|
1796
|
+
* @param {boolean} [headerOnly=false] Stop once the XML header has been read, leaving the
|
|
1797
|
+
* symbol and index tables on disk. This is the metadata-only path: the header is a few
|
|
1798
|
+
* kilobytes whatever the file's size, so reading a schema costs the same for a 40MB file as
|
|
1799
|
+
* for a 40GB one.
|
|
1384
1800
|
* @private
|
|
1385
1801
|
*/
|
|
1386
|
-
async _readData(maxRows = null) {
|
|
1387
|
-
if (maxRows === null) {
|
|
1388
|
-
this._buffer = await fs__default.default.promises.readFile(this._path);
|
|
1389
|
-
this._fileSize = this._buffer.length;
|
|
1390
|
-
return;
|
|
1391
|
-
}
|
|
1802
|
+
async _readData(maxRows = null, headerOnly = false) {
|
|
1392
1803
|
const HEADER_DELIMITER = "\r\n\0";
|
|
1393
1804
|
const CHUNK_SIZE = 64 * 1024;
|
|
1394
1805
|
const stream = fs__default.default.createReadStream(this._path, {
|
|
@@ -1397,7 +1808,6 @@ var init_QvdFileReader = __esm({
|
|
|
1397
1808
|
const headerChunks = [];
|
|
1398
1809
|
let headerBytes = 0;
|
|
1399
1810
|
let tail = Buffer.alloc(0);
|
|
1400
|
-
let headerBuffer = Buffer.alloc(0);
|
|
1401
1811
|
let headerDelimiterIndex = -1;
|
|
1402
1812
|
try {
|
|
1403
1813
|
for await (const chunk of stream) {
|
|
@@ -1441,7 +1851,7 @@ var init_QvdFileReader = __esm({
|
|
|
1441
1851
|
}
|
|
1442
1852
|
);
|
|
1443
1853
|
}
|
|
1444
|
-
headerBuffer = Buffer.concat(headerChunks);
|
|
1854
|
+
const headerBuffer = Buffer.concat(headerChunks);
|
|
1445
1855
|
const headerEndIndex = headerDelimiterIndex + HEADER_DELIMITER.length;
|
|
1446
1856
|
const headerXml = headerBuffer.subarray(0, headerEndIndex).toString();
|
|
1447
1857
|
const headerObj = await xml2__default.default.parseStringPromise(headerXml, { explicitArray: false });
|
|
@@ -1456,6 +1866,39 @@ var init_QvdFileReader = __esm({
|
|
|
1456
1866
|
const indexTableOffset = symbolTableOffset + symbolTableLength;
|
|
1457
1867
|
const recordSize = parseInt(headerObj["QvdTableHeader"]["RecordByteSize"], 10);
|
|
1458
1868
|
const totalRows = parseInt(headerObj["QvdTableHeader"]["NoOfRecords"], 10);
|
|
1869
|
+
if (headerOnly) {
|
|
1870
|
+
this._buffer = headerBuffer.subarray(0, headerEndIndex);
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
let headerFields = headerObj["QvdTableHeader"]?.["Fields"]?.["QvdFieldHeader"];
|
|
1874
|
+
if (headerFields && !Array.isArray(headerFields)) {
|
|
1875
|
+
headerFields = [headerFields];
|
|
1876
|
+
}
|
|
1877
|
+
const columnCount = Array.isArray(headerFields) ? headerFields.length : 0;
|
|
1878
|
+
const headerNumbersUsable = [symbolTableLength, recordSize, totalRows].every(
|
|
1879
|
+
(value) => Number.isSafeInteger(value) && value >= 0
|
|
1880
|
+
);
|
|
1881
|
+
if (headerNumbersUsable) {
|
|
1882
|
+
const { size: fileSize } = await fs__default.default.promises.stat(this._path);
|
|
1883
|
+
this._fileSize = fileSize;
|
|
1884
|
+
this._headerMatchesFile = headerEndIndex + symbolTableLength + totalRows * recordSize <= fileSize;
|
|
1885
|
+
}
|
|
1886
|
+
if (headerNumbersUsable && this._headerMatchesFile) {
|
|
1887
|
+
validateMemoryAvailability(
|
|
1888
|
+
symbolTableLength,
|
|
1889
|
+
maxRows,
|
|
1890
|
+
totalRows,
|
|
1891
|
+
this._path,
|
|
1892
|
+
this._memorySafetyFactor,
|
|
1893
|
+
columnCount,
|
|
1894
|
+
this._materialisesRows
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
if (maxRows === null) {
|
|
1898
|
+
this._buffer = await fs__default.default.promises.readFile(this._path);
|
|
1899
|
+
this._fileSize = this._buffer.length;
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1459
1902
|
const rowsToLoad = Math.min(maxRows, totalRows);
|
|
1460
1903
|
validateSymbolTableSizeEarly(symbolTableLength, this._path);
|
|
1461
1904
|
for (const [name, value] of [
|
|
@@ -1541,38 +1984,40 @@ var init_QvdFileReader = __esm({
|
|
|
1541
1984
|
stage: "parseHeader"
|
|
1542
1985
|
});
|
|
1543
1986
|
}
|
|
1987
|
+
const fields = this._header["QvdTableHeader"]?.["Fields"]?.["QvdFieldHeader"];
|
|
1988
|
+
const fieldCount = fields === void 0 || fields === null ? 0 : Array.isArray(fields) ? fields.length : 1;
|
|
1989
|
+
if (fieldCount === 0) {
|
|
1990
|
+
throw new exports.QvdCorruptedError("The QVD file header declares no fields", {
|
|
1991
|
+
file: this._path,
|
|
1992
|
+
stage: "parseHeader"
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1544
1995
|
this._headerOffset = headerBeginIndex;
|
|
1545
1996
|
this._symbolTableOffset = headerEndIndex;
|
|
1546
1997
|
this._indexTableOffset = this._symbolTableOffset + parseInt(this._header["QvdTableHeader"]["Offset"], 10);
|
|
1547
1998
|
}
|
|
1548
1999
|
/**
|
|
1549
|
-
*
|
|
1550
|
-
* Takes into account Phase 2.5 optimization which skips parsing unused symbols.
|
|
2000
|
+
* Establishes the geometry of the index table, and validates it.
|
|
1551
2001
|
*
|
|
1552
|
-
*
|
|
1553
|
-
*
|
|
1554
|
-
*
|
|
1555
|
-
*
|
|
1556
|
-
*
|
|
1557
|
-
*/
|
|
1558
|
-
_estimateMemoryUsage(symbolTableSize, maxRows, totalRows) {
|
|
1559
|
-
return estimateMemoryUsage(symbolTableSize, maxRows, totalRows);
|
|
1560
|
-
}
|
|
1561
|
-
/**
|
|
1562
|
-
* Analyzes the index table to determine which symbols are actually needed.
|
|
1563
|
-
* This is used for two-pass symbol filtering optimization.
|
|
2002
|
+
* Both passes over the index table - the symbol-usage analysis and the decode itself - need
|
|
2003
|
+
* exactly this, and they used to derive it separately with two copies of the same bit
|
|
2004
|
+
* unpacking. The copies are the reason the bias comment in the analysis pass warns so loudly
|
|
2005
|
+
* about keeping the sign in step with the other one: the two could drift, and #113 is what
|
|
2006
|
+
* that looks like when they do. There is one copy now.
|
|
1564
2007
|
*
|
|
1565
|
-
* @param {number}
|
|
1566
|
-
* @
|
|
2008
|
+
* @param {number|null} rowLimit Maximum rows of interest, or null for all of them.
|
|
2009
|
+
* @param {string} stage Stage name for any error raised here.
|
|
2010
|
+
* @return {{fields: Array<any>, recordSize: number, totalRows: number, rowsToLoad: number,
|
|
2011
|
+
* indexBuffer: Buffer}} The record geometry.
|
|
1567
2012
|
* @private
|
|
1568
2013
|
*/
|
|
1569
|
-
|
|
2014
|
+
_planIndexTable(rowLimit, stage) {
|
|
1570
2015
|
if (!this._buffer || !this._header || !this._indexTableOffset) {
|
|
1571
2016
|
throw new exports.QvdCorruptedError(
|
|
1572
2017
|
"The QVD file has not been loaded in the proper order or has not been loaded at all.",
|
|
1573
2018
|
{
|
|
1574
2019
|
file: this._path,
|
|
1575
|
-
stage
|
|
2020
|
+
stage
|
|
1576
2021
|
}
|
|
1577
2022
|
);
|
|
1578
2023
|
}
|
|
@@ -1582,35 +2027,58 @@ var init_QvdFileReader = __esm({
|
|
|
1582
2027
|
}
|
|
1583
2028
|
const recordSize = parseInt(this._header["QvdTableHeader"]["RecordByteSize"], 10);
|
|
1584
2029
|
const totalRows = parseInt(this._header["QvdTableHeader"]["NoOfRecords"], 10);
|
|
1585
|
-
const
|
|
2030
|
+
const rowsToLoad = rowLimit !== null ? Math.min(rowLimit, totalRows) : totalRows;
|
|
1586
2031
|
const indexTableLength = parseInt(this._header["QvdTableHeader"]["Length"], 10);
|
|
2032
|
+
validateIndexTableMetadata(
|
|
2033
|
+
recordSize,
|
|
2034
|
+
totalRows,
|
|
2035
|
+
indexTableLength,
|
|
2036
|
+
this._indexTableOffset,
|
|
2037
|
+
this._buffer.length,
|
|
2038
|
+
rowsToLoad,
|
|
2039
|
+
this._path,
|
|
2040
|
+
this._fileSize
|
|
2041
|
+
);
|
|
1587
2042
|
const indexBuffer = this._buffer.subarray(this._indexTableOffset, this._indexTableOffset + indexTableLength + 1);
|
|
2043
|
+
for (const field of fields) {
|
|
2044
|
+
validateFieldBitMetadata(field, recordSize, this._path);
|
|
2045
|
+
}
|
|
2046
|
+
assert2__default.default(
|
|
2047
|
+
rowsToLoad === 0 || recordSize === 0 || Math.floor(indexBuffer.length / recordSize) >= rowsToLoad,
|
|
2048
|
+
`The index table holds ${Math.floor(indexBuffer.length / (recordSize || 1))} whole records but ${rowsToLoad} were validated as present.`
|
|
2049
|
+
);
|
|
2050
|
+
return { fields, recordSize, totalRows, rowsToLoad, indexBuffer };
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Analyzes the index table to determine which symbols are actually needed.
|
|
2054
|
+
* This is used for two-pass symbol filtering optimization.
|
|
2055
|
+
*
|
|
2056
|
+
* @param {number} maxRows The maximum number of rows to analyze.
|
|
2057
|
+
* @return {Promise<Map<string, Set<number>>>} Map of field names to Set of needed symbol indices.
|
|
2058
|
+
* @private
|
|
2059
|
+
*/
|
|
2060
|
+
async _analyzeIndexTableSymbolUsage(maxRows) {
|
|
2061
|
+
const { fields, recordSize, rowsToLoad, indexBuffer } = this._planIndexTable(maxRows, "analyzeIndexTableSymbolUsage");
|
|
1588
2062
|
const symbolUsage = /* @__PURE__ */ new Map();
|
|
2063
|
+
const column = new Int32Array(rowsToLoad);
|
|
1589
2064
|
fields.forEach((field) => {
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
symbolIndex = this._convertBitsToInt32(mask.slice(bitOffset, bitOffset + bitWidth));
|
|
1605
|
-
}
|
|
1606
|
-
symbolIndex += bias;
|
|
1607
|
-
if (symbolIndex < 0) {
|
|
1608
|
-
return;
|
|
2065
|
+
const needed = /* @__PURE__ */ new Set();
|
|
2066
|
+
symbolUsage.set(field["FieldName"], needed);
|
|
2067
|
+
decodeIndexColumn(
|
|
2068
|
+
indexBuffer,
|
|
2069
|
+
recordSize,
|
|
2070
|
+
rowsToLoad,
|
|
2071
|
+
parseInt(field["BitOffset"], 10),
|
|
2072
|
+
parseInt(field["BitWidth"], 10),
|
|
2073
|
+
parseInt(field["Bias"], 10),
|
|
2074
|
+
column
|
|
2075
|
+
);
|
|
2076
|
+
for (let row = 0; row < rowsToLoad; row++) {
|
|
2077
|
+
if (column[row] >= 0) {
|
|
2078
|
+
needed.add(column[row]);
|
|
1609
2079
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
});
|
|
1613
|
-
}
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
1614
2082
|
return symbolUsage;
|
|
1615
2083
|
}
|
|
1616
2084
|
/**
|
|
@@ -1636,8 +2104,24 @@ var init_QvdFileReader = __esm({
|
|
|
1636
2104
|
const symbolTableSize = symbolBuffer.length;
|
|
1637
2105
|
const totalRows = parseInt(this._header["QvdTableHeader"]["NoOfRecords"], 10);
|
|
1638
2106
|
validateSymbolTableSize(symbolTableSize, this._path, totalRows);
|
|
1639
|
-
|
|
1640
|
-
|
|
2107
|
+
if (this._headerMatchesFile) {
|
|
2108
|
+
validateMemoryAvailability(
|
|
2109
|
+
symbolTableSize,
|
|
2110
|
+
maxRows,
|
|
2111
|
+
totalRows,
|
|
2112
|
+
this._path,
|
|
2113
|
+
this._memorySafetyFactor,
|
|
2114
|
+
Array.isArray(fields) ? fields.length : 1,
|
|
2115
|
+
this._materialisesRows
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
warnLargeSymbolTable(
|
|
2119
|
+
symbolTableSize,
|
|
2120
|
+
maxRows,
|
|
2121
|
+
totalRows,
|
|
2122
|
+
Array.isArray(fields) ? fields.length : 1,
|
|
2123
|
+
this._materialisesRows
|
|
2124
|
+
);
|
|
1641
2125
|
if (!Array.isArray(fields)) {
|
|
1642
2126
|
fields = [fields];
|
|
1643
2127
|
}
|
|
@@ -1671,93 +2155,85 @@ var init_QvdFileReader = __esm({
|
|
|
1671
2155
|
return symbols;
|
|
1672
2156
|
});
|
|
1673
2157
|
}
|
|
1674
|
-
/**
|
|
1675
|
-
* Utility method to convert a bit array to an integer value.
|
|
1676
|
-
*
|
|
1677
|
-
* @param {Array<number>} bits The bit array
|
|
1678
|
-
* @return {Number} The integer value
|
|
1679
|
-
*/
|
|
1680
|
-
_convertBitsToInt32(bits) {
|
|
1681
|
-
return convertBitsToInt32(bits);
|
|
1682
|
-
}
|
|
1683
2158
|
/**
|
|
1684
2159
|
* Parses the bit stuffed index table of the QVD file. This method is part of the parsing process
|
|
1685
2160
|
* and should not be called directly.
|
|
1686
2161
|
*
|
|
2162
|
+
* One `Int32Array` per field, filled by `decodeIndexColumn`, replacing an array per row filled
|
|
2163
|
+
* a bit at a time. The old route, per row, built an `Int32Array` of the record's bytes,
|
|
2164
|
+
* concatenated a binary string of `recordSize * 8` characters, split it into a character
|
|
2165
|
+
* array, reversed that, and mapped it to one number per *bit*; then per cell it sliced the
|
|
2166
|
+
* result again and summed `bit * Math.pow(2, index)`. Several arrays the length of the record
|
|
2167
|
+
* in bits, built and discarded for every row.
|
|
2168
|
+
*
|
|
2169
|
+
* Column-major is what makes the decode tight: a field's bit offset, width and bias are the
|
|
2170
|
+
* same for every row, so they are hoisted out of the loop and the inner loop does arithmetic
|
|
2171
|
+
* into a typed array and nothing else. Rows are assembled later, once, in `load()`.
|
|
2172
|
+
*
|
|
1687
2173
|
* @param {number|null} maxRows The maximum number of rows to parse. If null, all rows are parsed.
|
|
1688
2174
|
*/
|
|
1689
2175
|
async _parseIndexTable(maxRows = null) {
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
2176
|
+
const { fields, recordSize, rowsToLoad, indexBuffer } = this._planIndexTable(maxRows, "parseIndexTable");
|
|
2177
|
+
this._rowsDecoded = rowsToLoad;
|
|
2178
|
+
this._indexColumns = fields.map(
|
|
2179
|
+
(field) => decodeIndexColumn(
|
|
2180
|
+
indexBuffer,
|
|
2181
|
+
recordSize,
|
|
2182
|
+
rowsToLoad,
|
|
2183
|
+
parseInt(field["BitOffset"], 10),
|
|
2184
|
+
parseInt(field["BitWidth"], 10),
|
|
2185
|
+
parseInt(field["Bias"], 10),
|
|
2186
|
+
new Int32Array(rowsToLoad)
|
|
2187
|
+
)
|
|
2188
|
+
);
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Reads the file's schema and header metadata, without touching the symbol or index tables.
|
|
2192
|
+
*
|
|
2193
|
+
* Constant cost in the size of the file. `fromQvd(path, {maxRows: 0})` is not the same thing
|
|
2194
|
+
* and never was: the lazy path reads `headerEnd + symbolTableLength + rowsToLoad * recordSize`
|
|
2195
|
+
* bytes, so it still pulls the entire symbol table - 0.4MB on the taxi fixture, but hundreds
|
|
2196
|
+
* of megabytes on a high-cardinality file, and it has to be parsed as well as read.
|
|
2197
|
+
*
|
|
2198
|
+
* The memory check is deliberately not run for this. It sizes the rows a call will
|
|
2199
|
+
* materialise, and this materialises none; applying it would let a file too large to load
|
|
2200
|
+
* refuse to say what is in it.
|
|
2201
|
+
*
|
|
2202
|
+
* @return {Promise<import('./QvdDataFrame.js').QvdFileMetadata>} The file's schema and header.
|
|
2203
|
+
*/
|
|
2204
|
+
async loadMetadata() {
|
|
2205
|
+
await this._readData(null, true);
|
|
2206
|
+
await this._parseHeader();
|
|
2207
|
+
assert2__default.default(this._header, "The QVD file header has not been parsed.");
|
|
2208
|
+
const header = this._header["QvdTableHeader"];
|
|
2209
|
+
let fields = header["Fields"]?.["QvdFieldHeader"] ?? [];
|
|
1700
2210
|
if (!Array.isArray(fields)) {
|
|
1701
2211
|
fields = [fields];
|
|
1702
2212
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
const rowsToLoad = maxRows !== null ? Math.min(maxRows, totalRows) : totalRows;
|
|
1706
|
-
const indexTableLength = parseInt(this._header["QvdTableHeader"]["Length"], 10);
|
|
1707
|
-
validateIndexTableMetadata(
|
|
1708
|
-
recordSize,
|
|
1709
|
-
totalRows,
|
|
1710
|
-
indexTableLength,
|
|
1711
|
-
this._indexTableOffset,
|
|
1712
|
-
this._buffer.length,
|
|
1713
|
-
rowsToLoad,
|
|
1714
|
-
this._path,
|
|
1715
|
-
this._fileSize
|
|
1716
|
-
);
|
|
1717
|
-
const indexBuffer = this._buffer.subarray(this._indexTableOffset, this._indexTableOffset + indexTableLength + 1);
|
|
1718
|
-
for (const field of fields) {
|
|
1719
|
-
validateFieldBitMetadata(field, recordSize, this._path);
|
|
1720
|
-
}
|
|
1721
|
-
this._indexTable = [];
|
|
1722
|
-
for (let pointer = 0, rowCount = 0; pointer < indexBuffer.length && rowCount < rowsToLoad; pointer += recordSize, rowCount++) {
|
|
1723
|
-
if (pointer + recordSize > indexBuffer.length) {
|
|
1724
|
-
throw new exports.QvdCorruptedError("Buffer overflow reading index table record", {
|
|
1725
|
-
pointer,
|
|
1726
|
-
recordSize,
|
|
1727
|
-
bufferSize: indexBuffer.length,
|
|
1728
|
-
file: this._path,
|
|
1729
|
-
stage: "parseIndexTable"
|
|
1730
|
-
});
|
|
1731
|
-
}
|
|
1732
|
-
const bytes = new Int32Array(indexBuffer.subarray(pointer, pointer + recordSize));
|
|
1733
|
-
bytes.reverse();
|
|
1734
|
-
const mask = bytes.reduce((bits, byte) => bits + ("00000000" + byte.toString(2)).slice(-8), "").split("").reverse().map((bit) => parseInt(bit));
|
|
1735
|
-
const symbolIndices = [];
|
|
1736
|
-
fields.forEach((field) => {
|
|
1737
|
-
const bitOffset = parseInt(field["BitOffset"], 10);
|
|
1738
|
-
const bitWidth = parseInt(field["BitWidth"], 10);
|
|
1739
|
-
const bias = parseInt(field["Bias"], 10);
|
|
1740
|
-
let symbolIndex;
|
|
1741
|
-
if (bitWidth === 0) {
|
|
1742
|
-
symbolIndex = 0;
|
|
1743
|
-
} else {
|
|
1744
|
-
symbolIndex = this._convertBitsToInt32(mask.slice(bitOffset, bitOffset + bitWidth));
|
|
1745
|
-
}
|
|
1746
|
-
symbolIndex += bias;
|
|
1747
|
-
symbolIndices.push(symbolIndex);
|
|
1748
|
-
});
|
|
1749
|
-
this._indexTable.push(symbolIndices);
|
|
1750
|
-
}
|
|
1751
|
-
if (this._indexTable.length !== rowsToLoad) {
|
|
1752
|
-
throw new exports.QvdCorruptedError("Index table contains fewer records than expected", {
|
|
1753
|
-
expectedRows: rowsToLoad,
|
|
1754
|
-
actualRows: this._indexTable.length,
|
|
1755
|
-
totalRows,
|
|
1756
|
-
recordSize,
|
|
2213
|
+
if (fields.length === 0) {
|
|
2214
|
+
throw new exports.QvdCorruptedError("The QVD file header declares no fields", {
|
|
1757
2215
|
file: this._path,
|
|
1758
|
-
stage: "
|
|
2216
|
+
stage: "readMetadata"
|
|
1759
2217
|
});
|
|
1760
2218
|
}
|
|
2219
|
+
const columns = fields.map((field) => field["FieldName"]);
|
|
2220
|
+
const rowCount = parseInt(header["NoOfRecords"], 10);
|
|
2221
|
+
validateRecordCount(rowCount, this._path, "readMetadata");
|
|
2222
|
+
const shape = new exports.QvdDataFrame([], columns, header, {
|
|
2223
|
+
symbolTableBytes: parseInt(header["Offset"], 10),
|
|
2224
|
+
totalRows: rowCount,
|
|
2225
|
+
rowsLoaded: 0,
|
|
2226
|
+
symbolFiltering: false,
|
|
2227
|
+
symbolsKept: null
|
|
2228
|
+
});
|
|
2229
|
+
return {
|
|
2230
|
+
columns,
|
|
2231
|
+
rowCount,
|
|
2232
|
+
columnCount: columns.length,
|
|
2233
|
+
fields: columns.map((name) => shape.getFieldMetadata(name)),
|
|
2234
|
+
fileMetadata: shape.fileMetadata,
|
|
2235
|
+
metadata: header
|
|
2236
|
+
};
|
|
1761
2237
|
}
|
|
1762
2238
|
/**
|
|
1763
2239
|
* Loads the QVD file into memory and parses it.
|
|
@@ -1768,6 +2244,37 @@ var init_QvdFileReader = __esm({
|
|
|
1768
2244
|
* @return {Promise<QvdDataFrame>} The loaded QVD file.
|
|
1769
2245
|
*/
|
|
1770
2246
|
async load(maxRows = null) {
|
|
2247
|
+
const { columns, metadata, loadStats, resolvedByField } = await this._decode(maxRows);
|
|
2248
|
+
assert2__default.default(this._indexColumns, "The QVD file index table has not been parsed.");
|
|
2249
|
+
const indexColumns = this._indexColumns;
|
|
2250
|
+
const fieldCount = indexColumns.length;
|
|
2251
|
+
const data = new Array(this._rowsDecoded);
|
|
2252
|
+
for (let row = 0; row < this._rowsDecoded; row++) {
|
|
2253
|
+
const values = new Array(fieldCount);
|
|
2254
|
+
for (let field = 0; field < fieldCount; field++) {
|
|
2255
|
+
const symbolIndex = indexColumns[field][row];
|
|
2256
|
+
values[field] = symbolIndex < 0 ? null : resolvedByField[field][symbolIndex];
|
|
2257
|
+
}
|
|
2258
|
+
data[row] = values;
|
|
2259
|
+
}
|
|
2260
|
+
loadStats.rowsLoaded = data.length;
|
|
2261
|
+
return new exports.QvdDataFrame(data, columns, metadata, loadStats);
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Reads and decodes the file, stopping short of building rows.
|
|
2265
|
+
*
|
|
2266
|
+
* Everything `load()` and `loadColumnar()` have in common, which is everything except the
|
|
2267
|
+
* shape of the answer. Two read paths for one binary format is the drift risk #113 is the
|
|
2268
|
+
* standing example of - a stored index resolved one way here and another way there returns
|
|
2269
|
+
* plausible wrong values and throws nothing - so there is one path, and the two entry points
|
|
2270
|
+
* differ only in what they do with what it returns.
|
|
2271
|
+
*
|
|
2272
|
+
* @param {number|null} maxRows Maximum rows to decode, or null for all of them.
|
|
2273
|
+
* @return {Promise<{columns: Array<string>, metadata: any, loadStats: any,
|
|
2274
|
+
* resolvedByField: Array<Array<any>>}>} The decoded file.
|
|
2275
|
+
* @private
|
|
2276
|
+
*/
|
|
2277
|
+
async _decode(maxRows = null) {
|
|
1771
2278
|
if (maxRows !== null && (typeof maxRows !== "number" || !Number.isInteger(maxRows) || maxRows < 0)) {
|
|
1772
2279
|
throw new exports.QvdValidationError("maxRows must be a non-negative integer, or null to load all rows", {
|
|
1773
2280
|
provided: maxRows,
|
|
@@ -1788,46 +2295,57 @@ var init_QvdFileReader = __esm({
|
|
|
1788
2295
|
}
|
|
1789
2296
|
await this._parseSymbolTable(symbolsToKeep, maxRows);
|
|
1790
2297
|
await this._parseIndexTable(maxRows);
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
const
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
file: this._path
|
|
1800
|
-
});
|
|
2298
|
+
assert2__default.default(this._header, "The QVD file header has not been parsed.");
|
|
2299
|
+
assert2__default.default(this._symbolTable, "The QVD file symbol table has not been parsed.");
|
|
2300
|
+
assert2__default.default(this._indexColumns, "The QVD file index table has not been parsed.");
|
|
2301
|
+
const resolvedByField = this._symbolTable.map((symbols) => {
|
|
2302
|
+
const resolved = new Array(symbols.length);
|
|
2303
|
+
for (let index = 0; index < symbols.length; index++) {
|
|
2304
|
+
const value = symbols[index]?.toPrimaryValue();
|
|
2305
|
+
resolved[index] = typeof value === "string" && value.trim() !== "" && !isNaN(Number(value)) ? Number(value) : value;
|
|
1801
2306
|
}
|
|
1802
|
-
return
|
|
1803
|
-
|
|
1804
|
-
return null;
|
|
1805
|
-
}
|
|
1806
|
-
const symbol = this._symbolTable?.[fieldIndex]?.[symbolIndex];
|
|
1807
|
-
const value = symbol?.toPrimaryValue();
|
|
1808
|
-
if (typeof value === "string") {
|
|
1809
|
-
if (value.trim() !== "" && !isNaN(Number(value))) {
|
|
1810
|
-
return Number(value);
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
return value;
|
|
1814
|
-
});
|
|
1815
|
-
};
|
|
2307
|
+
return resolved;
|
|
2308
|
+
});
|
|
1816
2309
|
let fields = this._header["QvdTableHeader"]["Fields"]["QvdFieldHeader"];
|
|
1817
2310
|
if (!Array.isArray(fields)) {
|
|
1818
2311
|
fields = [fields];
|
|
1819
2312
|
}
|
|
1820
2313
|
const columns = fields.map((field) => field["FieldName"]);
|
|
1821
|
-
const data = this._indexTable.map((_, index) => getRow(index));
|
|
1822
2314
|
const metadata = this._header["QvdTableHeader"];
|
|
1823
2315
|
const loadStats = {
|
|
1824
2316
|
symbolTableBytes: parseInt(this._header["QvdTableHeader"]["Offset"], 10),
|
|
1825
2317
|
totalRows: parseInt(this._header["QvdTableHeader"]["NoOfRecords"], 10),
|
|
1826
|
-
rowsLoaded:
|
|
2318
|
+
rowsLoaded: this._rowsDecoded,
|
|
1827
2319
|
symbolFiltering: symbolsToKeep !== null,
|
|
1828
2320
|
symbolsKept
|
|
1829
2321
|
};
|
|
1830
|
-
return
|
|
2322
|
+
return { columns, metadata, loadStats, resolvedByField };
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* Reads the file as columns, without ever materialising rows.
|
|
2326
|
+
*
|
|
2327
|
+
* Shares every step with `load()` up to the point where rows would be built - see `_decode`.
|
|
2328
|
+
* What it keeps instead is what the decoder already produced: one `Int32Array` of stored
|
|
2329
|
+
* indices per field, and one resolved value per distinct symbol. On the 1.7M x 20 taxi
|
|
2330
|
+
* fixture that is 38.6 MiB against the 352.8 MiB `data` retains, because a column costs four
|
|
2331
|
+
* bytes per row rather than a boxed value per cell, and the symbols are a few thousand
|
|
2332
|
+
* entries shared across every row that uses them.
|
|
2333
|
+
*
|
|
2334
|
+
* @param {number|null} maxRows The maximum number of rows to decode.
|
|
2335
|
+
* @return {Promise<import('./QvdColumnTable.js').QvdColumnTable>} The decoded columns.
|
|
2336
|
+
*/
|
|
2337
|
+
async loadColumnar(maxRows = null) {
|
|
2338
|
+
const { columns, metadata, loadStats, resolvedByField } = await this._decode(maxRows);
|
|
2339
|
+
const { QvdColumnTable: QvdColumnTable2 } = await Promise.resolve().then(() => (init_QvdColumnTable(), QvdColumnTable_exports));
|
|
2340
|
+
assert2__default.default(this._indexColumns, "The QVD file index table has not been parsed.");
|
|
2341
|
+
return new QvdColumnTable2({
|
|
2342
|
+
columns,
|
|
2343
|
+
codesByField: this._indexColumns,
|
|
2344
|
+
symbolsByField: resolvedByField,
|
|
2345
|
+
rowCount: this._rowsDecoded,
|
|
2346
|
+
metadata,
|
|
2347
|
+
loadStats
|
|
2348
|
+
});
|
|
1831
2349
|
}
|
|
1832
2350
|
};
|
|
1833
2351
|
}
|
|
@@ -2267,10 +2785,11 @@ var init_QvdDataFrame = __esm({
|
|
|
2267
2785
|
* outside it is rejected. Defaults to the current working directory. To permit an entire
|
|
2268
2786
|
* volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or empty value falls
|
|
2269
2787
|
* back to the working directory rather than removing the restriction.
|
|
2270
|
-
* @param {number} [options.memorySafetyFactor=0.
|
|
2271
|
-
* may use. The budget is the
|
|
2272
|
-
*
|
|
2273
|
-
*
|
|
2788
|
+
* @param {number} [options.memorySafetyFactor=0.8] Fraction (0.0-1.0) of the memory budget a load
|
|
2789
|
+
* may use. The budget is the smaller of the V8 heap limit and any container memory limit. Default
|
|
2790
|
+
* is 0.8; the estimate it scales accounts for the rows and columns being materialised, so this is
|
|
2791
|
+
* headroom for garbage collection rather than compensation for an inaccurate figure.
|
|
2792
|
+
* **Zero disables the memory check entirely.**
|
|
2274
2793
|
* @param {number} [options.symbolFilteringThreshold=52428800] Symbol table size, in bytes, above which
|
|
2275
2794
|
* a lazy load switches to the two-pass filtering path. Defaults to 50MB.
|
|
2276
2795
|
* @throws {QvdValidationError} If options.maxRows is neither null/undefined nor a non-negative integer.
|
|
@@ -2285,6 +2804,34 @@ var init_QvdDataFrame = __esm({
|
|
|
2285
2804
|
};
|
|
2286
2805
|
return await new QvdFileReader2(path3, readerOptions).load(options.maxRows !== void 0 ? options.maxRows : null);
|
|
2287
2806
|
}
|
|
2807
|
+
/**
|
|
2808
|
+
* Reads a QVD file's schema and header metadata, without reading its data.
|
|
2809
|
+
*
|
|
2810
|
+
* Costs the same whatever the file's size, because it stops at the XML header - a few
|
|
2811
|
+
* kilobytes - and never touches the symbol or index tables.
|
|
2812
|
+
*
|
|
2813
|
+
* This is what `{maxRows: 0}` looks like but is not. That still reads and parses the whole
|
|
2814
|
+
* symbol table, which is 0.4MB on a 38MB taxi fixture but hundreds of megabytes on a
|
|
2815
|
+
* high-cardinality file. Use this when you want to know what is in a file rather than to
|
|
2816
|
+
* read any of it.
|
|
2817
|
+
*
|
|
2818
|
+
* No data frame comes back, deliberately: one with `data: []` would be indistinguishable
|
|
2819
|
+
* from an empty file at the call site.
|
|
2820
|
+
*
|
|
2821
|
+
* ```js
|
|
2822
|
+
* const {columns, rowCount, fields} = await QvdDataFrame.readMetadata('sales.qvd');
|
|
2823
|
+
* ```
|
|
2824
|
+
*
|
|
2825
|
+
* @param {string} path The path to the QVD file.
|
|
2826
|
+
* @param {Object} [options] Optional reading options.
|
|
2827
|
+
* @param {string} [options.allowedDir] Optional allowed directory path, applied exactly as it
|
|
2828
|
+
* is for `fromQvd`.
|
|
2829
|
+
* @return {Promise<QvdFileMetadata>} The file's schema and header metadata.
|
|
2830
|
+
*/
|
|
2831
|
+
static async readMetadata(path3, options = {}) {
|
|
2832
|
+
const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
|
|
2833
|
+
return await new QvdFileReader2(path3, { allowedDir: options.allowedDir }).loadMetadata();
|
|
2834
|
+
}
|
|
2288
2835
|
/**
|
|
2289
2836
|
* Constructs a data frame from a dictionary.
|
|
2290
2837
|
*
|
|
@@ -2311,6 +2858,7 @@ var init_QvdDataFrame = __esm({
|
|
|
2311
2858
|
// src/index.js
|
|
2312
2859
|
init_QvdSymbol();
|
|
2313
2860
|
init_QvdDataFrame();
|
|
2861
|
+
init_QvdColumnTable();
|
|
2314
2862
|
init_QvdFileReader();
|
|
2315
2863
|
init_QvdFileWriter();
|
|
2316
2864
|
init_QvdErrors();
|