Replies: 2 comments
|
Thanks @Xuanwo for putting this together — strong +1 on the overall direction, especially the mechanism-vs-coordination boundary. Keeping From the lance-spark perspective, this fixes a real structural limitation. Today's distributed One small question: write amplification on the concat step. The new column bytes hit storage twice — once when a worker writes its slice file, and again when concat copies the payload into the final file. For wide columns like embeddings (4KB+ per row), that second copy is real IO and object-store cost. This shouldn't block the proposal, though. Would it be possible to treat zero-rewrite stitching — making slice files independently referenceable so concat becomes metadata-only — as an explicit phase 2 if necessary? |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
This proposal adds two related low-level capabilities:
lance-fileprimitive for concatenating compatible encoded Lance files without decoding and re-encoding their page payloads.FileFragmentAPI for writing independently computed physical-row slices of one or more existing columns and combining a complete set of slices into aDataReplacementGroup.The API is intentionally a mechanism, not a distributed execution framework. Lance does not own task planning, scheduling, retries, progress, recovery, or commit granularity. Those remain the responsibility of Ray, Spark, or another caller-controlled coordinator.
No Lance file layout, manifest feature, reader layout, or transaction operation is added.
Motivation
Binary-copy compaction currently owns both compatibility checks and buffer-copy rewriting inside the dataset layer. This duplicates file-format knowledge and risks eligibility checks drifting from the actual rewrite behavior.
Distributed column recomputation has the same underlying file operation. Workers can independently compute and encode row-aligned pieces of one or more columns, but the final replacement needs a regular Lance file covering the entire fragment. Concatenating those already encoded files should not require decoding Arrow arrays or rerunning the UDF.
These two callers should share the encoded-file transformation while retaining their own dataset semantics:
Encoded file concatenation
The proposed one-shot primitive is conceptually:
Each input is a complete immutable Lance file. The primitive does not expose an internal plan and does not slice ranges out of a file.
It reads metadata once, determines the actions required for page, column, and file buffers, copies page payloads unchanged, relocates offsets and row-relative metadata, and writes a new footer. Exact-version details such as structural headers, alignment, and footer behavior remain in version-specific hooks.
Unsupportedmeans the files cannot be safely concatenated without re-encoding. Missing or corrupt inputs, short reads, and I/O failures remain errors and must not be treated as a compatibility fallback.output_factoryis only invoked after compatibility has been established. A single compatible full input can returnReusedwithout creating another object.FileFragment API
The proposed Rust surface is:
write_columnsis the full-fragment path. The plural name reflects thatschemamay contain one or more existing top-level fields.write_columns_slicewrites the same fields for a fragment-local, half-open physical row interval.sliceis used deliberately: Lance range reads can have logical semantics and apply deletion vectors, while this API is positional. The input must contain exactlyrows.end - rows.startrows, including values at deleted positions.ColumnSliceis an opaque immutable value describing a completed ordinary Lance file. It contains only the storage facts needed to validate and combine it, such as:DataFile.It is not a plan, task, writer, progress record, retry policy, or retention handle. It must support a version-tagged bytes round trip so workers can return it to a coordinator. The Python object should be picklable and expose equivalent
to_bytes/from_bytesmethods.concat_column_slicessorts slices by physical start position and requires all inputs to:[0, fragment.physical_rows())without gaps or overlaps.Duplicate, overlapping, incomplete, or mixed-field inputs are errors; there is no last-write-wins rule. The method re-reads the actual file metadata instead of trusting only serialized declarations. It calls
concat_filesin physical-row order, uses the single-file reuse path when possible, and falls back to ordered decode/re-encode onUnsupported. Missing or corrupt slice files remain errors.Neither successful nor failed concatenation deletes the input files. Their lifecycle remains caller-owned. Only complete fragment coverage can produce a publishable
DataReplacementGroup.Python API
Python should ship with the Rust API, using existing
LanceFragmentconventions:The Python binding resolves column names against the dataset schema and delegates schema, field-ID, row-count, and coverage validation to Rust.
rowsmust have finite non-negativestartandstop, withstepequal toNoneor1.Example
Workers can independently write multiple columns for disjoint physical slices:
The coordinator collects the results, combines complete fragment coverage, and commits using the existing transaction API:
Multiple fragments can be processed independently and their
DataReplacementGroups committed in one transaction.Compaction reuse
Compaction does not call the column-slice APIs. It only passes each output group's complete data files, in fragment row order, to
concat_files:The existing try-binary-copy mode falls back to decode/re-encode on
Unsupported; a forced binary-copy mode returns a typed error. File compatibility, buffer transformation, page-priority relocation, and footer handling have one authority inlance-file.Relationship to existing proposals
DataReplacementcommit.All reactions