timsaucer commented on issue #1676:
URL: 
https://github.com/apache/datafusion-python/issues/1676#issuecomment-5680959562

   Now that #1679 has merged, the shape it settled on differs from what this 
issue was written against, so recording what went stale and what the plan is.
   
   # What is stale
   
   | This issue says | Reality after #1679 |
   | --- | --- |
   | `__datafusion_session_extension__` | Renamed 
`__datafusion_session_components__` |
   | `SessionExtensionExportable` | Renamed `SessionComponentsExportable`, with 
a new sibling `SessionPlannerExportable` |
   | "#1672 adds `with_extensions`" | #1672 closed unmerged; the work landed as 
#1677 → #1678 → #1679 |
   | Components contain "optionally a query planner" | Planners are a separate 
second-phase hook. `SessionExtensionComponents` says so directly: "Query 
planners are not listed here." |
   | "matching the one-planner rule" | The per-call one-planner refusal was 
removed in #1679 — planners nest now, so the analogy has no referent. The 
surviving precedent for erroring is the duplicate-codec-id `ValueError` in 
`resolve_bundle_codec_id`. |
   | Design decision 1, on the derived context sharing its catalog provider 
list | Moot. `_derive_for_extensions` no longer exists; 
`_install_extension_codecs` does `Arc::clone` on the source context, so there 
is one `Arc<SessionContext>` per session and no derivation point to isolate at. 
Deriving one is the hazard #1679 removed. |
   | "the Rust `_install_extensions` helper is private" | It split into three 
private primitives (`_install_extension_codecs`, `_export_query_planner`, 
`_install_extension_planner`) and the composition moved to Python. Adding the 
function fields is now Python-only, with no Rust at all. |
   | "frozen dataclass with defaulted fields, so this can be added 
incrementally" | Still true, but `__post_init__` normalizes fields by the 
`_codecs` name suffix, and its comment reserves non-suffixed fields for things 
that must *not* become tuples — pointed the wrong way for a `udfs` field. Needs 
replacing with field metadata. |
   
   The core premise is not stale. #1679's own description renamed the hook 
"leaving room for the UDF and provider fields that will join the codec fields 
later."
   
   # Dropping `object_stores`
   
   There is no FFI object store type upstream — `datafusion-ffi` 55 has no such 
module, and this repository has no `__datafusion_object_store__` hook. 
`register_object_store` takes `StorageContexts`, a closed enum over five 
built-in pyclasses, so the field could only ever carry datafusion-python's own 
objects. A third-party Rust cdylib cannot produce one at all; it would have to 
import `datafusion.object_store` and call back into the host. A library that 
wants to ship a configured store hands the user an `AmazonS3` and the user 
calls `register_object_store`, which is already one line.
   
   I will file this separately as blocked on an upstream `FFI_ObjectStore`. 
Everything else in the list stays in scope: functions, table providers, catalog 
providers, and physical optimizer rules.
   
   # The constraint that shapes the rest
   
   The issue's argument for declarative components — the host validates 
everything before mutating anything — is still the right one, but it needs 
restating now that there is no derived context. `with_extensions` is currently 
transactional for free because codec chains live on the returned Python handle 
rather than on `SessionState`, and `_install_extension_planner` is the single 
write. Every field proposed here writes into the shared `SessionState`, and the 
returned handle is the same session as the source, so there is nothing to roll 
back to.
   
   The only design that keeps "nothing is written until every hook has returned 
and every capsule has been validated" literally true is to make the commit 
phase provably infallible:
   
   ```
   1. collect   every __datafusion_session_components__      fallible, writes 
nothing
   2. chains    _install_extension_codecs                    fallible, writes 
nothing
   3. resolve   declared components -> concrete objects      fallible, writes 
nothing
      + every __datafusion_session_planner__                 fallible, writes 
nothing
   4. commit    _install_extension_planner, then register_*  must be infallible
   ```
   
   Rollback should not be attempted as an alternative. `deregister_udf` removes 
a name but does not restore a built-in the bundle shadowed, and 
`register_catalog` returns the displaced provider that the commit would have 
discarded — so a rollback that deletes a user's pre-existing registration is 
worse than a partial apply.
   
   Checking the commit halves against upstream: `SessionContext::register_udf` 
