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