@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
@@ -2,13 +2,41 @@
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';
10
26
 
11
27
  import {getSchemaFromParquetReader} from './lib/parsers/get-parquet-schema';
28
+ import {
29
+ canGeoParquetRowGroupMatch,
30
+ combineParquetPredicates,
31
+ createGeoParquetBoundingBoxPredicate
32
+ } from './lib/geo/geoparquet-covering';
33
+ import {
34
+ decodeParquetSplitBlockBloomFilter,
35
+ encodeParquetBloomFilterValue,
36
+ hashParquetBloomFilterValue,
37
+ checkParquetSplitBlockBloomFilter
38
+ } from './lib/parquet-bloom-filter';
39
+ import {getParquetBloomFilterProbes} from './lib/parquet-bloom-filter-planner';
12
40
  import {
13
41
  canParquetRowGroupMatch,
14
42
  copyParquetPredicate,
@@ -31,8 +59,12 @@ import {
31
59
  type ParquetSourceWorkerOptions,
32
60
  type ParquetSourceWorkerResult
33
61
  } from './lib/parquet-source-worker-types';
62
+ import {sliceParquetBatch} from './lib/slice-parquet-batch';
34
63
  import {ParquetRangeFile} from './lib/sources/parquet-range-file';
64
+ import {toPrimitive} from './parquetjs/schema/types';
65
+ import type {ParquetType} from './parquetjs/schema/declare';
35
66
  import {
67
+ PARQUET_TABLE_QUERY_CAPABILITIES,
36
68
  PARQUET_SOURCE_CAPABILITIES,
37
69
  type ParquetSourceCapabilities
38
70
  } from './parquet-source-capabilities';
@@ -41,18 +73,24 @@ import type {
41
73
  ParquetBatch,
42
74
  ParquetColumnChunkMetadata,
43
75
  ParquetColumnChunkStatistics,
76
+ ParquetColumnChunkSizeStatistics,
77
+ ParquetGeospatialStatistics,
44
78
  ParquetMetadataRequestOptions,
45
79
  ParquetObjectVersion,
80
+ ParquetPageScanPlan,
46
81
  ParquetPredicate,
47
82
  ParquetRowGroupMetadata,
83
+ ParquetSortingColumn,
48
84
  ParquetSourceBatch,
49
85
  ParquetSourceLoaderOptions,
50
86
  ParquetSourceMetadata,
51
87
  ParquetSourceReadOptions,
88
+ ParquetSourceExplain,
52
89
  ParquetTelemetry,
53
90
  ParquetTelemetryEvent
54
91
  } from './parquet-source-types';
55
92
  export {
93
+ PARQUET_TABLE_QUERY_CAPABILITIES,
56
94
  PARQUET_SOURCE_CAPABILITIES,
57
95
  type ParquetSourceCapabilities
58
96
  } from './parquet-source-capabilities';
@@ -62,6 +100,8 @@ import {
62
100
  Encoding,
63
101
  type ColumnChunk,
64
102
  type FileMetaData,
103
+ type GeospatialStatistics as ParquetThriftGeospatialStatistics,
104
+ type RowGroup,
65
105
  type Statistics as ParquetThriftStatistics
66
106
  } from './parquetjs/parquet-thrift/index';
67
107
  import {ParquetReader} from './parquetjs/parser/parquet-reader';
@@ -84,10 +124,15 @@ export type {
84
124
  ParquetBatch,
85
125
  ParquetBatchMetadata,
86
126
  ParquetBatchProvenance,
127
+ ParquetBoundingBox,
87
128
  ParquetColumnChunkMetadata,
88
129
  ParquetColumnChunkStatistics,
130
+ ParquetColumnChunkSizeStatistics,
131
+ ParquetGeospatialBoundingBox,
132
+ ParquetGeospatialStatistics,
89
133
  ParquetMetadataRequestOptions,
90
134
  ParquetObjectVersion,
135
+ ParquetPageScanPlan,
91
136
  ParquetComparisonPredicate,
92
137
  ParquetInPredicate,
93
138
  ParquetLogicalPredicate,
@@ -99,10 +144,12 @@ export type {
99
144
  ParquetRangeRequestOptions,
100
145
  ParquetReadOptions,
101
146
  ParquetRowGroupMetadata,
147
+ ParquetSortingColumn,
102
148
  ParquetSourceBatch,
103
149
  ParquetSourceLoaderOptions,
104
150
  ParquetSourceMetadata,
105
151
  ParquetSourceReadOptions,
152
+ ParquetSourceExplain,
106
153
  ParquetTelemetry,
107
154
  ParquetTelemetryEvent
108
155
  } from './parquet-source-types';
@@ -149,6 +196,27 @@ type SettledParquetRowGroupRead =
149
196
  result?: never;
150
197
  };
151
198
 
199
+ type ParquetPhysicalRowGroupPlan = {
200
+ /** Predicate including any GeoParquet covering predicate derived from a bounding box. */
201
+ predicate?: ParquetPredicate;
202
+ /** Row groups retained by all physical pruning stages. */
203
+ rowGroupIndices: number[];
204
+ /** Candidate row groups explicitly requested by the caller. */
205
+ requested: number;
206
+ /** Row groups rejected by the caller metadata callback. */
207
+ prunedByCallback: number;
208
+ /** Row groups rejected by native geospatial statistics. */
209
+ prunedBySpatial: number;
210
+ /** Row groups rejected by column or covering statistics. */
211
+ prunedByStatistics: number;
212
+ /** Row groups rejected by split-block Bloom filters. */
213
+ prunedByBloomFilter: number;
214
+ /** Bloom-filter payloads read while planning. */
215
+ bloomFiltersRead: number;
216
+ /** Bloom-filter bytes read while planning. */
217
+ bloomFilterBytesRead: number;
218
+ };
219
+
152
220
  const {
153
221
  preload: _preloadParquetSourceLoader,
154
222
  createDataSource: _createMetadataDataSource,
@@ -169,7 +237,12 @@ export const ParquetSourceLoaderWithParser = {
169
237
  export {ParquetSourceLoaderWithParser as ParquetSourceLoader};
170
238
 
171
239
  /** Reusable Parquet source that caches footer/schema state and selectively reads byte ranges. */
172
- export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoaderOptions> {
240
+ export class ParquetSource
241
+ extends DataSource<string | Blob, ParquetSourceLoaderOptions>
242
+ implements TableScanSource<ParquetSourceBatch, ParquetPredicate>
243
+ {
244
+ /** Common projection, predicate, limit, streaming, and cancellation capabilities. */
245
+ readonly tableQueryCapabilities = PARQUET_TABLE_QUERY_CAPABILITIES;
173
246
  /** Immutable feature support for the current range-backed source implementation. */
174
247
  readonly capabilities: ParquetSourceCapabilities = PARQUET_SOURCE_CAPABILITIES;
175
248
  /** Shared initialization for this source instance. */
@@ -212,6 +285,99 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
212
285
  };
213
286
  }
214
287
 
288
+ /** Discovers schema, capabilities, and footer statistics without decoding Parquet data pages. */
289
+ async getQueryMetadata(options: ScanQueryMetadataOptions = {}): Promise<ScanQueryMetadata> {
290
+ const [schema, metadata] = await Promise.all([
291
+ this.getSchema({signal: options.signal}),
292
+ this.getMetadata({signal: options.signal})
293
+ ]);
294
+ return createScanQueryMetadata({
295
+ sourceType: 'parquet',
296
+ queryType: 'table',
297
+ execution: {status: 'supported', method: 'read'},
298
+ name: metadata.name,
299
+ schema,
300
+ capabilities: {table: this.tableQueryCapabilities},
301
+ columnRoles: getParquetColumnRoles(schema.fields.map(field => field.name)),
302
+ statistics: {rowCount: metadata.rowCount, byteLength: metadata.fileByteLength}
303
+ });
304
+ }
305
+
306
+ /** Plans a portable query using Parquet metadata without decoding data pages. */
307
+ async getScanPlan(options: ParquetSourceReadOptions = {}): Promise<ParquetSourceExplain> {
308
+ const readOptions = this.getReadOptions(options);
309
+ const initialization = await this.getInitialization(readOptions.signal);
310
+ const sourceColumnNames = initialization.schema.fields.map(field => field.name);
311
+ const signal = readOptions.signal ?? new AbortController().signal;
312
+ const physicalPlan = await this.planRowGroups(initialization, readOptions, signal);
313
+ const pagePlans = await this.planPages(initialization, readOptions, physicalPlan, signal);
314
+ const explanation = explainTableQuery(
315
+ sourceColumnNames,
316
+ {
317
+ columns: readOptions.columns,
318
+ predicate: physicalPlan.predicate,
319
+ limit: readOptions.limit
320
+ },
321
+ PARQUET_TABLE_QUERY_CAPABILITIES
322
+ );
323
+ return Object.freeze({
324
+ ...explanation,
325
+ source: 'parquet' as const,
326
+ rowGroups: Object.freeze({
327
+ indices: Object.freeze([...physicalPlan.rowGroupIndices]),
328
+ requested: physicalPlan.requested,
329
+ selected: physicalPlan.rowGroupIndices.length,
330
+ prunedByCallback: physicalPlan.prunedByCallback,
331
+ prunedBySpatial: physicalPlan.prunedBySpatial,
332
+ prunedByStatistics: physicalPlan.prunedByStatistics,
333
+ prunedByBloomFilter: physicalPlan.prunedByBloomFilter
334
+ }),
335
+ bloomFilters: Object.freeze({
336
+ read: physicalPlan.bloomFiltersRead,
337
+ bytesRead: physicalPlan.bloomFilterBytesRead
338
+ }),
339
+ pages: Object.freeze({
340
+ rowGroupsPlanned: new Set(pagePlans.map(plan => plan.rowGroupIndex)).size,
341
+ indexesRead: pagePlans.reduce((sum, plan) => sum + plan.indexesRead, 0),
342
+ total: pagePlans.reduce((sum, plan) => sum + plan.totalPages, 0),
343
+ selected: pagePlans.reduce((sum, plan) => sum + plan.selectedPages, 0),
344
+ rowsPruned: pagePlans.reduce((sum, plan) => sum + plan.rowsPruned, 0),
345
+ plans: Object.freeze(pagePlans)
346
+ })
347
+ });
348
+ }
349
+
350
+ /** Explains the common logical query and Parquet-specific physical scan plan. */
351
+ async explain(options: ParquetSourceReadOptions = {}): Promise<ParquetSourceExplain> {
352
+ return await this.getScanPlan(options);
353
+ }
354
+
355
+ /** Executes a previously computed physical plan while preserving its row-group decisions. */
356
+ async *executeScanPlan(
357
+ plan: ParquetSourceExplain,
358
+ options: ParquetSourceReadOptions = {}
359
+ ): AsyncIterable<ParquetSourceBatch> {
360
+ if (plan.source !== 'parquet') {
361
+ throw new Error('ParquetSource can only execute Parquet scan plans');
362
+ }
363
+ const predicateStep = plan.plan.find(step => step.kind === 'filter');
364
+ const limitStep = plan.plan.find(step => step.kind === 'limit');
365
+ yield* this.read({
366
+ ...options,
367
+ columns: options.columns ?? plan.outputColumns,
368
+ predicate:
369
+ options.predicate ??
370
+ (predicateStep?.kind === 'filter' ? predicateStep.predicate : undefined),
371
+ limit: options.limit ?? (limitStep?.kind === 'limit' ? limitStep.limit : undefined),
372
+ rowGroups: plan.rowGroups.indices
373
+ });
374
+ }
375
+
376
+ /** Common scan-architecture alias for selective Parquet reads. */
377
+ scan(options: ParquetSourceReadOptions = {}): AsyncIterable<ParquetSourceBatch> {
378
+ return this.read(options);
379
+ }
380
+
215
381
  /** Returns a copy of cumulative transport, decode, conversion, and pruning telemetry. */
216
382
  getTelemetry(): ParquetTelemetry {
217
383
  return Object.freeze({...this.telemetry});
@@ -219,10 +385,26 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
219
385
 
220
386
  /** Selectively fetches row groups and columns as ordered Arrow batches with source provenance. */
221
387
  async *read(options: ParquetSourceReadOptions = {}): AsyncIterable<ParquetSourceBatch> {
388
+ const startedAt = Date.now();
389
+ const telemetryBefore = this.getTelemetry();
222
390
  const readOptions = this.getReadOptions(options);
391
+ if (readOptions.limit === 0) {
392
+ emitScanExecutionTelemetry(
393
+ options.onTelemetry,
394
+ createParquetScanExecutionTelemetry(
395
+ telemetryBefore,
396
+ this.getTelemetry(),
397
+ startedAt,
398
+ 'early-terminated',
399
+ 'limit'
400
+ )
401
+ );
402
+ return;
403
+ }
223
404
  const readContext = createReadAbortContext(readOptions.signal);
224
405
  const inFlightReads = new Set<Promise<SettledParquetRowGroupRead>>();
225
406
  let completed = false;
407
+ let limitReached = false;
226
408
  let readError: unknown;
227
409
  this.activeReadControllers.add(readContext.abortController);
228
410
 
@@ -231,33 +413,21 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
231
413
  await this.getCompressionInitialization();
232
414
  throwIfAborted(readContext.abortController.signal);
233
415
 
234
- const candidateRowGroupIndices = normalizeRowGroupIndices(
235
- readOptions.rowGroups,
236
- initialization.fileMetadata.row_groups.length
416
+ const physicalPlan = await this.planRowGroups(
417
+ initialization,
418
+ readOptions,
419
+ readContext.abortController.signal
237
420
  );
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;
421
+ const {predicate, rowGroupIndices} = physicalPlan;
255
422
  this.recordTelemetry(
256
423
  'row-group-prune',
257
424
  {
258
- rowGroupsRequested: candidateRowGroupIndices.length,
259
- rowGroupsPruned: candidateRowGroupIndices.length - rowGroupIndices.length,
260
- rowGroupsPrunedByStatistics
425
+ rowGroupsRequested: physicalPlan.requested,
426
+ rowGroupsPruned: physicalPlan.requested - rowGroupIndices.length,
427
+ rowGroupsPrunedByStatistics: physicalPlan.prunedByStatistics,
428
+ rowGroupsPrunedByBloomFilter: physicalPlan.prunedByBloomFilter,
429
+ bloomFiltersRead: physicalPlan.bloomFiltersRead,
430
+ bloomFilterBytesRead: physicalPlan.bloomFilterBytesRead
261
431
  },
262
432
  {}
263
433
  );
@@ -271,9 +441,12 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
271
441
  const projectedSchema = projectSchema(initialization.schema, columns);
272
442
  const projectedColumnNames = columns.length ? new Set(columns) : undefined;
273
443
  const workerOptions = this.getWorkerOptions(concurrency, readContext.abortController.signal);
274
- const decodeOnWorker = canDecodeParquetSourceOnWorker(workerOptions);
444
+ const decodeOnWorker =
445
+ canDecodeParquetSourceOnWorker(workerOptions) &&
446
+ (!initialization.reader.encrypted || Boolean(this.options.parquet?.keyRetriever));
275
447
  const scheduledReads = new Map<number, Promise<SettledParquetRowGroupRead>>();
276
448
  let nextPositionToSchedule = 0;
449
+ let remainingRows = readOptions.limit ?? Number.POSITIVE_INFINITY;
277
450
 
278
451
  const scheduleReads = (): void => {
279
452
  while (
@@ -329,21 +502,30 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
329
502
  {arrowConversionDurationMs: deserializationDurationMs},
330
503
  {rowGroupIndex, durationMs: deserializationDurationMs}
331
504
  );
332
- const batch = createParquetBatchFromArrow(
333
- initialization.metadata,
334
- projectedSchema,
335
- rowGroupIndex,
336
- workerBatch.rowGroupRowOffset,
337
- arrowTable,
338
- workerBatch.rowCount,
339
- workerBatch.rowGroupRowIndices
505
+ const batch = sliceParquetBatch(
506
+ createParquetBatchFromArrow(
507
+ initialization.metadata,
508
+ projectedSchema,
509
+ rowGroupIndex,
510
+ workerBatch.rowGroupRowOffset,
511
+ arrowTable,
512
+ workerBatch.rowCount,
513
+ workerBatch.rowGroupRowIndices
514
+ ),
515
+ Math.min(workerBatch.rowCount, remainingRows)
340
516
  );
341
517
  this.recordTelemetry(
342
518
  'batch',
343
- {batchesEmitted: 1, rowsEmitted: workerBatch.rowCount},
344
- {rowGroupIndex, rowCount: workerBatch.rowCount}
519
+ {batchesEmitted: 1, rowsEmitted: batch.length},
520
+ {rowGroupIndex, rowCount: batch.length}
345
521
  );
346
522
  yield batch;
523
+ remainingRows -= batch.length;
524
+ if (remainingRows === 0) {
525
+ limitReached = true;
526
+ completed = true;
527
+ return;
528
+ }
347
529
  }
348
530
  continue;
349
531
  }
@@ -356,7 +538,11 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
356
538
  outputRowOffset += outputBatchSize
357
539
  ) {
358
540
  throwIfAborted(readContext.abortController.signal);
359
- const batchRowCount = Math.min(outputBatchSize, outputRowCount - outputRowOffset);
541
+ const batchRowCount = Math.min(
542
+ outputBatchSize,
543
+ outputRowCount - outputRowOffset,
544
+ remainingRows
545
+ );
360
546
  const batchRowIndices = rowIndices
361
547
  ? rowIndices!.slice(outputRowOffset, outputRowOffset + batchRowCount)
362
548
  : undefined;
@@ -388,6 +574,12 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
388
574
  {rowGroupIndex, rowCount: batchRowCount}
389
575
  );
390
576
  yield batch;
577
+ remainingRows -= batch.length;
578
+ if (remainingRows === 0) {
579
+ limitReached = true;
580
+ completed = true;
581
+ return;
582
+ }
391
583
  }
392
584
  }
393
585
  completed = true;
@@ -403,11 +595,201 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
403
595
  if (!completed && readError === undefined) {
404
596
  this.recordTelemetry('cancel', {cancellationCount: 1}, {});
405
597
  }
598
+ const cancelled = readContext.abortController.signal.aborted || readOptions.signal?.aborted;
406
599
  readContext.abortController.abort();
407
600
  readContext.removeSignalListener();
408
601
  this.activeReadControllers.delete(readContext.abortController);
409
602
  await Promise.allSettled([...inFlightReads]);
603
+ const status: ScanExecutionTelemetry['status'] = readError
604
+ ? cancelled
605
+ ? 'cancelled'
606
+ : 'failed'
607
+ : limitReached || !completed
608
+ ? 'early-terminated'
609
+ : 'completed';
610
+ emitScanExecutionTelemetry(
611
+ options.onTelemetry,
612
+ createParquetScanExecutionTelemetry(
613
+ telemetryBefore,
614
+ this.getTelemetry(),
615
+ startedAt,
616
+ status,
617
+ limitReached
618
+ ? 'limit'
619
+ : !completed && readError === undefined
620
+ ? 'consumer-return'
621
+ : undefined,
622
+ readError
623
+ )
624
+ );
625
+ }
626
+ }
627
+
628
+ /** Produces the physical row-group plan shared by explain and execution. */
629
+ private async planRowGroups(
630
+ initialization: ParquetSourceInitialization,
631
+ readOptions: ParquetSourceReadOptions,
632
+ signal: AbortSignal
633
+ ): Promise<ParquetPhysicalRowGroupPlan> {
634
+ throwIfAborted(signal);
635
+ const candidateRowGroupIndices = normalizeRowGroupIndices(
636
+ readOptions.rowGroups,
637
+ initialization.fileMetadata.row_groups.length
638
+ );
639
+ const callbackFilteredRowGroupIndices = readOptions.rowGroupFilter
640
+ ? candidateRowGroupIndices.filter(rowGroupIndex =>
641
+ readOptions.rowGroupFilter!(initialization.metadata.rowGroups[rowGroupIndex])
642
+ )
643
+ : candidateRowGroupIndices;
644
+ const spatiallyFilteredRowGroupIndices = readOptions.bbox
645
+ ? callbackFilteredRowGroupIndices.filter(rowGroupIndex =>
646
+ canGeoParquetRowGroupMatch(
647
+ initialization.metadata,
648
+ initialization.metadata.rowGroups[rowGroupIndex],
649
+ readOptions.bbox!,
650
+ readOptions.geometryColumn
651
+ )
652
+ )
653
+ : callbackFilteredRowGroupIndices;
654
+ const spatialPredicate = readOptions.bbox
655
+ ? createGeoParquetBoundingBoxPredicate(
656
+ initialization.metadata,
657
+ readOptions.bbox,
658
+ readOptions.geometryColumn
659
+ )
660
+ : undefined;
661
+ const predicate = combineParquetPredicates(readOptions.predicate, spatialPredicate);
662
+ if (predicate) {
663
+ validateParquetPredicate(
664
+ predicate,
665
+ new Set(initialization.schema.fields.map(field => field.name))
666
+ );
667
+ }
668
+ const statisticsRowGroupIndices = predicate
669
+ ? spatiallyFilteredRowGroupIndices.filter(rowGroupIndex =>
670
+ canParquetRowGroupMatch(predicate, initialization.metadata.rowGroups[rowGroupIndex])
671
+ )
672
+ : spatiallyFilteredRowGroupIndices;
673
+ const bloomFilterResult = predicate
674
+ ? await filterParquetRowGroupsWithBloomFilters(
675
+ initialization,
676
+ statisticsRowGroupIndices,
677
+ predicate,
678
+ signal
679
+ )
680
+ : {rowGroupIndices: statisticsRowGroupIndices, filtersRead: 0, bytesRead: 0};
681
+ return {
682
+ predicate,
683
+ rowGroupIndices: bloomFilterResult.rowGroupIndices,
684
+ requested: candidateRowGroupIndices.length,
685
+ prunedByCallback: candidateRowGroupIndices.length - callbackFilteredRowGroupIndices.length,
686
+ prunedBySpatial:
687
+ callbackFilteredRowGroupIndices.length - spatiallyFilteredRowGroupIndices.length,
688
+ prunedByStatistics:
689
+ spatiallyFilteredRowGroupIndices.length - statisticsRowGroupIndices.length,
690
+ prunedByBloomFilter:
691
+ statisticsRowGroupIndices.length - bloomFilterResult.rowGroupIndices.length,
692
+ bloomFiltersRead: bloomFilterResult.filtersRead,
693
+ bloomFilterBytesRead: bloomFilterResult.bytesRead
694
+ };
695
+ }
696
+
697
+ /** Produces explainable page-index and byte-range plans for retained row groups. */
698
+ private async planPages(
699
+ initialization: ParquetSourceInitialization,
700
+ readOptions: ParquetSourceReadOptions,
701
+ rowGroupPlan: ParquetPhysicalRowGroupPlan,
702
+ signal: AbortSignal
703
+ ): Promise<ParquetPageScanPlan[]> {
704
+ if (!rowGroupPlan.predicate) return [];
705
+ const columns = normalizeColumns(readOptions.columns, initialization.schema);
706
+ const predicateColumns = getParquetPredicateColumns(rowGroupPlan.predicate);
707
+ const decodedColumns =
708
+ columns.length === 0 ? [] : [...new Set([...columns, ...predicateColumns])];
709
+ const hasHiddenPredicateColumns =
710
+ columns.length > 0 && predicateColumns.some(column => !columns.includes(column));
711
+ const phases = hasHiddenPredicateColumns
712
+ ? [
713
+ {
714
+ phase: 'predicate' as const,
715
+ columns: predicateColumns.map(column => [column]),
716
+ predicate: rowGroupPlan.predicate
717
+ },
718
+ {
719
+ phase: 'projection' as const,
720
+ columns: columns.map(column => [column]),
721
+ predicate: undefined
722
+ }
723
+ ]
724
+ : [
725
+ {
726
+ phase: 'combined' as const,
727
+ columns: decodedColumns.map(column => [column]),
728
+ predicate: rowGroupPlan.predicate
729
+ }
730
+ ];
731
+ const plans: ParquetPageScanPlan[] = [];
732
+ for (const rowGroupIndex of rowGroupPlan.rowGroupIndices) {
733
+ const rowGroup = initialization.fileMetadata.row_groups[rowGroupIndex];
734
+ let predicateProvedEmpty = false;
735
+ for (const phase of phases) {
736
+ throwIfAborted(signal);
737
+ if (phase.phase === 'projection' && predicateProvedEmpty) {
738
+ continue;
739
+ }
740
+ await initialization.reader.resolveColumnMetadata(rowGroup, rowGroupIndex, phase.columns);
741
+ const pagePlan = phase.predicate
742
+ ? await createParquetPagePruningPlan(
743
+ initialization.file,
744
+ rowGroup,
745
+ initialization.parquetSchema,
746
+ phase.columns,
747
+ phase.predicate,
748
+ signal,
749
+ {
750
+ rowGroupOrdinal: rowGroupIndex,
751
+ int96AsTimestamp: Boolean(this.options.parquet?.int96AsTimestamp),
752
+ decryptModule: (bytes, module, rowGroupOrdinal, columnOrdinal, columnChunk) =>
753
+ initialization.reader.decryptIndexModule(
754
+ bytes,
755
+ module,
756
+ rowGroupOrdinal,
757
+ columnOrdinal,
758
+ columnChunk
759
+ )
760
+ }
761
+ )
762
+ : undefined;
763
+ plans.push(
764
+ pagePlan
765
+ ? Object.freeze({
766
+ rowGroupIndex,
767
+ phase: phase.phase,
768
+ columns: Object.freeze(phase.columns.map(column => Object.freeze([...column]))),
769
+ rowRanges: Object.freeze(
770
+ pagePlan.rowRanges.map(rowRange => Object.freeze({...rowRange}))
771
+ ),
772
+ indexesRead: pagePlan.indexCount,
773
+ totalPages: pagePlan.totalPageCount,
774
+ selectedPages: pagePlan.selectedPageCount,
775
+ rowsPruned: pagePlan.prunedRowCount,
776
+ ranges: Object.freeze(
777
+ getParquetPageReadRanges(
778
+ rowGroup,
779
+ phase.columns,
780
+ pagePlan,
781
+ initialization.parquetSchema
782
+ ).map(range => Object.freeze({...range}))
783
+ )
784
+ })
785
+ : createFullColumnScanPlan(rowGroup, rowGroupIndex, phase.phase, phase.columns)
786
+ );
787
+ if (phase.phase === 'predicate' && pagePlan?.rowRanges.length === 0) {
788
+ predicateProvedEmpty = true;
789
+ }
790
+ }
410
791
  }
792
+ return plans;
411
793
  }
412
794
 
413
795
  /** Closes the underlying readable file and aborts active remote requests. */
@@ -421,6 +803,7 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
421
803
  }
