@@ -1663,10 +1663,9 @@ impl PySessionContext {
16631663 /// **Writes nothing.** The codec chains belong to the returned handle
16641664 /// rather than to `SessionState`, so this phase is transactional for free:
16651665 /// a codec that fails to import, or that collides with an installed id,
1666- /// leaves the caller's context exactly as it was. Binding the planner is
1667- /// the only step that touches the session, and it is deferred to
1668- /// [`Self::_install_extension_planner`] so the planner hooks can run
1669- /// against the final chains.
1666+ /// leaves the caller's context exactly as it was. Everything that touches
1667+ /// the session is deferred to [`Self::_commit_extensions`] so the planner
1668+ /// hooks can run against the final chains.
16701669 ///
16711670 /// Codecs must arrive as objects exposing the capsule getter, never as
16721671 /// bare capsules — see [`resolve_bundle_codec_id`].
@@ -1717,49 +1716,87 @@ impl PySessionContext {
17171716 } )
17181717 }
17191718
1720- /// Re-export a planner a `__datafusion_session_planner__` hook returned as
1721- /// a capsule, so the next hook in the chain receives one either way.
1719+ /// Run the planner hooks and commit a `with_extensions` call.
17221720 ///
1723- /// A hook may hand back an object exposing `__datafusion_query_planner__`
1724- /// or a raw capsule; the next hook wraps whatever it is given and should
1725- /// not have to branch on which. Importing here also surfaces a malformed
1726- /// planner at the hook that produced it rather than at the final install.
1727- /// Writes nothing.
1728- pub fn _export_query_planner < ' py > (
1729- slf : & Bound < ' py , Self > ,
1730- planner : Bound < ' py , PyAny > ,
1731- ) -> PyDataFusionResult < Bound < ' py , PyCapsule > > {
1732- let ffi = ffi_query_planner_from_pycapsule ( & planner, Some ( slf. as_any ( ) ) ) ?;
1733- Ok ( create_query_planner_capsule ( slf. py ( ) , & ffi) ?)
1734- }
1735-
1736- /// Commit the query planner for a `with_extensions` call.
1721+ /// The second phase, run on the handle carrying the completed chains —
1722+ /// `session` is that same handle as the Python-level wrapper, which is
1723+ /// what each `__datafusion_session_planner__` hook receives. The hooks
1724+ /// run first, **in argument order**, each handed the planner built so
1725+ /// far as a capsule; a hook may hand back an object exposing
1726+ /// `__datafusion_query_planner__` or a raw capsule, and each return is
1727+ /// imported here so a malformed planner surfaces at the hook that
1728+ /// produced it rather than at the install. Returning `None` contributes
1729+ /// no planner. All of that writes nothing, so a hook that raises leaves
1730+ /// the session exactly as it was.
17371731 ///
1738- /// The second phase, run once every codec is installed and every planner
1739- /// hook has returned, so the planner is bound against the final chains.
1740- /// This is the one call in `with_extensions` that writes to the session,
1741- /// and it goes through this context's own `state_ref()`, so providers
1742- /// bound to it stay valid.
1732+ /// Everything after the hooks is the commit, and none of it can fail: a
1733+ /// registration whose commit can fail belongs in the resolve step, split
1734+ /// into an import that returns a resolved object and an insert that
1735+ /// cannot raise. There is one session here, shared with the receiver, so
1736+ /// a failure part-way through would have nothing to roll back to. The
1737+ /// reasoning is in docs/source/contributor-guide/ffi-internals.md, under
1738+ /// "Why `with_extensions` commits last".
17431739 ///
1744- /// `None` means no bundle supplied a planner. That still rebuilds
1745- /// whichever planner the session already holds against the new chains,
1746- /// exactly as `with_logical_extension_codec` does, and writes nothing at
1747- /// all if the session has no FFI planner to rebuild.
1740+ /// The planner is bound through this context's own `state_ref()`, so
1741+ /// providers bound to it stay valid. With no planner supplied the bind
1742+ /// still rebuilds whichever planner the session already holds against
1743+ /// the new chains, exactly as `with_logical_extension_codec` does —
1744+ /// unless `rebind_planner` is also false, meaning the call installed no
1745+ /// codec either. Then the bind is skipped entirely, the same way
1746+ /// [`Self::with_python_udf_inlining`] returns early for a no-op toggle:
1747+ /// there is nothing to rebind against, and the rebuild would drag a
1748+ /// planner sitting on another handle's codecs onto this one's.
17481749 ///
1749- /// The caller skips this step entirely when the call installed no codec
1750- /// and no planner, the same way [`Self::with_python_udf_inlining`] returns
1751- /// early for a no-op toggle: there is nothing to rebind against, and the
1752- /// rebuild would drag a planner sitting on another handle's codecs onto
1753- /// this one's.
1754- #[ pyo3( signature = ( planner=None ) ) ]
1755- pub fn _install_extension_planner < ' py > (
1750+ /// The functions are registered *after* the planner hooks have run, so a
1751+ /// hook never sees this call's functions in the registry — the
1752+ /// registrations have no fall-through, and a name is free to shadow one
1753+ /// the session already had.
1754+ pub fn _commit_extensions < ' py > (
17561755 slf : & Bound < ' py , Self > ,
1757- planner : Option < Bound < ' py , PyAny > > ,
1756+ extensions : Vec < Bound < ' py , PyAny > > ,
1757+ session : Bound < ' py , PyAny > ,
1758+ rebind_planner : bool ,
1759+ udfs : Vec < PyScalarUDF > ,
1760+ udafs : Vec < PyAggregateUDF > ,
1761+ udwfs : Vec < PyWindowUDF > ,
17581762 ) -> PyDataFusionResult < ( ) > {
1759- let planner = planner
1760- . map ( |planner| ffi_query_planner_from_pycapsule ( & planner, Some ( slf. as_any ( ) ) ) )
1761- . transpose ( ) ?;
1762- slf. borrow ( ) . set_session_query_planner ( planner) ;
1763+ let py = slf. py ( ) ;
1764+ // Nest the planners, outermost last. `planner` stays `None` when no
1765+ // bundle supplies one, which leaves an already-installed planner in
1766+ // place rather than wrapping the session's default in an FFI hop.
1767+ let mut planner: Option < FFI_QueryPlanner > = None ;
1768+ for extension in & extensions {
1769+ if !extension. hasattr ( "__datafusion_session_planner__" ) ? {
1770+ continue ;
1771+ }
1772+ let fallback = match & planner {
1773+ Some ( ffi) => create_query_planner_capsule ( py, ffi) ?,
1774+ None => slf. borrow ( ) . __datafusion_query_planner__ ( py, None ) ?,
1775+ } ;
1776+ let supplied =
1777+ extension. call_method1 ( "__datafusion_session_planner__" , ( & session, fallback) ) ?;
1778+ if supplied. is_none ( ) {
1779+ continue ;
1780+ }
1781+ planner = Some ( ffi_query_planner_from_pycapsule (
1782+ & supplied,
1783+ Some ( slf. as_any ( ) ) ,
1784+ ) ?) ;
1785+ }
1786+
1787+ if planner. is_some ( ) || rebind_planner {
1788+ slf. borrow ( ) . set_session_query_planner ( planner) ;
1789+ }
1790+ let this = slf. borrow ( ) ;
1791+ for udf in udfs {
1792+ this. ctx . register_udf ( udf. function ) ;
1793+ }
1794+ for udaf in udafs {
1795+ this. ctx . register_udaf ( udaf. function ) ;
1796+ }
1797+ for udwf in udwfs {
1798+ this. ctx . register_udwf ( udwf. function ) ;
1799+ }
17631800 Ok ( ( ) )
17641801 }
17651802}
0 commit comments