@@ -393,7 +393,6 @@ DuckDBPyConnection::RegisterScalarUDF(const string &name, const py::function &ud
393393 auto scalar_function = CreateScalarUDF (name, udf, parameters_p, return_type_p, type == PythonUDFType::ARROW,
394394 null_handling, exception_handling, side_effects);
395395 CreateScalarFunctionInfo info (scalar_function);
396-
397396 context.RegisterFunction (info);
398397
399398 auto dependency = make_uniq<ExternalDependency>();
@@ -403,6 +402,57 @@ DuckDBPyConnection::RegisterScalarUDF(const string &name, const py::function &ud
403402 return shared_from_this ();
404403}
405404
405+ shared_ptr<DuckDBPyConnection> DuckDBPyConnection::RegisterTableFunction (const string &name,
406+ const py::function &function,
407+ const py::object ¶meters,
408+ const py::object &schema,
409+ PythonTVFType type) {
410+
411+ auto &connection = con.GetConnection ();
412+ auto &context = *connection.context ;
413+
414+ if (context.transaction .HasActiveTransaction ()) {
415+ context.CancelTransaction ();
416+ }
417+
418+ if (registered_table_functions.find (name) != registered_table_functions.end ()) {
419+ throw NotImplementedException (" A table function by the name of '%s' is already registered, "
420+ " please unregister it first" ,
421+ name);
422+ }
423+
424+ auto table_function = CreateTableFunctionFromCallable (name, function, parameters, schema, type);
425+ CreateTableFunctionInfo info (table_function);
426+
427+ // re-registration: changing the callable to another
428+ info.on_conflict = OnCreateConflict::REPLACE_ON_CONFLICT;
429+
430+ context.RegisterFunction (info);
431+
432+ auto dependency = make_uniq<ExternalDependency>();
433+ dependency->AddDependency (" function" , PythonDependencyItem::Create (function));
434+ registered_table_functions[name] = std::move (dependency);
435+
436+ return shared_from_this ();
437+ }
438+
439+ shared_ptr<DuckDBPyConnection> DuckDBPyConnection::UnregisterTableFunction (const string &name) {
440+ auto entry = registered_table_functions.find (name);
441+ if (entry == registered_table_functions.end ()) {
442+ throw InvalidInputException (
443+ " No table function by the name of '%s' was found in the list of registered table functions" , name);
444+ }
445+
446+ auto &connection = con.GetConnection ();
447+ auto &context = *connection.context ;
448+
449+ // Remove from our registry.
450+ // TODO: Callable still exists in the function catalog, since duckdb doesn't (yet?) support removal
451+ registered_table_functions.erase (entry);
452+
453+ return shared_from_this ();
454+ }
455+
406456void DuckDBPyConnection::Initialize (py::handle &m) {
407457 auto connection_module =
408458 py::class_<DuckDBPyConnection, shared_ptr<DuckDBPyConnection>>(m, " DuckDBPyConnection" , py::module_local ());
@@ -411,6 +461,14 @@ void DuckDBPyConnection::Initialize(py::handle &m) {
411461 .def (" __exit__" , &DuckDBPyConnection::Exit, py::arg (" exc_type" ), py::arg (" exc" ), py::arg (" traceback" ));
412462 connection_module.def (" __del__" , &DuckDBPyConnection::Close);
413463
464+ connection_module.def (" create_table_function" , &DuckDBPyConnection::RegisterTableFunction,
465+ " Register a table valued function via Callable" , py::arg (" name" ), py::arg (" callable" ),
466+ py::arg (" parameters" ) = py::none (), py::arg (" schema" ) = py::none (),
467+ py::arg (" type" ) = PythonTVFType::TUPLES);
468+
469+ connection_module.def (" unregister_table_function" , &DuckDBPyConnection::UnregisterTableFunction,
470+ " Unregister a table valued function" , py::arg (" name" ));
471+
414472 InitializeConnectionMethods (connection_module);
415473 connection_module.def_property_readonly (" description" , &DuckDBPyConnection::GetDescription,
416474 " Get result set attributes, mainly column names" );
@@ -1575,7 +1633,12 @@ unique_ptr<DuckDBPyRelation> DuckDBPyConnection::RunQuery(const py::object &quer
15751633 }
15761634 if (res->type == QueryResultType::STREAM_RESULT) {
15771635 auto &stream_result = res->Cast <StreamQueryResult>();
1578- res = stream_result.Materialize ();
1636+ {
1637+ // Release the GIL, as Materialize *may* need the GIL (TVFs, for instance)
1638+ D_ASSERT (py::gil_check ());
1639+ py::gil_scoped_release release;
1640+ res = stream_result.Materialize ();
1641+ }
15791642 }
15801643 auto &materialized_result = res->Cast <MaterializedQueryResult>();
15811644 relation = make_shared_ptr<MaterializedRelation>(connection.context , materialized_result.TakeCollection (),
@@ -1826,6 +1889,7 @@ void DuckDBPyConnection::Close() {
18261889 // https://peps.python.org/pep-0249/#Connection.close
18271890 cursors.ClearCursors ();
18281891 registered_functions.clear ();
1892+ registered_table_functions.clear ();
18291893}
18301894
18311895void DuckDBPyConnection::Interrupt () {
0 commit comments