Companion to PARQUET_PUSHDOWN_DESIGN.md. Maps the design's six phases to thirteen shippable milestones, with the architectural decisions needed to start coding (residency, wire protocol, trust mode, predicate engine scope, side-index path, library choices) called out as defaults that the open-questions section asks to confirm. No code changes — this commit only adds the plan document. M0 does not start until the open questions are resolved.
15 KiB
Parquet Pushdown Dev Plan
Companion to PARQUET_PUSHDOWN_DESIGN.md. The design defines what the system does; this doc defines how it lands in this codebase, in what order, with what milestones.
Status
Draft. Open questions remain — see Open Questions. Coding does not start until those are resolved.
Goals for the implementation
- Ship the design's Phase 1 in production-quality form: a pushdown service that, given an Iceberg snapshot resolution, returns column-chunk byte ranges and prunes row groups using parsed-footer statistics.
- Establish the package layout, wire protocol, and trust-mode plumbing so later phases (scalar indexes, page-level pruning, deletes, vector search) attach without reshaping the surface.
- Avoid breaking any existing read path — pushdown is additive; standard S3 reads keep working.
What already exists in the codebase
| Component | Path | Reuse |
|---|---|---|
| Iceberg-Go client and table/manifest types | weed/s3api/iceberg/, weed/plugin/worker/iceberg/ |
Catalog reads, manifest parsing, planning |
Parquet reader (parquet-go/parquet-go v0.28.0) |
weed/mq/logstore/, weed/query/engine/parquet_scanner.go |
Footer parse, ColumnIndex/OffsetIndex |
| SQL/value type conversion | weed/query/engine/parquet_scanner.go, weed/query/sqltypes/ |
Predicate constant typing |
| Iceberg compaction worker (uses iceberg-go for planning) | weed/plugin/worker/iceberg/planning_index.go |
Reference for snapshot → file-list resolution |
| Filer gRPC + protobuf scaffolding | weed/pb/filer_pb/, weed/server/filer_grpc_server.go |
Wire-protocol pattern, auth |
| S3 IAM / bucket ACL | weed/s3api/, weed/iam/ |
Trust enforcement on data files |
| Substrait Go SDK (already a transitive dep) | github.com/substrait-io/substrait-go/v7 |
Predicate decode in v2 |
The dependencies needed for v1 (parquet-go, iceberg-go, RoaringBitmap/roaring, substrait-go/v7) are already in go.mod (some as indirect; will become direct).
Architectural decisions for v1
These are commitments unless an open question below changes them.
- Residency: filer-managed. The pushdown service runs inside the filer process. Rationale: simplest operational story; reuses filer auth, replication, and metadata; matches Phase 1's I/O profile (read footers and small side indexes). Volume-server colocation and a separate index-serving tier are deferred to Phase 2/3 once we have measurements.
- Wire protocol: gRPC. New service definition
parquet_pushdown_pb.ParquetPushdowninweed/pb/parquet_pushdown_pb/. REST/HTTP shim only if a connector requires it. JSON variant deferred. - Trust mode for v1: connector-trusted. The pushdown endpoint is reachable only inside the trust boundary.
PushdownStats.TrustMode = "connector-trusted". Catalog-validated mode is built as Phase 1.5, behind a flag, before any external exposure. Manifest-signed is not on the roadmap. - Predicate engine for v1: built-in subset evaluator. The wire format accepts Substrait
ExtendedExpression, but v1 implements only: comparisons (=, !=, <, <=, >, >=),IN,BETWEEN,IS NULL, and booleanAND/OR/NOTover them. Anything outside this subset returnsunsupported predicate, and the connector falls back to a standard scan. Full Substrait evaluation lands in Phase 2. - Side-index physical location: under a separate filer namespace
/__pushdown__/<table_uuid>/..., governed by the same filer ACLs. Out of any user-visible bucket prefix. Path layout matches the design's<system-prefix>/<table_uuid>/.../<identity>/shape. - Parquet library:
parquet-go/parquet-go(already in use elsewhere in the repo). Apachearrow-go/v18/parquetis not introduced. - Bitmap library:
RoaringBitmap/roaring. Used for position-delete bitmaps and DV decoding.
Code layout
New code lives under weed/parquet_pushdown/:
weed/parquet_pushdown/
service.go # gRPC service entry; dispatches to subsystems
request_validation.go # request-shape limits, trust-mode enforcement
catalog/ # Phase 1.5: catalog-validated mode
validator.go # cross-checks request DataFiles against manifest
footer/
cache.go # parsed-footer LRU, keyed by file identity
parser.go # footer + ColumnIndex/OffsetIndex extraction
index/
types.go # SideIndex interface, registry
bloom/ # Phase 2
bitmap/ # Phase 2
btree/ # Phase 2
page/ # Phase 3
vector/ivf/ # Phase 5
predicate/
substrait_decode.go # decode Substrait ExtendedExpression
iceberg_decode.go # decode Iceberg Expression JSON
eval_subset.go # v1 built-in evaluator (comparisons + boolean)
rowgroup_prune.go # zone-map pruning for v1
delete/
position_bitmap.go # Phase 4 — v2 position-delete merge + cache
deletion_vector.go # Phase 4 — v3 Puffin DV reader + cache
equality_eval.go # Phase 4 — equality-delete evaluator
storage/
filer_blob_store.go # read/write side-index blobs through filer
stats.go # PushdownStats accumulation
weed/pb/parquet_pushdown_pb/
parquet_pushdown.proto # request/response messages
parquet_pushdown.pb.go # generated
parquet_pushdown_grpc.pb.go # generated
weed/server/filer_grpc_pushdown.go # filer mounts the pushdown service
weed/command/ # `weed pushdown` subcommands (build/inspect indexes)
Tests live alongside the code (*_test.go). Integration tests for end-to-end paths land in test/parquet_pushdown/.
Milestones
Each milestone is a shippable PR (or small chain of PRs). M0–M2 together implement the design's Phase 1.
M0 — Infrastructure (1 PR)
parquet_pushdown.protowithPushdownRPC, request/response messages mirroring API Sketch. Generate Go bindings.- Empty service implementation that validates request shape (size limits, MaxRowIds cap, trust-mode tag) and returns
unimplementedfor the actual work. - Filer wiring: register the gRPC service on the filer's existing gRPC server. Add a feature flag
-pushdown.enabledso the service is off by default in the filer binary. weed pushdown pingCLI command for smoke testing.- Acceptance: integration test calls
Pushdownover gRPC, gets the expectedunimplementederror path with stats populated.
M1 — Parsed-footer cache + file/range-level pushdown (1 PR)
footer/parser.go: open Parquet file via the filer chunk reader, parse footer, ColumnIndex, OffsetIndex.footer/cache.go: LRU keyed by(path, size, etag-or-recordcount)per Index Consistency.- Service handles requests with
Predicate == nil: returnFileRangescovering the requestedColumnsfor eachDataFile(one entry per column chunk), no pruning yet. - Acceptance:
- Round-trip integration test against a 3-file Iceberg table built in
test/parquet_pushdown/fixtures/. - Footer cache hit-rate test (second call avoids re-parse; verified via
PushdownStats.FooterCacheHits).
- Round-trip integration test against a 3-file Iceberg table built in
M2 — Row-group zone-map pruning (1 PR)
predicate/eval_subset.go: evaluator for the v1 predicate subset.predicate/rowgroup_prune.go: read row-group statistics from the parsed footer, drop row groups where the predicate cannot be true, return per-data-fileRowGroupReflist.predicate/substrait_decode.go: decode just enough Substrait to feed the subset evaluator; everything else returnsunsupported.- Acceptance:
- Predicate
WHERE timestamp BETWEEN t1 AND t2against a 5M-row partitioned Iceberg table prunes to the expected row groups (verified by countingRowGroupRef). unsupported predicatereturns a status the connector can recognize and fall back from.
- Predicate
End of design Phase 1.
M3 — Catalog-validated trust mode (1 PR; optional before external exposure)
catalog/validator.go: read the Iceberg snapshot, verify(Path, SizeBytes, RecordCount, DataSequenceNumber, PartitionSpecId, PartitionValues)for eachDataFileand that theDeleteslist matches the manifest's attached delete files.- Configurable per-endpoint:
pushdown.trust=connector-trusted|catalog-validated. - Cache validated planning result keyed by
SnapshotIdto amortize manifest reads. - Acceptance: a tampered request (omitted delete file, wrong sequence number, file not in manifest) is rejected with
permission_denied.
M4 — Bloom-filter side index (1–2 PRs; design Phase 2 starts)
index/bloom/: builder reads Parquet column chunks, produces a SplitBlock Bloom Filter (Parquet's standard) per column chunk, stores viastorage/filer_blob_store.gokeyed bybloom.fid_<id>.- Predicate evaluator consults bloom for
=/INpredicates. - Background builder triggered on new Iceberg snapshot (hooks into the existing iceberg compaction worker — TODO: confirm hook point).
- Acceptance: equality predicate with no matches against a 10M-row column reads zero data needles; cache miss and miss-build also tested.
M5 — Bitmap and B-tree side indexes (1 PR each)
index/bitmap/for low/medium-cardinality columns; integrates withRoaringBitmap.index/btree/for sortable columns; range and point-lookup predicates.- Acceptance: micro-benchmarks vs M2's row-group-only pruning showing ≥10× row-group reduction on the standard test workload.
M6 — Page-level pruning (1 PR; design Phase 3)
index/page/: wraps Parquet's ColumnIndex + OffsetIndex; row-range translation across columns; dictionary-page byte-range inclusion per Page-Level Index.- Service returns
PageRefentries with byte ranges for each projected column. - Acceptance: predicate selectivity ≥0.99 returns ≤1 page per surviving row group across all projected columns.
M7–M9 — Iceberg deletes (3 PRs; design Phase 4)
- M7: position-delete bitmap caching, keyed per Position-delete bitmap cache key.
- M8: Puffin DV reader + cache (use
iceberg-goPuffin support if present, otherwise vendor a minimal Puffin parser). - M9: equality-delete evaluation accelerated by Phase 2 indexes.
- Acceptance: reference table with both delete forms returns identical row sets to a Spark scan of the same snapshot.
M10–M11 — Vector indexes (design Phase 5)
- M10: partitioned IVF index per Filtered-ANN strategy; per-tenant partitions, top-K merge, score correctness vs brute-force.
- M11: hybrid scalar+vector pushdown; planner selects partitioned-IVF vs post-filter+overscan based on selectivity estimate.
- Acceptance: recall ≥0.95 vs brute force at top-20, 10× speedup on the standard test set.
M12–M13 — Connector integrations (design Phase 6)
- M12: DuckDB extension as PoC; consumes the gRPC API directly.
- M13: Trino + Spark connectors; both use the same gRPC client.
Test strategy
- Unit tests: every package, with parquet-go in-memory fixtures.
- Integration tests:
test/parquet_pushdown/— spins up filer + a small Iceberg catalog (reuseweed/s3api/iceberg/infrastructure), generates Parquet data withparquet-gowriters, compares pushdown results to a brute-force scan. - Correctness oracle: every milestone validates that pushdown output is a strict subset of a brute-force Iceberg+Parquet scan with the same predicate, and that the projected-column byte ranges decode to the same rows. The oracle is the safety net that lets us refactor index internals freely.
- Performance suite: runs in CI weekly, not on every PR. Tracks footer-cache hit rate, row-group reduction, page reduction, end-to-end query latency vs a baseline standard scan.
Open Questions
These need answers before M0 starts. Most have defaults proposed above; confirm or override.
- Residency for v1 — confirm filer-managed? Alternative: a separate
weed pushdowndaemon. Filer-managed is simpler but couples pushdown availability to filer availability. Default: filer-managed. - Service surface — extend
filer_pbor newparquet_pushdown_pbpackage? New package is cleaner but adds a discovery hop for clients. Default: new package. - Trust mode for the very first integration tests — connector-trusted only, or do we need M3 (catalog-validated) before any test exercises the trust boundary? Default: connector-trusted for M0–M2, M3 lands before any external connector PoC (M12).
- Side-index path root.
/__pushdown__/is my proposal; is there an existing convention I should match? (I see/.snapshots/,/.iam/, etc. in other parts of the repo.) - Predicate evaluator scope for v1 — confirm the subset listed above (
=, !=, <, <=, >, >=,IN,BETWEEN,IS NULL, boolean ops)? Anything else you want supported up-front (e.g.LIKE)? Default: subset above;LIKEdeferred to inverted-index milestone. - Iceberg snapshot integration in M3 — use
iceberg-godirectly, or call into the existingweed/s3api/iceberg/andweed/plugin/worker/iceberg/code? The latter has table cache + lock support already. Default: route through the existing internal package, do not add a parallel iceberg-go integration. - Auth surface for the gRPC endpoint — reuse the filer's existing auth (mTLS / JWT), or add a pushdown-specific token? Default: reuse filer auth; the request principal must have read access to every
DataFilelisted. - Test corpus for M1. Is there an existing Iceberg-with-deletes fixture in
test/we should standardize on, or do we generate one from scratch intest/parquet_pushdown/fixtures/? Default: generate from scratch, parameterized over (with-position-deletes, with-equality-deletes, with-DVs, partitioned/unpartitioned). - Phase 1 acceptance bar. What's the definition of done that lets us call Phase 1 shipped — passing tests in CI, a benchmark showing measurable I/O reduction on a reference query, or a connector PoC? Default: tests + I/O-reduction benchmark; PoC slips to M12.
- Performance regression budget. Pushdown adds work on the read path. What overhead is acceptable when the predicate misses everything (worst case for pushdown)? Default: ≤10% added latency vs a direct ranged GET on a 10 MiB single-file scan.
Risk register
- Iceberg-Go maturity for v3 deletion vectors / Puffin. If
iceberg-go v0.5.0does not expose Puffin DV blobs, M8 needs a vendored Puffin parser. Spike during M0 to verify. - parquet-go ColumnIndex/OffsetIndex completeness. M6 depends on full ColumnIndex/OffsetIndex support. If the library returns nil for some files, fall back to row-group-only pruning per the design.
- Catalog read latency. M3's per-request manifest read must be cacheable per snapshot, otherwise the catalog-validated mode adds >100ms per request. Cache eviction strategy is part of M3's spec.
- Trust boundary leakage. A pushdown response must not return data references that a direct read would deny. The auth check (open question 7) is load-bearing; integration tests in M0 must include negative cases.