@loaders.gl/parquet 5.0.0-alpha.2 → 5.0.0-alpha.3

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