Skip to content
Open
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
10 changes: 10 additions & 0 deletions python/python/lance/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -3098,6 +3098,11 @@ def update(
def versions(self) -> List[Version]:
"""
Return all versions in this dataset.

Each entry is a :class:`Version` with ``version``, ``timestamp``,
``metadata``, and ``manifest_size``. ``manifest_size`` is the size in
bytes of that version's manifest file, or ``None`` if the commit
handler could not report it.
"""
versions = self._ds.versions()
for v in versions:
Expand Down Expand Up @@ -5923,6 +5928,11 @@ class Version(TypedDict):
version: int
timestamp: int | datetime
metadata: Dict[str, str]
#: Size of this version's manifest file, in bytes, or ``None`` when the
#: commit handler could not report the size. The manifest grows with the
#: number of columns and fragments, so this is useful for observing
#: metadata amplification in wide tables.
manifest_size: int | None


class VersionRef(TypedDict):
Expand Down
40 changes: 40 additions & 0 deletions python/python/tests/test_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,36 @@ def test_schema_metadata(tmp_path: Path):
assert ds.schema.field("b").metadata == {b"thisis": b"b"}


def _manifest_file_size(base_dir: Path, version: int) -> int:
"""Return the on-disk size of the manifest file for ``version``.

Manifest files may use V1 (``_versions/{version}.manifest``) or V2
(``_versions/{u64::MAX - version:020}.manifest``) naming, so resolve by
parsing the version back out of the file name rather than guessing.
"""
candidates = []
for path in (base_dir / "_versions").glob("*.manifest"):
try:
number = int(path.stem)
except ValueError:
# Detached versions are named ``d{version}.manifest``; they are not
# part of the attached version history, so skip them.
continue
# V2 stores the bitwise-inverted version so that the newest version
# sorts first lexicographically.
candidates.append(
(number if number < (1 << 63) else (1 << 64) - 1 - number, path)
)

matches = [
path for candidate_version, path in candidates if candidate_version == version
]
assert len(matches) == 1, (
f"expected exactly one manifest file for version {version}"
)
return matches[0].stat().st_size


def test_versions(tmp_path: Path):
table1 = pa.Table.from_pylist([{"a": 1, "b": 2}, {"a": 10, "b": 20}])
base_dir = tmp_path / "test"
Expand Down Expand Up @@ -477,6 +507,16 @@ def test_versions(tmp_path: Path):
assert isinstance(v1["metadata"], dict)
assert isinstance(v2["metadata"], dict)

# manifest_size must be pinned to the on-disk manifest file for each version.
for version in (v1, v2):
manifest_size = version["manifest_size"]
assert manifest_size is not None
assert manifest_size > 0
# The key is always present, but the value is optional by contract.
assert version.get("manifest_size") == _manifest_file_size(
base_dir, version["version"]
)


def test_version_id(tmp_path: Path):
table1 = pa.Table.from_pylist([{"a": 1, "b": 2}, {"a": 10, "b": 20}])
Expand Down
1 change: 1 addition & 0 deletions python/src/dataset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2080,6 +2080,7 @@ impl Dataset {
.unwrap();
let tup: Vec<(&String, &String)> = v.metadata.iter().collect();
dict.set_item("metadata", tup.into_py_dict(py)?).unwrap();
dict.set_item("manifest_size", v.manifest_size).unwrap();

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.

The runtime dictionary now gains manifest_size, but the public Version TypedDict in python/python/lance/dataset.py still declares only version, timestamp, and metadata. Typed callers therefore cannot use the advertised key: Pyright reports "manifest_size" is not a defined key in "Version". Please add manifest_size: int | None and document the optional/unknown case so the public Python type contract matches this binding.

Reproducer

Run against this head with the following file:

from lance.dataset import LanceDataset


def manifest_size(dataset: LanceDataset) -> int | None:
    return dataset.versions()[0]["manifest_size"]

uv run pyright gate_manifest_size_repro.py reports:

error: Could not access item in TypedDict
  "manifest_size" is not a defined key in "Version"

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.

Fixed in 10d93dae4: Version now declares manifest_size: int | None, and the original Pyright reproducer completes with zero errors.

dict.into_py_any(py)
})
.collect::<PyResult<Vec<_>>>()?;
Expand Down
18 changes: 16 additions & 2 deletions rust/lance/src/dataset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,14 @@ pub struct Version {

/// Key-value pairs of metadata.
pub metadata: BTreeMap<String, String>,

/// Size of the manifest file for this version, in bytes, if known.
///
/// On wide tables (many columns and/or fragments) the manifest is rewritten
/// in full on every commit; exposing its size per version makes metadata
/// growth observable via `list_versions`.
#[serde(default)]
pub manifest_size: Option<u64>,
}

/// A lightweight reference to an attached dataset version, which could be used to uniquely identify a version.
Expand All @@ -295,6 +303,7 @@ impl From<&Manifest> for Version {
version: m.version,
timestamp: m.timestamp(),
metadata: m.summary().into(),
manifest_size: None,
}
}
}
Expand Down Expand Up @@ -2611,8 +2620,13 @@ impl Dataset {
.commit_handler
.list_manifest_locations(&self.base, &self.object_store, false)
.try_filter_map(|location| async move {
match read_manifest(&self.object_store, &location.path, location.size).await {
Ok(manifest) => Ok(Some(Version::from(&manifest))),
let manifest_size = location.size;
match read_manifest(&self.object_store, &location.path, manifest_size).await {
Ok(manifest) => {
let mut version = Version::from(&manifest);
version.manifest_size = manifest_size;

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.

This assignment and the Python key have no corresponding committed regression coverage; the existing test_versions only checks version, timestamp, and metadata. The repository testing contract explicitly does not merge features without tests. Please extend that existing test to assert the returned manifest_size values, preferably against the corresponding _versions/*.manifest file sizes, so both this propagation and the binding key are pinned.

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.

Fixed in 10d93dae4: test_versions now compares every reported manifest_size with its corresponding on-disk manifest, and the focused test passes.

Ok(Some(version))
}
Err(e) => Err(e),
}
})
Expand Down
Loading