@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
|
@@ -3,21 +3,31 @@
|
|
|
3
3
|
// Copyright (c) vis.gl contributors
|
|
4
4
|
// Copyright (c) 2017 ironSource Ltd.
|
|
5
5
|
// Forked from https://github.com/kbajalc/parquets under MIT license
|
|
6
|
+
import { getWKBGeometryStatistics } from '@loaders.gl/gis';
|
|
6
7
|
import { PARQUET_CODECS } from "../codecs/index.js";
|
|
7
8
|
import * as Compression from "../compression.js";
|
|
8
9
|
import * as Shred from "../schema/shred.js";
|
|
9
|
-
import { BsonType, ColumnChunk, ColumnMetaData, CompressionCodec, ConvertedType, DataPageHeader, DataPageHeaderV2, DateType, DecimalType, DictionaryPageHeader, EdgeInterpolationAlgorithm, Encoding, EnumType, FieldRepetitionType, FileMetaData, Float16Type, GeographyType, GeometryType, IntType, JsonType, KeyValue, ListType, LogicalType, MapType, MicroSeconds, MilliSeconds, NanoSeconds, NullType, PageHeader, PageEncodingStats, PageType, RowGroup, SchemaElement, StringType, TimeType, TimeUnit, TimestampType, Type, UUIDType, VariantType } from "../parquet-thrift/index.js";
|
|
10
|
+
import { BsonType, BoundingBox, ColumnChunk, ColumnCryptoMetaData, ColumnIndex, ColumnMetaData, SizeStatistics, Statistics, SortingColumn, CompressionCodec, ConvertedType, DataPageHeader, DataPageHeaderV2, DateType, DecimalType, DictionaryPageHeader, EdgeInterpolationAlgorithm, Encoding, EnumType, FieldRepetitionType, FileMetaData, Float16Type, GeographyType, GeospatialStatistics, GeometryType, IntType, JsonType, KeyValue, ListType, LogicalType, MapType, MicroSeconds, MilliSeconds, NanoSeconds, NullType, PageHeader, PageEncodingStats, PageType, PageLocation, RowGroup, SchemaElement, StringType, TimeType, TimeUnit, TimestampType, Type, UUIDType, VariantType, OffsetIndex } from "../parquet-thrift/index.js";
|
|
10
11
|
import { osopen, oswrite, osclose } from "../utils/file-utils.js";
|
|
11
12
|
import { getBitWidth, serializeThrift } from "../utils/read-utils.js";
|
|
12
13
|
import { concatUint8Arrays, encodeUtf8, writeUInt32LE } from "../utils/binary-utils.js";
|
|
14
|
+
import { crc32 } from "../utils/crc32.js";
|
|
13
15
|
import { CompactInt64 } from "../utils/uint8-array-compact-protocol.js";
|
|
14
16
|
import { planColumnPages } from "./page-planner.js";
|
|
15
17
|
import { planDictionary } from "./dictionary-planner.js";
|
|
18
|
+
import { decodeParquetSplitBlockBloomFilter, encodeParquetBloomFilterValue, encodeParquetSplitBlockBloomFilter } from "../../lib/parquet-bloom-filter.js";
|
|
19
|
+
import { BoundaryOrder } from "../parquet-thrift/BoundaryOrder.js";
|
|
20
|
+
import { EncryptionWithColumnKey, EncryptionWithFooterKey } from "../parquet-thrift/ColumnCryptoMetaData.js";
|
|
21
|
+
import { EncryptionAlgorithm } from "../parquet-thrift/EncryptionAlgorithm.js";
|
|
22
|
+
import { FileCryptoMetaData } from "../parquet-thrift/FileCryptoMetaData.js";
|
|
23
|
+
import { PARQUET_MAGIC_ENCRYPTED } from "../../lib/constants.js";
|
|
24
|
+
import { createParquetModuleAad, createParquetFooterSignature, encryptParquetModule } from "../../lib/parquet-encryption.js";
|
|
16
25
|
/**
|
|
17
26
|
* Parquet File Magic String
|
|
18
27
|
*/
|
|
19
28
|
const PARQUET_MAGIC = 'PAR1';
|
|
20
29
|
const PARQUET_MAGIC_BYTES = encodeUtf8(PARQUET_MAGIC);
|
|
30
|
+
const PARQUET_MAGIC_ENCRYPTED_BYTES = encodeUtf8(PARQUET_MAGIC_ENCRYPTED);
|
|
21
31
|
/**
|
|
22
32
|
* Parquet File Format Version
|
|
23
33
|
*/
|
|
@@ -170,6 +180,18 @@ export class ParquetEnvelopeWriter {
|
|
|
170
180
|
dictionary;
|
|
171
181
|
columnDictionaries;
|
|
172
182
|
dictionaryPageSizeLimit;
|
|
183
|
+
bloomFilter;
|
|
184
|
+
pageIndex;
|
|
185
|
+
writePageChecksums;
|
|
186
|
+
writeSizeStatistics;
|
|
187
|
+
writeStatistics;
|
|
188
|
+
sortingColumns;
|
|
189
|
+
/** Optional modular-encryption configuration for the footer. */
|
|
190
|
+
encryption;
|
|
191
|
+
/** Optional plaintext-footer signature configuration. */
|
|
192
|
+
footerSignature;
|
|
193
|
+
/** File identifier used for encrypted column-module AAD. */
|
|
194
|
+
encryptionFileUnique;
|
|
173
195
|
constructor(schema, writeFn, closeFn, fileOffset, opts) {
|
|
174
196
|
this.schema = schema;
|
|
175
197
|
this.write = writeFn;
|
|
@@ -182,6 +204,40 @@ export class ParquetEnvelopeWriter {
|
|
|
182
204
|
this.dictionary = opts.dictionary ?? 'auto';
|
|
183
205
|
this.columnDictionaries = opts.columnDictionaries || {};
|
|
184
206
|
this.dictionaryPageSizeLimit = opts.dictionaryPageSizeLimit ?? 1024 * 1024;
|
|
207
|
+
this.bloomFilter = opts.bloomFilter;
|
|
208
|
+
this.pageIndex = opts.pageIndex;
|
|
209
|
+
this.writePageChecksums = Boolean(opts.writePageChecksums);
|
|
210
|
+
this.writeSizeStatistics = Boolean(opts.writeSizeStatistics);
|
|
211
|
+
this.writeStatistics = opts.writeStatistics;
|
|
212
|
+
this.sortingColumns = opts.sortingColumns;
|
|
213
|
+
const encryptionFileUnique = opts.encryption?.fileUnique
|
|
214
|
+
? new Uint8Array(opts.encryption.fileUnique)
|
|
215
|
+
: undefined;
|
|
216
|
+
const footerSignatureFileUnique = opts.footerSignature?.fileUnique
|
|
217
|
+
? new Uint8Array(opts.footerSignature.fileUnique)
|
|
218
|
+
: undefined;
|
|
219
|
+
if (encryptionFileUnique &&
|
|
220
|
+
footerSignatureFileUnique &&
|
|
221
|
+
!encryptionFileUnique.every((value, index) => value === footerSignatureFileUnique[index])) {
|
|
222
|
+
throw new Error('Parquet encryption and footer signature file_unique values must match');
|
|
223
|
+
}
|
|
224
|
+
const sharedFileUnique = encryptionFileUnique ??
|
|
225
|
+
footerSignatureFileUnique ??
|
|
226
|
+
(opts.encryption || opts.footerSignature ? createFileUnique() : undefined);
|
|
227
|
+
this.encryption = opts.encryption
|
|
228
|
+
? { ...opts.encryption, fileUnique: new Uint8Array(sharedFileUnique) }
|
|
229
|
+
: undefined;
|
|
230
|
+
this.footerSignature = opts.footerSignature
|
|
231
|
+
? {
|
|
232
|
+
...opts.footerSignature,
|
|
233
|
+
keyMetadata: new Uint8Array(opts.footerSignature.keyMetadata),
|
|
234
|
+
aadPrefix: opts.footerSignature.aadPrefix
|
|
235
|
+
? new Uint8Array(opts.footerSignature.aadPrefix)
|
|
236
|
+
: undefined,
|
|
237
|
+
fileUnique: new Uint8Array(sharedFileUnique)
|
|
238
|
+
}
|
|
239
|
+
: undefined;
|
|
240
|
+
this.encryptionFileUnique = sharedFileUnique;
|
|
185
241
|
}
|
|
186
242
|
writeSection(buf) {
|
|
187
243
|
this.offset += buf.length;
|
|
@@ -191,7 +247,7 @@ export class ParquetEnvelopeWriter {
|
|
|
191
247
|
* Encode the parquet file header
|
|
192
248
|
*/
|
|
193
249
|
writeHeader() {
|
|
194
|
-
return this.writeSection(PARQUET_MAGIC_BYTES);
|
|
250
|
+
return this.writeSection(this.encryption && !this.footerSignature ? PARQUET_MAGIC_ENCRYPTED_BYTES : PARQUET_MAGIC_BYTES);
|
|
195
251
|
}
|
|
196
252
|
/**
|
|
197
253
|
* Encode a parquet row group. The records object should be created using the
|
|
@@ -204,7 +260,16 @@ export class ParquetEnvelopeWriter {
|
|
|
204
260
|
useDataPageV2: this.useDataPageV2,
|
|
205
261
|
dictionary: this.dictionary,
|
|
206
262
|
columnDictionaries: this.columnDictionaries,
|
|
207
|
-
dictionaryPageSizeLimit: this.dictionaryPageSizeLimit
|
|
263
|
+
dictionaryPageSizeLimit: this.dictionaryPageSizeLimit,
|
|
264
|
+
bloomFilter: this.bloomFilter,
|
|
265
|
+
pageIndex: this.pageIndex,
|
|
266
|
+
writePageChecksums: this.writePageChecksums,
|
|
267
|
+
writeSizeStatistics: this.writeSizeStatistics,
|
|
268
|
+
writeStatistics: this.writeStatistics,
|
|
269
|
+
sortingColumns: this.sortingColumns,
|
|
270
|
+
encryption: this.encryption,
|
|
271
|
+
encryptionFileUnique: this.encryptionFileUnique,
|
|
272
|
+
rowGroupOrdinal: this.rowGroups.length
|
|
208
273
|
});
|
|
209
274
|
this.rowCount += records.rowCount;
|
|
210
275
|
this.rowGroups.push(rgroup.metadata);
|
|
@@ -213,12 +278,21 @@ export class ParquetEnvelopeWriter {
|
|
|
213
278
|
/**
|
|
214
279
|
* Write the parquet file footer
|
|
215
280
|
*/
|
|
216
|
-
writeFooter(userMetadata) {
|
|
281
|
+
async writeFooter(userMetadata) {
|
|
217
282
|
if (!userMetadata) {
|
|
218
283
|
// tslint:disable-next-line:no-parameter-reassignment
|
|
219
284
|
userMetadata = {};
|
|
220
285
|
}
|
|
221
|
-
|
|
286
|
+
if (this.footerSignature) {
|
|
287
|
+
await this.writeSection(await encodeSignedFooter(this.schema, this.rowCount, this.rowGroups, userMetadata, this.footerSignature));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const footer = encodeFooter(this.schema, this.rowCount, this.rowGroups, userMetadata);
|
|
291
|
+
if (!this.encryption) {
|
|
292
|
+
await this.writeSection(footer);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
await this.writeSection(await encodeEncryptedFooter(footer, this.encryption));
|
|
222
296
|
}
|
|
223
297
|
/**
|
|
224
298
|
* Set the parquet data page size. The data page size controls the maximum
|
|
@@ -276,7 +350,7 @@ function encodeValues(type, encoding, values, opts) {
|
|
|
276
350
|
/**
|
|
277
351
|
* Encode a parquet data page
|
|
278
352
|
*/
|
|
279
|
-
async function encodeDataPage(column, data, valueEncoding = column.encoding, valueBitWidth) {
|
|
353
|
+
async function encodeDataPage(column, data, valueEncoding = column.encoding, valueBitWidth, writePageChecksums = false, statistics, int96AsTimestamp = false) {
|
|
280
354
|
/* encode repetition and definition levels */
|
|
281
355
|
let rLevelsBuf = new Uint8Array(0);
|
|
282
356
|
if (column.rLevelMax > 0) {
|
|
@@ -295,7 +369,8 @@ async function encodeDataPage(column, data, valueEncoding = column.encoding, val
|
|
|
295
369
|
/* encode values */
|
|
296
370
|
const valuesBuf = encodeValues(column.primitiveType, valueEncoding, data.values, {
|
|
297
371
|
typeLength: column.typeLength,
|
|
298
|
-
bitWidth: valueBitWidth ?? column.typeLength
|
|
372
|
+
bitWidth: valueBitWidth ?? column.typeLength,
|
|
373
|
+
int96AsTimestamp
|
|
299
374
|
});
|
|
300
375
|
const dataBuf = concatUint8Arrays([rLevelsBuf, dLevelsBuf, valuesBuf]);
|
|
301
376
|
// compression = column.compression === 'UNCOMPRESSED' ? (compression || 'UNCOMPRESSED') : column.compression;
|
|
@@ -307,10 +382,12 @@ async function encodeDataPage(column, data, valueEncoding = column.encoding, val
|
|
|
307
382
|
num_values: data.count,
|
|
308
383
|
encoding: Encoding[valueEncoding],
|
|
309
384
|
definition_level_encoding: Encoding[PARQUET_RDLVL_ENCODING], // [PARQUET_RDLVL_ENCODING],
|
|
310
|
-
repetition_level_encoding: Encoding[PARQUET_RDLVL_ENCODING] // [PARQUET_RDLVL_ENCODING]
|
|
385
|
+
repetition_level_encoding: Encoding[PARQUET_RDLVL_ENCODING], // [PARQUET_RDLVL_ENCODING]
|
|
386
|
+
statistics
|
|
311
387
|
}),
|
|
312
388
|
uncompressed_page_size: dataBuf.length,
|
|
313
|
-
compressed_page_size: compressedBuf.length
|
|
389
|
+
compressed_page_size: compressedBuf.length,
|
|
390
|
+
crc: writePageChecksums ? crc32(compressedBuf) : undefined
|
|
314
391
|
});
|
|
315
392
|
/* concat page header, repetition and definition levels and values */
|
|
316
393
|
const headerBuf = serializeThrift(header);
|
|
@@ -320,11 +397,12 @@ async function encodeDataPage(column, data, valueEncoding = column.encoding, val
|
|
|
320
397
|
/**
|
|
321
398
|
* Encode a parquet data page (v2)
|
|
322
399
|
*/
|
|
323
|
-
async function encodeDataPageV2(column, data, rowCount, valueEncoding = column.encoding, valueBitWidth) {
|
|
400
|
+
async function encodeDataPageV2(column, data, rowCount, valueEncoding = column.encoding, valueBitWidth, writePageChecksums = false, statistics, int96AsTimestamp = false) {
|
|
324
401
|
/* encode values */
|
|
325
402
|
const valuesBuf = encodeValues(column.primitiveType, valueEncoding, data.values, {
|
|
326
403
|
typeLength: column.typeLength,
|
|
327
|
-
bitWidth: valueBitWidth ?? column.typeLength
|
|
404
|
+
bitWidth: valueBitWidth ?? column.typeLength,
|
|
405
|
+
int96AsTimestamp
|
|
328
406
|
});
|
|
329
407
|
// compression = column.compression === 'UNCOMPRESSED' ? (compression || 'UNCOMPRESSED') : column.compression;
|
|
330
408
|
const compressedBuf = await Compression.deflate(column.compression, valuesBuf);
|
|
@@ -353,10 +431,14 @@ async function encodeDataPageV2(column, data, rowCount, valueEncoding = column.e
|
|
|
353
431
|
encoding: Encoding[valueEncoding],
|
|
354
432
|
definition_levels_byte_length: dLevelsBuf.length,
|
|
355
433
|
repetition_levels_byte_length: rLevelsBuf.length,
|
|
356
|
-
is_compressed: column.compression !== 'UNCOMPRESSED'
|
|
434
|
+
is_compressed: column.compression !== 'UNCOMPRESSED',
|
|
435
|
+
statistics
|
|
357
436
|
}),
|
|
358
437
|
uncompressed_page_size: rLevelsBuf.length + dLevelsBuf.length + valuesBuf.length,
|
|
359
|
-
compressed_page_size: rLevelsBuf.length + dLevelsBuf.length + compressedBuf.length
|
|
438
|
+
compressed_page_size: rLevelsBuf.length + dLevelsBuf.length + compressedBuf.length,
|
|
439
|
+
crc: writePageChecksums
|
|
440
|
+
? crc32(concatUint8Arrays([rLevelsBuf, dLevelsBuf, compressedBuf]))
|
|
441
|
+
: undefined
|
|
360
442
|
});
|
|
361
443
|
/* concat page header, repetition and definition levels and values */
|
|
362
444
|
const headerBuf = serializeThrift(header);
|
|
@@ -364,9 +446,10 @@ async function encodeDataPageV2(column, data, rowCount, valueEncoding = column.e
|
|
|
364
446
|
return { header, headerSize: headerBuf.length, page };
|
|
365
447
|
}
|
|
366
448
|
/** Encodes one chunk-wide PLAIN dictionary page. */
|
|
367
|
-
async function encodeDictionaryPage(column, dictionaryValues) {
|
|
449
|
+
async function encodeDictionaryPage(column, dictionaryValues, writePageChecksums = false, int96AsTimestamp = false) {
|
|
368
450
|
const valuesBuffer = encodeValues(column.primitiveType, 'PLAIN', dictionaryValues, {
|
|
369
|
-
typeLength: column.typeLength
|
|
451
|
+
typeLength: column.typeLength,
|
|
452
|
+
int96AsTimestamp
|
|
370
453
|
});
|
|
371
454
|
const compressedBuffer = await Compression.deflate(column.compression, valuesBuffer);
|
|
372
455
|
const header = new PageHeader({
|
|
@@ -376,7 +459,8 @@ async function encodeDictionaryPage(column, dictionaryValues) {
|
|
|
376
459
|
encoding: Encoding.PLAIN
|
|
377
460
|
}),
|
|
378
461
|
uncompressed_page_size: valuesBuffer.length,
|
|
379
|
-
compressed_page_size: compressedBuffer.length
|
|
462
|
+
compressed_page_size: compressedBuffer.length,
|
|
463
|
+
crc: writePageChecksums ? crc32(compressedBuffer) : undefined
|
|
380
464
|
});
|
|
381
465
|
const headerBuffer = serializeThrift(header);
|
|
382
466
|
return {
|
|
@@ -385,14 +469,64 @@ async function encodeDictionaryPage(column, dictionaryValues) {
|
|
|
385
469
|
page: concatUint8Arrays([headerBuffer, compressedBuffer])
|
|
386
470
|
};
|
|
387
471
|
}
|
|
472
|
+
/** Computes native Parquet geospatial statistics for a GEOMETRY or GEOGRAPHY column chunk. */
|
|
473
|
+
function createGeospatialStatistics(column, values) {
|
|
474
|
+
const logicalType = column.logicalType?.type || column.originalType;
|
|
475
|
+
if (logicalType !== 'GEOMETRY' && logicalType !== 'GEOGRAPHY')
|
|
476
|
+
return undefined;
|
|
477
|
+
const geometryTypes = new Set();
|
|
478
|
+
let bbox;
|
|
479
|
+
// Vertex bounds are not conservative for GEOGRAPHY: non-linear edge algorithms can reach
|
|
480
|
+
// extrema between vertices. Until edge-aware geodesic bounds are available, omit the bbox while
|
|
481
|
+
// retaining the useful geometry-type statistics.
|
|
482
|
+
const canWriteBoundingBox = logicalType === 'GEOMETRY';
|
|
483
|
+
for (const value of values) {
|
|
484
|
+
if (!(value instanceof ArrayBuffer) && !ArrayBuffer.isView(value)) {
|
|
485
|
+
throw new Error(`${logicalType} columns must contain WKB binary values`);
|
|
486
|
+
}
|
|
487
|
+
const statistics = getWKBGeometryStatistics(value);
|
|
488
|
+
geometryTypes.add(statistics.geometryType);
|
|
489
|
+
if (canWriteBoundingBox) {
|
|
490
|
+
bbox = mergeGeometryBoundingBoxes(bbox, statistics.bbox);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return new GeospatialStatistics({
|
|
494
|
+
bbox: bbox ? new BoundingBox(bbox) : undefined,
|
|
495
|
+
geospatial_types: [...geometryTypes].sort((left, right) => left - right)
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
/** Merges finite per-dimension bounds without inventing absent Z or M dimensions. */
|
|
499
|
+
function mergeGeometryBoundingBoxes(target, source) {
|
|
500
|
+
if (!source)
|
|
501
|
+
return target;
|
|
502
|
+
if (!target)
|
|
503
|
+
return { ...source };
|
|
504
|
+
target.xmin = Math.min(target.xmin, source.xmin);
|
|
505
|
+
target.xmax = Math.max(target.xmax, source.xmax);
|
|
506
|
+
target.ymin = Math.min(target.ymin, source.ymin);
|
|
507
|
+
target.ymax = Math.max(target.ymax, source.ymax);
|
|
508
|
+
mergeOptionalBounds(target, source, 'zmin', Math.min);
|
|
509
|
+
mergeOptionalBounds(target, source, 'zmax', Math.max);
|
|
510
|
+
mergeOptionalBounds(target, source, 'mmin', Math.min);
|
|
511
|
+
mergeOptionalBounds(target, source, 'mmax', Math.max);
|
|
512
|
+
return target;
|
|
513
|
+
}
|
|
514
|
+
/** Merges one optional bound while preserving a dimension absent from every value. */
|
|
515
|
+
function mergeOptionalBounds(target, source, key, merge) {
|
|
516
|
+
const sourceValue = source[key];
|
|
517
|
+
if (sourceValue === undefined)
|
|
518
|
+
return;
|
|
519
|
+
target[key] = target[key] === undefined ? sourceValue : merge(target[key], sourceValue);
|
|
520
|
+
}
|
|
388
521
|
/**
|
|
389
522
|
* Encode an array of values into a parquet column chunk
|
|
390
523
|
*/
|
|
391
|
-
async function encodeColumnChunk(column, buffer, offset, opts) {
|
|
524
|
+
async function encodeColumnChunk(column, buffer, offset, opts, rowGroupOrdinal, columnOrdinal) {
|
|
392
525
|
const data = buffer.columnData[column.path.join()];
|
|
393
526
|
const baseOffset = (opts.baseOffset || 0) + offset;
|
|
394
527
|
/* encode data page(s) */
|
|
395
|
-
const
|
|
528
|
+
const pageRecords = [];
|
|
529
|
+
const pageLocations = [];
|
|
396
530
|
// tslint:disable-next-line:variable-name
|
|
397
531
|
let total_uncompressed_size = 0;
|
|
398
532
|
// tslint:disable-next-line:variable-name
|
|
@@ -401,12 +535,18 @@ async function encodeColumnChunk(column, buffer, offset, opts) {
|
|
|
401
535
|
const dictionaryPolicy = opts.columnDictionaries?.[column.path[0]] ?? opts.dictionary ?? 'auto';
|
|
402
536
|
const dictionaryPlan = planDictionary(column, Array.from(data.values), dictionaryPolicy, opts.dictionaryPageSizeLimit ?? 1024 * 1024);
|
|
403
537
|
if (dictionaryPlan) {
|
|
404
|
-
const result = await encodeDictionaryPage(column, dictionaryPlan.values);
|
|
405
|
-
|
|
538
|
+
const result = await encodeDictionaryPage(column, dictionaryPlan.values, opts.writePageChecksums, opts.int96AsTimestamp);
|
|
539
|
+
pageRecords.push({ page: result.page, headerSize: result.headerSize, dictionary: true });
|
|
406
540
|
total_uncompressed_size += result.header.uncompressed_page_size + result.headerSize;
|
|
407
541
|
total_compressed_size += result.header.compressed_page_size + result.headerSize;
|
|
408
542
|
}
|
|
409
|
-
const valueEncoding = dictionaryPlan
|
|
543
|
+
const valueEncoding = dictionaryPlan
|
|
544
|
+
? column.encoding === 'PLAIN_DICTIONARY'
|
|
545
|
+
? 'PLAIN_DICTIONARY'
|
|
546
|
+
: 'RLE_DICTIONARY'
|
|
547
|
+
: column.encoding === 'PLAIN_DICTIONARY' || column.encoding === 'RLE_DICTIONARY'
|
|
548
|
+
? 'PLAIN'
|
|
549
|
+
: column.encoding;
|
|
410
550
|
let dictionaryIndexOffset = 0;
|
|
411
551
|
for (const plannedPage of plannedPages) {
|
|
412
552
|
const pageData = dictionaryPlan
|
|
@@ -417,14 +557,51 @@ async function encodeColumnChunk(column, buffer, offset, opts) {
|
|
|
417
557
|
: plannedPage.data;
|
|
418
558
|
dictionaryIndexOffset += plannedPage.data.values.length;
|
|
419
559
|
const result = opts.useDataPageV2
|
|
420
|
-
? await encodeDataPageV2(column, pageData, plannedPage.rowCount, valueEncoding, dictionaryPlan?.bitWidth)
|
|
421
|
-
|
|
422
|
-
|
|
560
|
+
? await encodeDataPageV2(column, pageData, plannedPage.rowCount, valueEncoding, dictionaryPlan?.bitWidth, opts.writePageChecksums, isStatisticsEnabled(opts.writeStatistics, column)
|
|
561
|
+
? createColumnStatistics(column, plannedPage.data)
|
|
562
|
+
: undefined, opts.int96AsTimestamp)
|
|
563
|
+
: await encodeDataPage(column, pageData, valueEncoding, dictionaryPlan?.bitWidth, opts.writePageChecksums, isStatisticsEnabled(opts.writeStatistics, column)
|
|
564
|
+
? createColumnStatistics(column, plannedPage.data)
|
|
565
|
+
: undefined, opts.int96AsTimestamp);
|
|
566
|
+
pageRecords.push({ page: result.page, headerSize: result.headerSize, dictionary: false });
|
|
423
567
|
total_uncompressed_size += result.header.uncompressed_page_size + result.headerSize;
|
|
424
568
|
total_compressed_size += result.header.compressed_page_size + result.headerSize;
|
|
425
569
|
}
|
|
570
|
+
const encryption = opts.encryption;
|
|
571
|
+
const encryptedColumn = Boolean(encryption && shouldEncryptColumn(encryption, column));
|
|
572
|
+
const columnKeyMetadata = encryption ? getColumnKeyMetadata(encryption, column) : undefined;
|
|
573
|
+
const pages = encryptedColumn
|
|
574
|
+
? await encryptParquetColumnPages(pageRecords, encryption, opts.encryptionFileUnique, opts.rowGroupOrdinal ?? 0, columnOrdinal, columnKeyMetadata)
|
|
575
|
+
: pageRecords.map(record => record.page);
|
|
576
|
+
let pageOffset = 0;
|
|
577
|
+
let dataPageIndex = 0;
|
|
578
|
+
let firstRowIndex = 0;
|
|
579
|
+
for (const [pageIndex, page] of pages.entries()) {
|
|
580
|
+
if (!pageRecords[pageIndex].dictionary) {
|
|
581
|
+
pageLocations.push(new PageLocation({
|
|
582
|
+
offset: baseOffset + pageOffset,
|
|
583
|
+
compressed_page_size: page.length,
|
|
584
|
+
first_row_index: firstRowIndex
|
|
585
|
+
}));
|
|
586
|
+
firstRowIndex += plannedPages[dataPageIndex++].rowCount;
|
|
587
|
+
}
|
|
588
|
+
pageOffset += page.length;
|
|
589
|
+
}
|
|
590
|
+
total_compressed_size = pages.reduce((total, page) => total + page.length, 0);
|
|
426
591
|
const pagesBuf = concatUint8Arrays(pages);
|
|
427
592
|
// const compression = column.compression === 'UNCOMPRESSED' ? (opts.compression || 'UNCOMPRESSED') : column.compression;
|
|
593
|
+
const bloomFilterEnabled = opts.bloomFilter === true || opts.bloomFilter?.[column.path[0]] === true;
|
|
594
|
+
const bloomFilter = bloomFilterEnabled ? createColumnBloomFilter(column, data.values) : undefined;
|
|
595
|
+
const encodedBloomFilter = bloomFilter && encryptedColumn
|
|
596
|
+
? await encryptParquetBloomFilter(bloomFilter, encryption, opts.encryptionFileUnique, opts.rowGroupOrdinal ?? 0, columnOrdinal, columnKeyMetadata)
|
|
597
|
+
: bloomFilter;
|
|
598
|
+
const pageIndexEnabled = opts.pageIndex === true || opts.pageIndex?.[column.path[0]] === true;
|
|
599
|
+
const pageIndexes = pageIndexEnabled
|
|
600
|
+
? createColumnPageIndexes(column, plannedPages, pageLocations, opts.int96AsTimestamp)
|
|
601
|
+
: undefined;
|
|
602
|
+
const encodedPageIndexes = pageIndexes && encryptedColumn
|
|
603
|
+
? await encryptParquetPageIndexes(pageIndexes, encryption, opts.encryptionFileUnique, opts.rowGroupOrdinal ?? 0, columnOrdinal, columnKeyMetadata)
|
|
604
|
+
: pageIndexes;
|
|
428
605
|
/* prepare metadata header */
|
|
429
606
|
const metadata = new ColumnMetaData({
|
|
430
607
|
path_in_schema: column.path,
|
|
@@ -451,7 +628,14 @@ async function encodeColumnChunk(column, buffer, offset, opts) {
|
|
|
451
628
|
total_uncompressed_size: int64(total_uncompressed_size), // : pagesBuf.length,
|
|
452
629
|
total_compressed_size: int64(total_compressed_size),
|
|
453
630
|
type: Type[column.primitiveType],
|
|
454
|
-
codec: CompressionCodec[column.compression]
|
|
631
|
+
codec: CompressionCodec[column.compression],
|
|
632
|
+
bloom_filter_offset: encodedBloomFilter ? int64(baseOffset + pagesBuf.length) : undefined,
|
|
633
|
+
bloom_filter_length: encodedBloomFilter?.length,
|
|
634
|
+
geospatial_statistics: createGeospatialStatistics(column, data.values),
|
|
635
|
+
size_statistics: opts.writeSizeStatistics ? createSizeStatistics(column, data) : undefined,
|
|
636
|
+
statistics: isStatisticsEnabled(opts.writeStatistics, column)
|
|
637
|
+
? createColumnStatistics(column, data)
|
|
638
|
+
: undefined
|
|
455
639
|
});
|
|
456
640
|
/* list encodings */
|
|
457
641
|
metadata.encodings.push(Encoding[PARQUET_RDLVL_ENCODING]);
|
|
@@ -459,9 +643,390 @@ async function encodeColumnChunk(column, buffer, offset, opts) {
|
|
|
459
643
|
metadata.encodings.push(Encoding.PLAIN);
|
|
460
644
|
metadata.encodings.push(Encoding[valueEncoding]);
|
|
461
645
|
/* concat metadata header and data pages */
|
|
462
|
-
const
|
|
463
|
-
const
|
|
464
|
-
|
|
646
|
+
const metadataEncoded = serializeThrift(metadata);
|
|
647
|
+
const metadataSection = encryptedColumn ? undefined : metadataEncoded;
|
|
648
|
+
const metadataOffset = baseOffset + pagesBuf.length + (encodedBloomFilter?.length || 0);
|
|
649
|
+
const offsetIndexOffset = encodedPageIndexes
|
|
650
|
+
? metadataOffset + (metadataSection?.length || 0)
|
|
651
|
+
: undefined;
|
|
652
|
+
const columnIndexOffset = encodedPageIndexes?.columnIndex
|
|
653
|
+
? metadataOffset + (metadataSection?.length || 0) + encodedPageIndexes.offsetIndex.length
|
|
654
|
+
: undefined;
|
|
655
|
+
const body = concatUint8Arrays([
|
|
656
|
+
pagesBuf,
|
|
657
|
+
...(encodedBloomFilter ? [encodedBloomFilter] : []),
|
|
658
|
+
...(metadataSection ? [metadataSection] : []),
|
|
659
|
+
...(encodedPageIndexes
|
|
660
|
+
? [
|
|
661
|
+
encodedPageIndexes.offsetIndex,
|
|
662
|
+
...(encodedPageIndexes.columnIndex ? [encodedPageIndexes.columnIndex] : [])
|
|
663
|
+
]
|
|
664
|
+
: [])
|
|
665
|
+
]);
|
|
666
|
+
return {
|
|
667
|
+
body,
|
|
668
|
+
metadata,
|
|
669
|
+
metadataOffset,
|
|
670
|
+
offsetIndexOffset,
|
|
671
|
+
offsetIndexLength: encodedPageIndexes?.offsetIndex.length,
|
|
672
|
+
columnIndexOffset,
|
|
673
|
+
columnIndexLength: encodedPageIndexes?.columnIndex?.length,
|
|
674
|
+
...(encryptedColumn
|
|
675
|
+
? {
|
|
676
|
+
encryptedColumnMetadata: await encryptParquetModule(metadataEncoded, {
|
|
677
|
+
algorithm: encryption.algorithm ?? 'AES_GCM_V1',
|
|
678
|
+
aad: createParquetModuleAad(encryption.aadPrefix, opts.encryptionFileUnique, 'column-metadata', opts.rowGroupOrdinal ?? 0, columnOrdinal),
|
|
679
|
+
keyMetadata: columnKeyMetadata ?? encryption.keyMetadata,
|
|
680
|
+
keyRetriever: encryption.keyRetriever
|
|
681
|
+
}),
|
|
682
|
+
cryptoMetadata: new ColumnCryptoMetaData({
|
|
683
|
+
...(columnKeyMetadata
|
|
684
|
+
? {
|
|
685
|
+
ENCRYPTION_WITH_COLUMN_KEY: new EncryptionWithColumnKey({
|
|
686
|
+
path_in_schema: [...column.path],
|
|
687
|
+
key_metadata: new Uint8Array(columnKeyMetadata)
|
|
688
|
+
})
|
|
689
|
+
}
|
|
690
|
+
: { ENCRYPTION_WITH_FOOTER_KEY: new EncryptionWithFooterKey() })
|
|
691
|
+
})
|
|
692
|
+
}
|
|
693
|
+
: {})
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
/** Returns whether a writer column should use modular encryption. */
|
|
697
|
+
function shouldEncryptColumn(options, column) {
|
|
698
|
+
return Boolean(options.columnKeyMetadata?.[column.path[0]] ||
|
|
699
|
+
options.encryptColumns === true ||
|
|
700
|
+
options.encryptColumns?.[column.path[0]] === true);
|
|
701
|
+
}
|
|
702
|
+
/** Returns a defensive copy of the key metadata assigned to one top-level column. */
|
|
703
|
+
function getColumnKeyMetadata(options, column) {
|
|
704
|
+
const keyMetadata = options.columnKeyMetadata?.[column.path[0]];
|
|
705
|
+
return keyMetadata ? new Uint8Array(keyMetadata) : undefined;
|
|
706
|
+
}
|
|
707
|
+
/** Adds the four-byte module length required by encrypted page and Bloom-filter modules. */
|
|
708
|
+
function prefixEncryptedModule(module) {
|
|
709
|
+
const prefixed = new Uint8Array(module.byteLength + 4);
|
|
710
|
+
writeUInt32LE(prefixed, module.byteLength, 0);
|
|
711
|
+
prefixed.set(module, 4);
|
|
712
|
+
return prefixed;
|
|
713
|
+
}
|
|
714
|
+
/** Encrypts one column's dictionary and data pages using the Parquet module AAD rules. */
|
|
715
|
+
async function encryptParquetColumnPages(pageRecords, options, fileUnique, rowGroupOrdinal, columnOrdinal, keyMetadata) {
|
|
716
|
+
const encryptedPages = [];
|
|
717
|
+
let dataPageOrdinal = 0;
|
|
718
|
+
const algorithm = options.algorithm ?? 'AES_GCM_V1';
|
|
719
|
+
for (const pageRecord of pageRecords) {
|
|
720
|
+
const moduleType = pageRecord.dictionary ? 'dictionary-page' : 'data-page';
|
|
721
|
+
const headerModuleType = pageRecord.dictionary ? 'dictionary-page-header' : 'data-page-header';
|
|
722
|
+
const header = await encryptParquetModule(pageRecord.page.subarray(0, pageRecord.headerSize), {
|
|
723
|
+
algorithm,
|
|
724
|
+
aad: createParquetModuleAad(options.aadPrefix, fileUnique, headerModuleType, rowGroupOrdinal, columnOrdinal, pageRecord.dictionary ? undefined : dataPageOrdinal),
|
|
725
|
+
keyMetadata: keyMetadata ?? options.keyMetadata,
|
|
726
|
+
keyRetriever: options.keyRetriever
|
|
727
|
+
});
|
|
728
|
+
const body = await encryptParquetModule(pageRecord.page.subarray(pageRecord.headerSize), {
|
|
729
|
+
algorithm,
|
|
730
|
+
aad: createParquetModuleAad(options.aadPrefix, fileUnique, moduleType, rowGroupOrdinal, columnOrdinal, pageRecord.dictionary ? undefined : dataPageOrdinal),
|
|
731
|
+
keyMetadata: keyMetadata ?? options.keyMetadata,
|
|
732
|
+
keyRetriever: options.keyRetriever,
|
|
733
|
+
page: true
|
|
734
|
+
});
|
|
735
|
+
encryptedPages.push(concatUint8Arrays([prefixEncryptedModule(header), prefixEncryptedModule(body)]));
|
|
736
|
+
if (!pageRecord.dictionary)
|
|
737
|
+
dataPageOrdinal++;
|
|
738
|
+
}
|
|
739
|
+
return encryptedPages;
|
|
740
|
+
}
|
|
741
|
+
/** Encrypts a split-block Bloom filter header and bitset as independent Parquet modules. */
|
|
742
|
+
async function encryptParquetBloomFilter(bloomFilter, options, fileUnique, rowGroupOrdinal, columnOrdinal, keyMetadata) {
|
|
743
|
+
const parsed = decodeParquetSplitBlockBloomFilter(bloomFilter);
|
|
744
|
+
const algorithm = options.algorithm ?? 'AES_GCM_V1';
|
|
745
|
+
const keyOptions = {
|
|
746
|
+
algorithm,
|
|
747
|
+
keyMetadata: keyMetadata ?? options.keyMetadata,
|
|
748
|
+
keyRetriever: options.keyRetriever
|
|
749
|
+
};
|
|
750
|
+
const header = await encryptParquetModule(bloomFilter.subarray(0, parsed.headerByteLength), {
|
|
751
|
+
...keyOptions,
|
|
752
|
+
aad: createParquetModuleAad(options.aadPrefix, fileUnique, 'bloom-filter-header', rowGroupOrdinal, columnOrdinal)
|
|
753
|
+
});
|
|
754
|
+
const bitset = await encryptParquetModule(parsed.bitset, {
|
|
755
|
+
...keyOptions,
|
|
756
|
+
aad: createParquetModuleAad(options.aadPrefix, fileUnique, 'bloom-filter-bitset', rowGroupOrdinal, columnOrdinal)
|
|
757
|
+
});
|
|
758
|
+
return concatUint8Arrays([prefixEncryptedModule(header), prefixEncryptedModule(bitset)]);
|
|
759
|
+
}
|
|
760
|
+
/** Encrypts column and offset indexes independently while preserving their footer offsets. */
|
|
761
|
+
async function encryptParquetPageIndexes(pageIndexes, options, fileUnique, rowGroupOrdinal, columnOrdinal, keyMetadata) {
|
|
762
|
+
const algorithm = options.algorithm ?? 'AES_GCM_V1';
|
|
763
|
+
const encryptIndex = (bytes, module) => encryptParquetModule(bytes, {
|
|
764
|
+
algorithm,
|
|
765
|
+
aad: createParquetModuleAad(options.aadPrefix, fileUnique, module, rowGroupOrdinal, columnOrdinal),
|
|
766
|
+
keyMetadata: keyMetadata ?? options.keyMetadata,
|
|
767
|
+
keyRetriever: options.keyRetriever
|
|
768
|
+
});
|
|
769
|
+
return {
|
|
770
|
+
offsetIndex: await encryptIndex(pageIndexes.offsetIndex, 'offset-index'),
|
|
771
|
+
columnIndex: pageIndexes.columnIndex
|
|
772
|
+
? await encryptIndex(pageIndexes.columnIndex, 'column-index')
|
|
773
|
+
: undefined
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/** Builds page indexes for primitive leaves, including nested and repeated leaves. */
|
|
777
|
+
function createColumnPageIndexes(column, plannedPages, pageLocations, int96AsTimestamp = false) {
|
|
778
|
+
if (!column.primitiveType ||
|
|
779
|
+
!isPageIndexPhysicalType(column.primitiveType) ||
|
|
780
|
+
plannedPages.length !== pageLocations.length ||
|
|
781
|
+
plannedPages.length === 0) {
|
|
782
|
+
return undefined;
|
|
783
|
+
}
|
|
784
|
+
const offsetIndex = serializeThrift(new OffsetIndex({ page_locations: pageLocations }));
|
|
785
|
+
if (!supportsStatisticsSortOrder(column)) {
|
|
786
|
+
return { offsetIndex };
|
|
787
|
+
}
|
|
788
|
+
const nullPages = [];
|
|
789
|
+
const minValues = [];
|
|
790
|
+
const maxValues = [];
|
|
791
|
+
// A repeated leaf's definition-level count is not a row-level null count.
|
|
792
|
+
// Omit null_counts for those leaves so readers do not interpret entry counts
|
|
793
|
+
// as null counts while still benefiting from min/max page pruning.
|
|
794
|
+
const nullCounts = column.rLevelMax === 0 ? [] : undefined;
|
|
795
|
+
for (const plannedPage of plannedPages) {
|
|
796
|
+
const values = plannedPage.data.values;
|
|
797
|
+
const nullCount = plannedPage.data.count - values.length;
|
|
798
|
+
nullCounts?.push(nullCount);
|
|
799
|
+
if (values.length === 0) {
|
|
800
|
+
nullPages.push(true);
|
|
801
|
+
minValues.push(new Uint8Array(0));
|
|
802
|
+
maxValues.push(new Uint8Array(0));
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
const bounds = getPageBounds(values, column.primitiveType, column.typeLength, int96AsTimestamp);
|
|
806
|
+
if (!bounds)
|
|
807
|
+
return undefined;
|
|
808
|
+
nullPages.push(false);
|
|
809
|
+
minValues.push(bounds.min);
|
|
810
|
+
maxValues.push(bounds.max);
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
offsetIndex,
|
|
814
|
+
columnIndex: serializeThrift(new ColumnIndex({
|
|
815
|
+
null_pages: nullPages,
|
|
816
|
+
min_values: minValues,
|
|
817
|
+
max_values: maxValues,
|
|
818
|
+
boundary_order: BoundaryOrder.UNORDERED,
|
|
819
|
+
null_counts: nullCounts
|
|
820
|
+
}))
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function isPageIndexPhysicalType(physicalType) {
|
|
824
|
+
return (physicalType === 'BOOLEAN' ||
|
|
825
|
+
physicalType === 'INT32' ||
|
|
826
|
+
physicalType === 'INT64' ||
|
|
827
|
+
physicalType === 'INT96' ||
|
|
828
|
+
physicalType === 'FLOAT' ||
|
|
829
|
+
physicalType === 'DOUBLE' ||
|
|
830
|
+
physicalType === 'BYTE_ARRAY' ||
|
|
831
|
+
physicalType === 'FIXED_LEN_BYTE_ARRAY');
|
|
832
|
+
}
|
|
833
|
+
function getPageBounds(values, physicalType, typeLength, int96AsTimestamp = false) {
|
|
834
|
+
if (physicalType === 'INT96' && !int96AsTimestamp)
|
|
835
|
+
return undefined;
|
|
836
|
+
const normalizedValues = Array.from(values, value => normalizePageIndexValue(value, physicalType));
|
|
837
|
+
if (normalizedValues.some(value => value === undefined))
|
|
838
|
+
return undefined;
|
|
839
|
+
const comparableValues = normalizedValues;
|
|
840
|
+
let min = comparableValues[0];
|
|
841
|
+
let max = comparableValues[0];
|
|
842
|
+
for (const value of comparableValues.slice(1)) {
|
|
843
|
+
if (comparePageIndexValues(value, min, physicalType) < 0)
|
|
844
|
+
min = value;
|
|
845
|
+
if (comparePageIndexValues(value, max, physicalType) > 0)
|
|
846
|
+
max = value;
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
return {
|
|
850
|
+
min: encodePageIndexValue(min, physicalType, typeLength, int96AsTimestamp),
|
|
851
|
+
max: encodePageIndexValue(max, physicalType, typeLength, int96AsTimestamp)
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
catch {
|
|
855
|
+
return undefined;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function encodePageIndexValue(value, physicalType, typeLength, int96AsTimestamp) {
|
|
859
|
+
if (physicalType === 'INT96') {
|
|
860
|
+
if (!int96AsTimestamp) {
|
|
861
|
+
throw new Error('INT96 page indexes require timestamp mode');
|
|
862
|
+
}
|
|
863
|
+
return PARQUET_CODECS.PLAIN.encodeValues('INT96', [value], { int96AsTimestamp });
|
|
864
|
+
}
|
|
865
|
+
return encodeParquetBloomFilterValue(value, physicalType, typeLength);
|
|
866
|
+
}
|
|
867
|
+
function normalizePageIndexValue(value, physicalType) {
|
|
868
|
+
if (physicalType === 'BYTE_ARRAY' || physicalType === 'FIXED_LEN_BYTE_ARRAY') {
|
|
869
|
+
if (typeof value === 'string')
|
|
870
|
+
return new TextEncoder().encode(value);
|
|
871
|
+
if (value instanceof ArrayBuffer)
|
|
872
|
+
return new Uint8Array(value);
|
|
873
|
+
if (ArrayBuffer.isView(value)) {
|
|
874
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength).slice();
|
|
875
|
+
}
|
|
876
|
+
return undefined;
|
|
877
|
+
}
|
|
878
|
+
if (typeof value === 'boolean' ||
|
|
879
|
+
typeof value === 'number' ||
|
|
880
|
+
typeof value === 'bigint' ||
|
|
881
|
+
typeof value === 'string') {
|
|
882
|
+
return value;
|
|
883
|
+
}
|
|
884
|
+
return undefined;
|
|
885
|
+
}
|
|
886
|
+
function comparePageIndexValues(left, right, physicalType) {
|
|
887
|
+
if (physicalType === 'BYTE_ARRAY' || physicalType === 'FIXED_LEN_BYTE_ARRAY') {
|
|
888
|
+
const leftBytes = typeof left === 'string' ? new TextEncoder().encode(left) : left;
|
|
889
|
+
const rightBytes = typeof right === 'string' ? new TextEncoder().encode(right) : right;
|
|
890
|
+
if (!(leftBytes instanceof Uint8Array) || !(rightBytes instanceof Uint8Array))
|
|
891
|
+
return 0;
|
|
892
|
+
const length = Math.min(leftBytes.length, rightBytes.length);
|
|
893
|
+
for (let index = 0; index < length; index++) {
|
|
894
|
+
if (leftBytes[index] !== rightBytes[index]) {
|
|
895
|
+
return leftBytes[index] < rightBytes[index] ? -1 : 1;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return leftBytes.length - rightBytes.length;
|
|
899
|
+
}
|
|
900
|
+
if (typeof left === 'boolean' && typeof right === 'boolean')
|
|
901
|
+
return Number(left) - Number(right);
|
|
902
|
+
if (physicalType !== 'INT64' && physicalType !== 'INT96') {
|
|
903
|
+
const leftNumber = Number(left);
|
|
904
|
+
const rightNumber = Number(right);
|
|
905
|
+
return leftNumber < rightNumber ? -1 : leftNumber > rightNumber ? 1 : 0;
|
|
906
|
+
}
|
|
907
|
+
try {
|
|
908
|
+
const leftNumber = BigInt(left);
|
|
909
|
+
const rightNumber = BigInt(right);
|
|
910
|
+
return leftNumber < rightNumber ? -1 : leftNumber > rightNumber ? 1 : 0;
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
const leftNumber = Number(left);
|
|
914
|
+
const rightNumber = Number(right);
|
|
915
|
+
return leftNumber < rightNumber ? -1 : leftNumber > rightNumber ? 1 : 0;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/** Builds a Bloom filter for writer-supported physical scalar columns. */
|
|
919
|
+
function createColumnBloomFilter(column, values) {
|
|
920
|
+
const physicalType = column.primitiveType;
|
|
921
|
+
if (physicalType !== 'BOOLEAN' &&
|
|
922
|
+
physicalType !== 'INT32' &&
|
|
923
|
+
physicalType !== 'INT64' &&
|
|
924
|
+
physicalType !== 'FLOAT' &&
|
|
925
|
+
physicalType !== 'DOUBLE' &&
|
|
926
|
+
physicalType !== 'BYTE_ARRAY' &&
|
|
927
|
+
physicalType !== 'FIXED_LEN_BYTE_ARRAY') {
|
|
928
|
+
return undefined;
|
|
929
|
+
}
|
|
930
|
+
const bloomValues = values.map(value => {
|
|
931
|
+
if (value instanceof ArrayBuffer)
|
|
932
|
+
return new Uint8Array(value);
|
|
933
|
+
if (ArrayBuffer.isView(value)) {
|
|
934
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength).slice();
|
|
935
|
+
}
|
|
936
|
+
return value;
|
|
937
|
+
});
|
|
938
|
+
return encodeParquetSplitBlockBloomFilter(bloomValues, physicalType, column.typeLength);
|
|
939
|
+
}
|
|
940
|
+
/** Builds optional size statistics from one shredded column chunk. */
|
|
941
|
+
function createSizeStatistics(column, data) {
|
|
942
|
+
const repetitionLevelHistogram = createLevelHistogram(data.rlevels, column.rLevelMax);
|
|
943
|
+
const definitionLevelHistogram = createLevelHistogram(data.dlevels, column.dLevelMax);
|
|
944
|
+
let unencodedByteArrayDataBytes = 0;
|
|
945
|
+
if (column.primitiveType === 'BYTE_ARRAY') {
|
|
946
|
+
for (const value of data.values) {
|
|
947
|
+
if (typeof value === 'string') {
|
|
948
|
+
unencodedByteArrayDataBytes += encodeUtf8(value).byteLength;
|
|
949
|
+
}
|
|
950
|
+
else if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
|
|
951
|
+
unencodedByteArrayDataBytes += value.byteLength;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return new SizeStatistics({
|
|
956
|
+
unencoded_byte_array_data_bytes: column.primitiveType === 'BYTE_ARRAY' ? unencodedByteArrayDataBytes : undefined,
|
|
957
|
+
repetition_level_histogram: repetitionLevelHistogram,
|
|
958
|
+
definition_level_histogram: definitionLevelHistogram
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
/** Returns whether standard column statistics are enabled for one top-level field. */
|
|
962
|
+
function isStatisticsEnabled(option, column) {
|
|
963
|
+
return option === true || option?.[column.path[0]] === true;
|
|
964
|
+
}
|
|
965
|
+
/** Builds conservative standard min/max/null-count statistics for one column chunk. */
|
|
966
|
+
function createColumnStatistics(column, data) {
|
|
967
|
+
if (!column.primitiveType ||
|
|
968
|
+
!isPageIndexPhysicalType(column.primitiveType) ||
|
|
969
|
+
!supportsStatisticsSortOrder(column)) {
|
|
970
|
+
return undefined;
|
|
971
|
+
}
|
|
972
|
+
const statistics = new Statistics({
|
|
973
|
+
null_count: column.rLevelMax === 0 ? data.count - data.values.length : undefined,
|
|
974
|
+
is_min_value_exact: true,
|
|
975
|
+
is_max_value_exact: true
|
|
976
|
+
});
|
|
977
|
+
if (data.values.length === 0)
|
|
978
|
+
return statistics;
|
|
979
|
+
const bounds = getPageBounds(data.values, column.primitiveType, column.typeLength);
|
|
980
|
+
if (!bounds)
|
|
981
|
+
return undefined;
|
|
982
|
+
statistics.min_value = bounds.min;
|
|
983
|
+
statistics.max_value = bounds.max;
|
|
984
|
+
return statistics;
|
|
985
|
+
}
|
|
986
|
+
/** Returns whether the physical representation preserves the logical sort order. */
|
|
987
|
+
function supportsStatisticsSortOrder(column) {
|
|
988
|
+
const logicalType = column.logicalType?.type || column.originalType;
|
|
989
|
+
if (logicalType === 'FLOAT16')
|
|
990
|
+
return false;
|
|
991
|
+
if (logicalType === 'DECIMAL' || logicalType?.startsWith('DECIMAL_')) {
|
|
992
|
+
return column.primitiveType !== 'BYTE_ARRAY' && column.primitiveType !== 'FIXED_LEN_BYTE_ARRAY';
|
|
993
|
+
}
|
|
994
|
+
if (logicalType === 'INTEGER' || logicalType?.startsWith('UINT_')) {
|
|
995
|
+
return column.logicalType?.isSigned !== false && !logicalType.startsWith('UINT_');
|
|
996
|
+
}
|
|
997
|
+
return true;
|
|
998
|
+
}
|
|
999
|
+
/** Counts the occurrences of each definition or repetition level. */
|
|
1000
|
+
function createLevelHistogram(levels, maximumLevel) {
|
|
1001
|
+
const histogram = new Array(maximumLevel + 1).fill(0);
|
|
1002
|
+
for (const level of levels) {
|
|
1003
|
+
if (level >= 0 && level <= maximumLevel)
|
|
1004
|
+
histogram[level]++;
|
|
1005
|
+
}
|
|
1006
|
+
return histogram;
|
|
1007
|
+
}
|
|
1008
|
+
/** Converts writer-facing sort keys into Parquet leaf-column indexes. */
|
|
1009
|
+
function createSortingColumns(schema, sortingColumns) {
|
|
1010
|
+
if (!sortingColumns?.length)
|
|
1011
|
+
return undefined;
|
|
1012
|
+
const leafFields = schema.fieldList.filter(field => !field.isNested);
|
|
1013
|
+
const usedColumnIndexes = new Set();
|
|
1014
|
+
return sortingColumns.map(sortKey => {
|
|
1015
|
+
const columnIndex = leafFields.findIndex(field => (field.path.length === 1 && field.name === sortKey.column) ||
|
|
1016
|
+
(field.path.length > 1 && field.path.join('.') === sortKey.column));
|
|
1017
|
+
if (columnIndex < 0) {
|
|
1018
|
+
throw new Error(`Unknown Parquet sorting column ${sortKey.column}`);
|
|
1019
|
+
}
|
|
1020
|
+
if (usedColumnIndexes.has(columnIndex)) {
|
|
1021
|
+
throw new Error(`Duplicate Parquet sorting column ${sortKey.column}`);
|
|
1022
|
+
}
|
|
1023
|
+
usedColumnIndexes.add(columnIndex);
|
|
1024
|
+
return new SortingColumn({
|
|
1025
|
+
column_idx: columnIndex,
|
|
1026
|
+
descending: Boolean(sortKey.descending),
|
|
1027
|
+
nulls_first: Boolean(sortKey.nullsFirst)
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
465
1030
|
}
|
|
466
1031
|
/**
|
|
467
1032
|
* Encode a list of column values into a parquet row group
|
|
@@ -470,17 +1035,28 @@ async function encodeRowGroup(schema, data, opts) {
|
|
|
470
1035
|
const metadata = new RowGroup({
|
|
471
1036
|
num_rows: int64(data.rowCount),
|
|
472
1037
|
columns: [],
|
|
473
|
-
total_byte_size: int64(0)
|
|
1038
|
+
total_byte_size: int64(0),
|
|
1039
|
+
sorting_columns: createSortingColumns(schema, opts.sortingColumns)
|
|
474
1040
|
});
|
|
475
1041
|
let body = new Uint8Array(0);
|
|
476
1042
|
for (const field of schema.fieldList) {
|
|
477
1043
|
if (field.isNested) {
|
|
478
1044
|
continue; // eslint-disable-line no-continue
|
|
479
1045
|
}
|
|
480
|
-
const cchunkData = await encodeColumnChunk(field, data, body.length, opts);
|
|
1046
|
+
const cchunkData = await encodeColumnChunk(field, data, body.length, opts, opts.rowGroupOrdinal ?? 0, metadata.columns.length);
|
|
481
1047
|
const cchunk = new ColumnChunk({
|
|
482
1048
|
file_offset: int64(cchunkData.metadataOffset),
|
|
483
|
-
meta_data: cchunkData.metadata
|
|
1049
|
+
meta_data: cchunkData.encryptedColumnMetadata ? undefined : cchunkData.metadata,
|
|
1050
|
+
offset_index_offset: cchunkData.offsetIndexOffset === undefined
|
|
1051
|
+
? undefined
|
|
1052
|
+
: int64(cchunkData.offsetIndexOffset),
|
|
1053
|
+
offset_index_length: cchunkData.offsetIndexLength,
|
|
1054
|
+
column_index_offset: cchunkData.columnIndexOffset === undefined
|
|
1055
|
+
? undefined
|
|
1056
|
+
: int64(cchunkData.columnIndexOffset),
|
|
1057
|
+
column_index_length: cchunkData.columnIndexLength,
|
|
1058
|
+
crypto_metadata: cchunkData.cryptoMetadata,
|
|
1059
|
+
encrypted_column_metadata: cchunkData.encryptedColumnMetadata
|
|
484
1060
|
});
|
|
485
1061
|
metadata.columns.push(cchunk);
|
|
486
1062
|
metadata.total_byte_size = int64(Number(metadata.total_byte_size) + cchunkData.body.length);
|
|
@@ -491,14 +1067,38 @@ async function encodeRowGroup(schema, data, opts) {
|
|
|
491
1067
|
/**
|
|
492
1068
|
* Encode a parquet file metadata footer
|
|
493
1069
|
*/
|
|
494
|
-
function
|
|
1070
|
+
function encodeFooterMetadata(schema, rowCount, rowGroups, userMetadata, footerSignature) {
|
|
1071
|
+
const signatureAlgorithm = footerSignature?.algorithm ?? 'AES_GCM_V1';
|
|
1072
|
+
const signatureFileUnique = footerSignature?.fileUnique
|
|
1073
|
+
? new Uint8Array(footerSignature.fileUnique)
|
|
1074
|
+
: undefined;
|
|
495
1075
|
const metadata = new FileMetaData({
|
|
496
1076
|
version: PARQUET_VERSION,
|
|
497
1077
|
created_by: 'parquets',
|
|
498
1078
|
num_rows: int64(rowCount),
|
|
499
1079
|
row_groups: rowGroups,
|
|
500
1080
|
schema: [],
|
|
501
|
-
key_value_metadata: []
|
|
1081
|
+
key_value_metadata: [],
|
|
1082
|
+
encryption_algorithm: footerSignature
|
|
1083
|
+
? new EncryptionAlgorithm({
|
|
1084
|
+
...(signatureAlgorithm === 'AES_GCM_CTR_V1'
|
|
1085
|
+
? {
|
|
1086
|
+
AES_GCM_CTR_V1: {
|
|
1087
|
+
aad_prefix: footerSignature.aadPrefix,
|
|
1088
|
+
aad_file_unique: signatureFileUnique
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
: {
|
|
1092
|
+
AES_GCM_V1: {
|
|
1093
|
+
aad_prefix: footerSignature.aadPrefix,
|
|
1094
|
+
aad_file_unique: signatureFileUnique
|
|
1095
|
+
}
|
|
1096
|
+
})
|
|
1097
|
+
})
|
|
1098
|
+
: undefined,
|
|
1099
|
+
footer_signing_key_metadata: footerSignature
|
|
1100
|
+
? new Uint8Array(footerSignature.keyMetadata)
|
|
1101
|
+
: undefined
|
|
502
1102
|
});
|
|
503
1103
|
for (const key in userMetadata) {
|
|
504
1104
|
const kv = new KeyValue({
|
|
@@ -542,13 +1142,70 @@ function encodeFooter(schema, rowCount, rowGroups, userMetadata) {
|
|
|
542
1142
|
}
|
|
543
1143
|
metadata.schema.push(schemaElem);
|
|
544
1144
|
}
|
|
545
|
-
|
|
1145
|
+
return serializeThrift(metadata);
|
|
1146
|
+
}
|
|
1147
|
+
/** Encodes an unsigned plaintext footer with its standard Parquet trailer. */
|
|
1148
|
+
function encodeFooter(schema, rowCount, rowGroups, userMetadata) {
|
|
1149
|
+
const metadataEncoded = encodeFooterMetadata(schema, rowCount, rowGroups, userMetadata);
|
|
546
1150
|
const footerEncoded = new Uint8Array(metadataEncoded.length + 8);
|
|
547
1151
|
footerEncoded.set(metadataEncoded);
|
|
548
1152
|
writeUInt32LE(footerEncoded, metadataEncoded.length, metadataEncoded.length);
|
|
549
1153
|
footerEncoded.set(PARQUET_MAGIC_BYTES, metadataEncoded.length + 4);
|
|
550
1154
|
return footerEncoded;
|
|
551
1155
|
}
|
|
1156
|
+
/** Encodes a plaintext footer with its trailing 28-byte authenticated signature. */
|
|
1157
|
+
async function encodeSignedFooter(schema, rowCount, rowGroups, userMetadata, options) {
|
|
1158
|
+
const metadata = encodeFooterMetadata(schema, rowCount, rowGroups, userMetadata, options);
|
|
1159
|
+
const signature = await createParquetFooterSignature(metadata, options);
|
|
1160
|
+
const footerEncoded = new Uint8Array(metadata.length + signature.length + 8);
|
|
1161
|
+
footerEncoded.set(metadata);
|
|
1162
|
+
footerEncoded.set(signature, metadata.length);
|
|
1163
|
+
writeUInt32LE(footerEncoded, metadata.length + signature.length, metadata.length + signature.length);
|
|
1164
|
+
footerEncoded.set(PARQUET_MAGIC_BYTES, metadata.length + signature.length + 4);
|
|
1165
|
+
return footerEncoded;
|
|
1166
|
+
}
|
|
1167
|
+
/** Wraps a serialized footer in Parquet modular-encryption metadata and AES-GCM. */
|
|
1168
|
+
async function encodeEncryptedFooter(footer, options) {
|
|
1169
|
+
const algorithm = options.algorithm ?? 'AES_GCM_V1';
|
|
1170
|
+
const fileUnique = options.fileUnique ? new Uint8Array(options.fileUnique) : createFileUnique();
|
|
1171
|
+
if (fileUnique.byteLength !== 8) {
|
|
1172
|
+
throw new Error(`Parquet encrypted footer file_unique must be 8 bytes`);
|
|
1173
|
+
}
|
|
1174
|
+
const algorithmParameters = {
|
|
1175
|
+
aad_prefix: options.aadPrefix && new Uint8Array(options.aadPrefix),
|
|
1176
|
+
aad_file_unique: fileUnique,
|
|
1177
|
+
supply_aad_prefix: undefined
|
|
1178
|
+
};
|
|
1179
|
+
const encryptionAlgorithm = algorithm === 'AES_GCM_CTR_V1'
|
|
1180
|
+
? new EncryptionAlgorithm({ AES_GCM_CTR_V1: algorithmParameters })
|
|
1181
|
+
: new EncryptionAlgorithm({ AES_GCM_V1: algorithmParameters });
|
|
1182
|
+
const cryptoMetadata = serializeThrift(new FileCryptoMetaData({
|
|
1183
|
+
encryption_algorithm: encryptionAlgorithm,
|
|
1184
|
+
key_metadata: options.keyMetadata && new Uint8Array(options.keyMetadata)
|
|
1185
|
+
}));
|
|
1186
|
+
const footerMetadata = footer.subarray(0, footer.byteLength - 8);
|
|
1187
|
+
const encryptedFooter = await encryptParquetModule(footerMetadata, {
|
|
1188
|
+
algorithm,
|
|
1189
|
+
aad: createParquetModuleAad(options.aadPrefix, fileUnique, 'footer'),
|
|
1190
|
+
keyMetadata: options.keyMetadata,
|
|
1191
|
+
keyRetriever: options.keyRetriever
|
|
1192
|
+
});
|
|
1193
|
+
const metadataSize = cryptoMetadata.byteLength + encryptedFooter.byteLength;
|
|
1194
|
+
const trailer = new Uint8Array(8);
|
|
1195
|
+
writeUInt32LE(trailer, metadataSize, 0);
|
|
1196
|
+
trailer.set(encodeUtf8(PARQUET_MAGIC_ENCRYPTED), 4);
|
|
1197
|
+
return concatUint8Arrays([cryptoMetadata, encryptedFooter, trailer]);
|
|
1198
|
+
}
|
|
1199
|
+
/** Generates the eight-byte file identifier required by Parquet module AAD. */
|
|
1200
|
+
function createFileUnique() {
|
|
1201
|
+
const fileUnique = new Uint8Array(8);
|
|
1202
|
+
const cryptoProvider = globalThis.crypto;
|
|
1203
|
+
if (!cryptoProvider?.getRandomValues) {
|
|
1204
|
+
throw new Error('Parquet encrypted footer requires Web Crypto random values');
|
|
1205
|
+
}
|
|
1206
|
+
cryptoProvider.getRandomValues(fileUnique);
|
|
1207
|
+
return fileUnique;
|
|
1208
|
+
}
|
|
552
1209
|
/** Returns an explicit or inferred modern logical annotation for a writer field. */
|
|
553
1210
|
function getFieldLogicalType(field) {
|
|
554
1211
|
if (field.logicalType)
|