blob: 5033d547a8d38b3d5476ad824aad0e3ba8011a9e [file]
.. Licensed to the Apache Software Foundation (ASF) under one
.. or more contributor license agreements. See the NOTICE file
.. distributed with this work for additional information
.. regarding copyright ownership. The ASF licenses this file
.. to you under the Apache License, Version 2.0 (the
.. "License"); you may not use this file except in compliance
.. with the License. You may obtain a copy of the License at
.. http://www.apache.org/licenses/LICENSE-2.0
.. Unless required by applicable law or agreed to in writing,
.. software distributed under the License is distributed on an
.. "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
.. KIND, either express or implied. See the License for the
.. specific language governing permissions and limitations
.. under the License.
Real-time Write and Read
========================
Real-time mode makes writes visible to queries in the same process before a
snapshot commit. A shared ``RealtimeContext`` owns the uncommitted stores;
queries combine an immutable view of those stores with committed table files.
Readers without that context see only committed files.
Real-time storage is pluggable through the ``RealtimeStore`` interface. The
default implementation uses Apache Arrow to buffer rows and serve reads.
Applications can provide their own ``RealtimeStoreFactory`` through
``RealtimeContext::Create(factory)`` to customize storage and retrieval while
keeping Paimon's real-time write, read, and commit lifecycle.
Durability still comes from snapshot commits. Upstream must retain replayable
input and provide monotonically increasing offsets for each partition-bucket.
Real-time operations currently support only the main branch. Use a separate
context for each table, and coordinate a single active writer for each
partition-bucket.
Modes and Capabilities
----------------------
The table schema and options select the mode; there is no separate
``realtime.mode`` option.
.. list-table::
:header-rows: 1
:widths: 18 25 27 30
* - Mode
- Selection
- Uncommitted state
- Query semantics
* - Append
- No primary key; no real-time deduplicate key
- Arrow batches in write order
- Concatenate committed files and the visible memory tail
* - PK MOR
- Primary key; ``deletion-vectors.enabled=false``
- Batches sorted by primary key and sequence number
- Merge disk runs, sealed batches, and building batches by primary key
* - PK DV
- Primary key; ``deletion-vectors.enabled=true``
- The same sorted PK store as PK MOR
- Apply committed DVs, then merge disk and memory by primary key
* - Append deduplicate
- Append table with ``realtime.deduplicate-key-fields`` set and
``deletion-vectors.enabled=true``
- Mark superseded rows in an in-memory DV during writes; persist DV files
at commit
- Full predicate pushdown; apply the in-memory DV to disk and memory reads
without MOR
All four modes support read-before-prepare, sealed-but-uncommitted reads,
partition-bucket isolation, offset-aware commit and recovery, immutable read
views, projection, and optional sealed-segment spill with the default store.
The shared read pipeline also handles nested projection, selected MAP keys,
and supported VARIANT access over physical store fields. Predicate pruning
and exact residual filtering follow the mode-specific rules below.
Append accepts ``INSERT`` only. PK and append deduplicate also accept
``UPDATE_AFTER``, ``DELETE``, and ``UPDATE_BEFORE``. PK supports composite
primary keys, but its real-time merge engine is currently limited to
``merge-engine=deduplicate``. Append deduplicate currently accepts exactly one
non-null, non-nested key field and is experimental: its persisted index format
may change.
PK DV and append deduplicate are different designs. PK DV retains primary-key
merge semantics; append deduplicate uses a persisted stable row offset and
does not turn the table into a primary-key table.
Configuration
-------------
Common Options
~~~~~~~~~~~~~~
.. list-table::
:header-rows: 1
:widths: 33 15 52
* - Option
- Default
- Meaning
* - ``realtime.enabled``
- ``false``
- Enables real-time operations. The builders must also receive a shared
``RealtimeContext`` to select process-local writing and union reading.
* - ``realtime.spill-enabled``
- ``false``
- The default store synchronously spills frozen Arrow data to temporary
IPC files during explicit ``Seal``. Prepare does not trigger spill.
* - ``realtime.read-view-ttl``
- ``5 min``
- Positive lifetime of a scan's read-view ticket before reader creation.
* - ``realtime.store.stats-mode``
- ``none``
- ``none`` or ``full``. Full mode collects per-batch min/max and null counts
for conservative predicate pruning, including retained spill metadata.
* - ``realtime.deduplicate-key-fields``
- Unset
- Selects append deduplicate mode. Although expressed as a comma-separated
list, currently exactly one field is accepted.
* - ``bucket``
- Must be set
- Real-time mode requires fixed buckets: ``bucket > 0``.
* - ``read.batch-size``
- ``1024``
- Maximum raw batch size returned by the default store's query readers;
not the number of visible rows after bitmap filtering.
* - ``spill-compression``
- ``zstd``
- Compression used by temporary spill files.
* - ``spill-compression.zstd-level``
- ``1``
- Compression level when using Zstandard.
Set ``WriteContextBuilder::WithTempDirectory()`` when enabling spill. The
default factory rejects spill without a non-empty temporary directory or a
file system. Spill is explicit at seal boundaries; the normal write-buffer
memory manager does not automatically bound real-time store memory.
Setting ``realtime.enabled=true`` alone does not attach an in-process store.
This permits a separate ordinary writer to compact committed files without
joining the real-time context. It does not permit ordinary row writes into an
append deduplicate table.
Mode-specific Setup
~~~~~~~~~~~~~~~~~~~
For append mode, create a table without primary keys and use:
.. code-block:: text
bucket = 4
realtime.enabled = true
For PK MOR, define the table's primary keys and use:
.. code-block:: text
bucket = 4
realtime.enabled = true
merge-engine = deduplicate
deletion-vectors.enabled = false
For PK DV, define primary keys and enable DVs instead:
.. code-block:: text
bucket = 4
realtime.enabled = true
merge-engine = deduplicate
deletion-vectors.enabled = true
Both PK modes use the same real-time writer and store. PK DV uses the normal
PK deletion-vector and compaction infrastructure; it does not require the
append deduplicate ``.offset`` sidecar or an offset bitmap file index.
For append deduplicate, create a table with no primary keys. Its schema must
contain ``_REALTIME_OFFSET BIGINT NOT NULL`` and a non-null key, for example
``id BIGINT NOT NULL``. Deduplication is scoped to a partition: the same key may
exist independently in different partitions. The bucket key must use only
deduplicate key fields; applications are responsible for assigning rows to the
correct bucket. For example, use ``bucket-key=id``:
.. code-block:: text
bucket = 4
bucket-key = id
realtime.enabled = true
realtime.deduplicate-key-fields = id
deletion-vectors.enabled = true
deletion-vectors.bitmap64 = false
file-index.read.enabled = true
file-index.bitmap.columns = _REALTIME_OFFSET
The two indexes serve different purposes:
* The ``.offset`` sidecar looks up existing offsets by deduplicate key during
writes, so superseded rows can be marked in the in-memory DV.
* ``file-index.bitmap.columns=_REALTIME_OFFSET`` builds an offset index used
during queries to quickly map in-memory DV offsets to exact file row
positions and filter those rows.
The offset column must have an index supporting exact BIGINT ``IN`` and
``NOT_IN`` row-position matching. Among Paimon's default plugins, supported
choices include ``bitmap``, ``bsi``, and ``range-bitmap``. Indexes may be embedded
in file metadata or stored in an external file-index file. Approximate indexes
such as Bloom filters are insufficient.
Index capabilities are checked when creating a table and loading its schema,
not for each file during reads. Applications must keep the index configuration
consistent with existing files and ensure that those files have compatible
exact indexes.
The bitmap64 prohibition concerns persisted file DVs; logical deleted offsets
are still represented by ``RoaringBitmap64`` in memory.
The ``.offset`` sidecar lookup uses ``cache-page-size``,
``lookup.cache-max-memory-size``, and
``lookup.cache.high-priority-pool-ratio``. These configure its lookup cache,
not a total memory limit for the real-time store.
Input Schema and Offsets
------------------------
Offsets are application-assigned, non-negative ``int64`` values scoped to one
logical partition-bucket. They must be strictly increasing within each input
batch and across subsequent batches. Gaps are allowed; reuse or backward
movement is rejected. ``INT64_MAX`` cannot be an input offset because the
exclusive end must also fit in ``int64``.
.. list-table::
:header-rows: 1
:widths: 20 45 35
* - Mode
- ``FileStoreWrite::Write`` input layout
- Persisted offset
* - Append
- ``[_REALTIME_OFFSET, table fields...]``
- Internal prefix removed before writing data files
* - PK MOR / PK DV
- ``[_REALTIME_OFFSET, table fields...]``
- Internal prefix removed before writing data files
* - Append deduplicate
- Table schema in its declared field order, including ``_REALTIME_OFFSET``
- Normal table column retained in data files and compaction output
Do not add the internal offset prefix to the formal table schema in ordinary
append or PK mode. In append deduplicate mode, do not add a second prefix: the
offset is already a formal field and may occupy any declared position.
Construct Arrow batches in the required layout and set their partition and
bucket with ``RecordBatchBuilder::SetPartition()`` and ``SetBucket()``. Routing
and offset assignment belong to the application. Deduplication never crosses
partition-bucket boundaries.
For PK writes, the framework adds ``_SEQUENCE_NUMBER`` and ``_VALUE_KIND`` and
sorts each stored batch by primary key, then sequence number. Input offsets
are validated before sorting. The sequence number, not the numeric offset,
resolves PK versions; application-defined ``sequence.field`` is not supported
by real-time PK mode.
An ``OffsetRange`` is half-open: ``[begin, end)``. It is an event-progress
envelope, not a row count. For example, input offsets ``10, 12, 15`` cover
``[10, 16)``. Deletes consume offsets even when they produce no data-file row,
so a delete-only prepare can advance progress with an empty data increment.
API Lifecycle
-------------
Use the same ``RealtimeContext`` for writing, planning, and reading. Enable
streaming mode on the writer for progress-aware checkpoints; real-time union
queries themselves use batch scans.
The snippets below run inside a ``Status`` or ``Result``-returning function
using the ``paimon`` namespace. They assume an existing native table,
``table_path``, a stable ``commit_user``, the mode-specific ``options`` map,
and an input ``batch`` built as described above.
.. code-block:: cpp
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<RealtimeContext> realtime,
RealtimeContext::Create());
WriteContextBuilder write_builder(table_path, commit_user);
write_builder.SetOptions(options)
.WithStreamingMode(true)
.WithRealtimeContext(realtime);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<WriteContext> write_context,
write_builder.Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreWrite> writer,
FileStoreWrite::Create(std::move(write_context)));
PAIMON_RETURN_NOT_OK(writer->Write(std::move(batch)));
To query before preparing or committing:
.. code-block:: cpp
ScanContextBuilder scan_builder(table_path);
scan_builder.SetOptions(options).WithRealtimeContext(realtime);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ScanContext> scan_context,
scan_builder.Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<TableScan> scan,
TableScan::Create(std::move(scan_context)));
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Plan> plan, scan->CreatePlan());
ReadContextBuilder read_builder(table_path);
read_builder.SetOptions(options).WithRealtimeContext(realtime);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ReadContext> read_context,
read_builder.Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<TableRead> read,
TableRead::Create(std::move(read_context)));
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<BatchReader> reader,
read->CreateReader(plan->Splits()));
// Consume NextBatch() or NextBatchWithBitmap(), then Close() the reader.
Apply the same query predicate to scan and read. If exact residual filtering
must happen inside the library, call
``ReadContextBuilder::EnablePredicateFilter(true)``. File and batch statistics
are conservative pruning, not a substitute for evaluating the user predicate.
Checkpoint with progress-aware APIs:
.. code-block:: cpp
const int64_t checkpoint_id = 1;
PAIMON_ASSIGN_OR_RAISE(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(checkpoint_id));
CommitContextBuilder commit_builder(table_path, commit_user);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<CommitContext> commit_context,
commit_builder.SetOptions(options).Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreCommit> commit,
FileStoreCommit::Create(std::move(commit_context)));
PAIMON_ASSIGN_OR_RAISE(
int64_t snapshot_id,
commit->CommitWithProgress(progress, checkpoint_id, /*watermark=*/std::nullopt));
PAIMON_RETURN_NOT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
PAIMON_RETURN_NOT_OK(writer->Close());
The operations have different responsibilities:
1. ``Write`` publishes rows to the process-local building store.
2. Optional ``Seal`` freezes a segment, opens a new building segment, and may
spill. It does not create a committed snapshot or prepare data-file messages.
3. ``PrepareCommitWithProgress`` freezes the current building segment without
spilling and writes it together with earlier sealed segments to table data
files, returning their offset ranges together with commit messages. It does
not publish a snapshot or reclaim the real-time data.
4. ``CommitWithProgress`` atomically publishes files, indexes, and the updated
partition-bucket offset map. The snapshot's ``realtime.offsets`` property
references a metadata file containing the durable progress.
5. ``RefreshCommittedSnapshot`` loads the committed progress and reclaims
fully covered sealed segments. Deduplicate refresh also replaces the active
file-key lookup atomically with memory/DV advancement.
Keep every prepared-but-uncommitted ``RealtimeCommitProgress`` entry. If a later
checkpoint subsumes earlier prepares, pass the complete outstanding prefix to
commit, not just the latest prepare result. Offset gaps make omitted prepares
indistinguishable from valid sparse input, and skipping one can cause data loss.
``CommitMessage`` serialization does not contain the progress range or the
deduplicate ``deleted_offsets`` bitmap. Preserve the complete progress when
coordinating checkpoints. Snapshot conflicts are retried internally against
the latest snapshot; commit identifiers must follow the streaming commit
protocol. With a version-managed catalog, configure the writer and committer
with the same ``WithCatalog()`` catalog and identifier.
Read Views and Consistency
--------------------------
Planning first pins immutable memory views and then plans committed disk data.
For each active partition-bucket it captures two exclusive bounds:
.. code-block:: text
disk: progress covered by the selected snapshot
memory: [committed_end_offset, memory_end_offset)
Filtering memory by these bounds prevents duplicate reads after a commit and
excludes writes after the captured memory view. Existing views remain valid
across later writes, seals, spill publication, and refresh-driven reclamation.
Ordinary append scans keep earlier disk splits independently schedulable and
attach the memory view to a tail split. PK and append deduplicate group a
partition-bucket's disk splits with its memory view so that merge or pending
deletion semantics cover the whole bucket.
A real-time split carries an opaque read-view ticket. Resolve it with the same
context in the same process. Successful reader creation consumes the ticket;
failed creation retains it until its TTL expires. Once a reader is created,
the reader pins its view independently of ticket expiry. Replan if a ticket
expires, or if a previously consumed split needs to be read again.
An explicit snapshot id is accepted only if its progress is not behind the
context's reclaimed progress for relevant active stores. It does not restore
an old in-memory state and is not unrestricted historical time travel. For a
disk-only historical query, do not attach the real-time context.
Implementation by Mode
----------------------
Append
~~~~~~
``RealtimeAppendOnlyWriter`` validates offsets and writes to an
``APPEND_ONLY`` store. The default ``ArrowRealtimeStore`` retains building
batches and immutable sealed segments in write order. Query readers project
the requested fields, optionally prune stored batches using statistics, and
apply the visible offset range before removing the hidden offset column.
Preparing reads sealed segments through the ordinary ``AppendOnlyWriter``,
removing the internal offset prefix before writing table files. Queries do not
need key lookup or version merge; disk and memory readers are concatenated.
Primary Key MOR
~~~~~~~~~~~~~~~
``RealtimePrimaryKeyWriter`` materializes PK transport batches with sequence
number, row kind, and offset. Each sorted batch is an independent merge input;
multiple stored batches do not form a single globally sorted stream.
Query construction merges sorted disk sections and the memory readers, applies
deduplicate merge semantics, and drops final delete records. It collects all
disk splits belonging to the active partition-bucket, rather than merging
memory against only one disk split. Preparing similarly merges the sealed
streams before handing them to ``MergeTreeWriter`` to write normal PK files.
Only primary-key predicates are safe to push into unresolved versions. A
predicate on a value column must be evaluated after version merge: filtering
out the newest value first could incorrectly expose an older matching value.
Primary Key DV
~~~~~~~~~~~~~~
PK DV uses the same real-time PK write and sequence-number logic. During a
union read, committed file DVs remove already-invalid physical rows, but L0
runs and memory may still contain duplicate keys and delete records. The final
reader therefore still performs a primary-key merge across disk and memory.
For real-time scans, L0 is retained instead of being skipped as in the normal
DV batch scan. Predicate handling distinguishes storage levels:
* High-level files (``level > 0``) can use the full predicate for statistics and
file-index pruning under committed DV semantics.
* L0 and memory receive only primary-key predicates before merge.
* The full residual predicate is applied after merge when explicitly enabled
in the read context.
Enabling DVs does not make uncommitted PK data append-only and does not make
arbitrary value-predicate pushdown safe for L0 or memory.
Append Deduplicate / Offset DV
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This mode uses two independent lookup directions:
.. code-block:: text
key -> logical offset(s) memory key indexes / .offset sidecars
logical offset -> physical row positions exact _REALTIME_OFFSET file index
For each write, ``RealtimeDeduplicateWriter`` looks up existing keys across
committed files, sealed segments, and the building segment. Matching old offsets
are marked deleted, and within-batch duplicates are resolved in row order.
Add rows are appended to the store; delete events update only the key/DV state,
without persisting a delete row.
Key indexes and DV bitmaps use copy-on-write snapshots, so later writes do not
change the state held by existing lookups, read views, or seal checkpoints.
Memory reads use the read view's DV to exclude rows whose offsets are marked
deleted.
Disk reads apply both committed file DVs and pending offset deletions. Pending
deletions become per-file ``_REALTIME_OFFSET NOT IN (...)`` predicates, trimmed
using reliable min/max statistics or left untrimmed when statistics are missing
or ambiguous. The exact file index enforces these predicates independently of
optional residual filtering. The offset is omitted from the final result unless
the user selected it.
Prepare retains each seal's logical deletion checkpoint and writes surviving
add rows with their original offsets. Each output file gets a ``.offset``
sidecar mapping key fingerprints to offsets, separate from the exact offset
file index.
Each commit attempt resolves deleted offsets to row positions in the latest
snapshot's files and the prepared new files, then merges them into the committed
file DVs. Repeating this resolution on retries handles file changes caused by
conflicting commits or compaction.
Spill and Memory Accounting
---------------------------
The default store freezes the current building data and opens a new building
segment under a short lock. Explicit ``Seal`` writes the frozen segment to an
Arrow IPC spill file outside the write lock when spill is enabled. Prepare
freezes without spilling and writes directly to table data files, reading any
earlier spilled segments as needed. There is no background spill worker: the
seal/prepare thread waits for its I/O, while another thread can write to the
new building segment.
Deduplicate uses two-phase seal to freeze data, key index, offset range, and DV
checkpoint at the same boundary. The frozen segment remains queryable and its
keys remain discoverable while spill is in progress. Only after writing and
closing the file successfully is its stored representation replaced. A spill
failure keeps the in-memory segment and cleans up the incomplete file, but is
still terminal for the owning writer/context.
Old views can retain the memory representation after spill or reclamation;
new views use the published representation. Spill files are deleted when the
store and all remaining handles/readers have released them. They are temporary
storage, not a recoverable log after process restart. Deduplicate key indexes
and DVs remain in memory; full-mode statistics also remain resident after spill.
``RealtimeContext::GetMetrics()`` and real-time writer metrics expose:
* ``realtimeBuildingMemoryBytes``, ``realtimeSealedMemoryBytes``,
``realtimeTotalMemoryBytes``;
* ``realtimeBuildingRowCount``, ``realtimeSealedRowCount``,
``realtimeTotalRowCount``.
These are current store accounting, not process RSS or a visible-row count.
Rows include stored versions hidden by merge/DV filtering. Memory counts include
Arrow buffers and retained statistics; deduplicate also counts retained key-map
buffers, row-offset bitmaps, and distinct deletion checkpoints.
Recovery, Schema Changes, and Compaction
----------------------------------------
Recovery and Failure Handling
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Read durable progress with ``RealtimeSnapshotOffsets::ReadAllOffsets()`` or
``ReadOffset()`` from a committed snapshot. Replay starting at the returned
offset. If ``ReadOffset()`` returns ``-1`` for a partition-bucket, use the
upstream's initial position.
A new context restores committed progress, but uncommitted data requires
upstream replay. Reusing a healthy context preserves its uncommitted tail;
retain any outstanding prepared progress when replacing the writer.
After a write/flush, seal, prepare, or terminal progress-commit failure, discard
the writer and context and recover from the latest committed progress. Closing
with unprepared data also invalidates the context. Prepare alone is not durable:
retain prepared progress until commit, even after the writer closes.
Schema Changes
~~~~~~~~~~~~~~
Reusing a live store requires the same physical schema and mode. After a schema
change, recreate the context and writer and replay uncommitted input.
``data-evolution.enabled`` is not supported in real-time mode.
Deduplicate reads and lookups support historical file schemas, which must remain
available together with their key options. Key renames and field reordering are
supported when key field ID, type, and non-nullability stay compatible.
``_REALTIME_OFFSET`` must retain its name, field ID, and non-null int64 type.
Compaction and Destructive Operations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The real-time writer does not compact files automatically or support explicit
``Compact``. Use a separate ordinary compaction writer without
``WithRealtimeContext()``, then refresh to the resulting snapshot.
Append deduplicate compaction preserves surviving offsets, rebuilds indexes,
and remaps the latest deletions to the output files. External rewrites must not
drop the required offsets or indexes.
Overwrite, truncate, partition drop, and rollback may reset progress and are not
automatically coordinated with active real-time writers. Stop affected writers,
recreate their contexts, and reset the upstream recovery position before resuming.
Extension Points and Current Limits
-----------------------------------
``RealtimeContext::Create(factory)`` accepts a custom ``RealtimeStoreFactory``.
Custom stores manage buffering, read views, and spill while following the
``RealtimeStore`` contracts. Paimon manages table files, indexes, merge semantics,
and commits. Append deduplicate also keeps key indexes and DVs in the framework;
custom delegates need ``RealtimeStoreTwoPhaseSeal`` to enable its spill.
Current restrictions include:
* Native tables on the main branch with fixed positive bucket counts and full
write schemas only.
* Process-local batch union scans only. Streaming scans, limit pushdown,
global-index result splits, vector/full-text search, and count-only real-time
readers are unsupported.
* PK mode supports the deduplicate merge engine with default delete handling,
ascending sequence order, and ``changelog-producer=none``. Custom
``sequence.field``, floating-point keys, PK global indexes, lookup without DVs,
and read-optimized union scans are unsupported.
* Append deduplicate requires a single non-null scalar key, a persisted offset
column, exact offset file-index reads, and non-bitmap64 DVs. Existing ordinary
append files are not migrated automatically.
* Spill covers Arrow data, not key indexes or DVs, and provides neither a hard
memory limit nor crash recovery.
Code Map
--------
* ``include/paimon/realtime/``: public context, store, progress, and recovery APIs.
* ``src/paimon/core/realtime/``: mode-specific writers, default Arrow store,
deduplicate wrapper/indexes, schema layout, and memory-read pipeline.
* ``src/paimon/core/table/source/realtime_table_scan.cpp``: memory pinning,
snapshot-progress validation, and partition-bucket split grouping.
* ``src/paimon/core/table/source/append_only_table_read.cpp``: append union reads
and per-file pending offset-DV predicates.
* ``src/paimon/core/operation/merge_file_split_read.cpp``: PK disk/memory merge
and level-aware predicate pushdown.
* ``src/paimon/core/operation/commit/realtime_*``: durable progress, logical-DV
materialization, commit retry, and compaction DV remapping.
* ``test/inte/realtime_write_inte_test.cpp``: lifecycle and mode-specific
integration coverage; store/reader unit tests live next to their implementation.