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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/LICENSE +1 -1
  2. package/dist/bundled.d.ts +1 -1
  3. package/dist/bundled.d.ts.map +1 -1
  4. package/dist/bundled.js.map +1 -1
  5. package/dist/delta-format.d.ts +9 -0
  6. package/dist/delta-format.d.ts.map +1 -0
  7. package/dist/delta-format.js +12 -0
  8. package/dist/delta-format.js.map +1 -0
  9. package/dist/delta-source-loader-types.d.ts +27 -0
  10. package/dist/delta-source-loader-types.d.ts.map +1 -0
  11. package/dist/delta-source-loader-types.js +28 -0
  12. package/dist/delta-source-loader-types.js.map +1 -0
  13. package/dist/delta-source.d.ts +48 -0
  14. package/dist/delta-source.d.ts.map +1 -0
  15. package/dist/delta-source.js +222 -0
  16. package/dist/delta-source.js.map +1 -0
  17. package/dist/delta-types.d.ts +38 -0
  18. package/dist/delta-types.d.ts.map +1 -0
  19. package/dist/delta-types.js +5 -0
  20. package/dist/delta-types.js.map +1 -0
  21. package/dist/geoparquet-loader.js +1 -1
  22. package/dist/iceberg-rest-catalog.d.ts +49 -0
  23. package/dist/iceberg-rest-catalog.d.ts.map +1 -0
  24. package/dist/iceberg-rest-catalog.js +78 -0
  25. package/dist/iceberg-rest-catalog.js.map +1 -0
  26. package/dist/iceberg-table-source.d.ts +63 -0
  27. package/dist/iceberg-table-source.d.ts.map +1 -0
  28. package/dist/iceberg-table-source.js +797 -0
  29. package/dist/iceberg-table-source.js.map +1 -0
  30. package/dist/iceberg-types.d.ts +150 -0
  31. package/dist/iceberg-types.d.ts.map +1 -0
  32. package/dist/iceberg-types.js +5 -0
  33. package/dist/iceberg-types.js.map +1 -0
  34. package/dist/index.cjs +15841 -3306
  35. package/dist/index.cjs.map +3 -3
  36. package/dist/index.d.ts +8 -3
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +5 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/lib/arrow/convert-schema-from-parquet.d.ts +3 -1
  41. package/dist/lib/arrow/convert-schema-from-parquet.d.ts.map +1 -1
  42. package/dist/lib/arrow/convert-schema-from-parquet.js +94 -19
  43. package/dist/lib/arrow/convert-schema-from-parquet.js.map +1 -1
  44. package/dist/lib/arrow/create-nested-arrow-vector.js +89 -0
  45. package/dist/lib/arrow/create-nested-arrow-vector.js.map +1 -1
  46. package/dist/lib/constants.js +1 -1
  47. package/dist/lib/encoders/encode-table-to-parquet-js.d.ts.map +1 -1
  48. package/dist/lib/encoders/encode-table-to-parquet-js.js +202 -7
  49. package/dist/lib/encoders/encode-table-to-parquet-js.js.map +1 -1
  50. package/dist/lib/geo/geoparquet-covering.d.ts +19 -0
  51. package/dist/lib/geo/geoparquet-covering.d.ts.map +1 -0
  52. package/dist/lib/geo/geoparquet-covering.js +137 -0
  53. package/dist/lib/geo/geoparquet-covering.js.map +1 -0
  54. package/dist/lib/geo/geospatial-metadata.d.ts.map +1 -1
  55. package/dist/lib/geo/geospatial-metadata.js +39 -13
  56. package/dist/lib/geo/geospatial-metadata.js.map +1 -1
  57. package/dist/lib/parquet-bloom-filter-planner.d.ts +16 -0
  58. package/dist/lib/parquet-bloom-filter-planner.d.ts.map +1 -0
  59. package/dist/lib/parquet-bloom-filter-planner.js +57 -0
  60. package/dist/lib/parquet-bloom-filter-planner.js.map +1 -0
  61. package/dist/lib/parquet-bloom-filter.d.ts +30 -0
  62. package/dist/lib/parquet-bloom-filter.d.ts.map +1 -0
  63. package/dist/lib/parquet-bloom-filter.js +289 -0
  64. package/dist/lib/parquet-bloom-filter.js.map +1 -0
  65. package/dist/lib/parquet-encryption.d.ts +80 -0
  66. package/dist/lib/parquet-encryption.d.ts.map +1 -0
  67. package/dist/lib/parquet-encryption.js +190 -0
  68. package/dist/lib/parquet-encryption.js.map +1 -0
  69. package/dist/lib/parquet-page-index.d.ts +34 -5
  70. package/dist/lib/parquet-page-index.d.ts.map +1 -1
  71. package/dist/lib/parquet-page-index.js +114 -28
  72. package/dist/lib/parquet-page-index.js.map +1 -1
  73. package/dist/lib/parquet-predicate.d.ts +5 -1
  74. package/dist/lib/parquet-predicate.d.ts.map +1 -1
  75. package/dist/lib/parquet-predicate.js +42 -14
  76. package/dist/lib/parquet-predicate.js.map +1 -1
  77. package/dist/lib/parquet-source-worker-decoder.d.ts +4 -1
  78. package/dist/lib/parquet-source-worker-decoder.d.ts.map +1 -1
  79. package/dist/lib/parquet-source-worker-decoder.js +83 -17
  80. package/dist/lib/parquet-source-worker-decoder.js.map +1 -1
  81. package/dist/lib/parquet-source-worker-types.d.ts +24 -0
  82. package/dist/lib/parquet-source-worker-types.d.ts.map +1 -1
  83. package/dist/lib/parsers/get-parquet-schema.d.ts.map +1 -1
  84. package/dist/lib/parsers/get-parquet-schema.js +3 -1
  85. package/dist/lib/parsers/get-parquet-schema.js.map +1 -1
  86. package/dist/lib/parsers/parse-parquet-to-arrow-js.d.ts.map +1 -1
  87. package/dist/lib/parsers/parse-parquet-to-arrow-js.js +327 -18
  88. package/dist/lib/parsers/parse-parquet-to-arrow-js.js.map +1 -1
  89. package/dist/lib/parsers/parse-parquet-to-arrow.d.ts.map +1 -1
  90. package/dist/lib/parsers/parse-parquet-to-arrow.js +5 -5
  91. package/dist/lib/parsers/parse-parquet-to-arrow.js.map +1 -1
  92. package/dist/lib/parsers/parse-parquet-to-columns.d.ts.map +1 -1
  93. package/dist/lib/parsers/parse-parquet-to-columns.js +3 -1
  94. package/dist/lib/parsers/parse-parquet-to-columns.js.map +1 -1
  95. package/dist/lib/parsers/parse-parquet-to-json.d.ts.map +1 -1
  96. package/dist/lib/parsers/parse-parquet-to-json.js +18 -2
  97. package/dist/lib/parsers/parse-parquet-to-json.js.map +1 -1
  98. package/dist/lib/slice-parquet-batch.d.ts +9 -0
  99. package/dist/lib/slice-parquet-batch.d.ts.map +1 -0
  100. package/dist/lib/slice-parquet-batch.js +28 -0
  101. package/dist/lib/slice-parquet-batch.js.map +1 -0
  102. package/dist/lib/sources/parquet-range-file.d.ts +1 -1
  103. package/dist/lib/sources/parquet-range-file.d.ts.map +1 -1
  104. package/dist/lib/sources/parquet-range-file.js +28 -22
  105. package/dist/lib/sources/parquet-range-file.js.map +1 -1
  106. package/dist/lib/utils/normalize-parquet-options.d.ts.map +1 -1
  107. package/dist/lib/utils/normalize-parquet-options.js +15 -1
  108. package/dist/lib/utils/normalize-parquet-options.js.map +1 -1
  109. package/dist/parquet-dataset-source.cjs +16528 -4928
  110. package/dist/parquet-dataset-source.cjs.map +3 -3
  111. package/dist/parquet-dataset-source.d.ts +31 -4
  112. package/dist/parquet-dataset-source.d.ts.map +1 -1
  113. package/dist/parquet-dataset-source.js +296 -74
  114. package/dist/parquet-dataset-source.js.map +1 -1
  115. package/dist/parquet-encoded-page-types.d.ts +108 -0
  116. package/dist/parquet-encoded-page-types.d.ts.map +1 -0
  117. package/dist/parquet-encoded-page-types.js +5 -0
  118. package/dist/parquet-encoded-page-types.js.map +1 -0
  119. package/dist/parquet-js-loader-types.d.ts +2 -0
  120. package/dist/parquet-js-loader-types.d.ts.map +1 -1
  121. package/dist/parquet-js-loader-types.js +2 -1
  122. package/dist/parquet-js-loader-types.js.map +1 -1
  123. package/dist/parquet-js-loader.d.ts +1 -0
  124. package/dist/parquet-js-loader.d.ts.map +1 -1
  125. package/dist/parquet-js-loader.js +2 -1
  126. package/dist/parquet-js-loader.js.map +1 -1
  127. package/dist/parquet-js-writer.d.ts +23 -2
  128. package/dist/parquet-js-writer.d.ts.map +1 -1
  129. package/dist/parquet-js-writer.js +1 -1
  130. package/dist/parquet-js-writer.js.map +1 -1
  131. package/dist/parquet-loader-base.js +1 -1
  132. package/dist/parquet-loader-options.d.ts +11 -0
  133. package/dist/parquet-loader-options.d.ts.map +1 -1
  134. package/dist/parquet-loader-options.js.map +1 -1
  135. package/dist/parquet-source-capabilities.d.ts +8 -1
  136. package/dist/parquet-source-capabilities.d.ts.map +1 -1
  137. package/dist/parquet-source-capabilities.js +10 -0
  138. package/dist/parquet-source-capabilities.js.map +1 -1
  139. package/dist/parquet-source-loader-types.d.ts +10 -0
  140. package/dist/parquet-source-loader-types.d.ts.map +1 -1
  141. package/dist/parquet-source-loader-types.js +11 -1
  142. package/dist/parquet-source-loader-types.js.map +1 -1
  143. package/dist/parquet-source-loader.cjs +14121 -2796
  144. package/dist/parquet-source-loader.cjs.map +3 -3
  145. package/dist/parquet-source-loader.d.ts +51 -4
  146. package/dist/parquet-source-loader.d.ts.map +1 -1
  147. package/dist/parquet-source-loader.js +651 -38
  148. package/dist/parquet-source-loader.js.map +1 -1
  149. package/dist/parquet-source-types.d.ts +180 -30
  150. package/dist/parquet-source-types.d.ts.map +1 -1
  151. package/dist/parquet-source-worker.js +7378 -6989
  152. package/dist/parquet-source-worker.js.map +4 -4
  153. package/dist/parquet-worker-node.cjs +8501 -1045
  154. package/dist/parquet-worker-node.cjs.map +4 -4
  155. package/dist/parquet-worker-url.js +5 -1
  156. package/dist/parquet-worker-url.js.map +1 -1
  157. package/dist/parquet-worker.js +289 -218
  158. package/dist/parquet-worker.js.map +4 -4
  159. package/dist/parquetjs/codecs/declare.d.ts +15 -0
  160. package/dist/parquetjs/codecs/declare.d.ts.map +1 -1
  161. package/dist/parquetjs/codecs/declare.js +10 -0
  162. package/dist/parquetjs/codecs/declare.js.map +1 -1
  163. package/dist/parquetjs/codecs/delta.d.ts.map +1 -1
  164. package/dist/parquetjs/codecs/delta.js +356 -7
  165. package/dist/parquetjs/codecs/delta.js.map +1 -1
  166. package/dist/parquetjs/codecs/index.d.ts.map +1 -1
  167. package/dist/parquetjs/codecs/index.js +4 -0
  168. package/dist/parquetjs/codecs/index.js.map +1 -1
  169. package/dist/parquetjs/codecs/plain.d.ts +1 -1
  170. package/dist/parquetjs/codecs/plain.d.ts.map +1 -1
  171. package/dist/parquetjs/codecs/plain.js +153 -12
  172. package/dist/parquetjs/codecs/plain.js.map +1 -1
  173. package/dist/parquetjs/codecs/rle.d.ts +4 -0
  174. package/dist/parquetjs/codecs/rle.d.ts.map +1 -1
  175. package/dist/parquetjs/codecs/rle.js +406 -3
  176. package/dist/parquetjs/codecs/rle.js.map +1 -1
  177. package/dist/parquetjs/compression.d.ts +9 -0
  178. package/dist/parquetjs/compression.d.ts.map +1 -1
  179. package/dist/parquetjs/compression.js +86 -67
  180. package/dist/parquetjs/compression.js.map +1 -1
  181. package/dist/parquetjs/encoder/dictionary-planner.d.ts.map +1 -1
  182. package/dist/parquetjs/encoder/dictionary-planner.js +5 -2
  183. package/dist/parquetjs/encoder/dictionary-planner.js.map +1 -1
  184. package/dist/parquetjs/encoder/parquet-encoder.d.ts +43 -1
  185. package/dist/parquetjs/encoder/parquet-encoder.d.ts.map +1 -1
  186. package/dist/parquetjs/encoder/parquet-encoder.js +691 -34
  187. package/dist/parquetjs/encoder/parquet-encoder.js.map +1 -1
  188. package/dist/parquetjs/parquet-thrift/BoundingBox.d.ts +29 -0
  189. package/dist/parquetjs/parquet-thrift/BoundingBox.d.ts.map +1 -0
  190. package/dist/parquetjs/parquet-thrift/BoundingBox.js +85 -0
  191. package/dist/parquetjs/parquet-thrift/BoundingBox.js.map +1 -0
  192. package/dist/parquetjs/parquet-thrift/ColumnChunk.d.ts +5 -0
  193. package/dist/parquetjs/parquet-thrift/ColumnChunk.d.ts.map +1 -1
  194. package/dist/parquetjs/parquet-thrift/ColumnChunk.js +35 -0
  195. package/dist/parquetjs/parquet-thrift/ColumnChunk.js.map +1 -1
  196. package/dist/parquetjs/parquet-thrift/ColumnCryptoMetaData.d.ts +35 -0
  197. package/dist/parquetjs/parquet-thrift/ColumnCryptoMetaData.d.ts.map +1 -0
  198. package/dist/parquetjs/parquet-thrift/ColumnCryptoMetaData.js +135 -0
  199. package/dist/parquetjs/parquet-thrift/ColumnCryptoMetaData.js.map +1 -0
  200. package/dist/parquetjs/parquet-thrift/ColumnMetaData.d.ts +10 -0
  201. package/dist/parquetjs/parquet-thrift/ColumnMetaData.d.ts.map +1 -1
  202. package/dist/parquetjs/parquet-thrift/ColumnMetaData.js +73 -0
  203. package/dist/parquetjs/parquet-thrift/ColumnMetaData.js.map +1 -1
  204. package/dist/parquetjs/parquet-thrift/EncryptionAlgorithm.d.ts +22 -0
  205. package/dist/parquetjs/parquet-thrift/EncryptionAlgorithm.d.ts.map +1 -0
  206. package/dist/parquetjs/parquet-thrift/EncryptionAlgorithm.js +111 -0
  207. package/dist/parquetjs/parquet-thrift/EncryptionAlgorithm.js.map +1 -0
  208. package/dist/parquetjs/parquet-thrift/FileCryptoMetaData.d.ts +16 -0
  209. package/dist/parquetjs/parquet-thrift/FileCryptoMetaData.d.ts.map +1 -0
  210. package/dist/parquetjs/parquet-thrift/FileCryptoMetaData.js +57 -0
  211. package/dist/parquetjs/parquet-thrift/FileCryptoMetaData.js.map +1 -0
  212. package/dist/parquetjs/parquet-thrift/FileMetaData.d.ts +5 -0
  213. package/dist/parquetjs/parquet-thrift/FileMetaData.d.ts.map +1 -1
  214. package/dist/parquetjs/parquet-thrift/FileMetaData.js +35 -0
  215. package/dist/parquetjs/parquet-thrift/FileMetaData.js.map +1 -1
  216. package/dist/parquetjs/parquet-thrift/GeospatialStatistics.d.ts +18 -0
  217. package/dist/parquetjs/parquet-thrift/GeospatialStatistics.d.ts.map +1 -0
  218. package/dist/parquetjs/parquet-thrift/GeospatialStatistics.js +62 -0
  219. package/dist/parquetjs/parquet-thrift/GeospatialStatistics.js.map +1 -0
  220. package/dist/parquetjs/parquet-thrift/SizeStatistics.d.ts +26 -0
  221. package/dist/parquetjs/parquet-thrift/SizeStatistics.d.ts.map +1 -0
  222. package/dist/parquetjs/parquet-thrift/SizeStatistics.js +85 -0
  223. package/dist/parquetjs/parquet-thrift/SizeStatistics.js.map +1 -0
  224. package/dist/parquetjs/parquet-thrift/index.d.ts +6 -0
  225. package/dist/parquetjs/parquet-thrift/index.d.ts.map +1 -1
  226. package/dist/parquetjs/parquet-thrift/index.js +6 -0
  227. package/dist/parquetjs/parquet-thrift/index.js.map +1 -1
  228. package/dist/parquetjs/parser/decoders.d.ts +10 -1
  229. package/dist/parquetjs/parser/decoders.d.ts.map +1 -1
  230. package/dist/parquetjs/parser/decoders.js +318 -14
  231. package/dist/parquetjs/parser/decoders.js.map +1 -1
  232. package/dist/parquetjs/parser/encoded-pages.d.ts +24 -0
  233. package/dist/parquetjs/parser/encoded-pages.d.ts.map +1 -0
  234. package/dist/parquetjs/parser/encoded-pages.js +206 -0
  235. package/dist/parquetjs/parser/encoded-pages.js.map +1 -0
  236. package/dist/parquetjs/parser/parquet-reader.d.ts +88 -12
  237. package/dist/parquetjs/parser/parquet-reader.d.ts.map +1 -1
  238. package/dist/parquetjs/parser/parquet-reader.js +713 -66
  239. package/dist/parquetjs/parser/parquet-reader.js.map +1 -1
  240. package/dist/parquetjs/schema/declare.d.ts +22 -3
  241. package/dist/parquetjs/schema/declare.d.ts.map +1 -1
  242. package/dist/parquetjs/schema/declare.js.map +1 -1
  243. package/dist/parquetjs/schema/schema.d.ts.map +1 -1
  244. package/dist/parquetjs/schema/schema.js +10 -0
  245. package/dist/parquetjs/schema/schema.js.map +1 -1
  246. package/dist/parquetjs/schema/shred.d.ts.map +1 -1
  247. package/dist/parquetjs/schema/shred.js +219 -11
  248. package/dist/parquetjs/schema/shred.js.map +1 -1
  249. package/dist/parquetjs/schema/types.js +19 -21
  250. package/dist/parquetjs/schema/types.js.map +1 -1
  251. package/dist/parquetjs/schema/variant.d.ts +3 -0
  252. package/dist/parquetjs/schema/variant.d.ts.map +1 -0
  253. package/dist/parquetjs/schema/variant.js +248 -0
  254. package/dist/parquetjs/schema/variant.js.map +1 -0
  255. package/dist/parquetjs/utils/crc32.d.ts +3 -0
  256. package/dist/parquetjs/utils/crc32.d.ts.map +1 -0
  257. package/dist/parquetjs/utils/crc32.js +15 -0
  258. package/dist/parquetjs/utils/crc32.js.map +1 -0
  259. package/dist/parquetjs/utils/read-utils.d.ts +11 -1
  260. package/dist/parquetjs/utils/read-utils.d.ts.map +1 -1
  261. package/dist/parquetjs/utils/read-utils.js +21 -1
  262. package/dist/parquetjs/utils/read-utils.js.map +1 -1
  263. package/dist/unbundled.d.ts +2 -1
  264. package/dist/unbundled.d.ts.map +1 -1
  265. package/dist/unbundled.js +1 -0
  266. package/dist/unbundled.js.map +1 -1
  267. package/package.json +36 -13
  268. package/src/bundled.ts +10 -0
  269. package/src/delta-format.ts +12 -0
  270. package/src/delta-source-loader-types.ts +40 -0
  271. package/src/delta-source.ts +305 -0
  272. package/src/delta-types.ts +40 -0
  273. package/src/iceberg-rest-catalog.ts +122 -0
  274. package/src/iceberg-table-source.ts +1037 -0
  275. package/src/iceberg-types.ts +165 -0
  276. package/src/index.ts +59 -1
  277. package/src/lib/arrow/convert-schema-from-parquet.ts +113 -19
  278. package/src/lib/arrow/create-nested-arrow-vector.ts +116 -0
  279. package/src/lib/encoders/encode-table-to-parquet-js.ts +268 -7
  280. package/src/lib/geo/geoparquet-covering.ts +201 -0
  281. package/src/lib/geo/geospatial-metadata.ts +42 -15
  282. package/src/lib/parquet-bloom-filter-planner.ts +86 -0
  283. package/src/lib/parquet-bloom-filter.ts +345 -0
  284. package/src/lib/parquet-encryption.ts +344 -0
  285. package/src/lib/parquet-page-index.ts +180 -34
  286. package/src/lib/parquet-predicate.ts +52 -16
  287. package/src/lib/parquet-source-worker-decoder.ts +116 -20
  288. package/src/lib/parquet-source-worker-types.ts +25 -0
  289. package/src/lib/parsers/get-parquet-schema.ts +3 -1
  290. package/src/lib/parsers/parse-parquet-to-arrow-js.ts +397 -17
  291. package/src/lib/parsers/parse-parquet-to-arrow.ts +4 -5
  292. package/src/lib/parsers/parse-parquet-to-columns.ts +3 -1
  293. package/src/lib/parsers/parse-parquet-to-json.ts +18 -2
  294. package/src/lib/slice-parquet-batch.ts +32 -0
  295. package/src/lib/sources/parquet-range-file.ts +35 -29
  296. package/src/lib/utils/normalize-parquet-options.ts +17 -1
  297. package/src/parquet-dataset-source.ts +397 -85
  298. package/src/parquet-encoded-page-types.ts +122 -0
  299. package/src/parquet-js-loader-types.ts +1 -0
  300. package/src/parquet-js-loader.ts +2 -1
  301. package/src/parquet-js-writer.ts +30 -0
  302. package/src/parquet-loader-options.ts +11 -0
  303. package/src/parquet-source-capabilities.ts +18 -1
  304. package/src/parquet-source-loader-types.ts +10 -0
  305. package/src/parquet-source-loader.ts +1024 -52
  306. package/src/parquet-source-types.ts +197 -28
  307. package/src/parquet-worker-url.ts +6 -1
  308. package/src/parquetjs/codecs/declare.ts +27 -0
  309. package/src/parquetjs/codecs/delta.ts +466 -6
  310. package/src/parquetjs/codecs/index.ts +4 -0
  311. package/src/parquetjs/codecs/plain.ts +177 -11
  312. package/src/parquetjs/codecs/rle.ts +573 -3
  313. package/src/parquetjs/compression.ts +102 -76
  314. package/src/parquetjs/encoder/dictionary-planner.ts +7 -2
  315. package/src/parquetjs/encoder/parquet-encoder.ts +1016 -38
  316. package/src/parquetjs/parquet-thrift/BoundingBox.ts +108 -0
  317. package/src/parquetjs/parquet-thrift/ColumnChunk.ts +35 -0
  318. package/src/parquetjs/parquet-thrift/ColumnCryptoMetaData.ts +139 -0
  319. package/src/parquetjs/parquet-thrift/ColumnMetaData.ts +73 -0
  320. package/src/parquetjs/parquet-thrift/EncryptionAlgorithm.ts +127 -0
  321. package/src/parquetjs/parquet-thrift/FileCryptoMetaData.ts +60 -0
  322. package/src/parquetjs/parquet-thrift/FileMetaData.ts +35 -0
  323. package/src/parquetjs/parquet-thrift/GeospatialStatistics.ts +68 -0
  324. package/src/parquetjs/parquet-thrift/SizeStatistics.ts +98 -0
  325. package/src/parquetjs/parquet-thrift/index.ts +6 -0
  326. package/src/parquetjs/parser/decoders.ts +451 -26
  327. package/src/parquetjs/parser/encoded-pages.ts +278 -0
  328. package/src/parquetjs/parser/parquet-reader.ts +1112 -87
  329. package/src/parquetjs/schema/declare.ts +22 -2
  330. package/src/parquetjs/schema/schema.ts +10 -0
  331. package/src/parquetjs/schema/shred.ts +238 -11
  332. package/src/parquetjs/schema/types.ts +16 -25
  333. package/src/parquetjs/schema/variant.ts +313 -0
  334. package/src/parquetjs/utils/crc32.ts +15 -0
  335. package/src/parquetjs/utils/read-utils.ts +26 -1
  336. package/src/unbundled.ts +14 -0
