@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.
Files changed (336) hide show
  1. package/LICENSE +1 -1
  2. package/dist/bundled.d.ts +1 -1
  3. package/dist/bundled.d.ts.map +1 -1
  4. package/dist/bundled.js.map +1 -1
  5. package/dist/delta-format.d.ts +9 -0
  6. package/dist/delta-format.d.ts.map +1 -0
  7. package/dist/delta-format.js +12 -0
  8. package/dist/delta-format.js.map +1 -0
  9. package/dist/delta-source-loader-types.d.ts +27 -0
  10. package/dist/delta-source-loader-types.d.ts.map +1 -0
  11. package/dist/delta-source-loader-types.js +28 -0
  12. package/dist/delta-source-loader-types.js.map +1 -0
  13. package/dist/delta-source.d.ts +48 -0
  14. package/dist/delta-source.d.ts.map +1 -0
  15. package/dist/delta-source.js +222 -0
  16. package/dist/delta-source.js.map +1 -0
  17. package/dist/delta-types.d.ts +38 -0
  18. package/dist/delta-types.d.ts.map +1 -0
  19. package/dist/delta-types.js +5 -0
  20. package/dist/delta-types.js.map +1 -0
  21. package/dist/geoparquet-loader.js +1 -1
  22. package/dist/iceberg-rest-catalog.d.ts +49 -0
  23. package/dist/iceberg-rest-catalog.d.ts.map +1 -0
  24. package/dist/iceberg-rest-catalog.js +78 -0
  25. package/dist/iceberg-rest-catalog.js.map +1 -0
  26. package/dist/iceberg-table-source.d.ts +63 -0
  27. package/dist/iceberg-table-source.d.ts.map +1 -0
  28. package/dist/iceberg-table-source.js +797 -0
  29. package/dist/iceberg-table-source.js.map +1 -0
  30. package/dist/iceberg-types.d.ts +150 -0
  31. package/dist/iceberg-types.d.ts.map +1 -0
  32. package/dist/iceberg-types.js +5 -0
  33. package/dist/iceberg-types.js.map +1 -0
  34. package/dist/index.cjs +15841 -3306
  35. package/dist/index.cjs.map +3 -3
  36. package/dist/index.d.ts +8 -3
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +5 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/lib/arrow/convert-schema-from-parquet.d.ts +3 -1
  41. package/dist/lib/arrow/convert-schema-from-parquet.d.ts.map +1 -1
  42. package/dist/lib/arrow/convert-schema-from-parquet.js +94 -19
  43. package/dist/lib/arrow/convert-schema-from-parquet.js.map +1 -1
  44. package/dist/lib/arrow/create-nested-arrow-vector.js +89 -0
  45. package/dist/lib/arrow/create-nested-arrow-vector.js.map +1 -1
  46. package/dist/lib/constants.js +1 -1
  47. package/dist/lib/encoders/encode-table-to-parquet-js.d.ts.map +1 -1
  48. package/dist/lib/encoders/encode-table-to-parquet-js.js +202 -7
  49. package/dist/lib/encoders/encode-table-to-parquet-js.js.map +1 -1
  50. package/dist/lib/geo/geoparquet-covering.d.ts +19 -0
  51. package/dist/lib/geo/geoparquet-covering.d.ts.map +1 -0
  52. package/dist/lib/geo/geoparquet-covering.js +137 -0
  53. package/dist/lib/geo/geoparquet-covering.js.map +1 -0
  54. package/dist/lib/geo/geospatial-metadata.d.ts.map +1 -1
  55. package/dist/lib/geo/geospatial-metadata.js +39 -13
  56. package/dist/lib/geo/geospatial-metadata.js.map +1 -1
  57. package/dist/lib/parquet-bloom-filter-planner.d.ts +16 -0
  58. package/dist/lib/parquet-bloom-filter-planner.d.ts.map +1 -0
  59. package/dist/lib/parquet-bloom-filter-planner.js +57 -0
  60. package/dist/lib/parquet-bloom-filter-planner.js.map +1 -0
  61. package/dist/lib/parquet-bloom-filter.d.ts +30 -0
  62. package/dist/lib/parquet-bloom-filter.d.ts.map +1 -0
  63. package/dist/lib/parquet-bloom-filter.js +289 -0
  64. package/dist/lib/parquet-bloom-filter.js.map +1 -0
  65. package/dist/lib/parquet-encryption.d.ts +80 -0
  66. package/dist/lib/parquet-encryption.d.ts.map +1 -0
  67. package/dist/lib/parquet-encryption.js +190 -0
  68. package/dist/lib/parquet-encryption.js.map +1 -0
  69. package/dist/lib/parquet-page-index.d.ts +34 -5
  70. package/dist/lib/parquet-page-index.d.ts.map +1 -1
  71. package/dist/lib/parquet-page-index.js +114 -28
  72. package/dist/lib/parquet-page-index.js.map +1 -1
  73. package/dist/lib/parquet-predicate.d.ts +5 -1
  74. package/dist/lib/parquet-predicate.d.ts.map +1 -1
  75. package/dist/lib/parquet-predicate.js +42 -14
  76. package/dist/lib/parquet-predicate.js.map +1 -1
  77. package/dist/lib/parquet-source-worker-decoder.d.ts +4 -1
  78. package/dist/lib/parquet-source-worker-decoder.d.ts.map +1 -1
  79. package/dist/lib/parquet-source-worker-decoder.js +83 -17
  80. package/dist/lib/parquet-source-worker-decoder.js.map +1 -1
  81. package/dist/lib/parquet-source-worker-types.d.ts +24 -0
  82. package/dist/lib/parquet-source-worker-types.d.ts.map +1 -1
  83. package/dist/lib/parsers/get-parquet-schema.d.ts.map +1 -1
  84. package/dist/lib/parsers/get-parquet-schema.js +3 -1
  85. package/dist/lib/parsers/get-parquet-schema.js.map +1 -1
  86. package/dist/lib/parsers/parse-parquet-to-arrow-js.d.ts.map +1 -1
  87. package/dist/lib/parsers/parse-parquet-to-arrow-js.js +327 -18
  88. package/dist/lib/parsers/parse-parquet-to-arrow-js.js.map +1 -1
  89. package/dist/lib/parsers/parse-parquet-to-arrow.d.ts.map +1 -1
  90. package/dist/lib/parsers/parse-parquet-to-arrow.js +5 -5
  91. package/dist/lib/parsers/parse-parquet-to-arrow.js.map +1 -1
  92. package/dist/lib/parsers/parse-parquet-to-columns.d.ts.map +1 -1
  93. package/dist/lib/parsers/parse-parquet-to-columns.js +3 -1
  94. package/dist/lib/parsers/parse-parquet-to-columns.js.map +1 -1
  95. package/dist/lib/parsers/parse-parquet-to-json.d.ts.map +1 -1
  96. package/dist/lib/parsers/parse-parquet-to-json.js +18 -2
  97. package/dist/lib/parsers/parse-parquet-to-json.js.map +1 -1
  98. package/dist/lib/slice-parquet-batch.d.ts +9 -0
  99. package/dist/lib/slice-parquet-batch.d.ts.map +1 -0
  100. package/dist/lib/slice-parquet-batch.js +28 -0
  101. package/dist/lib/slice-parquet-batch.js.map +1 -0
  102. package/dist/lib/sources/parquet-range-file.d.ts +1 -1
  103. package/dist/lib/sources/parquet-range-file.d.ts.map +1 -1
  104. package/dist/lib/sources/parquet-range-file.js +28 -22
  105. package/dist/lib/sources/parquet-range-file.js.map +1 -1
  106. package/dist/lib/utils/normalize-parquet-options.d.ts.map +1 -1
  107. package/dist/lib/utils/normalize-parquet-options.js +15 -1
  108. package/dist/lib/utils/normalize-parquet-options.js.map +1 -1
  109. package/dist/parquet-dataset-source.cjs +16528 -4928
  110. package/dist/parquet-dataset-source.cjs.map +3 -3
  111. package/dist/parquet-dataset-source.d.ts +31 -4
  112. package/dist/parquet-dataset-source.d.ts.map +1 -1
  113. package/dist/parquet-dataset-source.js +296 -74
  114. package/dist/parquet-dataset-source.js.map +1 -1
  115. package/dist/parquet-encoded-page-types.d.ts +108 -0
  116. package/dist/parquet-encoded-page-types.d.ts.map +1 -0
  117. package/dist/parquet-encoded-page-types.js +5 -0
  118. package/dist/parquet-encoded-page-types.js.map +1 -0
  119. package/dist/parquet-js-loader-types.d.ts +2 -0
  120. package/dist/parquet-js-loader-types.d.ts.map +1 -1
  121. package/dist/parquet-js-loader-types.js +2 -1
  122. package/dist/parquet-js-loader-types.js.map +1 -1
  123. package/dist/parquet-js-loader.d.ts +1 -0
  124. package/dist/parquet-js-loader.d.ts.map +1 -1
  125. package/dist/parquet-js-loader.js +2 -1
  126. package/dist/parquet-js-loader.js.map +1 -1
  127. package/dist/parquet-js-writer.d.ts +23 -2
  128. package/dist/parquet-js-writer.d.ts.map +1 -1
  129. package/dist/parquet-js-writer.js +1 -1
  130. package/dist/parquet-js-writer.js.map +1 -1
  131. package/dist/parquet-loader-base.js +1 -1
  132. package/dist/parquet-loader-options.d.ts +11 -0
  133. package/dist/parquet-loader-options.d.ts.map +1 -1
  134. package/dist/parquet-loader-options.js.map +1 -1
  135. package/dist/parquet-source-capabilities.d.ts +8 -1
  136. package/dist/parquet-source-capabilities.d.ts.map +1 -1
  137. package/dist/parquet-source-capabilities.js +10 -0
  138. package/dist/parquet-source-capabilities.js.map +1 -1
  139. package/dist/parquet-source-loader-types.d.ts +10 -0
  140. package/dist/parquet-source-loader-types.d.ts.map +1 -1
  141. package/dist/parquet-source-loader-types.js +11 -1
  142. package/dist/parquet-source-loader-types.js.map +1 -1
  143. package/dist/parquet-source-loader.cjs +14121 -2796
  144. package/dist/parquet-source-loader.cjs.map +3 -3
  145. package/dist/parquet-source-loader.d.ts +51 -4
  146. package/dist/parquet-source-loader.d.ts.map +1 -1
  147. package/dist/parquet-source-loader.js +651 -38
  148. package/dist/parquet-source-loader.js.map +1 -1
  149. package/dist/parquet-source-types.d.ts +180 -30
  150. package/dist/parquet-source-types.d.ts.map +1 -1
  151. package/dist/parquet-source-worker.js +7378 -6989
  152. package/dist/parquet-source-worker.js.map +4 -4
  153. package/dist/parquet-worker-node.cjs +8501 -1045
  154. package/dist/parquet-worker-node.cjs.map +4 -4
  155. package/dist/parquet-worker-url.js +5 -1
  156. package/dist/parquet-worker-url.js.map +1 -1
  157. package/dist/parquet-worker.js +289 -218
  158. package/dist/parquet-worker.js.map +4 -4
  159. package/dist/parquetjs/codecs/declare.d.ts +15 -0
  160. package/dist/parquetjs/codecs/declare.d.ts.map +1 -1
  161. package/dist/parquetjs/codecs/declare.js +10 -0
  162. package/dist/parquetjs/codecs/declare.js.map +1 -1
  163. package/dist/parquetjs/codecs/delta.d.ts.map +1 -1
  164. package/dist/parquetjs/codecs/delta.js +356 -7
  165. package/dist/parquetjs/codecs/delta.js.map +1 -1
  166. package/dist/parquetjs/codecs/index.d.ts.map +1 -1
  167. package/dist/parquetjs/codecs/index.js +4 -0
  168. package/dist/parquetjs/codecs/index.js.map +1 -1
  169. package/dist/parquetjs/codecs/plain.d.ts +1 -1
  170. package/dist/parquetjs/codecs/plain.d.ts.map +1 -1
  171. package/dist/parquetjs/codecs/plain.js +153 -12
  172. package/dist/parquetjs/codecs/plain.js.map +1 -1
  173. package/dist/parquetjs/codecs/rle.d.ts +4 -0
  174. package/dist/parquetjs/codecs/rle.d.ts.map +1 -1
  175. package/dist/parquetjs/codecs/rle.js +406 -3
  176. package/dist/parquetjs/codecs/rle.js.map +1 -1
  177. package/dist/parquetjs/compression.d.ts +9 -0
  178. package/dist/parquetjs/compression.d.ts.map +1 -1
  179. package/dist/parquetjs/compression.js +86 -67
  180. package/dist/parquetjs/compression.js.map +1 -1
  181. package/dist/parquetjs/encoder/dictionary-planner.d.ts.map +1 -1
  182. package/dist/parquetjs/encoder/dictionary-planner.js +5 -2
  183. package/dist/parquetjs/encoder/dictionary-planner.js.map +1 -1
  184. package/dist/parquetjs/encoder/parquet-encoder.d.ts +43 -1
  185. package/dist/parquetjs/encoder/parquet-encoder.d.ts.map +1 -1
  186. package/dist/parquetjs/encoder/parquet-encoder.js +691 -34
  187. package/dist/parquetjs/encoder/parquet-encoder.js.map +1 -1
  188. package/dist/parquetjs/parquet-thrift/BoundingBox.d.ts +29 -0
  189. package/dist/parquetjs/parquet-thrift/BoundingBox.d.ts.map +1 -0
  190. package/dist/parquetjs/parquet-thrift/BoundingBox.js +85 -0
  191. package/dist/parquetjs/parquet-thrift/BoundingBox.js.map +1 -0
  192. package/dist/parquetjs/parquet-thrift/ColumnChunk.d.ts +5 -0
  193. package/dist/parquetjs/parquet-thrift/ColumnChunk.d.ts.map +1 -1
  194. package/dist/parquetjs/parquet-thrift/ColumnChunk.js +35 -0
  195. package/dist/parquetjs/parquet-thrift/ColumnChunk.js.map +1 -1
  196. package/dist/parquetjs/parquet-thrift/ColumnCryptoMetaData.d.ts +35 -0
  197. package/dist/parquetjs/parquet-thrift/ColumnCryptoMetaData.d.ts.map +1 -0
  198. package/dist/parquetjs/parquet-thrift/ColumnCryptoMetaData.js +135 -0
  199. package/dist/parquetjs/parquet-thrift/ColumnCryptoMetaData.js.map +1 -0
  200. package/dist/parquetjs/parquet-thrift/ColumnMetaData.d.ts +10 -0
  201. package/dist/parquetjs/parquet-thrift/ColumnMetaData.d.ts.map +1 -1
  202. package/dist/parquetjs/parquet-thrift/ColumnMetaData.js +73 -0
  203. package/dist/parquetjs/parquet-thrift/ColumnMetaData.js.map +1 -1
  204. package/dist/parquetjs/parquet-thrift/EncryptionAlgorithm.d.ts +22 -0
  205. package/dist/parquetjs/parquet-thrift/EncryptionAlgorithm.d.ts.map +1 -0
  206. package/dist/parquetjs/parquet-thrift/EncryptionAlgorithm.js +111 -0
  207. package/dist/parquetjs/parquet-thrift/EncryptionAlgorithm.js.map +1 -0
  208. package/dist/parquetjs/parquet-thrift/FileCryptoMetaData.d.ts +16 -0
  209. package/dist/parquetjs/parquet-thrift/FileCryptoMetaData.d.ts.map +1 -0
  210. package/dist/parquetjs/parquet-thrift/FileCryptoMetaData.js +57 -0
  211. package/dist/parquetjs/parquet-thrift/FileCryptoMetaData.js.map +1 -0
  212. package/dist/parquetjs/parquet-thrift/FileMetaData.d.ts +5 -0
  213. package/dist/parquetjs/parquet-thrift/FileMetaData.d.ts.map +1 -1
  214. package/dist/parquetjs/parquet-thrift/FileMetaData.js +35 -0
  215. package/dist/parquetjs/parquet-thrift/FileMetaData.js.map +1 -1
  216. package/dist/parquetjs/parquet-thrift/GeospatialStatistics.d.ts +18 -0
  217. package/dist/parquetjs/parquet-thrift/GeospatialStatistics.d.ts.map +1 -0
  218. package/dist/parquetjs/parquet-thrift/GeospatialStatistics.js +62 -0
  219. package/dist/parquetjs/parquet-thrift/GeospatialStatistics.js.map +1 -0
  220. package/dist/parquetjs/parquet-thrift/SizeStatistics.d.ts +26 -0
  221. package/dist/parquetjs/parquet-thrift/SizeStatistics.d.ts.map +1 -0
  222. package/dist/parquetjs/parquet-thrift/SizeStatistics.js +85 -0
  223. package/dist/parquetjs/parquet-thrift/SizeStatistics.js.map +1 -0
  224. package/dist/parquetjs/parquet-thrift/index.d.ts +6 -0
  225. package/dist/parquetjs/parquet-thrift/index.d.ts.map +1 -1
  226. package/dist/parquetjs/parquet-thrift/index.js +6 -0
  227. package/dist/parquetjs/parquet-thrift/index.js.map +1 -1
  228. package/dist/parquetjs/parser/decoders.d.ts +10 -1
  229. package/dist/parquetjs/parser/decoders.d.ts.map +1 -1
  230. package/dist/parquetjs/parser/decoders.js +318 -14
  231. package/dist/parquetjs/parser/decoders.js.map +1 -1
  232. package/dist/parquetjs/parser/encoded-pages.d.ts +24 -0
  233. package/dist/parquetjs/parser/encoded-pages.d.ts.map +1 -0
  234. package/dist/parquetjs/parser/encoded-pages.js +206 -0
  235. package/dist/parquetjs/parser/encoded-pages.js.map +1 -0
  236. package/dist/parquetjs/parser/parquet-reader.d.ts +88 -12
  237. package/dist/parquetjs/parser/parquet-reader.d.ts.map +1 -1
  238. package/dist/parquetjs/parser/parquet-reader.js +713 -66
  239. package/dist/parquetjs/parser/parquet-reader.js.map +1 -1
  240. package/dist/parquetjs/schema/declare.d.ts +22 -3
  241. package/dist/parquetjs/schema/declare.d.ts.map +1 -1
  242. package/dist/parquetjs/schema/declare.js.map +1 -1
  243. package/dist/parquetjs/schema/schema.d.ts.map +1 -1
  244. package/dist/parquetjs/schema/schema.js +10 -0
  245. package/dist/parquetjs/schema/schema.js.map +1 -1
  246. package/dist/parquetjs/schema/shred.d.ts.map +1 -1
  247. package/dist/parquetjs/schema/shred.js +219 -11
  248. package/dist/parquetjs/schema/shred.js.map +1 -1
  249. package/dist/parquetjs/schema/types.js +19 -21
  250. package/dist/parquetjs/schema/types.js.map +1 -1
  251. package/dist/parquetjs/schema/variant.d.ts +3 -0
  252. package/dist/parquetjs/schema/variant.d.ts.map +1 -0
  253. package/dist/parquetjs/schema/variant.js +248 -0
  254. package/dist/parquetjs/schema/variant.js.map +1 -0
  255. package/dist/parquetjs/utils/crc32.d.ts +3 -0
  256. package/dist/parquetjs/utils/crc32.d.ts.map +1 -0
  257. package/dist/parquetjs/utils/crc32.js +15 -0
  258. package/dist/parquetjs/utils/crc32.js.map +1 -0
  259. package/dist/parquetjs/utils/read-utils.d.ts +11 -1
  260. package/dist/parquetjs/utils/read-utils.d.ts.map +1 -1
  261. package/dist/parquetjs/utils/read-utils.js +21 -1
  262. package/dist/parquetjs/utils/read-utils.js.map +1 -1
  263. package/dist/unbundled.d.ts +2 -1
  264. package/dist/unbundled.d.ts.map +1 -1
  265. package/dist/unbundled.js +1 -0
  266. package/dist/unbundled.js.map +1 -1
  267. package/package.json +36 -13
  268. package/src/bundled.ts +10 -0
  269. package/src/delta-format.ts +12 -0
  270. package/src/delta-source-loader-types.ts +40 -0
  271. package/src/delta-source.ts +305 -0
  272. package/src/delta-types.ts +40 -0
  273. package/src/iceberg-rest-catalog.ts +122 -0
  274. package/src/iceberg-table-source.ts +1037 -0
  275. package/src/iceberg-types.ts +165 -0
  276. package/src/index.ts +59 -1
  277. package/src/lib/arrow/convert-schema-from-parquet.ts +113 -19
  278. package/src/lib/arrow/create-nested-arrow-vector.ts +116 -0
  279. package/src/lib/encoders/encode-table-to-parquet-js.ts +268 -7
  280. package/src/lib/geo/geoparquet-covering.ts +201 -0
  281. package/src/lib/geo/geospatial-metadata.ts +42 -15
  282. package/src/lib/parquet-bloom-filter-planner.ts +86 -0
  283. package/src/lib/parquet-bloom-filter.ts +345 -0
  284. package/src/lib/parquet-encryption.ts +344 -0
  285. package/src/lib/parquet-page-index.ts +180 -34
  286. package/src/lib/parquet-predicate.ts +52 -16
  287. package/src/lib/parquet-source-worker-decoder.ts +116 -20
  288. package/src/lib/parquet-source-worker-types.ts +25 -0
  289. package/src/lib/parsers/get-parquet-schema.ts +3 -1
  290. package/src/lib/parsers/parse-parquet-to-arrow-js.ts +397 -17
  291. package/src/lib/parsers/parse-parquet-to-arrow.ts +4 -5
  292. package/src/lib/parsers/parse-parquet-to-columns.ts +3 -1
  293. package/src/lib/parsers/parse-parquet-to-json.ts +18 -2
  294. package/src/lib/slice-parquet-batch.ts +32 -0
  295. package/src/lib/sources/parquet-range-file.ts +35 -29
  296. package/src/lib/utils/normalize-parquet-options.ts +17 -1
  297. package/src/parquet-dataset-source.ts +397 -85
  298. package/src/parquet-encoded-page-types.ts +122 -0
  299. package/src/parquet-js-loader-types.ts +1 -0
  300. package/src/parquet-js-loader.ts +2 -1
  301. package/src/parquet-js-writer.ts +30 -0
  302. package/src/parquet-loader-options.ts +11 -0
  303. package/src/parquet-source-capabilities.ts +18 -1
  304. package/src/parquet-source-loader-types.ts +10 -0
  305. package/src/parquet-source-loader.ts +1024 -52
  306. package/src/parquet-source-types.ts +197 -28
  307. package/src/parquet-worker-url.ts +6 -1
  308. package/src/parquetjs/codecs/declare.ts +27 -0
  309. package/src/parquetjs/codecs/delta.ts +466 -6
  310. package/src/parquetjs/codecs/index.ts +4 -0
  311. package/src/parquetjs/codecs/plain.ts +177 -11
  312. package/src/parquetjs/codecs/rle.ts +573 -3
  313. package/src/parquetjs/compression.ts +102 -76
  314. package/src/parquetjs/encoder/dictionary-planner.ts +7 -2
  315. package/src/parquetjs/encoder/parquet-encoder.ts +1016 -38
  316. package/src/parquetjs/parquet-thrift/BoundingBox.ts +108 -0
  317. package/src/parquetjs/parquet-thrift/ColumnChunk.ts +35 -0
  318. package/src/parquetjs/parquet-thrift/ColumnCryptoMetaData.ts +139 -0
  319. package/src/parquetjs/parquet-thrift/ColumnMetaData.ts +73 -0
  320. package/src/parquetjs/parquet-thrift/EncryptionAlgorithm.ts +127 -0
  321. package/src/parquetjs/parquet-thrift/FileCryptoMetaData.ts +60 -0
  322. package/src/parquetjs/parquet-thrift/FileMetaData.ts +35 -0
  323. package/src/parquetjs/parquet-thrift/GeospatialStatistics.ts +68 -0
  324. package/src/parquetjs/parquet-thrift/SizeStatistics.ts +98 -0
  325. package/src/parquetjs/parquet-thrift/index.ts +6 -0
  326. package/src/parquetjs/parser/decoders.ts +451 -26
  327. package/src/parquetjs/parser/encoded-pages.ts +278 -0
  328. package/src/parquetjs/parser/parquet-reader.ts +1112 -87
  329. package/src/parquetjs/schema/declare.ts +22 -2
  330. package/src/parquetjs/schema/schema.ts +10 -0
  331. package/src/parquetjs/schema/shred.ts +238 -11
  332. package/src/parquetjs/schema/types.ts +16 -25
  333. package/src/parquetjs/schema/variant.ts +313 -0
  334. package/src/parquetjs/utils/crc32.ts +15 -0
  335. package/src/parquetjs/utils/read-utils.ts +26 -1
  336. 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
- return this.writeSection(encodeFooter(this.schema, this.rowCount, this.rowGroups, userMetadata));
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 pages = [];
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
- pages.push(result.page);
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 ? 'RLE_DICTIONARY' : column.encoding;
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
- : await encodeDataPage(column, pageData, valueEncoding, dictionaryPlan?.bitWidth);
422
- pages.push(result.page);
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 metadataOffset = baseOffset + pagesBuf.length;
463
- const body = concatUint8Arrays([pagesBuf, serializeThrift(metadata)]);
464
- return { body, metadata, metadataOffset };
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 encodeFooter(schema, rowCount, rowGroups, userMetadata) {
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
- const metadataEncoded = serializeThrift(metadata);
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)