Skip to content

Commit ed40c37

Browse files
timsaucerclaude
andcommitted
fix: reject with_session=True for FFI UDTFs and qualify mutation docs
Raise TypeError when with_session=True is combined with an FFI-exported table function (one exposing __datafusion_table_function__). The Rust FFI branch does not consult the flag, so it would silently be dropped; guard both TableFunction.__init__ and the udtf() convenience entry. Qualify the doc claim that mutations through the injected session propagate to the caller: registry mutations do (shared Arc registries), but config changes do not (SessionConfig is cloned). Mirror the caveat in TableFunction.__init__ per the user-guide caveats convention. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 12cf674 commit ed40c37

3 files changed

Lines changed: 47 additions & 2 deletions

File tree

docs/source/user-guide/common-operations/udf-and-udfa.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,5 +465,8 @@ unchanged.
465465

466466
The injected ``session`` is a fresh :py:class:`~datafusion.SessionContext`
467467
wrapper backed by the same underlying state as the caller, so registries
468-
(tables, UDFs, catalogs) are visible. Mutations made through it affect
469-
the live session.
468+
(tables, UDFs, catalogs) are visible. Registry mutations (e.g. registering
469+
a new table or UDF) propagate to the live session because the registries
470+
are reference-counted and shared. Configuration changes made through the
471+
wrapper (e.g. setting session options) do **not** propagate — the wrapper
472+
holds its own clone of the session config.

python/datafusion/user_defined.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,9 +1096,26 @@ def __init__(
10961096
``with_session`` is ``False`` (the default), ``func`` is invoked
10971097
with the positional expression arguments only.
10981098
1099+
``with_session=True`` is only supported for pure-Python callables.
1100+
Passing it together with an FFI-exported table function (one
1101+
exposing ``__datafusion_table_function__``) raises
1102+
:class:`TypeError`.
1103+
1104+
Registry mutations performed through the injected session (such
1105+
as registering tables or UDFs) propagate to the caller's
1106+
:class:`SessionContext` because the registries are shared.
1107+
Configuration changes do **not** propagate; the wrapper holds
1108+
its own clone of the session config.
1109+
10991110
See :py:func:`udtf` for a convenience function and argument
11001111
descriptions.
11011112
"""
1113+
if with_session and hasattr(func, "__datafusion_table_function__"):
1114+
msg = (
1115+
"`with_session=True` is not supported for FFI-exported table "
1116+
"functions; session injection requires a pure-Python callable."
1117+
)
1118+
raise TypeError(msg)
11021119
registered = _wrap_session_kwarg_for_udtf(func) if with_session else func
11031120
self._udtf = df_internal.TableFunction(name, registered, ctx, with_session)
11041121

@@ -1139,6 +1156,13 @@ def udtf(*args: Any, with_session: bool = False, **kwargs: Any):
11391156
)
11401157
if args and hasattr(args[0], "__datafusion_table_function__"):
11411158
# Case 2: We have a datafusion FFI provided function
1159+
if with_session:
1160+
msg = (
1161+
"`with_session=True` is not supported for FFI-exported "
1162+
"table functions; session injection requires a "
1163+
"pure-Python callable."
1164+
)
1165+
raise TypeError(msg)
11421166
return TableFunction(args[1], args[0])
11431167
# Case 3: Used as a decorator with parameters
11441168
return TableFunction._create_table_udf_decorator(

python/tests/test_udtf.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717

1818
import pyarrow as pa
1919
import pyarrow.dataset as ds
20+
import pytest
2021
from datafusion import Expr, SessionContext, Table, udtf
2122
from datafusion.context import TableProviderExportable
23+
from datafusion.user_defined import TableFunction
2224

2325

2426
def python_table_function_inner(
@@ -215,3 +217,19 @@ def plain_func(n: Expr) -> TableProviderExportable:
215217
result = ctx.sql("SELECT * FROM plain_func(4)").collect()
216218

217219
assert result[0].column(0).to_pylist() == [0, 1, 2, 3]
220+
221+
222+
def test_with_session_rejected_for_ffi_table_function() -> None:
223+
"""`with_session=True` is incompatible with FFI-exported table functions."""
224+
225+
class FakeFFITableFunction:
226+
# Presence of this attribute is what marks a function as FFI-exported.
227+
__datafusion_table_function__ = "stub"
228+
229+
fake = FakeFFITableFunction()
230+
231+
with pytest.raises(TypeError, match="FFI-exported table functions"):
232+
udtf(fake, "fake_ffi", with_session=True)
233+
234+
with pytest.raises(TypeError, match="FFI-exported table functions"):
235+
TableFunction("fake_ffi", fake, with_session=True)

0 commit comments

Comments
 (0)