mirror of
https://github.com/lancedb/lancedb.git
synced 2026-09-01 11:08:55 +00:00
feat: support blob computed column refresh
This commit is contained in:
@@ -352,7 +352,9 @@ class Table:
|
||||
async def delete(self, filter: Union[str, PyExpr]) -> DeleteResult: ...
|
||||
async def add_columns(self, columns: list[tuple[str, str]]) -> AddColumnsResult: ...
|
||||
async def add_computed_columns(
|
||||
self, columns: list[tuple[str, str]]
|
||||
self,
|
||||
columns: list[tuple[str, str]],
|
||||
blob_columns: Optional[list[tuple[str, str]]] = None,
|
||||
) -> AddColumnsResult: ...
|
||||
async def add_function_columns(
|
||||
self, application_json: str, output_name: Optional[str]
|
||||
|
||||
@@ -976,8 +976,13 @@ class RemoteTable(Table):
|
||||
| None = None,
|
||||
*,
|
||||
computed: Dict[str, str] | None = None,
|
||||
computed_blobs: Dict[str, str] | None = None,
|
||||
) -> AddColumnsResult:
|
||||
return LOOP.run(self._table.add_columns(transforms, computed=computed))
|
||||
return LOOP.run(
|
||||
self._table.add_columns(
|
||||
transforms, computed=computed, computed_blobs=computed_blobs
|
||||
)
|
||||
)
|
||||
|
||||
def refresh_column(self, column: str):
|
||||
return LOOP.run(self._table.refresh_column(column))
|
||||
|
||||
@@ -2143,6 +2143,7 @@ class Table(ABC):
|
||||
| None = None,
|
||||
*,
|
||||
computed: Dict[str, str] | None = None,
|
||||
computed_blobs: Dict[str, str] | None = None,
|
||||
):
|
||||
"""
|
||||
Add new columns with defined values.
|
||||
@@ -2184,6 +2185,12 @@ class Table(ABC):
|
||||
server, and the refresh runs as a server job -- see
|
||||
[`refresh_column_async`][lancedb.table.Table.refresh_column_async].
|
||||
Cannot be combined with ``transforms``.
|
||||
computed_blobs: Dict[str, str], optional
|
||||
A map of Blob v2 output column names to SQL expressions returning
|
||||
``LargeBinary`` payload bytes. Blob inputs named by an expression
|
||||
are materialized as bytes, and refresh stores the result as Blob
|
||||
v2 so ``blob_columns()`` and Blob read APIs continue to recognize
|
||||
it. Cannot be combined with ``transforms``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -4300,8 +4307,13 @@ class LanceTable(Table):
|
||||
| None = None,
|
||||
*,
|
||||
computed: Dict[str, str] | None = None,
|
||||
computed_blobs: Dict[str, str] | None = None,
|
||||
) -> AddColumnsResult:
|
||||
return LOOP.run(self._table.add_columns(transforms, computed=computed))
|
||||
return LOOP.run(
|
||||
self._table.add_columns(
|
||||
transforms, computed=computed, computed_blobs=computed_blobs
|
||||
)
|
||||
)
|
||||
|
||||
def refresh_column(self, column: str) -> "RefreshColumnResult":
|
||||
"""Fill a computed column's unfilled rows. See
|
||||
@@ -6248,6 +6260,7 @@ class AsyncTable:
|
||||
| None = None,
|
||||
*,
|
||||
computed: dict[str, str] | None = None,
|
||||
computed_blobs: dict[str, str] | None = None,
|
||||
) -> AddColumnsResult:
|
||||
"""
|
||||
Add new columns with defined values.
|
||||
@@ -6283,6 +6296,11 @@ class AsyncTable:
|
||||
|
||||
On LanceDB Cloud and Enterprise the expression is planned by
|
||||
the server. Cannot be combined with ``transforms``.
|
||||
computed_blobs: Dict[str, str], optional
|
||||
A map of Blob v2 output column names to SQL expressions returning
|
||||
``LargeBinary`` payload bytes. Blob inputs are materialized as
|
||||
payload bytes during refresh. Cannot be combined with
|
||||
``transforms``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -6306,7 +6324,7 @@ class AsyncTable:
|
||||
function_output_name, function_application = next(iter(transforms.items()))
|
||||
|
||||
if function_application is not None:
|
||||
if computed:
|
||||
if computed or computed_blobs:
|
||||
raise ValueError(
|
||||
"add_columns cannot mix a Function application with SQL "
|
||||
"computed columns"
|
||||
@@ -6322,12 +6340,15 @@ class AsyncTable:
|
||||
{isinstance(f, pa.Field) for f in transforms}
|
||||
):
|
||||
transforms = pa.schema(transforms)
|
||||
if computed:
|
||||
if computed or computed_blobs:
|
||||
if transforms:
|
||||
raise ValueError(
|
||||
"add_columns cannot take both transforms and computed columns"
|
||||
)
|
||||
return await self._inner.add_computed_columns(list(computed.items()))
|
||||
return await self._inner.add_computed_columns(
|
||||
list((computed or {}).items()),
|
||||
list((computed_blobs or {}).items()),
|
||||
)
|
||||
if transforms is None:
|
||||
raise ValueError("add_columns requires transforms or computed columns")
|
||||
if isinstance(transforms, pa.Schema):
|
||||
|
||||
@@ -4087,6 +4087,42 @@ def test_computed_column_rejects_transforms_and_computed_together(tmp_path):
|
||||
table.add_columns({"a": "x + 1"}, computed={"b": "x * 2"})
|
||||
|
||||
|
||||
def test_computed_blob_input_and_explicit_output(tmp_path):
|
||||
schema = pa.schema([pa.field("id", pa.int64()), lancedb.blob("image")])
|
||||
db = lancedb.connect(tmp_path)
|
||||
table = db.create_table("computed_blob", schema=schema)
|
||||
table.add(
|
||||
[
|
||||
{"id": 1, "image": b"hello"},
|
||||
{"id": 2, "image": b""},
|
||||
{"id": 3, "image": None},
|
||||
]
|
||||
)
|
||||
|
||||
table.add_columns(
|
||||
computed={"payload_copy": "image"},
|
||||
computed_blobs={"image_copy": "image"},
|
||||
)
|
||||
assert table.refresh_column("payload_copy").rows_filled == 2
|
||||
assert table.refresh_column("image_copy").rows_filled == 2
|
||||
|
||||
values = table.to_arrow()["payload_copy"].combine_chunks().to_pylist()
|
||||
assert values == [b"hello", b"", None]
|
||||
assert table.blob_columns() == ["image", "image_copy"]
|
||||
|
||||
hits = table.search().with_row_id(True).limit(10).to_arrow()
|
||||
rows = sorted(zip(hits["id"].to_pylist(), hits["_rowid"].to_pylist()))
|
||||
copied = table.fetch_blobs("image_copy", [row_id for _, row_id in rows])
|
||||
assert copied.to_pylist() == [b"hello", b"", None]
|
||||
|
||||
|
||||
def test_computed_blob_rejects_eager_transforms(tmp_path):
|
||||
db = lancedb.connect(tmp_path)
|
||||
table = db.create_table("computed_blob_mixed", [{"x": 1}])
|
||||
with pytest.raises(ValueError):
|
||||
table.add_columns({"a": "x + 1"}, computed_blobs={"b": "x"})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_computed_column_async(tmp_path):
|
||||
db = await lancedb.connect_async(tmp_path)
|
||||
|
||||
@@ -1575,9 +1575,11 @@ impl Table {
|
||||
})
|
||||
}
|
||||
|
||||
#[pyo3(signature = (columns, blob_columns=None))]
|
||||
pub fn add_computed_columns(
|
||||
self_: PyRef<'_, Self>,
|
||||
columns: Vec<(String, String)>,
|
||||
blob_columns: Option<Vec<(String, String)>>,
|
||||
) -> PyResult<Bound<'_, PyAny>> {
|
||||
let inner = self_.inner_ref()?.clone();
|
||||
future_into_py(self_.py(), async move {
|
||||
@@ -1585,6 +1587,9 @@ impl Table {
|
||||
for (name, expression) in columns {
|
||||
builder = builder.computed(name, expression);
|
||||
}
|
||||
for (name, expression) in blob_columns.unwrap_or_default() {
|
||||
builder = builder.computed_blob(name, expression);
|
||||
}
|
||||
let result = builder.execute().await.infer_error()?;
|
||||
Ok(AddColumnsResult::from(result))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user