422
804
  await this.readableFile?.close();
423
805
  const initialization = await this.initializationPromise?.catch(() => null);
806
+ initialization?.reader.close();
424
807
  if (initialization?.file !== this.readableFile) {
425
808
  await initialization?.file.close();
426
809
  }
@@ -458,11 +841,17 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
458
841
  const rowGroups = options.rowGroups ?? this.options.parquet?.rowGroups;
459
842
  const columns = options.columns ?? this.options.parquet?.columns;
460
843
  const predicate = options.predicate ?? this.options.parquet?.predicate;
844
+ const bbox = options.bbox ?? this.options.parquet?.bbox;
845
+ const limit = options.limit ?? this.options.parquet?.limit;
846
+ validateTableQueryLimit(limit);
461
847
  return {
462
848
  rowGroups: rowGroups && [...rowGroups],
463
849
  columns: columns && [...columns],
464
850
  rowGroupFilter: options.rowGroupFilter ?? this.options.parquet?.rowGroupFilter,
465
851
  predicate: predicate ? copyParquetPredicate(predicate) : undefined,
852
+ limit,
853
+ bbox: bbox ? ([...bbox] as ParquetSourceReadOptions['bbox']) : undefined,
854
+ geometryColumn: options.geometryColumn ?? this.options.parquet?.geometryColumn,
466
855
  batchSize: options.batchSize ?? this.options.parquet?.batchSize,
467
856
  concurrency: options.concurrency ?? this.options.parquet?.concurrency,
468
857
  signal: options.signal
@@ -500,7 +889,25 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
500
889
  workerOptions: ParquetSourceWorkerOptions | undefined,
501
890
  signal: AbortSignal
502
891
  ): Promise<ParquetRowGroupReadResult> {
892
+ if (
893
+ predicate &&
894
+ projectedColumnNames &&
895
+ getParquetPredicateColumns(predicate).some(column => !projectedColumnNames.has(column))
896
+ ) {
897
+ return await this.readRowGroupWithLateMaterialization(
898
+ initialization,
899
+ rowGroupIndex,
900
+ columnList,
901
+ projectedSchema,
902
+ batchSize,
903
+ predicate,
904
+ projectedColumnNames,
905
+ workerOptions,
906
+ signal
907
+ );
908
+ }
503
909
  const rowGroup = initialization.fileMetadata.row_groups[rowGroupIndex];
910
+ await initialization.reader.resolveColumnMetadata(rowGroup, rowGroupIndex, columnList);
504
911
  const pagePlan = predicate
505
912
  ? await createParquetPagePruningPlan(
506
913
  initialization.file,
@@ -508,7 +915,19 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
508
915
  initialization.parquetSchema,
509
916
  columnList,
510
917
  predicate,
511
- signal
918
+ signal,
919
+ {
920
+ rowGroupOrdinal: rowGroupIndex,
921
+ int96AsTimestamp: Boolean(this.options.parquet?.int96AsTimestamp),
922
+ decryptModule: (bytes, module, rowGroupOrdinal, columnOrdinal, columnChunk) =>
923
+ initialization.reader.decryptIndexModule(
924
+ bytes,
925
+ module,
926
+ rowGroupOrdinal,
927
+ columnOrdinal,
928
+ columnChunk
929
+ )
930
+ }
512
931
  )
513
932
  : undefined;
514
933
  if (pagePlan) {
@@ -542,7 +961,8 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
542
961
  columnList,
543
962
  rowRange,
544
963
  pagePlan.pageLocations,
545
- signal
964
+ signal,
965
+ rowGroupIndex
546
966
  )
547
967
  )