returns `()` and swallows errors, so it is infallible. `register_catalog` 
returns the displaced provider, also infallible, but 
`PySessionContext::register_catalog_provider` does capsule import and insert in 
one function with no split point, so it needs a new private primitive. 
`register_table` returns a `Result` through `schema_for_ref`, so the schema 
lookup has to move into the resolve phase. `add_physical_optimizer_rule` does a 
full `SessionStateBuilder::new_from_existing(...).build()` per rule, so N rules 
is N whole-state clones and a failure on rule 3 leaves 1 and 2 committed; it 
needs batching into one rebuild.
   
   # Where resolution has to happen
   
   The fields divide by what their capsule getter needs, and that decides the 
ordering more than anything else does:
   
   **Group A — the getter takes no argument.** `udfs`, `udafs`, `udwfs`, 
`physical_optimizer_rules`. Resolution is session-independent and, for the 
function kinds, already happens in the extension library's own frame before the 
bundle hands anything over.
   
   **Group B — the getter receives the session or the logical codec.** `udtfs` 
(`PyTableFunction::new` passes the session in), `table_providers` 
(`PyTable::new(table, Some(session))`), `catalog_providers` (builds a capsule 
from the session's logical codec and passes that in). These must resolve 
against the handle returned by `_install_extension_codecs`, not against the 
`ctx` the components hook received. A provider resolved against the pre-install 
context encodes through a chain missing every other bundle's codec, and the 
failure does not surface until a decode in a different process. This is the 
same hazard the guide already documents for phase one: "Reading them in phase 
one gets the chains from before the call, missing even the bundle's own codecs."
   
   # Collisions
   
   Duplicate names among components declared in the *same* `with_extensions` 
call are a `ValueError` naming the name and both contributing bundles. 
Shadowing something the session already has stays legal — `ctx.udfs()` contains 
every DataFusion built-in, and `enable_spark_functions` overrides built-ins by 
design, so erroring on that would refuse legitimate overrides and would refuse 
`"datafusion"` for catalogs since the default catalog always exists. Namespaces 
are independent, so a UDF and a UDAF may share a name.
   
   Physical optimizer rules are exempt and never collide. The extension guide 
already says rules accumulate where planners nest, and erroring would 
contradict that.
   
   # Plan
   
   Four stacked PRs, split on the Group A/B boundary plus whether new Rust is 
needed, since that is what changes the shape of the diff:
   
   1. `udfs`, `udafs`, `udwfs` — Python only, no Rust. Smallest diff, largest 
conceptual load: it settles the field-metadata normalization mechanism, the 
resolve/commit structure, and the collision policy that the other three inherit.
   2. `physical_optimizer_rules` — one new private Rust primitive that imports 
every capsule and then applies them all to a single `SessionState` rebuild. 
Completes Group A.
   3. `udtfs` and `table_providers` — Group B. No new Rust, since 
`TableFunction(name, func, ctx)` and `Table(provider, ctx)` are already 
constructible from Python; the work is placing resolution after the codec 
install and moving the schema lookup out of the commit.
   4. `catalog_providers` — a new private primitive splitting the capsule 
import out of `register_catalog_provider`. Closes this issue.
   
   Each PR carries its own coverage across a real FFI boundary rather than 
deferring it, extending one bundle class in `examples/datafusion-ffi-example` 
by one field as the stack proceeds — that crate already exports a fixture for 
every field in scope.
   
   No upgrade guide entry and no `api change` label: every field is additive 
with a `()` default on a frozen dataclass, no hook signature changes, and no 
wire format changes.
   
   # One honest caveat
   
   For a library that ships a codec, worker-side function registration is 
already unnecessary — the codec alone rebuilds the function from the name in 
the plan, and the registry is tried first so the codec is the fallback rather 
than the path. The `udfs` field buys driver-side ergonomics, not worker parity. 
That is still worth shipping, it is just a smaller claim than the issue's 
framing makes.
   
   Worth stating in the guide too: `with_extensions` can never install 
*everything* a library provides. A config extension has to reach 
`SessionConfig` before the context exists, which no bundle hook can reach.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to