Skip to content

[Bug] Table Stream @snapshot() drops MoW keys rewritten with identical values after the consumption offset #70277

Open

Description

@morningman

Search before asking

  • I had searched in the issues and found no similar issues.

Version

master @ 72e9189 (2026-11-03). The code paths below are unchanged on master as of 2026-11-05.

What's Wrong?

For a Unique Key Merge-on-Write base table, <stream>@snapshot() rebuilds the image at the consumption offset for partitions that changed after the offset. The rebuild combines two parts (NormalizeOlapTableStreamScan.java#L318-L345):

  1. Base table part: rows whose commit TSO is not newer than the partition offset.
  2. Changelog part: a MIN_DELTA changelog scan from the offset, filtered to DELETE and UPDATE_BEFORE rows, which adds back the before image of rows changed after the offset (#L160-L166, #L222-L229).

MIN_DELTA drops a key's change group entirely when the first before image equals the last after image (the no-op elimination added in #66849). In the UPDATE_BEFORE_AFTER case, _min_delta_values_equal() returns true and nothing is emitted for the key (block_reader.cpp#L318-L323, #L133-L167).

So when a key is rewritten with identical values after the offset (an upsert of the same row, or a DELETE followed by an INSERT of the same values):

  • the base table part filters the row out, because its commit TSO is now newer than the offset;
  • the changelog part emits nothing for the key, because MIN_DELTA treats the change as a no-op.

The key is then missing from @snapshot(), although it existed at the offset and still exists.

IVM may be affected as well. The join delta handler reads the other side's pre-refresh image through the same snapshot path (IvmJoinDeltaHandler → IvmDeltaRewriteHelper.preSnapshot() → withSnapshot(...); IvmJoinDeltaHandler.java#L123-L149, IvmDeltaRewriteHelper.java#L425-L448), so a join MV refresh could miss matches for such keys.

What You Expected?

@snapshot() returns every row that was visible at the consumption offset. The no-op elimination is correct for consumers of net changes, but the snapshot rebuild needs the UPDATE_BEFORE of such a group, or must read the changelog without the elimination.

How to Reproduce?

-- fe.conf: enable_feature_changelog = true, enable_table_stream = true
CREATE TABLE t (k INT, v VARCHAR(10))
UNIQUE KEY(k)
DISTRIBUTED BY HASH(k) BUCKETS 1
PROPERTIES (
    "replication_num" = "1",
    "enable_unique_key_merge_on_write" = "true",
    "changelog.enable" = "true",
    "changelog.need_historical_value" = "true"
);
INSERT INTO t VALUES (1, 'a'), (2, 'b');

CREATE STREAM s ON TABLE t
PROPERTIES ("type" = "min_delta", "show_initial_rows" = "false");

-- identical rewrite after the offset
INSERT INTO t VALUES (1, 'a');

SELECT * FROM s@snapshot() ORDER BY k;
-- expected: (1, 'a'), (2, 'b')
-- suspected: (2, 'b') only

DELETE FROM t WHERE k = 1; INSERT INTO t VALUES (1, 'a'); between the stream creation and the snapshot read should hit the same path.

Anything Else?

Found by code reading while documenting the changelog and Table Stream features; the reproduction above has not been run yet. Regression cases for identical rewrites and for delete-then-reinsert of the same values, under both @snapshot() and a join IVM refresh, would cover this.

Are you willing to submit PR?

  • Yes I am willing to submit a PR!

Code of Conduct

Activity

  1. Doris-Breakwater commented on Nov 5, 2026

    @Doris-Breakwater
    Collaborator

    Initial triage: high-confidence snapshot correctness defect supported by the pinned source, with a credible join-IVM consequence. The SQL reproduction and an IVM refresh have not been executed in this investigation, so the predicted missing rows remain to be confirmed at runtime. This assessment is for 72e91896bbdd8760f8bd4e488e5672406b40ce52; it does not establish affected release branches.

    Verified code path

    1. Snapshot reconstruction for changed non-duplicate-key partitions explicitly requests MIN_DELTA, independently of the stream's configured type. It unions a historical-TSO-bounded base scan with changelog before images. The changelog branch retains only DELETE and UPDATE_BEFORE. See snapshot reconstruction and operation filter.
    2. The offset conversion preserves the intended boundary: the base branch includes commits through the consumption TSO, while the changelog starts after it. BE applies these as half-open TSO predicates. A replacement row committed after the offset fails the base predicate; the old MoW row is not recovered merely by filtering the current base image. See offset conversion and BE TSO filtering.
    3. In UPDATE_BEFORE_AFTER, the reader emits neither row when the first before image equals the final after image. This happens before the snapshot's operation filter can recover anything. DELETE → APPEND also maps to UPDATE_BEFORE_AFTER, so the delete/reinsert scenario is supported by the same mechanism. See suppression and transition matrix.

    The contract mismatch is that zero net change does not imply that no before image is needed to reconstruct the offset snapshot. This also covers a value round trip such as a → b → a, not only an immediately identical upsert. Existing reader tests intentionally expect such net-zero chains to disappear from ordinary MIN_DELTA output; those tests were inspected, not run. The equality check conservatively retains updates for unavailable/all-NULL before images or unsupported comparisons, so the omission is conditional; the supplied non-NULL VARCHAR example fits the supported comparison case.

    IVM impact: supported call path, unverified outcome

    The join handler uses the other side's pre-snapshot for a left-side delta. A focused regression should establish a baseline, rewrite a matching right-side MoW row identically, and insert a matching left-side row before incremental refresh. In the contribution ΔL JOIN R_pre, the missing right-side snapshot row can suppress the new match, while ΔR is empty. An identical rewrite alone does not demonstrate an incorrect MV refresh. Capture the actual rewritten plan to confirm which side supplies the pre-snapshot.

    Missing evidence and next steps

    • Run both supplied reproductions on the pinned build, waiting for writes to become visible. Capture actual base/stream/snapshot rows, FE and BE build identifiers, deployment mode, SHOW CREATE TABLE, SHOW CREATE STREAM, stream consumption state before/after, and EXPLAIN VERBOSE for the snapshot. The issue already supplies a useful version and minimal SQL; the main gap is executed evidence. Query IDs and relevant FE/BE logs are useful if execution diverges from this path.
    • Preserve per-key folding for snapshot reconstruction while retaining the first required before image even for equal endpoints, using a snapshot-specific reader contract. Keep ordinary net-change suppression semantics. A direct switch to DETAIL plus the existing operation filter is insufficient: a → b → a exposes intermediate before images and can add extra rows unless reconstruction selects the correct first image per key. Changing the stream type also does not bypass the hard-coded snapshot scan type.
    • Add snapshot regressions for identical upsert, delete/reinsert, and value round trips, alongside changed-value, insert-only, delete-only, and untouched-row controls. Check repeated snapshot reads do not advance consumption. Extend coverage to stream types sharing this reconstruction path and to small batches/compaction.
    • Add the join-IVM case above and compare incremental results against a fresh base-table evaluation with both enable_materialized_view_rewrite=false and enable_dml_materialized_view_rewrite=false. Confirm incremental execution rather than a complete-refresh fallback.

    Suggested routing: Table Stream/Nereids planning and BE changelog reader maintainers, with IVM maintainers reviewing the integration regression. No code changes were made.

    Breakwater-GitHub-Analysis-Slot: slot_9bbb44509cce

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions