blob: e373bb552e06e9d13f54e876ad5c50b0c260c07b [file] [view]
<!---
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
(extension_planners)=
# Query planners
A query planner turns a logical plan into a physical one. Contributing your own
is how a library changes the way queries execute rather than what data they can
reach — a distributed engine is the motivating case, and an optimizing rewriter
is another.
The session owns the codecs used for the exchange and supplies them to your
planner. That is what lets the planner decode provider-owned objects, and lets
datafusion-python decode the physical plan the planner returns. Your planner
uses `FFI_QueryPlanner::new_with_ffi_codecs` with the two codecs it takes off
the session, and never touches a task-context provider directly. That also
matches what installation does anyway:
{py:meth}`~datafusion.SessionContext.set_query_planner` builds the planner
against the codecs of the session that will run the query.
`MyQueryPlanner` in [`datafusion-ffi-query-planner-example`] is the worked
implementation.
## One planner per session
A session holds exactly one query planner. Calling `set_query_planner` again
**replaces** the installed planner instead of layering another one.
To chain planners, have the new planner wrap the capsule returned by
{py:meth}`SessionContext.__datafusion_query_planner__ <datafusion.SessionContext.__datafusion_query_planner__>`,
captured before the new planner is installed, and delegate to it explicitly:
```python
fallback = ctx.__datafusion_query_planner__()
ctx.set_query_planner(MyPlanner(fallback=fallback))
```
`set_query_planner` returns nothing. The query planner lives in `SessionState`,
so it is a property of the session rather than of a handle on it, and
installing one is visible to every context sharing that session. See
{ref}`extension_sessions`.
If the planners you are composing all ship as
{ref}`bundles <extension_bundles>`, prefer `with_extensions` — it does the
nesting for you and cannot capture a partial codec chain.
(planner_codec_rebinding)=
## Install codecs before a layered planner
Installing a codec on a session that already has a foreign planner rebuilds
that planner against the new chain: there is one planner, and it has to carry
the codecs currently in force. But that rebuild swaps the codecs on the
installed `ForeignQueryPlanner` handle, and **only that handle**.
A planner that wraps a fallback resolved that fallback when *it* was installed,
and holds the result inside its own library's private data — behind a
`create_physical_plan` function pointer, with no Python-side handle. A codec
installed afterwards therefore reaches the outer planner and not the fallback,
which keeps whichever codecs were in force when it was imported.
The stale codecs stay usable rather than dangling — they hold weak handles to
the one `Arc<SessionContext>` the session keeps alive — so the effect is a
fallback hop serializing with an older codec, not a failure. Neither side can
repair it; the reasons are in {ref}`ffi_internals_rebinding`, and a fix has to
come from upstream
([apache/datafusion#24762](https://github.com/apache/datafusion/issues/24762)).
So, three rules:
- **Install the codecs before a layered planner.**
- If a codec has to go in afterwards, install the outer planner again *on the
handle that holds the new codec* — that re-runs its getter, which re-imports
the fallback against that handle's codecs. Re-installing on the original
handle rebinds the session's planner back to the original handle's codecs
instead, which is the trap the query-planner example's test suite pins.
- Better, use {ref}`extension_bundles`, where there is no "afterwards" within a
call.
:::{note}
This is invisible in the examples here, which use one fallback in the same
cdylib as its wrapper; `datafusion-ffi` short-circuits a same-library hop
rather than serializing, so no codec runs. A fallback in a *different* library
would serialize, and would do it with the codecs it was imported with.
:::
[`datafusion-ffi-query-planner-example`]: https://github.com/apache/datafusion-python/tree/main/examples/datafusion-ffi-query-planner-example