@gscdump/engine 7.5.1 → 7.6.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 CHANGED
@@ -42,6 +42,19 @@ options produces identical bytes regardless of producer batch boundaries.
42
42
  This supports a counting/digest pass followed by a separately coordinated
43
43
  fixed-length upload, without the codec owning transport or commit semantics.
44
44
 
45
+ The streaming encoder normally refuses when retained footer/index descriptors
46
+ exceed `limits.maxRetainedMetadataBytes` (8 MiB by default). For wide values,
47
+ callers can explicitly choose `metadataOverflow: 'omit-statistics'`. On the
48
+ first overflow it omits column-chunk statistics and ColumnIndexes throughout
49
+ that file, including earlier groups. OffsetIndexes, encoding descriptors,
50
+ schema, row counts, data/dictionary pages and explicit `fileMetadata` remain
51
+ unchanged. Required descriptors must still fit the same budget, and the
52
+ group/footer output-buffer limit still applies. Missing statistics mean unknown
53
+ bounds, so readers may scan more pages or groups; they do not mean empty data.
54
+ If no overflow occurs, this option preserves the normal bytes. It bounds
55
+ retained optional descriptors, not their transient construction or total heap.
56
+ The array encoders and all omitted-option behavior remain unchanged.
57
+
45
58
  `decodeParquetRowGroups({ byteLength, slice }, options)` reads exact byte ranges
46
59
  and by default yields one physical row group at a time. The caller must pin every range
47
60
  to the same immutable object or ETag. It preserves the existing date, column
