Skip to content

Commit 1d0bd68

Browse files
committed
nanobind: DuckDBPyExpression convert-flag caster + None handling (kills crash cascade)
Add a custom type_caster<shared_ptr<DuckDBPyExpression>> (mirrors the DuckDBPyType one): keep cast_flags::convert so the registered implicit conversions (str->column, scalar->constant) fire for shared_ptr args, and when the inner caster yields no instance, construct through the registered Python ctor (None->NULL constant) -- a real owned object, no dangling -- with PyErr_Clear() on failure. Allow None on the Expression object-ctor (py::arg.none()). The PyErr_Clear is what eliminates the stale-PyErr segfault CASCADE: the full fast suite now runs clean in parallel (0 crashes, was unmeasurable). Failures 86 -> 66; expression/spark Expression cluster resolved (spark 6->3). Belt-and-suspenders None guard in CreateCompareExpression/Coalesce.
1 parent 1475be6 commit 1d0bd68

3 files changed

Lines changed: 77 additions & 1 deletion

File tree

src/duckdb_py/include/duckdb_python/expression/pyexpression.hpp

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,68 @@ struct DuckDBPyExpression : public std::enable_shared_from_this<DuckDBPyExpressi
139139
};
140140

141141
} // namespace duckdb
142+
143+
namespace nanobind {
144+
namespace detail {
145+
146+
// Custom type caster for std::shared_ptr<duckdb::DuckDBPyExpression>.
147+
//
148+
// Mirrors the DuckDBPyType caster (see pytype.hpp): nanobind's default std::shared_ptr<T> caster strips
149+
// cast_flags::convert before delegating to the inner caster, which disables the implicit conversions the
150+
// expression API relies on -- a Python str becomes a column expression and any other object becomes a
151+
// constant expression (registered via implicitly_convertible<py::str/py::object, DuckDBPyExpression>).
152+
// Those conversions construct brand-new, fully-owned DuckDBPyExpression objects, so they carry no dangling
153+
// risk; we therefore keep the convert flag. Visible in every TU that converts the type (pyexpression.cpp,
154+
// pyconnection.cpp, pyrelation.cpp all include this header).
155+
template <>
156+
struct type_caster<std::shared_ptr<duckdb::DuckDBPyExpression>> {
157+
using T = duckdb::DuckDBPyExpression;
158+
using Caster = make_caster<T>;
159+
NB_TYPE_CASTER(std::shared_ptr<T>, Caster::Name)
160+
161+
bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept {
162+
// NOTE: deliberately do NOT clear cast_flags::convert (see comment above).
163+
Caster caster;
164+
if (caster.from_python(src, flags, cleanup)) {
165+
T *ptr = caster.operator T *();
166+
if (ptr) {
167+
ft_object_guard guard(src);
168+
if (auto sp = ptr->weak_from_this().lock()) {
169+
value = std::static_pointer_cast<T>(std::move(sp));
170+
return true;
171+
}
172+
value = shared_from_python(ptr, src);
173+
return true;
174+
}
175+
}
176+
// The inner caster yielded no instance. nanobind maps Python None (and leaves some scalars) to an empty
177+
// shared_ptr here, whereas pybind11 ran the registered implicit conversion. Reproduce that by constructing
178+
// through the registered Python constructor (None -> NULL constant, str -> column, scalar -> constant). The
179+
// result is a real, owned object, so there is no dangling -- and unlike the empty-shared_ptr default, it
180+
// never leaves callers dereferencing a null. Clear the Python error on failure so a rejected conversion
181+
// doesn't leave a stale exception for the next operation.
182+
try {
183+
nanobind::object converted = nanobind::type<T>()(nanobind::borrow<nanobind::object>(src));
184+
value = nanobind::cast<std::shared_ptr<T>>(converted);
185+
return true;
186+
} catch (...) {
187+
PyErr_Clear();
188+
return false;
189+
}
190+
}
191+
192+
static handle from_cpp(const std::shared_ptr<T> &value, rv_policy, cleanup_list *cleanup) noexcept {
193+
// DuckDBPyExpression is non-polymorphic and registers no type_hook (simplified shared_ptr from_cpp).
194+
bool is_new = false;
195+
T *ptr = value.get();
196+
handle result = nb_type_put(&typeid(T), ptr, rv_policy::reference, cleanup, &is_new);
197+
if (is_new) {
198+
auto pp = std::static_pointer_cast<void>(value);
199+
shared_from_cpp(std::move(pp), result.ptr());
200+
}
201+
return result;
202+
}
203+
};
204+
205+
} // namespace detail
206+
} // namespace nanobind

src/duckdb_py/pyexpression.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,11 @@ std::shared_ptr<DuckDBPyExpression> DuckDBPyExpression::CreateCompareExpression(
205205
if (!py::try_cast<std::shared_ptr<DuckDBPyExpression>>(arg, py_expr)) {
206206
throw InvalidInputException("Please provide arguments of type Expression!");
207207
}
208+
if (!py_expr) {
209+
// nanobind maps Python None to an empty shared_ptr (rather than running the implicit conversion the
210+
// way it does for by-value/by-ref args); pybind11 turned None into a NULL constant expression here.
211+
py_expr = InternalConstantExpression(TransformPythonValue(nullptr, py::borrow<py::object>(arg)));
212+
}
208213
auto expr = py_expr->GetExpression().Copy();
209214
expressions.push_back(std::move(expr));
210215
}
@@ -237,6 +242,11 @@ std::shared_ptr<DuckDBPyExpression> DuckDBPyExpression::Coalesce(const py::args
237242
if (!py::try_cast<std::shared_ptr<DuckDBPyExpression>>(arg, py_expr)) {
238243
throw InvalidInputException("Please provide arguments of type Expression!");
239244
}
245+
if (!py_expr) {
246+
// nanobind maps Python None to an empty shared_ptr (rather than running the implicit conversion the
247+
// way it does for by-value/by-ref args); pybind11 turned None into a NULL constant expression here.
248+
py_expr = InternalConstantExpression(TransformPythonValue(nullptr, py::borrow<py::object>(arg)));
249+
}
240250
auto expr = py_expr->GetExpression().Copy();
241251
expressions.push_back(std::move(expr));
242252
}

src/duckdb_py/pyexpression/initialize.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,8 @@ static void InitializeImplicitConversion(py::class_<DuckDBPyExpression> &m) {
295295
m.def(py::new_([](const py::object &obj) {
296296
auto val = TransformPythonValue(nullptr, obj);
297297
return DuckDBPyExpression::InternalConstantExpression(std::move(val));
298-
}));
298+
}),
299+
py::arg("value").none()); // accept None -> NULL constant (nanobind rejects None for py::object otherwise)
299300
py::implicitly_convertible<py::str, DuckDBPyExpression>();
300301
py::implicitly_convertible<py::object, DuckDBPyExpression>();
301302
}

0 commit comments

Comments
 (0)