@@ -2,13 +2,42 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import {hydrateArrowTable} from '@loaders.gl/arrow';
5
+ import {dehydrateArrowTable, hydrateArrowTable, IndexedArrowTable} from '@loaders.gl/arrow';
6
6
  import type {CoreAPI, ReadableFile, SourceLoader} from '@loaders.gl/loader-utils';
7
- import {BlobFile, DataSource, isBrowser} from '@loaders.gl/loader-utils';
7
+ import {
8
+ BlobFile,
9
+ DataSource,
10
+ createScanQueryMetadata,
11
+ emitScanExecutionTelemetry,
12
+ explainTableQuery,
13
+ isBrowser,
14
+ makeTableScanBatch,
15
+ validateTableQueryLimit
16
+ } from '@loaders.gl/loader-utils';
17
+ import type {
18
+ ScanColumnRole,
19
+ ScanExecutionTelemetry,
20
+ ScanQueryMetadata,
21
+ ScanQueryMetadataOptions,
22
+ TableScanSource
23
+ } from '@loaders.gl/loader-utils';
8
24
  import type {ArrayType, ArrowTable, Schema} from '@loaders.gl/schema';
9
25
  import {convertTable} from '@loaders.gl/schema-utils';
26
+ import {getGeoMetadata} from '@loaders.gl/gis';
10
27
 
