Repository: duckdb/duckdb (language: python)

Issue:
Could not convert object (DICT) to the desired target type (STRUCT()) in an UDF
### What happens?

I try to make an UDF that returns a dict, however duckdb fails to retrieve and convert that dict to a struct.

### To Reproduce

```python
import duckdb
from duckdb.typing import *

con = duckdb.connect('test')
def return_dict():
    return {
    1: "one",
    "2": 2,
    "three": [1, 2, 3],
    False: True
}

con.create_function("return_dict", return_dict, [], dict())
```

Then I try to retrieve the dict:

```python
con.sql("SELECT return_dict()").fetchall()
```
```
InvalidInputException: Invalid Input Error: We could not convert the object {1: 'one', '2': 2, 'three': [1, 2, 3], False: True} to the desired target type (STRUCT())
```



### OS:

Windows 10 x64

### DuckDB Version:

1.1.1

### DuckDB Client:

Python

### Hardware:

_No response_

### Full Name:

Bjørnar Brende Smestad

### Affiliation:

NTNU

### What is the latest build you tested with? If possible, we recommend testing with the latest nightly build.

I have not tested with any build

### Did you include all relevant data sets for reproducing the issue?

Yes

### Did you include all code required to reproduce the issue?

- [X] Yes, I have

### Did you include all relevant configuration (e.g., CPU architecture, Python version, Linux distribution) to reproduce the issue?

- [X] Yes, I have


Discussion / hints:
Keys in duckdb-STRUCTs are all strings.
> Keys in duckdb-STRUCTs are all strings.

They will get auto converted into string, that's not the issue
The issue is that an incomplete type is given as the return type of the UDF

I can make it throw earlier, but it's not going to be fixed in the way the author likely wants

Fix patch:
diff --git a/tools/pythonpkg/src/typing/pytype.cpp b/tools/pythonpkg/src/typing/pytype.cpp
index 7b03f167121c..fc19c17ba123 100644
--- a/tools/pythonpkg/src/typing/pytype.cpp
+++ b/tools/pythonpkg/src/typing/pytype.cpp
@@ -270,6 +270,9 @@ static LogicalType FromGenericAlias(const py::object &obj) {
 static LogicalType FromDictionary(const py::object &obj) {
 	auto dict = py::reinterpret_steal<py::dict>(obj);
 	child_list_t<LogicalType> children;
+	if (dict.size() == 0) {
+		throw InvalidInputException("Could not convert empty dictionary to a duckdb STRUCT type");
+	}
 	children.reserve(dict.size());
 	for (auto &item : dict) {
 		auto &name_p = item.first;


Test patch:
diff --git a/tools/pythonpkg/tests/fast/test_type.py b/tools/pythonpkg/tests/fast/test_type.py
index 718b9defe310..d87a1cae38db 100644
--- a/tools/pythonpkg/tests/fast/test_type.py
+++ b/tools/pythonpkg/tests/fast/test_type.py
@@ -88,6 +88,12 @@ def test_struct_type(self):
         type = duckdb.struct_type([BIGINT, BOOLEAN])
         assert str(type) == 'STRUCT(v1 BIGINT, v2 BOOLEAN)'
 
+    def test_incomplete_struct_type(self):
+        with pytest.raises(
+            duckdb.InvalidInputException, match='Could not convert empty dictionary to a duckdb STRUCT type'
+        ):
+            type = duckdb.typing.DuckDBPyType(dict())
+
     def test_map_type(self):
         type = duckdb.map_type(duckdb.sqltype("BIGINT"), duckdb.sqltype("DECIMAL(10, 2)"))
         assert str(type) == 'MAP(BIGINT, DECIMAL(10,2))'