Skip to content

[client] Fix data hole in primary key table batch read#3627

Open
beryllw wants to merge 1 commit into
apache:mainfrom
beryllw:spark-kv-read-bugfix
Open

[client] Fix data hole in primary key table batch read#3627
beryllw wants to merge 1 commit into
apache:mainfrom
beryllw:spark-kv-read-bugfix

Conversation

@beryllw

@beryllw beryllw commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Purpose

Linked issue: close #3626

Brief change log

  • Fix silent data loss when batch-reading a primary key table from Spark: change log records whose keys are greater than the max snapshot key were dropped, so COUNT(*) < max_seq - min_seq + 1.
  • Root cause is in SortMergeReader: the merge was driven by the snapshot iterator and stopped once the snapshot was exhausted, discarding trailing change log records. It surfaces on the non-lake Spark batch read path (FlussUpsertPartitionReader), which calls readBatch() only once.

Rewrite SortMergeReader as a single two-way merge iterator over the snapshot stream and the change log stream (peek both heads, advance the smaller key; for an equal key the change log overrides the snapshot; a delete removes the row).The merged output is now identical regardless of how many times readBatch() is called — Spark calls it once, the lake scanner calls it repeatedly — which removes the structural cause of the dropped tail and also unifies projection handling. The public API is unchanged, so no caller needs to be modified.

Tests

API and Format

Documentation

@beryllw beryllw force-pushed the spark-kv-read-bugfix branch from a0d4bdb to cd0aaf5 Compare July 9, 2026 07:33
SortMergeReader merged the snapshot and change log driven solely by the
snapshot iterator, so once the snapshot was exhausted the trailing change
log records (keys greater than the max snapshot key) were silently dropped.
Spark's non-lake primary key batch read (FlussUpsertPartitionReader) calls
readBatch() only once and hit this directly, producing a data hole where
COUNT(*) < max_seq - min_seq + 1.

Rewrite SortMergeReader as a single two-way merge iterator so the merged
output is independent of how many times readBatch() is called (Spark calls
it once, the lake scanner calls it repeatedly). Add a unit test covering
change log keys beyond the max snapshot key and an end-to-end Spark ITCase.
@beryllw beryllw force-pushed the spark-kv-read-bugfix branch from cd0aaf5 to 0497f35 Compare July 9, 2026 08:01
@wuchong

wuchong commented Jul 9, 2026

Copy link
Copy Markdown
Member

cc @Yohahaha could you help to review this?

@Yohahaha Yohahaha left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for the fix!

the SortMergeReader looks more robust to me after extract two iters into one merge iter.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a correctness issue in Fluss primary-key table batch reads where trailing change-log records (with keys greater than the max snapshot key) could be silently dropped, causing “data holes” in Spark batch reads and inconsistent results depending on how the reader is consumed.

Changes:

  • Rewrites SortMergeReader into a single two-way merge iterator over snapshot + change log so trailing log records are emitted in-order and not dropped.
  • Adds a SortMergeReader unit test that validates change-log rows beyond the snapshot’s max key are emitted.
  • Adds a Spark UT reproducing/guarding against the monotonic-key “data hole” scenario.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/SparkPrimaryKeyTableReadTest.scala Adds Spark regression test asserting monotonic primary-key batch reads have no missing tail rows.
fluss-client/src/test/java/org/apache/fluss/client/table/scanner/SortMergeReaderTest.java Adds unit test ensuring change-log records beyond snapshot max key are emitted.
fluss-client/src/main/java/org/apache/fluss/client/table/scanner/SortMergeReader.java Replaces prior snapshot-driven merge with a two-way merge iterator over snapshot and change-log streams.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +202 to 206
@Override
public void close() {
snapshotRecordIterator.close();
changeLogIterator.close();
}
Comment on lines +86 to +89
/**
* Returns the merged-row iterator, or {@code null} when nothing is left. The same instance is
* returned on every call, so repeated invocations keep draining the snapshot and change log.
*/

@wuchong wuchong left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

I think the Copilots' review comment is not critical, because existing callers either fully consume the iterator before closing it or close the scanner when terminating early.

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.

[Bug] Spark batch read on primary key table returns a data hole (missing newest rows)

5 participants