11
28
  import {getSchemaFromParquetReader} from './lib/parsers/get-parquet-schema';
29
+ import {
30
+ canGeoParquetRowGroupMatch,
31
+ combineParquetPredicates,
32
+ createGeoParquetBoundingBoxPredicate
33
+ } from './lib/geo/geoparquet-covering';
34
+ import {
35
+ decodeParquetSplitBlockBloomFilter,
36
+ encodeParquetBloomFilterValue,
37
+ hashParquetBloomFilterValue,
38
+ checkParquetSplitBlockBloomFilter
39
+ } from './lib/parquet-bloom-filter';
40
+ import {getParquetBloomFilterProbes} from './lib/parquet-bloom-filter-planner';
12
41
  import {
13
42
  canParquetRowGroupMatch,
14
43
  copyParquetPredicate,
@@ -31,28 +60,43 @@ import {
31
60
  type ParquetSourceWorkerOptions,
32
61
  type ParquetSourceWorkerResult
33
62
  } from './lib/parquet-source-worker-types';
63
+ import {sliceParquetBatch} from './lib/slice-parquet-batch';
34
64
  import {ParquetRangeFile} from './lib/sources/parquet-range-file';
65
+ import {toPrimitive} from './parquetjs/schema/types';
66
+ import type {ParquetType} from './parquetjs/schema/declare';
35
67
  import {
68
+ PARQUET_TABLE_QUERY_CAPABILITIES,
36
69
  PARQUET_SOURCE_CAPABILITIES,
37
70
  type ParquetSourceCapabilities
38
71
  } from './parquet-source-capabilities';
39
72
  import {ParquetSourceLoader as ParquetSourceLoaderMetadata} from './parquet-source-loader-types';
73
+ import type {
74
+ ParquetEncodedPageBatch,
75
+ ParquetEncodedPageReadOptions,
76
+ ParquetDeferredPageFilter
77
+ } from './parquet-encoded-page-types';
40
78
  import type {
41
79
  ParquetBatch,
42
80
  ParquetColumnChunkMetadata,
43
81
  ParquetColumnChunkStatistics,
82
+ ParquetColumnChunkSizeStatistics,
83
+ ParquetGeospatialStatistics,
44
84
  ParquetMetadataRequestOptions,
45
85
  ParquetObjectVersion,
86
+ ParquetPageScanPlan,
46
87
  ParquetPredicate,
47
88
  ParquetRowGroupMetadata,
89
+ ParquetSortingColumn,
48
90
  ParquetSourceBatch,
49
91
  ParquetSourceLoaderOptions,
50
92
  ParquetSourceMetadata,
51
93
  ParquetSourceReadOptions,
94
+ ParquetSourceExplain,
52
95
  ParquetTelemetry,
53
96
  ParquetTelemetryEvent
54
97
  } from './parquet-source-types';
55
98
  export {
99
+ PARQUET_TABLE_QUERY_CAPABILITIES,
56
100
  PARQUET_SOURCE_CAPABILITIES,
57
101
  type ParquetSourceCapabilities
58
102
  } from './parquet-source-capabilities';
@@ -62,6 +106,8 @@ import {
62
106
  Encoding,
63
107
  type ColumnChunk,
64
108
  type FileMetaData,
109
+ type GeospatialStatistics as ParquetThriftGeospatialStatistics,
110
+ type RowGroup,
65
111
  type Statistics as ParquetThriftStatistics
66
112
  } from './parquetjs/parquet-thrift/index';
67
113
  import {ParquetReader} from './parquetjs/parser/parquet-reader';
@@ -80,14 +126,28 @@ import {
80
126
  } from './parquetjs/utils/binary-utils';
81
127
  import {fieldIndexOf} from './parquetjs/utils/read-utils';
82
128
 
129
+ export type {
130
+ ParquetEncodedColumnChunk,
131
+ ParquetEncodedPage,
132
+ ParquetEncodedPageBatch,
133
+ ParquetEncodedPageReadOptions,
134
+ ParquetEncodedPageSection,
135
+ ParquetDeferredPageFilter,
136
+ ParquetPageCompressionState
137
+ } from './parquet-encoded-page-types';
83
138
  export type {
84
139
  ParquetBatch,
85
140
  ParquetBatchMetadata,
86
141
  ParquetBatchProvenance,
142
+ ParquetBoundingBox,
87
143
  ParquetColumnChunkMetadata,
88
144
  ParquetColumnChunkStatistics,
145
+ ParquetColumnChunkSizeStatistics,
146
+ ParquetGeospatialBoundingBox,
147
+ ParquetGeospatialStatistics,
89
148
  ParquetMetadataRequestOptions,
90
149
  ParquetObjectVersion,
150
+ ParquetPageScanPlan,
91
151
  ParquetComparisonPredicate,
92
152
  ParquetInPredicate,
93
153
  ParquetLogicalPredicate,
@@ -99,10 +159,12 @@ export type {
99
159
  ParquetRangeRequestOptions,
100
160
  ParquetReadOptions,
101
161
  ParquetRowGroupMetadata,
162
+ ParquetSortingColumn,
102
163
  ParquetSourceBatch,
103
164
  ParquetSourceLoaderOptions,
104
165
  ParquetSourceMetadata,
105
166
  ParquetSourceReadOptions,
167
+ ParquetSourceExplain,
106
168
  ParquetTelemetry,
107
169
  ParquetTelemetryEvent
108
170
  } from './parquet-source-types';
@@ -149,6 +211,27 @@ type SettledParquetRowGroupRead =
149
211
  result?: never;
150
212
  };
151
213
 
214
+ type ParquetPhysicalRowGroupPlan = {
215
+ /** Predicate including any GeoParquet covering predicate derived from a bounding box. */
216
+ predicate?: ParquetPredicate;
217
+ /** Row groups retained by all physical pruning stages. */
218
+ rowGroupIndices: number[];
219
+ /** Candidate row groups explicitly requested by the caller. */
220
+ requested: number;
221
+ /** Row groups rejected by the caller metadata callback. */
222
+ prunedByCallback: number;
223
+ /** Row groups rejected by native geospatial statistics. */
224
+ prunedBySpatial: number;
225
+ /** Row groups rejected by column or covering statistics. */
226
+ prunedByStatistics: number;
227
+ /** Row groups rejected by split-block Bloom filters. */
228
+ prunedByBloomFilter: number;
229
+ /** Bloom-filter payloads read while planning. */
230
+ bloomFiltersRead: number;
231
+ /** Bloom-filter bytes read while planning. */
232
+ bloomFilterBytesRead: number;
233
+ };
234
+
152
235
  const {
153
236
  preload: _preloadParquetSourceLoader,
154
237
  createDataSource: _createMetadataDataSource,
@@ -169,7 +252,12 @@ export const ParquetSourceLoaderWithParser = {
169
252
  export {ParquetSourceLoaderWithParser as ParquetSourceLoader};
170
253
 
171
254
  /** Reusable Parquet source that caches footer/schema state and selectively reads byte ranges. */
172
- export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoaderOptions> {
255
+ export class ParquetSource
256
+ extends DataSource<string | Blob, ParquetSourceLoaderOptions>
257
+ implements TableScanSource<ParquetSourceBatch, ParquetPredicate>
258
+ {
259
+ /** Common projection, predicate, limit, streaming, and cancellation capabilities. */
260
+ readonly tableQueryCapabilities = PARQUET_TABLE_QUERY_CAPABILITIES;
173
261
  /** Immutable feature support for the current range-backed source implementation. */
174
262
  readonly capabilities: ParquetSourceCapabilities = PARQUET_SOURCE_CAPABILITIES;
175
263
  /** Shared initialization for this source instance. */
@@ -212,6 +300,179 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
212
300
  };
213
301
  }
214
302
 
303
+ /** Discovers schema, capabilities, and footer statistics without decoding Parquet data pages. */
304
+ async getQueryMetadata(options: ScanQueryMetadataOptions = {}): Promise<ScanQueryMetadata> {
305
+ const [schema, metadata] = await Promise.all([
306
+ this.getSchema({signal: options.signal}),
307
+ this.getMetadata({signal: options.signal})
308
+ ]);
309
+ return createScanQueryMetadata({
310
+ sourceType: 'parquet',
311
+ queryType: 'table',
312
+ execution: {status: 'supported', method: 'read'},
313
+ name: metadata.name,
314
+ schema,
315
+ capabilities: {table: this.tableQueryCapabilities},
316
+ columnRoles: getParquetColumnRoles(schema.fields.map(field => field.name)),
317
+ statistics: {rowCount: metadata.rowCount, byteLength: metadata.fileByteLength}
318
+ });
319
+ }
320
+
321
+ /** Plans a portable query using Parquet metadata without decoding data pages. */
322
+ async getScanPlan(options: ParquetSourceReadOptions = {}): Promise<ParquetSourceExplain> {
323
+ const readOptions = this.getReadOptions(options);
324
+ const initialization = await this.getInitialization(readOptions.signal);
325
+ const sourceColumnNames = initialization.schema.fields.map(field => field.name);
326
+ const signal = readOptions.signal ?? new AbortController().signal;
327
+ const physicalPlan = await this.planRowGroups(initialization, readOptions, signal);
328
+ const pagePlans = await this.planPages(initialization, readOptions, physicalPlan, signal);
329
+ const explanation = explainTableQuery(
330
+ sourceColumnNames,
331
+ {
332
+ columns: readOptions.columns,
333
+ predicate: physicalPlan.predicate,
334
+ limit: readOptions.limit
335
+ },
336
+ PARQUET_TABLE_QUERY_CAPABILITIES
337
+ );
338
+ return Object.freeze({
339
+ ...explanation,
340
+ source: 'parquet' as const,
341
+ rowGroups: Object.freeze({
342
+ indices: Object.freeze([...physicalPlan.rowGroupIndices]),
343
+ requested: physicalPlan.requested,
344
+ selected: physicalPlan.rowGroupIndices.length,
345
+ prunedByCallback: physicalPlan.prunedByCallback,
346
+ prunedBySpatial: physicalPlan.prunedBySpatial,
347
+ prunedByStatistics: physicalPlan.prunedByStatistics,
348
+ prunedByBloomFilter: physicalPlan.prunedByBloomFilter
349
+ }),
350
+ bloomFilters: Object.freeze({
351
+ read: physicalPlan.bloomFiltersRead,
352
+ bytesRead: physicalPlan.bloomFilterBytesRead
353
+ }),
354
+ pages: Object.freeze({
355
+ rowGroupsPlanned: new Set(pagePlans.map(plan => plan.rowGroupIndex)).size,
356
+ indexesRead: pagePlans.reduce((sum, plan) => sum + plan.indexesRead, 0),
357
+ total: pagePlans.reduce((sum, plan) => sum + plan.totalPages, 0),
358
+ selected: pagePlans.reduce((sum, plan) => sum + plan.selectedPages, 0),
359
+ rowsPruned: pagePlans.reduce((sum, plan) => sum + plan.rowsPruned, 0),
360
+ plans: Object.freeze(pagePlans)
361
+ })
362
+ });
363
+ }
364
+
365
+ /** Explains the common logical query and Parquet-specific physical scan plan. */
366
+ async explain(options: ParquetSourceReadOptions = {}): Promise<ParquetSourceExplain> {
367
+ return await this.getScanPlan(options);
368
+ }
369
+
370
+ /** Executes a previously computed physical plan while preserving its row-group decisions. */
371
+ async *executeScanPlan(
372
+ plan: ParquetSourceExplain,
373
+ options: ParquetSourceReadOptions = {}
374
+ ): AsyncIterable<ParquetSourceBatch> {
375
+ if (plan.source !== 'parquet') {
376
+ throw new Error('ParquetSource can only execute Parquet scan plans');
377
+ }
378
+ const predicateStep = plan.plan.find(step => step.kind === 'filter');
379
+ const limitStep = plan.plan.find(step => step.kind === 'limit');
380
+ yield* this.read({
381
+ ...options,
382
+ columns: options.columns ?? plan.outputColumns,
383
+ predicate:
384
+ options.predicate ??
385
+ (predicateStep?.kind === 'filter' ? predicateStep.predicate : undefined),
386
+ limit: options.limit ?? (limitStep?.kind === 'limit' ? limitStep.limit : undefined),
387
+ rowGroups: plan.rowGroups.indices
388
+ });
389
+ }
390
+
391
+ /** Common scan-architecture alias for selective Parquet reads. */
392
+ scan(options: ParquetSourceReadOptions = {}): AsyncIterable<ParquetSourceBatch> {
393
+ return this.read(options);
394
+ }
395
+
396
+ /**
397
+ * Reads candidate encoded pages without decoding values or constructing Arrow arrays.
398
+ *
399
+ * Footer statistics and Bloom filters prune complete row groups. Any exact predicate or spatial
400
+ * filter is returned as residual work, and its hidden columns are included in the page batch.
401
+ */
402
+ async *readPages(
403
+ options: ParquetEncodedPageReadOptions = {}
404
+ ): AsyncIterable<ParquetEncodedPageBatch> {
405
+ const readOptions = this.getReadOptions({
406
+ rowGroups: options.rowGroups,
407
+ columns: options.columns,
408
+ rowGroupFilter: options.rowGroupFilter,
409
+ predicate: options.predicate,
410
+ bbox: options.bbox,
411
+ geometryColumn: options.geometryColumn,
412
+ signal: options.signal
413
+ });
414
+ const readContext = createReadAbortContext(readOptions.signal);
415
+ this.activeReadControllers.add(readContext.abortController);
416
+ try {
417
+ const initialization = await this.getInitialization(readContext.abortController.signal);
418
+ await this.getCompressionInitialization();
419
+ throwIfAborted(readContext.abortController.signal);
420
+ const physicalPlan = await this.planRowGroups(
421
+ initialization,
422
+ readOptions,
423
+ readContext.abortController.signal
424
+ );
425
+ const projectedColumns = normalizeColumns(readOptions.columns, initialization.schema);
426
+ const predicateColumns = physicalPlan.predicate
427
+ ? getParquetPredicateColumns(physicalPlan.predicate)
428
+ : [];
429
+ const geoMetadata = readOptions.bbox
430
+ ? getGeoMetadata(initialization.schema.metadata)
431
+ : undefined;
432
+ const geometryColumn = readOptions.bbox
433
+ ? readOptions.geometryColumn || geoMetadata?.primary_column
434
+ : undefined;
435
+ const filterColumns = [
436
+ ...new Set([...predicateColumns, ...(geometryColumn ? [geometryColumn] : [])])
437
+ ];
438
+ const decodedColumns =
439
+ projectedColumns.length === 0 ? [] : [...new Set([...projectedColumns, ...filterColumns])];
440
+ const columnList = decodedColumns.map(column => [column]);
441
+ const preserveCompression = new Set(options.preserveCompression ?? []);
442
+ const residualFilter: ParquetDeferredPageFilter | undefined =
443
+ readOptions.predicate || readOptions.bbox
444
+ ? Object.freeze({
445
+ predicate: readOptions.predicate,
446
+ bbox: readOptions.bbox,
447
+ geometryColumn
448
+ })
449
+ : undefined;
450
+ for (const rowGroupIndex of physicalPlan.rowGroupIndices) {
451
+ throwIfAborted(readContext.abortController.signal);
452
+ const columns = await initialization.reader.readRowGroupEncodedPages(
453
+ initialization.parquetSchema,
454
+ initialization.fileMetadata.row_groups[rowGroupIndex],
455
+ columnList,
456
+ preserveCompression,
457
+ readContext.abortController.signal,
458
+ rowGroupIndex
459
+ );
460
+ yield Object.freeze({
461
+ shape: 'parquet-encoded-pages' as const,
462
+ rowGroup: initialization.metadata.rowGroups[rowGroupIndex],
463
+ projectedColumns: Object.freeze([...projectedColumns]),
464
+ filterColumns: Object.freeze(filterColumns),
465
+ columns,
466
+ residualFilter
467
+ });
468
+ }
469
+ } finally {
470
+ readContext.abortController.abort();
471
+ readContext.removeSignalListener();
472
+ this.activeReadControllers.delete(readContext.abortController);
473
+ }
474
+ }
475
+
215
476
  /** Returns a copy of cumulative transport, decode, conversion, and pruning telemetry. */
216
477
  getTelemetry(): ParquetTelemetry {
217
478
  return Object.freeze({...this.telemetry});
@@ -219,10 +480,26 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
219
480
 
220
481
  /** Selectively fetches row groups and columns as ordered Arrow batches with source provenance. */
221
482
  async *read(options: ParquetSourceReadOptions = {}): AsyncIterable<ParquetSourceBatch> {
483
+ const startedAt = Date.now();
484
+ const telemetryBefore = this.getTelemetry();
222
485
  const readOptions = this.getReadOptions(options);
486
+ if (readOptions.limit === 0) {
487
+ emitScanExecutionTelemetry(
488
+ options.onTelemetry,
489
+ createParquetScanExecutionTelemetry(
490
+ telemetryBefore,
491
+ this.getTelemetry(),
492
+ startedAt,
493
+ 'early-terminated',
494
+ 'limit'
495
+ )
496
+ );
497
+ return;
498
+ }
223
499
  const readContext = createReadAbortContext(readOptions.signal);
224
500
  const inFlightReads = new Set<Promise<SettledParquetRowGroupRead>>();
225
501
  let completed = false;
502
+ let limitReached = false;
226
503
  let readError: unknown;
227
504
  this.activeReadControllers.add(readContext.abortController);
228
505
 
@@ -231,33 +508,21 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
231
508
  await this.getCompressionInitialization();
232
509
  throwIfAborted(readContext.abortController.signal);
233
510
 
234
- const candidateRowGroupIndices = normalizeRowGroupIndices(
235
- readOptions.rowGroups,
236
- initialization.fileMetadata.row_groups.length
511
+ const physicalPlan = await this.planRowGroups(
512
+ initialization,
513
+ readOptions,
514
+ readContext.abortController.signal
237
515
  );
238
- const callbackFilteredRowGroupIndices = readOptions.rowGroupFilter
239
- ? candidateRowGroupIndices.filter(rowGroupIndex =>
240
- readOptions.rowGroupFilter!(initialization.metadata.rowGroups[rowGroupIndex])
241
- )
242
- : candidateRowGroupIndices;
243
- const predicate = readOptions.predicate;
244
- const availableColumns = new Set(initialization.schema.fields.map(field => field.name));
245
- if (predicate) {
246
- validateParquetPredicate(predicate, availableColumns);
247
- }
248
- const rowGroupIndices = predicate
249
- ? callbackFilteredRowGroupIndices.filter(rowGroupIndex =>
250
- canParquetRowGroupMatch(predicate, initialization.metadata.rowGroups[rowGroupIndex])
251
- )
252
- : callbackFilteredRowGroupIndices;
253
- const rowGroupsPrunedByStatistics =
254
- callbackFilteredRowGroupIndices.length - rowGroupIndices.length;
516
+ const {predicate, rowGroupIndices} = physicalPlan;
255
517
  this.recordTelemetry(
256
518
  'row-group-prune',
257
519
  {
258
- rowGroupsRequested: candidateRowGroupIndices.length,
259
- rowGroupsPruned: candidateRowGroupIndices.length - rowGroupIndices.length,
260
- rowGroupsPrunedByStatistics
520
+ rowGroupsRequested: physicalPlan.requested,
521
+ rowGroupsPruned: physicalPlan.requested - rowGroupIndices.length,
522
+ rowGroupsPrunedByStatistics: physicalPlan.prunedByStatistics,
523
+ rowGroupsPrunedByBloomFilter: physicalPlan.prunedByBloomFilter,
524
+ bloomFiltersRead: physicalPlan.bloomFiltersRead,
525
+ bloomFilterBytesRead: physicalPlan.bloomFilterBytesRead
261
526
  },
262
527
  {}
263
528
  );
@@ -271,9 +536,12 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
271
536
  const projectedSchema = projectSchema(initialization.schema, columns);
272
537
  const projectedColumnNames = columns.length ? new Set(columns) : undefined;
273
538
  const workerOptions = this.getWorkerOptions(concurrency, readContext.abortController.signal);
274
- const decodeOnWorker = canDecodeParquetSourceOnWorker(workerOptions);
539
+ const decodeOnWorker =
540
+ canDecodeParquetSourceOnWorker(workerOptions) &&
541
+ (!initialization.reader.encrypted || Boolean(this.options.parquet?.keyRetriever));
275
542
  const scheduledReads = new Map<number, Promise<SettledParquetRowGroupRead>>();
276
543
  let nextPositionToSchedule = 0;
544
+ let remainingRows = readOptions.limit ?? Number.POSITIVE_INFINITY;
277
545
 
278
546
  const scheduleReads = (): void => {
279
547
  while (
@@ -329,21 +597,30 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
329
597
  {arrowConversionDurationMs: deserializationDurationMs},
330
598
  {rowGroupIndex, durationMs: deserializationDurationMs}
331
599
  );
332
- const batch = createParquetBatchFromArrow(
333
- initialization.metadata,
334
- projectedSchema,
335
- rowGroupIndex,
336
- workerBatch.rowGroupRowOffset,
337
- arrowTable,
338
- workerBatch.rowCount,
339
- workerBatch.rowGroupRowIndices
600
+ const batch = sliceParquetBatch(
601
+ createParquetBatchFromArrow(
602
+ initialization.metadata,
603
+ projectedSchema,
604
+ rowGroupIndex,
605
+ workerBatch.rowGroupRowOffset,
606
+ arrowTable,
607
+ workerBatch.rowCount,
608
+ workerBatch.rowGroupRowIndices
609
+ ),
610
+ Math.min(workerBatch.rowCount, remainingRows)
340
611
  );
341
612
  this.recordTelemetry(
342
613
  'batch',
343
- {batchesEmitted: 1, rowsEmitted: workerBatch.rowCount},
344
- {rowGroupIndex, rowCount: workerBatch.rowCount}
614
+ {batchesEmitted: 1, rowsEmitted: batch.length},
615
+ {rowGroupIndex, rowCount: batch.length}
345
616
  );
346
617
  yield batch;
618
+ remainingRows -= batch.length;
619
+ if (remainingRows === 0) {
620
+ limitReached = true;
621
+ completed = true;
622
+ return;
623
+ }
347
624
  }
348
625
  continue;
349
626
  }
@@ -356,7 +633,11 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
356
633
  outputRowOffset += outputBatchSize
357
634
  ) {
358
635
  throwIfAborted(readContext.abortController.signal);
359
- const batchRowCount = Math.min(outputBatchSize, outputRowCount - outputRowOffset);
636
+ const batchRowCount = Math.min(
637
+ outputBatchSize,
638
+ outputRowCount - outputRowOffset,
639
+ remainingRows
640
+ );
360
641
  const batchRowIndices = rowIndices
361
642
  ? rowIndices!.slice(outputRowOffset, outputRowOffset + batchRowCount)
362
643
  : undefined;
@@ -388,6 +669,12 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
388
669
  {rowGroupIndex, rowCount: batchRowCount}
389
670
  );
390
671
  yield batch;
672
+ remainingRows -= batch.length;
673
+ if (remainingRows === 0) {
674
+ limitReached = true;
675
+ completed = true;
676
+ return;
677
+ }
391
678
  }
392
679
  }
393
680
  completed = true;
@@ -403,11 +690,201 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
403
690
  if (!completed && readError === undefined) {
404
691
  this.recordTelemetry('cancel', {cancellationCount: 1}, {});
405
692
  }
693
+ const cancelled = readContext.abortController.signal.aborted || readOptions.signal?.aborted;
406
694
  readContext.abortController.abort();
407
695
  readContext.removeSignalListener();
408
696
  this.activeReadControllers.delete(readContext.abortController);
409
697
  await Promise.allSettled([...inFlightReads]);
698
+ const status: ScanExecutionTelemetry['status'] = readError
699
+ ? cancelled
700
+ ? 'cancelled'
701
+ : 'failed'
702
+ : limitReached || !completed
703
+ ? 'early-terminated'
704
+ : 'completed';
705
+ emitScanExecutionTelemetry(
706
+ options.onTelemetry,
707
+ createParquetScanExecutionTelemetry(
708
+ telemetryBefore,
709
+ this.getTelemetry(),
710
+ startedAt,
711
+ status,
712
+ limitReached
713
+ ? 'limit'
714
+ : !completed && readError === undefined
715
+ ? 'consumer-return'
716
+ : undefined,
717
+ readError
718
+ )
719
+ );
720
+ }
721
+ }
722
+
723
+ /** Produces the physical row-group plan shared by explain and execution. */
724
+ private async planRowGroups(
725
+ initialization: ParquetSourceInitialization,
726
+ readOptions: ParquetSourceReadOptions,
727
+ signal: AbortSignal
728
+ ): Promise<ParquetPhysicalRowGroupPlan> {
729
+ throwIfAborted(signal);
730
+ const candidateRowGroupIndices = normalizeRowGroupIndices(
731
+ readOptions.rowGroups,
732
+ initialization.fileMetadata.row_groups.length
733
+ );
734
+ const callbackFilteredRowGroupIndices = readOptions.rowGroupFilter
735
+ ? candidateRowGroupIndices.filter(rowGroupIndex =>
736
+ readOptions.rowGroupFilter!(initialization.metadata.rowGroups[rowGroupIndex])
737
+ )
738
+ : candidateRowGroupIndices;
739
+ const spatiallyFilteredRowGroupIndices = readOptions.bbox
740
+ ? callbackFilteredRowGroupIndices.filter(rowGroupIndex =>
741
+ canGeoParquetRowGroupMatch(
742
+ initialization.metadata,
743
+ initialization.metadata.rowGroups[rowGroupIndex],
744
+ readOptions.bbox!,
745
+ readOptions.geometryColumn
746
+ )
747
+ )
748
+ : callbackFilteredRowGroupIndices;
749
+ const spatialPredicate = readOptions.bbox
750
+ ? createGeoParquetBoundingBoxPredicate(
751
+ initialization.metadata,
752
+ readOptions.bbox,
753
+ readOptions.geometryColumn
754
+ )
755
+ : undefined;
756
+ const predicate = combineParquetPredicates(readOptions.predicate, spatialPredicate);
757
+ if (predicate) {
758
+ validateParquetPredicate(
759
+ predicate,
760
+ new Set(initialization.schema.fields.map(field => field.name))
761
+ );
762
+ }
763
+ const statisticsRowGroupIndices = predicate
764
+ ? spatiallyFilteredRowGroupIndices.filter(rowGroupIndex =>
765
+ canParquetRowGroupMatch(predicate, initialization.metadata.rowGroups[rowGroupIndex])
766
+ )
767
+ : spatiallyFilteredRowGroupIndices;
768
+ const bloomFilterResult = predicate
769
+ ? await filterParquetRowGroupsWithBloomFilters(
770
+ initialization,
771
+ statisticsRowGroupIndices,
772
+ predicate,
773
+ signal
774
+ )
775
+ : {rowGroupIndices: statisticsRowGroupIndices, filtersRead: 0, bytesRead: 0};
776
+ return {
777
+ predicate,
778
+ rowGroupIndices: bloomFilterResult.rowGroupIndices,
779
+ requested: candidateRowGroupIndices.length,
780
+ prunedByCallback: candidateRowGroupIndices.length - callbackFilteredRowGroupIndices.length,
781
+ prunedBySpatial:
782
+ callbackFilteredRowGroupIndices.length - spatiallyFilteredRowGroupIndices.length,
783
+ prunedByStatistics:
784
+ spatiallyFilteredRowGroupIndices.length - statisticsRowGroupIndices.length,
785
+ prunedByBloomFilter:
786
+ statisticsRowGroupIndices.length - bloomFilterResult.rowGroupIndices.length,
787
+ bloomFiltersRead: bloomFilterResult.filtersRead,
788
+ bloomFilterBytesRead: bloomFilterResult.bytesRead
789
+ };
790
+ }
791
+
792
+ /** Produces explainable page-index and byte-range plans for retained row groups. */
793
+ private async planPages(
794
+ initialization: ParquetSourceInitialization,
795
+ readOptions: ParquetSourceReadOptions,
796
+ rowGroupPlan: ParquetPhysicalRowGroupPlan,
797
+ signal: AbortSignal
798
+ ): Promise<ParquetPageScanPlan[]> {
799
+ if (!rowGroupPlan.predicate) return [];
800
+ const columns = normalizeColumns(readOptions.columns, initialization.schema);
801
+ const predicateColumns = getParquetPredicateColumns(rowGroupPlan.predicate);
802
+ const decodedColumns =
803
+ columns.length === 0 ? [] : [...new Set([...columns, ...predicateColumns])];
804
+ const hasHiddenPredicateColumns =
805
+ columns.length > 0 && predicateColumns.some(column => !columns.includes(column));
806
+ const phases = hasHiddenPredicateColumns
807
+ ? [
808
+ {
809
+ phase: 'predicate' as const,
810
+ columns: predicateColumns.map(column => [column]),
811
+ predicate: rowGroupPlan.predicate
812
+ },
813
+ {
814
+ phase: 'projection' as const,
815
+ columns: columns.map(column => [column]),
816
+ predicate: undefined
817
+ }
818
+ ]
819
+ : [
820
+ {
821
+ phase: 'combined' as const,
822
+ columns: decodedColumns.map(column => [column]),
823
+ predicate: rowGroupPlan.predicate
824
+ }
825
+ ];
826
+ const plans: ParquetPageScanPlan[] = [];
827
+ for (const rowGroupIndex of rowGroupPlan.rowGroupIndices) {
828
+ const rowGroup = initialization.fileMetadata.row_groups[rowGroupIndex];
829
+ let predicateProvedEmpty = false;
830
+ for (const phase of phases) {
831
+ throwIfAborted(signal);
832
+ if (phase.phase === 'projection' && predicateProvedEmpty) {
833
+ continue;
834
+ }
835
+ await initialization.reader.resolveColumnMetadata(rowGroup, rowGroupIndex, phase.columns);
836
+ const pagePlan = phase.predicate
837
+ ? await createParquetPagePruningPlan(
838
+ initialization.file,
839
+ rowGroup,
840
+ initialization.parquetSchema,
841
+ phase.columns,
842
+ phase.predicate,
843
+ signal,
844
+ {
845
+ rowGroupOrdinal: rowGroupIndex,
846
+ int96AsTimestamp: Boolean(this.options.parquet?.int96AsTimestamp),
847
+ decryptModule: (bytes, module, rowGroupOrdinal, columnOrdinal, columnChunk) =>
848
+ initialization.reader.decryptIndexModule(
849
+ bytes,
850
+ module,
851
+ rowGroupOrdinal,
852
+ columnOrdinal,
853
+ columnChunk
854
+ )
855
+ }
856
+ )
857
+ : undefined;
858
+ plans.push(
859
+ pagePlan
860
+ ? Object.freeze({
861
+ rowGroupIndex,
862
+ phase: phase.phase,
863
+ columns: Object.freeze(phase.columns.map(column => Object.freeze([...column]))),
864
+ rowRanges: Object.freeze(
865
+ pagePlan.rowRanges.map(rowRange => Object.freeze({...rowRange}))
866
+ ),
867
+ indexesRead: pagePlan.indexCount,
868
+ totalPages: pagePlan.totalPageCount,
869
+ selectedPages: pagePlan.selectedPageCount,
870
+ rowsPruned: pagePlan.prunedRowCount,
871
+ ranges: Object.freeze(
872
+ getParquetPageReadRanges(
873
+ rowGroup,
874
+ phase.columns,
875
+ pagePlan,
876
+ initialization.parquetSchema
877
+ ).map(range => Object.freeze({...range}))
878
+ )
879
+ })
880
+ : createFullColumnScanPlan(rowGroup, rowGroupIndex, phase.phase, phase.columns)
881
+ );
882
+ if (phase.phase === 'predicate' && pagePlan?.rowRanges.length === 0) {
883
+ predicateProvedEmpty = true;
884
+ }
885
+ }
410
886
  }
887
+ return plans;
411
888
  }
412
889
 
413
890
  /** Closes the underlying readable file and aborts active remote requests. */
@@ -421,6 +898,7 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
421
898
  }
422
899
  await this.readableFile?.close();
423
900
  const initialization = await this.initializationPromise?.catch(() => null);
901
+ initialization?.reader.close();
424
902
  if (initialization?.file !== this.readableFile) {
425
903
  await initialization?.file.close();
426
904
  }
@@ -458,11 +936,17 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
458
936
  const rowGroups = options.rowGroups ?? this.options.parquet?.rowGroups;
459
937
  const columns = options.columns ?? this.options.parquet?.columns;
460
938
  const predicate = options.predicate ?? this.options.parquet?.predicate;
939
+ const bbox = options.bbox ?? this.options.parquet?.bbox;
940
+ const limit = options.limit ?? this.options.parquet?.limit;
941
+ validateTableQueryLimit(limit);
461
942
  return {
462
943
  rowGroups: rowGroups && [...rowGroups],
463
944
  columns: columns && [...columns],
464
945
  rowGroupFilter: options.rowGroupFilter ?? this.options.parquet?.rowGroupFilter,
465
946
  predicate: predicate ? copyParquetPredicate(predicate) : undefined,
947
+ limit,
948
+ bbox: bbox ? ([...bbox] as ParquetSourceReadOptions['bbox']) : undefined,
949
+ geometryColumn: options.geometryColumn ?? this.options.parquet?.geometryColumn,
466
950
  batchSize: options.batchSize ?? this.options.parquet?.batchSize,
467
951
  concurrency: options.concurrency ?? this.options.parquet?.concurrency,
468
952
  signal: options.signal
@@ -500,7 +984,25 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
500
984
  workerOptions: ParquetSourceWorkerOptions | undefined,
501
985
  signal: AbortSignal
502
986
  ): Promise<ParquetRowGroupReadResult> {
987
+ if (
988
+ predicate &&
989
+ projectedColumnNames &&
990
+ getParquetPredicateColumns(predicate).some(column => !projectedColumnNames.has(column))
991
+ ) {
992
+ return await this.readRowGroupWithLateMaterialization(
993
+ initialization,
994
+ rowGroupIndex,
995
+ columnList,
996
+ projectedSchema,
997
+ batchSize,
998
+ predicate,
999
+ projectedColumnNames,
1000
+ workerOptions,
1001
+ signal
1002
+ );
1003
+ }
503
1004
  const rowGroup = initialization.fileMetadata.row_groups[rowGroupIndex];
1005
+ await initialization.reader.resolveColumnMetadata(rowGroup, rowGroupIndex, columnList);
504
1006
  const pagePlan = predicate
505
1007
  ? await createParquetPagePruningPlan(
506
1008
  initialization.file,
@@ -508,7 +1010,19 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
508
1010
  initialization.parquetSchema,
509
1011
  columnList,
510
1012
  predicate,
511
- signal
1013
+ signal,
1014
+ {
1015
+ rowGroupOrdinal: rowGroupIndex,
1016
+ int96AsTimestamp: Boolean(this.options.parquet?.int96AsTimestamp),
1017
+ decryptModule: (bytes, module, rowGroupOrdinal, columnOrdinal, columnChunk) =>
1018
+ initialization.reader.decryptIndexModule(
1019
+ bytes,
1020
+ module,
1021
+ rowGroupOrdinal,
1022
+ columnOrdinal,
1023
+ columnChunk
1024
+ )
1025
+ }
512
1026
  )
513
1027
  : undefined;
514
1028
  if (pagePlan) {
@@ -542,7 +1056,8 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
542
1056
  columnList,
543
1057
  rowRange,
544
1058
  pagePlan.pageLocations,
545
- signal
1059
+ signal,
1060
+ rowGroupIndex
546
1061
  )
547
1062
  )
