Compare commits

...
Author SHA1 Message Date
Gatefixer 31573bc6c2 Merge remote-tracking branch 'origin/main' into gatekeeper/fix-2107-1
# Conflicts:
#	python/python/tests/test_table.py
2026-08-06 09:14:03 +00:00
Gatefixer f382c8548f fix(python): avoid deadlock for query-backed iterators 2026-08-05 23:33:37 +00:00
3 changed files with 47 additions and 6 deletions
+11 -2
View File
@@ -5,6 +5,7 @@
from __future__ import annotations from __future__ import annotations
from abc import abstractmethod from abc import abstractmethod
import asyncio
from datetime import timedelta from datetime import timedelta
from pathlib import Path from pathlib import Path
import sys import sys
@@ -1735,8 +1736,16 @@ class AsyncConnection(object):
if fill_value is None: if fill_value is None:
fill_value = 0.0 fill_value = 0.0
data, schema = sanitize_create_table( # Input preparation may advance a user-provided iterator. Keep that work
data, schema, metadata, on_bad_vectors, fill_value # off the background event loop so an iterator can use the synchronous
# LanceDB API without blocking the loop that API needs to make progress.
data, schema = await asyncio.to_thread(
sanitize_create_table,
data,
schema,
metadata,
on_bad_vectors,
fill_value,
) )
validate_schema(schema) validate_schema(schema)
+12 -4
View File
@@ -5143,13 +5143,18 @@ class AsyncTable:
if mode == "overwrite": if mode == "overwrite":
# For overwrite, apply the same preprocessing as create_table # For overwrite, apply the same preprocessing as create_table
# so vector columns are inferred as FixedSizeList. # so vector columns are inferred as FixedSizeList.
data, _ = sanitize_create_table( data, _ = await asyncio.to_thread(
data, None, on_bad_vectors=on_bad_vectors, fill_value=fill_value sanitize_create_table,
data,
None,
on_bad_vectors=on_bad_vectors,
fill_value=fill_value,
) )
elif on_bad_vectors != "error" or ( elif on_bad_vectors != "error" or (
schema.metadata is not None and b"embedding_functions" in schema.metadata schema.metadata is not None and b"embedding_functions" in schema.metadata
): ):
data = _sanitize_data( data = await asyncio.to_thread(
_sanitize_data,
data, data,
schema, schema,
metadata=schema.metadata, metadata=schema.metadata,
@@ -5158,7 +5163,10 @@ class AsyncTable:
allow_subschema=True, allow_subschema=True,
) )
_register_optional_converters() _register_optional_converters()
data = to_scannable(data) # Converting an iterator peeks at its first item. A synchronous query in
# that iterator schedules work on LOOP, so peeking on LOOP's own thread
# would deadlock waiting for itself.
data = await asyncio.to_thread(to_scannable, data)
progress, owns = _normalize_progress(progress) progress, owns = _normalize_progress(progress)
try: try:
return await self._inner.add( return await self._inner.add(
+24
View File
@@ -462,6 +462,30 @@ def test_add(mem_db: DBConnection):
_add(table, schema) _add(table, schema)
def test_add_from_iterator_that_queries_table(mem_db: DBConnection):
source = mem_db.create_table("source", data=pa.table({"id": range(16)}))
target = mem_db.create_table("target", schema=source.schema)
def batches():
for _ in range(5):
yield source.search().limit(10).to_arrow()
target.add(batches())
assert target.count_rows() == 50
def test_create_table_from_iterator_that_queries_table(mem_db: DBConnection):
source = mem_db.create_table("source", data=pa.table({"id": range(16)}))
def batches():
yield source.search().limit(10).to_arrow()
target = mem_db.create_table("target", data=batches())
assert target.count_rows() == 10
def test_add_releases_arrow_buffers_without_gc(mem_db: DBConnection): def test_add_releases_arrow_buffers_without_gc(mem_db: DBConnection):
"""Regression test for https://github.com/lancedb/lancedb/issues/2512.""" """Regression test for https://github.com/lancedb/lancedb/issues/2512."""
schema = pa.schema([pa.field("x", pa.int64())]) schema = pa.schema([pa.field("x", pa.int64())])