Skip to content

POC: incremental materialized view β€” derived data format + transparent read path - #212

Open
alchemist51 wants to merge 8 commits into
mainfrom
mv-poc-incremental-dataformat
Open

POC: incremental materialized view β€” derived data format + transparent read path#212
alchemist51 wants to merge 8 commits into
mainfrom
mv-poc-incremental-dataformat

Conversation

@alchemist51

@alchemist51 alchemist51 commented Aug 3, 2026

Copy link
Copy Markdown
Owner

What this proves

The incremental-MV design (per-segment aggregate state files as a composite-engine secondary data format) works end to end in a live cluster β€” both halves:

  1. Write path: state files built as segments flush, tracked as first-class catalog files (MVDataFormatPocIT)
  2. Read path (new): an aggregate query on the source index is transparently served from the state files, with per-segment fallback to raw (MVReadPathIT)

Write path (commits 1–3)

For the hardcoded view SELECT service, status, COUNT(*), SUM(latency_ms), MIN(latency_ms), MAX(latency_ms) FROM payments GROUP BY service, status:

  • Ingest: composite index (parquet primary + materialized_view secondary), golden docs in 2 segments
  • Streaming build: MVDocumentInput captures the referenced columns from the composite broadcast β†’ forward VSR buffer (4096-row rotation, rotate-before-append for rollback safety) β†’ Arrow C-Data β†’ DataFusion-maintained native state (Partial-mode accumulate + PartialReduce compaction) β†’ flush finalizes into a group-key-sorted state parquet. No primary re-read, no flush-time aggregation spike.
  • Catalog integration: state files ride Segment's format map β€” pinning, snapshot membership, per-format cleanup all free. Row-parity exemption (DataFormat.exemptFromRowParity()) lets a segment carry 8 raw rows next to 3 state rows.
  • Standalone Rust round-trip test proves the state contract (Partial β†’ parquet β†’ Final; merge-fold β†’ Final).

Read path (commits 4–7)

The full transparent-rewrite chain, star-tree style (gate once per request, substitute per segment, zero new downstream types):

planner: MVRewritePhase (post-decomposeAggregates, pre-mark)
  matches the canonicalized Aggregate+Scan against MV definitions
  β†’ records an annotation in a PlannerContext side-channel (tree untouched)
     ↓
FragmentConversionDriver: binding rides the shard-scan instruction
  (only when the fragment has a partial aggregate; never with QTF row-ids
   or delegated predicates)
     ↓
data node: ShardScanInstructionHandler computes the coverage split from
  ONE catalog snapshot (MV generations ∩ parquet generations β€” atomic)
  β†’ df_session_attach_mv(state file paths, covered raw file names)
     ↓
Rust mv_read: at prepare_partial_plan time the stripped Partial plan becomes
  UNION( scan(state files, positionally aliased to the partial output names),
         Partial over the raw scan narrowed to uncovered files )
     ↓
coordinator FINAL: unchanged β€” state files ARE Partial-mode output
  (zero-translation contract)

Fallback-first everywhere: any mismatch (plan shape, state schema, scan leaf shape, attach failure) keeps today's raw-only plan. The only possible failure mode is "no speedup", never a wrong answer.

MV-only proof (strict mode): a POC verification switch (mv.poc.strict_read) inverts the fallback contract to fail-closed β€” every fallback becomes a hard error, every raw file must be covered, and the native plan is the state-file scan alone (no raw scan node exists, raw parquet physically cannot be read). testStrictModeProvesQueryServedFromMVOnly runs the aggregate through the live cluster in this mode and gets the golden answers β€” constructive proof the query was served exclusively from the MV.

Proof (assertions layered so a silent fallback FAILS the test):

  • MVReadPathIT (2 shards): MockLogAppender expects the shard handler's mv-binding … attached event during the MV-enabled query; differential β€” MV-enabled results ≑ raw-path results ≑ goldens; negative control β€” a non-matching group-by doesn't bind and still answers correctly. Observed chain in one run: annotation β†’ bind=true β†’ attached 2 state files, 2 covered raw files β†’ mv_read: bound 2 state files; raw branch narrowed by 2 covered files.
  • tests/mv_read_union_test.rs (6 tests): mixed coverage equals the full-raw answer (a narrowing failure would double-count and fail), all-covered serves entirely from state files, mismatched state schema returns the untouched plan; strict variants β€” the strict plan's display contains no raw-dir path, an uncovered file errors, a mismatched schema errors.

Reading order

  • server/…/DataFormat.java + CatalogSnapshotManager.java β€” parity-exemption core change (small)
  • sandbox/plugins/mv-data-format/ β€” the format plugin (MVWriter is the write-path heart; MVReaderManager now exposes per-generation coverage)
  • analytics-engine …/planner/mv/ β€” MVRewritePhase + MVRegistry (the planner gate)
  • analytics-backend-datafusion …/ShardScanInstructionHandler.java + rust/src/mv_read.rs β€” the shard-side split + native union surgery
  • MVDataFormatPocIT (write path) and MVReadPathIT (read path) β€” the demos:
    ./gradlew :sandbox:plugins:mv-data-format:internalClusterTest -Dsandbox.enabled=true --tests 'org.opensearch.mv.MVDataFormatPocIT'
    ./gradlew :sandbox:qa:analytics-engine-coordinator:internalClusterTest -Dsandbox.enabled=true --tests 'org.opensearch.mv.MVReadPathIT'
    

