Repository navigation
feat(lance_ray): support distributed merge_into - #5296
Open
hfutatzhanghb wants to merge 31 commits into
Open
hfutatzhanghb wants to merge 31 commits into
hfutatzhanghb wants to merge 31 commits into
Conversation
Compare deletion-file and data-file identities only after a commit conflict so concurrent merge-on-read updates cannot be overwritten just because the fragment id still exists. The successful first commit does no extra fragment IO.
Rely on LanceDataset.commit to rebase concurrent appends. Resubmitting the same Update after bumping read_version stacked a second transaction on top of Lance's internal retries and could double-apply insert-only merges.
If LanceDataset.commit raises but the latest manifest already contains this operation's new fragments and deletion metadata, return that dataset instead of failing so a job-level retry cannot double-insert.
Require pylance 11+ and attach matched _rowid values via RowIdSequence so rewritten rows keep their identity.
Keep all (fragment, rowid) hits for a join key so one-to-many targets match pylance merge_insert instead of last-write-wins.
Uncommitted write_fragments use placeholder fragment ids, so lost-ack visibility must look up committed fragments by data file identity.
Avoid building an unbounded _rowid IN (...) SQL predicate when masking matched rows; pass the ids to LanceFragment.delete_rows.
delete_rows addresses rows by fragment-local physical offset from _rowaddr, not logical _rowid. Keep _rowid only for stable-id remap.
Keep apply fan-out at num_workers and yield only non-empty shuffle buckets so raising num_partitions no longer creates P^2 Ray objects.
Render date, timestamp, decimal, and binary keys as Lance SQL literals, and reject nested types on the driver before plan tasks.
Allocate rowid and offset helper names from the target schema so user fields with the default names are left intact on plan and apply.
Use arrow_cast ticks so nanosecond and timezone keys match exactly under both scan and BTREE lookups.
Contributor
Author
|
Hi, @rgao25 . Have optimized based on your great work. PTAL when have free time, Thanks very much! |
added 13 commits
October 9, 2026 18:40
Cast join keys before global dedupe, quantize decimals at column precision, and include nested field ids so stable-row-id rewrites do not reuse stale indexes. Apply tasks now stream plan buckets so peak memory follows the largest chunk.
Keep apply-task row ids and offsets in Arrow buffers, drop row ids after the duplicate check, and release offsets once the fragment deletion file is written. Stable row ids are attached from the bucket being written.
LanceFragment.delete_rows casts each offset with int(). Arrow scalars from a ChunkedArray fail that cast on the CI PyArrow build, so convert the packed buffer to a NumPy array for the call and release it immediately.
Deserialized identity columns can alias the whole bucket allocation. Copy row ids and offsets into compact Arrow buffers before they are kept across buckets.
pyarrow-stubs types ChunkedArray.chunk() as ChunkedArray and buffers() elements as Buffer | None. Read chunks and narrow those values.
NaN and infinity bypass dedupe and are not SQL literals. Record the real object-store footprint, orphan files, and the requirement to serialize every writer. Keep new fragments on the dataset storage version.
Keep the Ray dataset in its own variable so map_batches stays typed when stubs return Any. The join-key type comes from the target schema, which also avoids an extra limit(1) execution.
0.1 and 1e-05 now go through both the scan plan and an indexed IN probe.
Dictionary and float16 keys have no Arrow sort or inequality kernel, so sort and dedupe them on a decoded or widened column and keep the original type. Render Decimal256 literals with arrow_cast, and double backticks inside join-key identifiers.
Int64 minimum, negative-scale decimals, and dates outside Python's year range now sort and look up through typed values. Stable row ids stay UInt64, view-typed in-memory tables are cast before Ray, and backtick join columns are rejected because Lance 12 cannot read them.
CI PyArrow rejects Python numbers in a float16 array and requires numpy.float16 values. Compare the round-tripped keys as Python floats.
Multiplying a full-precision coefficient by a scale-0 power of ten overflows before the final cast. Carry that power as a precision-1 decimal at the same negative scale so the product coefficient fits.
10**76 does not fit in one scale-0 Decimal256. Build that power as 10**75 times 10**1, each already at a negative scale, so the coefficient multiply stays inside precision 76.
added 2 commits
October 10, 2026 15:34
CI PyArrow has no half-float is_nan or is_inf kernel. Widen only the finiteness check to float32, which preserves every float16 value.
Lance parses a bare SQL number as Float64 once it no longer fits in a signed 64-bit token. UInt64 keys at or above 2**63 now use the same typed arrow_cast as Int64's minimum.
Contributor
Author
|
Hi, @yanghua . Could you please review this PR when have free time ? Thanks very much ! This PR is coworked by @rgao25 and @hfutatzhanghb . |
Contributor
Author
|
Hi @yanghua @ddupg . Could you please review this PR when have free time? This PR is coworked by @rgao25 and @hfutatzhanghb . I think this is a very useful feature for large-scale updating. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
lance_ray.merge_into, a fragment-parallel counterpart of pylancemerge_insert(on).when_matched_update_all().when_not_matched_insert_all(), committed as one atomicLanceOperation.Update._rowidviaRowIdSequence, and delete matched rows withLanceFragment.delete_rowsusing local physical offsets from_rowaddr.fields_modified=[],fields_for_preserving_frag_bitmapset to every field id in pre-order (nested leaves included), andupdate_mode="rewrite_rows", so no existing index treats a rewritten fragment as already indexed. This mirrors the full-schema rewrite path inrust/lance/src/dataset/write/merge_insert.rs."01"against an integer column) collapse into one. Dictionary and float16 keys are sorted and compared on a decoded or widened helper column; the original column type is kept.pc.not_equal(NaN, NaN)is true andfloat("nan")does not collapse inside a Pythonset, so neither the dedupe nor the plan-side duplicate check would catch those rows, andrepr(nan)is not a Lance SQL literal.read_versionretry, treat a lost success ack as success (match new fragments by data-file identity), and fail when concurrent MoR rewrites deletion metadata.num_partitions(plan shards) fromnum_workers(apply owners) and yielding only non-empty buckets.arrow_cast. A join column name containing a backtick is rejected, because Lance 12 drops that field from its query schema and a filter cannot read it. An in-memory table withstring_vieworbinary_viewcolumns is cast to the target schema before Ray serializes it. Stable_rowidvalues stay UInt64, including ids at or above 2**63; insert rows carry a null placeholder instead of-1.data_storage_version.target_schemainstead ofDataset.schema(), so the driver no longer triggers alimit(1)run, and every Ray call result is re-bound through_require_ray_datasetso type narrowing does not depend on the Ray stubs.Notes and limitations (expanded in
docs/src/merge_into.md)num_partitionsbounds the deserialized source one apply worker holds, not cluster object-store use. After dedupe,materialize()plusto_arrow_refs()keep the full deduplicated source resident, the driver holds every bucket reference until commit, and join-all can copy one source row into several buckets.LanceDataset.commitand both commits succeed, so a key it inserts that this merge also inserts survives twice.write_fragmentsand deletion files already written bydelete_rowsare not removed when a later apply task fails or the commit conflicts. Reclaim them withLanceDataset.cleanup_old_versions(). The default does not delete unverified failed-transaction files younger than 7 days. Passdelete_unverified=Trueonly when no other process is writing the dataset.target_bases/target_all_bases) stay at the Lance writer defaults, sincemerge_intoexposes no write-option arguments.Test plan
tests/test_merge_into.pycovers e2e merge, insert-only, update-only, empty-source no-op, more fragments than workers, more partitions than workers, join-all within one fragment and across fragments (with stable row ids), stable_rowidpreservation, no nested-index reuse after a stable-row rewrite, scalar-index merges with filtered reads on the key column, float keys with and without a BTREE, bool keys, dictionary and float16 keys, a decimal256 lookup above 38 digits, rejection of a join column whose name contains a backtick, Int64 minimum keys with and without a BTREE, UInt64 keys at and above 263 with and without a BTREE, negative-scale decimal keys, including full-precision decimal128(38, -2), decimal256(76, -2), and decimal256(76, -76) with and without a BTREE, dates outside year 1–9999, UInt64 row ids at or above 263, and an in-memory string_view/binary_view source, a second merge over an existing deletion vector, storage-version preservation, merge-on-read behavior (deletion vector written and data files untouched; a fully updated fragment is removed), dedupe within and across chunks and for keys equal only after cast, nanosecond timestamp and time keys indexed and unindexed, non-finite float key rejection, empty temporal batches keeping one schema, backtick-quoted index field names, helper-name collisions, date/decimal/binary keys, directory-namespace e2e, lost-ack and failed-commit paths, and the driver validation matrix. Unit tests cover the_rowaddrsplit, bucket packing, delete offsets, packed match identities, schema field ids, and SQL literal rendering.uv run pytest tests/test_merge_into.py(CItestsjob; not run in this checkout because itspylance.pthpoints at an unbuilt source tree)ruff checkandruff format --checkclean;mypyreports no errors inlance_ray/merge_into.pyortests/test_merge_into.pydocs/src/merge_into.md(num_workers/num_partitionsbest practices, notes and limitations), cross-checked against the implementation and the Lance 12.0.0 sources