548
968
  )
@@ -551,7 +971,8 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
551
971
  initialization.parquetSchema,
552
972
  rowGroup,
553
973
  columnList,
554
- signal
974
+ signal,
975
+ rowGroupIndex
555
976
  )
556
977
  ];
557
978
  throwIfAborted(signal);
@@ -615,7 +1036,7 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
615
1036
  return Boolean(path && (columnList.length === 0 || fieldIndexOf(columnList, path) >= 0));
616
1037
  });
617
1038
  const rangeDescriptors = pagePlan
618
- ? getParquetPageReadRanges(rowGroup, columnList, pagePlan)
1039
+ ? getParquetPageReadRanges(rowGroup, columnList, pagePlan, initialization.parquetSchema)
619
1040
  : selectedColumnChunks.map(getColumnChunkRange);
620
1041
  const ranges = await Promise.all(
621
1042
  rangeDescriptors.map(async ({offset, length}) => {
@@ -624,6 +1045,26 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
624
1045
  );
625
1046
  throwIfAborted(signal);
626
1047
 
1048
+ const workerColumnChunks = await Promise.all(
1049
+ selectedColumnChunks.map(async columnChunk => {
1050
+ const descriptor = createParquetSourceWorkerColumnChunk(columnChunk);
1051
+ descriptor.columnOrdinal = rowGroup.columns.indexOf(columnChunk);
1052
+ if (columnChunk.crypto_metadata) {
1053
+ const columnOrdinal = rowGroup.columns.indexOf(columnChunk);
1054
+ const key = await initialization.reader.getColumnKeyForWorker(
1055
+ columnChunk,
1056
+ rowGroupIndex,
1057
+ columnOrdinal
1058
+ );
1059
+ descriptor.encrypted = true;
1060
+ descriptor.keyMetadata = key.keyMetadata?.slice().buffer;
1061
+ descriptor.keyMaterial = key.keyMaterial.slice().buffer;
1062
+ }
1063
+ return descriptor;
1064
+ })
1065
+ );
1066
+ const encryptionContext = initialization.reader.getEncryptionContextForWorker();
1067
+
627
1068
  let workerResult: ParquetSourceWorkerResult;
628
1069
  try {
629
1070
  workerResult = await decodeParquetSourceRowGroupOnWorker(
@@ -633,12 +1074,21 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
633
1074
  uncompressedByteLength: Number(rowGroup.total_byte_size),
634
1075
  schemaDefinition: initialization.parquetSchema.schema,
635
1076
  projectedSchema,
636
- columnChunks: selectedColumnChunks.map(createParquetSourceWorkerColumnChunk),
1077
+ columnChunks: workerColumnChunks,
637
1078
  ranges,
638
1079
  batchSize: batchSize || Math.max(Number(rowGroup.num_rows), 1),
639
1080
  predicate: predicate ? copyParquetPredicate(predicate) : undefined,
640
1081
  pagePlan,
641
- preserveBinary: Boolean(this.options.parquet?.preserveBinary)
1082
+ encryption: encryptionContext
1083
+ ? {
1084
+ algorithm: encryptionContext.algorithm,
1085
+ aadPrefix: encryptionContext.aadPrefix?.slice().buffer,
1086
+ fileUnique: encryptionContext.fileUnique.slice().buffer
1087
+ }
1088
+ : undefined,
1089
+ preserveBinary: Boolean(this.options.parquet?.preserveBinary),
1090
+ int96AsTimestamp: Boolean(this.options.parquet?.int96AsTimestamp),
1091
+ verifyPageChecksums: Boolean(this.options.parquet?.verifyPageChecksums)
642
1092
  },
643
1093
  workerOptions
644
1094
  );
@@ -670,6 +1120,72 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
670
1120
  return {rowGroupIndex, rowCount: workerResult.rowCount, workerResult};
671
1121
  }
672
1122
 
1123
+ /** Decodes predicate columns first, avoiding projected-column decoding for empty matches. */
1124
+ private async readRowGroupWithLateMaterialization(
1125
+ initialization: ParquetSourceInitialization,
1126
+ rowGroupIndex: number,
1127
+ columnList: string[][],
1128
+ projectedSchema: Schema,
1129
+ batchSize: number | undefined,
1130
+ predicate: ParquetPredicate,
1131
+ projectedColumnNames: ReadonlySet<string>,
1132
+ workerOptions: ParquetSourceWorkerOptions | undefined,
1133
+ signal: AbortSignal
1134
+ ): Promise<ParquetRowGroupReadResult> {
1135
+ const predicateColumns = getParquetPredicateColumns(predicate);
1136
+ const predicateColumnList = predicateColumns.map(column => [column]);
1137
+ const filterResult = await this.readRowGroup(
1138
+ initialization,
1139
+ rowGroupIndex,
1140
+ predicateColumnList,
1141
+ projectSchema(initialization.schema, predicateColumns),
1142
+ batchSize,
1143
+ predicate,
1144
+ new Set(predicateColumns),
1145
+ workerOptions,
1146
+ signal
1147
+ );
1148
+ const rowIndices = filterResult.rowIndices;
1149
+ const selectedRowIndices = rowIndices || getWorkerRowIndices(filterResult.workerResult);
1150
+ if (!selectedRowIndices || selectedRowIndices.length === 0) {
1151
+ return {rowGroupIndex, rowCount: 0, columns: {}, rowIndices: []};
1152
+ }
1153
+ const projectedColumnList = columnList.filter(columnPath =>
1154
+ projectedColumnNames.has(columnPath[0])
1155
+ );
1156
+ const projectedResult = await this.readRowGroup(
1157
+ initialization,
1158
+ rowGroupIndex,
1159
+ projectedColumnList,
1160
+ projectedSchema,
1161
+ batchSize,
1162
+ undefined,
1163
+ projectedColumnNames,
1164
+ workerOptions,
1165
+ signal
1166
+ );
1167
+ if (workerOptions && projectedResult.workerResult) {
1168
+ return {
1169
+ rowGroupIndex,
1170
+ rowCount: selectedRowIndices.length,
1171
+ rowIndices: selectedRowIndices,
1172
+ workerResult: gatherWorkerProjectedRows(
1173
+ projectedResult.workerResult,
1174
+ selectedRowIndices,
1175
+ batchSize
1176
+ )
1177
+ };
1178
+ }
1179
+ return {
1180
+ rowGroupIndex,
1181
+ rowCount: selectedRowIndices.length,
1182
+ rowIndices: selectedRowIndices,
1183
+ columns: projectedResult.columns
1184
+ ? gatherParquetColumns(projectedResult.columns, selectedRowIndices, projectedColumnNames)
1185
+ : {}
1186
+ };
1187
+ }
1188
+
673
1189
  /** Records the rows and pages avoided by one conservative page-index plan. */
674
1190
  private recordPagePruningTelemetry(rowGroupIndex: number, plan: ParquetPagePruningPlan): void {
675
1191
  this.recordTelemetry(
@@ -692,6 +1208,11 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
692
1208
  try {
693
1209
  const reader = new ParquetReader(file, {
694
1210
  preserveBinary: this.options.parquet?.preserveBinary,
1211
+ int96AsTimestamp: this.options.parquet?.int96AsTimestamp,
1212
+ verifyPageChecksums: this.options.parquet?.verifyPageChecksums,
1213
+ verifyFooterSignature: this.options.parquet?.verifyFooterSignature,
1214
+ keyRetriever: this.options.parquet?.keyRetriever,
1215
+ aadPrefix: this.options.parquet?.aadPrefix,
695
1216
  signal
696
1217
  });
697
1218
  const fileMetadata = await reader.getFileMetadata();
@@ -764,6 +1285,29 @@ export class ParquetSource extends DataSource<string | Blob, ParquetSourceLoader
764
1285
  }
765
1286
  }
766
1287
 
1288
+ function getParquetColumnRoles(columnNames: readonly string[]): Record<string, ScanColumnRole> {
1289
+ const roles: Record<string, ScanColumnRole> = {};
1290
+ for (const columnName of columnNames) {
1291
+ const normalizedName = columnName.toLowerCase();
1292
+ if (normalizedName === 'geometry' || normalizedName === 'geom' || normalizedName === 'shape') {
1293
+ roles[columnName] = 'geometry';
1294
+ } else if (
1295
+ normalizedName === 'longitude' ||
1296
+ normalizedName === 'lon' ||
1297
+ normalizedName === 'x'
1298
+ ) {
1299
+ roles[columnName] = 'longitude';
1300
+ } else if (
1301
+ normalizedName === 'latitude' ||
1302
+ normalizedName === 'lat' ||
1303
+ normalizedName === 'y'
1304
+ ) {
1305
+ roles[columnName] = 'latitude';
1306
+ }
1307
+ }
1308
+ return roles;
1309
+ }
1310
+
767
1311
  /** Normalizes a decoded Parquet footer into source metadata. */
768
1312
  function createParquetSourceMetadata(
769
1313
  data: string | Blob,
@@ -784,7 +1328,8 @@ function createParquetSourceMetadata(
784
1328
  rowOffset,
785
1329
  Number(rowGroup.num_rows),
786
1330
  Number(rowGroup.total_byte_size),
787
- columns
1331
+ columns,
1332
+ rowGroup.sorting_columns
788
1333
  );
789
1334
  rowOffset += normalizedRowGroup.rowCount;
790
1335
  return normalizedRowGroup;
@@ -821,6 +1366,8 @@ function createColumnChunkMetadata(
821
1366
  columnMetadata.statistics,
822
1367
  parquetSchema.findField(columnMetadata.path_in_schema)
823
1368
  );
1369
+ const geospatialStatistics = createGeospatialStatistics(columnMetadata.geospatial_statistics);
1370
+ const sizeStatistics = createSizeStatistics(columnMetadata.size_statistics);
824
1371
  const compressedByteLength = Number(columnMetadata.total_compressed_size);
825
1372
  const uncompressedByteLength = Number(columnMetadata.total_uncompressed_size);
826
1373
  return Object.freeze({
@@ -851,7 +1398,92 @@ function createColumnChunkMetadata(
851
1398
  ? undefined
852
1399
  : Number(columnChunk.offset_index_offset),
853
1400
  offsetIndexByteLength: columnChunk.offset_index_length,
854
- statistics
1401
+ bloomFilterOffset:
1402
+ columnMetadata.bloom_filter_offset === undefined
1403
+ ? undefined
1404
+ : Number(columnMetadata.bloom_filter_offset),
1405
+ bloomFilterByteLength: columnMetadata.bloom_filter_length,
1406
+ statistics,
1407
+ sizeStatistics,
1408
+ geospatialStatistics
1409
+ });
1410
+ }
1411
+
1412
+ /** Normalizes optional Parquet size statistics without inventing missing counts. */
1413
+ function createSizeStatistics(
1414
+ statistics:
1415
+ | {
1416
+ unencoded_byte_array_data_bytes?: {toNumber?: () => number} | number;
1417
+ repetition_level_histogram?: Array<{toNumber?: () => number} | number>;
1418
+ definition_level_histogram?: Array<{toNumber?: () => number} | number>;
1419
+ }
1420
+ | undefined
1421
+ ): ParquetColumnChunkSizeStatistics | undefined {
1422
+ if (!statistics) return undefined;
1423
+ const result: ParquetColumnChunkSizeStatistics = {
1424
+ unencodedByteArrayDataBytes:
1425
+ statistics.unencoded_byte_array_data_bytes === undefined
1426
+ ? undefined
1427
+ : toSafeNumber(statistics.unencoded_byte_array_data_bytes),
1428
+ repetitionLevelHistogram: normalizeSizeStatisticsList(statistics.repetition_level_histogram),
1429
+ definitionLevelHistogram: normalizeSizeStatisticsList(statistics.definition_level_histogram)
1430
+ };
1431
+ if (
1432
+ result.unencodedByteArrayDataBytes === undefined &&
1433
+ result.repetitionLevelHistogram === undefined &&
1434
+ result.definitionLevelHistogram === undefined
1435
+ ) {
1436
+ return undefined;
1437
+ }
1438
+ return Object.freeze({
1439
+ ...result,
1440
+ repetitionLevelHistogram: result.repetitionLevelHistogram
1441
+ ? Object.freeze(result.repetitionLevelHistogram)
1442
+ : undefined,
1443
+ definitionLevelHistogram: result.definitionLevelHistogram
1444
+ ? Object.freeze(result.definitionLevelHistogram)
1445
+ : undefined
1446
+ });
1447
+ }
1448
+
1449
+ function toSafeNumber(value: {toNumber?: () => number} | number): number | undefined {
1450
+ const numberValue = typeof value === 'number' ? value : value.toNumber?.();
1451
+ return numberValue !== undefined && Number.isSafeInteger(numberValue) && numberValue >= 0
1452
+ ? numberValue
1453
+ : undefined;
1454
+ }
1455
+
1456
+ /** Converts a Thrift int64 histogram only when every entry is a safe non-negative integer. */
1457
+ function normalizeSizeStatisticsList(
1458
+ values: Array<{toNumber?: () => number} | number> | undefined
1459
+ ): readonly number[] | undefined {
1460
+ if (!values) return undefined;
1461
+ const normalized = values.map(toSafeNumber);
1462
+ return normalized.every((value): value is number => value !== undefined) ? normalized : undefined;
1463
+ }
1464
+
1465
+ /** Normalizes native Parquet geospatial statistics from the decoded footer. */
1466
+ function createGeospatialStatistics(
1467
+ statistics: ParquetThriftGeospatialStatistics | undefined
1468
+ ): ParquetGeospatialStatistics | undefined {
1469
+ if (!statistics) return undefined;
1470
+ const bbox = statistics.bbox
1471
+ ? Object.freeze({
1472
+ xmin: statistics.bbox.xmin,
1473
+ xmax: statistics.bbox.xmax,
1474
+ ymin: statistics.bbox.ymin,
1475
+ ymax: statistics.bbox.ymax,
1476
+ zmin: statistics.bbox.zmin,
1477
+ zmax: statistics.bbox.zmax,
1478
+ mmin: statistics.bbox.mmin,
1479
+ mmax: statistics.bbox.mmax
1480
+ })
1481
+ : undefined;
1482
+ return Object.freeze({
1483
+ bbox,
1484
+ geometryTypes: statistics.geospatial_types
1485
+ ? Object.freeze([...statistics.geospatial_types])
1486
+ : undefined
855
1487
  });
856
1488
  }
857
1489
 
@@ -933,7 +1565,8 @@ function createRowGroupMetadata(
933
1565
  rowOffset: number,
934
1566
  rowCount: number,
935
1567
  uncompressedByteLength: number,
936
- columns: ParquetColumnChunkMetadata[]
1568
+ columns: ParquetColumnChunkMetadata[],
1569
+ sortingColumns: RowGroup['sorting_columns']
937
1570
  ): ParquetRowGroupMetadata {
938
1571
  const compressedByteLength = columns.reduce(
939
1572
  (sum, column) => sum + column.compressedByteLength,
@@ -947,10 +1580,54 @@ function createRowGroupMetadata(
947
1580
  uncompressedSize: uncompressedByteLength,
948
1581
  compressedByteLength,
949
1582
  compressedSize: compressedByteLength,
950
- columns: Object.freeze(columns)
1583
+ columns: Object.freeze(columns),
1584
+ sortingColumns: Object.freeze(
1585
+ (sortingColumns || []).map(
1586
+ sortingColumn =>
1587
+ Object.freeze({
1588
+ columnIndex: sortingColumn.column_idx,
1589
+ descending: sortingColumn.descending,
1590
+ nullsFirst: sortingColumn.nulls_first
1591
+ }) satisfies ParquetSortingColumn
1592
+ )
1593
+ )
951
1594
  });
952
1595
  }
953
1596
 
1597
+ /** Describes a phase that reads complete column chunks because no page index is available. */
1598
+ function createFullColumnScanPlan(
1599
+ rowGroup: RowGroup,
1600
+ rowGroupIndex: number,
1601
+ phase: ParquetPageScanPlan['phase'],
1602
+ columns: readonly string[][]
1603
+ ): ParquetPageScanPlan {
1604
+ const selectedColumnChunks = rowGroup.columns.filter(columnChunk => {
1605
+ const path = columnChunk.meta_data?.path_in_schema;
1606
+ return Boolean(
1607
+ path && (columns.length === 0 || columns.some(column => pathMatchesSelection(column, path)))
1608
+ );
1609
+ });
1610
+ const rowCount = Number(rowGroup.num_rows);
1611
+ return Object.freeze({
1612
+ rowGroupIndex,
1613
+ phase,
1614
+ columns: Object.freeze(columns.map(column => Object.freeze([...column]))),
1615
+ rowRanges: Object.freeze([Object.freeze({start: 0, end: rowCount})]),
1616
+ indexesRead: 0,
1617
+ totalPages: 0,
1618
+ selectedPages: 0,
1619
+ rowsPruned: 0,
1620
+ ranges: Object.freeze(
1621
+ selectedColumnChunks.map(columnChunk => Object.freeze(getColumnChunkRange(columnChunk)))
1622
+ )
1623
+ });
1624
+ }
1625
+
1626
+ /** Returns whether a requested path selects a footer leaf or one of its nested children. */
1627
+ function pathMatchesSelection(selection: readonly string[], path: readonly string[]): boolean {
1628
+ return selection.length <= path.length && selection.every((part, index) => part === path[index]);
1629
+ }
1630
+
954
1631
  /** Returns the complete contiguous byte range occupied by one Parquet column chunk. */
955
1632
  function getColumnChunkRange(columnChunk: ColumnChunk): {offset: number; length: number} {
956
1633
  if (columnChunk.file_path) {
@@ -1048,11 +1725,8 @@ function createParquetBatchFromArrow(
1048
1725
  : undefined
1049
1726
  });
1050
1727
  return {
1051
- batchType: 'data',
1052
- shape: 'arrow-table',
1728
+ ...makeTableScanBatch({shape: 'arrow-table', schema, data}),
1053
1729
  schemaType: 'explicit',
1054
- schema,
1055
- data,
1056
1730
  length: rowCount,
1057
1731
  metadata: provenance,
1058
1732
  ...provenance
@@ -1173,6 +1847,9 @@ function createParquetTelemetry(): ParquetTelemetry {
1173
1847
  rowGroupsRequested: 0,
1174
1848
  rowGroupsPruned: 0,
1175
1849
  rowGroupsPrunedByStatistics: 0,
1850
+ rowGroupsPrunedByBloomFilter: 0,
1851
+ bloomFiltersRead: 0,
1852
+ bloomFilterBytesRead: 0,
1176
1853
  rowGroupsPrunedByPageIndex: 0,
1177
1854
  pageIndexesRead: 0,
1178
1855
  pagesRead: 0,
@@ -1188,6 +1865,45 @@ function createParquetTelemetry(): ParquetTelemetry {
1188
1865
  };
1189
1866
  }
1190
1867
 
1868
+ /** Converts cumulative Parquet counters into one portable read-scoped execution snapshot. */
1869
+ function createParquetScanExecutionTelemetry(
1870
+ before: ParquetTelemetry,
1871
+ after: ParquetTelemetry,
1872
+ startedAt: number,
1873
+ status: ScanExecutionTelemetry['status'],
1874
+ earlyTerminationReason?: ScanExecutionTelemetry['earlyTerminationReason'],
1875
+ error?: unknown
1876
+ ): ScanExecutionTelemetry {
1877
+ const delta = Object.fromEntries(
1878
+ Object.keys(after).map(key => {
1879
+ const telemetryKey = key as keyof ParquetTelemetry;
1880
+ return [key, after[telemetryKey] - before[telemetryKey]];
1881
+ })
1882
+ ) as ParquetTelemetry;
1883
+ const rowsRead = Math.max(delta.predicateRowsTested, delta.rowsEmitted);
1884
+ return Object.freeze({
1885
+ status,
1886
+ sourcesPlanned: 1,
1887
+ sourcesRead:
1888
+ status === 'early-terminated' && earlyTerminationReason === 'limit' && rowsRead === 0 ? 0 : 1,
1889
+ batchesRead: delta.batchesEmitted,
1890
+ batchesDecoded: delta.rowGroupsDecoded,
1891
+ rowsRead,
1892
+ rowsTested: delta.predicateRowsTested || undefined,
1893
+ rowsRetained: delta.predicateRowsMatched || undefined,
1894
+ rowsReturned: delta.rowsEmitted,
1895
+ bytesRead: delta.downloadedBytes,
1896
+ bytesFetched: delta.downloadedBytes,
1897
+ filesOpened: rowsRead || delta.downloadedBytes ? 1 : 0,
1898
+ tasksOpened: delta.rowGroupsDecoded,
1899
+ rowsPruned: delta.rowsPrunedByPageIndex,
1900
+ durationMilliseconds: Date.now() - startedAt,
1901
+ earlyTerminationReason,
1902
+ details: Object.freeze({...delta}),
1903
+ ...(error === undefined ? {} : {error})
1904
+ });
1905
+ }
1906
+
1191
1907
  /** Returns a monotonic timestamp when available and falls back to wall-clock time. */
1192
1908
  function getCurrentTime(): number {
1193
1909
  return globalThis.performance?.now() ?? Date.now();
@@ -1259,6 +1975,167 @@ function getSourceName(data: string | Blob, url: string): string {
1259
1975
  return sourceName.replace(/\.parquet$/i, '') || 'parquet';
1260
1976
  }
1261
1977
 
1978
+ type ParquetBloomFilterReadResult = {
1979
+ rowGroupIndices: number[];
1980
+ filtersRead: number;
1981
+ bytesRead: number;
1982
+ };
1983
+
1984
+ /** Reconstructs exact row-group indexes from worker predicate batches. */
1985
+ function getWorkerRowIndices(workerResult?: ParquetSourceWorkerResult): number[] {
1986
+ if (!workerResult) return [];
1987
+ return workerResult.batches.flatMap(batch =>
1988
+ batch.rowGroupRowIndices
1989
+ ? [...batch.rowGroupRowIndices]
1990
+ : Array.from({length: batch.rowCount}, (_, index) => batch.rowGroupRowOffset + index)
1991
+ );
1992
+ }
1993
+
1994
+ /** Gathers projected worker Arrow batches through the shared indexed Arrow table utility. */
1995
+ function gatherWorkerProjectedRows(
1996
+ workerResult: ParquetSourceWorkerResult,
1997
+ rowIndices: readonly number[],
1998
+ batchSize: number | undefined
1999
+ ): ParquetSourceWorkerResult {
2000
+ const tables = workerResult.batches.map(batch => hydrateArrowTable(batch.arrowTable));
2001
+ if (tables.length === 0) {
2002
+ return {...workerResult, rowCount: 0, batches: []};
2003
+ }
2004
+ const table = tables.length === 1 ? tables[0] : tables[0].concat(...tables.slice(1));
2005
+ const indexedTable = new IndexedArrowTable(table, rowIndices);
2006
+ const outputBatchSize = batchSize || Math.max(rowIndices.length, 1);
2007
+ const batches: ParquetSourceWorkerResult['batches'] = [];
2008
+ for (let offset = 0; offset < rowIndices.length; offset += outputBatchSize) {
2009
+ const end = Math.min(offset + outputBatchSize, rowIndices.length);
2010
+ const batchIndexes = rowIndices.slice(offset, end);
2011
+ const batchTable = indexedTable.slice(offset, end).materializeArrowTable();
2012
+ batches.push({
2013
+ rowGroupRowOffset: batchIndexes[0],
2014
+ rowCount: batchIndexes.length,
2015
+ rowGroupRowIndices: [...batchIndexes],
2016
+ arrowTable: dehydrateArrowTable(batchTable)
2017
+ });
2018
+ }
2019
+ return {
2020
+ sourceRowCount: workerResult.sourceRowCount,
2021
+ rowCount: rowIndices.length,
2022
+ batches,
2023
+ decodeDurationMs: workerResult.decodeDurationMs,
2024
+ arrowConversionDurationMs: workerResult.arrowConversionDurationMs
2025
+ };
2026
+ }
2027
+
2028
+ /** Applies only proven-negative Bloom-filter checks to candidate row groups. */
2029
+ async function filterParquetRowGroupsWithBloomFilters(
2030
+ initialization: ParquetSourceInitialization,
2031
+ rowGroupIndices: readonly number[],
2032
+ predicate: ParquetPredicate,
2033
+ signal: AbortSignal
2034
+ ): Promise<ParquetBloomFilterReadResult> {
2035
+ const rowGroups: number[] = [];
2036
+ let filtersRead = 0;
2037
+ let bytesRead = 0;
2038
+ for (const rowGroupIndex of rowGroupIndices) {
2039
+ const rowGroup = initialization.metadata.rowGroups[rowGroupIndex];
2040
+ const probes = getParquetBloomFilterProbes(predicate, rowGroup);
2041
+ let keepRowGroup = true;
2042
+ for (const probe of probes) {
2043
+ if (
2044
+ probe.column.bloomFilterOffset === undefined ||
2045
+ probe.column.bloomFilterByteLength === undefined
2046
+ ) {
2047
+ continue;
2048
+ }
2049
+ let field: ParquetField;
2050
+ try {
2051
+ field = initialization.parquetSchema.findField([...probe.column.path]);
2052
+ } catch {
2053
+ throwIfAborted(signal);
2054
+ continue;
2055
+ }
2056
+ if (!field.primitiveType) continue;
2057
+ let filter: ReturnType<typeof decodeParquetSplitBlockBloomFilter>;
2058
+ try {
2059
+ const rawColumnOrdinal = initialization.fileMetadata.row_groups[
2060
+ rowGroupIndex
2061
+ ].columns.findIndex(
2062
+ columnChunk =>
2063
+ JSON.stringify(columnChunk.meta_data?.path_in_schema) ===
2064
+ JSON.stringify(probe.column.path)
2065
+ );
2066
+ const rawColumnChunk =
2067
+ rawColumnOrdinal >= 0
2068
+ ? initialization.fileMetadata.row_groups[rowGroupIndex].columns[rawColumnOrdinal]
2069
+ : undefined;
2070
+ const data = await initialization.file.read(
2071
+ probe.column.bloomFilterOffset,
2072
+ probe.column.bloomFilterByteLength,
2073
+ signal
2074
+ );
2075
+ const decodedBloomFilter = rawColumnChunk
2076
+ ? await initialization.reader.decryptBloomFilter(
2077
+ toUint8Array(data),
2078
+ rowGroupIndex,
2079
+ rawColumnOrdinal,
2080
+ rawColumnChunk
2081
+ )
2082
+ : toUint8Array(data);
2083
+ filter = decodeParquetSplitBlockBloomFilter(decodedBloomFilter);
2084
+ filtersRead++;
2085
+ bytesRead += data.byteLength;
2086
+ } catch {
2087
+ throwIfAborted(signal);
2088
+ continue;
2089
+ }
2090
+ if (
2091
+ filter.algorithm !== 'BLOCK' ||
2092
+ filter.hash !== 'XXHASH' ||
2093
+ filter.compression !== 'UNCOMPRESSED'
2094
+ ) {
2095
+ continue;
2096
+ }
2097
+ const mayContainValue = probe.values.some(value => {
2098
+ try {
2099
+ if (value instanceof Date) return true;
2100
+ const physicalValue = getParquetBloomFilterPhysicalValue(value, field);
2101
+ const encoded = encodeParquetBloomFilterValue(
2102
+ physicalValue,
2103
+ field.primitiveType as Parameters<typeof encodeParquetBloomFilterValue>[1],
2104
+ field.typeLength
2105
+ );
2106
+ return checkParquetSplitBlockBloomFilter(
2107
+ filter.bitset,
2108
+ hashParquetBloomFilterValue(encoded)
2109
+ );
2110
+ } catch {
2111
+ throwIfAborted(signal);
2112
+ return true;
2113
+ }
2114
+ });
2115
+ if (!mayContainValue) {
2116
+ keepRowGroup = false;
2117
+ break;
2118
+ }
2119
+ }
2120
+ if (keepRowGroup) rowGroups.push(rowGroupIndex);
2121
+ }
2122
+ return {rowGroupIndices: rowGroups, filtersRead, bytesRead};
2123
+ }
2124
+
2125
+ /** Converts logical predicate values into the physical values stored in Bloom filters. */
2126
+ function getParquetBloomFilterPhysicalValue(
2127
+ value: unknown,
2128
+ field: ParquetField
2129
+ ): boolean | number | bigint | string | Uint8Array {
2130
+ if (field.originalType?.startsWith('DECIMAL_')) {
2131
+ return toPrimitive(field.originalType as ParquetType, value, field) as
2132
+ | number
2133
+ | bigint
2134
+ | Uint8Array;
2135
+ }
2136
+ return value as boolean | number | bigint | string | Uint8Array;
2137
+ }
2138
+
1262
2139
  /** Returns true when at least one HTTP object validator was captured. */
1263
2140
  function hasObjectVersion(version?: ParquetObjectVersion): version is ParquetObjectVersion {
1264
2141
  return Boolean(version?.etag || version?.lastModified);