548
1063
  )
@@ -551,7 +1066,8 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
551
1066
  initialization.parquetSchema,
552
1067
  rowGroup,
553
1068
  columnList,
554
- signal
1069
+ signal,
1070
+ rowGroupIndex
555
1071
  )
556
1072
  ];
557
1073
  throwIfAborted(signal);
@@ -615,7 +1131,7 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
615
1131
  return Boolean(path && (columnList.length === 0 || fieldIndexOf(columnList, path) >= 0));
616
1132
  });
617
1133
  const rangeDescriptors = pagePlan
618
- ? getParquetPageReadRanges(rowGroup, columnList, pagePlan)
1134
+ ? getParquetPageReadRanges(rowGroup, columnList, pagePlan, initialization.parquetSchema)
619
1135
  : selectedColumnChunks.map(getColumnChunkRange);
620
1136
  const ranges = await Promise.all(
621
1137
  rangeDescriptors.map(async ({offset, length}) => {
@@ -624,6 +1140,26 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
624
1140
  );
625
1141
  throwIfAborted(signal);
626
1142
 
1143
+ const workerColumnChunks = await Promise.all(
1144
+ selectedColumnChunks.map(async columnChunk => {
1145
+ const descriptor = createParquetSourceWorkerColumnChunk(columnChunk);
1146
+ descriptor.columnOrdinal = rowGroup.columns.indexOf(columnChunk);
1147
+ if (columnChunk.crypto_metadata) {
1148
+ const columnOrdinal = rowGroup.columns.indexOf(columnChunk);
1149
+ const key = await initialization.reader.getColumnKeyForWorker(
1150
+ columnChunk,
1151
+ rowGroupIndex,
1152
+ columnOrdinal
1153
+ );
1154
+ descriptor.encrypted = true;
1155
+ descriptor.keyMetadata = key.keyMetadata?.slice().buffer;
1156
+ descriptor.keyMaterial = key.keyMaterial.slice().buffer;
1157
+ }
1158
+ return descriptor;
1159
+ })
1160
+ );
1161
+ const encryptionContext = initialization.reader.getEncryptionContextForWorker();
1162
+
627
1163
  let workerResult: ParquetSourceWorkerResult;
628
1164
  try {
629
1165
  workerResult = await decodeParquetSourceRowGroupOnWorker(
@@ -633,12 +1169,21 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
633
1169
  uncompressedByteLength: Number(rowGroup.total_byte_size),
634
1170
  schemaDefinition: initialization.parquetSchema.schema,
635
1171
  projectedSchema,
636
- columnChunks: selectedColumnChunks.map(createParquetSourceWorkerColumnChunk),
1172
+ columnChunks: workerColumnChunks,
637
1173
  ranges,
638
1174
  batchSize: batchSize || Math.max(Number(rowGroup.num_rows), 1),
639
1175
  predicate: predicate ? copyParquetPredicate(predicate) : undefined,
640
1176
  pagePlan,
641
- preserveBinary: Boolean(this.options.parquet?.preserveBinary)
1177
+ encryption: encryptionContext
1178
+ ? {
1179
+ algorithm: encryptionContext.algorithm,
1180
+ aadPrefix: encryptionContext.aadPrefix?.slice().buffer,
1181
+ fileUnique: encryptionContext.fileUnique.slice().buffer
1182
+ }
1183
+ : undefined,
1184
+ preserveBinary: Boolean(this.options.parquet?.preserveBinary),
1185
+ int96AsTimestamp: Boolean(this.options.parquet?.int96AsTimestamp),
1186
+ verifyPageChecksums: Boolean(this.options.parquet?.verifyPageChecksums)
642
1187
  },
643
1188
  workerOptions
644
1189
  );
@@ -670,6 +1215,72 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
670
1215
  return {rowGroupIndex, rowCount: workerResult.rowCount, workerResult};
671
1216
  }
672
1217
 
1218
+ /** Decodes predicate columns first, avoiding projected-column decoding for empty matches. */
1219
+ private async readRowGroupWithLateMaterialization(
1220
+ initialization: ParquetSourceInitialization,
1221
+ rowGroupIndex: number,
1222
+ columnList: string[][],
1223
+ projectedSchema: Schema,
1224
+ batchSize: number | undefined,
1225
+ predicate: ParquetPredicate,
1226
+ projectedColumnNames: ReadonlySet<string>,
1227
+ workerOptions: ParquetSourceWorkerOptions | undefined,
1228
+ signal: AbortSignal
1229
+ ): Promise<ParquetRowGroupReadResult> {
1230
+ const predicateColumns = getParquetPredicateColumns(predicate);
1231
+ const predicateColumnList = predicateColumns.map(column => [column]);
1232
+ const filterResult = await this.readRowGroup(
1233
+ initialization,
1234
+ rowGroupIndex,
1235
+ predicateColumnList,
1236
+ projectSchema(initialization.schema, predicateColumns),
1237
+ batchSize,
1238
+ predicate,
1239
+ new Set(predicateColumns),
1240
+ workerOptions,
1241
+ signal
1242
+ );
1243
+ const rowIndices = filterResult.rowIndices;
1244
+ const selectedRowIndices = rowIndices || getWorkerRowIndices(filterResult.workerResult);
1245
+ if (!selectedRowIndices || selectedRowIndices.length === 0) {
1246
+ return {rowGroupIndex, rowCount: 0, columns: {}, rowIndices: []};
1247
+ }
1248
+ const projectedColumnList = columnList.filter(columnPath =>
1249
+ projectedColumnNames.has(columnPath[0])
1250
+ );
1251
+ const projectedResult = await this.readRowGroup(
1252
+ initialization,
1253
+ rowGroupIndex,
1254
+ projectedColumnList,
1255
+ projectedSchema,
1256
+ batchSize,
1257
+ undefined,
1258
+ projectedColumnNames,
1259
+ workerOptions,
1260
+ signal
1261
+ );
1262
+ if (workerOptions && projectedResult.workerResult) {
1263
+ return {
1264
+ rowGroupIndex,
1265
+ rowCount: selectedRowIndices.length,
1266
+ rowIndices: selectedRowIndices,
1267
+ workerResult: gatherWorkerProjectedRows(
1268
+ projectedResult.workerResult,
1269
+ selectedRowIndices,
1270
+ batchSize
1271
+ )
1272
+ };
1273
+ }
1274
+ return {
1275
+ rowGroupIndex,
1276
+ rowCount: selectedRowIndices.length,
1277
+ rowIndices: selectedRowIndices,
1278
+ columns: projectedResult.columns
1279
+ ? gatherParquetColumns(projectedResult.columns, selectedRowIndices, projectedColumnNames)
1280
+ : {}
1281
+ };
1282
+ }
1283
+
673
1284
  /** Records the rows and pages avoided by one conservative page-index plan. */
674
1285
  private recordPagePruningTelemetry(rowGroupIndex: number, plan: ParquetPagePruningPlan): void {
675
1286
  this.recordTelemetry(
@@ -692,6 +1303,11 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
692
1303
  try {
693
1304
  const reader = new ParquetReader(file, {
694
1305
  preserveBinary: this.options.parquet?.preserveBinary,
1306
+ int96AsTimestamp: this.options.parquet?.int96AsTimestamp,
1307
+ verifyPageChecksums: this.options.parquet?.verifyPageChecksums,
1308
+ verifyFooterSignature: this.options.parquet?.verifyFooterSignature,
1309
+ keyRetriever: this.options.parquet?.keyRetriever,
1310
+ aadPrefix: this.options.parquet?.aadPrefix,
695
1311
  signal
696
1312
  });
697
1313
  const fileMetadata = await reader.getFileMetadata();
@@ -764,6 +1380,29 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
764
1380
  }
765
1381
  }
766
1382
 
1383
+ function getParquetColumnRoles(columnNames: readonly string[]): Record<string, ScanColumnRole> {
1384
+ const roles: Record<string, ScanColumnRole> = {};
1385
+ for (const columnName of columnNames) {
1386
+ const normalizedName = columnName.toLowerCase();
1387
+ if (normalizedName === 'geometry' || normalizedName === 'geom' || normalizedName === 'shape') {
1388
+ roles[columnName] = 'geometry';
1389
+ } else if (
1390
+ normalizedName === 'longitude' ||
1391
+ normalizedName === 'lon' ||
1392
+ normalizedName === 'x'
1393
+ ) {
1394
+ roles[columnName] = 'longitude';
1395
+ } else if (
1396
+ normalizedName === 'latitude' ||
1397
+ normalizedName === 'lat' ||
1398
+ normalizedName === 'y'
1399
+ ) {
1400
+ roles[columnName] = 'latitude';
1401
+ }
1402
+ }
1403
+ return roles;
1404
+ }
1405
+
767
1406
  /** Normalizes a decoded Parquet footer into source metadata. */
768
1407
  function createParquetSourceMetadata(
769
1408
  data: string | Blob,
@@ -784,7 +1423,8 @@ function createParquetSourceMetadata(
784
1423
  rowOffset,
785
1424
  Number(rowGroup.num_rows),
786
1425
  Number(rowGroup.total_byte_size),
787
- columns
1426
+ columns,
1427
+ rowGroup.sorting_columns
788
1428
  );
789
1429
  rowOffset += normalizedRowGroup.rowCount;
790
1430
  return normalizedRowGroup;
@@ -821,6 +1461,8 @@ function createColumnChunkMetadata(
821
1461
  columnMetadata.statistics,
822
1462
  parquetSchema.findField(columnMetadata.path_in_schema)
823
1463
  );
1464
+ const geospatialStatistics = createGeospatialStatistics(columnMetadata.geospatial_statistics);
1465
+ const sizeStatistics = createSizeStatistics(columnMetadata.size_statistics);
824
1466
  const compressedByteLength = Number(columnMetadata.total_compressed_size);
825
1467
  const uncompressedByteLength = Number(columnMetadata.total_uncompressed_size);
826
1468
  return Object.freeze({
@@ -851,7 +1493,92 @@ function createColumnChunkMetadata(
851
1493
  ? undefined
852
1494
  : Number(columnChunk.offset_index_offset),
853
1495
  offsetIndexByteLength: columnChunk.offset_index_length,
854
- statistics
1496
+ bloomFilterOffset:
1497
+ columnMetadata.bloom_filter_offset === undefined
1498
+ ? undefined
1499
+ : Number(columnMetadata.bloom_filter_offset),
1500
+ bloomFilterByteLength: columnMetadata.bloom_filter_length,
1501
+ statistics,
1502
+ sizeStatistics,
1503
+ geospatialStatistics
1504
+ });
1505
+ }
1506
+
1507
+ /** Normalizes optional Parquet size statistics without inventing missing counts. */
1508
+ function createSizeStatistics(
1509
+ statistics:
1510
+ | {
1511
+ unencoded_byte_array_data_bytes?: {toNumber?: () => number} | number;
1512
+ repetition_level_histogram?: Array<{toNumber?: () => number} | number>;
1513
+ definition_level_histogram?: Array<{toNumber?: () => number} | number>;
1514
+ }
1515
+ | undefined
1516
+ ): ParquetColumnChunkSizeStatistics | undefined {
1517
+ if (!statistics) return undefined;
1518
+ const result: ParquetColumnChunkSizeStatistics = {
1519
+ unencodedByteArrayDataBytes:
1520
+ statistics.unencoded_byte_array_data_bytes === undefined
1521
+ ? undefined
1522
+ : toSafeNumber(statistics.unencoded_byte_array_data_bytes),
1523
+ repetitionLevelHistogram: normalizeSizeStatisticsList(statistics.repetition_level_histogram),
1524
+ definitionLevelHistogram: normalizeSizeStatisticsList(statistics.definition_level_histogram)
1525
+ };
1526
+ if (
1527
+ result.unencodedByteArrayDataBytes === undefined &&
1528
+ result.repetitionLevelHistogram === undefined &&
1529
+ result.definitionLevelHistogram === undefined
1530
+ ) {
1531
+ return undefined;
1532
+ }
1533
+ return Object.freeze({
1534
+ ...result,
1535
+ repetitionLevelHistogram: result.repetitionLevelHistogram
1536
+ ? Object.freeze(result.repetitionLevelHistogram)
1537
+ : undefined,
1538
+ definitionLevelHistogram: result.definitionLevelHistogram
1539
+ ? Object.freeze(result.definitionLevelHistogram)
1540
+ : undefined
1541
+ });
1542
+ }
1543
+
1544
+ function toSafeNumber(value: {toNumber?: () => number} | number): number | undefined {
1545
+ const numberValue = typeof value === 'number' ? value : value.toNumber?.();
1546
+ return numberValue !== undefined && Number.isSafeInteger(numberValue) && numberValue >= 0
1547
+ ? numberValue
1548
+ : undefined;
1549
+ }
1550
+
1551
+ /** Converts a Thrift int64 histogram only when every entry is a safe non-negative integer. */
1552
+ function normalizeSizeStatisticsList(
1553
+ values: Array<{toNumber?: () => number} | number> | undefined
1554
+ ): readonly number[] | undefined {
1555
+ if (!values) return undefined;
1556
+ const normalized = values.map(toSafeNumber);
1557
+ return normalized.every((value): value is number => value !== undefined) ? normalized : undefined;
1558
+ }
1559
+
1560
+ /** Normalizes native Parquet geospatial statistics from the decoded footer. */
1561
+ function createGeospatialStatistics(
1562
+ statistics: ParquetThriftGeospatialStatistics | undefined
1563
+ ): ParquetGeospatialStatistics | undefined {
1564
+ if (!statistics) return undefined;
1565
+ const bbox = statistics.bbox
1566
+ ? Object.freeze({
1567
+ xmin: statistics.bbox.xmin,
1568
+ xmax: statistics.bbox.xmax,
1569
+ ymin: statistics.bbox.ymin,
1570
+ ymax: statistics.bbox.ymax,
1571
+ zmin: statistics.bbox.zmin,
1572
+ zmax: statistics.bbox.zmax,
1573
+ mmin: statistics.bbox.mmin,
1574
+ mmax: statistics.bbox.mmax
1575
+ })
1576
+ : undefined;
1577
+ return Object.freeze({
1578
+ bbox,
1579
+ geometryTypes: statistics.geospatial_types
1580
+ ? Object.freeze([...statistics.geospatial_types])
1581
+ : undefined
855
1582
  });
856
1583
  }
857
1584
 
@@ -933,7 +1660,8 @@ function createRowGroupMetadata(
933
1660
  rowOffset: number,
934
1661
  rowCount: number,
935
1662
  uncompressedByteLength: number,
936
- columns: ParquetColumnChunkMetadata[]
1663
+ columns: ParquetColumnChunkMetadata[],
1664
+ sortingColumns: RowGroup['sorting_columns']
937
1665
  ): ParquetRowGroupMetadata {
938
1666
  const compressedByteLength = columns.reduce(
939
1667
  (sum, column) => sum + column.compressedByteLength,
@@ -947,10 +1675,54 @@ function createRowGroupMetadata(
947
1675
  uncompressedSize: uncompressedByteLength,
948
1676
  compressedByteLength,
949
1677
  compressedSize: compressedByteLength,
950
- columns: Object.freeze(columns)
1678
+ columns: Object.freeze(columns),
1679
+ sortingColumns: Object.freeze(
1680
+ (sortingColumns || []).map(
1681
+ sortingColumn =>
1682
+ Object.freeze({
1683
+ columnIndex: sortingColumn.column_idx,
1684
+ descending: sortingColumn.descending,
1685
+ nullsFirst: sortingColumn.nulls_first
1686
+ }) satisfies ParquetSortingColumn
1687
+ )
1688
+ )
1689
+ });
1690
+ }
1691
+
1692
+ /** Describes a phase that reads complete column chunks because no page index is available. */
1693
+ function createFullColumnScanPlan(
1694
+ rowGroup: RowGroup,
1695
+ rowGroupIndex: number,
1696
+ phase: ParquetPageScanPlan['phase'],
1697
+ columns: readonly string[][]
1698
+ ): ParquetPageScanPlan {
1699
+ const selectedColumnChunks = rowGroup.columns.filter(columnChunk => {
1700
+ const path = columnChunk.meta_data?.path_in_schema;
1701
+ return Boolean(
1702
+ path && (columns.length === 0 || columns.some(column => pathMatchesSelection(column, path)))
1703
+ );
1704
+ });
1705
+ const rowCount = Number(rowGroup.num_rows);
1706
+ return Object.freeze({
1707
+ rowGroupIndex,
1708
+ phase,
1709
+ columns: Object.freeze(columns.map(column => Object.freeze([...column]))),
1710
+ rowRanges: Object.freeze([Object.freeze({start: 0, end: rowCount})]),
1711
+ indexesRead: 0,
1712
+ totalPages: 0,
1713
+ selectedPages: 0,
1714
+ rowsPruned: 0,
1715
+ ranges: Object.freeze(
1716
+ selectedColumnChunks.map(columnChunk => Object.freeze(getColumnChunkRange(columnChunk)))
1717
+ )
951
1718
  });
952
1719
  }
953
1720
 
1721
+ /** Returns whether a requested path selects a footer leaf or one of its nested children. */
1722
+ function pathMatchesSelection(selection: readonly string[], path: readonly string[]): boolean {
1723
+ return selection.length <= path.length && selection.every((part, index) => part === path[index]);
1724
+ }
1725
+
954
1726
  /** Returns the complete contiguous byte range occupied by one Parquet column chunk. */
955
1727
  function getColumnChunkRange(columnChunk: ColumnChunk): {offset: number; length: number} {
956
1728
  if (columnChunk.file_path) {
@@ -1048,11 +1820,8 @@ function createParquetBatchFromArrow(
1048
1820
  : undefined
1049
1821
  });
1050
1822
  return {
1051
- batchType: 'data',
1052
- shape: 'arrow-table',
1823
+ ...makeTableScanBatch({shape: 'arrow-table', schema, data}),
1053
1824
  schemaType: 'explicit',
1054
- schema,
1055
- data,
1056
1825
  length: rowCount,
1057
1826
  metadata: provenance,
1058
1827
  ...provenance
@@ -1173,6 +1942,9 @@ function createParquetTelemetry(): ParquetTelemetry {
1173
1942
  rowGroupsRequested: 0,
1174
1943
  rowGroupsPruned: 0,
1175
1944
  rowGroupsPrunedByStatistics: 0,
1945
+ rowGroupsPrunedByBloomFilter: 0,
1946
+ bloomFiltersRead: 0,
1947
+ bloomFilterBytesRead: 0,
1176
1948
  rowGroupsPrunedByPageIndex: 0,
1177
1949
  pageIndexesRead: 0,
1178
1950
  pagesRead: 0,
@@ -1188,6 +1960,45 @@ function createParquetTelemetry(): ParquetTelemetry {
1188
1960
  };
1189
1961
  }
1190
1962
 
1963
+ /** Converts cumulative Parquet counters into one portable read-scoped execution snapshot. */
1964
+ function createParquetScanExecutionTelemetry(
1965
+ before: ParquetTelemetry,
1966
+ after: ParquetTelemetry,
1967
+ startedAt: number,
1968
+ status: ScanExecutionTelemetry['status'],
1969
+ earlyTerminationReason?: ScanExecutionTelemetry['earlyTerminationReason'],
1970
+ error?: unknown
1971
+ ): ScanExecutionTelemetry {
1972
+ const delta = Object.fromEntries(
1973
+ Object.keys(after).map(key => {
1974
+ const telemetryKey = key as keyof ParquetTelemetry;
1975
+ return [key, after[telemetryKey] - before[telemetryKey]];
1976
+ })
1977
+ ) as ParquetTelemetry;
1978
+ const rowsRead = Math.max(delta.predicateRowsTested, delta.rowsEmitted);
1979
+ return Object.freeze({
1980
+ status,
1981
+ sourcesPlanned: 1,
1982
+ sourcesRead:
1983
+ status === 'early-terminated' && earlyTerminationReason === 'limit' && rowsRead === 0 ? 0 : 1,
1984
+ batchesRead: delta.batchesEmitted,
1985
+ batchesDecoded: delta.rowGroupsDecoded,
1986
+ rowsRead,
1987
+ rowsTested: delta.predicateRowsTested || undefined,
1988
+ rowsRetained: delta.predicateRowsMatched || undefined,
1989
+ rowsReturned: delta.rowsEmitted,
1990
+ bytesRead: delta.downloadedBytes,
1991
+ bytesFetched: delta.downloadedBytes,
1992
+ filesOpened: rowsRead || delta.downloadedBytes ? 1 : 0,
1993
+ tasksOpened: delta.rowGroupsDecoded,
1994
+ rowsPruned: delta.rowsPrunedByPageIndex,
1995
+ durationMilliseconds: Date.now() - startedAt,
1996
+ earlyTerminationReason,
1997
+ details: Object.freeze({...delta}),
1998
+ ...(error === undefined ? {} : {error})
1999
+ });
2000
+ }
2001
+
1191
2002
  /** Returns a monotonic timestamp when available and falls back to wall-clock time. */
1192
2003
  function getCurrentTime(): number {
1193
2004
  return globalThis.performance?.now() ?? Date.now();
@@ -1259,6 +2070,167 @@ function getSourceName(data: string | Blob, url: string): string {
1259
2070
  return sourceName.replace(/\.parquet$/i, '') || 'parquet';
1260
2071
  }
1261
2072
 
2073
+ type ParquetBloomFilterReadResult = {
2074
+ rowGroupIndices: number[];
2075
+ filtersRead: number;
2076
+ bytesRead: number;
2077
+ };
2078
+
2079
+ /** Reconstructs exact row-group indexes from worker predicate batches. */
2080
+ function getWorkerRowIndices(workerResult?: ParquetSourceWorkerResult): number[] {
2081
+ if (!workerResult) return [];
2082
+ return workerResult.batches.flatMap(batch =>
2083
+ batch.rowGroupRowIndices
2084
+ ? [...batch.rowGroupRowIndices]
2085
+ : Array.from({length: batch.rowCount}, (_, index) => batch.rowGroupRowOffset + index)
2086
+ );
2087
+ }
2088
+
2089
+ /** Gathers projected worker Arrow batches through the shared indexed Arrow table utility. */
2090
+ function gatherWorkerProjectedRows(
2091
+ workerResult: ParquetSourceWorkerResult,
2092
+ rowIndices: readonly number[],
2093
+ batchSize: number | undefined
2094
+ ): ParquetSourceWorkerResult {
2095
+ const tables = workerResult.batches.map(batch => hydrateArrowTable(batch.arrowTable));
2096
+ if (tables.length === 0) {
2097
+ return {...workerResult, rowCount: 0, batches: []};
2098
+ }
2099
+ const table = tables.length === 1 ? tables[0] : tables[0].concat(...tables.slice(1));
2100
+ const indexedTable = new IndexedArrowTable(table, rowIndices);
2101
+ const outputBatchSize = batchSize || Math.max(rowIndices.length, 1);
2102
+ const batches: ParquetSourceWorkerResult['batches'] = [];
2103
+ for (let offset = 0; offset < rowIndices.length; offset += outputBatchSize) {
2104
+ const end = Math.min(offset + outputBatchSize, rowIndices.length);
2105
+ const batchIndexes = rowIndices.slice(offset, end);
2106
+ const batchTable = indexedTable.slice(offset, end).materializeArrowTable();
2107
+ batches.push({
2108
+ rowGroupRowOffset: batchIndexes[0],
2109
+ rowCount: batchIndexes.length,
2110
+ rowGroupRowIndices: [...batchIndexes],
2111
+ arrowTable: dehydrateArrowTable(batchTable)
2112
+ });
2113
+ }
2114
+ return {
2115
+ sourceRowCount: workerResult.sourceRowCount,
2116
+ rowCount: rowIndices.length,
2117
+ batches,
2118
+ decodeDurationMs: workerResult.decodeDurationMs,
2119
+ arrowConversionDurationMs: workerResult.arrowConversionDurationMs
2120
+ };
2121
+ }
2122
+
2123
+ /** Applies only proven-negative Bloom-filter checks to candidate row groups. */
2124
+ async function filterParquetRowGroupsWithBloomFilters(
2125
+ initialization: ParquetSourceInitialization,
2126
+ rowGroupIndices: readonly number[],
2127
+ predicate: ParquetPredicate,
2128
+ signal: AbortSignal
2129
+ ): Promise<ParquetBloomFilterReadResult> {
2130
+ const rowGroups: number[] = [];
2131
+ let filtersRead = 0;
2132
+ let bytesRead = 0;
2133
+ for (const rowGroupIndex of rowGroupIndices) {
2134
+ const rowGroup = initialization.metadata.rowGroups[rowGroupIndex];
2135
+ const probes = getParquetBloomFilterProbes(predicate, rowGroup);
2136
+ let keepRowGroup = true;
2137
+ for (const probe of probes) {
2138
+ if (
2139
+ probe.column.bloomFilterOffset === undefined ||
2140
+ probe.column.bloomFilterByteLength === undefined
2141
+ ) {
2142
+ continue;
2143
+ }
2144
+ let field: ParquetField;
2145
+ try {
2146
+ field = initialization.parquetSchema.findField([...probe.column.path]);
2147
+ } catch {
2148
+ throwIfAborted(signal);
2149
+ continue;
2150
+ }
2151
+ if (!field.primitiveType) continue;
2152
+ let filter: ReturnType<typeof decodeParquetSplitBlockBloomFilter>;
2153
+ try {
2154
+ const rawColumnOrdinal = initialization.fileMetadata.row_groups[
2155
+ rowGroupIndex
2156
+ ].columns.findIndex(
2157
+ columnChunk =>
2158
+ JSON.stringify(columnChunk.meta_data?.path_in_schema) ===
2159
+ JSON.stringify(probe.column.path)
2160
+ );
2161
+ const rawColumnChunk =
2162
+ rawColumnOrdinal >= 0
2163
+ ? initialization.fileMetadata.row_groups[rowGroupIndex].columns[rawColumnOrdinal]
2164
+ : undefined;
2165
+ const data = await initialization.file.read(
2166
+ probe.column.bloomFilterOffset,
2167
+ probe.column.bloomFilterByteLength,
2168
+ signal
2169
+ );
2170
+ const decodedBloomFilter = rawColumnChunk
2171
+ ? await initialization.reader.decryptBloomFilter(
2172
+ toUint8Array(data),
2173
+ rowGroupIndex,
2174
+ rawColumnOrdinal,
2175
+ rawColumnChunk
2176
+ )
2177
+ : toUint8Array(data);
2178
+ filter = decodeParquetSplitBlockBloomFilter(decodedBloomFilter);
2179
+ filtersRead++;
2180
+ bytesRead += data.byteLength;
2181
+ } catch {
2182
+ throwIfAborted(signal);
2183
+ continue;
2184
+ }
2185
+ if (
2186
+ filter.algorithm !== 'BLOCK' ||
2187
+ filter.hash !== 'XXHASH' ||
2188
+ filter.compression !== 'UNCOMPRESSED'
2189
+ ) {
2190
+ continue;
2191
+ }
2192
+ const mayContainValue = probe.values.some(value => {
2193
+ try {
2194
+ if (value instanceof Date) return true;
2195
+ const physicalValue = getParquetBloomFilterPhysicalValue(value, field);
2196
+ const encoded = encodeParquetBloomFilterValue(
2197
+ physicalValue,
2198
+ field.primitiveType as Parameters<typeof encodeParquetBloomFilterValue>[1],
2199
+ field.typeLength
2200
+ );
2201
+ return checkParquetSplitBlockBloomFilter(
2202
+ filter.bitset,
2203
+ hashParquetBloomFilterValue(encoded)
2204
+ );
2205
+ } catch {
2206
+ throwIfAborted(signal);
2207
+ return true;
2208
+ }
2209
+ });
2210
+ if (!mayContainValue) {
2211
+ keepRowGroup = false;
2212
+ break;
2213
+ }
2214
+ }
2215
+ if (keepRowGroup) rowGroups.push(rowGroupIndex);
2216
+ }
2217
+ return {rowGroupIndices: rowGroups, filtersRead, bytesRead};
2218
+ }
2219
+
2220
+ /** Converts logical predicate values into the physical values stored in Bloom filters. */
2221
+ function getParquetBloomFilterPhysicalValue(
2222
+ value: unknown,
2223
+ field: ParquetField
2224
+ ): boolean | number | bigint | string | Uint8Array {
2225
+ if (field.originalType?.startsWith('DECIMAL_')) {
2226
+ return toPrimitive(field.originalType as ParquetType, value, field) as
2227
+ | number
2228
+ | bigint
2229
+ | Uint8Array;
2230
+ }
2231
+ return value as boolean | number | bigint | string | Uint8Array;
2232
+ }
2233
+
1262
2234
  /** Returns true when at least one HTTP object validator was captured. */
1263
2235
  function hasObjectVersion(version?: ParquetObjectVersion): version is ParquetObjectVersion {
1264
2236
  return Boolean(version?.etag || version?.lastModified);