POC hardcodes / known v0 gaps (not production)

  • Definition compiled in; MVRegistryHolder static wiring (replaced by cluster-state MV metadata at M0/M1); state-fingerprint carried on the wire but enforced only via the positional schema check
  • Single-shard queries don't bind: 1-shard fragments run SINGLE-mode aggregation (no Partial node = no surgery point). Tracked as W9 in the plan doc
  • POC writer parity on near-empty shard batches fails flush (pre-existing; the read-path IT routes docs to one shard). Tracked as W10
  • TopK fragments excluded from binding in v0; merges disabled; per-call tokio runtime on the write path; parity exemption as a hardcoded name set

Design decisions (D1–D8: annotation transport, surgery ordering, wire transport, coverage split, schema alignment, registry wiring) + full work log live in the KB: mv-search-side-integration-plan.md.

Not for merge — design-validation artifact for the incremental-MV track, now covering the complete write→read loop.

Proves per-segment materialized-view state files as a composite-engine
secondary data format, end to end in a cluster IT:

- MV state files (DataFusion Partial-mode aggregate output, sorted by
  group key) built at flush from the primary's parquet file; tracked as
  first-class format files in the catalog snapshot
- Row-parity exemption for derived formats: DataFormat.exemptFromRowParity()
  gating the CatalogSnapshotManager parity check (POC-hardcoded name set)
  and the CompositeMergeExecutor row-equality check
- Hardcoded search (Final fold over state files across segments) returns
  exact golden answers; duplicate groups across segments combine correctly
- Standalone round-trip test proving the state contract
  (Partial -> parquet -> Final; merge-fold -> Final)

POC scope: one hardcoded definition (COUNT(*) GROUP BY service), merges
disabled, private tokio runtime per build call, no memory-pool enrollment.
See kb mv-poc-results.md for lessons and the hardcode-replacement list.

Signed-off-by: Arpit Bandejiya <abandeji@amazon.com>
Replaces the derived-at-flush build (re-reading the primary's parquet)
with the forward-buffer / background-state model:

- MVDocumentInput captures the MV's referenced column from the composite
  broadcast (no capability claim needed - all formats see all fields)
- MVWriter buffers values into a small VSR; rotation (4096 rows, always
  BEFORE append so rollback only truncates the buffer) exports via Arrow
  C-Data and folds into a native sorted background state (BTreeMap for
  the POC count case; sorted-runs + k-way fold is the general design)
- flush = final rotation + finalize: background state written as the
  sorted MV state parquet. Zero flush-time aggregation work, no read of
  the primary file, no path-convention coupling

Same observable contract (sorted state file, schema, catalog
integration, golden answers) - the IT passes unchanged except the log
line ('streamed' vs 'built'). The derived build (mv_poc.rs mv_build_poc)
remains as the future merge/backfill path.

Signed-off-by: Arpit Bandejiya <abandeji@amazon.com>
Extends the POC view to
  SELECT service, status, COUNT(*), SUM(latency_ms), MIN(latency_ms),
         MAX(latency_ms) GROUP BY service, status

and replaces the hand-rolled BTreeMap fold with DataFusion's own state
algebra (hash aggregation until flush + one sort at flush):

- feed: Partial-mode aggregation over the fed batch (raw -> state rows),
  accumulated; held state bounded by PartialReduce compaction
  (state (+) state -- the engine operator, nothing hand-rolled)
- finalize: final PartialReduce fold -> single lexsort by the group-key
  columns (cost ~ groups) -> sorted state parquet
- state schema comes from the plan; Java knows only group keys + search
  template. min/max state columns are name[value] (probed via test)
- MVDocumentInput captures (service, status, latency_ms); forward buffer
  is a 3-vector VSR; rotation contract unchanged
- search v2: SQL template folding all four aggregates over state files
- noted-not-built optimization recorded in mv_writer.rs: sorted runs +
  SortPreservingMerge + GroupOrdered fold for high cardinality

IT: 6 state rows across 2 segments; goldens incl. a group split across
segments and min/max winners in different segments -- all exact.

Signed-off-by: Arpit Bandejiya <abandeji@amazon.com>
Expose per-generation coverage on MVReaderManager.MVReader:
coveredGenerations(), stateFilesByGeneration(), covers(gen). The writer
generation is engine-owned and shared across formats for one flush, so
the read-path coverage split is a set intersection between the MV
reader's generations and the primary format's β€” both from the same
catalog snapshot, making the split atomic with the snapshot.

First work item (W1) of the search-side integration plan; consumed by
the shard-side coverage split when the second scan source lands.
Also applies spotless formatting to the POC files.

Signed-off-by: Arpit Bandejiya <abandeji@amazon.com>
Match aggregate queries against materialized view definitions and
record rewrite options, without changing any plan:

- MVRegistry: planner-facing lookup (index -> eligible MV definitions
  in canonical post-decompose form). Production default is EMPTY (the
  phase is a zero-cost no-op) until MV metadata lands.
- MVRewritePhase: runs between decomposeAggregates and mark, so the
  query tree and stored definitions are identically canonicalized and
  the marking/CBO/split phases see today's exact shapes. v0 scope:
  Aggregate over (trivial Project over) TableScan, simple group type,
  no DISTINCT/FILTER calls; group-by exact set match; query aggregates
  a verbatim subset of the MV's. Self-joined tables are skipped.
- MVRewriteAnnotation lives in a PlannerContext side-channel keyed by
  table identity (not on the RelNode tree - survives rel copies), to
  be consumed at shard-fragment emission; the shard exercises the
  option per segment based on snapshot coverage.

Work items W2/W3 of mv-search-side-integration-plan.md (decisions
D1/D2). The DAG-layer binding (W4) and the shard-side coverage split
(W5/W6) follow.

Signed-off-by: Arpit Bandejiya <abandeji@amazon.com>
Completes the search read path for incremental materialized views
(W4/W5/W6 of the search-side integration plan):

- ShardScanInstructionNode carries an optional MV binding (mvId +
  state schema fingerprint) as optional fields, not a new instruction
  type, so instruction-list readers never see an unknown enum ordinal.
- FragmentConversionDriver threads the planner's side-channel
  annotations into instruction assembly; the binding travels only on a
  plain shard scan whose fragment has a partial aggregate β€” row-id
  (QTF) fragments and delegated-predicate fragments never bind (D3).
- ShardScanInstructionHandler computes the coverage split from the
  acquired reader's catalog snapshot (MV generations intersected with
  parquet generations β€” one snapshot, so the split is atomic) and
  attaches state-file paths + covered raw file names to the native
  session via a new df_session_attach_mv FFM call.
