Variant Shredding
Implementation plan for Parquet Variant Shredding in hyparquet/hyparquet-writer to enable predicate pushdown on semi-structured data fields with statistics-based row group skipping.
Goal
Implement Parquet Variant Shredding in hyparquet/hyparquet-writer to enable:
- Unified data model:
$id | $index (Variant) | $data (Variant) - Predicate pushdown on shredded Variant fields
- Statistics-based row group skipping
Parquet Variant Shredding Structure
Per Parquet spec:
optional group $index (VARIANT) {
required binary metadata;
optional binary value; // null when fully shredded
optional group typed_value {
optional group titleType { // shredded field
optional binary value; // null for typed
optional UTF8 typed_value; // <- STATISTICS HERE
}
optional group $type {
optional binary value;
optional UTF8 typed_value; // <- STATISTICS HERE
}
}
}Key insight: Statistics on typed_value columns enable predicate pushdown when the corresponding value is null.
Implementation Tasks
Phase 1: hyparquet Reader (Predicate Pushdown)
1.1 Dot-notation filter parsing (plan.js)
// Current: only top-level columns
{ titleType: "movie" }
// New: support nested Variant fields
{ "$index.titleType": "movie" }
// Maps to: $index.typed_value.titleType.typed_value1.2 Nested column statistics lookup
- Map dot-notation paths to Parquet column paths
- Read statistics from
typed_value.{field}.typed_valuecolumns - Apply predicate pushdown using those statistics
1.3 Filter evaluation on nested fields
- Extend
matchesFilter()to access nested Variant fields - Handle the
typed_valuevsvaluestructure
Phase 2: hyparquet-writer (Shredded Variant Writing)
2.1 VARIANT schema type (schema.js)
// New: VARIANT type with shredding
{
name: '$index',
type: 'VARIANT',
shredFields: ['$type', 'titleType', 'startYear'],
}2.2 Variant column writing (column.js)
- Create nested group structure per Parquet spec
- Write
metadatabinary - Write
valueas null for shredded fields - Write
typed_value.{field}.typed_valuewith actual values - Compute statistics for
typed_valuecolumns
2.3 Variant encoding (unconvert.js)
- Encode Variant metadata header
- Encode values in Variant binary format
- Handle shredding extraction
Phase 3: Integration
3.1 ParqueDB ETL updates
const buffer = parquetWriteBuffer({
columnData: [
{ name: '$id', type: 'STRING', data: ids },
{
name: '$index',
type: 'VARIANT',
shredFields: ['$type', 'titleType', 'startYear'],
data: indexObjects
},
{ name: '$data', type: 'VARIANT', data: fullObjects },
],
})3.2 Query execution
// Filter on shredded Variant field
const results = await parquetQuery({
file,
filter: { '$index.titleType': 'movie' }, // Uses statistics!
columns: ['$id', '$data'],
})Filter Path Mapping
| User Filter | Parquet Column Path | Statistics Source |
|---|---|---|
$index.titleType | $index.typed_value.titleType | typed_value.typed_value |
$index.$type | $index.typed_value.$type | typed_value.typed_value |
$data.genres | Not shredded | Must scan |
Statistics Behavior
From Parquet spec:
"Statistics for typed_value columns can be used for file, row group, or page skipping when value is always null (missing)."
This means:
- Shredded fields (value=null) -> Statistics work
- Non-shredded fields (value!=null) -> Must scan
Testing Plan
- Unit tests: Write/read shredded Variant roundtrip
- Statistics tests: Verify min/max are computed correctly
- Predicate pushdown tests: Verify row groups are skipped
- Benchmark: Compare with native columns approach
- DuckDB compatibility: Verify files are readable
Timeline
- Phase 1 (Reader): 2-3 days
- Phase 2 (Writer): 3-4 days
- Phase 3 (Integration): 1-2 days
- Testing: 2 days
References
Bloom Filter Indexes
Probabilistic data structures for efficient existence checks and set membership in ParqueDB, including standard Bloom filters, counting Bloom filters, cuckoo filters, and edge existence optimization.
Page Indexes Evaluation
Evaluation of DataFusion-style embedded page indexes (ColumnIndex/OffsetIndex) for ParqueDB, comparing with current row group stats and external bloom files approach.