Skip to content

[DISCUSS] Separate stable API into separate crates #25124

Description

@thinkharderdev

Is your feature request related to a problem or challenge?

Currently everything in DataFusion is released together which makes the upgrade path painful and causes dependency conflicts if you have a project which depends on multiple 3rd party dependencies which themselves depend on DataFusion.

For example, if I am building a query engine which should support multiple different table formats and want to use 3rd party dependencies which expose DataFusion TableProviders (e.g https://github.com/apache/iceberg-rust and https://github.com/delta-io/delta-rs), but also implement the core execution engine on top of DataFusion then I need to ensure all dependencies are in sync in terms of their DF dependency.

Describe the solution you'd like

Separate the stable API for internal implementation as separate crates. For instance instead of having just a datafusion-catalog crate with both the stable API and implementations we can have two crate:

  1. datafusion-catalog-api
  2. datafusion-catalog (which depends on datafusion-catalog-api

The -api crate would contain only trait definitions like:
TableProvider
CatalogProvider
etc.

The -api should (theoretically) evolve much more slowly than the implementation so 3rd party crates that want to expose a TableProvider can depend only on datafusion-catalog-api and I can consume that dependency while using any version of datafusion-catalog I want (provided it uses the same stable API).

Obviously there are quite a few details to work out about which APIs to "stabilize" in this way, which I will hand-wave past :)

Describe alternatives you've considered

A lot of alternative suggestions to the exact same issue have been proposed in #16622

Additional context

No response

Activity

  1. jayzhan211 commented on Sep 10, 2026

    @jayzhan211
    Contributor

    Thanks @thinkharderdev, the pain is real, and it's the same one described in #16622. I support the goal but have concerns about the mechanism. Here's where I land.

    1. Why -api crates alone won't solve this

    A trait is only as stable as the types in its signatures. Following the closure of TableProvider::scan:

    • &dyn Session → ConfigOptions, RuntimeEnv, function registry
    • &[Expr] → UDF types, Subquery → LogicalPlan
    • Arc<dyn ExecutionPlan> → PlanProperties, EquivalenceProperties, PhysicalExpr, TaskContext
    • plus Statistics, Constraints, DataFusionError, and arrow's SchemaRef

    So datafusion-catalog-api either depends on datafusion-expr / datafusion-physical-plan and breaks whenever they do, or absorbs those types and changes as often as core does. These are some of our most actively changing APIs.

    Two more limits:

    • Arrow sets a floor. Arrow types appear in every signature, and arrow-rs releases a new major version roughly quarterly. Downstream crates like delta-rs and iceberg-rust also expose arrow types directly, so apps must match arrow versions with them regardless of what we do.
    • Providers use more than traits. Many build on our implementation crates (parquet scan, FileScanConfig, pruning), so they'd still depend on those crates.

    A crate split would add crates and publishing overhead without making the API more stable.

    2. Proposal: longer breaking-change period (1 month → 2 months, extend if needed)

    • Keep releasing monthly, but only every other release is a major (breaking) release. The release in between is semver-compatible (N.0 → N.1 → (N+1).0), so downstream crates get a two-month upgrade window instead of a monthly race.
    • Enforce it with cargo-semver-checks in CI (Semver-checks for all crate on merge and push #16078). Breaking PRs get an api-change label and wait for the next major.
    • Prefer "add new + deprecate old" so most changes can still land in minor releases. That also keeps the queue of held PRs small.
    • Re-evaluate after a few cycles. If downstream still can't keep up, extend to 3 months, which would also line up with arrow's major cadence.

    This needs no refactoring, only a release policy plus CI.

    3. Teach contributors (human and AI agents) how to handle breaking changes

    A lot of PRs are now written with coding agents. Longer breaking periods only work if both humans and agents default to non-breaking changes. Two lightweight pieces:

    a) A repo-level AGENTS.md section on API changes, which most agents read automatically, e.g.:

    ## Public API changes
    - Treat every `pub` item in published crates as public API.
    - Prefer additive changes: new trait methods with default impls, new
      constructors/builders, `#[non_exhaustive]` on new enums/structs.
    - Never change an existing signature in place. Add the new API and mark the
      old one `#[deprecated(since = "X.Y.0", note = "use `new_fn` instead")]`,
      following the API health policy.
    - Run `cargo semver-checks` before opening a PR. If a break is unavoidable,
      say so in the PR description, add the `api-change` label, and add an
      entry to the upgrade guide.

    b) Inline markers on key extension points (TableProvider, ExecutionPlan, Expr, Session, ...), so an agent sees the rule right where it edits:

    /// ...
    ///
    /// # API stability
    /// This trait is a public extension point implemented by downstream crates.
    /// Do not change existing method signatures; add new methods with default
    /// implementations instead. See AGENTS.md#public-api-changes.
    pub trait TableProvider: ...

    The doc section also helps human contributors and shows up on docs.rs. cargo-semver-checks in CI remains the backstop, since neither humans nor agents will get it right every time.

  2. alamb commented on Sep 12, 2026

    @alamb
    Contributor

    Thanks for filing this @thinkharderdev

    Those are good points @jayzhan211 -- I think the fact that Arrow types appear in most APIs is ok as Arrow itself has longer stable windows (3-4 months rather than 1 month) but it is still a concern.

    However, it is also a great point that actually figuring out what types are part of the public API, and thus can not change all that frequently, will likely be hard (handwaving aside) and maybe that makes this infeasible 🤔

    A lot of PRs are now written with coding agents. Longer breaking periods only work if both humans and agents default to non-breaking changes. Two lightweight pieces:

    Yeah, I agree this is the core tradeoff

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions