@loaders.gl/parquet 5.0.0-alpha.2 → 5.0.0-alpha.3
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/LICENSE +1 -1
- package/dist/bundled.d.ts +1 -1
- package/dist/bundled.d.ts.map +1 -1
- package/dist/bundled.js.map +1 -1
- package/dist/delta-format.d.ts +9 -0
- package/dist/delta-format.d.ts.map +1 -0
- package/dist/delta-format.js +12 -0
- package/dist/delta-format.js.map +1 -0
- package/dist/delta-source-loader-types.d.ts +27 -0
- package/dist/delta-source-loader-types.d.ts.map +1 -0
- package/dist/delta-source-loader-types.js +28 -0
- package/dist/delta-source-loader-types.js.map +1 -0
- package/dist/delta-source.d.ts +48 -0
- package/dist/delta-source.d.ts.map +1 -0
- package/dist/delta-source.js +222 -0
- package/dist/delta-source.js.map +1 -0
- package/dist/delta-types.d.ts +38 -0
- package/dist/delta-types.d.ts.map +1 -0
- package/dist/delta-types.js +5 -0
- package/dist/delta-types.js.map +1 -0
- package/dist/geoparquet-loader.js +1 -1
- package/dist/iceberg-rest-catalog.d.ts +49 -0
- package/dist/iceberg-rest-catalog.d.ts.map +1 -0
- package/dist/iceberg-rest-catalog.js +78 -0
- package/dist/iceberg-rest-catalog.js.map +1 -0
- package/dist/iceberg-table-source.d.ts +63 -0
- package/dist/iceberg-table-source.d.ts.map +1 -0
- package/dist/iceberg-table-source.js +797 -0
- package/dist/iceberg-table-source.js.map +1 -0
- package/dist/iceberg-types.d.ts +150 -0
- package/dist/iceberg-types.d.ts.map +1 -0
- package/dist/iceberg-types.js +5 -0
- package/dist/iceberg-types.js.map +1 -0
- package/dist/index.cjs +15005 -3143
- package/dist/index.cjs.map +3 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/arrow/convert-schema-from-parquet.d.ts +3 -1
- package/dist/lib/arrow/convert-schema-from-parquet.d.ts.map +1 -1
- package/dist/lib/arrow/convert-schema-from-parquet.js +94 -19
- package/dist/lib/arrow/convert-schema-from-parquet.js.map +1 -1
- package/dist/lib/arrow/create-nested-arrow-vector.js +89 -0
- package/dist/lib/arrow/create-nested-arrow-vector.js.map +1 -1
- package/dist/lib/constants.js +1 -1
- package/dist/lib/encoders/encode-table-to-parquet-js.d.ts.map +1 -1
- package/dist/lib/encoders/encode-table-to-parquet-js.js +202 -7
- package/dist/lib/encoders/encode-table-to-parquet-js.js.map +1 -1
- package/dist/lib/geo/geoparquet-covering.d.ts +19 -0
- package/dist/lib/geo/geoparquet-covering.d.ts.map +1 -0
- package/dist/lib/geo/geoparquet-covering.js +137 -0
- package/dist/lib/geo/geoparquet-covering.js.map +1 -0
- package/dist/lib/geo/geospatial-metadata.d.ts.map +1 -1
- package/dist/lib/geo/geospatial-metadata.js +39 -13
- package/dist/lib/geo/geospatial-metadata.js.map +1 -1
- package/dist/lib/parquet-bloom-filter-planner.d.ts +16 -0
- package/dist/lib/parquet-bloom-filter-planner.d.ts.map +1 -0
- package/dist/lib/parquet-bloom-filter-planner.js +57 -0
- package/dist/lib/parquet-bloom-filter-planner.js.map +1 -0
- package/dist/lib/parquet-bloom-filter.d.ts +30 -0
- package/dist/lib/parquet-bloom-filter.d.ts.map +1 -0
- package/dist/lib/parquet-bloom-filter.js +289 -0
- package/dist/lib/parquet-bloom-filter.js.map +1 -0
- package/dist/lib/parquet-encryption.d.ts +80 -0
- package/dist/lib/parquet-encryption.d.ts.map +1 -0
- package/dist/lib/parquet-encryption.js +190 -0
- package/dist/lib/parquet-encryption.js.map +1 -0
- package/dist/lib/parquet-page-index.d.ts +34 -5
- package/dist/lib/parquet-page-index.d.ts.map +1 -1
- package/dist/lib/parquet-page-index.js +114 -28
- package/dist/lib/parquet-page-index.js.map +1 -1
- package/dist/lib/parquet-predicate.d.ts +5 -1
- package/dist/lib/parquet-predicate.d.ts.map +1 -1
- package/dist/lib/parquet-predicate.js +42 -14
- package/dist/lib/parquet-predicate.js.map +1 -1
- package/dist/lib/parquet-source-worker-decoder.d.ts +4 -1
- package/dist/lib/parquet-source-worker-decoder.d.ts.map +1 -1
- package/dist/lib/parquet-source-worker-decoder.js +83 -17
- package/dist/lib/parquet-source-worker-decoder.js.map +1 -1
- package/dist/lib/parquet-source-worker-types.d.ts +24 -0
- package/dist/lib/parquet-source-worker-types.d.ts.map +1 -1
- package/dist/lib/parsers/get-parquet-schema.d.ts.map +1 -1
- package/dist/lib/parsers/get-parquet-schema.js +3 -1
- package/dist/lib/parsers/get-parquet-schema.js.map +1 -1
- package/dist/lib/parsers/parse-parquet-to-arrow-js.d.ts.map +1 -1
- package/dist/lib/parsers/parse-parquet-to-arrow-js.js +327 -18
- package/dist/lib/parsers/parse-parquet-to-arrow-js.js.map +1 -1
- package/dist/lib/parsers/parse-parquet-to-arrow.d.ts.map +1 -1
- package/dist/lib/parsers/parse-parquet-to-arrow.js +5 -5
- package/dist/lib/parsers/parse-parquet-to-arrow.js.map +1 -1
- package/dist/lib/parsers/parse-parquet-to-columns.d.ts.map +1 -1
- package/dist/lib/parsers/parse-parquet-to-columns.js +3 -1
- package/dist/lib/parsers/parse-parquet-to-columns.js.map +1 -1
- package/dist/lib/parsers/parse-parquet-to-json.d.ts.map +1 -1
- package/dist/lib/parsers/parse-parquet-to-json.js +18 -2
- package/dist/lib/parsers/parse-parquet-to-json.js.map +1 -1
- package/dist/lib/slice-parquet-batch.d.ts +9 -0
- package/dist/lib/slice-parquet-batch.d.ts.map +1 -0
- package/dist/lib/slice-parquet-batch.js +28 -0
- package/dist/lib/slice-parquet-batch.js.map +1 -0
- package/dist/lib/sources/parquet-range-file.d.ts +1 -1
- package/dist/lib/sources/parquet-range-file.d.ts.map +1 -1
- package/dist/lib/sources/parquet-range-file.js +28 -22
- package/dist/lib/sources/parquet-range-file.js.map +1 -1
- package/dist/lib/utils/normalize-parquet-options.d.ts.map +1 -1
- package/dist/lib/utils/normalize-parquet-options.js +15 -1
- package/dist/lib/utils/normalize-parquet-options.js.map +1 -1
- package/dist/parquet-dataset-source.cjs +15766 -4896
- package/dist/parquet-dataset-source.cjs.map +3 -3
- package/dist/parquet-dataset-source.d.ts +31 -4
- package/dist/parquet-dataset-source.d.ts.map +1 -1
- package/dist/parquet-dataset-source.js +296 -74
- package/dist/parquet-dataset-source.js.map +1 -1
- package/dist/parquet-js-loader-types.d.ts +2 -0
- package/dist/parquet-js-loader-types.d.ts.map +1 -1
- package/dist/parquet-js-loader-types.js +2 -1
- package/dist/parquet-js-loader-types.js.map +1 -1
- package/dist/parquet-js-loader.d.ts +1 -0
- package/dist/parquet-js-loader.d.ts.map +1 -1
- package/dist/parquet-js-loader.js +2 -1
- package/dist/parquet-js-loader.js.map +1 -1
- package/dist/parquet-js-writer.d.ts +23 -2
- package/dist/parquet-js-writer.d.ts.map +1 -1
- package/dist/parquet-js-writer.js +1 -1
- package/dist/parquet-js-writer.js.map +1 -1
- package/dist/parquet-loader-base.js +1 -1
- package/dist/parquet-loader-options.d.ts +11 -0
- package/dist/parquet-loader-options.d.ts.map +1 -1
- package/dist/parquet-loader-options.js.map +1 -1
- package/dist/parquet-source-capabilities.d.ts +8 -1
- package/dist/parquet-source-capabilities.d.ts.map +1 -1
- package/dist/parquet-source-capabilities.js +10 -0
- package/dist/parquet-source-capabilities.js.map +1 -1
- package/dist/parquet-source-loader-types.d.ts +10 -0
- package/dist/parquet-source-loader-types.d.ts.map +1 -1
- package/dist/parquet-source-loader-types.js +11 -1
- package/dist/parquet-source-loader-types.js.map +1 -1
- package/dist/parquet-source-loader.cjs +14911 -4316
- package/dist/parquet-source-loader.cjs.map +3 -3
- package/dist/parquet-source-loader.d.ts +42 -4
- package/dist/parquet-source-loader.d.ts.map +1 -1
- package/dist/parquet-source-loader.js +585 -38
- package/dist/parquet-source-loader.js.map +1 -1
- package/dist/parquet-source-types.d.ts +180 -30
- package/dist/parquet-source-types.d.ts.map +1 -1
- package/dist/parquet-source-worker.js +7055 -6989
- package/dist/parquet-source-worker.js.map +4 -4
- package/dist/parquet-worker-node.cjs +8505 -1041
- package/dist/parquet-worker-node.cjs.map +4 -4
- package/dist/parquet-worker-url.js +5 -1
- package/dist/parquet-worker-url.js.map +1 -1
- package/dist/parquet-worker.js +289 -218
- package/dist/parquet-worker.js.map +4 -4
- package/dist/parquetjs/codecs/declare.d.ts +15 -0
- package/dist/parquetjs/codecs/declare.d.ts.map +1 -1
- package/dist/parquetjs/codecs/declare.js +10 -0
- package/dist/parquetjs/codecs/declare.js.map +1 -1
- package/dist/parquetjs/codecs/delta.d.ts.map +1 -1
- package/dist/parquetjs/codecs/delta.js +356 -7
- package/dist/parquetjs/codecs/delta.js.map +1 -1
- package/dist/parquetjs/codecs/index.d.ts.map +1 -1
- package/dist/parquetjs/codecs/index.js +4 -0
- package/dist/parquetjs/codecs/index.js.map +1 -1
- package/dist/parquetjs/codecs/plain.d.ts +1 -1
- package/dist/parquetjs/codecs/plain.d.ts.map +1 -1
- package/dist/parquetjs/codecs/plain.js +153 -12
- package/dist/parquetjs/codecs/plain.js.map +1 -1
- package/dist/parquetjs/codecs/rle.d.ts +4 -0
- package/dist/parquetjs/codecs/rle.d.ts.map +1 -1
- package/dist/parquetjs/codecs/rle.js +406 -3
- package/dist/parquetjs/codecs/rle.js.map +1 -1
- package/dist/parquetjs/compression.d.ts +9 -0
- package/dist/parquetjs/compression.d.ts.map +1 -1
- package/dist/parquetjs/compression.js +86 -67
- package/dist/parquetjs/compression.js.map +1 -1
- package/dist/parquetjs/encoder/dictionary-planner.d.ts.map +1 -1
- package/dist/parquetjs/encoder/dictionary-planner.js +5 -2
- package/dist/parquetjs/encoder/dictionary-planner.js.map +1 -1
- package/dist/parquetjs/encoder/parquet-encoder.d.ts +43 -1
- package/dist/parquetjs/encoder/parquet-encoder.d.ts.map +1 -1
- package/dist/parquetjs/encoder/parquet-encoder.js +691 -34
- package/dist/parquetjs/encoder/parquet-encoder.js.map +1 -1
- package/dist/parquetjs/parquet-thrift/BoundingBox.d.ts +29 -0
- package/dist/parquetjs/parquet-thrift/BoundingBox.d.ts.map +1 -0
- package/dist/parquetjs/parquet-thrift/BoundingBox.js +85 -0
- package/dist/parquetjs/parquet-thrift/BoundingBox.js.map +1 -0
- package/dist/parquetjs/parquet-thrift/ColumnChunk.d.ts +5 -0
- package/dist/parquetjs/parquet-thrift/ColumnChunk.d.ts.map +1 -1
- package/dist/parquetjs/parquet-thrift/ColumnChunk.js +35 -0
- package/dist/parquetjs/parquet-thrift/ColumnChunk.js.map +1 -1
- package/dist/parquetjs/parquet-thrift/ColumnCryptoMetaData.d.ts +35 -0
- package/dist/parquetjs/parquet-thrift/ColumnCryptoMetaData.d.ts.map +1 -0
- package/dist/parquetjs/parquet-thrift/ColumnCryptoMetaData.js +135 -0
- package/dist/parquetjs/parquet-thrift/ColumnCryptoMetaData.js.map +1 -0
- package/dist/parquetjs/parquet-thrift/ColumnMetaData.d.ts +10 -0
- package/dist/parquetjs/parquet-thrift/ColumnMetaData.d.ts.map +1 -1
- package/dist/parquetjs/parquet-thrift/ColumnMetaData.js +73 -0
- package/dist/parquetjs/parquet-thrift/ColumnMetaData.js.map +1 -1
- package/dist/parquetjs/parquet-thrift/EncryptionAlgorithm.d.ts +22 -0
- package/dist/parquetjs/parquet-thrift/EncryptionAlgorithm.d.ts.map +1 -0
- package/dist/parquetjs/parquet-thrift/EncryptionAlgorithm.js +111 -0
- package/dist/parquetjs/parquet-thrift/EncryptionAlgorithm.js.map +1 -0
- package/dist/parquetjs/parquet-thrift/FileCryptoMetaData.d.ts +16 -0
- package/dist/parquetjs/parquet-thrift/FileCryptoMetaData.d.ts.map +1 -0
- package/dist/parquetjs/parquet-thrift/FileCryptoMetaData.js +57 -0
- package/dist/parquetjs/parquet-thrift/FileCryptoMetaData.js.map +1 -0
- package/dist/parquetjs/parquet-thrift/FileMetaData.d.ts +5 -0
- package/dist/parquetjs/parquet-thrift/FileMetaData.d.ts.map +1 -1
- package/dist/parquetjs/parquet-thrift/FileMetaData.js +35 -0
- package/dist/parquetjs/parquet-thrift/FileMetaData.js.map +1 -1
- package/dist/parquetjs/parquet-thrift/GeospatialStatistics.d.ts +18 -0
- package/dist/parquetjs/parquet-thrift/GeospatialStatistics.d.ts.map +1 -0
- package/dist/parquetjs/parquet-thrift/GeospatialStatistics.js +62 -0
- package/dist/parquetjs/parquet-thrift/GeospatialStatistics.js.map +1 -0
- package/dist/parquetjs/parquet-thrift/SizeStatistics.d.ts +26 -0
- package/dist/parquetjs/parquet-thrift/SizeStatistics.d.ts.map +1 -0
- package/dist/parquetjs/parquet-thrift/SizeStatistics.js +85 -0
- package/dist/parquetjs/parquet-thrift/SizeStatistics.js.map +1 -0
- package/dist/parquetjs/parquet-thrift/index.d.ts +6 -0
- package/dist/parquetjs/parquet-thrift/index.d.ts.map +1 -1
- package/dist/parquetjs/parquet-thrift/index.js +6 -0
- package/dist/parquetjs/parquet-thrift/index.js.map +1 -1
- package/dist/parquetjs/parser/decoders.d.ts +10 -1
- package/dist/parquetjs/parser/decoders.d.ts.map +1 -1
- package/dist/parquetjs/parser/decoders.js +318 -14
- package/dist/parquetjs/parser/decoders.js.map +1 -1
- package/dist/parquetjs/parser/parquet-reader.d.ts +78 -12
- package/dist/parquetjs/parser/parquet-reader.d.ts.map +1 -1
- package/dist/parquetjs/parser/parquet-reader.js +605 -66
- package/dist/parquetjs/parser/parquet-reader.js.map +1 -1
- package/dist/parquetjs/schema/declare.d.ts +22 -3
- package/dist/parquetjs/schema/declare.d.ts.map +1 -1
- package/dist/parquetjs/schema/declare.js.map +1 -1
- package/dist/parquetjs/schema/schema.d.ts.map +1 -1
- package/dist/parquetjs/schema/schema.js +10 -0
- package/dist/parquetjs/schema/schema.js.map +1 -1
- package/dist/parquetjs/schema/shred.d.ts.map +1 -1
- package/dist/parquetjs/schema/shred.js +219 -11
- package/dist/parquetjs/schema/shred.js.map +1 -1
- package/dist/parquetjs/schema/types.js +19 -21
- package/dist/parquetjs/schema/types.js.map +1 -1
- package/dist/parquetjs/schema/variant.d.ts +3 -0
- package/dist/parquetjs/schema/variant.d.ts.map +1 -0
- package/dist/parquetjs/schema/variant.js +248 -0
- package/dist/parquetjs/schema/variant.js.map +1 -0
- package/dist/parquetjs/utils/crc32.d.ts +3 -0
- package/dist/parquetjs/utils/crc32.d.ts.map +1 -0
- package/dist/parquetjs/utils/crc32.js +15 -0
- package/dist/parquetjs/utils/crc32.js.map +1 -0
- package/dist/parquetjs/utils/read-utils.d.ts +11 -1
- package/dist/parquetjs/utils/read-utils.d.ts.map +1 -1
- package/dist/parquetjs/utils/read-utils.js +21 -1
- package/dist/parquetjs/utils/read-utils.js.map +1 -1
- package/dist/unbundled.d.ts +2 -1
- package/dist/unbundled.d.ts.map +1 -1
- package/dist/unbundled.js +1 -0
- package/dist/unbundled.js.map +1 -1
- package/package.json +36 -11
- package/src/bundled.ts +3 -0
- package/src/delta-format.ts +12 -0
- package/src/delta-source-loader-types.ts +40 -0
- package/src/delta-source.ts +305 -0
- package/src/delta-types.ts +40 -0
- package/src/iceberg-rest-catalog.ts +122 -0
- package/src/iceberg-table-source.ts +1037 -0
- package/src/iceberg-types.ts +165 -0
- package/src/index.ts +50 -1
- package/src/lib/arrow/convert-schema-from-parquet.ts +113 -19
- package/src/lib/arrow/create-nested-arrow-vector.ts +116 -0
- package/src/lib/encoders/encode-table-to-parquet-js.ts +268 -7
- package/src/lib/geo/geoparquet-covering.ts +201 -0
- package/src/lib/geo/geospatial-metadata.ts +42 -15
- package/src/lib/parquet-bloom-filter-planner.ts +86 -0
- package/src/lib/parquet-bloom-filter.ts +345 -0
- package/src/lib/parquet-encryption.ts +344 -0
- package/src/lib/parquet-page-index.ts +180 -34
- package/src/lib/parquet-predicate.ts +52 -16
- package/src/lib/parquet-source-worker-decoder.ts +116 -20
- package/src/lib/parquet-source-worker-types.ts +25 -0
- package/src/lib/parsers/get-parquet-schema.ts +3 -1
- package/src/lib/parsers/parse-parquet-to-arrow-js.ts +397 -17
- package/src/lib/parsers/parse-parquet-to-arrow.ts +4 -5
- package/src/lib/parsers/parse-parquet-to-columns.ts +3 -1
- package/src/lib/parsers/parse-parquet-to-json.ts +18 -2
- package/src/lib/slice-parquet-batch.ts +32 -0
- package/src/lib/sources/parquet-range-file.ts +35 -29
- package/src/lib/utils/normalize-parquet-options.ts +17 -1
- package/src/parquet-dataset-source.ts +397 -85
- package/src/parquet-js-loader-types.ts +1 -0
- package/src/parquet-js-loader.ts +2 -1
- package/src/parquet-js-writer.ts +30 -0
- package/src/parquet-loader-options.ts +11 -0
- package/src/parquet-source-capabilities.ts +18 -1
- package/src/parquet-source-loader-types.ts +10 -0
- package/src/parquet-source-loader.ts +929 -52
- package/src/parquet-source-types.ts +197 -28
- package/src/parquet-worker-url.ts +6 -1
- package/src/parquetjs/codecs/declare.ts +27 -0
- package/src/parquetjs/codecs/delta.ts +466 -6
- package/src/parquetjs/codecs/index.ts +4 -0
- package/src/parquetjs/codecs/plain.ts +177 -11
- package/src/parquetjs/codecs/rle.ts +573 -3
- package/src/parquetjs/compression.ts +102 -76
- package/src/parquetjs/encoder/dictionary-planner.ts +7 -2
- package/src/parquetjs/encoder/parquet-encoder.ts +1016 -38
- package/src/parquetjs/parquet-thrift/BoundingBox.ts +108 -0
- package/src/parquetjs/parquet-thrift/ColumnChunk.ts +35 -0
- package/src/parquetjs/parquet-thrift/ColumnCryptoMetaData.ts +139 -0
- package/src/parquetjs/parquet-thrift/ColumnMetaData.ts +73 -0
- package/src/parquetjs/parquet-thrift/EncryptionAlgorithm.ts +127 -0
- package/src/parquetjs/parquet-thrift/FileCryptoMetaData.ts +60 -0
- package/src/parquetjs/parquet-thrift/FileMetaData.ts +35 -0
- package/src/parquetjs/parquet-thrift/GeospatialStatistics.ts +68 -0
- package/src/parquetjs/parquet-thrift/SizeStatistics.ts +98 -0
- package/src/parquetjs/parquet-thrift/index.ts +6 -0
- package/src/parquetjs/parser/decoders.ts +451 -26
- package/src/parquetjs/parser/parquet-reader.ts +935 -87
- package/src/parquetjs/schema/declare.ts +22 -2
- package/src/parquetjs/schema/schema.ts +10 -0
- package/src/parquetjs/schema/shred.ts +238 -11
- package/src/parquetjs/schema/types.ts +16 -25
- package/src/parquetjs/schema/variant.ts +313 -0
- package/src/parquetjs/utils/crc32.ts +15 -0
- package/src/parquetjs/utils/read-utils.ts +26 -1
- package/src/unbundled.ts +14 -0
|
@@ -7,11 +7,26 @@
|
|
|
7
7
|
import type {ReadableFile} from '@loaders.gl/loader-utils';
|
|
8
8
|
|
|
9
9
|
import {ParquetSchema} from '../schema/schema';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
decodeSchema,
|
|
12
|
+
decodeDataPages,
|
|
13
|
+
decodePage,
|
|
14
|
+
decodeUncompressedDataPages,
|
|
15
|
+
decodeUncompressedDictionaryBuffer
|
|
16
|
+
} from './decoders';
|
|
11
17
|
import {materializeRows} from '../schema/shred';
|
|
18
|
+
import {createParquetPageDecompressor, type ParquetPageDecompressor} from '../compression';
|
|
12
19
|
|
|
13
20
|
import {PARQUET_MAGIC, PARQUET_MAGIC_ENCRYPTED} from '../../lib/constants';
|
|
14
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
ColumnChunk,
|
|
23
|
+
CompressionCodec,
|
|
24
|
+
Encoding,
|
|
25
|
+
FileMetaData,
|
|
26
|
+
PageType,
|
|
27
|
+
RowGroup,
|
|
28
|
+
Type
|
|
29
|
+
} from '../parquet-thrift/index';
|
|
15
30
|
import {
|
|
16
31
|
ParquetRowGroup,
|
|
17
32
|
ParquetCompression,
|
|
@@ -20,7 +35,15 @@ import {
|
|
|
20
35
|
ParquetReaderContext,
|
|
21
36
|
type ParquetLevelBuffer
|
|
22
37
|
} from '../schema/declare';
|
|
23
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
decodeColumnMetadata,
|
|
40
|
+
decodeFileCryptoMetadata,
|
|
41
|
+
decodeFileMetadata,
|
|
42
|
+
decodePageHeader,
|
|
43
|
+
getThriftEnum,
|
|
44
|
+
fieldIndexOf,
|
|
45
|
+
serializeThrift
|
|
46
|
+
} from '../utils/read-utils';
|
|
24
47
|
import {decodeString, readUInt32LE, toUint8Array} from '../utils/binary-utils';
|
|
25
48
|
import {CompactInt64} from '../utils/uint8-array-compact-protocol';
|
|
26
49
|
import type {
|
|
@@ -28,10 +51,55 @@ import type {
|
|
|
28
51
|
ParquetPageLocations,
|
|
29
52
|
ParquetRowRange
|
|
30
53
|
} from '../../lib/parquet-page-index';
|
|
54
|
+
import {
|
|
55
|
+
createParquetModuleAad,
|
|
56
|
+
decryptParquetModule,
|
|
57
|
+
readParquetEncryptedModule,
|
|
58
|
+
verifyParquetFooterSignature,
|
|
59
|
+
type ParquetEncryptionAlgorithm,
|
|
60
|
+
type ParquetKeyRetriever
|
|
61
|
+
} from '../../lib/parquet-encryption';
|
|
62
|
+
import type {ParquetEncryptionModule} from '../../lib/parquet-encryption';
|
|
31
63
|
|
|
32
64
|
/** Bounds concurrent range requests when a row group contains unusually many selected columns. */
|
|
33
65
|
const MAXIMUM_CONCURRENT_COLUMN_READS = 16;
|
|
34
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Returns whether column metadata guarantees that every data page can decode directly into an
|
|
69
|
+
* Arrow-compatible byte buffer. Level encodings are included in the metadata encoding set and do
|
|
70
|
+
* not prevent the byte-array fast path.
|
|
71
|
+
*/
|
|
72
|
+
function hasOnlyArrowByteArrayDataPageEncodings(
|
|
73
|
+
encodings: Encoding[],
|
|
74
|
+
dictionaryPageOffset: number | undefined
|
|
75
|
+
): boolean {
|
|
76
|
+
if (dictionaryPageOffset !== undefined || encodings.length === 0) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
for (const encoding of encodings) {
|
|
80
|
+
if (
|
|
81
|
+
encoding !== Encoding.PLAIN &&
|
|
82
|
+
encoding !== Encoding.DELTA_LENGTH_BYTE_ARRAY &&
|
|
83
|
+
encoding !== Encoding.DELTA_BYTE_ARRAY &&
|
|
84
|
+
encoding !== Encoding.RLE &&
|
|
85
|
+
encoding !== Encoding.BIT_PACKED
|
|
86
|
+
) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Returns the algorithm name represented by the Thrift encryption union. */
|
|
94
|
+
function getEncryptionAlgorithm(value: {
|
|
95
|
+
AES_GCM_V1?: unknown;
|
|
96
|
+
AES_GCM_CTR_V1?: unknown;
|
|
97
|
+
}): ParquetEncryptionAlgorithm {
|
|
98
|
+
if (value.AES_GCM_V1) return 'AES_GCM_V1';
|
|
99
|
+
if (value.AES_GCM_CTR_V1) return 'AES_GCM_CTR_V1';
|
|
100
|
+
throw new Error('Unsupported Parquet encryption algorithm');
|
|
101
|
+
}
|
|
102
|
+
|
|
35
103
|
export type ParquetReaderProps = {
|
|
36
104
|
/** Maximum dictionary-page read size. */
|
|
37
105
|
defaultDictionarySize?: number;
|
|
@@ -43,8 +111,22 @@ export type ParquetReaderProps = {
|
|
|
43
111
|
useTypedValueBuffers?: boolean;
|
|
44
112
|
/** Decode repetition and definition levels into compact unsigned typed arrays. */
|
|
45
113
|
useTypedLevelBuffers?: boolean;
|
|
114
|
+
/** Decode eligible PLAIN BYTE_ARRAY columns into Arrow-compatible contiguous buffers. */
|
|
115
|
+
useArrowByteArrayBuffers?: boolean;
|
|
116
|
+
/** Verify page-header CRC values when present. Disabled by default for throughput. */
|
|
117
|
+
verifyPageChecksums?: boolean;
|
|
118
|
+
/** Decode legacy INT96 values as epoch nanoseconds for Arrow timestamp columns. */
|
|
119
|
+
int96AsTimestamp?: boolean;
|
|
120
|
+
/** Verify plaintext-footer signatures when present. Enabled by default. */
|
|
121
|
+
verifyFooterSignature?: boolean;
|
|
46
122
|
/** Abort signal forwarded to every underlying random-access read. */
|
|
47
123
|
signal?: AbortSignal;
|
|
124
|
+
/** Resolves modular-encryption keys from the file's key metadata. */
|
|
125
|
+
keyRetriever?: ParquetKeyRetriever;
|
|
126
|
+
/** AAD prefix for files that intentionally omit it from FileCryptoMetaData. */
|
|
127
|
+
aadPrefix?: Uint8Array;
|
|
128
|
+
/** Pre-resolved encryption context used by worker-local readers. */
|
|
129
|
+
encryptionContext?: ParquetReaderEncryptionContext;
|
|
48
130
|
};
|
|
49
131
|
|
|
50
132
|
/** Properties for initializing a ParquetRowGroupReader */
|
|
@@ -57,6 +139,26 @@ export type ParquetIterationProps = {
|
|
|
57
139
|
signal?: AbortSignal;
|
|
58
140
|
};
|
|
59
141
|
|
|
142
|
+
type NormalizedParquetReaderProps = Required<
|
|
143
|
+
Omit<ParquetReaderProps, 'signal' | 'keyRetriever' | 'aadPrefix' | 'encryptionContext'>
|
|
144
|
+
> &
|
|
145
|
+
Pick<ParquetReaderProps, 'signal' | 'keyRetriever' | 'aadPrefix' | 'encryptionContext'>;
|
|
146
|
+
|
|
147
|
+
/** File-level parameters needed to authenticate modular-encrypted modules. */
|
|
148
|
+
type ParquetEncryptionContext = {
|
|
149
|
+
algorithm: ParquetEncryptionAlgorithm;
|
|
150
|
+
aadPrefix?: Uint8Array;
|
|
151
|
+
fileUnique: Uint8Array;
|
|
152
|
+
footerKeyMetadata?: Uint8Array;
|
|
153
|
+
footerSigningKeyMetadata?: Uint8Array;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** File-level encryption parameters needed to decrypt worker-transferred page modules. */
|
|
157
|
+
export type ParquetReaderEncryptionContext = Pick<
|
|
158
|
+
ParquetEncryptionContext,
|
|
159
|
+
'algorithm' | 'aadPrefix' | 'fileUnique' | 'footerKeyMetadata'
|
|
160
|
+
>;
|
|
161
|
+
|
|
60
162
|
/**
|
|
61
163
|
* The parquet envelope reader allows direct, unbuffered access to the individual
|
|
62
164
|
* sections of the parquet file, namely the header, footer and the row groups.
|
|
@@ -64,32 +166,154 @@ export type ParquetIterationProps = {
|
|
|
64
166
|
* rows from a parquet file use the ParquetReader instead
|
|
65
167
|
*/
|
|
66
168
|
export class ParquetReader {
|
|
67
|
-
static defaultProps:
|
|
169
|
+
static defaultProps: NormalizedParquetReaderProps = {
|
|
68
170
|
// max ArrayBuffer size in js is 2Gb
|
|
69
171
|
defaultDictionarySize: 2147483648,
|
|
70
172
|
preserveBinary: false,
|
|
71
173
|
retainByteArrayViews: false,
|
|
72
174
|
useTypedValueBuffers: false,
|
|
73
175
|
useTypedLevelBuffers: false,
|
|
74
|
-
|
|
176
|
+
useArrowByteArrayBuffers: false,
|
|
177
|
+
verifyPageChecksums: false,
|
|
178
|
+
int96AsTimestamp: false,
|
|
179
|
+
verifyFooterSignature: true,
|
|
180
|
+
signal: undefined,
|
|
181
|
+
keyRetriever: undefined,
|
|
182
|
+
aadPrefix: undefined,
|
|
183
|
+
encryptionContext: undefined
|
|
75
184
|
};
|
|
76
185
|
|
|
77
|
-
props:
|
|
186
|
+
props: NormalizedParquetReaderProps;
|
|
78
187
|
file: ReadableFile;
|
|
79
188
|
metadata: Promise<FileMetaData> | null = null;
|
|
80
189
|
/** Parsed Parquet schema shared by metadata, iteration, and materialization paths. */
|
|
81
190
|
private schema: Promise<ParquetSchema> | null = null;
|
|
191
|
+
/** Encryption parameters decoded from plaintext or encrypted footer metadata. */
|
|
192
|
+
private encryptionContext?: ParquetEncryptionContext;
|
|
193
|
+
/** Resolved keys shared by encrypted modules in this reader instance. */
|
|
194
|
+
private readonly encryptionKeyCache = new Map<string, Promise<ArrayBuffer | ArrayBufferView>>();
|
|
195
|
+
/** Compression backends selected once and shared by every page in this reader. */
|
|
196
|
+
private readonly pageDecompressors = new Map<ParquetCompression, ParquetPageDecompressor>();
|
|
82
197
|
|
|
83
198
|
constructor(file: ReadableFile, props?: ParquetReaderProps) {
|
|
84
199
|
this.file = file;
|
|
85
200
|
this.props = {...ParquetReader.defaultProps, ...props};
|
|
201
|
+
this.encryptionContext = this.props.encryptionContext;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Whether the decoded footer describes an encrypted Parquet file. */
|
|
205
|
+
get encrypted(): boolean {
|
|
206
|
+
return Boolean(this.encryptionContext);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Whether this reader decodes legacy INT96 values as epoch nanoseconds. */
|
|
210
|
+
get int96AsTimestamp(): boolean {
|
|
211
|
+
return this.props.int96AsTimestamp;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Returns a cloneable snapshot of the file encryption context for worker decoding. */
|
|
215
|
+
getEncryptionContextForWorker(): ParquetReaderEncryptionContext | undefined {
|
|
216
|
+
const encryptionContext = this.encryptionContext;
|
|
217
|
+
if (!encryptionContext) return undefined;
|
|
218
|
+
return {
|
|
219
|
+
algorithm: encryptionContext.algorithm,
|
|
220
|
+
aadPrefix: encryptionContext.aadPrefix && new Uint8Array(encryptionContext.aadPrefix),
|
|
221
|
+
fileUnique: new Uint8Array(encryptionContext.fileUnique),
|
|
222
|
+
footerKeyMetadata:
|
|
223
|
+
encryptionContext.footerKeyMetadata && new Uint8Array(encryptionContext.footerKeyMetadata)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Resolves one column key on the caller thread before transferring encrypted ranges. */
|
|
228
|
+
async getColumnKeyForWorker(
|
|
229
|
+
columnChunk: ColumnChunk,
|
|
230
|
+
rowGroupOrdinal: number,
|
|
231
|
+
columnOrdinal: number
|
|
232
|
+
): Promise<{keyMetadata?: Uint8Array; keyMaterial: Uint8Array}> {
|
|
233
|
+
if (!this.encryptionContext || !this.props.keyRetriever) {
|
|
234
|
+
throw new Error('Encrypted Parquet worker reads require parquet.keyRetriever');
|
|
235
|
+
}
|
|
236
|
+
const keyMetadata = this.getColumnKeyMetadata(columnChunk);
|
|
237
|
+
const keyMaterial = await this.props.keyRetriever(keyMetadata, {
|
|
238
|
+
algorithm: this.encryptionContext.algorithm,
|
|
239
|
+
aad: createParquetModuleAad(
|
|
240
|
+
this.encryptionContext.aadPrefix,
|
|
241
|
+
this.encryptionContext.fileUnique,
|
|
242
|
+
'data-page',
|
|
243
|
+
rowGroupOrdinal,
|
|
244
|
+
columnOrdinal,
|
|
245
|
+
0
|
|
246
|
+
)
|
|
247
|
+
});
|
|
248
|
+
const keyBytes =
|
|
249
|
+
keyMaterial instanceof ArrayBuffer
|
|
250
|
+
? new Uint8Array(keyMaterial)
|
|
251
|
+
: new Uint8Array(keyMaterial.buffer, keyMaterial.byteOffset, keyMaterial.byteLength);
|
|
252
|
+
return {keyMetadata, keyMaterial: keyBytes};
|
|
86
253
|
}
|
|
87
254
|
|
|
88
255
|
close(): void {
|
|
256
|
+
this.encryptionKeyCache.clear();
|
|
257
|
+
this.pageDecompressors.clear();
|
|
89
258
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
90
259
|
this.file.close();
|
|
91
260
|
}
|
|
92
261
|
|
|
262
|
+
/** Returns the reader-scoped decoder for one Parquet compression method. */
|
|
263
|
+
private getPageDecompressor(compression: ParquetCompression): ParquetPageDecompressor {
|
|
264
|
+
let decompressor = this.pageDecompressors.get(compression);
|
|
265
|
+
if (!decompressor) {
|
|
266
|
+
decompressor = createParquetPageDecompressor(compression);
|
|
267
|
+
this.pageDecompressors.set(compression, decompressor);
|
|
268
|
+
}
|
|
269
|
+
return decompressor;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Reads a byte range, retaining a zero-copy view when the complete file is already in memory. */
|
|
273
|
+
private async readBytes(
|
|
274
|
+
start: number,
|
|
275
|
+
length: number,
|
|
276
|
+
signal?: AbortSignal
|
|
277
|
+
): Promise<Uint8Array> {
|
|
278
|
+
const effectiveSignal = signal ?? this.props.signal;
|
|
279
|
+
if (effectiveSignal?.aborted) {
|
|
280
|
+
throw new Error('Request aborted');
|
|
281
|
+
}
|
|
282
|
+
if (this.file.handle instanceof ArrayBuffer) {
|
|
283
|
+
return new Uint8Array(this.file.handle, start, length);
|
|
284
|
+
}
|
|
285
|
+
return toUint8Array(await this.file.read(start, length, effectiveSignal));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Returns a reader-scoped key resolver that avoids repeating key-provider work per module. */
|
|
289
|
+
private getCachedKeyRetriever(): ParquetKeyRetriever {
|
|
290
|
+
const keyRetriever = this.props.keyRetriever;
|
|
291
|
+
if (!keyRetriever) {
|
|
292
|
+
throw new Error('Encrypted Parquet requires parquet.keyRetriever');
|
|
293
|
+
}
|
|
294
|
+
return (keyMetadata, context) => {
|
|
295
|
+
const encryptionContext = this.encryptionContext;
|
|
296
|
+
const cacheKey = createEncryptionKeyCacheKey(
|
|
297
|
+
context.algorithm,
|
|
298
|
+
keyMetadata,
|
|
299
|
+
context.aad,
|
|
300
|
+
encryptionContext?.aadPrefix?.byteLength ?? this.props.aadPrefix?.byteLength,
|
|
301
|
+
encryptionContext?.fileUnique.byteLength
|
|
302
|
+
);
|
|
303
|
+
let keyPromise = this.encryptionKeyCache.get(cacheKey);
|
|
304
|
+
if (!keyPromise) {
|
|
305
|
+
keyPromise = Promise.resolve().then(() => keyRetriever(keyMetadata, context));
|
|
306
|
+
this.encryptionKeyCache.set(cacheKey, keyPromise);
|
|
307
|
+
void keyPromise.catch(() => {
|
|
308
|
+
if (this.encryptionKeyCache.get(cacheKey) === keyPromise) {
|
|
309
|
+
this.encryptionKeyCache.delete(cacheKey);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return keyPromise;
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
93
317
|
// HIGH LEVEL METHODS
|
|
94
318
|
|
|
95
319
|
/** Yield one row at a time */
|
|
@@ -130,7 +354,8 @@ export class ParquetReader {
|
|
|
130
354
|
schema,
|
|
131
355
|
metadata.row_groups[rowGroupIndex],
|
|
132
356
|
columnList,
|
|
133
|
-
props?.signal
|
|
357
|
+
props?.signal,
|
|
358
|
+
rowGroupIndex
|
|
134
359
|
);
|
|
135
360
|
yield rowGroup;
|
|
136
361
|
}
|
|
@@ -175,13 +400,12 @@ export class ParquetReader {
|
|
|
175
400
|
|
|
176
401
|
/** Metadata is stored in the footer */
|
|
177
402
|
async readHeader(signal?: AbortSignal): Promise<void> {
|
|
178
|
-
const
|
|
179
|
-
const magic = decodeString(toUint8Array(arrayBuffer));
|
|
403
|
+
const magic = decodeString(await this.readBytes(0, PARQUET_MAGIC.length, signal));
|
|
180
404
|
switch (magic) {
|
|
181
405
|
case PARQUET_MAGIC:
|
|
182
406
|
break;
|
|
183
407
|
case PARQUET_MAGIC_ENCRYPTED:
|
|
184
|
-
|
|
408
|
+
break;
|
|
185
409
|
default:
|
|
186
410
|
throw new Error(`Invalid parquet file (magic=${magic})`);
|
|
187
411
|
}
|
|
@@ -190,34 +414,258 @@ export class ParquetReader {
|
|
|
190
414
|
/** Metadata is stored in the footer */
|
|
191
415
|
async readFooter(signal?: AbortSignal): Promise<FileMetaData> {
|
|
192
416
|
const trailerLen = PARQUET_MAGIC.length + 4;
|
|
193
|
-
const
|
|
194
|
-
this.file.size - trailerLen,
|
|
195
|
-
trailerLen,
|
|
196
|
-
signal ?? this.props.signal
|
|
197
|
-
);
|
|
198
|
-
const trailer = toUint8Array(arrayBuffer);
|
|
417
|
+
const trailer = await this.readBytes(this.file.size - trailerLen, trailerLen, signal);
|
|
199
418
|
|
|
200
419
|
const magic = decodeString(trailer, 4);
|
|
420
|
+
const metadataSize = readUInt32LE(trailer, 0);
|
|
421
|
+
const metadataOffset = this.file.size - metadataSize - trailerLen;
|
|
422
|
+
if (magic === PARQUET_MAGIC_ENCRYPTED) {
|
|
423
|
+
return await this.readEncryptedFooter(metadataSize, metadataOffset, signal);
|
|
424
|
+
}
|
|
201
425
|
if (magic !== PARQUET_MAGIC) {
|
|
202
426
|
throw new Error(`Not a valid parquet file (magic="${magic})`);
|
|
203
427
|
}
|
|
204
428
|
|
|
205
|
-
const metadataSize = readUInt32LE(trailer, 0);
|
|
206
|
-
const metadataOffset = this.file.size - metadataSize - trailerLen;
|
|
207
429
|
if (metadataOffset < PARQUET_MAGIC.length) {
|
|
208
430
|
throw new Error(`Invalid metadata size ${metadataOffset}`);
|
|
209
431
|
}
|
|
210
432
|
|
|
211
|
-
const
|
|
212
|
-
metadataOffset,
|
|
213
|
-
metadataSize,
|
|
214
|
-
signal ?? this.props.signal
|
|
215
|
-
);
|
|
216
|
-
const metadataBuf = toUint8Array(arrayBuffer2);
|
|
433
|
+
const metadataBuf = await this.readBytes(metadataOffset, metadataSize, signal);
|
|
217
434
|
// let metadata = new parquet_thrift.FileMetaData();
|
|
218
435
|
// parquet_util.decodeThrift(metadata, metadataBuf);
|
|
219
436
|
|
|
220
|
-
const
|
|
437
|
+
const decoded = decodeFileMetadata(metadataBuf);
|
|
438
|
+
const {metadata} = decoded;
|
|
439
|
+
this.setEncryptionContext(metadata);
|
|
440
|
+
if (this.props.verifyFooterSignature && this.encryptionContext?.footerSigningKeyMetadata) {
|
|
441
|
+
// The compact-protocol decoder may consume bytes from the signature that
|
|
442
|
+
// happen to look like valid Thrift fields. The format defines the
|
|
443
|
+
// signature as the final fixed 28 bytes of the footer range, so use the
|
|
444
|
+
// physical boundary rather than the decoder's consumed length.
|
|
445
|
+
const signatureOffset = metadataBuf.byteLength - 28;
|
|
446
|
+
if (signatureOffset < decoded.length) {
|
|
447
|
+
throw new Error('Invalid Parquet plaintext-footer signature boundary');
|
|
448
|
+
}
|
|
449
|
+
const signature = metadataBuf.subarray(signatureOffset);
|
|
450
|
+
const encryptionContext = this.encryptionContext;
|
|
451
|
+
if (!this.props.keyRetriever) {
|
|
452
|
+
throw new Error('Signed Parquet footer requires parquet.keyRetriever');
|
|
453
|
+
}
|
|
454
|
+
await verifyParquetFooterSignature(metadataBuf.subarray(0, signatureOffset), signature, {
|
|
455
|
+
algorithm: encryptionContext.algorithm,
|
|
456
|
+
aad: createParquetModuleAad(
|
|
457
|
+
encryptionContext.aadPrefix,
|
|
458
|
+
encryptionContext.fileUnique,
|
|
459
|
+
'footer'
|
|
460
|
+
),
|
|
461
|
+
keyMetadata: encryptionContext.footerSigningKeyMetadata,
|
|
462
|
+
keyRetriever: this.getCachedKeyRetriever()
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return metadata;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Reads and decrypts an encrypted footer after the standard trailer. */
|
|
469
|
+
private async readEncryptedFooter(
|
|
470
|
+
metadataSize: number,
|
|
471
|
+
metadataOffset: number,
|
|
472
|
+
signal?: AbortSignal
|
|
473
|
+
): Promise<FileMetaData> {
|
|
474
|
+
if (!this.props.keyRetriever) {
|
|
475
|
+
throw new Error('Encrypted Parquet footer requires parquet.keyRetriever');
|
|
476
|
+
}
|
|
477
|
+
const encryptedFooter = await this.readBytes(metadataOffset, metadataSize, signal);
|
|
478
|
+
const cryptoMetadata = decodeFileCryptoMetadata(encryptedFooter);
|
|
479
|
+
const algorithm = getEncryptionAlgorithm(cryptoMetadata.metadata.encryption_algorithm);
|
|
480
|
+
const algorithmMetadata =
|
|
481
|
+
cryptoMetadata.metadata.encryption_algorithm.AES_GCM_V1 ||
|
|
482
|
+
cryptoMetadata.metadata.encryption_algorithm.AES_GCM_CTR_V1;
|
|
483
|
+
const aadPrefix = algorithmMetadata?.aad_prefix || this.props.aadPrefix;
|
|
484
|
+
if (!algorithmMetadata?.aad_file_unique) {
|
|
485
|
+
throw new Error('Encrypted Parquet footer is missing aad_file_unique');
|
|
486
|
+
}
|
|
487
|
+
const aad = createParquetModuleAad(aadPrefix, algorithmMetadata.aad_file_unique, 'footer');
|
|
488
|
+
const plaintext = await decryptParquetModule(encryptedFooter.subarray(cryptoMetadata.length), {
|
|
489
|
+
algorithm,
|
|
490
|
+
aad,
|
|
491
|
+
keyMetadata: cryptoMetadata.metadata.key_metadata,
|
|
492
|
+
keyRetriever: this.getCachedKeyRetriever()
|
|
493
|
+
});
|
|
494
|
+
this.encryptionContext = {
|
|
495
|
+
algorithm,
|
|
496
|
+
aadPrefix,
|
|
497
|
+
fileUnique: algorithmMetadata.aad_file_unique,
|
|
498
|
+
footerKeyMetadata: cryptoMetadata.metadata.key_metadata
|
|
499
|
+
};
|
|
500
|
+
const metadata = decodeFileMetadata(plaintext).metadata;
|
|
501
|
+
return metadata;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Records the encryption parameters exposed by plaintext-footer files. */
|
|
505
|
+
private setEncryptionContext(metadata: FileMetaData): void {
|
|
506
|
+
const encryptionAlgorithm = metadata.encryption_algorithm;
|
|
507
|
+
if (!encryptionAlgorithm) return;
|
|
508
|
+
const algorithm = getEncryptionAlgorithm(encryptionAlgorithm);
|
|
509
|
+
const algorithmMetadata = encryptionAlgorithm.AES_GCM_V1 || encryptionAlgorithm.AES_GCM_CTR_V1;
|
|
510
|
+
if (!algorithmMetadata?.aad_file_unique) {
|
|
511
|
+
throw new Error('Encrypted Parquet footer is missing aad_file_unique');
|
|
512
|
+
}
|
|
513
|
+
this.encryptionContext = {
|
|
514
|
+
algorithm,
|
|
515
|
+
aadPrefix: algorithmMetadata.aad_prefix || this.props.aadPrefix,
|
|
516
|
+
fileUnique: algorithmMetadata.aad_file_unique,
|
|
517
|
+
footerSigningKeyMetadata: metadata.footer_signing_key_metadata
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Resolves only the selected encrypted columns, using crypto metadata paths before decryption. */
|
|
522
|
+
async resolveColumnMetadata(
|
|
523
|
+
rowGroup: RowGroup,
|
|
524
|
+
rowGroupOrdinal: number,
|
|
525
|
+
selectedColumnPaths?: readonly string[][]
|
|
526
|
+
): Promise<void> {
|
|
527
|
+
for (let columnOrdinal = 0; columnOrdinal < rowGroup.columns.length; columnOrdinal++) {
|
|
528
|
+
const columnChunk = rowGroup.columns[columnOrdinal];
|
|
529
|
+
const path = getColumnChunkPath(columnChunk);
|
|
530
|
+
if (
|
|
531
|
+
selectedColumnPaths &&
|
|
532
|
+
path &&
|
|
533
|
+
!selectedColumnPaths.some(
|
|
534
|
+
selectedPath => fieldIndexOf([Array.from(selectedPath)], path) >= 0
|
|
535
|
+
)
|
|
536
|
+
) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (columnChunk.encrypted_column_metadata && !columnChunk.meta_data) {
|
|
540
|
+
columnChunk.meta_data = await this.getColumnMetadata(
|
|
541
|
+
columnChunk,
|
|
542
|
+
rowGroupOrdinal,
|
|
543
|
+
getPhysicalColumnOrdinal(columnChunk, columnOrdinal)
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Decrypts one encrypted column index or offset index module for selective planning. */
|
|
550
|
+
async decryptIndexModule(
|
|
551
|
+
bytes: Uint8Array,
|
|
552
|
+
module: 'column-index' | 'offset-index',
|
|
553
|
+
rowGroupOrdinal: number,
|
|
554
|
+
columnOrdinal: number,
|
|
555
|
+
columnChunk: ColumnChunk
|
|
556
|
+
): Promise<Uint8Array> {
|
|
557
|
+
if (!columnChunk.crypto_metadata) return bytes;
|
|
558
|
+
const encryptionContext = this.encryptionContext;
|
|
559
|
+
if (!encryptionContext || !this.props.keyRetriever) {
|
|
560
|
+
throw new Error('Encrypted Parquet indexes require parquet.keyRetriever');
|
|
561
|
+
}
|
|
562
|
+
const aad = createParquetModuleAad(
|
|
563
|
+
encryptionContext.aadPrefix,
|
|
564
|
+
encryptionContext.fileUnique,
|
|
565
|
+
module,
|
|
566
|
+
rowGroupOrdinal,
|
|
567
|
+
columnOrdinal
|
|
568
|
+
);
|
|
569
|
+
return await decryptParquetModule(bytes, {
|
|
570
|
+
algorithm: encryptionContext.algorithm,
|
|
571
|
+
aad,
|
|
572
|
+
keyMetadata: this.getColumnKeyMetadata(columnChunk),
|
|
573
|
+
keyRetriever: this.getCachedKeyRetriever()
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Decrypts the encrypted Bloom-filter header and bitset modules for one column chunk. */
|
|
578
|
+
async decryptBloomFilter(
|
|
579
|
+
bytes: Uint8Array,
|
|
580
|
+
rowGroupOrdinal: number,
|
|
581
|
+
columnOrdinal: number,
|
|
582
|
+
columnChunk: ColumnChunk
|
|
583
|
+
): Promise<Uint8Array> {
|
|
584
|
+
if (!columnChunk.crypto_metadata) return bytes;
|
|
585
|
+
const encryptionContext = this.encryptionContext;
|
|
586
|
+
if (!encryptionContext || !this.props.keyRetriever) {
|
|
587
|
+
throw new Error('Encrypted Parquet Bloom filters require parquet.keyRetriever');
|
|
588
|
+
}
|
|
589
|
+
const header = readParquetEncryptedModule(bytes);
|
|
590
|
+
const bitset = readParquetEncryptedModule(bytes, header.nextOffset);
|
|
591
|
+
const keyMetadata = this.getColumnKeyMetadata(columnChunk);
|
|
592
|
+
const headerBytes = await decryptParquetModule(header.bytes, {
|
|
593
|
+
algorithm: encryptionContext.algorithm,
|
|
594
|
+
aad: createParquetModuleAad(
|
|
595
|
+
encryptionContext.aadPrefix,
|
|
596
|
+
encryptionContext.fileUnique,
|
|
597
|
+
'bloom-filter-header',
|
|
598
|
+
rowGroupOrdinal,
|
|
599
|
+
columnOrdinal
|
|
600
|
+
),
|
|
601
|
+
keyMetadata,
|
|
602
|
+
keyRetriever: this.getCachedKeyRetriever()
|
|
603
|
+
});
|
|
604
|
+
const bitsetBytes = await decryptParquetModule(bitset.bytes, {
|
|
605
|
+
algorithm: encryptionContext.algorithm,
|
|
606
|
+
aad: createParquetModuleAad(
|
|
607
|
+
encryptionContext.aadPrefix,
|
|
608
|
+
encryptionContext.fileUnique,
|
|
609
|
+
'bloom-filter-bitset',
|
|
610
|
+
rowGroupOrdinal,
|
|
611
|
+
columnOrdinal
|
|
612
|
+
),
|
|
613
|
+
keyMetadata,
|
|
614
|
+
keyRetriever: this.getCachedKeyRetriever()
|
|
615
|
+
});
|
|
616
|
+
const plaintext = new Uint8Array(headerBytes.byteLength + bitsetBytes.byteLength);
|
|
617
|
+
plaintext.set(headerBytes);
|
|
618
|
+
plaintext.set(bitsetBytes, headerBytes.byteLength);
|
|
619
|
+
return plaintext;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Resolves a column's plaintext metadata, decrypting column-specific metadata when present. */
|
|
623
|
+
private async getColumnMetadata(
|
|
624
|
+
columnChunk: ColumnChunk,
|
|
625
|
+
rowGroupOrdinal: number,
|
|
626
|
+
columnOrdinal: number
|
|
627
|
+
): Promise<NonNullable<ColumnChunk['meta_data']>> {
|
|
628
|
+
if (!columnChunk.encrypted_column_metadata) {
|
|
629
|
+
if (!columnChunk.meta_data) {
|
|
630
|
+
throw new Error('Parquet column metadata is missing');
|
|
631
|
+
}
|
|
632
|
+
return columnChunk.meta_data;
|
|
633
|
+
}
|
|
634
|
+
if (!this.props.keyRetriever) {
|
|
635
|
+
throw new Error('Encrypted Parquet column metadata requires parquet.keyRetriever');
|
|
636
|
+
}
|
|
637
|
+
const encryptionContext = this.encryptionContext;
|
|
638
|
+
if (!encryptionContext) {
|
|
639
|
+
throw new Error('Encrypted Parquet column metadata has no file encryption context');
|
|
640
|
+
}
|
|
641
|
+
const columnKey = columnChunk.crypto_metadata?.ENCRYPTION_WITH_COLUMN_KEY;
|
|
642
|
+
const usesFooterKey = Boolean(columnChunk.crypto_metadata?.ENCRYPTION_WITH_FOOTER_KEY);
|
|
643
|
+
if (!columnKey && !usesFooterKey) {
|
|
644
|
+
throw new Error('Encrypted Parquet column metadata has no key reference');
|
|
645
|
+
}
|
|
646
|
+
const aad = createParquetModuleAad(
|
|
647
|
+
encryptionContext.aadPrefix,
|
|
648
|
+
encryptionContext.fileUnique,
|
|
649
|
+
'column-metadata',
|
|
650
|
+
rowGroupOrdinal,
|
|
651
|
+
columnOrdinal
|
|
652
|
+
);
|
|
653
|
+
const plaintext = await decryptParquetModule(columnChunk.encrypted_column_metadata, {
|
|
654
|
+
algorithm: encryptionContext.algorithm,
|
|
655
|
+
aad,
|
|
656
|
+
keyMetadata:
|
|
657
|
+
columnKey?.key_metadata ??
|
|
658
|
+
encryptionContext.footerKeyMetadata ??
|
|
659
|
+
encryptionContext.footerSigningKeyMetadata,
|
|
660
|
+
keyRetriever: this.getCachedKeyRetriever()
|
|
661
|
+
});
|
|
662
|
+
const metadata = decodeColumnMetadata(plaintext).metadata;
|
|
663
|
+
if (
|
|
664
|
+
columnKey &&
|
|
665
|
+
JSON.stringify(metadata.path_in_schema) !== JSON.stringify(columnKey.path_in_schema)
|
|
666
|
+
) {
|
|
667
|
+
throw new Error('Encrypted Parquet column metadata path does not match crypto metadata');
|
|
668
|
+
}
|
|
221
669
|
return metadata;
|
|
222
670
|
}
|
|
223
671
|
|
|
@@ -226,12 +674,48 @@ export class ParquetReader {
|
|
|
226
674
|
schema: ParquetSchema,
|
|
227
675
|
rowGroup: RowGroup,
|
|
228
676
|
columnList: string[][],
|
|
229
|
-
signal?: AbortSignal
|
|
677
|
+
signal?: AbortSignal,
|
|
678
|
+
rowGroupOrdinal = 0
|
|
230
679
|
): Promise<ParquetRowGroup> {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
680
|
+
await this.resolveColumnMetadata(rowGroup, rowGroupOrdinal, columnList);
|
|
681
|
+
const selectedColumnChunks: Array<{
|
|
682
|
+
columnChunk: ColumnChunk;
|
|
683
|
+
metadata: NonNullable<ColumnChunk['meta_data']>;
|
|
684
|
+
columnOrdinal: number;
|
|
685
|
+
}> = [];
|
|
686
|
+
for (let columnOrdinal = 0; columnOrdinal < rowGroup.columns.length; columnOrdinal++) {
|
|
687
|
+
const columnChunk = rowGroup.columns[columnOrdinal];
|
|
688
|
+
const path = getColumnChunkPath(columnChunk);
|
|
689
|
+
if (columnList.length > 0 && (!path || fieldIndexOf(columnList, path) < 0)) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const physicalColumnOrdinal = getPhysicalColumnOrdinal(columnChunk, columnOrdinal);
|
|
693
|
+
const metadata =
|
|
694
|
+
columnChunk.meta_data ||
|
|
695
|
+
(await this.getColumnMetadata(columnChunk, rowGroupOrdinal, physicalColumnOrdinal));
|
|
696
|
+
if (columnList.length === 0 || fieldIndexOf(columnList, metadata.path_in_schema) >= 0) {
|
|
697
|
+
selectedColumnChunks.push({columnChunk, metadata, columnOrdinal: physicalColumnOrdinal});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (
|
|
701
|
+
this.file.handle instanceof ArrayBuffer &&
|
|
702
|
+
selectedColumnChunks.every(
|
|
703
|
+
({columnChunk, metadata}) =>
|
|
704
|
+
(columnChunk.file_path === undefined || columnChunk.file_path === null) &&
|
|
705
|
+
!columnChunk.crypto_metadata &&
|
|
706
|
+
getThriftEnum(CompressionCodec, metadata.codec) === 'UNCOMPRESSED'
|
|
707
|
+
)
|
|
708
|
+
) {
|
|
709
|
+
return {
|
|
710
|
+
rowCount: Number(rowGroup.num_rows),
|
|
711
|
+
columnData: Object.fromEntries(
|
|
712
|
+
selectedColumnChunks.map(({columnChunk, metadata}) => [
|
|
713
|
+
metadata.path_in_schema.join(),
|
|
714
|
+
this.readUncompressedColumnChunkFromMemory(schema, columnChunk, metadata)
|
|
715
|
+
])
|
|
716
|
+
)
|
|
717
|
+
};
|
|
718
|
+
}
|
|
235
719
|
const columnEntries: Array<readonly [string, ParquetColumnChunk]> = [];
|
|
236
720
|
for (
|
|
237
721
|
let batchStart = 0;
|
|
@@ -244,10 +728,27 @@ export class ParquetReader {
|
|
|
244
728
|
);
|
|
245
729
|
columnEntries.push(
|
|
246
730
|
...(await Promise.all(
|
|
247
|
-
columnBatch.map(
|
|
248
|
-
const columnKey =
|
|
249
|
-
|
|
250
|
-
|
|
731
|
+
columnBatch.map(({columnChunk, metadata, columnOrdinal}) => {
|
|
732
|
+
const columnKey = metadata.path_in_schema.join();
|
|
733
|
+
if (
|
|
734
|
+
this.file.handle instanceof ArrayBuffer &&
|
|
735
|
+
(columnChunk.file_path === undefined || columnChunk.file_path === null) &&
|
|
736
|
+
!columnChunk.crypto_metadata &&
|
|
737
|
+
getThriftEnum(CompressionCodec, metadata.codec) === 'UNCOMPRESSED'
|
|
738
|
+
) {
|
|
739
|
+
return [
|
|
740
|
+
columnKey,
|
|
741
|
+
this.readUncompressedColumnChunkFromMemory(schema, columnChunk, metadata)
|
|
742
|
+
] as const;
|
|
743
|
+
}
|
|
744
|
+
return this.readColumnChunk(
|
|
745
|
+
schema,
|
|
746
|
+
columnChunk,
|
|
747
|
+
signal,
|
|
748
|
+
rowGroupOrdinal,
|
|
749
|
+
columnOrdinal,
|
|
750
|
+
metadata
|
|
751
|
+
).then(columnData => [columnKey, columnData] as const);
|
|
251
752
|
})
|
|
252
753
|
))
|
|
253
754
|
);
|
|
@@ -258,23 +759,40 @@ export class ParquetReader {
|
|
|
258
759
|
};
|
|
259
760
|
}
|
|
260
761
|
|
|
261
|
-
/** Reads independently materializable
|
|
762
|
+
/** Reads independently materializable non-repeated columns for one row range using page indexes. */
|
|
262
763
|
async readRowGroupRange(
|
|
263
764
|
schema: ParquetSchema,
|
|
264
765
|
rowGroup: RowGroup,
|
|
265
766
|
columnList: string[][],
|
|
266
767
|
rowRange: ParquetRowRange,
|
|
267
768
|
pageLocations: ParquetPageLocations,
|
|
268
|
-
signal?: AbortSignal
|
|
769
|
+
signal?: AbortSignal,
|
|
770
|
+
rowGroupOrdinal = 0
|
|
269
771
|
): Promise<ParquetRowGroup> {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
772
|
+
await this.resolveColumnMetadata(rowGroup, rowGroupOrdinal, columnList);
|
|
773
|
+
const selectedColumnChunks: Array<{
|
|
774
|
+
columnChunk: ColumnChunk;
|
|
775
|
+
metadata: NonNullable<ColumnChunk['meta_data']>;
|
|
776
|
+
columnOrdinal: number;
|
|
777
|
+
}> = [];
|
|
778
|
+
for (let columnOrdinal = 0; columnOrdinal < rowGroup.columns.length; columnOrdinal++) {
|
|
779
|
+
const columnChunk = rowGroup.columns[columnOrdinal];
|
|
780
|
+
const path = getColumnChunkPath(columnChunk);
|
|
781
|
+
if (columnList.length > 0 && (!path || fieldIndexOf(columnList, path) < 0)) {
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const physicalColumnOrdinal = getPhysicalColumnOrdinal(columnChunk, columnOrdinal);
|
|
785
|
+
const metadata =
|
|
786
|
+
columnChunk.meta_data ||
|
|
787
|
+
(await this.getColumnMetadata(columnChunk, rowGroupOrdinal, physicalColumnOrdinal));
|
|
788
|
+
if (columnList.length === 0 || fieldIndexOf(columnList, metadata.path_in_schema) >= 0) {
|
|
789
|
+
selectedColumnChunks.push({columnChunk, metadata, columnOrdinal: physicalColumnOrdinal});
|
|
790
|
+
}
|
|
791
|
+
}
|
|
274
792
|
const columnEntries = await Promise.all(
|
|
275
|
-
selectedColumnChunks.map(async columnChunk => {
|
|
276
|
-
const columnKey =
|
|
277
|
-
const pages = pageLocations[
|
|
793
|
+
selectedColumnChunks.map(async ({columnChunk, metadata, columnOrdinal}) => {
|
|
794
|
+
const columnKey = metadata.path_in_schema.join();
|
|
795
|
+
const pages = pageLocations[JSON.stringify(metadata.path_in_schema)];
|
|
278
796
|
if (!pages) {
|
|
279
797
|
throw new Error(`Parquet offset index missing for ${columnKey}`);
|
|
280
798
|
}
|
|
@@ -283,7 +801,10 @@ export class ParquetReader {
|
|
|
283
801
|
columnChunk,
|
|
284
802
|
rowRange,
|
|
285
803
|
pages,
|
|
286
|
-
signal
|
|
804
|
+
signal,
|
|
805
|
+
rowGroupOrdinal,
|
|
806
|
+
columnOrdinal,
|
|
807
|
+
metadata
|
|
287
808
|
);
|
|
288
809
|
return [columnKey, columnData] as const;
|
|
289
810
|
})
|
|
@@ -294,41 +815,118 @@ export class ParquetReader {
|
|
|
294
815
|
};
|
|
295
816
|
}
|
|
296
817
|
|
|
818
|
+
/**
|
|
819
|
+
* Each row group contains column chunks for all the columns.
|
|
820
|
+
*/
|
|
821
|
+
private readUncompressedColumnChunkFromMemory(
|
|
822
|
+
schema: ParquetSchema,
|
|
823
|
+
columnChunk: ColumnChunk,
|
|
824
|
+
metadata: NonNullable<ColumnChunk['meta_data']>
|
|
825
|
+
): ParquetColumnChunk {
|
|
826
|
+
if (!(this.file.handle instanceof ArrayBuffer)) {
|
|
827
|
+
throw new Error('Synchronous Parquet column reads require an in-memory ArrayBuffer');
|
|
828
|
+
}
|
|
829
|
+
const field = schema.findField(metadata.path_in_schema);
|
|
830
|
+
const type: PrimitiveType = getThriftEnum(Type, metadata.type) as PrimitiveType;
|
|
831
|
+
if (type !== field.primitiveType) {
|
|
832
|
+
throw new Error(`chunk type not matching schema: ${type}`);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const pagesOffset = Number(metadata.data_page_offset);
|
|
836
|
+
const dictionaryPageOffset = metadata.dictionary_page_offset;
|
|
837
|
+
const validDictionaryPageOffset =
|
|
838
|
+
dictionaryPageOffset !== undefined && Number(dictionaryPageOffset) > 0
|
|
839
|
+
? Number(dictionaryPageOffset)
|
|
840
|
+
: undefined;
|
|
841
|
+
const chunkOffset = Math.min(pagesOffset, validDictionaryPageOffset ?? pagesOffset);
|
|
842
|
+
const chunkEnd = chunkOffset + Number(metadata.total_compressed_size);
|
|
843
|
+
const chunkSize = Math.min(this.file.size - chunkOffset, Math.max(0, chunkEnd - chunkOffset));
|
|
844
|
+
const chunkBuffer = new Uint8Array(this.file.handle, chunkOffset, chunkSize);
|
|
845
|
+
const pagesRelativeOffset = pagesOffset - chunkOffset;
|
|
846
|
+
const pagesSize = Math.min(
|
|
847
|
+
chunkBuffer.length - pagesRelativeOffset,
|
|
848
|
+
Math.max(0, chunkEnd - pagesOffset)
|
|
849
|
+
);
|
|
850
|
+
const context: ParquetReaderContext = {
|
|
851
|
+
type,
|
|
852
|
+
rLevelMax: field.rLevelMax,
|
|
853
|
+
dLevelMax: field.dLevelMax,
|
|
854
|
+
compression: 'UNCOMPRESSED',
|
|
855
|
+
column: field,
|
|
856
|
+
numValues: metadata.num_values,
|
|
857
|
+
dictionary: [],
|
|
858
|
+
decompressPage: this.getPageDecompressor('UNCOMPRESSED'),
|
|
859
|
+
preserveBinary: this.props.preserveBinary,
|
|
860
|
+
retainByteArrayViews: this.props.retainByteArrayViews,
|
|
861
|
+
useTypedValueBuffers: this.props.useTypedValueBuffers,
|
|
862
|
+
useTypedLevelBuffers: this.props.useTypedLevelBuffers,
|
|
863
|
+
useArrowByteArrayBuffers: this.props.useArrowByteArrayBuffers,
|
|
864
|
+
// Metadata proves whether the decoder may skip its otherwise duplicated page-header scan.
|
|
865
|
+
// Keep the scan for mixed/unknown encodings: it is both a correctness guard and fallback.
|
|
866
|
+
hasOnlyArrowByteArrayDataPages: hasOnlyArrowByteArrayDataPageEncodings(
|
|
867
|
+
metadata.encodings,
|
|
868
|
+
validDictionaryPageOffset
|
|
869
|
+
),
|
|
870
|
+
verifyPageChecksums: this.props.verifyPageChecksums,
|
|
871
|
+
int96AsTimestamp: this.props.int96AsTimestamp
|
|
872
|
+
};
|
|
873
|
+
let dictionary: unknown[] | undefined;
|
|
874
|
+
if (validDictionaryPageOffset !== undefined) {
|
|
875
|
+
const dictionaryRelativeOffset = validDictionaryPageOffset - chunkOffset;
|
|
876
|
+
const dictionarySize = Math.min(
|
|
877
|
+
Math.max(0, pagesOffset - validDictionaryPageOffset),
|
|
878
|
+
chunkBuffer.length - dictionaryRelativeOffset,
|
|
879
|
+
this.props.defaultDictionarySize
|
|
880
|
+
);
|
|
881
|
+
dictionary = decodeUncompressedDictionaryBuffer(
|
|
882
|
+
chunkBuffer.subarray(dictionaryRelativeOffset, dictionaryRelativeOffset + dictionarySize),
|
|
883
|
+
context
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
return decodeUncompressedDataPages(
|
|
887
|
+
chunkBuffer.subarray(pagesRelativeOffset, pagesRelativeOffset + pagesSize),
|
|
888
|
+
{...context, dictionary}
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
297
892
|
/**
|
|
298
893
|
* Each row group contains column chunks for all the columns.
|
|
299
894
|
*/
|
|
300
895
|
async readColumnChunk(
|
|
301
896
|
schema: ParquetSchema,
|
|
302
897
|
colChunk: ColumnChunk,
|
|
303
|
-
signal?: AbortSignal
|
|
898
|
+
signal?: AbortSignal,
|
|
899
|
+
rowGroupOrdinal = 0,
|
|
900
|
+
columnOrdinal = 0,
|
|
901
|
+
resolvedMetadata?: NonNullable<ColumnChunk['meta_data']>
|
|
304
902
|
): Promise<ParquetColumnChunk> {
|
|
305
903
|
if (colChunk.file_path !== undefined && colChunk.file_path !== null) {
|
|
306
904
|
throw new Error('external references are not supported');
|
|
307
905
|
}
|
|
308
906
|
|
|
309
|
-
const
|
|
310
|
-
|
|
907
|
+
const metadata = resolvedMetadata || colChunk.meta_data;
|
|
908
|
+
if (!metadata) {
|
|
909
|
+
throw new Error('Parquet column metadata is missing');
|
|
910
|
+
}
|
|
911
|
+
const field = schema.findField(metadata.path_in_schema);
|
|
912
|
+
const type: PrimitiveType = getThriftEnum(Type, metadata.type) as any;
|
|
311
913
|
|
|
312
914
|
if (type !== field.primitiveType) {
|
|
313
915
|
throw new Error(`chunk type not matching schema: ${type}`);
|
|
314
916
|
}
|
|
315
917
|
|
|
316
|
-
const compression: ParquetCompression = getThriftEnum(
|
|
317
|
-
CompressionCodec,
|
|
318
|
-
colChunk.meta_data?.codec!
|
|
319
|
-
) as any;
|
|
918
|
+
const compression: ParquetCompression = getThriftEnum(CompressionCodec, metadata.codec) as any;
|
|
320
919
|
|
|
321
|
-
const pagesOffset = Number(
|
|
322
|
-
const dictionaryPageOffset =
|
|
920
|
+
const pagesOffset = Number(metadata.data_page_offset);
|
|
921
|
+
const dictionaryPageOffset = metadata.dictionary_page_offset;
|
|
323
922
|
const validDictionaryPageOffset =
|
|
324
923
|
dictionaryPageOffset !== undefined && Number(dictionaryPageOffset) > 0
|
|
325
924
|
? Number(dictionaryPageOffset)
|
|
326
925
|
: undefined;
|
|
327
926
|
const chunkOffset = Math.min(pagesOffset, validDictionaryPageOffset ?? pagesOffset);
|
|
328
|
-
const chunkEnd = chunkOffset + Number(
|
|
927
|
+
const chunkEnd = chunkOffset + Number(metadata.total_compressed_size);
|
|
329
928
|
const chunkSize = Math.min(this.file.size - chunkOffset, Math.max(0, chunkEnd - chunkOffset));
|
|
330
|
-
const
|
|
331
|
-
const chunkBuffer = toUint8Array(arrayBuffer);
|
|
929
|
+
const chunkBuffer = await this.readBytes(chunkOffset, chunkSize, signal);
|
|
332
930
|
const pagesRelativeOffset = pagesOffset - chunkOffset;
|
|
333
931
|
const pagesSize = Math.min(
|
|
334
932
|
chunkBuffer.length - pagesRelativeOffset,
|
|
@@ -341,13 +939,23 @@ export class ParquetReader {
|
|
|
341
939
|
dLevelMax: field.dLevelMax,
|
|
342
940
|
compression,
|
|
343
941
|
column: field,
|
|
344
|
-
numValues:
|
|
942
|
+
numValues: metadata.num_values,
|
|
345
943
|
dictionary: [],
|
|
944
|
+
decompressPage: this.getPageDecompressor(compression),
|
|
346
945
|
// Options - TBD is this the right place for these?
|
|
347
946
|
preserveBinary: this.props.preserveBinary,
|
|
348
947
|
retainByteArrayViews: this.props.retainByteArrayViews,
|
|
349
948
|
useTypedValueBuffers: this.props.useTypedValueBuffers,
|
|
350
|
-
useTypedLevelBuffers: this.props.useTypedLevelBuffers
|
|
949
|
+
useTypedLevelBuffers: this.props.useTypedLevelBuffers,
|
|
950
|
+
useArrowByteArrayBuffers: this.props.useArrowByteArrayBuffers,
|
|
951
|
+
// Avoid parsing every page header twice when trusted column metadata already establishes the
|
|
952
|
+
// direct Arrow byte-array invariant. Unknown/mixed encodings retain the defensive scan.
|
|
953
|
+
hasOnlyArrowByteArrayDataPages: hasOnlyArrowByteArrayDataPageEncodings(
|
|
954
|
+
metadata.encodings,
|
|
955
|
+
validDictionaryPageOffset
|
|
956
|
+
),
|
|
957
|
+
verifyPageChecksums: this.props.verifyPageChecksums,
|
|
958
|
+
int96AsTimestamp: this.props.int96AsTimestamp
|
|
351
959
|
};
|
|
352
960
|
|
|
353
961
|
let dictionary: any[] | undefined;
|
|
@@ -363,30 +971,132 @@ export class ParquetReader {
|
|
|
363
971
|
dictionaryRelativeOffset,
|
|
364
972
|
dictionaryRelativeOffset + dictionarySize
|
|
365
973
|
);
|
|
366
|
-
|
|
974
|
+
const decodedDictionaryBuffer = colChunk.crypto_metadata
|
|
975
|
+
? await this.decryptColumnPages(
|
|
976
|
+
dictionaryBuffer,
|
|
977
|
+
context,
|
|
978
|
+
rowGroupOrdinal,
|
|
979
|
+
columnOrdinal,
|
|
980
|
+
this.getColumnKeyMetadata(colChunk),
|
|
981
|
+
true
|
|
982
|
+
)
|
|
983
|
+
: dictionaryBuffer;
|
|
984
|
+
dictionary = await decodeDictionaryBuffer(decodedDictionaryBuffer, context);
|
|
367
985
|
}
|
|
368
986
|
|
|
369
987
|
dictionary = context.dictionary?.length ? context.dictionary : dictionary;
|
|
370
|
-
const pagesBuf =
|
|
988
|
+
const pagesBuf = colChunk.crypto_metadata
|
|
989
|
+
? await this.decryptColumnPages(
|
|
990
|
+
chunkBuffer.subarray(pagesRelativeOffset, pagesRelativeOffset + pagesSize),
|
|
991
|
+
context,
|
|
992
|
+
rowGroupOrdinal,
|
|
993
|
+
columnOrdinal,
|
|
994
|
+
this.getColumnKeyMetadata(colChunk)
|
|
995
|
+
)
|
|
996
|
+
: chunkBuffer.subarray(pagesRelativeOffset, pagesRelativeOffset + pagesSize);
|
|
371
997
|
return await decodeDataPages(pagesBuf, {...context, dictionary});
|
|
372
998
|
}
|
|
373
999
|
|
|
374
|
-
/**
|
|
1000
|
+
/** Decrypts the length-prefixed page header and data modules in one column chunk. */
|
|
1001
|
+
private async decryptColumnPages(
|
|
1002
|
+
encryptedPages: Uint8Array,
|
|
1003
|
+
context: ParquetReaderContext,
|
|
1004
|
+
rowGroupOrdinal: number,
|
|
1005
|
+
columnOrdinal: number,
|
|
1006
|
+
keyMetadata: Uint8Array | undefined,
|
|
1007
|
+
includesDictionary = false,
|
|
1008
|
+
dataPageOrdinalStart = 0
|
|
1009
|
+
): Promise<Uint8Array> {
|
|
1010
|
+
if (!this.encryptionContext || !this.props.keyRetriever) {
|
|
1011
|
+
throw new Error('Encrypted Parquet pages require parquet.keyRetriever');
|
|
1012
|
+
}
|
|
1013
|
+
const plaintextPages: Uint8Array[] = [];
|
|
1014
|
+
let offset = 0;
|
|
1015
|
+
let dataPageOrdinal = dataPageOrdinalStart;
|
|
1016
|
+
let pageIndex = 0;
|
|
1017
|
+
while (offset < encryptedPages.length) {
|
|
1018
|
+
const headerModule = readParquetEncryptedModule(encryptedPages, offset);
|
|
1019
|
+
offset = headerModule.nextOffset;
|
|
1020
|
+
const dictionaryPage = includesDictionary && pageIndex === 0;
|
|
1021
|
+
const headerModuleType: ParquetEncryptionModule = dictionaryPage
|
|
1022
|
+
? 'dictionary-page-header'
|
|
1023
|
+
: 'data-page-header';
|
|
1024
|
+
const headerAad = createParquetModuleAad(
|
|
1025
|
+
this.encryptionContext.aadPrefix,
|
|
1026
|
+
this.encryptionContext.fileUnique,
|
|
1027
|
+
headerModuleType,
|
|
1028
|
+
rowGroupOrdinal,
|
|
1029
|
+
columnOrdinal,
|
|
1030
|
+
dictionaryPage ? undefined : dataPageOrdinal
|
|
1031
|
+
);
|
|
1032
|
+
const headerBytes = await decryptParquetModule(headerModule.bytes, {
|
|
1033
|
+
algorithm: this.encryptionContext.algorithm,
|
|
1034
|
+
aad: headerAad,
|
|
1035
|
+
keyMetadata,
|
|
1036
|
+
keyRetriever: this.getCachedKeyRetriever()
|
|
1037
|
+
});
|
|
1038
|
+
const {pageHeader} = decodePageHeader(headerBytes);
|
|
1039
|
+
const pageType = getThriftEnum(PageType, pageHeader.type);
|
|
1040
|
+
const dataModule = readParquetEncryptedModule(encryptedPages, offset);
|
|
1041
|
+
offset = dataModule.nextOffset;
|
|
1042
|
+
const dataAad = createParquetModuleAad(
|
|
1043
|
+
this.encryptionContext.aadPrefix,
|
|
1044
|
+
this.encryptionContext.fileUnique,
|
|
1045
|
+
dictionaryPage ? 'dictionary-page' : 'data-page',
|
|
1046
|
+
rowGroupOrdinal,
|
|
1047
|
+
columnOrdinal,
|
|
1048
|
+
dictionaryPage ? undefined : dataPageOrdinal
|
|
1049
|
+
);
|
|
1050
|
+
const dataBytes = await decryptParquetModule(dataModule.bytes, {
|
|
1051
|
+
algorithm: this.encryptionContext.algorithm,
|
|
1052
|
+
aad: dataAad,
|
|
1053
|
+
keyMetadata,
|
|
1054
|
+
keyRetriever: this.getCachedKeyRetriever(),
|
|
1055
|
+
page: true
|
|
1056
|
+
});
|
|
1057
|
+
plaintextPages.push(serializeThrift(pageHeader), dataBytes);
|
|
1058
|
+
if (pageType !== 'DICTIONARY_PAGE') dataPageOrdinal++;
|
|
1059
|
+
pageIndex++;
|
|
1060
|
+
}
|
|
1061
|
+
const plaintextLength = plaintextPages.reduce((total, page) => total + page.length, 0);
|
|
1062
|
+
const plaintext = new Uint8Array(plaintextLength);
|
|
1063
|
+
let plaintextOffset = 0;
|
|
1064
|
+
for (const page of plaintextPages) {
|
|
1065
|
+
plaintext.set(page, plaintextOffset);
|
|
1066
|
+
plaintextOffset += page.length;
|
|
1067
|
+
}
|
|
1068
|
+
return plaintext;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/** Resolves the key metadata reference for an encrypted column. */
|
|
1072
|
+
private getColumnKeyMetadata(columnChunk: ColumnChunk): Uint8Array | undefined {
|
|
1073
|
+
return (
|
|
1074
|
+
columnChunk.crypto_metadata?.ENCRYPTION_WITH_COLUMN_KEY?.key_metadata ??
|
|
1075
|
+
this.encryptionContext?.footerKeyMetadata ??
|
|
1076
|
+
this.encryptionContext?.footerSigningKeyMetadata
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/** Reads and decodes contiguous data pages that begin and end on complete row boundaries. */
|
|
375
1081
|
async readColumnChunkRange(
|
|
376
1082
|
schema: ParquetSchema,
|
|
377
1083
|
columnChunk: ColumnChunk,
|
|
378
1084
|
rowRange: ParquetRowRange,
|
|
379
1085
|
pages: readonly ParquetDataPageLocation[],
|
|
380
|
-
signal?: AbortSignal
|
|
1086
|
+
signal?: AbortSignal,
|
|
1087
|
+
rowGroupOrdinal = 0,
|
|
1088
|
+
columnOrdinal = 0,
|
|
1089
|
+
resolvedMetadata?: NonNullable<ColumnChunk['meta_data']>
|
|
381
1090
|
): Promise<ParquetColumnChunk> {
|
|
382
1091
|
if (columnChunk.file_path !== undefined && columnChunk.file_path !== null) {
|
|
383
1092
|
throw new Error('external references are not supported');
|
|
384
1093
|
}
|
|
385
|
-
const columnMetadata = columnChunk.meta_data
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
throw new Error('Selective Parquet page reads currently require flat primitive columns');
|
|
1094
|
+
const columnMetadata = resolvedMetadata || columnChunk.meta_data;
|
|
1095
|
+
if (!columnMetadata) {
|
|
1096
|
+
throw new Error('Parquet column metadata is missing');
|
|
389
1097
|
}
|
|
1098
|
+
const encrypted = Boolean(columnChunk.crypto_metadata);
|
|
1099
|
+
const field = schema.findField(columnMetadata.path_in_schema);
|
|
390
1100
|
const type: PrimitiveType = getThriftEnum(Type, columnMetadata.type) as any;
|
|
391
1101
|
if (type !== field.primitiveType) {
|
|
392
1102
|
throw new Error(`chunk type not matching schema: ${type}`);
|
|
@@ -401,39 +1111,102 @@ export class ParquetReader {
|
|
|
401
1111
|
if (!overlappingPages.length) {
|
|
402
1112
|
throw new Error('Parquet page plan does not overlap the requested row range');
|
|
403
1113
|
}
|
|
404
|
-
|
|
1114
|
+
let firstPageIndex = pages.indexOf(overlappingPages[0]);
|
|
405
1115
|
const lastPage = overlappingPages[overlappingPages.length - 1];
|
|
1116
|
+
let firstPage = pages[firstPageIndex];
|
|
406
1117
|
const context: ParquetReaderContext = {
|
|
407
1118
|
type,
|
|
408
1119
|
rLevelMax: field.rLevelMax,
|
|
409
1120
|
dLevelMax: field.dLevelMax,
|
|
410
1121
|
compression,
|
|
411
1122
|
column: field,
|
|
412
|
-
|
|
1123
|
+
// Repeated leaves have one level entry per physical value, not one per logical row. Let the
|
|
1124
|
+
// page decoder consume the selected page range and preserve all repetition levels.
|
|
1125
|
+
numValues:
|
|
1126
|
+
field.rLevelMax === 0
|
|
1127
|
+
? new CompactInt64(lastPage.endRowIndex - firstPage.firstRowIndex)
|
|
1128
|
+
: undefined,
|
|
413
1129
|
dictionary: [],
|
|
1130
|
+
decompressPage: this.getPageDecompressor(compression),
|
|
414
1131
|
preserveBinary: this.props.preserveBinary,
|
|
415
1132
|
retainByteArrayViews: this.props.retainByteArrayViews,
|
|
416
|
-
useTypedValueBuffers: this.props.useTypedValueBuffers
|
|
1133
|
+
useTypedValueBuffers: this.props.useTypedValueBuffers,
|
|
1134
|
+
useArrowByteArrayBuffers: this.props.useArrowByteArrayBuffers,
|
|
1135
|
+
verifyPageChecksums: this.props.verifyPageChecksums,
|
|
1136
|
+
int96AsTimestamp: this.props.int96AsTimestamp
|
|
417
1137
|
};
|
|
418
1138
|
|
|
419
1139
|
let dictionary: any[] | undefined;
|
|
420
1140
|
const dictionaryPageOffset = Number(columnMetadata.dictionary_page_offset);
|
|
421
1141
|
if (Number.isSafeInteger(dictionaryPageOffset) && dictionaryPageOffset > 0) {
|
|
422
1142
|
const dictionaryLength = Math.max(0, pages[0].offset - dictionaryPageOffset);
|
|
423
|
-
const dictionaryBuffer =
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
1143
|
+
const dictionaryBuffer = await this.readBytes(dictionaryPageOffset, dictionaryLength, signal);
|
|
1144
|
+
const decodedDictionaryBuffer = encrypted
|
|
1145
|
+
? await this.decryptColumnPages(
|
|
1146
|
+
dictionaryBuffer,
|
|
1147
|
+
context,
|
|
1148
|
+
rowGroupOrdinal,
|
|
1149
|
+
columnOrdinal,
|
|
1150
|
+
this.getColumnKeyMetadata(columnChunk),
|
|
1151
|
+
true
|
|
1152
|
+
)
|
|
1153
|
+
: dictionaryBuffer;
|
|
1154
|
+
dictionary = await decodeDictionaryBuffer(decodedDictionaryBuffer, context);
|
|
427
1155
|
}
|
|
428
1156
|
|
|
1157
|
+
if (field.rLevelMax !== 0) {
|
|
1158
|
+
// Probe predecessor pages independently, then decode the final range once. This avoids
|
|
1159
|
+
// quadratic re-decoding when a large repeated row spans many pages.
|
|
1160
|
+
while (firstPageIndex > 0) {
|
|
1161
|
+
const probeBuffer = await this.readBytes(
|
|
1162
|
+
firstPage.offset,
|
|
1163
|
+
firstPage.compressedByteLength,
|
|
1164
|
+
signal
|
|
1165
|
+
);
|
|
1166
|
+
const decodedProbeBuffer = encrypted
|
|
1167
|
+
? await this.decryptColumnPages(
|
|
1168
|
+
probeBuffer,
|
|
1169
|
+
context,
|
|
1170
|
+
rowGroupOrdinal,
|
|
1171
|
+
columnOrdinal,
|
|
1172
|
+
this.getColumnKeyMetadata(columnChunk),
|
|
1173
|
+
false,
|
|
1174
|
+
firstPageIndex
|
|
1175
|
+
)
|
|
1176
|
+
: probeBuffer;
|
|
1177
|
+
const probe = await decodeDataPages(decodedProbeBuffer, {...context, dictionary});
|
|
1178
|
+
if (probe.rlevels[0] === 0) {
|
|
1179
|
+
break;
|
|
1180
|
+
}
|
|
1181
|
+
firstPage = pages[--firstPageIndex];
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
429
1184
|
const dataLength = lastPage.offset + lastPage.compressedByteLength - firstPage.offset;
|
|
430
|
-
const dataBuffer =
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
1185
|
+
const dataBuffer = await this.readBytes(firstPage.offset, dataLength, signal);
|
|
1186
|
+
const decodedDataBuffer = encrypted
|
|
1187
|
+
? await this.decryptColumnPages(
|
|
1188
|
+
dataBuffer,
|
|
1189
|
+
context,
|
|
1190
|
+
rowGroupOrdinal,
|
|
1191
|
+
columnOrdinal,
|
|
1192
|
+
this.getColumnKeyMetadata(columnChunk),
|
|
1193
|
+
false,
|
|
1194
|
+
firstPageIndex
|
|
1195
|
+
)
|
|
1196
|
+
: dataBuffer;
|
|
1197
|
+
const decoded = await decodeDataPages(decodedDataBuffer, {...context, dictionary});
|
|
1198
|
+
if (field.rLevelMax !== 0) {
|
|
1199
|
+
const rowStart = rowRange.start - firstPage.firstRowIndex;
|
|
1200
|
+
return sliceRepeatedColumnChunk(
|
|
1201
|
+
decoded,
|
|
1202
|
+
Math.max(0, rowStart),
|
|
1203
|
+
rowRange.end - rowRange.start,
|
|
1204
|
+
field.dLevelMax
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
434
1207
|
const relativeStart = rowRange.start - firstPage.firstRowIndex;
|
|
435
1208
|
const relativeEnd = relativeStart + rowRange.end - rowRange.start;
|
|
436
|
-
return
|
|
1209
|
+
return sliceNonRepeatedColumnChunk(decoded, field.dLevelMax, relativeStart, relativeEnd);
|
|
437
1210
|
}
|
|
438
1211
|
|
|
439
1212
|
/**
|
|
@@ -465,16 +1238,59 @@ export class ParquetReader {
|
|
|
465
1238
|
this.file.size - dictionaryPageOffset,
|
|
466
1239
|
this.props.defaultDictionarySize
|
|
467
1240
|
);
|
|
468
|
-
const
|
|
469
|
-
dictionaryPageOffset,
|
|
470
|
-
dictionarySize,
|
|
471
|
-
signal ?? this.props.signal
|
|
472
|
-
);
|
|
473
|
-
const pagesBuf = toUint8Array(arrayBuffer);
|
|
1241
|
+
const pagesBuf = await this.readBytes(dictionaryPageOffset, dictionarySize, signal);
|
|
474
1242
|
return await decodeDictionaryBuffer(pagesBuf, context);
|
|
475
1243
|
}
|
|
476
1244
|
}
|
|
477
1245
|
|
|
1246
|
+
/** Returns a column path available without decrypting column metadata when possible. */
|
|
1247
|
+
function getColumnChunkPath(columnChunk: ColumnChunk): string[] | undefined {
|
|
1248
|
+
return (
|
|
1249
|
+
columnChunk.meta_data?.path_in_schema ??
|
|
1250
|
+
columnChunk.crypto_metadata?.ENCRYPTION_WITH_COLUMN_KEY?.path_in_schema
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/** Builds a stable cache key while preserving module scope for context-dependent key providers. */
|
|
1255
|
+
function createEncryptionKeyCacheKey(
|
|
1256
|
+
algorithm: ParquetEncryptionAlgorithm,
|
|
1257
|
+
keyMetadata: Uint8Array | undefined,
|
|
1258
|
+
aad: Uint8Array,
|
|
1259
|
+
aadPrefixByteLength: number | undefined,
|
|
1260
|
+
fileUniqueByteLength: number | undefined
|
|
1261
|
+
): string {
|
|
1262
|
+
const metadataKey = keyMetadata ? Array.from(keyMetadata).join(',') : 'footer';
|
|
1263
|
+
if (fileUniqueByteLength === undefined) {
|
|
1264
|
+
return `${algorithm}:${metadataKey}:aad:${Array.from(aad).join(',')}`;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const moduleOffset = (aadPrefixByteLength ?? 0) + fileUniqueByteLength;
|
|
1268
|
+
if (moduleOffset + 5 > aad.byteLength) {
|
|
1269
|
+
return `${algorithm}:${metadataKey}:aad:${Array.from(aad).join(',')}`;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const moduleType = aad[moduleOffset];
|
|
1273
|
+
if (moduleType === 0) {
|
|
1274
|
+
return `${algorithm}:${metadataKey}:footer`;
|
|
1275
|
+
}
|
|
1276
|
+
const view = new DataView(
|
|
1277
|
+
aad.buffer,
|
|
1278
|
+
aad.byteOffset + moduleOffset,
|
|
1279
|
+
aad.byteLength - moduleOffset
|
|
1280
|
+
);
|
|
1281
|
+
const rowGroupOrdinal = view.getInt16(1, true);
|
|
1282
|
+
const columnOrdinal = view.getInt16(3, true);
|
|
1283
|
+
return `${algorithm}:${metadataKey}:module:${moduleType}:row-group:${rowGroupOrdinal}:column:${columnOrdinal}`;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/** Returns the original row-group ordinal for a worker-reduced column list. */
|
|
1287
|
+
function getPhysicalColumnOrdinal(columnChunk: ColumnChunk, fallbackOrdinal: number): number {
|
|
1288
|
+
return (
|
|
1289
|
+
(columnChunk as ColumnChunk & {parquetColumnOrdinal?: number}).parquetColumnOrdinal ??
|
|
1290
|
+
fallbackOrdinal
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
478
1294
|
/** Decodes one dictionary page from an already-read column-chunk range. */
|
|
479
1295
|
async function decodeDictionaryBuffer(
|
|
480
1296
|
dictionaryBuffer: Uint8Array,
|
|
@@ -485,8 +1301,8 @@ async function decodeDictionaryBuffer(
|
|
|
485
1301
|
return decodedPage.dictionary!;
|
|
486
1302
|
}
|
|
487
1303
|
|
|
488
|
-
/** Slices one decoded
|
|
489
|
-
function
|
|
1304
|
+
/** Slices one decoded non-repeated column chunk while preserving optional-value alignment. */
|
|
1305
|
+
function sliceNonRepeatedColumnChunk(
|
|
490
1306
|
columnChunk: ParquetColumnChunk,
|
|
491
1307
|
definitionLevelMaximum: number,
|
|
492
1308
|
start: number,
|
|
@@ -504,7 +1320,39 @@ function sliceFlatColumnChunk(
|
|
|
504
1320
|
};
|
|
505
1321
|
}
|
|
506
1322
|
|
|
507
|
-
/**
|
|
1323
|
+
/** Slices a repeated column by logical rows while preserving its level/value alignment. */
|
|
1324
|
+
function sliceRepeatedColumnChunk(
|
|
1325
|
+
columnChunk: ParquetColumnChunk,
|
|
1326
|
+
rowStart: number,
|
|
1327
|
+
rowCount: number,
|
|
1328
|
+
definitionLevelMaximum: number
|
|
1329
|
+
): ParquetColumnChunk {
|
|
1330
|
+
const rowStarts: number[] = [];
|
|
1331
|
+
for (let levelIndex = 0; levelIndex < columnChunk.rlevels.length; levelIndex++) {
|
|
1332
|
+
if (columnChunk.rlevels[levelIndex] === 0) {
|
|
1333
|
+
rowStarts.push(levelIndex);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
const levelStart = rowStarts[rowStart];
|
|
1337
|
+
const endRow = rowStart + rowCount;
|
|
1338
|
+
const levelEnd = rowStarts[endRow] ?? columnChunk.rlevels.length;
|
|
1339
|
+
if (levelStart === undefined || endRow > rowStarts.length || levelEnd < levelStart) {
|
|
1340
|
+
throw new Error('Parquet repeated page range does not contain the requested rows');
|
|
1341
|
+
}
|
|
1342
|
+
const valueStart = countDefinedValues(columnChunk.dlevels, definitionLevelMaximum, 0, levelStart);
|
|
1343
|
+
const valueEnd =
|
|
1344
|
+
valueStart +
|
|
1345
|
+
countDefinedValues(columnChunk.dlevels, definitionLevelMaximum, levelStart, levelEnd);
|
|
1346
|
+
return {
|
|
1347
|
+
rlevels: columnChunk.rlevels.slice(levelStart, levelEnd),
|
|
1348
|
+
dlevels: columnChunk.dlevels.slice(levelStart, levelEnd),
|
|
1349
|
+
values: columnChunk.values.slice(valueStart, valueEnd) as typeof columnChunk.values,
|
|
1350
|
+
count: levelEnd - levelStart,
|
|
1351
|
+
pageHeaders: columnChunk.pageHeaders
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/** Counts defined primitive values represented by one non-repeated level interval. */
|
|
508
1356
|
function countDefinedValues(
|
|
509
1357
|
definitionLevels: ParquetLevelBuffer,
|
|
510
1358
|
definitionLevelMaximum: number,
|