Skip to content

Commit 0f57ddf

Browse files
committed
fix pandas
1 parent 976dc5b commit 0f57ddf

3 files changed

Lines changed: 29 additions & 9 deletions

File tree

src/duckdb_py/native/python_conversion.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ vector<Identifier> TransformStructKeys(py::handle keys, idx_t size, const Logica
6161
vector<Identifier> res;
6262
res.reserve(size);
6363
for (idx_t i = 0; i < size; i++) {
64-
res.emplace_back(Identifier(py::cast<std::string>(keys.attr("__getitem__")(i))));
64+
// Stringify via str() so non-string keys (e.g. the integer keys of a hashable-key MAP, which DuckDB
65+
// produces as a plain {1: 10} dict) are accepted -- nanobind's nb::cast<std::string> rejects non-str.
66+
res.emplace_back(Identifier(py::cast<std::string>(py::str(keys.attr("__getitem__")(i)))));
6567
}
6668
return res;
6769
}
@@ -1037,8 +1039,12 @@ void TransformPythonObjectInternal(optional_ptr<ClientContext> context, py::hand
10371039
break;
10381040
}
10391041
case PythonObjectType::Bytes: {
1040-
const string &ele_string = py::cast<string>(ele);
1041-
OP::HandleBlob(result, param, const_data_ptr_t(ele_string.data()), ele_string.size());
1042+
// Read the buffer directly (mirrors the ByteArray branch above): nanobind's nb::cast<std::string> rejects
1043+
// a bytes object (pybind11 accepted it), so go through the CPython API instead.
1044+
char *bytes_buffer;
1045+
Py_ssize_t bytes_length;
1046+
PyBytes_AsStringAndSize(ele.ptr(), &bytes_buffer, &bytes_length); // NOLINT
1047+
OP::HandleBlob(result, param, const_data_ptr_cast(bytes_buffer), idx_t(bytes_length));
10421048
break;
10431049
}
10441050
case PythonObjectType::NdArray:

src/duckdb_py/pandas/analyzer.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,8 +337,10 @@ LogicalType PandasAnalyzer::DictToStruct(const PyDictionary &dict, bool &can_con
337337
for (idx_t i = 0; i < dict.len; i++) {
338338
auto dict_key = dict.keys.attr("__getitem__")(i);
339339

340-
//! Have to already transform here because the child_list needs a string as key
341-
auto key = Identifier(py::cast<std::string>(dict_key));
340+
//! Have to already transform here because the child_list needs a string as key. Stringify via str() so
341+
//! non-string keys (e.g. the integer keys of a hashable-key MAP, produced as a plain {1: 10} dict) are
342+
//! accepted -- nanobind's nb::cast<std::string> rejects non-str objects, whereas pybind11 stringified them.
343+
auto key = Identifier(py::cast<std::string>(py::str(dict_key)));
342344

343345
auto dict_val = dict.values.attr("__getitem__")(i);
344346
auto val = GetItemType(dict_val, can_convert);

src/duckdb_py/pandas/bind.cpp

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,19 @@ static LogicalType BindColumn(ClientContext &context, PandasBindColumn &column_p
6363
D_ASSERT(py::hasattr(column.attr("cat"), "categories"));
6464
NumpyArray categories(column.attr("cat").attr("categories"));
6565
auto categories_pd_type = ConvertNumpyType(categories.GetArray().attr("dtype"));
66-
if (categories_pd_type.type == NumpyNullableType::OBJECT) {
66+
// Legacy categories are backed by an `object` dtype; pandas >= 3.0 backs string categories with the new
67+
// StringDtype (reported as "str"), so treat both as string categories -> ENUM.
68+
if (categories_pd_type.type == NumpyNullableType::OBJECT ||
69+
categories_pd_type.type == NumpyNullableType::STRING) {
6770
// Let's hope the object type is a string.
6871
bind_data.numpy_type.type = NumpyNullableType::CATEGORY;
69-
vector<string> enum_entries = py::cast<vector<string>>(categories.GetArray());
72+
// str()-ify each category individually: pandas >= 3.0 categories are a StringArray whose elements are
73+
// numpy str scalars, which nanobind's vector<string>/string casters reject (py::cast<vector<string>>
74+
// on the array throws). Iterating + py::str handles both that and the legacy object[str] case.
75+
vector<string> enum_entries;
76+
for (auto category : categories.GetArray()) {
77+
enum_entries.push_back(py::cast<std::string>(py::str(category)));
78+
}
7079
idx_t size = enum_entries.size();
7180
Vector enum_entries_vec(LogicalType::VARCHAR, size);
7281
auto enum_entries_ptr = FlatVector::GetDataMutable<string_t>(enum_entries_vec);
@@ -75,8 +84,11 @@ static LogicalType BindColumn(ClientContext &context, PandasBindColumn &column_p
7584
}
7685
D_ASSERT(py::hasattr(column.attr("cat"), "codes"));
7786
column_type = LogicalType::ENUM(enum_entries_vec, size);
78-
NumpyArray pandas_col(column.attr("cat").attr("codes"));
79-
bind_data.internal_categorical_type = py::cast<std::string>(py::str(pandas_col.GetArray().attr("dtype")));
87+
// .to_numpy(): pandas >= 3.0 returns cat.codes as a Series (no .strides/.ctypes), but the scan needs a
88+
// real ndarray backing buffer; materialize it. (Older pandas returned an ndarray here directly.)
89+
NumpyArray pandas_col(column.attr("cat").attr("codes").attr("to_numpy")());
90+
bind_data.internal_categorical_type =
91+
py::cast<std::string>(py::str(py::object(pandas_col.GetArray().attr("dtype"))));
8092
bind_data.pandas_col = std::make_unique<PandasNumpyColumn>(std::move(pandas_col));
8193
} else {
8294
NumpyArray pandas_col(column.attr("to_numpy")());

0 commit comments

Comments
 (0)