@loaders.gl/parquet 5.0.0-alpha.2 → 5.0.0-alpha.4
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 +15841 -3306
- package/dist/index.cjs.map +3 -3
- package/dist/index.d.ts +8 -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 +16528 -4928
- 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-encoded-page-types.d.ts +108 -0
- package/dist/parquet-encoded-page-types.d.ts.map +1 -0
- package/dist/parquet-encoded-page-types.js +5 -0
- package/dist/parquet-encoded-page-types.js.map +1 -0
- 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 +14121 -2796
- package/dist/parquet-source-loader.cjs.map +3 -3
- package/dist/parquet-source-loader.d.ts +51 -4
- package/dist/parquet-source-loader.d.ts.map +1 -1
- package/dist/parquet-source-loader.js +651 -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 +7378 -6989
- package/dist/parquet-source-worker.js.map +4 -4
- package/dist/parquet-worker-node.cjs +8501 -1045
- 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/encoded-pages.d.ts +24 -0
- package/dist/parquetjs/parser/encoded-pages.d.ts.map +1 -0
- package/dist/parquetjs/parser/encoded-pages.js +206 -0
- package/dist/parquetjs/parser/encoded-pages.js.map +1 -0
- package/dist/parquetjs/parser/parquet-reader.d.ts +88 -12
- package/dist/parquetjs/parser/parquet-reader.d.ts.map +1 -1
- package/dist/parquetjs/parser/parquet-reader.js +713 -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 -13
- package/src/bundled.ts +10 -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 +59 -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-encoded-page-types.ts +122 -0
- 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 +1024 -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/encoded-pages.ts +278 -0
- package/src/parquetjs/parser/parquet-reader.ts +1112 -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
|
@@ -4,15 +4,46 @@
|
|
|
4
4
|
// Copyright (c) 2017 ironSource Ltd.
|
|
5
5
|
// Forked from https://github.com/kbajalc/parquets under MIT license
|
|
6
6
|
import { ParquetSchema } from "../schema/schema.js";
|
|
7
|
-
import { decodeSchema, decodeDataPages, decodePage } from "./decoders.js";
|
|
7
|
+
import { decodeSchema, decodeDataPages, decodePage, decodeUncompressedDataPages, decodeUncompressedDictionaryBuffer } from "./decoders.js";
|
|
8
|
+
import { scanParquetEncodedPages } from "./encoded-pages.js";
|
|
8
9
|
import { materializeRows } from "../schema/shred.js";
|
|
10
|
+
import { createParquetPageDecompressor } from "../compression.js";
|
|
9
11
|
import { PARQUET_MAGIC, PARQUET_MAGIC_ENCRYPTED } from "../../lib/constants.js";
|
|
10
|
-
import { CompressionCodec, Type } from "../parquet-thrift/index.js";
|
|
11
|
-
import { decodeFileMetadata, getThriftEnum, fieldIndexOf } from "../utils/read-utils.js";
|
|
12
|
+
import { CompressionCodec, Encoding, PageType, Type } from "../parquet-thrift/index.js";
|
|
13
|
+
import { decodeColumnMetadata, decodeFileCryptoMetadata, decodeFileMetadata, decodePageHeader, getThriftEnum, fieldIndexOf, serializeThrift } from "../utils/read-utils.js";
|
|
12
14
|
import { decodeString, readUInt32LE, toUint8Array } from "../utils/binary-utils.js";
|
|
13
15
|
import { CompactInt64 } from "../utils/uint8-array-compact-protocol.js";
|
|
16
|
+
import { createParquetModuleAad, decryptParquetModule, readParquetEncryptedModule, verifyParquetFooterSignature } from "../../lib/parquet-encryption.js";
|
|
14
17
|
/** Bounds concurrent range requests when a row group contains unusually many selected columns. */
|
|
15
18
|
const MAXIMUM_CONCURRENT_COLUMN_READS = 16;
|
|
19
|
+
/**
|
|
20
|
+
* Returns whether column metadata guarantees that every data page can decode directly into an
|
|
21
|
+
* Arrow-compatible byte buffer. Level encodings are included in the metadata encoding set and do
|
|
22
|
+
* not prevent the byte-array fast path.
|
|
23
|
+
*/
|
|
24
|
+
function hasOnlyArrowByteArrayDataPageEncodings(encodings, dictionaryPageOffset) {
|
|
25
|
+
if (dictionaryPageOffset !== undefined || encodings.length === 0) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
for (const encoding of encodings) {
|
|
29
|
+
if (encoding !== Encoding.PLAIN &&
|
|
30
|
+
encoding !== Encoding.DELTA_LENGTH_BYTE_ARRAY &&
|
|
31
|
+
encoding !== Encoding.DELTA_BYTE_ARRAY &&
|
|
32
|
+
encoding !== Encoding.RLE &&
|
|
33
|
+
encoding !== Encoding.BIT_PACKED) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
/** Returns the algorithm name represented by the Thrift encryption union. */
|
|
40
|
+
function getEncryptionAlgorithm(value) {
|
|
41
|
+
if (value.AES_GCM_V1)
|
|
42
|
+
return 'AES_GCM_V1';
|
|
43
|
+
if (value.AES_GCM_CTR_V1)
|
|
44
|
+
return 'AES_GCM_CTR_V1';
|
|
45
|
+
throw new Error('Unsupported Parquet encryption algorithm');
|
|
46
|
+
}
|
|
16
47
|
/**
|
|
17
48
|
* The parquet envelope reader allows direct, unbuffered access to the individual
|
|
18
49
|
* sections of the parquet file, namely the header, footer and the row groups.
|
|
@@ -27,21 +58,114 @@ export class ParquetReader {
|
|
|
27
58
|
retainByteArrayViews: false,
|
|
28
59
|
useTypedValueBuffers: false,
|
|
29
60
|
useTypedLevelBuffers: false,
|
|
30
|
-
|
|
61
|
+
useArrowByteArrayBuffers: false,
|
|
62
|
+
verifyPageChecksums: false,
|
|
63
|
+
int96AsTimestamp: false,
|
|
64
|
+
verifyFooterSignature: true,
|
|
65
|
+
signal: undefined,
|
|
66
|
+
keyRetriever: undefined,
|
|
67
|
+
aadPrefix: undefined,
|
|
68
|
+
encryptionContext: undefined
|
|
31
69
|
};
|
|
32
70
|
props;
|
|
33
71
|
file;
|
|
34
72
|
metadata = null;
|
|
35
73
|
/** Parsed Parquet schema shared by metadata, iteration, and materialization paths. */
|
|
36
74
|
schema = null;
|
|
75
|
+
/** Encryption parameters decoded from plaintext or encrypted footer metadata. */
|
|
76
|
+
encryptionContext;
|
|
77
|
+
/** Resolved keys shared by encrypted modules in this reader instance. */
|
|
78
|
+
encryptionKeyCache = new Map();
|
|
79
|
+
/** Compression backends selected once and shared by every page in this reader. */
|
|
80
|
+
pageDecompressors = new Map();
|
|
37
81
|
constructor(file, props) {
|
|
38
82
|
this.file = file;
|
|
39
83
|
this.props = { ...ParquetReader.defaultProps, ...props };
|
|
84
|
+
this.encryptionContext = this.props.encryptionContext;
|
|
85
|
+
}
|
|
86
|
+
/** Whether the decoded footer describes an encrypted Parquet file. */
|
|
87
|
+
get encrypted() {
|
|
88
|
+
return Boolean(this.encryptionContext);
|
|
89
|
+
}
|
|
90
|
+
/** Whether this reader decodes legacy INT96 values as epoch nanoseconds. */
|
|
91
|
+
get int96AsTimestamp() {
|
|
92
|
+
return this.props.int96AsTimestamp;
|
|
93
|
+
}
|
|
94
|
+
/** Returns a cloneable snapshot of the file encryption context for worker decoding. */
|
|
95
|
+
getEncryptionContextForWorker() {
|
|
96
|
+
const encryptionContext = this.encryptionContext;
|
|
97
|
+
if (!encryptionContext)
|
|
98
|
+
return undefined;
|
|
99
|
+
return {
|
|
100
|
+
algorithm: encryptionContext.algorithm,
|
|
101
|
+
aadPrefix: encryptionContext.aadPrefix && new Uint8Array(encryptionContext.aadPrefix),
|
|
102
|
+
fileUnique: new Uint8Array(encryptionContext.fileUnique),
|
|
103
|
+
footerKeyMetadata: encryptionContext.footerKeyMetadata && new Uint8Array(encryptionContext.footerKeyMetadata)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Resolves one column key on the caller thread before transferring encrypted ranges. */
|
|
107
|
+
async getColumnKeyForWorker(columnChunk, rowGroupOrdinal, columnOrdinal) {
|
|
108
|
+
if (!this.encryptionContext || !this.props.keyRetriever) {
|
|
109
|
+
throw new Error('Encrypted Parquet worker reads require parquet.keyRetriever');
|
|
110
|
+
}
|
|
111
|
+
const keyMetadata = this.getColumnKeyMetadata(columnChunk);
|
|
112
|
+
const keyMaterial = await this.props.keyRetriever(keyMetadata, {
|
|
113
|
+
algorithm: this.encryptionContext.algorithm,
|
|
114
|
+
aad: createParquetModuleAad(this.encryptionContext.aadPrefix, this.encryptionContext.fileUnique, 'data-page', rowGroupOrdinal, columnOrdinal, 0)
|
|
115
|
+
});
|
|
116
|
+
const keyBytes = keyMaterial instanceof ArrayBuffer
|
|
117
|
+
? new Uint8Array(keyMaterial)
|
|
118
|
+
: new Uint8Array(keyMaterial.buffer, keyMaterial.byteOffset, keyMaterial.byteLength);
|
|
119
|
+
return { keyMetadata, keyMaterial: keyBytes };
|
|
40
120
|
}
|
|
41
121
|
close() {
|
|
122
|
+
this.encryptionKeyCache.clear();
|
|
123
|
+
this.pageDecompressors.clear();
|
|
42
124
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
43
125
|
this.file.close();
|
|
44
126
|
}
|
|
127
|
+
/** Returns the reader-scoped decoder for one Parquet compression method. */
|
|
128
|
+
getPageDecompressor(compression) {
|
|
129
|
+
let decompressor = this.pageDecompressors.get(compression);
|
|
130
|
+
if (!decompressor) {
|
|
131
|
+
decompressor = createParquetPageDecompressor(compression);
|
|
132
|
+
this.pageDecompressors.set(compression, decompressor);
|
|
133
|
+
}
|
|
134
|
+
return decompressor;
|
|
135
|
+
}
|
|
136
|
+
/** Reads a byte range, retaining a zero-copy view when the complete file is already in memory. */
|
|
137
|
+
async readBytes(start, length, signal) {
|
|
138
|
+
const effectiveSignal = signal ?? this.props.signal;
|
|
139
|
+
if (effectiveSignal?.aborted) {
|
|
140
|
+
throw new Error('Request aborted');
|
|
141
|
+
}
|
|
142
|
+
if (this.file.handle instanceof ArrayBuffer) {
|
|
143
|
+
return new Uint8Array(this.file.handle, start, length);
|
|
144
|
+
}
|
|
145
|
+
return toUint8Array(await this.file.read(start, length, effectiveSignal));
|
|
146
|
+
}
|
|
147
|
+
/** Returns a reader-scoped key resolver that avoids repeating key-provider work per module. */
|
|
148
|
+
getCachedKeyRetriever() {
|
|
149
|
+
const keyRetriever = this.props.keyRetriever;
|
|
150
|
+
if (!keyRetriever) {
|
|
151
|
+
throw new Error('Encrypted Parquet requires parquet.keyRetriever');
|
|
152
|
+
}
|
|
153
|
+
return (keyMetadata, context) => {
|
|
154
|
+
const encryptionContext = this.encryptionContext;
|
|
155
|
+
const cacheKey = createEncryptionKeyCacheKey(context.algorithm, keyMetadata, context.aad, encryptionContext?.aadPrefix?.byteLength ?? this.props.aadPrefix?.byteLength, encryptionContext?.fileUnique.byteLength);
|
|
156
|
+
let keyPromise = this.encryptionKeyCache.get(cacheKey);
|
|
157
|
+
if (!keyPromise) {
|
|
158
|
+
keyPromise = Promise.resolve().then(() => keyRetriever(keyMetadata, context));
|
|
159
|
+
this.encryptionKeyCache.set(cacheKey, keyPromise);
|
|
160
|
+
void keyPromise.catch(() => {
|
|
161
|
+
if (this.encryptionKeyCache.get(cacheKey) === keyPromise) {
|
|
162
|
+
this.encryptionKeyCache.delete(cacheKey);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return keyPromise;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
45
169
|
// HIGH LEVEL METHODS
|
|
46
170
|
/** Yield one row at a time */
|
|
47
171
|
async *rowIterator(props) {
|
|
@@ -71,7 +195,7 @@ export class ParquetReader {
|
|
|
71
195
|
if (!Number.isInteger(rowGroupIndex) || rowGroupIndex < 0 || rowGroupIndex >= rowGroupCount) {
|
|
72
196
|
throw new Error(`Invalid Parquet row-group index ${rowGroupIndex}`);
|
|
73
197
|
}
|
|
74
|
-
const rowGroup = await this.readRowGroup(schema, metadata.row_groups[rowGroupIndex], columnList, props?.signal);
|
|
198
|
+
const rowGroup = await this.readRowGroup(schema, metadata.row_groups[rowGroupIndex], columnList, props?.signal, rowGroupIndex);
|
|
75
199
|
yield rowGroup;
|
|
76
200
|
}
|
|
77
201
|
}
|
|
@@ -109,13 +233,12 @@ export class ParquetReader {
|
|
|
109
233
|
// LOW LEVEL METHODS
|
|
110
234
|
/** Metadata is stored in the footer */
|
|
111
235
|
async readHeader(signal) {
|
|
112
|
-
const
|
|
113
|
-
const magic = decodeString(toUint8Array(arrayBuffer));
|
|
236
|
+
const magic = decodeString(await this.readBytes(0, PARQUET_MAGIC.length, signal));
|
|
114
237
|
switch (magic) {
|
|
115
238
|
case PARQUET_MAGIC:
|
|
116
239
|
break;
|
|
117
240
|
case PARQUET_MAGIC_ENCRYPTED:
|
|
118
|
-
|
|
241
|
+
break;
|
|
119
242
|
default:
|
|
120
243
|
throw new Error(`Invalid parquet file (magic=${magic})`);
|
|
121
244
|
}
|
|
@@ -123,37 +246,234 @@ export class ParquetReader {
|
|
|
123
246
|
/** Metadata is stored in the footer */
|
|
124
247
|
async readFooter(signal) {
|
|
125
248
|
const trailerLen = PARQUET_MAGIC.length + 4;
|
|
126
|
-
const
|
|
127
|
-
const trailer = toUint8Array(arrayBuffer);
|
|
249
|
+
const trailer = await this.readBytes(this.file.size - trailerLen, trailerLen, signal);
|
|
128
250
|
const magic = decodeString(trailer, 4);
|
|
251
|
+
const metadataSize = readUInt32LE(trailer, 0);
|
|
252
|
+
const metadataOffset = this.file.size - metadataSize - trailerLen;
|
|
253
|
+
if (magic === PARQUET_MAGIC_ENCRYPTED) {
|
|
254
|
+
return await this.readEncryptedFooter(metadataSize, metadataOffset, signal);
|
|
255
|
+
}
|
|
129
256
|
if (magic !== PARQUET_MAGIC) {
|
|
130
257
|
throw new Error(`Not a valid parquet file (magic="${magic})`);
|
|
131
258
|
}
|
|
132
|
-
const metadataSize = readUInt32LE(trailer, 0);
|
|
133
|
-
const metadataOffset = this.file.size - metadataSize - trailerLen;
|
|
134
259
|
if (metadataOffset < PARQUET_MAGIC.length) {
|
|
135
260
|
throw new Error(`Invalid metadata size ${metadataOffset}`);
|
|
136
261
|
}
|
|
137
|
-
const
|
|
138
|
-
const metadataBuf = toUint8Array(arrayBuffer2);
|
|
262
|
+
const metadataBuf = await this.readBytes(metadataOffset, metadataSize, signal);
|
|
139
263
|
// let metadata = new parquet_thrift.FileMetaData();
|
|
140
264
|
// parquet_util.decodeThrift(metadata, metadataBuf);
|
|
141
|
-
const
|
|
265
|
+
const decoded = decodeFileMetadata(metadataBuf);
|
|
266
|
+
const { metadata } = decoded;
|
|
267
|
+
this.setEncryptionContext(metadata);
|
|
268
|
+
if (this.props.verifyFooterSignature && this.encryptionContext?.footerSigningKeyMetadata) {
|
|
269
|
+
// The compact-protocol decoder may consume bytes from the signature that
|
|
270
|
+
// happen to look like valid Thrift fields. The format defines the
|
|
271
|
+
// signature as the final fixed 28 bytes of the footer range, so use the
|
|
272
|
+
// physical boundary rather than the decoder's consumed length.
|
|
273
|
+
const signatureOffset = metadataBuf.byteLength - 28;
|
|
274
|
+
if (signatureOffset < decoded.length) {
|
|
275
|
+
throw new Error('Invalid Parquet plaintext-footer signature boundary');
|
|
276
|
+
}
|
|
277
|
+
const signature = metadataBuf.subarray(signatureOffset);
|
|
278
|
+
const encryptionContext = this.encryptionContext;
|
|
279
|
+
if (!this.props.keyRetriever) {
|
|
280
|
+
throw new Error('Signed Parquet footer requires parquet.keyRetriever');
|
|
281
|
+
}
|
|
282
|
+
await verifyParquetFooterSignature(metadataBuf.subarray(0, signatureOffset), signature, {
|
|
283
|
+
algorithm: encryptionContext.algorithm,
|
|
284
|
+
aad: createParquetModuleAad(encryptionContext.aadPrefix, encryptionContext.fileUnique, 'footer'),
|
|
285
|
+
keyMetadata: encryptionContext.footerSigningKeyMetadata,
|
|
286
|
+
keyRetriever: this.getCachedKeyRetriever()
|
|
287
|
+
});
|
|
288
|
+
}
|
|
142
289
|
return metadata;
|
|
143
290
|
}
|
|
144
|
-
/**
|
|
145
|
-
async
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
291
|
+
/** Reads and decrypts an encrypted footer after the standard trailer. */
|
|
292
|
+
async readEncryptedFooter(metadataSize, metadataOffset, signal) {
|
|
293
|
+
if (!this.props.keyRetriever) {
|
|
294
|
+
throw new Error('Encrypted Parquet footer requires parquet.keyRetriever');
|
|
295
|
+
}
|
|
296
|
+
const encryptedFooter = await this.readBytes(metadataOffset, metadataSize, signal);
|
|
297
|
+
const cryptoMetadata = decodeFileCryptoMetadata(encryptedFooter);
|
|
298
|
+
const algorithm = getEncryptionAlgorithm(cryptoMetadata.metadata.encryption_algorithm);
|
|
299
|
+
const algorithmMetadata = cryptoMetadata.metadata.encryption_algorithm.AES_GCM_V1 ||
|
|
300
|
+
cryptoMetadata.metadata.encryption_algorithm.AES_GCM_CTR_V1;
|
|
301
|
+
const aadPrefix = algorithmMetadata?.aad_prefix || this.props.aadPrefix;
|
|
302
|
+
if (!algorithmMetadata?.aad_file_unique) {
|
|
303
|
+
throw new Error('Encrypted Parquet footer is missing aad_file_unique');
|
|
304
|
+
}
|
|
305
|
+
const aad = createParquetModuleAad(aadPrefix, algorithmMetadata.aad_file_unique, 'footer');
|
|
306
|
+
const plaintext = await decryptParquetModule(encryptedFooter.subarray(cryptoMetadata.length), {
|
|
307
|
+
algorithm,
|
|
308
|
+
aad,
|
|
309
|
+
keyMetadata: cryptoMetadata.metadata.key_metadata,
|
|
310
|
+
keyRetriever: this.getCachedKeyRetriever()
|
|
149
311
|
});
|
|
312
|
+
this.encryptionContext = {
|
|
313
|
+
algorithm,
|
|
314
|
+
aadPrefix,
|
|
315
|
+
fileUnique: algorithmMetadata.aad_file_unique,
|
|
316
|
+
footerKeyMetadata: cryptoMetadata.metadata.key_metadata
|
|
317
|
+
};
|
|
318
|
+
const metadata = decodeFileMetadata(plaintext).metadata;
|
|
319
|
+
return metadata;
|
|
320
|
+
}
|
|
321
|
+
/** Records the encryption parameters exposed by plaintext-footer files. */
|
|
322
|
+
setEncryptionContext(metadata) {
|
|
323
|
+
const encryptionAlgorithm = metadata.encryption_algorithm;
|
|
324
|
+
if (!encryptionAlgorithm)
|
|
325
|
+
return;
|
|
326
|
+
const algorithm = getEncryptionAlgorithm(encryptionAlgorithm);
|
|
327
|
+
const algorithmMetadata = encryptionAlgorithm.AES_GCM_V1 || encryptionAlgorithm.AES_GCM_CTR_V1;
|
|
328
|
+
if (!algorithmMetadata?.aad_file_unique) {
|
|
329
|
+
throw new Error('Encrypted Parquet footer is missing aad_file_unique');
|
|
330
|
+
}
|
|
331
|
+
this.encryptionContext = {
|
|
332
|
+
algorithm,
|
|
333
|
+
aadPrefix: algorithmMetadata.aad_prefix || this.props.aadPrefix,
|
|
334
|
+
fileUnique: algorithmMetadata.aad_file_unique,
|
|
335
|
+
footerSigningKeyMetadata: metadata.footer_signing_key_metadata
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
/** Resolves only the selected encrypted columns, using crypto metadata paths before decryption. */
|
|
339
|
+
async resolveColumnMetadata(rowGroup, rowGroupOrdinal, selectedColumnPaths) {
|
|
340
|
+
for (let columnOrdinal = 0; columnOrdinal < rowGroup.columns.length; columnOrdinal++) {
|
|
341
|
+
const columnChunk = rowGroup.columns[columnOrdinal];
|
|
342
|
+
const path = getColumnChunkPath(columnChunk);
|
|
343
|
+
if (selectedColumnPaths &&
|
|
344
|
+
path &&
|
|
345
|
+
!selectedColumnPaths.some(selectedPath => fieldIndexOf([Array.from(selectedPath)], path) >= 0)) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (columnChunk.encrypted_column_metadata && !columnChunk.meta_data) {
|
|
349
|
+
columnChunk.meta_data = await this.getColumnMetadata(columnChunk, rowGroupOrdinal, getPhysicalColumnOrdinal(columnChunk, columnOrdinal));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/** Decrypts one encrypted column index or offset index module for selective planning. */
|
|
354
|
+
async decryptIndexModule(bytes, module, rowGroupOrdinal, columnOrdinal, columnChunk) {
|
|
355
|
+
if (!columnChunk.crypto_metadata)
|
|
356
|
+
return bytes;
|
|
357
|
+
const encryptionContext = this.encryptionContext;
|
|
358
|
+
if (!encryptionContext || !this.props.keyRetriever) {
|
|
359
|
+
throw new Error('Encrypted Parquet indexes require parquet.keyRetriever');
|
|
360
|
+
}
|
|
361
|
+
const aad = createParquetModuleAad(encryptionContext.aadPrefix, encryptionContext.fileUnique, module, rowGroupOrdinal, columnOrdinal);
|
|
362
|
+
return await decryptParquetModule(bytes, {
|
|
363
|
+
algorithm: encryptionContext.algorithm,
|
|
364
|
+
aad,
|
|
365
|
+
keyMetadata: this.getColumnKeyMetadata(columnChunk),
|
|
366
|
+
keyRetriever: this.getCachedKeyRetriever()
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
/** Decrypts the encrypted Bloom-filter header and bitset modules for one column chunk. */
|
|
370
|
+
async decryptBloomFilter(bytes, rowGroupOrdinal, columnOrdinal, columnChunk) {
|
|
371
|
+
if (!columnChunk.crypto_metadata)
|
|
372
|
+
return bytes;
|
|
373
|
+
const encryptionContext = this.encryptionContext;
|
|
374
|
+
if (!encryptionContext || !this.props.keyRetriever) {
|
|
375
|
+
throw new Error('Encrypted Parquet Bloom filters require parquet.keyRetriever');
|
|
376
|
+
}
|
|
377
|
+
const header = readParquetEncryptedModule(bytes);
|
|
378
|
+
const bitset = readParquetEncryptedModule(bytes, header.nextOffset);
|
|
379
|
+
const keyMetadata = this.getColumnKeyMetadata(columnChunk);
|
|
380
|
+
const headerBytes = await decryptParquetModule(header.bytes, {
|
|
381
|
+
algorithm: encryptionContext.algorithm,
|
|
382
|
+
aad: createParquetModuleAad(encryptionContext.aadPrefix, encryptionContext.fileUnique, 'bloom-filter-header', rowGroupOrdinal, columnOrdinal),
|
|
383
|
+
keyMetadata,
|
|
384
|
+
keyRetriever: this.getCachedKeyRetriever()
|
|
385
|
+
});
|
|
386
|
+
const bitsetBytes = await decryptParquetModule(bitset.bytes, {
|
|
387
|
+
algorithm: encryptionContext.algorithm,
|
|
388
|
+
aad: createParquetModuleAad(encryptionContext.aadPrefix, encryptionContext.fileUnique, 'bloom-filter-bitset', rowGroupOrdinal, columnOrdinal),
|
|
389
|
+
keyMetadata,
|
|
390
|
+
keyRetriever: this.getCachedKeyRetriever()
|
|
391
|
+
});
|
|
392
|
+
const plaintext = new Uint8Array(headerBytes.byteLength + bitsetBytes.byteLength);
|
|
393
|
+
plaintext.set(headerBytes);
|
|
394
|
+
plaintext.set(bitsetBytes, headerBytes.byteLength);
|
|
395
|
+
return plaintext;
|
|
396
|
+
}
|
|
397
|
+
/** Resolves a column's plaintext metadata, decrypting column-specific metadata when present. */
|
|
398
|
+
async getColumnMetadata(columnChunk, rowGroupOrdinal, columnOrdinal) {
|
|
399
|
+
if (!columnChunk.encrypted_column_metadata) {
|
|
400
|
+
if (!columnChunk.meta_data) {
|
|
401
|
+
throw new Error('Parquet column metadata is missing');
|
|
402
|
+
}
|
|
403
|
+
return columnChunk.meta_data;
|
|
404
|
+
}
|
|
405
|
+
if (!this.props.keyRetriever) {
|
|
406
|
+
throw new Error('Encrypted Parquet column metadata requires parquet.keyRetriever');
|
|
407
|
+
}
|
|
408
|
+
const encryptionContext = this.encryptionContext;
|
|
409
|
+
if (!encryptionContext) {
|
|
410
|
+
throw new Error('Encrypted Parquet column metadata has no file encryption context');
|
|
411
|
+
}
|
|
412
|
+
const columnKey = columnChunk.crypto_metadata?.ENCRYPTION_WITH_COLUMN_KEY;
|
|
413
|
+
const usesFooterKey = Boolean(columnChunk.crypto_metadata?.ENCRYPTION_WITH_FOOTER_KEY);
|
|
414
|
+
if (!columnKey && !usesFooterKey) {
|
|
415
|
+
throw new Error('Encrypted Parquet column metadata has no key reference');
|
|
416
|
+
}
|
|
417
|
+
const aad = createParquetModuleAad(encryptionContext.aadPrefix, encryptionContext.fileUnique, 'column-metadata', rowGroupOrdinal, columnOrdinal);
|
|
418
|
+
const plaintext = await decryptParquetModule(columnChunk.encrypted_column_metadata, {
|
|
419
|
+
algorithm: encryptionContext.algorithm,
|
|
420
|
+
aad,
|
|
421
|
+
keyMetadata: columnKey?.key_metadata ??
|
|
422
|
+
encryptionContext.footerKeyMetadata ??
|
|
423
|
+
encryptionContext.footerSigningKeyMetadata,
|
|
424
|
+
keyRetriever: this.getCachedKeyRetriever()
|
|
425
|
+
});
|
|
426
|
+
const metadata = decodeColumnMetadata(plaintext).metadata;
|
|
427
|
+
if (columnKey &&
|
|
428
|
+
JSON.stringify(metadata.path_in_schema) !== JSON.stringify(columnKey.path_in_schema)) {
|
|
429
|
+
throw new Error('Encrypted Parquet column metadata path does not match crypto metadata');
|
|
430
|
+
}
|
|
431
|
+
return metadata;
|
|
432
|
+
}
|
|
433
|
+
/** Data is stored in row groups (similar to Apache Arrow record batches) */
|
|
434
|
+
async readRowGroup(schema, rowGroup, columnList, signal, rowGroupOrdinal = 0) {
|
|
435
|
+
await this.resolveColumnMetadata(rowGroup, rowGroupOrdinal, columnList);
|
|
436
|
+
const selectedColumnChunks = [];
|
|
437
|
+
for (let columnOrdinal = 0; columnOrdinal < rowGroup.columns.length; columnOrdinal++) {
|
|
438
|
+
const columnChunk = rowGroup.columns[columnOrdinal];
|
|
439
|
+
const path = getColumnChunkPath(columnChunk);
|
|
440
|
+
if (columnList.length > 0 && (!path || fieldIndexOf(columnList, path) < 0)) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const physicalColumnOrdinal = getPhysicalColumnOrdinal(columnChunk, columnOrdinal);
|
|
444
|
+
const metadata = columnChunk.meta_data ||
|
|
445
|
+
(await this.getColumnMetadata(columnChunk, rowGroupOrdinal, physicalColumnOrdinal));
|
|
446
|
+
if (columnList.length === 0 || fieldIndexOf(columnList, metadata.path_in_schema) >= 0) {
|
|
447
|
+
selectedColumnChunks.push({ columnChunk, metadata, columnOrdinal: physicalColumnOrdinal });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (this.file.handle instanceof ArrayBuffer &&
|
|
451
|
+
selectedColumnChunks.every(({ columnChunk, metadata }) => (columnChunk.file_path === undefined || columnChunk.file_path === null) &&
|
|
452
|
+
!columnChunk.crypto_metadata &&
|
|
453
|
+
getThriftEnum(CompressionCodec, metadata.codec) === 'UNCOMPRESSED')) {
|
|
454
|
+
return {
|
|
455
|
+
rowCount: Number(rowGroup.num_rows),
|
|
456
|
+
columnData: Object.fromEntries(selectedColumnChunks.map(({ columnChunk, metadata }) => [
|
|
457
|
+
metadata.path_in_schema.join(),
|
|
458
|
+
this.readUncompressedColumnChunkFromMemory(schema, columnChunk, metadata)
|
|
459
|
+
]))
|
|
460
|
+
};
|
|
461
|
+
}
|
|
150
462
|
const columnEntries = [];
|
|
151
463
|
for (let batchStart = 0; batchStart < selectedColumnChunks.length; batchStart += MAXIMUM_CONCURRENT_COLUMN_READS) {
|
|
152
464
|
const columnBatch = selectedColumnChunks.slice(batchStart, batchStart + MAXIMUM_CONCURRENT_COLUMN_READS);
|
|
153
|
-
columnEntries.push(...(await Promise.all(columnBatch.map(
|
|
154
|
-
const columnKey =
|
|
155
|
-
|
|
156
|
-
|
|
465
|
+
columnEntries.push(...(await Promise.all(columnBatch.map(({ columnChunk, metadata, columnOrdinal }) => {
|
|
466
|
+
const columnKey = metadata.path_in_schema.join();
|
|
467
|
+
if (this.file.handle instanceof ArrayBuffer &&
|
|
468
|
+
(columnChunk.file_path === undefined || columnChunk.file_path === null) &&
|
|
469
|
+
!columnChunk.crypto_metadata &&
|
|
470
|
+
getThriftEnum(CompressionCodec, metadata.codec) === 'UNCOMPRESSED') {
|
|
471
|
+
return [
|
|
472
|
+
columnKey,
|
|
473
|
+
this.readUncompressedColumnChunkFromMemory(schema, columnChunk, metadata)
|
|
474
|
+
];
|
|
475
|
+
}
|
|
476
|
+
return this.readColumnChunk(schema, columnChunk, signal, rowGroupOrdinal, columnOrdinal, metadata).then(columnData => [columnKey, columnData]);
|
|
157
477
|
}))));
|
|
158
478
|
}
|
|
159
479
|
return {
|
|
@@ -161,19 +481,66 @@ export class ParquetReader {
|
|
|
161
481
|
columnData: Object.fromEntries(columnEntries)
|
|
162
482
|
};
|
|
163
483
|
}
|
|
164
|
-
/**
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
484
|
+
/**
|
|
485
|
+
* Reads selected column chunks as encoded page descriptors without materializing values.
|
|
486
|
+
*
|
|
487
|
+
* Page headers, encryption, checksums, and optional decompression remain reader concerns. Value,
|
|
488
|
+
* dictionary, and level decoding is deliberately left to the downstream consumer.
|
|
489
|
+
*/
|
|
490
|
+
async readRowGroupEncodedPages(schema, rowGroup, columnList, preserveCompression, signal, rowGroupOrdinal = 0) {
|
|
491
|
+
await this.resolveColumnMetadata(rowGroup, rowGroupOrdinal, columnList);
|
|
492
|
+
const selectedColumnChunks = [];
|
|
493
|
+
for (let columnOrdinal = 0; columnOrdinal < rowGroup.columns.length; columnOrdinal++) {
|
|
494
|
+
const columnChunk = rowGroup.columns[columnOrdinal];
|
|
495
|
+
const path = getColumnChunkPath(columnChunk);
|
|
496
|
+
if (columnList.length > 0 && (!path || fieldIndexOf(columnList, path) < 0))
|
|
497
|
+
continue;
|
|
498
|
+
const physicalColumnOrdinal = getPhysicalColumnOrdinal(columnChunk, columnOrdinal);
|
|
499
|
+
const metadata = columnChunk.meta_data ||
|
|
500
|
+
(await this.getColumnMetadata(columnChunk, rowGroupOrdinal, physicalColumnOrdinal));
|
|
501
|
+
if (columnList.length === 0 || fieldIndexOf(columnList, metadata.path_in_schema) >= 0) {
|
|
502
|
+
selectedColumnChunks.push({
|
|
503
|
+
columnChunk,
|
|
504
|
+
metadata,
|
|
505
|
+
columnOrdinal: physicalColumnOrdinal
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const columns = [];
|
|
510
|
+
for (let batchStart = 0; batchStart < selectedColumnChunks.length; batchStart += MAXIMUM_CONCURRENT_COLUMN_READS) {
|
|
511
|
+
columns.push(...(await Promise.all(selectedColumnChunks
|
|
512
|
+
.slice(batchStart, batchStart + MAXIMUM_CONCURRENT_COLUMN_READS)
|
|
513
|
+
.map(async ({ columnChunk, metadata, columnOrdinal }) => {
|
|
514
|
+
const encodedColumn = await this.readColumnChunkEncodedPages(schema, columnChunk, preserveCompression, signal, rowGroupOrdinal, columnOrdinal, metadata);
|
|
515
|
+
return encodedColumn;
|
|
516
|
+
}))));
|
|
517
|
+
}
|
|
518
|
+
return Object.freeze(columns);
|
|
519
|
+
}
|
|
520
|
+
/** Reads independently materializable non-repeated columns for one row range using page indexes. */
|
|
521
|
+
async readRowGroupRange(schema, rowGroup, columnList, rowRange, pageLocations, signal, rowGroupOrdinal = 0) {
|
|
522
|
+
await this.resolveColumnMetadata(rowGroup, rowGroupOrdinal, columnList);
|
|
523
|
+
const selectedColumnChunks = [];
|
|
524
|
+
for (let columnOrdinal = 0; columnOrdinal < rowGroup.columns.length; columnOrdinal++) {
|
|
525
|
+
const columnChunk = rowGroup.columns[columnOrdinal];
|
|
526
|
+
const path = getColumnChunkPath(columnChunk);
|
|
527
|
+
if (columnList.length > 0 && (!path || fieldIndexOf(columnList, path) < 0)) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const physicalColumnOrdinal = getPhysicalColumnOrdinal(columnChunk, columnOrdinal);
|
|
531
|
+
const metadata = columnChunk.meta_data ||
|
|
532
|
+
(await this.getColumnMetadata(columnChunk, rowGroupOrdinal, physicalColumnOrdinal));
|
|
533
|
+
if (columnList.length === 0 || fieldIndexOf(columnList, metadata.path_in_schema) >= 0) {
|
|
534
|
+
selectedColumnChunks.push({ columnChunk, metadata, columnOrdinal: physicalColumnOrdinal });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const columnEntries = await Promise.all(selectedColumnChunks.map(async ({ columnChunk, metadata, columnOrdinal }) => {
|
|
538
|
+
const columnKey = metadata.path_in_schema.join();
|
|
539
|
+
const pages = pageLocations[JSON.stringify(metadata.path_in_schema)];
|
|
173
540
|
if (!pages) {
|
|
174
541
|
throw new Error(`Parquet offset index missing for ${columnKey}`);
|
|
175
542
|
}
|
|
176
|
-
const columnData = await this.readColumnChunkRange(schema, columnChunk, rowRange, pages, signal);
|
|
543
|
+
const columnData = await this.readColumnChunkRange(schema, columnChunk, rowRange, pages, signal, rowGroupOrdinal, columnOrdinal, metadata);
|
|
177
544
|
return [columnKey, columnData];
|
|
178
545
|
}));
|
|
179
546
|
return {
|
|
@@ -184,26 +551,80 @@ export class ParquetReader {
|
|
|
184
551
|
/**
|
|
185
552
|
* Each row group contains column chunks for all the columns.
|
|
186
553
|
*/
|
|
187
|
-
|
|
554
|
+
readUncompressedColumnChunkFromMemory(schema, columnChunk, metadata) {
|
|
555
|
+
if (!(this.file.handle instanceof ArrayBuffer)) {
|
|
556
|
+
throw new Error('Synchronous Parquet column reads require an in-memory ArrayBuffer');
|
|
557
|
+
}
|
|
558
|
+
const field = schema.findField(metadata.path_in_schema);
|
|
559
|
+
const type = getThriftEnum(Type, metadata.type);
|
|
560
|
+
if (type !== field.primitiveType) {
|
|
561
|
+
throw new Error(`chunk type not matching schema: ${type}`);
|
|
562
|
+
}
|
|
563
|
+
const pagesOffset = Number(metadata.data_page_offset);
|
|
564
|
+
const dictionaryPageOffset = metadata.dictionary_page_offset;
|
|
565
|
+
const validDictionaryPageOffset = dictionaryPageOffset !== undefined && Number(dictionaryPageOffset) > 0
|
|
566
|
+
? Number(dictionaryPageOffset)
|
|
567
|
+
: undefined;
|
|
568
|
+
const chunkOffset = Math.min(pagesOffset, validDictionaryPageOffset ?? pagesOffset);
|
|
569
|
+
const chunkEnd = chunkOffset + Number(metadata.total_compressed_size);
|
|
570
|
+
const chunkSize = Math.min(this.file.size - chunkOffset, Math.max(0, chunkEnd - chunkOffset));
|
|
571
|
+
const chunkBuffer = new Uint8Array(this.file.handle, chunkOffset, chunkSize);
|
|
572
|
+
const pagesRelativeOffset = pagesOffset - chunkOffset;
|
|
573
|
+
const pagesSize = Math.min(chunkBuffer.length - pagesRelativeOffset, Math.max(0, chunkEnd - pagesOffset));
|
|
574
|
+
const context = {
|
|
575
|
+
type,
|
|
576
|
+
rLevelMax: field.rLevelMax,
|
|
577
|
+
dLevelMax: field.dLevelMax,
|
|
578
|
+
compression: 'UNCOMPRESSED',
|
|
579
|
+
column: field,
|
|
580
|
+
numValues: metadata.num_values,
|
|
581
|
+
dictionary: [],
|
|
582
|
+
decompressPage: this.getPageDecompressor('UNCOMPRESSED'),
|
|
583
|
+
preserveBinary: this.props.preserveBinary,
|
|
584
|
+
retainByteArrayViews: this.props.retainByteArrayViews,
|
|
585
|
+
useTypedValueBuffers: this.props.useTypedValueBuffers,
|
|
586
|
+
useTypedLevelBuffers: this.props.useTypedLevelBuffers,
|
|
587
|
+
useArrowByteArrayBuffers: this.props.useArrowByteArrayBuffers,
|
|
588
|
+
// Metadata proves whether the decoder may skip its otherwise duplicated page-header scan.
|
|
589
|
+
// Keep the scan for mixed/unknown encodings: it is both a correctness guard and fallback.
|
|
590
|
+
hasOnlyArrowByteArrayDataPages: hasOnlyArrowByteArrayDataPageEncodings(metadata.encodings, validDictionaryPageOffset),
|
|
591
|
+
verifyPageChecksums: this.props.verifyPageChecksums,
|
|
592
|
+
int96AsTimestamp: this.props.int96AsTimestamp
|
|
593
|
+
};
|
|
594
|
+
let dictionary;
|
|
595
|
+
if (validDictionaryPageOffset !== undefined) {
|
|
596
|
+
const dictionaryRelativeOffset = validDictionaryPageOffset - chunkOffset;
|
|
597
|
+
const dictionarySize = Math.min(Math.max(0, pagesOffset - validDictionaryPageOffset), chunkBuffer.length - dictionaryRelativeOffset, this.props.defaultDictionarySize);
|
|
598
|
+
dictionary = decodeUncompressedDictionaryBuffer(chunkBuffer.subarray(dictionaryRelativeOffset, dictionaryRelativeOffset + dictionarySize), context);
|
|
599
|
+
}
|
|
600
|
+
return decodeUncompressedDataPages(chunkBuffer.subarray(pagesRelativeOffset, pagesRelativeOffset + pagesSize), { ...context, dictionary });
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Each row group contains column chunks for all the columns.
|
|
604
|
+
*/
|
|
605
|
+
async readColumnChunk(schema, colChunk, signal, rowGroupOrdinal = 0, columnOrdinal = 0, resolvedMetadata) {
|
|
188
606
|
if (colChunk.file_path !== undefined && colChunk.file_path !== null) {
|
|
189
607
|
throw new Error('external references are not supported');
|
|
190
608
|
}
|
|
191
|
-
const
|
|
192
|
-
|
|
609
|
+
const metadata = resolvedMetadata || colChunk.meta_data;
|
|
610
|
+
if (!metadata) {
|
|
611
|
+
throw new Error('Parquet column metadata is missing');
|
|
612
|
+
}
|
|
613
|
+
const field = schema.findField(metadata.path_in_schema);
|
|
614
|
+
const type = getThriftEnum(Type, metadata.type);
|
|
193
615
|
if (type !== field.primitiveType) {
|
|
194
616
|
throw new Error(`chunk type not matching schema: ${type}`);
|
|
195
617
|
}
|
|
196
|
-
const compression = getThriftEnum(CompressionCodec,
|
|
197
|
-
const pagesOffset = Number(
|
|
198
|
-
const dictionaryPageOffset =
|
|
618
|
+
const compression = getThriftEnum(CompressionCodec, metadata.codec);
|
|
619
|
+
const pagesOffset = Number(metadata.data_page_offset);
|
|
620
|
+
const dictionaryPageOffset = metadata.dictionary_page_offset;
|
|
199
621
|
const validDictionaryPageOffset = dictionaryPageOffset !== undefined && Number(dictionaryPageOffset) > 0
|
|
200
622
|
? Number(dictionaryPageOffset)
|
|
201
623
|
: undefined;
|
|
202
624
|
const chunkOffset = Math.min(pagesOffset, validDictionaryPageOffset ?? pagesOffset);
|
|
203
|
-
const chunkEnd = chunkOffset + Number(
|
|
625
|
+
const chunkEnd = chunkOffset + Number(metadata.total_compressed_size);
|
|
204
626
|
const chunkSize = Math.min(this.file.size - chunkOffset, Math.max(0, chunkEnd - chunkOffset));
|
|
205
|
-
const
|
|
206
|
-
const chunkBuffer = toUint8Array(arrayBuffer);
|
|
627
|
+
const chunkBuffer = await this.readBytes(chunkOffset, chunkSize, signal);
|
|
207
628
|
const pagesRelativeOffset = pagesOffset - chunkOffset;
|
|
208
629
|
const pagesSize = Math.min(chunkBuffer.length - pagesRelativeOffset, Math.max(0, chunkEnd - pagesOffset));
|
|
209
630
|
const context = {
|
|
@@ -212,35 +633,174 @@ export class ParquetReader {
|
|
|
212
633
|
dLevelMax: field.dLevelMax,
|
|
213
634
|
compression,
|
|
214
635
|
column: field,
|
|
215
|
-
numValues:
|
|
636
|
+
numValues: metadata.num_values,
|
|
216
637
|
dictionary: [],
|
|
638
|
+
decompressPage: this.getPageDecompressor(compression),
|
|
217
639
|
// Options - TBD is this the right place for these?
|
|
218
640
|
preserveBinary: this.props.preserveBinary,
|
|
219
641
|
retainByteArrayViews: this.props.retainByteArrayViews,
|
|
220
642
|
useTypedValueBuffers: this.props.useTypedValueBuffers,
|
|
221
|
-
useTypedLevelBuffers: this.props.useTypedLevelBuffers
|
|
643
|
+
useTypedLevelBuffers: this.props.useTypedLevelBuffers,
|
|
644
|
+
useArrowByteArrayBuffers: this.props.useArrowByteArrayBuffers,
|
|
645
|
+
// Avoid parsing every page header twice when trusted column metadata already establishes the
|
|
646
|
+
// direct Arrow byte-array invariant. Unknown/mixed encodings retain the defensive scan.
|
|
647
|
+
hasOnlyArrowByteArrayDataPages: hasOnlyArrowByteArrayDataPageEncodings(metadata.encodings, validDictionaryPageOffset),
|
|
648
|
+
verifyPageChecksums: this.props.verifyPageChecksums,
|
|
649
|
+
int96AsTimestamp: this.props.int96AsTimestamp
|
|
222
650
|
};
|
|
223
651
|
let dictionary;
|
|
224
652
|
if (validDictionaryPageOffset !== undefined) {
|
|
225
653
|
const dictionaryRelativeOffset = validDictionaryPageOffset - chunkOffset;
|
|
226
654
|
const dictionarySize = Math.min(Math.max(0, pagesOffset - validDictionaryPageOffset), chunkBuffer.length - dictionaryRelativeOffset, this.props.defaultDictionarySize);
|
|
227
655
|
const dictionaryBuffer = chunkBuffer.subarray(dictionaryRelativeOffset, dictionaryRelativeOffset + dictionarySize);
|
|
228
|
-
|
|
656
|
+
const decodedDictionaryBuffer = colChunk.crypto_metadata
|
|
657
|
+
? await this.decryptColumnPages(dictionaryBuffer, context, rowGroupOrdinal, columnOrdinal, this.getColumnKeyMetadata(colChunk), true)
|
|
658
|
+
: dictionaryBuffer;
|
|
659
|
+
dictionary = await decodeDictionaryBuffer(decodedDictionaryBuffer, context);
|
|
229
660
|
}
|
|
230
661
|
dictionary = context.dictionary?.length ? context.dictionary : dictionary;
|
|
231
|
-
const pagesBuf =
|
|
662
|
+
const pagesBuf = colChunk.crypto_metadata
|
|
663
|
+
? await this.decryptColumnPages(chunkBuffer.subarray(pagesRelativeOffset, pagesRelativeOffset + pagesSize), context, rowGroupOrdinal, columnOrdinal, this.getColumnKeyMetadata(colChunk))
|
|
664
|
+
: chunkBuffer.subarray(pagesRelativeOffset, pagesRelativeOffset + pagesSize);
|
|
232
665
|
return await decodeDataPages(pagesBuf, { ...context, dictionary });
|
|
233
666
|
}
|
|
234
|
-
/** Reads
|
|
235
|
-
async
|
|
667
|
+
/** Reads one column chunk as encoded dictionaries, levels, and values for deferred decoding. */
|
|
668
|
+
async readColumnChunkEncodedPages(schema, columnChunk, preserveCompression, signal, rowGroupOrdinal = 0, columnOrdinal = 0, resolvedMetadata) {
|
|
236
669
|
if (columnChunk.file_path !== undefined && columnChunk.file_path !== null) {
|
|
237
670
|
throw new Error('external references are not supported');
|
|
238
671
|
}
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
672
|
+
const metadata = resolvedMetadata || columnChunk.meta_data;
|
|
673
|
+
if (!metadata)
|
|
674
|
+
throw new Error('Parquet column metadata is missing');
|
|
675
|
+
const field = schema.findField(metadata.path_in_schema);
|
|
676
|
+
const physicalType = getThriftEnum(Type, metadata.type);
|
|
677
|
+
if (physicalType !== field.primitiveType) {
|
|
678
|
+
throw new Error(`chunk type not matching schema: ${physicalType}`);
|
|
679
|
+
}
|
|
680
|
+
const compression = getThriftEnum(CompressionCodec, metadata.codec);
|
|
681
|
+
const dataPageOffset = Number(metadata.data_page_offset);
|
|
682
|
+
const dictionaryPageOffset = metadata.dictionary_page_offset;
|
|
683
|
+
const validDictionaryPageOffset = dictionaryPageOffset !== undefined && Number(dictionaryPageOffset) > 0
|
|
684
|
+
? Number(dictionaryPageOffset)
|
|
685
|
+
: undefined;
|
|
686
|
+
const chunkOffset = Math.min(dataPageOffset, validDictionaryPageOffset ?? dataPageOffset);
|
|
687
|
+
const chunkEnd = chunkOffset + Number(metadata.total_compressed_size);
|
|
688
|
+
const chunkByteLength = Math.min(this.file.size - chunkOffset, Math.max(0, chunkEnd - chunkOffset));
|
|
689
|
+
const chunkBuffer = await this.readBytes(chunkOffset, chunkByteLength, signal);
|
|
690
|
+
const dataPageRelativeOffset = dataPageOffset - chunkOffset;
|
|
691
|
+
const dataPageByteLength = Math.min(chunkBuffer.byteLength - dataPageRelativeOffset, Math.max(0, chunkEnd - dataPageOffset));
|
|
692
|
+
const context = {
|
|
693
|
+
column: field,
|
|
694
|
+
compression,
|
|
695
|
+
decompressPage: this.getPageDecompressor(compression),
|
|
696
|
+
preserveCompression,
|
|
697
|
+
verifyPageChecksums: this.props.verifyPageChecksums
|
|
698
|
+
};
|
|
699
|
+
let dictionary;
|
|
700
|
+
if (validDictionaryPageOffset !== undefined) {
|
|
701
|
+
const dictionaryRelativeOffset = validDictionaryPageOffset - chunkOffset;
|
|
702
|
+
const dictionaryByteLength = Math.min(Math.max(0, dataPageOffset - validDictionaryPageOffset), chunkBuffer.byteLength - dictionaryRelativeOffset, this.props.defaultDictionarySize);
|
|
703
|
+
const dictionaryBuffer = chunkBuffer.subarray(dictionaryRelativeOffset, dictionaryRelativeOffset + dictionaryByteLength);
|
|
704
|
+
const plaintextDictionaryBuffer = columnChunk.crypto_metadata
|
|
705
|
+
? await this.decryptColumnPages(dictionaryBuffer, {
|
|
706
|
+
type: physicalType,
|
|
707
|
+
rLevelMax: field.rLevelMax,
|
|
708
|
+
dLevelMax: field.dLevelMax,
|
|
709
|
+
compression,
|
|
710
|
+
column: field
|
|
711
|
+
}, rowGroupOrdinal, columnOrdinal, this.getColumnKeyMetadata(columnChunk), true)
|
|
712
|
+
: dictionaryBuffer;
|
|
713
|
+
dictionary = (await scanParquetEncodedPages(plaintextDictionaryBuffer, context))[0];
|
|
714
|
+
}
|
|
715
|
+
const dataPageBuffer = chunkBuffer.subarray(dataPageRelativeOffset, dataPageRelativeOffset + dataPageByteLength);
|
|
716
|
+
const plaintextDataPageBuffer = columnChunk.crypto_metadata
|
|
717
|
+
? await this.decryptColumnPages(dataPageBuffer, {
|
|
718
|
+
type: physicalType,
|
|
719
|
+
rLevelMax: field.rLevelMax,
|
|
720
|
+
dLevelMax: field.dLevelMax,
|
|
721
|
+
compression,
|
|
722
|
+
column: field
|
|
723
|
+
}, rowGroupOrdinal, columnOrdinal, this.getColumnKeyMetadata(columnChunk))
|
|
724
|
+
: dataPageBuffer;
|
|
725
|
+
const pages = await scanParquetEncodedPages(plaintextDataPageBuffer, context);
|
|
726
|
+
return Object.freeze({
|
|
727
|
+
path: Object.freeze([...metadata.path_in_schema]),
|
|
728
|
+
physicalType,
|
|
729
|
+
typeLength: field.typeLength,
|
|
730
|
+
maxRepetitionLevel: field.rLevelMax,
|
|
731
|
+
maxDefinitionLevel: field.dLevelMax,
|
|
732
|
+
compression,
|
|
733
|
+
valueCount: Number(metadata.num_values),
|
|
734
|
+
dictionary,
|
|
735
|
+
pages: Object.freeze(pages)
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
/** Decrypts the length-prefixed page header and data modules in one column chunk. */
|
|
739
|
+
async decryptColumnPages(encryptedPages, context, rowGroupOrdinal, columnOrdinal, keyMetadata, includesDictionary = false, dataPageOrdinalStart = 0) {
|
|
740
|
+
if (!this.encryptionContext || !this.props.keyRetriever) {
|
|
741
|
+
throw new Error('Encrypted Parquet pages require parquet.keyRetriever');
|
|
742
|
+
}
|
|
743
|
+
const plaintextPages = [];
|
|
744
|
+
let offset = 0;
|
|
745
|
+
let dataPageOrdinal = dataPageOrdinalStart;
|
|
746
|
+
let pageIndex = 0;
|
|
747
|
+
while (offset < encryptedPages.length) {
|
|
748
|
+
const headerModule = readParquetEncryptedModule(encryptedPages, offset);
|
|
749
|
+
offset = headerModule.nextOffset;
|
|
750
|
+
const dictionaryPage = includesDictionary && pageIndex === 0;
|
|
751
|
+
const headerModuleType = dictionaryPage
|
|
752
|
+
? 'dictionary-page-header'
|
|
753
|
+
: 'data-page-header';
|
|
754
|
+
const headerAad = createParquetModuleAad(this.encryptionContext.aadPrefix, this.encryptionContext.fileUnique, headerModuleType, rowGroupOrdinal, columnOrdinal, dictionaryPage ? undefined : dataPageOrdinal);
|
|
755
|
+
const headerBytes = await decryptParquetModule(headerModule.bytes, {
|
|
756
|
+
algorithm: this.encryptionContext.algorithm,
|
|
757
|
+
aad: headerAad,
|
|
758
|
+
keyMetadata,
|
|
759
|
+
keyRetriever: this.getCachedKeyRetriever()
|
|
760
|
+
});
|
|
761
|
+
const { pageHeader } = decodePageHeader(headerBytes);
|
|
762
|
+
const pageType = getThriftEnum(PageType, pageHeader.type);
|
|
763
|
+
const dataModule = readParquetEncryptedModule(encryptedPages, offset);
|
|
764
|
+
offset = dataModule.nextOffset;
|
|
765
|
+
const dataAad = createParquetModuleAad(this.encryptionContext.aadPrefix, this.encryptionContext.fileUnique, dictionaryPage ? 'dictionary-page' : 'data-page', rowGroupOrdinal, columnOrdinal, dictionaryPage ? undefined : dataPageOrdinal);
|
|
766
|
+
const dataBytes = await decryptParquetModule(dataModule.bytes, {
|
|
767
|
+
algorithm: this.encryptionContext.algorithm,
|
|
768
|
+
aad: dataAad,
|
|
769
|
+
keyMetadata,
|
|
770
|
+
keyRetriever: this.getCachedKeyRetriever(),
|
|
771
|
+
page: true
|
|
772
|
+
});
|
|
773
|
+
plaintextPages.push(serializeThrift(pageHeader), dataBytes);
|
|
774
|
+
if (pageType !== 'DICTIONARY_PAGE')
|
|
775
|
+
dataPageOrdinal++;
|
|
776
|
+
pageIndex++;
|
|
777
|
+
}
|
|
778
|
+
const plaintextLength = plaintextPages.reduce((total, page) => total + page.length, 0);
|
|
779
|
+
const plaintext = new Uint8Array(plaintextLength);
|
|
780
|
+
let plaintextOffset = 0;
|
|
781
|
+
for (const page of plaintextPages) {
|
|
782
|
+
plaintext.set(page, plaintextOffset);
|
|
783
|
+
plaintextOffset += page.length;
|
|
784
|
+
}
|
|
785
|
+
return plaintext;
|
|
786
|
+
}
|
|
787
|
+
/** Resolves the key metadata reference for an encrypted column. */
|
|
788
|
+
getColumnKeyMetadata(columnChunk) {
|
|
789
|
+
return (columnChunk.crypto_metadata?.ENCRYPTION_WITH_COLUMN_KEY?.key_metadata ??
|
|
790
|
+
this.encryptionContext?.footerKeyMetadata ??
|
|
791
|
+
this.encryptionContext?.footerSigningKeyMetadata);
|
|
792
|
+
}
|
|
793
|
+
/** Reads and decodes contiguous data pages that begin and end on complete row boundaries. */
|
|
794
|
+
async readColumnChunkRange(schema, columnChunk, rowRange, pages, signal, rowGroupOrdinal = 0, columnOrdinal = 0, resolvedMetadata) {
|
|
795
|
+
if (columnChunk.file_path !== undefined && columnChunk.file_path !== null) {
|
|
796
|
+
throw new Error('external references are not supported');
|
|
797
|
+
}
|
|
798
|
+
const columnMetadata = resolvedMetadata || columnChunk.meta_data;
|
|
799
|
+
if (!columnMetadata) {
|
|
800
|
+
throw new Error('Parquet column metadata is missing');
|
|
243
801
|
}
|
|
802
|
+
const encrypted = Boolean(columnChunk.crypto_metadata);
|
|
803
|
+
const field = schema.findField(columnMetadata.path_in_schema);
|
|
244
804
|
const type = getThriftEnum(Type, columnMetadata.type);
|
|
245
805
|
if (type !== field.primitiveType) {
|
|
246
806
|
throw new Error(`chunk type not matching schema: ${type}`);
|
|
@@ -250,33 +810,67 @@ export class ParquetReader {
|
|
|
250
810
|
if (!overlappingPages.length) {
|
|
251
811
|
throw new Error('Parquet page plan does not overlap the requested row range');
|
|
252
812
|
}
|
|
253
|
-
|
|
813
|
+
let firstPageIndex = pages.indexOf(overlappingPages[0]);
|
|
254
814
|
const lastPage = overlappingPages[overlappingPages.length - 1];
|
|
815
|
+
let firstPage = pages[firstPageIndex];
|
|
255
816
|
const context = {
|
|
256
817
|
type,
|
|
257
818
|
rLevelMax: field.rLevelMax,
|
|
258
819
|
dLevelMax: field.dLevelMax,
|
|
259
820
|
compression,
|
|
260
821
|
column: field,
|
|
261
|
-
|
|
822
|
+
// Repeated leaves have one level entry per physical value, not one per logical row. Let the
|
|
823
|
+
// page decoder consume the selected page range and preserve all repetition levels.
|
|
824
|
+
numValues: field.rLevelMax === 0
|
|
825
|
+
? new CompactInt64(lastPage.endRowIndex - firstPage.firstRowIndex)
|
|
826
|
+
: undefined,
|
|
262
827
|
dictionary: [],
|
|
828
|
+
decompressPage: this.getPageDecompressor(compression),
|
|
263
829
|
preserveBinary: this.props.preserveBinary,
|
|
264
830
|
retainByteArrayViews: this.props.retainByteArrayViews,
|
|
265
|
-
useTypedValueBuffers: this.props.useTypedValueBuffers
|
|
831
|
+
useTypedValueBuffers: this.props.useTypedValueBuffers,
|
|
832
|
+
useArrowByteArrayBuffers: this.props.useArrowByteArrayBuffers,
|
|
833
|
+
verifyPageChecksums: this.props.verifyPageChecksums,
|
|
834
|
+
int96AsTimestamp: this.props.int96AsTimestamp
|
|
266
835
|
};
|
|
267
836
|
let dictionary;
|
|
268
837
|
const dictionaryPageOffset = Number(columnMetadata.dictionary_page_offset);
|
|
269
838
|
if (Number.isSafeInteger(dictionaryPageOffset) && dictionaryPageOffset > 0) {
|
|
270
839
|
const dictionaryLength = Math.max(0, pages[0].offset - dictionaryPageOffset);
|
|
271
|
-
const dictionaryBuffer =
|
|
272
|
-
|
|
840
|
+
const dictionaryBuffer = await this.readBytes(dictionaryPageOffset, dictionaryLength, signal);
|
|
841
|
+
const decodedDictionaryBuffer = encrypted
|
|
842
|
+
? await this.decryptColumnPages(dictionaryBuffer, context, rowGroupOrdinal, columnOrdinal, this.getColumnKeyMetadata(columnChunk), true)
|
|
843
|
+
: dictionaryBuffer;
|
|
844
|
+
dictionary = await decodeDictionaryBuffer(decodedDictionaryBuffer, context);
|
|
845
|
+
}
|
|
846
|
+
if (field.rLevelMax !== 0) {
|
|
847
|
+
// Probe predecessor pages independently, then decode the final range once. This avoids
|
|
848
|
+
// quadratic re-decoding when a large repeated row spans many pages.
|
|
849
|
+
while (firstPageIndex > 0) {
|
|
850
|
+
const probeBuffer = await this.readBytes(firstPage.offset, firstPage.compressedByteLength, signal);
|
|
851
|
+
const decodedProbeBuffer = encrypted
|
|
852
|
+
? await this.decryptColumnPages(probeBuffer, context, rowGroupOrdinal, columnOrdinal, this.getColumnKeyMetadata(columnChunk), false, firstPageIndex)
|
|
853
|
+
: probeBuffer;
|
|
854
|
+
const probe = await decodeDataPages(decodedProbeBuffer, { ...context, dictionary });
|
|
855
|
+
if (probe.rlevels[0] === 0) {
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
firstPage = pages[--firstPageIndex];
|
|
859
|
+
}
|
|
273
860
|
}
|
|
274
861
|
const dataLength = lastPage.offset + lastPage.compressedByteLength - firstPage.offset;
|
|
275
|
-
const dataBuffer =
|
|
276
|
-
const
|
|
862
|
+
const dataBuffer = await this.readBytes(firstPage.offset, dataLength, signal);
|
|
863
|
+
const decodedDataBuffer = encrypted
|
|
864
|
+
? await this.decryptColumnPages(dataBuffer, context, rowGroupOrdinal, columnOrdinal, this.getColumnKeyMetadata(columnChunk), false, firstPageIndex)
|
|
865
|
+
: dataBuffer;
|
|
866
|
+
const decoded = await decodeDataPages(decodedDataBuffer, { ...context, dictionary });
|
|
867
|
+
if (field.rLevelMax !== 0) {
|
|
868
|
+
const rowStart = rowRange.start - firstPage.firstRowIndex;
|
|
869
|
+
return sliceRepeatedColumnChunk(decoded, Math.max(0, rowStart), rowRange.end - rowRange.start, field.dLevelMax);
|
|
870
|
+
}
|
|
277
871
|
const relativeStart = rowRange.start - firstPage.firstRowIndex;
|
|
278
872
|
const relativeEnd = relativeStart + rowRange.end - rowRange.start;
|
|
279
|
-
return
|
|
873
|
+
return sliceNonRepeatedColumnChunk(decoded, field.dLevelMax, relativeStart, relativeEnd);
|
|
280
874
|
}
|
|
281
875
|
/**
|
|
282
876
|
* Getting dictionary for allows to flatten values by indices.
|
|
@@ -296,19 +890,47 @@ export class ParquetReader {
|
|
|
296
890
|
return [];
|
|
297
891
|
}
|
|
298
892
|
const dictionarySize = Math.min(Math.max(0, pagesOffset - dictionaryPageOffset), this.file.size - dictionaryPageOffset, this.props.defaultDictionarySize);
|
|
299
|
-
const
|
|
300
|
-
const pagesBuf = toUint8Array(arrayBuffer);
|
|
893
|
+
const pagesBuf = await this.readBytes(dictionaryPageOffset, dictionarySize, signal);
|
|
301
894
|
return await decodeDictionaryBuffer(pagesBuf, context);
|
|
302
895
|
}
|
|
303
896
|
}
|
|
897
|
+
/** Returns a column path available without decrypting column metadata when possible. */
|
|
898
|
+
function getColumnChunkPath(columnChunk) {
|
|
899
|
+
return (columnChunk.meta_data?.path_in_schema ??
|
|
900
|
+
columnChunk.crypto_metadata?.ENCRYPTION_WITH_COLUMN_KEY?.path_in_schema);
|
|
901
|
+
}
|
|
902
|
+
/** Builds a stable cache key while preserving module scope for context-dependent key providers. */
|
|
903
|
+
function createEncryptionKeyCacheKey(algorithm, keyMetadata, aad, aadPrefixByteLength, fileUniqueByteLength) {
|
|
904
|
+
const metadataKey = keyMetadata ? Array.from(keyMetadata).join(',') : 'footer';
|
|
905
|
+
if (fileUniqueByteLength === undefined) {
|
|
906
|
+
return `${algorithm}:${metadataKey}:aad:${Array.from(aad).join(',')}`;
|
|
907
|
+
}
|
|
908
|
+
const moduleOffset = (aadPrefixByteLength ?? 0) + fileUniqueByteLength;
|
|
909
|
+
if (moduleOffset + 5 > aad.byteLength) {
|
|
910
|
+
return `${algorithm}:${metadataKey}:aad:${Array.from(aad).join(',')}`;
|
|
911
|
+
}
|
|
912
|
+
const moduleType = aad[moduleOffset];
|
|
913
|
+
if (moduleType === 0) {
|
|
914
|
+
return `${algorithm}:${metadataKey}:footer`;
|
|
915
|
+
}
|
|
916
|
+
const view = new DataView(aad.buffer, aad.byteOffset + moduleOffset, aad.byteLength - moduleOffset);
|
|
917
|
+
const rowGroupOrdinal = view.getInt16(1, true);
|
|
918
|
+
const columnOrdinal = view.getInt16(3, true);
|
|
919
|
+
return `${algorithm}:${metadataKey}:module:${moduleType}:row-group:${rowGroupOrdinal}:column:${columnOrdinal}`;
|
|
920
|
+
}
|
|
921
|
+
/** Returns the original row-group ordinal for a worker-reduced column list. */
|
|
922
|
+
function getPhysicalColumnOrdinal(columnChunk, fallbackOrdinal) {
|
|
923
|
+
return (columnChunk.parquetColumnOrdinal ??
|
|
924
|
+
fallbackOrdinal);
|
|
925
|
+
}
|
|
304
926
|
/** Decodes one dictionary page from an already-read column-chunk range. */
|
|
305
927
|
async function decodeDictionaryBuffer(dictionaryBuffer, context) {
|
|
306
928
|
const cursor = { buffer: dictionaryBuffer, offset: 0, size: dictionaryBuffer.length };
|
|
307
929
|
const decodedPage = await decodePage(cursor, context);
|
|
308
930
|
return decodedPage.dictionary;
|
|
309
931
|
}
|
|
310
|
-
/** Slices one decoded
|
|
311
|
-
function
|
|
932
|
+
/** Slices one decoded non-repeated column chunk while preserving optional-value alignment. */
|
|
933
|
+
function sliceNonRepeatedColumnChunk(columnChunk, definitionLevelMaximum, start, end) {
|
|
312
934
|
const valueStart = countDefinedValues(columnChunk.dlevels, definitionLevelMaximum, 0, start);
|
|
313
935
|
const valueEnd = valueStart + countDefinedValues(columnChunk.dlevels, definitionLevelMaximum, start, end);
|
|
314
936
|
return {
|
|
@@ -319,7 +941,32 @@ function sliceFlatColumnChunk(columnChunk, definitionLevelMaximum, start, end) {
|
|
|
319
941
|
pageHeaders: columnChunk.pageHeaders
|
|
320
942
|
};
|
|
321
943
|
}
|
|
322
|
-
/**
|
|
944
|
+
/** Slices a repeated column by logical rows while preserving its level/value alignment. */
|
|
945
|
+
function sliceRepeatedColumnChunk(columnChunk, rowStart, rowCount, definitionLevelMaximum) {
|
|
946
|
+
const rowStarts = [];
|
|
947
|
+
for (let levelIndex = 0; levelIndex < columnChunk.rlevels.length; levelIndex++) {
|
|
948
|
+
if (columnChunk.rlevels[levelIndex] === 0) {
|
|
949
|
+
rowStarts.push(levelIndex);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const levelStart = rowStarts[rowStart];
|
|
953
|
+
const endRow = rowStart + rowCount;
|
|
954
|
+
const levelEnd = rowStarts[endRow] ?? columnChunk.rlevels.length;
|
|
955
|
+
if (levelStart === undefined || endRow > rowStarts.length || levelEnd < levelStart) {
|
|
956
|
+
throw new Error('Parquet repeated page range does not contain the requested rows');
|
|
957
|
+
}
|
|
958
|
+
const valueStart = countDefinedValues(columnChunk.dlevels, definitionLevelMaximum, 0, levelStart);
|
|
959
|
+
const valueEnd = valueStart +
|
|
960
|
+
countDefinedValues(columnChunk.dlevels, definitionLevelMaximum, levelStart, levelEnd);
|
|
961
|
+
return {
|
|
962
|
+
rlevels: columnChunk.rlevels.slice(levelStart, levelEnd),
|
|
963
|
+
dlevels: columnChunk.dlevels.slice(levelStart, levelEnd),
|
|
964
|
+
values: columnChunk.values.slice(valueStart, valueEnd),
|
|
965
|
+
count: levelEnd - levelStart,
|
|
966
|
+
pageHeaders: columnChunk.pageHeaders
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
/** Counts defined primitive values represented by one non-repeated level interval. */
|
|
323
970
|
function countDefinedValues(definitionLevels, definitionLevelMaximum, start, end) {
|
|
324
971
|
let count = 0;
|
|
325
972
|
for (let index = start; index < Math.min(end, definitionLevels.length); index++) {
|