qvdjs 0.10.0 → 0.10.1

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 CHANGED
@@ -135,6 +135,43 @@ console.log(df.head(5));
135
135
  The above example loads the _qvdjs_ library and parses an example QVD file. A QVD file is typically loaded using the static
136
136
  `QvdDataFrame.fromQvd` function of the `QvdDataFrame` class itself. After loading the file's content, numerous methods and properties are available to work with the parsed data.
137
137
 
138
+ ### Three ways to open a file
139
+
140
+ `fromQvd` is the general one, and often not the one you want. A QVD is an XML header, then a
141
+ symbol table of every distinct value, then a bit-packed index table of one code per cell — so how
142
+ far into the file a read has to go is what separates these, and all three take the same options.
143
+
144
+ | You want | Call | How far it reads |
145
+ | --------------------------------------- | --------------------------------- | ---------------------------------------------------------------------- |
146
+ | Rows, to index, iterate or write back | `QvdDataFrame.fromQvd(path)` | Everything, and materialises every row |
147
+ | A few columns of a large file | `QvdColumnTable.fromQvd(path)` | Everything, but stops before building rows — 141 MiB against 385 MiB |
148
+ | Only the schema: names, row count, types | `QvdDataFrame.readMetadata(path)` | The header alone. Constant cost, whatever the file's size |
149
+
150
+ ```javascript
151
+ import {QvdDataFrame, QvdColumnTable} from 'qvdjs';
152
+
153
+ // What is in this file? Costs the same whether it is 20 KB or 20 GB.
154
+ const {columns, rowCount} = await QvdDataFrame.readMetadata('sales.qvd');
155
+ console.log(`${rowCount} rows x ${columns.length} columns`);
156
+
157
+ // Sum one column without ever building a row.
158
+ const table = await QvdColumnTable.fromQvd('sales.qvd');
159
+ let total = 0;
160
+ for (const value of table.column('amount')) {
161
+ if (typeof value === 'number') total += value;
162
+ }
163
+
164
+ // Rows, when you want rows.
165
+ const df = await QvdDataFrame.fromQvd('sales.qvd');
166
+ console.log(df.head(5));
167
+ ```
168
+
169
+ Reaching for `fromQvd` when you wanted one of the other two is the common mistake, and
170
+ `fromQvd(path, {maxRows: 0})` is not a substitute for `readMetadata`: it loads no rows but still
171
+ reads and parses the whole symbol table, which grows with the data. See
172
+ [QvdColumnTable](#qvdcolumntable) and the [API Documentation](#api-documentation) for the
173
+ details and the measurements behind the table above.
174
+
138
175
  ### Lazy Loading
139
176
 
140
177
  For large QVD files, you can load only a specific number of rows to improve performance and reduce memory usage. The library implements **lazy loading** - it reads only the necessary portions of the file from disk, not the entire file.
package/dist/index.cjs CHANGED
@@ -957,6 +957,24 @@ var init_memoryUtils = __esm({
957
957
  });
958
958
 
959
959
  // src/util/validationUtils.js
960
+ function validateHeaderStructure(headerObj, filePath, stage) {
961
+ const tableHeader = headerObj?.["QvdTableHeader"];
962
+ if (tableHeader === null || typeof tableHeader !== "object" || Array.isArray(tableHeader)) {
963
+ throw new exports.QvdCorruptedError("The XML header contains no usable QvdTableHeader element", {
964
+ rootElements: headerObj && typeof headerObj === "object" ? Object.keys(headerObj) : [],
965
+ file: filePath,
966
+ stage
967
+ });
968
+ }
969
+ const symbolTableLength = parseInt(tableHeader["Offset"], 10);
970
+ if (isNaN(symbolTableLength) || !Number.isSafeInteger(symbolTableLength) || symbolTableLength < 0) {
971
+ throw new exports.QvdCorruptedError("Invalid symbol table offset", {
972
+ offset: tableHeader["Offset"],
973
+ file: filePath,
974
+ stage
975
+ });
976
+ }
977
+ }
960
978
  function validateSymbolTableSizeEarly(symbolTableLength, filePath) {
961
979
  const heapLimit = getHeapLimit();
962
980
  const MAX_SYMBOL_TABLE_SIZE = heapLimit * 0.125;
@@ -1861,6 +1879,7 @@ var init_QvdFileReader = __esm({
1861
1879
  stage: "readData"
1862
1880
  });
1863
1881
  }
1882
+ validateHeaderStructure(headerObj, this._path, "readData");
1864
1883
  const symbolTableOffset = headerEndIndex;
1865
1884
  const symbolTableLength = parseInt(headerObj["QvdTableHeader"]["Offset"], 10);
1866
1885
  const indexTableOffset = symbolTableOffset + symbolTableLength;
@@ -1984,14 +2003,26 @@ var init_QvdFileReader = __esm({
1984
2003
  stage: "parseHeader"
1985
2004
  });
1986
2005
  }
2006
+ validateHeaderStructure(this._header, this._path, "parseHeader");
1987
2007
  const fields = this._header["QvdTableHeader"]?.["Fields"]?.["QvdFieldHeader"];
1988
- const fieldCount = fields === void 0 || fields === null ? 0 : Array.isArray(fields) ? fields.length : 1;
1989
- if (fieldCount === 0) {
2008
+ const fieldList = fields === void 0 || fields === null ? [] : Array.isArray(fields) ? fields : [fields];
2009
+ if (fieldList.length === 0) {
1990
2010
  throw new exports.QvdCorruptedError("The QVD file header declares no fields", {
1991
2011
  file: this._path,
1992
2012
  stage: "parseHeader"
1993
2013
  });
1994
2014
  }
2015
+ const malformedIndex = fieldList.findIndex(
2016
+ (field) => field === null || typeof field !== "object" || Array.isArray(field)
2017
+ );
2018
+ if (malformedIndex !== -1) {
2019
+ throw new exports.QvdCorruptedError("The QVD file header declares a field with no properties", {
2020
+ fieldIndex: malformedIndex,
2021
+ fieldCount: fieldList.length,
2022
+ file: this._path,
2023
+ stage: "parseHeader"
2024
+ });
2025
+ }
1995
2026
  this._headerOffset = headerBeginIndex;
1996
2027
  this._symbolTableOffset = headerEndIndex;
1997
2028
  this._indexTableOffset = this._symbolTableOffset + parseInt(this._header["QvdTableHeader"]["Offset"], 10);