@@ -29,6 +29,13 @@ export interface EncodeParquetBatchesOptions extends EncodeParquetOptions {
29
29
  columns: readonly ColumnDef[];
30
30
  /** Maximum rows per output group. Default 25000, matching the array codec. */
31
31
  rowGroupSize?: number;
32
+ /**
33
+ * Opt in to omitting all footer column statistics and ColumnIndexes if they
34
+ * exceed the retained metadata limit. Default: refuse. OffsetIndexes,
35
+ * required descriptors and explicit file metadata remain bounded/preserved.
36
+ * Missing statistics reduce pruning; this does not bound transient codec heap.
37
+ */
38
+ metadataOverflow?: 'omit-statistics';
32
39
  limits?: {
33
40
  /** Refuse a producer batch with more rows. Default 25000. */
34
41
  maxBatchRows?: number;
@@ -219,6 +219,11 @@ function streamLimit(value, fallback, name) {
219
219
  function scalarBytes(value) {
220
220
  return typeof value === "string" ? value.length * 3 + 8 : 16;
221
221
  }
222
+ var RetainedMetadataLimitError = class extends RangeError {
223
+ constructor() {
224
+ super("parquet stream exceeds maxRetainedMetadataBytes");
225
+ }
226
+ };
222
227
  function retainedDescriptorBytes(value, remaining) {
223
228
  const seen = /* @__PURE__ */ new Set();
224
229
  let bytes = 0;
@@ -233,12 +238,14 @@ function retainedDescriptorBytes(value, remaining) {
233
238
  visit(value[key]);
234
239
  }
235
240
  } else bytes += scalarBytes(value);
236
- if (bytes > remaining) throw new RangeError("parquet stream exceeds maxRetainedMetadataBytes");
241
+ if (bytes > remaining) throw new RetainedMetadataLimitError();
237
242
  };
238
243
  visit(value);
239
244
  return bytes;
240
245
  }
241
246
  async function encodeParquetRowBatches(batches, opts, write) {
247
+ const metadataOverflow = opts.metadataOverflow;
248
+ if (metadataOverflow !== void 0 && metadataOverflow !== "omit-statistics") throw new RangeError("parquet stream metadataOverflow must be omit-statistics when provided");
242
249
  const rowGroupSize = streamLimit(opts.rowGroupSize, ROW_GROUP_SIZE, "rowGroupSize");
243
250
  const maxBatchRows = streamLimit(opts.limits?.maxBatchRows, ROW_GROUP_SIZE, "maxBatchRows");
244
251
  const maxBatchBytes = streamLimit(opts.limits?.maxBatchBytes, 8388608, "maxBatchBytes");
@@ -260,7 +267,8 @@ async function encodeParquetRowBatches(batches, opts, write) {
260
267
  metadataBytes += scalarBytes(key) + scalarBytes(opts.fileMetadata[key]);
261
268
  if (metadataBytes > maxOutputBufferBytes) throw new RangeError("parquet stream file metadata exceeds maxOutputBufferBytes");
262
269
  }
263
- let retainedMetadataBytes = retainedDescriptorBytes([schema, opts.fileMetadata], maxRetainedMetadataBytes);
270
+ const initialMetadataBytes = retainedDescriptorBytes([schema, opts.fileMetadata], maxRetainedMetadataBytes);
271
+ let retainedMetadataBytes = initialMetadataBytes;
264
272
  const writer = new ByteWriter(Math.min(65536, maxOutputBufferBytes));
265
273
  writer.ensure = (size) => {
266
274
  const required = writer.index + size;
@@ -293,6 +301,14 @@ async function encodeParquetRowBatches(batches, opts, write) {
293
301
  let groupBytes = 0;
294
302
  let groups = 0;
295
303
  let rowCount = 0;
304
+ let omitStatistics = false;
305
+ const omitOptionalMetadata = (firstIndex) => {
306
+ for (let index = firstIndex; index < parquet.pendingIndexes.length; index++) {
307
+ const pending = parquet.pendingIndexes[index];
308
+ if (pending.chunk.meta_data) delete pending.chunk.meta_data.statistics;
309
+ delete pending.columnIndex;
310
+ }
311
+ };
296
312
  const flushGroup = async () => {
297
313
  if (!groupRows) return;
298
314
  if (++groups > maxRowGroups) throw new RangeError("parquet stream exceeds maxRowGroups");
@@ -306,7 +322,21 @@ async function encodeParquetRowBatches(batches, opts, write) {
306
322
  })),
307
323
  rowGroupSize: groupRows
308
324
  });
309
- retainedMetadataBytes += retainedDescriptorBytes([parquet.row_groups[parquet.row_groups.length - 1], parquet.pendingIndexes.slice(firstIndex)], maxRetainedMetadataBytes - retainedMetadataBytes);
325
+ if (omitStatistics) omitOptionalMetadata(firstIndex);
326
+ try {
327
+ retainedMetadataBytes += retainedDescriptorBytes([parquet.row_groups[parquet.row_groups.length - 1], parquet.pendingIndexes.slice(firstIndex)], maxRetainedMetadataBytes - retainedMetadataBytes);
328
+ } catch (error) {
329
+ if (!(error instanceof RetainedMetadataLimitError) || metadataOverflow !== "omit-statistics" || omitStatistics) throw error;
330
+ omitStatistics = true;
331
+ omitOptionalMetadata(0);
332
+ retainedMetadataBytes = initialMetadataBytes;
333
+ let index = 0;
334
+ for (const group of parquet.row_groups) {
335
+ const end = index + group.columns.length;
336
+ retainedMetadataBytes += retainedDescriptorBytes([group, parquet.pendingIndexes.slice(index, end)], maxRetainedMetadataBytes - retainedMetadataBytes);
337
+ index = end;
338
+ }
339
+ }
310
340
  values = codecs.map(() => []);
311
341
  groupRows = 0;
312
342
  groupBytes = 0;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine",
3
3
  "type": "module",
4
- "version": "7.5.1",
4
+ "version": "7.6.0",
5
5
  "description": "Append-only Parquet/DuckDB storage engine + planner + adapters for the gscdump pipeline. Node + edge runtimes; opt-in heavy peers.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -189,10 +189,10 @@
189
189
  }
190
190
  },
191
191
  "dependencies": {
192
- "@gscdump/contracts": "^7.5.1",
193
- "@gscdump/lakehouse": "^7.5.1",
192
+ "@gscdump/contracts": "^7.6.0",
193
+ "@gscdump/lakehouse": "^7.6.0",
194
194
  "drizzle-orm": "1.0.0-rc.4",
195
- "gscdump": "^7.5.1",
195
+ "gscdump": "^7.6.0",
196
196
  "pathe": "^2.0.3",
197
197
  "proper-lockfile": "^4.1.2"
198
198
  },