qvdjs 0.7.0 → 0.9.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 +53 -4
- package/dist/index.cjs +50 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +49 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ structure and vice versa. The library is written to be used in a Node.js environ
|
|
|
31
31
|
- [Important: Symbol Table and High-Cardinality Fields](#important-symbol-table-and-high-cardinality-fields)
|
|
32
32
|
- [Performance Optimizations](#performance-optimizations)
|
|
33
33
|
- [QVD File Size Limitations](#qvd-file-size-limitations)
|
|
34
|
+
- [Why Safety Limits Exist](#why-safety-limits-exist)
|
|
34
35
|
- [Progress Tracking for Large File Writes](#progress-tracking-for-large-file-writes)
|
|
35
36
|
- [Working with Metadata](#working-with-metadata)
|
|
36
37
|
- [Security Considerations](#security-considerations)
|
|
@@ -220,9 +221,17 @@ There is unfortunately no simple answer to this question, as it very much depend
|
|
|
220
221
|
|
|
221
222
|
The maximum QVD file size you can handle with qvdjs depends on several factors and there is no single fixed limit:
|
|
222
223
|
|
|
223
|
-
- **Node.js Memory Limits**: By default, Node.js limits heap memory to approximately 4GB (varies by architecture and Node.js version). You can increase
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
- **Node.js Memory Limits**: By default, Node.js limits heap memory to approximately 4GB (varies by architecture and Node.js version). The library **automatically detects your configured heap size** and scales its safety limits accordingly. You can increase heap size using the `--max-old-space-size` flag (e.g., `node --max-old-space-size=16384 script.js` for 16GB), and qvdjs will automatically allow larger files.
|
|
225
|
+
|
|
226
|
+
For larger heap configurations, you can also adjust the `memorySafetyFactor` option (default 0.3 = 30%) to make more efficient use of available memory:
|
|
227
|
+
|
|
228
|
+
```javascript
|
|
229
|
+
const df = await QvdDataFrame.fromQvd('large-file.qvd', {
|
|
230
|
+
memorySafetyFactor: 0.5, // Use 50% of heap instead of default 30%
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
To phrase it differently: There is no magic here - viewing a 40 GB QVD on a laptop with 24 GB RAM will not work. That laptop may in fact struggle with QVD files larger than 4-6 GB depending on data characteristics - or happily work with 10+ GB files if the data is very friendly.
|
|
226
235
|
|
|
227
236
|
- **Data Characteristics**: The actual memory consumption depends _heavily_ on what's inside your QVD:
|
|
228
237
|
- **Field Cardinality**: Files with high-cardinality fields (many unique values per field, like unique IDs or timestamps) require more memory for symbol tables. This is usually the biggest factor, at least if there are many rows too.
|
|
@@ -235,11 +244,45 @@ The maximum QVD file size you can handle with qvdjs depends on several factors a
|
|
|
235
244
|
- **Practical Guidance**:
|
|
236
245
|
- For typical business data with moderate cardinality, files up to 1-2GB usually work well with default Node.js settings
|
|
237
246
|
- High-cardinality data (unique values in most rows) may limit you to smaller files (hundreds of MB)
|
|
247
|
+
- **With increased heap** (e.g., 16GB+), you can handle proportionally larger files by adjusting `memorySafetyFactor`
|
|
238
248
|
- Use lazy loading (`maxRows` option) when possible to reduce memory footprint when reading
|
|
239
249
|
- Monitor memory usage with tools like `process.memoryUsage()` for your specific use cases
|
|
240
250
|
- Consider processing large datasets in chunks or using streaming approaches if you hit memory limits. Clever things can be done by doing multiple passes over the file instead of loading everything at once.
|
|
241
251
|
|
|
242
|
-
If you consistently work with very large QVD files, consider increasing Node.js memory limits or splitting your data into multiple smaller QVD files.
|
|
252
|
+
If you consistently work with very large QVD files, consider increasing Node.js memory limits (with matching `memorySafetyFactor` adjustment) or splitting your data into multiple smaller QVD files.
|
|
253
|
+
|
|
254
|
+
#### Why Safety Limits Exist
|
|
255
|
+
|
|
256
|
+
The library implements **dynamic safety limits** to prevent catastrophic crashes. Without these limits:
|
|
257
|
+
|
|
258
|
+
- **Your application will crash hard** - Node.js terminates with `FATAL ERROR: Reached heap limit` when attempting to load files that are too large
|
|
259
|
+
- **No error handling is possible** - JavaScript try-catch blocks cannot intercept out-of-memory (OOM) crashes at the V8 engine level
|
|
260
|
+
- **The entire process dies** - Not just the QVD operation, but your entire application terminates ungracefully
|
|
261
|
+
|
|
262
|
+
The safety limits **prevent these crashes** by checking available memory _before_ attempting to load files, throwing graceful `QvdValidationError` exceptions that you can catch and handle. While the multi-tier safety system may seem complex, it ensures your application stays running and provides helpful error messages with recommendations (like using `maxRows` parameter or increasing heap size) instead of cryptic fatal errors.
|
|
263
|
+
|
|
264
|
+
**Example without safety limits:**
|
|
265
|
+
|
|
266
|
+
```javascript
|
|
267
|
+
// Process crashes with no chance to recover
|
|
268
|
+
const df = await QvdDataFrame.fromQvd('huge-file.qvd'); // 💥 FATAL ERROR
|
|
269
|
+
// Your application is now terminated
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Example with safety limits:**
|
|
273
|
+
|
|
274
|
+
```javascript
|
|
275
|
+
try {
|
|
276
|
+
const df = await QvdDataFrame.fromQvd('huge-file.qvd');
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if (error.name === 'QvdValidationError') {
|
|
279
|
+
console.log('File too large, trying with maxRows:', error.context.recommendedMaxRows);
|
|
280
|
+
// ✅ Your application continues running
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
For more technical details about the memory safety system, including specific thresholds and the four-tier protection model, see [docs/DYNAMIC_SAFETY_LIMITS.md](docs/DYNAMIC_SAFETY_LIMITS.md). For practical examples, see [docs/examples/heap-scaling-example.md](docs/examples/heap-scaling-example.md).
|
|
243
286
|
|
|
244
287
|
### Progress Tracking for Large File Writes
|
|
245
288
|
|
|
@@ -543,6 +586,7 @@ to a `QvdDataFrame` instance.
|
|
|
543
586
|
- `options` (object, optional): Loading options
|
|
544
587
|
- `maxRows` (number, optional): Maximum number of rows to load. If not specified, all rows are loaded. This is useful for loading only a subset of data from large QVD files to improve performance and reduce memory usage.
|
|
545
588
|
- `allowedDir` (string, optional): Base directory for file access validation. Defaults to current working directory (CWD). The file path must resolve to a location within this directory to prevent path traversal attacks. Set to a specific directory in production environments with user-provided paths.
|
|
589
|
+
- `memorySafetyFactor` (number, optional): Memory safety factor (0.0-1.0). Default is 0.3 (30%). Determines what percentage of available memory (or V8 heap limit, whichever is smaller) can be used. Increase this (e.g., to 0.5 or 0.7) when running with larger heap sizes via `--max-old-space-size` to allow processing of proportionally larger files.
|
|
546
590
|
|
|
547
591
|
**Example:**
|
|
548
592
|
|
|
@@ -557,6 +601,11 @@ const dfLazy = await QvdDataFrame.fromQvd('path/to/file.qvd', {maxRows: 1000});
|
|
|
557
601
|
const dfSecure = await QvdDataFrame.fromQvd('reports/sales.qvd', {
|
|
558
602
|
allowedDir: '/var/data/qvd-files',
|
|
559
603
|
});
|
|
604
|
+
|
|
605
|
+
// Load with increased memory usage for large heap configurations
|
|
606
|
+
const dfLarge = await QvdDataFrame.fromQvd('large-file.qvd', {
|
|
607
|
+
memorySafetyFactor: 0.5, // Use 50% of heap instead of default 30%
|
|
608
|
+
});
|
|
560
609
|
```
|
|
561
610
|
|
|
562
611
|
#### `static fromDict(dict: object): Promise<QvdDataFrame>`
|
package/dist/index.cjs
CHANGED
|
@@ -6,6 +6,7 @@ var crypto = require('crypto');
|
|
|
6
6
|
var xml2 = require('xml2js');
|
|
7
7
|
var assert = require('assert');
|
|
8
8
|
var os = require('os');
|
|
9
|
+
var v8 = require('v8');
|
|
9
10
|
|
|
10
11
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
12
|
|
|
@@ -15,6 +16,7 @@ var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
|
15
16
|
var xml2__default = /*#__PURE__*/_interopDefault(xml2);
|
|
16
17
|
var assert__default = /*#__PURE__*/_interopDefault(assert);
|
|
17
18
|
var os__default = /*#__PURE__*/_interopDefault(os);
|
|
19
|
+
var v8__default = /*#__PURE__*/_interopDefault(v8);
|
|
18
20
|
|
|
19
21
|
var __defProp = Object.defineProperty;
|
|
20
22
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -658,6 +660,9 @@ var init_bitUtils = __esm({
|
|
|
658
660
|
"src/util/bitUtils.js"() {
|
|
659
661
|
}
|
|
660
662
|
});
|
|
663
|
+
function getHeapLimit() {
|
|
664
|
+
return v8__default.default.getHeapStatistics().heap_size_limit;
|
|
665
|
+
}
|
|
661
666
|
function estimateMemoryUsage(symbolTableSize, maxRows, totalRows) {
|
|
662
667
|
const FULL_PARSE_OVERHEAD = 6;
|
|
663
668
|
const MINIMAL_OVERHEAD = 0.01;
|
|
@@ -670,11 +675,15 @@ function estimateMemoryUsage(symbolTableSize, maxRows, totalRows) {
|
|
|
670
675
|
const skippedSymbolsMemory = symbolTableSize * (1 - symbolPercentage) * MINIMAL_OVERHEAD;
|
|
671
676
|
return keptSymbolsMemory + skippedSymbolsMemory;
|
|
672
677
|
}
|
|
673
|
-
function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath) {
|
|
678
|
+
function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.3) {
|
|
679
|
+
if (typeof safetyFactor !== "number" || safetyFactor < 0 || safetyFactor > 1) {
|
|
680
|
+
throw new exports.QvdValidationError("safetyFactor must be a number between 0.0 and 1.0", { safetyFactor });
|
|
681
|
+
}
|
|
674
682
|
const availableMemory = os__default.default.freemem();
|
|
683
|
+
const heapLimit = getHeapLimit();
|
|
675
684
|
const estimatedMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows);
|
|
676
|
-
const
|
|
677
|
-
const maxAllowedMemory =
|
|
685
|
+
const effectiveLimit = Math.min(availableMemory, heapLimit);
|
|
686
|
+
const maxAllowedMemory = effectiveLimit * safetyFactor;
|
|
678
687
|
if (estimatedMemory > maxAllowedMemory) {
|
|
679
688
|
const safeSymbolPercentage = maxAllowedMemory / (symbolTableSize * 6);
|
|
680
689
|
const safeRowPercentage = Math.pow(safeSymbolPercentage, 2);
|
|
@@ -682,14 +691,20 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
682
691
|
const sizeMB = Math.round(symbolTableSize / 1024 / 1024);
|
|
683
692
|
const estimatedMB = Math.round(estimatedMemory / 1024 / 1024);
|
|
684
693
|
const availableMB = Math.round(maxAllowedMemory / 1024 / 1024);
|
|
694
|
+
const heapLimitMB = Math.round(heapLimit / 1024 / 1024);
|
|
695
|
+
const availableRamMB = Math.round(availableMemory / 1024 / 1024);
|
|
696
|
+
const limitingFactor = heapLimit < availableMemory ? "V8 heap limit" : "available RAM";
|
|
685
697
|
throw new exports.QvdValidationError(
|
|
686
|
-
`Insufficient memory to load file safely. Symbol table: ${sizeMB}MB, Estimated memory needed: ${estimatedMB}MB, Available: ${availableMB}MB. Try loading fewer rows using the maxRows parameter (recommended: ${recommendedMaxRows.toLocaleString()} rows or less).`,
|
|
698
|
+
`Insufficient memory to load file safely. Symbol table: ${sizeMB}MB, Estimated memory needed: ${estimatedMB}MB, Available: ${availableMB}MB (limited by ${limitingFactor}: ${heapLimitMB}MB heap / ${availableRamMB}MB RAM). Try loading fewer rows using the maxRows parameter (recommended: ${recommendedMaxRows.toLocaleString()} rows or less).`,
|
|
687
699
|
{
|
|
688
700
|
file: filePath,
|
|
689
701
|
symbolTableSize,
|
|
690
702
|
symbolTableSizeMB: sizeMB,
|
|
691
703
|
estimatedMemoryMB: estimatedMB,
|
|
692
704
|
availableMemoryMB: availableMB,
|
|
705
|
+
heapLimitMB,
|
|
706
|
+
availableRamMB,
|
|
707
|
+
limitingFactor,
|
|
693
708
|
totalRows,
|
|
694
709
|
maxRows,
|
|
695
710
|
recommendedMaxRows
|
|
@@ -698,13 +713,15 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
|
|
|
698
713
|
}
|
|
699
714
|
}
|
|
700
715
|
function warnLargeSymbolTable(symbolTableSize, maxRows, totalRows) {
|
|
701
|
-
const
|
|
716
|
+
const heapLimit = getHeapLimit();
|
|
717
|
+
const LARGE_SYMBOL_TABLE_WARNING = heapLimit * 0.125;
|
|
702
718
|
if (symbolTableSize > LARGE_SYMBOL_TABLE_WARNING && (maxRows === null || maxRows >= totalRows)) {
|
|
703
719
|
const sizeMB = Math.round(symbolTableSize / 1024 / 1024);
|
|
704
720
|
const estimatedMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows);
|
|
705
721
|
const estimatedMB = Math.round(estimatedMemory / 1024 / 1024);
|
|
722
|
+
const warnMB = Math.round(LARGE_SYMBOL_TABLE_WARNING / 1024 / 1024);
|
|
706
723
|
console.warn(
|
|
707
|
-
`\u26A0\uFE0F Large symbol table detected (${sizeMB}MB). Loading all ${totalRows.toLocaleString()} rows will use ~${estimatedMB}MB RAM. Consider using the maxRows parameter for better performance and lower memory usage.`
|
|
724
|
+
`\u26A0\uFE0F Large symbol table detected (${sizeMB}MB > ${warnMB}MB threshold). Loading all ${totalRows.toLocaleString()} rows will use ~${estimatedMB}MB RAM. Consider using the maxRows parameter for better performance and lower memory usage.`
|
|
708
725
|
);
|
|
709
726
|
}
|
|
710
727
|
}
|
|
@@ -716,35 +733,43 @@ var init_memoryUtils = __esm({
|
|
|
716
733
|
|
|
717
734
|
// src/util/validationUtils.js
|
|
718
735
|
function validateSymbolTableSizeEarly(symbolTableLength, filePath) {
|
|
719
|
-
const
|
|
736
|
+
const heapLimit = getHeapLimit();
|
|
737
|
+
const MAX_SYMBOL_TABLE_SIZE = heapLimit * 0.125;
|
|
720
738
|
if (symbolTableLength > MAX_SYMBOL_TABLE_SIZE) {
|
|
721
739
|
const sizeMB = Math.round(symbolTableLength / 1024 / 1024);
|
|
722
740
|
const maxMB = Math.round(MAX_SYMBOL_TABLE_SIZE / 1024 / 1024);
|
|
741
|
+
const heapMB = Math.round(heapLimit / 1024 / 1024);
|
|
723
742
|
throw new exports.QvdValidationError(
|
|
724
|
-
`Symbol table too large (${sizeMB}MB exceeds ${maxMB}MB limit). This QVD file contains extremely high-cardinality fields.
|
|
743
|
+
`Symbol table too large (${sizeMB}MB exceeds ${maxMB}MB limit for lazy loading). This QVD file contains extremely high-cardinality fields. Limit scales with heap size (current: ${heapMB}MB, limit: 12.5% = ${maxMB}MB). Consider: (1) loading the full file without maxRows, (2) increasing heap size with --max-old-space-size, or (3) aggregating high-cardinality fields.`,
|
|
725
744
|
{
|
|
726
745
|
file: filePath,
|
|
727
746
|
symbolTableSize: symbolTableLength,
|
|
728
747
|
symbolTableSizeMB: sizeMB,
|
|
729
748
|
maxAllowed: MAX_SYMBOL_TABLE_SIZE,
|
|
730
|
-
maxAllowedMB: maxMB
|
|
749
|
+
maxAllowedMB: maxMB,
|
|
750
|
+
heapLimitMB: heapMB,
|
|
751
|
+
limitPercentage: 12.5
|
|
731
752
|
}
|
|
732
753
|
);
|
|
733
754
|
}
|
|
734
755
|
}
|
|
735
756
|
function validateSymbolTableSize(symbolTableLength, filePath, totalRows) {
|
|
736
|
-
const
|
|
757
|
+
const heapLimit = getHeapLimit();
|
|
758
|
+
const ABSOLUTE_MAX_SYMBOL_TABLE = heapLimit * 0.5;
|
|
737
759
|
if (symbolTableLength > ABSOLUTE_MAX_SYMBOL_TABLE) {
|
|
738
760
|
const sizeMB = Math.round(symbolTableLength / 1024 / 1024);
|
|
739
761
|
const maxMB = Math.round(ABSOLUTE_MAX_SYMBOL_TABLE / 1024 / 1024);
|
|
762
|
+
const heapMB = Math.round(heapLimit / 1024 / 1024);
|
|
740
763
|
throw new exports.QvdValidationError(
|
|
741
|
-
`Symbol table exceeds absolute maximum size (${sizeMB}MB > ${maxMB}MB). This QVD file has pathological cardinality (likely a data modeling issue).
|
|
764
|
+
`Symbol table exceeds absolute maximum size (${sizeMB}MB > ${maxMB}MB). This QVD file has pathological cardinality (likely a data modeling issue). Limit scales with heap size (current: ${heapMB}MB, limit: 50% = ${maxMB}MB). Consider: (1) increasing heap size with --max-old-space-size, (2) aggregating high-cardinality fields, (3) splitting the data, or (4) using a different format.`,
|
|
742
765
|
{
|
|
743
766
|
file: filePath,
|
|
744
767
|
symbolTableSize: symbolTableLength,
|
|
745
768
|
symbolTableSizeMB: sizeMB,
|
|
746
769
|
maxAllowed: ABSOLUTE_MAX_SYMBOL_TABLE,
|
|
747
770
|
maxAllowedMB: maxMB,
|
|
771
|
+
heapLimitMB: heapMB,
|
|
772
|
+
limitPercentage: 50,
|
|
748
773
|
totalRows
|
|
749
774
|
}
|
|
750
775
|
);
|
|
@@ -881,6 +906,7 @@ function validateFieldBitMetadata(field, recordSize, filePath) {
|
|
|
881
906
|
var init_validationUtils = __esm({
|
|
882
907
|
"src/util/validationUtils.js"() {
|
|
883
908
|
init_QvdErrors();
|
|
909
|
+
init_memoryUtils();
|
|
884
910
|
}
|
|
885
911
|
});
|
|
886
912
|
|
|
@@ -1163,10 +1189,14 @@ var init_QvdFileReader = __esm({
|
|
|
1163
1189
|
* @param {Object} [options={}] Options for the reader.
|
|
1164
1190
|
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
|
|
1165
1191
|
* path must be within this directory. Defaults to current working directory.
|
|
1192
|
+
* @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines
|
|
1193
|
+
* what percentage of available memory (or V8 heap limit, whichever is smaller) can be used
|
|
1194
|
+
* for loading QVD files. Default is 0.3 (30%). Increase for larger heap configurations.
|
|
1166
1195
|
*/
|
|
1167
1196
|
constructor(filePath, options = {}) {
|
|
1168
|
-
const { allowedDir } = options;
|
|
1197
|
+
const { allowedDir, memorySafetyFactor = 0.3 } = options;
|
|
1169
1198
|
this._path = validatePath(filePath, allowedDir);
|
|
1199
|
+
this._memorySafetyFactor = memorySafetyFactor;
|
|
1170
1200
|
this._buffer = null;
|
|
1171
1201
|
this._headerOffset = null;
|
|
1172
1202
|
this._symbolTableOffset = null;
|
|
@@ -1390,7 +1420,7 @@ var init_QvdFileReader = __esm({
|
|
|
1390
1420
|
const symbolTableSize = symbolBuffer.length;
|
|
1391
1421
|
const totalRows = parseInt(this._header["QvdTableHeader"]["NoOfRecords"], 10);
|
|
1392
1422
|
validateSymbolTableSize(symbolTableSize, this._path, totalRows);
|
|
1393
|
-
validateMemoryAvailability(symbolTableSize, maxRows, totalRows, this._path);
|
|
1423
|
+
validateMemoryAvailability(symbolTableSize, maxRows, totalRows, this._path, this._memorySafetyFactor);
|
|
1394
1424
|
warnLargeSymbolTable(symbolTableSize, maxRows, totalRows);
|
|
1395
1425
|
if (!Array.isArray(fields)) {
|
|
1396
1426
|
fields = [fields];
|
|
@@ -1971,11 +2001,17 @@ var init_QvdDataFrame = __esm({
|
|
|
1971
2001
|
* @param {Object} [options] Optional loading options.
|
|
1972
2002
|
* @param {number|null} [options.maxRows] The maximum number of rows to load. If not specified, all rows are loaded.
|
|
1973
2003
|
* @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path must be within this directory.
|
|
2004
|
+
* @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines what percentage
|
|
2005
|
+
* of available memory (or V8 heap limit, whichever is smaller) can be used. Default is 0.3 (30%).
|
|
2006
|
+
* Increase this (e.g., to 0.5 or 0.7) when running with larger heap sizes via --max-old-space-size.
|
|
1974
2007
|
* @return {Promise<QvdDataFrame>} The data frame of the QVD file.
|
|
1975
2008
|
*/
|
|
1976
2009
|
static async fromQvd(path3, options = {}) {
|
|
1977
2010
|
const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
|
|
1978
|
-
const readerOptions = {
|
|
2011
|
+
const readerOptions = {
|
|
2012
|
+
allowedDir: options.allowedDir,
|
|
2013
|
+
memorySafetyFactor: options.memorySafetyFactor
|
|
2014
|
+
};
|
|
1979
2015
|
return await new QvdFileReader2(path3, readerOptions).load(options.maxRows !== void 0 ? options.maxRows : null);
|
|
1980
2016
|
}
|
|
1981
2017
|
/**
|