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