(ffi)= (extension_guide)=

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: {ref}user_guide_extensions.
  • Changing datafusion-python itself, including the framing that makes this protocol work: {doc}../contributor-guide/index.

The protocol described here is a public, versioned contract. When a hook's signature changes, the change is documented in {doc}../user-guide/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 {doc}why-ffi and {doc}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 {doc}capsule-protocol for what that means and {ref}extension_getter_argument for the argument every getter in the middle group receives.

Each hook's capsule name follows from its own name by {ref}the naming rule <extension_capsule_protocol>, so it is not repeated here.

HookContributesArgumentDocumented on
__datafusion_table_provider__one tablecodec source{doc}table-providers
__datafusion_table_provider_factory__tables built by CREATE EXTERNAL TABLEcodec source{doc}table-providers
__datafusion_catalog_provider__a named set of schemascodec source{doc}table-providers
__datafusion_catalog_provider_list__the whole catalog namespacecodec source{doc}table-providers
__datafusion_schema_provider__a named set of tablescodec source{doc}table-providers
__datafusion_table_function__a table-valued functionsession{doc}functions
__datafusion_scalar_udf__a scalar functionnone{doc}functions
__datafusion_aggregate_udf__an aggregate functionnone{doc}functions
__datafusion_window_udf__a window functionnone{doc}functions
__datafusion_logical_extension_codec__a logical codecsession{doc}codecs
__datafusion_physical_extension_codec__a physical codecsession{doc}codecs
__datafusion_codec_id__the wire id a codec's payloads carry—{ref}extension_codec_ids
__datafusion_query_planner__a query plannersession{doc}query-planners
__datafusion_session_components__a bundle's codecsctx{doc}bundles
__datafusion_session_planner__a bundle's planner, wrapping fallbackctx, fallback{doc}bundles
__datafusion_physical_optimizer_rule__a physical optimizer rulenone{ref}extension_other_hooks
__datafusion_extension_options__typed entries in the session confignone{ref}extension_other_hooks
__datafusion_task_context_provider__the host's task contextnone{ref}extension_task_context_provider

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. {ref}extension_getter_argument 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.

:maxdepth: 2

why-ffi
capsule-protocol
table-providers
functions
codecs
bundles
query-planners
other-components
sessions
checklist