timsaucer commented on PR #1679:
URL: 
https://github.com/apache/datafusion-python/pull/1679#issuecomment-5544543804

   Thanks for chasing this down — the `write_*` failures are real, but I think 
the fix is much smaller than a new planner API, and it is not in this PR.
   
   **Where it actually breaks.** `FFI_LogicalExtensionCodec` has no vtable 
entries for file formats at all. Both hooks are hardcoded stubs in 
`datafusion-ffi` (`src/proto/logical_extension_codec.rs`, 
`try_decode_file_format` / `try_encode_file_format` → `not_impl_err!("FFI does 
not support ...")`). So *any* codec that crosses the FFI boundary loses 
file-format support, and `df.write_csv/write_parquet/write_json` build a 
`LogicalPlan::Copy` that carries a `FileFormatFactory`. That is the whole 
failure.
   
   **This PR already survives it on the datafusion-python side.** 
`PythonLogicalCodec::try_{encode,decode}_file_format` go through 
`chain_encode`/`chain_decode`, which collect a chained codec's error and fall 
through to the terminal `DefaultLogicalExtensionCodec`. Built-in formats encode 
and decode fine there. The direction that fails is the mirror image: Ballista 
holds an `FFI_LogicalExtensionCodec` wrapping the Python session's codec and 
calls it directly, with no Default fallback behind it.
   
   **Two fixes, both much cheaper than a new API:**
   
   1. *Ballista side, today, no upstream change.* Compose the FFI codec you get 
from Python with a `DefaultLogicalExtensionCodec` fallback on the two 
file-format hooks — the same shape `PythonLogicalCodec` uses here. One file, 
unblocks DF 55.
   
   2. *Upstream `datafusion-ffi`, ~8 lines.* Make those two hooks delegate to a 
local `DefaultLogicalExtensionCodec` instead of returning `not_impl_err!`. No 
new vtable entries, so no ABI break, so it can ship in a 55.x patch. It works 
because built-in file formats are fully self-describing protobuf and nothing 
has to cross the boundary: 
`DefaultLogicalExtensionCodec::try_encode_file_format` in `datafusion-proto` 
covers csv/json/arrow/avro/parquet by downcasting the factory locally. What is 
left unsupported is a *custom* `FileFormatFactory` owned by the foreign 
library, and I agree that one needs a real `FFI_FileFormatFactory` and will not 
make the 55 window — but it is also not what is breaking `write_*`.
   
   **On `CallbackPlanner`.** I do not think the concept holds up, and I want to 
be concrete about why rather than just deferring it.
   
   A `QueryPlanner` returning a `CallbackExec` leaf replaces the *entire* plan 
with one opaque node: no children, no partitioning DataFusion can see, no 
pushdown, no repartition, no limit, no statistics. That is not planning, it is 
interception at the root. It is coherent for exactly one architecture — thin 
client, full delegation — which is what Ballista's `DistributedExec` already 
is; the local plan is degenerate because none of the work is local. But it has 
no answer for a hybrid plan, a local table joined against a remote one, because 
there is nothing left to split on. All or nothing.
   
   It also buys very little over what already ships:
   
   ```python
   plan_bytes = df.logical_plan().to_bytes(ctx)
   batches = my_grpc_client.execute(plan_bytes)   # your gRPC logic, in Python
   result = ctx.from_arrow(batches)               # any __arrow_c_stream__ 
object
   ```
   
   All three exist today, no new API. The only thing `CallbackPlanner` adds on 
top is transparency — the user keeps writing `ctx.sql(...).collect()` and never 
sees the interception. That is real, but it is a thin return for a permanent 
public API, and a second planner mechanism sitting next to the FFI query 
planner means two ways to do one thing.
   
   If the goal is *partial* delegation rather than whole-plan delegation, the 
design that answers it already exists: a table provider exported from Python 
(`__datafusion_table_provider__`, see 
`docs/source/user-guide/io/table_provider.md`). Remote data appears as a table, 
DataFusion plans around it, filter and projection pushdown work, and local and 
remote sources mix in one query. That composes; a root-level callback does not.
   
   The part that makes me most hesitant is the stated rationale: *"As 
`CallbackPlanner` have same library marker as df python, `FFILogicalCodec` will 
not be triggered."* That shapes a public API around which cdylib the code is 
compiled into, in order to route around an eight-line gap in `datafusion-ffi`. 
Fixing the gap is the smaller and more durable change.
   
   **One genuinely open question, independent of all of the above** — and I 
think it is the same "chaining of codecs" point you flagged. Once Ballista's 
codec is installed on the Python session it becomes a chain entry, and chain 
entries write a framed payload (`DFPYCHN` + codec id) rather than bare bytes. A 
plain Rust `BallistaCodec` in the scheduler will not strip that envelope. So 
how does the scheduler decode — does it link `PythonLogicalCodec`, or does 
Ballista's codec stay off the chain? I am happy to add whatever hook makes the 
first option workable; I would rather solve that than work around it.
   
   Proposal: keep #1679 as is, since the file-format gap predates it and is 
orthogonal, and I will open the upstream issue for (2). If you still want the 
callback route after the above, let us give it its own issue so the design can 
be argued on its own terms.
   


-- 
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