Lance · Blob v2 · Design proposal
Blob Reuse Index (BRI)
Reorganize the rows. Keep the bytes.
Compaction should be able to move rows without copying their unchanged blob payloads. BRI gives new DataFiles a durable reference to existing sidecars.
01 / The coupling
New file. New blob address.
A sidecar is a separate file holding blob bytes. A row stores a descriptor with a file-local ID; the containing DataFile’s stem supplies the directory.
Compaction writes surviving rows into new DataFiles. Under default addressing, those rows need new sidecars—even when the payloads are unchanged.
The row layout and payload placement are coupled by the address.
02 / The storage contract
New reference. Same bytes.
Add BRI to the DataFile entry in the table manifest. The physical .lance file still stores Blob v2 descriptors in its encoded column data; the reused sidecar still holds the payload.
The example below shows one surviving Packed value after compaction: its local blob_id becomes 1, while its original position and size are preserved.
F42.lance
<base>/data/F42.lance
Blob column → packed-struct descriptor values
| Field | Type | Value |
|---|---|---|
| kind | u8 | 1 · Packed |
| position | u64 | 65536 |
| size | u64 | 65536 |
| blob_id | u32 | 1 |
| blob_uri | utf8 | "" |
One descriptor per Blob value. These are decoded fields, not fixed byte offsets. BRI adds no field to the descriptor or the file footer.
DataFile metadata
<table>/_versions/*.manifest
Manifest
└─ fragments[]
└─ files[] : DataFile
├─ path: "F42.lance"
├─ base_id: absent
└─ blob_reuse_index
sources groups mappings by base and directory. Its absent base_id inherits F42’s base; both are absent here, so the primary root applies.
local_ids[i] → physical_ids[i]: the sequences are positionally paired. Stored as RowIdSequence and EncodedU64Array, they are shown expanded here.
03 / Existing sidecar · payload unchanged
F42: blob_id 1 → BRI → (F42’s base, F17, physical ID 3)
Open <base>/data/F17/<encoded(3)>.blob, then read [65536, 131072). Here, encoded(3) stands for the existing blob-ID filename encoding.
No BRI entry? Resolve the ID under the containing DataFile’s own directory. New and repacked sidecars use this ordinary path.
03 / The lifetime
Keep the reference. Keep the sidecar.
Cleanup follows every retained manifest, including historical versions. Each DataFile keeps its own sidecar directory live; BRI additionally protects the exact objects it references.
- Immutable mapping
- Changing BRI requires a new physical DataFile.
- Direct targets
- Recompaction resolves the physical object, never a chain of old manifests.
- Safe rollback
- An uncommitted output owns its new files, never its reused sources.
The integration contract
Every consumer must honor the reference.
| Boundary | Required behavior |
|---|---|
| Manifest validation | Reject ambiguous IDs, invalid source bases, and misaligned mapping sequences when opening the manifest. |
| Readers & writers | BRI requires both feature flags; unsupported clients reject it. Files without BRI retain default addressing. |
| Clones | Shallow clones preserve mappings and effective bases. Deep clones copy the dependencies and remap them into independent storage. |
| Full rewrite | Removing the last BRI clears its flags in that manifest. Older retained versions with BRI still require aware clients. |
Row layout and blob placement can evolve independently—connected by an explicit, durable reference.
- Format & configuration ↗
- Manifest schema ↗
- Descriptor encoding ↗
- Compaction tests ↗
- Cleanup tracking ↗
Implementation contract, not a measured performance claim. Encoding details and policy defaults are in the linked source snapshot.