- Rust mv_read: at prepare_partial_plan time the stripped Partial plan
  becomes UNION(scan of state files positionally aliased to the
  partial output names, Partial over the raw scan narrowed to
  uncovered files). Fallback-first: any mismatch (plan shape, schema
  shape, scan leaf shape) keeps the raw-only plan β€” never wrong, only
  slower. TopK fragments are excluded in v0.
- MVRegistryHolder: POC static wiring of the planner registry until MV
  metadata provides per-query cluster-state resolution.

Proven by tests/mv_read_union_test.rs: mixed coverage equals the full
raw answer (narrowing failure would double-count), all-covered serves
entirely from state files, and a mismatched state schema falls back to
the untouched raw plan.

Signed-off-by: Arpit Bandejiya <abandeji@amazon.com>
MVReadPathIT drives a SQL aggregate on the source index through the
complete chain: MVRewritePhase annotation (POC registry via
MVRegistryHolder) -> fragment binding -> shard-side coverage attach ->
native union over state files -> coordinator FINAL.

Assertions are layered so a silent fallback FAILS the test:
- MockLogAppender expects the shard handler's 'mv-binding attached'
  INFO event during the MV-enabled query (the log only fires after
  df_session_attach_mv returns success).
- Differential: MV-enabled results equal raw-path results, both equal
  golden values.
- Negative control: a query whose group-by differs from the MV
  definition doesn't bind and still answers correctly.

Findings encoded in the test setup:
- Single-shard fragments run SINGLE-mode aggregation (no Partial node),
  so the MV binding never applies there β€” the IT uses 2 shards to get
  the partial/final split. v0 scope gap tracked in the plan doc.
- The POC MV writer fails flush row-parity on near-empty per-shard
  batches; docs are routed to one shard (the empty shard still
  exercises the no-coverage path end to end).

Also promotes the attach/union success logs to INFO for operability.

Signed-off-by: Arpit Bandejiya <abandeji@amazon.com>
@alchemist51 alchemist51 changed the title POC: incremental materialized view as a derived data format POC: incremental materialized view β€” derived data format + transparent read path Aug 11, 2026
POC verification mode (system property mv.poc.strict_read) that inverts
the read path's fallback-first contract to fail-closed:

- Every fallback in mv_read (plan shape, state schema, scan leaf,
  binding not applied) becomes a hard error instead of a silent raw
  fallback; an attach failure in the shard handler also fails closed.
- Every raw file must be MV-covered; any uncovered file is an error.
- The produced plan is the positionally-aliased state-file scan ALONE β€”
  no raw scan node exists in the plan, so raw parquet physically cannot
  be read. A successful strict query is therefore proof the answer came
  exclusively from MV state files.

Proof:
- Rust (6/6): strict all-covered plan's display contains no raw-dir
  path and folds to golden answers; strict with an uncovered file
  errors; strict with a mismatched state schema errors; the three
  non-strict tests are unchanged.
- MVReadPathIT (3/3): new strict test sets the property, asserts the
  handler's '[STRICT MV-only]' attach event via MockLogAppender, and
  gets golden results β€” with fallbacks hard-failing, success proves
  MV-only serving end to end through the live query path.

Testing/POC only; production keeps the fallback-first contract.

Signed-off-by: Arpit Bandejiya <abandeji@amazon.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant