Skip to content

Commit a9d6436

Browse files
committed
Add Tuple support
1 parent 9b9a0ac commit a9d6436

10 files changed

Lines changed: 28 additions & 25 deletions

File tree

_duckdb-stubs/_typing.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ Note:
133133
We use lowercase here to be able to reuse this `Literal` in the `DTypeIdentifiers` `Literal`.
134134
"""
135135

136-
NestedIds: TypeAlias = Literal["list", "struct", "array", "enum", "map", "decimal", "union"]
136+
NestedIds: TypeAlias = Literal["list", "struct", "tuple", "array", "enum", "map", "decimal", "union"]
137137
"""Identifiers for nested types in `DuckDBPyType.id`."""
138138

139139
PyTypeIds: TypeAlias = Builtins | NestedIds

duckdb/experimental/spark/sql/type_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def convert_nested_type(dtype: DuckDBPyType) -> DataType: # noqa: D103
9494
"DuckDB union types cannot be directly mapped to PySpark types."
9595
)
9696
raise ContributionsAcceptedError(msg)
97-
if id == "struct":
97+
if id == "struct" or id == "tuple":
9898
children: list[tuple[str, DuckDBPyType]] = dtype.children
9999
fields = [StructField(x[0], convert_type(x[1])) for x in children]
100100
return StructType(fields)
@@ -105,7 +105,7 @@ def convert_nested_type(dtype: DuckDBPyType) -> DataType: # noqa: D103
105105

106106
def convert_type(dtype: DuckDBPyType) -> DataType: # noqa: D103
107107
id = dtype.id
108-
if id in ["list", "struct", "map", "array"]:
108+
if id in ["list", "struct", "tuple", "map", "array"]:
109109
return convert_nested_type(dtype)
110110
if id == "decimal":
111111
children: list[tuple[str, DuckDBPyType]] = dtype.children

external/duckdb

Submodule duckdb updated 178 files

src/duckdb_py/native/python_conversion.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ Value TransformDictionaryToStruct(optional_ptr<ClientContext> context, const PyD
117117
const LogicalType &target_type = LogicalType::UNKNOWN) {
118118
auto struct_keys = TransformStructKeys(dict.keys, dict.len, target_type);
119119

120-
bool struct_target = target_type.id() == LogicalTypeId::STRUCT;
120+
bool struct_target = target_type.id() == LogicalTypeId::STRUCT || target_type.id() == LogicalTypeId::TUPLE;
121121
if (struct_target && dict.len != StructType::GetChildCount(target_type)) {
122122
throw InvalidInputException("We could not convert the object %s to the desired target type (%s)",
123123
dict.ToString(), target_type.ToString());
@@ -252,7 +252,7 @@ Value TransformTupleToStruct(optional_ptr<ClientContext> context, py::handle ele
252252
auto tuple = py::cast<py::tuple>(ele);
253253
auto size = py::len(tuple);
254254

255-
D_ASSERT(target_type.id() == LogicalTypeId::STRUCT);
255+
D_ASSERT(target_type.id() == LogicalTypeId::STRUCT || target_type.id() == LogicalTypeId::TUPLE);
256256
auto child_types = StructType::GetChildTypes(target_type);
257257
auto child_count = child_types.size();
258258
if (size != child_count) {
@@ -558,7 +558,7 @@ struct PythonValueConversion {
558558

559559
static void HandleTuple(optional_ptr<ClientContext> context, Value &result, const LogicalType &target_type,
560560
py::handle ele, idx_t list_size) {
561-
if (target_type.id() == LogicalTypeId::STRUCT) {
561+
if (target_type.id() == LogicalTypeId::STRUCT || target_type.id() == LogicalTypeId::TUPLE) {
562562
result = TransformTupleToStruct(context, ele, target_type);
563563
return;
564564
}
@@ -584,6 +584,7 @@ struct PythonValueConversion {
584584
PyDictionary dict = PyDictionary(py::reinterpret_borrow<py::object>(ele));
585585
switch (target_type.id()) {
586586
case LogicalTypeId::STRUCT:
587+
case LogicalTypeId::TUPLE:
587588
return TransformDictionaryToStruct(context, dict, target_type);
588589
case LogicalTypeId::MAP:
589590
return TransformDictionaryToMap(context, dict, target_type);
@@ -886,6 +887,7 @@ struct PythonVectorConversion {
886887
auto &result_type = result.GetType();
887888
switch (result_type.id()) {
888889
case LogicalTypeId::STRUCT:
890+
case LogicalTypeId::TUPLE:
889891
ConvertTupleToStruct(context, result, result_offset, ele, tuple_size);
890892
break;
891893
case LogicalTypeId::ARRAY:
@@ -981,6 +983,7 @@ void TransformPythonObjectInternal(optional_ptr<ClientContext> context, py::hand
981983
auto &conversion_target = OP::ConversionTarget(result, param);
982984
switch (conversion_target.id()) {
983985
case LogicalTypeId::STRUCT:
986+
case LogicalTypeId::TUPLE:
984987
case LogicalTypeId::UNKNOWN:
985988
case LogicalTypeId::LIST:
986989
case LogicalTypeId::ARRAY:

src/duckdb_py/native/python_objects.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@ static bool KeyIsHashable(const LogicalType &type) {
461461
return true;
462462
}
463463
case LogicalTypeId::STRUCT:
464+
case LogicalTypeId::TUPLE:
464465
return false;
465466
case LogicalTypeId::SQLNULL:
466467
// A SQLNULL key is always NULL, and Python's None is hashable.
@@ -692,7 +693,8 @@ py::object PythonObject::FromValue(const Value &val, const LogicalType &type,
692693
}
693694
return std::move(py_struct);
694695
}
695-
case LogicalTypeId::STRUCT: {
696+
case LogicalTypeId::STRUCT:
697+
case LogicalTypeId::TUPLE: {
696698
return FromStruct(val, type, client_properties);
697699
}
698700
case LogicalTypeId::UUID: {

src/duckdb_py/numpy/array_wrapper.cpp

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -295,21 +295,10 @@ struct ArrayConvert {
295295
};
296296

297297
struct StructConvert {
298-
static py::dict ConvertValue(Vector &input, idx_t chunk_offset, NumpyAppendData &append_data) {
299-
auto &client_properties = append_data.client_properties;
300-
301-
py::dict py_struct;
298+
// Delegate to FromStruct so unnamed structs / TUPLE values become Python tuples (named ones stay dicts).
299+
static py::object ConvertValue(Vector &input, idx_t chunk_offset, NumpyAppendData &append_data) {
302300
auto val = input.GetValue(chunk_offset);
303-
auto &child_types = StructType::GetChildTypes(input.GetType());
304-
auto &struct_children = StructValue::GetChildren(val);
305-
306-
for (idx_t i = 0; i < struct_children.size(); i++) {
307-
auto &child_entry = child_types[i];
308-
auto &child_name = child_entry.first;
309-
auto &child_type = child_entry.second;
310-
py_struct[child_name.c_str()] = PythonObject::FromValue(struct_children[i], child_type, client_properties);
311-
}
312-
return py_struct;
301+
return PythonObject::FromStruct(val, input.GetType(), append_data.client_properties);
313302
}
314303
};
315304

@@ -714,6 +703,7 @@ void ArrayWrapper::Append(idx_t current_offset, Vector &input, idx_t source_size
714703
may_have_null = ConvertNested<py::object, duckdb_py_convert::UnionConvert>(append_data);
715704
break;
716705
case LogicalTypeId::STRUCT:
706+
case LogicalTypeId::TUPLE:
717707
may_have_null = ConvertNested<py::object, duckdb_py_convert::StructConvert>(append_data);
718708
break;
719709
case LogicalTypeId::VARIANT:

src/duckdb_py/numpy/raw_array_wrapper.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ static idx_t GetNumpyTypeWidth(const LogicalType &type) {
5858
case LogicalTypeId::LIST:
5959
case LogicalTypeId::MAP:
6060
case LogicalTypeId::STRUCT:
61+
case LogicalTypeId::TUPLE:
6162
case LogicalTypeId::UNION:
6263
case LogicalTypeId::UUID:
6364
case LogicalTypeId::ARRAY:
@@ -124,6 +125,7 @@ string RawArrayWrapper::DuckDBToNumpyDtype(const LogicalType &type) {
124125
case LogicalTypeId::LIST:
125126
case LogicalTypeId::MAP:
126127
case LogicalTypeId::STRUCT:
128+
case LogicalTypeId::TUPLE:
127129
case LogicalTypeId::UNION:
128130
case LogicalTypeId::UUID:
129131
case LogicalTypeId::ARRAY:

src/duckdb_py/typing/pytype.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ bool DuckDBPyType::EqualsString(const string &type_str) const {
5151

5252
std::shared_ptr<DuckDBPyType> DuckDBPyType::GetAttribute(const string &name) const {
5353
auto name_identifier = Identifier(name);
54-
if (type.id() == LogicalTypeId::STRUCT || type.id() == LogicalTypeId::UNION) {
54+
if (type.id() == LogicalTypeId::STRUCT || type.id() == LogicalTypeId::TUPLE || type.id() == LogicalTypeId::UNION) {
5555
auto &children = StructType::GetChildTypes(type);
5656
for (idx_t i = 0; i < children.size(); i++) {
5757
auto &child = children[i];
@@ -372,6 +372,7 @@ py::list DuckDBPyType::Children() const {
372372
switch (type.id()) {
373373
case LogicalTypeId::LIST:
374374
case LogicalTypeId::STRUCT:
375+
case LogicalTypeId::TUPLE:
375376
case LogicalTypeId::UNION:
376377
case LogicalTypeId::MAP:
377378
case LogicalTypeId::ARRAY:
@@ -403,7 +404,7 @@ py::list DuckDBPyType::Children() const {
403404
children.append(py::make_tuple("values", strings_list));
404405
return children;
405406
}
406-
if (id == LogicalTypeId::STRUCT || id == LogicalTypeId::UNION) {
407+
if (id == LogicalTypeId::STRUCT || id == LogicalTypeId::TUPLE || id == LogicalTypeId::UNION) {
407408
auto &struct_children = StructType::GetChildTypes(type);
408409
for (idx_t i = 0; i < struct_children.size(); i++) {
409410
auto &child = struct_children[i];

tests/fast/api/test_duckdb_query.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def test_parametrized_explain(self, duckdb_cursor):
5757
duckdb_cursor.execute(query, params)
5858

5959
results = duckdb_cursor.fetchall()
60-
assert "EXPLAIN_ANALYZE" in results[0][1]
60+
assert "Total Time" in results[0][1]
6161

6262
def test_named_param(self):
6363
con = duckdb.connect()

tests/fast/spark/test_spark_types.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,5 +135,10 @@ def test_all_types_schema(self, spark):
135135
),
136136
StructField("map", MapType(StringType(), StringType(), True), True),
137137
StructField("time_ns", TimeNSType(), True),
138+
StructField(
139+
"tuple",
140+
StructType([StructField("", IntegerType(), True), StructField("", StringType(), True)]),
141+
True,
142+
),
138143
]
139144
)

0 commit comments

Comments
 (0)