@gscdump/engine 7.4.0 → 7.5.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 +112 -1
- package/dist/adapters/hyparquet.d.mts +27 -0
- package/dist/adapters/hyparquet.mjs +146 -20
- package/dist/adapters/parquet-bounded-values.d.mts +18 -0
- package/dist/adapters/parquet-bounded-values.mjs +318 -0
- package/dist/adapters/parquet-decode-budget.mjs +61 -0
- package/dist/adapters/parquet-page-header.mjs +136 -0
- package/dist/adapters/parquet-range-cache.mjs +41 -0
- package/dist/adapters/parquet-value-cursor.mjs +108 -0
- package/dist/node_modules/.pnpm/hyparquet@1.31.1/node_modules/hyparquet/src/index.mjs +3 -3
- package/dist/node_modules/.pnpm/hyparquet@1.31.1/node_modules/hyparquet/src/node.mjs +3 -3
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -43,11 +43,122 @@ 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
45
|
`decodeParquetRowGroups({ byteLength, slice }, options)` reads exact byte ranges
|
|
46
|
-
and yields one physical row group at a time. The caller must pin every range
|
|
46
|
+
and by default yields one physical row group at a time. The caller must pin every range
|
|
47
47
|
to the same immutable object or ETag. It preserves the existing date, column
|
|
48
48
|
projection, row-window and filter behavior. An absent, corrupt, oversized or
|
|
49
49
|
incompletely read file throws; only valid empty Parquet yields no rows.
|
|
50
50
|
|
|
51
|
+
The range reader alone accepts an optional `indexedRowWindow` policy:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
decodeParquetRowGroups(pinnedObject, {
|
|
55
|
+
indexedRowWindow: {
|
|
56
|
+
targetRows: 1000,
|
|
57
|
+
requiredIndexedColumns: ['query', 'url'],
|
|
58
|
+
rangeCacheBytes: 2 * 1024 * 1024,
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Each group uses windows only when all designated columns are present, selected,
|
|
64
|
+
have usable offset-index descriptors, and have no dictionary encoding. The
|
|
65
|
+
schema must contain only flat scalar/UTF8/date fields; binary, nested and other
|
|
66
|
+
unsupported schemas retain the original group read. Ineligible groups use the
|
|
67
|
+
original uncached path, so `targetRows` is a target rather than a hard batch
|
|
68
|
+
limit. Include every potentially large column in `requiredIndexedColumns`.
|
|
69
|
+
All existing physical-group admission limits still apply.
|
|
70
|
+
|
|
71
|
+
Before emitting an eligible group's first window, the reader checks index
|
|
72
|
+
structure for projected columns, or all indexed columns when a filter may need
|
|
73
|
+
unprojected fields: row zero, increasing row positions, ordered nonoverlapping
|
|
74
|
+
page ranges and physical column bounds. A failure throws without falling back
|
|
75
|
+
after partial output. This assumes metadata row offsets agree with physical
|
|
76
|
+
page contents; it does not independently verify page-header row counts or
|
|
77
|
+
detect every deliberately consistent but false index.
|
|
78
|
+
|
|
79
|
+
The optional range cache defaults to zero, allows at most 2 MiB of payload and
|
|
80
|
+
128 entries, and lives only through one eligible group. It is cleared on
|
|
81
|
+
completion, error or iterator return; another pass rereads the pinned object.
|
|
82
|
+
It does not cache decoded rows or provide publication/ETag coordination. Windows
|
|
83
|
+
can otherwise reread overlapping pages and substantially increase range I/O.
|
|
84
|
+
They still decode whole boundary pages, and dictionaries in other columns can
|
|
85
|
+
still allocate in full. These options do not bound decompressed page sizes or
|
|
86
|
+
the total heap; the unchanged fallback also retains its existing memory costs.
|
|
87
|
+
Buffered decoders, encoders and all omitted-option behavior remain unchanged.
|
|
88
|
+
|
|
89
|
+
The range reader can also opt into `limits.maxReadAndDecompressBytes`. This is a
|
|
90
|
+
**cumulative buffer-work budget for each physical group**, shared across all its
|
|
91
|
+
indexed windows and concurrent column reads. It charges requested source-range
|
|
92
|
+
bytes before dispatch and SNAPPY output bytes before allocating the fixed-size
|
|
93
|
+
output buffer. Cache hits do not allocate another range, while repeated
|
|
94
|
+
decompression still consumes budget. Group indexes are included; the initial
|
|
95
|
+
footer has its separate metadata limit. Raw/uncompressed pages and V2 levels
|
|
96
|
+
are covered by the range charge, so compressed data is deliberately counted
|
|
97
|
+
twice. Earlier charges are never refunded, including after a failed read. Failure
|
|
98
|
+
or iterator return closes the budget, preventing new work and rejecting late
|
|
99
|
+
range results. It cannot physically cancel already dispatched reads: the range
|
|
100
|
+
source interface has no cancellation hook.
|
|
101
|
+
|
|
102
|
+
ZSTD decompression refuses under this option: the current allocating decoder
|
|
103
|
+
can allocate from its frame header before checking the Parquet output size.
|
|
104
|
+
Without the option, ZSTD and all existing decoding behavior remain unchanged.
|
|
105
|
+
The budget does not measure JavaScript strings, expanded values, row containers,
|
|
106
|
+
internal prefetch or dictionary/page stitching copies, codec working state or
|
|
107
|
+
caller-retained batches. It is
|
|
108
|
+
not a total heap bound or protection against all malformed Parquet contents.
|
|
109
|
+
It can reject valid files that previously decoded, particularly old files whose
|
|
110
|
+
writer understated uncompressed footer sizes. Callers must preserve those files
|
|
111
|
+
and choose their own explicit retry/repair policy; refusal is never empty data.
|
|
112
|
+
|
|
113
|
+
`decodeParquetRowGroups(source, { boundedValues: {} })` selects a separate strict
|
|
114
|
+
value-cursor path. It supports flat REQUIRED/OPTIONAL scalar columns, explicitly
|
|
115
|
+
annotated UTF8/STRING, PLAIN and dictionary data pages (V1/V2), and SNAPPY or
|
|
116
|
+
UNCOMPRESSED data. Projection, physical row ranges, nulls, date normalization and
|
|
117
|
+
row order are preserved. Nested/binary/other logical types, other codecs or value
|
|
118
|
+
encodings, BOOLEAN dictionaries, filters and `indexedRowWindow` combinations
|
|
119
|
+
refuse; this mode never silently falls back to the eager reader. All selected
|
|
120
|
+
page headers are inspected before the first output to detect unsupported later
|
|
121
|
+
pages. Corrupt value bodies or over-limit individual values may fail after an
|
|
122
|
+
earlier batch; consumers must finish validating their entire input before
|
|
123
|
+
publishing a replacement.
|
|
124
|
+
Explicit row bounds in this strict mode must be nonnegative safe integers, with
|
|
125
|
+
positive Infinity also accepted for an open-ended `rowEnd`. Omitted bounds use
|
|
126
|
+
the full input, and reversed or zero-length ranges are empty.
|
|
127
|
+
|
|
128
|
+
The cursor retains at most one current data page and one encoded dictionary per
|
|
129
|
+
selected column. UTF8 strings, definition levels and dictionary IDs are consumed
|
|
130
|
+
incrementally. Dictionaries retain encoded bytes and a four-byte offset per
|
|
131
|
+
entry, rather than an array of decoded strings. Final bit-packed padding is
|
|
132
|
+
validated but not expanded. These options have independent meanings:
|
|
133
|
+
|
|
134
|
+
| `boundedValues` option | Default | Admission rule |
|
|
135
|
+
| --- | --- | --- |
|
|
136
|
+
| `targetRows` | 1000 | Hard emitted batch row limit |
|
|
137
|
+
| `maxBatchBytes` | 1 MiB | Before materialization: 128 per row, 16 per cell slot, 64 per cell plus twice UTF8 byte length or 32 for a fixed scalar |
|
|
138
|
+
| `maxValueBytes` | 256 KiB | Encoded bytes of one string, before decoding |
|
|
139
|
+
| `maxActiveBytes` | 24 MiB | Per group: sum of each column's largest compressed + uncompressed data page, dictionary compressed + uncompressed bytes, dictionary offset tables, plus one maximum header probe |
|
|
140
|
+
| `maxPages` | 16384 | Retained page-descriptor count across the selected input |
|
|
141
|
+
| `maxHeaderBytes` | 64 KiB | Header probe cap; starts at 512 bytes and grows only on truncation |
|
|
142
|
+
| `maxDictionaryEntries` | 100000 | Entry count for each encoded dictionary |
|
|
143
|
+
|
|
144
|
+
Active-buffer admission uses real page headers, not the often understated
|
|
145
|
+
uncompressed sizes in legacy writer footers. Page transitions release the old
|
|
146
|
+
cursor before reading/decompressing its successor; columns load sequentially.
|
|
147
|
+
Batch bytes are a conservative value-materialization charge, **not a measurement
|
|
148
|
+
of V8 object overhead or an isolate heap guarantee**. The footer (separately
|
|
149
|
+
bounded), page-descriptor objects, fixed cursor/schema state, caller-retained
|
|
150
|
+
batches, codec/runtime overhead and concurrent application work remain separate.
|
|
151
|
+
Setting larger limits deliberately permits a larger working set. A single page
|
|
152
|
+
still decompresses in full, and a consumer that retains all emitted batches
|
|
153
|
+
removes the benefit. This mode does not make arbitrary malicious files safe.
|
|
154
|
+
|
|
155
|
+
The existing `maxReadAndDecompressBytes` option also applies: a group's header
|
|
156
|
+
probes, physical reads and SNAPPY output share one cumulative budget. Headers
|
|
157
|
+
are scanned once; immutable/pinned object bytes are required across that scan
|
|
158
|
+
and later reads. Iterator return/error releases cursor references; already
|
|
159
|
+
dispatched source reads cannot be physically aborted. Defaults, the buffered
|
|
160
|
+
decoder and indexed-window eligibility/fallback are unchanged.
|
|
161
|
+
|
|
51
162
|
Both APIs have configurable admission limits. The encoder bounds producer
|
|
52
163
|
batch rows/value bytes, output group rows/value bytes, retained descriptor counts
|
|
53
164
|
and scalar bytes (including statistics), and the current encoded group/footer
|
|
@@ -2,6 +2,7 @@ import { ParquetQueryFilter } from "../node_modules/.pnpm/hyparquet@1.31.1/node_
|
|
|
2
2
|
import "../node_modules/.pnpm/hyparquet@1.31.1/node_modules/hyparquet/types/node.mjs";
|
|
3
3
|
import { CodecCtx, DataSource, ParquetCodec, Row, TableName, WriteResult } from "../storage.mjs";
|
|
4
4
|
import { ColumnDef } from "../schema.mjs";
|
|
5
|
+
import { BoundedValuesOptions } from "./parquet-bounded-values.mjs";
|
|
5
6
|
export interface EncodeParquetOptions {
|
|
6
7
|
/** File footer metadata, without adding fact columns. Keys are encoded in sorted order. */
|
|
7
8
|
fileMetadata?: Readonly<Record<string, string>>;
|
|
@@ -85,6 +86,25 @@ export interface ParquetRangeSource {
|
|
|
85
86
|
slice: (start: number, end?: number) => ArrayBuffer | Promise<ArrayBuffer>;
|
|
86
87
|
}
|
|
87
88
|
export interface DecodeParquetGroupsOptions extends DecodeParquetOptions {
|
|
89
|
+
/**
|
|
90
|
+
* Strict opt-in value cursors for flat scalar PLAIN/dictionary SNAPPY or raw
|
|
91
|
+
* pages. Rejects unsupported inputs instead of falling back to eager decoding.
|
|
92
|
+
* Page buffers and emitted values have separate bounds; not an isolate heap cap.
|
|
93
|
+
* Cannot be combined with filters or indexedRowWindow.
|
|
94
|
+
*/
|
|
95
|
+
boundedValues?: BoundedValuesOptions;
|
|
96
|
+
/**
|
|
97
|
+
* Opt-in windows for flat scalar groups whose designated, selected columns
|
|
98
|
+
* have offset indexes and no dictionary encoding. Ineligible groups retain
|
|
99
|
+
* the original physical-group read, so targetRows is not a hard batch limit.
|
|
100
|
+
* Windows still decode complete overlapping pages, not a bounded heap size.
|
|
101
|
+
*/
|
|
102
|
+
indexedRowWindow?: {
|
|
103
|
+
targetRows: number;
|
|
104
|
+
requiredIndexedColumns: readonly string[];
|
|
105
|
+
/** Per eligible group; 0 disables caching. Default 0, maximum 2 MiB/128 entries. */
|
|
106
|
+
rangeCacheBytes?: number;
|
|
107
|
+
};
|
|
88
108
|
limits?: {
|
|
89
109
|
/** Footer bytes before deserialization. Default 1 MiB. */
|
|
90
110
|
maxMetadataBytes?: number;
|
|
@@ -96,6 +116,13 @@ export interface DecodeParquetGroupsOptions extends DecodeParquetOptions {
|
|
|
96
116
|
maxRowGroups?: number;
|
|
97
117
|
/** Maximum individual range request. Default 16 MiB. */
|
|
98
118
|
maxReadBytes?: number;
|
|
119
|
+
/**
|
|
120
|
+
* Optional cumulative source-range + SNAPPY-output bytes per physical group,
|
|
121
|
+
* including all indexed windows. Charged before reads/output allocation;
|
|
122
|
+
* cache hits do not allocate another range. Deliberately conservative, not
|
|
123
|
+
* total JS heap. ZSTD refuses only with this option. Omission is unchanged.
|
|
124
|
+
*/
|
|
125
|
+
maxReadAndDecompressBytes?: number;
|
|
99
126
|
};
|
|
100
127
|
}
|
|
101
128
|
/**
|
|
@@ -2,12 +2,16 @@ import { TABLE_METADATA } from "../drizzle-schema.mjs";
|
|
|
2
2
|
import { SCHEMAS } from "../schema.mjs";
|
|
3
3
|
import { decompress } from "../node_modules/.pnpm/fzstd@0.1.1/node_modules/fzstd/esm/index.mjs";
|
|
4
4
|
import { parquetMetadata, parquetMetadataAsync, parquetSchema } from "../node_modules/.pnpm/hyparquet@1.31.1/node_modules/hyparquet/src/metadata.mjs";
|
|
5
|
+
import { readOffsetIndex } from "../node_modules/.pnpm/hyparquet@1.31.1/node_modules/hyparquet/src/indexes.mjs";
|
|
5
6
|
import { parquetReadObjects } from "../node_modules/.pnpm/hyparquet@1.31.1/node_modules/hyparquet/src/read.mjs";
|
|
6
7
|
import "../node_modules/.pnpm/hyparquet@1.31.1/node_modules/hyparquet/src/node.mjs";
|
|
7
8
|
import { ByteWriter } from "../node_modules/.pnpm/hyparquet-writer@0.16.10_patch_hash_d26d002aede61ad95dd633d6557b56897cdcbc45dfc3a5d93fa0e9075ad5425f/node_modules/hyparquet-writer/src/bytewriter.mjs";
|
|
8
9
|
import { ParquetWriter } from "../node_modules/.pnpm/hyparquet-writer@0.16.10_patch_hash_d26d002aede61ad95dd633d6557b56897cdcbc45dfc3a5d93fa0e9075ad5425f/node_modules/hyparquet-writer/src/parquet-writer.mjs";
|
|
9
10
|
import { parquetWriteRows } from "../node_modules/.pnpm/hyparquet-writer@0.16.10_patch_hash_d26d002aede61ad95dd633d6557b56897cdcbc45dfc3a5d93fa0e9075ad5425f/node_modules/hyparquet-writer/src/write-rows.mjs";
|
|
10
11
|
import "../node_modules/.pnpm/hyparquet-writer@0.16.10_patch_hash_d26d002aede61ad95dd633d6557b56897cdcbc45dfc3a5d93fa0e9075ad5425f/node_modules/hyparquet-writer/src/node.mjs";
|
|
12
|
+
import { parquetDecodeBudget } from "./parquet-decode-budget.mjs";
|
|
13
|
+
import { boundedValuesPolicy, decodeBoundedValues } from "./parquet-bounded-values.mjs";
|
|
14
|
+
import { parquetRangeCache } from "./parquet-range-cache.mjs";
|
|
11
15
|
const ROW_GROUP_SIZE = 25e3;
|
|
12
16
|
const DEFAULT_COMPACTION_READ_CONCURRENCY = 8;
|
|
13
17
|
const MAX_COMPACTION_READ_CONCURRENCY = 32;
|
|
@@ -363,6 +367,19 @@ async function* decodeParquetGroups(bytes, opts = {}) {
|
|
|
363
367
|
yield* decodeGroupsFromMetadata(file, await parquetMetadataAsync(file), opts);
|
|
364
368
|
}
|
|
365
369
|
async function* decodeParquetRowGroups(file, opts = {}) {
|
|
370
|
+
const values = boundedValuesPolicy(opts.boundedValues);
|
|
371
|
+
const selection = values ? {
|
|
372
|
+
columns: opts.columns?.slice(),
|
|
373
|
+
rowStart: opts.rowStart,
|
|
374
|
+
rowEnd: opts.rowEnd
|
|
375
|
+
} : void 0;
|
|
376
|
+
if (values) for (const key of ["rowStart", "rowEnd"]) {
|
|
377
|
+
const value = selection[key];
|
|
378
|
+
if (value !== void 0 && !(key === "rowEnd" && value === Infinity) && (!Number.isSafeInteger(value) || value < 0)) throw new RangeError(`invalid boundedValues.${key}`);
|
|
379
|
+
}
|
|
380
|
+
if (values && (opts.filter || opts.indexedRowWindow)) throw new Error("boundedValues cannot be combined with filter or indexedRowWindow");
|
|
381
|
+
const indexedWindow = indexedWindowPolicy(opts.indexedRowWindow);
|
|
382
|
+
const maxReadAndDecompressBytes = opts.limits?.maxReadAndDecompressBytes === void 0 ? void 0 : streamLimit(opts.limits.maxReadAndDecompressBytes, 0, "maxReadAndDecompressBytes");
|
|
366
383
|
const maxMetadataBytes = streamLimit(opts.limits?.maxMetadataBytes, 1048576, "maxMetadataBytes");
|
|
367
384
|
const maxRowGroupRows = streamLimit(opts.limits?.maxRowGroupRows, ROW_GROUP_SIZE, "maxRowGroupRows");
|
|
368
385
|
const maxRowGroupBytes = streamLimit(opts.limits?.maxRowGroupBytes, 16777216, "maxRowGroupBytes");
|
|
@@ -396,9 +413,101 @@ async function* decodeParquetRowGroups(file, opts = {}) {
|
|
|
396
413
|
totalRows += rows;
|
|
397
414
|
}
|
|
398
415
|
if (!Number.isSafeInteger(totalRows) || BigInt(totalRows) !== metadata.num_rows) throw new Error("parquet stream row-count metadata mismatch");
|
|
399
|
-
|
|
416
|
+
if (values) {
|
|
417
|
+
yield* decodeBoundedValues(bounded, metadata, values, maxReadBytes, selection, maxReadAndDecompressBytes);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
yield* decodeGroupsFromMetadata(bounded, metadata, opts, indexedWindow && {
|
|
421
|
+
...indexedWindow,
|
|
422
|
+
maxReadBytes
|
|
423
|
+
}, maxReadAndDecompressBytes);
|
|
424
|
+
}
|
|
425
|
+
function indexedWindowPolicy(policy) {
|
|
426
|
+
if (policy === void 0) return void 0;
|
|
427
|
+
const { targetRows, requiredIndexedColumns, rangeCacheBytes = 0 } = policy;
|
|
428
|
+
if (!Number.isSafeInteger(targetRows) || targetRows < 1 || !Array.isArray(requiredIndexedColumns) || requiredIndexedColumns.length < 1 || requiredIndexedColumns.length > 64 || requiredIndexedColumns.some((name) => typeof name !== "string" || name.length === 0) || !Number.isSafeInteger(rangeCacheBytes) || rangeCacheBytes < 0 || rangeCacheBytes > 2097152) throw new RangeError("invalid indexedRowWindow policy");
|
|
429
|
+
return {
|
|
430
|
+
targetRows,
|
|
431
|
+
requiredIndexedColumns: [...new Set(requiredIndexedColumns)],
|
|
432
|
+
rangeCacheBytes
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function windowScalar(element) {
|
|
436
|
+
if (element.num_children || element.repetition_type === "REPEATED") return false;
|
|
437
|
+
const converted = element.converted_type;
|
|
438
|
+
const logical = element.logical_type?.type;
|
|
439
|
+
if (element.type === "BYTE_ARRAY") return (!converted || converted === "UTF8") && (!logical || logical === "STRING");
|
|
440
|
+
return [
|
|
441
|
+
"BOOLEAN",
|
|
442
|
+
"INT32",
|
|
443
|
+
"INT64",
|
|
444
|
+
"INT96",
|
|
445
|
+
"FLOAT",
|
|
446
|
+
"DOUBLE"
|
|
447
|
+
].includes(element.type ?? "") && (!converted || [
|
|
448
|
+
"DATE",
|
|
449
|
+
"TIME_MILLIS",
|
|
450
|
+
"TIME_MICROS",
|
|
451
|
+
"TIMESTAMP_MILLIS",
|
|
452
|
+
"TIMESTAMP_MICROS",
|
|
453
|
+
"UINT_8",
|
|
454
|
+
"UINT_16",
|
|
455
|
+
"UINT_32",
|
|
456
|
+
"UINT_64",
|
|
457
|
+
"INT_8",
|
|
458
|
+
"INT_16",
|
|
459
|
+
"INT_32",
|
|
460
|
+
"INT_64"
|
|
461
|
+
].includes(converted)) && (!logical || [
|
|
462
|
+
"DATE",
|
|
463
|
+
"TIME",
|
|
464
|
+
"TIMESTAMP",
|
|
465
|
+
"INTEGER"
|
|
466
|
+
].includes(logical));
|
|
400
467
|
}
|
|
401
|
-
|
|
468
|
+
function indexedGroup(group, names, columns, fileBytes, policy) {
|
|
469
|
+
if (group.columns.length !== names.size || new Set(group.columns.map((col) => col.meta_data?.path_in_schema[0])).size !== names.size || group.columns.some((col) => col.meta_data?.path_in_schema.length !== 1 || !names.has(col.meta_data.path_in_schema[0]))) return false;
|
|
470
|
+
return policy.requiredIndexedColumns.every((name) => {
|
|
471
|
+
if (columns && !columns.includes(name)) return false;
|
|
472
|
+
const matches = group.columns.filter((col) => col.meta_data?.path_in_schema[0] === name);
|
|
473
|
+
if (matches.length !== 1) return false;
|
|
474
|
+
const chunk = matches[0];
|
|
475
|
+
const meta = chunk.meta_data;
|
|
476
|
+
if (meta.dictionary_page_offset !== void 0 && meta.dictionary_page_offset !== null || meta.encodings.some((encoding) => encoding === "PLAIN_DICTIONARY" || encoding === "RLE_DICTIONARY")) return false;
|
|
477
|
+
const start = Number(chunk.offset_index_offset);
|
|
478
|
+
const length = chunk.offset_index_length;
|
|
479
|
+
return Number.isSafeInteger(start) && start >= 4 && Number.isSafeInteger(length) && length > 0 && length <= policy.maxReadBytes && Number.isSafeInteger(start + length) && start + length <= fileBytes;
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
async function validateWindowIndexes(file, group, columns, hasFilter) {
|
|
483
|
+
for (const column of group.columns) {
|
|
484
|
+
const meta = column.meta_data;
|
|
485
|
+
if (columns && !hasFilter && !columns.includes(meta.path_in_schema[0])) continue;
|
|
486
|
+
if (!column.offset_index_offset || !column.offset_index_length) continue;
|
|
487
|
+
const start = Number(column.offset_index_offset);
|
|
488
|
+
const bytes = await file.slice(start, start + column.offset_index_length);
|
|
489
|
+
const pages = readOffsetIndex({
|
|
490
|
+
view: new DataView(bytes),
|
|
491
|
+
offset: 0
|
|
492
|
+
}).page_locations;
|
|
493
|
+
const groupRows = Number(group.num_rows);
|
|
494
|
+
const dataStart = Number(meta.data_page_offset);
|
|
495
|
+
const columnStart = Number(meta.dictionary_page_offset ?? meta.data_page_offset);
|
|
496
|
+
const columnEnd = columnStart + Number(meta.total_compressed_size);
|
|
497
|
+
if (!pages.length || pages.length > groupRows || !Number.isSafeInteger(dataStart) || dataStart < 4 || !Number.isSafeInteger(columnStart) || columnStart < 4 || !Number.isSafeInteger(columnEnd) || columnEnd > file.byteLength) throw new Error("parquet stream invalid offset index");
|
|
498
|
+
let previousRow = -1;
|
|
499
|
+
let previousEnd = dataStart;
|
|
500
|
+
for (const [index, page] of pages.entries()) {
|
|
501
|
+
const row = Number(page.first_row_index);
|
|
502
|
+
const offset = Number(page.offset);
|
|
503
|
+
const size = page.compressed_page_size;
|
|
504
|
+
if (!Number.isSafeInteger(row) || row <= previousRow || row >= groupRows || index === 0 && row !== 0 || !Number.isSafeInteger(offset) || offset < previousEnd || !Number.isSafeInteger(size) || size < 1 || !Number.isSafeInteger(offset + size) || offset + size > columnEnd) throw new Error("parquet stream invalid offset index");
|
|
505
|
+
previousRow = row;
|
|
506
|
+
previousEnd = offset + size;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async function* decodeGroupsFromMetadata(file, metadata, opts, indexedWindow, maxReadAndDecompressBytes) {
|
|
402
511
|
const tree = parquetSchema(metadata);
|
|
403
512
|
const present = /* @__PURE__ */ new Set();
|
|
404
513
|
const dateColumns = [];
|
|
@@ -410,29 +519,46 @@ async function* decodeGroupsFromMetadata(file, metadata, opts) {
|
|
|
410
519
|
}
|
|
411
520
|
const columns = opts.columns?.filter((name) => present.has(name));
|
|
412
521
|
const normalizeColumns = nested ? null : dateColumns;
|
|
522
|
+
const windowSchema = indexedWindow && !nested && present.size === tree.children.length && tree.children.every((child) => windowScalar(child.element));
|
|
413
523
|
let groupStart = 0;
|
|
414
524
|
for (const group of metadata.row_groups) {
|
|
415
525
|
const groupEnd = groupStart + Number(group.num_rows);
|
|
416
526
|
const rowStart = Math.max(groupStart, opts.rowStart ?? 0);
|
|
417
527
|
const rowEnd = Math.min(groupEnd, opts.rowEnd ?? Infinity);
|
|
418
|
-
if (rowStart < rowEnd)
|
|
419
|
-
file,
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
528
|
+
if (rowStart < rowEnd) {
|
|
529
|
+
const window = windowSchema && indexedWindow && indexedGroup(group, present, columns, file.byteLength, indexedWindow) ? indexedWindow : void 0;
|
|
530
|
+
const budget = maxReadAndDecompressBytes === void 0 ? void 0 : parquetDecodeBudget(file, maxReadAndDecompressBytes);
|
|
531
|
+
const groupFile = budget?.file ?? file;
|
|
532
|
+
const cached = window?.rangeCacheBytes ? parquetRangeCache(groupFile, window.rangeCacheBytes) : void 0;
|
|
533
|
+
try {
|
|
534
|
+
if (window) await validateWindowIndexes(cached?.file ?? groupFile, group, columns, !!opts.filter);
|
|
535
|
+
for (let start = rowStart; start < rowEnd;) {
|
|
536
|
+
const end = Math.min(rowEnd, start + (window?.targetRows ?? rowEnd - rowStart));
|
|
537
|
+
yield normalizeDecodedDates(await parquetReadObjects({
|
|
538
|
+
file: cached?.file ?? groupFile,
|
|
539
|
+
compressors: budget?.compressors ?? { ZSTD: (input) => decompress(input) },
|
|
540
|
+
metadata: {
|
|
541
|
+
...metadata,
|
|
542
|
+
row_groups: [group],
|
|
543
|
+
num_rows: group.num_rows
|
|
544
|
+
},
|
|
545
|
+
columns,
|
|
546
|
+
rowStart: start - groupStart,
|
|
547
|
+
rowEnd: end - groupStart,
|
|
548
|
+
useOffsetIndex: start > groupStart || end < groupEnd,
|
|
549
|
+
...opts.filter ? {
|
|
550
|
+
filter: opts.filter,
|
|
551
|
+
useBloomFilters: true,
|
|
552
|
+
usePageIndex: true
|
|
553
|
+
} : {}
|
|
554
|
+
}), normalizeColumns);
|
|
555
|
+
start = end;
|
|
556
|
+
}
|
|
557
|
+
} finally {
|
|
558
|
+
budget?.close();
|
|
559
|
+
cached?.close();
|
|
560
|
+
}
|
|
561
|
+
}
|
|
436
562
|
groupStart = groupEnd;
|
|
437
563
|
}
|
|
438
564
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "../node_modules/.pnpm/hyparquet@1.31.1/node_modules/hyparquet/types/node.mjs";
|
|
2
|
+
import "../storage.mjs";
|
|
3
|
+
export interface BoundedValuesOptions {
|
|
4
|
+
/** Hard emitted row ceiling, default 1000. */
|
|
5
|
+
targetRows?: number;
|
|
6
|
+
/** Conservative UTF16 + row/cell charge before materialization. Default 1 MiB. */
|
|
7
|
+
maxBatchBytes?: number;
|
|
8
|
+
/** Maximum encoded bytes of one string, checked before UTF8 conversion. Default 256 KiB. */
|
|
9
|
+
maxValueBytes?: number;
|
|
10
|
+
/** Aggregate page/dictionary backing-buffer admission, not total JS heap. Default 24 MiB. */
|
|
11
|
+
maxActiveBytes?: number;
|
|
12
|
+
/** Maximum retained page descriptors across the selected input. Default 16384. */
|
|
13
|
+
maxPages?: number;
|
|
14
|
+
/** Maximum page header probe. Default 64 KiB. */
|
|
15
|
+
maxHeaderBytes?: number;
|
|
16
|
+
/** Maximum entries per dictionary; offsets consume 4 bytes per entry. Default 100000. */
|
|
17
|
+
maxDictionaryEntries?: number;
|
|
18
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { snappyUncompress } from "../node_modules/.pnpm/hyparquet@1.31.1/node_modules/hyparquet/src/snappy.mjs";
|
|
2
|
+
import "../node_modules/.pnpm/hyparquet@1.31.1/node_modules/hyparquet/src/node.mjs";
|
|
3
|
+
import { parquetDecodeBudget } from "./parquet-decode-budget.mjs";
|
|
4
|
+
import { BoundedHeaderTruncatedError, boundedPageHeader } from "./parquet-page-header.mjs";
|
|
5
|
+
import { hybridCursor, plainCursor } from "./parquet-value-cursor.mjs";
|
|
6
|
+
function boundedValuesPolicy(options) {
|
|
7
|
+
if (options === void 0) return void 0;
|
|
8
|
+
const policy = {
|
|
9
|
+
targetRows: 1e3,
|
|
10
|
+
maxBatchBytes: 1048576,
|
|
11
|
+
maxValueBytes: 262144,
|
|
12
|
+
maxActiveBytes: 25165824,
|
|
13
|
+
maxPages: 16384,
|
|
14
|
+
maxHeaderBytes: 65536,
|
|
15
|
+
maxDictionaryEntries: 1e5,
|
|
16
|
+
...options
|
|
17
|
+
};
|
|
18
|
+
for (const [key, value] of Object.entries(policy)) if (!Number.isSafeInteger(value) || value < 1) throw new RangeError(`invalid boundedValues.${key}`);
|
|
19
|
+
return policy;
|
|
20
|
+
}
|
|
21
|
+
const supportedEncodings = /* @__PURE__ */ new Set([
|
|
22
|
+
0,
|
|
23
|
+
2,
|
|
24
|
+
8
|
|
25
|
+
]);
|
|
26
|
+
function natural(value, label) {
|
|
27
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`bounded Parquet invalid ${label}`);
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function scalar(element) {
|
|
31
|
+
if (element.num_children || !["REQUIRED", "OPTIONAL"].includes(element.repetition_type ?? "")) return false;
|
|
32
|
+
const converted = element.converted_type;
|
|
33
|
+
const logical = element.logical_type?.type;
|
|
34
|
+
if (element.type === "BYTE_ARRAY") return (converted === "UTF8" || logical === "STRING") && (!converted || converted === "UTF8") && (!logical || logical === "STRING");
|
|
35
|
+
return [
|
|
36
|
+
"BOOLEAN",
|
|
37
|
+
"INT32",
|
|
38
|
+
"INT64",
|
|
39
|
+
"INT96",
|
|
40
|
+
"FLOAT",
|
|
41
|
+
"DOUBLE"
|
|
42
|
+
].includes(element.type ?? "") && (!converted || [
|
|
43
|
+
"DATE",
|
|
44
|
+
"TIME_MILLIS",
|
|
45
|
+
"TIME_MICROS",
|
|
46
|
+
"TIMESTAMP_MILLIS",
|
|
47
|
+
"TIMESTAMP_MICROS",
|
|
48
|
+
"UINT_8",
|
|
49
|
+
"UINT_16",
|
|
50
|
+
"UINT_32",
|
|
51
|
+
"UINT_64",
|
|
52
|
+
"INT_8",
|
|
53
|
+
"INT_16",
|
|
54
|
+
"INT_32",
|
|
55
|
+
"INT_64"
|
|
56
|
+
].includes(converted)) && (!logical || [
|
|
57
|
+
"DATE",
|
|
58
|
+
"TIME",
|
|
59
|
+
"TIMESTAMP",
|
|
60
|
+
"INTEGER"
|
|
61
|
+
].includes(logical));
|
|
62
|
+
}
|
|
63
|
+
async function readHeader(file, start, end, maxBytes) {
|
|
64
|
+
const limit = Math.min(end - start, maxBytes);
|
|
65
|
+
for (let length = Math.min(512, limit);;) try {
|
|
66
|
+
return boundedPageHeader(await file.slice(start, start + length));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (!(error instanceof BoundedHeaderTruncatedError) || length === limit) throw error;
|
|
69
|
+
length = Math.min(limit, length * 2);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function planInput(file, metadata, policy, maxReadBytes, columns, rowStart = 0, rowEnd = Infinity, maxReadAndDecompressBytes) {
|
|
73
|
+
const schema = metadata.schema.slice(1);
|
|
74
|
+
if (metadata.schema[0]?.num_children !== schema.length || schema.some((element) => !scalar(element)) || new Set(schema.map((element) => element.name)).size !== schema.length) throw new Error("bounded Parquet requires a flat scalar REQUIRED/OPTIONAL schema");
|
|
75
|
+
const selected = columns ? schema.filter((element) => columns.includes(element.name)) : schema;
|
|
76
|
+
const output = [];
|
|
77
|
+
let start = 0;
|
|
78
|
+
let pageCount = 0;
|
|
79
|
+
try {
|
|
80
|
+
for (const group of metadata.row_groups) {
|
|
81
|
+
const rows = Number(group.num_rows);
|
|
82
|
+
const plan = {
|
|
83
|
+
start,
|
|
84
|
+
rows,
|
|
85
|
+
columns: []
|
|
86
|
+
};
|
|
87
|
+
start += rows;
|
|
88
|
+
if (start <= rowStart || plan.start >= rowEnd) continue;
|
|
89
|
+
plan.budget = maxReadAndDecompressBytes === void 0 ? void 0 : parquetDecodeBudget(file, maxReadAndDecompressBytes);
|
|
90
|
+
output.push(plan);
|
|
91
|
+
const groupFile = plan.budget?.file ?? file;
|
|
92
|
+
let active = Math.min(policy.maxHeaderBytes, maxReadBytes);
|
|
93
|
+
for (const element of selected) {
|
|
94
|
+
const matches = group.columns.filter((col) => col.meta_data?.path_in_schema.length === 1 && col.meta_data.path_in_schema[0] === element.name);
|
|
95
|
+
if (matches.length !== 1) throw new Error("bounded Parquet missing or duplicate column");
|
|
96
|
+
const chunk = matches[0];
|
|
97
|
+
const meta = chunk.meta_data;
|
|
98
|
+
if (chunk.file_path || meta.type !== element.type || Number(meta.num_values) !== rows || !["SNAPPY", "UNCOMPRESSED"].includes(meta.codec)) throw new Error("bounded Parquet unsupported column or codec");
|
|
99
|
+
const first = Number(meta.dictionary_page_offset ?? meta.data_page_offset);
|
|
100
|
+
const end = first + Number(meta.total_compressed_size);
|
|
101
|
+
if (!Number.isSafeInteger(first) || first < 4 || !Number.isSafeInteger(end) || end > file.byteLength || end < first) throw new Error("bounded Parquet invalid column range");
|
|
102
|
+
const column = {
|
|
103
|
+
element,
|
|
104
|
+
meta,
|
|
105
|
+
pages: []
|
|
106
|
+
};
|
|
107
|
+
let pageRows = 0;
|
|
108
|
+
let maxData = 0;
|
|
109
|
+
for (let offset = first; offset < end;) {
|
|
110
|
+
if (++pageCount > policy.maxPages) throw new RangeError("bounded Parquet exceeds maxPages");
|
|
111
|
+
const header = await readHeader(groupFile, offset, end, Math.min(policy.maxHeaderBytes, maxReadBytes));
|
|
112
|
+
natural(header.compressed, "compressed page bytes");
|
|
113
|
+
natural(header.uncompressed, "uncompressed page bytes");
|
|
114
|
+
natural(header.count, "page values");
|
|
115
|
+
const page = {
|
|
116
|
+
...header,
|
|
117
|
+
offset: offset + header.bytes
|
|
118
|
+
};
|
|
119
|
+
offset = page.offset + page.compressed;
|
|
120
|
+
if (offset > end || page.compressed > maxReadBytes) throw new RangeError("bounded Parquet page exceeds range bounds");
|
|
121
|
+
if (page.kind === 2) {
|
|
122
|
+
if (column.dictionary || column.pages.length || page.encoding !== 0 || page.count > policy.maxDictionaryEntries) throw new Error("bounded Parquet unsupported dictionary");
|
|
123
|
+
column.dictionary = page;
|
|
124
|
+
active += page.compressed + page.uncompressed + (page.count + 1) * 4;
|
|
125
|
+
} else {
|
|
126
|
+
if (!page.count || !supportedEncodings.has(page.encoding) || page.encoding !== 0 && !column.dictionary) throw new Error("bounded Parquet unsupported data encoding");
|
|
127
|
+
if (page.kind === 0 && (page.definitionEncoding !== 3 || page.repetitionEncoding !== 3)) throw new Error("bounded Parquet unsupported level encoding");
|
|
128
|
+
if (page.kind === 3) {
|
|
129
|
+
natural(page.nulls, "null count");
|
|
130
|
+
natural(page.rows, "page rows");
|
|
131
|
+
natural(page.definitions, "definition bytes");
|
|
132
|
+
natural(page.repetitions, "repetition bytes");
|
|
133
|
+
if (page.rows !== page.count || page.nulls > page.count || page.repetitions !== 0 || page.definitions > page.compressed || page.definitions > page.uncompressed || element.repetition_type === "REQUIRED" && (page.definitions !== 0 || page.nulls !== 0)) throw new Error("bounded Parquet invalid flat V2 page");
|
|
134
|
+
}
|
|
135
|
+
pageRows += page.count;
|
|
136
|
+
if (pageRows > rows) throw new Error("bounded Parquet excess physical rows");
|
|
137
|
+
maxData = Math.max(maxData, page.compressed + page.uncompressed);
|
|
138
|
+
column.pages.push(page);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (pageRows !== rows) throw new Error("bounded Parquet incomplete physical rows");
|
|
142
|
+
if (column.dictionary && element.type === "BOOLEAN") throw new Error("bounded Parquet BOOLEAN dictionary is unsupported");
|
|
143
|
+
active += maxData;
|
|
144
|
+
if (!Number.isSafeInteger(active) || active > policy.maxActiveBytes) throw new RangeError("bounded Parquet exceeds maxActiveBytes");
|
|
145
|
+
plan.columns.push(column);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return output;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
for (const plan of output) plan.budget?.close();
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function inflate(input, size, compressed, decompress) {
|
|
155
|
+
if (!compressed) {
|
|
156
|
+
if (input.length !== size) throw new Error("bounded Parquet uncompressed page size mismatch");
|
|
157
|
+
return input;
|
|
158
|
+
}
|
|
159
|
+
if (decompress) return decompress(input, size);
|
|
160
|
+
const output = new Uint8Array(size);
|
|
161
|
+
snappyUncompress(input, output);
|
|
162
|
+
return output;
|
|
163
|
+
}
|
|
164
|
+
function dataCursor(page, input, plan, policy, dictionary, decompress) {
|
|
165
|
+
let definitions;
|
|
166
|
+
let levels;
|
|
167
|
+
let values;
|
|
168
|
+
if (page.kind === 0) {
|
|
169
|
+
const decoded = inflate(input, page.uncompressed, plan.meta.codec === "SNAPPY", decompress);
|
|
170
|
+
let offset = 0;
|
|
171
|
+
if (plan.element.repetition_type === "OPTIONAL") {
|
|
172
|
+
if (decoded.length < 4) throw new Error("bounded Parquet missing definition levels");
|
|
173
|
+
offset = 4 + new DataView(decoded.buffer, decoded.byteOffset, decoded.byteLength).getUint32(0, true);
|
|
174
|
+
if (offset > decoded.length) throw new Error("bounded Parquet truncated definition levels");
|
|
175
|
+
levels = decoded.subarray(4, offset);
|
|
176
|
+
}
|
|
177
|
+
values = decoded.subarray(offset);
|
|
178
|
+
} else {
|
|
179
|
+
const length = page.definitions;
|
|
180
|
+
if (plan.element.repetition_type === "OPTIONAL") levels = input.subarray(0, length);
|
|
181
|
+
values = inflate(input.subarray(length), page.uncompressed - length, page.isCompressed !== false && plan.meta.codec === "SNAPPY", decompress);
|
|
182
|
+
}
|
|
183
|
+
let present = page.count;
|
|
184
|
+
if (levels) {
|
|
185
|
+
definitions = hybridCursor(levels, 1, page.count);
|
|
186
|
+
present = 0;
|
|
187
|
+
for (let i = 0; i < page.count; i++) present += definitions.next();
|
|
188
|
+
definitions.finish();
|
|
189
|
+
definitions = hybridCursor(levels, 1, page.count);
|
|
190
|
+
}
|
|
191
|
+
if (page.kind === 3 && page.count - present !== page.nulls) throw new Error("bounded Parquet null count mismatch");
|
|
192
|
+
let plain;
|
|
193
|
+
let ids;
|
|
194
|
+
if (page.encoding === 0) plain = plainCursor(values, plan.element, present, policy.maxValueBytes);
|
|
195
|
+
else {
|
|
196
|
+
if (!dictionary || !values.length) throw new Error("bounded Parquet missing dictionary IDs");
|
|
197
|
+
ids = hybridCursor(values.subarray(1), values[0], present);
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
next() {
|
|
201
|
+
if (definitions && definitions.next() === 0) return {
|
|
202
|
+
bytes: 64,
|
|
203
|
+
value: () => null
|
|
204
|
+
};
|
|
205
|
+
if (plain) return plain.next();
|
|
206
|
+
const id = ids.next();
|
|
207
|
+
if (id >= dictionary.offsets.length - 1) throw new Error("bounded Parquet dictionary ID out of range");
|
|
208
|
+
const start = dictionary.offsets[id];
|
|
209
|
+
const end = dictionary.offsets[id + 1];
|
|
210
|
+
return plainCursor(dictionary.bytes.subarray(start, end), plan.element, 1, policy.maxValueBytes).next();
|
|
211
|
+
},
|
|
212
|
+
finish() {
|
|
213
|
+
definitions?.finish();
|
|
214
|
+
plain?.finish();
|
|
215
|
+
ids?.finish();
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
var ColumnCursor = class {
|
|
220
|
+
file;
|
|
221
|
+
plan;
|
|
222
|
+
policy;
|
|
223
|
+
decompress;
|
|
224
|
+
dictionary;
|
|
225
|
+
current;
|
|
226
|
+
pageIndex = 0;
|
|
227
|
+
remaining = 0;
|
|
228
|
+
constructor(file, plan, policy, decompress) {
|
|
229
|
+
this.file = file;
|
|
230
|
+
this.plan = plan;
|
|
231
|
+
this.policy = policy;
|
|
232
|
+
this.decompress = decompress;
|
|
233
|
+
}
|
|
234
|
+
needsPage() {
|
|
235
|
+
return this.remaining === 0;
|
|
236
|
+
}
|
|
237
|
+
async load() {
|
|
238
|
+
if (this.remaining) return;
|
|
239
|
+
this.current?.finish();
|
|
240
|
+
this.current = void 0;
|
|
241
|
+
if (!this.dictionary && this.plan.dictionary) {
|
|
242
|
+
const page = this.plan.dictionary;
|
|
243
|
+
const bytes = inflate(new Uint8Array(await this.file.slice(page.offset, page.offset + page.compressed)), page.uncompressed, this.plan.meta.codec === "SNAPPY", this.decompress);
|
|
244
|
+
const offsets = new Uint32Array(page.count + 1);
|
|
245
|
+
const cursor = plainCursor(bytes, this.plan.element, page.count, this.policy.maxValueBytes);
|
|
246
|
+
for (let index = 0; index < page.count; index++) {
|
|
247
|
+
offsets[index] = cursor.position();
|
|
248
|
+
cursor.skip();
|
|
249
|
+
}
|
|
250
|
+
offsets[page.count] = cursor.position();
|
|
251
|
+
cursor.finish();
|
|
252
|
+
this.dictionary = {
|
|
253
|
+
bytes,
|
|
254
|
+
offsets
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const page = this.plan.pages[this.pageIndex++];
|
|
258
|
+
if (!page) throw new Error("bounded Parquet exhausted column");
|
|
259
|
+
const input = new Uint8Array(await this.file.slice(page.offset, page.offset + page.compressed));
|
|
260
|
+
this.current = dataCursor(page, input, this.plan, this.policy, this.dictionary, this.decompress);
|
|
261
|
+
this.remaining = page.count;
|
|
262
|
+
}
|
|
263
|
+
next() {
|
|
264
|
+
if (!this.current || this.remaining-- < 1) throw new Error("bounded Parquet column is not ready");
|
|
265
|
+
return this.current.next();
|
|
266
|
+
}
|
|
267
|
+
close() {
|
|
268
|
+
this.current = void 0;
|
|
269
|
+
this.dictionary = void 0;
|
|
270
|
+
}
|
|
271
|
+
finish() {
|
|
272
|
+
this.current?.finish();
|
|
273
|
+
if (this.remaining || this.pageIndex !== this.plan.pages.length) throw new Error("bounded Parquet incomplete column");
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
async function* decodeBoundedValues(file, metadata, policy, maxReadBytes, options, maxReadAndDecompressBytes) {
|
|
277
|
+
const plans = await planInput(file, metadata, policy, maxReadBytes, options.columns, options.rowStart, options.rowEnd, maxReadAndDecompressBytes);
|
|
278
|
+
try {
|
|
279
|
+
for (const plan of plans) {
|
|
280
|
+
const budget = plan.budget;
|
|
281
|
+
const cursors = plan.columns.map((column) => new ColumnCursor(budget?.file ?? file, column, policy, budget?.compressors.SNAPPY));
|
|
282
|
+
let batch = [];
|
|
283
|
+
let batchBytes = 0;
|
|
284
|
+
try {
|
|
285
|
+
for (let index = 0; index < plan.rows; index++) {
|
|
286
|
+
for (const cursor of cursors) if (cursor.needsPage()) await cursor.load();
|
|
287
|
+
const tokens = cursors.map((cursor) => cursor.next());
|
|
288
|
+
const position = plan.start + index;
|
|
289
|
+
if (position < (options.rowStart ?? 0) || position >= (options.rowEnd ?? Infinity)) continue;
|
|
290
|
+
const bytes = 128 + tokens.reduce((sum, token) => sum + 16 + token.bytes, 0);
|
|
291
|
+
if (bytes > policy.maxBatchBytes) throw new RangeError("bounded Parquet row exceeds maxBatchBytes");
|
|
292
|
+
if (batch.length && (batch.length >= policy.targetRows || batchBytes + bytes > policy.maxBatchBytes)) {
|
|
293
|
+
yield batch;
|
|
294
|
+
batch = [];
|
|
295
|
+
batchBytes = 0;
|
|
296
|
+
}
|
|
297
|
+
const row = {};
|
|
298
|
+
for (let col = 0; col < tokens.length; col++) Object.defineProperty(row, plan.columns[col].element.name, {
|
|
299
|
+
value: tokens[col].value(),
|
|
300
|
+
enumerable: true,
|
|
301
|
+
configurable: true,
|
|
302
|
+
writable: true
|
|
303
|
+
});
|
|
304
|
+
batch.push(row);
|
|
305
|
+
batchBytes += bytes;
|
|
306
|
+
}
|
|
307
|
+
for (const cursor of cursors) cursor.finish();
|
|
308
|
+
if (batch.length) yield batch;
|
|
309
|
+
} finally {
|
|
310
|
+
for (const cursor of cursors) cursor.close();
|
|
311
|
+
budget?.close();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} finally {
|
|
315
|
+
for (const plan of plans) plan.budget?.close();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
export { boundedValuesPolicy, decodeBoundedValues };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { snappyUncompress } from "../node_modules/.pnpm/hyparquet@1.31.1/node_modules/hyparquet/src/snappy.mjs";
|
|
2
|
+
import "../node_modules/.pnpm/hyparquet@1.31.1/node_modules/hyparquet/src/node.mjs";
|
|
3
|
+
function parquetDecodeBudget(source, maxBytes) {
|
|
4
|
+
let remaining = maxBytes;
|
|
5
|
+
let closed = false;
|
|
6
|
+
let failure;
|
|
7
|
+
function fail(error) {
|
|
8
|
+
if (!closed) {
|
|
9
|
+
closed = true;
|
|
10
|
+
failure = error;
|
|
11
|
+
}
|
|
12
|
+
throw failure;
|
|
13
|
+
}
|
|
14
|
+
function assertOpen() {
|
|
15
|
+
if (closed) throw failure;
|
|
16
|
+
}
|
|
17
|
+
function charge(bytes) {
|
|
18
|
+
assertOpen();
|
|
19
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > remaining) fail(/* @__PURE__ */ new RangeError("parquet stream exceeds maxReadAndDecompressBytes"));
|
|
20
|
+
remaining -= bytes;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
file: {
|
|
24
|
+
byteLength: source.byteLength,
|
|
25
|
+
async slice(start, end = source.byteLength) {
|
|
26
|
+
try {
|
|
27
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start || end > source.byteLength) fail(/* @__PURE__ */ new RangeError("parquet stream invalid decode-buffer range"));
|
|
28
|
+
charge(end - start);
|
|
29
|
+
const bytes = await source.slice(start, end);
|
|
30
|
+
assertOpen();
|
|
31
|
+
if (bytes.byteLength !== end - start) fail(/* @__PURE__ */ new Error("parquet stream returned an incomplete decode-buffer range"));
|
|
32
|
+
return bytes;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
fail(error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
compressors: {
|
|
39
|
+
SNAPPY(input, size) {
|
|
40
|
+
charge(size);
|
|
41
|
+
try {
|
|
42
|
+
const output = new Uint8Array(size);
|
|
43
|
+
snappyUncompress(input, output);
|
|
44
|
+
return output;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
fail(error);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
ZSTD() {
|
|
50
|
+
fail(/* @__PURE__ */ new Error("parquet stream maxReadAndDecompressBytes does not support ZSTD"));
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
close() {
|
|
54
|
+
if (!closed) {
|
|
55
|
+
closed = true;
|
|
56
|
+
failure = /* @__PURE__ */ new Error("parquet stream decode-buffer budget is closed");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export { parquetDecodeBudget };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
var BoundedHeaderTruncatedError = class extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super("bounded Parquet truncated page header");
|
|
4
|
+
}
|
|
5
|
+
};
|
|
6
|
+
function boundedPageHeader(bytes) {
|
|
7
|
+
const view = new DataView(bytes);
|
|
8
|
+
let offset = 0;
|
|
9
|
+
let fields = 0;
|
|
10
|
+
const take = (length) => {
|
|
11
|
+
if (!Number.isSafeInteger(length) || length < 0) throw new Error("bounded Parquet invalid page-header size");
|
|
12
|
+
if (offset + length > view.byteLength) throw new BoundedHeaderTruncatedError();
|
|
13
|
+
const start = offset;
|
|
14
|
+
offset += length;
|
|
15
|
+
return start;
|
|
16
|
+
};
|
|
17
|
+
const byte = () => view.getUint8(take(1));
|
|
18
|
+
const varint = () => {
|
|
19
|
+
let value = 0;
|
|
20
|
+
for (let shift = 0; shift < 49; shift += 7) {
|
|
21
|
+
const b = byte();
|
|
22
|
+
value += (b & 127) * 2 ** shift;
|
|
23
|
+
if (!(b & 128) && Number.isSafeInteger(value)) return value;
|
|
24
|
+
}
|
|
25
|
+
throw new Error("bounded Parquet invalid header integer");
|
|
26
|
+
};
|
|
27
|
+
const integer = () => {
|
|
28
|
+
const value = varint();
|
|
29
|
+
return value % 2 ? -(value + 1) / 2 : value / 2;
|
|
30
|
+
};
|
|
31
|
+
const skip = (type, depth) => {
|
|
32
|
+
if (depth > 8 || ++fields > 512) throw new Error("bounded Parquet excessive page-header structure");
|
|
33
|
+
if (type === 1 || type === 2) return;
|
|
34
|
+
if (type === 3) {
|
|
35
|
+
take(1);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (type === 4 || type === 5) {
|
|
39
|
+
varint();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (type === 6) {
|
|
43
|
+
for (let i = 0; i < 10; i++) if (!(byte() & 128)) return;
|
|
44
|
+
throw new Error("bounded Parquet invalid header int64");
|
|
45
|
+
}
|
|
46
|
+
if (type === 7) {
|
|
47
|
+
take(8);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (type === 8) {
|
|
51
|
+
take(varint());
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (type === 12) {
|
|
55
|
+
struct(depth + 1);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
throw new Error("bounded Parquet unsupported page-header field");
|
|
59
|
+
};
|
|
60
|
+
function struct(depth, select) {
|
|
61
|
+
let id = 0;
|
|
62
|
+
const seen = /* @__PURE__ */ new Set();
|
|
63
|
+
while (true) {
|
|
64
|
+
const field = byte();
|
|
65
|
+
if (field === 0) return;
|
|
66
|
+
const type = field & 15;
|
|
67
|
+
id = field >> 4 ? id + (field >> 4) : integer();
|
|
68
|
+
if (id < 1 || seen.has(id)) throw new Error("bounded Parquet invalid page-header field");
|
|
69
|
+
seen.add(id);
|
|
70
|
+
if (!select?.(id, type)) skip(type, depth);
|
|
71
|
+
if (++fields > 512) throw new Error("bounded Parquet excessive page-header fields");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const root = {};
|
|
75
|
+
const children = {};
|
|
76
|
+
struct(0, (id, type) => {
|
|
77
|
+
if ([
|
|
78
|
+
1,
|
|
79
|
+
2,
|
|
80
|
+
3
|
|
81
|
+
].includes(id)) {
|
|
82
|
+
if (type !== 5) throw new Error("bounded Parquet invalid page-header number");
|
|
83
|
+
root[id] = integer();
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if ([
|
|
87
|
+
5,
|
|
88
|
+
7,
|
|
89
|
+
8
|
|
90
|
+
].includes(id)) {
|
|
91
|
+
if (type !== 12) throw new Error("bounded Parquet invalid page-header body");
|
|
92
|
+
const body = {};
|
|
93
|
+
children[id] = body;
|
|
94
|
+
struct(1, (field, fieldType) => {
|
|
95
|
+
if (field > (id === 5 ? 4 : id === 7 ? 2 : 7)) return false;
|
|
96
|
+
if (id === 8 && field === 7) {
|
|
97
|
+
if (fieldType !== 1 && fieldType !== 2) throw new Error("bounded Parquet invalid compression boolean");
|
|
98
|
+
body[field] = fieldType === 1;
|
|
99
|
+
} else {
|
|
100
|
+
if (fieldType !== 5) throw new Error("bounded Parquet invalid data-header number");
|
|
101
|
+
body[field] = integer();
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
});
|
|
109
|
+
const kind = root[1];
|
|
110
|
+
const body = children[kind === 0 ? 5 : kind === 2 ? 7 : 8];
|
|
111
|
+
if (![
|
|
112
|
+
0,
|
|
113
|
+
2,
|
|
114
|
+
3
|
|
115
|
+
].includes(kind) || !body) throw new Error("bounded Parquet unsupported page type");
|
|
116
|
+
return {
|
|
117
|
+
bytes: offset,
|
|
118
|
+
kind,
|
|
119
|
+
compressed: root[3],
|
|
120
|
+
uncompressed: root[2],
|
|
121
|
+
count: body[1],
|
|
122
|
+
encoding: body[kind === 3 ? 4 : 2],
|
|
123
|
+
...kind === 0 ? {
|
|
124
|
+
definitionEncoding: body[3],
|
|
125
|
+
repetitionEncoding: body[4]
|
|
126
|
+
} : {},
|
|
127
|
+
...kind === 3 ? {
|
|
128
|
+
nulls: body[2],
|
|
129
|
+
rows: body[3],
|
|
130
|
+
definitions: body[5],
|
|
131
|
+
repetitions: body[6],
|
|
132
|
+
isCompressed: body[7] === void 0 ? true : body[7]
|
|
133
|
+
} : {}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export { BoundedHeaderTruncatedError, boundedPageHeader };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function parquetRangeCache(source, maxBytes) {
|
|
2
|
+
const entries = /* @__PURE__ */ new Map();
|
|
3
|
+
let retainedBytes = 0;
|
|
4
|
+
let closed = false;
|
|
5
|
+
return {
|
|
6
|
+
file: {
|
|
7
|
+
byteLength: source.byteLength,
|
|
8
|
+
async slice(start, end = source.byteLength) {
|
|
9
|
+
const key = `${start}:${end}`;
|
|
10
|
+
const cached = entries.get(key);
|
|
11
|
+
if (cached) {
|
|
12
|
+
entries.delete(key);
|
|
13
|
+
entries.set(key, cached);
|
|
14
|
+
return cached;
|
|
15
|
+
}
|
|
16
|
+
const bytes = await source.slice(start, end);
|
|
17
|
+
if (!closed && bytes.byteLength > 0 && bytes.byteLength <= maxBytes) {
|
|
18
|
+
const previous = entries.get(key);
|
|
19
|
+
if (previous) {
|
|
20
|
+
retainedBytes -= previous.byteLength;
|
|
21
|
+
entries.delete(key);
|
|
22
|
+
}
|
|
23
|
+
while (entries.size >= 128 || retainedBytes + bytes.byteLength > maxBytes) {
|
|
24
|
+
const oldest = entries.keys().next().value;
|
|
25
|
+
retainedBytes -= entries.get(oldest).byteLength;
|
|
26
|
+
entries.delete(oldest);
|
|
27
|
+
}
|
|
28
|
+
entries.set(key, bytes);
|
|
29
|
+
retainedBytes += bytes.byteLength;
|
|
30
|
+
}
|
|
31
|
+
return bytes;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
close() {
|
|
35
|
+
closed = true;
|
|
36
|
+
entries.clear();
|
|
37
|
+
retainedBytes = 0;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export { parquetRangeCache };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { DEFAULT_PARSERS, convert } from "../node_modules/.pnpm/hyparquet@1.31.1/node_modules/hyparquet/src/convert.mjs";
|
|
2
|
+
const utf8 = new TextDecoder();
|
|
3
|
+
function hybridCursor(bytes, width, count) {
|
|
4
|
+
if (!Number.isInteger(width) || width < 0 || width > 32) throw new Error("bounded Parquet invalid bit width");
|
|
5
|
+
let offset = 0;
|
|
6
|
+
let remaining = count;
|
|
7
|
+
let run = 0;
|
|
8
|
+
let repeat = 0;
|
|
9
|
+
let packed = false;
|
|
10
|
+
let bit = 0;
|
|
11
|
+
let packedStart = 0;
|
|
12
|
+
const readByte = () => {
|
|
13
|
+
if (offset >= bytes.length) throw new Error("bounded Parquet truncated RLE stream");
|
|
14
|
+
return bytes[offset++];
|
|
15
|
+
};
|
|
16
|
+
const next = () => {
|
|
17
|
+
if (remaining-- <= 0) throw new Error("bounded Parquet too many encoded values");
|
|
18
|
+
if (width === 0 && bytes.length === 0) return 0;
|
|
19
|
+
if (!run) {
|
|
20
|
+
let header = 0;
|
|
21
|
+
let shift = 0;
|
|
22
|
+
while (true) {
|
|
23
|
+
const b = readByte();
|
|
24
|
+
header += (b & 127) * 2 ** shift;
|
|
25
|
+
if (!(b & 128)) break;
|
|
26
|
+
shift += 7;
|
|
27
|
+
if (shift > 28) throw new Error("bounded Parquet invalid RLE run");
|
|
28
|
+
}
|
|
29
|
+
packed = !!(header & 1);
|
|
30
|
+
run = packed ? Math.floor(header / 2) * 8 : header / 2;
|
|
31
|
+
if (!Number.isSafeInteger(run) || run < 1 || run > 2147483647 || !packed && run > remaining + 1) throw new Error(`bounded Parquet invalid RLE run length: run=${run}, remaining=${remaining + 1}, packed=${packed}, width=${width}`);
|
|
32
|
+
if (packed) {
|
|
33
|
+
packedStart = offset;
|
|
34
|
+
bit = 0;
|
|
35
|
+
offset += run * width / 8;
|
|
36
|
+
if (offset > bytes.length) throw new Error("bounded Parquet truncated bit-packed stream");
|
|
37
|
+
} else {
|
|
38
|
+
repeat = 0;
|
|
39
|
+
for (let b = 0; b < Math.ceil(width / 8); b++) repeat += readByte() * 2 ** (8 * b);
|
|
40
|
+
if (repeat >= 2 ** width) throw new Error("bounded Parquet RLE value exceeds bit width");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
run--;
|
|
44
|
+
if (!packed) return repeat;
|
|
45
|
+
let value = 0;
|
|
46
|
+
for (let b = 0; b < width; b++, bit++) value += (bytes[packedStart + Math.floor(bit / 8)] >> bit % 8 & 1) * 2 ** b;
|
|
47
|
+
return value;
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
next,
|
|
51
|
+
finish() {
|
|
52
|
+
if (remaining !== 0 || offset !== bytes.length || run && !packed) throw new Error(`bounded Parquet trailing or incomplete RLE data: remaining=${remaining}, offset=${offset}, bytes=${bytes.length}, run=${run}, packed=${packed}, width=${width}`);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function plainCursor(bytes, element, count, maxValueBytes) {
|
|
57
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
58
|
+
let offset = 0;
|
|
59
|
+
let index = 0;
|
|
60
|
+
const width = element.type === "BOOLEAN" ? 0 : element.type === "INT32" || element.type === "FLOAT" ? 4 : element.type === "INT96" ? 12 : 8;
|
|
61
|
+
const decoder = {
|
|
62
|
+
element,
|
|
63
|
+
type: element.type,
|
|
64
|
+
parsers: DEFAULT_PARSERS,
|
|
65
|
+
schemaPath: []
|
|
66
|
+
};
|
|
67
|
+
const take = () => {
|
|
68
|
+
if (index >= count) throw new Error("bounded Parquet too many plain values");
|
|
69
|
+
let size = width;
|
|
70
|
+
if (element.type === "BYTE_ARRAY") {
|
|
71
|
+
if (offset + 4 > bytes.length) throw new Error("bounded Parquet truncated string length");
|
|
72
|
+
size = view.getUint32(offset, true);
|
|
73
|
+
offset += 4;
|
|
74
|
+
if (size > maxValueBytes) throw new RangeError("bounded Parquet exceeds maxValueBytes");
|
|
75
|
+
}
|
|
76
|
+
const start = element.type === "BOOLEAN" ? Math.floor(index / 8) : offset;
|
|
77
|
+
if (start + (element.type === "BOOLEAN" ? 1 : size) > bytes.length) throw new Error("bounded Parquet truncated plain value");
|
|
78
|
+
offset = element.type === "BOOLEAN" ? Math.ceil((index + 1) / 8) : offset + size;
|
|
79
|
+
return {
|
|
80
|
+
start,
|
|
81
|
+
size,
|
|
82
|
+
index: index++
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
next() {
|
|
87
|
+
const { start, size, index: bitIndex } = take();
|
|
88
|
+
return {
|
|
89
|
+
bytes: 64 + (element.type === "BYTE_ARRAY" ? size * 2 : 32),
|
|
90
|
+
value() {
|
|
91
|
+
if (element.type === "BYTE_ARRAY") return utf8.decode(bytes.subarray(start, start + size));
|
|
92
|
+
let raw = element.type === "BOOLEAN" ? !!(view.getUint8(start) & 1 << bitIndex % 8) : element.type === "INT32" ? view.getInt32(start, true) : element.type === "INT64" ? view.getBigInt64(start, true) : element.type === "INT96" ? BigInt(view.getInt32(start + 8, true)) << 64n | view.getBigInt64(start, true) : element.type === "FLOAT" ? view.getFloat32(start, true) : view.getFloat64(start, true);
|
|
93
|
+
if (element.type === "INT64" && (element.converted_type === "UINT_64" || element.logical_type?.type === "INTEGER" && !element.logical_type.isSigned)) raw = BigInt.asUintN(64, raw);
|
|
94
|
+
const result = convert([raw], decoder)[0];
|
|
95
|
+
return result instanceof Date ? `${result.getUTCFullYear()}-${String(result.getUTCMonth() + 1).padStart(2, "0")}-${String(result.getUTCDate()).padStart(2, "0")}` : result;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
skip() {
|
|
100
|
+
take();
|
|
101
|
+
},
|
|
102
|
+
position: () => offset,
|
|
103
|
+
finish() {
|
|
104
|
+
if (index !== count || offset !== bytes.length) throw new Error("bounded Parquet trailing or incomplete plain data");
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export { hybridCursor, plainCursor };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { parquetMetadata, parquetMetadataAsync, parquetSchema } from "./metadata.mjs";
|
|
2
|
-
import "./indexes.mjs";
|
|
2
|
+
import { readOffsetIndex } from "./indexes.mjs";
|
|
3
3
|
import { toJson } from "./utils.mjs";
|
|
4
|
-
import "./snappy.mjs";
|
|
4
|
+
import { snappyUncompress } from "./snappy.mjs";
|
|
5
5
|
import "./scan.mjs";
|
|
6
6
|
import { parquetReadObjects } from "./read.mjs";
|
|
7
7
|
import "./query.mjs";
|
|
8
|
-
export { parquetMetadata, parquetMetadataAsync, parquetReadObjects, parquetSchema, toJson };
|
|
8
|
+
export { parquetMetadata, parquetMetadataAsync, parquetReadObjects, parquetSchema, readOffsetIndex, snappyUncompress, toJson };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parquetMetadata, parquetMetadataAsync, parquetSchema } from "./metadata.mjs";
|
|
2
|
-
import "./indexes.mjs";
|
|
2
|
+
import { readOffsetIndex } from "./indexes.mjs";
|
|
3
3
|
import { toJson } from "./utils.mjs";
|
|
4
|
-
import "./snappy.mjs";
|
|
4
|
+
import { snappyUncompress } from "./snappy.mjs";
|
|
5
5
|
import { parquetReadObjects } from "./read.mjs";
|
|
6
6
|
import "./index.mjs";
|
|
7
|
-
export { parquetMetadata, parquetMetadataAsync, parquetReadObjects, parquetSchema, toJson };
|
|
7
|
+
export { parquetMetadata, parquetMetadataAsync, parquetReadObjects, parquetSchema, readOffsetIndex, snappyUncompress, toJson };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/engine",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "7.
|
|
4
|
+
"version": "7.5.1",
|
|
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.
|
|
193
|
-
"@gscdump/lakehouse": "^7.
|
|
192
|
+
"@gscdump/contracts": "^7.5.1",
|
|
193
|
+
"@gscdump/lakehouse": "^7.5.1",
|
|
194
194
|
"drizzle-orm": "1.0.0-rc.4",
|
|
195
|
-
"gscdump": "^7.
|
|
195
|
+
"gscdump": "^7.5.1",
|
|
196
196
|
"pathe": "^2.0.3",
|
|
197
197
|
"proper-lockfile": "^4.1.2"
|
|
198
198
|
},
|