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