| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
Introduces JavaScalarUdf struct with its Signature, return_type, JNI references, and a ScalarUDFImpl impl whose invoke_with_args returns NotImplemented (placeholder for Task 6). Adds volatility_from_byte helper. Dead-code lints suppressed with comments referencing Task 5/6.
Add end-to-end UDF registration: SessionContext.registerUdf serialises the return/arg types as Arrow IPC, passes them to native via registerScalarUdf JNI, which decodes the schema, constructs a JavaScalarUdf with an exact Signature, and registers it on the DataFusion SessionContext. SQL planning now resolves registered UDFs by name; invocation still returns NotImplemented until Task 6.
Replace the NotImplemented stub with the real per-call data flow: materialise ColumnarValue args to arrays, pack them into a StructArray, export via to_ffi, call JniBridge.invokeScalarUdf over JNI with four FFI addresses, then import the result via from_ffi and return it as a ColumnarValue::Array. Update ScalarUdfTest to assert correct values from an AddOne UDF registered against a three-row constant table.
… and zero-arg case - Clear pending JNI exceptions in jthrowable_to_string fallback branches to avoid undefined behavior on thread detach - Add SAFETY comment on the from_ffi unsafe block explaining FFI initialization guarantees - Replace usize-to-i32 silent truncation with i32::try_from checked conversion for row count - Replace StructArray::new with StructArray::try_new_with_length to handle zero-arg UDFs
There was a problem hiding this comment.
Thanks for letting this one linger so I could take a look!
Sorry, something went wrong.
| public void registerUdf( | ||
| String name, | ||
| ScalarUdf udf, | ||
| ArrowType returnType, | ||
| List<ArrowType> argTypes, | ||
| Volatility volatility) { |
There was a problem hiding this comment.
The equivalent rust function takes a ScalarUDF struct that exposes many of these arguments, rather than passing them here at the top level. Is it worth emulating that structure here?
This is obviously a much broader question, it could be asked about nearly every public API we add. I waffled on it myself, and ended up deciding to strictly follow the rust API as closely as possible (my ScalarUDF is here - not saying we should emulate that; just for reference). My justification was that it would be easier to stay in line with the upstream rust as it evolved, and it would be clearer what Java supported.
Whatever we decide, I think we ought to document the strategy going forward, especially for the benefit of coding agents. I attempted this in my own bindings with Claude rules for defining the Public API as well as for documenting it, going so far as to ensure that Javadocs provide a working link to the rust equivalent. I'd be happy to do a version of this for these new bindings (wouldn't make sense to use those I've linked exactly).
Sorry, something went wrong.
There was a problem hiding this comment.
I agree that documenting the strategy makes sense. Please go ahead and work on this when you have time. I also agree that we should stay as close to the Rust APIs as we can. I did some refactoring of this - let me know what you think.
Sorry, something went wrong.
| ## Implement | ||
|
|
||
| Implement the `ScalarUdf` interface: | ||
|
|
||
| ```java | ||
| import java.util.List; | ||
| import org.apache.arrow.memory.BufferAllocator; | ||
| import org.apache.arrow.vector.FieldVector; | ||
| import org.apache.arrow.vector.IntVector; | ||
| import org.apache.datafusion.ScalarUdf; | ||
|
|
||
| public final class AddOne implements ScalarUdf { | ||
| @Override | ||
| public FieldVector evaluate(BufferAllocator allocator, List<FieldVector> args) { | ||
| IntVector in = (IntVector) args.get(0); | ||
| IntVector out = new IntVector("add_one", allocator); | ||
| out.allocateNew(in.getValueCount()); | ||
| for (int i = 0; i < in.getValueCount(); i++) { | ||
| if (in.isNull(i)) out.setNull(i); | ||
| else out.set(i, in.get(i) + 1); | ||
| } | ||
| out.setValueCount(in.getValueCount()); | ||
| return out; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Allocate any new vectors — including the result — from the supplied | ||
| `BufferAllocator`. The input vectors are read-only views; do not close them. | ||
| Ownership of the returned vector transfers to the framework on return. |
There was a problem hiding this comment.
Great API, great docs! Similar to but better than what I did.
Sorry, something went wrong.
| } | ||
| } | ||
|
|
||
| /// Best-effort: extract class name and getMessage() from a Java throwable. |
There was a problem hiding this comment.
I think it would be nice to propagate the stack trace (in full or in part), at least optionally. In my bindings I made it a session configurable option, though I could imagine many other strategies. The implementation would look quite different in the bridge.
Could make sense as follow on work for all upcalls, rather than in the scope of this PR.
Sorry, something went wrong.
There was a problem hiding this comment.
That sounds like a good idea. I filed #55 to track this
Sorry, something went wrong.
…der class Mirror DataFusion's Rust shape: ScalarFunction (formerly the eval-only ScalarUdf interface) now carries name/argTypes/returnType/volatility and the per-batch evaluate body, mirroring ScalarUDFImpl. ScalarUdf becomes a holder that wraps a ScalarFunction and is what SessionContext.registerUdf takes — mirroring the ScalarUDF struct. The holder caches the four metadata values in its constructor so impls that allocate per call aren't double-evaluated at registration. The JNI bridge takes a ScalarFunction directly; the wrapper is bypassed on the hot path.
There was a problem hiding this comment.
Looks great! I'll work on the rust/java API alignment documentation as a priority.
The other key documentation unlock that let me let coding agents loose implementing features without close supervision, was the patterns to follow in the rust bridge code. The version from my bindings is much less relevant here given the architectural differences, but I think something like it makes sense. I have much less conviction on that stuff too - my rust experience is as a hobbiest only (hundreds of hours of experience) vs my java experience (ten thousand+ hours) - but I'll try to do something at least.
Projects like these are so much more feasible these days with well-guided coding agents, which @timsaucer is clearly leaning into with the python bindings.
Sorry, something went wrong.
I look forward to having some LLM skills in this repo. I think this type of project is ideal for LLMs. Thanks for the review! |
Sorry, something went wrong.
Agreed! It was the realization of how good they'd gotten in the last six months that finally convinced me to take the project seriously. Even though it's a large surface area that requires a lot of code, adding features and keeping up with upstream changes should be a lot of implementing the same patterns over and over. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Which issue does this PR close?
N/A
Rationale for this change
Add support for implement scalar UDFs in Java.
What changes are included in this PR?
Public Java API
Internals
Refinements from the design discussion
Docs + example
How are these changes tested?
make test from a clean checkout. The new ScalarUdfTest has 12 tests covering: