fix(python): surface Function drop warnings (#4880)

Remote Python `drop_function` currently discards the dependent-view
warnings returned by SQL DROP. Both the synchronous and coroutine APIs
now emit those warnings through `warnings.warn` while preserving their
boolean result and executing DROP once. The server support is already
merged in lancedb/sophon#9820.

The Rust decoder reads warnings across every result batch, including an
empty first batch. Missing warning columns remain compatible with older
servers; malformed or null warning lists are rejected. The hidden
binding bridge carries the warnings to Python. The separate
`drop_function_async` API and its `(bool, Job)` contract are unchanged.
Python warning filters can raise after the catalog mutation has
completed.

Validation: a compiled PyO3/public-server reproduction failed before the
fix and passed afterward; the public async path also returned `true`
with the expected dependent-view warning. Final source passes two
focused Rust tests, two Python wrapper tests, strict targeted Clippy,
formatting, and Ruff. Three decoder tests cover batch/null/type and
older-server behavior. The public runs preceded a two-file
simplification of the completed Job projection; focused final-source
checks cover that delta. Current-main integration is conflict-free, and
its overlapping changes concern independent retry-policy APIs/tests.

Documentation now covers warning filters, the post-removal
warnings-as-errors behavior, and silent compatibility with older
servers. The follow-up changes only prose and a Python docstring; the
runtime validation above remains applicable.

Refs ENT-3659. Whole-issue independent acceptance remains pending.
This commit is contained in:
Xuanwo
2026-11-06 10:32:20 +08:00
committed by GitHub
parent 2f496529ca
commit 1b40e479d3
12 changed files with 330 additions and 22 deletions
+3
View File
@@ -201,6 +201,9 @@ class Connection(object):
async def drop_function(
self, name: str, version: str, namespace_path: Optional[List[str]] = None
) -> bool: ...
async def _drop_function_with_warnings(
self, name: str, version: str, namespace_path: Optional[List[str]] = None
) -> Tuple[bool, list[str]]: ...
async def drop_function_async(
self, name: str, version: str, namespace_path: Optional[List[str]] = None
) -> Tuple[bool, Job]: ...
+26 -3
View File
@@ -11,6 +11,7 @@ from pathlib import Path
from dataclasses import asdict
import json
import sys
import warnings
from typing import (
TYPE_CHECKING,
Any,
@@ -1085,7 +1086,9 @@ class DBConnection(EnforceOverrides):
Returns True when the revision was removed and False for a repeated drop
or a missing name. A revision never published under an existing name is
an error. Dropping the last visible revision releases the name and
schedules physical cleanup in the background. Use
schedules physical cleanup in the background. When the remote server
reports dependent warnings, they are emitted as ``UserWarning`` before
this method returns. Use
:meth:`drop_function_async` to
access the query job, whose completion confirms catalog removal of the
revision. Local connections raise NotImplementedError.
@@ -3647,9 +3650,29 @@ class AsyncConnection(object):
Returns True when the requested revision was removed and False for a
repeated drop or a missing name. A revision never published under an
existing name is an error. Only the last visible revision's removal
releases the name and schedules background object cleanup.
releases the name and schedules background object cleanup. When the
remote server reports a warning, it is emitted as ``UserWarning``
before this method returns, subject to the standard ``warnings``
filters. A warnings-as-errors setting can therefore raise after the
catalog removal; it does not roll back the drop. Older servers that
omit the warning field remain silent.
"""
return await self._inner.drop_function(name, version, namespace_path)
dropped, drop_warnings = await self._drop_function_with_warnings(
name, version, namespace_path
)
for warning in drop_warnings:
warnings.warn(warning, UserWarning, stacklevel=2)
return dropped
async def _drop_function_with_warnings(
self,
name: str,
version: str,
namespace_path: Optional[List[str]] = None,
) -> Tuple[bool, List[str]]:
return await self._inner._drop_function_with_warnings(
name, version, namespace_path
)
async def drop_function_async(
self,
+5 -2
View File
@@ -1024,11 +1024,14 @@ class RemoteDBConnection(DBConnection):
version: str,
namespace_path: Optional[List[str]] = None,
) -> bool:
return LOOP.run(
self._conn.drop_function(
dropped, drop_warnings = LOOP.run(
self._conn._drop_function_with_warnings(
name, version=version, namespace_path=namespace_path
)
)
for warning in drop_warnings:
warnings.warn(warning, UserWarning, stacklevel=2)
return dropped
@override
def drop_function_async(
@@ -1972,6 +1972,45 @@ def test_sync_catalog_path_preserves_execution_controls():
assert _assert_execution_control_encoding(version) == definition
def test_sync_drop_function_emits_server_warnings_without_changing_bool_api():
class _DropConnection:
async def drop_function(self, name, *, version, namespace_path=None):
return True
async def _drop_function_with_warnings(
self, name, *, version, namespace_path=None
):
return True, [
'materialized view "pinned" references Function "embed" version "1"'
]
connection = object.__new__(RemoteDBConnection)
connection._conn = _DropConnection()
with pytest.warns(UserWarning, match="references Function"):
assert connection.drop_function("embed", version="1") is True
@pytest.mark.asyncio
async def test_async_drop_function_emits_server_warnings_without_changing_bool_api():
class _Inner:
async def _drop_function_with_warnings(
self, name, version, namespace_path=None
):
return True, [
'materialized view "pinned" references Function "embed" version "1"'
]
async def drop_function(self, name, version, namespace_path=None):
return True
connection = object.__new__(AsyncConnection)
connection._inner = _Inner()
with pytest.warns(UserWarning, match="references Function"):
assert await connection.drop_function("embed", version="1") is True
@contextlib.contextmanager
def _mock_remote_function_catalog():
state = {"requests": [], "version": None}
+17
View File
@@ -928,6 +928,23 @@ impl Connection {
})
}
#[pyo3(signature = (name, version, namespace_path=None))]
pub fn _drop_function_with_warnings(
self_: PyRef<'_, Self>,
name: String,
version: String,
namespace_path: Option<Vec<String>>,
) -> PyResult<Bound<'_, PyAny>> {
let inner = self_.get_inner()?.clone();
let namespace_path = namespace_path.unwrap_or_default();
future_into_py(self_.py(), async move {
inner
.drop_function_with_warnings(name, version, &namespace_path)
.await
.infer_error()
})
}
#[pyo3(signature = (name, version, namespace_path=None))]
pub fn drop_function_async(
self_: PyRef<'_, Self>,