mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-14 15:52:26 +00:00
feat: list a table's per-row Function errors from the client (#4208)
A refresh running under a skip policy records each row it skipped, with the failing input and the error, but the client could not read that store: the server exposes it over SQL and, since recently, a REST route. A user who hit per-row failures still had to open a SQL session. `Table::function_errors` calls the route. The listing is table-addressed with optional job and column filters, the same addressing the SQL surface uses, so the two cannot disagree about what a table's errors are. The two non-record signals come back as their own fields rather than as rows: capped-fragment summaries, and whether the listing stopped at its limit. Local tables refuse rather than answer with an empty list. Python and Node expose the same call, with the same optional filters.
This commit is contained in:
@@ -982,6 +982,71 @@ def test_remote_refresh_async_returns_typed_terminal_result():
|
||||
assert result.version == 8
|
||||
|
||||
|
||||
def test_remote_function_errors_lists_the_rows_a_refresh_skipped():
|
||||
listing = {
|
||||
"records": [
|
||||
{
|
||||
"job_id": "j-7",
|
||||
"fragment_id": 3,
|
||||
"row_offset": 9,
|
||||
"column": "embedding",
|
||||
"function": "embed",
|
||||
"function_version": "2",
|
||||
"table_version": 11,
|
||||
"error_type": "ValueError",
|
||||
"error_message": "bad input 'x'",
|
||||
"created_at_millis": 1700000000000,
|
||||
}
|
||||
],
|
||||
"fragments": [
|
||||
{
|
||||
"job_id": "j-7",
|
||||
"fragment_id": 4,
|
||||
"rows_skipped": 500,
|
||||
"rows_recorded": 100,
|
||||
}
|
||||
],
|
||||
"truncated": True,
|
||||
}
|
||||
bodies = []
|
||||
|
||||
def handler(request):
|
||||
content_len = int(request.headers.get("Content-Length", 0))
|
||||
body = request.rfile.read(content_len) if content_len > 0 else b""
|
||||
if request.path == "/v1/table/test/errors":
|
||||
bodies.append(json.loads(body))
|
||||
request.send_response(200)
|
||||
request.send_header("Content-Type", "application/json")
|
||||
request.end_headers()
|
||||
request.wfile.write(json.dumps(listing).encode())
|
||||
elif request.path == "/v1/table/test/describe/":
|
||||
request.send_response(200)
|
||||
request.send_header("Content-Type", "application/json")
|
||||
request.end_headers()
|
||||
request.wfile.write(
|
||||
json.dumps({"version": 1, "schema": {"fields": []}}).encode()
|
||||
)
|
||||
else:
|
||||
request.send_response(404)
|
||||
request.end_headers()
|
||||
|
||||
with mock_lancedb_connection(handler) as db:
|
||||
table = db.open_table("test")
|
||||
errors = table.function_errors(job_id="j-7", column="embedding", limit=2)
|
||||
everything = table.function_errors()
|
||||
|
||||
assert bodies == [{"job_id": "j-7", "column": "embedding", "limit": 2}, {}]
|
||||
assert errors.truncated is True
|
||||
assert [r.error_message for r in errors.records] == ["bad input 'x'"]
|
||||
assert errors.records[0].row_offset == 9
|
||||
assert errors.records[0].function_version == "2"
|
||||
assert (errors.fragments[0].rows_skipped, errors.fragments[0].rows_recorded) == (
|
||||
500,
|
||||
100,
|
||||
)
|
||||
assert everything.truncated is True
|
||||
|
||||
|
||||
def test_remote_job_wait_raises_on_failure():
|
||||
from lancedb.exceptions import JobFailedError
|
||||
from lancedb.index import BTree
|
||||
|
||||
@@ -4327,6 +4327,13 @@ async def test_computed_column_async(tmp_path):
|
||||
assert (await table.to_arrow())["tripled"].to_pylist() == [9]
|
||||
|
||||
|
||||
def test_function_errors_are_remote_only(tmp_path):
|
||||
db = lancedb.connect(tmp_path)
|
||||
table = db.create_table("t", [{"x": 1}])
|
||||
with pytest.raises(NotImplementedError, match="LanceDB Cloud and Enterprise"):
|
||||
table.function_errors()
|
||||
|
||||
|
||||
def test_refresh_column_async_returns_job(tmp_path):
|
||||
db = lancedb.connect(tmp_path)
|
||||
table = db.create_table("computed_job", [{"x": 1}, {"x": 2}])
|
||||
|
||||
Reference in New Issue
Block a user