Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions python/python/lance/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -5991,6 +5991,76 @@ class DataReplacement(BaseOperation):

replacements: List[LanceOperation.DataReplacementGroup]

@dataclass
class DataOverlayFile:
"""
An overlay file supplying new values for a subset of
``(physical offset, field)`` cells of a fragment, resolved on read and
layered over the base data without rewriting the base files.

The overlay is dense or sparse depending on the shape of ``offsets``:
pass a flat ``List[int]`` for a dense overlay (one offset list shared by
every field in ``data_file``) or a ``List[List[int]]`` for a sparse
overlay (one offset list per field, in the order of the file's fields).
Offsets are **physical** row offsets (positions in the base files,
counting deleted rows), like deletion vectors.

Attributes
----------
data_file : DataFile
The Lance data file storing the overlay's new cell values — one
value column per covered field. The value at each covered offset is
stored at the rank (0-based count of covered offsets below it) of
that offset in the field's coverage.
offsets : Union[List[int], List[List[int]]]
The covered physical row offsets. A flat list is dense coverage
(shared by every field); a list of per-field lists is sparse
coverage (in field order). Each list must be strictly ascending
with no duplicates, since the Nth offset maps to the Nth value row
in ``data_file``; a non-ascending list raises ``ValueError``.
committed_version : Optional[int]
The dataset version at which this overlay became effective. Leave as
``None`` when creating an overlay to commit — the commit stamps it.
It is populated when reading an existing fragment's overlays so they
round-trip through :class:`FragmentMetadata`.
"""

data_file: DataFile
offsets: Union[List[int], List[List[int]]]
committed_version: Optional[int] = None

@dataclass
class DataOverlayGroup:
"""
Overlay files to append to a single fragment.

Attributes
----------
fragment_id : int
The id of the fragment the overlays apply to.
overlays : List[LanceOperation.DataOverlayFile]
The overlay files to append, ordered oldest-first (a later entry is
newer and wins where coverage overlaps).
"""

fragment_id: int
overlays: List[LanceOperation.DataOverlayFile]

@dataclass
class DataOverlay(BaseOperation):
"""
Operation that appends data overlay files to fragments.

Overlays are appended to each fragment's existing overlays (overlays
written by concurrent commits are preserved) and resolved on read
over the base data without rewriting it.

If multiple groups target the same data then the values in the
latest group take precedence.
"""

groups: List[LanceOperation.DataOverlayGroup]

@dataclass
class Project(BaseOperation):
"""
Expand Down
41 changes: 38 additions & 3 deletions python/python/lance/fragment.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
ColumnOrdering,
DatasetBasePath,
LanceDataset,
LanceOperation,
LanceScanner,
ReaderLike,
Transaction,
Expand Down Expand Up @@ -78,6 +79,11 @@ class FragmentMetadata:
The row created at version metadata, if any.
last_updated_at_version_meta : Optional[RowDatasetVersionMeta]
The row last updated at version metadata, if any.
overlays : List[LanceOperation.DataOverlayFile]
The data overlay files layered over this fragment's base data, if any.
Overlays are created via :class:`LanceOperation.DataOverlay`; they are
carried here so they survive operations that round-trip fragment
metadata (e.g. a manual ``Delete``, ``Update``, or ``Merge`` commit).
"""

id: int
Expand All @@ -87,6 +93,7 @@ class FragmentMetadata:
row_id_meta: Optional[RowIdMeta] = None
created_at_version_meta: Optional[RowDatasetVersionMeta] = None
last_updated_at_version_meta: Optional[RowDatasetVersionMeta] = None
overlays: List["LanceOperation.DataOverlayFile"] = field(default_factory=list)

@property
def num_deletions(self) -> int:
Expand All @@ -110,12 +117,25 @@ def data_files(self) -> List[DataFile]:

def to_json(self) -> dict:
"""Get this as a simple JSON-serializable dictionary."""
files = [asdict(f) for f in self.files]
for f in files:
f["path"] = f.pop("_path")

def _data_file_to_json(f: DataFile) -> dict:
d = asdict(f)
d["path"] = d.pop("_path")
return d

files = [_data_file_to_json(f) for f in self.files]
overlays = [
dict(
data_file=_data_file_to_json(o.data_file),
offsets=o.offsets,
committed_version=o.committed_version,
)
for o in self.overlays
]
return dict(
id=self.id,
files=files,
overlays=overlays,
physical_rows=self.physical_rows,
deletion_file=(
self.deletion_file.asdict() if self.deletion_file is not None else None
Expand Down Expand Up @@ -159,6 +179,20 @@ def from_json(json_data: str) -> FragmentMetadata:
json.dumps(last_updated_at_version_meta)
)

overlays = []
overlays_json = json_data.get("overlays")
if overlays_json:
from .dataset import LanceOperation

overlays = [
LanceOperation.DataOverlayFile(
data_file=DataFile(**o["data_file"]),
offsets=o["offsets"],
committed_version=o.get("committed_version"),
)
for o in overlays_json
]

return FragmentMetadata(
id=json_data["id"],
files=[DataFile(**f) for f in json_data["files"]],
Expand All @@ -167,6 +201,7 @@ def from_json(json_data: str) -> FragmentMetadata:
row_id_meta=row_id_meta,
created_at_version_meta=created_at_version_meta,
last_updated_at_version_meta=last_updated_at_version_meta,
overlays=overlays,
)


Expand Down
Loading
Loading