Extension Guide#
This section is for people writing a library that plugs into datafusion-python — a package that contributes table providers, functions, a catalog, extension codecs, or a query planner, usually written in Rust and exposed through PyO3.
Two neighbouring audiences are served elsewhere:
Using an extension library someone else published: Using extension libraries.
Changing datafusion-python itself, including the framing that makes this protocol work: Contributor Guide.
The protocol described here is a public, versioned contract. When a hook’s signature changes, the change is documented in Upgrade Guides with a before-and-after, and objects built against a different DataFusion major version are rejected with a clear error rather than used as-is.
Start here#
If you have not built an extension before, read Why FFI and
The capsule protocol in order. Together they explain why your library must
not depend on the datafusion-python crate, and the one convention every hook
follows. After that the pages are independent — go to the one matching what
you are contributing.
The three roles in a query#
A single query can involve three independent native libraries, and much of this section only makes sense once they are distinct in your head:
datafusion-python — the host. Owns the session, and decodes whatever comes back from the other two.
A provider library — owns table providers, catalogs, and functions, plus the codecs that serialize them.
A planner library — owns a query planner and the configuration it needs.
The worked examples in this repository use two separate crates,
datafusion-ffi-example and datafusion-ffi-query-planner-example, so
each role has a distinct shared-library identity. A real library may play more
than one role; keeping them separate in the examples is what makes the
boundaries observable.
The session owns the codecs used for the exchange and supplies them to the foreign planner. That is what lets the planner decode provider-owned objects, and lets datafusion-python decode the physical plan the planner returns.
Hook reference#
Every integration point is a dunder method named __datafusion_*__. The
convention is uniform enough to be worth stating once: your object exposes the
getter, datafusion-python calls it, and it returns a PyCapsule wrapping an
FFI-safe struct. See The capsule protocol for what that means and
What your getter receives for the argument every getter in the middle
group receives.
Each hook’s capsule name follows from its own name by the naming rule, so it is not repeated here.
Hook |
Contributes |
Argument |
Documented on |
|---|---|---|---|
|
one table |
codec source |
|
|
tables built by |
codec source |
|
|
a named set of schemas |
codec source |
|
|
the whole catalog namespace |
codec source |
|
|
a named set of tables |
codec source |
|
|
a table-valued function |
session |
|
|
a scalar function |
none |
|
|
an aggregate function |
none |
|
|
a window function |
none |
|
|
a logical codec |
session |
|
|
a physical codec |
session |
|
|
the wire id a codec’s payloads carry |
— |
|
|
a query planner |
session |
|
|
a bundle’s codecs |
|
|
|
a bundle’s planner, wrapping |
|
|
|
a physical optimizer rule |
none |
|
|
typed entries in the session config |
none |
|
|
the host’s task context |
none |
Three rows are not like the others. __datafusion_task_context_provider__ is
implemented by the host, not by your library — you read it off the session
you are handed. __datafusion_codec_id__ is a plain string attribute rather
than a method returning a capsule. And the two session_ hooks are dispatched
from Python and return objects rather than capsules.
“codec source” in the argument column means the value is something you can read the host’s logical extension codec off, which is not always a session. What your getter receives explains why, and what to do with it.
python/tests/test_docstrings.py compares this table against the hook names
the package actually dispatches, so a hook added or renamed without touching
this page fails the suite.