web: sync-guard gradient repair, FineWeb-Edu hardcoded (parquet /data streaming), dispatch-optimized backward (bit-identical), IEEE-754 oracle + corpus mutation tests, TEST_RESULTS.md
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- web/TEST_RESULTS.md +93 -0
- web/node_modules/.package-lock.json +35 -0
- web/node_modules/hyparquet/LICENSE +7 -0
- web/node_modules/hyparquet/README.md +248 -0
- web/node_modules/hyparquet/package.json +68 -0
- web/node_modules/hyparquet/src/assemble.js +257 -0
- web/node_modules/hyparquet/src/bloom.js +224 -0
- web/node_modules/hyparquet/src/column.js +202 -0
- web/node_modules/hyparquet/src/constants.js +95 -0
- web/node_modules/hyparquet/src/convert.js +210 -0
- web/node_modules/hyparquet/src/datapage.js +246 -0
- web/node_modules/hyparquet/src/delta.js +104 -0
- web/node_modules/hyparquet/src/encoding.js +162 -0
- web/node_modules/hyparquet/src/filter.js +170 -0
- web/node_modules/hyparquet/src/geoparquet.js +38 -0
- web/node_modules/hyparquet/src/index.js +51 -0
- web/node_modules/hyparquet/src/indexes.js +46 -0
- web/node_modules/hyparquet/src/metadata.js +339 -0
- web/node_modules/hyparquet/src/node.js +34 -0
- web/node_modules/hyparquet/src/plain.js +180 -0
- web/node_modules/hyparquet/src/plan.js +189 -0
- web/node_modules/hyparquet/src/query.js +163 -0
- web/node_modules/hyparquet/src/read.js +230 -0
- web/node_modules/hyparquet/src/rowgroup.js +225 -0
- web/node_modules/hyparquet/src/schema.js +159 -0
- web/node_modules/hyparquet/src/snappy.js +127 -0
- web/node_modules/hyparquet/src/thrift.js +156 -0
- web/node_modules/hyparquet/src/types.d.ts +563 -0
- web/node_modules/hyparquet/src/utils.js +290 -0
- web/node_modules/hyparquet/src/variant.js +366 -0
- web/node_modules/hyparquet/src/wkb.js +128 -0
- web/node_modules/hyparquet/src/xxhash.js +102 -0
- web/node_modules/hyparquet/types/assemble.d.ts +25 -0
- web/node_modules/hyparquet/types/assemble.d.ts.map +1 -0
- web/node_modules/hyparquet/types/bloom.d.ts +51 -0
- web/node_modules/hyparquet/types/bloom.d.ts.map +1 -0
- web/node_modules/hyparquet/types/column.d.ts +33 -0
- web/node_modules/hyparquet/types/column.d.ts.map +1 -0
- web/node_modules/hyparquet/types/constants.d.ts +17 -0
- web/node_modules/hyparquet/types/constants.d.ts.map +1 -0
- web/node_modules/hyparquet/types/convert.d.ts +38 -0
- web/node_modules/hyparquet/types/convert.d.ts.map +1 -0
- web/node_modules/hyparquet/types/datapage.d.ts +33 -0
- web/node_modules/hyparquet/types/datapage.d.ts.map +1 -0
- web/node_modules/hyparquet/types/delta.d.ts +20 -0
- web/node_modules/hyparquet/types/delta.d.ts.map +1 -0
- web/node_modules/hyparquet/types/encoding.d.ts +23 -0
- web/node_modules/hyparquet/types/encoding.d.ts.map +1 -0
- web/node_modules/hyparquet/types/filter.d.ts +43 -0
- web/node_modules/hyparquet/types/filter.d.ts.map +1 -0
web/TEST_RESULTS.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Test results — 2026-07-15
|
| 2 |
+
|
| 3 |
+
All suites run with `npm test` (chains all nine). Every suite exits 0.
|
| 4 |
+
Hardware for GPU numbers: NVIDIA via WebGPU, DP4A int8 dot path, exact-gated
|
| 5 |
+
against the verified units at init.
|
| 6 |
+
|
| 7 |
+
| suite | what it proves | result |
|
| 8 |
+
|---|---|---|
|
| 9 |
+
| `test_core.js` | float trainer converges, replicas bit-identical | PASS — loss 33.71 → 0.000000, replica diff 0.000e+0 |
|
| 10 |
+
| `test_verified.js` | training THROUGH the int8 units converges, replicas bit-identical | PASS — loss 300.6 → 1.41, replica diff 0.000e+0 |
|
| 11 |
+
| `test_ieee.js` | the JS epilogue mirror is IEEE-754 spec-correct, not merely agreeable | PASS — 500k+ checks, 0 disagreements; rejects the old round-once mirror on 34% of inputs |
|
| 12 |
+
| `test_gates.js` | the exact kernel gate rejects real bugs, accepts the real kernel | PASS — 5/5 injected bugs rejected, clean kernel accepted |
|
| 13 |
+
| `test_metamorphic.js` | correctness properties that need no reference implementation | PASS — 6/6 properties hold; catches stride/swap/acc= mutants |
|
| 14 |
+
| `test_corpus.js` | mutation-scores the oracles with an external bug taxonomy | PASS — properties 2/4 (2/2 loop, 0/2 math), differential 4/4, control clean |
|
| 15 |
+
| `test_optimizer.js` | DaisyAdam beats SGD through the units, deterministic replicas | PASS — 1.59 vs 1.95, replica diff 0.000e+0 |
|
| 16 |
+
| `test_transformer.js` | the transformer LM trains through the units end to end | PASS — loss 4.75 → 1.26 (baseline 4.56), replica diff 0.000e+0 |
|
| 17 |
+
| `test_unit_backward.js` | int8 STE gradients do not damage convergence | PASS — units/float loss ratio 1.007 |
|
| 18 |
+
|
| 19 |
+
## The IEEE-754 oracle (`test_ieee.js`)
|
| 20 |
+
|
| 21 |
+
Built from the binary32 definition in exact BigInt arithmetic — no
|
| 22 |
+
`Math.fround` anywhere in the oracle, so neither side was tuned to the other.
|
| 23 |
+
|
| 24 |
+
```
|
| 25 |
+
i32ToF32Spec matches Math.fround on 200000 random int32 (incl. |s| > 2^24)
|
| 26 |
+
mulF32Spec matches the correctly-rounded product on 300000 draws (subnormal..overflow)
|
| 27 |
+
Verified.epi vs the oracle: 200000 random triples, 0 disagreements
|
| 28 |
+
tie-to-even ladder around 2^24: 378 cases, 0 disagreements
|
| 29 |
+
bgemmJS outputs rebuilt from the raw int32 accumulator via the oracle: 0 disagreements
|
| 30 |
+
the oracle REJECTS the round-once mirror shipped before: 68314/200000 inputs (34.16%)
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
That last line is the teeth: an oracle that never disagrees with anything
|
| 34 |
+
proves nothing. This one rejects the exact bug the old `1e-6` tolerance hid.
|
| 35 |
+
|
| 36 |
+
## Oracle mutation scores (`test_corpus.js`)
|
| 37 |
+
|
| 38 |
+
Bugs ported from an external taxonomy
|
| 39 |
+
([dipankarsarkar/gpuemu-corpus](https://huggingface.co/datasets/dipankarsarkar/gpuemu-corpus))
|
| 40 |
+
so the bug list has a different author than the checks.
|
| 41 |
+
|
| 42 |
+
| bug | lives in | properties | differential |
|
| 43 |
+
|---|---|---|---|
|
| 44 |
+
| `acc=` instead of `acc+=` | loop | CAUGHT (sensitivity) | CAUGHT |
|
| 45 |
+
| missing bounds guard (mult-of-8) | loop | CAUGHT (nonTriviality) | CAUGHT |
|
| 46 |
+
| dropped constant factor (2×) | math | missed | CAUGHT |
|
| 47 |
+
| wrong leaky-ReLU alpha | math | missed | CAUGHT |
|
| 48 |
+
|
| 49 |
+
The split is the finding: properties are a **loop-bug detector** (2/2), blind
|
| 50 |
+
to structure-preserving value bugs (0/2). Those are owned by the differential
|
| 51 |
+
gate against the spec-correct mirror. Each check class has an owner.
|
| 52 |
+
|
| 53 |
+
## Backward rework: bit-identity + GPU wall clock
|
| 54 |
+
|
| 55 |
+
The backward was reworked for dispatch efficiency: independent GEMMs
|
| 56 |
+
overlapped, the QKV weight-gradient and dln1in trios fused into single
|
| 57 |
+
batched (batch=3) GEMMs, and the `g.emb` operand quantized column-wise in one
|
| 58 |
+
pass instead of transpose-then-quantize. **None of this may change a bit** —
|
| 59 |
+
block scales are per-row/per-column per batch element, so fusion is exact,
|
| 60 |
+
and every fused sum keeps the original per-add f32 rounding schedule.
|
| 61 |
+
|
| 62 |
+
Bit-identity, old backward vs new (CPU mirrors, Node):
|
| 63 |
+
|
| 64 |
+
```
|
| 65 |
+
char-96, float backward: loss + all 20480 gradient floats bit-identical
|
| 66 |
+
char-96, unit backward: loss + all 20480 gradient floats bit-identical
|
| 67 |
+
Spikewhale 16k, float backward: loss + all 545792 gradient floats bit-identical
|
| 68 |
+
Spikewhale 16k, unit backward: loss + all 545792 gradient floats bit-identical
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
GPU (c=32 t=32 b=8 layers=2 heads=2, 16512-token vocab, 15 measured steps,
|
| 72 |
+
gradient FNV hash compared old vs new on-device):
|
| 73 |
+
|
| 74 |
+
| config | ms/step | grad hash |
|
| 75 |
+
|---|---|---|
|
| 76 |
+
| old backward, float | 286 | `7ff11308` |
|
| 77 |
+
| old backward, units | 466 | `62596547` |
|
| 78 |
+
| new backward, float | 286 | `7ff11308` (identical) |
|
| 79 |
+
| new backward, units | 346 | `62596547` (identical) |
|
| 80 |
+
|
| 81 |
+
- float path: unchanged speed, unchanged bits — this is what ships enabled.
|
| 82 |
+
- unit-backward path (dormant, `cfg.unitBackward`): cost drops **1.63× → 1.21×**
|
| 83 |
+
vs float. Because the rework is bit-identical, the convergence curves from
|
| 84 |
+
the unit-backward experiment stand unchanged; only the wall-clock exchange
|
| 85 |
+
rate moved. At equal wall clock float still wins (~1.5% at the 200-step
|
| 86 |
+
horizon), so `unitBackward` stays off by default.
|
| 87 |
+
|
| 88 |
+
One bug was caught during the rework, by the bit-identity check itself: the
|
| 89 |
+
fused q+k+v sum initially ran in f64 and rounded once, where the old code
|
| 90 |
+
rounded to f32 after each add — a last-ulp fork that would have split
|
| 91 |
+
replicas. Fixed by matching the rounding schedule (`Math.fround` per add).
|
| 92 |
+
Same lesson as the epilogue mirror: match the rounding schedule, not just
|
| 93 |
+
the values.
|
web/node_modules/.package-lock.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "daisychain-web",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"node_modules/hyparquet": {
|
| 8 |
+
"version": "1.26.2",
|
| 9 |
+
"resolved": "https://registry.npmjs.org/hyparquet/-/hyparquet-1.26.2.tgz",
|
| 10 |
+
"integrity": "sha512-6qjyK7R2tZ4vnYP1J8NpcCeDPK1UIYk3xh5XgtQUnUM1iwkYbvI6Mz3xtmpMd6AfCCeoUoJVbEq29k5GReZRWA==",
|
| 11 |
+
"license": "MIT"
|
| 12 |
+
},
|
| 13 |
+
"node_modules/ws": {
|
| 14 |
+
"version": "8.21.0",
|
| 15 |
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
| 16 |
+
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
| 17 |
+
"license": "MIT",
|
| 18 |
+
"engines": {
|
| 19 |
+
"node": ">=10.0.0"
|
| 20 |
+
},
|
| 21 |
+
"peerDependencies": {
|
| 22 |
+
"bufferutil": "^4.0.1",
|
| 23 |
+
"utf-8-validate": ">=5.0.2"
|
| 24 |
+
},
|
| 25 |
+
"peerDependenciesMeta": {
|
| 26 |
+
"bufferutil": {
|
| 27 |
+
"optional": true
|
| 28 |
+
},
|
| 29 |
+
"utf-8-validate": {
|
| 30 |
+
"optional": true
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
}
|
web/node_modules/hyparquet/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
The MIT License (MIT)
|
| 2 |
+
|
| 3 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
| 4 |
+
|
| 5 |
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
| 6 |
+
|
| 7 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
web/node_modules/hyparquet/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# hyparquet
|
| 2 |
+
|
| 3 |
+

|
| 4 |
+
|
| 5 |
+
[](https://www.npmjs.com/package/hyparquet)
|
| 6 |
+
[](https://www.npmjs.com/package/hyparquet)
|
| 7 |
+
[](https://github.com/hyparam/hyparquet/actions)
|
| 8 |
+
[](https://opensource.org/licenses/MIT)
|
| 9 |
+

|
| 10 |
+
[](https://www.npmjs.com/package/hyparquet?activeTab=dependencies)
|
| 11 |
+
|
| 12 |
+
Dependency free since 2023!
|
| 13 |
+
|
| 14 |
+
## What is hyparquet?
|
| 15 |
+
|
| 16 |
+
**Hyparquet** is a JavaScript library for parsing [Apache Parquet](https://parquet.apache.org) files in the browser. Apache Parquet is a popular columnar storage format that is widely used in data engineering, data science, and machine learning applications for storing large datasets. Hyparquet is designed to read parquet files efficiently over http, so that parquet files in cloud storage can be queried directly from the browser without needing a server.
|
| 17 |
+
|
| 18 |
+
> Part of **[HypStack](https://hypstack.ai/)**, an open-source stack for AI observability.
|
| 19 |
+
|
| 20 |
+
- Works in browsers and node.js
|
| 21 |
+
- Pure JavaScript, no dependencies
|
| 22 |
+
- Supports all parquet types, encodings, and compression codecs
|
| 23 |
+
- Minimizes data fetching using HTTP range requests
|
| 24 |
+
- Includes TypeScript definitions
|
| 25 |
+
|
| 26 |
+
## Parquet Viewer
|
| 27 |
+
|
| 28 |
+
**Try hyparquet online**: Drag and drop your parquet file onto [hyperparam.app](https://hyperparam.app) to view it directly in your browser. This service is powered by hyparquet's in-browser capabilities.
|
| 29 |
+
|
| 30 |
+
[](https://hyperparam.app/)
|
| 31 |
+
|
| 32 |
+
## Quick Start
|
| 33 |
+
|
| 34 |
+
### Browser Example
|
| 35 |
+
|
| 36 |
+
In the browser use `asyncBufferFromUrl` to wrap a url for reading asynchronously over the network.
|
| 37 |
+
It is recommended that you filter by row and column to limit fetch size:
|
| 38 |
+
|
| 39 |
+
```javascript
|
| 40 |
+
const { asyncBufferFromUrl, parquetReadObjects } = await import('https://cdn.jsdelivr.net/npm/hyparquet/src/hyparquet.min.js')
|
| 41 |
+
|
| 42 |
+
const url = 'https://hyperparam-public.s3.amazonaws.com/bunnies.parquet'
|
| 43 |
+
const file = await asyncBufferFromUrl({ url }) // wrap url for async fetching
|
| 44 |
+
const data = await parquetReadObjects({
|
| 45 |
+
file,
|
| 46 |
+
columns: ['Breed Name', 'Lifespan'],
|
| 47 |
+
rowStart: 10,
|
| 48 |
+
rowEnd: 20,
|
| 49 |
+
})
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### Node.js Example
|
| 53 |
+
|
| 54 |
+
To read the contents of a local parquet file in a node.js environment use `asyncBufferFromFile`:
|
| 55 |
+
|
| 56 |
+
```javascript
|
| 57 |
+
const { asyncBufferFromFile, parquetReadObjects } = await import('hyparquet')
|
| 58 |
+
|
| 59 |
+
const file = await asyncBufferFromFile('example.parquet')
|
| 60 |
+
const data = await parquetReadObjects({ file })
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
Note: hyparquet is published as an ES module, so dynamic `import()` may be required for old versions of node.
|
| 64 |
+
|
| 65 |
+
## Parquet Writing
|
| 66 |
+
|
| 67 |
+
To create parquet files from javascript, check out the [hyparquet-writer](https://github.com/hyparam/hyparquet-writer) package.
|
| 68 |
+
|
| 69 |
+
## Advanced Usage
|
| 70 |
+
|
| 71 |
+
### Reading Metadata
|
| 72 |
+
|
| 73 |
+
You can read just the metadata, including schema and data statistics using the `parquetMetadataAsync` function. This is useful for getting the schema, number of rows, and column names without reading the entire file.
|
| 74 |
+
|
| 75 |
+
```javascript
|
| 76 |
+
import { parquetMetadataAsync, parquetSchema } from 'hyparquet'
|
| 77 |
+
|
| 78 |
+
const file = await asyncBufferFromUrl({ url })
|
| 79 |
+
const metadata = await parquetMetadataAsync(file)
|
| 80 |
+
// Get total number of rows (convert bigint to number)
|
| 81 |
+
const numRows = Number(metadata.num_rows)
|
| 82 |
+
// Get nested table schema
|
| 83 |
+
const schema = parquetSchema(metadata)
|
| 84 |
+
// Get top-level column header names
|
| 85 |
+
const columnNames = schema.children.map(e => e.element.name)
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
### AsyncBuffer
|
| 89 |
+
|
| 90 |
+
Hyparquet requires an argument `file` of type `AsyncBuffer`. An `AsyncBuffer` is similar to a js `ArrayBuffer` but the `slice` method can return async `Promise<ArrayBuffer>`. This makes it a useful way to represent a remote file.
|
| 91 |
+
|
| 92 |
+
```typescript
|
| 93 |
+
type Awaitable<T> = T | Promise<T>
|
| 94 |
+
interface AsyncBuffer {
|
| 95 |
+
byteLength: number
|
| 96 |
+
slice(start: number, end?: number): Awaitable<ArrayBuffer>
|
| 97 |
+
}
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
In most cases, you should probably use `asyncBufferFromUrl` or `asyncBufferFromFile` to create an `AsyncBuffer` for hyparquet.
|
| 101 |
+
|
| 102 |
+
#### asyncBufferFromUrl
|
| 103 |
+
|
| 104 |
+
If you want to read a parquet file remotely over http, use `asyncBufferFromUrl` to wrap an http url as an `AsyncBuffer` using http range requests.
|
| 105 |
+
|
| 106 |
+
- Pass `requestInit` option to provide additional fetch headers for authentication (optional)
|
| 107 |
+
- Pass `byteLength` if you know the file size to save a round trip HEAD request (optional)
|
| 108 |
+
|
| 109 |
+
```typescript
|
| 110 |
+
const url = 'https://s3.hyperparam.app/wiki_en.parquet'
|
| 111 |
+
const requestInit = { headers: { Authorization: 'Bearer my_token' } } // auth header
|
| 112 |
+
const byteLength = 415958713 // optional
|
| 113 |
+
const file: AsyncBuffer = await asyncBufferFromUrl({ url, requestInit, byteLength })
|
| 114 |
+
const data = await parquetReadObjects({ file })
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
#### asyncBufferFromFile
|
| 118 |
+
|
| 119 |
+
If you are in a node.js environment, use `asyncBufferFromFile` to wrap a local file as an `AsyncBuffer`:
|
| 120 |
+
|
| 121 |
+
```typescript
|
| 122 |
+
import { asyncBufferFromFile, parquetReadObjects } from 'hyparquet'
|
| 123 |
+
|
| 124 |
+
const file: AsyncBuffer = await asyncBufferFromFile('example.parquet')
|
| 125 |
+
const data = await parquetReadObjects({ file })
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
#### ArrayBuffer
|
| 129 |
+
|
| 130 |
+
You can provide an `ArrayBuffer` anywhere that an `AsyncBuffer` is expected. This is useful if you already have the entire parquet file in memory.
|
| 131 |
+
|
| 132 |
+
### parquetRead vs parquetReadObjects
|
| 133 |
+
|
| 134 |
+
#### parquetReadObjects
|
| 135 |
+
|
| 136 |
+
`parquetReadObjects` is a convenience wrapper around `parquetRead` that returns the complete rows as `Promise<Record<string, any>[]>`. This is the simplest way to read parquet files.
|
| 137 |
+
|
| 138 |
+
```typescript
|
| 139 |
+
parquetReadObjects({ file }): Promise<Record<string, any>[]>
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
#### parquetRead
|
| 143 |
+
|
| 144 |
+
`parquetRead` is the "base" function for reading parquet files.
|
| 145 |
+
It returns a `Promise<void>` that resolves when the file has been read or rejected if an error occurs.
|
| 146 |
+
Data is returned via `onComplete` or `onChunk` or `onPage` callbacks passed as arguments.
|
| 147 |
+
|
| 148 |
+
The reason for this design is that parquet is a column-oriented format, and returning data in row-oriented format requires transposing the column data. This is an expensive operation in javascript. If you don't pass in an `onComplete` argument to `parquetRead`, hyparquet will skip this transpose step and save memory.
|
| 149 |
+
|
| 150 |
+
### Chunk Streaming
|
| 151 |
+
|
| 152 |
+
The `onChunk` callback returns column-oriented data as it is ready. `onChunk` will always return top-level columns, including structs, assembled as a single column. This may require waiting for multiple sub-columns to all load before assembly can occur.
|
| 153 |
+
|
| 154 |
+
The `onPage` callback returns column-oriented page data as it is ready. `onPage` will NOT assemble struct columns and will always return individual sub-column data. Note that `onPage` _will_ assemble nested lists.
|
| 155 |
+
|
| 156 |
+
In some cases, `onPage` can return data sooner than `onChunk`.
|
| 157 |
+
|
| 158 |
+
```typescript
|
| 159 |
+
interface ColumnData {
|
| 160 |
+
columnName: string
|
| 161 |
+
columnData: ArrayLike<any>
|
| 162 |
+
rowStart: number
|
| 163 |
+
rowEnd: number
|
| 164 |
+
}
|
| 165 |
+
await parquetRead({
|
| 166 |
+
file,
|
| 167 |
+
onChunk(chunk: ColumnData) {
|
| 168 |
+
console.log('chunk', chunk)
|
| 169 |
+
},
|
| 170 |
+
onPage(chunk: ColumnData) {
|
| 171 |
+
console.log('page', chunk)
|
| 172 |
+
},
|
| 173 |
+
})
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
### Returned row format
|
| 177 |
+
|
| 178 |
+
By default, the `onComplete` function returns an **array** of values for each row: `[value]`. If you would prefer each row to be an **object**: `{ columnName: value }`, set the option `rowFormat` to `'object'`.
|
| 179 |
+
|
| 180 |
+
```javascript
|
| 181 |
+
import { parquetRead } from 'hyparquet'
|
| 182 |
+
|
| 183 |
+
await parquetRead({
|
| 184 |
+
file,
|
| 185 |
+
rowFormat: 'object',
|
| 186 |
+
onComplete: data => console.log(data),
|
| 187 |
+
})
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
The `parquetReadObjects` function defaults to `rowFormat: 'object'`.
|
| 191 |
+
|
| 192 |
+
### Binary columns
|
| 193 |
+
|
| 194 |
+
Hyparquet defaults to decoding binary columns as utf8 text strings. A parquet `BYTE_ARRAY` column may contain arbitrary binary data or utf8 encoded text data. In theory, a column should be annotated as [LogicalType](https://github.com/apache/parquet-format/blob/master/LogicalTypes.md) STRING if it contains utf8 text. But in practice, many parquet files omit this annotation. Hyparquet's default decoding behavior can be disabled by setting the `utf8` option to `false`. The `utf8` option only affects `BYTE_ARRAY` columns _without_ an annotation.
|
| 195 |
+
|
| 196 |
+
If Hyparquet detects a [GeoParquet](https://geoparquet.org/) file, any geospatial column will be marked with the GEOMETRY or GEOGRAPHY logical type and decoded to GeoJSON geometries. Set the `geoparquet` option to `false` to disable this behavior.
|
| 197 |
+
|
| 198 |
+
## Compression
|
| 199 |
+
|
| 200 |
+
By default, hyparquet supports uncompressed and snappy-compressed parquet files.
|
| 201 |
+
To support the full range of parquet compression codecs (gzip, brotli, zstd, etc), use the [hyparquet-compressors](https://github.com/hyparam/hyparquet-compressors) package.
|
| 202 |
+
|
| 203 |
+
```javascript
|
| 204 |
+
import { parquetReadObjects } from 'hyparquet'
|
| 205 |
+
import { compressors } from 'hyparquet-compressors'
|
| 206 |
+
|
| 207 |
+
const data = await parquetReadObjects({ file, compressors })
|
| 208 |
+
```
|
| 209 |
+
|
| 210 |
+
| Codec | hyparquet | with hyparquet-compressors |
|
| 211 |
+
|---------------|-----------|----------------------------|
|
| 212 |
+
| Uncompressed | ✅ | ✅ |
|
| 213 |
+
| Snappy | ✅ | ✅ |
|
| 214 |
+
| GZip | ❌ | ✅ |
|
| 215 |
+
| LZO | ❌ | ✅ |
|
| 216 |
+
| Brotli | ❌ | ✅ |
|
| 217 |
+
| LZ4 | ❌ | ✅ |
|
| 218 |
+
| ZSTD | ❌ | ✅ |
|
| 219 |
+
| LZ4_RAW | ❌ | ✅ |
|
| 220 |
+
|
| 221 |
+
## References
|
| 222 |
+
|
| 223 |
+
- https://github.com/apache/parquet-format
|
| 224 |
+
- https://github.com/apache/parquet-testing
|
| 225 |
+
- https://github.com/apache/thrift
|
| 226 |
+
- https://github.com/apache/arrow
|
| 227 |
+
- https://github.com/dask/fastparquet
|
| 228 |
+
- https://github.com/duckdb/duckdb
|
| 229 |
+
- https://github.com/google/snappy
|
| 230 |
+
- https://github.com/hyparam/hightable
|
| 231 |
+
- https://github.com/hyparam/hysnappy
|
| 232 |
+
- https://github.com/hyparam/hyparquet-compressors
|
| 233 |
+
- https://github.com/ironSource/parquetjs
|
| 234 |
+
- https://github.com/zhipeng-jia/snappyjs
|
| 235 |
+
|
| 236 |
+
Sample project that shows how to build a parquet viewer using hyparquet, react, and [HighTable](https://github.com/hyparam/hightable):
|
| 237 |
+
|
| 238 |
+
- Hyparquet Demo: [https://hyparam.github.io/demos/hyparquet/](https://hyparam.github.io/demos/hyparquet/)
|
| 239 |
+
- Hyparquet Demo Source Code: [https://github.com/hyparam/demos/tree/master/hyparquet](https://github.com/hyparam/demos/tree/master/hyparquet)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
## Contributions
|
| 244 |
+
|
| 245 |
+
Contributions are welcome!
|
| 246 |
+
If you have suggestions, bug reports, or feature requests, please open an issue or submit a pull request.
|
| 247 |
+
|
| 248 |
+
Hyparquet development is supported by an open-source grant from Hugging Face :hugs:
|
web/node_modules/hyparquet/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "hyparquet",
|
| 3 |
+
"version": "1.26.2",
|
| 4 |
+
"description": "Parquet file parser for JavaScript",
|
| 5 |
+
"author": "Hyperparam",
|
| 6 |
+
"homepage": "https://hyperparam.app",
|
| 7 |
+
"keywords": [
|
| 8 |
+
"ai",
|
| 9 |
+
"data",
|
| 10 |
+
"dataset",
|
| 11 |
+
"hyperparam",
|
| 12 |
+
"hyparquet",
|
| 13 |
+
"geoparquet",
|
| 14 |
+
"llm",
|
| 15 |
+
"ml",
|
| 16 |
+
"parquet",
|
| 17 |
+
"parquetjs",
|
| 18 |
+
"parser",
|
| 19 |
+
"snappy",
|
| 20 |
+
"thrift"
|
| 21 |
+
],
|
| 22 |
+
"license": "MIT",
|
| 23 |
+
"repository": {
|
| 24 |
+
"type": "git",
|
| 25 |
+
"url": "git+https://github.com/hyparam/hyparquet.git"
|
| 26 |
+
},
|
| 27 |
+
"files": [
|
| 28 |
+
"src",
|
| 29 |
+
"types"
|
| 30 |
+
],
|
| 31 |
+
"type": "module",
|
| 32 |
+
"types": "types/index.d.ts",
|
| 33 |
+
"main": "src/index.js",
|
| 34 |
+
"exports": {
|
| 35 |
+
".": {
|
| 36 |
+
"browser": {
|
| 37 |
+
"types": "./types/index.d.ts",
|
| 38 |
+
"import": "./src/index.js"
|
| 39 |
+
},
|
| 40 |
+
"default": {
|
| 41 |
+
"types": "./types/node.d.ts",
|
| 42 |
+
"import": "./src/node.js"
|
| 43 |
+
}
|
| 44 |
+
},
|
| 45 |
+
"./src/*.js": {
|
| 46 |
+
"types": "./types/*.d.ts",
|
| 47 |
+
"import": "./src/*.js"
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
"sideEffects": false,
|
| 51 |
+
"scripts": {
|
| 52 |
+
"build:types": "tsc -p ./tsconfig.build.json",
|
| 53 |
+
"coverage": "vitest run --coverage --coverage.include=src",
|
| 54 |
+
"lint": "eslint",
|
| 55 |
+
"lint:fix": "eslint --fix",
|
| 56 |
+
"prepare": "npm run build:types",
|
| 57 |
+
"test": "vitest run"
|
| 58 |
+
},
|
| 59 |
+
"devDependencies": {
|
| 60 |
+
"@types/node": "26.0.1",
|
| 61 |
+
"@vitest/coverage-v8": "4.1.9",
|
| 62 |
+
"eslint": "9.39.4",
|
| 63 |
+
"eslint-plugin-jsdoc": "63.0.10",
|
| 64 |
+
"hyparquet-compressors": "1.1.1",
|
| 65 |
+
"typescript": "6.0.3",
|
| 66 |
+
"vitest": "4.1.9"
|
| 67 |
+
}
|
| 68 |
+
}
|
web/node_modules/hyparquet/src/assemble.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getMaxDefinitionLevel, isListLike, isMapLike } from './schema.js'
|
| 2 |
+
import { decodeVariantColumn } from './variant.js'
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Reconstructs a complex nested structure from flat arrays of values and
|
| 6 |
+
* definition and repetition levels, according to Dremel encoding.
|
| 7 |
+
*
|
| 8 |
+
* @param {any[]} output
|
| 9 |
+
* @param {number[] | undefined} definitionLevels
|
| 10 |
+
* @param {number[]} repetitionLevels
|
| 11 |
+
* @param {DecodedArray} values
|
| 12 |
+
* @param {SchemaTree[]} schemaPath
|
| 13 |
+
* @returns {DecodedArray}
|
| 14 |
+
*/
|
| 15 |
+
export function assembleLists(output, definitionLevels, repetitionLevels, values, schemaPath) {
|
| 16 |
+
const maxDefinitionLevel = getMaxDefinitionLevel(schemaPath)
|
| 17 |
+
// If no def/rep levels, synthesize def levels at max
|
| 18 |
+
if (!definitionLevels?.length && !repetitionLevels.length) {
|
| 19 |
+
if (!maxDefinitionLevel || !values.length) return values
|
| 20 |
+
definitionLevels = new Array(values.length).fill(maxDefinitionLevel)
|
| 21 |
+
}
|
| 22 |
+
const n = definitionLevels?.length || repetitionLevels.length
|
| 23 |
+
const repetitionPath = schemaPath.map(({ element }) => element.repetition_type)
|
| 24 |
+
let valueIndex = 0
|
| 25 |
+
|
| 26 |
+
// Track state of nested structures
|
| 27 |
+
const containerStack = [output]
|
| 28 |
+
let currentContainer = output
|
| 29 |
+
let currentDepth = 0 // schema depth
|
| 30 |
+
let currentDefLevel = 0 // list depth
|
| 31 |
+
let currentRepLevel = 0
|
| 32 |
+
|
| 33 |
+
if (repetitionLevels[0]) {
|
| 34 |
+
// continue previous row
|
| 35 |
+
while (currentDepth < repetitionPath.length - 2 && currentRepLevel < repetitionLevels[0]) {
|
| 36 |
+
currentDepth++
|
| 37 |
+
if (repetitionPath[currentDepth] !== 'REQUIRED') {
|
| 38 |
+
// go into last list
|
| 39 |
+
currentContainer = currentContainer.at(-1)
|
| 40 |
+
containerStack.push(currentContainer)
|
| 41 |
+
currentDefLevel++
|
| 42 |
+
}
|
| 43 |
+
if (repetitionPath[currentDepth] === 'REPEATED') currentRepLevel++
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
for (let i = 0; i < n; i++) {
|
| 48 |
+
// assert(currentDefLevel === containerStack.length - 1)
|
| 49 |
+
const def = definitionLevels?.length ? definitionLevels[i] : maxDefinitionLevel
|
| 50 |
+
const rep = repetitionLevels[i]
|
| 51 |
+
|
| 52 |
+
// Pop up to start of rep level
|
| 53 |
+
while (currentDepth && (rep < currentRepLevel || repetitionPath[currentDepth] !== 'REPEATED')) {
|
| 54 |
+
if (repetitionPath[currentDepth] !== 'REQUIRED') {
|
| 55 |
+
containerStack.pop()
|
| 56 |
+
currentDefLevel--
|
| 57 |
+
}
|
| 58 |
+
if (repetitionPath[currentDepth] === 'REPEATED') currentRepLevel--
|
| 59 |
+
currentDepth--
|
| 60 |
+
}
|
| 61 |
+
// @ts-expect-error won't be empty
|
| 62 |
+
currentContainer = containerStack.at(-1)
|
| 63 |
+
|
| 64 |
+
// Go deeper to end of definition level
|
| 65 |
+
while (
|
| 66 |
+
(currentDepth < repetitionPath.length - 2 || repetitionPath[currentDepth + 1] === 'REPEATED') &&
|
| 67 |
+
(currentDefLevel < def || repetitionPath[currentDepth + 1] === 'REQUIRED')
|
| 68 |
+
) {
|
| 69 |
+
currentDepth++
|
| 70 |
+
if (repetitionPath[currentDepth] !== 'REQUIRED') {
|
| 71 |
+
/** @type {any[]} */
|
| 72 |
+
const newList = []
|
| 73 |
+
currentContainer.push(newList)
|
| 74 |
+
currentContainer = newList
|
| 75 |
+
containerStack.push(newList)
|
| 76 |
+
currentDefLevel++
|
| 77 |
+
}
|
| 78 |
+
if (repetitionPath[currentDepth] === 'REPEATED') currentRepLevel++
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// Add value or null based on definition level
|
| 82 |
+
if (def === maxDefinitionLevel) {
|
| 83 |
+
// assert(currentDepth === maxDefinitionLevel || currentDepth === repetitionPath.length - 2)
|
| 84 |
+
currentContainer.push(values[valueIndex++])
|
| 85 |
+
} else if (currentDepth === repetitionPath.length - 2) {
|
| 86 |
+
currentContainer.push(null)
|
| 87 |
+
} else {
|
| 88 |
+
currentContainer.push([])
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// Handle edge cases for empty inputs or single-level data
|
| 93 |
+
if (!output.length) {
|
| 94 |
+
// return max definition level of nested lists
|
| 95 |
+
for (let i = 0; i < maxDefinitionLevel; i++) {
|
| 96 |
+
/** @type {any[]} */
|
| 97 |
+
const newList = []
|
| 98 |
+
currentContainer.push(newList)
|
| 99 |
+
currentContainer = newList
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
return output
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
/**
|
| 107 |
+
* Assemble a nested structure from subcolumn data.
|
| 108 |
+
*
|
| 109 |
+
* @param {Map<string, DecodedArray>} subcolumnData
|
| 110 |
+
* @param {SchemaTree} schema top-level schema element
|
| 111 |
+
* @param {ParquetParsers} parsers
|
| 112 |
+
* @param {number} [depth] depth of nested structure
|
| 113 |
+
*/
|
| 114 |
+
export function assembleNested(subcolumnData, schema, parsers, depth = 0) {
|
| 115 |
+
const path = schema.path.join('.')
|
| 116 |
+
const optional = schema.element.repetition_type === 'OPTIONAL'
|
| 117 |
+
const nextDepth = optional ? depth + 1 : depth
|
| 118 |
+
|
| 119 |
+
if (isListLike(schema)) {
|
| 120 |
+
let sublist = schema.children[0]
|
| 121 |
+
let subDepth = nextDepth
|
| 122 |
+
if (sublist.children.length === 1) {
|
| 123 |
+
sublist = sublist.children[0]
|
| 124 |
+
subDepth++
|
| 125 |
+
}
|
| 126 |
+
assembleNested(subcolumnData, sublist, parsers, subDepth)
|
| 127 |
+
|
| 128 |
+
const subcolumn = sublist.path.join('.')
|
| 129 |
+
const values = subcolumnData.get(subcolumn)
|
| 130 |
+
if (!values) throw new Error('parquet list column missing values')
|
| 131 |
+
if (optional) flattenAtDepth(values, depth)
|
| 132 |
+
subcolumnData.set(path, values)
|
| 133 |
+
subcolumnData.delete(subcolumn)
|
| 134 |
+
return
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
if (isMapLike(schema)) {
|
| 138 |
+
const mapName = schema.children[0].element.name
|
| 139 |
+
|
| 140 |
+
// Assemble keys and values
|
| 141 |
+
assembleNested(subcolumnData, schema.children[0].children[0], parsers, nextDepth + 1)
|
| 142 |
+
assembleNested(subcolumnData, schema.children[0].children[1], parsers, nextDepth + 1)
|
| 143 |
+
|
| 144 |
+
const keys = subcolumnData.get(`${path}.${mapName}.key`)
|
| 145 |
+
const values = subcolumnData.get(`${path}.${mapName}.value`)
|
| 146 |
+
|
| 147 |
+
if (!keys) throw new Error('parquet map column missing keys')
|
| 148 |
+
if (!values) throw new Error('parquet map column missing values')
|
| 149 |
+
if (keys.length !== values.length) {
|
| 150 |
+
throw new Error('parquet map column key/value length mismatch')
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
const out = assembleMaps(keys, values, nextDepth)
|
| 154 |
+
if (optional) flattenAtDepth(out, depth)
|
| 155 |
+
|
| 156 |
+
subcolumnData.delete(`${path}.${mapName}.key`)
|
| 157 |
+
subcolumnData.delete(`${path}.${mapName}.value`)
|
| 158 |
+
subcolumnData.set(path, out)
|
| 159 |
+
return
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// Struct-like column
|
| 163 |
+
if (schema.children.length) {
|
| 164 |
+
// construct a meta struct and then invert
|
| 165 |
+
const invertDepth = schema.element.repetition_type === 'REQUIRED' ? depth : depth + 1
|
| 166 |
+
/** @type {Record<string, any>} */
|
| 167 |
+
const struct = {}
|
| 168 |
+
for (const child of schema.children) {
|
| 169 |
+
assembleNested(subcolumnData, child, parsers, invertDepth)
|
| 170 |
+
const childData = subcolumnData.get(child.path.join('.'))
|
| 171 |
+
if (!childData) throw new Error('parquet struct missing child data')
|
| 172 |
+
struct[child.element.name] = childData
|
| 173 |
+
}
|
| 174 |
+
// remove children
|
| 175 |
+
for (const child of schema.children) {
|
| 176 |
+
subcolumnData.delete(child.path.join('.'))
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// invert struct by depth
|
| 180 |
+
let inverted = invertStruct(struct, invertDepth)
|
| 181 |
+
if (schema.element.logical_type?.type === 'VARIANT') {
|
| 182 |
+
inverted = decodeVariantColumn(inverted, parsers)
|
| 183 |
+
}
|
| 184 |
+
if (optional) flattenAtDepth(inverted, depth)
|
| 185 |
+
subcolumnData.set(path, inverted)
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
/**
|
| 190 |
+
* @import {DecodedArray, ParquetParsers, SchemaTree} from '../src/types.js'
|
| 191 |
+
* @param {DecodedArray} arr
|
| 192 |
+
* @param {number} depth
|
| 193 |
+
*/
|
| 194 |
+
function flattenAtDepth(arr, depth) {
|
| 195 |
+
for (let i = 0; i < arr.length; i++) {
|
| 196 |
+
if (depth) {
|
| 197 |
+
flattenAtDepth(arr[i], depth - 1)
|
| 198 |
+
} else {
|
| 199 |
+
arr[i] = arr[i][0]
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
/**
|
| 205 |
+
* @param {DecodedArray} keys
|
| 206 |
+
* @param {DecodedArray} values
|
| 207 |
+
* @param {number} depth
|
| 208 |
+
* @returns {any[]}
|
| 209 |
+
*/
|
| 210 |
+
function assembleMaps(keys, values, depth) {
|
| 211 |
+
const out = []
|
| 212 |
+
for (let i = 0; i < keys.length; i++) {
|
| 213 |
+
if (depth) {
|
| 214 |
+
out.push(assembleMaps(keys[i], values[i], depth - 1)) // go deeper
|
| 215 |
+
} else {
|
| 216 |
+
if (keys[i]) {
|
| 217 |
+
/** @type {Record<string, any>} */
|
| 218 |
+
const obj = {}
|
| 219 |
+
for (let j = 0; j < keys[i].length; j++) {
|
| 220 |
+
const value = values[i][j]
|
| 221 |
+
obj[keys[i][j]] = value === undefined ? null : value
|
| 222 |
+
}
|
| 223 |
+
out.push(obj)
|
| 224 |
+
} else {
|
| 225 |
+
out.push(undefined)
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
return out
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
/**
|
| 233 |
+
* Invert a struct-like object by depth.
|
| 234 |
+
*
|
| 235 |
+
* @param {Record<string, any[]>} struct
|
| 236 |
+
* @param {number} depth
|
| 237 |
+
* @returns {any[]}
|
| 238 |
+
*/
|
| 239 |
+
function invertStruct(struct, depth) {
|
| 240 |
+
const keys = Object.keys(struct)
|
| 241 |
+
const length = struct[keys[0]]?.length
|
| 242 |
+
const out = []
|
| 243 |
+
for (let i = 0; i < length; i++) {
|
| 244 |
+
/** @type {Record<string, any>} */
|
| 245 |
+
const obj = {}
|
| 246 |
+
for (const key of keys) {
|
| 247 |
+
if (struct[key].length !== length) throw new Error('parquet struct parsing error')
|
| 248 |
+
obj[key] = struct[key][i]
|
| 249 |
+
}
|
| 250 |
+
if (depth) {
|
| 251 |
+
out.push(invertStruct(obj, depth - 1)) // deeper
|
| 252 |
+
} else {
|
| 253 |
+
out.push(obj)
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
return out
|
| 257 |
+
}
|
web/node_modules/hyparquet/src/bloom.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Split Block Bloom Filter (https://github.com/apache/parquet-format/blob/master/BloomFilter.md)
|
| 2 |
+
// A bloom filter is a sequence of 32-byte blocks. Each block holds 8 little-endian uint32 words.
|
| 3 |
+
// Insertion sets one bit per word, chosen by salting the low 32 bits of an xxhash64.
|
| 4 |
+
// Membership requires all 8 bits to be set; misses are exact, hits are probabilistic.
|
| 5 |
+
|
| 6 |
+
import { deserializeTCompactProtocol } from './thrift.js'
|
| 7 |
+
import { xxhash64 } from './xxhash.js'
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* @import {BloomFilter, DataReader, ParquetQueryFilter, SchemaElement} from '../src/types.js'
|
| 11 |
+
*/
|
| 12 |
+
|
| 13 |
+
const textEncoder = new TextEncoder()
|
| 14 |
+
|
| 15 |
+
const SALT = new Uint32Array([
|
| 16 |
+
0x47b6137b, 0x44974d91, 0x8824ad5b, 0xa2b7289d,
|
| 17 |
+
0x705495c7, 0x2df1424b, 0x9efc4947, 0x5c6bfb31,
|
| 18 |
+
])
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* Map the high 32 bits of a hash to a block index in [0, numBlocks).
|
| 22 |
+
*
|
| 23 |
+
* @param {bigint} hash
|
| 24 |
+
* @param {number} numBlocks
|
| 25 |
+
* @returns {number}
|
| 26 |
+
*/
|
| 27 |
+
function blockIndex(hash, numBlocks) {
|
| 28 |
+
return Number((hash >> 32n) * BigInt(numBlocks) >> 32n)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* Per-block mask: 8 uint32 words, each with a single bit set at position `(low32 * SALT[i]) >> 27`.
|
| 33 |
+
*
|
| 34 |
+
* @param {bigint} hash
|
| 35 |
+
* @returns {Uint32Array}
|
| 36 |
+
*/
|
| 37 |
+
function blockMask(hash) {
|
| 38 |
+
const m = new Uint32Array(8)
|
| 39 |
+
const low = Number(hash & 0xffffffffn) | 0
|
| 40 |
+
for (let i = 0; i < 8; i++) {
|
| 41 |
+
m[i] = 1 << (Math.imul(low, SALT[i]) >>> 27)
|
| 42 |
+
}
|
| 43 |
+
return m
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/**
|
| 47 |
+
* Insert a hash into a Split Block Bloom Filter.
|
| 48 |
+
*
|
| 49 |
+
* @param {Uint32Array} blocks bloom filter words (8 * numBlocks long)
|
| 50 |
+
* @param {bigint} hash 64-bit xxhash of the parquet-plain-encoded value
|
| 51 |
+
*/
|
| 52 |
+
export function sbbfInsert(blocks, hash) {
|
| 53 |
+
const offset = blockIndex(hash, blocks.length >> 3) << 3
|
| 54 |
+
const m = blockMask(hash)
|
| 55 |
+
for (let i = 0; i < 8; i++) {
|
| 56 |
+
blocks[offset + i] |= m[i]
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/**
|
| 61 |
+
* Test whether a hash might be present in a Split Block Bloom Filter.
|
| 62 |
+
* False positives are possible; false negatives are not.
|
| 63 |
+
*
|
| 64 |
+
* @param {Uint32Array} blocks bloom filter words (8 * numBlocks long)
|
| 65 |
+
* @param {bigint} hash 64-bit xxhash of the parquet-plain-encoded value
|
| 66 |
+
* @returns {boolean}
|
| 67 |
+
*/
|
| 68 |
+
export function sbbfContains(blocks, hash) {
|
| 69 |
+
const offset = blockIndex(hash, blocks.length >> 3) << 3
|
| 70 |
+
const m = blockMask(hash)
|
| 71 |
+
for (let i = 0; i < 8; i++) {
|
| 72 |
+
if ((blocks[offset + i] & m[i]) === 0) return false
|
| 73 |
+
}
|
| 74 |
+
return true
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/**
|
| 78 |
+
* Parse a Split Block Bloom Filter from a reader positioned at the BloomFilterHeader.
|
| 79 |
+
* Returns undefined when the header advertises an unsupported algorithm, hash, or
|
| 80 |
+
* compression — callers should treat that as "cannot use this bloom filter."
|
| 81 |
+
*
|
| 82 |
+
* @param {DataReader} reader
|
| 83 |
+
* @returns {BloomFilter | undefined}
|
| 84 |
+
*/
|
| 85 |
+
export function readBloomFilter(reader) {
|
| 86 |
+
const header = deserializeTCompactProtocol(reader)
|
| 87 |
+
const numBytes = header.field_1
|
| 88 |
+
if (typeof numBytes !== 'number' || numBytes <= 0 || numBytes % 32 !== 0) return undefined
|
| 89 |
+
// BloomFilterAlgorithm / Hash / Compression are unions with a single supported variant each.
|
| 90 |
+
if (!header.field_2?.field_1) return undefined // algorithm must be BLOCK
|
| 91 |
+
if (!header.field_3?.field_1) return undefined // hash must be XXHASH
|
| 92 |
+
if (!header.field_4?.field_1) return undefined // compression must be UNCOMPRESSED
|
| 93 |
+
|
| 94 |
+
const { view, offset } = reader
|
| 95 |
+
if (offset + numBytes > view.byteLength) {
|
| 96 |
+
throw new Error(`parquet bloom filter truncated: need ${numBytes} bytes, have ${view.byteLength - offset}`)
|
| 97 |
+
}
|
| 98 |
+
// Reader offset is not 4-aligned in general, and we want endian-portable reads.
|
| 99 |
+
const blocks = new Uint32Array(numBytes >> 2)
|
| 100 |
+
for (let i = 0; i < blocks.length; i++) {
|
| 101 |
+
blocks[i] = view.getUint32(offset + i * 4, true)
|
| 102 |
+
}
|
| 103 |
+
reader.offset = offset + numBytes
|
| 104 |
+
return { numBytes, blocks }
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/**
|
| 108 |
+
* Hash a JS filter value as its parquet PLAIN-encoded bytes, suitable for a
|
| 109 |
+
* bloom filter lookup. Returns undefined when the column's parser is lossy or
|
| 110 |
+
* ambiguous (DATE, TIMESTAMP_*, DECIMAL, JSON, BSON, INT96, FLOAT16, UUID,
|
| 111 |
+
* GEOMETRY, GEOGRAPHY, INTERVAL) or when the JS value type doesn't match the
|
| 112 |
+
* column. Callers must treat undefined as "bloom filter cannot help."
|
| 113 |
+
*
|
| 114 |
+
* @param {any} value
|
| 115 |
+
* @param {SchemaElement} element
|
| 116 |
+
* @returns {bigint | undefined}
|
| 117 |
+
*/
|
| 118 |
+
export function hashParquetValue(value, element) {
|
| 119 |
+
if (value === null || value === undefined) return undefined
|
| 120 |
+
const { type, converted_type, logical_type } = element
|
| 121 |
+
|
| 122 |
+
if (type === 'BOOLEAN') {
|
| 123 |
+
if (typeof value !== 'boolean') return undefined
|
| 124 |
+
return xxhash64(new Uint8Array([value ? 1 : 0]))
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
if (type === 'FLOAT') {
|
| 128 |
+
if (typeof value !== 'number') return undefined
|
| 129 |
+
const buf = new ArrayBuffer(4)
|
| 130 |
+
new DataView(buf).setFloat32(0, value, true)
|
| 131 |
+
return xxhash64(new Uint8Array(buf))
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
if (type === 'DOUBLE') {
|
| 135 |
+
if (typeof value !== 'number') return undefined
|
| 136 |
+
const buf = new ArrayBuffer(8)
|
| 137 |
+
new DataView(buf).setFloat64(0, value, true)
|
| 138 |
+
return xxhash64(new Uint8Array(buf))
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
if (type === 'INT32') {
|
| 142 |
+
if (converted_type === 'DATE' || converted_type === 'DECIMAL' || converted_type === 'TIME_MILLIS') return undefined
|
| 143 |
+
if (logical_type?.type === 'DATE' || logical_type?.type === 'TIME' || logical_type?.type === 'DECIMAL') return undefined
|
| 144 |
+
if (typeof value !== 'number' || !Number.isInteger(value)) return undefined
|
| 145 |
+
const buf = new ArrayBuffer(4)
|
| 146 |
+
new DataView(buf).setInt32(0, value | 0, true)
|
| 147 |
+
return xxhash64(new Uint8Array(buf))
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
if (type === 'INT64') {
|
| 151 |
+
if (converted_type === 'TIMESTAMP_MILLIS' || converted_type === 'TIMESTAMP_MICROS') return undefined
|
| 152 |
+
if (converted_type === 'TIME_MICROS' || converted_type === 'DECIMAL') return undefined
|
| 153 |
+
if (logical_type?.type === 'TIMESTAMP' || logical_type?.type === 'TIME' || logical_type?.type === 'DECIMAL') return undefined
|
| 154 |
+
let bigValue
|
| 155 |
+
if (typeof value === 'bigint') bigValue = value
|
| 156 |
+
else if (typeof value === 'number' && Number.isSafeInteger(value)) bigValue = BigInt(value)
|
| 157 |
+
else return undefined
|
| 158 |
+
const buf = new ArrayBuffer(8)
|
| 159 |
+
new DataView(buf).setBigUint64(0, BigInt.asUintN(64, bigValue), true)
|
| 160 |
+
return xxhash64(new Uint8Array(buf))
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
if (type === 'BYTE_ARRAY') {
|
| 164 |
+
if (converted_type === 'JSON' || converted_type === 'BSON' || converted_type === 'DECIMAL') return undefined
|
| 165 |
+
if (logical_type?.type === 'JSON' || logical_type?.type === 'BSON' || logical_type?.type === 'VARIANT') return undefined
|
| 166 |
+
if (logical_type?.type === 'GEOMETRY' || logical_type?.type === 'GEOGRAPHY') return undefined
|
| 167 |
+
if (typeof value === 'string') return xxhash64(textEncoder.encode(value))
|
| 168 |
+
if (value instanceof Uint8Array) return xxhash64(value)
|
| 169 |
+
return undefined
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
if (type === 'FIXED_LEN_BYTE_ARRAY') {
|
| 173 |
+
if (converted_type === 'DECIMAL' || converted_type === 'INTERVAL') return undefined
|
| 174 |
+
if (logical_type?.type === 'DECIMAL' || logical_type?.type === 'UUID' || logical_type?.type === 'FLOAT16') return undefined
|
| 175 |
+
if (logical_type?.type === 'GEOMETRY' || logical_type?.type === 'GEOGRAPHY') return undefined
|
| 176 |
+
if (value instanceof Uint8Array) return xxhash64(value)
|
| 177 |
+
return undefined
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
// INT96 deprecated, or type missing on group columns
|
| 181 |
+
return undefined
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
/**
|
| 185 |
+
* Top-level column names that appear in $eq or $in predicates within a filter.
|
| 186 |
+
* These are the only columns where a bloom filter can prove a value's absence
|
| 187 |
+
* and let us skip a row group; any other operator can't be helped by a bloom.
|
| 188 |
+
*
|
| 189 |
+
* @param {ParquetQueryFilter | undefined} filter
|
| 190 |
+
* @returns {Set<string>}
|
| 191 |
+
*/
|
| 192 |
+
export function bloomEligibleColumns(filter) {
|
| 193 |
+
/** @type {Set<string>} */
|
| 194 |
+
const out = new Set()
|
| 195 |
+
walkBloomEligible(filter, out)
|
| 196 |
+
return out
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
/**
|
| 200 |
+
* @param {ParquetQueryFilter | undefined} filter
|
| 201 |
+
* @param {Set<string>} out
|
| 202 |
+
*/
|
| 203 |
+
function walkBloomEligible(filter, out) {
|
| 204 |
+
if (!filter) return
|
| 205 |
+
if ('$and' in filter && Array.isArray(filter.$and)) {
|
| 206 |
+
for (const sub of filter.$and) walkBloomEligible(sub, out)
|
| 207 |
+
return
|
| 208 |
+
}
|
| 209 |
+
if ('$or' in filter && Array.isArray(filter.$or)) {
|
| 210 |
+
for (const sub of filter.$or) walkBloomEligible(sub, out)
|
| 211 |
+
return
|
| 212 |
+
}
|
| 213 |
+
// $nor would need to prove presence, not absence — bloom can't help.
|
| 214 |
+
if ('$nor' in filter) return
|
| 215 |
+
for (const [field, condition] of Object.entries(filter)) {
|
| 216 |
+
if (field.startsWith('$')) continue
|
| 217 |
+
if (typeof condition === 'object' && condition !== null && !Array.isArray(condition)) {
|
| 218 |
+
if ('$eq' in condition || '$in' in condition) out.add(field)
|
| 219 |
+
} else {
|
| 220 |
+
// primitive / null / array condition is an implicit $eq
|
| 221 |
+
out.add(field)
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
}
|
web/node_modules/hyparquet/src/column.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @import {ColumnDecoder, DataReader, DecodedArray, PageHeader, PageResult, RowGroupSelect, SubColumnData} from '../src/types.js'
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import { assembleLists } from './assemble.js'
|
| 6 |
+
import { Encodings, PageTypes } from './constants.js'
|
| 7 |
+
import { convert, convertWithDictionary } from './convert.js'
|
| 8 |
+
import { decompressPage, readDataPage, readDataPageV2 } from './datapage.js'
|
| 9 |
+
import { readPlain } from './plain.js'
|
| 10 |
+
import { isFlatColumn } from './schema.js'
|
| 11 |
+
import { deserializeTCompactProtocol } from './thrift.js'
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Parse column data from a buffer.
|
| 15 |
+
*
|
| 16 |
+
* @param {DataReader} reader
|
| 17 |
+
* @param {RowGroupSelect} rowGroupSelect row group selection
|
| 18 |
+
* @param {ColumnDecoder} columnDecoder column decoder params
|
| 19 |
+
* @param {(chunk: SubColumnData) => void} [onPage] callback for each page
|
| 20 |
+
* @returns {{ data: DecodedArray[], skipped: number }}
|
| 21 |
+
*/
|
| 22 |
+
export function readColumn(reader, { groupStart, selectStart, selectEnd }, columnDecoder, onPage) {
|
| 23 |
+
const { pathInSchema, schemaPath } = columnDecoder
|
| 24 |
+
const isFlat = isFlatColumn(schemaPath)
|
| 25 |
+
/** @type {DecodedArray[]} */
|
| 26 |
+
const chunks = []
|
| 27 |
+
/** @type {DecodedArray | undefined} */
|
| 28 |
+
let dictionary = undefined
|
| 29 |
+
/** @type {DecodedArray | undefined} */
|
| 30 |
+
let lastChunk = undefined
|
| 31 |
+
let rowCount = 0
|
| 32 |
+
let skipped = 0
|
| 33 |
+
|
| 34 |
+
const emitLastChunk = onPage && (() => {
|
| 35 |
+
lastChunk && onPage({
|
| 36 |
+
pathInSchema,
|
| 37 |
+
columnData: lastChunk,
|
| 38 |
+
rowStart: groupStart + rowCount - lastChunk.length,
|
| 39 |
+
rowEnd: groupStart + rowCount,
|
| 40 |
+
})
|
| 41 |
+
})
|
| 42 |
+
|
| 43 |
+
while (isFlat ? rowCount < selectEnd : reader.offset < reader.view.byteLength - 1) {
|
| 44 |
+
if (reader.offset >= reader.view.byteLength - 1) break // end of reader
|
| 45 |
+
|
| 46 |
+
// read page header
|
| 47 |
+
const header = parquetHeader(reader)
|
| 48 |
+
if (header.type === 'DICTIONARY_PAGE') {
|
| 49 |
+
const { data } = readPage(reader, header, columnDecoder, dictionary, undefined, 0)
|
| 50 |
+
if (data) dictionary = convert(data, columnDecoder)
|
| 51 |
+
} else {
|
| 52 |
+
const lastChunkLength = lastChunk?.length || 0
|
| 53 |
+
const result = readPage(reader, header, columnDecoder, dictionary, lastChunk, selectStart - rowCount)
|
| 54 |
+
if (result.skipped) {
|
| 55 |
+
// skipped page - just advance row count, don't add to chunks
|
| 56 |
+
if (!chunks.length) {
|
| 57 |
+
skipped += result.skipped
|
| 58 |
+
}
|
| 59 |
+
rowCount += result.skipped
|
| 60 |
+
} else if (result.data && lastChunk === result.data) {
|
| 61 |
+
// continued from previous page
|
| 62 |
+
rowCount += result.data.length - lastChunkLength
|
| 63 |
+
} else if (result.data && result.data.length) {
|
| 64 |
+
emitLastChunk?.()
|
| 65 |
+
chunks.push(result.data)
|
| 66 |
+
rowCount += result.data.length
|
| 67 |
+
lastChunk = result.data
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
emitLastChunk?.()
|
| 72 |
+
|
| 73 |
+
return { data: chunks, skipped }
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/**
|
| 77 |
+
* Read a page (data or dictionary) from a buffer.
|
| 78 |
+
*
|
| 79 |
+
* @param {DataReader} reader
|
| 80 |
+
* @param {PageHeader} header
|
| 81 |
+
* @param {ColumnDecoder} columnDecoder
|
| 82 |
+
* @param {DecodedArray | undefined} dictionary
|
| 83 |
+
* @param {DecodedArray | undefined} previousChunk
|
| 84 |
+
* @param {number} pageStart skip this many rows in the page
|
| 85 |
+
* @returns {PageResult}
|
| 86 |
+
*/
|
| 87 |
+
export function readPage(reader, header, columnDecoder, dictionary, previousChunk, pageStart) {
|
| 88 |
+
const { type, element, schemaPath, codec, compressors } = columnDecoder
|
| 89 |
+
// read compressed_page_size bytes
|
| 90 |
+
const compressedBytes = new Uint8Array(
|
| 91 |
+
reader.view.buffer, reader.view.byteOffset + reader.offset, header.compressed_page_size
|
| 92 |
+
)
|
| 93 |
+
reader.offset += header.compressed_page_size
|
| 94 |
+
|
| 95 |
+
// parse page data by type
|
| 96 |
+
if (header.type === 'DATA_PAGE') {
|
| 97 |
+
const daph = header.data_page_header
|
| 98 |
+
if (!daph) throw new Error('parquet data page header is undefined')
|
| 99 |
+
|
| 100 |
+
// skip unnecessary non-nested pages
|
| 101 |
+
if (pageStart > daph.num_values && isFlatColumn(schemaPath)) {
|
| 102 |
+
return { skipped: daph.num_values }
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
const page = decompressPage(compressedBytes, Number(header.uncompressed_page_size), codec, compressors)
|
| 106 |
+
const { definitionLevels, repetitionLevels, dataPage } = readDataPage(page, daph, columnDecoder)
|
| 107 |
+
// assert(!daph.statistics?.null_count || daph.statistics.null_count === BigInt(daph.num_values - dataPage.length))
|
| 108 |
+
|
| 109 |
+
// convert types, dereference dictionary, and assemble lists
|
| 110 |
+
const values = convertWithDictionary(dataPage, dictionary, daph.encoding, columnDecoder)
|
| 111 |
+
const output = Array.isArray(previousChunk) ? previousChunk : []
|
| 112 |
+
const assembled = assembleLists(output, definitionLevels, repetitionLevels, values, schemaPath)
|
| 113 |
+
return { skipped: 0, data: assembled }
|
| 114 |
+
} else if (header.type === 'DATA_PAGE_V2') {
|
| 115 |
+
const daph2 = header.data_page_header_v2
|
| 116 |
+
if (!daph2) throw new Error('parquet data page header v2 is undefined')
|
| 117 |
+
|
| 118 |
+
// skip unnecessary pages
|
| 119 |
+
if (pageStart > daph2.num_rows) {
|
| 120 |
+
return { skipped: daph2.num_values }
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
const { definitionLevels, repetitionLevels, dataPage } =
|
| 124 |
+
readDataPageV2(compressedBytes, header, columnDecoder)
|
| 125 |
+
|
| 126 |
+
// convert types, dereference dictionary, and assemble lists
|
| 127 |
+
const values = convertWithDictionary(dataPage, dictionary, daph2.encoding, columnDecoder)
|
| 128 |
+
const output = Array.isArray(previousChunk) ? previousChunk : []
|
| 129 |
+
const assembled = assembleLists(output, definitionLevels, repetitionLevels, values, schemaPath)
|
| 130 |
+
return { skipped: 0, data: assembled }
|
| 131 |
+
} else if (header.type === 'DICTIONARY_PAGE') {
|
| 132 |
+
const diph = header.dictionary_page_header
|
| 133 |
+
if (!diph) throw new Error('parquet dictionary page header is undefined')
|
| 134 |
+
|
| 135 |
+
const page = decompressPage(
|
| 136 |
+
compressedBytes, Number(header.uncompressed_page_size), codec, compressors
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
const reader = { view: new DataView(page.buffer, page.byteOffset, page.byteLength), offset: 0 }
|
| 140 |
+
const dictArray = readPlain(reader, type, diph.num_values, element.type_length)
|
| 141 |
+
return { skipped: 0, data: dictArray }
|
| 142 |
+
} else {
|
| 143 |
+
throw new Error(`parquet unsupported page type: ${header.type}`)
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
/**
|
| 148 |
+
* Read parquet header from a buffer.
|
| 149 |
+
*
|
| 150 |
+
* @param {DataReader} reader
|
| 151 |
+
* @returns {PageHeader}
|
| 152 |
+
*/
|
| 153 |
+
function parquetHeader(reader) {
|
| 154 |
+
const header = deserializeTCompactProtocol(reader)
|
| 155 |
+
|
| 156 |
+
// Parse parquet header from thrift data
|
| 157 |
+
const type = PageTypes[header.field_1]
|
| 158 |
+
const uncompressed_page_size = header.field_2
|
| 159 |
+
const compressed_page_size = header.field_3
|
| 160 |
+
const crc = header.field_4
|
| 161 |
+
const data_page_header = header.field_5 && {
|
| 162 |
+
num_values: header.field_5.field_1,
|
| 163 |
+
encoding: Encodings[header.field_5.field_2],
|
| 164 |
+
definition_level_encoding: Encodings[header.field_5.field_3],
|
| 165 |
+
repetition_level_encoding: Encodings[header.field_5.field_4],
|
| 166 |
+
statistics: header.field_5.field_5 && {
|
| 167 |
+
max: header.field_5.field_5.field_1,
|
| 168 |
+
min: header.field_5.field_5.field_2,
|
| 169 |
+
null_count: header.field_5.field_5.field_3,
|
| 170 |
+
distinct_count: header.field_5.field_5.field_4,
|
| 171 |
+
max_value: header.field_5.field_5.field_5,
|
| 172 |
+
min_value: header.field_5.field_5.field_6,
|
| 173 |
+
},
|
| 174 |
+
}
|
| 175 |
+
const index_page_header = header.field_6
|
| 176 |
+
const dictionary_page_header = header.field_7 && {
|
| 177 |
+
num_values: header.field_7.field_1,
|
| 178 |
+
encoding: Encodings[header.field_7.field_2],
|
| 179 |
+
is_sorted: header.field_7.field_3,
|
| 180 |
+
}
|
| 181 |
+
const data_page_header_v2 = header.field_8 && {
|
| 182 |
+
num_values: header.field_8.field_1,
|
| 183 |
+
num_nulls: header.field_8.field_2,
|
| 184 |
+
num_rows: header.field_8.field_3,
|
| 185 |
+
encoding: Encodings[header.field_8.field_4],
|
| 186 |
+
definition_levels_byte_length: header.field_8.field_5,
|
| 187 |
+
repetition_levels_byte_length: header.field_8.field_6,
|
| 188 |
+
is_compressed: header.field_8.field_7 === undefined ? true : header.field_8.field_7, // default true
|
| 189 |
+
statistics: header.field_8.field_8,
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
return {
|
| 193 |
+
type,
|
| 194 |
+
uncompressed_page_size,
|
| 195 |
+
compressed_page_size,
|
| 196 |
+
crc,
|
| 197 |
+
data_page_header,
|
| 198 |
+
index_page_header,
|
| 199 |
+
dictionary_page_header,
|
| 200 |
+
data_page_header_v2,
|
| 201 |
+
}
|
| 202 |
+
}
|
web/node_modules/hyparquet/src/constants.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
/** @type {import('../src/types.d.ts').ParquetType[]} */
|
| 3 |
+
export const ParquetTypes = [
|
| 4 |
+
'BOOLEAN',
|
| 5 |
+
'INT32',
|
| 6 |
+
'INT64',
|
| 7 |
+
'INT96', // deprecated
|
| 8 |
+
'FLOAT',
|
| 9 |
+
'DOUBLE',
|
| 10 |
+
'BYTE_ARRAY',
|
| 11 |
+
'FIXED_LEN_BYTE_ARRAY',
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
/** @type {import('../src/types.d.ts').Encoding[]} */
|
| 15 |
+
export const Encodings = [
|
| 16 |
+
'PLAIN',
|
| 17 |
+
'GROUP_VAR_INT', // deprecated
|
| 18 |
+
'PLAIN_DICTIONARY',
|
| 19 |
+
'RLE',
|
| 20 |
+
'BIT_PACKED', // deprecated
|
| 21 |
+
'DELTA_BINARY_PACKED',
|
| 22 |
+
'DELTA_LENGTH_BYTE_ARRAY',
|
| 23 |
+
'DELTA_BYTE_ARRAY',
|
| 24 |
+
'RLE_DICTIONARY',
|
| 25 |
+
'BYTE_STREAM_SPLIT',
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
/** @type {import('../src/types.d.ts').FieldRepetitionType[]} */
|
| 29 |
+
export const FieldRepetitionTypes = [
|
| 30 |
+
'REQUIRED',
|
| 31 |
+
'OPTIONAL',
|
| 32 |
+
'REPEATED',
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
/** @type {import('../src/types.d.ts').ConvertedType[]} */
|
| 36 |
+
export const ConvertedTypes = [
|
| 37 |
+
'UTF8',
|
| 38 |
+
'MAP',
|
| 39 |
+
'MAP_KEY_VALUE',
|
| 40 |
+
'LIST',
|
| 41 |
+
'ENUM',
|
| 42 |
+
'DECIMAL',
|
| 43 |
+
'DATE',
|
| 44 |
+
'TIME_MILLIS',
|
| 45 |
+
'TIME_MICROS',
|
| 46 |
+
'TIMESTAMP_MILLIS',
|
| 47 |
+
'TIMESTAMP_MICROS',
|
| 48 |
+
'UINT_8',
|
| 49 |
+
'UINT_16',
|
| 50 |
+
'UINT_32',
|
| 51 |
+
'UINT_64',
|
| 52 |
+
'INT_8',
|
| 53 |
+
'INT_16',
|
| 54 |
+
'INT_32',
|
| 55 |
+
'INT_64',
|
| 56 |
+
'JSON',
|
| 57 |
+
'BSON',
|
| 58 |
+
'INTERVAL',
|
| 59 |
+
]
|
| 60 |
+
|
| 61 |
+
/** @type {import('../src/types.d.ts').CompressionCodec[]} */
|
| 62 |
+
export const CompressionCodecs = [
|
| 63 |
+
'UNCOMPRESSED',
|
| 64 |
+
'SNAPPY',
|
| 65 |
+
'GZIP',
|
| 66 |
+
'LZO',
|
| 67 |
+
'BROTLI',
|
| 68 |
+
'LZ4',
|
| 69 |
+
'ZSTD',
|
| 70 |
+
'LZ4_RAW',
|
| 71 |
+
]
|
| 72 |
+
|
| 73 |
+
/** @type {import('../src/types.d.ts').PageType[]} */
|
| 74 |
+
export const PageTypes = [
|
| 75 |
+
'DATA_PAGE',
|
| 76 |
+
'INDEX_PAGE',
|
| 77 |
+
'DICTIONARY_PAGE',
|
| 78 |
+
'DATA_PAGE_V2',
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
/** @type {import('../src/types.d.ts').BoundaryOrder[]} */
|
| 82 |
+
export const BoundaryOrders = [
|
| 83 |
+
'UNORDERED',
|
| 84 |
+
'ASCENDING',
|
| 85 |
+
'DESCENDING',
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
/** @type {import('../src/types.d.ts').EdgeInterpolationAlgorithm[]} */
|
| 89 |
+
export const EdgeInterpolationAlgorithms = [
|
| 90 |
+
'SPHERICAL',
|
| 91 |
+
'VINCENTY',
|
| 92 |
+
'THOMAS',
|
| 93 |
+
'ANDOYER',
|
| 94 |
+
'KARNEY',
|
| 95 |
+
]
|
web/node_modules/hyparquet/src/convert.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { wkbToGeojson } from './wkb.js'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* @import {ColumnDecoder, DecodedArray, Encoding, ParquetParsers} from '../src/types.js'
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
const decoder = new TextDecoder()
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* Default type parsers when no custom ones are given
|
| 11 |
+
* @type ParquetParsers
|
| 12 |
+
*/
|
| 13 |
+
export const DEFAULT_PARSERS = {
|
| 14 |
+
timestampFromMilliseconds(millis) {
|
| 15 |
+
return new Date(Number(millis))
|
| 16 |
+
},
|
| 17 |
+
timestampFromMicroseconds(micros) {
|
| 18 |
+
return new Date(Number(micros / 1000n))
|
| 19 |
+
},
|
| 20 |
+
timestampFromNanoseconds(nanos) {
|
| 21 |
+
return new Date(Number(nanos / 1000000n))
|
| 22 |
+
},
|
| 23 |
+
dateFromDays(days) {
|
| 24 |
+
return new Date(days * 86400000)
|
| 25 |
+
},
|
| 26 |
+
stringFromBytes(bytes) {
|
| 27 |
+
return bytes && decoder.decode(bytes)
|
| 28 |
+
},
|
| 29 |
+
jsonFromBytes(bytes) {
|
| 30 |
+
return bytes && JSON.parse(decoder.decode(bytes))
|
| 31 |
+
},
|
| 32 |
+
geometryFromBytes(bytes) {
|
| 33 |
+
return bytes && wkbToGeojson({ view: new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength), offset: 0 })
|
| 34 |
+
},
|
| 35 |
+
geographyFromBytes(bytes) {
|
| 36 |
+
return bytes && wkbToGeojson({ view: new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength), offset: 0 })
|
| 37 |
+
},
|
| 38 |
+
uuidFromBytes(bytes) {
|
| 39 |
+
if (!bytes) return undefined
|
| 40 |
+
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('')
|
| 41 |
+
return hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) + '-' + hex.slice(16, 20) + '-' + hex.slice(20, 32)
|
| 42 |
+
},
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/**
|
| 46 |
+
* Convert known types from primitive to rich, and dereference dictionary.
|
| 47 |
+
*
|
| 48 |
+
* @param {DecodedArray} data series of primitive types
|
| 49 |
+
* @param {DecodedArray | undefined} dictionary
|
| 50 |
+
* @param {Encoding} encoding
|
| 51 |
+
* @param {ColumnDecoder} columnDecoder
|
| 52 |
+
* @returns {DecodedArray} series of rich types
|
| 53 |
+
*/
|
| 54 |
+
export function convertWithDictionary(data, dictionary, encoding, columnDecoder) {
|
| 55 |
+
if (dictionary && encoding.endsWith('_DICTIONARY')) {
|
| 56 |
+
let output = data
|
| 57 |
+
if (data instanceof Uint8Array && !(dictionary instanceof Uint8Array)) {
|
| 58 |
+
// @ts-expect-error upgrade data to match dictionary type with fancy constructor
|
| 59 |
+
output = new dictionary.constructor(data.length)
|
| 60 |
+
}
|
| 61 |
+
for (let i = 0; i < data.length; i++) {
|
| 62 |
+
output[i] = dictionary[data[i]]
|
| 63 |
+
}
|
| 64 |
+
return output
|
| 65 |
+
} else {
|
| 66 |
+
return convert(data, columnDecoder)
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
/**
|
| 71 |
+
* Convert known types from primitive to rich.
|
| 72 |
+
*
|
| 73 |
+
* @param {DecodedArray} data series of primitive types
|
| 74 |
+
* @param {ColumnDecoder} columnDecoder
|
| 75 |
+
* @returns {DecodedArray} series of rich types
|
| 76 |
+
*/
|
| 77 |
+
export function convert(data, columnDecoder) {
|
| 78 |
+
const { element, parsers, utf8 = true, schemaPath } = columnDecoder
|
| 79 |
+
const { type, converted_type: ctype, logical_type: ltype } = element
|
| 80 |
+
const nullable = element.repetition_type !== 'REQUIRED'
|
| 81 |
+
|
| 82 |
+
// Skip utf8 conversion for plain BYTE_ARRAY inside VARIANT
|
| 83 |
+
const isVariant = schemaPath?.some(s => s.element.logical_type?.type === 'VARIANT')
|
| 84 |
+
if (isVariant && type === 'BYTE_ARRAY' && ctype !== 'UTF8' && ltype?.type !== 'STRING') {
|
| 85 |
+
return data
|
| 86 |
+
}
|
| 87 |
+
if (ctype === 'DECIMAL') {
|
| 88 |
+
const scale = element.scale || 0
|
| 89 |
+
const factor = 10 ** -scale
|
| 90 |
+
const arr = new Array(data.length)
|
| 91 |
+
for (let i = 0; i < arr.length; i++) {
|
| 92 |
+
if (data[i] instanceof Uint8Array) {
|
| 93 |
+
arr[i] = parseDecimal(data[i]) * factor
|
| 94 |
+
} else {
|
| 95 |
+
arr[i] = Number(data[i]) * factor
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
return arr
|
| 99 |
+
}
|
| 100 |
+
if (!ctype && type === 'INT96') {
|
| 101 |
+
return Array.from(data).map(v => parsers.timestampFromNanoseconds(parseInt96Nanos(v)))
|
| 102 |
+
}
|
| 103 |
+
if (ctype === 'DATE') {
|
| 104 |
+
return Array.from(data).map(v => parsers.dateFromDays(v))
|
| 105 |
+
}
|
| 106 |
+
if (ctype === 'TIMESTAMP_MILLIS') {
|
| 107 |
+
return Array.from(data).map(v => parsers.timestampFromMilliseconds(v))
|
| 108 |
+
}
|
| 109 |
+
if (ctype === 'TIMESTAMP_MICROS') {
|
| 110 |
+
return Array.from(data).map(v => parsers.timestampFromMicroseconds(v))
|
| 111 |
+
}
|
| 112 |
+
if (ctype === 'JSON') {
|
| 113 |
+
return data.map(v => parsers.jsonFromBytes(v))
|
| 114 |
+
}
|
| 115 |
+
if (ctype === 'BSON') {
|
| 116 |
+
throw new Error('parquet bson not supported')
|
| 117 |
+
}
|
| 118 |
+
if (ctype === 'INTERVAL') {
|
| 119 |
+
throw new Error('parquet interval not supported')
|
| 120 |
+
}
|
| 121 |
+
if (ltype?.type === 'GEOMETRY') {
|
| 122 |
+
return data.map(v => parsers.geometryFromBytes(v))
|
| 123 |
+
}
|
| 124 |
+
if (ltype?.type === 'GEOGRAPHY') {
|
| 125 |
+
return data.map(v => parsers.geographyFromBytes(v))
|
| 126 |
+
}
|
| 127 |
+
if (ltype?.type === 'UUID') {
|
| 128 |
+
return data.map(v => parsers.uuidFromBytes(v))
|
| 129 |
+
}
|
| 130 |
+
if (ctype === 'UTF8' || ltype?.type === 'STRING' || utf8 && type === 'BYTE_ARRAY') {
|
| 131 |
+
return data.map(v => parsers.stringFromBytes(v))
|
| 132 |
+
}
|
| 133 |
+
if (ctype === 'UINT_64' || ltype?.type === 'INTEGER' && ltype.bitWidth === 64 && !ltype.isSigned) {
|
| 134 |
+
if (data instanceof BigInt64Array) return new BigUint64Array(data.buffer, data.byteOffset, data.length)
|
| 135 |
+
const arr = nullable ? new Array(data.length) : new BigUint64Array(data.length)
|
| 136 |
+
for (let i = 0; i < arr.length; i++) arr[i] = data[i]
|
| 137 |
+
return arr
|
| 138 |
+
}
|
| 139 |
+
if (ctype === 'UINT_32' || ltype?.type === 'INTEGER' && ltype.bitWidth === 32 && !ltype.isSigned) {
|
| 140 |
+
if (data instanceof Int32Array) return new Uint32Array(data.buffer, data.byteOffset, data.length)
|
| 141 |
+
const arr = nullable ? new Array(data.length) : new Uint32Array(data.length)
|
| 142 |
+
for (let i = 0; i < arr.length; i++) {
|
| 143 |
+
arr[i] = data[i] < 0 ? 4294967296 + data[i] : data[i]
|
| 144 |
+
}
|
| 145 |
+
return arr
|
| 146 |
+
}
|
| 147 |
+
if (ltype?.type === 'FLOAT16') {
|
| 148 |
+
return Array.from(data).map(parseFloat16)
|
| 149 |
+
}
|
| 150 |
+
if (ltype?.type === 'TIMESTAMP') {
|
| 151 |
+
const { unit } = ltype
|
| 152 |
+
/** @type {ParquetParsers[keyof ParquetParsers]} */
|
| 153 |
+
let parser = parsers.timestampFromMilliseconds
|
| 154 |
+
if (unit === 'MICROS') parser = parsers.timestampFromMicroseconds
|
| 155 |
+
if (unit === 'NANOS') parser = parsers.timestampFromNanoseconds
|
| 156 |
+
const arr = new Array(data.length)
|
| 157 |
+
for (let i = 0; i < arr.length; i++) {
|
| 158 |
+
arr[i] = parser(data[i])
|
| 159 |
+
}
|
| 160 |
+
return arr
|
| 161 |
+
}
|
| 162 |
+
return data
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
/**
|
| 166 |
+
* @param {Uint8Array} bytes
|
| 167 |
+
* @returns {number}
|
| 168 |
+
*/
|
| 169 |
+
export function parseDecimal(bytes) {
|
| 170 |
+
if (!bytes.length) return 0
|
| 171 |
+
|
| 172 |
+
let value = 0n
|
| 173 |
+
for (const byte of bytes) {
|
| 174 |
+
value = value * 256n + BigInt(byte)
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
// handle signed
|
| 178 |
+
const bits = bytes.length * 8
|
| 179 |
+
if (value >= 2n ** BigInt(bits - 1)) {
|
| 180 |
+
value -= 2n ** BigInt(bits)
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
return Number(value)
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
/**
|
| 187 |
+
* Converts INT96 date format (hi 32bit days, lo 64bit nanos) to nanos since epoch
|
| 188 |
+
* @param {bigint} value
|
| 189 |
+
* @returns {bigint}
|
| 190 |
+
*/
|
| 191 |
+
function parseInt96Nanos(value) {
|
| 192 |
+
const days = (value >> 64n) - 2440588n
|
| 193 |
+
const nano = value & 0xffffffffffffffffn
|
| 194 |
+
return days * 86400000000000n + nano
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
/**
|
| 198 |
+
* @param {Uint8Array | undefined} bytes
|
| 199 |
+
* @returns {number | undefined}
|
| 200 |
+
*/
|
| 201 |
+
export function parseFloat16(bytes) {
|
| 202 |
+
if (!bytes) return undefined
|
| 203 |
+
const int16 = bytes[1] << 8 | bytes[0]
|
| 204 |
+
const sign = int16 >> 15 ? -1 : 1
|
| 205 |
+
const exp = int16 >> 10 & 0x1f
|
| 206 |
+
const frac = int16 & 0x3ff
|
| 207 |
+
if (exp === 0) return sign * 2 ** -14 * (frac / 1024) // subnormals
|
| 208 |
+
if (exp === 0x1f) return frac ? NaN : sign * Infinity
|
| 209 |
+
return sign * 2 ** (exp - 15) * (1 + frac / 1024)
|
| 210 |
+
}
|
web/node_modules/hyparquet/src/datapage.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @import {ColumnDecoder, CompressionCodec, Compressors, DataPage, DataPageHeader, DataPageHeaderV2, DataReader, DecodedArray, PageHeader, SchemaTree} from '../src/types.js'
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import { deltaBinaryUnpack, deltaByteArray, deltaLengthByteArray } from './delta.js'
|
| 6 |
+
import { byteStreamSplit, readRleBitPackedHybrid } from './encoding.js'
|
| 7 |
+
import { readPlain } from './plain.js'
|
| 8 |
+
import { getMaxDefinitionLevel, getMaxRepetitionLevel } from './schema.js'
|
| 9 |
+
import { snappyUncompress } from './snappy.js'
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Read a data page from uncompressed reader.
|
| 13 |
+
*
|
| 14 |
+
* @param {Uint8Array} bytes raw page data (should already be decompressed)
|
| 15 |
+
* @param {DataPageHeader} daph data page header
|
| 16 |
+
* @param {ColumnDecoder} columnDecoder
|
| 17 |
+
* @returns {DataPage} definition levels, repetition levels, and array of values
|
| 18 |
+
*/
|
| 19 |
+
export function readDataPage(bytes, daph, { type, element, schemaPath }) {
|
| 20 |
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
| 21 |
+
const reader = { view, offset: 0 }
|
| 22 |
+
/** @type {DecodedArray} */
|
| 23 |
+
let dataPage
|
| 24 |
+
|
| 25 |
+
// repetition and definition levels
|
| 26 |
+
const repetitionLevels = readRepetitionLevels(reader, daph, schemaPath)
|
| 27 |
+
// assert(!repetitionLevels.length || repetitionLevels.length === daph.num_values)
|
| 28 |
+
const { definitionLevels, numNulls } = readDefinitionLevels(reader, daph, schemaPath)
|
| 29 |
+
// assert(!definitionLevels.length || definitionLevels.length === daph.num_values)
|
| 30 |
+
|
| 31 |
+
// read values based on encoding
|
| 32 |
+
const nValues = daph.num_values - numNulls
|
| 33 |
+
if (daph.encoding === 'PLAIN') {
|
| 34 |
+
dataPage = readPlain(reader, type, nValues, element.type_length)
|
| 35 |
+
} else if (
|
| 36 |
+
daph.encoding === 'PLAIN_DICTIONARY' ||
|
| 37 |
+
daph.encoding === 'RLE_DICTIONARY' ||
|
| 38 |
+
daph.encoding === 'RLE'
|
| 39 |
+
) {
|
| 40 |
+
const bitWidth = type === 'BOOLEAN' ? 1 : view.getUint8(reader.offset++)
|
| 41 |
+
if (bitWidth) {
|
| 42 |
+
dataPage = new Array(nValues)
|
| 43 |
+
if (type === 'BOOLEAN') {
|
| 44 |
+
readRleBitPackedHybrid(reader, bitWidth, dataPage)
|
| 45 |
+
dataPage = dataPage.map(x => !!x) // convert to boolean
|
| 46 |
+
} else {
|
| 47 |
+
// assert(daph.encoding.endsWith('_DICTIONARY'))
|
| 48 |
+
readRleBitPackedHybrid(reader, bitWidth, dataPage, view.byteLength - reader.offset)
|
| 49 |
+
}
|
| 50 |
+
} else {
|
| 51 |
+
dataPage = new Uint8Array(nValues) // nValue zeroes
|
| 52 |
+
}
|
| 53 |
+
} else if (daph.encoding === 'BYTE_STREAM_SPLIT') {
|
| 54 |
+
dataPage = byteStreamSplit(reader, nValues, type, element.type_length)
|
| 55 |
+
} else if (daph.encoding === 'DELTA_BINARY_PACKED') {
|
| 56 |
+
const int32 = type === 'INT32'
|
| 57 |
+
dataPage = int32 ? new Int32Array(nValues) : new BigInt64Array(nValues)
|
| 58 |
+
deltaBinaryUnpack(reader, nValues, dataPage)
|
| 59 |
+
} else if (daph.encoding === 'DELTA_LENGTH_BYTE_ARRAY') {
|
| 60 |
+
dataPage = new Array(nValues)
|
| 61 |
+
deltaLengthByteArray(reader, nValues, dataPage)
|
| 62 |
+
} else {
|
| 63 |
+
throw new Error(`parquet unsupported encoding: ${daph.encoding}`)
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
return { definitionLevels, repetitionLevels, dataPage }
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/**
|
| 70 |
+
* @param {DataReader} reader data view for the page
|
| 71 |
+
* @param {DataPageHeader} daph data page header
|
| 72 |
+
* @param {SchemaTree[]} schemaPath
|
| 73 |
+
* @returns {any[]} repetition levels and number of bytes read
|
| 74 |
+
*/
|
| 75 |
+
function readRepetitionLevels(reader, daph, schemaPath) {
|
| 76 |
+
if (schemaPath.length > 1) {
|
| 77 |
+
const maxRepetitionLevel = getMaxRepetitionLevel(schemaPath)
|
| 78 |
+
if (maxRepetitionLevel) {
|
| 79 |
+
const values = new Array(daph.num_values)
|
| 80 |
+
readRleBitPackedHybrid(reader, bitWidth(maxRepetitionLevel), values)
|
| 81 |
+
return values
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
return []
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/**
|
| 88 |
+
* @param {DataReader} reader data view for the page
|
| 89 |
+
* @param {DataPageHeader} daph data page header
|
| 90 |
+
* @param {SchemaTree[]} schemaPath
|
| 91 |
+
* @returns {{ definitionLevels: number[], numNulls: number }} definition levels
|
| 92 |
+
*/
|
| 93 |
+
function readDefinitionLevels(reader, daph, schemaPath) {
|
| 94 |
+
const maxDefinitionLevel = getMaxDefinitionLevel(schemaPath)
|
| 95 |
+
if (!maxDefinitionLevel) return { definitionLevels: [], numNulls: 0 }
|
| 96 |
+
|
| 97 |
+
const definitionLevels = new Array(daph.num_values)
|
| 98 |
+
readRleBitPackedHybrid(reader, bitWidth(maxDefinitionLevel), definitionLevels)
|
| 99 |
+
|
| 100 |
+
// count nulls
|
| 101 |
+
let numNulls = daph.num_values
|
| 102 |
+
for (const def of definitionLevels) {
|
| 103 |
+
if (def === maxDefinitionLevel) numNulls--
|
| 104 |
+
}
|
| 105 |
+
if (numNulls === 0) definitionLevels.length = 0
|
| 106 |
+
|
| 107 |
+
return { definitionLevels, numNulls }
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/**
|
| 111 |
+
* @param {Uint8Array} compressedBytes
|
| 112 |
+
* @param {number} uncompressed_page_size
|
| 113 |
+
* @param {CompressionCodec} codec
|
| 114 |
+
* @param {Compressors | undefined} compressors
|
| 115 |
+
* @returns {Uint8Array}
|
| 116 |
+
*/
|
| 117 |
+
export function decompressPage(compressedBytes, uncompressed_page_size, codec, compressors) {
|
| 118 |
+
/** @type {Uint8Array} */
|
| 119 |
+
let page
|
| 120 |
+
const customDecompressor = compressors?.[codec]
|
| 121 |
+
if (codec === 'UNCOMPRESSED') {
|
| 122 |
+
page = compressedBytes
|
| 123 |
+
} else if (customDecompressor) {
|
| 124 |
+
page = customDecompressor(compressedBytes, uncompressed_page_size)
|
| 125 |
+
} else if (codec === 'SNAPPY') {
|
| 126 |
+
page = new Uint8Array(uncompressed_page_size)
|
| 127 |
+
snappyUncompress(compressedBytes, page)
|
| 128 |
+
} else {
|
| 129 |
+
throw new Error(`parquet unsupported compression codec: ${codec}`)
|
| 130 |
+
}
|
| 131 |
+
if (page?.length !== uncompressed_page_size) {
|
| 132 |
+
throw new Error(`parquet decompressed page length ${page?.length} does not match header ${uncompressed_page_size}`)
|
| 133 |
+
}
|
| 134 |
+
return page
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
/**
|
| 139 |
+
* Read a data page from the given Uint8Array.
|
| 140 |
+
*
|
| 141 |
+
* @param {Uint8Array} compressedBytes raw page data
|
| 142 |
+
* @param {PageHeader} ph page header
|
| 143 |
+
* @param {ColumnDecoder} columnDecoder
|
| 144 |
+
* @returns {DataPage} definition levels, repetition levels, and array of values
|
| 145 |
+
*/
|
| 146 |
+
export function readDataPageV2(compressedBytes, ph, columnDecoder) {
|
| 147 |
+
const view = new DataView(compressedBytes.buffer, compressedBytes.byteOffset, compressedBytes.byteLength)
|
| 148 |
+
const reader = { view, offset: 0 }
|
| 149 |
+
const { type, element, schemaPath, codec, compressors } = columnDecoder
|
| 150 |
+
const daph2 = ph.data_page_header_v2
|
| 151 |
+
if (!daph2) throw new Error('parquet data page header v2 is undefined')
|
| 152 |
+
|
| 153 |
+
// repetition levels
|
| 154 |
+
const repetitionLevels = readRepetitionLevelsV2(reader, daph2, schemaPath)
|
| 155 |
+
reader.offset = daph2.repetition_levels_byte_length // readVarInt() => len for boolean v2?
|
| 156 |
+
|
| 157 |
+
// definition levels
|
| 158 |
+
const definitionLevels = readDefinitionLevelsV2(reader, daph2, schemaPath)
|
| 159 |
+
// assert(reader.offset === daph2.repetition_levels_byte_length + daph2.definition_levels_byte_length)
|
| 160 |
+
|
| 161 |
+
const uncompressedPageSize = ph.uncompressed_page_size - daph2.definition_levels_byte_length - daph2.repetition_levels_byte_length
|
| 162 |
+
|
| 163 |
+
let page = compressedBytes.subarray(reader.offset)
|
| 164 |
+
if (daph2.is_compressed !== false) {
|
| 165 |
+
page = decompressPage(page, uncompressedPageSize, codec, compressors)
|
| 166 |
+
}
|
| 167 |
+
const pageView = new DataView(page.buffer, page.byteOffset, page.byteLength)
|
| 168 |
+
const pageReader = { view: pageView, offset: 0 }
|
| 169 |
+
|
| 170 |
+
// read values based on encoding
|
| 171 |
+
/** @type {DecodedArray} */
|
| 172 |
+
let dataPage
|
| 173 |
+
const nValues = daph2.num_values - daph2.num_nulls
|
| 174 |
+
if (daph2.encoding === 'PLAIN') {
|
| 175 |
+
dataPage = readPlain(pageReader, type, nValues, element.type_length)
|
| 176 |
+
} else if (daph2.encoding === 'RLE') {
|
| 177 |
+
// assert(type === 'BOOLEAN')
|
| 178 |
+
dataPage = new Array(nValues)
|
| 179 |
+
readRleBitPackedHybrid(pageReader, 1, dataPage)
|
| 180 |
+
dataPage = dataPage.map(x => !!x)
|
| 181 |
+
} else if (
|
| 182 |
+
daph2.encoding === 'PLAIN_DICTIONARY' ||
|
| 183 |
+
daph2.encoding === 'RLE_DICTIONARY'
|
| 184 |
+
) {
|
| 185 |
+
const bitWidth = pageView.getUint8(pageReader.offset++)
|
| 186 |
+
dataPage = new Array(nValues)
|
| 187 |
+
readRleBitPackedHybrid(pageReader, bitWidth, dataPage, uncompressedPageSize - 1)
|
| 188 |
+
} else if (daph2.encoding === 'DELTA_BINARY_PACKED') {
|
| 189 |
+
const int32 = type === 'INT32'
|
| 190 |
+
dataPage = int32 ? new Int32Array(nValues) : new BigInt64Array(nValues)
|
| 191 |
+
deltaBinaryUnpack(pageReader, nValues, dataPage)
|
| 192 |
+
} else if (daph2.encoding === 'DELTA_LENGTH_BYTE_ARRAY') {
|
| 193 |
+
dataPage = new Array(nValues)
|
| 194 |
+
deltaLengthByteArray(pageReader, nValues, dataPage)
|
| 195 |
+
} else if (daph2.encoding === 'DELTA_BYTE_ARRAY') {
|
| 196 |
+
dataPage = new Array(nValues)
|
| 197 |
+
deltaByteArray(pageReader, nValues, dataPage)
|
| 198 |
+
} else if (daph2.encoding === 'BYTE_STREAM_SPLIT') {
|
| 199 |
+
dataPage = byteStreamSplit(pageReader, nValues, type, element.type_length)
|
| 200 |
+
} else {
|
| 201 |
+
throw new Error(`parquet unsupported encoding: ${daph2.encoding}`)
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
return { definitionLevels, repetitionLevels, dataPage }
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/**
|
| 208 |
+
* @param {DataReader} reader
|
| 209 |
+
* @param {DataPageHeaderV2} daph2 data page header v2
|
| 210 |
+
* @param {SchemaTree[]} schemaPath
|
| 211 |
+
* @returns {any[]} repetition levels
|
| 212 |
+
*/
|
| 213 |
+
function readRepetitionLevelsV2(reader, daph2, schemaPath) {
|
| 214 |
+
const maxRepetitionLevel = getMaxRepetitionLevel(schemaPath)
|
| 215 |
+
if (!maxRepetitionLevel) return []
|
| 216 |
+
|
| 217 |
+
const values = new Array(daph2.num_values)
|
| 218 |
+
readRleBitPackedHybrid(reader, bitWidth(maxRepetitionLevel), values, daph2.repetition_levels_byte_length)
|
| 219 |
+
return values
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/**
|
| 223 |
+
* @param {DataReader} reader
|
| 224 |
+
* @param {DataPageHeaderV2} daph2 data page header v2
|
| 225 |
+
* @param {SchemaTree[]} schemaPath
|
| 226 |
+
* @returns {number[] | undefined} definition levels
|
| 227 |
+
*/
|
| 228 |
+
function readDefinitionLevelsV2(reader, daph2, schemaPath) {
|
| 229 |
+
const maxDefinitionLevel = getMaxDefinitionLevel(schemaPath)
|
| 230 |
+
if (maxDefinitionLevel) {
|
| 231 |
+
// V2 we know the length
|
| 232 |
+
const values = new Array(daph2.num_values)
|
| 233 |
+
readRleBitPackedHybrid(reader, bitWidth(maxDefinitionLevel), values, daph2.definition_levels_byte_length)
|
| 234 |
+
return values
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
/**
|
| 239 |
+
* Minimum bits needed to store value.
|
| 240 |
+
*
|
| 241 |
+
* @param {number} value
|
| 242 |
+
* @returns {number}
|
| 243 |
+
*/
|
| 244 |
+
function bitWidth(value) {
|
| 245 |
+
return 32 - Math.clz32(value)
|
| 246 |
+
}
|
web/node_modules/hyparquet/src/delta.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @import {DataReader} from '../src/types.js'
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import { readVarInt, readZigZagBigInt } from './thrift.js'
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* @param {DataReader} reader
|
| 9 |
+
* @param {number} count number of values to read
|
| 10 |
+
* @param {Int32Array | BigInt64Array} output
|
| 11 |
+
*/
|
| 12 |
+
export function deltaBinaryUnpack(reader, count, output) {
|
| 13 |
+
const int32 = output instanceof Int32Array
|
| 14 |
+
const blockSize = readVarInt(reader)
|
| 15 |
+
const miniblockPerBlock = readVarInt(reader)
|
| 16 |
+
readVarInt(reader) // assert(=== count)
|
| 17 |
+
let value = readZigZagBigInt(reader) // first value
|
| 18 |
+
let outputIndex = 0
|
| 19 |
+
output[outputIndex++] = int32 ? Number(value) : value
|
| 20 |
+
|
| 21 |
+
const valuesPerMiniblock = blockSize / miniblockPerBlock
|
| 22 |
+
|
| 23 |
+
while (outputIndex < count) {
|
| 24 |
+
// new block
|
| 25 |
+
const minDelta = readZigZagBigInt(reader)
|
| 26 |
+
const bitWidths = new Uint8Array(miniblockPerBlock)
|
| 27 |
+
for (let i = 0; i < miniblockPerBlock; i++) {
|
| 28 |
+
bitWidths[i] = reader.view.getUint8(reader.offset++)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
for (let i = 0; i < miniblockPerBlock && outputIndex < count; i++) {
|
| 32 |
+
// new miniblock
|
| 33 |
+
const bitWidth = BigInt(bitWidths[i])
|
| 34 |
+
if (bitWidth) {
|
| 35 |
+
let bitpackPos = 0n
|
| 36 |
+
let miniblockCount = valuesPerMiniblock
|
| 37 |
+
const mask = (1n << bitWidth) - 1n
|
| 38 |
+
while (miniblockCount && outputIndex < count) {
|
| 39 |
+
let bits = BigInt(reader.view.getUint8(reader.offset)) >> bitpackPos & mask // TODO: don't re-read value every time
|
| 40 |
+
bitpackPos += bitWidth
|
| 41 |
+
while (bitpackPos >= 8) {
|
| 42 |
+
bitpackPos -= 8n
|
| 43 |
+
reader.offset++
|
| 44 |
+
if (bitpackPos) {
|
| 45 |
+
bits |= BigInt(reader.view.getUint8(reader.offset)) << bitWidth - bitpackPos & mask
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
const delta = minDelta + bits
|
| 49 |
+
value += delta
|
| 50 |
+
output[outputIndex++] = int32 ? Number(value) : value
|
| 51 |
+
miniblockCount--
|
| 52 |
+
}
|
| 53 |
+
if (miniblockCount) {
|
| 54 |
+
// consume leftover miniblock
|
| 55 |
+
reader.offset += Math.ceil((miniblockCount * Number(bitWidth) + Number(bitpackPos)) / 8)
|
| 56 |
+
}
|
| 57 |
+
} else {
|
| 58 |
+
for (let j = 0; j < valuesPerMiniblock && outputIndex < count; j++) {
|
| 59 |
+
value += minDelta
|
| 60 |
+
output[outputIndex++] = int32 ? Number(value) : value
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/**
|
| 68 |
+
* @param {DataReader} reader
|
| 69 |
+
* @param {number} count
|
| 70 |
+
* @param {Uint8Array[]} output
|
| 71 |
+
*/
|
| 72 |
+
export function deltaLengthByteArray(reader, count, output) {
|
| 73 |
+
const lengths = new Int32Array(count)
|
| 74 |
+
deltaBinaryUnpack(reader, count, lengths)
|
| 75 |
+
for (let i = 0; i < count; i++) {
|
| 76 |
+
output[i] = new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, lengths[i])
|
| 77 |
+
reader.offset += lengths[i]
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/**
|
| 82 |
+
* @param {DataReader} reader
|
| 83 |
+
* @param {number} count
|
| 84 |
+
* @param {Uint8Array[]} output
|
| 85 |
+
*/
|
| 86 |
+
export function deltaByteArray(reader, count, output) {
|
| 87 |
+
const prefixData = new Int32Array(count)
|
| 88 |
+
deltaBinaryUnpack(reader, count, prefixData)
|
| 89 |
+
const suffixData = new Int32Array(count)
|
| 90 |
+
deltaBinaryUnpack(reader, count, suffixData)
|
| 91 |
+
|
| 92 |
+
for (let i = 0; i < count; i++) {
|
| 93 |
+
const suffix = new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, suffixData[i])
|
| 94 |
+
if (prefixData[i]) {
|
| 95 |
+
// copy from previous value
|
| 96 |
+
output[i] = new Uint8Array(prefixData[i] + suffixData[i])
|
| 97 |
+
output[i].set(output[i - 1].subarray(0, prefixData[i]))
|
| 98 |
+
output[i].set(suffix, prefixData[i])
|
| 99 |
+
} else {
|
| 100 |
+
output[i] = suffix
|
| 101 |
+
}
|
| 102 |
+
reader.offset += suffixData[i]
|
| 103 |
+
}
|
| 104 |
+
}
|
web/node_modules/hyparquet/src/encoding.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @import {DataReader, DecodedArray, ParquetType} from '../src/types.js'
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import { readVarInt } from './thrift.js'
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Read values from a run-length encoded/bit-packed hybrid encoding.
|
| 9 |
+
*
|
| 10 |
+
* If length is zero, then read int32 length at the start.
|
| 11 |
+
*
|
| 12 |
+
* @param {DataReader} reader
|
| 13 |
+
* @param {number} width - bitwidth
|
| 14 |
+
* @param {DecodedArray} output
|
| 15 |
+
* @param {number} [length] - length of the encoded data
|
| 16 |
+
*/
|
| 17 |
+
export function readRleBitPackedHybrid(reader, width, output, length) {
|
| 18 |
+
if (length === undefined) {
|
| 19 |
+
length = reader.view.getUint32(reader.offset, true)
|
| 20 |
+
reader.offset += 4
|
| 21 |
+
}
|
| 22 |
+
const startOffset = reader.offset
|
| 23 |
+
let seen = 0
|
| 24 |
+
while (seen < output.length) {
|
| 25 |
+
const header = readVarInt(reader)
|
| 26 |
+
if (header & 1) {
|
| 27 |
+
// bit-packed
|
| 28 |
+
seen = readBitPacked(reader, header, width, output, seen)
|
| 29 |
+
} else {
|
| 30 |
+
// rle
|
| 31 |
+
const count = header >>> 1
|
| 32 |
+
readRle(reader, count, width, output, seen)
|
| 33 |
+
seen += count
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
reader.offset = startOffset + length // duckdb writes an empty block
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/**
|
| 40 |
+
* Run-length encoding: read value with bitWidth and repeat it count times.
|
| 41 |
+
*
|
| 42 |
+
* @param {DataReader} reader
|
| 43 |
+
* @param {number} count
|
| 44 |
+
* @param {number} bitWidth
|
| 45 |
+
* @param {DecodedArray} output
|
| 46 |
+
* @param {number} seen
|
| 47 |
+
*/
|
| 48 |
+
function readRle(reader, count, bitWidth, output, seen) {
|
| 49 |
+
const width = bitWidth + 7 >> 3
|
| 50 |
+
let value = 0
|
| 51 |
+
for (let i = 0; i < width; i++) {
|
| 52 |
+
value |= reader.view.getUint8(reader.offset++) << (i << 3)
|
| 53 |
+
}
|
| 54 |
+
// assert(value < 1 << bitWidth)
|
| 55 |
+
|
| 56 |
+
// repeat value count times
|
| 57 |
+
for (let i = 0; i < count; i++) {
|
| 58 |
+
output[seen + i] = value
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/**
|
| 63 |
+
* Read a bit-packed run of the rle/bitpack hybrid.
|
| 64 |
+
* Supports width > 8 (crossing bytes).
|
| 65 |
+
*
|
| 66 |
+
* @param {DataReader} reader
|
| 67 |
+
* @param {number} header - bit-pack header
|
| 68 |
+
* @param {number} bitWidth
|
| 69 |
+
* @param {DecodedArray} output
|
| 70 |
+
* @param {number} seen
|
| 71 |
+
* @returns {number} total output values so far
|
| 72 |
+
*/
|
| 73 |
+
function readBitPacked(reader, header, bitWidth, output, seen) {
|
| 74 |
+
let count = header >> 1 << 3 // values to read
|
| 75 |
+
const mask = (1 << bitWidth) - 1
|
| 76 |
+
|
| 77 |
+
let data = 0
|
| 78 |
+
if (reader.offset < reader.view.byteLength) {
|
| 79 |
+
data = reader.view.getUint8(reader.offset++)
|
| 80 |
+
} else if (mask) {
|
| 81 |
+
// sometimes out-of-bounds reads are masked out
|
| 82 |
+
throw new Error(`parquet bitpack offset ${reader.offset} out of range`)
|
| 83 |
+
}
|
| 84 |
+
let left = 8
|
| 85 |
+
let right = 0
|
| 86 |
+
|
| 87 |
+
// read values
|
| 88 |
+
while (count) {
|
| 89 |
+
// if we have crossed a byte boundary, shift the data
|
| 90 |
+
if (right > 8) {
|
| 91 |
+
right -= 8
|
| 92 |
+
left -= 8
|
| 93 |
+
data >>>= 8
|
| 94 |
+
} else if (left - right < bitWidth) {
|
| 95 |
+
// if we don't have bitWidth number of bits to read, read next byte
|
| 96 |
+
data |= reader.view.getUint8(reader.offset) << left
|
| 97 |
+
reader.offset++
|
| 98 |
+
left += 8
|
| 99 |
+
} else {
|
| 100 |
+
if (seen < output.length) {
|
| 101 |
+
// emit value
|
| 102 |
+
output[seen++] = data >> right & mask
|
| 103 |
+
}
|
| 104 |
+
count--
|
| 105 |
+
right += bitWidth
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
return seen
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
/**
|
| 113 |
+
* @param {DataReader} reader
|
| 114 |
+
* @param {number} count
|
| 115 |
+
* @param {ParquetType} type
|
| 116 |
+
* @param {number | undefined} typeLength
|
| 117 |
+
* @returns {DecodedArray}
|
| 118 |
+
*/
|
| 119 |
+
export function byteStreamSplit(reader, count, type, typeLength) {
|
| 120 |
+
const width = byteWidth(type, typeLength)
|
| 121 |
+
const bytes = new Uint8Array(count * width)
|
| 122 |
+
for (let b = 0; b < width; b++) {
|
| 123 |
+
for (let i = 0; i < count; i++) {
|
| 124 |
+
bytes[i * width + b] = reader.view.getUint8(reader.offset++)
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
// interpret bytes as typed array
|
| 128 |
+
if (type === 'FLOAT') return new Float32Array(bytes.buffer)
|
| 129 |
+
else if (type === 'DOUBLE') return new Float64Array(bytes.buffer)
|
| 130 |
+
else if (type === 'INT32') return new Int32Array(bytes.buffer)
|
| 131 |
+
else if (type === 'INT64') return new BigInt64Array(bytes.buffer)
|
| 132 |
+
else if (type === 'FIXED_LEN_BYTE_ARRAY') {
|
| 133 |
+
// split into arrays of typeLength
|
| 134 |
+
const split = new Array(count)
|
| 135 |
+
for (let i = 0; i < count; i++) {
|
| 136 |
+
split[i] = bytes.subarray(i * width, (i + 1) * width)
|
| 137 |
+
}
|
| 138 |
+
return split
|
| 139 |
+
}
|
| 140 |
+
throw new Error(`parquet byte_stream_split unsupported type: ${type}`)
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/**
|
| 144 |
+
* @param {ParquetType} type
|
| 145 |
+
* @param {number | undefined} typeLength
|
| 146 |
+
* @returns {number}
|
| 147 |
+
*/
|
| 148 |
+
function byteWidth(type, typeLength) {
|
| 149 |
+
switch (type) {
|
| 150 |
+
case 'INT32':
|
| 151 |
+
case 'FLOAT':
|
| 152 |
+
return 4
|
| 153 |
+
case 'INT64':
|
| 154 |
+
case 'DOUBLE':
|
| 155 |
+
return 8
|
| 156 |
+
case 'FIXED_LEN_BYTE_ARRAY':
|
| 157 |
+
if (!typeLength) throw new Error('parquet byteWidth missing type_length')
|
| 158 |
+
return typeLength
|
| 159 |
+
default:
|
| 160 |
+
throw new Error(`parquet unsupported type: ${type}`)
|
| 161 |
+
}
|
| 162 |
+
}
|
web/node_modules/hyparquet/src/filter.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @import {BloomFilter, ParquetQueryFilter, RowGroup, SchemaElement} from '../src/types.js'
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import { hashParquetValue, sbbfContains } from './bloom.js'
|
| 6 |
+
import { equals } from './utils.js'
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Returns an array of top-level column names needed to evaluate the filter.
|
| 10 |
+
*
|
| 11 |
+
* @param {ParquetQueryFilter} [filter]
|
| 12 |
+
* @returns {string[]}
|
| 13 |
+
*/
|
| 14 |
+
export function columnsNeededForFilter(filter) {
|
| 15 |
+
if (!filter) return []
|
| 16 |
+
/** @type {string[]} */
|
| 17 |
+
const columns = []
|
| 18 |
+
if ('$and' in filter && Array.isArray(filter.$and)) {
|
| 19 |
+
columns.push(...filter.$and.flatMap(columnsNeededForFilter))
|
| 20 |
+
} else if ('$or' in filter && Array.isArray(filter.$or)) {
|
| 21 |
+
columns.push(...filter.$or.flatMap(columnsNeededForFilter))
|
| 22 |
+
} else if ('$nor' in filter && Array.isArray(filter.$nor)) {
|
| 23 |
+
columns.push(...filter.$nor.flatMap(columnsNeededForFilter))
|
| 24 |
+
} else {
|
| 25 |
+
// Map dot-notation paths to top-level column names
|
| 26 |
+
columns.push(...Object.keys(filter).map(key => key.split('.')[0]))
|
| 27 |
+
}
|
| 28 |
+
return [...new Set(columns)]
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* Match a record against a query filter
|
| 33 |
+
*
|
| 34 |
+
* @param {Record<string, any>} record
|
| 35 |
+
* @param {ParquetQueryFilter} filter
|
| 36 |
+
* @param {boolean} [strict]
|
| 37 |
+
* @returns {boolean}
|
| 38 |
+
*/
|
| 39 |
+
export function matchFilter(record, filter, strict = true) {
|
| 40 |
+
if ('$and' in filter && Array.isArray(filter.$and)) {
|
| 41 |
+
return filter.$and.every(subQuery => matchFilter(record, subQuery, strict))
|
| 42 |
+
}
|
| 43 |
+
if ('$or' in filter && Array.isArray(filter.$or)) {
|
| 44 |
+
return filter.$or.some(subQuery => matchFilter(record, subQuery, strict))
|
| 45 |
+
}
|
| 46 |
+
if ('$nor' in filter && Array.isArray(filter.$nor)) {
|
| 47 |
+
return !filter.$nor.some(subQuery => matchFilter(record, subQuery, strict))
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
return Object.entries(filter).every(([field, condition]) => {
|
| 51 |
+
const value = resolve(record, field)
|
| 52 |
+
|
| 53 |
+
// implicit $eq for non-object conditions
|
| 54 |
+
if (typeof condition !== 'object' || condition === null || Array.isArray(condition)) {
|
| 55 |
+
return equals(value, condition, strict)
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
return Object.entries(condition || {}).every(([operator, target]) => {
|
| 59 |
+
if (operator === '$gt') return value > target
|
| 60 |
+
if (operator === '$gte') return value >= target
|
| 61 |
+
if (operator === '$lt') return value < target
|
| 62 |
+
if (operator === '$lte') return value <= target
|
| 63 |
+
if (operator === '$eq') return equals(value, target, strict)
|
| 64 |
+
if (operator === '$ne') return !equals(value, target, strict)
|
| 65 |
+
if (operator === '$in') return Array.isArray(target) && target.includes(value)
|
| 66 |
+
if (operator === '$nin') return Array.isArray(target) && !target.includes(value)
|
| 67 |
+
if (operator === '$not') return !matchFilter({ [field]: value }, { [field]: target }, strict)
|
| 68 |
+
return true
|
| 69 |
+
})
|
| 70 |
+
})
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/**
|
| 74 |
+
* Check if a row group can be skipped based on filter and column statistics,
|
| 75 |
+
* optionally consulting per-column bloom filters for equality predicates that
|
| 76 |
+
* statistics can't decide.
|
| 77 |
+
*
|
| 78 |
+
* @param {object} options
|
| 79 |
+
* @param {RowGroup} options.rowGroup
|
| 80 |
+
* @param {string[]} options.physicalColumns
|
| 81 |
+
* @param {ParquetQueryFilter | undefined} options.filter
|
| 82 |
+
* @param {boolean} [options.strict]
|
| 83 |
+
* @param {Record<string, BloomFilter>} [options.bloomFilters] keyed by top-level column name
|
| 84 |
+
* @param {Record<string, SchemaElement>} [options.schemaElements] keyed by top-level column name, required to use bloomFilters
|
| 85 |
+
* @returns {boolean} true if the row group can be skipped
|
| 86 |
+
*/
|
| 87 |
+
export function canSkipRowGroup({ rowGroup, physicalColumns, filter, strict = true, bloomFilters, schemaElements }) {
|
| 88 |
+
if (!filter) return false
|
| 89 |
+
|
| 90 |
+
// Handle logical operators
|
| 91 |
+
if ('$and' in filter && Array.isArray(filter.$and)) {
|
| 92 |
+
// For AND, we can skip if ANY condition allows skipping
|
| 93 |
+
return filter.$and.some(subFilter => canSkipRowGroup({ rowGroup, physicalColumns, filter: subFilter, strict, bloomFilters, schemaElements }))
|
| 94 |
+
}
|
| 95 |
+
if ('$or' in filter && Array.isArray(filter.$or)) {
|
| 96 |
+
// For OR, we can skip only if ALL conditions allow skipping
|
| 97 |
+
return filter.$or.every(subFilter => canSkipRowGroup({ rowGroup, physicalColumns, filter: subFilter, strict, bloomFilters, schemaElements }))
|
| 98 |
+
}
|
| 99 |
+
if ('$nor' in filter && Array.isArray(filter.$nor)) {
|
| 100 |
+
// For NOR, we can skip if none of the conditions allow skipping
|
| 101 |
+
// This is complex, so we'll be conservative and not skip
|
| 102 |
+
return false
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// Check column filters
|
| 106 |
+
for (const [field, condition] of Object.entries(filter)) {
|
| 107 |
+
// Find the column chunk for this field
|
| 108 |
+
const columnIndex = physicalColumns.indexOf(field)
|
| 109 |
+
if (columnIndex === -1) continue
|
| 110 |
+
|
| 111 |
+
const stats = rowGroup.columns[columnIndex].meta_data?.statistics
|
| 112 |
+
const { min, max, min_value, max_value } = stats || {}
|
| 113 |
+
const minVal = min_value !== undefined ? min_value : min
|
| 114 |
+
const maxVal = max_value !== undefined ? max_value : max
|
| 115 |
+
const haveStats = minVal !== undefined && maxVal !== undefined
|
| 116 |
+
|
| 117 |
+
const bloom = bloomFilters?.[field]
|
| 118 |
+
const element = schemaElements?.[field]
|
| 119 |
+
|
| 120 |
+
// Handle operators
|
| 121 |
+
for (const [operator, target] of Object.entries(condition || {})) {
|
| 122 |
+
// Statistics-based skipping
|
| 123 |
+
if (haveStats) {
|
| 124 |
+
if (operator === '$gt' && maxVal <= target) return true
|
| 125 |
+
if (operator === '$gte' && maxVal < target) return true
|
| 126 |
+
if (operator === '$lt' && minVal >= target) return true
|
| 127 |
+
if (operator === '$lte' && minVal > target) return true
|
| 128 |
+
if (operator === '$eq' && (target < minVal || target > maxVal)) return true
|
| 129 |
+
if (operator === '$ne' && equals(minVal, maxVal, strict) && equals(minVal, target, strict)) return true
|
| 130 |
+
if (operator === '$in' && Array.isArray(target) && target.every(v => v < minVal || v > maxVal)) return true
|
| 131 |
+
if (operator === '$nin' && Array.isArray(target) && equals(minVal, maxVal, strict) && target.includes(minVal)) return true
|
| 132 |
+
}
|
| 133 |
+
// Bloom-filter skipping for equality predicates (proves absence, never presence)
|
| 134 |
+
if (bloom && element) {
|
| 135 |
+
if (operator === '$eq') {
|
| 136 |
+
const hash = hashParquetValue(target, element)
|
| 137 |
+
if (hash !== undefined && !sbbfContains(bloom.blocks, hash)) return true
|
| 138 |
+
}
|
| 139 |
+
if (operator === '$in' && Array.isArray(target) && target.length > 0) {
|
| 140 |
+
let allAbsent = true
|
| 141 |
+
for (const v of target) {
|
| 142 |
+
const h = hashParquetValue(v, element)
|
| 143 |
+
if (h === undefined || sbbfContains(bloom.blocks, h)) {
|
| 144 |
+
allAbsent = false
|
| 145 |
+
break
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
if (allAbsent) return true
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
return false
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
/**
|
| 158 |
+
* Resolve a dot-notation path to a value in a nested object.
|
| 159 |
+
*
|
| 160 |
+
* @param {Record<string, any>} record
|
| 161 |
+
* @param {string} path
|
| 162 |
+
* @returns {any}
|
| 163 |
+
*/
|
| 164 |
+
function resolve(record, path) {
|
| 165 |
+
let value = record
|
| 166 |
+
for (const part of path.split('.')) {
|
| 167 |
+
value = value?.[part]
|
| 168 |
+
}
|
| 169 |
+
return value
|
| 170 |
+
}
|
web/node_modules/hyparquet/src/geoparquet.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @param {SchemaElement[]} schema
|
| 3 |
+
* @param {KeyValue[] | undefined} key_value_metadata
|
| 4 |
+
* @returns {void}
|
| 5 |
+
*/
|
| 6 |
+
export function markGeoColumns(schema, key_value_metadata) {
|
| 7 |
+
// Prepare the list of GeoParquet columns
|
| 8 |
+
/** @type {Map<string, LogicalType>} */
|
| 9 |
+
const columns = new Map()
|
| 10 |
+
const geo = key_value_metadata?.find(({ key }) => key === 'geo')?.value
|
| 11 |
+
const decodedColumns = (geo && JSON.parse(geo)?.columns) ?? {}
|
| 12 |
+
for (const [name, column] of Object.entries(decodedColumns)) {
|
| 13 |
+
if (column.encoding !== 'WKB') continue
|
| 14 |
+
|
| 15 |
+
const type = column.edges === 'spherical' ? 'GEOGRAPHY' : 'GEOMETRY'
|
| 16 |
+
const id = column.crs?.id ?? column.crs?.ids?.[0]
|
| 17 |
+
const crs = id ? `${id.authority}:${id.code.toString()}` : undefined
|
| 18 |
+
// Note: we can't infer GEOGRAPHY's algorithm from GeoParquet
|
| 19 |
+
columns.set(name, { type, crs })
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// Mark schema elements with logical type
|
| 23 |
+
// Only look at root-level columns of type BYTE_ARRAY without existing logical_type
|
| 24 |
+
for (let i = 1; i < schema.length; i++) { // skip root
|
| 25 |
+
const { logical_type, name, num_children, type } = schema[i]
|
| 26 |
+
if (num_children) {
|
| 27 |
+
i += num_children
|
| 28 |
+
continue // skip the element and its children
|
| 29 |
+
}
|
| 30 |
+
if (type === 'BYTE_ARRAY' && !logical_type) {
|
| 31 |
+
schema[i].logical_type = columns.get(name)
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* @import {KeyValue, LogicalType, SchemaElement} from '../src/types.js'
|
| 38 |
+
*/
|
web/node_modules/hyparquet/src/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export { readColumnIndex, readOffsetIndex } from './indexes.js'
|
| 2 |
+
export { parquetMetadata, parquetMetadataAsync, parquetSchema } from './metadata.js'
|
| 3 |
+
export { parquetRead, parquetReadObjects } from './read.js'
|
| 4 |
+
export { parquetQuery } from './query.js'
|
| 5 |
+
export { snappyUncompress } from './snappy.js'
|
| 6 |
+
export { asyncBufferFromUrl, byteLengthFromUrl, cachedAsyncBuffer, flatten, toJson } from './utils.js'
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Explicitly export types for use in downstream typescript projects through
|
| 10 |
+
* `import { ParquetReadOptions } from 'hyparquet'` for example.
|
| 11 |
+
*
|
| 12 |
+
* @template {any} T
|
| 13 |
+
* @typedef {import('../src/types.d.ts').Awaitable<T>} Awaitable<T>
|
| 14 |
+
*/
|
| 15 |
+
/**
|
| 16 |
+
* @typedef {import('../src/types.d.ts').AsyncBuffer} AsyncBuffer
|
| 17 |
+
* @typedef {import('../src/types.d.ts').AsyncRowGroup} AsyncRowGroup
|
| 18 |
+
* @typedef {import('../src/types.d.ts').DataReader} DataReader
|
| 19 |
+
* @typedef {import('../src/types.d.ts').FileMetaData} FileMetaData
|
| 20 |
+
* @typedef {import('../src/types.d.ts').SchemaTree} SchemaTree
|
| 21 |
+
* @typedef {import('../src/types.d.ts').SchemaElement} SchemaElement
|
| 22 |
+
* @typedef {import('../src/types.d.ts').ParquetType} ParquetType
|
| 23 |
+
* @typedef {import('../src/types.d.ts').FieldRepetitionType} FieldRepetitionType
|
| 24 |
+
* @typedef {import('../src/types.d.ts').ConvertedType} ConvertedType
|
| 25 |
+
* @typedef {import('../src/types.d.ts').TimeUnit} TimeUnit
|
| 26 |
+
* @typedef {import('../src/types.d.ts').LogicalType} LogicalType
|
| 27 |
+
* @typedef {import('../src/types.d.ts').RowGroup} RowGroup
|
| 28 |
+
* @typedef {import('../src/types.d.ts').ColumnChunk} ColumnChunk
|
| 29 |
+
* @typedef {import('../src/types.d.ts').ColumnMetaData} ColumnMetaData
|
| 30 |
+
* @typedef {import('../src/types.d.ts').Encoding} Encoding
|
| 31 |
+
* @typedef {import('../src/types.d.ts').CompressionCodec} CompressionCodec
|
| 32 |
+
* @typedef {import('../src/types.d.ts').Compressors} Compressors
|
| 33 |
+
* @typedef {import('../src/types.d.ts').KeyValue} KeyValue
|
| 34 |
+
* @typedef {import('../src/types.d.ts').Statistics} Statistics
|
| 35 |
+
* @typedef {import('../src/types.d.ts').GeospatialStatistics} GeospatialStatistics
|
| 36 |
+
* @typedef {import('../src/types.d.ts').BoundingBox} BoundingBox
|
| 37 |
+
* @typedef {import('../src/types.d.ts').PageType} PageType
|
| 38 |
+
* @typedef {import('../src/types.d.ts').PageHeader} PageHeader
|
| 39 |
+
* @typedef {import('../src/types.d.ts').DataPageHeader} DataPageHeader
|
| 40 |
+
* @typedef {import('../src/types.d.ts').DictionaryPageHeader} DictionaryPageHeader
|
| 41 |
+
* @typedef {import('../src/types.d.ts').DecodedArray} DecodedArray
|
| 42 |
+
* @typedef {import('../src/types.d.ts').OffsetIndex} OffsetIndex
|
| 43 |
+
* @typedef {import('../src/types.d.ts').ColumnIndex} ColumnIndex
|
| 44 |
+
* @typedef {import('../src/types.d.ts').BoundaryOrder} BoundaryOrder
|
| 45 |
+
* @typedef {import('../src/types.d.ts').ColumnData} ColumnData
|
| 46 |
+
* @typedef {import('../src/types.d.ts').SubColumnData} SubColumnData
|
| 47 |
+
* @typedef {import('../src/types.d.ts').ParquetReadOptions} ParquetReadOptions
|
| 48 |
+
* @typedef {import('../src/types.d.ts').MetadataOptions} MetadataOptions
|
| 49 |
+
* @typedef {import('../src/types.d.ts').ParquetParsers} ParquetParsers
|
| 50 |
+
* @typedef {import('../src/types.d.ts').ParquetQueryFilter} ParquetQueryFilter
|
| 51 |
+
*/
|
web/node_modules/hyparquet/src/indexes.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @import {ColumnIndex, DataReader, OffsetIndex, PageLocation, ParquetParsers, SchemaElement} from '../src/types.js'
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import { BoundaryOrders } from './constants.js'
|
| 6 |
+
import { DEFAULT_PARSERS } from './convert.js'
|
| 7 |
+
import { convertMetadata } from './metadata.js'
|
| 8 |
+
import { deserializeTCompactProtocol } from './thrift.js'
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* @param {DataReader} reader
|
| 12 |
+
* @param {SchemaElement} schema
|
| 13 |
+
* @param {ParquetParsers | undefined} parsers
|
| 14 |
+
* @returns {ColumnIndex}
|
| 15 |
+
*/
|
| 16 |
+
export function readColumnIndex(reader, schema, parsers = undefined) {
|
| 17 |
+
parsers = { ...DEFAULT_PARSERS, ...parsers }
|
| 18 |
+
|
| 19 |
+
const thrift = deserializeTCompactProtocol(reader)
|
| 20 |
+
return {
|
| 21 |
+
null_pages: thrift.field_1,
|
| 22 |
+
min_values: thrift.field_2.map((/** @type {any} */ m) => convertMetadata(m, schema, parsers)),
|
| 23 |
+
max_values: thrift.field_3.map((/** @type {any} */ m) => convertMetadata(m, schema, parsers)),
|
| 24 |
+
boundary_order: BoundaryOrders[thrift.field_4],
|
| 25 |
+
null_counts: thrift.field_5,
|
| 26 |
+
repetition_level_histograms: thrift.field_6,
|
| 27 |
+
definition_level_histograms: thrift.field_7,
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* @param {DataReader} reader
|
| 33 |
+
* @returns {OffsetIndex}
|
| 34 |
+
*/
|
| 35 |
+
export function readOffsetIndex(reader) {
|
| 36 |
+
const thrift = deserializeTCompactProtocol(reader)
|
| 37 |
+
return {
|
| 38 |
+
// @ts-ignore
|
| 39 |
+
page_locations: thrift.field_1.map(loc => ({
|
| 40 |
+
offset: loc.field_1,
|
| 41 |
+
compressed_page_size: loc.field_2,
|
| 42 |
+
first_row_index: loc.field_3,
|
| 43 |
+
})),
|
| 44 |
+
unencoded_byte_array_data_bytes: thrift.field_2,
|
| 45 |
+
}
|
| 46 |
+
}
|
web/node_modules/hyparquet/src/metadata.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { CompressionCodecs, ConvertedTypes, EdgeInterpolationAlgorithms, Encodings, FieldRepetitionTypes, PageTypes, ParquetTypes } from './constants.js'
|
| 2 |
+
import { DEFAULT_PARSERS, parseDecimal, parseFloat16 } from './convert.js'
|
| 3 |
+
import { getSchemaPath } from './schema.js'
|
| 4 |
+
import { deserializeTCompactProtocol } from './thrift.js'
|
| 5 |
+
import { markGeoColumns } from './geoparquet.js'
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* @import {AsyncBuffer, FileMetaData, KeyValue, LogicalType, MetadataOptions, MinMaxType, ParquetParsers, SchemaElement, SchemaTree, Statistics, TimeUnit} from '../src/types.js'
|
| 9 |
+
*/
|
| 10 |
+
|
| 11 |
+
export const defaultInitialFetchSize = 1 << 19 // 512kb
|
| 12 |
+
|
| 13 |
+
const decoder = new TextDecoder()
|
| 14 |
+
function decode(/** @type {Uint8Array} */ value) {
|
| 15 |
+
return value && decoder.decode(value)
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
/**
|
| 19 |
+
* Read parquet metadata from an async buffer.
|
| 20 |
+
*
|
| 21 |
+
* An AsyncBuffer is like an ArrayBuffer, but the slices are loaded
|
| 22 |
+
* asynchronously, possibly over the network.
|
| 23 |
+
*
|
| 24 |
+
* You must provide the byteLength of the buffer, typically from a HEAD request.
|
| 25 |
+
*
|
| 26 |
+
* In theory, you could use suffix-range requests to fetch the end of the file,
|
| 27 |
+
* and save a round trip. But in practice, this doesn't work because chrome
|
| 28 |
+
* deems suffix-range requests as a not-safe-listed header, and will require
|
| 29 |
+
* a pre-flight. So the byteLength is required.
|
| 30 |
+
*
|
| 31 |
+
* To make this efficient, we initially request the last 512kb of the file,
|
| 32 |
+
* which is likely to contain the metadata. If the metadata length exceeds the
|
| 33 |
+
* initial fetch, 512kb, we request the rest of the metadata from the AsyncBuffer.
|
| 34 |
+
*
|
| 35 |
+
* This ensures that we either make one 512kb initial request for the metadata,
|
| 36 |
+
* or a second request for up to the metadata size.
|
| 37 |
+
*
|
| 38 |
+
* @param {AsyncBuffer} asyncBuffer parquet file contents
|
| 39 |
+
* @param {MetadataOptions & { initialFetchSize?: number }} options initial fetch size in bytes (default 512kb)
|
| 40 |
+
* @returns {Promise<FileMetaData>} parquet metadata object
|
| 41 |
+
*/
|
| 42 |
+
export async function parquetMetadataAsync(asyncBuffer, { parsers, initialFetchSize = defaultInitialFetchSize, geoparquet = true } = {}) {
|
| 43 |
+
if (!asyncBuffer || !(asyncBuffer.byteLength >= 0)) throw new Error('parquet expected AsyncBuffer')
|
| 44 |
+
|
| 45 |
+
// fetch last bytes (footer) of the file
|
| 46 |
+
const footerOffset = Math.max(0, asyncBuffer.byteLength - initialFetchSize)
|
| 47 |
+
const footerBuffer = await asyncBuffer.slice(footerOffset, asyncBuffer.byteLength)
|
| 48 |
+
|
| 49 |
+
// Check for parquet magic number "PAR1"
|
| 50 |
+
const footerView = new DataView(footerBuffer)
|
| 51 |
+
if (footerView.getUint32(footerBuffer.byteLength - 4, true) !== 0x31524150) {
|
| 52 |
+
throw new Error('parquet file invalid (footer != PAR1)')
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Parquet files store metadata at the end of the file
|
| 56 |
+
// Metadata length is 4 bytes before the last PAR1
|
| 57 |
+
const metadataLength = footerView.getUint32(footerBuffer.byteLength - 8, true)
|
| 58 |
+
if (metadataLength > asyncBuffer.byteLength - 8) {
|
| 59 |
+
throw new Error(`parquet metadata length ${metadataLength} exceeds available buffer ${asyncBuffer.byteLength - 8}`)
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// check if metadata size fits inside the initial fetch
|
| 63 |
+
if (metadataLength + 8 > initialFetchSize) {
|
| 64 |
+
// fetch the rest of the metadata
|
| 65 |
+
const metadataOffset = asyncBuffer.byteLength - metadataLength - 8
|
| 66 |
+
const metadataBuffer = await asyncBuffer.slice(metadataOffset, footerOffset)
|
| 67 |
+
// combine initial fetch with the new slice
|
| 68 |
+
const combinedBuffer = new ArrayBuffer(metadataLength + 8)
|
| 69 |
+
const combinedView = new Uint8Array(combinedBuffer)
|
| 70 |
+
combinedView.set(new Uint8Array(metadataBuffer))
|
| 71 |
+
combinedView.set(new Uint8Array(footerBuffer), footerOffset - metadataOffset)
|
| 72 |
+
return parquetMetadata(combinedBuffer, { parsers, geoparquet })
|
| 73 |
+
} else {
|
| 74 |
+
// parse metadata from the footer
|
| 75 |
+
return parquetMetadata(footerBuffer, { parsers, geoparquet })
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* Read parquet metadata from a buffer synchronously.
|
| 81 |
+
*
|
| 82 |
+
* @param {ArrayBuffer} arrayBuffer parquet file footer
|
| 83 |
+
* @param {MetadataOptions} options metadata parsing options
|
| 84 |
+
* @returns {FileMetaData} parquet metadata object
|
| 85 |
+
*/
|
| 86 |
+
export function parquetMetadata(arrayBuffer, { parsers, geoparquet = true } = {}) {
|
| 87 |
+
if (!(arrayBuffer instanceof ArrayBuffer)) throw new Error('parquet expected ArrayBuffer')
|
| 88 |
+
const view = new DataView(arrayBuffer)
|
| 89 |
+
|
| 90 |
+
// Use default parsers if not given
|
| 91 |
+
parsers = { ...DEFAULT_PARSERS, ...parsers }
|
| 92 |
+
|
| 93 |
+
// Validate footer magic number "PAR1"
|
| 94 |
+
if (view.byteLength < 8) {
|
| 95 |
+
throw new Error('parquet file is too short')
|
| 96 |
+
}
|
| 97 |
+
if (view.getUint32(view.byteLength - 4, true) !== 0x31524150) {
|
| 98 |
+
throw new Error('parquet file invalid (footer != PAR1)')
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// Parquet files store metadata at the end of the file
|
| 102 |
+
// Metadata length is 4 bytes before the last PAR1
|
| 103 |
+
const metadataLengthOffset = view.byteLength - 8
|
| 104 |
+
const metadataLength = view.getUint32(metadataLengthOffset, true)
|
| 105 |
+
if (metadataLength > view.byteLength - 8) {
|
| 106 |
+
// {metadata}, metadata_length, PAR1
|
| 107 |
+
throw new Error(`parquet metadata length ${metadataLength} exceeds available buffer ${view.byteLength - 8}`)
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
const metadataOffset = metadataLengthOffset - metadataLength
|
| 111 |
+
const reader = { view, offset: metadataOffset }
|
| 112 |
+
const metadata = deserializeTCompactProtocol(reader)
|
| 113 |
+
|
| 114 |
+
// Parse metadata from thrift data
|
| 115 |
+
const version = metadata.field_1
|
| 116 |
+
/** @type {SchemaElement[]} */
|
| 117 |
+
const schema = metadata.field_2.map((/** @type {any} */ field) => ({
|
| 118 |
+
type: ParquetTypes[field.field_1],
|
| 119 |
+
type_length: field.field_2,
|
| 120 |
+
repetition_type: FieldRepetitionTypes[field.field_3],
|
| 121 |
+
name: decode(field.field_4),
|
| 122 |
+
num_children: field.field_5,
|
| 123 |
+
converted_type: ConvertedTypes[field.field_6],
|
| 124 |
+
scale: field.field_7,
|
| 125 |
+
precision: field.field_8,
|
| 126 |
+
field_id: field.field_9,
|
| 127 |
+
logical_type: logicalType(field.field_10),
|
| 128 |
+
}))
|
| 129 |
+
// schema element per column index
|
| 130 |
+
const columnSchema = schema.filter(e => e.type)
|
| 131 |
+
const num_rows = metadata.field_3
|
| 132 |
+
const row_groups = metadata.field_4.map((/** @type {any} */ rowGroup) => ({
|
| 133 |
+
columns: rowGroup.field_1.map((/** @type {any} */ column, /** @type {number} */ columnIndex) => ({
|
| 134 |
+
file_path: decode(column.field_1),
|
| 135 |
+
file_offset: column.field_2,
|
| 136 |
+
meta_data: column.field_3 && {
|
| 137 |
+
type: ParquetTypes[column.field_3.field_1],
|
| 138 |
+
encodings: column.field_3.field_2?.map((/** @type {number} */ e) => Encodings[e]),
|
| 139 |
+
path_in_schema: column.field_3.field_3.map(decode),
|
| 140 |
+
codec: CompressionCodecs[column.field_3.field_4],
|
| 141 |
+
num_values: column.field_3.field_5,
|
| 142 |
+
total_uncompressed_size: column.field_3.field_6,
|
| 143 |
+
total_compressed_size: column.field_3.field_7,
|
| 144 |
+
key_value_metadata: column.field_3.field_8?.map((/** @type {any} */ kv) => ({
|
| 145 |
+
key: decode(kv.field_1),
|
| 146 |
+
value: decode(kv.field_2),
|
| 147 |
+
})),
|
| 148 |
+
data_page_offset: column.field_3.field_9,
|
| 149 |
+
index_page_offset: column.field_3.field_10,
|
| 150 |
+
dictionary_page_offset: column.field_3.field_11,
|
| 151 |
+
statistics: convertStats(column.field_3.field_12, columnSchema[columnIndex], parsers),
|
| 152 |
+
encoding_stats: column.field_3.field_13?.map((/** @type {any} */ encodingStat) => ({
|
| 153 |
+
page_type: PageTypes[encodingStat.field_1],
|
| 154 |
+
encoding: Encodings[encodingStat.field_2],
|
| 155 |
+
count: encodingStat.field_3,
|
| 156 |
+
})),
|
| 157 |
+
bloom_filter_offset: column.field_3.field_14,
|
| 158 |
+
bloom_filter_length: column.field_3.field_15,
|
| 159 |
+
size_statistics: column.field_3.field_16 && {
|
| 160 |
+
unencoded_byte_array_data_bytes: column.field_3.field_16.field_1,
|
| 161 |
+
repetition_level_histogram: column.field_3.field_16.field_2,
|
| 162 |
+
definition_level_histogram: column.field_3.field_16.field_3,
|
| 163 |
+
},
|
| 164 |
+
geospatial_statistics: column.field_3.field_17 && {
|
| 165 |
+
bbox: column.field_3.field_17.field_1 && {
|
| 166 |
+
xmin: column.field_3.field_17.field_1.field_1,
|
| 167 |
+
xmax: column.field_3.field_17.field_1.field_2,
|
| 168 |
+
ymin: column.field_3.field_17.field_1.field_3,
|
| 169 |
+
ymax: column.field_3.field_17.field_1.field_4,
|
| 170 |
+
zmin: column.field_3.field_17.field_1.field_5,
|
| 171 |
+
zmax: column.field_3.field_17.field_1.field_6,
|
| 172 |
+
mmin: column.field_3.field_17.field_1.field_7,
|
| 173 |
+
mmax: column.field_3.field_17.field_1.field_8,
|
| 174 |
+
},
|
| 175 |
+
geospatial_types: column.field_3.field_17.field_2,
|
| 176 |
+
},
|
| 177 |
+
},
|
| 178 |
+
offset_index_offset: column.field_4,
|
| 179 |
+
offset_index_length: column.field_5,
|
| 180 |
+
column_index_offset: column.field_6,
|
| 181 |
+
column_index_length: column.field_7,
|
| 182 |
+
crypto_metadata: column.field_8,
|
| 183 |
+
encrypted_column_metadata: column.field_9,
|
| 184 |
+
})),
|
| 185 |
+
total_byte_size: rowGroup.field_2,
|
| 186 |
+
num_rows: rowGroup.field_3,
|
| 187 |
+
sorting_columns: rowGroup.field_4?.map((/** @type {any} */ sortingColumn) => ({
|
| 188 |
+
column_idx: sortingColumn.field_1,
|
| 189 |
+
descending: sortingColumn.field_2,
|
| 190 |
+
nulls_first: sortingColumn.field_3,
|
| 191 |
+
})),
|
| 192 |
+
file_offset: rowGroup.field_5,
|
| 193 |
+
total_compressed_size: rowGroup.field_6,
|
| 194 |
+
ordinal: rowGroup.field_7,
|
| 195 |
+
}))
|
| 196 |
+
/** @type {KeyValue[] | undefined} */
|
| 197 |
+
const key_value_metadata = metadata.field_5?.map((/** @type {any} */ kv) => ({
|
| 198 |
+
key: decode(kv.field_1),
|
| 199 |
+
value: decode(kv.field_2),
|
| 200 |
+
}))
|
| 201 |
+
const created_by = decode(metadata.field_6)
|
| 202 |
+
|
| 203 |
+
if (geoparquet) {
|
| 204 |
+
markGeoColumns(schema, key_value_metadata)
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
return {
|
| 208 |
+
version,
|
| 209 |
+
schema,
|
| 210 |
+
num_rows,
|
| 211 |
+
row_groups,
|
| 212 |
+
key_value_metadata,
|
| 213 |
+
created_by,
|
| 214 |
+
metadata_length: metadataLength,
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
/**
|
| 219 |
+
* Return a tree of schema elements from parquet metadata.
|
| 220 |
+
*
|
| 221 |
+
* @param {{schema: SchemaElement[]}} metadata parquet metadata object
|
| 222 |
+
* @returns {SchemaTree} tree of schema elements
|
| 223 |
+
*/
|
| 224 |
+
export function parquetSchema({ schema }) {
|
| 225 |
+
return getSchemaPath(schema, [])[0]
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
/**
|
| 229 |
+
* @param {any} logicalType
|
| 230 |
+
* @returns {LogicalType | undefined}
|
| 231 |
+
*/
|
| 232 |
+
function logicalType(logicalType) {
|
| 233 |
+
if (logicalType?.field_1) return { type: 'STRING' }
|
| 234 |
+
if (logicalType?.field_2) return { type: 'MAP' }
|
| 235 |
+
if (logicalType?.field_3) return { type: 'LIST' }
|
| 236 |
+
if (logicalType?.field_4) return { type: 'ENUM' }
|
| 237 |
+
if (logicalType?.field_5) return {
|
| 238 |
+
type: 'DECIMAL',
|
| 239 |
+
scale: logicalType.field_5.field_1,
|
| 240 |
+
precision: logicalType.field_5.field_2,
|
| 241 |
+
}
|
| 242 |
+
if (logicalType?.field_6) return { type: 'DATE' }
|
| 243 |
+
if (logicalType?.field_7) return {
|
| 244 |
+
type: 'TIME',
|
| 245 |
+
isAdjustedToUTC: logicalType.field_7.field_1,
|
| 246 |
+
unit: timeUnit(logicalType.field_7.field_2),
|
| 247 |
+
}
|
| 248 |
+
if (logicalType?.field_8) return {
|
| 249 |
+
type: 'TIMESTAMP',
|
| 250 |
+
isAdjustedToUTC: logicalType.field_8.field_1,
|
| 251 |
+
unit: timeUnit(logicalType.field_8.field_2),
|
| 252 |
+
}
|
| 253 |
+
if (logicalType?.field_10) return {
|
| 254 |
+
type: 'INTEGER',
|
| 255 |
+
bitWidth: logicalType.field_10.field_1,
|
| 256 |
+
isSigned: logicalType.field_10.field_2,
|
| 257 |
+
}
|
| 258 |
+
if (logicalType?.field_11) return { type: 'NULL' }
|
| 259 |
+
if (logicalType?.field_12) return { type: 'JSON' }
|
| 260 |
+
if (logicalType?.field_13) return { type: 'BSON' }
|
| 261 |
+
if (logicalType?.field_14) return { type: 'UUID' }
|
| 262 |
+
if (logicalType?.field_15) return { type: 'FLOAT16' }
|
| 263 |
+
if (logicalType?.field_16) return {
|
| 264 |
+
type: 'VARIANT',
|
| 265 |
+
specification_version: logicalType.field_16.field_1,
|
| 266 |
+
}
|
| 267 |
+
if (logicalType?.field_17) return {
|
| 268 |
+
type: 'GEOMETRY',
|
| 269 |
+
crs: decode(logicalType.field_17.field_1),
|
| 270 |
+
}
|
| 271 |
+
if (logicalType?.field_18) return {
|
| 272 |
+
type: 'GEOGRAPHY',
|
| 273 |
+
crs: decode(logicalType.field_18.field_1),
|
| 274 |
+
algorithm: EdgeInterpolationAlgorithms[logicalType.field_18.field_2],
|
| 275 |
+
}
|
| 276 |
+
return logicalType
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
/**
|
| 280 |
+
* @param {any} unit
|
| 281 |
+
* @returns {TimeUnit}
|
| 282 |
+
*/
|
| 283 |
+
function timeUnit(unit) {
|
| 284 |
+
if (unit.field_1) return 'MILLIS'
|
| 285 |
+
if (unit.field_2) return 'MICROS'
|
| 286 |
+
if (unit.field_3) return 'NANOS'
|
| 287 |
+
throw new Error('parquet time unit required')
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
/**
|
| 291 |
+
* Convert column statistics based on column type.
|
| 292 |
+
*
|
| 293 |
+
* @param {any} stats
|
| 294 |
+
* @param {SchemaElement} schema
|
| 295 |
+
* @param {ParquetParsers} parsers
|
| 296 |
+
* @returns {Statistics}
|
| 297 |
+
*/
|
| 298 |
+
function convertStats(stats, schema, parsers) {
|
| 299 |
+
return stats && {
|
| 300 |
+
max: convertMetadata(stats.field_1, schema, parsers),
|
| 301 |
+
min: convertMetadata(stats.field_2, schema, parsers),
|
| 302 |
+
null_count: stats.field_3,
|
| 303 |
+
distinct_count: stats.field_4,
|
| 304 |
+
max_value: convertMetadata(stats.field_5, schema, parsers),
|
| 305 |
+
min_value: convertMetadata(stats.field_6, schema, parsers),
|
| 306 |
+
is_max_value_exact: stats.field_7,
|
| 307 |
+
is_min_value_exact: stats.field_8,
|
| 308 |
+
}
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
/**
|
| 312 |
+
* @param {Uint8Array | undefined} value
|
| 313 |
+
* @param {SchemaElement} schema
|
| 314 |
+
* @param {ParquetParsers} parsers
|
| 315 |
+
* @returns {MinMaxType | undefined}
|
| 316 |
+
*/
|
| 317 |
+
export function convertMetadata(value, schema, parsers) {
|
| 318 |
+
const { type, converted_type, logical_type } = schema
|
| 319 |
+
if (value === undefined) return value
|
| 320 |
+
if (type === 'BOOLEAN') return value[0] === 1
|
| 321 |
+
if (type === 'BYTE_ARRAY') return parsers.stringFromBytes(value)
|
| 322 |
+
const view = new DataView(value.buffer, value.byteOffset, value.byteLength)
|
| 323 |
+
if (type === 'FLOAT' && view.byteLength === 4) return view.getFloat32(0, true)
|
| 324 |
+
if (type === 'DOUBLE' && view.byteLength === 8) return view.getFloat64(0, true)
|
| 325 |
+
if (type === 'INT32' && converted_type === 'DATE') return parsers.dateFromDays(view.getInt32(0, true))
|
| 326 |
+
if (type === 'INT64' && converted_type === 'TIMESTAMP_MILLIS') return parsers.timestampFromMilliseconds(view.getBigInt64(0, true))
|
| 327 |
+
if (type === 'INT64' && converted_type === 'TIMESTAMP_MICROS') return parsers.timestampFromMicroseconds(view.getBigInt64(0, true))
|
| 328 |
+
if (type === 'INT64' && logical_type?.type === 'TIMESTAMP' && logical_type?.unit === 'NANOS') return parsers.timestampFromNanoseconds(view.getBigInt64(0, true))
|
| 329 |
+
if (type === 'INT64' && logical_type?.type === 'TIMESTAMP' && logical_type?.unit === 'MICROS') return parsers.timestampFromMicroseconds(view.getBigInt64(0, true))
|
| 330 |
+
if (type === 'INT64' && logical_type?.type === 'TIMESTAMP') return parsers.timestampFromMilliseconds(view.getBigInt64(0, true))
|
| 331 |
+
if (type === 'INT32' && view.byteLength === 4) return view.getInt32(0, true)
|
| 332 |
+
if (type === 'INT64' && view.byteLength === 8) return view.getBigInt64(0, true)
|
| 333 |
+
if (converted_type === 'DECIMAL') return parseDecimal(value) * 10 ** -(schema.scale || 0)
|
| 334 |
+
if (logical_type?.type === 'FLOAT16') return parseFloat16(value)
|
| 335 |
+
if (logical_type?.type === 'UUID') return parsers.uuidFromBytes(value)
|
| 336 |
+
if (type === 'FIXED_LEN_BYTE_ARRAY') return value
|
| 337 |
+
// assert(false)
|
| 338 |
+
return value
|
| 339 |
+
}
|
web/node_modules/hyparquet/src/node.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @import {AsyncBuffer} from '../src/types.js'
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import { createReadStream, promises as fs } from 'fs'
|
| 6 |
+
|
| 7 |
+
export * from './index.js'
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* Construct an AsyncBuffer for a local file using node fs package.
|
| 11 |
+
*
|
| 12 |
+
* @param {string} filename
|
| 13 |
+
* @returns {Promise<AsyncBuffer>}
|
| 14 |
+
*/
|
| 15 |
+
export async function asyncBufferFromFile(filename) {
|
| 16 |
+
const { size } = await fs.stat(filename)
|
| 17 |
+
return {
|
| 18 |
+
byteLength: size,
|
| 19 |
+
slice(start, end) {
|
| 20 |
+
// read file slice
|
| 21 |
+
const reader = createReadStream(filename, { start, end })
|
| 22 |
+
return new Promise((resolve, reject) => {
|
| 23 |
+
/** @type {any[]} */
|
| 24 |
+
const chunks = []
|
| 25 |
+
reader.on('data', chunk => chunks.push(chunk))
|
| 26 |
+
reader.on('error', reject)
|
| 27 |
+
reader.on('end', () => {
|
| 28 |
+
const buffer = Buffer.concat(chunks)
|
| 29 |
+
resolve(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength))
|
| 30 |
+
})
|
| 31 |
+
})
|
| 32 |
+
},
|
| 33 |
+
}
|
| 34 |
+
}
|
web/node_modules/hyparquet/src/plain.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Read `count` values of the given type from the reader.view.
|
| 3 |
+
*
|
| 4 |
+
* @param {DataReader} reader - buffer to read data from
|
| 5 |
+
* @param {ParquetType} type - parquet type of the data
|
| 6 |
+
* @param {number} count - number of values to read
|
| 7 |
+
* @param {number | undefined} fixedLength - length of each fixed length byte array
|
| 8 |
+
* @returns {DecodedArray} array of values
|
| 9 |
+
*/
|
| 10 |
+
export function readPlain(reader, type, count, fixedLength) {
|
| 11 |
+
if (count === 0) return []
|
| 12 |
+
if (type === 'BOOLEAN') {
|
| 13 |
+
return readPlainBoolean(reader, count)
|
| 14 |
+
} else if (type === 'INT32') {
|
| 15 |
+
return readPlainInt32(reader, count)
|
| 16 |
+
} else if (type === 'INT64') {
|
| 17 |
+
return readPlainInt64(reader, count)
|
| 18 |
+
} else if (type === 'INT96') {
|
| 19 |
+
return readPlainInt96(reader, count)
|
| 20 |
+
} else if (type === 'FLOAT') {
|
| 21 |
+
return readPlainFloat(reader, count)
|
| 22 |
+
} else if (type === 'DOUBLE') {
|
| 23 |
+
return readPlainDouble(reader, count)
|
| 24 |
+
} else if (type === 'BYTE_ARRAY') {
|
| 25 |
+
return readPlainByteArray(reader, count)
|
| 26 |
+
} else if (type === 'FIXED_LEN_BYTE_ARRAY') {
|
| 27 |
+
if (!fixedLength) throw new Error('parquet missing fixed length')
|
| 28 |
+
return readPlainByteArrayFixed(reader, count, fixedLength)
|
| 29 |
+
} else {
|
| 30 |
+
throw new Error(`parquet unhandled type: ${type}`)
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/**
|
| 35 |
+
* Read `count` boolean values.
|
| 36 |
+
*
|
| 37 |
+
* @param {DataReader} reader
|
| 38 |
+
* @param {number} count
|
| 39 |
+
* @returns {boolean[]}
|
| 40 |
+
*/
|
| 41 |
+
function readPlainBoolean(reader, count) {
|
| 42 |
+
const values = new Array(count)
|
| 43 |
+
for (let i = 0; i < count; i++) {
|
| 44 |
+
const byteOffset = reader.offset + (i / 8 | 0)
|
| 45 |
+
const bitOffset = i % 8
|
| 46 |
+
const byte = reader.view.getUint8(byteOffset)
|
| 47 |
+
values[i] = (byte & 1 << bitOffset) !== 0
|
| 48 |
+
}
|
| 49 |
+
reader.offset += Math.ceil(count / 8)
|
| 50 |
+
return values
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/**
|
| 54 |
+
* Read `count` int32 values.
|
| 55 |
+
*
|
| 56 |
+
* @param {DataReader} reader
|
| 57 |
+
* @param {number} count
|
| 58 |
+
* @returns {Int32Array}
|
| 59 |
+
*/
|
| 60 |
+
function readPlainInt32(reader, count) {
|
| 61 |
+
const values = (reader.view.byteOffset + reader.offset) % 4
|
| 62 |
+
? new Int32Array(align(reader.view.buffer, reader.view.byteOffset + reader.offset, count * 4))
|
| 63 |
+
: new Int32Array(reader.view.buffer, reader.view.byteOffset + reader.offset, count)
|
| 64 |
+
reader.offset += count * 4
|
| 65 |
+
return values
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
/**
|
| 69 |
+
* Read `count` int64 values.
|
| 70 |
+
*
|
| 71 |
+
* @param {DataReader} reader
|
| 72 |
+
* @param {number} count
|
| 73 |
+
* @returns {BigInt64Array}
|
| 74 |
+
*/
|
| 75 |
+
function readPlainInt64(reader, count) {
|
| 76 |
+
const values = (reader.view.byteOffset + reader.offset) % 8
|
| 77 |
+
? new BigInt64Array(align(reader.view.buffer, reader.view.byteOffset + reader.offset, count * 8))
|
| 78 |
+
: new BigInt64Array(reader.view.buffer, reader.view.byteOffset + reader.offset, count)
|
| 79 |
+
reader.offset += count * 8
|
| 80 |
+
return values
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
/**
|
| 84 |
+
* Read `count` int96 values.
|
| 85 |
+
*
|
| 86 |
+
* @param {DataReader} reader
|
| 87 |
+
* @param {number} count
|
| 88 |
+
* @returns {bigint[]}
|
| 89 |
+
*/
|
| 90 |
+
function readPlainInt96(reader, count) {
|
| 91 |
+
const values = new Array(count)
|
| 92 |
+
for (let i = 0; i < count; i++) {
|
| 93 |
+
const low = reader.view.getBigInt64(reader.offset + i * 12, true)
|
| 94 |
+
const high = reader.view.getInt32(reader.offset + i * 12 + 8, true)
|
| 95 |
+
values[i] = BigInt(high) << 64n | low
|
| 96 |
+
}
|
| 97 |
+
reader.offset += count * 12
|
| 98 |
+
return values
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/**
|
| 102 |
+
* Read `count` float values.
|
| 103 |
+
*
|
| 104 |
+
* @param {DataReader} reader
|
| 105 |
+
* @param {number} count
|
| 106 |
+
* @returns {Float32Array}
|
| 107 |
+
*/
|
| 108 |
+
function readPlainFloat(reader, count) {
|
| 109 |
+
const values = (reader.view.byteOffset + reader.offset) % 4
|
| 110 |
+
? new Float32Array(align(reader.view.buffer, reader.view.byteOffset + reader.offset, count * 4))
|
| 111 |
+
: new Float32Array(reader.view.buffer, reader.view.byteOffset + reader.offset, count)
|
| 112 |
+
reader.offset += count * 4
|
| 113 |
+
return values
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/**
|
| 117 |
+
* Read `count` double values.
|
| 118 |
+
*
|
| 119 |
+
* @param {DataReader} reader
|
| 120 |
+
* @param {number} count
|
| 121 |
+
* @returns {Float64Array}
|
| 122 |
+
*/
|
| 123 |
+
function readPlainDouble(reader, count) {
|
| 124 |
+
const values = (reader.view.byteOffset + reader.offset) % 8
|
| 125 |
+
? new Float64Array(align(reader.view.buffer, reader.view.byteOffset + reader.offset, count * 8))
|
| 126 |
+
: new Float64Array(reader.view.buffer, reader.view.byteOffset + reader.offset, count)
|
| 127 |
+
reader.offset += count * 8
|
| 128 |
+
return values
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
/**
|
| 132 |
+
* Read `count` byte array values.
|
| 133 |
+
*
|
| 134 |
+
* @param {DataReader} reader
|
| 135 |
+
* @param {number} count
|
| 136 |
+
* @returns {Uint8Array[]}
|
| 137 |
+
*/
|
| 138 |
+
function readPlainByteArray(reader, count) {
|
| 139 |
+
const values = new Array(count)
|
| 140 |
+
for (let i = 0; i < count; i++) {
|
| 141 |
+
const length = reader.view.getUint32(reader.offset, true)
|
| 142 |
+
reader.offset += 4
|
| 143 |
+
values[i] = new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, length)
|
| 144 |
+
reader.offset += length
|
| 145 |
+
}
|
| 146 |
+
return values
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
/**
|
| 150 |
+
* Read a fixed length byte array.
|
| 151 |
+
*
|
| 152 |
+
* @param {DataReader} reader
|
| 153 |
+
* @param {number} count
|
| 154 |
+
* @param {number} fixedLength
|
| 155 |
+
* @returns {Uint8Array[]}
|
| 156 |
+
*/
|
| 157 |
+
function readPlainByteArrayFixed(reader, count, fixedLength) {
|
| 158 |
+
// assert(reader.view.byteLength - reader.offset >= count * fixedLength)
|
| 159 |
+
const values = new Array(count)
|
| 160 |
+
for (let i = 0; i < count; i++) {
|
| 161 |
+
values[i] = new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, fixedLength)
|
| 162 |
+
reader.offset += fixedLength
|
| 163 |
+
}
|
| 164 |
+
return values
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
/**
|
| 168 |
+
* Create a new buffer with the offset and size.
|
| 169 |
+
*
|
| 170 |
+
* @import {DataReader, DecodedArray, ParquetType} from '../src/types.js'
|
| 171 |
+
* @param {ArrayBufferLike} buffer
|
| 172 |
+
* @param {number} offset
|
| 173 |
+
* @param {number} size
|
| 174 |
+
* @returns {ArrayBuffer}
|
| 175 |
+
*/
|
| 176 |
+
function align(buffer, offset, size) {
|
| 177 |
+
const aligned = new ArrayBuffer(size)
|
| 178 |
+
new Uint8Array(aligned).set(new Uint8Array(buffer, offset, size))
|
| 179 |
+
return aligned
|
| 180 |
+
}
|
web/node_modules/hyparquet/src/plan.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { bloomEligibleColumns, readBloomFilter } from './bloom.js'
|
| 2 |
+
import { canSkipRowGroup } from './filter.js'
|
| 3 |
+
import { parquetSchema } from './metadata.js'
|
| 4 |
+
import { getPhysicalColumns } from './schema.js'
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* @import {AsyncBuffer, BloomFilter, ByteRange, ChunkPlan, FileMetaData, GroupPlan, ParquetQueryFilter, ParquetReadOptions, QueryPlan, SchemaElement} from '../src/types.js'
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
// Combine column chunks if less than 2mb
|
| 11 |
+
const runLimit = 1 << 21 // 2mb
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Plan which byte ranges to read to satisfy a read request.
|
| 15 |
+
* Metadata must be non-null.
|
| 16 |
+
*
|
| 17 |
+
* @param {ParquetReadOptions & { bloomFiltersByGroup?: Record<string, BloomFilter>[], schemaElements?: Record<string, SchemaElement> }} options
|
| 18 |
+
* @returns {QueryPlan}
|
| 19 |
+
*/
|
| 20 |
+
export function parquetPlan({ metadata, rowStart = 0, rowEnd = Infinity, columns, filter, filterStrict = true, useOffsetIndex = false, bloomFiltersByGroup, schemaElements }) {
|
| 21 |
+
if (!metadata) throw new Error('parquetPlan requires metadata')
|
| 22 |
+
/** @type {GroupPlan[]} */
|
| 23 |
+
const groups = []
|
| 24 |
+
/** @type {ByteRange[]} */
|
| 25 |
+
const fetches = []
|
| 26 |
+
/** @type {ByteRange[]} */
|
| 27 |
+
const indexes = []
|
| 28 |
+
const physicalColumns = getPhysicalColumns(parquetSchema(metadata))
|
| 29 |
+
|
| 30 |
+
// find which row groups to read
|
| 31 |
+
let groupStart = 0 // first row index of the current group
|
| 32 |
+
let rgIdx = 0
|
| 33 |
+
for (const rowGroup of metadata.row_groups) {
|
| 34 |
+
const groupRows = Number(rowGroup.num_rows)
|
| 35 |
+
const groupEnd = groupStart + groupRows
|
| 36 |
+
const bloomFilters = bloomFiltersByGroup?.[rgIdx]
|
| 37 |
+
// if row group overlaps with row range, add it to the plan
|
| 38 |
+
if (groupRows > 0 && groupEnd > rowStart && groupStart < rowEnd && !canSkipRowGroup({ rowGroup, physicalColumns, filter, strict: filterStrict, bloomFilters, schemaElements })) {
|
| 39 |
+
/** @type {ChunkPlan[]} */
|
| 40 |
+
const chunks = []
|
| 41 |
+
let groupStartByte = Infinity
|
| 42 |
+
let groupEndByte = -Infinity
|
| 43 |
+
// loop through each column chunk
|
| 44 |
+
for (const chunk of rowGroup.columns) {
|
| 45 |
+
const meta = chunk.meta_data
|
| 46 |
+
if (chunk.file_path) throw new Error('parquet file_path not supported')
|
| 47 |
+
if (!meta) throw new Error('parquet column metadata is undefined')
|
| 48 |
+
// add included column chunks to the plan
|
| 49 |
+
if (!columns || columns.includes(meta.path_in_schema[0])) {
|
| 50 |
+
// full column chunk
|
| 51 |
+
const columnOffset = meta.dictionary_page_offset || meta.data_page_offset
|
| 52 |
+
const startByte = Number(columnOffset)
|
| 53 |
+
const endByte = Number(columnOffset + meta.total_compressed_size)
|
| 54 |
+
// update group byte range
|
| 55 |
+
if (startByte < groupStartByte) groupStartByte = startByte
|
| 56 |
+
if (endByte > groupEndByte) groupEndByte = endByte
|
| 57 |
+
|
| 58 |
+
if (useOffsetIndex && chunk.offset_index_offset && chunk.offset_index_length && (rowStart > groupStart || rowEnd < groupEnd)) {
|
| 59 |
+
const offsetIndexStart = Number(chunk.offset_index_offset)
|
| 60 |
+
chunks.push({
|
| 61 |
+
columnMetadata: meta,
|
| 62 |
+
offsetIndex: {
|
| 63 |
+
startByte: offsetIndexStart,
|
| 64 |
+
endByte: offsetIndexStart + chunk.offset_index_length,
|
| 65 |
+
},
|
| 66 |
+
range: { startByte, endByte },
|
| 67 |
+
})
|
| 68 |
+
} else {
|
| 69 |
+
chunks.push({
|
| 70 |
+
columnMetadata: meta,
|
| 71 |
+
range: { startByte, endByte },
|
| 72 |
+
})
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
const selectStart = Math.max(rowStart - groupStart, 0)
|
| 78 |
+
const selectEnd = Math.min(rowEnd - groupStart, groupRows)
|
| 79 |
+
groups.push({ chunks, rowGroup, groupStart, groupRows, selectStart, selectEnd })
|
| 80 |
+
|
| 81 |
+
// combine runs of column chunks
|
| 82 |
+
/** @type {ByteRange | undefined} */
|
| 83 |
+
let run
|
| 84 |
+
for (const chunk of chunks) {
|
| 85 |
+
if ('offsetIndex' in chunk) {
|
| 86 |
+
indexes.push(chunk.offsetIndex)
|
| 87 |
+
} else {
|
| 88 |
+
const { range } = chunk
|
| 89 |
+
if (columns) {
|
| 90 |
+
fetches.push(range)
|
| 91 |
+
} else if (run && range.endByte - run.startByte <= runLimit) {
|
| 92 |
+
// extend range
|
| 93 |
+
run.endByte = range.endByte
|
| 94 |
+
} else {
|
| 95 |
+
// new range
|
| 96 |
+
if (run) fetches.push(run)
|
| 97 |
+
run = { ...range }
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
if (run) fetches.push(run)
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
groupStart = groupEnd
|
| 105 |
+
rgIdx++
|
| 106 |
+
}
|
| 107 |
+
if (!isFinite(rowEnd)) rowEnd = groupStart
|
| 108 |
+
fetches.push(...indexes)
|
| 109 |
+
|
| 110 |
+
return { metadata, rowStart, rowEnd, columns, fetches, groups }
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/**
|
| 114 |
+
* Fetch bloom filters for $eq / $in columns of row groups not already provably
|
| 115 |
+
* skippable by statistics alone. Returns an array indexed by row-group ordinal;
|
| 116 |
+
* each entry maps top-level column name → BloomFilter for any chunk whose
|
| 117 |
+
* bloom filter we were able to parse. Adds one round-trip when at least one
|
| 118 |
+
* bloom filter is fetched; otherwise returns synchronously.
|
| 119 |
+
*
|
| 120 |
+
* @param {object} options
|
| 121 |
+
* @param {AsyncBuffer} options.file
|
| 122 |
+
* @param {FileMetaData} options.metadata
|
| 123 |
+
* @param {ParquetQueryFilter} options.filter
|
| 124 |
+
* @param {boolean} [options.filterStrict]
|
| 125 |
+
* @returns {Promise<Record<string, BloomFilter>[]>}
|
| 126 |
+
*/
|
| 127 |
+
export async function prefetchBloomFilters({ file, metadata, filter, filterStrict = true }) {
|
| 128 |
+
const result = metadata.row_groups.map(() => /** @type {Record<string, BloomFilter>} */ ({}))
|
| 129 |
+
const eligibleCols = bloomEligibleColumns(filter)
|
| 130 |
+
if (eligibleCols.size === 0) return result
|
| 131 |
+
const physicalColumns = getPhysicalColumns(parquetSchema(metadata))
|
| 132 |
+
|
| 133 |
+
/** @type {Promise<void>[]} */
|
| 134 |
+
const tasks = []
|
| 135 |
+
metadata.row_groups.forEach((rowGroup, rgIdx) => {
|
| 136 |
+
if (canSkipRowGroup({ rowGroup, physicalColumns, filter, strict: filterStrict })) return
|
| 137 |
+
for (const colName of eligibleCols) {
|
| 138 |
+
const columnIdx = physicalColumns.indexOf(colName)
|
| 139 |
+
if (columnIdx === -1) continue
|
| 140 |
+
const meta = rowGroup.columns[columnIdx]?.meta_data
|
| 141 |
+
if (!meta?.bloom_filter_offset || !meta.bloom_filter_length) continue
|
| 142 |
+
const start = Number(meta.bloom_filter_offset)
|
| 143 |
+
const end = start + meta.bloom_filter_length
|
| 144 |
+
tasks.push((async () => {
|
| 145 |
+
const buffer = await file.slice(start, end)
|
| 146 |
+
const bloom = readBloomFilter({ view: new DataView(buffer), offset: 0 })
|
| 147 |
+
if (bloom) result[rgIdx][colName] = bloom
|
| 148 |
+
})())
|
| 149 |
+
}
|
| 150 |
+
})
|
| 151 |
+
|
| 152 |
+
if (tasks.length) await Promise.all(tasks)
|
| 153 |
+
return result
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
/**
|
| 157 |
+
* Prefetch byte ranges from an AsyncBuffer.
|
| 158 |
+
*
|
| 159 |
+
* @param {AsyncBuffer} file
|
| 160 |
+
* @param {QueryPlan} plan
|
| 161 |
+
* @returns {AsyncBuffer}
|
| 162 |
+
*/
|
| 163 |
+
export function prefetchAsyncBuffer(file, { fetches }) {
|
| 164 |
+
// fetch byte ranges from the file
|
| 165 |
+
const promises = fetches.map(({ startByte, endByte }) => file.slice(startByte, endByte))
|
| 166 |
+
return {
|
| 167 |
+
byteLength: file.byteLength,
|
| 168 |
+
slice(start, end = file.byteLength) {
|
| 169 |
+
// find matching slice
|
| 170 |
+
const index = fetches.findIndex(({ startByte, endByte }) => startByte <= start && end <= endByte)
|
| 171 |
+
if (index < 0) {
|
| 172 |
+
// fallback to direct read
|
| 173 |
+
return file.slice(start, end)
|
| 174 |
+
}
|
| 175 |
+
if (fetches[index].startByte !== start || fetches[index].endByte !== end) {
|
| 176 |
+
// slice a subrange of the prefetch
|
| 177 |
+
const startOffset = start - fetches[index].startByte
|
| 178 |
+
const endOffset = end - fetches[index].startByte
|
| 179 |
+
if (promises[index] instanceof Promise) {
|
| 180 |
+
return promises[index].then(buffer => buffer.slice(startOffset, endOffset))
|
| 181 |
+
} else {
|
| 182 |
+
return promises[index].slice(startOffset, endOffset)
|
| 183 |
+
}
|
| 184 |
+
} else {
|
| 185 |
+
return promises[index]
|
| 186 |
+
}
|
| 187 |
+
},
|
| 188 |
+
}
|
| 189 |
+
}
|
web/node_modules/hyparquet/src/query.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @import {BaseParquetReadOptions} from '../src/types.js'
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import { parquetMetadataAsync, parquetSchema } from './metadata.js'
|
| 6 |
+
import { parquetReadColumn, parquetReadObjects } from './read.js'
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Wraps parquetRead with orderBy support.
|
| 10 |
+
* This is a parquet-aware query engine that can read a subset of rows and columns.
|
| 11 |
+
* Accepts optional orderBy column name to sort the results.
|
| 12 |
+
* Note that using orderBy may SIGNIFICANTLY increase the query time.
|
| 13 |
+
*
|
| 14 |
+
* @param {BaseParquetReadOptions & { orderBy?: string }} options
|
| 15 |
+
* @returns {Promise<Record<string, any>[]>} resolves when all requested rows and columns are parsed
|
| 16 |
+
*/
|
| 17 |
+
export async function parquetQuery(options) {
|
| 18 |
+
if (!options.file || !(options.file.byteLength >= 0)) {
|
| 19 |
+
throw new Error('parquet expected AsyncBuffer')
|
| 20 |
+
}
|
| 21 |
+
options.metadata ??= await parquetMetadataAsync(options.file, options)
|
| 22 |
+
|
| 23 |
+
const { metadata, rowStart = 0, columns, orderBy, filter } = options
|
| 24 |
+
if (rowStart < 0) throw new Error('parquet rowStart must be positive')
|
| 25 |
+
const rowEnd = options.rowEnd ?? Number(metadata.num_rows)
|
| 26 |
+
|
| 27 |
+
// Validate orderBy column exists
|
| 28 |
+
if (orderBy) {
|
| 29 |
+
const allColumns = parquetSchema(options.metadata).children.map(c => c.element.name)
|
| 30 |
+
if (!allColumns.includes(orderBy)) {
|
| 31 |
+
throw new Error(`parquet orderBy column not found: ${orderBy}`)
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
if (filter && !orderBy && rowEnd < metadata.num_rows) {
|
| 36 |
+
// iterate through row groups and filter until we have enough rows
|
| 37 |
+
/** @type {Record<string, any>[]} */
|
| 38 |
+
const filteredRows = []
|
| 39 |
+
let groupStart = 0
|
| 40 |
+
for (const group of metadata.row_groups) {
|
| 41 |
+
const groupEnd = groupStart + Number(group.num_rows)
|
| 42 |
+
// TODO: if expected > group size, start fetching next groups
|
| 43 |
+
const groupData = await parquetReadObjects({
|
| 44 |
+
...options, rowStart: groupStart, rowEnd: groupEnd,
|
| 45 |
+
})
|
| 46 |
+
filteredRows.push(...groupData)
|
| 47 |
+
if (filteredRows.length >= rowEnd) break
|
| 48 |
+
groupStart = groupEnd
|
| 49 |
+
}
|
| 50 |
+
return filteredRows.slice(rowStart, rowEnd)
|
| 51 |
+
} else if (filter && orderBy) {
|
| 52 |
+
// read all rows with orderBy column included for sorting
|
| 53 |
+
const readColumns = columns && !columns.includes(orderBy)
|
| 54 |
+
? [...columns, orderBy]
|
| 55 |
+
: columns
|
| 56 |
+
|
| 57 |
+
const results = await parquetReadObjects({
|
| 58 |
+
...options, rowStart: undefined, rowEnd: undefined, columns: readColumns,
|
| 59 |
+
})
|
| 60 |
+
|
| 61 |
+
// sort by orderBy column
|
| 62 |
+
results.sort((a, b) => compare(a[orderBy], b[orderBy]))
|
| 63 |
+
|
| 64 |
+
// project out orderBy column if not originally requested
|
| 65 |
+
if (readColumns !== columns) {
|
| 66 |
+
for (const row of results) {
|
| 67 |
+
delete row[orderBy]
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
return results.slice(rowStart, rowEnd)
|
| 72 |
+
} else if (filter) {
|
| 73 |
+
// filter without orderBy, read all matching rows
|
| 74 |
+
const results = await parquetReadObjects({
|
| 75 |
+
...options, rowStart: undefined, rowEnd: undefined,
|
| 76 |
+
})
|
| 77 |
+
return results.slice(rowStart, rowEnd)
|
| 78 |
+
} else if (typeof orderBy === 'string') {
|
| 79 |
+
// sorted but unfiltered: fetch orderBy column first
|
| 80 |
+
const orderColumn = await parquetReadColumn({
|
| 81 |
+
...options, rowStart: undefined, rowEnd: undefined, columns: [orderBy],
|
| 82 |
+
})
|
| 83 |
+
|
| 84 |
+
// compute row groups to fetch
|
| 85 |
+
const sortedIndices = Array.from(orderColumn, (_, index) => index)
|
| 86 |
+
.sort((a, b) => compare(orderColumn[a], orderColumn[b]))
|
| 87 |
+
.slice(rowStart, rowEnd)
|
| 88 |
+
|
| 89 |
+
const sparseData = await parquetReadRows({ ...options, rows: sortedIndices })
|
| 90 |
+
// warning: the type Record<string, any> & {__index__: number})[] is simplified into Record<string, any>[]
|
| 91 |
+
// when returning. The data contains the __index__ property, but it's not exposed as such.
|
| 92 |
+
const data = sortedIndices.map(index => sparseData[index])
|
| 93 |
+
return data
|
| 94 |
+
} else {
|
| 95 |
+
return await parquetReadObjects(options)
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/**
|
| 100 |
+
* Reads a list rows from a parquet file, reading only the row groups that contain the rows.
|
| 101 |
+
* Returns a sparse array of rows.
|
| 102 |
+
* @param {BaseParquetReadOptions & { rows: number[] }} options
|
| 103 |
+
* @returns {Promise<(Record<string, any> & {__index__: number})[]>}
|
| 104 |
+
*/
|
| 105 |
+
async function parquetReadRows(options) {
|
| 106 |
+
const { file, rows } = options
|
| 107 |
+
options.metadata ??= await parquetMetadataAsync(file, options)
|
| 108 |
+
const { row_groups: rowGroups } = options.metadata
|
| 109 |
+
// Compute row groups to fetch
|
| 110 |
+
const groupIncluded = Array(rowGroups.length).fill(false)
|
| 111 |
+
let groupStart = 0
|
| 112 |
+
const groupEnds = rowGroups.map(group => groupStart += Number(group.num_rows))
|
| 113 |
+
for (const index of rows) {
|
| 114 |
+
const groupIndex = groupEnds.findIndex(end => index < end)
|
| 115 |
+
groupIncluded[groupIndex] = true
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// Compute row ranges to fetch
|
| 119 |
+
const rowRanges = []
|
| 120 |
+
let rangeStart
|
| 121 |
+
groupStart = 0
|
| 122 |
+
for (let i = 0; i < groupIncluded.length; i++) {
|
| 123 |
+
const groupEnd = groupStart + Number(rowGroups[i].num_rows)
|
| 124 |
+
if (groupIncluded[i]) {
|
| 125 |
+
if (rangeStart === undefined) {
|
| 126 |
+
rangeStart = groupStart
|
| 127 |
+
}
|
| 128 |
+
} else {
|
| 129 |
+
if (rangeStart !== undefined) {
|
| 130 |
+
rowRanges.push([rangeStart, groupEnd])
|
| 131 |
+
rangeStart = undefined
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
groupStart = groupEnd
|
| 135 |
+
}
|
| 136 |
+
if (rangeStart !== undefined) {
|
| 137 |
+
rowRanges.push([rangeStart, groupStart])
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// Fetch by row group and map to rows
|
| 141 |
+
/** @type {(Record<string, any> & {__index__: number})[]} */
|
| 142 |
+
const sparseData = Array(Number(options.metadata.num_rows))
|
| 143 |
+
for (const [rangeStart, rangeEnd] of rowRanges) {
|
| 144 |
+
// TODO: fetch in parallel
|
| 145 |
+
const groupData = await parquetReadObjects({ ...options, rowStart: rangeStart, rowEnd: rangeEnd })
|
| 146 |
+
for (let i = rangeStart; i < rangeEnd; i++) {
|
| 147 |
+
// warning: if the row contains a column named __index__, it will overwrite the index.
|
| 148 |
+
sparseData[i] = { __index__: i, ...groupData[i - rangeStart] }
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
return sparseData
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
/**
|
| 155 |
+
* @param {any} a
|
| 156 |
+
* @param {any} b
|
| 157 |
+
* @returns {number}
|
| 158 |
+
*/
|
| 159 |
+
function compare(a, b) {
|
| 160 |
+
if (a < b) return -1
|
| 161 |
+
if (a > b) return 1
|
| 162 |
+
return 0 // TODO: null handling
|
| 163 |
+
}
|
web/node_modules/hyparquet/src/read.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @import {AsyncRowGroup, BaseParquetReadOptions, DecodedArray, ParquetReadOptions, SchemaElement} from '../src/types.js'
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import { columnsNeededForFilter, matchFilter } from './filter.js'
|
| 6 |
+
import { parquetMetadataAsync, parquetSchema } from './metadata.js'
|
| 7 |
+
import { parquetPlan, prefetchAsyncBuffer, prefetchBloomFilters } from './plan.js'
|
| 8 |
+
import { assembleAsync, asyncGroupToRows, readRowGroup } from './rowgroup.js'
|
| 9 |
+
import { concat } from './utils.js'
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Read parquet data rows from a file-like object.
|
| 13 |
+
* Reads the minimal number of row groups and columns to satisfy the request.
|
| 14 |
+
*
|
| 15 |
+
* Returns a void promise when complete.
|
| 16 |
+
* Errors are thrown on the returned promise.
|
| 17 |
+
* Data is returned in callbacks onComplete, onChunk, onPage, NOT the return promise.
|
| 18 |
+
* See parquetReadObjects for a more convenient API.
|
| 19 |
+
*
|
| 20 |
+
* @param {ParquetReadOptions} options read options
|
| 21 |
+
* @returns {Promise<void>} resolves when all requested rows and columns are parsed, all errors are thrown here
|
| 22 |
+
*/
|
| 23 |
+
export async function parquetRead(options) {
|
| 24 |
+
// load metadata if not provided
|
| 25 |
+
options.metadata ??= await parquetMetadataAsync(options.file, options)
|
| 26 |
+
|
| 27 |
+
const { rowStart = 0, rowEnd, columns, onChunk, onComplete, rowFormat, filter, filterStrict = true } = options
|
| 28 |
+
|
| 29 |
+
// Filter requires object format to match column names
|
| 30 |
+
if (filter && rowFormat !== 'object') {
|
| 31 |
+
throw new Error('parquet filter requires rowFormat: "object"')
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// Include filter columns in the read plan
|
| 35 |
+
const filterColumns = columnsNeededForFilter(filter)
|
| 36 |
+
if (filterColumns.length) {
|
| 37 |
+
const schemaColumns = parquetSchema(options.metadata).children.map(c => c.element.name)
|
| 38 |
+
const missingColumns = filterColumns.filter(c => !schemaColumns.includes(c))
|
| 39 |
+
if (missingColumns.length) {
|
| 40 |
+
throw new Error(`parquet filter columns not found: ${missingColumns.join(', ')}`)
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
let readColumns = columns
|
| 44 |
+
let requiresProjection = false
|
| 45 |
+
if (columns && filter) {
|
| 46 |
+
const missingFilterColumns = filterColumns.filter(c => !columns.includes(c))
|
| 47 |
+
if (missingFilterColumns.length) {
|
| 48 |
+
readColumns = [...columns, ...missingFilterColumns]
|
| 49 |
+
requiresProjection = true
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// read row groups with expanded columns
|
| 54 |
+
let readOptions = readColumns !== columns ? { ...options, columns: readColumns } : options
|
| 55 |
+
readOptions = await withBloomFilters(readOptions)
|
| 56 |
+
const asyncGroups = parquetReadAsync(readOptions)
|
| 57 |
+
|
| 58 |
+
// skip assembly if no onComplete or onChunk, but wait for reading to finish
|
| 59 |
+
if (!onComplete && !onChunk) {
|
| 60 |
+
await awaitAllColumns(asyncGroups)
|
| 61 |
+
return
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// assemble struct columns
|
| 65 |
+
const schemaTree = parquetSchema(options.metadata)
|
| 66 |
+
const assembled = asyncGroups.map(arg => assembleAsync(arg, schemaTree, options.parsers))
|
| 67 |
+
|
| 68 |
+
// onChunk emit all chunks (don't await). Rejection is surfaced by awaitAllColumns below.
|
| 69 |
+
if (onChunk) {
|
| 70 |
+
for (const asyncGroup of assembled) {
|
| 71 |
+
for (const asyncColumn of asyncGroup.asyncColumns) {
|
| 72 |
+
asyncColumn.data.then(({ data, skipped }) => {
|
| 73 |
+
let rowStart = asyncGroup.groupStart + skipped
|
| 74 |
+
for (const columnData of data) {
|
| 75 |
+
onChunk({
|
| 76 |
+
columnName: asyncColumn.pathInSchema[0],
|
| 77 |
+
columnData,
|
| 78 |
+
rowStart,
|
| 79 |
+
rowEnd: rowStart + columnData.length,
|
| 80 |
+
})
|
| 81 |
+
rowStart += columnData.length
|
| 82 |
+
}
|
| 83 |
+
}, () => {})
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// onComplete transpose column chunks to rows
|
| 89 |
+
if (onComplete) {
|
| 90 |
+
// wait for all reads to settle so a sibling rejection cannot leak
|
| 91 |
+
await awaitAllColumns(assembled)
|
| 92 |
+
// loosen the types to avoid duplicate code
|
| 93 |
+
/** @type {any[]} */
|
| 94 |
+
const rows = []
|
| 95 |
+
for (const asyncGroup of assembled) {
|
| 96 |
+
// filter to rows in range
|
| 97 |
+
const selectStart = Math.max(rowStart - asyncGroup.groupStart, 0)
|
| 98 |
+
const selectEnd = Math.min((rowEnd ?? Infinity) - asyncGroup.groupStart, asyncGroup.groupRows)
|
| 99 |
+
// transpose column chunks to rows in output
|
| 100 |
+
const groupData = rowFormat === 'object' ?
|
| 101 |
+
await asyncGroupToRows(asyncGroup, selectStart, selectEnd, readColumns, 'object') :
|
| 102 |
+
await asyncGroupToRows(asyncGroup, selectStart, selectEnd, columns, 'array')
|
| 103 |
+
|
| 104 |
+
// Apply filter and projection
|
| 105 |
+
if (filter) {
|
| 106 |
+
// eslint-disable-next-line no-extra-parens
|
| 107 |
+
for (const row of /** @type {Record<string, any>[]} */ (groupData)) {
|
| 108 |
+
if (matchFilter(row, filter, filterStrict)) {
|
| 109 |
+
if (requiresProjection && columns) {
|
| 110 |
+
for (const col of filterColumns) {
|
| 111 |
+
if (!columns.includes(col)) delete row[col]
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
rows.push(row)
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
} else {
|
| 118 |
+
concat(rows, groupData)
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
onComplete(rows)
|
| 122 |
+
} else {
|
| 123 |
+
// wait for all async groups to finish (complete takes care of this)
|
| 124 |
+
await awaitAllColumns(assembled)
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
/**
|
| 129 |
+
* Await every column promise across the given row groups via Promise.allSettled
|
| 130 |
+
* so no rejection escapes as an unhandledRejection. Throws the first rejection.
|
| 131 |
+
*
|
| 132 |
+
* @param {AsyncRowGroup[]} asyncGroups
|
| 133 |
+
* @returns {Promise<void>}
|
| 134 |
+
*/
|
| 135 |
+
async function awaitAllColumns(asyncGroups) {
|
| 136 |
+
const all = asyncGroups.flatMap(g => g.asyncColumns.map(c => c.data))
|
| 137 |
+
const results = await Promise.allSettled(all)
|
| 138 |
+
const failed = results.find(r => r.status === 'rejected')
|
| 139 |
+
if (failed) throw failed.reason
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
/**
|
| 143 |
+
* @param {ParquetReadOptions} options read options
|
| 144 |
+
* @returns {AsyncRowGroup[]}
|
| 145 |
+
*/
|
| 146 |
+
export function parquetReadAsync(options) {
|
| 147 |
+
if (!options.metadata) throw new Error('parquet requires metadata')
|
| 148 |
+
// TODO: validate options (start, end, columns, etc)
|
| 149 |
+
|
| 150 |
+
// prefetch byte ranges
|
| 151 |
+
const plan = parquetPlan(options)
|
| 152 |
+
options.file = prefetchAsyncBuffer(options.file, plan)
|
| 153 |
+
|
| 154 |
+
// read row groups
|
| 155 |
+
return plan.groups.map(groupPlan => readRowGroup(options, plan, groupPlan))
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
/**
|
| 159 |
+
* Reads a single column from a parquet file.
|
| 160 |
+
*
|
| 161 |
+
* @param {BaseParquetReadOptions} options
|
| 162 |
+
* @returns {Promise<DecodedArray>}
|
| 163 |
+
*/
|
| 164 |
+
export async function parquetReadColumn(options) {
|
| 165 |
+
if (options.columns?.length !== 1) {
|
| 166 |
+
throw new Error('parquetReadColumn expected columns: [columnName]')
|
| 167 |
+
}
|
| 168 |
+
options.metadata ??= await parquetMetadataAsync(options.file, options)
|
| 169 |
+
const asyncGroups = parquetReadAsync(await withBloomFilters(options))
|
| 170 |
+
|
| 171 |
+
// assemble struct columns
|
| 172 |
+
const schemaTree = parquetSchema(options.metadata)
|
| 173 |
+
const assembled = asyncGroups.map(arg => assembleAsync(arg, schemaTree, options.parsers))
|
| 174 |
+
|
| 175 |
+
// wait for all reads to settle so a sibling rejection cannot leak
|
| 176 |
+
await awaitAllColumns(assembled)
|
| 177 |
+
|
| 178 |
+
/** @type {DecodedArray} */
|
| 179 |
+
const columnData = []
|
| 180 |
+
for (const rg of assembled) {
|
| 181 |
+
const { data } = await rg.asyncColumns[0].data
|
| 182 |
+
for (const chunk of data) {
|
| 183 |
+
concat(columnData, chunk)
|
| 184 |
+
}
|
| 185 |
+
}
|
| 186 |
+
return columnData
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
/**
|
| 190 |
+
* Conditionally fetch bloom filters and attach them (and the per-column schema
|
| 191 |
+
* elements they require) to options so parquetPlan can use them for row-group
|
| 192 |
+
* pruning. Returns options unchanged when there's no filter or the user has
|
| 193 |
+
* disabled bloom pushdown.
|
| 194 |
+
*
|
| 195 |
+
* @param {BaseParquetReadOptions} options
|
| 196 |
+
* @returns {Promise<BaseParquetReadOptions>}
|
| 197 |
+
*/
|
| 198 |
+
async function withBloomFilters(options) {
|
| 199 |
+
if (!options.useBloomFilters) return options
|
| 200 |
+
if (!options.filter || !options.metadata) return options
|
| 201 |
+
const schemaTree = parquetSchema(options.metadata)
|
| 202 |
+
/** @type {Record<string, SchemaElement>} */
|
| 203 |
+
const schemaElements = {}
|
| 204 |
+
for (const child of schemaTree.children) schemaElements[child.element.name] = child.element
|
| 205 |
+
const bloomFiltersByGroup = await prefetchBloomFilters({
|
| 206 |
+
file: options.file,
|
| 207 |
+
metadata: options.metadata,
|
| 208 |
+
filter: options.filter,
|
| 209 |
+
filterStrict: options.filterStrict,
|
| 210 |
+
})
|
| 211 |
+
// eslint-disable-next-line no-extra-parens
|
| 212 |
+
return /** @type {BaseParquetReadOptions} */ ({ ...options, bloomFiltersByGroup, schemaElements })
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
/**
|
| 216 |
+
* This is a helper function to read parquet row data as a promise.
|
| 217 |
+
* It is a wrapper around the more configurable parquetRead function.
|
| 218 |
+
*
|
| 219 |
+
* @param {Omit<ParquetReadOptions, 'onComplete'>} options
|
| 220 |
+
* @returns {Promise<Record<string, any>[]>} resolves when all requested rows and columns are parsed
|
| 221 |
+
*/
|
| 222 |
+
export function parquetReadObjects(options) {
|
| 223 |
+
return new Promise((onComplete, reject) => {
|
| 224 |
+
parquetRead({
|
| 225 |
+
...options,
|
| 226 |
+
rowFormat: 'object', // force object output
|
| 227 |
+
onComplete,
|
| 228 |
+
}).catch(reject)
|
| 229 |
+
})
|
| 230 |
+
}
|
web/node_modules/hyparquet/src/rowgroup.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @import {AsyncColumn, AsyncRowGroup, DecodedArray, GroupPlan, ParquetParsers, ParquetReadOptions, QueryPlan, SchemaTree} from '../src/types.js'
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import { assembleNested } from './assemble.js'
|
| 6 |
+
import { readColumn } from './column.js'
|
| 7 |
+
import { DEFAULT_PARSERS } from './convert.js'
|
| 8 |
+
import { readOffsetIndex } from './indexes.js'
|
| 9 |
+
import { getSchemaPath } from './schema.js'
|
| 10 |
+
import { flatten } from './utils.js'
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* Read a row group from a file-like object.
|
| 14 |
+
*
|
| 15 |
+
* @param {ParquetReadOptions} options
|
| 16 |
+
* @param {QueryPlan} plan
|
| 17 |
+
* @param {GroupPlan} groupPlan
|
| 18 |
+
* @returns {AsyncRowGroup} resolves to column data
|
| 19 |
+
*/
|
| 20 |
+
export function readRowGroup(options, { metadata }, groupPlan) {
|
| 21 |
+
/** @type {AsyncColumn[]} */
|
| 22 |
+
const asyncColumns = []
|
| 23 |
+
|
| 24 |
+
// read column data
|
| 25 |
+
for (const chunk of groupPlan.chunks) {
|
| 26 |
+
const { data_page_offset, dictionary_page_offset, path_in_schema: pathInSchema } = chunk.columnMetadata
|
| 27 |
+
const schemaPath = getSchemaPath(metadata.schema, pathInSchema)
|
| 28 |
+
const columnDecoder = {
|
| 29 |
+
pathInSchema,
|
| 30 |
+
element: schemaPath[schemaPath.length - 1].element,
|
| 31 |
+
schemaPath,
|
| 32 |
+
parsers: { ...DEFAULT_PARSERS, ...options.parsers },
|
| 33 |
+
...options,
|
| 34 |
+
...chunk.columnMetadata,
|
| 35 |
+
}
|
| 36 |
+
let { startByte, endByte } = chunk.range
|
| 37 |
+
|
| 38 |
+
// non-offset-index case
|
| 39 |
+
if (!('offsetIndex' in chunk)) {
|
| 40 |
+
asyncColumns.push({
|
| 41 |
+
pathInSchema,
|
| 42 |
+
data: Promise.resolve(options.file.slice(startByte, endByte))
|
| 43 |
+
.then(buffer => {
|
| 44 |
+
const reader = { view: new DataView(buffer), offset: 0 }
|
| 45 |
+
return readColumn(reader, groupPlan, columnDecoder, options.onPage)
|
| 46 |
+
}),
|
| 47 |
+
})
|
| 48 |
+
continue
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// offset-index case
|
| 52 |
+
asyncColumns.push({
|
| 53 |
+
pathInSchema,
|
| 54 |
+
// fetch offset index
|
| 55 |
+
data: Promise.resolve(options.file.slice(chunk.offsetIndex.startByte, chunk.offsetIndex.endByte))
|
| 56 |
+
.then(async arrayBuffer => {
|
| 57 |
+
// use offset index to read only necessary pages
|
| 58 |
+
const { selectStart, selectEnd } = groupPlan
|
| 59 |
+
const pages = readOffsetIndex({ view: new DataView(arrayBuffer), offset: 0 }).page_locations
|
| 60 |
+
let skipped = -1
|
| 61 |
+
// include dictionary if present, handle polars missing dictionary_page_offset
|
| 62 |
+
const hasDict = dictionary_page_offset || data_page_offset < pages[0].offset
|
| 63 |
+
for (let i = 0; i < pages.length; i++) {
|
| 64 |
+
const page = pages[i]
|
| 65 |
+
const pageStart = Number(page.first_row_index)
|
| 66 |
+
const pageEnd = i + 1 < pages.length
|
| 67 |
+
? Number(pages[i + 1].first_row_index)
|
| 68 |
+
: groupPlan.groupRows // last page extends to end of row group
|
| 69 |
+
// check if page overlaps with [selectStart, selectEnd)
|
| 70 |
+
if (skipped < 0 && !hasDict && pageEnd > selectStart) {
|
| 71 |
+
startByte = Number(page.offset)
|
| 72 |
+
skipped = pageStart
|
| 73 |
+
}
|
| 74 |
+
if (pageStart < selectEnd) {
|
| 75 |
+
endByte = Number(page.offset) + page.compressed_page_size
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
if (skipped < 0) skipped = 0
|
| 79 |
+
const buffer = await options.file.slice(startByte, endByte)
|
| 80 |
+
const reader = { view: new DataView(buffer), offset: 0 }
|
| 81 |
+
// adjust row selection for skipped pages
|
| 82 |
+
const adjustedGroupPlan = skipped ? {
|
| 83 |
+
...groupPlan,
|
| 84 |
+
groupStart: groupPlan.groupStart + skipped,
|
| 85 |
+
selectStart: groupPlan.selectStart - skipped,
|
| 86 |
+
selectEnd: groupPlan.selectEnd - skipped,
|
| 87 |
+
} : groupPlan
|
| 88 |
+
const { data, skipped: columnSkipped } = readColumn(reader, adjustedGroupPlan, columnDecoder, options.onPage)
|
| 89 |
+
return {
|
| 90 |
+
data,
|
| 91 |
+
skipped: skipped + columnSkipped,
|
| 92 |
+
}
|
| 93 |
+
}),
|
| 94 |
+
})
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
return { groupStart: groupPlan.groupStart, groupRows: groupPlan.groupRows, asyncColumns }
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/**
|
| 101 |
+
* @overload
|
| 102 |
+
* @param {AsyncRowGroup} asyncGroup
|
| 103 |
+
* @param {number} selectStart
|
| 104 |
+
* @param {number} selectEnd
|
| 105 |
+
* @param {string[] | undefined} columns
|
| 106 |
+
* @param {'object'} rowFormat
|
| 107 |
+
* @returns {Promise<Record<string, any>[]>} resolves to row data
|
| 108 |
+
*/
|
| 109 |
+
/**
|
| 110 |
+
* @overload
|
| 111 |
+
* @param {AsyncRowGroup} asyncGroup
|
| 112 |
+
* @param {number} selectStart
|
| 113 |
+
* @param {number} selectEnd
|
| 114 |
+
* @param {string[] | undefined} columns
|
| 115 |
+
* @param {'array'} [rowFormat]
|
| 116 |
+
* @returns {Promise<any[][]>} resolves to row data
|
| 117 |
+
*/
|
| 118 |
+
/**
|
| 119 |
+
* @param {AsyncRowGroup} asyncGroup
|
| 120 |
+
* @param {number} selectStart
|
| 121 |
+
* @param {number} selectEnd
|
| 122 |
+
* @param {string[] | undefined} columns
|
| 123 |
+
* @param {'object' | 'array'} [rowFormat]
|
| 124 |
+
* @returns {Promise<Record<string, any>[] | any[][]>} resolves to row data
|
| 125 |
+
*/
|
| 126 |
+
export async function asyncGroupToRows({ asyncColumns }, selectStart, selectEnd, columns, rowFormat) {
|
| 127 |
+
// TODO: do it without flatten
|
| 128 |
+
const asyncPages = await Promise.all(asyncColumns.map(column =>
|
| 129 |
+
column.data.then(({ skipped, data }) => ({ skipped, data: flatten(data) }))
|
| 130 |
+
))
|
| 131 |
+
|
| 132 |
+
// transpose columns into rows
|
| 133 |
+
const selectCount = selectEnd - selectStart
|
| 134 |
+
if (rowFormat === 'object') {
|
| 135 |
+
/** @type {Record<string, any>[]} */
|
| 136 |
+
const groupData = Array(selectCount)
|
| 137 |
+
for (let selectRow = 0; selectRow < selectCount; selectRow++) {
|
| 138 |
+
// return each row as an object
|
| 139 |
+
/** @type {Record<string, any>} */
|
| 140 |
+
const rowData = {}
|
| 141 |
+
for (let i = 0; i < asyncColumns.length; i++) {
|
| 142 |
+
const { data, skipped } = asyncPages[i]
|
| 143 |
+
rowData[asyncColumns[i].pathInSchema[0]] = data[selectStart + selectRow - skipped]
|
| 144 |
+
}
|
| 145 |
+
groupData[selectRow] = rowData
|
| 146 |
+
}
|
| 147 |
+
return groupData
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
// careful mapping of column order for rowFormat: array
|
| 151 |
+
const includedColumnNames = asyncColumns
|
| 152 |
+
.map(child => child.pathInSchema[0])
|
| 153 |
+
.filter(name => !columns || columns.includes(name))
|
| 154 |
+
const columnOrder = columns ?? includedColumnNames
|
| 155 |
+
const columnIndexes = columnOrder.map(name => asyncColumns.findIndex(column => column.pathInSchema[0] === name))
|
| 156 |
+
|
| 157 |
+
/** @type {any[][]} */
|
| 158 |
+
const groupData = Array(selectCount)
|
| 159 |
+
for (let selectRow = 0; selectRow < selectCount; selectRow++) {
|
| 160 |
+
// return each row as an array
|
| 161 |
+
const rowData = Array(asyncColumns.length)
|
| 162 |
+
for (let i = 0; i < columnOrder.length; i++) {
|
| 163 |
+
const colIdx = columnIndexes[i]
|
| 164 |
+
if (colIdx < 0) throw new Error(`parquet column not found: ${columnOrder[i]}`)
|
| 165 |
+
const { data, skipped } = asyncPages[colIdx]
|
| 166 |
+
rowData[i] = data[selectStart + selectRow - skipped]
|
| 167 |
+
}
|
| 168 |
+
groupData[selectRow] = rowData
|
| 169 |
+
}
|
| 170 |
+
return groupData
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
/**
|
| 174 |
+
* Assemble physical columns into top-level columns asynchronously.
|
| 175 |
+
*
|
| 176 |
+
* @param {AsyncRowGroup} asyncRowGroup
|
| 177 |
+
* @param {SchemaTree} schemaTree
|
| 178 |
+
* @param {ParquetParsers} [parsers]
|
| 179 |
+
* @returns {AsyncRowGroup}
|
| 180 |
+
*/
|
| 181 |
+
export function assembleAsync(asyncRowGroup, schemaTree, parsers) {
|
| 182 |
+
const { asyncColumns } = asyncRowGroup
|
| 183 |
+
parsers = { ...DEFAULT_PARSERS, ...parsers }
|
| 184 |
+
/** @type {AsyncColumn[]} */
|
| 185 |
+
const assembled = []
|
| 186 |
+
for (const child of schemaTree.children) {
|
| 187 |
+
if (child.children.length) {
|
| 188 |
+
const childColumns = asyncColumns.filter(column => column.pathInSchema[0] === child.element.name)
|
| 189 |
+
if (!childColumns.length) continue
|
| 190 |
+
|
| 191 |
+
assembled.push({
|
| 192 |
+
pathInSchema: child.path,
|
| 193 |
+
data: (async () => {
|
| 194 |
+
// collect subcolumn data — Promise.all observes every rejection so
|
| 195 |
+
// a sibling failure cannot leak as an unhandledRejection
|
| 196 |
+
const resolved = await Promise.all(childColumns.map(c => c.data))
|
| 197 |
+
/** @type {Map<string, DecodedArray>} */
|
| 198 |
+
const subcolumnData = new Map()
|
| 199 |
+
let minLength = Infinity
|
| 200 |
+
for (let i = 0; i < childColumns.length; i++) {
|
| 201 |
+
const flat = flatten(resolved[i].data)
|
| 202 |
+
subcolumnData.set(childColumns[i].pathInSchema.join('.'), flat)
|
| 203 |
+
minLength = Math.min(minLength, flat.length)
|
| 204 |
+
}
|
| 205 |
+
// trim sub-columns to same length (offset index may read different pages per column)
|
| 206 |
+
for (const [key, value] of subcolumnData) {
|
| 207 |
+
if (value.length > minLength) {
|
| 208 |
+
subcolumnData.set(key, value.slice(0, minLength))
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
// assemble the column
|
| 212 |
+
assembleNested(subcolumnData, child, parsers)
|
| 213 |
+
const assembled = subcolumnData.get(child.element.name)
|
| 214 |
+
if (!assembled) throw new Error('parquet column data not assembled')
|
| 215 |
+
return { data: [assembled], skipped: 0 }
|
| 216 |
+
})(),
|
| 217 |
+
})
|
| 218 |
+
} else {
|
| 219 |
+
// leaf node, return the column
|
| 220 |
+
const asyncColumn = asyncColumns.find(column => column.pathInSchema[0] === child.element.name)
|
| 221 |
+
if (asyncColumn) assembled.push(asyncColumn)
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
return { ...asyncRowGroup, asyncColumns: assembled }
|
| 225 |
+
}
|
web/node_modules/hyparquet/src/schema.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Build a tree from the schema elements.
|
| 3 |
+
*
|
| 4 |
+
* @param {SchemaElement[]} schema
|
| 5 |
+
* @param {number} rootIndex index of the root element
|
| 6 |
+
* @param {string[]} path path to the element
|
| 7 |
+
* @returns {SchemaTree} tree of schema elements
|
| 8 |
+
*/
|
| 9 |
+
function schemaTree(schema, rootIndex, path) {
|
| 10 |
+
const element = schema[rootIndex]
|
| 11 |
+
const children = []
|
| 12 |
+
let count = 1
|
| 13 |
+
|
| 14 |
+
// Read the specified number of children
|
| 15 |
+
if (element.num_children) {
|
| 16 |
+
while (children.length < element.num_children) {
|
| 17 |
+
const childElement = schema[rootIndex + count]
|
| 18 |
+
const child = schemaTree(schema, rootIndex + count, [...path, childElement.name])
|
| 19 |
+
count += child.count
|
| 20 |
+
children.push(child)
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
return { count, element, children, path }
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/**
|
| 28 |
+
* Get schema elements from the root to the given element name.
|
| 29 |
+
*
|
| 30 |
+
* @param {SchemaElement[]} schema
|
| 31 |
+
* @param {string[]} name path to the element
|
| 32 |
+
* @returns {SchemaTree[]} list of schema elements
|
| 33 |
+
*/
|
| 34 |
+
export function getSchemaPath(schema, name) {
|
| 35 |
+
let tree = schemaTree(schema, 0, [])
|
| 36 |
+
const path = [tree]
|
| 37 |
+
for (const part of name) {
|
| 38 |
+
const child = tree.children.find(child => child.element.name === part)
|
| 39 |
+
if (!child) throw new Error(`parquet schema element not found: ${name}`)
|
| 40 |
+
path.push(child)
|
| 41 |
+
tree = child
|
| 42 |
+
}
|
| 43 |
+
return path
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/**
|
| 47 |
+
* Get all physical (leaf) column names.
|
| 48 |
+
*
|
| 49 |
+
* @param {SchemaTree} schemaTree
|
| 50 |
+
* @returns {string[]} list of physical column names
|
| 51 |
+
*/
|
| 52 |
+
export function getPhysicalColumns(schemaTree) {
|
| 53 |
+
/** @type {string[]} */
|
| 54 |
+
const columns = []
|
| 55 |
+
/** @param {SchemaTree} node */
|
| 56 |
+
function traverse(node) {
|
| 57 |
+
if (node.children.length) {
|
| 58 |
+
for (const child of node.children) {
|
| 59 |
+
traverse(child)
|
| 60 |
+
}
|
| 61 |
+
} else {
|
| 62 |
+
columns.push(node.path.join('.'))
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
traverse(schemaTree)
|
| 66 |
+
return columns
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/**
|
| 70 |
+
* Get the max repetition level for a given schema path.
|
| 71 |
+
*
|
| 72 |
+
* @param {SchemaTree[]} schemaPath
|
| 73 |
+
* @returns {number} max repetition level
|
| 74 |
+
*/
|
| 75 |
+
export function getMaxRepetitionLevel(schemaPath) {
|
| 76 |
+
let maxLevel = 0
|
| 77 |
+
for (const { element } of schemaPath) {
|
| 78 |
+
if (element.repetition_type === 'REPEATED') {
|
| 79 |
+
maxLevel++
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
return maxLevel
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/**
|
| 86 |
+
* Get the max definition level for a given schema path.
|
| 87 |
+
*
|
| 88 |
+
* @param {SchemaTree[]} schemaPath
|
| 89 |
+
* @returns {number} max definition level
|
| 90 |
+
*/
|
| 91 |
+
export function getMaxDefinitionLevel(schemaPath) {
|
| 92 |
+
let maxLevel = 0
|
| 93 |
+
for (const { element } of schemaPath.slice(1)) {
|
| 94 |
+
if (element.repetition_type !== 'REQUIRED') {
|
| 95 |
+
maxLevel++
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
return maxLevel
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/**
|
| 102 |
+
* Check if a column is list-like.
|
| 103 |
+
*
|
| 104 |
+
* @param {SchemaTree} schema
|
| 105 |
+
* @returns {boolean} true if list-like
|
| 106 |
+
*/
|
| 107 |
+
export function isListLike(schema) {
|
| 108 |
+
if (!schema) return false
|
| 109 |
+
if (schema.element.converted_type !== 'LIST') return false
|
| 110 |
+
if (schema.children.length > 1) return false
|
| 111 |
+
|
| 112 |
+
const firstChild = schema.children[0]
|
| 113 |
+
if (firstChild.children.length > 1) return false
|
| 114 |
+
if (firstChild.element.repetition_type !== 'REPEATED') return false
|
| 115 |
+
|
| 116 |
+
return true
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
/**
|
| 120 |
+
* Check if a column is map-like.
|
| 121 |
+
*
|
| 122 |
+
* @param {SchemaTree} schema
|
| 123 |
+
* @returns {boolean} true if map-like
|
| 124 |
+
*/
|
| 125 |
+
export function isMapLike(schema) {
|
| 126 |
+
if (!schema) return false
|
| 127 |
+
if (schema.element.converted_type !== 'MAP') return false
|
| 128 |
+
if (schema.children.length > 1) return false
|
| 129 |
+
|
| 130 |
+
const firstChild = schema.children[0]
|
| 131 |
+
if (firstChild.children.length !== 2) return false
|
| 132 |
+
if (firstChild.element.repetition_type !== 'REPEATED') return false
|
| 133 |
+
|
| 134 |
+
const keyChild = firstChild.children.find(child => child.element.name === 'key')
|
| 135 |
+
if (keyChild?.element.repetition_type === 'REPEATED') return false
|
| 136 |
+
|
| 137 |
+
const valueChild = firstChild.children.find(child => child.element.name === 'value')
|
| 138 |
+
if (valueChild?.element.repetition_type === 'REPEATED') return false
|
| 139 |
+
|
| 140 |
+
return true
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/**
|
| 144 |
+
* Returns true if a column is non-nested.
|
| 145 |
+
*
|
| 146 |
+
* @param {SchemaTree[]} schemaPath
|
| 147 |
+
* @returns {boolean}
|
| 148 |
+
*/
|
| 149 |
+
export function isFlatColumn(schemaPath) {
|
| 150 |
+
if (schemaPath.length !== 2) return false
|
| 151 |
+
const [, column] = schemaPath
|
| 152 |
+
if (column.element.repetition_type === 'REPEATED') return false
|
| 153 |
+
if (column.children.length) return false
|
| 154 |
+
return true
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
/**
|
| 158 |
+
* @import {SchemaElement, SchemaTree} from '../src/types.js'
|
| 159 |
+
*/
|
web/node_modules/hyparquet/src/snappy.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* The MIT License (MIT)
|
| 3 |
+
* Copyright (c) 2016 Zhipeng Jia
|
| 4 |
+
* https://github.com/zhipeng-jia/snappyjs
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
const WORD_MASK = [0, 0xff, 0xffff, 0xffffff, 0xffffffff]
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* Copy bytes from one array to another
|
| 11 |
+
*
|
| 12 |
+
* @param {Uint8Array} fromArray source array
|
| 13 |
+
* @param {number} fromPos source position
|
| 14 |
+
* @param {Uint8Array} toArray destination array
|
| 15 |
+
* @param {number} toPos destination position
|
| 16 |
+
* @param {number} length number of bytes to copy
|
| 17 |
+
*/
|
| 18 |
+
function copyBytes(fromArray, fromPos, toArray, toPos, length) {
|
| 19 |
+
for (let i = 0; i < length; i++) {
|
| 20 |
+
toArray[toPos + i] = fromArray[fromPos + i]
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* Decompress snappy data.
|
| 26 |
+
* Accepts an output buffer to avoid allocating a new buffer for each call.
|
| 27 |
+
*
|
| 28 |
+
* @param {Uint8Array} input compressed data
|
| 29 |
+
* @param {Uint8Array} output output buffer
|
| 30 |
+
*/
|
| 31 |
+
export function snappyUncompress(input, output) {
|
| 32 |
+
const inputLength = input.byteLength
|
| 33 |
+
const outputLength = output.byteLength
|
| 34 |
+
let pos = 0
|
| 35 |
+
let outPos = 0
|
| 36 |
+
|
| 37 |
+
// skip preamble (contains uncompressed length as varint)
|
| 38 |
+
while (pos < inputLength) {
|
| 39 |
+
const c = input[pos]
|
| 40 |
+
pos++
|
| 41 |
+
if (c < 128) {
|
| 42 |
+
break
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
if (outputLength && pos >= inputLength) {
|
| 46 |
+
throw new Error('invalid snappy length header')
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
while (pos < inputLength) {
|
| 50 |
+
const c = input[pos]
|
| 51 |
+
let len = 0
|
| 52 |
+
pos++
|
| 53 |
+
|
| 54 |
+
if (pos >= inputLength) {
|
| 55 |
+
throw new Error('missing eof marker')
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// There are two types of elements, literals and copies (back references)
|
| 59 |
+
if ((c & 0x3) === 0) {
|
| 60 |
+
// Literals are uncompressed data stored directly in the byte stream
|
| 61 |
+
let len = (c >>> 2) + 1
|
| 62 |
+
// Longer literal length is encoded in multiple bytes
|
| 63 |
+
if (len > 60) {
|
| 64 |
+
if (pos + 3 >= inputLength) {
|
| 65 |
+
throw new Error('snappy error literal pos + 3 >= inputLength')
|
| 66 |
+
}
|
| 67 |
+
const lengthSize = len - 60 // length bytes - 1
|
| 68 |
+
len = input[pos]
|
| 69 |
+
+ (input[pos + 1] << 8)
|
| 70 |
+
+ (input[pos + 2] << 16)
|
| 71 |
+
+ (input[pos + 3] << 24)
|
| 72 |
+
len = (len & WORD_MASK[lengthSize]) + 1
|
| 73 |
+
pos += lengthSize
|
| 74 |
+
}
|
| 75 |
+
if (pos + len > inputLength) {
|
| 76 |
+
throw new Error('snappy error literal exceeds input length')
|
| 77 |
+
}
|
| 78 |
+
copyBytes(input, pos, output, outPos, len)
|
| 79 |
+
pos += len
|
| 80 |
+
outPos += len
|
| 81 |
+
} else {
|
| 82 |
+
// Copy elements
|
| 83 |
+
let offset = 0 // offset back from current position to read
|
| 84 |
+
switch (c & 0x3) {
|
| 85 |
+
case 1:
|
| 86 |
+
// Copy with 1-byte offset
|
| 87 |
+
len = (c >>> 2 & 0x7) + 4
|
| 88 |
+
offset = input[pos] + (c >>> 5 << 8)
|
| 89 |
+
pos++
|
| 90 |
+
break
|
| 91 |
+
case 2:
|
| 92 |
+
// Copy with 2-byte offset
|
| 93 |
+
if (inputLength <= pos + 1) {
|
| 94 |
+
throw new Error('snappy error end of input')
|
| 95 |
+
}
|
| 96 |
+
len = (c >>> 2) + 1
|
| 97 |
+
offset = input[pos] + (input[pos + 1] << 8)
|
| 98 |
+
pos += 2
|
| 99 |
+
break
|
| 100 |
+
case 3:
|
| 101 |
+
// Copy with 4-byte offset
|
| 102 |
+
if (inputLength <= pos + 3) {
|
| 103 |
+
throw new Error('snappy error end of input')
|
| 104 |
+
}
|
| 105 |
+
len = (c >>> 2) + 1
|
| 106 |
+
offset = input[pos]
|
| 107 |
+
+ (input[pos + 1] << 8)
|
| 108 |
+
+ (input[pos + 2] << 16)
|
| 109 |
+
+ (input[pos + 3] << 24)
|
| 110 |
+
pos += 4
|
| 111 |
+
break
|
| 112 |
+
default:
|
| 113 |
+
break
|
| 114 |
+
}
|
| 115 |
+
if (offset === 0 || isNaN(offset)) {
|
| 116 |
+
throw new Error(`invalid offset ${offset} pos ${pos} inputLength ${inputLength}`)
|
| 117 |
+
}
|
| 118 |
+
if (offset > outPos) {
|
| 119 |
+
throw new Error('cannot copy from before start of buffer')
|
| 120 |
+
}
|
| 121 |
+
copyBytes(output, outPos - offset, output, outPos, len)
|
| 122 |
+
outPos += len
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
if (outPos !== outputLength) throw new Error('premature end of input')
|
| 127 |
+
}
|
web/node_modules/hyparquet/src/thrift.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @import {DataReader, ThriftObject, ThriftType} from '../src/types.js'
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
// TCompactProtocol types
|
| 6 |
+
const STOP = 0
|
| 7 |
+
const TRUE = 1
|
| 8 |
+
const FALSE = 2
|
| 9 |
+
const BYTE = 3
|
| 10 |
+
const I16 = 4
|
| 11 |
+
const I32 = 5
|
| 12 |
+
const I64 = 6
|
| 13 |
+
const DOUBLE = 7
|
| 14 |
+
const BINARY = 8
|
| 15 |
+
const LIST = 9
|
| 16 |
+
const STRUCT = 12
|
| 17 |
+
|
| 18 |
+
/**
|
| 19 |
+
* Parse TCompactProtocol
|
| 20 |
+
*
|
| 21 |
+
* @param {DataReader} reader
|
| 22 |
+
* @returns {{ [key: `field_${number}`]: any }}
|
| 23 |
+
*/
|
| 24 |
+
export function deserializeTCompactProtocol(reader) {
|
| 25 |
+
/** @type {ThriftObject} */
|
| 26 |
+
const value = {}
|
| 27 |
+
let fid = 0
|
| 28 |
+
|
| 29 |
+
while (reader.offset < reader.view.byteLength) {
|
| 30 |
+
// Parse each field based on its type and add to the result object
|
| 31 |
+
const byte = reader.view.getUint8(reader.offset++)
|
| 32 |
+
const type = byte & 0x0f
|
| 33 |
+
if (type === STOP) break
|
| 34 |
+
const delta = byte >> 4
|
| 35 |
+
fid = delta ? fid + delta : readZigZag(reader)
|
| 36 |
+
value[`field_${fid}`] = readElement(reader, type)
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
return value
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Read a single element based on its type
|
| 44 |
+
*
|
| 45 |
+
* @param {DataReader} reader
|
| 46 |
+
* @param {number} type
|
| 47 |
+
* @returns {ThriftType}
|
| 48 |
+
*/
|
| 49 |
+
function readElement(reader, type) {
|
| 50 |
+
switch (type) {
|
| 51 |
+
case TRUE:
|
| 52 |
+
return true
|
| 53 |
+
case FALSE:
|
| 54 |
+
return false
|
| 55 |
+
case BYTE:
|
| 56 |
+
return reader.view.getInt8(reader.offset++)
|
| 57 |
+
case I16:
|
| 58 |
+
case I32:
|
| 59 |
+
return readZigZag(reader)
|
| 60 |
+
case I64:
|
| 61 |
+
return readZigZagBigInt(reader)
|
| 62 |
+
case DOUBLE: {
|
| 63 |
+
const value = reader.view.getFloat64(reader.offset, true)
|
| 64 |
+
reader.offset += 8
|
| 65 |
+
return value
|
| 66 |
+
}
|
| 67 |
+
case BINARY: {
|
| 68 |
+
const stringLength = readVarInt(reader)
|
| 69 |
+
const strBytes = new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, stringLength)
|
| 70 |
+
reader.offset += stringLength
|
| 71 |
+
return strBytes
|
| 72 |
+
}
|
| 73 |
+
case LIST: {
|
| 74 |
+
const byte = reader.view.getUint8(reader.offset++)
|
| 75 |
+
const elemType = byte & 0x0f
|
| 76 |
+
let listSize = byte >> 4
|
| 77 |
+
if (listSize === 15) {
|
| 78 |
+
listSize = readVarInt(reader)
|
| 79 |
+
}
|
| 80 |
+
const boolType = elemType === TRUE || elemType === FALSE
|
| 81 |
+
const values = new Array(listSize)
|
| 82 |
+
for (let i = 0; i < listSize; i++) {
|
| 83 |
+
values[i] = boolType ? readElement(reader, BYTE) === 1 : readElement(reader, elemType)
|
| 84 |
+
}
|
| 85 |
+
return values
|
| 86 |
+
}
|
| 87 |
+
case STRUCT:
|
| 88 |
+
// main function handles struct parsing
|
| 89 |
+
return deserializeTCompactProtocol(reader)
|
| 90 |
+
default:
|
| 91 |
+
// MAP, SET, UUID not used by parquet
|
| 92 |
+
throw new Error(`thrift unhandled type: ${type}`)
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/**
|
| 97 |
+
* Read varint aka Unsigned LEB128.
|
| 98 |
+
*
|
| 99 |
+
* @param {DataReader} reader
|
| 100 |
+
* @returns {number}
|
| 101 |
+
*/
|
| 102 |
+
export function readVarInt(reader) {
|
| 103 |
+
let result = 0
|
| 104 |
+
let shift = 0
|
| 105 |
+
while (true) {
|
| 106 |
+
// Read groups of 7 low bits until high bit is 0
|
| 107 |
+
const byte = reader.view.getUint8(reader.offset++)
|
| 108 |
+
result |= (byte & 0x7f) << shift
|
| 109 |
+
if (!(byte & 0x80)) {
|
| 110 |
+
return result
|
| 111 |
+
}
|
| 112 |
+
shift += 7
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/**
|
| 117 |
+
* Read a varint as a bigint.
|
| 118 |
+
*
|
| 119 |
+
* @param {DataReader} reader
|
| 120 |
+
* @returns {bigint}
|
| 121 |
+
*/
|
| 122 |
+
function readVarBigInt(reader) {
|
| 123 |
+
let result = 0n
|
| 124 |
+
let shift = 0n
|
| 125 |
+
while (true) {
|
| 126 |
+
const byte = reader.view.getUint8(reader.offset++)
|
| 127 |
+
result |= BigInt(byte & 0x7f) << shift
|
| 128 |
+
if (!(byte & 0x80)) {
|
| 129 |
+
return result
|
| 130 |
+
}
|
| 131 |
+
shift += 7n
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/**
|
| 136 |
+
* Read a zigzag number.
|
| 137 |
+
* Zigzag folds positive and negative numbers into the positive number space.
|
| 138 |
+
*
|
| 139 |
+
* @param {DataReader} reader
|
| 140 |
+
* @returns {number}
|
| 141 |
+
*/
|
| 142 |
+
export function readZigZag(reader) {
|
| 143 |
+
const zigzag = readVarInt(reader)
|
| 144 |
+
return zigzag >>> 1 ^ -(zigzag & 1)
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
/**
|
| 148 |
+
* Read a zigzag bigint.
|
| 149 |
+
*
|
| 150 |
+
* @param {DataReader} reader
|
| 151 |
+
* @returns {bigint}
|
| 152 |
+
*/
|
| 153 |
+
export function readZigZagBigInt(reader) {
|
| 154 |
+
const zigzag = readVarBigInt(reader)
|
| 155 |
+
return zigzag >> 1n ^ -(zigzag & 1n)
|
| 156 |
+
}
|
web/node_modules/hyparquet/src/types.d.ts
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
/**
|
| 3 |
+
* Custom parsers for columns
|
| 4 |
+
*/
|
| 5 |
+
export interface ParquetParsers {
|
| 6 |
+
timestampFromMilliseconds(millis: bigint): any
|
| 7 |
+
timestampFromMicroseconds(micros: bigint): any
|
| 8 |
+
timestampFromNanoseconds(nanos: bigint): any
|
| 9 |
+
dateFromDays(days: number): any
|
| 10 |
+
stringFromBytes(bytes: Uint8Array): any
|
| 11 |
+
jsonFromBytes(bytes: Uint8Array): any
|
| 12 |
+
geometryFromBytes(bytes: Uint8Array): any
|
| 13 |
+
geographyFromBytes(bytes: Uint8Array): any
|
| 14 |
+
uuidFromBytes(bytes: Uint8Array): any
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Parquet Metadata options for metadata parsing
|
| 19 |
+
*/
|
| 20 |
+
export interface MetadataOptions {
|
| 21 |
+
parsers?: ParquetParsers // custom parsers to decode advanced types
|
| 22 |
+
geoparquet?: boolean // parse geoparquet metadata and set logical type to geometry/geography for geospatial columns (default true)
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* Parquet query options for reading data
|
| 27 |
+
*/
|
| 28 |
+
export interface BaseParquetReadOptions {
|
| 29 |
+
file: AsyncBuffer // file-like object containing parquet data
|
| 30 |
+
metadata?: FileMetaData // parquet metadata, will be parsed if not provided
|
| 31 |
+
columns?: string[] // columns to read, all columns if undefined
|
| 32 |
+
filter?: ParquetQueryFilter // filter applied to rows (requires rowFormat: 'object', onChunk is not filtered)
|
| 33 |
+
filterStrict?: boolean // if true filtering uses strict equality (default true)
|
| 34 |
+
rowStart?: number // first requested row index (inclusive)
|
| 35 |
+
rowEnd?: number // last requested row index (exclusive)
|
| 36 |
+
onChunk?: (chunk: ColumnData) => void // called when a column chunk is parsed. chunks may contain data outside the requested range.
|
| 37 |
+
onPage?: (chunk: SubColumnData) => void // called when a data page is parsed. pages may contain data outside the requested range.
|
| 38 |
+
compressors?: Compressors // custom decompressors
|
| 39 |
+
utf8?: boolean // decode byte arrays as utf8 strings (default true)
|
| 40 |
+
parsers?: ParquetParsers // custom parsers to decode advanced types
|
| 41 |
+
geoparquet?: boolean // parse geoparquet metadata and set logical type to geometry/geography for geospatial columns (default true)
|
| 42 |
+
useOffsetIndex?: boolean // use offset index to limit column chunk reads when available (default false)
|
| 43 |
+
useBloomFilters?: boolean // fetch bloom filters to enable row-group skipping on $eq/$in predicates (default false)
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
interface ArrayRowFormat {
|
| 47 |
+
rowFormat?: 'array' // format of each row passed to the onComplete function. Can be omitted, as it's the default.
|
| 48 |
+
onComplete?: (rows: any[][]) => void // called when all requested rows and columns are parsed
|
| 49 |
+
}
|
| 50 |
+
interface ObjectRowFormat {
|
| 51 |
+
rowFormat: 'object' // format of each row passed to the onComplete function
|
| 52 |
+
onComplete?: (rows: Record<string, any>[]) => void // called when all requested rows and columns are parsed
|
| 53 |
+
}
|
| 54 |
+
export type ParquetReadOptions = BaseParquetReadOptions & (ArrayRowFormat | ObjectRowFormat)
|
| 55 |
+
|
| 56 |
+
/**
|
| 57 |
+
* Parquet query options for filtering data
|
| 58 |
+
*/
|
| 59 |
+
export type ParquetQueryFilter =
|
| 60 |
+
| ParquetQueryColumnsFilter
|
| 61 |
+
| { $and: ParquetQueryFilter[] }
|
| 62 |
+
| { $or: ParquetQueryFilter[] }
|
| 63 |
+
| { $nor: ParquetQueryFilter[] }
|
| 64 |
+
type ParquetQueryColumnsFilter = { [key: string]: ParquetQueryOperator }
|
| 65 |
+
export type ParquetQueryValue = string | number | bigint | boolean | object | null | undefined
|
| 66 |
+
export type ParquetQueryOperator = {
|
| 67 |
+
$gt?: ParquetQueryValue
|
| 68 |
+
$gte?: ParquetQueryValue
|
| 69 |
+
$lt?: ParquetQueryValue
|
| 70 |
+
$lte?: ParquetQueryValue
|
| 71 |
+
$eq?: ParquetQueryValue
|
| 72 |
+
$ne?: ParquetQueryValue
|
| 73 |
+
$in?: ParquetQueryValue[]
|
| 74 |
+
$nin?: ParquetQueryValue[]
|
| 75 |
+
$not?: ParquetQueryOperator
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/**
|
| 79 |
+
* A run of column data
|
| 80 |
+
*/
|
| 81 |
+
export interface ColumnData {
|
| 82 |
+
columnName: string
|
| 83 |
+
columnData: DecodedArray
|
| 84 |
+
rowStart: number
|
| 85 |
+
rowEnd: number // exclusive
|
| 86 |
+
}
|
| 87 |
+
/**
|
| 88 |
+
* A run of sub-column data (pre-assembly)
|
| 89 |
+
*/
|
| 90 |
+
export interface SubColumnData {
|
| 91 |
+
pathInSchema: string[]
|
| 92 |
+
columnData: DecodedArray
|
| 93 |
+
rowStart: number
|
| 94 |
+
rowEnd: number // exclusive
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
/**
|
| 98 |
+
* File-like object that can read slices of a file asynchronously.
|
| 99 |
+
*/
|
| 100 |
+
export interface AsyncBuffer {
|
| 101 |
+
byteLength: number
|
| 102 |
+
slice(start: number, end?: number): Awaitable<ArrayBuffer>
|
| 103 |
+
}
|
| 104 |
+
export type Awaitable<T> = T | Promise<T>
|
| 105 |
+
export interface ByteRange {
|
| 106 |
+
startByte: number
|
| 107 |
+
endByte: number // exclusive
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
export interface DataReader {
|
| 111 |
+
view: DataView
|
| 112 |
+
offset: number
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Parquet file metadata types
|
| 116 |
+
export interface FileMetaData {
|
| 117 |
+
version: number
|
| 118 |
+
schema: SchemaElement[]
|
| 119 |
+
num_rows: bigint
|
| 120 |
+
row_groups: RowGroup[]
|
| 121 |
+
key_value_metadata?: KeyValue[]
|
| 122 |
+
created_by?: string
|
| 123 |
+
// column_orders?: ColumnOrder[]
|
| 124 |
+
// encryption_algorithm?: EncryptionAlgorithm
|
| 125 |
+
// footer_signing_key_metadata?: Uint8Array
|
| 126 |
+
metadata_length: number
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
export interface SchemaTree {
|
| 130 |
+
children: SchemaTree[]
|
| 131 |
+
count: number
|
| 132 |
+
element: SchemaElement
|
| 133 |
+
path: string[]
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
export interface SchemaElement {
|
| 137 |
+
type?: ParquetType
|
| 138 |
+
type_length?: number
|
| 139 |
+
repetition_type?: FieldRepetitionType
|
| 140 |
+
name: string
|
| 141 |
+
num_children?: number
|
| 142 |
+
converted_type?: ConvertedType
|
| 143 |
+
scale?: number
|
| 144 |
+
precision?: number
|
| 145 |
+
field_id?: number
|
| 146 |
+
logical_type?: LogicalType
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
export type ParquetType =
|
| 150 |
+
| 'BOOLEAN'
|
| 151 |
+
| 'INT32'
|
| 152 |
+
| 'INT64'
|
| 153 |
+
| 'INT96' // deprecated
|
| 154 |
+
| 'FLOAT'
|
| 155 |
+
| 'DOUBLE'
|
| 156 |
+
| 'BYTE_ARRAY'
|
| 157 |
+
| 'FIXED_LEN_BYTE_ARRAY'
|
| 158 |
+
|
| 159 |
+
export type FieldRepetitionType =
|
| 160 |
+
| 'REQUIRED'
|
| 161 |
+
| 'OPTIONAL'
|
| 162 |
+
| 'REPEATED'
|
| 163 |
+
|
| 164 |
+
export type ConvertedType =
|
| 165 |
+
| 'UTF8'
|
| 166 |
+
| 'MAP'
|
| 167 |
+
| 'MAP_KEY_VALUE'
|
| 168 |
+
| 'LIST'
|
| 169 |
+
| 'ENUM'
|
| 170 |
+
| 'DECIMAL'
|
| 171 |
+
| 'DATE'
|
| 172 |
+
| 'TIME_MILLIS'
|
| 173 |
+
| 'TIME_MICROS'
|
| 174 |
+
| 'TIMESTAMP_MILLIS'
|
| 175 |
+
| 'TIMESTAMP_MICROS'
|
| 176 |
+
| 'UINT_8'
|
| 177 |
+
| 'UINT_16'
|
| 178 |
+
| 'UINT_32'
|
| 179 |
+
| 'UINT_64'
|
| 180 |
+
| 'INT_8'
|
| 181 |
+
| 'INT_16'
|
| 182 |
+
| 'INT_32'
|
| 183 |
+
| 'INT_64'
|
| 184 |
+
| 'JSON'
|
| 185 |
+
| 'BSON'
|
| 186 |
+
| 'INTERVAL'
|
| 187 |
+
|
| 188 |
+
export type TimeUnit = 'MILLIS' | 'MICROS' | 'NANOS'
|
| 189 |
+
|
| 190 |
+
type EdgeInterpolationAlgorithm = 'SPHERICAL' | 'VINCENTY' | 'THOMAS' | 'ANDOYER' | 'KARNEY'
|
| 191 |
+
|
| 192 |
+
export type LogicalType =
|
| 193 |
+
| { type: 'STRING' }
|
| 194 |
+
| { type: 'MAP' }
|
| 195 |
+
| { type: 'LIST' }
|
| 196 |
+
| { type: 'ENUM' }
|
| 197 |
+
| { type: 'DATE' }
|
| 198 |
+
| { type: 'INTERVAL' }
|
| 199 |
+
| { type: 'NULL' }
|
| 200 |
+
| { type: 'JSON' }
|
| 201 |
+
| { type: 'BSON' }
|
| 202 |
+
| { type: 'UUID' }
|
| 203 |
+
| { type: 'FLOAT16' }
|
| 204 |
+
| { type: 'DECIMAL', precision: number, scale: number }
|
| 205 |
+
| { type: 'TIME', isAdjustedToUTC: boolean, unit: TimeUnit }
|
| 206 |
+
| { type: 'TIMESTAMP', isAdjustedToUTC: boolean, unit: TimeUnit }
|
| 207 |
+
| { type: 'INTEGER', bitWidth: number, isSigned: boolean }
|
| 208 |
+
| { type: 'VARIANT', specification_version?: number }
|
| 209 |
+
| { type: 'GEOMETRY', crs?: string }
|
| 210 |
+
| { type: 'GEOGRAPHY', crs?: string, algorithm?: EdgeInterpolationAlgorithm }
|
| 211 |
+
|
| 212 |
+
export interface RowGroup {
|
| 213 |
+
columns: ColumnChunk[]
|
| 214 |
+
total_byte_size: bigint
|
| 215 |
+
num_rows: bigint
|
| 216 |
+
sorting_columns?: SortingColumn[]
|
| 217 |
+
file_offset?: bigint
|
| 218 |
+
total_compressed_size?: bigint
|
| 219 |
+
ordinal?: number
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
export interface ColumnChunk {
|
| 223 |
+
file_path?: string
|
| 224 |
+
file_offset: bigint
|
| 225 |
+
meta_data?: ColumnMetaData
|
| 226 |
+
offset_index_offset?: bigint
|
| 227 |
+
offset_index_length?: number
|
| 228 |
+
column_index_offset?: bigint
|
| 229 |
+
column_index_length?: number
|
| 230 |
+
crypto_metadata?: ColumnCryptoMetaData
|
| 231 |
+
encrypted_column_metadata?: Uint8Array
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
export interface ColumnMetaData {
|
| 235 |
+
type: ParquetType
|
| 236 |
+
encodings: Encoding[]
|
| 237 |
+
path_in_schema: string[]
|
| 238 |
+
codec: CompressionCodec
|
| 239 |
+
num_values: bigint
|
| 240 |
+
total_uncompressed_size: bigint
|
| 241 |
+
total_compressed_size: bigint
|
| 242 |
+
key_value_metadata?: KeyValue[]
|
| 243 |
+
data_page_offset: bigint
|
| 244 |
+
index_page_offset?: bigint
|
| 245 |
+
dictionary_page_offset?: bigint
|
| 246 |
+
statistics?: Statistics
|
| 247 |
+
encoding_stats?: PageEncodingStats[]
|
| 248 |
+
bloom_filter_offset?: bigint
|
| 249 |
+
bloom_filter_length?: number
|
| 250 |
+
size_statistics?: SizeStatistics
|
| 251 |
+
geospatial_statistics?: GeospatialStatistics
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
type ColumnCryptoMetaData = Record<string, never>
|
| 255 |
+
|
| 256 |
+
export type Encoding =
|
| 257 |
+
| 'PLAIN'
|
| 258 |
+
| 'GROUP_VAR_INT' // deprecated
|
| 259 |
+
| 'PLAIN_DICTIONARY'
|
| 260 |
+
| 'RLE'
|
| 261 |
+
| 'BIT_PACKED' // deprecated
|
| 262 |
+
| 'DELTA_BINARY_PACKED'
|
| 263 |
+
| 'DELTA_LENGTH_BYTE_ARRAY'
|
| 264 |
+
| 'DELTA_BYTE_ARRAY'
|
| 265 |
+
| 'RLE_DICTIONARY'
|
| 266 |
+
| 'BYTE_STREAM_SPLIT'
|
| 267 |
+
|
| 268 |
+
export type CompressionCodec =
|
| 269 |
+
| 'UNCOMPRESSED'
|
| 270 |
+
| 'SNAPPY'
|
| 271 |
+
| 'GZIP'
|
| 272 |
+
| 'LZO'
|
| 273 |
+
| 'BROTLI'
|
| 274 |
+
| 'LZ4'
|
| 275 |
+
| 'ZSTD'
|
| 276 |
+
| 'LZ4_RAW'
|
| 277 |
+
|
| 278 |
+
export type Compressors = {
|
| 279 |
+
[K in CompressionCodec]?: (input: Uint8Array, outputLength: number) => Uint8Array
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
export interface KeyValue {
|
| 283 |
+
key: string
|
| 284 |
+
value?: string
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
export type MinMaxType = bigint | boolean | number | string | Date | Uint8Array
|
| 288 |
+
|
| 289 |
+
export interface Statistics {
|
| 290 |
+
max?: MinMaxType
|
| 291 |
+
min?: MinMaxType
|
| 292 |
+
null_count?: bigint
|
| 293 |
+
distinct_count?: bigint
|
| 294 |
+
max_value?: MinMaxType
|
| 295 |
+
min_value?: MinMaxType
|
| 296 |
+
is_max_value_exact?: boolean
|
| 297 |
+
is_min_value_exact?: boolean
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
interface SizeStatistics {
|
| 301 |
+
unencoded_byte_array_data_bytes?: bigint
|
| 302 |
+
repetition_level_histogram?: bigint[]
|
| 303 |
+
definition_level_histogram?: bigint[]
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
export interface GeospatialStatistics {
|
| 307 |
+
bbox?: BoundingBox
|
| 308 |
+
geospatial_types?: number[]
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
export interface BoundingBox {
|
| 312 |
+
xmin: number
|
| 313 |
+
xmax: number
|
| 314 |
+
ymin: number
|
| 315 |
+
ymax: number
|
| 316 |
+
zmin?: number
|
| 317 |
+
zmax?: number
|
| 318 |
+
mmin?: number
|
| 319 |
+
mmax?: number
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
export interface PageEncodingStats {
|
| 323 |
+
page_type: PageType
|
| 324 |
+
encoding: Encoding
|
| 325 |
+
count: number
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
export interface BloomFilter {
|
| 329 |
+
numBytes: number
|
| 330 |
+
blocks: Uint32Array
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
export type PageType =
|
| 334 |
+
'DATA_PAGE' |
|
| 335 |
+
'INDEX_PAGE' |
|
| 336 |
+
'DICTIONARY_PAGE' |
|
| 337 |
+
'DATA_PAGE_V2'
|
| 338 |
+
|
| 339 |
+
interface SortingColumn {
|
| 340 |
+
column_idx: number
|
| 341 |
+
descending: boolean
|
| 342 |
+
nulls_first: boolean
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
// Parquet file header types
|
| 346 |
+
export interface PageHeader {
|
| 347 |
+
type: PageType
|
| 348 |
+
uncompressed_page_size: number
|
| 349 |
+
compressed_page_size: number
|
| 350 |
+
crc?: number
|
| 351 |
+
data_page_header?: DataPageHeader
|
| 352 |
+
index_page_header?: IndexPageHeader
|
| 353 |
+
dictionary_page_header?: DictionaryPageHeader
|
| 354 |
+
data_page_header_v2?: DataPageHeaderV2
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
export interface DataPageHeader {
|
| 358 |
+
num_values: number
|
| 359 |
+
encoding: Encoding
|
| 360 |
+
definition_level_encoding: Encoding
|
| 361 |
+
repetition_level_encoding: Encoding
|
| 362 |
+
statistics?: Statistics
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
type IndexPageHeader = Record<string, never>
|
| 366 |
+
|
| 367 |
+
export interface DictionaryPageHeader {
|
| 368 |
+
num_values: number
|
| 369 |
+
encoding: Encoding
|
| 370 |
+
is_sorted?: boolean
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
export interface DataPageHeaderV2 {
|
| 374 |
+
num_values: number
|
| 375 |
+
num_nulls: number
|
| 376 |
+
num_rows: number
|
| 377 |
+
encoding: Encoding
|
| 378 |
+
definition_levels_byte_length: number
|
| 379 |
+
repetition_levels_byte_length: number
|
| 380 |
+
is_compressed?: boolean
|
| 381 |
+
statistics?: Statistics
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
interface DataPage {
|
| 385 |
+
definitionLevels: number[] | undefined
|
| 386 |
+
repetitionLevels: number[]
|
| 387 |
+
dataPage: DecodedArray
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
export type DecodedArray =
|
| 391 |
+
| Uint8Array
|
| 392 |
+
| Uint32Array
|
| 393 |
+
| Int32Array
|
| 394 |
+
| BigInt64Array
|
| 395 |
+
| BigUint64Array
|
| 396 |
+
| Float32Array
|
| 397 |
+
| Float64Array
|
| 398 |
+
| any[]
|
| 399 |
+
|
| 400 |
+
/** Wrapper around decoded page data */
|
| 401 |
+
export interface PageResult {
|
| 402 |
+
skipped: number
|
| 403 |
+
data?: DecodedArray
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
export interface OffsetIndex {
|
| 407 |
+
page_locations: PageLocation[]
|
| 408 |
+
unencoded_byte_array_data_bytes?: bigint[]
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
interface PageLocation {
|
| 412 |
+
offset: bigint
|
| 413 |
+
compressed_page_size: number
|
| 414 |
+
first_row_index: bigint
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
export interface ColumnIndex {
|
| 418 |
+
null_pages: boolean[]
|
| 419 |
+
min_values: Uint8Array[]
|
| 420 |
+
max_values: Uint8Array[]
|
| 421 |
+
boundary_order: BoundaryOrder
|
| 422 |
+
null_counts?: bigint[]
|
| 423 |
+
repetition_level_histograms?: bigint[]
|
| 424 |
+
definition_level_histograms?: bigint[]
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
export type BoundaryOrder = 'UNORDERED' | 'ASCENDING' | 'DESCENDING'
|
| 428 |
+
|
| 429 |
+
export interface VariantMetadata {
|
| 430 |
+
dictionary: string[]
|
| 431 |
+
sorted: boolean
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
export type ThriftObject = { [ key: `field_${number}` ]: ThriftType }
|
| 435 |
+
export type ThriftType = boolean | number | bigint | Uint8Array | ThriftType[] | ThriftObject
|
| 436 |
+
|
| 437 |
+
/**
|
| 438 |
+
* Query plan for which byte ranges to read.
|
| 439 |
+
*/
|
| 440 |
+
export interface QueryPlan {
|
| 441 |
+
metadata: FileMetaData
|
| 442 |
+
rowStart: number
|
| 443 |
+
rowEnd?: number
|
| 444 |
+
columns?: string[] // columns to read
|
| 445 |
+
fetches: ByteRange[] // byte ranges to fetch
|
| 446 |
+
groups: GroupPlan[] // byte ranges by row group
|
| 447 |
+
}
|
| 448 |
+
// Plan for one group
|
| 449 |
+
interface GroupPlan {
|
| 450 |
+
chunks: ChunkPlan[]
|
| 451 |
+
rowGroup: RowGroup // row group metadata
|
| 452 |
+
groupStart: number // row index of the first row in the group
|
| 453 |
+
selectStart: number // row index in the group to start reading
|
| 454 |
+
selectEnd: number // row index in the group to stop reading
|
| 455 |
+
groupRows: number // number of rows in the group
|
| 456 |
+
}
|
| 457 |
+
// Plan for one column within a row group
|
| 458 |
+
type ChunkPlan = ChunkFull | ChunkOffsetIndexed
|
| 459 |
+
// full column chunk
|
| 460 |
+
interface ChunkFull {
|
| 461 |
+
columnMetadata: ColumnMetaData
|
| 462 |
+
range: ByteRange
|
| 463 |
+
}
|
| 464 |
+
// column chunk with offset index pending
|
| 465 |
+
interface ChunkOffsetIndexed {
|
| 466 |
+
columnMetadata: ColumnMetaData
|
| 467 |
+
offsetIndex: ByteRange
|
| 468 |
+
range: ByteRange
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
export interface ColumnDecoder {
|
| 472 |
+
pathInSchema: string[]
|
| 473 |
+
type: ParquetType
|
| 474 |
+
element: SchemaElement
|
| 475 |
+
schemaPath: SchemaTree[]
|
| 476 |
+
codec: CompressionCodec
|
| 477 |
+
parsers: ParquetParsers
|
| 478 |
+
compressors?: Compressors
|
| 479 |
+
utf8?: boolean
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
export interface RowGroupSelect {
|
| 483 |
+
groupStart: number // row index of the first row in the group
|
| 484 |
+
selectStart: number // row index in the group to start reading
|
| 485 |
+
selectEnd: number // row index in the group to stop reading
|
| 486 |
+
groupRows: number
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
export interface AsyncRowGroup {
|
| 490 |
+
groupStart: number
|
| 491 |
+
groupRows: number
|
| 492 |
+
asyncColumns: AsyncColumn[]
|
| 493 |
+
}
|
| 494 |
+
export interface AsyncColumn {
|
| 495 |
+
pathInSchema: string[]
|
| 496 |
+
data: Promise<AsyncPages>
|
| 497 |
+
}
|
| 498 |
+
interface AsyncPages {
|
| 499 |
+
skipped: number // rows skipped from groupStart to first row of this column data
|
| 500 |
+
data: DecodedArray[]
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
/**
|
| 504 |
+
* Geometry types based on the GeoJSON specification (RFC 7946)
|
| 505 |
+
*/
|
| 506 |
+
export type Geometry =
|
| 507 |
+
| Point
|
| 508 |
+
| MultiPoint
|
| 509 |
+
| LineString
|
| 510 |
+
| MultiLineString
|
| 511 |
+
| Polygon
|
| 512 |
+
| MultiPolygon
|
| 513 |
+
| GeometryCollection
|
| 514 |
+
|
| 515 |
+
/**
|
| 516 |
+
* Position is an array of at least two numbers.
|
| 517 |
+
* The order should be [longitude, latitude] with optional properties (eg- altitude).
|
| 518 |
+
*/
|
| 519 |
+
export type Position = number[]
|
| 520 |
+
|
| 521 |
+
export interface Point {
|
| 522 |
+
type: 'Point'
|
| 523 |
+
coordinates: Position
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
export interface MultiPoint {
|
| 527 |
+
type: 'MultiPoint'
|
| 528 |
+
coordinates: Position[]
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
export interface LineString {
|
| 532 |
+
type: 'LineString'
|
| 533 |
+
coordinates: Position[]
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
/**
|
| 537 |
+
* Each element is one LineString.
|
| 538 |
+
*/
|
| 539 |
+
export interface MultiLineString {
|
| 540 |
+
type: 'MultiLineString'
|
| 541 |
+
coordinates: Position[][]
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
/**
|
| 545 |
+
* Each element is a linear ring.
|
| 546 |
+
*/
|
| 547 |
+
export interface Polygon {
|
| 548 |
+
type: 'Polygon'
|
| 549 |
+
coordinates: Position[][]
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
/**
|
| 553 |
+
* Each element is one Polygon.
|
| 554 |
+
*/
|
| 555 |
+
export interface MultiPolygon {
|
| 556 |
+
type: 'MultiPolygon'
|
| 557 |
+
coordinates: Position[][][]
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
export interface GeometryCollection {
|
| 561 |
+
type: 'GeometryCollection'
|
| 562 |
+
geometries: Geometry[]
|
| 563 |
+
}
|
web/node_modules/hyparquet/src/utils.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @import {AsyncBuffer, Awaitable, DecodedArray} from '../src/types.js'
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import { defaultInitialFetchSize } from './metadata.js'
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Replace bigint, date, etc with legal JSON types.
|
| 9 |
+
*
|
| 10 |
+
* @param {any} obj object to convert
|
| 11 |
+
* @returns {unknown} converted object
|
| 12 |
+
*/
|
| 13 |
+
export function toJson(obj) {
|
| 14 |
+
if (obj === undefined) return null
|
| 15 |
+
if (typeof obj === 'bigint') return Number(obj)
|
| 16 |
+
if (Object.is(obj, -0)) return 0
|
| 17 |
+
if (Array.isArray(obj)) return obj.map(toJson)
|
| 18 |
+
if (obj instanceof Uint8Array) return Array.from(obj)
|
| 19 |
+
if (obj instanceof Date) return obj.toISOString()
|
| 20 |
+
if (obj instanceof Object) {
|
| 21 |
+
/** @type {Record<string, unknown>} */
|
| 22 |
+
const newObj = {}
|
| 23 |
+
for (const key of Object.keys(obj)) {
|
| 24 |
+
if (obj[key] === undefined) continue
|
| 25 |
+
newObj[key] = toJson(obj[key])
|
| 26 |
+
}
|
| 27 |
+
return newObj
|
| 28 |
+
}
|
| 29 |
+
return obj
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* Concatenate two arrays fast.
|
| 34 |
+
*
|
| 35 |
+
* @param {any[]} aaa
|
| 36 |
+
* @param {DecodedArray} bbb
|
| 37 |
+
*/
|
| 38 |
+
export function concat(aaa, bbb) {
|
| 39 |
+
const chunk = 10000
|
| 40 |
+
for (let i = 0; i < bbb.length; i += chunk) {
|
| 41 |
+
aaa.push(...bbb.slice(i, i + chunk))
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/**
|
| 46 |
+
* Deep equality.
|
| 47 |
+
*
|
| 48 |
+
* @param {any} a
|
| 49 |
+
* @param {any} b
|
| 50 |
+
* @param {boolean} [strict]
|
| 51 |
+
* @returns {boolean}
|
| 52 |
+
*/
|
| 53 |
+
export function equals(a, b, strict = true) {
|
| 54 |
+
// eslint-disable-next-line eqeqeq
|
| 55 |
+
if (strict ? a === b : a == b) return true
|
| 56 |
+
if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false
|
| 57 |
+
|
| 58 |
+
if (a instanceof Uint8Array && b instanceof Uint8Array) {
|
| 59 |
+
if (a.length !== b.length) return false
|
| 60 |
+
for (let i = 0; i < a.length; i++) {
|
| 61 |
+
if (a[i] !== b[i]) return false
|
| 62 |
+
}
|
| 63 |
+
return true
|
| 64 |
+
}
|
| 65 |
+
if (Array.isArray(a) && Array.isArray(b)) {
|
| 66 |
+
if (a.length !== b.length) return false
|
| 67 |
+
for (let i = 0; i < a.length; i++) {
|
| 68 |
+
if (!equals(a[i], b[i], strict)) return false
|
| 69 |
+
}
|
| 70 |
+
return true
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
const aKeys = Object.keys(a)
|
| 74 |
+
if (aKeys.length !== Object.keys(b).length) return false
|
| 75 |
+
for (const k of aKeys) {
|
| 76 |
+
if (!equals(a[k], b[k], strict)) return false
|
| 77 |
+
}
|
| 78 |
+
return true
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/**
|
| 82 |
+
* Get the byte length using fetch with a ranged GET request.
|
| 83 |
+
* Aborts the request if server returns 200 instead of 206.
|
| 84 |
+
*
|
| 85 |
+
* @param {string} url
|
| 86 |
+
* @param {RequestInit} [requestInit] fetch options
|
| 87 |
+
* @param {typeof globalThis.fetch} [fetchFn] fetch function to use
|
| 88 |
+
* @returns {Promise<number>}
|
| 89 |
+
*/
|
| 90 |
+
async function byteLengthFromUrlUsingGet(url, requestInit = {}, fetchFn = globalThis.fetch) {
|
| 91 |
+
const controller = new AbortController()
|
| 92 |
+
const headers = new Headers(requestInit.headers)
|
| 93 |
+
headers.set('Range', 'bytes=0-0')
|
| 94 |
+
|
| 95 |
+
const res = await fetchFn(url, {
|
| 96 |
+
...requestInit,
|
| 97 |
+
headers,
|
| 98 |
+
signal: controller.signal,
|
| 99 |
+
})
|
| 100 |
+
|
| 101 |
+
if (!res.ok) throw new Error(`fetch with range failed ${res.status}`)
|
| 102 |
+
|
| 103 |
+
// Server supports Range requests (206 Partial Content)
|
| 104 |
+
if (res.status === 206) {
|
| 105 |
+
const contentRange = res.headers.get('Content-Range')
|
| 106 |
+
if (!contentRange) throw new Error('missing content-range header')
|
| 107 |
+
|
| 108 |
+
// Parse "bytes 0-0/9446073" to get total length
|
| 109 |
+
const match = contentRange.match(/bytes \d+-\d+\/(\d+)/)
|
| 110 |
+
if (!match) throw new Error(`invalid content-range header: ${contentRange}`)
|
| 111 |
+
|
| 112 |
+
return parseInt(match[1])
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Server ignored Range and returned 200 - get Content-Length and abort request
|
| 116 |
+
if (res.status === 200) {
|
| 117 |
+
const contentLength = res.headers.get('Content-Length')
|
| 118 |
+
|
| 119 |
+
// Abort the request to stop any ongoing download
|
| 120 |
+
controller.abort()
|
| 121 |
+
|
| 122 |
+
if (contentLength) return parseInt(contentLength)
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
throw new Error('server does not support range requests and missing content-length')
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
/**
|
| 129 |
+
* Get the byte length of a URL using a HEAD request.
|
| 130 |
+
* If HEAD fails with 403 (e.g., with signed S3 URLs), falls back to a ranged GET request.
|
| 131 |
+
* If HEAD succeeds but Content-Length is missing, falls back to GET with range.
|
| 132 |
+
* If requestInit is provided, it will be passed to fetch.
|
| 133 |
+
*
|
| 134 |
+
* @param {string} url
|
| 135 |
+
* @param {RequestInit} [requestInit] fetch options
|
| 136 |
+
* @param {typeof globalThis.fetch} [customFetch] fetch function to use
|
| 137 |
+
* @returns {Promise<number>}
|
| 138 |
+
*/
|
| 139 |
+
export async function byteLengthFromUrl(url, requestInit, customFetch) {
|
| 140 |
+
const fetch = customFetch ?? globalThis.fetch
|
| 141 |
+
const res = await fetch(url, { ...requestInit, method: 'HEAD' })
|
| 142 |
+
|
| 143 |
+
// If HEAD request is forbidden (common with signed S3 URLs), try GET with range
|
| 144 |
+
if (res.status === 403) {
|
| 145 |
+
return byteLengthFromUrlUsingGet(url, requestInit, fetch)
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
if (!res.ok) throw new Error(`fetch head failed ${res.status}`)
|
| 149 |
+
const length = res.headers.get('Content-Length')
|
| 150 |
+
// If Content-Length is missing from HEAD, fallback to GET with range
|
| 151 |
+
if (!length) {
|
| 152 |
+
return byteLengthFromUrlUsingGet(url, requestInit, fetch)
|
| 153 |
+
}
|
| 154 |
+
return parseInt(length)
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
/**
|
| 158 |
+
* Construct an AsyncBuffer for a URL.
|
| 159 |
+
* If byteLength is not provided, will make a HEAD request to get the file size.
|
| 160 |
+
* If fetch is provided, it will be used instead of the global fetch.
|
| 161 |
+
* If requestInit is provided, it will be passed to fetch.
|
| 162 |
+
*
|
| 163 |
+
* @param {object} options
|
| 164 |
+
* @param {string} options.url
|
| 165 |
+
* @param {number} [options.byteLength]
|
| 166 |
+
* @param {typeof globalThis.fetch} [options.fetch] fetch function to use
|
| 167 |
+
* @param {RequestInit} [options.requestInit]
|
| 168 |
+
* @returns {Promise<AsyncBuffer>}
|
| 169 |
+
*/
|
| 170 |
+
export async function asyncBufferFromUrl({ url, byteLength, requestInit, fetch: customFetch }) {
|
| 171 |
+
if (!url) throw new Error('missing url')
|
| 172 |
+
const fetch = customFetch ?? globalThis.fetch
|
| 173 |
+
// byte length from HEAD request
|
| 174 |
+
byteLength ??= await byteLengthFromUrl(url, requestInit, fetch)
|
| 175 |
+
|
| 176 |
+
/**
|
| 177 |
+
* A promise for the whole buffer, if range requests are not supported.
|
| 178 |
+
* @type {Promise<ArrayBuffer>|undefined}
|
| 179 |
+
*/
|
| 180 |
+
let buffer = undefined
|
| 181 |
+
const init = requestInit || {}
|
| 182 |
+
|
| 183 |
+
return {
|
| 184 |
+
byteLength,
|
| 185 |
+
async slice(start, end) {
|
| 186 |
+
if (buffer) {
|
| 187 |
+
return buffer.then(buffer => buffer.slice(start, end))
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
const headers = new Headers(init.headers)
|
| 191 |
+
const endStr = end === undefined ? '' : end - 1
|
| 192 |
+
headers.set('Range', `bytes=${start}-${endStr}`)
|
| 193 |
+
|
| 194 |
+
const res = await fetch(url, { ...init, headers })
|
| 195 |
+
if (!res.ok || !res.body) throw new Error(`fetch failed ${res.status}`)
|
| 196 |
+
|
| 197 |
+
if (res.status === 200) {
|
| 198 |
+
// Endpoint does not support range requests and returned the whole object
|
| 199 |
+
buffer = res.arrayBuffer()
|
| 200 |
+
return buffer.then(buffer => buffer.slice(start, end))
|
| 201 |
+
} else if (res.status === 206) {
|
| 202 |
+
// The endpoint supports range requests and sent us the requested range
|
| 203 |
+
return res.arrayBuffer()
|
| 204 |
+
} else {
|
| 205 |
+
throw new Error(`fetch received unexpected status code ${res.status}`)
|
| 206 |
+
}
|
| 207 |
+
},
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
/**
|
| 212 |
+
* Returns a cached layer on top of an AsyncBuffer. For caching slices of a file
|
| 213 |
+
* that are read multiple times, possibly over a network.
|
| 214 |
+
*
|
| 215 |
+
* @param {AsyncBuffer} file file-like object to cache
|
| 216 |
+
* @param {{ minSize?: number }} [options]
|
| 217 |
+
* @returns {AsyncBuffer} cached file-like object
|
| 218 |
+
*/
|
| 219 |
+
export function cachedAsyncBuffer({ byteLength, slice }, { minSize = defaultInitialFetchSize } = {}) {
|
| 220 |
+
if (byteLength < minSize) {
|
| 221 |
+
// Cache whole file if it's small
|
| 222 |
+
const buffer = slice(0, byteLength)
|
| 223 |
+
return {
|
| 224 |
+
byteLength,
|
| 225 |
+
async slice(start, end) {
|
| 226 |
+
return (await buffer).slice(start, end)
|
| 227 |
+
},
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
const cache = new Map()
|
| 231 |
+
return {
|
| 232 |
+
byteLength,
|
| 233 |
+
/**
|
| 234 |
+
* @param {number} start
|
| 235 |
+
* @param {number} [end]
|
| 236 |
+
* @returns {Awaitable<ArrayBuffer>}
|
| 237 |
+
*/
|
| 238 |
+
slice(start, end) {
|
| 239 |
+
const key = cacheKey(start, end, byteLength)
|
| 240 |
+
const cached = cache.get(key)
|
| 241 |
+
if (cached) return cached
|
| 242 |
+
// cache miss, read from file
|
| 243 |
+
const promise = slice(start, end)
|
| 244 |
+
cache.set(key, promise)
|
| 245 |
+
return promise
|
| 246 |
+
},
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
/**
|
| 252 |
+
* Returns canonical cache key for a byte range 'start,end'.
|
| 253 |
+
* Normalize int-range and suffix-range requests to the same key.
|
| 254 |
+
*
|
| 255 |
+
* @param {number} start start byte of range
|
| 256 |
+
* @param {number} [end] end byte of range, or undefined for suffix range
|
| 257 |
+
* @param {number} [size] size of file, or undefined for suffix range
|
| 258 |
+
* @returns {string}
|
| 259 |
+
*/
|
| 260 |
+
function cacheKey(start, end, size) {
|
| 261 |
+
if (start < 0) {
|
| 262 |
+
if (end !== undefined) throw new Error(`invalid suffix range [${start}, ${end}]`)
|
| 263 |
+
if (size === undefined) return `${start},`
|
| 264 |
+
return `${size + start},${size}`
|
| 265 |
+
} else if (end !== undefined) {
|
| 266 |
+
if (start > end) throw new Error(`invalid empty range [${start}, ${end}]`)
|
| 267 |
+
return `${start},${end}`
|
| 268 |
+
} else if (size === undefined) {
|
| 269 |
+
return `${start},`
|
| 270 |
+
} else {
|
| 271 |
+
return `${start},${size}`
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
/**
|
| 276 |
+
* Flatten a list of lists into a single list.
|
| 277 |
+
*
|
| 278 |
+
* @param {DecodedArray[]} [chunks]
|
| 279 |
+
* @returns {DecodedArray}
|
| 280 |
+
*/
|
| 281 |
+
export function flatten(chunks) {
|
| 282 |
+
if (!chunks) return []
|
| 283 |
+
if (chunks.length === 1) return chunks[0]
|
| 284 |
+
/** @type {any[]} */
|
| 285 |
+
const output = []
|
| 286 |
+
for (const chunk of chunks) {
|
| 287 |
+
concat(output, chunk)
|
| 288 |
+
}
|
| 289 |
+
return output
|
| 290 |
+
}
|
web/node_modules/hyparquet/src/variant.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { DEFAULT_PARSERS } from './convert.js'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* @import {DataReader, ParquetParsers, VariantMetadata} from '../src/types.js'
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
const decoder = new TextDecoder()
|
| 8 |
+
/** @type {WeakMap<object, Map<string, VariantMetadata>>} */
|
| 9 |
+
const metadataCache = new WeakMap()
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Recursively decode variant structs into native values.
|
| 13 |
+
*
|
| 14 |
+
* @param {any} value
|
| 15 |
+
* @param {ParquetParsers} [parsers]
|
| 16 |
+
* @returns {any}
|
| 17 |
+
*/
|
| 18 |
+
export function decodeVariantColumn(value, parsers = DEFAULT_PARSERS) {
|
| 19 |
+
if (Array.isArray(value)) {
|
| 20 |
+
return value.map(entry => decodeVariantColumn(entry, parsers))
|
| 21 |
+
}
|
| 22 |
+
if (typeof value !== 'object') return value
|
| 23 |
+
|
| 24 |
+
if ('metadata' in value) {
|
| 25 |
+
const metadata = parseVariantMetadata(value.metadata)
|
| 26 |
+
|
| 27 |
+
// Decode shredded fields from typed_value
|
| 28 |
+
const shreddedFields = value.typed_value && decodeTypedValue(value.typed_value, metadata, parsers)
|
| 29 |
+
|
| 30 |
+
// Decode binary value (may contain additional fields for partially shredded objects)
|
| 31 |
+
const binaryValue = value.value && readVariant(makeReader(value.value), metadata, parsers)
|
| 32 |
+
|
| 33 |
+
// Merge shredded and binary values for partially shredded objects
|
| 34 |
+
if (shreddedFields && binaryValue) {
|
| 35 |
+
return { ...binaryValue, ...shreddedFields }
|
| 36 |
+
}
|
| 37 |
+
return shreddedFields ?? binaryValue
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
return value
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* Decode a shredded variant typed_value field.
|
| 45 |
+
*
|
| 46 |
+
* @param {any} typedValue
|
| 47 |
+
* @param {VariantMetadata} metadata
|
| 48 |
+
* @param {ParquetParsers} parsers
|
| 49 |
+
* @returns {any}
|
| 50 |
+
*/
|
| 51 |
+
function decodeTypedValue(typedValue, metadata, parsers) {
|
| 52 |
+
if (typedValue instanceof Date) return typedValue
|
| 53 |
+
|
| 54 |
+
// Handle {typed_value, value} wrapper - unwrap and recurse
|
| 55 |
+
if (typedValue && typeof typedValue === 'object' && !Array.isArray(typedValue) && !(typedValue instanceof Uint8Array)) {
|
| 56 |
+
if ('typed_value' in typedValue && typedValue.typed_value !== null && typedValue.typed_value !== undefined) {
|
| 57 |
+
return decodeTypedValue(typedValue.typed_value, metadata, parsers)
|
| 58 |
+
}
|
| 59 |
+
if ('value' in typedValue && typedValue.value instanceof Uint8Array) {
|
| 60 |
+
return readVariant(makeReader(typedValue.value), metadata, parsers)
|
| 61 |
+
}
|
| 62 |
+
if ('typed_value' in typedValue || 'value' in typedValue) {
|
| 63 |
+
// both null/missing → field is null
|
| 64 |
+
return null
|
| 65 |
+
}
|
| 66 |
+
// Shredded object: each field value gets decoded
|
| 67 |
+
/** @type {Record<string, any>} */
|
| 68 |
+
const result = {}
|
| 69 |
+
for (const [key, field] of Object.entries(typedValue)) {
|
| 70 |
+
if (!metadata.dictionary.includes(key)) continue
|
| 71 |
+
result[key] = decodeTypedValue(field, metadata, parsers)
|
| 72 |
+
}
|
| 73 |
+
return result
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// Uint8Array: decode as binary variant
|
| 77 |
+
if (typedValue instanceof Uint8Array) {
|
| 78 |
+
return readVariant(makeReader(typedValue), metadata, parsers)
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// Arrays
|
| 82 |
+
if (Array.isArray(typedValue)) {
|
| 83 |
+
return typedValue.map(element => decodeTypedValue(element, metadata, parsers))
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
return typedValue
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/**
|
| 90 |
+
* @param {Uint8Array} bytes
|
| 91 |
+
* @returns {DataReader}
|
| 92 |
+
*/
|
| 93 |
+
function makeReader(bytes) {
|
| 94 |
+
return { view: new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength), offset: 0 }
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
/**
|
| 98 |
+
* Parse and cache variant metadata dictionary.
|
| 99 |
+
*
|
| 100 |
+
* @param {Uint8Array} bytes
|
| 101 |
+
* @returns {VariantMetadata}
|
| 102 |
+
*/
|
| 103 |
+
function parseVariantMetadata(bytes) {
|
| 104 |
+
let bufferCache = metadataCache.get(bytes.buffer)
|
| 105 |
+
if (!bufferCache) {
|
| 106 |
+
bufferCache = new Map()
|
| 107 |
+
metadataCache.set(bytes.buffer, bufferCache)
|
| 108 |
+
}
|
| 109 |
+
const key = `${bytes.byteOffset}:${bytes.byteLength}`
|
| 110 |
+
const cached = bufferCache.get(key)
|
| 111 |
+
if (cached) return cached
|
| 112 |
+
|
| 113 |
+
const reader = makeReader(bytes)
|
| 114 |
+
const header = reader.view.getUint8(reader.offset++)
|
| 115 |
+
const version = header & 0x0f
|
| 116 |
+
if (version !== 1) throw new Error(`parquet unsupported variant metadata version: ${version}`)
|
| 117 |
+
const sorted = (header >> 4 & 0x1) === 1
|
| 118 |
+
const offsetSize = (header >> 6 & 0x3) + 1
|
| 119 |
+
|
| 120 |
+
const dictionarySize = readUnsigned(reader, offsetSize)
|
| 121 |
+
|
| 122 |
+
const offsets = new Array(dictionarySize + 1)
|
| 123 |
+
for (let i = 0; i < offsets.length; i++) {
|
| 124 |
+
offsets[i] = readUnsigned(reader, offsetSize)
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
const base = reader.offset
|
| 128 |
+
const dictionary = new Array(dictionarySize)
|
| 129 |
+
for (let i = 0; i < dictionarySize; i++) {
|
| 130 |
+
const start = offsets[i]
|
| 131 |
+
const end = offsets[i + 1]
|
| 132 |
+
const strBytes = new Uint8Array(bytes.buffer, bytes.byteOffset + base + start, end - start)
|
| 133 |
+
dictionary[i] = decoder.decode(strBytes)
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
const metadata = { dictionary, sorted }
|
| 137 |
+
bufferCache.set(key, metadata)
|
| 138 |
+
return metadata
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/**
|
| 142 |
+
* @param {DataReader} reader
|
| 143 |
+
* @param {number} byteWidth
|
| 144 |
+
* @returns {number}
|
| 145 |
+
*/
|
| 146 |
+
function readUnsigned(reader, byteWidth) {
|
| 147 |
+
let value = 0
|
| 148 |
+
for (let i = 0; i < byteWidth; i++) {
|
| 149 |
+
value |= reader.view.getUint8(reader.offset + i) << i * 8
|
| 150 |
+
}
|
| 151 |
+
reader.offset += byteWidth
|
| 152 |
+
return value
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
/**
|
| 156 |
+
* @param {DataReader} reader
|
| 157 |
+
* @param {VariantMetadata} metadata
|
| 158 |
+
* @param {ParquetParsers} parsers
|
| 159 |
+
* @returns {any}
|
| 160 |
+
*/
|
| 161 |
+
function readVariant(reader, metadata, parsers) {
|
| 162 |
+
const typeByte = reader.view.getUint8(reader.offset++)
|
| 163 |
+
const basicType = typeByte & 0x3
|
| 164 |
+
const header = typeByte >> 2
|
| 165 |
+
if (basicType === 0) return readVariantPrimitive(reader, header, parsers)
|
| 166 |
+
if (basicType === 2) return readVariantObject(reader, header, metadata, parsers)
|
| 167 |
+
if (basicType === 3) return readVariantArray(reader, header, metadata, parsers)
|
| 168 |
+
// else short string
|
| 169 |
+
const bytes = new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, header)
|
| 170 |
+
reader.offset += header
|
| 171 |
+
return decoder.decode(bytes)
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
/**
|
| 175 |
+
* @param {DataReader} reader
|
| 176 |
+
* @param {number} typeId
|
| 177 |
+
* @param {ParquetParsers} parsers
|
| 178 |
+
* @returns {any}
|
| 179 |
+
*/
|
| 180 |
+
function readVariantPrimitive(reader, typeId, parsers) {
|
| 181 |
+
switch (typeId) {
|
| 182 |
+
case 0: return null
|
| 183 |
+
case 1: return true
|
| 184 |
+
case 2: return false
|
| 185 |
+
case 3: {
|
| 186 |
+
const value = reader.view.getInt8(reader.offset)
|
| 187 |
+
reader.offset += 1
|
| 188 |
+
return value
|
| 189 |
+
}
|
| 190 |
+
case 4: {
|
| 191 |
+
const value = reader.view.getInt16(reader.offset, true)
|
| 192 |
+
reader.offset += 2
|
| 193 |
+
return value
|
| 194 |
+
}
|
| 195 |
+
case 5: {
|
| 196 |
+
const value = reader.view.getInt32(reader.offset, true)
|
| 197 |
+
reader.offset += 4
|
| 198 |
+
return value
|
| 199 |
+
}
|
| 200 |
+
case 6: {
|
| 201 |
+
const value = reader.view.getBigInt64(reader.offset, true)
|
| 202 |
+
reader.offset += 8
|
| 203 |
+
return value
|
| 204 |
+
}
|
| 205 |
+
case 7: {
|
| 206 |
+
const value = reader.view.getFloat64(reader.offset, true)
|
| 207 |
+
reader.offset += 8
|
| 208 |
+
return value
|
| 209 |
+
}
|
| 210 |
+
case 8:
|
| 211 |
+
return readVariantDecimal(reader, 4)
|
| 212 |
+
case 9:
|
| 213 |
+
return readVariantDecimal(reader, 8)
|
| 214 |
+
case 10:
|
| 215 |
+
return readVariantDecimal(reader, 16)
|
| 216 |
+
case 11: {
|
| 217 |
+
const value = reader.view.getInt32(reader.offset, true)
|
| 218 |
+
reader.offset += 4
|
| 219 |
+
return parsers.dateFromDays(value)
|
| 220 |
+
}
|
| 221 |
+
case 12: // timestamp_micros (utc)
|
| 222 |
+
case 13: { // timestamp_micros_ntz (no timezone)
|
| 223 |
+
const value = reader.view.getBigInt64(reader.offset, true)
|
| 224 |
+
reader.offset += 8
|
| 225 |
+
return parsers.timestampFromMicroseconds(value)
|
| 226 |
+
}
|
| 227 |
+
case 14: {
|
| 228 |
+
const value = reader.view.getFloat32(reader.offset, true)
|
| 229 |
+
reader.offset += 4
|
| 230 |
+
return value
|
| 231 |
+
}
|
| 232 |
+
case 15:
|
| 233 |
+
return readVariantBinary(reader)
|
| 234 |
+
case 16: {
|
| 235 |
+
const bytes = readVariantBinary(reader)
|
| 236 |
+
return decoder.decode(bytes)
|
| 237 |
+
}
|
| 238 |
+
case 17: {
|
| 239 |
+
// time: microseconds since midnight
|
| 240 |
+
const value = reader.view.getBigInt64(reader.offset, true)
|
| 241 |
+
reader.offset += 8
|
| 242 |
+
return value
|
| 243 |
+
}
|
| 244 |
+
case 18: // timestamp_nanos (utc)
|
| 245 |
+
case 19: { // timestamp_nanos_ntz (no timezone)
|
| 246 |
+
const value = reader.view.getBigInt64(reader.offset, true)
|
| 247 |
+
reader.offset += 8
|
| 248 |
+
return parsers.timestampFromNanoseconds(value)
|
| 249 |
+
}
|
| 250 |
+
case 20: {
|
| 251 |
+
const bytes = new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, 16)
|
| 252 |
+
reader.offset += 16
|
| 253 |
+
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('')
|
| 254 |
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
|
| 255 |
+
}
|
| 256 |
+
default:
|
| 257 |
+
throw new Error(`parquet unsupported variant primitive type: ${typeId}`)
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
/**
|
| 262 |
+
* @param {DataReader} reader
|
| 263 |
+
* @param {number} header
|
| 264 |
+
* @param {VariantMetadata} metadata
|
| 265 |
+
* @param {ParquetParsers} parsers
|
| 266 |
+
* @returns {Record<string, any>}
|
| 267 |
+
*/
|
| 268 |
+
function readVariantObject(reader, header, metadata, parsers) {
|
| 269 |
+
const offsetWidth = (header & 0x3) + 1
|
| 270 |
+
const idWidth = (header >> 2 & 0x3) + 1
|
| 271 |
+
const isLarge = header >> 4 & 0x1
|
| 272 |
+
const numElements = isLarge ? readUnsigned(reader, 4) : reader.view.getUint8(reader.offset++)
|
| 273 |
+
|
| 274 |
+
/** @type {number[]} */
|
| 275 |
+
const fieldIds = new Array(numElements)
|
| 276 |
+
for (let i = 0; i < numElements; i++) {
|
| 277 |
+
fieldIds[i] = readUnsigned(reader, idWidth)
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
const offsets = new Array(numElements + 1)
|
| 281 |
+
for (let i = 0; i < offsets.length; i++) {
|
| 282 |
+
offsets[i] = readUnsigned(reader, offsetWidth)
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
/** @type {Record<string, any>} */
|
| 286 |
+
const out = {}
|
| 287 |
+
for (let i = 0; i < numElements; i++) {
|
| 288 |
+
const key = metadata.dictionary[fieldIds[i]]
|
| 289 |
+
// Read value at the given offset
|
| 290 |
+
const valueReader = {
|
| 291 |
+
view: reader.view,
|
| 292 |
+
offset: reader.offset + offsets[i],
|
| 293 |
+
}
|
| 294 |
+
out[key] = readVariant(valueReader, metadata, parsers)
|
| 295 |
+
}
|
| 296 |
+
reader.offset += offsets[offsets.length - 1]
|
| 297 |
+
return out
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
/**
|
| 301 |
+
* @param {DataReader} reader
|
| 302 |
+
* @param {number} header
|
| 303 |
+
* @param {VariantMetadata} metadata
|
| 304 |
+
* @param {ParquetParsers} parsers
|
| 305 |
+
* @returns {any[]}
|
| 306 |
+
*/
|
| 307 |
+
function readVariantArray(reader, header, metadata, parsers) {
|
| 308 |
+
const fieldOffsetSize = header & 0x3
|
| 309 |
+
const isLarge = header >> 2 & 0x1
|
| 310 |
+
const offsetWidth = fieldOffsetSize + 1
|
| 311 |
+
const numElements = readUnsigned(reader, isLarge ? 4 : 1)
|
| 312 |
+
|
| 313 |
+
const offsets = new Array(numElements + 1)
|
| 314 |
+
for (let i = 0; i < offsets.length; i++) {
|
| 315 |
+
offsets[i] = readUnsigned(reader, offsetWidth)
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
const valuesStart = reader.offset
|
| 319 |
+
const result = new Array(numElements)
|
| 320 |
+
for (let i = 0; i < numElements; i++) {
|
| 321 |
+
const valueReader = {
|
| 322 |
+
view: reader.view,
|
| 323 |
+
offset: valuesStart + offsets[i],
|
| 324 |
+
}
|
| 325 |
+
result[i] = readVariant(valueReader, metadata, parsers)
|
| 326 |
+
}
|
| 327 |
+
reader.offset = valuesStart + offsets[offsets.length - 1]
|
| 328 |
+
return result
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
/**
|
| 332 |
+
* @param {DataReader} reader
|
| 333 |
+
* @param {number} width
|
| 334 |
+
* @returns {number}
|
| 335 |
+
*/
|
| 336 |
+
function readVariantDecimal(reader, width) {
|
| 337 |
+
const scale = reader.view.getUint8(reader.offset)
|
| 338 |
+
reader.offset += 1
|
| 339 |
+
let unscaled
|
| 340 |
+
if (width === 4) {
|
| 341 |
+
unscaled = BigInt(reader.view.getInt32(reader.offset, true))
|
| 342 |
+
reader.offset += 4
|
| 343 |
+
} else if (width === 8) {
|
| 344 |
+
unscaled = reader.view.getBigInt64(reader.offset, true)
|
| 345 |
+
reader.offset += 8
|
| 346 |
+
} else {
|
| 347 |
+
const low = reader.view.getBigUint64(reader.offset, true)
|
| 348 |
+
const high = reader.view.getBigInt64(reader.offset + 8, true)
|
| 349 |
+
unscaled = high << 64n | low
|
| 350 |
+
reader.offset += 16
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
return Number(unscaled) * 10 ** -scale
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
/**
|
| 357 |
+
* @param {DataReader} reader
|
| 358 |
+
* @returns {Uint8Array}
|
| 359 |
+
*/
|
| 360 |
+
function readVariantBinary(reader) {
|
| 361 |
+
const length = reader.view.getUint32(reader.offset, true)
|
| 362 |
+
reader.offset += 4
|
| 363 |
+
const bytes = new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, length)
|
| 364 |
+
reader.offset += length
|
| 365 |
+
return bytes
|
| 366 |
+
}
|
web/node_modules/hyparquet/src/wkb.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* WKB (Well-Known Binary) decoder for geometry objects.
|
| 3 |
+
*
|
| 4 |
+
* @param {DataReader} reader
|
| 5 |
+
* @returns {Geometry} geometry object
|
| 6 |
+
*/
|
| 7 |
+
export function wkbToGeojson(reader) {
|
| 8 |
+
const flags = getFlags(reader)
|
| 9 |
+
|
| 10 |
+
if (flags.type === 1) { // Point
|
| 11 |
+
return { type: 'Point', coordinates: readPosition(reader, flags) }
|
| 12 |
+
} else if (flags.type === 2) { // LineString
|
| 13 |
+
return { type: 'LineString', coordinates: readLine(reader, flags) }
|
| 14 |
+
} else if (flags.type === 3) { // Polygon
|
| 15 |
+
return { type: 'Polygon', coordinates: readPolygon(reader, flags) }
|
| 16 |
+
} else if (flags.type === 4) { // MultiPoint
|
| 17 |
+
const points = []
|
| 18 |
+
for (let i = 0; i < flags.count; i++) {
|
| 19 |
+
points.push(readPosition(reader, getFlags(reader)))
|
| 20 |
+
}
|
| 21 |
+
return { type: 'MultiPoint', coordinates: points }
|
| 22 |
+
} else if (flags.type === 5) { // MultiLineString
|
| 23 |
+
const lines = []
|
| 24 |
+
for (let i = 0; i < flags.count; i++) {
|
| 25 |
+
lines.push(readLine(reader, getFlags(reader)))
|
| 26 |
+
}
|
| 27 |
+
return { type: 'MultiLineString', coordinates: lines }
|
| 28 |
+
} else if (flags.type === 6) { // MultiPolygon
|
| 29 |
+
const polygons = []
|
| 30 |
+
for (let i = 0; i < flags.count; i++) {
|
| 31 |
+
polygons.push(readPolygon(reader, getFlags(reader)))
|
| 32 |
+
}
|
| 33 |
+
return { type: 'MultiPolygon', coordinates: polygons }
|
| 34 |
+
} else if (flags.type === 7) { // GeometryCollection
|
| 35 |
+
const geometries = []
|
| 36 |
+
for (let i = 0; i < flags.count; i++) {
|
| 37 |
+
geometries.push(wkbToGeojson(reader))
|
| 38 |
+
}
|
| 39 |
+
return { type: 'GeometryCollection', geometries }
|
| 40 |
+
} else {
|
| 41 |
+
throw new Error(`Unsupported geometry type: ${flags.type}`)
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/**
|
| 46 |
+
* Extract ISO WKB flags and base geometry type.
|
| 47 |
+
*
|
| 48 |
+
* @param {DataReader} reader
|
| 49 |
+
* @returns {WkbFlags}
|
| 50 |
+
*/
|
| 51 |
+
function getFlags(reader) {
|
| 52 |
+
const { view } = reader
|
| 53 |
+
const littleEndian = view.getUint8(reader.offset++) === 1
|
| 54 |
+
const rawType = view.getUint32(reader.offset, littleEndian)
|
| 55 |
+
reader.offset += 4
|
| 56 |
+
|
| 57 |
+
const type = rawType % 1000
|
| 58 |
+
const flags = Math.floor(rawType / 1000)
|
| 59 |
+
|
| 60 |
+
let count = 0
|
| 61 |
+
if (type > 1 && type <= 7) {
|
| 62 |
+
count = view.getUint32(reader.offset, littleEndian)
|
| 63 |
+
reader.offset += 4
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// XY, XYZ, XYM, XYZM
|
| 67 |
+
let dim = 2
|
| 68 |
+
if (flags) dim++
|
| 69 |
+
if (flags === 3) dim++
|
| 70 |
+
|
| 71 |
+
return { littleEndian, type, dim, count }
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/**
|
| 75 |
+
* @param {DataReader} reader
|
| 76 |
+
* @param {WkbFlags} flags
|
| 77 |
+
* @returns {number[]}
|
| 78 |
+
*/
|
| 79 |
+
function readPosition(reader, flags) {
|
| 80 |
+
const points = []
|
| 81 |
+
for (let i = 0; i < flags.dim; i++) {
|
| 82 |
+
const coord = reader.view.getFloat64(reader.offset, flags.littleEndian)
|
| 83 |
+
reader.offset += 8
|
| 84 |
+
points.push(coord)
|
| 85 |
+
}
|
| 86 |
+
return points
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/**
|
| 90 |
+
* @param {DataReader} reader
|
| 91 |
+
* @param {WkbFlags} flags
|
| 92 |
+
* @returns {number[][]}
|
| 93 |
+
*/
|
| 94 |
+
function readLine(reader, flags) {
|
| 95 |
+
const points = []
|
| 96 |
+
for (let i = 0; i < flags.count; i++) {
|
| 97 |
+
points.push(readPosition(reader, flags))
|
| 98 |
+
}
|
| 99 |
+
return points
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/**
|
| 103 |
+
* @param {DataReader} reader
|
| 104 |
+
* @param {WkbFlags} flags
|
| 105 |
+
* @returns {number[][][]}
|
| 106 |
+
*/
|
| 107 |
+
function readPolygon(reader, flags) {
|
| 108 |
+
const { view } = reader
|
| 109 |
+
const rings = []
|
| 110 |
+
for (let r = 0; r < flags.count; r++) {
|
| 111 |
+
const count = view.getUint32(reader.offset, flags.littleEndian)
|
| 112 |
+
reader.offset += 4
|
| 113 |
+
rings.push(readLine(reader, { ...flags, count }))
|
| 114 |
+
}
|
| 115 |
+
return rings
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/**
|
| 119 |
+
* @typedef {object} WkbFlags
|
| 120 |
+
* @property {boolean} littleEndian
|
| 121 |
+
* @property {number} type
|
| 122 |
+
* @property {number} dim
|
| 123 |
+
* @property {number} count
|
| 124 |
+
*/
|
| 125 |
+
|
| 126 |
+
/**
|
| 127 |
+
* @import {DataReader, Geometry} from '../src/types.js'
|
| 128 |
+
*/
|
web/node_modules/hyparquet/src/xxhash.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// XxHash64 (https://github.com/Cyan4973/xxHash/blob/dev/doc/xxhash_spec.md)
|
| 2 |
+
// Used by parquet bloom filters: hashes the PLAIN encoding of column values with seed 0.
|
| 3 |
+
|
| 4 |
+
const MASK = 0xffffffffffffffffn
|
| 5 |
+
const PRIME1 = 0x9e3779b185ebca87n
|
| 6 |
+
const PRIME2 = 0xc2b2ae3d27d4eb4fn
|
| 7 |
+
const PRIME3 = 0x165667b19e3779f9n
|
| 8 |
+
const PRIME4 = 0x85ebca77c2b2ae63n
|
| 9 |
+
const PRIME5 = 0x27d4eb2f165667c5n
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* @param {bigint} x
|
| 13 |
+
* @param {bigint} r rotation amount in bits (1..63)
|
| 14 |
+
* @returns {bigint}
|
| 15 |
+
*/
|
| 16 |
+
function rotl64(x, r) {
|
| 17 |
+
return (x << r | x >> 64n - r) & MASK
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* @param {bigint} acc
|
| 22 |
+
* @param {bigint} val
|
| 23 |
+
* @returns {bigint}
|
| 24 |
+
*/
|
| 25 |
+
function round(acc, val) {
|
| 26 |
+
acc = acc + val * PRIME2 & MASK
|
| 27 |
+
acc = rotl64(acc, 31n)
|
| 28 |
+
return acc * PRIME1 & MASK
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* @param {bigint} acc
|
| 33 |
+
* @param {bigint} val
|
| 34 |
+
* @returns {bigint}
|
| 35 |
+
*/
|
| 36 |
+
function mergeRound(acc, val) {
|
| 37 |
+
acc ^= round(0n, val)
|
| 38 |
+
return acc * PRIME1 + PRIME4 & MASK
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/**
|
| 42 |
+
* Compute the 64-bit xxHash of a byte buffer.
|
| 43 |
+
*
|
| 44 |
+
* @param {Uint8Array} input
|
| 45 |
+
* @param {bigint} [seed]
|
| 46 |
+
* @returns {bigint} 64-bit hash
|
| 47 |
+
*/
|
| 48 |
+
export function xxhash64(input, seed = 0n) {
|
| 49 |
+
const view = new DataView(input.buffer, input.byteOffset, input.byteLength)
|
| 50 |
+
const len = input.byteLength
|
| 51 |
+
let offset = 0
|
| 52 |
+
let h64
|
| 53 |
+
|
| 54 |
+
if (len >= 32) {
|
| 55 |
+
let v1 = seed + PRIME1 + PRIME2 & MASK
|
| 56 |
+
let v2 = seed + PRIME2 & MASK
|
| 57 |
+
let v3 = seed
|
| 58 |
+
let v4 = seed - PRIME1 & MASK
|
| 59 |
+
|
| 60 |
+
while (offset + 32 <= len) {
|
| 61 |
+
v1 = round(v1, view.getBigUint64(offset, true)); offset += 8
|
| 62 |
+
v2 = round(v2, view.getBigUint64(offset, true)); offset += 8
|
| 63 |
+
v3 = round(v3, view.getBigUint64(offset, true)); offset += 8
|
| 64 |
+
v4 = round(v4, view.getBigUint64(offset, true)); offset += 8
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
h64 = rotl64(v1, 1n) + rotl64(v2, 7n) + rotl64(v3, 12n) + rotl64(v4, 18n) & MASK
|
| 68 |
+
h64 = mergeRound(h64, v1)
|
| 69 |
+
h64 = mergeRound(h64, v2)
|
| 70 |
+
h64 = mergeRound(h64, v3)
|
| 71 |
+
h64 = mergeRound(h64, v4)
|
| 72 |
+
} else {
|
| 73 |
+
h64 = seed + PRIME5 & MASK
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
h64 = h64 + BigInt(len) & MASK
|
| 77 |
+
|
| 78 |
+
while (offset + 8 <= len) {
|
| 79 |
+
h64 ^= round(0n, view.getBigUint64(offset, true))
|
| 80 |
+
h64 = rotl64(h64, 27n) * PRIME1 + PRIME4 & MASK
|
| 81 |
+
offset += 8
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
if (offset + 4 <= len) {
|
| 85 |
+
h64 ^= BigInt(view.getUint32(offset, true)) * PRIME1 & MASK
|
| 86 |
+
h64 = rotl64(h64, 23n) * PRIME2 + PRIME3 & MASK
|
| 87 |
+
offset += 4
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
while (offset < len) {
|
| 91 |
+
h64 ^= BigInt(view.getUint8(offset)) * PRIME5 & MASK
|
| 92 |
+
h64 = rotl64(h64, 11n) * PRIME1 & MASK
|
| 93 |
+
offset += 1
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
h64 ^= h64 >> 33n
|
| 97 |
+
h64 = h64 * PRIME2 & MASK
|
| 98 |
+
h64 ^= h64 >> 29n
|
| 99 |
+
h64 = h64 * PRIME3 & MASK
|
| 100 |
+
h64 ^= h64 >> 32n
|
| 101 |
+
return h64
|
| 102 |
+
}
|
web/node_modules/hyparquet/types/assemble.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Reconstructs a complex nested structure from flat arrays of values and
|
| 3 |
+
* definition and repetition levels, according to Dremel encoding.
|
| 4 |
+
*
|
| 5 |
+
* @param {any[]} output
|
| 6 |
+
* @param {number[] | undefined} definitionLevels
|
| 7 |
+
* @param {number[]} repetitionLevels
|
| 8 |
+
* @param {DecodedArray} values
|
| 9 |
+
* @param {SchemaTree[]} schemaPath
|
| 10 |
+
* @returns {DecodedArray}
|
| 11 |
+
*/
|
| 12 |
+
export function assembleLists(output: any[], definitionLevels: number[] | undefined, repetitionLevels: number[], values: DecodedArray, schemaPath: SchemaTree[]): DecodedArray;
|
| 13 |
+
/**
|
| 14 |
+
* Assemble a nested structure from subcolumn data.
|
| 15 |
+
*
|
| 16 |
+
* @param {Map<string, DecodedArray>} subcolumnData
|
| 17 |
+
* @param {SchemaTree} schema top-level schema element
|
| 18 |
+
* @param {ParquetParsers} parsers
|
| 19 |
+
* @param {number} [depth] depth of nested structure
|
| 20 |
+
*/
|
| 21 |
+
export function assembleNested(subcolumnData: Map<string, DecodedArray>, schema: SchemaTree, parsers: ParquetParsers, depth?: number): void;
|
| 22 |
+
import type { DecodedArray } from '../src/types.js';
|
| 23 |
+
import type { SchemaTree } from '../src/types.js';
|
| 24 |
+
import type { ParquetParsers } from '../src/types.js';
|
| 25 |
+
//# sourceMappingURL=assemble.d.ts.map
|
web/node_modules/hyparquet/types/assemble.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version":3,"file":"assemble.d.ts","sourceRoot":"","sources":["../src/assemble.js"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,sCAPW,GAAG,EAAE,oBACL,MAAM,EAAE,GAAG,SAAS,oBACpB,MAAM,EAAE,UACR,YAAY,cACZ,UAAU,EAAE,GACV,YAAY,CA2FxB;AAED;;;;;;;GAOG;AACH,8CALW,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,UACzB,UAAU,WACV,cAAc,UACd,MAAM,QA2EhB;kCAG0D,iBAAiB;gCAAjB,iBAAiB;oCAAjB,iBAAiB"}
|
web/node_modules/hyparquet/types/bloom.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Insert a hash into a Split Block Bloom Filter.
|
| 3 |
+
*
|
| 4 |
+
* @param {Uint32Array} blocks bloom filter words (8 * numBlocks long)
|
| 5 |
+
* @param {bigint} hash 64-bit xxhash of the parquet-plain-encoded value
|
| 6 |
+
*/
|
| 7 |
+
export function sbbfInsert(blocks: Uint32Array, hash: bigint): void;
|
| 8 |
+
/**
|
| 9 |
+
* Test whether a hash might be present in a Split Block Bloom Filter.
|
| 10 |
+
* False positives are possible; false negatives are not.
|
| 11 |
+
*
|
| 12 |
+
* @param {Uint32Array} blocks bloom filter words (8 * numBlocks long)
|
| 13 |
+
* @param {bigint} hash 64-bit xxhash of the parquet-plain-encoded value
|
| 14 |
+
* @returns {boolean}
|
| 15 |
+
*/
|
| 16 |
+
export function sbbfContains(blocks: Uint32Array, hash: bigint): boolean;
|
| 17 |
+
/**
|
| 18 |
+
* Parse a Split Block Bloom Filter from a reader positioned at the BloomFilterHeader.
|
| 19 |
+
* Returns undefined when the header advertises an unsupported algorithm, hash, or
|
| 20 |
+
* compression — callers should treat that as "cannot use this bloom filter."
|
| 21 |
+
*
|
| 22 |
+
* @param {DataReader} reader
|
| 23 |
+
* @returns {BloomFilter | undefined}
|
| 24 |
+
*/
|
| 25 |
+
export function readBloomFilter(reader: DataReader): BloomFilter | undefined;
|
| 26 |
+
/**
|
| 27 |
+
* Hash a JS filter value as its parquet PLAIN-encoded bytes, suitable for a
|
| 28 |
+
* bloom filter lookup. Returns undefined when the column's parser is lossy or
|
| 29 |
+
* ambiguous (DATE, TIMESTAMP_*, DECIMAL, JSON, BSON, INT96, FLOAT16, UUID,
|
| 30 |
+
* GEOMETRY, GEOGRAPHY, INTERVAL) or when the JS value type doesn't match the
|
| 31 |
+
* column. Callers must treat undefined as "bloom filter cannot help."
|
| 32 |
+
*
|
| 33 |
+
* @param {any} value
|
| 34 |
+
* @param {SchemaElement} element
|
| 35 |
+
* @returns {bigint | undefined}
|
| 36 |
+
*/
|
| 37 |
+
export function hashParquetValue(value: any, element: SchemaElement): bigint | undefined;
|
| 38 |
+
/**
|
| 39 |
+
* Top-level column names that appear in $eq or $in predicates within a filter.
|
| 40 |
+
* These are the only columns where a bloom filter can prove a value's absence
|
| 41 |
+
* and let us skip a row group; any other operator can't be helped by a bloom.
|
| 42 |
+
*
|
| 43 |
+
* @param {ParquetQueryFilter | undefined} filter
|
| 44 |
+
* @returns {Set<string>}
|
| 45 |
+
*/
|
| 46 |
+
export function bloomEligibleColumns(filter: ParquetQueryFilter | undefined): Set<string>;
|
| 47 |
+
import type { DataReader } from '../src/types.js';
|
| 48 |
+
import type { BloomFilter } from '../src/types.js';
|
| 49 |
+
import type { SchemaElement } from '../src/types.js';
|
| 50 |
+
import type { ParquetQueryFilter } from '../src/types.js';
|
| 51 |
+
//# sourceMappingURL=bloom.d.ts.map
|
web/node_modules/hyparquet/types/bloom.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version":3,"file":"bloom.d.ts","sourceRoot":"","sources":["../src/bloom.js"],"names":[],"mappings":"AA6CA;;;;;GAKG;AACH,mCAHW,WAAW,QACX,MAAM,QAQhB;AAED;;;;;;;GAOG;AACH,qCAJW,WAAW,QACX,MAAM,GACJ,OAAO,CASnB;AAED;;;;;;;GAOG;AACH,wCAHW,UAAU,GACR,WAAW,GAAG,SAAS,CAsBnC;AAED;;;;;;;;;;GAUG;AACH,wCAJW,GAAG,WACH,aAAa,GACX,MAAM,GAAG,SAAS,CAkE9B;AAED;;;;;;;GAOG;AACH,6CAHW,kBAAkB,GAAG,SAAS,GAC5B,GAAG,CAAC,MAAM,CAAC,CAOvB;gCA3L4E,iBAAiB;iCAAjB,iBAAiB;mCAAjB,iBAAiB;wCAAjB,iBAAiB"}
|
web/node_modules/hyparquet/types/column.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Parse column data from a buffer.
|
| 3 |
+
*
|
| 4 |
+
* @param {DataReader} reader
|
| 5 |
+
* @param {RowGroupSelect} rowGroupSelect row group selection
|
| 6 |
+
* @param {ColumnDecoder} columnDecoder column decoder params
|
| 7 |
+
* @param {(chunk: SubColumnData) => void} [onPage] callback for each page
|
| 8 |
+
* @returns {{ data: DecodedArray[], skipped: number }}
|
| 9 |
+
*/
|
| 10 |
+
export function readColumn(reader: DataReader, { groupStart, selectStart, selectEnd }: RowGroupSelect, columnDecoder: ColumnDecoder, onPage?: (chunk: SubColumnData) => void): {
|
| 11 |
+
data: DecodedArray[];
|
| 12 |
+
skipped: number;
|
| 13 |
+
};
|
| 14 |
+
/**
|
| 15 |
+
* Read a page (data or dictionary) from a buffer.
|
| 16 |
+
*
|
| 17 |
+
* @param {DataReader} reader
|
| 18 |
+
* @param {PageHeader} header
|
| 19 |
+
* @param {ColumnDecoder} columnDecoder
|
| 20 |
+
* @param {DecodedArray | undefined} dictionary
|
| 21 |
+
* @param {DecodedArray | undefined} previousChunk
|
| 22 |
+
* @param {number} pageStart skip this many rows in the page
|
| 23 |
+
* @returns {PageResult}
|
| 24 |
+
*/
|
| 25 |
+
export function readPage(reader: DataReader, header: PageHeader, columnDecoder: ColumnDecoder, dictionary: DecodedArray | undefined, previousChunk: DecodedArray | undefined, pageStart: number): PageResult;
|
| 26 |
+
import type { DataReader } from '../src/types.js';
|
| 27 |
+
import type { RowGroupSelect } from '../src/types.js';
|
| 28 |
+
import type { ColumnDecoder } from '../src/types.js';
|
| 29 |
+
import type { SubColumnData } from '../src/types.js';
|
| 30 |
+
import type { DecodedArray } from '../src/types.js';
|
| 31 |
+
import type { PageHeader } from '../src/types.js';
|
| 32 |
+
import type { PageResult } from '../src/types.js';
|
| 33 |
+
//# sourceMappingURL=column.d.ts.map
|
web/node_modules/hyparquet/types/column.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version":3,"file":"column.d.ts","sourceRoot":"","sources":["../src/column.js"],"names":[],"mappings":"AAYA;;;;;;;;GAQG;AACH,mCANW,UAAU,0CACV,cAAc,iBACd,aAAa,WACb,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAC5B;IAAE,IAAI,EAAE,YAAY,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAsDrD;AAED;;;;;;;;;;GAUG;AACH,iCARW,UAAU,UACV,UAAU,iBACV,aAAa,cACb,YAAY,GAAG,SAAS,iBACxB,YAAY,GAAG,SAAS,aACxB,MAAM,GACJ,UAAU,CA4DtB;gCA/IgH,iBAAiB;oCAAjB,iBAAiB;mCAAjB,iBAAiB;mCAAjB,iBAAiB;kCAAjB,iBAAiB;gCAAjB,iBAAiB;gCAAjB,iBAAiB"}
|
web/node_modules/hyparquet/types/constants.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('../src/types.d.ts').ParquetType[]} */
|
| 2 |
+
export const ParquetTypes: import("../src/types.d.ts").ParquetType[];
|
| 3 |
+
/** @type {import('../src/types.d.ts').Encoding[]} */
|
| 4 |
+
export const Encodings: import("../src/types.d.ts").Encoding[];
|
| 5 |
+
/** @type {import('../src/types.d.ts').FieldRepetitionType[]} */
|
| 6 |
+
export const FieldRepetitionTypes: import("../src/types.d.ts").FieldRepetitionType[];
|
| 7 |
+
/** @type {import('../src/types.d.ts').ConvertedType[]} */
|
| 8 |
+
export const ConvertedTypes: import("../src/types.d.ts").ConvertedType[];
|
| 9 |
+
/** @type {import('../src/types.d.ts').CompressionCodec[]} */
|
| 10 |
+
export const CompressionCodecs: import("../src/types.d.ts").CompressionCodec[];
|
| 11 |
+
/** @type {import('../src/types.d.ts').PageType[]} */
|
| 12 |
+
export const PageTypes: import("../src/types.d.ts").PageType[];
|
| 13 |
+
/** @type {import('../src/types.d.ts').BoundaryOrder[]} */
|
| 14 |
+
export const BoundaryOrders: import("../src/types.d.ts").BoundaryOrder[];
|
| 15 |
+
/** @type {import('../src/types.d.ts').EdgeInterpolationAlgorithm[]} */
|
| 16 |
+
export const EdgeInterpolationAlgorithms: import("../src/types.d.ts").EdgeInterpolationAlgorithm[];
|
| 17 |
+
//# sourceMappingURL=constants.d.ts.map
|
web/node_modules/hyparquet/types/constants.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.js"],"names":[],"mappings":"AACA,wDAAwD;AACxD,2BADW,OAAO,mBAAmB,EAAE,WAAW,EAAE,CAUnD;AAED,qDAAqD;AACrD,wBADW,OAAO,mBAAmB,EAAE,QAAQ,EAAE,CAYhD;AAED,gEAAgE;AAChE,mCADW,OAAO,mBAAmB,EAAE,mBAAmB,EAAE,CAK3D;AAED,0DAA0D;AAC1D,6BADW,OAAO,mBAAmB,EAAE,aAAa,EAAE,CAwBrD;AAED,6DAA6D;AAC7D,gCADW,OAAO,mBAAmB,EAAE,gBAAgB,EAAE,CAUxD;AAED,qDAAqD;AACrD,wBADW,OAAO,mBAAmB,EAAE,QAAQ,EAAE,CAMhD;AAED,0DAA0D;AAC1D,6BADW,OAAO,mBAAmB,EAAE,aAAa,EAAE,CAKrD;AAED,uEAAuE;AACvE,0CADW,OAAO,mBAAmB,EAAE,0BAA0B,EAAE,CAOlE"}
|
web/node_modules/hyparquet/types/convert.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Convert known types from primitive to rich, and dereference dictionary.
|
| 3 |
+
*
|
| 4 |
+
* @param {DecodedArray} data series of primitive types
|
| 5 |
+
* @param {DecodedArray | undefined} dictionary
|
| 6 |
+
* @param {Encoding} encoding
|
| 7 |
+
* @param {ColumnDecoder} columnDecoder
|
| 8 |
+
* @returns {DecodedArray} series of rich types
|
| 9 |
+
*/
|
| 10 |
+
export function convertWithDictionary(data: DecodedArray, dictionary: DecodedArray | undefined, encoding: Encoding, columnDecoder: ColumnDecoder): DecodedArray;
|
| 11 |
+
/**
|
| 12 |
+
* Convert known types from primitive to rich.
|
| 13 |
+
*
|
| 14 |
+
* @param {DecodedArray} data series of primitive types
|
| 15 |
+
* @param {ColumnDecoder} columnDecoder
|
| 16 |
+
* @returns {DecodedArray} series of rich types
|
| 17 |
+
*/
|
| 18 |
+
export function convert(data: DecodedArray, columnDecoder: ColumnDecoder): DecodedArray;
|
| 19 |
+
/**
|
| 20 |
+
* @param {Uint8Array} bytes
|
| 21 |
+
* @returns {number}
|
| 22 |
+
*/
|
| 23 |
+
export function parseDecimal(bytes: Uint8Array): number;
|
| 24 |
+
/**
|
| 25 |
+
* @param {Uint8Array | undefined} bytes
|
| 26 |
+
* @returns {number | undefined}
|
| 27 |
+
*/
|
| 28 |
+
export function parseFloat16(bytes: Uint8Array | undefined): number | undefined;
|
| 29 |
+
/**
|
| 30 |
+
* Default type parsers when no custom ones are given
|
| 31 |
+
* @type ParquetParsers
|
| 32 |
+
*/
|
| 33 |
+
export const DEFAULT_PARSERS: ParquetParsers;
|
| 34 |
+
import type { DecodedArray } from '../src/types.js';
|
| 35 |
+
import type { Encoding } from '../src/types.js';
|
| 36 |
+
import type { ColumnDecoder } from '../src/types.js';
|
| 37 |
+
import type { ParquetParsers } from '../src/types.js';
|
| 38 |
+
//# sourceMappingURL=convert.d.ts.map
|
web/node_modules/hyparquet/types/convert.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../src/convert.js"],"names":[],"mappings":"AA4CA;;;;;;;;GAQG;AACH,4CANW,YAAY,cACZ,YAAY,GAAG,SAAS,YACxB,QAAQ,iBACR,aAAa,GACX,YAAY,CAgBxB;AAED;;;;;;GAMG;AACH,8BAJW,YAAY,iBACZ,aAAa,GACX,YAAY,CAwFxB;AAED;;;GAGG;AACH,oCAHW,UAAU,GACR,MAAM,CAiBlB;AAaD;;;GAGG;AACH,oCAHW,UAAU,GAAG,SAAS,GACpB,MAAM,GAAG,SAAS,CAW9B;AAzMD;;;GAGG;AACH,8BAFS,cAAc,CAgCtB;kCAvCuE,iBAAiB;8BAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB"}
|
web/node_modules/hyparquet/types/datapage.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Read a data page from uncompressed reader.
|
| 3 |
+
*
|
| 4 |
+
* @param {Uint8Array} bytes raw page data (should already be decompressed)
|
| 5 |
+
* @param {DataPageHeader} daph data page header
|
| 6 |
+
* @param {ColumnDecoder} columnDecoder
|
| 7 |
+
* @returns {DataPage} definition levels, repetition levels, and array of values
|
| 8 |
+
*/
|
| 9 |
+
export function readDataPage(bytes: Uint8Array, daph: DataPageHeader, { type, element, schemaPath }: ColumnDecoder): DataPage;
|
| 10 |
+
/**
|
| 11 |
+
* @param {Uint8Array} compressedBytes
|
| 12 |
+
* @param {number} uncompressed_page_size
|
| 13 |
+
* @param {CompressionCodec} codec
|
| 14 |
+
* @param {Compressors | undefined} compressors
|
| 15 |
+
* @returns {Uint8Array}
|
| 16 |
+
*/
|
| 17 |
+
export function decompressPage(compressedBytes: Uint8Array, uncompressed_page_size: number, codec: CompressionCodec, compressors: Compressors | undefined): Uint8Array;
|
| 18 |
+
/**
|
| 19 |
+
* Read a data page from the given Uint8Array.
|
| 20 |
+
*
|
| 21 |
+
* @param {Uint8Array} compressedBytes raw page data
|
| 22 |
+
* @param {PageHeader} ph page header
|
| 23 |
+
* @param {ColumnDecoder} columnDecoder
|
| 24 |
+
* @returns {DataPage} definition levels, repetition levels, and array of values
|
| 25 |
+
*/
|
| 26 |
+
export function readDataPageV2(compressedBytes: Uint8Array, ph: PageHeader, columnDecoder: ColumnDecoder): DataPage;
|
| 27 |
+
import type { DataPageHeader } from '../src/types.js';
|
| 28 |
+
import type { ColumnDecoder } from '../src/types.js';
|
| 29 |
+
import type { DataPage } from '../src/types.js';
|
| 30 |
+
import type { CompressionCodec } from '../src/types.js';
|
| 31 |
+
import type { Compressors } from '../src/types.js';
|
| 32 |
+
import type { PageHeader } from '../src/types.js';
|
| 33 |
+
//# sourceMappingURL=datapage.d.ts.map
|
web/node_modules/hyparquet/types/datapage.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version":3,"file":"datapage.d.ts","sourceRoot":"","sources":["../src/datapage.js"],"names":[],"mappings":"AAUA;;;;;;;GAOG;AACH,oCALW,UAAU,QACV,cAAc,iCACd,aAAa,GACX,QAAQ,CAkDpB;AA2CD;;;;;;GAMG;AACH,gDANW,UAAU,0BACV,MAAM,SACN,gBAAgB,eAChB,WAAW,GAAG,SAAS,GACrB,UAAU,CAoBtB;AAGD;;;;;;;GAOG;AACH,gDALW,UAAU,MACV,UAAU,iBACV,aAAa,GACX,QAAQ,CA6DpB;oCA3M4J,iBAAiB;mCAAjB,iBAAiB;8BAAjB,iBAAiB;sCAAjB,iBAAiB;iCAAjB,iBAAiB;gCAAjB,iBAAiB"}
|
web/node_modules/hyparquet/types/delta.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @param {DataReader} reader
|
| 3 |
+
* @param {number} count number of values to read
|
| 4 |
+
* @param {Int32Array | BigInt64Array} output
|
| 5 |
+
*/
|
| 6 |
+
export function deltaBinaryUnpack(reader: DataReader, count: number, output: Int32Array | BigInt64Array): void;
|
| 7 |
+
/**
|
| 8 |
+
* @param {DataReader} reader
|
| 9 |
+
* @param {number} count
|
| 10 |
+
* @param {Uint8Array[]} output
|
| 11 |
+
*/
|
| 12 |
+
export function deltaLengthByteArray(reader: DataReader, count: number, output: Uint8Array[]): void;
|
| 13 |
+
/**
|
| 14 |
+
* @param {DataReader} reader
|
| 15 |
+
* @param {number} count
|
| 16 |
+
* @param {Uint8Array[]} output
|
| 17 |
+
*/
|
| 18 |
+
export function deltaByteArray(reader: DataReader, count: number, output: Uint8Array[]): void;
|
| 19 |
+
import type { DataReader } from '../src/types.js';
|
| 20 |
+
//# sourceMappingURL=delta.d.ts.map
|
web/node_modules/hyparquet/types/delta.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version":3,"file":"delta.d.ts","sourceRoot":"","sources":["../src/delta.js"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,0CAJW,UAAU,SACV,MAAM,UACN,UAAU,GAAG,aAAa,QAuDpC;AAED;;;;GAIG;AACH,6CAJW,UAAU,SACV,MAAM,UACN,UAAU,EAAE,QAStB;AAED;;;;GAIG;AACH,uCAJW,UAAU,SACV,MAAM,UACN,UAAU,EAAE,QAoBtB;gCAtG4B,iBAAiB"}
|
web/node_modules/hyparquet/types/encoding.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Read values from a run-length encoded/bit-packed hybrid encoding.
|
| 3 |
+
*
|
| 4 |
+
* If length is zero, then read int32 length at the start.
|
| 5 |
+
*
|
| 6 |
+
* @param {DataReader} reader
|
| 7 |
+
* @param {number} width - bitwidth
|
| 8 |
+
* @param {DecodedArray} output
|
| 9 |
+
* @param {number} [length] - length of the encoded data
|
| 10 |
+
*/
|
| 11 |
+
export function readRleBitPackedHybrid(reader: DataReader, width: number, output: DecodedArray, length?: number): void;
|
| 12 |
+
/**
|
| 13 |
+
* @param {DataReader} reader
|
| 14 |
+
* @param {number} count
|
| 15 |
+
* @param {ParquetType} type
|
| 16 |
+
* @param {number | undefined} typeLength
|
| 17 |
+
* @returns {DecodedArray}
|
| 18 |
+
*/
|
| 19 |
+
export function byteStreamSplit(reader: DataReader, count: number, type: ParquetType, typeLength: number | undefined): DecodedArray;
|
| 20 |
+
import type { DataReader } from '../src/types.js';
|
| 21 |
+
import type { DecodedArray } from '../src/types.js';
|
| 22 |
+
import type { ParquetType } from '../src/types.js';
|
| 23 |
+
//# sourceMappingURL=encoding.d.ts.map
|
web/node_modules/hyparquet/types/encoding.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version":3,"file":"encoding.d.ts","sourceRoot":"","sources":["../src/encoding.js"],"names":[],"mappings":"AAMA;;;;;;;;;GASG;AACH,+CALW,UAAU,SACV,MAAM,UACN,YAAY,WACZ,MAAM,QAsBhB;AA2ED;;;;;;GAMG;AACH,wCANW,UAAU,SACV,MAAM,QACN,WAAW,cACX,MAAM,GAAG,SAAS,GAChB,YAAY,CAwBxB;gCA3IuD,iBAAiB;kCAAjB,iBAAiB;iCAAjB,iBAAiB"}
|
web/node_modules/hyparquet/types/filter.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Returns an array of top-level column names needed to evaluate the filter.
|
| 3 |
+
*
|
| 4 |
+
* @param {ParquetQueryFilter} [filter]
|
| 5 |
+
* @returns {string[]}
|
| 6 |
+
*/
|
| 7 |
+
export function columnsNeededForFilter(filter?: ParquetQueryFilter): string[];
|
| 8 |
+
/**
|
| 9 |
+
* Match a record against a query filter
|
| 10 |
+
*
|
| 11 |
+
* @param {Record<string, any>} record
|
| 12 |
+
* @param {ParquetQueryFilter} filter
|
| 13 |
+
* @param {boolean} [strict]
|
| 14 |
+
* @returns {boolean}
|
| 15 |
+
*/
|
| 16 |
+
export function matchFilter(record: Record<string, any>, filter: ParquetQueryFilter, strict?: boolean): boolean;
|
| 17 |
+
/**
|
| 18 |
+
* Check if a row group can be skipped based on filter and column statistics,
|
| 19 |
+
* optionally consulting per-column bloom filters for equality predicates that
|
| 20 |
+
* statistics can't decide.
|
| 21 |
+
*
|
| 22 |
+
* @param {object} options
|
| 23 |
+
* @param {RowGroup} options.rowGroup
|
| 24 |
+
* @param {string[]} options.physicalColumns
|
| 25 |
+
* @param {ParquetQueryFilter | undefined} options.filter
|
| 26 |
+
* @param {boolean} [options.strict]
|
| 27 |
+
* @param {Record<string, BloomFilter>} [options.bloomFilters] keyed by top-level column name
|
| 28 |
+
* @param {Record<string, SchemaElement>} [options.schemaElements] keyed by top-level column name, required to use bloomFilters
|
| 29 |
+
* @returns {boolean} true if the row group can be skipped
|
| 30 |
+
*/
|
| 31 |
+
export function canSkipRowGroup({ rowGroup, physicalColumns, filter, strict, bloomFilters, schemaElements }: {
|
| 32 |
+
rowGroup: RowGroup;
|
| 33 |
+
physicalColumns: string[];
|
| 34 |
+
filter: ParquetQueryFilter | undefined;
|
| 35 |
+
strict?: boolean | undefined;
|
| 36 |
+
bloomFilters?: Record<string, BloomFilter> | undefined;
|
| 37 |
+
schemaElements?: Record<string, SchemaElement> | undefined;
|
| 38 |
+
}): boolean;
|
| 39 |
+
import type { ParquetQueryFilter } from '../src/types.js';
|
| 40 |
+
import type { RowGroup } from '../src/types.js';
|
| 41 |
+
import type { BloomFilter } from '../src/types.js';
|
| 42 |
+
import type { SchemaElement } from '../src/types.js';
|
| 43 |
+
//# sourceMappingURL=filter.d.ts.map
|
web/node_modules/hyparquet/types/filter.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../src/filter.js"],"names":[],"mappings":"AAOA;;;;;GAKG;AACH,gDAHW,kBAAkB,GAChB,MAAM,EAAE,CAiBpB;AAED;;;;;;;GAOG;AACH,oCALW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,UACnB,kBAAkB,WAClB,OAAO,GACL,OAAO,CAkCnB;AAED;;;;;;;;;;;;;GAaG;AACH,6GARG;IAA0B,QAAQ,EAA1B,QAAQ;IACU,eAAe,EAAjC,MAAM,EAAE;IACgC,MAAM,EAA9C,kBAAkB,GAAG,SAAS;IACZ,MAAM;IACc,YAAY;IACV,cAAc;CAC9D,GAAU,OAAO,CAsEnB;wCAzJ0E,iBAAiB;8BAAjB,iBAAiB;iCAAjB,iBAAiB;mCAAjB,iBAAiB"}
|